Deterministic design contracts for pen.dev mockups.
Penflow is a Python CLI that turns a pen.dev .pen file into a verifiable contract. It parses .pen JSON, validates the semantic context attached to screens and components, exports the expected UI tree, validates the actual UI tree produced by your app's runtime adapter, and compares both structurally. Every result is a deterministic JSON report, so agents and CI can tell whether an implementation still matches its design.
Penflow is the design-contract companion of LiveSpec: LiveSpec keeps specifications alive next to the code, Penflow keeps UI mockups and runtime screens aligned with those specifications. Penflow stays runtime-agnostic: browsers, simulators, application runtimes, and pen.dev itself stay outside the Python core.
Status: alpha (
0.1.0, not yet published to PyPI). Commands and report schemas may still change.
The project is licensed under the MIT License. Before contributing or reporting a vulnerability, read CONTRIBUTING.md and SECURITY.md. Release status and provenance limits are recorded in CHANGELOG.md, RELEASE.md, and PROVENANCE.md.
Penflow requires Python 3.12 or newer. The repository is the current distribution source; no PyPI release is claimed.
Install the CLI from GitHub:
uv tool install "git+https://github.com/julien-m/penflow.git"
penflow --helpFor development from a clone:
git clone https://github.com/julien-m/penflow.git
cd penflow
uv sync --extra dev --locked
uv run penflow --help
uv run python -m pytestTo build the same wheel and source archive checked by CI:
uv build
python3 -m venv /tmp/penflow-release-check
/tmp/penflow-release-check/bin/python -m pip install dist/penflow-0.1.0-py3-none-any.whl
/tmp/penflow-release-check/bin/penflow --helpThe release checklist in RELEASE.md defines the version/tag/build order. Do not publish an artifact whose version differs between pyproject.toml, penflow.__version__, and the tag.
Run the demo inside a fresh consumer project (never inside the Penflow checkout):
mkdir penflow-demo && cd penflow-demo && git init -q
penflow doctor --project . --fix --json
penflow demo --out penflow --project .doctor --fix creates the canonical penflow/ui.pen and the managed .gitignore block; demo then writes a sample contract, semantic tree, expected and actual UI trees, generated code stubs and the comparison report under penflow/, so you can inspect the three-tree workflow described in Three Trees before wiring your own project.
Run Penflow against the exact consumer project, never the Penflow source checkout. At the start and end of every Penflow task, capture a read-only Doctor receipt:
penflow doctor --project . --check --json--project selects that exact directory (or an existing file's parent); without it, the exact CWD is selected. Doctor never widens the boundary to the enclosing repository. A collective Git worktree may therefore contain several independent Penflow projects, but each selected project must contain exactly one regular, case-sensitive ui.pen. Keep every logical screen revision inside that infinite canvas as a top-level frame with penflow.lifecycle schema 1: one active revision per logical_screen_id, plus any draft and archive revisions. Unmarked legacy top-level frames remain active for compatibility; downstream exports consume active regions only. The internal git_root is used only for scoped Git interrogation and transient Git metadata coordination; .pen discovery, ignore ownership, temporary inventory, repair and cleanup remain below <consumer-project-root>.
Technical working data belongs only below ignored .penflow-tmp/, .penflow-run/, and .mockup-validation/. Producer preflight reconciles eligible completed, failed, or proven-dead runs; postflight removes successful working payload after durable outputs are finalized. Live owners, unknown files, symlinks, escaped paths, tracked/staged data, referenced evidence, and ui.pen are preserved and block unsafe cleanup.
penflow doctor --project . --fix --json may initialize penflow/ui.pen when none exists and install the managed .gitignore block. It never chooses among multiple .pen files or rewrites Git history/index state. Resolve multiple_pen_files manually by consolidating the intended screen revisions into the canonical canvas, then deliberately remove or migrate the extra files. For tracked or staged temporary paths, review them and manually unstage/untrack them before rerunning Doctor.
Exit codes are 0 PASS, 1 project-health block, 2 usage error, 3 untrustworthy I/O/schema result, and 4 cleanup safety block.
Local form submits use kind: state_change, event: submit, a guard and a same-screen
success transition. They require no invented network mutation or permission; mutation-bearing
actions keep all request checks. C51 still requires the action, both guard outcomes and the
transition to be observed. See the local submit clarification of the JSON-only contract v2 (feature 045).
flow-ui-contract/contract.json is the canonical strict v2 business contract. Flow/screen Markdown is a deterministic, hash-bound projection produced by export-flow-docs; legacy Markdown is accepted only through explicit migration. Strict coverage is bidirectional: every contract node has one Pencil anchor and every authored Pencil node is explicitly semantic, inherited, or decorative.
The normal workflow produces one versioned run-report.json and revalidates it at the consumer's final transition. Its profile comes from the requested deliverable: design checks the approved contract and native design; implementation adds the actual application build, executed scenarios and observed outcomes. Missing runtime evidence never changes an implementation request into a design request.
The workflow supplies these inputs automatically; the equivalent CLI calls are:
penflow run --target penflow --profile implementation --build-manifest "$RUNNER_BUILD_MANIFEST" --project . --json
penflow validate-report penflow/run-report.json --schema --required-profile implementation --build-manifest "$RUNNER_BUILD_MANIFEST" --project . --jsonFor design closure, the workflow selects design and omits the build manifest. --project is the exact consumer project, while the workspace may be its penflow/ or handoff/penflow/ directory. The manifest is produced by the external runner for the build actually exercised; a Git revision, static fixture or file hash alone is not runtime evidence.
C51 derives requirements and applicability from reviewed LiveSpec sources or an authenticated frozen Brainstorm contract. It joins those requirements to contract obligations, native evidence and typed observations. Every required gate and obligation must pass: missing, skipped, stale or unknown evidence blocks closure. Revalidation checks current inputs and caller identities instead of trusting a stored PASS. Historical reports and the opt-in C47 --strict-closure path remain readable but do not certify C51.
The public Python boundary is penflow.verification.produce_verification_report(...) and validate_verification_report(...). The latter returns penflow-verification-validation version 1 with the checked report hash, exact project/workspace, profile and build-manifest identity. Read the verification workflow for the schemas, automatic producer steps and integration contract; read the adapter guides for the external runtime boundary. Generated code remains a scaffold until these implementation checks pass.
Penflow exits non-zero on warning-severity issues by default. Errors always fail; warnings fail unless you pass the global --lenient flag (accepted before or after the subcommand on every validating command):
- Applies to
validate-pen,validate-flow-specs,validate-flow-contract,validate-semantic-tree,validate-actual,validate-flow,validate-screen,validate-code-ir,validate-report,validate-visual-evidence,compare-tree,detect-drift,lint-tokens,check-consistency,audit-visual,export-expected,ci. compare-treeandcidefault threshold is 0.98 (was 0.9).--lenientrestores the 0.9 default; an explicit--thresholdalways wins.export-semantic-tree,reverse-pen, andextract-tokensnow report issues and exit 1 on them (previously always exit 0).reverse-penflags every generated artifact still containing[NEEDS CLARIFICATION]asreverse.clarification_needed;extract-tokensflags uninterpretable values astokens.value.unparsed.detect-driftcarries blocking data: each issue has ablockingboolean (BLOCKING_DRIFT_CODES=drift.semantic_tree_invalid,drift.semantic_node_missing_in_pen,drift.pencil_context_obsolete,drift.clarification_marker), the summary has ablockingcount, and the report status gainsBLOCKED. Blocking issues exit 1 even with--lenient.- Silent coercions now warn: non-keyword string bbox values emit
pen.bbox.invalid(viavalidate-pen/inspect; Pencil sizing keywordsfill_container/fit_content(...)are exempt), and unparsable theme dimensions emittheme.value.unparsed(vialint-tokens). jsonschema>=4is a hard dependency; the minimal fallback validators were removed.
Migration: append --lenient to any pipeline that tolerated warnings, or fix the warnings (recommended).
In this block, <workspace> is <consumer-project-root>/penflow; every
canonical artifact stays below that workspace while Doctor governs the exact
consumer project root.
penflow doctor --project <consumer-project-root> --check --json
penflow run --target <workspace> --project <consumer-project-root> --theme-css <workspace>/theme.css --references <workspace>/references --strict --json
penflow doctor --project <consumer-project-root> --check --json
penflow inspect <workspace>/ui.pen --project <consumer-project-root>
penflow pencil-sync <workspace>/ui.pen --project <consumer-project-root> --expect-screens 8 --json
penflow verify-pencil-persistence <workspace>/observation.json --document <workspace>/ui.pen --run-id run-001 --operation save-current-pencil-document --out <workspace>/persistence-receipt.json --project <consumer-project-root> --json
penflow validate-pen <workspace>/ui.pen --project <consumer-project-root>
penflow export-expected <workspace>/ui.pen -o <workspace>/expected-ui-tree.json --project <consumer-project-root>
penflow check-consistency <workspace>/expected-ui-tree.json --out <workspace>/consistency-report.json --project <consumer-project-root> [--group-by role|role+intent|role+intent+variant|role+intent+variant+size-class]
penflow audit-visual <workspace>/expected-ui-tree.json --theme-css <workspace>/theme.css [--layout-snapshot <workspace>/.mockup-validation/visual-evidence/layout-snapshot.json] --out <workspace>/visual-metrics-report.json --project <consumer-project-root>
penflow theme-fingerprint <workspace>/theme.css --project <consumer-project-root> --json
penflow validate-visual-evidence <workspace>/.mockup-validation/visual-evidence --expected <workspace>/expected-ui-tree.json --theme-css <workspace>/theme.css --project <consumer-project-root> --json
penflow validate-reference-fidelity <workspace>/.mockup-validation/visual-evidence --references <workspace>/references --project <consumer-project-root> --json
penflow validate-actual <workspace>/actual-ui-tree.json --project <consumer-project-root> [--schema]
penflow validate-flow-contract <workspace>/flow-ui-contract/contract.json --project <consumer-project-root> --json
penflow validate-causal-evidence <workspace>/flow-ui-contract/contract.json <workspace>/evidence/causal-evidence.json --project <consumer-project-root> --json
penflow export-semantic-tree <workspace>/flow-ui-contract/contract.json --out <workspace>/semantic-ui-tree.json --project <consumer-project-root>
penflow validate-semantic-tree <workspace>/semantic-ui-tree.json --project <consumer-project-root> --json
penflow draft-pen-from-tree <workspace>/semantic-ui-tree.json --out <workspace>/ui.pen --theme-css <workspace>/theme.css --project <consumer-project-root> --json
penflow lint-tokens <workspace>/ui.pen --theme-css <workspace>/theme.css --out <workspace>/token-lint-report.json --project <consumer-project-root> --json
penflow enrich-pen <workspace>/ui.pen <workspace>/semantic-ui-tree.json --out <workspace>/ui.pen --project <consumer-project-root> --json
penflow map-pencil-context <workspace>/ui.pen <workspace>/semantic-ui-tree.json --out <workspace>/pencil-context-map.json --project <consumer-project-root> --strict-coverage --contract <workspace>/flow-ui-contract/contract.json --coverage-out <workspace>/semantic-context-coverage.json --json
penflow audit-context-coverage <workspace>/ui.pen --contract <workspace>/flow-ui-contract/contract.json --out <workspace>/semantic-context-coverage.json --project <consumer-project-root> --json
penflow export-flow-docs <workspace>/flow-ui-contract/contract.json --out <workspace>/flow-ui-contract --project <consumer-project-root> --json
penflow export-flow-docs <workspace>/flow-ui-contract/contract.json --out <workspace>/flow-ui-contract --check --project <consumer-project-root> --json
penflow migrate-flow-md-to-contract-json <workspace>/flow-ui-contract --out <workspace>/flow-ui-contract/contract.json --project <consumer-project-root> --json
penflow reverse-pen <workspace>/ui.pen --out <workspace>/reverse-output --project <consumer-project-root> --json
penflow detect-drift <workspace>/flow-ui-contract/contract.json <workspace>/semantic-ui-tree.json <workspace>/ui.pen --out <workspace>/drift-report.json --project <consumer-project-root> --json
penflow checklist-from-drift <workspace>/drift-report.json --out <workspace>/checklist.json --project <consumer-project-root> [--group <name>] --json
penflow code-ir --from-context <workspace>/ui.pen --contract <workspace>/flow-ui-contract/contract.json --flow-id checkout --flow-name "Checkout flow" --out <workspace>/code-ir.json --project <consumer-project-root>
penflow validate-code-ir <workspace>/code-ir.json --project <consumer-project-root> --json
penflow compare-tree <workspace>/expected-ui-tree.json <workspace>/actual-ui-tree.json --out <workspace>/compare-report.json --project <consumer-project-root> --json
penflow review-report <workspace>/compare-report.json --out <workspace>/review-report.json --project <consumer-project-root> --json
penflow fix-report <workspace>/compare-report.json --out <workspace>/fix-report.json --project <consumer-project-root> --json
penflow validate-report <workspace>/review-report.json --schema --project <consumer-project-root> --json
penflow demo --out <workspace> --project <consumer-project-root>penflow run --target <workspace> [--theme-css f] [--references dir] [--strict] --json is the single-command CI/operator wrapper for an existing Penflow workspace. Its summary and every gate carry the same runId, identity version, current-run provenance, and SHA-256 lineage for ui.pen, contract, expected/actual trees, theme, references, renderer, and export timestamp. Missing required inputs, explicitly requested missing optional paths, source mutation, or citation/provenance divergence make the run nonzero; unarmed absent theme/reference inputs receive deterministic not-provided receipts. The command preserves Penflow's runtime-agnostic boundary: no browser, simulator, Playwright, or Pencil MCP control is added to core; the optional pencil-sync trigger only runs when explicitly armed with PENFLOW_RUN_PENCIL_SYNC=1, otherwise it is reported as SKIP. That trigger never substitutes for the separate C46 persistence receipt.
penflow run --target <workspace> --references <workspace>/references --strict-closure --json
is the explicit C47 closure check. It does not run the mutating pipeline: it revalidates the
existing .penflow-run/pencil-persistence-receipt.json, requires a fresh
runtime-capture actual receipt with sessionId, validates the declared homologous
references and PASS-only structured blocking visual proof, and requires every artifact to
share one run id. It writes only .penflow-run/strict-closure.json, whose schema and
integrity bind all workspace input bytes. Missing, BLOCKED_VISUAL_NOT_RUN,
PASSED_WITH_WARNINGS, stale, replayed, mismatched, non-blocking, generic, duplicated, or
tampered evidence exits 1 with structured issues. Without --strict-closure, existing
penflow run behavior is unchanged.
For dense multi-screen mockups, prefer penflow check-consistency expected-ui-tree.json --group-by role+intent+variant+size-class --out consistency-report.json --json. The size class buckets node height so compact headers are not compared with full-height panels; anonymous containers without stable intent, variant, or human name are skipped, and repeated divergences are aggregated per group.
Read docs/theme-contract.md before generating theme.css for draft-pen-from-tree, lint-tokens, or Brander-style handoff. The stable contract accepts canonical and --pf-* aliases for core colors, panel/panel-2, success/warning/info, font-sans/font-mono, font-size-xs through font-size-xl, plus dynamic palette-* color slots and radius-* numeric slots. lint-tokens records the effective spacing_scale, radius_scale, and fontsize_scale in the strict report inputs; theme-declared font sizes and radii extend defaults unless the matching CLI override is passed.
Run penflow pencil-sync <ui.pen> [--expect-min-nodes N] [--expect-screens N] [--app <Pen.app>] [--save-timeout SECONDS] --json after any pen.dev editor or MCP execute mutation, with ui.pen open in pen.dev (open ui.pen in pen.dev and confirm it with get_app_state before any MCP call). It resolves exactly one pen.dev bundle (--app, then PENFLOW_PEN_APP, then the single Pen.app in /Applications or ~/Applications) and saves through the one running process of that executable, never a legacy Pencil.app sharing the bundle id dev.pencil.desktop; ambiguity fails before any script runs. It is only an operator trigger: it reports pre/post SHA-256 plus inspection, always declares persistence_status: UNVERIFIED and closes_persistence: false, and never uses mtime, pane text, or trigger success to close persistence.
pen.dev 1.2.16's MCP cannot save: execute exposes only Insert, Copy, Update, Replace, Move, Delete, SetVariables, GetVariables, Get, Print, TakeScreenshot, Export, Generate and FindEmptySpace, execute with filePath requires the file open in the editor, and no mutation reaches disk without a save. pencil-sync is the save step: after the request it polls until the digest changes and the bytes parse, up to --save-timeout (default 5 s), because pen.dev writes after the request returns.
Install the dedicated save helper once with penflow doctor --install-save-helper --json (writes the versioned, background-only ~/Applications/Penflow Save.app; idempotent; --save-helper-dir installs elsewhere and PENFLOW_SAVE_HELPER points pencil-sync at it). Then add Penflow Save.app once in System Settings > Privacy & Security > Accessibility and allow its Automation prompt for System Events. The grant belongs to the helper, so it survives Claude, Codex or terminal updates; without the helper, pencil-sync falls back to direct osascript and the calling app needs Accessibility.
The operator must preserve distinct pre/post snapshot files and write a penflow-pencil-persistence-observation containing the exact run_id, document path, requested operation, trigger provider/status, snapshot paths, and claimed SHA-256 values. Then run penflow verify-pencil-persistence observation.json --document ui.pen --run-id <exact-run> --operation <exact-operation> --out persistence-receipt.json --json followed by penflow validate-report persistence-receipt.json --schema --json. The verifier independently re-reads observation, pre/post/current bytes; only a PASS receipt with closes_persistence: true closes the gate. No-op/same-digest, trigger-only, stale, replayed, tampered, missing, or mismatched evidence exits nonzero.
Penflow reasons about three distinct UI tree contracts. Each has a different producer and a different lifecycle phase. Penflow itself never executes a runtime — it validates and compares trees that other producers hand to it.
| Tree | Lifecycle | Producer | Consumer | Contract |
|---|---|---|---|---|
semantic-ui-tree.json |
BEFORE-CODE (design intent) | Penflow, exported from strict contract.json v2 (Markdown is docs/migration only) |
draft-pen-from-tree, enrich-pen, detect-drift |
semantic schema |
expected-ui-tree.json |
DESIGN-DERIVED (post-design) | Penflow, derived from .pen + Pencil context (feature 006) |
compare-tree |
pre-existing |
actual-ui-tree.json |
AFTER-CODE (runtime) | External runtime adapter (SwiftUI / React / Rust / …, feature 038) | validate-actual, compare-tree |
C12 |
Pass --schema to penflow validate-actual to additionally run the JSON Schema (draft 2020-12) check on top of the structural validator.
Agents normally should not hand-write semantic-ui-tree.json; they should author or migrate flow-ui-contract/contract.json and run penflow export-semantic-tree. The legacy formation rules are documented in Read .agent-sync/skills/flow-ui-contract/references/semantic-ui-tree.md: flow specs become flows[], screen specs become screens[], visible hierarchy becomes recursive semantic nodes, and business context becomes intent, binding, entity, states, validations, side_effects, test_id, and source_map.
Adapter conformance docs live under docs/adapters/. The C13 guides tell external producers in three target stacks — Web DOM, SwiftUI + XCTest, and React Native — how to emit an actual-ui-tree.json (contract C12) that penflow validate-actual --schema accepts on the first try. Causal transition evidence defines the C44 pre/action/post, identity, receipt, and independent-outcome packet accepted by validate-causal-evidence. Penflow ships zero runtime code for these stacks — it never launches a browser, simulator, or application runtime.
Run penflow demo --out examples/full to regenerate the contract C14/C20 fixture. Use examples/full/ to inspect the complete chain: contract.json, documentation flow specs, semantic tree, enriched Pencil file, expected tree, code IR, actual-tree fixture, compare report, review report, and fix report.
To prove the default Stage C1 bootstrap from an empty directory:
fresh=$(mktemp -d)
penflow demo --out "$fresh" --json
penflow run --target "$fresh" --jsonpenflow demo derives actual-ui-tree.json deterministically from its generated expected tree and installs package-owned synthetic example data labeled static-demo-fixture. This immutable packet is not a fresh runtime, renderer, or model verdict. The command does not launch or copy from a browser, simulator, runtime adapter, test helper, or examples/full; penflow run fails closed if the generated actual tree or load-bearing visual evidence is removed or corrupted.
Read docs/project-integration.md when adding Penflow to an existing app repository. It explains the recommended penflow/ workspace layout, forward adoption, Pencil enrichment, legacy reverse migration, implementation handoff through code-ir.json, runtime actual-ui-tree.json verification, and a minimal CI gate.
Read .agent-sync/skills/mockup-factory/SKILL.md when generating, auditing, or optimizing Pencil mockups. The Penflow-local Mockup Factory uses flow-ui-contract, semantic-ui-tree.json, .pen, pencil-context-map.json, expected-ui-tree.json, and detect-drift reports as deterministic anchors. Since feature 052 the audit rubric is prescriptive: every category maps to a scripted gate (lint-tokens, check-consistency, audit-visual, detect-drift, compare-tree, validate-visual-evidence), Critical is exactly any error-severity gate issue or blocking: true item, and fixes run in a bounded fix-re-audit loop (max 3 iterations, full chain rerun, no PASS without a final clean run; residual criticals escalate). The design audit verdict is the strict audit-report.json (kind: penflow-mockup-audit, with iterations, per-gate {gate, command, exit_code, report_path, result}, and residual_issues[]) — penflow validate-report audit-report.json --schema checks that audit. C51 run-report.json production and fresh revalidation close the complete design workflow afterward.
Since feature 068, Read .agent-sync/skills/flow-ui-contract/references/design-read.md for the landing/showcase <target>/design-read.json artifact: a design-read-protocol one-liner plus integer variance, motion, and density dials. Mockup Factory reads that artifact when present; Read .agent-sync/skills/mockup-factory/references/design-taste-advisory.md for the .mockup-validation/design-taste-advisory.md anti-default and AI-tell review report. The report is advisory only: it never changes gate status, audit-report.json status, schemas, or exit-code policy.
Since feature 055 the contract-gate checklist is machine-seeded: penflow checklist-from-drift drift-report.json --out checklist.json [--group <name>] --json maps every drift issue to one anchored open gate: contract item (id drift-<code>-<NNN> in _issue_sort_key order, copied severity/blocking, screen_id derived from semantic_id/path with a _workspace fallback so nothing is dropped). The command is deterministic (byte-identical reruns, no timestamps, top-level sourceReport traceability) and fail-closed (malformed JSON, wrong kind, or a schema-invalid report exits 1 without creating or modifying --out); items for the other five gates are appended at the skill layer after seeding.
Before final PASS, Mockup Factory also runs the Visual Evidence Gate. Read .agent-sync/skills/mockup-factory/references/visual-validation.md before applying it. When Pencil rendering is available, the gate must export PNGs, show every exported screen as a Markdown image with its absolute path, run visual-preview url <absolute-path> when available for browser annotation, dump a layout snapshot to layout-snapshot.json, run audit-visual with --layout-snapshot so overlap/overflow uses real Pencil bboxes, run a vision pass answering ONLY the five locked categories (visual_balance, rendering_artifacts, readability, perceived_hierarchy, imagery_quality), and write .mockup-validation/visual-evidence/manifest.json plus vision-report.json (kind: penflow-vision-report; legacy visual-report.json is deprecated). The verdict is penflow validate-visual-evidence <dir> --expected expected-ui-tree.json exiting 0 — the gate is the exit code, not prose; preview URLs are annotation-only and not canonical proof. The schema rejects deterministic categories (contrast, touch targets, overflow belong to audit-visual), so vision cannot hand-wave computable facts. Since feature 053, multi-screen sets also require a cross-mockup coherence pass: export all audited screens at a uniform scale, capture one contact-sheet.png, expose it through the same Markdown image + visual-preview url channels, and answer ONLY the four coherence categories (cross_screen_rhythm, palette_coherence, typography_coherence, component_recognition) in the vision report's coherence section — required when the expected tree has ≥ 2 screens (evidence.coherence.missing), with a non-empty contact-sheet PNG on disk; the numeric cross-screen anchor stays the feature 047 check-consistency report. If visual-preview is unavailable, operators keep the Markdown image and record browser preview unavailable without changing the deterministic evidence packet by itself. Without a renderer the status is BLOCKED_VISUAL_NOT_RUN or escalated rather than silently passing structural-only validation.
When a workspace has references/references-manifest.json, each reference entry is {screen_id, file, sha256, source, mapping: {surface, screen_id, region_id, state, crop}} and files resolve relative to references/. The evidence manifest declares that file via references_manifest; vision-report.json must add reference_fidelity entries for every referenced screen with the locked categories layout_match, palette_match, component_parity, and density_match, citing both reference and export files. The additional gate is penflow validate-reference-fidelity .mockup-validation/visual-evidence --references references --json: copied exports fail as reference.evidence_is_reference_copy, and strong deterministic-vs-LLM disagreement fails as reference.llm_metric_disagreement.
Since feature 070 (contract C45), manifest.json and vision-report.json must share one exact runId; every required screen, coherence, and reference criterion is blocking: true, has result: pass, and carries structured proof for surface, screen, hierarchical region, state, assertion, measurement, and hashed artifacts with in-bounds crops. Reference entries add an explicit homologous mapping. Narrative-only evidence, warning/SKIP status, invalid or stretched crops, reference bytes copied as actual, region drift, and one actual/reference SHA reused across non-homologous mappings all exit nonzero.
Since feature 056 (contract C31), evidence packets are bindable to the exact theme bytes they were rendered against. penflow theme-fingerprint theme.css --json emits {path, sha256} — the SHA-256 of the file's raw bytes (no normalization, reproducible via shasum -a 256; missing/unreadable file exits 2). The skill layer writes that block plus an opaque runId (ISO-8601 timestamp + short slug recommended) into manifest.json — both fields are optional and backward compatible. Arming the gate with penflow validate-visual-evidence <dir> --theme-css theme.css requires the manifest stamp to match the current file: a missing stamp emits evidence.theme.missing, a mismatching one emits evidence.theme.stale (both error severity, exit 1), so any theme edit deterministically invalidates stale evidence and forces a re-run of the bounded loop; a --theme-css path missing on disk is a boundary error (exit 2). Without the flag, behavior and output are byte-identical to before — stamps stay inert.
Install the package first so the penflow console script is available.
Start from flow-ui-contract/contract.json, create a semantic tree, draft a theme-aware Pencil file, validate it, then produce design/code handoff artifacts.
penflow validate-flow-contract examples/full/flow-ui-contract/contract.json --json
penflow export-semantic-tree examples/full/flow-ui-contract/contract.json --out semantic-ui-tree.json --json
penflow validate-semantic-tree semantic-ui-tree.json --json
penflow draft-pen-from-tree semantic-ui-tree.json --out ui.pen --theme-css theme.css --json
penflow validate-pen ui.pen --json
penflow export-expected ui.pen --out expected-ui-tree.json --json
penflow code-ir --from-context ui.pen --semantic-tree semantic-ui-tree.json --contract examples/full/flow-ui-contract/contract.json --flow-id checkout --flow-name "Checkout flow" --out code-ir.json --json
penflow validate-code-ir code-ir.json --jsondraft-pen-from-tree is a structural draft stage. If its JSON output is saved as draft-report.json, the report includes fidelity: "wireframe": draft = structure / wireframe, PAS un mockup final ; le hi-fi passe par mockup-factory.
Legacy Markdown-only docs may still mention penflow export-semantic-tree examples/full/flow-ui-contract --out semantic-ui-tree.json --json; strict v2 uses contract.json.
Start from an existing visual .pen, inject semantic context, preserve layout, validate, and generate expected/code IR artifacts.
consumer=$(mktemp -d)
trap 'rm -rf -- "$consumer"' EXIT
git -C "$consumer" init
cp -R examples/full/flow-ui-contract "$consumer/flow-ui-contract"
cp examples/full/ui.pen "$consumer/ui.pen"
penflow doctor --project "$consumer" --check --json > "$consumer/.penflow-doctor-start.json" || test "$?" -eq 1
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); a={"pen.missing","gitignore.managed_block_missing","gitignore.managed_block_outdated","gitignore.managed_block_duplicate"}; c={i["code"] for i in d["stable_result"]["findings"]}; raise SystemExit(0 if c and c <= a else 1)' "$consumer/.penflow-doctor-start.json"
penflow doctor --project "$consumer" --fix --json
penflow doctor --project "$consumer" --check --json
penflow export-semantic-tree "$consumer/flow-ui-contract/contract.json" --project "$consumer" --out "$consumer/semantic-ui-tree.json" --json
penflow validate-semantic-tree "$consumer/semantic-ui-tree.json" --project "$consumer" --json
penflow enrich-pen "$consumer/ui.pen" "$consumer/semantic-ui-tree.json" --project "$consumer" --out "$consumer/ui.pen" --json
penflow map-pencil-context "$consumer/ui.pen" "$consumer/semantic-ui-tree.json" --project "$consumer" --out "$consumer/pencil-context-map.json" --strict-anchors --json
penflow validate-pen "$consumer/ui.pen" --project "$consumer" --json
penflow export-expected "$consumer/ui.pen" --project "$consumer" --out "$consumer/expected-ui-tree.json" --json
penflow code-ir --from-context "$consumer/ui.pen" --semantic-tree "$consumer/semantic-ui-tree.json" --context-map "$consumer/pencil-context-map.json" --contract "$consumer/flow-ui-contract/contract.json" --flow-id checkout --flow-name "Checkout flow" --project "$consumer" --out "$consumer/code-ir.json" --json
penflow doctor --project "$consumer" --check --jsonStart from legacy Pencil, recover flow/screen specs and a semantic tree, then validate the recovered contract.
consumer=$(mktemp -d)
trap 'rm -rf -- "$consumer"' EXIT
git -C "$consumer" init
cp examples/full/ui.pen "$consumer/ui.pen"
penflow doctor --project "$consumer" --check --json > "$consumer/.penflow-doctor-start.json" || test "$?" -eq 1
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); a={"pen.missing","gitignore.managed_block_missing","gitignore.managed_block_outdated","gitignore.managed_block_duplicate"}; c={i["code"] for i in d["stable_result"]["findings"]}; raise SystemExit(0 if c and c <= a else 1)' "$consumer/.penflow-doctor-start.json"
penflow doctor --project "$consumer" --fix --json
penflow doctor --project "$consumer" --check --json
penflow reverse-pen "$consumer/ui.pen" --project "$consumer" --out "$consumer/reverse-output" --lenient --json
penflow validate-semantic-tree "$consumer/reverse-output/semantic-ui-tree.json" --project "$consumer" --json
penflow validate-pen "$consumer/ui.pen" --project "$consumer" --json
penflow doctor --project "$consumer" --check --jsonreverse-pen --out <dir> writes a directory containing semantic-ui-tree.json and flow-ui-contract/; validate <dir>/semantic-ui-tree.json, not the directory path itself.
Reverse output marks missing business logic as [NEEDS CLARIFICATION]; do not invent unknown validations, side effects, or state transitions.
Strict default: reverse-pen exits 1 while clarification markers remain (reverse.clarification_needed); pass --lenient to keep scripted chains green, then resolve the markers.
After implementation, an external runtime adapter writes actual-ui-tree.json. Penflow validates and compares that file; it does not launch or inspect the running app.
compare-tree checks presence, role, text, hierarchy, order, bbox (±bbox_px), colors, radius (±radius_px), typography (±font_px), and — feature 048 — layout properties: layout and alignItems exact-match (node.visual.layout, node.visual.alignItems), gap and padding within ±spacing_px (default 2; node.visual.gap, node.visual.padding). Padding forms are normalized (16 ≡ [16, 16] ≡ [16, 16, 16, 16]). Tolerances are overridable per node via context.tolerance (e.g. tolerance: {spacing_px: 4}); absent keys are never compared, so pre-048 trees stay green.
penflow validate-actual actual-ui-tree.json --schema --json
penflow validate-actual-provenance <workspace> --expected expected-ui-tree.json --surface web --json
penflow compare-tree expected-ui-tree.json actual-ui-tree.json --out compare-report.json --json
penflow review-report compare-report.json --out review-report.json --json
penflow fix-report compare-report.json --out fix-report.json --json
penflow validate-report compare-report.json --schema --jsonSince P0-2 (contract C2), the adapter also writes a content-bound sidecar actual-ui-tree.provenance.json (kind: penflow-actual-provenance) next to the actual tree, and validate-actual-provenance is the final penflow run gate. The receipt records sourceKind (runtime-capture for a fresh capture/export, static-demo-fixture for explicitly qualified frozen example data), producer, capturedAt (ISO-8601 UTC), target.surface, and content.sha256 (SHA-256 of the actual-tree bytes). Penflow never captures a runtime — it only validates this receipt from local files and rejects, fail-closed, a missing/incomplete receipt (provenance.receipt.missing/.invalid), a forged digest (provenance.digest.mismatch), a content.path naming another file (provenance.content.path_mismatch), a stale/future/unparseable runtime-capture timestamp (provenance.timestamp.stale/.future/.invalid; fixtures are exempt), an inadmissible source kind under --require-fresh (provenance.source_kind.inadmissible), an incompatible --surface (provenance.target.incompatible), and a runtime-capture whose root is byte-identical to the design-derived expected tree (provenance.actual.derived_from_expected). A qualified static-demo-fixture may reuse expected bytes but can never masquerade as fresh. The penflow run gate arms the runtime-capture freshness policy against the current run's export boundary with a documented default window of 86400s (24h), overridable via penflow run --actual-max-age-seconds N; a static-demo-fixture is exempt and that exemption is unreachable with sourceKind=runtime-capture. Any explicit max-age (--actual-max-age-seconds, --max-age-seconds, or programmatic) must be finite and strictly positive — nan, inf, -inf, overflowing 1e999, 0, and negatives are rejected fail-closed with a structured error and non-zero exit before any report is written (allow_nan=False guarantees no NaN/Infinity JSON), and an unparseable internal run boundary fails explicitly. content.path must be a relative name without .. traversal (provenance.content.path_invalid); the content.sha256 digest is the authority over the bytes.
Run the contract C16 regression proof with pytest, then reproduce the final CLI chain on the stable fixtures.
set -euo pipefail
python3 -m pytest -q
consumer=$(mktemp -d)
trap 'rm -rf -- "$consumer"' EXIT
git -C "$consumer" init
cp -R examples/full/flow-ui-contract "$consumer/flow-ui-contract"
cp examples/full/actual-ui-tree.json "$consumer/actual-ui-tree.json"
cp examples/demo/theme.css "$consumer/theme.css"
penflow doctor --project "$consumer" --check --json > "$consumer/.penflow-doctor-start.json" || test "$?" -eq 1
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); a={"pen.missing","gitignore.managed_block_missing","gitignore.managed_block_outdated","gitignore.managed_block_duplicate"}; c={i["code"] for i in d["stable_result"]["findings"]}; raise SystemExit(0 if c and c <= a else 1)' "$consumer/.penflow-doctor-start.json"
penflow doctor --project "$consumer" --fix --json
penflow doctor --project "$consumer" --check --json
penflow validate-flow-contract "$consumer/flow-ui-contract/contract.json" --project "$consumer" --json
penflow export-semantic-tree "$consumer/flow-ui-contract/contract.json" --project "$consumer" --out "$consumer/semantic-ui-tree.json" --json
penflow validate-semantic-tree "$consumer/semantic-ui-tree.json" --project "$consumer" --json
penflow draft-pen-from-tree "$consumer/semantic-ui-tree.json" --project "$consumer" --out "$consumer/penflow/ui.pen" --theme-css "$consumer/theme.css" --json
penflow validate-pen "$consumer/penflow/ui.pen" --project "$consumer" --json
penflow export-expected "$consumer/penflow/ui.pen" --project "$consumer" -o "$consumer/expected-ui-tree.json" --json
penflow enrich-pen "$consumer/penflow/ui.pen" "$consumer/semantic-ui-tree.json" --project "$consumer" --out "$consumer/penflow/ui.pen" --json
penflow validate-pen "$consumer/penflow/ui.pen" --project "$consumer" --json
penflow reverse-pen "$consumer/penflow/ui.pen" --project "$consumer" --out "$consumer/reverse-output" --lenient --json
penflow validate-semantic-tree "$consumer/reverse-output/semantic-ui-tree.json" --project "$consumer" --json
penflow validate-actual "$consumer/actual-ui-tree.json" --schema --project "$consumer" --json
penflow compare-tree "$consumer/expected-ui-tree.json" "$consumer/actual-ui-tree.json" --project "$consumer" --out "$consumer/compare.json" --json
penflow review-report "$consumer/compare.json" --project "$consumer" --out "$consumer/review.json" --json
penflow fix-report "$consumer/compare.json" --project "$consumer" --out "$consumer/fix.json" --json
penflow doctor --project "$consumer" --check --json$consumer/reverse-output is a reverse output directory; its semantic tree lives at $consumer/reverse-output/semantic-ui-tree.json and recovered flow specs live under $consumer/reverse-output/flow-ui-contract/. Penflow-owned working snapshots and run payloads stay below $consumer/.penflow-tmp/ and are reconciled by preflight/postflight.
penflow demo --out examples/full --json
penflow validate-semantic-tree examples/full/semantic-ui-tree.json --json
penflow validate-pen examples/full/ui.pen --json
penflow validate-actual examples/full/actual-ui-tree.json --schema --json
penflow compare-tree examples/full/expected-ui-tree.json examples/full/actual-ui-tree.json --out /tmp/penflow-compare.json --json.penstays JSON-first and runtime-agnostic.contextcarries semantic design intent:semantic_id,role,intent,binding,repeat,entity,variant,test_id,tolerance.- Pencil
contextis written askey=valuepairs separated by;(semantic_id=cockpit-live.git; role=container). Attach it duringexecuteInsert(legacybatch_design) insertion for screen frames, sections, interactive components, and lists. Runmap-pencil-context --strict-anchorsbefore downstream.penreaders; use--create-placeholdersonly as explicit repair scaffolding because it reportsPARTIAL. - Semantic anti-gaming is fail-closed:
validate-semantic-treerejects repeated list-like containers withoutrepeat, andvalidate-flow-contract --jsonreports per-screeninteractive_coveragewhile rejecting unbound interactive nodes. - Runtime frameworks provide actual UI trees; Penflow validates and compares those trees.
- The primary validation is structural and semantic, not pixel-perfect.
- Demo
.penfiles use Pencil document schema2.11with context shorthand strings so they can be opened in Pencil and parsed by Penflow.
Read .agent-sync/skills/penflow/SKILL.md to use the project-local skill interface. It is stored under .agent-sync/skills/ so cc-hub/agent-sync can symlink it when requested.
Read .agent-sync/skills/flow-ui-contract/SKILL.md to capture flow/screen behavior around Pencil artifacts. Invoke the installed skill as flow-ui-contract; legacy wording such as specify flows should route there, but no separate specify-flows slash command is installed. It writes validated flow-ui-contract/flows/*.md and flow-ui-contract/screens/*.md files, and reports fresh / stale / unspecified coverage with python3 .agent-sync/skills/flow-ui-contract/validator.py --status --target <target>.
Read .agent-sync/skills/mockup-factory/SKILL.md to run the tree-aware guided UX/UI optimizer for Pencil mockups. The skill can orchestrate external Pencil/audit tooling, but Penflow Python core stays runtime-agnostic.
The pre-Pencil semantic contract is defined in penflow/schemas/semantic-ui-tree.schema.json, with a deterministic example at examples/demo/semantic-ui-tree.json. Export it from flow-ui-contract/contract.json with penflow export-semantic-tree path/to/flow-ui-contract/contract.json --out semantic-ui-tree.json, validate it with penflow validate-semantic-tree semantic-ui-tree.json --json, then create a theme-aware Pencil draft with penflow draft-pen-from-tree semantic-ui-tree.json --out ui.pen --theme-css theme.css --json, enrich the canonical Pencil file in place with penflow enrich-pen ui.pen semantic-ui-tree.json --out ui.pen --json, emit an explicit mapping artifact with penflow map-pencil-context ui.pen semantic-ui-tree.json --out pencil-context-map.json --strict-anchors --json, reverse ui.pen with penflow reverse-pen ui.pen --out reverse-output --json, or report drift with penflow detect-drift path/to/flow-ui-contract/contract.json semantic-ui-tree.json ui.pen --out drift-report.json --json. validate-flow-contract --json includes interactive_coverage, and static screens without interactive actions must declare static: true plus static_reason. This schema is distinct from post-implementation actual UI trees.
detect-drift selects canonical C20 when its first argument is contract.json or a directory containing it. It checks the complete stored semantic projection and exhaustive C20/Pencil coverage before canonical drift. Relative and absolute provenance paths may differ only when they resolve to the same selected contract; JSON pointers and all semantic fields remain exact. Invalid C20, changed projections and mismatched Pencil context block even with --lenient. Directories without C20 keep legacy Markdown behavior. This preparation diagnostic does not require or certify a C51 report; after moving a workspace, use the existing export/enrichment steps to refresh stale generated artifacts.
Contributor checks are listed in CONTRIBUTING.md. The Python pre-commit hook in .githooks/pre-commit runs ruff and mypy (enable it with git config core.hooksPath .githooks), and GitHub Actions runs .github/workflows/ci.yml and .github/workflows/security.yml.