requirements-as-code 0.4.2__tar.gz → 0.5.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.4.2/requirements_as_code.egg-info → requirements_as_code-0.5.0}/PKG-INFO +62 -1
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/README.md +61 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.5.0-artifact-improvement.md +5 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/artifacts.py +11 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/classification.py +24 -2
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/cli.py +40 -6
- requirements_as_code-0.5.0/rac/improve.py +84 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/outputs.py +71 -1
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0/requirements_as_code.egg-info}/PKG-INFO +62 -1
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/requirements_as_code.egg-info/SOURCES.txt +2 -0
- requirements_as_code-0.5.0/tests/test_improve.py +213 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/.github/workflows/python-publish.yml +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/.gitignore +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/LICENSE +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/examples/example_dashboard_v1.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/examples/example_dashboard_v2.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-001-markdown-first.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-002-ai-optional.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-003-structured-outputs-first.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-004-artifact-model.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-005-cli-first.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-006-ingest-over-rewrite.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-007-json-contract-stability.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-008-agent-ready-architecture.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-009-ai-assisted-development.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-010-documents-are-not-artifacts.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-011-file-first-pipeline.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-012-open-core-strategy.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-013-leverage-existing-source-control-systems.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-014-viewer-agnostic-knowledge-artifacts.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-015-explorer-as-consumer.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-016-rac-managed-knowledge-not-work.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.0-workspace-analysis.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.1-review-engine.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.2-mcp-server.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.4-claude-skills.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.4-python-sdk.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/archive/v0.5-decisions.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.2-stats.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.3-ingest.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.3.1-formats.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.4-inspect.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.4.1-expansion.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.4.1-inspect-expansion.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.4.2-decision-metadata.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.5.1-guided-improvement.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.6-roadmaps.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.7-prompts.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.8-explorer.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/pyproject.toml +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/__init__.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/diff.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/fs.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/ingest.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/inspect.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/models.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/parser.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/stats.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/rac/validate.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/requirements_as_code.egg-info/dependency_links.txt +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/requirements_as_code.egg-info/entry_points.txt +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/requirements_as_code.egg-info/requires.txt +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/requirements_as_code.egg-info/top_level.txt +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/setup.cfg +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/conftest.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/bad_category.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/bad_status.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/minimal.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/portfolio/01_accepted_arch.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/portfolio/02_proposed_process.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/portfolio/03_no_metadata.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/with_metadata.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/diff/new.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/diff/old.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/ingest/sample.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/ambiguous.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/decision.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/nested/another_requirement.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/requirement.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/duplicate_ids.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/empty_req_text.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/malformed_id.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_id.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_problem.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_requirements.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_title.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/multiple_titles.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/broken.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/feature_a.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/feature_b.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/sub/feature_c.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/valid/bullet_requirements.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/valid/feature.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/valid/minimal.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/valid/warnings.md +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_cli.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_decision_metadata.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_diff.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_ingest.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_inspect.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_parser.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_stats.py +0 -0
- {requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/test_validate.py +0 -0
{requirements_as_code-0.4.2/requirements_as_code.egg-info → requirements_as_code-0.5.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: requirements-as-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: RAC — lint and diff product requirements written in Markdown.
|
|
5
5
|
Author: tcballard
|
|
6
6
|
License-Expression: MIT
|
|
@@ -616,6 +616,67 @@ usage errors (file not found, or a non-Markdown file — convert it with
|
|
|
616
616
|
|
|
617
617
|
---
|
|
618
618
|
|
|
619
|
+
## Improve
|
|
620
|
+
|
|
621
|
+
Where `inspect` tells you *what an artifact is*, `improve` tells you *what to add
|
|
622
|
+
next*. It reports the required and recommended sections an artifact is missing —
|
|
623
|
+
**deterministically, from the schema, with no AI** (ADR-002) — and can emit
|
|
624
|
+
Markdown templates for them. `improve` is **advisory and read-only**: it never
|
|
625
|
+
modifies your files and never generates content beyond `_TODO_` placeholders.
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
rac improve requirement.md
|
|
629
|
+
rac improve requirement.md --template # Markdown skeletons for missing sections
|
|
630
|
+
rac improve requirement.md --json
|
|
631
|
+
cat requirement.md | rac improve - # stdin
|
|
632
|
+
rac ingest prd.docx --stdout | rac improve -
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Default output:
|
|
636
|
+
|
|
637
|
+
```text
|
|
638
|
+
Artifact Type: Requirement
|
|
639
|
+
|
|
640
|
+
Missing Required:
|
|
641
|
+
(none)
|
|
642
|
+
|
|
643
|
+
Missing Recommended:
|
|
644
|
+
- Risks
|
|
645
|
+
- Assumptions
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
`--template` turns the gaps into a ready-to-paste skeleton (required sections
|
|
649
|
+
first, then recommended), with a short guidance hint per section:
|
|
650
|
+
|
|
651
|
+
```markdown
|
|
652
|
+
## Risks
|
|
653
|
+
|
|
654
|
+
_TODO_
|
|
655
|
+
|
|
656
|
+
<!-- Potential implementation, delivery, or adoption risks -->
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
So you can go straight from `rac inspect requirement.md` to
|
|
660
|
+
`rac improve requirement.md --template` without consulting any documentation.
|
|
661
|
+
|
|
662
|
+
`--json` returns a stable contract (ADR-007):
|
|
663
|
+
|
|
664
|
+
```json
|
|
665
|
+
{
|
|
666
|
+
"type": "requirement",
|
|
667
|
+
"missing_required": [],
|
|
668
|
+
"missing_recommended": ["risks", "assumptions"]
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
v0.5.0 generates suggestions for **Requirement** artifacts. Other known types
|
|
673
|
+
(e.g. Decision) and Unknown documents return a short explanatory message instead.
|
|
674
|
+
`improve` is advisory: it exits `0` for any completed analysis (with or without
|
|
675
|
+
suggestions) and `2` for usage errors. The presence of suggestions never changes
|
|
676
|
+
the exit code.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
619
680
|
## Review (Planned)
|
|
620
681
|
|
|
621
682
|
AI-assisted product review.
|
|
@@ -576,6 +576,67 @@ usage errors (file not found, or a non-Markdown file — convert it with
|
|
|
576
576
|
|
|
577
577
|
---
|
|
578
578
|
|
|
579
|
+
## Improve
|
|
580
|
+
|
|
581
|
+
Where `inspect` tells you *what an artifact is*, `improve` tells you *what to add
|
|
582
|
+
next*. It reports the required and recommended sections an artifact is missing —
|
|
583
|
+
**deterministically, from the schema, with no AI** (ADR-002) — and can emit
|
|
584
|
+
Markdown templates for them. `improve` is **advisory and read-only**: it never
|
|
585
|
+
modifies your files and never generates content beyond `_TODO_` placeholders.
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
rac improve requirement.md
|
|
589
|
+
rac improve requirement.md --template # Markdown skeletons for missing sections
|
|
590
|
+
rac improve requirement.md --json
|
|
591
|
+
cat requirement.md | rac improve - # stdin
|
|
592
|
+
rac ingest prd.docx --stdout | rac improve -
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Default output:
|
|
596
|
+
|
|
597
|
+
```text
|
|
598
|
+
Artifact Type: Requirement
|
|
599
|
+
|
|
600
|
+
Missing Required:
|
|
601
|
+
(none)
|
|
602
|
+
|
|
603
|
+
Missing Recommended:
|
|
604
|
+
- Risks
|
|
605
|
+
- Assumptions
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
`--template` turns the gaps into a ready-to-paste skeleton (required sections
|
|
609
|
+
first, then recommended), with a short guidance hint per section:
|
|
610
|
+
|
|
611
|
+
```markdown
|
|
612
|
+
## Risks
|
|
613
|
+
|
|
614
|
+
_TODO_
|
|
615
|
+
|
|
616
|
+
<!-- Potential implementation, delivery, or adoption risks -->
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
So you can go straight from `rac inspect requirement.md` to
|
|
620
|
+
`rac improve requirement.md --template` without consulting any documentation.
|
|
621
|
+
|
|
622
|
+
`--json` returns a stable contract (ADR-007):
|
|
623
|
+
|
|
624
|
+
```json
|
|
625
|
+
{
|
|
626
|
+
"type": "requirement",
|
|
627
|
+
"missing_required": [],
|
|
628
|
+
"missing_recommended": ["risks", "assumptions"]
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
v0.5.0 generates suggestions for **Requirement** artifacts. Other known types
|
|
633
|
+
(e.g. Decision) and Unknown documents return a short explanatory message instead.
|
|
634
|
+
`improve` is advisory: it exits `0` for any completed analysis (with or without
|
|
635
|
+
suggestions) and `2` for usage errors. The presence of suggestions never changes
|
|
636
|
+
the exit code.
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
579
640
|
## Review (Planned)
|
|
580
641
|
|
|
581
642
|
AI-assisted product review.
|
|
@@ -232,6 +232,11 @@ rac improve requirement.md --template
|
|
|
232
232
|
|
|
233
233
|
without consulting documentation.
|
|
234
234
|
|
|
235
|
+
### Completion
|
|
236
|
+
|
|
237
|
+
A user can take a valid but incomplete Requirement and generate a complete
|
|
238
|
+
RAC-compliant skeleton using only `rac improve --template`.
|
|
239
|
+
|
|
235
240
|
---
|
|
236
241
|
|
|
237
242
|
## Acceptance Criteria
|
|
@@ -35,6 +35,10 @@ class ArtifactSpec:
|
|
|
35
35
|
# A value present in one of these sections that is not in its allowed set is
|
|
36
36
|
# a validation error; a missing section is not (metadata stays optional).
|
|
37
37
|
metadata: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
38
|
+
# Short authoring hints per normalized section name, surfaced by `rac improve
|
|
39
|
+
# --template` as guidance comments. Optional; sections without a hint render
|
|
40
|
+
# without one.
|
|
41
|
+
descriptions: dict[str, str] = field(default_factory=dict)
|
|
38
42
|
# Synonyms: alternate normalized headings that map onto a canonical section
|
|
39
43
|
# name (e.g. "success criteria" -> "success metrics"). Applied before
|
|
40
44
|
# matching, so synonyms contribute to confidence. Matching is deterministic
|
|
@@ -57,6 +61,13 @@ ARTIFACT_SPECS: tuple[ArtifactSpec, ...] = (
|
|
|
57
61
|
display="Requirement",
|
|
58
62
|
required=("problem", "requirements"),
|
|
59
63
|
recommended=("success metrics", "risks", "assumptions"),
|
|
64
|
+
descriptions={
|
|
65
|
+
"problem": "The user or business problem this addresses",
|
|
66
|
+
"requirements": "Numbered requirement statements, e.g. [REQ-001] ...",
|
|
67
|
+
"success metrics": "How success will be measured",
|
|
68
|
+
"risks": "Potential implementation, delivery, or adoption risks",
|
|
69
|
+
"assumptions": "Assumptions this artifact depends on",
|
|
70
|
+
},
|
|
60
71
|
synonyms={
|
|
61
72
|
"success criteria": "success metrics",
|
|
62
73
|
"kpis": "success metrics",
|
|
@@ -47,16 +47,38 @@ class Classification:
|
|
|
47
47
|
missing_sections: list[str]
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _mapped(product: Product, spec) -> set[str]:
|
|
51
|
+
"""The document's ``##`` headings, with this spec's synonyms applied.
|
|
52
|
+
|
|
53
|
+
The single source of synonym-aware section matching, shared by scoring
|
|
54
|
+
(:func:`score_artifacts`) and the scoring-independent :func:`missing_sections`.
|
|
55
|
+
"""
|
|
56
|
+
return {spec.synonyms.get(h, h) for h in product.sections}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def missing_sections(product: Product, spec) -> tuple[list[str], list[str]]:
|
|
60
|
+
"""Return ``(missing_required, missing_recommended)`` for ``spec``.
|
|
61
|
+
|
|
62
|
+
Synonym-aware and in schema declaration order. Independent of confidence
|
|
63
|
+
scoring (no :class:`TypeScore`) so callers like ``improve`` depend only on the
|
|
64
|
+
schema, not on classification internals.
|
|
65
|
+
"""
|
|
66
|
+
mapped = _mapped(product, spec)
|
|
67
|
+
return (
|
|
68
|
+
[s for s in spec.required if s not in mapped],
|
|
69
|
+
[s for s in spec.recommended if s not in mapped],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
50
73
|
def score_artifacts(product: Product) -> list[TypeScore]:
|
|
51
74
|
"""Score the document against every artifact type, best fit first.
|
|
52
75
|
|
|
53
76
|
Synonyms (e.g. "success criteria" -> "success metrics") are applied before
|
|
54
77
|
matching, so they contribute to the score deterministically.
|
|
55
78
|
"""
|
|
56
|
-
headings = list(product.sections)
|
|
57
79
|
scores: list[TypeScore] = []
|
|
58
80
|
for spec in ARTIFACT_SPECS:
|
|
59
|
-
mapped =
|
|
81
|
+
mapped = _mapped(product, spec)
|
|
60
82
|
matched_required = [s for s in spec.required if s in mapped]
|
|
61
83
|
matched_recommended = [s for s in spec.recommended if s in mapped]
|
|
62
84
|
missing = [s for s in spec.expected if s not in mapped]
|
|
@@ -6,9 +6,10 @@ Commands:
|
|
|
6
6
|
rac stats <directory> [--json]
|
|
7
7
|
rac ingest <file> [-o OUT | --stdout] [--force] [--json]
|
|
8
8
|
rac inspect <file.md | -> [--json]
|
|
9
|
+
rac improve <file.md | -> [--json | --template]
|
|
9
10
|
|
|
10
11
|
Exit codes:
|
|
11
|
-
0 success (incl. inspect reporting Unknown)
|
|
12
|
+
0 success (incl. inspect/improve reporting Unknown)
|
|
12
13
|
1 validate: errors found; stats: no valid features or decisions;
|
|
13
14
|
ingest: conversion failed
|
|
14
15
|
2 usage / IO error (file not found, not a directory, unsupported type,
|
|
@@ -26,6 +27,7 @@ from . import outputs
|
|
|
26
27
|
from .diff import diff as diff_asts
|
|
27
28
|
from .ingest import ConversionError, UnsupportedDocument, ingest
|
|
28
29
|
from .classification import score_artifacts
|
|
30
|
+
from .improve import improve_product
|
|
29
31
|
from .inspect import build_inspection, inspect_directory
|
|
30
32
|
from .parser import parse, parse_file
|
|
31
33
|
from .stats import collect_stats
|
|
@@ -126,8 +128,8 @@ def cmd_ingest(args: argparse.Namespace) -> int:
|
|
|
126
128
|
return EXIT_OK
|
|
127
129
|
|
|
128
130
|
|
|
129
|
-
def
|
|
130
|
-
"""Read
|
|
131
|
+
def _read_markdown_input(target: str, command: str) -> str:
|
|
132
|
+
"""Read a Markdown file or stdin (``-``) for ``command`` (inspect/improve)."""
|
|
131
133
|
if target == "-":
|
|
132
134
|
return sys.stdin.read()
|
|
133
135
|
path = Path(target)
|
|
@@ -136,7 +138,7 @@ def _read_inspect_input(target: str) -> str:
|
|
|
136
138
|
raise SystemExit(EXIT_USAGE)
|
|
137
139
|
if path.suffix.lower() not in (".md", ".markdown"):
|
|
138
140
|
print(
|
|
139
|
-
f"rac:
|
|
141
|
+
f"rac: {command} expects a Markdown file; "
|
|
140
142
|
f"convert it first with: rac ingest {target}",
|
|
141
143
|
file=sys.stderr,
|
|
142
144
|
)
|
|
@@ -160,7 +162,7 @@ def cmd_inspect(args: argparse.Namespace) -> int:
|
|
|
160
162
|
return EXIT_OK
|
|
161
163
|
|
|
162
164
|
# Single file (or stdin).
|
|
163
|
-
text =
|
|
165
|
+
text = _read_markdown_input(args.file, "inspect")
|
|
164
166
|
product = parse(text)
|
|
165
167
|
result = build_inspection(product)
|
|
166
168
|
if args.verbose and not args.json:
|
|
@@ -173,6 +175,19 @@ def cmd_inspect(args: argparse.Namespace) -> int:
|
|
|
173
175
|
return EXIT_OK
|
|
174
176
|
|
|
175
177
|
|
|
178
|
+
def cmd_improve(args: argparse.Namespace) -> int:
|
|
179
|
+
text = _read_markdown_input(args.file, "improve")
|
|
180
|
+
result = improve_product(parse(text))
|
|
181
|
+
if args.json:
|
|
182
|
+
print(outputs.render_improve_json(result))
|
|
183
|
+
elif args.template:
|
|
184
|
+
print(outputs.render_improve_template(result))
|
|
185
|
+
else:
|
|
186
|
+
print(outputs.render_improve_human(result))
|
|
187
|
+
# Advisory: a completed analysis always succeeds, with or without suggestions.
|
|
188
|
+
return EXIT_OK
|
|
189
|
+
|
|
190
|
+
|
|
176
191
|
def build_parser() -> argparse.ArgumentParser:
|
|
177
192
|
version_str = f"rac {__version__}"
|
|
178
193
|
|
|
@@ -276,7 +291,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
276
291
|
)
|
|
277
292
|
p_inspect.set_defaults(func=cmd_inspect)
|
|
278
293
|
|
|
279
|
-
|
|
294
|
+
p_improve = sub.add_parser(
|
|
295
|
+
"improve",
|
|
296
|
+
help="Suggest missing sections (and templates) for an artifact.",
|
|
297
|
+
parents=[version_parent],
|
|
298
|
+
)
|
|
299
|
+
p_improve.add_argument(
|
|
300
|
+
"file",
|
|
301
|
+
help="A Markdown file, or '-' to read from stdin.",
|
|
302
|
+
)
|
|
303
|
+
improve_mode = p_improve.add_mutually_exclusive_group()
|
|
304
|
+
improve_mode.add_argument(
|
|
305
|
+
"--json", action="store_true", help="Emit JSON instead of human-readable text."
|
|
306
|
+
)
|
|
307
|
+
improve_mode.add_argument(
|
|
308
|
+
"--template",
|
|
309
|
+
action="store_true",
|
|
310
|
+
help="Emit Markdown templates for the missing sections.",
|
|
311
|
+
)
|
|
312
|
+
p_improve.set_defaults(func=cmd_improve)
|
|
313
|
+
|
|
280
314
|
return parser
|
|
281
315
|
|
|
282
316
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Artifact improvement — deterministic, schema-driven guidance (ADR-002).
|
|
2
|
+
|
|
3
|
+
`rac improve <file>` is RAC's first *advisory* capability: it reports which
|
|
4
|
+
required and recommended sections an artifact is missing and can emit Markdown
|
|
5
|
+
templates for them. It is strictly read-only (REQ-004) and generates no content
|
|
6
|
+
beyond schema-derived placeholders — no AI, no rewriting.
|
|
7
|
+
|
|
8
|
+
Improvement depends only on the artifact *type* and a *schema comparison*
|
|
9
|
+
(:func:`rac.classification.missing_sections`); it never reaches into classification
|
|
10
|
+
confidence internals. v0.5.0 produces suggestions for Requirement artifacts only;
|
|
11
|
+
other known types and Unknown return no suggestions (the renderers explain why).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from .artifacts import spec_for
|
|
19
|
+
from .classification import classify, missing_sections
|
|
20
|
+
from .models import Product
|
|
21
|
+
from .parser import parse, parse_file
|
|
22
|
+
|
|
23
|
+
# The single artifact type that v0.5.0 generates suggestions for. Written as a
|
|
24
|
+
# constant (not hard-coded throughout) so widening scope later is a one-line change.
|
|
25
|
+
SUPPORTED_TYPE = "requirement"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ImprovementResult:
|
|
30
|
+
"""Typed improvement analysis for one artifact (ADR-003).
|
|
31
|
+
|
|
32
|
+
Section names are stored normalized (e.g. ``"success metrics"``); renderers
|
|
33
|
+
format them. ``to_dict`` is the stable JSON contract (ADR-007):
|
|
34
|
+
``{type, missing_required, missing_recommended}``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
type: str # classified artifact type, or "unknown"
|
|
38
|
+
missing_required: list[str]
|
|
39
|
+
missing_recommended: list[str]
|
|
40
|
+
# Reserved for future Unknown handling (e.g. "closest match" guidance). Always
|
|
41
|
+
# None in v0.5.0 and intentionally not serialized yet — additive later (ADR-007).
|
|
42
|
+
closest_type: str | None = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def supported(self) -> bool:
|
|
46
|
+
"""True when this is a type ``improve`` generates suggestions for."""
|
|
47
|
+
return self.type == SUPPORTED_TYPE
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict:
|
|
50
|
+
return {
|
|
51
|
+
"type": self.type,
|
|
52
|
+
"missing_required": [_snake(s) for s in self.missing_required],
|
|
53
|
+
"missing_recommended": [_snake(s) for s in self.missing_recommended],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _snake(section: str) -> str:
|
|
58
|
+
return section.replace(" ", "_")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def improve_product(product: Product) -> ImprovementResult:
|
|
62
|
+
"""Analyze a parsed ``product`` and return improvement guidance."""
|
|
63
|
+
artifact_type = classify(product).type
|
|
64
|
+
if artifact_type != SUPPORTED_TYPE:
|
|
65
|
+
# Decision / other known types and Unknown: no suggestions in v0.5.0.
|
|
66
|
+
return ImprovementResult(
|
|
67
|
+
type=artifact_type, missing_required=[], missing_recommended=[]
|
|
68
|
+
)
|
|
69
|
+
spec = spec_for(SUPPORTED_TYPE)
|
|
70
|
+
assert spec is not None # the requirement spec always exists
|
|
71
|
+
missing_required, missing_recommended = missing_sections(product, spec)
|
|
72
|
+
return ImprovementResult(
|
|
73
|
+
type=artifact_type,
|
|
74
|
+
missing_required=missing_required,
|
|
75
|
+
missing_recommended=missing_recommended,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def improve_text(text: str) -> ImprovementResult:
|
|
80
|
+
return improve_product(parse(text))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def improve_file(path: str) -> ImprovementResult:
|
|
84
|
+
return improve_product(parse_file(path))
|
|
@@ -10,8 +10,9 @@ import json
|
|
|
10
10
|
import sys
|
|
11
11
|
from dataclasses import asdict
|
|
12
12
|
|
|
13
|
-
from .artifacts import ARTIFACT_SPECS
|
|
13
|
+
from .artifacts import ARTIFACT_SPECS, spec_for
|
|
14
14
|
from .classification import CONFIDENCE_THRESHOLD, TypeScore
|
|
15
|
+
from .improve import ImprovementResult
|
|
15
16
|
from .ingest import IngestResult
|
|
16
17
|
from .inspect import DirectoryInspection, InspectionResult
|
|
17
18
|
from .models import Diff, Issue, Product
|
|
@@ -374,6 +375,75 @@ def render_dir_inspect_json(d: DirectoryInspection) -> str:
|
|
|
374
375
|
return json.dumps(payload, indent=2)
|
|
375
376
|
|
|
376
377
|
|
|
378
|
+
# --- improve -----------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
# Shown when guidance cannot be produced. Ordering everywhere is required-first,
|
|
381
|
+
# then recommended (schema declaration order within each).
|
|
382
|
+
_UNKNOWN_MESSAGE = (
|
|
383
|
+
"Unable to generate improvement guidance.\n"
|
|
384
|
+
"Artifact type could not be determined."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _unsupported_message(result: ImprovementResult) -> str:
|
|
389
|
+
"""Generic guidance for a known but unsupported artifact type (e.g. Decision)."""
|
|
390
|
+
return (
|
|
391
|
+
f"Artifact Type: {result.type.title()}\n\n"
|
|
392
|
+
"Improvement guidance is not currently available for this artifact type."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def render_improve_human(result: ImprovementResult) -> str:
|
|
397
|
+
if result.type == "unknown":
|
|
398
|
+
return _UNKNOWN_MESSAGE
|
|
399
|
+
if not result.supported:
|
|
400
|
+
return _unsupported_message(result)
|
|
401
|
+
|
|
402
|
+
lines = [_bold(f"Artifact Type: {result.type.title()}"), ""]
|
|
403
|
+
if not result.missing_required and not result.missing_recommended:
|
|
404
|
+
lines.append("Nothing to improve — all expected sections present.")
|
|
405
|
+
return "\n".join(lines)
|
|
406
|
+
|
|
407
|
+
def block(title: str, names: list[str]) -> None:
|
|
408
|
+
lines.extend([_bold(title)])
|
|
409
|
+
if names:
|
|
410
|
+
lines.extend(f" - {s.title()}" for s in names)
|
|
411
|
+
else:
|
|
412
|
+
lines.append(" (none)")
|
|
413
|
+
lines.append("")
|
|
414
|
+
|
|
415
|
+
block("Missing Required:", result.missing_required)
|
|
416
|
+
block("Missing Recommended:", result.missing_recommended)
|
|
417
|
+
return "\n".join(lines).rstrip()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def render_improve_json(result: ImprovementResult) -> str:
|
|
421
|
+
return json.dumps(result.to_dict(), indent=2)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def render_improve_template(result: ImprovementResult) -> str:
|
|
425
|
+
"""Emit Markdown templates for missing sections (required first)."""
|
|
426
|
+
if result.type == "unknown":
|
|
427
|
+
return _UNKNOWN_MESSAGE
|
|
428
|
+
if not result.supported:
|
|
429
|
+
return _unsupported_message(result)
|
|
430
|
+
|
|
431
|
+
missing = result.missing_required + result.missing_recommended
|
|
432
|
+
if not missing:
|
|
433
|
+
return "# Nothing to add — all expected sections present."
|
|
434
|
+
|
|
435
|
+
spec = spec_for(result.type)
|
|
436
|
+
descriptions = spec.descriptions if spec else {}
|
|
437
|
+
blocks: list[str] = []
|
|
438
|
+
for section in missing:
|
|
439
|
+
block = f"## {section.title()}\n\n_TODO_"
|
|
440
|
+
hint = descriptions.get(section)
|
|
441
|
+
if hint:
|
|
442
|
+
block += f"\n\n<!-- {hint} -->"
|
|
443
|
+
blocks.append(block)
|
|
444
|
+
return "\n\n".join(blocks) + "\n"
|
|
445
|
+
|
|
446
|
+
|
|
377
447
|
# --- ingest ------------------------------------------------------------------
|
|
378
448
|
|
|
379
449
|
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0/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.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: RAC — lint and diff product requirements written in Markdown.
|
|
5
5
|
Author: tcballard
|
|
6
6
|
License-Expression: MIT
|
|
@@ -616,6 +616,67 @@ usage errors (file not found, or a non-Markdown file — convert it with
|
|
|
616
616
|
|
|
617
617
|
---
|
|
618
618
|
|
|
619
|
+
## Improve
|
|
620
|
+
|
|
621
|
+
Where `inspect` tells you *what an artifact is*, `improve` tells you *what to add
|
|
622
|
+
next*. It reports the required and recommended sections an artifact is missing —
|
|
623
|
+
**deterministically, from the schema, with no AI** (ADR-002) — and can emit
|
|
624
|
+
Markdown templates for them. `improve` is **advisory and read-only**: it never
|
|
625
|
+
modifies your files and never generates content beyond `_TODO_` placeholders.
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
rac improve requirement.md
|
|
629
|
+
rac improve requirement.md --template # Markdown skeletons for missing sections
|
|
630
|
+
rac improve requirement.md --json
|
|
631
|
+
cat requirement.md | rac improve - # stdin
|
|
632
|
+
rac ingest prd.docx --stdout | rac improve -
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Default output:
|
|
636
|
+
|
|
637
|
+
```text
|
|
638
|
+
Artifact Type: Requirement
|
|
639
|
+
|
|
640
|
+
Missing Required:
|
|
641
|
+
(none)
|
|
642
|
+
|
|
643
|
+
Missing Recommended:
|
|
644
|
+
- Risks
|
|
645
|
+
- Assumptions
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
`--template` turns the gaps into a ready-to-paste skeleton (required sections
|
|
649
|
+
first, then recommended), with a short guidance hint per section:
|
|
650
|
+
|
|
651
|
+
```markdown
|
|
652
|
+
## Risks
|
|
653
|
+
|
|
654
|
+
_TODO_
|
|
655
|
+
|
|
656
|
+
<!-- Potential implementation, delivery, or adoption risks -->
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
So you can go straight from `rac inspect requirement.md` to
|
|
660
|
+
`rac improve requirement.md --template` without consulting any documentation.
|
|
661
|
+
|
|
662
|
+
`--json` returns a stable contract (ADR-007):
|
|
663
|
+
|
|
664
|
+
```json
|
|
665
|
+
{
|
|
666
|
+
"type": "requirement",
|
|
667
|
+
"missing_required": [],
|
|
668
|
+
"missing_recommended": ["risks", "assumptions"]
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
v0.5.0 generates suggestions for **Requirement** artifacts. Other known types
|
|
673
|
+
(e.g. Decision) and Unknown documents return a short explanatory message instead.
|
|
674
|
+
`improve` is advisory: it exits `0` for any completed analysis (with or without
|
|
675
|
+
suggestions) and `2` for usage errors. The presence of suggestions never changes
|
|
676
|
+
the exit code.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
619
680
|
## Review (Planned)
|
|
620
681
|
|
|
621
682
|
AI-assisted product review.
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/requirements_as_code.egg-info/SOURCES.txt
RENAMED
|
@@ -45,6 +45,7 @@ rac/classification.py
|
|
|
45
45
|
rac/cli.py
|
|
46
46
|
rac/diff.py
|
|
47
47
|
rac/fs.py
|
|
48
|
+
rac/improve.py
|
|
48
49
|
rac/ingest.py
|
|
49
50
|
rac/inspect.py
|
|
50
51
|
rac/models.py
|
|
@@ -62,6 +63,7 @@ tests/conftest.py
|
|
|
62
63
|
tests/test_cli.py
|
|
63
64
|
tests/test_decision_metadata.py
|
|
64
65
|
tests/test_diff.py
|
|
66
|
+
tests/test_improve.py
|
|
65
67
|
tests/test_ingest.py
|
|
66
68
|
tests/test_inspect.py
|
|
67
69
|
tests/test_parser.py
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Tests for artifact improvement (`rac improve`, v0.5.0).
|
|
2
|
+
|
|
3
|
+
Advisory, deterministic, schema-driven, read-only. Requirement artifacts get
|
|
4
|
+
missing-section suggestions; other known types and Unknown get explanatory
|
|
5
|
+
guidance. Exit code is always 0 for a completed analysis, 2 for usage errors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from rac.cli import main
|
|
17
|
+
from rac.improve import improve_file, improve_text
|
|
18
|
+
|
|
19
|
+
from conftest import fixture_path
|
|
20
|
+
|
|
21
|
+
# A requirement missing a *required* section (Requirements) but still classifying
|
|
22
|
+
# as a requirement — it keeps enough recommended sections to clear the threshold.
|
|
23
|
+
NO_REQUIREMENTS = (
|
|
24
|
+
"# Feature\n\n## Problem\n\np\n\n## Success Metrics\n\n- m\n\n## Risks\n\n- r\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --- service layer ----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_requirement_reports_missing_recommended():
|
|
32
|
+
result = improve_file(fixture_path("inspect", "requirement.md"))
|
|
33
|
+
assert result.type == "requirement"
|
|
34
|
+
assert result.missing_required == []
|
|
35
|
+
assert "risks" in result.missing_recommended
|
|
36
|
+
assert "assumptions" in result.missing_recommended
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_requirement_reports_missing_required():
|
|
40
|
+
result = improve_text(NO_REQUIREMENTS)
|
|
41
|
+
assert result.type == "requirement"
|
|
42
|
+
assert result.missing_required == ["requirements"]
|
|
43
|
+
assert result.missing_recommended == ["assumptions"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_complete_requirement_has_nothing_missing():
|
|
47
|
+
complete = (
|
|
48
|
+
"# Feature\n\n## Problem\n\np\n\n## Requirements\n\n[REQ-001] x\n\n"
|
|
49
|
+
"## Success Metrics\n\n- m\n\n## Risks\n\n- r\n\n## Assumptions\n\n- a\n"
|
|
50
|
+
)
|
|
51
|
+
result = improve_text(complete)
|
|
52
|
+
assert result.type == "requirement"
|
|
53
|
+
assert result.missing_required == []
|
|
54
|
+
assert result.missing_recommended == []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_unknown_artifact_yields_no_suggestions():
|
|
58
|
+
result = improve_file(fixture_path("inspect", "ambiguous.md"))
|
|
59
|
+
assert result.type == "unknown"
|
|
60
|
+
assert result.missing_required == []
|
|
61
|
+
assert result.missing_recommended == []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_decision_is_out_of_scope_for_v050():
|
|
65
|
+
result = improve_file(fixture_path("decision", "with_metadata.md"))
|
|
66
|
+
assert result.type == "decision"
|
|
67
|
+
assert not result.supported
|
|
68
|
+
assert result.missing_required == []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_improve_does_not_depend_on_typescore():
|
|
72
|
+
# Decoupling guard: improvement must not reach into classification scoring.
|
|
73
|
+
import inspect as _inspect
|
|
74
|
+
|
|
75
|
+
import rac.improve as improve_mod
|
|
76
|
+
|
|
77
|
+
src = _inspect.getsource(improve_mod)
|
|
78
|
+
assert "TypeScore" not in src
|
|
79
|
+
assert "score_artifacts" not in src
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --- JSON contract (ADR-007) ------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_json_shape_is_stable(capsys):
|
|
86
|
+
rc = main(["improve", fixture_path("inspect", "requirement.md"), "--json"])
|
|
87
|
+
assert rc == 0
|
|
88
|
+
payload = json.loads(capsys.readouterr().out)
|
|
89
|
+
assert set(payload) == {"type", "missing_required", "missing_recommended"}
|
|
90
|
+
assert payload["type"] == "requirement"
|
|
91
|
+
# closest_type is reserved on the model but not serialized in v0.5.0.
|
|
92
|
+
assert "closest_type" not in payload
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_json_uses_snake_cased_section_names(capsys):
|
|
96
|
+
# "success metrics" -> "success_metrics" when missing.
|
|
97
|
+
text = "# F\n\n## Problem\n\np\n\n## Requirements\n\n[REQ-001] x\n"
|
|
98
|
+
monkey_stdin(text)
|
|
99
|
+
rc = main(["improve", "-", "--json"])
|
|
100
|
+
assert rc == 0
|
|
101
|
+
payload = json.loads(capsys.readouterr().out)
|
|
102
|
+
assert "success_metrics" in payload["missing_recommended"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_json_unknown_has_empty_arrays(capsys):
|
|
106
|
+
rc = main(["improve", fixture_path("inspect", "ambiguous.md"), "--json"])
|
|
107
|
+
assert rc == 0
|
|
108
|
+
payload = json.loads(capsys.readouterr().out)
|
|
109
|
+
assert payload["type"] == "unknown"
|
|
110
|
+
assert payload["missing_required"] == []
|
|
111
|
+
assert payload["missing_recommended"] == []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- template (REQ-003) -----------------------------------------------------
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_template_emits_todo_and_guidance(capsys):
|
|
118
|
+
rc = main(["improve", fixture_path("inspect", "requirement.md"), "--template"])
|
|
119
|
+
assert rc == 0
|
|
120
|
+
out = capsys.readouterr().out
|
|
121
|
+
assert "## Risks" in out
|
|
122
|
+
assert "_TODO_" in out
|
|
123
|
+
assert "<!--" in out # schema guidance comment
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_template_orders_required_before_recommended(capsys):
|
|
127
|
+
monkey_stdin(NO_REQUIREMENTS)
|
|
128
|
+
rc = main(["improve", "-", "--template"])
|
|
129
|
+
assert rc == 0
|
|
130
|
+
out = capsys.readouterr().out
|
|
131
|
+
assert out.index("## Requirements") < out.index("## Assumptions")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# --- human output -----------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_human_output_lists_missing(capsys):
|
|
138
|
+
rc = main(["improve", fixture_path("inspect", "requirement.md")])
|
|
139
|
+
assert rc == 0
|
|
140
|
+
out = capsys.readouterr().out
|
|
141
|
+
assert "Artifact Type: Requirement" in out
|
|
142
|
+
assert "Missing Recommended:" in out
|
|
143
|
+
assert "Risks" in out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_human_unknown_message(capsys):
|
|
147
|
+
rc = main(["improve", fixture_path("inspect", "ambiguous.md")])
|
|
148
|
+
assert rc == 0
|
|
149
|
+
out = capsys.readouterr().out
|
|
150
|
+
assert "Unable to generate improvement guidance." in out
|
|
151
|
+
assert "could not be determined" in out
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_human_decision_is_generic_not_requirement_worded(capsys):
|
|
155
|
+
rc = main(["improve", fixture_path("decision", "with_metadata.md")])
|
|
156
|
+
assert rc == 0
|
|
157
|
+
out = capsys.readouterr().out
|
|
158
|
+
assert "Artifact Type: Decision" in out
|
|
159
|
+
assert "not currently available for this artifact type" in out
|
|
160
|
+
assert "Requirement" not in out # no hard-coded requirement wording
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# --- pipelines (ADR-011) ----------------------------------------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def monkey_stdin(text: str) -> None:
|
|
167
|
+
import sys
|
|
168
|
+
|
|
169
|
+
sys.stdin = io.StringIO(text)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_stdin_matches_file(monkeypatch, capsys):
|
|
173
|
+
text = Path(fixture_path("inspect", "requirement.md")).read_text()
|
|
174
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(text))
|
|
175
|
+
rc = main(["improve", "-", "--json"])
|
|
176
|
+
assert rc == 0
|
|
177
|
+
from_stdin = json.loads(capsys.readouterr().out)
|
|
178
|
+
assert from_stdin == improve_text(text).to_dict()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# --- usage errors (exit 2) --------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_missing_file_exits_two():
|
|
185
|
+
with pytest.raises(SystemExit) as exc:
|
|
186
|
+
main(["improve", fixture_path("inspect", "does_not_exist.md")])
|
|
187
|
+
assert exc.value.code == 2
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_non_markdown_file_exits_two(tmp_path):
|
|
191
|
+
bad = tmp_path / "notes.txt"
|
|
192
|
+
bad.write_text("hello")
|
|
193
|
+
with pytest.raises(SystemExit) as exc:
|
|
194
|
+
main(["improve", str(bad)])
|
|
195
|
+
assert exc.value.code == 2
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_json_and_template_are_mutually_exclusive():
|
|
199
|
+
with pytest.raises(SystemExit): # argparse mutually-exclusive error
|
|
200
|
+
main(["improve", fixture_path("inspect", "requirement.md"), "--json", "--template"])
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --- read-only (REQ-004) ----------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_improve_does_not_modify_the_file(tmp_path):
|
|
207
|
+
f = tmp_path / "req.md"
|
|
208
|
+
original = "# F\n\n## Problem\n\np\n\n## Requirements\n\n[REQ-001] x\n"
|
|
209
|
+
f.write_text(original)
|
|
210
|
+
before = f.stat().st_mtime_ns
|
|
211
|
+
main(["improve", str(f), "--template"])
|
|
212
|
+
assert f.read_text() == original
|
|
213
|
+
assert f.stat().st_mtime_ns == before
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-001-markdown-first.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/adr/adr-002-ai-optional.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.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.4.2 → requirements_as_code-0.5.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
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.0-workspace-analysis.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.1-review-engine.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.2-mcp-server.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.4-claude-skills.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/future/v1.4-python-sdk.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/archive/v0.5-decisions.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/planning/roadmap/v0.3.1-formats.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.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
|
|
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.4.2 → requirements_as_code-0.5.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.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/bad_category.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/bad_status.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/minimal.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/decision/with_metadata.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/ambiguous.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/decision.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/inspect/requirement.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/duplicate_ids.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/empty_req_text.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/malformed_id.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_id.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_problem.md
RENAMED
|
File without changes
|
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/missing_title.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/invalid/multiple_titles.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/broken.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/feature_a.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.0}/tests/fixtures/portfolio/feature_b.md
RENAMED
|
File without changes
|
{requirements_as_code-0.4.2 → requirements_as_code-0.5.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
|