requirements-as-code 0.5.2__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {requirements_as_code-0.5.2/requirements_as_code.egg-info → requirements_as_code-0.6.1}/PKG-INFO +1 -1
  2. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/artifacts.py +47 -4
  3. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/cli.py +9 -5
  4. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/outputs.py +28 -0
  5. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/schema.py +3 -0
  6. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/stats.py +45 -3
  7. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/validate.py +46 -4
  8. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1/requirements_as_code.egg-info}/PKG-INFO +1 -1
  9. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/requirements_as_code.egg-info/SOURCES.txt +4 -0
  10. requirements_as_code-0.6.1/tests/fixtures/roadmap/minimal.md +9 -0
  11. requirements_as_code-0.6.1/tests/fixtures/roadmap/missing_initiatives.md +17 -0
  12. requirements_as_code-0.6.1/tests/fixtures/roadmap/valid.md +31 -0
  13. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_inspect.py +2 -2
  14. requirements_as_code-0.6.1/tests/test_roadmap.py +380 -0
  15. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_schema.py +7 -4
  16. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/.github/workflows/python-publish.yml +0 -0
  17. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/.gitignore +0 -0
  18. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/LICENSE +0 -0
  19. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/README.md +0 -0
  20. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/examples/example_dashboard_v1.md +0 -0
  21. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/examples/example_dashboard_v2.md +0 -0
  22. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-001-markdown-first.md +0 -0
  23. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-002-ai-optional.md +0 -0
  24. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-003-structured-outputs-first.md +0 -0
  25. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-004-artifact-model.md +0 -0
  26. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-005-cli-first.md +0 -0
  27. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-006-ingest-over-rewrite.md +0 -0
  28. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-007-json-contract-stability.md +0 -0
  29. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-008-agent-ready-architecture.md +0 -0
  30. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-009-ai-assisted-development.md +0 -0
  31. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-010-documents-are-not-artifacts.md +0 -0
  32. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-011-file-first-pipeline.md +0 -0
  33. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-012-open-core-strategy.md +0 -0
  34. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-013-leverage-existing-source-control-systems.md +0 -0
  35. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-014-viewer-agnostic-knowledge-artifacts.md +0 -0
  36. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-015-explorer-as-consumer.md +0 -0
  37. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-016-relationships-as-structural-references.md +0 -0
  38. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/adr/adr-017-rac-managed-knowledge-not-work.md +0 -0
  39. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/future/v1.0-workspace-analysis.md +0 -0
  40. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/future/v1.1-review-engine.md +0 -0
  41. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/future/v1.2-mcp-server.md +0 -0
  42. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/future/v1.4-claude-skills.md +0 -0
  43. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/future/v1.4-python-sdk.md +0 -0
  44. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/archive/v0.5-decisions.md +0 -0
  45. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/archive/v0.7-prompts.md +0 -0
  46. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.2-stats.md +0 -0
  47. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.3-ingest.md +0 -0
  48. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.3.1-formats.md +0 -0
  49. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.4-inspect.md +0 -0
  50. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.4.1-expansion.md +0 -0
  51. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.4.1-inspect-expansion.md +0 -0
  52. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.4.2-decision-metadata.md +0 -0
  53. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.5.0-artifact-improvement.md +0 -0
  54. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.5.1-guided-improvement.md +0 -0
  55. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.5.2-schema.md +0 -0
  56. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.6.0-roadmap-artifacts.md +0 -0
  57. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.6.1-roadmap-improvement.md +0 -0
  58. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.6.2-prompt-artifact.md +0 -0
  59. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.7.0-relationship-metadata.md +0 -0
  60. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.7.1-relationship-inspection.md +0 -0
  61. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.7.2-relationship-validation.md +0 -0
  62. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.8.0-explorer-foundation.md +0 -0
  63. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.8.1-explorer-experience.md +0 -0
  64. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/planning/roadmap/v0.8.2-knowledge-operations.md +0 -0
  65. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/pyproject.toml +0 -0
  66. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/__init__.py +0 -0
  67. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/classification.py +0 -0
  68. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/diff.py +0 -0
  69. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/fs.py +0 -0
  70. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/improve.py +0 -0
  71. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/ingest.py +0 -0
  72. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/inspect.py +0 -0
  73. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/models.py +0 -0
  74. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/rac/parser.py +0 -0
  75. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/requirements_as_code.egg-info/dependency_links.txt +0 -0
  76. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/requirements_as_code.egg-info/entry_points.txt +0 -0
  77. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/requirements_as_code.egg-info/requires.txt +0 -0
  78. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/requirements_as_code.egg-info/top_level.txt +0 -0
  79. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/setup.cfg +0 -0
  80. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/conftest.py +0 -0
  81. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/bad_category.md +0 -0
  82. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/bad_status.md +0 -0
  83. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/minimal.md +0 -0
  84. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/portfolio/01_accepted_arch.md +0 -0
  85. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/portfolio/02_proposed_process.md +0 -0
  86. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/portfolio/03_no_metadata.md +0 -0
  87. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/decision/with_metadata.md +0 -0
  88. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/diff/new.md +0 -0
  89. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/diff/old.md +0 -0
  90. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/ingest/sample.md +0 -0
  91. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/inspect/ambiguous.md +0 -0
  92. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/inspect/decision.md +0 -0
  93. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/inspect/nested/another_requirement.md +0 -0
  94. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/inspect/requirement.md +0 -0
  95. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/duplicate_ids.md +0 -0
  96. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/empty_req_text.md +0 -0
  97. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/malformed_id.md +0 -0
  98. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/missing_id.md +0 -0
  99. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/missing_problem.md +0 -0
  100. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/missing_requirements.md +0 -0
  101. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/missing_title.md +0 -0
  102. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/invalid/multiple_titles.md +0 -0
  103. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/portfolio/broken.md +0 -0
  104. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/portfolio/feature_a.md +0 -0
  105. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/portfolio/feature_b.md +0 -0
  106. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/portfolio/sub/feature_c.md +0 -0
  107. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/valid/bullet_requirements.md +0 -0
  108. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/valid/feature.md +0 -0
  109. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/valid/minimal.md +0 -0
  110. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/fixtures/valid/warnings.md +0 -0
  111. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_cli.py +0 -0
  112. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_decision_metadata.py +0 -0
  113. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_diff.py +0 -0
  114. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_improve.py +0 -0
  115. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_ingest.py +0 -0
  116. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_parser.py +0 -0
  117. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_stats.py +0 -0
  118. {requirements_as_code-0.5.2 → requirements_as_code-0.6.1}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requirements-as-code
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: RAC — lint and diff product requirements written in Markdown.
5
5
  Author: tcballard
