Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0672edfc3f | |||
| 1415c85d35 | |||
| 72b359c9a9 | |||
| 711b61d63e | |||
| 3ea36ef3ab | |||
| 6e27df7404 | |||
| 00d0043866 | |||
| fd423e2df1 | |||
| 8947e89d7b |
+221
-1
@@ -48,6 +48,75 @@ 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-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 |
|
| `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 |
|
| 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)
|
||||||
|
|
||||||
|
Gitea Actions limitations (confirmed via research, supersedes any
|
||||||
|
GitHub-Actions assumptions):
|
||||||
|
|
||||||
|
- `actions/upload-artifact@v3` / `download-artifact@v3` work; v4 is NOT supported by act_runner.
|
||||||
|
- Artifacts are scoped to a single workflow run; **re-dispatch starts a new run, so artifacts do NOT survive between dispatches**.
|
||||||
|
- `workflow_dispatch` API: `POST /api/v1/repos/{owner}/{repo}/actions/workflows/{filename}.yml/dispatches` with body `{ "ref": "<branch>", "inputs": {...} }`.
|
||||||
|
- `on: workflow_call` + `uses: <owner>/<repo>/.gitea/workflows/<file>@<ref>` works; pin to `@milestone/v1.0-initial`.
|
||||||
|
- `actions/checkout@v4` supports cross-repo (pass `repository:` + `ref:` + `token: ${{ secrets.GITEA_TOKEN }}` for private repos).
|
||||||
|
- File-contents API: POST to create (201), PUT to update (must include current `sha`, obtained via GET).
|
||||||
|
- `${{ secrets.GITEA_TOKEN }}` is a manually-created PAT secret on the `acdl` + `acdl-contracts` repos; the auto-injected token is current-repo only and cannot cross-repo.
|
||||||
|
- No native approval-gate UI; gates are `workflow_dispatch` inputs (`approve_qa`, `approve_prod`).
|
||||||
|
|
||||||
|
### Approval-gate + state-persistence approach (D-027, D-028 refined)
|
||||||
|
|
||||||
|
Because re-dispatch starts a new run and artifacts do not survive:
|
||||||
|
|
||||||
|
1. The pipeline workflow has `workflow_dispatch` inputs:
|
||||||
|
- `contract-ref` (string; default `main`) — the ref on `acdl-contracts` carrying the contract.
|
||||||
|
- `approve_qa` (boolean; default `false`) — the human sets this to `true` to advance past QA.
|
||||||
|
- `approve_prod` (boolean; default `false`) — the human sets this to `true` to advance past Prod.
|
||||||
|
|
||||||
|
2. Each stage job (`dev`, `qa-gate`, `prod-gate`, `finalize`) writes its evidence to `acdl-evidence` via the file-contents API (PUT `audit.json` with the new event appended). This is the persistent state across re-dispatches.
|
||||||
|
|
||||||
|
3. **Dev stage** (always runs on dispatch): check out `acdl` + `acdl-contracts@<contract-ref>`, run `policy_checker.py` + `confidence_signal.py`; if `score < 0.50`, write a `dev_rejected` evidence event and exit 1 (Act 4). Otherwise run `mock_executor.sh`, write a `dev_applied` evidence event, and exit 0. The run ends here.
|
||||||
|
|
||||||
|
4. **QA gate** (next dispatch with `approve_qa=true`): check out, run `evidence_writer.py --stage qa --event "qa approved"`, commit updated `audit.json` to `acdl-evidence`. Exit 0. The run ends.
|
||||||
|
|
||||||
|
5. **Prod gate** (next dispatch with `approve_prod=true`): same as QA but `--stage prod`.
|
||||||
|
|
||||||
|
6. **Finalize** (same dispatch as Prod, chained via `needs: prod-gate`): write the `finalize` evidence event, commit final `audit.json` to `acdl-evidence`. The raw URL now serves the updated timeline.
|
||||||
|
|
||||||
|
Because each stage is a separate dispatch, the workflow file uses `if:` conditions on each job:
|
||||||
|
- `dev` runs when `inputs.approve_qa != true && inputs.approve_prod != true` (the initial dispatch).
|
||||||
|
- `qa-gate` runs when `inputs.approve_qa == true && inputs.approve_prod != true`.
|
||||||
|
- `prod-gate` runs when `inputs.approve_prod == true`.
|
||||||
|
- `finalize` runs after `prod-gate` (`needs: prod-gate`).
|
||||||
|
|
||||||
|
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
|
## Data Flow
|
||||||
|
|
||||||
@@ -100,4 +169,155 @@ for pushes.
|
|||||||
There is no `package.json`; ACDL is bash + python stubs. The verification gate
|
There is no `package.json`; ACDL is bash + python stubs. The verification gate
|
||||||
substitutes `bash -n` and `python -m py_compile` for `npm run typecheck`, and
|
substitutes `bash -n` and `python -m py_compile` for `npm run typecheck`, and
|
||||||
per-phase `scripts/verify_phaseNN.sh` for `npm test`. `npm run build` is a
|
per-phase `scripts/verify_phaseNN.sh` for `npm test`. `npm run build` is a
|
||||||
no-op (no build step). See PERSONAS.md / VERIFICATION note.
|
no-op (no build step). See PERSONAS.md / VERIFICATION note.
|
||||||
|
|
||||||
|
## L1 module schema (Phase 02 research)
|
||||||
|
|
||||||
|
Each L1 module lives at `modules/l1/<name>/` with exactly two files:
|
||||||
|
|
||||||
|
- `manifest.yaml` — declares the L1's identity + a flat `inputs:` map.
|
||||||
|
Schema (D-017):
|
||||||
|
```yaml
|
||||||
|
name: l1-eks-fargate # matches the folder name
|
||||||
|
kind: l1 # literal "l1"; substrate-agnostic
|
||||||
|
description: <one-line>
|
||||||
|
inputs:
|
||||||
|
<key>:
|
||||||
|
description: <one-line>
|
||||||
|
type: string # only "string" allowed (flat, max-depth-1)
|
||||||
|
```
|
||||||
|
- `mock_apply.sh` — uniform stub per D-007 + D-018:
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: <name>] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: <name>] OK"
|
||||||
|
exit 0
|
||||||
|
```
|
||||||
|
`mock_apply.sh` does NOT read input values; the manifest is for traceability
|
||||||
|
and for Phase 03's `mock_executor.sh` to enumerate the L1s in an L2.
|
||||||
|
|
||||||
|
### L1 list (fixed per REQ-02 / D-019)
|
||||||
|
|
||||||
|
| Folder | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `l1-eks-fargate` | Serverless container compute substrate |
|
||||||
|
| `l1-iam-role` | Identity and access role primitive |
|
||||||
|
| `l1-lambda` | Event-driven function primitive |
|
||||||
|
| `l1-api-gateway` | HTTP routing primitive |
|
||||||
|
| `l1-eventbridge` | Event bus primitive |
|
||||||
|
| `l1-sqs` | Queue primitive |
|
||||||
|
| `l1-s3` | Object store primitive |
|
||||||
|
| `l1-cloudwatch` | Observability primitive |
|
||||||
|
|
||||||
|
L1 modules are single-purpose, substrate-agnostic, max-depth-1 (per
|
||||||
|
PROJECT.md Constraints). They do not compose with other L1s.
|
||||||
|
|
||||||
|
## L2 module schema + core scripts (Phase 03 research)
|
||||||
|
|
||||||
|
### L2 manifest.yaml schema (D-020)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: l2-commodity-price-feed # matches the folder name
|
||||||
|
kind: l2 # literal "l2"
|
||||||
|
description: <one-line>
|
||||||
|
l1s: # ordered list of L1 references
|
||||||
|
- name: l1-eks-fargate # MUST match an existing L1 folder name
|
||||||
|
inputs:
|
||||||
|
cluster_name: price-feed-cluster
|
||||||
|
region: us-east-1
|
||||||
|
cpu_arch: arm64
|
||||||
|
- name: l1-lambda
|
||||||
|
inputs:
|
||||||
|
function_name: price-ingest
|
||||||
|
runtime: python3.11
|
||||||
|
handler: index.handler
|
||||||
|
# ... up to 5 L1 references per L2 (max-depth-5 per REQ-05; L2->L1 is depth 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
L2s reference L1s **by name only** (no path); `mock_executor.sh` resolves
|
||||||
|
the name to `modules/l1/<name>/`.
|
||||||
|
|
||||||
|
### L2 list (fixed per REQ-04)
|
||||||
|
|
||||||
|
| Folder | Description | L1s (per S&P Global Energy / Platts use cases) |
|
||||||
|
|--------|-------------|------------------------------------------------|
|
||||||
|
| `l2-invoice-service` | Billing + invoicing microservice | `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-sqs`, `l1-s3` |
|
||||||
|
| `l2-commodity-price-feed` | Real-time price ingestion | `l1-eks-fargate`, `l1-lambda`, `l1-api-gateway`, `l1-eventbridge`, `l1-s3` |
|
||||||
|
| `l2-energy-analytics-api` | Historical query API | `l1-eks-fargate`, `l1-api-gateway`, `l1-lambda`, `l1-s3`, `l1-cloudwatch` |
|
||||||
|
| `l2-regulatory-reporting` | Compliance + reporting | `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-sqs`, `l1-s3` |
|
||||||
|
|
||||||
|
Each L2 references exactly 5 L1s (within the max-depth-5 constraint; L2→L1
|
||||||
|
is depth 1, so depth-5 is generous but the spec caps composition depth at
|
||||||
|
5 — the count is 5 to demonstrate a realistic composed stack).
|
||||||
|
|
||||||
|
### contract.yaml schema (D-021)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
stack: l2-commodity-price-feed # MUST match an existing L2 folder name
|
||||||
|
inputs: # top-level params for the L2 (optional)
|
||||||
|
environment: dev
|
||||||
|
owner: platform-team
|
||||||
|
public-ingress: false # bool; true triggers POLICY_VIOLATION:PUBLIC_INGRESS
|
||||||
|
```
|
||||||
|
|
||||||
|
The `public-ingress` key is the only policy-enforced field in Phase 03.
|
||||||
|
Phase 04's pipeline reads `contract.yaml`, runs `policy_checker.py`, then
|
||||||
|
`mock_executor.sh` to apply the L2.
|
||||||
|
|
||||||
|
### state.json shape (D-022)
|
||||||
|
|
||||||
|
`mock_executor.sh` writes `state.json` to its working directory:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"l2": "l2-commodity-price-feed",
|
||||||
|
"l1s": [
|
||||||
|
{"name": "l1-eks-fargate", "applied": true, "exit_code": 0},
|
||||||
|
{"name": "l1-lambda", "applied": true, "exit_code": 0},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"contract": {
|
||||||
|
"stack": "l2-commodity-price-feed",
|
||||||
|
"inputs": {...},
|
||||||
|
"public-ingress": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### audit.json event + hash chain (D-023)
|
||||||
|
|
||||||
|
`audit.json` is a JSON array of event objects. `evidence_writer.py`
|
||||||
|
appends one event per call. Hash chain:
|
||||||
|
|
||||||
|
1. Construct the event dict with `hash` set to empty string.
|
||||||
|
2. Serialize via `json.dumps(event, sort_keys=True, separators=(",", ":"))` — canonical JSON (deterministic key order, no whitespace).
|
||||||
|
3. Compute `hash = sha256(canonical_json.encode("utf-8")).hexdigest()`.
|
||||||
|
4. Set `event["hash"] = hash`.
|
||||||
|
5. Append to `audit.json`.
|
||||||
|
|
||||||
|
Genesis event (when `audit.json` is empty or missing):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"seq": 0,
|
||||||
|
"ts": "2026-07-21T13:00:00Z",
|
||||||
|
"stage": "genesis",
|
||||||
|
"event": "audit log initialized",
|
||||||
|
"prev_hash": "GENESIS",
|
||||||
|
"hash": "<sha256 of the canonical json of this event with hash empty>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Subsequent events: `seq = prev.seq + 1`, `prev_hash = prev.hash`.
|
||||||
|
|
||||||
|
### Core script I/O contracts
|
||||||
|
|
||||||
|
| Script | Input | Output | Exit |
|
||||||
|
|--------|-------|--------|------|
|
||||||
|
| `mock_executor.sh` | `<contract.yaml path>` (argv[1]); reads L2 manifest from `modules/l2/<contract.stack>/manifest.yaml` | writes `state.json` to cwd; prints per-L1 progress | 0 on all-L1s-pass; non-zero on any L1 failure |
|
||||||
|
| `policy_checker.py` | `<contract.yaml path>` (argv[1]) | stdout: `POLICY_PASS` or `POLICY_VIOLATION:PUBLIC_INGRESS` | 0 on pass; 1 on violation |
|
||||||
|
| `confidence_signal.py` | `<contract.yaml path>` (argv[1]); calls policy_checker | stdout: `{"score": 0.90|0.40, "reason": "..."}` | 0 always (per D-024; pipeline decides gate) |
|
||||||
|
| `evidence_writer.py` | argv: `--stage <dev|qa|prod|finalize|genesis>` `--event "<text>"` `--audit <path to audit.json>` (default `./audit.json`) | appends event to audit.json; prints the new event's hash + seq | 0 on success; 1 on I/O error |
|
||||||
|
| `l3b_agent_stub.py` | argv[1] = issue body text (or stdin if no argv); optional `-o <path>` (default stdout) | writes a `contract.yaml` (D-021 schema) with `stack` set by the D-008 keyword map | 0 on success; 1 on empty input |
|
||||||
@@ -59,12 +59,12 @@ verification_toolchain:
|
|||||||
|
|
||||||
### frontend-engineer
|
### frontend-engineer
|
||||||
- **Domain:** frontend
|
- **Domain:** frontend
|
||||||
- **Active:** false
|
- **Active:** true # REACTIVATED for Phase 05 (evidence UI + dry run)
|
||||||
- **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).
|
- **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 (reactivates in Phase 05)
|
- **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
|
- **Frameworks:** vanilla-js, dom-api, fetch-api
|
||||||
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url
|
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url, relative-url-for-audit-json
|
||||||
- **Territory:** `acdl-evidence/index.html` (Phase 05)
|
- **Territory:** `evidence-ui/**` (the source-of-truth `index.html` in the `acdl` repo; pushed to `acdl-evidence` by `run_demo.sh`)
|
||||||
|
|
||||||
## Phase-specific overrides
|
## Phase-specific overrides
|
||||||
|
|
||||||
|
|||||||
+61
-57
@@ -1,96 +1,100 @@
|
|||||||
---
|
---
|
||||||
phase: 01
|
phase: 05
|
||||||
name: repo-scaffolding
|
name: evidence-ui-and-demo-dry-run
|
||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_type: feature
|
milestone_type: feature
|
||||||
status: planned
|
status: planned
|
||||||
requirements: [REQ-01, REQ-09, REQ-10]
|
requirements: [REQ-13, REQ-14, REQ-15]
|
||||||
must_haves:
|
must_haves:
|
||||||
- "Repo acdl-contracts exists under continuous-intelligence and is pushable (HTTP 200 on GET /repos/continuous-intelligence/acdl-contracts)"
|
- "evidence-ui/index.html exists: single HTML file with inline CSS + JS, no external resources, no frameworks (D-032, REQ-14)"
|
||||||
- "Repo acdl-evidence exists under continuous-intelligence and is pushable (HTTP 200 on GET /repos/continuous-intelligence/acdl-evidence)"
|
- "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"
|
||||||
- "Raw URL https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html returns HTTP 200 with placeholder HTML (D-012/D-016 substitute for Pages check)"
|
- "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)"
|
||||||
- "Branches qa and prod exist on acdl-contracts (visible stand-in for unsupported Gitea environments; D-013)"
|
- "scripts/run_demo.sh is idempotent: re-running overwrites both files on acdl-evidence and produces the same audit.json (deterministic hash chain)"
|
||||||
- "scripts/gitea_setup.sh is idempotent and exits 0 (re-running against existing repos is a no-op)"
|
- "Act 4 produces a 'dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS' evidence event with score 0.40 (the Safety Net)"
|
||||||
- "scripts/verify_phase01.sh passes: enumerates repos, fetches the raw index.html, lists qa/prod branches, exits 0"
|
- "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:
|
verification:
|
||||||
typecheck: "bash -n scripts/*.sh"
|
typecheck: "bash -n scripts/*.sh && python3 -m py_compile scripts/*.py"
|
||||||
test: "scripts/verify_phase01.sh"
|
test: "scripts/verify_phase05.sh"
|
||||||
build: no-op
|
build: no-op
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 01 — repo-scaffolding PLAN
|
# Phase 05 — evidence-ui-and-demo-dry-run PLAN
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Stand up the three-repo scaffold under the `continuous-intelligence` Gitea org
|
Build the vanilla-JS timeline UI and run the 4 demo acts as a local dry
|
||||||
and the Phase 01 visible artifacts in the local `acdl` checkout: the
|
run that produces a real `audit.json` and publishes it (plus the UI) to
|
||||||
Gitea-setup script, the workflow skeletons, and the Phase 01 verification
|
`acdl-evidence` main. This is the milestone's capstone phase.
|
||||||
script. After this phase, Phases 02-05 can push stub modules, core scripts,
|
|
||||||
and workflows into the right repos.
|
|
||||||
|
|
||||||
## Requirements covered
|
## Requirements covered
|
||||||
|
|
||||||
- REQ-01: All demo code lives under `continuous-intelligence` Gitea org
|
- 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-09: Three repos exist (`acdl`, `acdl-contracts`, `acdl-evidence`)
|
- REQ-14: `index.html` uses vanilla JS to fetch `audit.json` and render events as a timeline
|
||||||
- REQ-10: reusable pipeline runs Dev → QA (approval) → Prod (approval) → Finalize; Phase 01 builds the skeleton (gates wired in Phase 04)
|
- REQ-15: All four demo acts reproduce deterministically in a dry run
|
||||||
|
|
||||||
## Waves (vertical slices, executed in domain priority order)
|
## Waves (vertical slices, domain priority order)
|
||||||
|
|
||||||
### Wave 1 — coordination (lead-developer)
|
### Wave 1 — frontend-engineer (the UI)
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks:**
|
||||||
|
|
||||||
- **T-1.1** Add `.gitignore` (ignore `audit.json` artifacts, `__pycache__/`, `*.pyc`, `state.json`, `.env`). Create repo directory layout markers: `scripts/.gitkeep`, `modules/l1/.gitkeep`, `modules/l2/.gitkeep`, `.gitea/workflows/.gitkeep`. Add a top-level `README.md` with the project name, the 4-act demo summary, and a pointer to `.ciagent/PROJECT.md`.
|
- **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.
|
||||||
- **T-1.2** Add `scripts/verify_phase01.sh` — the Phase 01 verification script. It reads `$ACDL_GITEA_TOKEN` from the env, calls the Gitea API to confirm both new repos exist, curls the raw `index.html` URL, lists branches on `acdl-contracts` looking for `qa` and `prod`, and prints a PASS/FAIL summary. Exits 0 on success, non-zero on any failure. Idempotent.
|
|
||||||
|
|
||||||
**Files owned (territory):**
|
**Files owned:** `evidence-ui/index.html`
|
||||||
- `.gitignore`, `README.md`
|
|
||||||
- `scripts/.gitkeep`, `modules/l1/.gitkeep`, `modules/l2/.gitkeep`, `.gitea/workflows/.gitkeep`
|
|
||||||
- `scripts/verify_phase01.sh`
|
|
||||||
|
|
||||||
**Commits:** one per task, `---ci---` block has `phase: 1, status: plan-as-execute, persona: lead-developer, task: T-1.x, requirements.covered: [REQ-01]`.
|
**Commits:** one commit, `phase: 5, status: plan-as-execute, persona: frontend-engineer, task: T-5.1, requirements.covered: [REQ-14]`.
|
||||||
|
|
||||||
### Wave 2 — backend (backend-engineer)
|
### Wave 2 — backend-engineer (the dry-run script)
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks:**
|
||||||
|
|
||||||
- **T-2.1** Add `scripts/gitea_setup.sh`. Idempotent. Reads `$ACDL_GITEA_TOKEN` and `$GITEA_HOST` (default `https://git.cloudinit.dev`). Creates `acdl-contracts` and `acdl-evidence` under `continuous-intelligence` if missing (POST `/orgs/continuous-intelligence/repos` with `auto_init: true`, `default_branch: "main"`, `private: true`). Pushes a placeholder `index.html` to `acdl-evidence` main via the Gitea file-contents API (POST `/repos/{owner}/{repo}/contents/{path}` with base64 content + "Initial placeholder" commit message). Creates `qa` and `prod` branches on `acdl-contracts` from `main` via the Gitea branch API (POST `/repos/{owner}/{repo}/branches`). All HTTP errors are logged with status + body; the script is idempotent (409 / "already exists" treated as success). Uses `curl` + `python3 -c` for base64 encoding; no jq dependency.
|
- **T-5.2** Create `scripts/run_demo.sh` — the Phase 05 dry-run simulation. It:
|
||||||
- **T-2.2** Add `acdl/.gitea/workflows/pipeline.yml` skeleton. `on: workflow_call`. Four jobs: `dev` (runs-on ubuntu-latest, placeholder "Dev stage" step), `qa-gate` (needs dev, runs-on ubuntu-latest, placeholder "Awaiting QA approval" step; in Phase 04 this becomes a `workflow_dispatch` approval input per D-013), `prod-gate` (needs qa-gate, placeholder "Awaiting Prod approval"), `finalize` (needs prod-gate, placeholder "Commit audit.json to acdl-evidence"). All steps are explicit placeholders marked `# Phase 04 will implement`. Comment at top documents the branch-pin rule (`@milestone/v1.0-initial`).
|
1. Accepts an optional `--no-upload` flag (for testing without hitting Gitea).
|
||||||
- **T-2.3** Add `acdl-contracts/.gitea/workflows/issue-to-contract.yml` skeleton (committed to the `acdl` repo under `contracts-repo/.gitea/workflows/` as a reference copy; pushed to the actual `acdl-contracts` repo in Phase 04). `on: issues` with `types: [opened]`. One job `parse-and-trigger` with placeholder steps for: checkout, run `l3b_agent_stub.py`, commit `contract.yaml`, push, trigger the `acdl` pipeline via `workflow_dispatch` API (D-014). Marked `# Phase 04 will implement`.
|
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 (territory):**
|
Cleanup: write contracts under `contracts/` (gitignored) so the working tree stays clean.
|
||||||
- `scripts/gitea_setup.sh`
|
|
||||||
- `.gitea/workflows/pipeline.yml`
|
|
||||||
- `contracts-repo/.gitea/workflows/issue-to-contract.yml` (reference copy in the `acdl` repo; source of truth for Phase 04)
|
|
||||||
|
|
||||||
**Commits:** one per task, `---ci---` block has `persona: backend-engineer, task: T-2.x, requirements.covered: [REQ-01 or REQ-09 or REQ-10]`.
|
**Files owned:** `scripts/run_demo.sh`
|
||||||
|
|
||||||
### Wave 3 — coordination (lead-developer, verification wiring)
|
**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:**
|
**Tasks:**
|
||||||
|
|
||||||
- **T-3.1** Run `scripts/gitea_setup.sh` against the live Gitea (executed as part of EXECUTE; recorded as a commit only if it modifies repo state — it does not, so no commit. Instead, the verify run in Wave 3 confirms the artifacts exist via the API.) Add `scripts/verify_phase01.sh` invocation note to README.md ("Run `scripts/verify_phase01.sh` after `scripts/gitea_setup.sh` to confirm Phase 01 success criteria"). Update `.ciagent/REQUIREMENTS.md` Traceability table to mark REQ-01/09/10 as `covered` (pending VERIFY confirmation). Update `.ciagent/ROADMAP.md` Phase 01 status to `executing` (will flip to `complete` on SHIP).
|
- **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 (territory):**
|
**Files owned:** `scripts/verify_phase05.sh`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`
|
||||||
- `README.md` (update)
|
|
||||||
- `.ciagent/REQUIREMENTS.md` (traceability update only)
|
|
||||||
- `.ciagent/ROADMAP.md` (phase status update only)
|
|
||||||
|
|
||||||
**Commits:** one for T-3.1 (a `chore(P01)` update with `phase: 1, status: execute, persona: lead-developer, task: T-3.1`).
|
**Commits:** one per task, `phase: 5, status: plan-as-execute, persona: lead-developer, task: T-5.3/5.4`.
|
||||||
|
|
||||||
## Wave ordering rationale
|
## Wave ordering
|
||||||
|
|
||||||
- Wave 1 (coordination) creates the directory skeleton + verification script so Wave 2's scripts have a place to live and a check to satisfy.
|
- Wave 1 (frontend) builds the UI.
|
||||||
- Wave 2 (backend) builds the Gitea setup script and workflow skeletons.
|
- Wave 2 (backend) builds the dry-run script.
|
||||||
- Wave 3 (coordination) wires the verification script into README and updates traceability after Wave 2's scripts exist.
|
- Wave 3 (lead-developer) verifies + traceability.
|
||||||
- infra-stub-engineer and frontend-engineer have 0 tasks this phase (per PERSONAS.md), so their persona groups are skipped.
|
|
||||||
|
|
||||||
## Dependencies on other phases
|
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).
|
||||||
|
|
||||||
None. Phase 01 is self-contained. Phases 02-05 depend on Phase 01 having created the two new repos and the workflow skeletons.
|
## Dependencies
|
||||||
|
|
||||||
## Risk notes
|
- 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.
|
||||||
- If `gitea_setup.sh` hits a 401/403, treat as an escalation (token scope insufficient). Per run.md Step 4: retry once, then escalate. Do NOT proceed to SHIP with uncreated repos.
|
|
||||||
- The Gitea file-contents API requires the file to NOT already exist on first POST. The script must check existence first (GET) and skip POST if 200. Otherwise 422.
|
|
||||||
- Creating branches requires the default branch to exist first (`auto_init: true` handles this).
|
|
||||||
+18
-1
@@ -75,4 +75,21 @@ Build a runnable demo (Linux + GitHub/Gitea Actions) that walks executives throu
|
|||||||
| D-013 | Gitea has no environments API and ignores `jobs.<id>.environment` — model QA/Prod gates as `workflow_dispatch` approval inputs (D-004 fallback) | Research confirms `environment:` blocks are ignored by act_runner | Approval gates become dispatch inputs; "environments" become workflow job names + optional branch protection on `qa`/`prod` branches |
|
| D-013 | Gitea has no environments API and ignores `jobs.<id>.environment` — model QA/Prod gates as `workflow_dispatch` approval inputs (D-004 fallback) | Research confirms `environment:` blocks are ignored by act_runner | Approval gates become dispatch inputs; "environments" become workflow job names + optional branch protection on `qa`/`prod` branches |
|
||||||
| D-014 | Cross-repo triggering uses the `workflow_dispatch` Gitea API (POST `/actions/workflows/{id}/dispatches`) from inside a step instead of `repository_dispatch` | Gitea Actions does not support `repository_dispatch` | Issue-trigger workflow calls the main pipeline via authenticated dispatch from a step |
|
| D-014 | Cross-repo triggering uses the `workflow_dispatch` Gitea API (POST `/actions/workflows/{id}/dispatches`) from inside a step instead of `repository_dispatch` | Gitea Actions does not support `repository_dispatch` | Issue-trigger workflow calls the main pipeline via authenticated dispatch from a step |
|
||||||
| D-015 | New repos `acdl-contracts` and `acdl-evidence` use `default_branch: "main"` with `auto_init: true` | Matches Gitea `DEFAULT_BRANCH=main`; required for the default branch to exist before any push | Reusable-workflow `uses:` references still pin `acdl` workflows to `@milestone/v1.0-initial` |
|
| D-015 | New repos `acdl-contracts` and `acdl-evidence` use `default_branch: "main"` with `auto_init: true` | Matches Gitea `DEFAULT_BRANCH=main`; required for the default branch to exist before any push | Reusable-workflow `uses:` references still pin `acdl` workflows to `@milestone/v1.0-initial` |
|
||||||
| D-016 | Pages placeholder for Phase 01 is a minimal HTML stub (`<title>ACDL Evidence</title>` + "evidence stream coming soon"); full UI deferred to Phase 05 | Phase 01 success criterion is "Pages returns 200 with placeholder index.html" but Gitea has no Pages | Raw-URL HTTP 200 against `index.html` substitutes for the Pages check; full timeline UI built in Phase 05 |
|
| D-016 | Pages placeholder for Phase 01 is a minimal HTML stub (`<title>ACDL Evidence</title>` + "evidence stream coming soon"); full UI deferred to Phase 05 | Phase 01 success criterion is "Pages returns 200 with placeholder index.html" but Gitea has no Pages | Raw-URL HTTP 200 against `index.html` substitutes for the Pages check; full timeline UI built in Phase 05 |
|
||||||
|
| D-017 | Each L1 `manifest.yaml` declares a single `inputs:` map of named string keys with descriptions; no nested types (substrate-agnostic, max-depth-1) | REQ-02/03 say "declared inputs"; spec forbids composition and cloud-specific types | Uniform, parseable schema that Phase 03's `mock_executor.sh` can read with python+yaml |
|
||||||
|
| D-018 | L1 `mock_apply.sh` reads its own `manifest.yaml` for self-identification but ignores the input values (uniform stub per D-007) | D-007 mandates a literal echo + 1s sleep + exit 0; inputs are declared for traceability, not consumed | Predictable evidence events + clean separation from Phase 03 where L2s pass inputs to L1s |
|
||||||
|
| D-019 | The 8 L1 names are fixed per REQ-02: `l1-eks-fargate`, `l1-iam-role`, `l1-lambda`, `l1-api-gateway`, `l1-eventbridge`, `l1-sqs`, `l1-s3`, `l1-cloudwatch` | REQ-02 literal | Phase 02 enumerates them exactly; no naming freedom |
|
||||||
|
| D-020 | L2 `manifest.yaml` schema: `name`, `kind: l2`, `description`, `l1s:` (list of `{name, inputs: map}` entries). L2 references L1s by name (no path); inputs are string maps per L1 manifest declarations | REQ-04 says L2 "composes L1s"; REQ-05 caps depth at 5 (L2→L1 is depth 1) | mock_executor.sh reads `l1s:` and invokes each L1's `mock_apply.sh` |
|
||||||
|
| D-021 | `contract.yaml` schema: `stack` (L2 name), `inputs` (string map for the L2's top-level params), optional `public-ingress: bool` (the policy violation key per REQ-07) | REQ-07 cites `public-ingress: true` as the forbidden key; REQ-08's confidence signal keys off policy pass/fail | Single flat schema drives both policy_checker and the mock_executor |
|
||||||
|
| D-022 | `mock_executor.sh` writes `state.json` with shape `{"l2": "<name>", "l1s": [{"name":"...","applied":true,"exit_code":0}], "contract": <contract.yaml parsed>}` to the cwd; idempotent (overwrites) | REQ-06 says "writes state.json" but does not specify shape | Deterministic, parseable; Phase 05's evidence UI can include it in the audit trail |
|
||||||
|
| D-023 | `evidence_writer.py` appends events to `audit.json` (a JSON array of event objects). Each event: `{"seq": N, "ts": <iso8601>, "stage": "dev|qa|prod|finalize", "event": "<string>", "prev_hash": "<sha256>", "hash": "<sha256 of canonical json of this event with hash field empty>"}`. The genesis event has `prev_hash: "GENESIS"` and `seq: 0` | D-005 mandates hash-chained ledger; canonical JSON for deterministic hashing | Visible tamper-evidence without overengineering; Phase 05 UI reads the array |
|
||||||
|
| D-024 | `confidence_signal.py` reads `contract.yaml`, calls `policy_checker.py` (as a subprocess or import), returns base 0.90 on pass and 0.40 with reason code on policy failure; prints `{"score": 0.90|0.40, "reason": "<POLICY_VIOLATION:...|>"}` to stdout; exit 0 always | REQ-08 literal: base 0.90, drops to 0.40, gate ≥ 0.50 | Deterministic JSON output for the pipeline to consume |
|
||||||
|
| D-025 | `policy_checker.py` reads `contract.yaml`, fails with exit code 1 and stdout `POLICY_VIOLATION:PUBLIC_INGRESS` if `public-ingress: true`; otherwise exits 0 with stdout `POLICY_PASS` | REQ-07 literal | Single source of policy truth; called by confidence_signal and the pipeline directly |
|
||||||
|
| D-026 | `l3b_agent_stub.py` reads Issue body text from argv[1] (or stdin if no argv), applies the D-008 keyword map, writes a `contract.yaml` to stdout (or to `-o <path>`). Output contract uses the D-021 schema with `stack:` set to the mapped L2 name and a fixed `inputs:` map per L2 | D-008 + Act 3 example; L3B must produce the same contract format as L3A | Deterministic keyword parser; no external APIs |
|
||||||
|
| 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-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 |
|
||||||
+15
-15
@@ -57,18 +57,18 @@
|
|||||||
|
|
||||||
| Requirement | Phase | Status |
|
| Requirement | Phase | Status |
|
||||||
|-------------|-------|--------|
|
|-------------|-------|--------|
|
||||||
| REQ-01 | 1 | covered (pending VERIFY) |
|
| REQ-01 | 1 | complete (v1.0.1) |
|
||||||
| REQ-02 | 2 | pending |
|
| REQ-02 | 2 | complete (v1.0.2) |
|
||||||
| REQ-03 | 2 | pending |
|
| REQ-03 | 2 | complete (v1.0.2) |
|
||||||
| REQ-04 | 3 | pending |
|
| REQ-04 | 3 | complete (v1.0.3) |
|
||||||
| REQ-05 | 3 | pending |
|
| REQ-05 | 3 | complete (v1.0.3) |
|
||||||
| REQ-06 | 3 | pending |
|
| REQ-06 | 3 | complete (v1.0.3) |
|
||||||
| REQ-07 | 3 | pending |
|
| REQ-07 | 3 | complete (v1.0.3) |
|
||||||
| REQ-08 | 3 | pending |
|
| REQ-08 | 3 | complete (v1.0.3) |
|
||||||
| REQ-09 | 1 | covered (pending VERIFY) |
|
| REQ-09 | 1 | complete (v1.0.1) |
|
||||||
| REQ-10 | 4 | partial (skeleton in Phase 01; full impl in Phase 04) |
|
| REQ-10 | 4 | complete (v1.0.4) |
|
||||||
| REQ-11 | 3 | pending |
|
| REQ-11 | 3 | complete (v1.0.3) |
|
||||||
| REQ-12 | 4 | partial (skeleton in Phase 01; full impl in Phase 04) |
|
| REQ-12 | 4 | complete (v1.0.4) |
|
||||||
| REQ-13 | 5 | pending |
|
| REQ-13 | 5 | covered (pending VERIFY) |
|
||||||
| REQ-14 | 5 | pending |
|
| REQ-14 | 5 | covered (pending VERIFY) |
|
||||||
| REQ-15 | 5 | pending |
|
| REQ-15 | 5 | covered (pending VERIFY) |
|
||||||
+5
-5
@@ -8,7 +8,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
|||||||
|
|
||||||
### Phase 01 — repo-scaffolding
|
### Phase 01 — repo-scaffolding
|
||||||
- **Description:** Create the three repos under `continuous-intelligence` (`acdl-contracts`, `acdl-evidence`; `acdl` already exists), seed directory layouts, configure Pages on `acdl-evidence`, add environment protection for `qa` and `prod` on `acdl-contracts`.
|
- **Description:** Create the three repos under `continuous-intelligence` (`acdl-contracts`, `acdl-evidence`; `acdl` already exists), seed directory layouts, configure Pages on `acdl-evidence`, add environment protection for `qa` and `prod` on `acdl-contracts`.
|
||||||
- **Status:** executing
|
- **Status:** complete (v1.0.1)
|
||||||
- **Depends on:** —
|
- **Depends on:** —
|
||||||
- **Requirements:** REQ-01, REQ-09, REQ-10
|
- **Requirements:** REQ-01, REQ-09, REQ-10
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
@@ -18,7 +18,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
|||||||
|
|
||||||
### Phase 02 — l1-modules
|
### Phase 02 — l1-modules
|
||||||
- **Description:** Create all 8 L1 module folders under `acdl/modules/l1/`, each with `manifest.yaml` (declared inputs) and `mock_apply.sh` (uniform echo + 1s sleep + exit 0).
|
- **Description:** Create all 8 L1 module folders under `acdl/modules/l1/`, each with `manifest.yaml` (declared inputs) and `mock_apply.sh` (uniform echo + 1s sleep + exit 0).
|
||||||
- **Status:** not_started
|
- **Status:** complete (v1.0.2)
|
||||||
- **Depends on:** [1]
|
- **Depends on:** [1]
|
||||||
- **Requirements:** REQ-02, REQ-03
|
- **Requirements:** REQ-02, REQ-03
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
@@ -27,7 +27,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
|||||||
|
|
||||||
### Phase 03 — l2-modules-and-core-scripts
|
### Phase 03 — l2-modules-and-core-scripts
|
||||||
- **Description:** Create the 4 L2 compositions under `acdl/modules/l2/` referencing L1s, plus the 5 core scripts in `acdl/scripts/` (`mock_executor.sh`, `policy_checker.py`, `confidence_signal.py`, `evidence_writer.py`, `l3b_agent_stub.py`).
|
- **Description:** Create the 4 L2 compositions under `acdl/modules/l2/` referencing L1s, plus the 5 core scripts in `acdl/scripts/` (`mock_executor.sh`, `policy_checker.py`, `confidence_signal.py`, `evidence_writer.py`, `l3b_agent_stub.py`).
|
||||||
- **Status:** not_started
|
- **Status:** complete (v1.0.3)
|
||||||
- **Depends on:** [2]
|
- **Depends on:** [2]
|
||||||
- **Requirements:** REQ-04, REQ-05, REQ-06, REQ-07
|
- **Requirements:** REQ-04, REQ-05, REQ-06, REQ-07
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
@@ -39,7 +39,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
|
|||||||
|
|
||||||
### Phase 04 — pipeline-and-approval-gates
|
### Phase 04 — pipeline-and-approval-gates
|
||||||
- **Description:** Build the reusable pipeline workflow in `acdl/.gitea/workflows/` (Dev → QA → Prod → Finalize) plus the issue-triggered L3B workflow in `acdl-contracts/.gitea/workflows/`. Wire environment protection for QA and Prod.
|
- **Description:** Build the reusable pipeline workflow in `acdl/.gitea/workflows/` (Dev → QA → Prod → Finalize) plus the issue-triggered L3B workflow in `acdl-contracts/.gitea/workflows/`. Wire environment protection for QA and Prod.
|
||||||
- **Status:** not_started
|
- **Status:** complete (v1.0.4)
|
||||||
- **Depends on:** [3]
|
- **Depends on:** [3]
|
||||||
- **Requirements:** REQ-08, REQ-09, REQ-10, REQ-12
|
- **Requirements:** REQ-08, REQ-09, REQ-10, REQ-12
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
@@ -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
|
### 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.
|
- **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]
|
- **Depends on:** [4]
|
||||||
- **Requirements:** REQ-11, REQ-13, REQ-14, REQ-15
|
- **Requirements:** REQ-11, REQ-13, REQ-14, REQ-15
|
||||||
- **Success Criteria:**
|
- **Success Criteria:**
|
||||||
|
|||||||
+118
-40
@@ -1,75 +1,153 @@
|
|||||||
# ACDL reusable pipeline workflow (Phase 01 skeleton).
|
# ACDL pipeline workflow (Phase 04 implementation).
|
||||||
#
|
#
|
||||||
# This workflow is called from acdl-contracts via:
|
# 3-dispatch approval-gate topology (D-027 / D-028; ARCHITECTURE.md
|
||||||
# uses: continuous-intelligence/acdl/.gitea/workflows/pipeline.yml@milestone/v1.0-initial
|
# "Phase 04 pipeline topology"):
|
||||||
#
|
#
|
||||||
# Branch pinning rule (see .ciagent/ARCHITECTURE.md): the `acdl` repo's default
|
# Dispatch 1 (initial): approve_qa=false, approve_prod=false
|
||||||
# branch is `milestone/v1.0-initial`, so `uses:` references must pin to
|
# -> runs the `dev` job (policy check, confidence
|
||||||
# `@milestone/v1.0-initial`, NOT `@main`.
|
# gate, mock_executor, evidence + finalize).
|
||||||
|
# Dispatch 2 (QA approve): approve_qa=true, approve_prod=false
|
||||||
|
# -> runs the `qa-gate` job (records QA approval
|
||||||
|
# in the audit chain via evidence_writer +
|
||||||
|
# finalize_evidence).
|
||||||
|
# Dispatch 3 (Prod approve): approve_prod=true
|
||||||
|
# -> runs the `prod-gate` job, then the `finalize`
|
||||||
|
# job (needs: prod-gate) which writes the final
|
||||||
|
# evidence event and commits audit.json to
|
||||||
|
# acdl-evidence.
|
||||||
#
|
#
|
||||||
# Phase 04 will implement the actual stage logic + approval gates (D-013:
|
# Gitea Actions limitations driving this design:
|
||||||
# Gitea has no environments API; gates become workflow_dispatch approval
|
# - No `repository_dispatch` trigger (D-014).
|
||||||
# inputs).
|
# - No environments API / `environment:` blocks are ignored (D-013).
|
||||||
|
# - Re-dispatch starts a NEW run; artifacts do NOT survive between runs,
|
||||||
|
# so state is persisted to acdl-evidence via the file-contents API
|
||||||
|
# (D-028 / finalize_evidence.py) instead of via artifacts.
|
||||||
|
#
|
||||||
|
# Branch-pin rule (ARCHITECTURE.md "Branch pinning rule"):
|
||||||
|
# This workflow lives on `acdl`'s default branch `milestone/v1.0-initial`.
|
||||||
|
# Cross-repo `uses:` references (e.g. the issue-trigger's checkout of
|
||||||
|
# l3b_agent_stub.py) MUST pin to `@milestone/v1.0-initial`, NOT `@main`
|
||||||
|
# (the `acdl` repo has no `main` branch). This workflow is invoked via
|
||||||
|
# the workflow_dispatch API (D-014), NOT via `workflow_call`, so the
|
||||||
|
# `uses:` rule applies to the issue-trigger's checkout of the acdl repo,
|
||||||
|
# not to this file itself.
|
||||||
name: acdl-pipeline
|
name: acdl-pipeline
|
||||||
|
|
||||||
on:
|
"on":
|
||||||
workflow_call:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
contract-ref:
|
contract-ref:
|
||||||
description: "Ref on acdl-contracts that triggered the pipeline"
|
description: "Ref on acdl-contracts that carries the contract"
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: main
|
default: main
|
||||||
|
approve_qa:
|
||||||
|
description: "Human approval to advance past QA"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
approve_prod:
|
||||||
|
description: "Human approval to advance past Prod"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dev:
|
dev:
|
||||||
name: "Dev (autonomous)"
|
name: "Dev (autonomous)"
|
||||||
|
if: inputs.approve_qa != true && inputs.approve_prod != true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Phase 04 will implement: checkout acdl + acdl-contracts, run
|
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||||
# policy_checker.py, mock_executor.sh, confidence_signal.py, write
|
uses: actions/checkout@v4
|
||||||
# evidence via evidence_writer.py.
|
with:
|
||||||
- name: "Dev stage placeholder"
|
ref: milestone/v1.0-initial
|
||||||
|
|
||||||
|
- name: "Checkout acdl-contracts at contract-ref"
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: continuous-intelligence/acdl-contracts
|
||||||
|
ref: ${{ inputs.contract-ref }}
|
||||||
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
path: acdl-contracts
|
||||||
|
|
||||||
|
- name: "Policy check"
|
||||||
run: |
|
run: |
|
||||||
echo "Dev stage placeholder (Phase 01 skeleton)"
|
python3 scripts/policy_checker.py acdl-contracts/contract.yaml
|
||||||
echo "Phase 04 will run policy_checker, mock_executor, confidence_signal, evidence_writer"
|
|
||||||
exit 0
|
- name: "Confidence signal"
|
||||||
|
id: confidence
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
SCORE_JSON=$(python3 scripts/confidence_signal.py acdl-contracts/contract.yaml)
|
||||||
|
echo "$SCORE_JSON"
|
||||||
|
echo "score_json=$SCORE_JSON" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: "Apply or reject based on confidence (gate < 0.50)"
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
SCORE=$(python3 -c "import json,sys; print(json.load(sys.stdin)['score'])" <<< '${{ steps.confidence.outputs.score_json }}')
|
||||||
|
python3 -c "import sys; sys.exit(0 if float('${SCORE}') >= 0.50 else 1)"
|
||||||
|
THRESHOLD_RC=$?
|
||||||
|
if [ "$THRESHOLD_RC" -ne 0 ]; then
|
||||||
|
python3 scripts/evidence_writer.py --stage dev --event "dev rejected: confidence < 0.50" --audit audit.json
|
||||||
|
python3 scripts/finalize_evidence.py --audit audit.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("acdl-contracts/contract.yaml"))["stack"])')
|
||||||
|
bash scripts/mock_executor.sh acdl-contracts/contract.yaml
|
||||||
|
python3 scripts/evidence_writer.py --stage dev --event "dev applied: ${STACK}" --audit audit.json
|
||||||
|
python3 scripts/finalize_evidence.py --audit audit.json
|
||||||
|
|
||||||
|
- name: "Upload dev state artifacts (best-effort)"
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dev-state
|
||||||
|
path: |
|
||||||
|
audit.json
|
||||||
|
state.json
|
||||||
|
|
||||||
qa-gate:
|
qa-gate:
|
||||||
name: "QA (manual approval)"
|
name: "QA (manual approval)"
|
||||||
needs: dev
|
if: inputs.approve_qa == true && inputs.approve_prod != true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Phase 04 will implement: gate via workflow_dispatch approval input
|
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||||
# (D-013 fallback; Gitea ignores jobs.<id>.environment).
|
uses: actions/checkout@v4
|
||||||
- name: "QA gate placeholder"
|
with:
|
||||||
|
ref: milestone/v1.0-initial
|
||||||
|
|
||||||
|
- name: "Record QA approval in evidence"
|
||||||
run: |
|
run: |
|
||||||
echo "QA gate placeholder (Phase 01 skeleton)"
|
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit audit.json
|
||||||
echo "Phase 04 will pause here for human approval via workflow_dispatch"
|
python3 scripts/finalize_evidence.py --audit audit.json
|
||||||
exit 0
|
|
||||||
|
|
||||||
prod-gate:
|
prod-gate:
|
||||||
name: "Prod (manual approval)"
|
name: "Prod (manual approval)"
|
||||||
needs: qa-gate
|
if: inputs.approve_prod == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Phase 04 will implement: same approval-input gate as qa-gate.
|
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||||
- name: "Prod gate placeholder"
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: milestone/v1.0-initial
|
||||||
|
|
||||||
|
- name: "Record Prod approval in evidence"
|
||||||
run: |
|
run: |
|
||||||
echo "Prod gate placeholder (Phase 01 skeleton)"
|
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit audit.json
|
||||||
echo "Phase 04 will pause here for human approval via workflow_dispatch"
|
python3 scripts/finalize_evidence.py --audit audit.json
|
||||||
exit 0
|
|
||||||
|
|
||||||
finalize:
|
finalize:
|
||||||
name: "Finalize (publish evidence)"
|
name: "Finalize (publish evidence)"
|
||||||
needs: prod-gate
|
needs: [prod-gate]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Phase 04/05 will implement: commit audit.json to acdl-evidence main
|
- name: "Checkout acdl (this repo, pinned to milestone/v1.0-initial)"
|
||||||
# via the Gitea file-contents API; raw URL republishes index.html +
|
uses: actions/checkout@v4
|
||||||
# audit.json for the timeline UI (D-012).
|
with:
|
||||||
- name: "Finalize placeholder"
|
ref: milestone/v1.0-initial
|
||||||
|
|
||||||
|
- name: "Write finalize event + commit audit.json to acdl-evidence"
|
||||||
run: |
|
run: |
|
||||||
echo "Finalize placeholder (Phase 01 skeleton)"
|
python3 scripts/evidence_writer.py --stage finalize --event "pipeline complete: audit.json committed to acdl-evidence" --audit audit.json
|
||||||
echo "Phase 04/05 will commit audit.json to acdl-evidence main"
|
python3 scripts/finalize_evidence.py --audit audit.json
|
||||||
exit 0
|
|
||||||
@@ -1,18 +1,35 @@
|
|||||||
# ACDL issue-to-contract workflow (Phase 01 skeleton, reference copy).
|
# ACDL issue-to-contract workflow (Phase 04 implementation).
|
||||||
#
|
|
||||||
# This file is the source-of-truth copy kept in the `acdl` repo under
|
|
||||||
# contracts-repo/.gitea/workflows/. Phase 04 will push it to the actual
|
|
||||||
# `acdl-contracts` repo under .gitea/workflows/ and implement the real
|
|
||||||
# step bodies.
|
|
||||||
#
|
#
|
||||||
# Trigger: a new Issue is opened in acdl-contracts. The workflow runs
|
# Trigger: a new Issue is opened in acdl-contracts. The workflow runs
|
||||||
# l3b_agent_stub.py to map the Issue body to a contract.yaml, commits the
|
# l3b_agent_stub.py (checked out from the `acdl` repo, pinned to
|
||||||
# contract to a new branch, closes the Issue, and triggers the main
|
# @milestone/v1.0-initial) to map the Issue body to a contract.yaml, commits
|
||||||
# pipeline in the `acdl` repo via the workflow_dispatch API (D-014; Gitea
|
# the contract to a new branch `contract/<issue-number>` on acdl-contracts
|
||||||
# Actions does not support repository_dispatch).
|
# via the Gitea file-contents API, closes the Issue with a comment, and
|
||||||
|
# dispatches the main pipeline in the `acdl` repo via the workflow_dispatch
|
||||||
|
# API (D-014; Gitea Actions does not support repository_dispatch).
|
||||||
|
#
|
||||||
|
# Cross-repo trigger (D-014):
|
||||||
|
# The final step POSTs to
|
||||||
|
# /api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches
|
||||||
|
# with body {"ref": "milestone/v1.0-initial",
|
||||||
|
# "inputs": {"contract-ref": "contract/<issue-number>"}}.
|
||||||
|
#
|
||||||
|
# Branch-pin rule (ARCHITECTURE.md):
|
||||||
|
# The `acdl` repo's default branch is `milestone/v1.0-initial`, so the
|
||||||
|
# checkout step pins `ref: milestone/v1.0-initial`. The pipeline dispatch
|
||||||
|
# also pins `ref: milestone/v1.0-initial` (the workflow file lives on
|
||||||
|
# that branch). The new `contract/<n>` branch is created on acdl-contracts
|
||||||
|
# (whose default branch is `main`, per D-015).
|
||||||
|
#
|
||||||
|
# File-contents POST with `new_branch` (D-030):
|
||||||
|
# The POST to /repos/.../contents/contract.yaml includes
|
||||||
|
# `new_branch: contract/<n>`, which tells Gitea to create the file on a
|
||||||
|
# NEW branch off the current head of `branch: main` instead of committing
|
||||||
|
# directly to main. This avoids a separate branch-create + commit round
|
||||||
|
# trip.
|
||||||
name: issue-to-contract
|
name: issue-to-contract
|
||||||
|
|
||||||
on:
|
"on":
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [opened]
|
||||||
|
|
||||||
@@ -20,21 +37,109 @@ jobs:
|
|||||||
parse-and-trigger:
|
parse-and-trigger:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Phase 04 will implement:
|
- name: "Checkout acdl (pinned to milestone/v1.0-initial for l3b_agent_stub.py)"
|
||||||
# 1. checkout acdl-contracts (so l3b_agent_stub.py is available).
|
uses: actions/checkout@v4
|
||||||
# 2. run: python3 scripts/l3b_agent_stub.py "${{ gitea.event.issue.body }}" > contract.yaml
|
with:
|
||||||
# 3. parse the generated contract; commit it to a new branch
|
repository: continuous-intelligence/acdl
|
||||||
# (e.g. contract/<issue-number>).
|
ref: milestone/v1.0-initial
|
||||||
# 4. push the branch.
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
# 5. close the Issue with a comment linking to the pipeline run.
|
|
||||||
# 6. trigger the main pipeline:
|
- name: "Parse Issue body into contract.yaml"
|
||||||
# curl -X POST \
|
env:
|
||||||
# -H "Authorization: token ${GITEA_TOKEN}" \
|
ISSUE_BODY: ${{ gitea.event.issue.body }}
|
||||||
# https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/workflows/<id>/dispatches \
|
|
||||||
# -d '{"ref":"milestone/v1.0-initial","inputs":{"contract-ref":"<branch>"}}'
|
|
||||||
- name: "Issue-trigger placeholder"
|
|
||||||
run: |
|
run: |
|
||||||
echo "issue-to-contract placeholder (Phase 01 skeleton)"
|
# Pass the Issue body via an env var to avoid shell injection from
|
||||||
echo "Issue body: ${{ gitea.event.issue.body }}"
|
# arbitrary Issue text. l3b_agent_stub.py reads argv[1]; we pass
|
||||||
echo "Phase 04 will run l3b_agent_stub.py, commit contract.yaml, close issue, dispatch pipeline"
|
# the env var quoted so no metacharacter interpretation happens.
|
||||||
exit 0
|
python3 scripts/l3b_agent_stub.py "$ISSUE_BODY" -o contract.yaml
|
||||||
|
echo "--- generated contract.yaml ---"
|
||||||
|
cat contract.yaml
|
||||||
|
|
||||||
|
- name: "Commit contract.yaml to new branch contract/${{ gitea.event.issue.number }} on acdl-contracts"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("contract.yaml"))["stack"])')
|
||||||
|
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
||||||
|
BRANCH="contract/${ISSUE_NUMBER}"
|
||||||
|
HOST="https://git.cloudinit.dev"
|
||||||
|
API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/contents/contract.yaml"
|
||||||
|
B64=$(base64 -w 0 contract.yaml)
|
||||||
|
BODY=$(python3 -c "
|
||||||
|
import json
|
||||||
|
print(json.dumps({
|
||||||
|
'content': '${B64}',
|
||||||
|
'message': 'l3b: contract for issue #${ISSUE_NUMBER}',
|
||||||
|
'branch': 'main',
|
||||||
|
'new_branch': '${BRANCH}'
|
||||||
|
}))
|
||||||
|
")
|
||||||
|
STATUS=$(curl -sS -o /tmp/contract_post.json -w "%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$BODY" \
|
||||||
|
"$API")
|
||||||
|
echo "POST contract.yaml -> HTTP ${STATUS}"
|
||||||
|
cat /tmp/contract_post.json || true
|
||||||
|
case "$STATUS" in
|
||||||
|
201) echo "contract.yaml committed on branch ${BRANCH}" ;;
|
||||||
|
*) echo "ERROR: file-contents POST failed (HTTP ${STATUS})" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
echo "STACK=${STACK}" >> "$GITHUB_ENV"
|
||||||
|
echo "BRANCH=${BRANCH}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: "Comment on Issue + close it"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
||||||
|
HOST="https://git.cloudinit.dev"
|
||||||
|
ISSUES_API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/issues/${ISSUE_NUMBER}"
|
||||||
|
COMMENT_BODY=$(python3 -c "
|
||||||
|
import json
|
||||||
|
print(json.dumps({'body': 'Generated contract.yaml for stack \`' + '${STACK}' + '\` on branch \`' + '${BRANCH}' + '\`. Pipeline dispatched.'}))
|
||||||
|
")
|
||||||
|
curl -sS -o /tmp/comment.json -w "comment HTTP %{http_code}\n" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$COMMENT_BODY" \
|
||||||
|
"${ISSUES_API}/comments"
|
||||||
|
CLOSE_BODY='{"state":"closed"}'
|
||||||
|
curl -sS -o /tmp/close.json -w "close HTTP %{http_code}\n" \
|
||||||
|
-X PATCH \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$CLOSE_BODY" \
|
||||||
|
"${ISSUES_API}"
|
||||||
|
|
||||||
|
- name: "Dispatch the pipeline on acdl (contract-ref = contract/${{ gitea.event.issue.number }})"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
|
||||||
|
HOST="https://git.cloudinit.dev"
|
||||||
|
DISPATCH_URL="${HOST}/api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches"
|
||||||
|
BODY=$(python3 -c "
|
||||||
|
import json
|
||||||
|
print(json.dumps({
|
||||||
|
'ref': 'milestone/v1.0-initial',
|
||||||
|
'inputs': {'contract-ref': 'contract/${ISSUE_NUMBER}'}
|
||||||
|
}))
|
||||||
|
")
|
||||||
|
STATUS=$(curl -sS -o /tmp/dispatch.json -w "%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$BODY" \
|
||||||
|
"$DISPATCH_URL")
|
||||||
|
echo "pipeline dispatch -> HTTP ${STATUS}"
|
||||||
|
cat /tmp/dispatch.json || true
|
||||||
|
case "$STATUS" in
|
||||||
|
201|202|204) echo "pipeline dispatched (contract-ref=contract/${ISSUE_NUMBER})" ;;
|
||||||
|
*) echo "ERROR: pipeline dispatch failed (HTTP ${STATUS})" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: l1-api-gateway
|
||||||
|
kind: l1
|
||||||
|
description: HTTP routing primitive
|
||||||
|
inputs:
|
||||||
|
api_name:
|
||||||
|
description: Name of the API Gateway REST/HTTP API
|
||||||
|
type: string
|
||||||
|
stage_name:
|
||||||
|
description: Name of the deployment stage (e.g. dev, prod)
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-api-gateway] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-api-gateway] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: l1-cloudwatch
|
||||||
|
kind: l1
|
||||||
|
description: Observability primitive
|
||||||
|
inputs:
|
||||||
|
log_group_name:
|
||||||
|
description: Name of the CloudWatch log group
|
||||||
|
type: string
|
||||||
|
metric_namespace:
|
||||||
|
description: Namespace under which custom metrics are emitted
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-cloudwatch] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-cloudwatch] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
name: l1-eks-fargate
|
||||||
|
kind: l1
|
||||||
|
description: Serverless container compute substrate
|
||||||
|
inputs:
|
||||||
|
cluster_name:
|
||||||
|
description: Name of the EKS cluster to target
|
||||||
|
type: string
|
||||||
|
region:
|
||||||
|
description: AWS region the cluster runs in
|
||||||
|
type: string
|
||||||
|
cpu_arch:
|
||||||
|
description: CPU architecture for Fargate pods (x86_64 or arm64)
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-eks-fargate] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-eks-fargate] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: l1-eventbridge
|
||||||
|
kind: l1
|
||||||
|
description: Event bus primitive
|
||||||
|
inputs:
|
||||||
|
bus_name:
|
||||||
|
description: Name of the EventBridge bus
|
||||||
|
type: string
|
||||||
|
rule_name:
|
||||||
|
description: Name of the event rule on the bus
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-eventbridge] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-eventbridge] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: l1-iam-role
|
||||||
|
kind: l1
|
||||||
|
description: Identity and access role primitive
|
||||||
|
inputs:
|
||||||
|
role_name:
|
||||||
|
description: Name of the IAM role to create
|
||||||
|
type: string
|
||||||
|
trust_policy:
|
||||||
|
description: JSON trust policy document defining who can assume the role
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-iam-role] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-iam-role] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
name: l1-lambda
|
||||||
|
kind: l1
|
||||||
|
description: Event-driven function primitive
|
||||||
|
inputs:
|
||||||
|
function_name:
|
||||||
|
description: Name of the Lambda function
|
||||||
|
type: string
|
||||||
|
runtime:
|
||||||
|
description: Lambda runtime identifier (e.g. python3.12, nodejs20.x)
|
||||||
|
type: string
|
||||||
|
handler:
|
||||||
|
description: Handler entrypoint in the form module.function
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-lambda] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-lambda] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
name: l1-s3
|
||||||
|
kind: l1
|
||||||
|
description: Object store primitive
|
||||||
|
inputs:
|
||||||
|
bucket_name:
|
||||||
|
description: Globally unique name of the S3 bucket
|
||||||
|
type: string
|
||||||
|
region:
|
||||||
|
description: AWS region the bucket lives in
|
||||||
|
type: string
|
||||||
|
retention_days:
|
||||||
|
description: Number of days to retain objects before expiration
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-s3] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-s3] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: l1-sqs
|
||||||
|
kind: l1
|
||||||
|
description: Queue primitive
|
||||||
|
inputs:
|
||||||
|
queue_name:
|
||||||
|
description: Name of the SQS queue
|
||||||
|
type: string
|
||||||
|
visibility_timeout:
|
||||||
|
description: Visibility timeout in seconds for in-flight messages
|
||||||
|
type: string
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "[L1: l1-sqs] applying..."
|
||||||
|
sleep 1
|
||||||
|
echo "[L1: l1-sqs] OK"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: l2-commodity-price-feed
|
||||||
|
kind: l2
|
||||||
|
description: Real-time commodity price ingestion from Platts
|
||||||
|
l1s:
|
||||||
|
- name: l1-eks-fargate
|
||||||
|
inputs:
|
||||||
|
cluster_name: price-feed-cluster
|
||||||
|
region: us-east-1
|
||||||
|
cpu_arch: arm64
|
||||||
|
- name: l1-lambda
|
||||||
|
inputs:
|
||||||
|
function_name: price-ingest
|
||||||
|
runtime: python3.11
|
||||||
|
handler: index.handler
|
||||||
|
- name: l1-api-gateway
|
||||||
|
inputs:
|
||||||
|
api_name: platts-price-api
|
||||||
|
stage_name: dev
|
||||||
|
- name: l1-eventbridge
|
||||||
|
inputs:
|
||||||
|
bus_name: price-events
|
||||||
|
rule_name: price-publish-rule
|
||||||
|
- name: l1-s3
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-price-archive
|
||||||
|
region: us-east-1
|
||||||
|
retention_days: "90"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: l2-energy-analytics-api
|
||||||
|
kind: l2
|
||||||
|
description: Historical energy analytics query API
|
||||||
|
l1s:
|
||||||
|
- name: l1-eks-fargate
|
||||||
|
inputs:
|
||||||
|
cluster_name: analytics-cluster
|
||||||
|
region: us-east-1
|
||||||
|
cpu_arch: arm64
|
||||||
|
- name: l1-api-gateway
|
||||||
|
inputs:
|
||||||
|
api_name: energy-analytics-api
|
||||||
|
stage_name: dev
|
||||||
|
- name: l1-lambda
|
||||||
|
inputs:
|
||||||
|
function_name: analytics-query
|
||||||
|
runtime: python3.11
|
||||||
|
handler: index.handler
|
||||||
|
- name: l1-s3
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-analytics-data
|
||||||
|
region: us-east-1
|
||||||
|
retention_days: "2555"
|
||||||
|
- name: l1-cloudwatch
|
||||||
|
inputs:
|
||||||
|
log_group_name: /acdl/analytics-api
|
||||||
|
metric_namespace: acdl/analytics
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: l2-invoice-service
|
||||||
|
kind: l2
|
||||||
|
description: Billing and invoicing microservice for energy trades
|
||||||
|
l1s:
|
||||||
|
- name: l1-eks-fargate
|
||||||
|
inputs:
|
||||||
|
cluster_name: invoice-cluster
|
||||||
|
region: us-east-1
|
||||||
|
cpu_arch: arm64
|
||||||
|
- name: l1-iam-role
|
||||||
|
inputs:
|
||||||
|
role_name: invoice-service-role
|
||||||
|
trust_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||||
|
- name: l1-lambda
|
||||||
|
inputs:
|
||||||
|
function_name: invoice-generator
|
||||||
|
runtime: python3.11
|
||||||
|
handler: index.handler
|
||||||
|
- name: l1-sqs
|
||||||
|
inputs:
|
||||||
|
queue_name: invoice-queue
|
||||||
|
visibility_timeout: "60"
|
||||||
|
- name: l1-s3
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-invoice-archive
|
||||||
|
region: us-east-1
|
||||||
|
retention_days: "365"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: l2-regulatory-reporting
|
||||||
|
kind: l2
|
||||||
|
description: Regulatory compliance and reporting for energy trading
|
||||||
|
l1s:
|
||||||
|
- name: l1-eks-fargate
|
||||||
|
inputs:
|
||||||
|
cluster_name: regulatory-cluster
|
||||||
|
region: us-east-1
|
||||||
|
cpu_arch: arm64
|
||||||
|
- name: l1-iam-role
|
||||||
|
inputs:
|
||||||
|
role_name: regulatory-reporting-role
|
||||||
|
trust_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
|
||||||
|
- name: l1-lambda
|
||||||
|
inputs:
|
||||||
|
function_name: regulatory-reporter
|
||||||
|
runtime: python3.11
|
||||||
|
handler: index.handler
|
||||||
|
- name: l1-sqs
|
||||||
|
inputs:
|
||||||
|
queue_name: regulatory-queue
|
||||||
|
visibility_timeout: "120"
|
||||||
|
- name: l1-s3
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-regulatory-archive
|
||||||
|
region: us-east-1
|
||||||
|
retention_days: "2555"
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""confidence_signal.py — REQ-08 / D-024
|
||||||
|
|
||||||
|
Reads a contract.yaml, invokes policy_checker.py as a subprocess, and emits
|
||||||
|
a deterministic JSON confidence score.
|
||||||
|
|
||||||
|
policy pass -> {"score": 0.90, "reason": "POLICY_PASS"}
|
||||||
|
policy fail -> {"score": 0.40, "reason": "<violation code>"}
|
||||||
|
|
||||||
|
Exit 0 ALWAYS (per D-024): the pipeline decides the gate, not this script's
|
||||||
|
exit code.
|
||||||
|
|
||||||
|
Input: argv[1] = path to a contract.yaml file.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("usage: confidence_signal.py <contract.yaml>", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
contract_path = sys.argv[1]
|
||||||
|
|
||||||
|
# Resolve policy_checker.py relative to this script so it works regardless
|
||||||
|
# of cwd. Use python3 + script path (not ./) per the contract.
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
policy_checker = os.path.join(here, "policy_checker.py")
|
||||||
|
|
||||||
|
proc = subprocess.run(
|
||||||
|
["python3", policy_checker, contract_path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode == 0:
|
||||||
|
score = "0.90"
|
||||||
|
# POLICY_PASS is the expected stdout; strip any trailing whitespace.
|
||||||
|
reason = proc.stdout.strip() or "POLICY_PASS"
|
||||||
|
else:
|
||||||
|
score = "0.40"
|
||||||
|
# The violation code (e.g. "POLICY_VIOLATION:PUBLIC_INGRESS") is on stdout.
|
||||||
|
reason = proc.stdout.strip() or "POLICY_VIOLATION:UNKNOWN"
|
||||||
|
|
||||||
|
# Emit with literal score (two-decimal form per the contract) and a quoted
|
||||||
|
# reason. Constructed manually so json.dumps does not collapse 0.90 -> 0.9.
|
||||||
|
print('{"score": ' + score + ', "reason": ' + json.dumps(reason) + '}')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+123
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""evidence_writer.py — REQ-11 / D-023 / D-005
|
||||||
|
|
||||||
|
Appends a hash-chained event to audit.json.
|
||||||
|
|
||||||
|
Each event: {"seq": N, "ts": <iso8601 UTC>, "stage": "...", "event": "...",
|
||||||
|
"prev_hash": "<sha256 or GENESIS>", "hash": "<sha256 of canonical json of this event with hash empty>"}
|
||||||
|
|
||||||
|
Hash chain (D-023):
|
||||||
|
1. Build event dict with hash = "" (empty string).
|
||||||
|
2. canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
||||||
|
3. hash = sha256(canonical.encode("utf-8")).hexdigest()
|
||||||
|
4. event["hash"] = hash
|
||||||
|
5. append to audit.json
|
||||||
|
|
||||||
|
Auto-genesis: if audit.json is empty/missing and --stage is not "genesis",
|
||||||
|
a genesis event (seq 0, prev_hash "GENESIS") is inserted first.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
--stage <dev|qa|prod|finalize|genesis> (required)
|
||||||
|
--event "<text>" (required)
|
||||||
|
--audit <path> (optional, default ./audit.json)
|
||||||
|
Output: stdout {"seq": N, "hash": "..."}
|
||||||
|
Exit: 0 on success, 1 on I/O error.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
GENESIS_EVENT_TEXT = "audit log initialized"
|
||||||
|
|
||||||
|
|
||||||
|
def now_iso8601_utc() -> str:
|
||||||
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def compute_hash(event: dict) -> str:
|
||||||
|
"""Compute the sha256 hash of an event using canonical JSON (D-023)."""
|
||||||
|
tmp = dict(event)
|
||||||
|
tmp["hash"] = ""
|
||||||
|
canonical = json.dumps(tmp, sort_keys=True, separators=(",", ":"))
|
||||||
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def make_event(seq: int, stage: str, event_text: str, prev_hash: str) -> dict:
|
||||||
|
event = {
|
||||||
|
"seq": seq,
|
||||||
|
"ts": now_iso8601_utc(),
|
||||||
|
"stage": stage,
|
||||||
|
"event": event_text,
|
||||||
|
"prev_hash": prev_hash,
|
||||||
|
"hash": "",
|
||||||
|
}
|
||||||
|
event["hash"] = compute_hash(event)
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
def load_audit(audit_path: str) -> list:
|
||||||
|
if not os.path.exists(audit_path):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(audit_path, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
return []
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return []
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_write(audit_path: str, data: list) -> None:
|
||||||
|
tmp_path = audit_path + ".tmp"
|
||||||
|
with open(tmp_path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(data, fh, indent=2)
|
||||||
|
fh.write("\n")
|
||||||
|
os.replace(tmp_path, audit_path)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Append a hash-chained event to audit.json")
|
||||||
|
parser.add_argument("--stage", required=True,
|
||||||
|
choices=["dev", "qa", "prod", "finalize", "genesis"])
|
||||||
|
parser.add_argument("--event", required=True)
|
||||||
|
parser.add_argument("--audit", default="./audit.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
events = load_audit(args.audit)
|
||||||
|
|
||||||
|
# Auto-genesis: if the log is empty and the caller did not ask for a
|
||||||
|
# genesis event, seed one first.
|
||||||
|
if len(events) == 0 and args.stage != "genesis":
|
||||||
|
genesis = make_event(seq=0, stage="genesis", event_text=GENESIS_EVENT_TEXT,
|
||||||
|
prev_hash="GENESIS")
|
||||||
|
events.append(genesis)
|
||||||
|
|
||||||
|
# Determine the new seq + prev_hash.
|
||||||
|
if events:
|
||||||
|
last = events[-1]
|
||||||
|
seq = last["seq"] + 1
|
||||||
|
prev_hash = last["hash"]
|
||||||
|
else:
|
||||||
|
seq = 0
|
||||||
|
prev_hash = "GENESIS"
|
||||||
|
|
||||||
|
new_event = make_event(seq=seq, stage=args.stage, event_text=args.event,
|
||||||
|
prev_hash=prev_hash)
|
||||||
|
events.append(new_event)
|
||||||
|
|
||||||
|
try:
|
||||||
|
atomic_write(args.audit, events)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"evidence_writer: I/O error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(json.dumps({"seq": new_event["seq"], "hash": new_event["hash"]}))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+182
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""finalize_evidence.py — REQ-10 / D-028 / D-029
|
||||||
|
|
||||||
|
Uploads (PUT or POST) a local `audit.json` to the `acdl-evidence` repo on
|
||||||
|
Gitea via the file-contents API. Used by the pipeline workflow steps to
|
||||||
|
persist the hash-chained audit trail to `acdl-evidence` between dispatches
|
||||||
|
(D-028 state-persistence across re-dispatches; D-029 finalize step).
|
||||||
|
|
||||||
|
Uses only the Python standard library (urllib.request) so it has no
|
||||||
|
external dependency on `requests`. Auth header: `Authorization: token <token>`.
|
||||||
|
|
||||||
|
Input (argv flags):
|
||||||
|
--audit <path> (required) local audit.json file to upload
|
||||||
|
--owner <org> (optional, default continuous-intelligence)
|
||||||
|
--repo <name> (optional, default acdl-evidence)
|
||||||
|
--branch <name> (optional, default main)
|
||||||
|
--path <remote path> (optional, default audit.json) path in the repo
|
||||||
|
--token-env <env var> (optional, default ACDL_GITEA_TOKEN)
|
||||||
|
--host <url> (optional, default https://git.cloudinit.dev)
|
||||||
|
--message <commit msg> (optional, default chore(evidence): update audit.json)
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
1. Read the token from os.environ[token_env]. Missing -> stderr + exit 1.
|
||||||
|
2. Read the local audit file; base64-encode it.
|
||||||
|
3. GET the current file at .../contents/<path>?ref=<branch> to discover
|
||||||
|
the existing `sha`. 200 -> capture sha (update mode). 404 -> no sha
|
||||||
|
(create mode). Other errors -> exit 1.
|
||||||
|
4. If sha set: PUT with body {content, message, branch, sha}.
|
||||||
|
If no sha: POST with body {content, message, branch}.
|
||||||
|
5. Print {"uploaded": true, "path": "<path>", "sha": "<new sha>"} to
|
||||||
|
stdout and exit 0.
|
||||||
|
6. On any HTTP error: print
|
||||||
|
{"uploaded": false, "status": <code>, "body": "<body>"} to stdout
|
||||||
|
and exit 1.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
def _request(method: str, url: str, token: str, body: dict = None):
|
||||||
|
"""Perform an HTTP request with the Gitea auth header. Returns
|
||||||
|
(status_code, response_body_text). Raises URLError on network failure."""
|
||||||
|
data = None
|
||||||
|
headers = {"Authorization": f"token {token}",
|
||||||
|
"Accept": "application/json"}
|
||||||
|
if body is not None:
|
||||||
|
data = json.dumps(body).encode("utf-8")
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
return resp.getcode(), resp.read().decode("utf-8", "replace")
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
# HTTPError carries the response body
|
||||||
|
try:
|
||||||
|
body_text = exc.read().decode("utf-8", "replace")
|
||||||
|
except Exception:
|
||||||
|
body_text = ""
|
||||||
|
return exc.code, body_text
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
# Network-level failure (connection refused, DNS, timeout). Return
|
||||||
|
# a synthetic 0 status + the reason so callers can report cleanly
|
||||||
|
# without a stack trace.
|
||||||
|
return 0, f"URLError: {exc.reason}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_sha(host: str, owner: str, repo: str, path: str,
|
||||||
|
branch: str, token: str):
|
||||||
|
"""Return (sha-or-None, error_status_or_None). On 200 returns the sha.
|
||||||
|
On 404 returns (None, None). Other codes return (None, (status, body))."""
|
||||||
|
qs = urllib.parse.urlencode({"ref": branch})
|
||||||
|
url = f"{host}/api/v1/repos/{owner}/{repo}/contents/{path}?{qs}"
|
||||||
|
status, body = _request("GET", url, token)
|
||||||
|
if status == 200:
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
return data.get("sha"), None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None, (status, body)
|
||||||
|
if status == 404:
|
||||||
|
return None, None
|
||||||
|
return None, (status, body)
|
||||||
|
|
||||||
|
|
||||||
|
def upload(host: str, owner: str, repo: str, path: str, branch: str,
|
||||||
|
message: str, content_b64: str, sha, token: str):
|
||||||
|
"""PUT (update) or POST (create) the file. Returns (new_sha, None) on
|
||||||
|
success or (None, (status, body)) on HTTP error."""
|
||||||
|
url = f"{host}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
||||||
|
if sha:
|
||||||
|
body = {"content": content_b64, "message": message,
|
||||||
|
"branch": branch, "sha": sha}
|
||||||
|
status, resp = _request("PUT", url, token, body)
|
||||||
|
else:
|
||||||
|
body = {"content": content_b64, "message": message, "branch": branch}
|
||||||
|
status, resp = _request("POST", url, token, body)
|
||||||
|
if status in (200, 201):
|
||||||
|
try:
|
||||||
|
data = json.loads(resp)
|
||||||
|
# The file-contents API returns the new content object either at
|
||||||
|
# top-level `content` (POST create) or `content` (PUT update).
|
||||||
|
new_sha = None
|
||||||
|
if isinstance(data, dict):
|
||||||
|
content_obj = data.get("content") or data
|
||||||
|
if isinstance(content_obj, dict):
|
||||||
|
new_sha = content_obj.get("sha")
|
||||||
|
return new_sha, None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None, None
|
||||||
|
return None, (status, resp)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Upload a local audit.json to the acdl-evidence Gitea "
|
||||||
|
"repo via the file-contents API (D-028/D-029).")
|
||||||
|
parser.add_argument("--audit", required=True,
|
||||||
|
help="Local audit.json file to upload")
|
||||||
|
parser.add_argument("--owner", default="continuous-intelligence",
|
||||||
|
help="Gitea org (default: continuous-intelligence)")
|
||||||
|
parser.add_argument("--repo", default="acdl-evidence",
|
||||||
|
help="Gitea repo (default: acdl-evidence)")
|
||||||
|
parser.add_argument("--branch", default="main",
|
||||||
|
help="Target branch (default: main)")
|
||||||
|
parser.add_argument("--path", default="audit.json",
|
||||||
|
help="Remote path in the repo (default: audit.json)")
|
||||||
|
parser.add_argument("--token-env", default="ACDL_GITEA_TOKEN",
|
||||||
|
help="Env var name holding the Gitea token "
|
||||||
|
"(default: ACDL_GITEA_TOKEN)")
|
||||||
|
parser.add_argument("--host", default="https://git.cloudinit.dev",
|
||||||
|
help="Gitea host URL (default: https://git.cloudinit.dev)")
|
||||||
|
parser.add_argument("--message", default="chore(evidence): update audit.json",
|
||||||
|
help="Commit message (default: chore(evidence): "
|
||||||
|
"update audit.json)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
token = os.environ.get(args.token_env)
|
||||||
|
if not token:
|
||||||
|
print(f"finalize_evidence: required env var {args.token_env} is not "
|
||||||
|
f"set", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Read + base64-encode the local audit file. Missing/unreadable file is
|
||||||
|
# a clean exit 1 (no stack trace).
|
||||||
|
try:
|
||||||
|
with open(args.audit, "rb") as fh:
|
||||||
|
raw = fh.read()
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"finalize_evidence: cannot read {args.audit}: {exc}",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
content_b64 = base64.b64encode(raw).decode("ascii")
|
||||||
|
|
||||||
|
# Discover existing sha (update vs create).
|
||||||
|
sha, err = get_existing_sha(args.host, args.owner, args.repo,
|
||||||
|
args.path, args.branch, token)
|
||||||
|
if err is not None:
|
||||||
|
status, body = err
|
||||||
|
print(json.dumps({"uploaded": False, "status": status, "body": body}))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Upload (PUT if sha, POST otherwise).
|
||||||
|
new_sha, err = upload(args.host, args.owner, args.repo, args.path,
|
||||||
|
args.branch, args.message, content_b64, sha, token)
|
||||||
|
if err is not None:
|
||||||
|
status, body = err
|
||||||
|
print(json.dumps({"uploaded": False, "status": status, "body": body}))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(json.dumps({"uploaded": True, "path": args.path,
|
||||||
|
"sha": new_sha}))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""l3b_agent_stub.py — D-008 / D-026 / D-021
|
||||||
|
|
||||||
|
Parses a GitHub/Gitea Issue body by keywords and emits a contract.yaml that
|
||||||
|
selects an L2 stack. This is the agentic (L3B) entry surface: deterministic
|
||||||
|
keyword matching, no external AI APIs.
|
||||||
|
|
||||||
|
D-008 keyword map (priority order — first match wins):
|
||||||
|
gas, price, ingest, data-lake -> l2-commodity-price-feed
|
||||||
|
invoice, billing -> l2-invoice-service
|
||||||
|
analytics, historical, query -> l2-energy-analytics-api
|
||||||
|
regulatory, compliance, reporting, trading
|
||||||
|
-> l2-regulatory-reporting
|
||||||
|
(no match) -> l2-invoice-service (fallback)
|
||||||
|
|
||||||
|
Output contract.yaml (D-021 schema):
|
||||||
|
stack: <mapped L2 name>
|
||||||
|
inputs:
|
||||||
|
environment: dev
|
||||||
|
owner: citizen-developer
|
||||||
|
source: l3b-agent-stub
|
||||||
|
public-ingress: false
|
||||||
|
|
||||||
|
Input:
|
||||||
|
argv[1] = issue body text (or stdin if argv[1] absent/empty)
|
||||||
|
-o <path> = write the contract to a file (default: stdout)
|
||||||
|
Exit:
|
||||||
|
0 on success, 1 on empty input
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# Ordered keyword groups -> L2 stack mapping (D-008). First match wins.
|
||||||
|
KEYWORD_MAP = [
|
||||||
|
(("gas", "price", "ingest", "data-lake"), "l2-commodity-price-feed"),
|
||||||
|
(("invoice", "billing"), "l2-invoice-service"),
|
||||||
|
(("analytics", "historical", "query"), "l2-energy-analytics-api"),
|
||||||
|
(("regulatory", "compliance", "reporting", "trading"), "l2-regulatory-reporting"),
|
||||||
|
]
|
||||||
|
|
||||||
|
FALLBACK_STACK = "l2-invoice-service"
|
||||||
|
|
||||||
|
|
||||||
|
def map_issue_to_stack(text: str) -> str:
|
||||||
|
lowered = text.lower()
|
||||||
|
for keywords, stack in KEYWORD_MAP:
|
||||||
|
for kw in keywords:
|
||||||
|
if kw in lowered:
|
||||||
|
return stack
|
||||||
|
return FALLBACK_STACK
|
||||||
|
|
||||||
|
|
||||||
|
def render_contract(stack: str) -> str:
|
||||||
|
# Fixed-schema YAML (D-021). Emitted as text (no yaml dependency needed).
|
||||||
|
return (
|
||||||
|
f"stack: {stack}\n"
|
||||||
|
"inputs:\n"
|
||||||
|
" environment: dev\n"
|
||||||
|
" owner: citizen-developer\n"
|
||||||
|
" source: l3b-agent-stub\n"
|
||||||
|
"public-ingress: false\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_issue_body(args: list) -> str:
|
||||||
|
"""Read issue body from args[0] (already-stripped argv, no script name)
|
||||||
|
or stdin. Empty -> error."""
|
||||||
|
if len(args) >= 1 and args[0].strip():
|
||||||
|
return args[0]
|
||||||
|
# Fall back to stdin if argv body is absent or empty.
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
data = sys.stdin.read()
|
||||||
|
if data.strip():
|
||||||
|
return data
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_output_flag(argv: list):
|
||||||
|
"""Extract -o <path> from argv (returns (rest, output_path))."""
|
||||||
|
output_path = None
|
||||||
|
rest = []
|
||||||
|
i = 1
|
||||||
|
while i < len(argv):
|
||||||
|
arg = argv[i]
|
||||||
|
if arg == "-o":
|
||||||
|
if i + 1 < len(argv):
|
||||||
|
output_path = argv[i + 1]
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("l3b_agent_stub: -o requires a path argument", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
rest.append(arg)
|
||||||
|
i += 1
|
||||||
|
return rest, output_path
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
rest, output_path = parse_output_flag(sys.argv)
|
||||||
|
body = read_issue_body(rest)
|
||||||
|
if not body.strip():
|
||||||
|
print("l3b_agent_stub: empty issue body (no argv[1] and no stdin)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
stack = map_issue_to_stack(body)
|
||||||
|
contract = render_contract(stack)
|
||||||
|
|
||||||
|
if output_path:
|
||||||
|
with open(output_path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(contract)
|
||||||
|
else:
|
||||||
|
sys.stdout.write(contract)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# mock_executor.sh — REQ-06 / D-022
|
||||||
|
#
|
||||||
|
# Reads a contract.yaml, resolves the L2 composition, invokes each L1's
|
||||||
|
# mock_apply.sh in order, and writes state.json to the current working
|
||||||
|
# directory.
|
||||||
|
#
|
||||||
|
# Input: argv[1] = path to a contract.yaml file.
|
||||||
|
# Output:
|
||||||
|
# - stdout: per-L1 progress (echoed from each mock_apply.sh)
|
||||||
|
# - state.json in cwd: {"l2": "...", "l1s": [...], "contract": {...}}
|
||||||
|
# Exit:
|
||||||
|
# 0 if all L1s exit 0; 1 if any L1 exited non-zero (state.json is still
|
||||||
|
# written with the recorded exit codes).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "usage: mock_executor.sh <contract.yaml>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTRACT_PATH="$1"
|
||||||
|
|
||||||
|
if [[ ! -f "$CONTRACT_PATH" ]]; then
|
||||||
|
echo "contract not found: $CONTRACT_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Parse the contract (stack + full contract dict) via python3 + yaml. ---
|
||||||
|
# Emit stack on line 1 and the full contract JSON on line 2, then read both
|
||||||
|
# lines into separate bash variables (so the JSON's internal spaces survive).
|
||||||
|
CONTRACT_PARSED=$(python3 - "$CONTRACT_PATH" <<'PY'
|
||||||
|
import sys, json, yaml
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
contract = yaml.safe_load(fh)
|
||||||
|
if not isinstance(contract, dict):
|
||||||
|
sys.stderr.write("contract is not a mapping\n")
|
||||||
|
sys.exit(2)
|
||||||
|
stack = contract.get("stack", "")
|
||||||
|
# Use a compact JSON (no spaces) so the single-line contract survives bash
|
||||||
|
# variable capture cleanly.
|
||||||
|
print(stack)
|
||||||
|
print(json.dumps(contract, sort_keys=True, separators=(",", ":")))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
|
||||||
|
STACK=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '1p')
|
||||||
|
CONTRACT_JSON=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '2p')
|
||||||
|
|
||||||
|
if [[ -z "$STACK" ]]; then
|
||||||
|
echo "contract missing 'stack' key" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Resolve the L2 manifest. ---
|
||||||
|
L2_MANIFEST="modules/l2/${STACK}/manifest.yaml"
|
||||||
|
if [[ ! -f "$L2_MANIFEST" ]]; then
|
||||||
|
echo "L2_NOT_FOUND: ${STACK}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Read the L2's l1s: list (ordered names) via python. ---
|
||||||
|
L1_NAMES_JSON=$(python3 - "$L2_MANIFEST" <<'PY'
|
||||||
|
import sys, json, yaml
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
manifest = yaml.safe_load(fh)
|
||||||
|
l1s = manifest.get("l1s", []) if isinstance(manifest, dict) else []
|
||||||
|
names = [entry.get("name", "") for entry in l1s if isinstance(entry, dict)]
|
||||||
|
print(json.dumps(names))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Invoke each L1's mock_apply.sh in order, recording exit codes. ---
|
||||||
|
# Build the l1s results array in JSON via python, appending as we go.
|
||||||
|
RESULTS_JSON="[]"
|
||||||
|
|
||||||
|
ALL_OK=0
|
||||||
|
while IFS= read -r L1_NAME; do
|
||||||
|
L1_SCRIPT="modules/l1/${L1_NAME}/mock_apply.sh"
|
||||||
|
if [[ ! -f "$L1_SCRIPT" ]]; then
|
||||||
|
echo "L1_NOT_FOUND: ${L1_NAME}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Capture stdout + exit code. stderr passes through.
|
||||||
|
L1_OUT=$(bash "$L1_SCRIPT")
|
||||||
|
L1_RC=$?
|
||||||
|
|
||||||
|
# Echo the L1's stdout so the pipeline sees the progress lines.
|
||||||
|
printf '%s\n' "$L1_OUT"
|
||||||
|
|
||||||
|
# Record {"name": ..., "applied": true, "exit_code": ...}.
|
||||||
|
RESULTS_JSON=$(python3 - "$RESULTS_JSON" "$L1_NAME" "$L1_RC" <<'PY'
|
||||||
|
import sys, json
|
||||||
|
results = json.loads(sys.argv[1])
|
||||||
|
name = sys.argv[2]
|
||||||
|
rc = int(sys.argv[3])
|
||||||
|
results.append({"name": name, "applied": True, "exit_code": rc})
|
||||||
|
print(json.dumps(results))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ $L1_RC -ne 0 ]]; then
|
||||||
|
ALL_OK=1
|
||||||
|
fi
|
||||||
|
done < <(python3 -c "import sys, json; print('\n'.join(json.loads(sys.argv[1])))" "$L1_NAMES_JSON")
|
||||||
|
|
||||||
|
# --- Write state.json to the current working directory (D-022). ---
|
||||||
|
python3 - "$RESULTS_JSON" "$STACK" "$CONTRACT_JSON" <<'PY'
|
||||||
|
import sys, json
|
||||||
|
results = json.loads(sys.argv[1])
|
||||||
|
stack = sys.argv[2]
|
||||||
|
contract = json.loads(sys.argv[3])
|
||||||
|
state = {
|
||||||
|
"l2": stack,
|
||||||
|
"l1s": results,
|
||||||
|
"contract": contract,
|
||||||
|
}
|
||||||
|
with open("state.json", "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(state, fh, indent=2)
|
||||||
|
fh.write("\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
exit "$ALL_OK"
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""policy_checker.py — REQ-07 / D-025
|
||||||
|
|
||||||
|
Reads a contract.yaml and enforces the single Phase-03 policy rule:
|
||||||
|
`public-ingress: true` is forbidden.
|
||||||
|
|
||||||
|
Input: argv[1] = path to a contract.yaml file.
|
||||||
|
Output: stdout "POLICY_PASS" or "POLICY_VIOLATION:PUBLIC_INGRESS"
|
||||||
|
Exit: 0 on pass, 1 on violation.
|
||||||
|
|
||||||
|
Idempotent, no side effects (no file writes). Treats an absent or falsy
|
||||||
|
`public-ingress` key as a pass.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("usage: policy_checker.py <contract.yaml>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
contract_path = sys.argv[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(contract_path, "r", encoding="utf-8") as fh:
|
||||||
|
contract = yaml.safe_load(fh)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"contract not found: {contract_path}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
print(f"invalid yaml: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# Treat missing/non-mapping as no policy violation.
|
||||||
|
if not isinstance(contract, dict):
|
||||||
|
print("POLICY_PASS")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
public_ingress = contract.get("public-ingress", False)
|
||||||
|
|
||||||
|
if public_ingress is True:
|
||||||
|
print("POLICY_VIOLATION:PUBLIC_INGRESS")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("POLICY_PASS")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+258
@@ -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
|
||||||
Executable
+135
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 02 verification script.
|
||||||
|
# Confirms the 8 L1 module folders exist under modules/l1/ with the exact
|
||||||
|
# names from REQ-02, each containing a valid manifest.yaml (D-017 schema)
|
||||||
|
# and a uniform mock_apply.sh (D-007 + D-018) that exits 0 with the
|
||||||
|
# expected echo markers.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify_phase02.sh
|
||||||
|
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
L1_DIR="${ROOT}/modules/l1"
|
||||||
|
|
||||||
|
# Expected L1 names per REQ-02 / D-019.
|
||||||
|
EXPECTED_L1S=(
|
||||||
|
l1-eks-fargate
|
||||||
|
l1-iam-role
|
||||||
|
l1-lambda
|
||||||
|
l1-api-gateway
|
||||||
|
l1-eventbridge
|
||||||
|
l1-sqs
|
||||||
|
l1-s3
|
||||||
|
l1-cloudwatch
|
||||||
|
)
|
||||||
|
|
||||||
|
fail_count=0
|
||||||
|
pass() { printf ' [PASS] %s\n' "$1"; }
|
||||||
|
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
|
||||||
|
|
||||||
|
echo "== Phase 02 verification =="
|
||||||
|
echo "L1 dir: ${L1_DIR}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Check 1: exactly 8 L1 folders with the expected names ---
|
||||||
|
echo "-- Check 1: 8 L1 folders with expected names --"
|
||||||
|
if [ ! -d "$L1_DIR" ]; then
|
||||||
|
fail "modules/l1/ does not exist"
|
||||||
|
echo
|
||||||
|
echo "== Summary =="
|
||||||
|
echo "Phase 02 verification FAILED (${fail_count} check(s) failed)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual_folders=$(ls "$L1_DIR" | sort | tr '\n' ' ')
|
||||||
|
expected_folders=$(printf '%s\n' "${EXPECTED_L1S[@]}" | sort | tr '\n' ' ')
|
||||||
|
if [ "$actual_folders" = "$expected_folders" ]; then
|
||||||
|
pass "exactly 8 L1 folders present and named correctly"
|
||||||
|
else
|
||||||
|
fail "L1 folder list mismatch"
|
||||||
|
echo " expected: $expected_folders"
|
||||||
|
echo " actual: $actual_folders"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Per-L1 checks ---
|
||||||
|
for l1 in "${EXPECTED_L1S[@]}"; do
|
||||||
|
echo "-- L1: ${l1} --"
|
||||||
|
dir="${L1_DIR}/${l1}"
|
||||||
|
|
||||||
|
# Check 2a: folder exists
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
fail "${l1}: folder missing"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
pass "${l1}: folder exists"
|
||||||
|
|
||||||
|
# Check 2b: manifest.yaml exists + parses + name matches folder + kind=l1
|
||||||
|
manifest="${dir}/manifest.yaml"
|
||||||
|
if [ ! -f "$manifest" ]; then
|
||||||
|
fail "${l1}: manifest.yaml missing"
|
||||||
|
else
|
||||||
|
manifest_ok=$(python3 -c "
|
||||||
|
import yaml, sys
|
||||||
|
try:
|
||||||
|
d = yaml.safe_load(open('${manifest}'))
|
||||||
|
name = d.get('name') == '${l1}'
|
||||||
|
kind = d.get('kind') == 'l1'
|
||||||
|
has_inputs = isinstance(d.get('inputs'), dict)
|
||||||
|
sys.exit(0 if (name and kind and has_inputs) else 1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f' parse error: {e}', file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
" 2>/dev/null; echo $?)
|
||||||
|
if [ "$manifest_ok" = "0" ]; then
|
||||||
|
pass "${l1}: manifest.yaml valid (name=${l1}, kind=l1, inputs present)"
|
||||||
|
else
|
||||||
|
fail "${l1}: manifest.yaml invalid (name/kind/inputs check failed; rc=${manifest_ok})"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2c: mock_apply.sh exists + executable + bash -n clean
|
||||||
|
apply="${dir}/mock_apply.sh"
|
||||||
|
if [ ! -f "$apply" ]; then
|
||||||
|
fail "${l1}: mock_apply.sh missing"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ ! -x "$apply" ]; then
|
||||||
|
fail "${l1}: mock_apply.sh not executable"
|
||||||
|
else
|
||||||
|
pass "${l1}: mock_apply.sh is executable"
|
||||||
|
fi
|
||||||
|
if ! bash -n "$apply" 2>/dev/null; then
|
||||||
|
fail "${l1}: mock_apply.sh bash -n failed"
|
||||||
|
else
|
||||||
|
pass "${l1}: mock_apply.sh bash -n clean"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2d: end-to-end run: exit 0 + expected markers, completes in <2s
|
||||||
|
start=$(date +%s)
|
||||||
|
output=$("$apply" 2>&1)
|
||||||
|
rc=$?
|
||||||
|
elapsed=$(( $(date +%s) - start ))
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
fail "${l1}: mock_apply.sh exited ${rc}"
|
||||||
|
elif ! echo "$output" | grep -qF "[L1: ${l1}] applying..."; then
|
||||||
|
fail "${l1}: missing '[L1: ${l1}] applying...' marker"
|
||||||
|
elif ! echo "$output" | grep -qF "[L1: ${l1}] OK"; then
|
||||||
|
fail "${l1}: missing '[L1: ${l1}] OK' marker"
|
||||||
|
elif [ "$elapsed" -lt 1 ] || [ "$elapsed" -gt 2 ]; then
|
||||||
|
fail "${l1}: run took ${elapsed}s (expected ~1s; 1<=t<=2 ok)"
|
||||||
|
else
|
||||||
|
pass "${l1}: mock_apply.sh runs, exits 0, markers correct (${elapsed}s)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Summary =="
|
||||||
|
if [ "$fail_count" -eq 0 ]; then
|
||||||
|
echo "Phase 02 verification PASSED (8 L1 modules, all checks ok)"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Phase 02 verification FAILED (${fail_count} check(s) failed)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+240
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 03 verification script.
|
||||||
|
# Confirms the 4 L2 modules and the 5 core scripts conform to their contracts.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify_phase03.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)); }
|
||||||
|
|
||||||
|
# Expected L2 names per REQ-04.
|
||||||
|
EXPECTED_L2S=(
|
||||||
|
l2-invoice-service
|
||||||
|
l2-commodity-price-feed
|
||||||
|
l2-energy-analytics-api
|
||||||
|
l2-regulatory-reporting
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "== Phase 03 verification =="
|
||||||
|
echo "Root: ${ROOT}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Check 1: exactly 4 L2 folders with the expected names ---
|
||||||
|
echo "-- Check 1: 4 L2 folders with expected names --"
|
||||||
|
actual=$(ls modules/l2/ 2>/dev/null | sort | tr '\n' ' ')
|
||||||
|
expected=$(printf '%s\n' "${EXPECTED_L2S[@]}" | sort | tr '\n' ' ')
|
||||||
|
if [ "$actual" = "$expected" ]; then
|
||||||
|
pass "exactly 4 L2 folders present and named correctly"
|
||||||
|
else
|
||||||
|
fail "L2 folder list mismatch"
|
||||||
|
echo " expected: $expected"
|
||||||
|
echo " actual: $actual"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 2: each L2 manifest.yaml validates + references 5 existing L1s ---
|
||||||
|
echo "-- Check 2: L2 manifests reference 5 existing L1s --"
|
||||||
|
l2_validate=$(python3 << 'PYEOF' || true
|
||||||
|
import yaml, glob, os, sys
|
||||||
|
ok = True
|
||||||
|
l1s = set(os.listdir('modules/l1'))
|
||||||
|
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
||||||
|
d = yaml.safe_load(open(f))
|
||||||
|
folder = os.path.basename(os.path.dirname(f))
|
||||||
|
problems = []
|
||||||
|
if d.get('name') != folder: problems.append(f"name != {folder}")
|
||||||
|
if d.get('kind') != 'l2': problems.append("kind != l2")
|
||||||
|
refs = [x.get('name') for x in d.get('l1s', [])]
|
||||||
|
if len(refs) != 5: problems.append(f"expected 5 l1s, got {len(refs)}")
|
||||||
|
unknown = [r for r in refs if r not in l1s]
|
||||||
|
if unknown: problems.append(f"unknown L1 refs: {unknown}")
|
||||||
|
# each l1 entry must have an inputs: map
|
||||||
|
for x in d.get('l1s', []):
|
||||||
|
if not isinstance(x.get('inputs'), dict): problems.append(f"l1 {x.get('name')} missing inputs map")
|
||||||
|
status = 'OK' if not problems else 'FAIL: ' + '; '.join(problems)
|
||||||
|
print(f' [{status}] {f}')
|
||||||
|
if problems: ok = False
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
echo "$l2_validate"
|
||||||
|
if [ "$l2_validate" = "" ] || echo "$l2_validate" | grep -q FAIL; then
|
||||||
|
if ! echo "$l2_validate" | grep -q PASS; then
|
||||||
|
fail "one or more L2 manifests invalid (see above)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
pass "all 4 L2 manifests valid"
|
||||||
|
fi
|
||||||
|
# Re-run for the explicit pass/fail count
|
||||||
|
python3 << 'PYEOF' > /tmp/l2_check.txt 2>&1 || true
|
||||||
|
import yaml, glob, os, sys
|
||||||
|
ok = True
|
||||||
|
l1s = set(os.listdir('modules/l1'))
|
||||||
|
for f in sorted(glob.glob('modules/l2/*/manifest.yaml')):
|
||||||
|
d = yaml.safe_load(open(f))
|
||||||
|
folder = os.path.basename(os.path.dirname(f))
|
||||||
|
if d.get('name') != folder: ok = False
|
||||||
|
if d.get('kind') != 'l2': ok = False
|
||||||
|
refs = [x.get('name') for x in d.get('l1s', [])]
|
||||||
|
if len(refs) != 5: ok = False
|
||||||
|
if any(r not in l1s for r in refs): ok = False
|
||||||
|
for x in d.get('l1s', []):
|
||||||
|
if not isinstance(x.get('inputs'), dict): ok = False
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
PYEOF
|
||||||
|
if [ $? -eq 0 ]; then pass "all 4 L2 manifests pass structural + reference checks"; else fail "L2 manifest structural check"; fi
|
||||||
|
|
||||||
|
# --- Check 3: typecheck (bash -n + py_compile + yaml load) ---
|
||||||
|
echo "-- Check 3: typecheck --"
|
||||||
|
if bash -n scripts/mock_executor.sh; then pass "bash -n mock_executor.sh"; else fail "bash -n mock_executor.sh"; fi
|
||||||
|
if python3 -m py_compile scripts/policy_checker.py scripts/confidence_signal.py scripts/evidence_writer.py scripts/l3b_agent_stub.py 2>/dev/null; then
|
||||||
|
pass "py_compile all 4 python scripts"
|
||||||
|
else
|
||||||
|
fail "py_compile"
|
||||||
|
fi
|
||||||
|
if python3 -c "import yaml, glob; [yaml.safe_load(open(f)) for f in glob.glob('modules/l2/*/manifest.yaml')]" 2>/dev/null; then
|
||||||
|
pass "yaml load all L2 manifests"
|
||||||
|
else
|
||||||
|
fail "yaml load L2 manifests"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 4: policy_checker (D-025) ---
|
||||||
|
echo "-- Check 4: policy_checker behavior (D-025) --"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK" "$ROOT/tmp_pass_contract.yaml" "$ROOT/tmp_fail_contract.yaml" "$ROOT/state.json" 2>/dev/null || true' EXIT
|
||||||
|
printf 'stack: l2-commodity-price-feed\npublic-ingress: false\n' > "$WORK/pass.yaml"
|
||||||
|
printf 'stack: l2-regulatory-reporting\npublic-ingress: true\n' > "$WORK/fail.yaml"
|
||||||
|
out=$(python3 scripts/policy_checker.py "$WORK/pass.yaml" 2>&1); rc=$?
|
||||||
|
if [ "$out" = "POLICY_PASS" ] && [ "$rc" = "0" ]; then
|
||||||
|
pass "policy_checker pass contract -> POLICY_PASS exit 0"
|
||||||
|
else
|
||||||
|
fail "policy_checker pass contract: got '$out' exit=$rc"
|
||||||
|
fi
|
||||||
|
out=$(python3 scripts/policy_checker.py "$WORK/fail.yaml" 2>&1); rc=$?
|
||||||
|
if [ "$out" = "POLICY_VIOLATION:PUBLIC_INGRESS" ] && [ "$rc" = "1" ]; then
|
||||||
|
pass "policy_checker fail contract -> POLICY_VIOLATION:PUBLIC_INGRESS exit 1"
|
||||||
|
else
|
||||||
|
fail "policy_checker fail contract: got '$out' exit=$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 5: confidence_signal (D-024) ---
|
||||||
|
echo "-- Check 5: confidence_signal behavior (D-024) --"
|
||||||
|
out=$(python3 scripts/confidence_signal.py "$WORK/pass.yaml" 2>&1); rc=$?
|
||||||
|
if echo "$out" | grep -q '"score": 0.90' && [ "$rc" = "0" ]; then
|
||||||
|
pass "confidence_signal pass -> score 0.90 exit 0"
|
||||||
|
else
|
||||||
|
fail "confidence_signal pass: got '$out' exit=$rc"
|
||||||
|
fi
|
||||||
|
out=$(python3 scripts/confidence_signal.py "$WORK/fail.yaml" 2>&1); rc=$?
|
||||||
|
if echo "$out" | grep -q '"score": 0.40' && [ "$rc" = "0" ]; then
|
||||||
|
pass "confidence_signal fail -> score 0.40 exit 0"
|
||||||
|
else
|
||||||
|
fail "confidence_signal fail: got '$out' exit=$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 6: evidence_writer hash chain (D-023) ---
|
||||||
|
echo "-- Check 6: evidence_writer hash chain (D-023) --"
|
||||||
|
rm -f "$WORK/audit.json"
|
||||||
|
python3 scripts/evidence_writer.py --stage dev --event "dev start" --audit "$WORK/audit.json" > /dev/null
|
||||||
|
python3 scripts/evidence_writer.py --stage qa --event "qa approved" --audit "$WORK/audit.json" > /dev/null
|
||||||
|
python3 scripts/evidence_writer.py --stage prod --event "prod approved" --audit "$WORK/audit.json" > /dev/null
|
||||||
|
chain_ok=$(python3 << PYEOF
|
||||||
|
import json, hashlib, sys
|
||||||
|
try:
|
||||||
|
events = json.load(open("$WORK/audit.json"))
|
||||||
|
assert len(events) == 4, f"expected 4 (genesis + 3), got {len(events)}"
|
||||||
|
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_ok" = "OK" ]; then
|
||||||
|
pass "evidence_writer: 4 events, GENESIS + 3, chain links + hashes valid"
|
||||||
|
else
|
||||||
|
fail "evidence_writer chain: $chain_ok"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 7: mock_executor (D-022) ---
|
||||||
|
echo "-- Check 7: mock_executor writes state.json (D-022) --"
|
||||||
|
rm -f "$ROOT/state.json"
|
||||||
|
out=$(bash scripts/mock_executor.sh "$WORK/pass.yaml" 2>&1); rc=$?
|
||||||
|
if [ "$rc" != "0" ]; then
|
||||||
|
fail "mock_executor exit $rc (expected 0)"
|
||||||
|
else
|
||||||
|
me_ok=$(python3 << PYEOF
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
s = json.load(open("$ROOT/state.json"))
|
||||||
|
assert s['l2'] == 'l2-commodity-price-feed', f"l2 mismatch: {s.get('l2')}"
|
||||||
|
assert 'l1s' in s and len(s['l1s']) == 5, f"expected 5 l1s, got {len(s.get('l1s', []))}"
|
||||||
|
assert all(x['applied'] is True and x['exit_code'] == 0 for x in s['l1s']), "l1 not all applied+0"
|
||||||
|
assert 'contract' in s, "missing contract field"
|
||||||
|
print("OK")
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"FAIL: {ex}")
|
||||||
|
sys.exit(1)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
if [ "$me_ok" = "OK" ]; then
|
||||||
|
pass "mock_executor: state.json with l2 + 5 l1s (all exit 0) + contract"
|
||||||
|
else
|
||||||
|
fail "mock_executor state.json: $me_ok"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$ROOT/state.json"
|
||||||
|
|
||||||
|
# --- Check 8: l3b_agent_stub D-008 keyword map ---
|
||||||
|
echo "-- Check 8: l3b_agent_stub keyword map (D-008) --"
|
||||||
|
act3=$(python3 scripts/l3b_agent_stub.py "We need to ingest natural gas prices from Platts and report on compliance." 2>&1)
|
||||||
|
if echo "$act3" | grep -q 'stack: l2-commodity-price-feed'; then
|
||||||
|
pass "l3b Act 3 example -> l2-commodity-price-feed"
|
||||||
|
else
|
||||||
|
fail "l3b Act 3 example: got '$act3'"
|
||||||
|
fi
|
||||||
|
fallback=$(python3 scripts/l3b_agent_stub.py "please deploy something" 2>&1)
|
||||||
|
if echo "$fallback" | grep -q 'stack: l2-invoice-service'; then
|
||||||
|
pass "l3b fallback (no keywords) -> l2-invoice-service"
|
||||||
|
else
|
||||||
|
fail "l3b fallback: got '$fallback'"
|
||||||
|
fi
|
||||||
|
regulatory=$(python3 scripts/l3b_agent_stub.py "regulatory compliance reporting for trading desk" 2>&1)
|
||||||
|
if echo "$regulatory" | grep -q 'stack: l2-regulatory-reporting'; then
|
||||||
|
pass "l3b regulatory keywords -> l2-regulatory-reporting"
|
||||||
|
else
|
||||||
|
fail "l3b regulatory: got '$regulatory'"
|
||||||
|
fi
|
||||||
|
invoice=$(python3 scripts/l3b_agent_stub.py "monthly invoice and billing reconciliation" 2>&1)
|
||||||
|
if echo "$invoice" | grep -q 'stack: l2-invoice-service'; then
|
||||||
|
pass "l3b invoice keywords -> l2-invoice-service"
|
||||||
|
else
|
||||||
|
fail "l3b invoice: got '$invoice'"
|
||||||
|
fi
|
||||||
|
analytics=$(python3 scripts/l3b_agent_stub.py "historical analytics and query API" 2>&1)
|
||||||
|
if echo "$analytics" | grep -q 'stack: l2-energy-analytics-api'; then
|
||||||
|
pass "l3b analytics keywords -> l2-energy-analytics-api"
|
||||||
|
else
|
||||||
|
fail "l3b analytics: got '$analytics'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Summary =="
|
||||||
|
if [ "$fail_count" -eq 0 ]; then
|
||||||
|
echo "Phase 03 verification PASSED (4 L2s + 5 core scripts, all checks ok)"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Phase 03 verification FAILED (${fail_count} check(s) failed)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+186
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 04 verification script.
|
||||||
|
# Confirms the pipeline workflow + issue trigger + finalize_evidence.py
|
||||||
|
# conform to the Phase 04 plan and the Gitea Actions topology in
|
||||||
|
# ARCHITECTURE.md. Does NOT execute a real Gitea Actions run (act_runner
|
||||||
|
# is not registered in this environment); validates structure + syntax
|
||||||
|
# + a dry-run of finalize_evidence.py against a dead host.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify_phase04.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)); }
|
||||||
|
|
||||||
|
echo "== Phase 04 verification =="
|
||||||
|
echo "Root: ${ROOT}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Check 1: typecheck ---
|
||||||
|
echo "-- Check 1: typecheck --"
|
||||||
|
if bash -n scripts/finalize_evidence.py 2>/dev/null || python3 -m py_compile scripts/finalize_evidence.py 2>/dev/null; then
|
||||||
|
pass "py_compile finalize_evidence.py"
|
||||||
|
else
|
||||||
|
fail "py_compile finalize_evidence.py"
|
||||||
|
fi
|
||||||
|
if python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/pipeline.yml')); yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))" 2>/dev/null; then
|
||||||
|
pass "yaml load both workflows"
|
||||||
|
else
|
||||||
|
fail "yaml load workflows"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 2: pipeline.yml structure ---
|
||||||
|
echo "-- Check 2: pipeline.yml structure (D-027, D-028) --"
|
||||||
|
p_struct=$(python3 << 'PYEOF'
|
||||||
|
import yaml, sys
|
||||||
|
try:
|
||||||
|
d = yaml.safe_load(open('.gitea/workflows/pipeline.yml'))
|
||||||
|
on = d.get('on', d.get(True)) or {}
|
||||||
|
assert 'workflow_dispatch' in on, 'no workflow_dispatch trigger'
|
||||||
|
inputs = on['workflow_dispatch']['inputs']
|
||||||
|
assert set(inputs.keys()) == {'contract-ref', 'approve_qa', 'approve_prod'}, f'inputs: {set(inputs.keys())}'
|
||||||
|
assert inputs['contract-ref']['type'] == 'string', 'contract-ref type'
|
||||||
|
assert inputs['approve_qa']['type'] == 'boolean', 'approve_qa type'
|
||||||
|
assert inputs['approve_prod']['type'] == 'boolean', 'approve_prod type'
|
||||||
|
jobs = d['jobs']
|
||||||
|
assert set(jobs.keys()) == {'dev', 'qa-gate', 'prod-gate', 'finalize'}, f'jobs: {set(jobs.keys())}'
|
||||||
|
dev_if = jobs['dev'].get('if', '')
|
||||||
|
assert 'approve_qa' in dev_if and 'approve_prod' in dev_if, f'dev.if: {dev_if}'
|
||||||
|
qa_if = jobs['qa-gate'].get('if', '')
|
||||||
|
assert 'approve_qa' in qa_if, f'qa-gate.if: {qa_if}'
|
||||||
|
prod_if = jobs['prod-gate'].get('if', '')
|
||||||
|
assert 'approve_prod' in prod_if, f'prod-gate.if: {prod_if}'
|
||||||
|
fin_needs = jobs['finalize'].get('needs', [])
|
||||||
|
assert fin_needs == ['prod-gate'] or fin_needs == 'prod-gate', f'finalize.needs: {fin_needs}'
|
||||||
|
# All jobs runs-on ubuntu-latest
|
||||||
|
for name, job in jobs.items():
|
||||||
|
assert job.get('runs-on') == 'ubuntu-latest', f'{name} runs-on: {job.get("runs-on")}'
|
||||||
|
print('OK')
|
||||||
|
except AssertionError as ex:
|
||||||
|
print(f'FAIL: {ex}')
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as ex:
|
||||||
|
print(f'FAIL: {ex}')
|
||||||
|
sys.exit(1)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
if [ "$p_struct" = "OK" ]; then
|
||||||
|
pass "pipeline.yml: 3 inputs + 4 jobs + correct if: conditions + finalize.needs=prod-gate"
|
||||||
|
else
|
||||||
|
fail "pipeline.yml structure: $p_struct"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 3: pipeline.yml references core scripts ---
|
||||||
|
echo "-- Check 3: pipeline.yml references core scripts (D-029) --"
|
||||||
|
text=$(cat .gitea/workflows/pipeline.yml)
|
||||||
|
missing=""
|
||||||
|
for ref in policy_checker.py confidence_signal.py mock_executor.sh evidence_writer.py finalize_evidence.py; do
|
||||||
|
if ! echo "$text" | grep -qF "$ref"; then
|
||||||
|
missing="$missing $ref"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$missing" ]; then
|
||||||
|
pass "pipeline.yml references all 5 core scripts"
|
||||||
|
else
|
||||||
|
fail "pipeline.yml missing references:$missing"
|
||||||
|
fi
|
||||||
|
# Branch-pin documentation
|
||||||
|
if echo "$text" | grep -q 'milestone/v1.0-initial'; then
|
||||||
|
pass "pipeline.yml documents branch-pin to milestone/v1.0-initial"
|
||||||
|
else
|
||||||
|
fail "pipeline.yml missing branch-pin reference"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 4: issue-to-contract.yml structure ---
|
||||||
|
echo "-- Check 4: issue-to-contract.yml structure (D-030) --"
|
||||||
|
i_struct=$(python3 << 'PYEOF'
|
||||||
|
import yaml, sys
|
||||||
|
try:
|
||||||
|
d = yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))
|
||||||
|
on = d.get('on', d.get(True)) or {}
|
||||||
|
assert 'issues' in on, 'no issues trigger'
|
||||||
|
assert on['issues']['types'] == ['opened'], f'types: {on["issues"]["types"]}'
|
||||||
|
assert 'parse-and-trigger' in d['jobs'], 'no parse-and-trigger job'
|
||||||
|
assert d['jobs']['parse-and-trigger'].get('runs-on') == 'ubuntu-latest', 'runs-on'
|
||||||
|
print('OK')
|
||||||
|
except AssertionError as ex:
|
||||||
|
print(f'FAIL: {ex}')
|
||||||
|
sys.exit(1)
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
if [ "$i_struct" = "OK" ]; then
|
||||||
|
pass "issue-to-contract.yml: issues[opened] + parse-and-trigger job"
|
||||||
|
else
|
||||||
|
fail "issue-to-contract.yml structure: $i_struct"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 5: issue-to-contract.yml references + dispatch endpoint ---
|
||||||
|
echo "-- Check 5: issue-to-contract.yml references + dispatch (D-014, D-030) --"
|
||||||
|
text=$(cat contracts-repo/.gitea/workflows/issue-to-contract.yml)
|
||||||
|
missing=""
|
||||||
|
for ref in l3b_agent_stub.py 'actions/workflows/pipeline.yml/dispatches' 'contract-ref' 'gitea.event.issue.number' 'GITEA_TOKEN' 'new_branch'; do
|
||||||
|
if ! echo "$text" | grep -qF "$ref"; then
|
||||||
|
missing="$missing $ref"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$missing" ]; then
|
||||||
|
pass "issue-to-contract.yml: l3b_agent_stub + dispatch + contract-ref + issue number + token + new_branch"
|
||||||
|
else
|
||||||
|
fail "issue-to-contract.yml missing references:$missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 6: finalize_evidence.py --help + clean failure ---
|
||||||
|
echo "-- Check 6: finalize_evidence.py CLI + clean failure modes ---"
|
||||||
|
out=$(python3 scripts/finalize_evidence.py --help 2>&1); rc=$?
|
||||||
|
if [ "$rc" = "0" ] && echo "$out" | grep -qi 'usage\|--audit\|--owner'; then
|
||||||
|
pass "finalize_evidence.py --help exits 0 with usage"
|
||||||
|
else
|
||||||
|
fail "finalize_evidence.py --help: rc=$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Missing audit file (with a fake token so it gets past the env check) → exit 1, no stack trace
|
||||||
|
out=$(ACDL_GITEA_TOKEN=fake python3 scripts/finalize_evidence.py --audit /tmp/definitely_nonexistent_audit.json 2>&1); rc=$?
|
||||||
|
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
||||||
|
pass "finalize_evidence.py missing file → exit 1, no stack trace"
|
||||||
|
else
|
||||||
|
fail "finalize_evidence.py missing file: rc=$rc, out='$out'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Missing token env (audit file present) → exit 1, no stack trace
|
||||||
|
printf '[]\n' > /tmp/empty_audit.json
|
||||||
|
out=$(env -u ACDL_GITEA_TOKEN python3 scripts/finalize_evidence.py --audit /tmp/empty_audit.json 2>&1); rc=$?
|
||||||
|
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
||||||
|
pass "finalize_evidence.py missing token env → exit 1, no stack trace"
|
||||||
|
else
|
||||||
|
fail "finalize_evidence.py missing token: rc=$rc, out='$out'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check 7: finalize_evidence.py dry-run against a dead host (clean failure) ---
|
||||||
|
echo "-- Check 7: finalize_evidence.py dry-run against dead host ---"
|
||||||
|
# Use a real audit.json but point at a host that will refuse the connection.
|
||||||
|
printf '[{"seq":0,"ts":"2026-07-21T00:00:00Z","stage":"genesis","event":"init","prev_hash":"GENESIS","hash":"x"}]\n' > /tmp/real_audit.json
|
||||||
|
out=$(ACDL_GITEA_TOKEN=fake GITEA_HOST=http://127.0.0.1:0 python3 scripts/finalize_evidence.py --audit /tmp/real_audit.json --host http://127.0.0.1:0 2>&1); rc=$?
|
||||||
|
if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then
|
||||||
|
pass "finalize_evidence.py dead host → exit 1, no stack trace (clean API failure)"
|
||||||
|
else
|
||||||
|
fail "finalize_evidence.py dead host: rc=$rc, out='$out'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f /tmp/empty_audit.json /tmp/real_audit.json
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Summary =="
|
||||||
|
if [ "$fail_count" -eq 0 ]; then
|
||||||
|
echo "Phase 04 verification PASSED (pipeline + issue trigger + finalize helper, all checks ok)"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Phase 04 verification FAILED (${fail_count} check(s) failed)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+213
@@ -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
|
||||||
Reference in New Issue
Block a user