Skip to content

v0.20.0 GA blocker: align plan.edges[] field names (from/to vs source/target) #458

@MScottAdams

Description

@MScottAdams

Summary

Schema, code, docs, and test fixtures disagree on the field names used in plan.edges[] entries. Schema and docs use {"from", "to", "type"}; code and fixtures use {"source", "target", "type"}. Latent correctness bug that will fire the moment the #436 migrator runs against real-world speckit data or the #435 aggregator builds cross-scope dep maps from schema-compliant input.

Filed per the #437 post-walkthrough sanity sweep (2026-04-20). v0.20.0 GA blocker (#8 of 8).

Evidence (four-way disagreement)

Schema requires from / to / type

From vbrief/schemas/vbrief-core.schema.json:132-144:

"Edge": {
  "type": "object",
  "required": ["from", "to", "type"],
  "properties": {
    "from": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$"},
    "to": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$"},
    "type": {"type": "string", "description": "Core types: blocks, informs, invalidates, suggests. Custom types allowed."}
  }
}

Speckit doc example matches schema

From strategies/speckit.md:176-180:

"edges": [
  { "from": "t2.1", "to": "t3.3", "type": "blocks" },
  { "from": "t3.1", "to": "t3.2", "type": "blocks" }
]

Code reads source / target (disagrees with schema)

From scripts/roadmap_render.py:80-99:

for edge in edges:
    source = edge.get("source", "")
    target = edge.get("target", "")
    if source and target:
        dep_map.setdefault(target, []).append(source)

Test fixture matches code (disagrees with schema and docs)

From tests/cli/test_roadmap_render.py:107-111:

"edges": [
    {"source": "task-a", "target": "task-b"},
    {"source": "task-a", "target": "task-c"},
    {"source": "task-b", "target": "task-d"},
],

Why this is a v0.20.0 GA blocker

  1. speckit Phase 4 collides with plan.vbrief.json definition (no scope vBRIEFs emitted) #436 Opt 1 migrator depends on reading existing speckit edges[]. Real-world speckit users (per the migrator's design) have speckit-shaped plan.vbrief.json files. If those files follow the speckit.md example convention (from/to), the migrator code must read from/to. If the migrator reuses _build_edge_map's source/target pattern, it silently produces an empty dep map for every IP-N edge, losing all dependency information.

  2. spec rendering does not aggregate scope vBRIEFs from lifecycle folders (v0.20 model) #435 aggregator cross-scope topo-sort depends on reading edges[] consistently. Same silent-empty-map failure mode if a scope vBRIEF stores edges under one convention and the reader expects the other.

  3. Silent failure, not loud. edge.get("source", "") returns empty string for {"from": ..., "to": ...} inputs. No error, no warning. Edge data effectively disappears. Topo sort degenerates to filename-only order.

  4. Schema is published. vbrief/vbrief.md:306-308 points at ./schemas/vbrief-core.schema.json as the canonical source. Users who read the schema and write {"from", "to"} will get broken behavior from tools that expect {"source", "target"}.

Proposed fix

Three-part alignment:

1. Make roadmap_render.py (and the #435 aggregator implementation) bilingual

Update _build_edge_map to read both conventions:

for edge in edges:
    # Schema-canonical first; code-historical fallback
    src = edge.get("from") or edge.get("source", "")
    tgt = edge.get("to")   or edge.get("target", "")
    if src and tgt:
        dep_map.setdefault(tgt, []).append(src)

2. Canonical convention: schema wins

Going forward, new vBRIEFs emit {"from", "to", "type"} (matches schema + speckit doc). Old vBRIEFs with source/target continue to work via the bilingual reader for at least one release cycle.

3. Update test fixtures to match schema

tests/cli/test_roadmap_render.py:107-111 (and any similar fixtures) switch to {"from", "to"}. Add one legacy fixture asserting source/target still reads correctly (backward-compat regression guard).

4. Migrator (part of #436) reads from/to as primary with source/target fallback

Uses the same bilingual pattern so real-world speckit data is read correctly regardless of which convention was used historically.

Test coverage

  • test_edge_map_from_to_keys -- asserts canonical {"from", "to"} input produces correct dep map.
  • test_edge_map_source_target_keys -- asserts legacy {"source", "target"} input produces correct dep map (backward-compat).
  • test_edge_map_mixed_keys_within_single_plan -- edge case: mix of both in same edges[] array should all resolve.

Acceptance criteria

  • scripts/roadmap_render.py _build_edge_map reads both {from, to} and {source, target}; prefers from/to when both present.
  • scripts/spec_render.py (spec rendering does not aggregate scope vBRIEFs from lifecycle folders (v0.20 model) #435 aggregator's cross-scope topo sort) uses the same bilingual read pattern.
  • scripts/migrate_vbrief.py speckit-plan translator reads both conventions for input edges.
  • tests/cli/test_roadmap_render.py primary fixture switches to {from, to}; legacy {source, target} fixture added for backward-compat regression.
  • strategies/speckit.md Phase 4 doc continues to use {from, to} (already does -- no change).
  • vbrief/vbrief.md (if it documents edges anywhere) uses {from, to} consistently.

Estimated effort

~1 hour. Small code change (bilingual reader in 2-3 places), fixture update, doc grep, tests.

Related

GA status

v0.20.0 GA blocker. Must land before PR #403 can merge. Small scope, low risk, high correctness value -- the bilingual reader pattern protects against future data-shape drift.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions