Paula Patterns — Feasibility & Whack-a-Field Review: WP/Op Schema Model

Scope reviewed: wp-op-schema-model.md idea note against the code as-is on clean main. Read-only.

Headline: The idea diagnoses a real pain (whole-file hash churn) but mis-locates the fix. Two of the three things it proposes to build already exist in this codebase. The residual problem is a narrow boundary eviction, not a new store. Framed as written, the maximal version would create a fourth parallel WP authority — the exact leak this lens exists to catch.

1. Authoritative-vs-derived split — partially realistic, with one hard trap

The idea treats "structured authoritative, markdown derived (one-way)" as a novel bet. It is already the shipped pattern for tasks.md:

  • core/wps_manifest.py:170 generate_tasks_md_from_manifest() renders tasks.md from the wps.yaml Pydantic manifest, stamped "_Generated by finalize-tasks from wps.yaml. Do not edit directly._" (wps_manifest.py:184). So wps.yaml (structured) → tasks.md (derived, one-way) is proven and live.

The trap is conflating two different markdown surfaces:

Surface Authoring reality One-way-derivable?
tasks.md (index/rollup) machine-generated already ✅ yes — already is
tasks/WP##.md body (the prompt) the prompt body IS the agent's instructions; human- and agent-edited no

The WP body is not decoration — it is load-bearing content consumed as prose:

  • Dashboard renders the body verbatim as prompt_markdown (dashboard/scanner.py:976), and pulls the card title from a body regex ^#\s+Work Package Prompt: in preference to frontmatter title (scanner.py:895-901).
  • Subtask progress is counted from - [ ] T### checkboxes in the body or tasks.md, not frontmatter (scanner.py:950-961).
  • Agents read the WP body as their work order.

Verdict on Q1: "structured authoritative → derived index markdown" is realistic and already done for tasks.md. "Derived one-way WP body" is not realistic — the body is authored, not projected. The metadata half can be authoritative-structured; the prose half must stay round-trippable. Any spec that says "markdown becomes a derived view" without splitting index from prompt body is undercut on contact.

2. Whack-a-field census — the model the idea asks for is ~70% already built

The note's premise ("no single model, scattered defensive parsing") is stale. There are today three structured WP representations plus the body, and the read side is already largely consolidated:

Structured authorities that already exist:

  1. WPMetadata (status/wp_metadata.py:183) — frozen Pydantic v2, extra="forbid", ~40 typed fields covering identity, deps, branch contract, execution context, workflow, review, descriptive. This is the "code-owned logical model" the idea proposes to create.
  2. WorkPackageEntry / WpsManifest (core/wps_manifest.py:15,61) — Pydantic planning manifest, the authoritative source for tasks.md.
  3. FrontmatterManager.WP_FIELD_ORDER (frontmatter.py:49) — the write-path field list + validator.