6
6
  License-Expression: MIT
@@ -9,10 +9,10 @@ there is a single source of truth.
9
9
  Section names are normalized (stripped + casefolded) for matching; ``display``
10
10
  holds the human-facing label.
11
11
 
12
- v0.4 defines only the two artifact types that have a concrete schema today:
13
- Requirement (RAC's own format / validator) and Decision (the ADR format used in
14
- this repository). Roadmap, Prompt, and Meeting are intentionally deferred until
15
- their schemas are formalized — see planning/roadmap/.
12
+ Three artifact types have a concrete schema today: Requirement (RAC's own format /
13
+ validator), Decision (the ADR format used in this repository), and Roadmap
14
+ (outcome- and initiative-focused knowledge, added in v0.6.0). Prompt and Meeting
15
+ are intentionally deferred until their schemas are formalized — see planning/roadmap/.
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -139,6 +139,49 @@ ARTIFACT_SPECS: tuple[ArtifactSpec, ...] = (
139
139
  "options considered": "alternatives considered",
140
140
  },
141
141
  ),
142
+ ArtifactSpec(
143
+ name="roadmap",
144
+ display="Roadmap",
145
+ required=("outcomes", "initiatives"),
146
+ recommended=("success measures", "assumptions", "risks"),
147
+ # Relationship sections are recognized but never scored or templated; they
148
+ # exist so v0.6.0 roadmaps can reference Decisions/Requirements as text
149
+ # without RAC analyzing those links (relationship analysis is v0.7.x).
150
+ optional=("related decisions", "related requirements"),
151
+ descriptions={
152
+ "outcomes": "The user, business, or operational outcomes this roadmap pursues",
153
+ "initiatives": "The major bodies of work that support those outcomes",
154
+ "success measures": "How progress toward the outcomes will be measured",
155
+ "assumptions": "Conditions that must hold for this roadmap to stay valid",
156
+ "risks": "What could prevent the outcomes from being achieved",
157
+ },
158
+ guidance={
159
+ "outcomes": (
160
+ "What user, business, or operational outcomes matter?",
161
+ "Why are these outcomes important now?",
162
+ ),
163
+ "initiatives": (
164
+ "What major bodies of work support these outcomes?",
165
+ "How does each initiative connect to an outcome?",
166
+ ),
167
+ "success measures": (
168
+ "How will the team know the roadmap is succeeding?",
169
+ "What observable signals would show progress?",
170
+ ),
171
+ "assumptions": (
172
+ "What must be true for this roadmap to remain valid?",
173
+ ),
174
+ "risks": (
175
+ "What could prevent these outcomes from being achieved?",
176
+ ),
177
+ },
178
+ # Artifact-scoped: this only normalizes "success metrics" when scoring a
179
+ # document against the Roadmap spec (see rac.classification._mapped), so it
180
+ # never affects the Requirement spec's canonical "success metrics" section.
181
+ synonyms={
182
+ "success metrics": "success measures",
183
+ },
184
+ ),
142
185
  )
143
186
 
144
187
 
@@ -90,10 +90,14 @@ def cmd_stats(args: argparse.Namespace) -> int:
90
90
  else:
91
91
  print(outputs.render_stats_human(stats))
92
92
  # Success as long as the portfolio has analysable content: at least one valid
93
- # feature or at least one decision. Invalid files are reported but don't fail
94
- # the run on their own. (A future --strict flag will fail the run if *any*
95
- # file is invalid, for CI use.)
96
- has_content = stats.valid_features > 0 or stats.decision_count > 0
93
+ # feature, one decision, or one valid roadmap. Invalid files are reported but
94
+ # don't fail the run on their own. (A future --strict flag will fail the run if
95
+ # *any* file is invalid, for CI use.)
96
+ has_content = (
97
+ stats.valid_features > 0
98
+ or stats.decision_count > 0
99
+ or stats.valid_roadmaps > 0
100
+ )
97
101
  return EXIT_OK if has_content else EXIT_VALIDATION_FAILED
