docs(lanes): document proof gallery (C)
This commit is contained in:
@@ -387,6 +387,7 @@ graph TD
|
|||||||
| `lib/lane-env.js` | (A2) `seedEnv` copies a repo's real `.env` into a lane on first boot (or `--force`) and rewrites the declared `ENV_REWRITE` keys (`DATABASE_URL`/`REDIS_URL`/`UPLOAD_DIR`) in place, byte-identical otherwise. A `--force` refresh preserves `ENV_PRESERVE` keys (e.g. `JWT_SECRET`) from the lane's own existing file — swapping in the source's secret would 401 a running lane until reboot. Falls back to `.env.example` with a warning when the source is missing. Refuses on an adopted lane: that file is the user's real config |
|
| `lib/lane-env.js` | (A2) `seedEnv` copies a repo's real `.env` into a lane on first boot (or `--force`) and rewrites the declared `ENV_REWRITE` keys (`DATABASE_URL`/`REDIS_URL`/`UPLOAD_DIR`) in place, byte-identical otherwise. A `--force` refresh preserves `ENV_PRESERVE` keys (e.g. `JWT_SECRET`) from the lane's own existing file — swapping in the source's secret would 401 a running lane until reboot. Falls back to `.env.example` with a warning when the source is missing. Refuses on an adopted lane: that file is the user's real config |
|
||||||
| `lib/lane-services.js` | (A2) `ensureDatabase`/`dropDatabase` call the profile's `db-create`/`db-drop` hooks — CCAM stays stack-agnostic on purpose. A state-dir marker file tracks whether a slot's database was already created, since a plain `createdb` can't be re-run safely and CCAM can't assume the hook is idempotent; this is also how `upLane` knows to seed only a freshly-created database. `dropDatabase` asserts the lane is `managed` and that the name being dropped is one this lane's own slot actually derives (itself or its `_test` sibling) before spawning anything |
|
| `lib/lane-services.js` | (A2) `ensureDatabase`/`dropDatabase` call the profile's `db-create`/`db-drop` hooks — CCAM stays stack-agnostic on purpose. A state-dir marker file tracks whether a slot's database was already created, since a plain `createdb` can't be re-run safely and CCAM can't assume the hook is idempotent; this is also how `upLane` knows to seed only a freshly-created database. `dropDatabase` asserts the lane is `managed` and that the name being dropped is one this lane's own slot actually derives (itself or its `_test` sibling) before spawning anything |
|
||||||
| `lib/lane-features.js` | (B) Per-feature state and archive. `activateFeature` archives the lane's current active feature (if different) and restores the target's saved stage onto the live `lanes` row — the row stays the one live view every other reader already uses. `canonicalizeSlug` is a DELIBERATELY separate rule from `worktree.js:slugify` (drops a leading `feat/`, keeps `[A-Za-z0-9._-]`, does not lowercase) — the two must never be conflated. `clearLane` (`lib/lanes.js`) archives the active feature (if any) before resetting; a lane that never activated one is unaffected |
|
| `lib/lane-features.js` | (B) Per-feature state and archive. `activateFeature` archives the lane's current active feature (if different) and restores the target's saved stage onto the live `lanes` row — the row stays the one live view every other reader already uses. `canonicalizeSlug` is a DELIBERATELY separate rule from `worktree.js:slugify` (drops a leading `feat/`, keeps `[A-Za-z0-9._-]`, does not lowercase) — the two must never be conflated. `clearLane` (`lib/lanes.js`) archives the active feature (if any) before resetting; a lane that never activated one is unaffected |
|
||||||
|
| `lib/proof.js` | (C) Proof gallery: lists/serves/deletes QC screenshots grouped by feature slug (Task B's `lane_features`) and phase, under `<lane.cwd>/.playwright-mcp/proof/<slug>/<group>/`. Every path is resolved, realpath'd, and containment-checked before touching `fs` — the entire security surface. `ensureProofLink` ports Shipyard's `ensure_proof_link` (converge a stray clone-root `proof/` onto the canonical dir); never auto-invoked, exposed only as `ccam lanes proof-link` |
|
||||||
| `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `ccam lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive |
|
| `lib/named-lock.js` | (D) Cross-lane named locks — the OTHER axis from `lib/lane-lock.js`'s per-lane, in-process serialization, deliberately a separate module. `mkdir` is the atomicity primitive (EEXIST decides "already held" in one syscall, never check-then-create). `LOCK_MAX_HOLD` (default 2700s) breaks a stale holder on the next acquire, floored at 300s so the floor — not the configurable default — is the actual safety property: nothing can force-break a live holder by setting the env var low. Single-shot only; the CLI's `ccam lock acquire` owns the polling loop, keeping the server side non-orchestrating like every other lane primitive |
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|||||||
+81
@@ -467,6 +467,87 @@ Response: `{ "lane": <same shape GET /api/lanes/:id returns>, "feature": <same s
|
|||||||
- **404** `{ "error": { "code": "ENOLANE", "message": "lane not found" } }`.
|
- **404** `{ "error": { "code": "ENOLANE", "message": "lane not found" } }`.
|
||||||
|
|
||||||
|
|
||||||
|
#### Lane proof gallery
|
||||||
|
|
||||||
|
Proof gallery (QC screenshots) captured at `<lane.cwd>/.playwright-mcp/proof/<slug>/<phase-group>/` and grouped by feature slug (the same slug from per-feature state). Read-only access and deletion are path-contained: every proof file is resolved, realpath'd, and checked to be inside the lane's proof root before serving or deleting.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/lanes/:id/proof
|
||||||
|
```
|
||||||
|
|
||||||
|
List every feature with proof (or an activated-but-not-yet-shot feature from `lane_features`), most recently touched first:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"slug": "auth-redesign",
|
||||||
|
"groups": {
|
||||||
|
"desktop": ["01-login.png", "02-signup.png"],
|
||||||
|
"mobile": ["01-login.png"]
|
||||||
|
},
|
||||||
|
"ticket_report": "auth-redesign/ticket/REPORT.html",
|
||||||
|
"mtime": 1722755400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each feature has:
|
||||||
|
- **`slug`** (string) — canonicalized feature slug
|
||||||
|
- **`groups`** (object) — `{ "phase": ["image1.png", "image2.png"] }` — phase-groups with their images
|
||||||
|
- **`ticket_report`** (string) — relative path to ticket report HTML, or empty if none exists
|
||||||
|
- **`mtime`** (number) — Unix seconds of the most recent file touched in this feature's proof tree (0 if the feature has no proof yet)
|
||||||
|
|
||||||
|
**Status codes:**
|
||||||
|
- **200** — success
|
||||||
|
- **404** `{ "error": { "code": "ENOLANE", "message": "lane not found" } }` — lane doesn't exist
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/lanes/:id/proof/:slug/:group/:file
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetch a single proof file (PNG, JPG, or HTML). The file is resolved with path containment checks before serving:
|
||||||
|
|
||||||
|
**Status codes:**
|
||||||
|
- **200** — file content, `Content-Type` set to `image/png`, `image/jpeg`, or `text/html`
|
||||||
|
- **404** — lane, slug, group, or file not found, or path escapes containment
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/lanes/:id/proof/:slug
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete proof at one of three granularities: request body `{ "group": "phase-name", "images": ["file1.png"] }` deletes specific images from a phase, `{ "group": "phase-name" }` deletes an entire phase, or empty `{}` deletes all phases **except** `ticket/`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "deleted": 3 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status codes:**
|
||||||
|
- **200** — success, returns count of deleted files
|
||||||
|
- **400** `{ "error": { "code": "EBADPATH", "message": "…" } }` — invalid slug, group, or image name; or attempt to delete `ticket/`
|
||||||
|
- **404** `{ "error": { "code": "ENOLANE", "message": "lane not found" } }` — lane doesn't exist
|
||||||
|
- **404** `{ "error": { "code": "ENOFEATURE", "message": "…" } }` — slug doesn't exist
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/lanes/:id/proof-link
|
||||||
|
```
|
||||||
|
|
||||||
|
Converge the clone-root `proof/` directory (if it exists at `<lane.cwd>/proof`) onto the canonical `.playwright-mcp/proof`. This is an explicit operation, never automatic — exposed only as `ccam lanes proof-link`. Idempotent.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "linked": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`linked`** (boolean) — `true` if the link was created or fixed, `false` if the symlink already pointed to the right place or a collision was avoided
|
||||||
|
|
||||||
|
**Status codes:**
|
||||||
|
- **200** — success
|
||||||
|
- **404** `{ "error": { "code": "ENOLANE", "message": "lane not found" } }` — lane doesn't exist
|
||||||
|
|
||||||
|
|
||||||
### Locks
|
### Locks
|
||||||
|
|
||||||
Cross-lane named locks (`server/lib/named-lock.js`) — the OTHER axis from a
|
Cross-lane named locks (`server/lib/named-lock.js`) — the OTHER axis from a
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ A lane is a durable unit of parallel agent work — one working directory, many
|
|||||||
| `ccam lanes runtime [<id>]` | Slot, ports (flagging any that stepped aside from its base), the lane's database name and Redis index when its profile declares them (see [Data isolation](LANES.md#data-isolation-database-redis-and-env-a2)), per-service liveness, log paths, and the last boot error |
|
| `ccam lanes runtime [<id>]` | Slot, ports (flagging any that stepped aside from its base), the lane's database name and Redis index when its profile declares them (see [Data isolation](LANES.md#data-isolation-database-redis-and-env-a2)), per-service liveness, log paths, and the last boot error |
|
||||||
| `ccam lanes logs [<id>] <service> [--tail N]` | Tail one service or hook log (`--tail` in bytes, default 64 KiB) |
|
| `ccam lanes logs [<id>] <service> [--tail N]` | Tail one service or hook log (`--tail` in bytes, default 64 KiB) |
|
||||||
| `ccam lanes hook [<id>] <name> [args…]` | Run one of the profile's hooks: `bootstrap`, `boot`, `health`, `migrate`, `seed`, `ci-gate`, `e2e`, `regen`, `db-create`, `db-drop` |
|
| `ccam lanes hook [<id>] <name> [args…]` | Run one of the profile's hooks: `bootstrap`, `boot`, `health`, `migrate`, `seed`, `ci-gate`, `e2e`, `regen`, `db-create`, `db-drop` |
|
||||||
|
| `ccam lanes proof-link [<id>]` | Converge clone-root `proof/` onto `.playwright-mcp/proof` (idempotent, never run automatically) |
|
||||||
| `ccam lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
| `ccam lock status [<name>]` | Show one lock's holder, or every currently-held lock |
|
||||||
| `ccam lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
|
| `ccam lock acquire <name> [--holder X] [--timeout N]` | Acquire a cross-lane named lock, polling until free (or `--timeout` seconds elapse). Holder defaults to the calling lane (`lane<slot>`) |
|
||||||
| `ccam lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
| `ccam lock release <name> [--holder X]` | Release a lock. Refused (409) when `--holder` doesn't match the current owner |
|
||||||
|
|||||||
@@ -877,6 +877,53 @@ curl -X POST http://localhost:4820/api/lanes/5/remove \
|
|||||||
|
|
||||||
**Actions gated behind confirmation:** `remove` requires the `confirm` flag to prevent accidental deletion.
|
**Actions gated behind confirmation:** `remove` requires the `confirm` flag to prevent accidental deletion.
|
||||||
|
|
||||||
|
## Proof gallery
|
||||||
|
|
||||||
|
A lane's proof gallery is a collection of QC screenshots captured during active work, grouped by feature (the same slug from **Per-feature state and archive** above) and phase (e.g. desktop, mobile, e2e, visual). A QC agent running an MCP server like `playwright-mcp` captures screenshots to disk at `<lane.cwd>/.playwright-mcp/proof/<slug>/<phase-group>/` and generates an optional ticket report at `<slug>/ticket/REPORT.html`. The Workspace gallery panel displays these files grouped by feature, using the same feature picker as the per-feature state section.
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
Proof files live inside the lane's own `cwd`, not under `LANES_ROOT`:
|
||||||
|
|
||||||
|
```
|
||||||
|
<lane.cwd>/.playwright-mcp/proof/
|
||||||
|
├── auth-redesign/
|
||||||
|
│ ├── desktop/
|
||||||
|
│ │ ├── 01-login.png
|
||||||
|
│ │ └── 02-signup.png
|
||||||
|
│ ├── mobile/
|
||||||
|
│ │ ├── 01-login.png
|
||||||
|
│ │ └── 02-signup.png
|
||||||
|
│ └── ticket/
|
||||||
|
│ └── REPORT.html
|
||||||
|
├── another-feature/
|
||||||
|
│ ├── e2e/
|
||||||
|
│ │ └── 01-user-flow.png
|
||||||
|
│ └── ticket/
|
||||||
|
│ └── REPORT.html
|
||||||
|
└── …
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linking proof directories (the `ensure_proof_link` primitive)
|
||||||
|
|
||||||
|
An MCP server's `--output-dir` flag may point to different locations across runs. To converge all proof onto one canonical path regardless, CCAM provides the `ensure_proof_link` operation, exposed only as the explicit `ccam lanes proof-link` CLI command — it is **never automatic**. This matches the "CCAM does not orchestrate" rule: a session decides when to link, not the dashboard.
|
||||||
|
|
||||||
|
`ensure_proof_link` works as follows:
|
||||||
|
|
||||||
|
1. If a stray `proof/` directory exists at `<lane.cwd>/proof`, merge its contents into the canonical `.playwright-mcp/proof` (no-clobber — existing files are never overwritten) and replace it with a symlink.
|
||||||
|
2. If a `proof/` symlink already points to `.playwright-mcp/proof`, the operation is a no-op.
|
||||||
|
3. If a file named `proof` exists (neither directory nor symlink), it is left untouched to avoid clobbering a user's file.
|
||||||
|
|
||||||
|
The operation is idempotent and always safe — running it multiple times on the same lane has no side effects.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
Every path the proof module touches — reading, serving, or deleting files — is resolved, realpath'd, and checked to be inside the lane's proof root before any filesystem operation. Symlink escapes, path traversal (`../`), and bad characters (`/`, `\`, `..` in segment names) are all rejected with `EBADPATH` errors. `deleteProof` never removes the `ticket/` directory, only the phase-group subdirectories and their images, so a lane's ticket report persists even when phase proofs are pruned.
|
||||||
|
|
||||||
|
### Gallery panel and feature selection
|
||||||
|
|
||||||
|
The Workspace page's gallery panel reuses the feature picker from **Per-feature state and archive** — there is only one selector, not a separate one for proofs. Selecting a feature displays that feature's saved pipeline (from per-feature state) and its proof gallery (if any proofs exist for that feature).
|
||||||
|
|
||||||
## Cross-lane named locks
|
## Cross-lane named locks
|
||||||
|
|
||||||
Cross-lane named locks serialize work that thrashes a shared machine — builds, e2e runs, database migrations — across all lanes, not just within one lane. This is a separate axis from `withLaneLock`, the in-process per-lane lock in `server/lib/lane-lock.js`: `lane-lock` is about _when_ a lane runs internal operations; `named-lock` is about which _other lanes_ must wait.
|
Cross-lane named locks serialize work that thrashes a shared machine — builds, e2e runs, database migrations — across all lanes, not just within one lane. This is a separate axis from `withLaneLock`, the in-process per-lane lock in `server/lib/lane-lock.js`: `lane-lock` is about _when_ a lane runs internal operations; `named-lock` is about which _other lanes_ must wait.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ stronger is a separate design (§ Future).
|
|||||||
| **A2** | Data isolation: `.env`, database, Redis index | A1 | ✅ **done** 2026-08-03 |
|
| **A2** | Data isolation: `.env`, database, Redis index | A1 | ✅ **done** 2026-08-03 |
|
||||||
| **A3** | Stack detection + profile scaffolding | A2 | ✅ **done** 2026-08-04 |
|
| **A3** | Stack detection + profile scaffolding | A2 | ✅ **done** 2026-08-04 |
|
||||||
| **B** | Per-feature state + archive | — | ✅ **done** 2026-08-04 |
|
| **B** | Per-feature state + archive | — | ✅ **done** 2026-08-04 |
|
||||||
| **C** | Proof gallery | B | planned |
|
| **C** | Proof gallery | B | ✅ **done** 2026-08-04 |
|
||||||
| **D** | Cross-lane named locks | — | ✅ **done** 2026-08-04 |
|
| **D** | Cross-lane named locks | — | ✅ **done** 2026-08-04 |
|
||||||
| **E** | `ship-feature` skill + QC agents | A2·B·C·D | planned |
|
| **E** | `ship-feature` skill + QC agents | A2·B·C·D | planned |
|
||||||
| **F** | Integrations (tracker / dev-QC / CI) | E | planned |
|
| **F** | Integrations (tracker / dev-QC / CI) | E | planned |
|
||||||
|
|||||||
Reference in New Issue
Block a user