requirements-as-code 0.7.0__tar.gz → 0.7.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 (151) hide show
  1. {requirements_as_code-0.7.0/requirements_as_code.egg-info → requirements_as_code-0.7.1}/PKG-INFO +73 -1
  2. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/README.md +72 -0
  3. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/artifacts.py +5 -17
  4. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/cli.py +58 -1
  5. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/outputs.py +56 -0
  6. requirements_as_code-0.7.1/rac/relationships.py +241 -0
  7. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/stats.py +2 -2
  8. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1/requirements_as_code.egg-info}/PKG-INFO +73 -1
  9. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/SOURCES.txt +3 -0
  10. requirements_as_code-0.7.1/tests/fixtures/no_relationships/decision.md +22 -0
  11. requirements_as_code-0.7.1/tests/fixtures/no_relationships/requirement.md +17 -0
  12. requirements_as_code-0.7.1/tests/test_relationships_cmd.py +233 -0
  13. requirements_as_code-0.7.0/rac/relationships.py +0 -86
  14. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/.github/workflows/python-publish.yml +0 -0
  15. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/.gitignore +0 -0
  16. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/LICENSE +0 -0
  17. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/examples/example_dashboard_v1.md +0 -0
  18. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/examples/example_dashboard_v2.md +0 -0
  19. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-001-markdown-first.md +0 -0
  20. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-002-ai-optional.md +0 -0
  21. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-003-structured-outputs-first.md +0 -0
  22. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-004-artifact-model.md +0 -0
  23. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-005-cli-first.md +0 -0
  24. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-006-ingest-over-rewrite.md +0 -0
  25. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-007-json-contract-stability.md +0 -0
  26. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-008-agent-ready-architecture.md +0 -0
  27. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-009-ai-assisted-development.md +0 -0
  28. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-010-documents-are-not-artifacts.md +0 -0
  29. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-011-file-first-pipeline.md +0 -0
  30. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-012-open-core-strategy.md +0 -0
  31. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-013-leverage-existing-source-control-systems.md +0 -0
  32. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-014-viewer-agnostic-knowledge-artifacts.md +0 -0
  33. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-015-explorer-as-consumer.md +0 -0
  34. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-016-relationships-as-structural-references.md +0 -0
  35. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-017-rac-managed-knowledge-not-work.md +0 -0
  36. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-command-surface.md +0 -0
  37. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-first-run-experience.md +0 -0
  38. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-health-model.md +0 -0
  39. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-import-workflow.md +0 -0
  40. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-knowledge-graph.md +0 -0
  41. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-visual-system.md +0 -0
  42. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.0-workspace-analysis.md +0 -0
  43. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.1-review-engine.md +0 -0
  44. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.2-mcp-server.md +0 -0
  45. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.4-claude-skills.md +0 -0
  46. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.4-python-sdk.md +0 -0
  47. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/archive/v0.5-decisions.md +0 -0
  48. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/archive/v0.7-prompts.md +0 -0
  49. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.2-stats.md +0 -0
  50. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.3-ingest.md +0 -0
  51. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.3.1-formats.md +0 -0
  52. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4-inspect.md +0 -0
  53. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4.1-expansion.md +0 -0
  54. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4.1-inspect-expansion.md +0 -0
  55. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4.2-decision-metadata.md +0 -0
  56. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.5.0-artifact-improvement.md +0 -0
  57. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.5.1-guided-improvement.md +0 -0
  58. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.5.2-schema.md +0 -0
  59. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.0-roadmap-artifacts.md +0 -0
  60. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.1-roadmap-improvement.md +0 -0
  61. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.2-prompt-artifact.md +0 -0
  62. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.3-design-artifacts.md +0 -0
  63. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.0-relationship-metadata.md +0 -0
  64. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.1-relationship-inspection.md +0 -0
  65. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.2-relationship-validation.md +0 -0
  66. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.3-repo-intelligence.md +0 -0
  67. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.4-repo-indexing.md +0 -0
  68. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.0-service-api.md +0 -0
  69. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.1-repository-model.md +0 -0
  70. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.2-interactive-runtime-readiness.md +0 -0
  71. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.3-integration-freeze.md +0 -0
  72. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.0-explorer-foundation.md +0 -0
  73. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.1-explorer-experience.md +0 -0
  74. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.2-knowledge-operations.md +0 -0
  75. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.3-intelligence-views.md +0 -0
  76. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/pyproject.toml +0 -0
  77. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/__init__.py +0 -0
  78. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/classification.py +0 -0
  79. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/diff.py +0 -0
  80. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/fs.py +0 -0
  81. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/improve.py +0 -0
  82. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/ingest.py +0 -0
  83. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/inspect.py +0 -0
  84. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/models.py +0 -0
  85. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/parser.py +0 -0
  86. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/schema.py +0 -0
  87. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/validate.py +0 -0
  88. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/dependency_links.txt +0 -0
  89. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/entry_points.txt +0 -0
  90. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/requires.txt +0 -0
  91. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/top_level.txt +0 -0
  92. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/setup.cfg +0 -0
  93. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/conftest.py +0 -0
  94. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/bad_category.md +0 -0
  95. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/bad_status.md +0 -0
  96. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/minimal.md +0 -0
  97. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/portfolio/01_accepted_arch.md +0 -0
  98. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/portfolio/02_proposed_process.md +0 -0
  99. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/portfolio/03_no_metadata.md +0 -0
  100. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/with_metadata.md +0 -0
  101. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/design/minimal.md +0 -0
  102. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/design/missing_constraints.md +0 -0
  103. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/design/valid.md +0 -0
  104. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/diff/new.md +0 -0
  105. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/diff/old.md +0 -0
  106. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/ingest/sample.md +0 -0
  107. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/ambiguous.md +0 -0
  108. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/decision.md +0 -0
  109. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/nested/another_requirement.md +0 -0
  110. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/requirement.md +0 -0
  111. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/duplicate_ids.md +0 -0
  112. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/empty_req_text.md +0 -0
  113. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/malformed_id.md +0 -0
  114. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_id.md +0 -0
  115. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_problem.md +0 -0
  116. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_requirements.md +0 -0
  117. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_title.md +0 -0
  118. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/multiple_titles.md +0 -0
  119. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/broken.md +0 -0
  120. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/feature_a.md +0 -0
  121. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/feature_b.md +0 -0
  122. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/sub/feature_c.md +0 -0
  123. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/prompt/minimal.md +0 -0
  124. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/prompt/missing_output.md +0 -0
  125. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/prompt/valid.md +0 -0
  126. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/decision_with_links.md +0 -0
  127. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/design_with_links.md +0 -0
  128. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/prompt_with_links.md +0 -0
  129. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/requirement_with_links.md +0 -0
  130. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/roadmap_with_links.md +0 -0
  131. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/roadmap/minimal.md +0 -0
  132. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/roadmap/missing_initiatives.md +0 -0
  133. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/roadmap/valid.md +0 -0
  134. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/bullet_requirements.md +0 -0
  135. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/feature.md +0 -0
  136. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/minimal.md +0 -0
  137. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/warnings.md +0 -0
  138. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_cli.py +0 -0
  139. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_decision_metadata.py +0 -0
  140. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_design.py +0 -0
  141. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_diff.py +0 -0
  142. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_improve.py +0 -0
  143. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_ingest.py +0 -0
  144. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_inspect.py +0 -0
  145. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_parser.py +0 -0
  146. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_prompt.py +0 -0
  147. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_relationships.py +0 -0
  148. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_roadmap.py +0 -0
  149. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_schema.py +0 -0
  150. {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_stats.py +0 -0
  151. {requirements_as_code-0.7.0 → requirements_as_code-0.7.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.7.0
3
+ Version: 0.7.1
4
4
  Summary: RAC — lint and diff product requirements written in Markdown.
5
5
  Author: tcballard
6
6
  License-Expression: MIT
@@ -863,6 +863,78 @@ registered schemas are supported; custom schemas are out of scope.
863
863
 
864
864
  ---
865
865
 
866
+ ## Relationships
867
+
868
+ Inspect the explicit [relationship metadata](#relationship-metadata) declared
869
+ across a repository — a deterministic, read-only view of how artifacts reference
870
+ one another, without opening every file.
871
+
872
+ ```bash
873
+ rac relationships planning/
874
+ rac relationships planning/ --json
875
+ rac relationships planning/ --top-level # don't recurse into subdirectories
876
+ rac relationships requirements/search.md # a single file works too
877
+ ```
878
+
879
+ ```text
880
+ Relationships
881
+
882
+ Files Inspected: 24
883
+ Artifacts With Relationships: 8
884
+ Relationships Found: 14
885
+
886
+ By Type:
887
+ - Related Requirements: 4
888
+ - Related Decisions: 6
889
+ - Related Roadmaps: 2
890
+ - Related Prompts: 1
891
+ - Supersedes: 1
892
+
893
+ requirements/search.md
894
+ Related Decisions:
895
+ - ADR-004
896
+ ```
897
+
898
+ `--json` returns structured data for automation and future viewers (ADR-015 —
899
+ relationship intelligence lives in RAC Core, not the UI):
900
+
901
+ ```json
902
+ {
903
+ "directory": "planning",
904
+ "recursive": true,
905
+ "total_files": 24,
906
+ "artifacts_with_relationships": 8,
907
+ "relationship_count": 14,
908
+ "counts": { "related_decisions": 6, "related_requirements": 4 },
909
+ "artifacts": [
910
+ {
911
+ "path": "requirements/search.md",
912
+ "type": "requirement",
913
+ "relationships": { "related_decisions": ["ADR-004"] }
914
+ }
915
+ ]
916
+ }
917
+ ```
918
+
919
+ Notes:
920
+
921
+ - **Counts are individual references.** `relationship_count` equals
922
+ `sum(counts.values())`; an artifact listing two decisions contributes two.
923
+ - **`total_files`** counts every Markdown file considered — including files with
924
+ no relationships and Unknown artifacts.
925
+ - **Supersedes is a relationship here.** Unlike `rac inspect` (where `supersedes`
926
+ is a top-level scalar), this command reports it alongside the `related_*`
927
+ sections, in both `counts` and per-artifact `relationships`.
928
+ - **Spec-driven.** Extraction is keyed off each artifact type's declared sections.
929
+ Unknown artifacts are counted but contribute no relationships (and never appear
930
+ in `artifacts`); RAC does not scan arbitrary Markdown.
931
+ - **Read-only and exit `0`.** The command never modifies files and exits `0`
932
+ whether or not relationships are found (`2` only for a missing path or a
933
+ non-Markdown file). It does **not** resolve or validate targets — broken-link
934
+ detection is a later release.
935
+
936
+ ---
937
+
866
938
  ## Review (Planned)
867
939
 
868
940
  AI-assisted product review.
@@ -823,6 +823,78 @@ registered schemas are supported; custom schemas are out of scope.
823
823
 
824
824
  ---
825
825
 
826
+ ## Relationships
827
+
828
+ Inspect the explicit [relationship metadata](#relationship-metadata) declared
829
+ across a repository — a deterministic, read-only view of how artifacts reference
830
+ one another, without opening every file.
831
+
832
+ ```bash
833
+ rac relationships planning/
834
+ rac relationships planning/ --json
835
+ rac relationships planning/ --top-level # don't recurse into subdirectories
836
+ rac relationships requirements/search.md # a single file works too
837
+ ```
838
+
839
+ ```text
840
+ Relationships
841
+
842
+ Files Inspected: 24
843
+ Artifacts With Relationships: 8
844
+ Relationships Found: 14
845
+
846
+ By Type:
847
+ - Related Requirements: 4
848
+ - Related Decisions: 6
849
+ - Related Roadmaps: 2
850
+ - Related Prompts: 1
851
+ - Supersedes: 1
852
+
853
+ requirements/search.md
854
+ Related Decisions:
855
+ - ADR-004
856
+ ```
857
+
858
+ `--json` returns structured data for automation and future viewers (ADR-015 —
859
+ relationship intelligence lives in RAC Core, not the UI):
860
+
861
+ ```json
862
+ {
863
+ "directory": "planning",
864
+ "recursive": true,
865
+ "total_files": 24,
866
+ "artifacts_with_relationships": 8,
867
+ "relationship_count": 14,
868
+ "counts": { "related_decisions": 6, "related_requirements": 4 },
869
+ "artifacts": [
870
+ {
871
+ "path": "requirements/search.md",
872
+ "type": "requirement",
873
+ "relationships": { "related_decisions": ["ADR-004"] }
874
+ }
875
+ ]
876
+ }
877
+ ```
878
+
879
+ Notes:
880
+
881
+ - **Counts are individual references.** `relationship_count` equals
882
+ `sum(counts.values())`; an artifact listing two decisions contributes two.
883
+ - **`total_files`** counts every Markdown file considered — including files with
884
+ no relationships and Unknown artifacts.
885
+ - **Supersedes is a relationship here.** Unlike `rac inspect` (where `supersedes`
886
+ is a top-level scalar), this command reports it alongside the `related_*`
887
+ sections, in both `counts` and per-artifact `relationships`.
888
+ - **Spec-driven.** Extraction is keyed off each artifact type's declared sections.
889
+ Unknown artifacts are counted but contribute no relationships (and never appear
890
+ in `artifacts`); RAC does not scan arbitrary Markdown.
891
+ - **Read-only and exit `0`.** The command never modifies files and exits `0`
892
+ whether or not relationships are found (`2` only for a missing path or a
893
+ non-Markdown file). It does **not** resolve or validate targets — broken-link
894
+ detection is a later release.
895
+
896
+ ---
897
+
826
898
  ## Review (Planned)
827
899
 
828
900
  AI-assisted product review.
@@ -68,23 +68,11 @@ class ArtifactSpec:
68
68
  # extracted, but never scored, never templated, never reported as missing
69
69
  # (REQ-002). v0.7.0 treats them as metadata only — RAC extracts and counts the
70
70
  # references but does not resolve, validate, or graph them.
71
-
72
- # The cross-artifact "Related X" sections. These populate the ``relationships``
73
- # dict in ``rac inspect`` output. ``related designs`` is included so every peer
74
- # artifact type can be referenced.
75
- RELATED_SECTIONS: tuple[str, ...] = (
76
- "related requirements",
77
- "related decisions",
78
- "related roadmaps",
79
- "related prompts",
80
- "related designs",
81
- )
82
-
83
- # The full relationship-section vocabulary, including ``supersedes``. ``stats``
84
- # counts presence across all of these. ``supersedes`` is the one exception that
85
- # does *not* appear in the inspect ``relationships`` dict: it remains a top-level
86
- # scalar field for backwards compatibility (see rac.inspect / ADR-007).
87
- RELATIONSHIP_SECTIONS: tuple[str, ...] = RELATED_SECTIONS + ("supersedes",)
71
+ #
72
+ # The relationship-section vocabulary and its canonical ordering live in
73
+ # :mod:`rac.relationships` (the module that owns relationship logic). This module
74
+ # only owns the human-facing ``descriptions`` for those sections, which the specs
75
+ # below consume.
88
76
 
89
77
  # One-line descriptions for every relationship section, surfaced by `rac schema`.
90
78
  # Relationship sections deliberately carry no ``guidance`` — guidance gates
@@ -8,9 +8,11 @@ Commands:
8
8
  rac inspect <file.md | -> [--json]
9
9
  rac improve <file.md | -> [--json | --template]
10
10
  rac schema [--list] [type] [--json | --template]
11
+ rac relationships <dir | file.md> [--json] [--top-level]
11
12
 
12
13
  Exit codes:
13
- 0 success (incl. inspect/improve reporting Unknown)
14
+ 0 success (incl. inspect/improve reporting Unknown; relationships found or
15
+ not)
14
16
  1 validate: errors found; stats: no valid known artifacts; ingest:
15
17
  conversion failed
16
18
  2 usage / IO error (file not found, not a directory, unsupported type,
@@ -31,6 +33,10 @@ from .classification import score_artifacts
31
33
  from .improve import improve_product
32
34
  from .inspect import build_inspection, inspect_directory
33
35
  from .parser import parse, parse_file
36
+ from .relationships import (
37
+ build_relationship_report,
38
+ build_relationship_report_file,
39
+ )
34
40
  from .schema import available_schemas, schema_reference
35
41
  from .stats import collect_stats
36
42
  from .validate import has_errors, validate
@@ -236,6 +242,34 @@ def cmd_schema(args: argparse.Namespace) -> int:
236
242
  return EXIT_OK
237
243
 
238
244
 
245
+ def cmd_relationships(args: argparse.Namespace) -> int:
246
+ path = Path(args.path)
247
+ if path.is_dir():
248
+ # --recursive is the default; --top-level disables it. If both are given,
249
+ # --top-level wins (mirrors `rac inspect`).
250
+ report = build_relationship_report(args.path, recursive=not args.top_level)
251
+ elif path.is_file():
252
+ if path.suffix.lower() not in (".md", ".markdown"):
253
+ print(
254
+ f"rac: relationships expects a Markdown file or directory; "
255
+ f"convert it first with: rac ingest {args.path}",
256
+ file=sys.stderr,
257
+ )
258
+ raise SystemExit(EXIT_USAGE)
259
+ report = build_relationship_report_file(args.path)
260
+ else:
261
+ print(f"rac: path not found: {args.path}", file=sys.stderr)
262
+ raise SystemExit(EXIT_USAGE)
263
+
264
+ if args.json:
265
+ print(outputs.render_relationships_json(report))
266
+ else:
267
+ print(outputs.render_relationships_human(report))
268
+ # A completed inspection always succeeds — finding no relationships is a valid
269
+ # outcome, not an error (REQ-010).
270
+ return EXIT_OK
271
+
272
+
239
273
  def build_parser() -> argparse.ArgumentParser:
240
274
  version_str = f"rac {__version__}"
241
275
 
@@ -387,6 +421,29 @@ def build_parser() -> argparse.ArgumentParser:
387
421
  )
388
422
  p_schema.set_defaults(func=cmd_schema)
389
423
 
424
+ p_relationships = sub.add_parser(
425
+ "relationships",
426
+ help="Inspect explicit relationships across a directory (or single file).",
427
+ parents=[version_parent],
428
+ )
429
+ p_relationships.add_argument(
430
+ "path", help="A directory to scan, or a single Markdown file."
431
+ )
432
+ p_relationships.add_argument(
433
+ "--json", action="store_true", help="Emit JSON instead of human-readable text."
434
+ )
435
+ p_relationships.add_argument(
436
+ "--top-level",
437
+ action="store_true",
438
+ help="When inspecting a directory, only its top-level files (no recursion).",
439
+ )
440
+ p_relationships.add_argument(
441
+ "--recursive",
442
+ action="store_true",
443
+ help="Recurse into subdirectories (the default; accepted for clarity).",
444
+ )
445
+ p_relationships.set_defaults(func=cmd_relationships)
446
+
390
447
  return parser
391
448
 
392
449
 
@@ -16,6 +16,7 @@ from .improve import ImprovementResult
16
16
  from .ingest import IngestResult
17
17
  from .inspect import DirectoryInspection, InspectionResult
18
18
  from .models import Diff, Issue, Product
19
+ from .relationships import RelationshipReport
19
20
  from .schema import SchemaReference, template_sections
20
21
  from .stats import PortfolioStats
21
22
 
@@ -623,6 +624,61 @@ def render_schema_template(ref: SchemaReference) -> str:
623
624
  return "\n\n".join(blocks) + "\n"
624
625
 
625
626
 
627
+ # --- relationships -----------------------------------------------------------
628
+
629
+
630
+ def _relationship_label(snake_section: str) -> str:
631
+ """``related_decisions`` -> ``Related Decisions``; ``supersedes`` -> ``Supersedes``."""
632
+ return snake_section.replace("_", " ").title()
633
+
634
+
635
+ def render_relationships_human(report: RelationshipReport) -> str:
636
+ lines = [
637
+ _bold("Relationships"),
638
+ "",
639
+ f"Files Inspected: {report.total_files}",
640
+ f"Artifacts With Relationships: {report.artifacts_with_relationships}",
641
+ f"Relationships Found: {report.relationship_count}",
642
+ ]
643
+
644
+ counts = report.counts
645
+ if counts:
646
+ lines += ["", _bold("By Type:")]
647
+ lines.extend(
648
+ f"- {_relationship_label(section)}: {count}"
649
+ for section, count in counts.items()
650
+ )
651
+
652
+ # Per-artifact detail (REQ-005), only for artifacts that declare relationships.
653
+ for artifact in report.artifacts:
654
+ lines += ["", artifact.path]
655
+ for section, refs in artifact.relationships.items():
656
+ lines.append(f" {_relationship_label(section)}:")
657
+ lines.extend(f" - {ref}" for ref in refs)
658
+
659
+ return "\n".join(lines)
660
+
661
+
662
+ def render_relationships_json(report: RelationshipReport) -> str:
663
+ payload = {
664
+ "directory": report.directory,
665
+ "recursive": report.recursive,
666
+ "total_files": report.total_files,
667
+ "artifacts_with_relationships": report.artifacts_with_relationships,
668
+ "relationship_count": report.relationship_count,
669
+ "counts": report.counts,
670
+ "artifacts": [
671
+ {
672
+ "path": artifact.path,
673
+ "type": artifact.type,
674
+ "relationships": artifact.relationships,
675
+ }
676
+ for artifact in report.artifacts
677
+ ],
678
+ }
679
+ return json.dumps(payload, indent=2)
680
+
681
+
626
682
  # --- ingest ------------------------------------------------------------------
627
683
 
628
684
 
@@ -0,0 +1,241 @@
1
+ """Relationship metadata service — extract cross-artifact references (v0.7.0).
2
+
3
+ Relationships are explicit Markdown sections (``## Related Decisions``,
4
+ ``## Supersedes``, ...) that reference other artifacts (ADR-016). This module is
5
+ the single home for turning those sections into reference strings, shared by
6
+ ``rac inspect`` (which exposes them as the additive ``relationships`` field) and
7
+ ``rac stats`` (which counts their presence).
8
+
9
+ It is pure and deterministic (ADR-002 / ADR-016): it parses section text only and
10
+ never resolves, validates, or graphs the references — v0.7.0 is metadata only.
11
+
12
+ Recognition is spec-driven (REQ-002): only the relationship sections an artifact
13
+ type declares in :attr:`ArtifactSpec.optional` are considered, so a section is
14
+ recognized exactly where its schema allows it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from dataclasses import dataclass, field
21
+
22
+ from .artifacts import ArtifactSpec, spec_for
23
+ from .classification import classify
24
+ from .fs import find_markdown_files
25
+ from .models import Product
26
+ from .parser import parse_file
27
+
28
+ # The cross-artifact "Related X" sections. These populate the ``relationships``
29
+ # dict in ``rac inspect`` output. ``related designs`` is included so every peer
30
+ # artifact type can be referenced.
31
+ RELATED_SECTIONS: tuple[str, ...] = (
32
+ "related requirements",
33
+ "related decisions",
34
+ "related roadmaps",
35
+ "related prompts",
36
+ "related designs",
37
+ )
38
+
39
+ # The full relationship-section vocabulary and its canonical ordering, including
40
+ # ``supersedes``. This module owns the ordering; ``stats`` and the
41
+ # ``relationships`` command both render by-type output in this order. ``supersedes``
42
+ # is the one section that does *not* appear in the inspect ``relationships`` dict:
43
+ # there it stays a top-level scalar for backwards compatibility (ADR-007).
44
+ RELATIONSHIP_SECTIONS: tuple[str, ...] = RELATED_SECTIONS + ("supersedes",)
45
+
46
+ # A *well-formed* leading Markdown list marker: ``-``, ``*``, ``+``, or ``N.``
47
+ # followed by whitespace. Only these are stripped; any other leading text is
48
+ # preserved verbatim, so references like "REQ-001 (blocked)" or a path beginning
49
+ # with "../" survive intact (the whole line is the reference, per ADR-016).
50
+ _LIST_MARKER_RE = re.compile(r"^(?:[-*+]|\d+\.)\s+")
51
+
52
+
53
+ def _snake(section: str) -> str:
54
+ return section.replace(" ", "_")
55
+
56
+
57
+ def parse_references(body: str) -> list[str]:
58
+ """Split a relationship section body into individual reference strings.
59
+
60
+ One reference per non-empty line. A well-formed leading list marker is
61
+ stripped; otherwise the line is preserved verbatim. No ID parsing and no
62
+ resolution — the line text *is* the reference.
63
+ """
64
+ references: list[str] = []
65
+ for line in body.splitlines():
66
+ stripped = line.strip()
67
+ if not stripped:
68
+ continue
69
+ references.append(_LIST_MARKER_RE.sub("", stripped, count=1).strip())
70
+ return references
71
+
72
+
73
+ def _collect(
74
+ product: Product, spec: ArtifactSpec, allowed: tuple[str, ...]
75
+ ) -> dict[str, list[str]]:
76
+ """References for the relationship sections in ``spec.optional`` ∩ ``allowed``.
77
+
78
+ Returns ``{snake_section -> [references]}`` in ``spec.optional`` order (each
79
+ artifact's own schema order), including only sections present with at least
80
+ one parsed reference. The single core behind the two public extractors.
81
+ """
82
+ relationships: dict[str, list[str]] = {}
83
+ for section in spec.optional:
84
+ if section not in allowed:
85
+ continue
86
+ body = product.sections.get(section)
87
+ if not body:
88
+ continue
89
+ refs = parse_references(body)
90
+ if refs:
91
+ relationships[_snake(section)] = refs
92
+ return relationships
93
+
94
+
95
+ def extract_relationships(product: Product, spec: ArtifactSpec) -> dict[str, list[str]]:
96
+ """Cross-artifact references for ``rac inspect``.
97
+
98
+ Excludes ``supersedes`` — that stays a top-level scalar in inspect output
99
+ (ADR-007). Order follows ``spec.optional`` (the artifact's own schema order).
100
+ """
101
+ return _collect(product, spec, RELATED_SECTIONS)
102
+
103
+
104
+ def extract_relationships_full(
105
+ product: Product, spec: ArtifactSpec
106
+ ) -> dict[str, list[str]]:
107
+ """Cross-artifact references for ``rac relationships`` — *including* Supersedes.
108
+
109
+ The repository-level relationship command treats Supersedes as a first-class
110
+ relationship (REQ-003), so it is reported here alongside the ``related_*``
111
+ sections. Order follows ``spec.optional``.
112
+ """
113
+ return _collect(product, spec, RELATIONSHIP_SECTIONS)
114
+
115
+
116
+ def present_relationship_sections(product: Product, spec: ArtifactSpec) -> list[str]:
117
+ """Relationship sections ``product`` declares *and* populates.
118
+
119
+ Spec-driven and inclusive of ``supersedes`` (unlike
120
+ :func:`extract_relationships`). A section counts only when present with at
121
+ least one parsed reference (REQ-011). Returns the normalized section names in
122
+ ``spec.optional`` order, for ``rac stats`` declared-presence counts.
123
+ """
124
+ present: list[str] = []
125
+ for section in spec.optional:
126
+ if section not in RELATIONSHIP_SECTIONS:
127
+ continue
128
+ body = product.sections.get(section)
129
+ if body and parse_references(body):
130
+ present.append(section)
131
+ return present
132
+
133
+
134
+ # --- Repository-level relationship inspection (v0.7.1) -----------------------
135
+ #
136
+ # `rac relationships <path>` discovers the explicit relationships declared across
137
+ # a tree of artifacts (ADR-015: repository intelligence in Core, exposed via CLI +
138
+ # JSON for future consumers). It is read-only and deterministic: it reports the
139
+ # references that exist, but never resolves, validates, or graphs them.
140
+
141
+
142
+ @dataclass
143
+ class ArtifactRelationships:
144
+ """One artifact's relationships in a repository report.
145
+
146
+ ``relationships`` includes Supersedes (unlike ``rac inspect``) and is keyed by
147
+ snake_case section name in the artifact's own ``spec.optional`` order.
148
+ """
149
+
150
+ path: str
151
+ type: str
152
+ relationships: dict[str, list[str]]
153
+
154
+
155
+ @dataclass
156
+ class RelationshipReport:
157
+ """Repository-level relationship inspection result (ADR-003).
158
+
159
+ ``total_files`` counts every Markdown file considered — including files with
160
+ no relationships and Unknown artifacts. ``artifacts`` lists only those with at
161
+ least one relationship. Counts are *reference* counts (each declared target is
162
+ one relationship), aggregated by type in the canonical
163
+ :data:`RELATIONSHIP_SECTIONS` order.
164
+ """
165
+
166
+ directory: str
167
+ recursive: bool
168
+ total_files: int
169
+ artifacts: list[ArtifactRelationships] = field(default_factory=list)
170
+
171
+ @property
172
+ def artifacts_with_relationships(self) -> int:
173
+ return len(self.artifacts)
174
+
175
+ @property
176
+ def counts(self) -> dict[str, int]:
177
+ """References per relationship type, canonical order, zero types omitted."""
178
+ totals: dict[str, int] = {}
179
+ for artifact in self.artifacts:
180
+ for section, refs in artifact.relationships.items():
181
+ totals[section] = totals.get(section, 0) + len(refs)
182
+ return {
183
+ _snake(section): totals[_snake(section)]
184
+ for section in RELATIONSHIP_SECTIONS
185
+ if _snake(section) in totals
186
+ }
187
+
188
+ @property
189
+ def relationship_count(self) -> int:
190
+ """Total references found across all artifacts (sum of ``counts``)."""
191
+ return sum(self.counts.values())
192
+
193
+
194
+ def _artifact_relationships(path: str) -> tuple[str, dict[str, list[str]]]:
195
+ """Classify the file at ``path`` and extract its full relationships.
196
+
197
+ Returns ``(type, relationships)``. Unknown artifacts (no spec) yield an empty
198
+ relationship dict — extraction stays spec-driven (REQ-007), no generic scan.
199
+ """
200
+ product = parse_file(path)
201
+ artifact_type = classify(product).type
202
+ spec = spec_for(artifact_type)
203
+ relationships = extract_relationships_full(product, spec) if spec else {}
204
+ return artifact_type, relationships
205
+
206
+
207
+ def _build_report(
208
+ directory: str, paths: list, recursive: bool
209
+ ) -> RelationshipReport:
210
+ """Assemble a :class:`RelationshipReport` from ``paths`` (already ordered)."""
211
+ artifacts: list[ArtifactRelationships] = []
212
+ for path in paths:
213
+ artifact_type, relationships = _artifact_relationships(str(path))
214
+ if relationships:
215
+ artifacts.append(
216
+ ArtifactRelationships(
217
+ path=str(path), type=artifact_type, relationships=relationships
218
+ )
219
+ )
220
+ return RelationshipReport(
221
+ directory=directory,
222
+ recursive=recursive,
223
+ total_files=len(paths),
224
+ artifacts=artifacts,
225
+ )
226
+
227
+
228
+ def build_relationship_report(
229
+ directory: str, recursive: bool = True
230
+ ) -> RelationshipReport:
231
+ """Inspect explicit relationships across a directory of Markdown files."""
232
+ paths = find_markdown_files(directory, recursive=recursive)
233
+ return _build_report(directory, paths, recursive)
234
+
235
+
236
+ def build_relationship_report_file(path: str) -> RelationshipReport:
237
+ """Inspect relationships in a single file (REQ-009).
238
+
239
+ Same model as a directory report, with one file and ``recursive=False``.
240
+ """
241
+ return _build_report(path, [path], recursive=False)
@@ -19,11 +19,11 @@ from __future__ import annotations
19
19
 
20
20
  from dataclasses import dataclass, field
21
21
 
22
- from .artifacts import RELATIONSHIP_SECTIONS, spec_for
22
+ from .artifacts import spec_for
23
23
  from .fs import find_markdown_files
24
24
  from .inspect import build_inspection
25
25
  from .parser import parse_file
26
- from .relationships import present_relationship_sections
26
+ from .relationships import RELATIONSHIP_SECTIONS, present_relationship_sections
27
27
  from .validate import validate
28
28
 
29
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requirements-as-code
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: RAC — lint and diff product requirements written in Markdown.
5
5
  Author: tcballard
6
6
  License-Expression: MIT
@@ -863,6 +863,78 @@ registered schemas are supported; custom schemas are out of scope.
863
863
 
864
864
  ---
865
865
 
866
+ ## Relationships
867
+
868
+ Inspect the explicit [relationship metadata](#relationship-metadata) declared
869
+ across a repository — a deterministic, read-only view of how artifacts reference
870
+ one another, without opening every file.
871
+
872
+ ```bash
873
+ rac relationships planning/
874
+ rac relationships planning/ --json
875
+ rac relationships planning/ --top-level # don't recurse into subdirectories
876
+ rac relationships requirements/search.md # a single file works too
877
+ ```
878
+
879
+ ```text
880
+ Relationships
881
+
882
+ Files Inspected: 24
883
+ Artifacts With Relationships: 8
884
+ Relationships Found: 14
885
+
886
+ By Type:
887
+ - Related Requirements: 4
888
+ - Related Decisions: 6
889
+ - Related Roadmaps: 2
890
+ - Related Prompts: 1
891
+ - Supersedes: 1
892
+
893
+ requirements/search.md
894
+ Related Decisions:
895
+ - ADR-004
896
+ ```
897
+
898
+ `--json` returns structured data for automation and future viewers (ADR-015 —
899
+ relationship intelligence lives in RAC Core, not the UI):
900
+
901
+ ```json
902
+ {
903
+ "directory": "planning",
904
+ "recursive": true,
905
+ "total_files": 24,
906
+ "artifacts_with_relationships": 8,
907
+ "relationship_count": 14,
908
+ "counts": { "related_decisions": 6, "related_requirements": 4 },
909
+ "artifacts": [
910
+ {
911
+ "path": "requirements/search.md",
912
+ "type": "requirement",
913
+ "relationships": { "related_decisions": ["ADR-004"] }
914
+ }
915
+ ]
916
+ }
917
+ ```
918
+
919
+ Notes:
920
+
921
+ - **Counts are individual references.** `relationship_count` equals
922
+ `sum(counts.values())`; an artifact listing two decisions contributes two.
923
+ - **`total_files`** counts every Markdown file considered — including files with
924
+ no relationships and Unknown artifacts.
925
+ - **Supersedes is a relationship here.** Unlike `rac inspect` (where `supersedes`
926
+ is a top-level scalar), this command reports it alongside the `related_*`
927
+ sections, in both `counts` and per-artifact `relationships`.
928
+ - **Spec-driven.** Extraction is keyed off each artifact type's declared sections.
929
+ Unknown artifacts are counted but contribute no relationships (and never appear
930
+ in `artifacts`); RAC does not scan arbitrary Markdown.
931
+ - **Read-only and exit `0`.** The command never modifies files and exits `0`
932
+ whether or not relationships are found (`2` only for a missing path or a
933
+ non-Markdown file). It does **not** resolve or validate targets — broken-link
934
+ detection is a later release.
935
+
936
+ ---
937
+
866
938
  ## Review (Planned)
867
939
 
868
940
  AI-assisted product review.