WP & Op Schema Model
| Field | Value |
|---|---|
| Date | 2026-07-16 |
| Stage | Idea — pending research grounding |
| Author | Operator (via Spec Kitty session SK_DESIGN) |
| Branch | design/wp-op-schema-model (isolated worktree) |
| Grounds against | model-first doctrine schema precedent, degodding roadmap, mission-type→doctrine migration, Op record schema v2 |
| Related | model-first-schema-generation.md, src/specify_cli/invocation/record.py, src/specify_cli/core/wps_manifest.py, src/specify_cli/task_metadata_validation.py |
This is an idea note, not a decision. It captures intent verbatim from the originating session and frames the research questions. It is deliberately unresolved on trade-offs; the accompanying research squad grounds it against the code as-is, the roadmap/vision, and the existing ADRs before any spec/plan work is contemplated.
The itch
Recent and in-flight roadmap movement — degodding (god-module decomposition),
specify_cli cleanup, and moving mission-type configuration into doctrine —
has surfaced that the current representation of Work-Package (WP) files is
lacklustre. Today a WP is a markdown file with a YAML frontmatter block
(kitty-specs/<mission>/tasks/WP##.md): structured metadata up top, free-prose
body below.
Observed pain points with that format:
- Hash / content-address mismatch churn. Minor, semantically-inert edits (e.g. flipping a task to complete) shift content and trip hashcode-mismatch warnings/errors. The content-address machinery treats the whole file as one opaque blob, so bookkeeping mutations are indistinguishable from substantive ones.
- Context sizing / retrieval friction. The prose-heavy body is expensive to load and hard to slice; there is no clean way to pull "just the scope" or "just the acceptance criteria" without parsing free text.
- Inconsistencies. Free-form bodies drift in structure across missions; there is no enforced shape, so downstream tooling parses defensively.
- Duplicated information. The same facts live in both the frontmatter metadata and the body prose (title, anchors, requirement refs, scope headings), and the two can diverge.
The idea
With WP prompt templates shifting into doctrine elements (the mission-type→doctrine migration), this is an opportune moment to revamp the WP representation into a formal schema format:
- A code-owned logical model (Pydantic, matching the existing model-first doctrine-schema pattern) is the single source of truth for a WP's shape.
- A schema + schema checks validate WP records — no hand-authored, drift-prone structure.
- These models live inside the code logic and are used to create, update, and process WP records (not re-parsed ad hoc at each call site).
- On disk, records still render to markdown for human readability and dashboard display — but the authoritative representation is structured (YAML / model), and the markdown is a derived view, not the source.
The distinction that unblocks the hash-churn problem: separate semantic content (scope, requirements, acceptance) from mutable bookkeeping (lane, task-complete flags, history) so a status flip does not perturb the content-addressed identity of the semantic record.
Extension: Op records should carry "why & what", not just "something was done"
The same logical-model + schema approach applies to the Op run-type (ad-hoc
dispatch / profile-invocation). Today an Op record
(src/specify_cli/invocation/record.py, schema v2) captures the facts of an
invocation — request_text, action, mode_of_work, outcome, closed_by
— i.e. that something was dispatched and how it closed. It does not
persist a structured account of the why and what — the scope the Op set for
itself, the intent, the change surface, the reasoning. It stores that something
was done, not what was undertaken and to what end.
This is incongruent with Spec Kitty's core vision (specifications ahead of
implementation; traceability from intent to artefact) and it blurs the line
between ad-hoc and op: if an Op leaves no more trace of intent than a raw
ad-hoc action, the governance overhead buys little.
Applying the same schema model to Op scope would let an Op carry a lightweight, structured intent + scope + outcome record — a "small WP for a non-mission action" — restoring the traceability that distinguishes a governed Op from an ungoverned ad-hoc edit.
Why now (timing)
- WP templates → doctrine. The prompt templates that shape WP files are being lifted into doctrine artefacts; changing the representation at the same moment avoids a second disruptive migration later.
- Degodding /
specify_clicleanup. Numerous call sites parse WP frontmatter and bodies today (wps_manifest.py,task_metadata_validation.py,status/*,dependency_parser.py, …). A single code-owned model is exactly the kind of consolidation the degodding roadmap favours (one authority, ports over scattered parsing). - Precedent exists. Model-first schema generation
already made Pydantic models the single source of truth for all 10 doctrine
YAML schemas, with
scripts/generate_schemas.py --checkas a drift gate. This idea extends that proven pattern from doctrine artefacts to execution (WP) and op artefacts.
Explicitly out of scope for the idea (for the squad to confirm/deny)
- A specific migration mechanism or backwards-compat window.
- Whether markdown remains round-trippable or becomes strictly one-way (derived).
- The exact content-address boundary (what counts as semantic vs bookkeeping).
- Any commitment to a version number or milestone.
Research questions to ground this
- Code as-is. Where and how are WP files created, mutated, hashed, and parsed today? Enumerate the call sites and the current content-address / hash-mismatch machinery. What does the frontmatter↔body duplication actually cost? How does the Op record (schema v2) store intent today, and what is genuinely missing vs. the WP shape?
- Roadmap / vision alignment. Does this fit the degodding,
specify_clicleanup, and mission-type→doctrine directions — or does it collide with in-flight work (e.g. runtime/state overhaul #1619, infra-logic separation #2173, doctrine extensibility)? Is now genuinely the right seam? - ADR / precedent. Which existing ADRs bind here (doctrine-layer merge semantics, shared-package boundary, model-first schema generation, status append-only event model)? Does the append-only status event log already solve the "bookkeeping churn" half, meaning the WP file only needs to shed its mutable fields rather than adopt a new store?
- Feasibility & risk. Is a structured-authoritative / markdown-derived split realistic given human-editing workflows and the many agents that read WP files? What breaks? What is the smallest first slice that de-risks the rest?
Research Synthesis (2026-07-16)
A three-lens research squad grounded the idea against the code as-is (architect-alphonso), the roadmap/vision + ADRs (researcher-robbie), and architectural feasibility / whack-a-field risk (paula-patterns). All three converged independently. Verbatim reports are archived alongside this note; the synthesis:
What the idea gets right (confirmed)
- The parse-surface sprawl is real and worse than stated. There are
5 distinct frontmatter parsers (
frontmatter.py:327,template/renderer.py:23,task_utils/support.py:191regex-scraper,manifest.py:68,doc_analysis/gap_analysis.py:83) and 2 typed WP models (status/wp_metadata.py:183WPMetadata;task_utils/support.py:269WorkPackage, a raw-string regex scraper), across ~49 consumer modules. Consolidation onto one model is a genuine degodding-shaped win. - The hash-churn pain is confirmed exactly. The mission dossier hashes
the whole WP file as opaque bytes (
dossier/hasher.py:14), feeds everywp*.mdinto a parity hash (dossier/indexer.py:242,dossier/snapshot.py), so any inert edit — a subtask checkbox flip, ahistoryappend — trips drift. Sync'sbody_upload.py:52,116,121whole-file-hashes the same files (content_hash_mismatchguard). - The Op intent/scope gap is real.
OpStartedEvent/OpCompletedEvent(invocation/record.py:39,65) persistrequest_text+action+outcomebut no structured scope / change-surface / reasoning / done-criteria. Governance context is stored as a hash, not readable content. A contract already exists to extend:contracts/op-record-events.md. - The model-first machinery is proven and reusable.
scripts/generate_schemas.py(10 doctrine schemas,--checkdrift gate) can registerWPMetadata/ the Op models mechanically cheaply.
What the idea gets wrong / over-scopes (the decisive findings)
- The "code-owned model" is ~60–70% already shipped.
WPMetadatais already a frozen Pydantic v2 model,extra="forbid", ~40 typed fields, and already the read authority for 23 modules. The idea proposes building what largely exists. It needs electing (makeWP_FIELD_ORDERandWorkPackageEntryderive fromWPMetadata), not inventing. - "Bookkeeping churn needs a new store" is half-false. Lane/review status
was already evicted from the file into the append-only
status.events.jsonlmodel (frontmatter.py:47-49comment;status/reducer.py); the residual lane-in-frontmatter code is stamped MIGRATION-ONLY (task_metadata_validation.py:82,181). The bookkeeping/semantic split the idea calls its key unblock is already the shipped architecture for the fields that churned most. Onlyhistory+shell_pid/shell_pid_created_at- prose-body edits still perturb the hash.
- The WP body cannot be one-way-derived. The body IS the agent's work
order and is authored by humans and agents; the dashboard renders it
verbatim and even reads the card title from a body regex in preference to
frontmatter (
dashboard/scanner.py:895,950,976). Only the index (tasks.md) is safely derived — and that already is, generated one-way fromwps.yaml(core/wps_manifest.py:170,184). "Markdown becomes a derived view" is undercut on contact unless it splits index from prompt body. - The structured-authoritative precedent already exists and stalled.
wps.yaml(code-owned Pydantic + YAML + generated markdown) reached only 5 of 278 missions. The blocker was never the model — it was authority migration and human/agent editing workflow. Any future "YAML authoritative" push must first answer whywps.yamldidn't win. - Whack-a-field risk. Dropping a new
WPRecord"single source of truth" next to the existing three field lists (frontmatter.py:49,wps_manifest.py:16,wp_metadata.py:197— which already drift, held in sync by a hand-maintained comment tax) makes four authorities, not one. The prerequisite is unifying the existing three first.
Prior art — this ground is largely already owned
- #2093 / #2400 (WP-metadata authority split) — already adjudicated
(REWORK-staged). It decided the semantic-vs-bookkeeping split the opposite
way on authority: static design-intent stays frontmatter-canonical;
dynamic runtime state (
agent/shell_pid/history/reviews) retires to the event log. This must be reconciled with, not re-litigated. - ADR 2026-06-11-1 + epic #1804 (Op as first-class artifact) — owns the Op
record shape and its enrichment, C-005 no-parallel-primitive. An Op
intent/scope field belongs here as a field extension; a separate
"small-WP-for-an-Op" primitive would re-open the exact divergence the ADR
closes. Must also respect
MinimalViableTrailPolicy(record.py:128) — scope is opt-in Tier-2, not mandatory. - ADR 2026-06-06-1 +
wps.yaml— already a structured,extra="forbid"machine-readable WP manifest. ADR 2026-06-07-1 / status event model — lane already event-owned. The "no code-owned model exists" premise is only half true.
Sequencing — this is a downstream consumer, not a peer of the current wave
The 3.2.x milestone posture is explicitly "no new shadow paths / adopt don't build." This idea is a consumer of #1868 (seam-binding), #2173 (ports), #1797 (degod), and #2468's keystone #2467 — it belongs behind them, ideally expressed as a #2173-style port over WP/Op parsing. Critically, #1619 (runtime/state overhaul) is actively re-cutting the WP/Mission/MissionRun aggregate model (its domain dialectic is unsettled — refutations at the consolidation gate); formalising a WP logical model now front-runs a domain map mid-revision.
Disposition — document, narrow, re-parent. Not "spec now."
The instinct (code-owned models + model-first schemas) is the house style and on-direction, but the note as written is deflated: its two headline justifications are already owned, one decided the opposite way on authority. Recommended path, smallest-blast-radius first:
| # | Slice | Owner / parent | Risk |
|---|---|---|---|
| 0 | Tidy-first: unify the 3 field lists — make WP_FIELD_ORDER + WorkPackageEntry derive from WPMetadata; register WPMetadata in generate_schemas.py. Pure debt, no behaviour change. |
degod / #1797 lineage | low |
| 1 | Semantic-only content-address: hash a normalized WPMetadata projection (drop history/shell_pid*, normalize body) instead of raw bytes in dossier/{hasher,indexer}.py + sync/body_upload.py. Kills the hash-churn pain with no authority migration, no markdown rewrite. Highest signal. |
dossier / sync | low–med |
| 2 | Retire the WorkPackage regex scraper (task_utils/support.py:269) onto WPMetadata. |
degod | low |
| 3 | Independent: add an optional structured scope/intent field to OpStartedEvent, respecting MinimalViableTrailPolicy. Do not bundle with WP work. |
ADR 2026-06-11-1 / #1804 | low |
| X | Full "YAML authoritative, markdown-body derived." | Deferred — needs wps.yaml post-mortem + #1619 aggregate boundaries settled + #2093/#2400 reconciliation. |
high |
Reconcile-before-spec, in priority order: (1) #2093/#2400 authority decision; (2) ADR 2026-06-11-1 / #1804 for the Op half; (3) #1619 aggregate model; (4) #1868/#2173/#1797/#2468 sequencing.
Slices 0–3 are legitimate, low-risk, and mostly filable as reconciliation items under existing epics rather than a new construction mission. Slice X is the only part matching the note's original ambition, and it is explicitly not now.
Related open tracker tickets
A three-lens tracker sweep (2026-07-16) mapped the open issues across all four elements — see wp-op-schema-related-tickets.md. Key result: the idea's headline ambitions are already filed — #1676 (P0, deterministic structured authoring) and #424/#425 (JSON-canonical planning artifacts, markdown-derived) are the Slice-X architecture; #2093/#2400 already adjudicated the WP-metadata authority split. The template-move timing is owned by the #2652 arc (#2658 templates-as-config, #2659 template discovery) + #2677 (DRG edges → WP templates/assets). Only two elements are unticketed: the semantic-only content-hash fix, and an optional Op scope/intent field (which attaches to #2400 / ADR 2026-06-11-1, not a new epic — the #1804 Ops layer is closed/shipped). Disposition therefore firms up to reconcile-and-attach, not a new mission.