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.
- {requirements_as_code-0.7.0/requirements_as_code.egg-info → requirements_as_code-0.7.1}/PKG-INFO +73 -1
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/README.md +72 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/artifacts.py +5 -17
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/cli.py +58 -1
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/outputs.py +56 -0
- requirements_as_code-0.7.1/rac/relationships.py +241 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/stats.py +2 -2
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1/requirements_as_code.egg-info}/PKG-INFO +73 -1
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/SOURCES.txt +3 -0
- requirements_as_code-0.7.1/tests/fixtures/no_relationships/decision.md +22 -0
- requirements_as_code-0.7.1/tests/fixtures/no_relationships/requirement.md +17 -0
- requirements_as_code-0.7.1/tests/test_relationships_cmd.py +233 -0
- requirements_as_code-0.7.0/rac/relationships.py +0 -86
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/.github/workflows/python-publish.yml +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/.gitignore +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/LICENSE +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/examples/example_dashboard_v1.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/examples/example_dashboard_v2.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-001-markdown-first.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-002-ai-optional.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-003-structured-outputs-first.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-004-artifact-model.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-005-cli-first.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-006-ingest-over-rewrite.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-007-json-contract-stability.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-008-agent-ready-architecture.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-009-ai-assisted-development.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-010-documents-are-not-artifacts.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-011-file-first-pipeline.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-012-open-core-strategy.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-013-leverage-existing-source-control-systems.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-014-viewer-agnostic-knowledge-artifacts.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-015-explorer-as-consumer.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-016-relationships-as-structural-references.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/adr/adr-017-rac-managed-knowledge-not-work.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-command-surface.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-first-run-experience.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-health-model.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-import-workflow.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-knowledge-graph.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/designs/explorer-visual-system.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.0-workspace-analysis.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.1-review-engine.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.2-mcp-server.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.4-claude-skills.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/future/v1.4-python-sdk.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/archive/v0.5-decisions.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/archive/v0.7-prompts.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.2-stats.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.3-ingest.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.3.1-formats.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4-inspect.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4.1-expansion.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4.1-inspect-expansion.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.4.2-decision-metadata.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.5.0-artifact-improvement.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.5.1-guided-improvement.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.5.2-schema.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.0-roadmap-artifacts.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.1-roadmap-improvement.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.2-prompt-artifact.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.6.3-design-artifacts.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.0-relationship-metadata.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.1-relationship-inspection.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.2-relationship-validation.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.3-repo-intelligence.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.7.4-repo-indexing.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.0-service-api.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.1-repository-model.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.2-interactive-runtime-readiness.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.8.3-integration-freeze.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.0-explorer-foundation.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.1-explorer-experience.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.2-knowledge-operations.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/planning/roadmap/v0.9.3-intelligence-views.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/pyproject.toml +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/__init__.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/classification.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/diff.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/fs.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/improve.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/ingest.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/inspect.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/models.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/parser.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/schema.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/rac/validate.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/dependency_links.txt +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/entry_points.txt +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/requires.txt +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/requirements_as_code.egg-info/top_level.txt +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/setup.cfg +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/conftest.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/bad_category.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/bad_status.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/minimal.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/portfolio/01_accepted_arch.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/portfolio/02_proposed_process.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/portfolio/03_no_metadata.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/decision/with_metadata.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/design/minimal.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/design/missing_constraints.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/design/valid.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/diff/new.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/diff/old.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/ingest/sample.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/ambiguous.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/decision.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/nested/another_requirement.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/inspect/requirement.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/duplicate_ids.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/empty_req_text.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/malformed_id.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_id.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_problem.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_requirements.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/missing_title.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/invalid/multiple_titles.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/broken.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/feature_a.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/feature_b.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/portfolio/sub/feature_c.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/prompt/minimal.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/prompt/missing_output.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/prompt/valid.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/decision_with_links.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/design_with_links.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/prompt_with_links.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/requirement_with_links.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/relationships/roadmap_with_links.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/roadmap/minimal.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/roadmap/missing_initiatives.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/roadmap/valid.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/bullet_requirements.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/feature.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/minimal.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/fixtures/valid/warnings.md +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_cli.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_decision_metadata.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_design.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_diff.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_improve.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_ingest.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_inspect.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_parser.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_prompt.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_relationships.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_roadmap.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_schema.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_stats.py +0 -0
- {requirements_as_code-0.7.0 → requirements_as_code-0.7.1}/tests/test_validate.py +0 -0
{requirements_as_code-0.7.0/requirements_as_code.egg-info → requirements_as_code-0.7.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: requirements-as-code
|
|
3
|
-
Version: 0.7.
|
|
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
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
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
|
|
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
|
|
{requirements_as_code-0.7.0 → requirements_as_code-0.7.1/requirements_as_code.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: requirements-as-code
|
|
3
|
-
Version: 0.7.
|
|
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.
|