Read-path is already unified: 23 modules consume the typed read_wp_frontmatterWPMetadata (dashboard, dependency_graph, requirement_mapping, ownership, merge, workflow_executor…). Only 9 use the raw-dict path, and those are non-WP or migration-only: task_metadata_validation.py (explicitly MIGRATION-ONLY, task_metadata_validation.py:82,181), the generic template/* renderers, doc_analysis/gap_analysis.py. So "scattered defensive parsing" of WP structure is mostly already retired.

Where the naive model would leak (my core competency): The real duplication is three independent field-name lists for one object: WP_FIELD_ORDER (frontmatter.py:49), WorkPackageEntry fields (wps_manifest.py:16-26), and WPMetadata fields (wp_metadata.py:197-282). They already drift — the codebase carries a comment tax keeping shell_pid_created_at in lockstep by hand across frontmatter.py:26 and wp_metadata.py:252-257 because Pydantic attr names can't import the shared constant. Dropping a new "single source of truth" WPRecord next to these three would make it four — a layer on top, not a consolidation. That is the whack-a-field failure mode, and the idea as written walks straight into it.

Verdict on Q2: a single model does NOT need inventing; it needs electing. Pick WPMetadata (already the read authority, already extra="forbid") as the canonical shape and make WP_FIELD_ORDER and WorkPackageEntry derive from it, or the whole exercise adds a layer instead of removing one.

3. Bookkeeping vs semantic boundary — the store already exists; the fix is eviction, not invention

The idea asks (Q3) whether the append-only event log already owns the mutable half. It does, and the migration is already ~90% done:

  • frontmatter.py:47-48,284-285 explicitly states mutable status fields (lane, review_status, reviewed_by, review_feedback) are "managed exclusively via the canonical event log and are NOT written here."
  • The only code that reads/writes lane in frontmatter is stamped MIGRATION-ONLY (task_metadata_validation.py:82,181).
  • status.events.jsonl is the sole lane authority (per CLAUDE.md status-model section; status/store.py, status/emit.py).

So the correct boundary is already drawn for lane/review. What still lives in the WP file and churns the hash is the residual runtime/bookkeeping tail the earlier migration didn't finish evicting:

  • shell_pid, shell_pid_created_at, base_branch, base_commit, planning_base_branch, created_at, history (frontmatter.py:60-68), written at claim/implement time via write_shell_pid_claim_to_file (frontmatter.py:393) and add_history_entry (frontmatter.py:176).
  • Subtask completion checkboxes flipping in the body/tasks.md.

Why that churns the hash: dossier/hasher.py:14 hash_file() and sync/body_upload.py:116 compute a whole-file SHA-256 over WP files matching ^tasks/WP\d+.*\.md$ (body_upload.py:52). Any byte change — a shell_pid claim write, a history append, a checkbox flip — moves the whole-file hash → dossier "parity drift" review noise (.contextive/dossier.yml:37) + sync re-upload + the content_hash_mismatch guard (body_upload.py:121).

Verdict on Q3: the idea's "invent a new structured store for bookkeeping" is the wrong fix — the store (event log) already exists. The right boundary is: finish the eviction that lane/review started — move the residual mutable/runtime fields out of the hashed WP file, and/or exclude them from the content hash. No new authority required.

4. Smallest de-risking slice + ordering

Maximal version (as the note frames it) — over-engineered: "rewrite WP + Op + markdown pipeline, new authoritative store, markdown derived." This (a) builds a model that ~70% exists, (b) risks a 4th field authority, (c) breaks WP-body authoring, and (d) collides head-on with in-flight work (see risks). Do not scope this.

Recommended minimal first slice (de-risks the actual pain, ~1 mission):

Evict the residual mutable/runtime fields from the WP file's hashed surface, reusing the existing stores. Continue the lane/review precedent: move shell_pid/shell_pid_created_at/base_*/created_at/history into the event log or a status sidecar, and stop whole-file-hashing the prompt — hash the semantic frontmatter + body only. One migration. No new model. No markdown-derived rewrite.

This alone kills the hash-churn (the one concrete, recurring pain) and touches nothing that authors read.

Tidy-first ordering (enabler sequence):

  1. Tidy (pure debt, no behaviour): unify the three field-name lists — make WP_FIELD_ORDER and WorkPackageEntry derive from WPMetadata (or a shared field registry). Kills the existing hand-maintained drift tax before anything is added. This is the true enabler; skipping it guarantees the 4th-authority leak.
  2. Boundary fix (the slice above): evict residual mutable fields → event log/sidecar; narrow the content hash. Delivers the hash-churn win.
  3. Only if still wanted, later: promote wps.yaml to authoritative semantic store and generate the WP body scaffold from it — extending the tasks.md generation that already works (wps_manifest.py:170) — while keeping the body round-trippable for human/agent edits.
  4. Op record (decouple entirely): OpStartedEvent is already Pydantic (invocation/record.py:39). Adding optional structured intent/scope fields is a small, independent change — do NOT bundle it with WP work; coupling them doubles the blast radius for no shared code.

Verdict: Feasible-with-caveats — but the idea over-scopes; the real fix is small

The pain is real and the direction is sound, but the framing ("build a single Pydantic model, make markdown derived") describes work that is 60–70% already shipped (WPMetadata, wps.yamltasks.md, event-log ownership of lane/review). The genuine residual is a narrow boundary eviction + field-list unification, not a new store or a pipeline rewrite. Scope it that way and it's a clean, low-risk mission; scope it as written and it manufactures a new parallel authority.

Top 3 risks:

  1. New parallel authority (the whack-a-field leak). Adding a WPRecord "single source of truth" next to the existing WPMetadata + WorkPackageEntry + WP_FIELD_ORDER makes four field lists, not one. Mandatory precondition: unify the existing three (frontmatter.py:49, wps_manifest.py:16, wp_metadata.py:197) first, electing WPMetadata as canonical.
  2. Markdown round-trip breakage. The WP body is authored instructions consumed as prose (scanner.py:895,976; agents read it as their work order). "One-way derived markdown" is safe for the tasks.md index (already done) but breaks the prompt body. A spec that doesn't split index from body is undercut.
  3. In-flight collision / sequencing. The exact surfaces this touches are under active change: coord-authority status work (#2160) also writes the shell_pid claim into frontmatter (frontmatter.py:393), plus infra-logic separation (#2173) and mission-type→doctrine. Evicting frontmatter fields races writers being restructured elsewhere — land the tidy-first field unification before those diverge further.