98
102
 
99
103
 
@@ -363,7 +367,7 @@ def build_parser() -> argparse.ArgumentParser:
363
367
  p_schema.add_argument(
364
368
  "schema",
365
369
  nargs="?",
366
- help="Schema name, e.g. requirement or decision.",
370
+ help="Schema name, e.g. requirement, decision, or roadmap.",
367
371
  )
368
372
  p_schema.add_argument(
369
373
  "--list",
@@ -227,6 +227,24 @@ def render_stats_human(s: PortfolioStats) -> str:
227
227
  breakdown("Status", s.decision_status_counts)
228
228
  breakdown("Category", s.decision_category_counts)
229
229
 
230
+ # Roadmaps are reported separately and lightly (count + invalid only); the
231
+ # section is omitted entirely when there are none.
232
+ if s.roadmaps:
233
+ lines += [
234
+ "",
235
+ _bold("Roadmaps"),
236
+ "========",
237
+ "",
238
+ f"Total: {s.roadmap_count}",
239
+ f"Valid: {s.valid_roadmaps}",
240
+ ]
241
+ invalid_roadmaps = s.invalid_roadmaps
242
+ if invalid_roadmaps:
243
+ lines += ["", _bold(f"Invalid Roadmaps ({len(invalid_roadmaps)})")]
244
+ for r in invalid_roadmaps:
245
+ reasons = ", ".join(r.error_codes) or "unknown"
246
+ lines.append(f" {_red(r.path)} — {reasons}")
247
+
230
248
  return "\n".join(lines)
231
249
 
232
250
 
@@ -264,6 +282,16 @@ def render_stats_json(s: PortfolioStats) -> str:
264
282
  "by_status": s.decision_status_counts,
265
283
  "by_category": s.decision_category_counts,
266
284
  }
285
+ # Additive: only present when the portfolio contains roadmaps. Lightweight by
286
+ # design — count and validity only (no section-completeness breakdown).
287
+ if s.roadmaps:
288
+ payload["roadmaps"] = {
289
+ "count": s.roadmap_count,
290
+ "valid": s.valid_roadmaps,
291
+ "invalid": [
292
+ {"file": r.path, "errors": r.error_codes} for r in s.invalid_roadmaps
293
+ ],
294
+ }
267
295
  return json.dumps(payload, indent=2)
268
296
 
269
297
 
@@ -135,6 +135,9 @@ def _free_text_todo(section: str) -> str:
135
135
  "or adoption risks."
136
136
  ),
137
137
  "assumptions": "TODO: describe conditions assumed to be true.",
138
+ "outcomes": "TODO: describe the outcomes this roadmap is intended to achieve.",
139
+ "initiatives": "TODO: describe the major initiatives that support the outcomes.",
140
+ "success measures": "TODO: describe how progress or success will be measured.",
138
141
  "context": "TODO: describe the situation, constraints, and background.",
139
142
  "decision": "TODO: describe the decision that has been made.",
140
143
  "consequences": (
@@ -4,9 +4,10 @@
4
4
  each one, and aggregates the results. Like the rest of RAC, it works on the
5
5
  Product AST: every `.md` is parsed into a :class:`~rac.models.Product`.
6
6
 
7
- Requirement and Decision artifacts are aggregated separately so that one never
8
- distorts the other: requirement totals/averages span only requirement files, and
9
- decisions get their own status/category breakdown.
7
+ Requirement, Decision, and Roadmap artifacts are aggregated separately so that one
8
+ never distorts another: requirement totals/averages span only requirement files,
9
+ decisions get their own status/category breakdown, and roadmaps get a lightweight
10
+ count of how many exist and how many are valid.
10
11
 
11
12
  Counting basis: requirement totals, averages, and the per-feature breakdown span
12
13
  *all* parsed requirement files (including ones that fail validation). A file
@@ -49,6 +50,21 @@ class DecisionStat:
49
50
  supersedes: str | None = None
50
51
 
51
52
 
53
+ @dataclass
54
+ class RoadmapStat:
55
+ """Per-file result for a Roadmap artifact (kept separate from features).
56
+
57
+ Deliberately lightweight (v0.6.0): identity plus validity. Section-completeness
58
+ or quality breakdowns are intentionally absent — those belong to `rac improve`,
59
+ not portfolio statistics.
60
+ """
61
+
62
+ path: str
63
+ name: str # the roadmap title, or the filename stem if it has none
64
+ valid: bool
65
+ error_codes: list[str]
66
+
67
+
52
68
  @dataclass
53
69
  class PortfolioStats:
54
70
  """Aggregate view over all discovered requirement files."""
@@ -56,6 +72,7 @@ class PortfolioStats:
56
72
  directory: str
57
73
  features: list[FeatureStat] = field(default_factory=list)
58
74
  decisions: list[DecisionStat] = field(default_factory=list)
75
+ roadmaps: list[RoadmapStat] = field(default_factory=list)
59
76
 
60
77
  # --- counts (requirement features) ---
61
78
  @property
@@ -138,6 +155,19 @@ class PortfolioStats:
138
155
  """Decisions grouped by category, in schema order, omitting empty buckets."""
139
156
  return _bucket(self.decisions, "category", "category")
140
157
 
158
+ # --- roadmaps ---
159
+ @property
160
+ def roadmap_count(self) -> int:
161
+ return len(self.roadmaps)
162
+
163
+ @property
164
+ def valid_roadmaps(self) -> int:
165
+ return sum(1 for r in self.roadmaps if r.valid)
166
+
167
+ @property
168
+ def invalid_roadmaps(self) -> list[RoadmapStat]:
169
+ return [r for r in self.roadmaps if not r.valid]
170
+
141
171
 
142
172
  def _bucket(decisions: list[DecisionStat], attr: str, metadata_key: str) -> dict[str, int]:
143
173
  """Count ``decisions`` by ``attr`` in the artifact spec's declared order."""
@@ -183,6 +213,18 @@ def collect_stats(directory: str) -> PortfolioStats:
183
213
  )
