-
Notifications
You must be signed in to change notification settings - Fork 0
[APO-120] Add visual design memory #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
165aa1d
ed14d79
7a777d3
fc6574c
9452b87
40cd8cf
bd3add9
e959b81
e0c375c
e2b7d6d
a74bfec
d0cd428
61f300f
8406b2a
a5795bc
4710265
fc1506d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| name: product-validation | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| 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 }}/tabellio-validation-${{ github.event.pull_request.head.sha || github.sha }}/.artifacts/tabellio/visual-captures/ | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| 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: c4789837ee5ad779198141d6ffd2fea77792fbfb | ||
| path: .tabellio-runner | ||
|
hudsonaikins marked this conversation as resolved.
|
||
| 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 | ||
| 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 | ||
| 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 | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: tabellio-validation-${{ github.event.pull_request.head.sha || github.sha }} | ||
| path: | | ||
| tabellio-validation-result.json | ||
| refs/tabellio/validations/ | ||
| .artifacts/tabellio/ | ||
|
hudsonaikins marked this conversation as resolved.
|
||
| include-hidden-files: true | ||
| if-no-files-found: warn | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| .DS_Store | ||
| .agent-permit/ | ||
| .artifacts/ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new product-validation workflow uploads generated evidence from Useful? React with 👍 / 👎. |
||
| .codex/ | ||
| .coverage | ||
| .entire/ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| { | ||
| "schemaVersion": "tabellio-adapter/v0.1", | ||
| "profiles": { | ||
| "static": { | ||
| "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"] | ||
| ], | ||
| "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" | ||
| }, | ||
| "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": [ | ||
| {"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" | ||
| }, | ||
| "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" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { defineConfig } from "@playwright/test"; | ||
|
|
||
| import baseConfig from "./playwright.config"; | ||
|
|
||
| export default defineConfig({ | ||
| ...baseConfig, | ||
| reporter: [["list"]], | ||
| testMatch: /design-memory\.capture\.ts/, | ||
| use: { | ||
| ...baseConfig.use, | ||
| screenshot: "off", | ||
| trace: "off", | ||
|
Comment on lines
+9
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this capture configuration runs on a host with a different locale or timezone, identical commits render different visible dates and times because the dashboard calls Useful? React with 👍 / 👎. |
||
| }, | ||
| webServer: { | ||
| 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", | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| 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; | ||
| type DashboardFixtureSource = { | ||
| generatedAt: string; | ||
| findings: Array<Record<string, unknown>>; | ||
| repos: Array<Record<string, unknown>>; | ||
| runs: Array<Record<string, unknown>>; | ||
| }; | ||
|
|
||
| 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<string, unknown>; | ||
| 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<string, unknown>; | ||
| 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 }); | ||
|
Comment on lines
+80
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
|
|
||
| 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 }) => { | ||
| 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) => { | ||
| 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"); | ||
| 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}" }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a dashboard component ignores Useful? React with 👍 / 👎. |
||
| 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); | ||
|
hudsonaikins marked this conversation as resolved.
|
||
| 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" }); | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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": [] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On subsequent dashboard changes, this empty, still- Useful? React with 👍 / 👎. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}`); |
Uh oh!
There was an error while loading. Please reload this page.