From 165aa1d368e6b86f8c7cb8523a1245cf0b545848 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 13:45:19 -0700 Subject: [PATCH 01/17] Add APO-120 product validation gate --- .github/workflows/product-validation.yml | 37 ++++++++++++++++++ .gitignore | 1 + .tabellio/validators.json | 50 ++++++++++++++++++++++++ AGENTS.md | 8 ++++ tabellio.validation.json | 35 +++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 .github/workflows/product-validation.yml create mode 100644 .tabellio/validators.json create mode 100644 tabellio.validation.json diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml new file mode 100644 index 0000000..4865c7f --- /dev/null +++ b/.github/workflows/product-validation.yml @@ -0,0 +1,37 @@ +name: product-validation + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + with: {fetch-depth: 0, persist-credentials: false} + - uses: actions/setup-node@v6 + with: {node-version: 22} + - uses: actions/setup-python@v6 + with: {python-version: "3.12"} + - uses: astral-sh/setup-uv@v6 + with: {enable-cache: true} + - name: Validate exact candidate + run: | + set -o pipefail + npm exec --yes --package=github:IntelIP/Tabellio#9ab5a1f1d3bd199b42705e462067ed34a2121439 -- \ + tabellio-validate gate --repo . --repo-id IntelIP/agent-permit-office \ + --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json + - name: Upload validation result + if: always() + uses: actions/upload-artifact@v4 + with: + name: tabellio-validation-${{ github.sha }} + path: | + tabellio-validation-result.json + refs/tabellio/validations/ + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 939d9a0..cda684b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .agent-permit/ +.artifacts/ .codex/ .coverage .entire/ diff --git a/.tabellio/validators.json b/.tabellio/validators.json new file mode 100644 index 0000000..cb67fb9 --- /dev/null +++ b/.tabellio/validators.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": "tabellio-adapter/v0.1", + "profiles": { + "static": { + "commands": [ + ["uv", "sync", "--frozen", "--all-extras", "--dev"], + ["bun", "install", "--frozen-lockfile", "--cwd", "dashboard"], + ["bun", "install", "--frozen-lockfile", "--cwd", "docs-site"], + ["python3", "tools/release_check.py"] + ], + "metrics": [], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Locked environment and release packaging checks" + }, + "schema": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_artifacts.py", "tests/unit/test_models.py", "tests/unit/test_sarif.py", "tests/unit/test_rule_registry.py"]], + "metrics": [{"name": "scanner_artifact_contracts_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Deterministic scanner artifact and SARIF schemas" + }, + "semantic": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_baseline.py", "tests/unit/test_evals.py", "tests/unit/test_permit_engine.py", "tests/unit/test_policy.py"]], + "metrics": [{"name": "permit_policy_oracles_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Permit decisions, policy boundaries, and evaluation oracles" + }, + "workflow": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_cli.py", "tests/unit/test_cli_ci_mode.py", "tests/unit/test_demo.py", "tests/unit/test_fixtures.py", "tests/unit/test_github_action_packaging.py"]], + "metrics": [{"name": "cli_ci_proofpack_workflows_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "CLI, CI mode, fixtures, and packaged action workflows" + }, + "operational": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_model_provider.py", "tests/unit/test_db.py"]], + "metrics": [ + {"name": "local_provider_and_storage_boundaries_pass", "unit": "boolean", "passValue": 1, "failValue": 0}, + {"name": "live_model_calls", "unit": "calls", "passValue": 0, "failValue": 1}, + {"name": "validation_external_cost_usd", "unit": "usd", "passValue": 0, "failValue": 1} + ], + "cost": {"telemetry": "available", "usd": 0, "modelCalls": 0, "toolCalls": 0}, + "summary": "Local provider and storage paths with no live model calls" + }, + "security": { + "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_capability_graph.py", "tests/unit/test_ci_workflows.py", "tests/unit/test_credential_refs.py", "tests/unit/test_mcp_config.py", "tests/unit/test_path_finder.py", "tests/unit/test_prompt_instructions.py"]], + "metrics": [{"name": "agent_attack_path_controls_pass", "unit": "boolean", "passValue": 1, "failValue": 0}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Credential, MCP, workflow, prompt, and attack-path controls" + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 5eb3071..056083f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,3 +60,11 @@ For docs-only work: cd docs-site bun run build ``` + +## Product validation gate + +- Unit tests are structural evidence only. Before review or merge readiness, run the committed `tabellio.validation.json` against the exact candidate commit with `tabellio-validate gate`. +- Schema, semantic, workflow, operational, and security evidence must pass. `blocked` is not `passed`; a new commit invalidates earlier evidence. +- Preserve PermitGraph's boundary: validators may scan committed fixtures and this repository, but never execute target repository code, MCP servers, target workflows, or paid model providers. +- Keep validation external cost at zero. Upload generated evidence from CI; do not commit it. +- Track rollout and failures in Plane item `APO-120`. diff --git a/tabellio.validation.json b/tabellio.validation.json new file mode 100644 index 0000000..b706287 --- /dev/null +++ b/tabellio.validation.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "tabellio-validation/v0.2", + "id": "apo-product-validity", + "failFast": false, + "requireEntireCheckpoint": false, + "acceptance": { + "id": "APO-120", + "source": "plane", + "risk": "critical", + "outcomes": [ + "PermitGraph produces valid, deterministic scanner and proof-pack artifacts.", + "Permit decisions follow policy and evaluation oracles.", + "CLI and GitHub Action workflows preserve CI fail-closed behavior.", + "Credential, MCP, workflow, prompt, and attack-path controls detect unsafe expansion." + ], + "invariants": [ + "Deterministic scanner artifacts remain the source of truth.", + "Validation never executes target code or calls a live model provider." + ], + "forbiddenOutcomes": [ + "Model prose creates a finding without scanner evidence.", + "Validation executes a target repository tool, workflow, MCP server, or package script.", + "Unknown provider cost is treated as zero." + ], + "requiredValidatorTypes": ["schema", "semantic", "workflow", "operational", "security"] + }, + "validators": [ + {"id": "static-release", "type": "static", "argv": ["tabellio-validator", "--profile", "static"], "cwd": ".", "timeoutMs": 900000, "required": true, "evidence": null, "policy": {"metricThresholds": [], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "schema-scanner-artifacts", "type": "schema", "argv": ["tabellio-validator", "--profile", "schema", "--validator-id", "schema-scanner-artifacts", "--out", ".artifacts/tabellio/schema.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/schema.json"}, "policy": {"metricThresholds": [{"metric": "scanner_artifact_contracts_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "semantic-permit-policy", "type": "semantic", "argv": ["tabellio-validator", "--profile", "semantic", "--validator-id", "semantic-permit-policy", "--out", ".artifacts/tabellio/semantic.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/semantic.json"}, "policy": {"metricThresholds": [{"metric": "permit_policy_oracles_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "workflow-cli-ci", "type": "workflow", "argv": ["tabellio-validator", "--profile", "workflow", "--validator-id", "workflow-cli-ci", "--out", ".artifacts/tabellio/workflow.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/workflow.json"}, "policy": {"metricThresholds": [{"metric": "cli_ci_proofpack_workflows_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "operational-provider-boundary", "type": "operational", "argv": ["tabellio-validator", "--profile", "operational", "--validator-id", "operational-provider-boundary", "--out", ".artifacts/tabellio/operational.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/operational.json"}, "policy": {"metricThresholds": [{"metric": "local_provider_and_storage_boundaries_pass", "operator": "eq", "value": 1}, {"metric": "live_model_calls", "operator": "eq", "value": 0}, {"metric": "validation_external_cost_usd", "operator": "eq", "value": 0}], "maxCostUsd": 0, "requireCostTelemetry": true}}, + {"id": "security-agent-boundaries", "type": "security", "argv": ["tabellio-validator", "--profile", "security", "--validator-id", "security-agent-boundaries", "--out", ".artifacts/tabellio/security.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/security.json"}, "policy": {"metricThresholds": [{"metric": "agent_attack_path_controls_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}} + ] +} From ed14d794a25abf28e8ca0eff11467e1ee3a7d39c Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 13:49:53 -0700 Subject: [PATCH 02/17] Pin hardened Tabellio runner --- .github/workflows/product-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index 4865c7f..73ec812 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -23,7 +23,7 @@ jobs: - name: Validate exact candidate run: | set -o pipefail - npm exec --yes --package=github:IntelIP/Tabellio#9ab5a1f1d3bd199b42705e462067ed34a2121439 -- \ + npm exec --yes --package=github:IntelIP/Tabellio#7558d49bad23cf8393fe766c799d81423ad7cf7d -- \ tabellio-validate gate --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json - name: Upload validation result From 7a777d34d5f42497eb902b70f82f39f4f06e6336 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 13:59:14 -0700 Subject: [PATCH 03/17] chore(validation): separate cost evidence from outcomes --- .tabellio/validators.json | 4 +--- tabellio.validation.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.tabellio/validators.json b/.tabellio/validators.json index cb67fb9..44d6910 100644 --- a/.tabellio/validators.json +++ b/.tabellio/validators.json @@ -33,9 +33,7 @@ "operational": { "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_model_provider.py", "tests/unit/test_db.py"]], "metrics": [ - {"name": "local_provider_and_storage_boundaries_pass", "unit": "boolean", "passValue": 1, "failValue": 0}, - {"name": "live_model_calls", "unit": "calls", "passValue": 0, "failValue": 1}, - {"name": "validation_external_cost_usd", "unit": "usd", "passValue": 0, "failValue": 1} + {"name": "local_provider_and_storage_boundaries_pass", "unit": "boolean", "passValue": 1, "failValue": 0} ], "cost": {"telemetry": "available", "usd": 0, "modelCalls": 0, "toolCalls": 0}, "summary": "Local provider and storage paths with no live model calls" diff --git a/tabellio.validation.json b/tabellio.validation.json index b706287..aeb7c97 100644 --- a/tabellio.validation.json +++ b/tabellio.validation.json @@ -29,7 +29,7 @@ {"id": "schema-scanner-artifacts", "type": "schema", "argv": ["tabellio-validator", "--profile", "schema", "--validator-id", "schema-scanner-artifacts", "--out", ".artifacts/tabellio/schema.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/schema.json"}, "policy": {"metricThresholds": [{"metric": "scanner_artifact_contracts_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, {"id": "semantic-permit-policy", "type": "semantic", "argv": ["tabellio-validator", "--profile", "semantic", "--validator-id", "semantic-permit-policy", "--out", ".artifacts/tabellio/semantic.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/semantic.json"}, "policy": {"metricThresholds": [{"metric": "permit_policy_oracles_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, {"id": "workflow-cli-ci", "type": "workflow", "argv": ["tabellio-validator", "--profile", "workflow", "--validator-id", "workflow-cli-ci", "--out", ".artifacts/tabellio/workflow.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/workflow.json"}, "policy": {"metricThresholds": [{"metric": "cli_ci_proofpack_workflows_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, - {"id": "operational-provider-boundary", "type": "operational", "argv": ["tabellio-validator", "--profile", "operational", "--validator-id", "operational-provider-boundary", "--out", ".artifacts/tabellio/operational.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/operational.json"}, "policy": {"metricThresholds": [{"metric": "local_provider_and_storage_boundaries_pass", "operator": "eq", "value": 1}, {"metric": "live_model_calls", "operator": "eq", "value": 0}, {"metric": "validation_external_cost_usd", "operator": "eq", "value": 0}], "maxCostUsd": 0, "requireCostTelemetry": true}}, + {"id": "operational-provider-boundary", "type": "operational", "argv": ["tabellio-validator", "--profile", "operational", "--validator-id", "operational-provider-boundary", "--out", ".artifacts/tabellio/operational.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/operational.json"}, "policy": {"metricThresholds": [{"metric": "local_provider_and_storage_boundaries_pass", "operator": "eq", "value": 1}], "maxCostUsd": 0, "requireCostTelemetry": true}}, {"id": "security-agent-boundaries", "type": "security", "argv": ["tabellio-validator", "--profile", "security", "--validator-id", "security-agent-boundaries", "--out", ".artifacts/tabellio/security.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/security.json"}, "policy": {"metricThresholds": [{"metric": "agent_attack_path_controls_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}} ] } From fc6574c8655035363213b61f01065aaadeb890a2 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 16:04:28 -0700 Subject: [PATCH 04/17] feat: add PermitGraph visual design memory --- .github/workflows/product-validation.yml | 9 ++- .tabellio/validators.json | 6 ++ dashboard/package.json | 1 + dashboard/playwright.design.config.ts | 17 +++++ dashboard/tests/e2e/design-memory.capture.ts | 52 +++++++++++++++ design/baselines/manifest.json | 11 ++++ .../decisions/0001-design-memory-bootstrap.md | 5 ++ design/product.design.json | 63 +++++++++++++++++++ scripts/design/finalize-captures.mjs | 27 ++++++++ tabellio.validation.json | 4 +- 10 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 dashboard/playwright.design.config.ts create mode 100644 dashboard/tests/e2e/design-memory.capture.ts create mode 100644 design/baselines/manifest.json create mode 100644 design/decisions/0001-design-memory-bootstrap.md create mode 100644 design/product.design.json create mode 100644 scripts/design/finalize-captures.mjs diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index 73ec812..dc64460 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -11,19 +11,25 @@ jobs: validate: runs-on: ubuntu-latest timeout-minutes: 30 + env: + TABELLIO_ARTIFACT_BASE_URI: artifact+github://IntelIP/agent-permit-office/actions/runs/${{ github.run_id }}/design-captures/ steps: - uses: actions/checkout@v6 with: {fetch-depth: 0, persist-credentials: false} - uses: actions/setup-node@v6 with: {node-version: 22} + - uses: oven-sh/setup-bun@v2 + with: {bun-version: "1.3.3"} - uses: actions/setup-python@v6 with: {python-version: "3.12"} - uses: astral-sh/setup-uv@v6 with: {enable-cache: true} + - name: Install Chromium runtime + run: bun x playwright@1.61.1 install --with-deps chromium - name: Validate exact candidate run: | set -o pipefail - npm exec --yes --package=github:IntelIP/Tabellio#7558d49bad23cf8393fe766c799d81423ad7cf7d -- \ + npm exec --yes --package=github:IntelIP/Tabellio#870eb739381215ce6e6f4cf11339dd558ef64f44 -- \ tabellio-validate gate --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json - name: Upload validation result @@ -34,4 +40,5 @@ jobs: path: | tabellio-validation-result.json refs/tabellio/validations/ + .artifacts/tabellio/ if-no-files-found: warn diff --git a/.tabellio/validators.json b/.tabellio/validators.json index 44d6910..048e730 100644 --- a/.tabellio/validators.json +++ b/.tabellio/validators.json @@ -30,6 +30,12 @@ "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, "summary": "CLI, CI mode, fixtures, and packaged action workflows" }, + "visual": { + "commands": [["bun", "run", "--cwd", "dashboard", "build"], ["bun", "run", "--cwd", "dashboard", "design:capture"], ["node", "scripts/design/finalize-captures.mjs"]], + "metrics": [{"name": "visual_capture_count", "unit": "count", "pattern": "Visual capture count: ([0-9]+)"}], + "cost": {"telemetry": "not_applicable", "usd": null, "modelCalls": null, "toolCalls": null}, + "summary": "Deterministic PermitGraph dashboard matrix with font and overflow checks" + }, "operational": { "commands": [["uv", "run", "--frozen", "pytest", "-q", "tests/unit/test_model_provider.py", "tests/unit/test_db.py"]], "metrics": [ diff --git a/dashboard/package.json b/dashboard/package.json index f3a765c..6f787e8 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -11,6 +11,7 @@ "lint": "eslint .", "preview": "vite preview", "test:e2e": "playwright test", + "design:capture": "playwright test --config playwright.design.config.ts", "test:unit": "vitest run tests/unit" }, "dependencies": { diff --git a/dashboard/playwright.design.config.ts b/dashboard/playwright.design.config.ts new file mode 100644 index 0000000..7e50784 --- /dev/null +++ b/dashboard/playwright.design.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "@playwright/test"; + +import baseConfig from "./playwright.config"; + +export default defineConfig(baseConfig, { + reporter: [["list"]], + testMatch: /design-memory\.capture\.ts/, + use: { + screenshot: "off", + trace: "off", + }, + webServer: { + command: "VITE_AGENT_PERMIT_API_URL= bun run dev -- --host 127.0.0.1", + reuseExistingServer: true, + url: "http://127.0.0.1:5173", + }, +}); diff --git a/dashboard/tests/e2e/design-memory.capture.ts b/dashboard/tests/e2e/design-memory.capture.ts new file mode 100644 index 0000000..c7e865d --- /dev/null +++ b/dashboard/tests/e2e/design-memory.capture.ts @@ -0,0 +1,52 @@ +import { mkdirSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { expect, test } from "@playwright/test"; + +type Profile = { + policy: { + surfaces: Array<{ id: string; kind: "route" | "component"; target: string; viewports: string[]; themes: string[]; states: string[] }>; + viewports: Array<{ id: string; width: number; height: number }>; + }; +}; + +const dashboardRoot = process.cwd(); +const repoRoot = resolve(dashboardRoot, ".."); +const profile = JSON.parse(readFileSync(resolve(repoRoot, "design/product.design.json"), "utf8")) as Profile; +const dashboardSnapshot = readFileSync(resolve(dashboardRoot, "src/data/generated/dashboardSnapshot.json"), "utf8"); +const output = resolve(repoRoot, ".artifacts/tabellio/visual-captures"); +mkdirSync(output, { recursive: true }); + +test.describe("Tabellio visual capture matrix", () => { + test.describe.configure({ mode: "serial" }); + + for (const surface of profile.policy.surfaces) { + if (surface.kind !== "route") throw new Error(`Capture adapter missing for component surface: ${surface.id}`); + for (const viewportId of surface.viewports) for (const theme of surface.themes) for (const state of surface.states) { + if (state !== "default") throw new Error(`Capture adapter missing for state: ${surface.id}/${state}`); + const viewport = profile.policy.viewports.find((entry) => entry.id === viewportId); + if (!viewport) throw new Error(`Unknown viewport: ${viewportId}`); + const id = `${surface.id}--${viewportId}--${theme}--${state}`; + + test(id, async ({ page }) => { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await page.emulateMedia({ colorScheme: theme as "light" | "dark", reducedMotion: "reduce" }); + await page.route("**/api/snapshot", async (route) => route.fulfill({ body: dashboardSnapshot, contentType: "application/json", status: 200 })); + await page.goto(surface.target, { waitUntil: "domcontentloaded" }); + await page.evaluate((selectedTheme) => { + document.documentElement.classList.remove("light", "dark"); + document.documentElement.classList.add(selectedTheme); + document.documentElement.style.colorScheme = selectedTheme; + }, theme); + await page.addStyleTag({ content: "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}" }); + await page.evaluate(() => document.fonts.ready); + + await expect(page.getByRole("heading", { name: "Repository findings" })).toBeVisible(); + await expect(page.locator("html")).toHaveClass(new RegExp(`(?:^|\\s)${theme}(?:\\s|$)`)); + expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain("DM Sans"); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true); + await page.screenshot({ path: resolve(output, `${id}.png`), animations: "disabled" }); + }); + } + } +}); diff --git a/design/baselines/manifest.json b/design/baselines/manifest.json new file mode 100644 index 0000000..0f53ed5 --- /dev/null +++ b/design/baselines/manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": "tabellio-visual-baselines/v0.1", + "status": "proposed", + "productId": "permitgraph/dashboard", + "profilePath": "design/product.design.json", + "profileDigest": "03ef2f28268a93862651ebd0a8ae8ac2fd24b350619e968cc9a43c56e5189e7a", + "sourceCommit": null, + "approvedAt": null, + "approvedBy": null, + "captures": [] +} diff --git a/design/decisions/0001-design-memory-bootstrap.md b/design/decisions/0001-design-memory-bootstrap.md new file mode 100644 index 0000000..15a8dbe --- /dev/null +++ b/design/decisions/0001-design-memory-bootstrap.md @@ -0,0 +1,5 @@ +# Design memory bootstrap + +PermitGraph keeps its current visual authority in `dashboard/src/index.css` and shadcn configuration in `dashboard/components.json`. The portable product profile references those sources and preserves the separation between deterministic permit evidence and model explanation. + +Initial screenshot baselines remain proposed until dashboard routes and permit states are captured from an exact committed head and approved by a human. diff --git a/design/product.design.json b/design/product.design.json new file mode 100644 index 0000000..aa24bbd --- /dev/null +++ b/design/product.design.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": "tabellio-product-design/v0.1", + "productId": "permitgraph/dashboard", + "title": "PermitGraph Dashboard", + "audience": ["Security engineers", "Platform operators", "AI governance teams"], + "platforms": ["web", "docs"], + "visualThesis": "Calm security-control software that makes permit state, evidence, risk paths, and remediation decisions immediately legible without alarmist decoration.", + "sources": { + "tokens": [ + {"path": "dashboard/src/index.css", "digest": "25661b152f617fdc477e91eba08c0eb6b969e70c5a168b47db697ec74ee25581"} + ], + "components": [ + {"path": "dashboard/components.json", "digest": "49101704a7f660f485a362e1d31d0f123877c34b58bc0cdc7796e99a7fe4438a"} + ], + "decisions": ["AGENTS.md", "design/decisions/0001-design-memory-bootstrap.md"] + }, + "referenceLock": { + "primary": "Existing PermitGraph dashboard tokens and deterministic evidence-review workflow", + "preserve": [ + "DM Sans typography", + "Muted teal primary actions", + "Distinct approved, review, blocked, critical, agent, and artifact semantics", + "Evidence-first information hierarchy", + "True black high-contrast dark surfaces" + ], + "borrow": ["Security operations patterns from real control-plane interfaces"], + "roleRules": [ + "Permit colors communicate deterministic status only", + "Risk emphasis must remain subordinate to evidence and remediation", + "Use Phosphor icons for product actions" + ], + "reject": [ + "Cyberpunk decoration", + "Red-first fear signaling without evidence", + "Generic compliance dashboard card grids", + "Model prose presented as deterministic finding state" + ], + "mediaStrategy": "Prefer graph paths, proof-pack artifacts, and real scanner output over decorative illustration." + }, + "policy": { + "themes": ["light", "dark"], + "viewports": [ + {"id": "desktop", "width": 1440, "height": 1000, "deviceScaleFactor": 1}, + {"id": "mobile", "width": 390, "height": 844, "deviceScaleFactor": 1} + ], + "states": ["default", "hover", "focus", "disabled", "loading", "empty", "error", "approved", "review", "blocked", "critical"], + "surfaces": [ + {"id": "dashboard", "kind": "route", "target": "/", "viewports": ["desktop", "mobile"], "themes": ["light", "dark"], "states": ["default"]} + ], + "forbiddenPatterns": [ + "Status conveyed by color alone", + "Raw colors outside canonical token definitions", + "Target repository code execution during validation", + "Motion without reduced-motion behavior" + ], + "accessibility": { + "standard": "WCAG 2.2 AA", + "keyboardRequired": true, + "reducedMotionRequired": true + } + }, + "baselines": {"manifest": "design/baselines/manifest.json"} +} diff --git a/scripts/design/finalize-captures.mjs b/scripts/design/finalize-captures.mjs new file mode 100644 index 0000000..d466c8a --- /dev/null +++ b/scripts/design/finalize-captures.mjs @@ -0,0 +1,27 @@ +import { execFileSync, spawnSync } from "node:child_process"; + +const sourceCommit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(); +const artifactBaseUri = process.env.TABELLIO_ARTIFACT_BASE_URI?.trim(); +if (!artifactBaseUri) { + throw new Error("TABELLIO_ARTIFACT_BASE_URI is required; visual evidence without a durable URI is blocked."); +} + +const args = [ + "--repo", ".", + "--profile", "design/product.design.json", + "--captures-dir", ".artifacts/tabellio/visual-captures", + "--artifact-base-uri", artifactBaseUri, + "--source-commit", sourceCommit, + "--out", ".artifacts/tabellio/visual-baseline-candidate.json", +]; +const script = process.env.TABELLIO_DESIGN_CAPTURES_SCRIPT?.trim(); +const command = script ? process.execPath : "tabellio-design-captures"; +const result = spawnSync(command, script ? [script, ...args] : args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], +}); +if (result.stdout) process.stdout.write(result.stdout); +if (result.stderr) process.stderr.write(result.stderr); +if (result.status !== 0) process.exit(result.status ?? 1); +const payload = JSON.parse(result.stdout); +console.log(`Visual capture count: ${payload.captureCount}`); diff --git a/tabellio.validation.json b/tabellio.validation.json index aeb7c97..562cd8e 100644 --- a/tabellio.validation.json +++ b/tabellio.validation.json @@ -11,6 +11,7 @@ "PermitGraph produces valid, deterministic scanner and proof-pack artifacts.", "Permit decisions follow policy and evaluation oracles.", "CLI and GitHub Action workflows preserve CI fail-closed behavior.", + "PermitGraph dashboard captures cover declared viewport and theme states.", "Credential, MCP, workflow, prompt, and attack-path controls detect unsafe expansion." ], "invariants": [ @@ -22,13 +23,14 @@ "Validation executes a target repository tool, workflow, MCP server, or package script.", "Unknown provider cost is treated as zero." ], - "requiredValidatorTypes": ["schema", "semantic", "workflow", "operational", "security"] + "requiredValidatorTypes": ["schema", "semantic", "workflow", "visual", "operational", "security"] }, "validators": [ {"id": "static-release", "type": "static", "argv": ["tabellio-validator", "--profile", "static"], "cwd": ".", "timeoutMs": 900000, "required": true, "evidence": null, "policy": {"metricThresholds": [], "maxCostUsd": null, "requireCostTelemetry": false}}, {"id": "schema-scanner-artifacts", "type": "schema", "argv": ["tabellio-validator", "--profile", "schema", "--validator-id", "schema-scanner-artifacts", "--out", ".artifacts/tabellio/schema.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/schema.json"}, "policy": {"metricThresholds": [{"metric": "scanner_artifact_contracts_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, {"id": "semantic-permit-policy", "type": "semantic", "argv": ["tabellio-validator", "--profile", "semantic", "--validator-id", "semantic-permit-policy", "--out", ".artifacts/tabellio/semantic.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/semantic.json"}, "policy": {"metricThresholds": [{"metric": "permit_policy_oracles_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, {"id": "workflow-cli-ci", "type": "workflow", "argv": ["tabellio-validator", "--profile", "workflow", "--validator-id", "workflow-cli-ci", "--out", ".artifacts/tabellio/workflow.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/workflow.json"}, "policy": {"metricThresholds": [{"metric": "cli_ci_proofpack_workflows_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}}, + {"id": "visual-dashboard", "type": "visual", "argv": ["tabellio-validator", "--profile", "visual", "--validator-id", "visual-dashboard", "--out", ".artifacts/tabellio/visual.json"], "cwd": ".", "timeoutMs": 600000, "required": true, "evidence": {"path": ".artifacts/tabellio/visual.json"}, "policy": {"metricThresholds": [{"metric": "visual_capture_count", "operator": "eq", "value": 4}], "maxCostUsd": null, "requireCostTelemetry": false}}, {"id": "operational-provider-boundary", "type": "operational", "argv": ["tabellio-validator", "--profile", "operational", "--validator-id", "operational-provider-boundary", "--out", ".artifacts/tabellio/operational.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/operational.json"}, "policy": {"metricThresholds": [{"metric": "local_provider_and_storage_boundaries_pass", "operator": "eq", "value": 1}], "maxCostUsd": 0, "requireCostTelemetry": true}}, {"id": "security-agent-boundaries", "type": "security", "argv": ["tabellio-validator", "--profile", "security", "--validator-id", "security-agent-boundaries", "--out", ".artifacts/tabellio/security.json"], "cwd": ".", "timeoutMs": 300000, "required": true, "evidence": {"path": ".artifacts/tabellio/security.json"}, "policy": {"metricThresholds": [{"metric": "agent_attack_path_controls_pass", "operator": "eq", "value": 1}], "maxCostUsd": null, "requireCostTelemetry": false}} ] From 9452b87e903d28b49d0f9375176cfd3ab76df726 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:27:45 -0700 Subject: [PATCH 05/17] fix: preserve policy labels in visual fixtures --- dashboard/tests/e2e/design-memory.capture.ts | 68 +++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/dashboard/tests/e2e/design-memory.capture.ts b/dashboard/tests/e2e/design-memory.capture.ts index c7e865d..048a84f 100644 --- a/dashboard/tests/e2e/design-memory.capture.ts +++ b/dashboard/tests/e2e/design-memory.capture.ts @@ -13,7 +13,70 @@ type Profile = { const dashboardRoot = process.cwd(); const repoRoot = resolve(dashboardRoot, ".."); const profile = JSON.parse(readFileSync(resolve(repoRoot, "design/product.design.json"), "utf8")) as Profile; -const dashboardSnapshot = readFileSync(resolve(dashboardRoot, "src/data/generated/dashboardSnapshot.json"), "utf8"); +type DashboardFixtureSource = { + generatedAt: string; + findings: Array>; + repos: Array>; + runs: Array>; +}; + +const dashboardSnapshot = JSON.parse( + readFileSync(resolve(dashboardRoot, "src/data/generated/dashboardSnapshot.json"), "utf8"), +) as DashboardFixtureSource; +const apiSnapshot = JSON.stringify({ + generatedAt: dashboardSnapshot.generatedAt, + repositories: dashboardSnapshot.repos.map((repo) => ({ + id: repo.id, + label: repo.label, + local_path: repo.source, + })), + runs: dashboardSnapshot.runs.map((run) => { + const repo = dashboardSnapshot.repos.find((candidate) => candidate.id === run.repoId); + const metrics = (run.metrics ?? {}) as Record; + const artifacts = Array.isArray(run.artifacts) ? run.artifacts : []; + return { + artifact_dir: artifacts[0] ?? "", + branch: "fixture", + cache_hit_ratio: metrics.cacheHitRatio ?? 0, + cached_tokens: metrics.cachedTokens ?? 0, + completed_at: run.completedAt, + controls_count: metrics.controls ?? 0, + findings_count: metrics.findings ?? 0, + graph_paths_count: metrics.graphPaths ?? 0, + local_path: repo?.source ?? "fixture", + model: null, + model_calls: metrics.modelCalls ?? 0, + permit_status: run.status, + repository_id: run.repoId, + repository_label: repo?.label ?? run.repoId, + run_id: run.id, + status: "completed", + total_tokens: metrics.totalTokens ?? 0, + }; + }), + findings: dashboardSnapshot.findings.map((finding) => { + const metrics = (finding.metrics ?? {}) as Record; + return { + cache_hit_ratio: metrics.cacheHitRatio ?? 0, + cached_tokens: metrics.cachedTokens ?? 0, + controls_count: metrics.controls ?? 0, + finding_id: finding.id, + findings_count: metrics.findings ?? 1, + graph_paths_count: metrics.graphPaths ?? 0, + local_path: finding.source, + model_calls: metrics.modelCalls ?? 0, + permit_status: finding.status, + recommendation: finding.remediation, + repository_label: finding.repo, + risk: finding.summary, + rule_id: finding.rule, + run_id: finding.runId, + severity: finding.severity, + title: finding.title, + total_tokens: metrics.totalTokens ?? 0, + }; + }), +}); const output = resolve(repoRoot, ".artifacts/tabellio/visual-captures"); mkdirSync(output, { recursive: true }); @@ -31,7 +94,7 @@ test.describe("Tabellio visual capture matrix", () => { test(id, async ({ page }) => { await page.setViewportSize({ width: viewport.width, height: viewport.height }); await page.emulateMedia({ colorScheme: theme as "light" | "dark", reducedMotion: "reduce" }); - await page.route("**/api/snapshot", async (route) => route.fulfill({ body: dashboardSnapshot, contentType: "application/json", status: 200 })); + await page.route("**/api/snapshot", async (route) => route.fulfill({ body: apiSnapshot, contentType: "application/json", status: 200 })); await page.goto(surface.target, { waitUntil: "domcontentloaded" }); await page.evaluate((selectedTheme) => { document.documentElement.classList.remove("light", "dark"); @@ -42,6 +105,7 @@ test.describe("Tabellio visual capture matrix", () => { await page.evaluate(() => document.fonts.ready); await expect(page.getByRole("heading", { name: "Repository findings" })).toBeVisible(); + await expect(page.getByText("unknown policy", { exact: true })).toHaveCount(0); await expect(page.locator("html")).toHaveClass(new RegExp(`(?:^|\\s)${theme}(?:\\s|$)`)); expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain("DM Sans"); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true); From 40cd8cf9cf0950031b1f4afa96ea4abdd0bb2dbf Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:33:21 -0700 Subject: [PATCH 06/17] chore: pin validated Tabellio release --- .github/workflows/product-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index dc64460..6b429f1 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -29,7 +29,7 @@ jobs: - name: Validate exact candidate run: | set -o pipefail - npm exec --yes --package=github:IntelIP/Tabellio#870eb739381215ce6e6f4cf11339dd558ef64f44 -- \ + npm exec --yes --package=github:IntelIP/Tabellio#c5076f33bd93f96561cf7d9eb106ec7c137a6fe8 -- \ tabellio-validate gate --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json - name: Upload validation result From bd3add9b872440d8759e9c904b271dd6556079aa Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:39:54 -0700 Subject: [PATCH 07/17] fix: provision Chromium for isolated validation --- .tabellio/validators.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.tabellio/validators.json b/.tabellio/validators.json index 048e730..982a097 100644 --- a/.tabellio/validators.json +++ b/.tabellio/validators.json @@ -5,6 +5,7 @@ "commands": [ ["uv", "sync", "--frozen", "--all-extras", "--dev"], ["bun", "install", "--frozen-lockfile", "--cwd", "dashboard"], + ["bun", "run", "--cwd", "dashboard", "playwright", "install", "chromium"], ["bun", "install", "--frozen-lockfile", "--cwd", "docs-site"], ["python3", "tools/release_check.py"] ], From e959b812231533709b48dca592ec6ba0375def0f Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:44:25 -0700 Subject: [PATCH 08/17] fix: capture dashboard from production build --- dashboard/playwright.design.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/playwright.design.config.ts b/dashboard/playwright.design.config.ts index 7e50784..d1998aa 100644 --- a/dashboard/playwright.design.config.ts +++ b/dashboard/playwright.design.config.ts @@ -10,8 +10,8 @@ export default defineConfig(baseConfig, { trace: "off", }, webServer: { - command: "VITE_AGENT_PERMIT_API_URL= bun run dev -- --host 127.0.0.1", + command: "VITE_AGENT_PERMIT_API_URL= bun run build && bun run preview -- --host 127.0.0.1", reuseExistingServer: true, - url: "http://127.0.0.1:5173", + url: "http://127.0.0.1:4173", }, }); From e0c375c5972c36d00dbb86e67fd33e7f0fc27e58 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:47:02 -0700 Subject: [PATCH 09/17] fix: override dashboard capture server --- dashboard/playwright.design.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dashboard/playwright.design.config.ts b/dashboard/playwright.design.config.ts index d1998aa..c3c6b9e 100644 --- a/dashboard/playwright.design.config.ts +++ b/dashboard/playwright.design.config.ts @@ -2,10 +2,12 @@ import { defineConfig } from "@playwright/test"; import baseConfig from "./playwright.config"; -export default defineConfig(baseConfig, { +export default defineConfig({ + ...baseConfig, reporter: [["list"]], testMatch: /design-memory\.capture\.ts/, use: { + ...baseConfig.use, screenshot: "off", trace: "off", }, From e2b7d6d6e44c7ae4406c96c4e22cf088dcc87054 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:47:25 -0700 Subject: [PATCH 10/17] fix: align dashboard preview capture port --- dashboard/playwright.design.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/playwright.design.config.ts b/dashboard/playwright.design.config.ts index c3c6b9e..469dce3 100644 --- a/dashboard/playwright.design.config.ts +++ b/dashboard/playwright.design.config.ts @@ -12,8 +12,8 @@ export default defineConfig({ trace: "off", }, webServer: { - command: "VITE_AGENT_PERMIT_API_URL= bun run build && bun run preview -- --host 127.0.0.1", + command: "VITE_AGENT_PERMIT_API_URL= bun run build && bun run preview -- --host 127.0.0.1 --port 5173", reuseExistingServer: true, - url: "http://127.0.0.1:4173", + url: "http://127.0.0.1:5173", }, }); From a74bfecc48326dbfa34b3742000cfe2e7ecc0bf0 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:52:43 -0700 Subject: [PATCH 11/17] chore: advance Tabellio validation pin --- .github/workflows/product-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index 6b429f1..64a288b 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -29,7 +29,7 @@ jobs: - name: Validate exact candidate run: | set -o pipefail - npm exec --yes --package=github:IntelIP/Tabellio#c5076f33bd93f96561cf7d9eb106ec7c137a6fe8 -- \ + npm exec --yes --package=github:IntelIP/Tabellio#49f3975c29cdd3dd725b8410e3e908fb6dfbcc1d -- \ tabellio-validate gate --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json - name: Upload validation result From d0cd4284fe4f3e33951fbdc11cd48f92be570c69 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 18:57:34 -0700 Subject: [PATCH 12/17] fix: validate exact PR head in CI --- .github/workflows/product-validation.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index 64a288b..eb54711 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -15,11 +15,21 @@ jobs: TABELLIO_ARTIFACT_BASE_URI: artifact+github://IntelIP/agent-permit-office/actions/runs/${{ github.run_id }}/design-captures/ steps: - uses: actions/checkout@v6 - with: {fetch-depth: 0, persist-credentials: false} + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v6 with: {node-version: 22} - uses: oven-sh/setup-bun@v2 with: {bun-version: "1.3.3"} + - name: Checkout pinned Tabellio runner + uses: actions/checkout@v6 + with: + repository: IntelIP/Tabellio + ref: 0bfa1160bc97726cab1847e2eccfa4d18cafbc7d + path: .tabellio-runner + persist-credentials: false - uses: actions/setup-python@v6 with: {python-version: "3.12"} - uses: astral-sh/setup-uv@v6 @@ -29,14 +39,15 @@ jobs: - name: Validate exact candidate run: | set -o pipefail - npm exec --yes --package=github:IntelIP/Tabellio#49f3975c29cdd3dd725b8410e3e908fb6dfbcc1d -- \ - tabellio-validate gate --repo . --repo-id IntelIP/agent-permit-office \ + install -m 755 .tabellio-runner/scripts/tabellio-validator.mjs "$RUNNER_TEMP/tabellio-validator" + PATH="$RUNNER_TEMP:$PATH" node .tabellio-runner/scripts/tabellio-validate.mjs gate \ + --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json - name: Upload validation result if: always() uses: actions/upload-artifact@v4 with: - name: tabellio-validation-${{ github.sha }} + name: tabellio-validation-${{ github.event.pull_request.head.sha || github.sha }} path: | tabellio-validation-result.json refs/tabellio/validations/ From 61f300f1f7aed73906d6af15425d1b5a5358cd2b Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 19:09:42 -0700 Subject: [PATCH 13/17] fix: bootstrap visual evidence finalizer --- .github/workflows/product-validation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index eb54711..b33f4f5 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -40,6 +40,7 @@ jobs: run: | set -o pipefail install -m 755 .tabellio-runner/scripts/tabellio-validator.mjs "$RUNNER_TEMP/tabellio-validator" + install -m 755 .tabellio-runner/scripts/finalize-tabellio-visual-captures.mjs "$RUNNER_TEMP/tabellio-design-captures" PATH="$RUNNER_TEMP:$PATH" node .tabellio-runner/scripts/tabellio-validate.mjs gate \ --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json From 8406b2aa3c4514fdde02e3374d696a74bdba7d0d Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 19:13:16 -0700 Subject: [PATCH 14/17] fix: preserve visual finalizer module path --- .github/workflows/product-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index b33f4f5..1fe2ab5 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -40,7 +40,7 @@ jobs: run: | set -o pipefail install -m 755 .tabellio-runner/scripts/tabellio-validator.mjs "$RUNNER_TEMP/tabellio-validator" - install -m 755 .tabellio-runner/scripts/finalize-tabellio-visual-captures.mjs "$RUNNER_TEMP/tabellio-design-captures" + ln -s "$GITHUB_WORKSPACE/.tabellio-runner/scripts/finalize-tabellio-visual-captures.mjs" "$RUNNER_TEMP/tabellio-design-captures" PATH="$RUNNER_TEMP:$PATH" node .tabellio-runner/scripts/tabellio-validate.mjs gate \ --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json From a5795bc505da4febb601f5d88d1398d2e874f2ec Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 19:49:00 -0700 Subject: [PATCH 15/17] fix: address visual validation review findings --- .github/workflows/product-validation.yml | 11 ++++++++--- dashboard/playwright.design.config.ts | 4 ++-- dashboard/tests/e2e/design-memory.capture.ts | 7 ++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index 1fe2ab5..f62f27a 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -2,6 +2,8 @@ name: product-validation on: pull_request: + push: + branches: [main] workflow_dispatch: permissions: @@ -30,6 +32,8 @@ jobs: ref: 0bfa1160bc97726cab1847e2eccfa4d18cafbc7d path: .tabellio-runner persist-credentials: false + - name: Isolate validator from candidate scan + run: mv .tabellio-runner "$RUNNER_TEMP/tabellio-runner" - uses: actions/setup-python@v6 with: {python-version: "3.12"} - uses: astral-sh/setup-uv@v6 @@ -39,9 +43,9 @@ jobs: - name: Validate exact candidate run: | set -o pipefail - install -m 755 .tabellio-runner/scripts/tabellio-validator.mjs "$RUNNER_TEMP/tabellio-validator" - ln -s "$GITHUB_WORKSPACE/.tabellio-runner/scripts/finalize-tabellio-visual-captures.mjs" "$RUNNER_TEMP/tabellio-design-captures" - PATH="$RUNNER_TEMP:$PATH" node .tabellio-runner/scripts/tabellio-validate.mjs gate \ + install -m 755 "$RUNNER_TEMP/tabellio-runner/scripts/tabellio-validator.mjs" "$RUNNER_TEMP/tabellio-validator" + ln -s "$RUNNER_TEMP/tabellio-runner/scripts/finalize-tabellio-visual-captures.mjs" "$RUNNER_TEMP/tabellio-design-captures" + PATH="$RUNNER_TEMP:$PATH" node "$RUNNER_TEMP/tabellio-runner/scripts/tabellio-validate.mjs" gate \ --repo . --repo-id IntelIP/agent-permit-office \ --base origin/main --commit HEAD --manifest tabellio.validation.json | tee tabellio-validation-result.json - name: Upload validation result @@ -53,4 +57,5 @@ jobs: tabellio-validation-result.json refs/tabellio/validations/ .artifacts/tabellio/ + include-hidden-files: true if-no-files-found: warn diff --git a/dashboard/playwright.design.config.ts b/dashboard/playwright.design.config.ts index 469dce3..6cbcf49 100644 --- a/dashboard/playwright.design.config.ts +++ b/dashboard/playwright.design.config.ts @@ -12,8 +12,8 @@ export default defineConfig({ trace: "off", }, webServer: { - command: "VITE_AGENT_PERMIT_API_URL= bun run build && bun run preview -- --host 127.0.0.1 --port 5173", - reuseExistingServer: true, + command: "VITE_AGENT_PERMIT_API_URL=/api bun run build && bun run preview -- --host 127.0.0.1 --port 5173", + reuseExistingServer: false, url: "http://127.0.0.1:5173", }, }); diff --git a/dashboard/tests/e2e/design-memory.capture.ts b/dashboard/tests/e2e/design-memory.capture.ts index 048a84f..e02ed1a 100644 --- a/dashboard/tests/e2e/design-memory.capture.ts +++ b/dashboard/tests/e2e/design-memory.capture.ts @@ -92,9 +92,13 @@ test.describe("Tabellio visual capture matrix", () => { const id = `${surface.id}--${viewportId}--${theme}--${state}`; test(id, async ({ page }) => { + let fixtureRequests = 0; await page.setViewportSize({ width: viewport.width, height: viewport.height }); await page.emulateMedia({ colorScheme: theme as "light" | "dark", reducedMotion: "reduce" }); - await page.route("**/api/snapshot", async (route) => route.fulfill({ body: apiSnapshot, contentType: "application/json", status: 200 })); + await page.route("**/api/snapshot", async (route) => { + fixtureRequests += 1; + await route.fulfill({ body: apiSnapshot, contentType: "application/json", status: 200 }); + }); await page.goto(surface.target, { waitUntil: "domcontentloaded" }); await page.evaluate((selectedTheme) => { document.documentElement.classList.remove("light", "dark"); @@ -105,6 +109,7 @@ test.describe("Tabellio visual capture matrix", () => { await page.evaluate(() => document.fonts.ready); await expect(page.getByRole("heading", { name: "Repository findings" })).toBeVisible(); + expect(fixtureRequests).toBeGreaterThan(0); await expect(page.getByText("unknown policy", { exact: true })).toHaveCount(0); await expect(page.locator("html")).toHaveClass(new RegExp(`(?:^|\\s)${theme}(?:\\s|$)`)); expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain("DM Sans"); From 471026561e89cd11d1c981106989bbefb3810e9b Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 19:55:07 -0700 Subject: [PATCH 16/17] fix: bind durable live visual evidence --- .github/workflows/product-validation.yml | 2 +- dashboard/tests/e2e/design-memory.capture.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index f62f27a..7d4c2d5 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - TABELLIO_ARTIFACT_BASE_URI: artifact+github://IntelIP/agent-permit-office/actions/runs/${{ github.run_id }}/design-captures/ + TABELLIO_ARTIFACT_BASE_URI: artifact+github://IntelIP/agent-permit-office/actions/runs/${{ github.run_id }}/tabellio-validation-${{ github.event.pull_request.head.sha || github.sha }}/.artifacts/tabellio/visual-captures/ steps: - uses: actions/checkout@v6 with: diff --git a/dashboard/tests/e2e/design-memory.capture.ts b/dashboard/tests/e2e/design-memory.capture.ts index e02ed1a..1e11889 100644 --- a/dashboard/tests/e2e/design-memory.capture.ts +++ b/dashboard/tests/e2e/design-memory.capture.ts @@ -109,6 +109,7 @@ test.describe("Tabellio visual capture matrix", () => { await page.evaluate(() => document.fonts.ready); await expect(page.getByRole("heading", { name: "Repository findings" })).toBeVisible(); + await expect(page.getByText("Live Worker data", { exact: true })).toBeVisible(); expect(fixtureRequests).toBeGreaterThan(0); await expect(page.getByText("unknown policy", { exact: true })).toHaveCount(0); await expect(page.locator("html")).toHaveClass(new RegExp(`(?:^|\\s)${theme}(?:\\s|$)`)); From fc1506d4bb88472bcc13f70ae01c08ea35623029 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 16 Jul 2026 20:00:38 -0700 Subject: [PATCH 17/17] chore: pin merged Tabellio validator --- .github/workflows/product-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/product-validation.yml b/.github/workflows/product-validation.yml index 7d4c2d5..5d06fb2 100644 --- a/.github/workflows/product-validation.yml +++ b/.github/workflows/product-validation.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v6 with: repository: IntelIP/Tabellio - ref: 0bfa1160bc97726cab1847e2eccfa4d18cafbc7d + ref: c4789837ee5ad779198141d6ffd2fea77792fbfb path: .tabellio-runner persist-credentials: false - name: Isolate validator from candidate scan