184
214
  )
185
215
  continue
216
+ if result.type == "roadmap":
217
+ issues = validate(product)
218
+ error_codes = [i.code for i in issues if i.severity == "error"]
219
+ stats.roadmaps.append(
220
+ RoadmapStat(
221
+ path=str(path),
222
+ name=name,
223
+ valid=not error_codes,
224
+ error_codes=error_codes,
225
+ )
226
+ )
227
+ continue
186
228
  issues = validate(product)
187
229
  error_codes = [i.code for i in issues if i.severity == "error"]
188
230
  stats.features.append(
@@ -32,12 +32,17 @@ def has_errors(issues: list[Issue]) -> bool:
32
32
  def validate(product: Product) -> list[Issue]:
33
33
  """Check ``product`` and return all structural and quality findings.
34
34
 
35
- Dispatches on artifact type: Decisions are validated against the Decision
36
- schema; everything else (Requirements and Unknown documents) keeps RAC's
37
- original Requirement rules unchanged.
35
+ Dispatches on artifact type. Each type with its own schema is routed
36
+ explicitly; the final ``_validate_requirement`` arm is a
37
+ backwards-compatibility fallback for Unknown/legacy documents (and RAC's
38
+ original Requirement rules), *not* the long-term model — new artifact types
39
+ must be routed explicitly above it.
38
40
  """
39
- if classify(product).type == "decision":
41
+ artifact_type = classify(product).type
42
+ if artifact_type == "decision":
40
43
  return _validate_decision(product)
44
+ if artifact_type == "roadmap":
45
+ return _validate_roadmap(product)
41
46
  return _validate_requirement(product)
42
47
 
43
48
 
@@ -102,6 +107,43 @@ def _validate_decision(product: Product) -> list[Issue]:
102
107
  return issues
103
108
 
104
109
 
110
+ def _validate_roadmap(product: Product) -> list[Issue]:
111
+ """Validate a Roadmap artifact (REQ-003).
112
+
113
+ Required sections (Outcomes, Initiatives) must be present; missing recommended
114
+ sections never fail. Roadmaps carry no constrained metadata (no owners, dates,
115
+ or status — ADR-017: RAC manages knowledge, not work).
116
+ """
117
+ spec = spec_for("roadmap")
118
+ assert spec is not None # the roadmap spec always exists
119
+ issues: list[Issue] = []
120
+
121
+ if not product.title:
122
+ issues.append(Issue("error", "missing-title", "File has no top-level # title."))
123
+
124
+ if product.extra_title_lines:
125
+ issues.append(
126
+ Issue(
127
+ "error",
128
+ "multiple-titles",
129
+ "File has more than one top-level # title; expected exactly one.",
130
+ product.extra_title_lines[0],
131
+ )
132
+ )
133
+
134
+ for section in spec.required:
135
+ if section not in product.sections:
136
+ issues.append(
137
+ Issue(
138
+ "error",
139
+ f"missing-{section}",
140
+ f"Roadmap is missing a ## {section.title()} section.",
141
+ )
142
+ )
143
+
144
+ return issues
145
+
146
+
105
147
  def _validate_requirement(product: Product) -> list[Issue]:
106
148
  """Check ``product`` and return all structural and quality findings."""
107
149
  issues: list[Issue] = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requirements-as-code
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: RAC — lint and diff product requirements written in Markdown.
5
5
  Author: tcballard
6
6
  License-Expression: MIT
@@ -77,6 +77,7 @@ tests/test_improve.py
77
77
  tests/test_ingest.py
78
78
  tests/test_inspect.py
79
79
  tests/test_parser.py
80
+ tests/test_roadmap.py
80
81
  tests/test_schema.py
81
82
  tests/test_stats.py
82
83
  tests/test_validate.py
@@ -106,6 +107,9 @@ tests/fixtures/portfolio/broken.md
106
107
  tests/fixtures/portfolio/feature_a.md
107
108
  tests/fixtures/portfolio/feature_b.md
108
109
  tests/fixtures/portfolio/sub/feature_c.md
110
+ tests/fixtures/roadmap/minimal.md
111
+ tests/fixtures/roadmap/missing_initiatives.md
112
+ tests/fixtures/roadmap/valid.md
109
113
  tests/fixtures/valid/bullet_requirements.md
110
114
  tests/fixtures/valid/feature.md
111
115
  tests/fixtures/valid/minimal.md
@@ -0,0 +1,9 @@
1
+ # Billing Roadmap
2
+
3
+ ## Outcomes
4
+
5
+ - Customers can self-serve plan changes without contacting support.
6
+
7
+ ## Initiatives
8
+
9
+ - Build a self-service plan-change flow with proration.
@@ -0,0 +1,17 @@
1
+ # Onboarding Roadmap
2
+
3
+ ## Outcomes
4
+
5
+ - New users reach first value within their first session.
6
+
7
+ ## Success Measures
8
+
9
+ - 70% of new accounts complete activation within 24 hours.
10
+
11
+ ## Assumptions
12
+
13
+ - Most new accounts arrive through a self-serve signup, not sales.
14
+
15
+ ## Risks
16
+
17
+ - A longer activation flow could increase early drop-off.
@@ -0,0 +1,31 @@
1
+ # Mobile Platform Roadmap
2
+
3
+ ## Outcomes
4
+
5
+ - Customers can complete every core workflow from a phone, not just the desktop app.
6
+ - Mobile becomes a credible reason to choose us over competitors in field-heavy teams.
7
+
8
+ ## Initiatives
9
+
10
+ - Rebuild the navigation shell as a responsive, offline-capable client.
11
+ - Ship native push notifications for the three highest-volume workflows.
12
+ - Close the feature gap on reporting so mobile is not a read-only experience.
13
+
14
+ ## Success Measures
15
+
16
+ - 40% of weekly active accounts have at least one mobile session.
17
+ - Mobile task-completion rate reaches parity (within 10%) of desktop.
18
+
19
+ ## Assumptions
20
+
21
+ - The existing API can serve mobile clients without a separate backend.
22
+ - Customers in the target segment carry company-managed devices.
23
+
24
+ ## Risks
25
+
26
+ - Offline sync conflicts could erode trust if data is silently lost.
27
+ - App store review cycles may slow the cadence of fixes.
28
+
29
+ ## Related Decisions
30
+
31
+ - ADR-014 chose a single shared API for web and mobile.
@@ -26,9 +26,9 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
26
26
  # --- service layer ----------------------------------------------------------
27
27
 
28
28
 
29
- def test_artifact_specs_are_the_two_concrete_types():
29
+ def test_artifact_specs_are_the_concrete_types():
30
30
  names = {spec.name for spec in ARTIFACT_SPECS}
31
- assert names == {"requirement", "decision"}
31
+ assert names == {"requirement", "decision", "roadmap"}
32
32
 
33
33
 
34
34
  def test_parse_captures_title_and_sections():
@@ -0,0 +1,380 @@
1
+ """Tests for the Roadmap artifact type (v0.6.0).
2
+
3
+ Roadmap is a first-class artifact recognized by its Outcomes/Initiatives sections.
4
+ It rides the shared, schema-driven machinery (classify, validate, stats, improve,
5
+ schema) the same way Requirement and Decision do. Per ADR-017 and the v0.6.0 spec
6
+ it carries no work-management metadata (owners, dates, status), and improvement
7
+ stays strictly structural (missing sections + schema guidance, never quality).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import io
13
+ import json
14
+
15
+ import pytest
16
+
17
+ import rac.improve as improve_mod
18
+ from rac.artifacts import ARTIFACT_SPECS, spec_for
19
+ from rac.cli import main
20
+ from rac.classification import classify
21
+ from rac.improve import improve_file, improve_text, supports_improve
22
+ from rac.inspect import inspect_file
23
+ from rac.parser import parse, parse_file
24
+ from rac.schema import available_schemas, schema_reference
25
+ from rac.stats import collect_stats
26
+ from rac.validate import has_errors, validate
27
+
28
+ from conftest import fixture_path
29
+
30
+
31
+ def _stdin(monkeypatch, text: str) -> None:
32
+ monkeypatch.setattr("sys.stdin", io.StringIO(text))
33
+
34
+
35
+ # --- classification ---------------------------------------------------------
36
+
37
+
38
+ def test_roadmap_classifies_as_roadmap():
39
+ result = inspect_file(fixture_path("roadmap", "valid.md"))
40
+ assert result.type == "roadmap"
41
+ assert result.confidence >= 0.5
42
+
43
+
44
+ def test_minimal_roadmap_classifies_on_required_sections_alone():
45
+ # Outcomes + Initiatives only -> 2/3.5 fit, above the 0.5 threshold.
46
+ result = inspect_file(fixture_path("roadmap", "minimal.md"))
47
+ assert result.type == "roadmap"
48
+
49
+
50
+ def test_requirement_does_not_classify_as_roadmap():
51
+ # A Requirement has no Outcomes/Initiatives, so it never wins the roadmap slot.
52
+ assert inspect_file(fixture_path("valid", "feature.md")).type == "requirement"
53
+
54
+
55
+ def test_roadmap_carries_no_metadata():
56
+ # Roadmap manages knowledge, not work: no status/category/supersedes fields.
57
+ spec = spec_for("roadmap")
58
+ assert spec is not None
59
+ assert spec.metadata == {}
60
+
61
+
62
+ # --- validation -------------------------------------------------------------
63
+
64
+
65
+ def _codes(parts):
66
+ return {i.code for i in validate(parse_file(fixture_path(*parts)))}
67
+
68
+
69
+ def test_valid_roadmap_has_no_errors():
70
+ assert not has_errors(validate(parse_file(fixture_path("roadmap", "valid.md"))))
71
+
72
+
73
+ def test_minimal_roadmap_has_no_errors():
74
+ # Missing recommended sections must never fail validation (REQ-003).
75
+ assert not has_errors(validate(parse_file(fixture_path("roadmap", "minimal.md"))))
76
+
77
+
78
+ def test_roadmap_missing_required_section_fails():
79
+ codes = _codes(("roadmap", "missing_initiatives.md"))
80
+ assert "missing-initiatives" in codes
81
+
82
+
83
+ def test_roadmap_missing_title_fails(monkeypatch):
84
+ text = "## Outcomes\n\n- o\n\n## Initiatives\n\n- i\n"
85
+ issues = validate(parse(text))
86
+ assert "missing-title" in {i.code for i in issues}
87
+
88
+
89
+ # --- schema reference & template --------------------------------------------
90
+
91
+
92
+ def test_roadmap_is_a_registered_schema():
93
+ assert "roadmap" in available_schemas()
94
+
95
+
96
+ def test_schema_reference_shape():
97
+ ref = schema_reference("roadmap")
98
+ assert ref is not None
99
+ assert ref.required == ["outcomes", "initiatives"]
100
+ assert ref.recommended == ["success measures", "assumptions", "risks"]
101
+ assert ref.optional == ["related decisions", "related requirements"]
102
+
103
+
104
+ def test_schema_json_includes_optional_relationship_sections(capsys):
105
+ rc = main(["schema", "roadmap", "--json"])
106
+ assert rc == 0
107
+ payload = json.loads(capsys.readouterr().out)
108
+ assert payload["type"] == "roadmap"
109
+ assert payload["required"] == ["outcomes", "initiatives"]
110
+ assert payload["optional"] == ["related_decisions", "related_requirements"]
111
+
112
+
113
+ def test_schema_human_shows_optional_relationship_sections(capsys):
114
+ rc = main(["schema", "roadmap"])
115
+ assert rc == 0
116
+ out = capsys.readouterr().out
117
+ assert "Artifact Type: Roadmap" in out
118
+ assert "Related Decisions" in out
119
+
120
+
121
+ def test_template_omits_optional_relationship_sections(capsys):
122
+ rc = main(["schema", "roadmap", "--template"])
123
+ assert rc == 0
124
+ out = capsys.readouterr().out
125
+ assert "## Outcomes" in out
126
+ assert "## Initiatives" in out
127
+ assert "## Success Measures" in out
128
+ # Optional relationship sections stay out of the starter (v0.7.x territory).
129
+ assert "## Related Decisions" not in out
130
+ assert "## Related Requirements" not in out
131
+
132
+
133
+ def test_template_passes_validation(monkeypatch):
134
+ ref = schema_reference("roadmap")
135
+ assert ref is not None
136
+ from rac.outputs import render_schema_template
137
+
138
+ template = render_schema_template(ref)
139
+ _stdin(monkeypatch, template)
140
+ assert main(["validate", "-"]) == 0
141
+
142
+
143
+ # --- improvement (structural only) ------------------------------------------
144
+
145
+
146
+ def test_roadmap_is_supported_when_guidance_is_complete():
147
+ spec = spec_for("roadmap")
148
+ assert spec is not None
149
+ assert supports_improve(spec) is True
150
+
151
+
152
+ def test_complete_roadmap_has_nothing_to_improve():
153
+ result = improve_file(fixture_path("roadmap", "valid.md"))
154
+ assert result.type == "roadmap"
155
+ assert result.missing_required == []
156
+ assert result.missing_recommended == []
157
+
158
+
159
+ def test_minimal_roadmap_reports_missing_recommended():
160
+ result = improve_file(fixture_path("roadmap", "minimal.md"))
161
+ assert result.type == "roadmap"
162
+ assert result.missing_required == []
163
+ assert result.missing_recommended == ["success measures", "assumptions", "risks"]
164
+ assert result.guidance["success measures"]
165
+
166
+
167
+ def test_improve_json_shape_is_structural_only(capsys):
168
+ rc = main(["improve", fixture_path("roadmap", "minimal.md"), "--json"])
169
+ assert rc == 0
170
+ payload = json.loads(capsys.readouterr().out)
171
+ # Exactly the shared structural contract — no quality/score fields.
172
+ assert set(payload) == {
173
+ "type",
174
+ "missing_required",
175
+ "missing_recommended",
176
+ "guidance",
177
+ }
178
+ assert payload["type"] == "roadmap"
179
+ assert "success_measures" in payload["missing_recommended"]
180
+ assert payload["guidance"]["success_measures"] == [
181
+ "How will the team know the roadmap is succeeding?",
182
+ "What observable signals would show progress?",
183
+ ]
184
+ # risks stays single-line (REQ-006).
185
+ assert payload["guidance"]["risks"] == [
186
+ "What could prevent these outcomes from being achieved?"
187
+ ]
188
+
189
+
190
+ def test_improve_human_lists_missing_with_guidance(capsys):
191
+ rc = main(["improve", fixture_path("roadmap", "minimal.md")])
192
+ assert rc == 0
193
+ out = capsys.readouterr().out
194
+ assert "Artifact Type: Roadmap" in out
195
+ assert "Missing Recommended:" in out
196
+ assert "Success Measures" in out
197
+
198
+
199
+ def test_success_measures_guidance_is_enriched():
200
+ # v0.6.1 hardening: Success Measures carries both prompting questions
201
+ # (REQ-004/006). Enrichment flows from the ArtifactSpec, not a renderer.
202
+ spec = spec_for("roadmap")
203
+ assert spec is not None
204
+ assert spec.guidance["success measures"] == (
205
+ "How will the team know the roadmap is succeeding?",
206
+ "What observable signals would show progress?",
207
+ )
208
+
209
+
210
+ def test_improve_separates_missing_required_from_recommended():
211
+ # A roadmap with enough signal to classify (Outcomes + 3 recommended) but
212
+ # missing the required Initiatives section reports them in separate buckets.
213
+ text = (
214
+ "# R\n\n## Outcomes\n\n- o\n\n## Success Measures\n\n- m\n\n"
215
+ "## Assumptions\n\n- a\n\n## Risks\n\n- r\n"
216
+ )
217
+ result = improve_text(text)
218
+ assert result.type == "roadmap"
219
+ assert result.missing_required == ["initiatives"]
220
+ assert result.missing_recommended == []
221
+
222
+
223
+ def test_improve_template_orders_required_before_recommended(monkeypatch, capsys):
224
+ # Only Outcomes present -> Initiatives (required) must precede the recommended
225
+ # sections in the emitted template (REQ-005).
226
+ text = (
227
+ "# R\n\n## Outcomes\n\n- o\n\n## Assumptions\n\n- a\n\n## Risks\n\n- r\n"
228
+ )
229
+ _stdin(monkeypatch, text)
230
+ rc = main(["improve", "-", "--template"])
231
+ assert rc == 0
232
+ out = capsys.readouterr().out
233
+ assert out.index("## Initiatives") < out.index("## Success Measures")
234
+
235
+
236
+ def test_improve_does_not_modify_the_roadmap(tmp_path):
237
+ f = tmp_path / "roadmap.md"
238
+ content = "# R\n\n## Outcomes\n\n- o\n\n## Initiatives\n\n- i\n"
239
+ f.write_text(content)
240
+ before = f.stat().st_mtime_ns
241
+ main(["improve", str(f), "--template"])
242
+ assert f.read_text() == content
243
+ assert f.stat().st_mtime_ns == before
244
+
245
+
246
+ # --- inspection CLI ---------------------------------------------------------
247
+
248
+
249
+ def test_cli_inspect_human(capsys):
250
+ rc = main(["inspect", fixture_path("roadmap", "valid.md")])
251
+ assert rc == 0
252
+ out = capsys.readouterr().out
253
+ assert "Artifact Type: Roadmap" in out
254
+
255
+
256
+ def test_cli_inspect_json(capsys):
257
+ rc = main(["inspect", fixture_path("roadmap", "valid.md"), "--json"])
258
+ assert rc == 0
259
+ payload = json.loads(capsys.readouterr().out)
260
+ assert payload["type"] == "roadmap"
261
+ # No decision-style metadata leaks onto a roadmap.
262
+ assert "status" not in payload
263
+ assert "category" not in payload
264
+
265
+
266
+ def test_cli_validate_invalid_roadmap_exits_one(capsys):
267
+ rc = main(["validate", fixture_path("roadmap", "missing_initiatives.md")])
268
+ assert rc == 1
269
+
270
+
271
+ def test_cli_validate_valid_roadmap_exits_zero():
272
+ assert main(["validate", fixture_path("roadmap", "valid.md")]) == 0
273
+
274
+
275
+ # --- statistics: separated aggregation --------------------------------------
276
+
277
+
278
+ def test_stats_counts_roadmaps_separately():
279
+ s = collect_stats(fixture_path("roadmap"))
280
+ assert s.roadmap_count == 3
281
+ # Roadmaps never count as requirement features.
282
+ assert s.files_found == 0
283
+ assert s.total_requirements == 0
284
+ assert s.valid_roadmaps == 2 # valid.md + minimal.md
285
+ assert len(s.invalid_roadmaps) == 1
286
+ assert s.invalid_roadmaps[0].path.endswith("missing_initiatives.md")
287
+
288
+
289
+ def test_stats_human_shows_roadmaps_section(capsys):
290
+ rc = main(["stats", fixture_path("roadmap")])
291
+ assert rc == 0
292
+ out = capsys.readouterr().out
293
+ assert "Roadmaps" in out
294
+ assert "Total: 3" in out
295
+ assert "Valid: 2" in out
296
+ assert "Invalid Roadmaps (1)" in out
297
+
298
+
299
+ def test_stats_json_includes_roadmaps_block(capsys):
300
+ rc = main(["stats", fixture_path("roadmap"), "--json"])
301
+ assert rc == 0
302
+ payload = json.loads(capsys.readouterr().out)
303
+ assert payload["roadmaps"]["count"] == 3
304
+ assert payload["roadmaps"]["valid"] == 2
305
+ assert payload["roadmaps"]["invalid"][0]["file"].endswith("missing_initiatives.md")
306
+
307
+
308
+ # --- regression: existing artifact behavior is unchanged (Amendment 6) ------
309
+
310
+
311
+ def test_requirement_validation_unchanged():
312
+ # The original Requirement rules still fire exactly as before.
313
+ assert not has_errors(validate(parse_file(fixture_path("valid", "feature.md"))))
314
+ assert "missing-title" in _codes(("invalid", "missing_title.md"))
315
+ assert "missing-requirements" in _codes(("invalid", "missing_requirements.md"))
316
+
317
+
318
+ def test_decision_validation_unchanged():
319
+ # A Decision still routes to the Decision validator, not roadmap/requirement.
320
+ product = parse_file(fixture_path("decision", "with_metadata.md"))
321
+ assert classify(product).type == "decision"
322
+ assert not has_errors(validate(product))
323
+
324
+
325
+ def test_requirement_only_stats_omit_roadmaps_block(capsys):
326
+ rc = main(["stats", fixture_path("portfolio"), "--json"])
327
+ assert rc == 0
328
+ payload = json.loads(capsys.readouterr().out)
329
+ # Additive guarantee: no roadmaps key when the portfolio has none.
330
+ assert "roadmaps" not in payload
331
+
332
+
333
+ def test_decision_stats_unchanged_and_omit_roadmaps(capsys):
334
+ rc = main(["stats", fixture_path("decision", "portfolio"), "--json"])
335
+ assert rc == 0
336
+ payload = json.loads(capsys.readouterr().out)
337
+ assert payload["decisions"]["count"] == 3
338
+ assert "roadmaps" not in payload
339
+
340
+
341
+ # --- architecture: improvement stays ArtifactSpec-driven --------------------
342
+
343
+
344
+ def test_improve_support_is_artifact_spec_driven():
345
+ # The real v0.6.0 achievement: improve support is earned through complete
346
+ # ArtifactSpec guidance, via the shared pipeline — never per-type logic.
347
+ for spec in ARTIFACT_SPECS:
348
+ complete = set(spec.expected) <= set(spec.guidance)
349
+ assert supports_improve(spec) is complete
350
+
351
+ # No artifact-specific improve engine exists: the public surface is generic.
352
+ public = {name for name in vars(improve_mod) if not name.startswith("_")}
353
+ artifact_specific = {
354
+ name
355
+ for name in public
356
+ if any(t in name for t in ("roadmap", "requirement", "decision"))
357
+ }
358
+ assert artifact_specific == set()
359
+
360
+
361
+ def test_roadmap_guidance_has_no_work_management_fields():
362
+ # ADR-017 / REQ-008: guidance must not push work-management *fields* onto
363
+ # roadmaps. Field-oriented (not a broad word ban): normal strategic language
364
+ # like "milestone" or "timeline" stays allowed.
365
+ forbidden_fields = [
366
+ "owner:",
367
+ "assignee:",
368
+ "status:",
369
+ "sprint:",
370
+ "due date:",
371
+ "deadline:",
372
+ "priority:",
373
+ ]
374
+ spec = spec_for("roadmap")
375
+ assert spec is not None
376
+ blob = " ".join(
377
+ line for lines in spec.guidance.values() for line in lines
378
+ ).casefold()
379
+ for field in forbidden_fields:
380
+ assert field not in blob
@@ -15,7 +15,7 @@ from conftest import fixture_path
15
15
 
16
16
 
17
17
  def test_available_schemas_are_registered_artifacts():
18
- assert available_schemas() == ["requirement", "decision"]
18
+ assert available_schemas() == ["requirement", "decision", "roadmap"]
19
19
 
20
20
 
21
21
  def test_schema_reference_consumes_artifact_spec():
@@ -36,6 +36,7 @@ def test_schema_list_human(capsys):
36
36
  "Available Schemas:\n"
37
37
  "- requirement\n"
38
38
  "- decision\n"
39
+ "- roadmap\n"
39
40
  )
40
41
 
41
42
 
@@ -43,7 +44,7 @@ def test_schema_list_json(capsys):
43
44
  rc = main(["schema", "--list", "--json"])
44
45
  assert rc == 0
45
46
  payload = json.loads(capsys.readouterr().out)
46
- assert payload == {"schemas": ["requirement", "decision"]}
47
+ assert payload == {"schemas": ["requirement", "decision", "roadmap"]}
47
48
 
48
49
 
49
50
  def test_schema_human_requirement(capsys):
@@ -147,14 +148,16 @@ def test_decision_template_is_validation_safe(capsys, monkeypatch):
147
148
 
148
149
 
149
150
  def test_unknown_schema_exits_two_and_lists_available(capsys):
151
+ # "meeting" is a deferred type with no concrete schema yet (see rac/artifacts.py).
150
152
  with pytest.raises(SystemExit) as exc:
151
- main(["schema", "roadmap"])
153
+ main(["schema", "meeting"])
152
154
  assert exc.value.code == 2
153
155
  err = capsys.readouterr().err
154
- assert "Unknown schema: roadmap" in err
156
+ assert "Unknown schema: meeting" in err
155
157
  assert "Available schemas:" in err
156
158
  assert "- requirement" in err
157
159
  assert "- decision" in err
160
+ assert "- roadmap" in err
158
161
 
159
162
 
160
163
  def test_schema_requires_name_or_list(capsys):