Skip to content

One-way projection of a Plan into external surfaces #7

Description

@schickling-assistant

Problem

The roadmap already names this direction:

Rendering a Plan into an external issue tracker for human or cross-organization visibility. Compass remains the authority and the projection is one-way. Bidirectional sync is explicitly not a direction: it reintroduces the dual-authority problem the design exists to avoid.

This issue does not argue for the direction. It records the design work a predecessor in-house planning design did on exactly this problem, including a working reference implementation, so that when it is picked up the non-obvious parts are already known.

The pressure behind it is real and unaddressed today: CMP-T04 accepts that "Plan history is not pull-request reviewable" and says "Review of intent, if wanted, is a projection." That sentence is an IOU, and this is the design behind it.

The parts worth keeping

One semantic model, adapters underneath. Every target renders from the same structured projection of the Plan, never from target-specific hand-authored text. The predecessor stated it as: the render model must not fork into separate per-target content hierarchies, and divergence between targets "must be an adapter concern." Concretely — a section list (what the Plan is for, what needs a decision, what is ready, what is in flight, recent reasoning, full Step inventory) is defined once, and each target translates sections into its native blocks. Where a target is genuinely more capable it uses that capability for the same section rather than inventing a section: collapsible history renders as a disclosure element on one target and a toggle on another, and is the same section.

A binding names the exact region it owns. Easy to skip, expensive to skip. A projection does not own "the page" — it owns a named region: this file, this issue body, this comment, this keyed block range. Anything outside the owned region is human territory and is never touched. The predecessor's binding record carried the target, the projection kind, the owned region, the source version last rendered, and a hash of what was written. That last pair is what makes the next two properties possible at all.

Re-rendering unchanged state writes nothing. Rendering is deterministic from the projection, so re-rendering identical state produces identical output, which is compared against the recorded hash and skipped. Without this a projection loop becomes a notification generator: every reader of the external surface is pinged on every render whether or not anything changed, and they stop reading it. This is the property that decides whether the feature is used or muted.

One serializing render owner per binding. Two machines rendering the same Plan into the same region will clobber each other, and the loser's write looks successful. The predecessor settled this as a single serializing owner per binding, with other actors requesting a render through it rather than racing the surface. This is the same primitive the roadmap already reaches for under "Contention between concurrent workers" — a serializing owner rather than a lease — so it is the existing preference applied to a second resource, not a new mechanism.

Edits inside the owned region are drift: reported, never imported. This is where the one-way rule becomes operational rather than aspirational. Someone will edit the rendered body; a checkbox will get ticked. The predecessor's rule was that an edit inside a region the projection owns is a drift observation and never canonical state, and specifically that a ticked box in an external tool completes nothing. That is the same position as CMP.DM-R13 ("An external record never completes a Step") — this is just where it gets enforced.

Stable anchors so feedback can be aimed. Rendered output includes stable identifiers for the things someone might want to point at: the Plan, each Step, each version. Since a Step's identity is the name it was declared under (CMP.DM-R08) and is not opaque, this is nearly free, and it makes external discussion referenceable back into the Plan instead of ending in a thread.

If input is ever wanted, it starts structured. Explicitly out of scope, and worth recording because it constrains the layout chosen now: the predecessor concluded that future inbound input should arrive through structured actions or human-owned regions outside the generated one — never by diffing a generated body and guessing what the human meant. Leaving a human-owned region in the layout now costs nothing and is the difference between a later inbound path being possible and being a parser.

The reference implementation

The predecessor built a working prototype — roughly 1,100 lines across a schema and a renderer, plus a fixture and its rendered output. What it does:

  • A schema declares the whole projection: the Plan and its metadata, versions with their stated reasons and a summarised structural diff, the work graph and its edges, notes, derived conditions, and the review records that design had.
  • A fixture is a single structured document — the only source of truth in the prototype.
  • A renderer decodes the fixture through the schema first, then emits Markdown. The decode-first ordering is the load-bearing part: the renderer cannot silently emit a field the model does not define, so the projection is provably a function of the model rather than of whatever the author of the renderer remembered.
  • The rendered output is checked in beside the fixture, so a change to the renderer shows up as a diff a reviewer reads rather than as a description of a change.

It could seed a draft PR, with two honest caveats.

Caveat 1 — the prototype's schema is not this project's model. It was built for a different design and carries a Kubernetes-shaped metadata/spec/status envelope, a 0–4 priority bucket, t-shirt size estimates, nine work-item statuses, and eleven relationship types. That vocabulary is squarely what this project's ontology refuses: Plan lists _Avoid_: ticket, issue, epic, backlog, board, and a mutable status field is exactly what Progress Event lists under _Avoid_. None of it transfers. What transfers is the pipeline shape — declare the projection, decode before rendering, render sections rather than targets, keep a fixture and its output side by side — and the target-side design above.

Caveat 2 — a checked-in rendered artifact must not become a byte-pinned test. CMP.EVAL-R02 is explicit that evals "assert observable properties, never exact output", because "a suite that pins bytes would churn on unrelated changes and be regenerated without being read" — and with identity being a content hash, expected outputs would churn constantly. So a rendered file is a review aid, not an assertion. The properties worth asserting, and which fit CMP.EVAL-R02 cleanly, are self-comparisons: render the same state twice and require the two to be identical, and re-render after no change and require that no write occurred. Those prove determinism and the skip-unchanged rule without pinning a single byte in the suite.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions