requirements-as-code 0.5.2__tar.gz → 0.6.0__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.
- {requirements_as_code-0.5.2/requirements_as_code.egg-info → requirements_as_code-0.6.0}/PKG-INFO +1 -1
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/artifacts.py +46 -4
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/cli.py +9 -5
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/outputs.py +28 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/schema.py +3 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/stats.py +45 -3
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/validate.py +46 -4
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0/requirements_as_code.egg-info}/PKG-INFO +1 -1
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/SOURCES.txt +4 -0
- requirements_as_code-0.6.0/tests/fixtures/roadmap/minimal.md +9 -0
- requirements_as_code-0.6.0/tests/fixtures/roadmap/missing_initiatives.md +17 -0
- requirements_as_code-0.6.0/tests/fixtures/roadmap/valid.md +31 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_inspect.py +2 -2
- requirements_as_code-0.6.0/tests/test_roadmap.py +285 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_schema.py +7 -4
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/.github/workflows/python-publish.yml +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/.gitignore +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/LICENSE +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/README.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/examples/example_dashboard_v1.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/examples/example_dashboard_v2.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-001-markdown-first.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-002-ai-optional.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-003-structured-outputs-first.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-004-artifact-model.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-005-cli-first.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-006-ingest-over-rewrite.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-007-json-contract-stability.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-008-agent-ready-architecture.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-009-ai-assisted-development.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-010-documents-are-not-artifacts.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-011-file-first-pipeline.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-012-open-core-strategy.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-013-leverage-existing-source-control-systems.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-014-viewer-agnostic-knowledge-artifacts.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-015-explorer-as-consumer.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-016-relationships-as-structural-references.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-017-rac-managed-knowledge-not-work.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.0-workspace-analysis.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.1-review-engine.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.2-mcp-server.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.4-claude-skills.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.4-python-sdk.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/archive/v0.5-decisions.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/archive/v0.7-prompts.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.2-stats.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.3-ingest.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.3.1-formats.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.4-inspect.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.4.1-expansion.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.4.1-inspect-expansion.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.4.2-decision-metadata.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.5.0-artifact-improvement.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.5.1-guided-improvement.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.5.2-schema.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.6.0-roadmap-artifacts.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.6.1-roadmap-improvement.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.6.2-prompt-artifact.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.7.0-relationship-metadata.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.7.1-relationship-inspection.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.7.2-relationship-validation.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.8.0-explorer-foundation.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.8.1-explorer-experience.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.8.2-knowledge-operations.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/pyproject.toml +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/__init__.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/classification.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/diff.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/fs.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/improve.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/ingest.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/inspect.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/models.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/rac/parser.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/dependency_links.txt +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/entry_points.txt +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/requires.txt +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/top_level.txt +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/setup.cfg +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/conftest.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/bad_category.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/bad_status.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/minimal.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/portfolio/01_accepted_arch.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/portfolio/02_proposed_process.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/portfolio/03_no_metadata.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/with_metadata.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/diff/new.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/diff/old.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/ingest/sample.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/ambiguous.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/decision.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/nested/another_requirement.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/requirement.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/duplicate_ids.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/empty_req_text.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/malformed_id.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_id.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_problem.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_requirements.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_title.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/multiple_titles.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/broken.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/feature_a.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/feature_b.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/sub/feature_c.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/valid/bullet_requirements.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/valid/feature.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/valid/minimal.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/valid/warnings.md +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_cli.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_decision_metadata.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_diff.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_improve.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_ingest.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_parser.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_stats.py +0 -0
- {requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/test_validate.py +0 -0
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,48 @@ 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
|
+
),
|
|
170
|
+
"assumptions": (
|
|
171
|
+
"What must be true for this roadmap to remain valid?",
|
|
172
|
+
),
|
|
173
|
+
"risks": (
|
|
174
|
+
"What could prevent these outcomes from being achieved?",
|
|
175
|
+
),
|
|
176
|
+
},
|
|
177
|
+
# Artifact-scoped: this only normalizes "success metrics" when scoring a
|
|
178
|
+
# document against the Roadmap spec (see rac.classification._mapped), so it
|
|
179
|
+
# never affects the Requirement spec's canonical "success metrics" section.
|
|
180
|
+
synonyms={
|
|
181
|
+
"success metrics": "success measures",
|
|
182
|
+
},
|
|
183
|
+
),
|
|
142
184
|
)
|
|
143
185
|
|
|
144
186
|
|
|
@@ -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
|
|
94
|
-
# the run on their own. (A future --strict flag will fail the run if
|
|
95
|
-
# file is invalid, for CI use.)
|
|
96
|
-
has_content =
|
|
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
|
|
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
|
|
8
|
-
distorts
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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] = []
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/SOURCES.txt
RENAMED
|
@@ -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,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
|
|
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,285 @@
|
|
|
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
|
+
from rac.artifacts import spec_for
|
|
18
|
+
from rac.cli import main
|
|
19
|
+
from rac.classification import classify
|
|
20
|
+
from rac.improve import improve_file, supports_improve
|
|
21
|
+
from rac.inspect import inspect_file
|
|
22
|
+
from rac.parser import parse, parse_file
|
|
23
|
+
from rac.schema import available_schemas, schema_reference
|
|
24
|
+
from rac.stats import collect_stats
|
|
25
|
+
from rac.validate import has_errors, validate
|
|
26
|
+
|
|
27
|
+
from conftest import fixture_path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _stdin(monkeypatch, text: str) -> None:
|
|
31
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(text))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --- classification ---------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_roadmap_classifies_as_roadmap():
|
|
38
|
+
result = inspect_file(fixture_path("roadmap", "valid.md"))
|
|
39
|
+
assert result.type == "roadmap"
|
|
40
|
+
assert result.confidence >= 0.5
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_minimal_roadmap_classifies_on_required_sections_alone():
|
|
44
|
+
# Outcomes + Initiatives only -> 2/3.5 fit, above the 0.5 threshold.
|
|
45
|
+
result = inspect_file(fixture_path("roadmap", "minimal.md"))
|
|
46
|
+
assert result.type == "roadmap"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_requirement_does_not_classify_as_roadmap():
|
|
50
|
+
# A Requirement has no Outcomes/Initiatives, so it never wins the roadmap slot.
|
|
51
|
+
assert inspect_file(fixture_path("valid", "feature.md")).type == "requirement"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_roadmap_carries_no_metadata():
|
|
55
|
+
# Roadmap manages knowledge, not work: no status/category/supersedes fields.
|
|
56
|
+
spec = spec_for("roadmap")
|
|
57
|
+
assert spec is not None
|
|
58
|
+
assert spec.metadata == {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- validation -------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _codes(parts):
|
|
65
|
+
return {i.code for i in validate(parse_file(fixture_path(*parts)))}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_valid_roadmap_has_no_errors():
|
|
69
|
+
assert not has_errors(validate(parse_file(fixture_path("roadmap", "valid.md"))))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_minimal_roadmap_has_no_errors():
|
|
73
|
+
# Missing recommended sections must never fail validation (REQ-003).
|
|
74
|
+
assert not has_errors(validate(parse_file(fixture_path("roadmap", "minimal.md"))))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_roadmap_missing_required_section_fails():
|
|
78
|
+
codes = _codes(("roadmap", "missing_initiatives.md"))
|
|
79
|
+
assert "missing-initiatives" in codes
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_roadmap_missing_title_fails(monkeypatch):
|
|
83
|
+
text = "## Outcomes\n\n- o\n\n## Initiatives\n\n- i\n"
|
|
84
|
+
issues = validate(parse(text))
|
|
85
|
+
assert "missing-title" in {i.code for i in issues}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --- schema reference & template --------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_roadmap_is_a_registered_schema():
|
|
92
|
+
assert "roadmap" in available_schemas()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_schema_reference_shape():
|
|
96
|
+
ref = schema_reference("roadmap")
|
|
97
|
+
assert ref is not None
|
|
98
|
+
assert ref.required == ["outcomes", "initiatives"]
|
|
99
|
+
assert ref.recommended == ["success measures", "assumptions", "risks"]
|
|
100
|
+
assert ref.optional == ["related decisions", "related requirements"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_schema_json_includes_optional_relationship_sections(capsys):
|
|
104
|
+
rc = main(["schema", "roadmap", "--json"])
|
|
105
|
+
assert rc == 0
|
|
106
|
+
payload = json.loads(capsys.readouterr().out)
|
|
107
|
+
assert payload["type"] == "roadmap"
|
|
108
|
+
assert payload["required"] == ["outcomes", "initiatives"]
|
|
109
|
+
assert payload["optional"] == ["related_decisions", "related_requirements"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_schema_human_shows_optional_relationship_sections(capsys):
|
|
113
|
+
rc = main(["schema", "roadmap"])
|
|
114
|
+
assert rc == 0
|
|
115
|
+
out = capsys.readouterr().out
|
|
116
|
+
assert "Artifact Type: Roadmap" in out
|
|
117
|
+
assert "Related Decisions" in out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_template_omits_optional_relationship_sections(capsys):
|
|
121
|
+
rc = main(["schema", "roadmap", "--template"])
|
|
122
|
+
assert rc == 0
|
|
123
|
+
out = capsys.readouterr().out
|
|
124
|
+
assert "## Outcomes" in out
|
|
125
|
+
assert "## Initiatives" in out
|
|
126
|
+
assert "## Success Measures" in out
|
|
127
|
+
# Optional relationship sections stay out of the starter (v0.7.x territory).
|
|
128
|
+
assert "## Related Decisions" not in out
|
|
129
|
+
assert "## Related Requirements" not in out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_template_passes_validation(monkeypatch):
|
|
133
|
+
ref = schema_reference("roadmap")
|
|
134
|
+
assert ref is not None
|
|
135
|
+
from rac.outputs import render_schema_template
|
|
136
|
+
|
|
137
|
+
template = render_schema_template(ref)
|
|
138
|
+
_stdin(monkeypatch, template)
|
|
139
|
+
assert main(["validate", "-"]) == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- improvement (structural only) ------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_roadmap_is_supported_when_guidance_is_complete():
|
|
146
|
+
spec = spec_for("roadmap")
|
|
147
|
+
assert spec is not None
|
|
148
|
+
assert supports_improve(spec) is True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_complete_roadmap_has_nothing_to_improve():
|
|
152
|
+
result = improve_file(fixture_path("roadmap", "valid.md"))
|
|
153
|
+
assert result.type == "roadmap"
|
|
154
|
+
assert result.missing_required == []
|
|
155
|
+
assert result.missing_recommended == []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_minimal_roadmap_reports_missing_recommended():
|
|
159
|
+
result = improve_file(fixture_path("roadmap", "minimal.md"))
|
|
160
|
+
assert result.type == "roadmap"
|
|
161
|
+
assert result.missing_required == []
|
|
162
|
+
assert result.missing_recommended == ["success measures", "assumptions", "risks"]
|
|
163
|
+
assert result.guidance["success measures"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_improve_json_shape_is_structural_only(capsys):
|
|
167
|
+
rc = main(["improve", fixture_path("roadmap", "minimal.md"), "--json"])
|
|
168
|
+
assert rc == 0
|
|
169
|
+
payload = json.loads(capsys.readouterr().out)
|
|
170
|
+
# Exactly the shared structural contract — no quality/score fields.
|
|
171
|
+
assert set(payload) == {
|
|
172
|
+
"type",
|
|
173
|
+
"missing_required",
|
|
174
|
+
"missing_recommended",
|
|
175
|
+
"guidance",
|
|
176
|
+
}
|
|
177
|
+
assert payload["type"] == "roadmap"
|
|
178
|
+
assert "success_measures" in payload["missing_recommended"]
|
|
179
|
+
assert payload["guidance"]["success_measures"] == [
|
|
180
|
+
"How will the team know the roadmap is succeeding?"
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_improve_human_lists_missing_with_guidance(capsys):
|
|
185
|
+
rc = main(["improve", fixture_path("roadmap", "minimal.md")])
|
|
186
|
+
assert rc == 0
|
|
187
|
+
out = capsys.readouterr().out
|
|
188
|
+
assert "Artifact Type: Roadmap" in out
|
|
189
|
+
assert "Missing Recommended:" in out
|
|
190
|
+
assert "Success Measures" in out
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --- inspection CLI ---------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_cli_inspect_human(capsys):
|
|
197
|
+
rc = main(["inspect", fixture_path("roadmap", "valid.md")])
|
|
198
|
+
assert rc == 0
|
|
199
|
+
out = capsys.readouterr().out
|
|
200
|
+
assert "Artifact Type: Roadmap" in out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_cli_inspect_json(capsys):
|
|
204
|
+
rc = main(["inspect", fixture_path("roadmap", "valid.md"), "--json"])
|
|
205
|
+
assert rc == 0
|
|
206
|
+
payload = json.loads(capsys.readouterr().out)
|
|
207
|
+
assert payload["type"] == "roadmap"
|
|
208
|
+
# No decision-style metadata leaks onto a roadmap.
|
|
209
|
+
assert "status" not in payload
|
|
210
|
+
assert "category" not in payload
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_cli_validate_invalid_roadmap_exits_one(capsys):
|
|
214
|
+
rc = main(["validate", fixture_path("roadmap", "missing_initiatives.md")])
|
|
215
|
+
assert rc == 1
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_cli_validate_valid_roadmap_exits_zero():
|
|
219
|
+
assert main(["validate", fixture_path("roadmap", "valid.md")]) == 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# --- statistics: separated aggregation --------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_stats_counts_roadmaps_separately():
|
|
226
|
+
s = collect_stats(fixture_path("roadmap"))
|
|
227
|
+
assert s.roadmap_count == 3
|
|
228
|
+
# Roadmaps never count as requirement features.
|
|
229
|
+
assert s.files_found == 0
|
|
230
|
+
assert s.total_requirements == 0
|
|
231
|
+
assert s.valid_roadmaps == 2 # valid.md + minimal.md
|
|
232
|
+
assert len(s.invalid_roadmaps) == 1
|
|
233
|
+
assert s.invalid_roadmaps[0].path.endswith("missing_initiatives.md")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_stats_human_shows_roadmaps_section(capsys):
|
|
237
|
+
rc = main(["stats", fixture_path("roadmap")])
|
|
238
|
+
assert rc == 0
|
|
239
|
+
out = capsys.readouterr().out
|
|
240
|
+
assert "Roadmaps" in out
|
|
241
|
+
assert "Total: 3" in out
|
|
242
|
+
assert "Valid: 2" in out
|
|
243
|
+
assert "Invalid Roadmaps (1)" in out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_stats_json_includes_roadmaps_block(capsys):
|
|
247
|
+
rc = main(["stats", fixture_path("roadmap"), "--json"])
|
|
248
|
+
assert rc == 0
|
|
249
|
+
payload = json.loads(capsys.readouterr().out)
|
|
250
|
+
assert payload["roadmaps"]["count"] == 3
|
|
251
|
+
assert payload["roadmaps"]["valid"] == 2
|
|
252
|
+
assert payload["roadmaps"]["invalid"][0]["file"].endswith("missing_initiatives.md")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# --- regression: existing artifact behavior is unchanged (Amendment 6) ------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_requirement_validation_unchanged():
|
|
259
|
+
# The original Requirement rules still fire exactly as before.
|
|
260
|
+
assert not has_errors(validate(parse_file(fixture_path("valid", "feature.md"))))
|
|
261
|
+
assert "missing-title" in _codes(("invalid", "missing_title.md"))
|
|
262
|
+
assert "missing-requirements" in _codes(("invalid", "missing_requirements.md"))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_decision_validation_unchanged():
|
|
266
|
+
# A Decision still routes to the Decision validator, not roadmap/requirement.
|
|
267
|
+
product = parse_file(fixture_path("decision", "with_metadata.md"))
|
|
268
|
+
assert classify(product).type == "decision"
|
|
269
|
+
assert not has_errors(validate(product))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_requirement_only_stats_omit_roadmaps_block(capsys):
|
|
273
|
+
rc = main(["stats", fixture_path("portfolio"), "--json"])
|
|
274
|
+
assert rc == 0
|
|
275
|
+
payload = json.loads(capsys.readouterr().out)
|
|
276
|
+
# Additive guarantee: no roadmaps key when the portfolio has none.
|
|
277
|
+
assert "roadmaps" not in payload
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_decision_stats_unchanged_and_omit_roadmaps(capsys):
|
|
281
|
+
rc = main(["stats", fixture_path("decision", "portfolio"), "--json"])
|
|
282
|
+
assert rc == 0
|
|
283
|
+
payload = json.loads(capsys.readouterr().out)
|
|
284
|
+
assert payload["decisions"]["count"] == 3
|
|
285
|
+
assert "roadmaps" not in payload
|
|
@@ -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", "
|
|
153
|
+
main(["schema", "meeting"])
|
|
152
154
|
assert exc.value.code == 2
|
|
153
155
|
err = capsys.readouterr().err
|
|
154
|
-
assert "Unknown schema:
|
|
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):
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-001-markdown-first.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-002-ai-optional.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-004-artifact-model.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/adr/adr-012-open-core-strategy.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.0-workspace-analysis.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.1-review-engine.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.2-mcp-server.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.4-claude-skills.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/future/v1.4-python-sdk.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/archive/v0.5-decisions.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/archive/v0.7-prompts.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.3.1-formats.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.4.1-expansion.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/planning/roadmap/v0.6.2-prompt-artifact.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/requirements_as_code.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/bad_category.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/bad_status.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/minimal.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/decision/with_metadata.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/ambiguous.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/decision.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/inspect/requirement.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/duplicate_ids.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/empty_req_text.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/malformed_id.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_id.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_problem.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/missing_title.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/invalid/multiple_titles.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/broken.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/feature_a.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/feature_b.md
RENAMED
|
File without changes
|
{requirements_as_code-0.5.2 → requirements_as_code-0.6.0}/tests/fixtures/portfolio/sub/feature_c.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|