docassert 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {docassert-0.2.0/docassert.egg-info → docassert-0.2.1}/PKG-INFO +6 -2
  2. {docassert-0.2.0 → docassert-0.2.1}/README.md +5 -1
  3. {docassert-0.2.0 → docassert-0.2.1}/docassert/__init__.py +1 -1
  4. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/consistency.yaml +4 -0
  5. {docassert-0.2.0 → docassert-0.2.1}/docassert/cli.py +43 -21
  6. {docassert-0.2.0 → docassert-0.2.1}/docassert/consistency.py +22 -2
  7. {docassert-0.2.0 → docassert-0.2.1/docassert.egg-info}/PKG-INFO +6 -2
  8. {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/SOURCES.txt +1 -0
  9. docassert-0.2.1/tests/test_defects.py +85 -0
  10. {docassert-0.2.0 → docassert-0.2.1}/LICENSE +0 -0
  11. {docassert-0.2.0 → docassert-0.2.1}/NOTICE +0 -0
  12. {docassert-0.2.0 → docassert-0.2.1}/docassert/__main__.py +0 -0
  13. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  14. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  15. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  16. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  17. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  18. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  19. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  20. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  21. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  22. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  23. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/project.criteria.yaml +0 -0
  24. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  25. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  26. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  27. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  28. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  29. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  30. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  31. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  32. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  33. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  34. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/profiles/lean-startup.yaml +0 -0
  35. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  36. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/adr.schema.json +0 -0
  37. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  38. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/brd.schema.json +0 -0
  39. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/business-case.schema.json +0 -0
  40. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/charter.schema.json +0 -0
  41. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  42. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/frnfr.schema.json +0 -0
  43. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  44. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  45. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/prd.schema.json +0 -0
  46. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/project.schema.json +0 -0
  47. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  48. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  49. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  50. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/risk-register.schema.json +0 -0
  51. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  52. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/runbook.schema.json +0 -0
  53. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/status-report.schema.json +0 -0
  54. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/test-cases.schema.json +0 -0
  55. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/user-story.schema.json +0 -0
  56. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/adr.template.md +0 -0
  57. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/benefits-realization.template.md +0 -0
  58. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/brd.template.md +0 -0
  59. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/business-case.template.md +0 -0
  60. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/charter.template.md +0 -0
  61. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  62. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/frnfr.template.md +0 -0
  63. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  64. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  65. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/prd.template.md +0 -0
  66. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/project.template.md +0 -0
  67. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  68. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  69. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  70. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/risk-register.template.md +0 -0
  71. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/rollback-plan.template.md +0 -0
  72. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/runbook.template.md +0 -0
  73. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/status-report.template.md +0 -0
  74. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/test-cases.template.md +0 -0
  75. {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/user-story.template.md +0 -0
  76. {docassert-0.2.0 → docassert-0.2.1}/docassert/config.py +0 -0
  77. {docassert-0.2.0 → docassert-0.2.1}/docassert/extract.py +0 -0
  78. {docassert-0.2.0 → docassert-0.2.1}/docassert/graph.py +0 -0
  79. {docassert-0.2.0 → docassert-0.2.1}/docassert/loader.py +0 -0
  80. {docassert-0.2.0 → docassert-0.2.1}/docassert/models.py +0 -0
  81. {docassert-0.2.0 → docassert-0.2.1}/docassert/profiles.py +0 -0
  82. {docassert-0.2.0 → docassert-0.2.1}/docassert/projects.py +0 -0
  83. {docassert-0.2.0 → docassert-0.2.1}/docassert/report.py +0 -0
  84. {docassert-0.2.0 → docassert-0.2.1}/docassert/rtm.py +0 -0
  85. {docassert-0.2.0 → docassert-0.2.1}/docassert/semantic.py +0 -0
  86. {docassert-0.2.0 → docassert-0.2.1}/docassert/status.py +0 -0
  87. {docassert-0.2.0 → docassert-0.2.1}/docassert/structural.py +0 -0
  88. {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/dependency_links.txt +0 -0
  89. {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/entry_points.txt +0 -0
  90. {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/requires.txt +0 -0
  91. {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/top_level.txt +0 -0
  92. {docassert-0.2.0 → docassert-0.2.1}/pyproject.toml +0 -0
  93. {docassert-0.2.0 → docassert-0.2.1}/setup.cfg +0 -0
  94. {docassert-0.2.0 → docassert-0.2.1}/tests/test_config.py +0 -0
  95. {docassert-0.2.0 → docassert-0.2.1}/tests/test_consistency.py +0 -0
  96. {docassert-0.2.0 → docassert-0.2.1}/tests/test_extract.py +0 -0
  97. {docassert-0.2.0 → docassert-0.2.1}/tests/test_graph.py +0 -0
  98. {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_delivery.py +0 -0
  99. {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_governance.py +0 -0
  100. {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_operate.py +0 -0
  101. {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_reporting.py +0 -0
  102. {docassert-0.2.0 → docassert-0.2.1}/tests/test_profiles.py +0 -0
  103. {docassert-0.2.0 → docassert-0.2.1}/tests/test_projects.py +0 -0
  104. {docassert-0.2.0 → docassert-0.2.1}/tests/test_status.py +0 -0
  105. {docassert-0.2.0 → docassert-0.2.1}/tests/test_structural.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
5
5
  Author: C4G Enterprises Inc.
6
6
  License: Apache-2.0
@@ -80,7 +80,7 @@ you can customize them.
80
80
 
81
81
  | Command | What it does |
82
82
  |---|---|
83
- | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
83
+ | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
84
84
  | `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
85
85
  | `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
86
86
  | `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
@@ -89,6 +89,10 @@ you can customize them.
89
89
  | `docassert init [DIR]` | Scaffold the default config into a repo. |
90
90
  | `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
91
91
 
92
+ Every document-reading command accepts `--documents-dir` (default `documents/`).
93
+ AI alignment grades at most `alignment_limit` links per run (default 25; set it
94
+ in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
95
+
92
96
  ## Document kinds
93
97
 
94
98
  Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
@@ -42,7 +42,7 @@ you can customize them.
42
42
 
43
43
  | Command | What it does |
44
44
  |---|---|
45
- | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
45
+ | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
46
46
  | `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
47
47
  | `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
48
48
  | `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
@@ -51,6 +51,10 @@ you can customize them.
51
51
  | `docassert init [DIR]` | Scaffold the default config into a repo. |
52
52
  | `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
53
53
 
54
+ Every document-reading command accepts `--documents-dir` (default `documents/`).
55
+ AI alignment grades at most `alignment_limit` links per run (default 25; set it
56
+ in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
57
+
54
58
  ## Document kinds
55
59
 
56
60
  Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
@@ -5,4 +5,4 @@ standard: deterministic structural checks that gate a merge, plus optional
5
5
  AI-graded semantic checks that advise.
6
6
  """
7
7
 
8
- __version__ = "0.2.0"
8
+ __version__ = "0.2.1"
@@ -35,6 +35,10 @@ coverage:
35
35
 
36
36
  # Advisory AI alignment: for each relation, judge whether the child genuinely
37
37
  # fulfils the parent it links to. Never blocks.
38
+ # Each graded link costs one API call; `alignment_limit` caps calls per run
39
+ # (0 = no cap).
40
+ alignment_limit: 25
41
+
38
42
  alignment:
39
43
  - relation: traces
40
44
  prompt: >
@@ -3,8 +3,10 @@
3
3
  docassert validate documents/charters/aurora.md
4
4
  docassert validate documents/**/*.md --junit out.xml --markdown comment.md
5
5
 
6
- Exit code = number of BLOCKING (structural) failures. Advisory (AI) failures
7
- never affect the exit code, so CI is gated only by deterministic checks.
6
+ Exit code = number of BLOCKING (structural) failures, capped at 125 so large
7
+ counts can't wrap around the 8-bit exit-status space (256 failures must never
8
+ read as success). Advisory (AI) failures never affect the exit code, so CI is
9
+ gated only by deterministic checks.
8
10
  """
9
11
  from __future__ import annotations
10
12
 
@@ -23,15 +25,24 @@ from .models import CheckResult
23
25
  from .semantic import run_semantic
24
26
  from .structural import run_structural
25
27
 
26
- # The user's documents live here; criteria / schema / consistency.yaml / profiles
27
- # resolve via `config` (local override packaged default).
28
- DOCUMENTS_DIR = Path("documents")
28
+ # Default documents location; every document-reading command accepts
29
+ # --documents-dir to override it. Criteria / schema / consistency.yaml /
30
+ # profiles resolve via `config` (local override → packaged default).
31
+ DEFAULT_DOCUMENTS_DIR = "documents"
29
32
 
33
+ # POSIX exit statuses are 8-bit; 126+ carry shell meanings. Cap so a failure
34
+ # count can never wrap to 0.
35
+ _EXIT_CAP = 125
30
36
 
31
- def _build_id_index() -> dict[str, list[str]]:
32
- """Map document id -> [paths] across all documents/, for uniqueness checks."""
37
+
38
+ def _capped(failures: int) -> int:
39
+ return min(failures, _EXIT_CAP)
40
+
41
+
42
+ def _build_id_index(documents_dir: Path) -> dict[str, list[str]]:
43
+ """Map document id -> [paths] across the documents tree, for uniqueness checks."""
33
44
  index: dict[str, list[str]] = defaultdict(list)
34
- for path in DOCUMENTS_DIR.rglob("*.md"):
45
+ for path in documents_dir.rglob("*.md"):
35
46
  try:
36
47
  doc = load(path)
37
48
  except ValueError:
@@ -86,7 +97,7 @@ def cmd_validate(args: argparse.Namespace) -> int:
86
97
  print("docassert: no markdown documents matched.", file=sys.stderr)
87
98
  return 0
88
99
 
89
- id_index = _build_id_index()
100
+ id_index = _build_id_index(Path(args.documents_dir))
90
101
  results_by_doc: dict[str, list[CheckResult]] = {}
91
102
  for path in files:
92
103
  try:
@@ -106,12 +117,12 @@ def cmd_validate(args: argparse.Namespace) -> int:
106
117
  if args.markdown:
107
118
  Path(args.markdown).write_text(report.markdown(results_by_doc))
108
119
 
109
- return sum(1 for rs in results_by_doc.values()
110
- for r in rs if r.is_blocking_failure)
120
+ return _capped(sum(1 for rs in results_by_doc.values()
121
+ for r in rs if r.is_blocking_failure))
111
122
 
112
123
 
113
124
  def cmd_consistency(args: argparse.Namespace) -> int:
114
- results = run_consistency(DOCUMENTS_DIR, with_semantic=not args.no_semantic)
125
+ results = run_consistency(args.documents_dir, with_semantic=not args.no_semantic)
115
126
  results_by_doc = {"consistency (cross-document)": results}
116
127
 
117
128
  print(report.console(results_by_doc))
@@ -123,7 +134,7 @@ def cmd_consistency(args: argparse.Namespace) -> int:
123
134
  Path(args.markdown).write_text(
124
135
  report.markdown(results_by_doc, title="docassert consistency"))
125
136
 
126
- return sum(1 for r in results if r.is_blocking_failure)
137
+ return _capped(sum(1 for r in results if r.is_blocking_failure))
127
138
 
128
139
 
129
140
  def _project_code(value: str | None) -> str | None:
@@ -132,7 +143,7 @@ def _project_code(value: str | None) -> str | None:
132
143
 
133
144
 
134
145
  def cmd_rtm(args: argparse.Namespace) -> int:
135
- graph = build_graph(DOCUMENTS_DIR)
146
+ graph = build_graph(args.documents_dir)
136
147
  code = _project_code(args.project)
137
148
  text = rtm.render_csv(graph, code) if args.csv else rtm.render_markdown(graph, code)
138
149
  if args.out:
@@ -145,7 +156,7 @@ def cmd_rtm(args: argparse.Namespace) -> int:
145
156
 
146
157
  def cmd_projects(args: argparse.Namespace) -> int:
147
158
  from . import projects as proj
148
- plist = proj.load_projects(DOCUMENTS_DIR)
159
+ plist = proj.load_projects(args.documents_dir)
149
160
  issues = proj.registry_issues(plist)
150
161
  for issue in issues:
151
162
  print(f"docassert: {issue}", file=sys.stderr)
@@ -172,7 +183,7 @@ def cmd_projects(args: argparse.Namespace) -> int:
172
183
  def cmd_status(args: argparse.Namespace) -> int:
173
184
  from . import status as status_mod
174
185
  if args.index:
175
- index = status_mod.build_index(DOCUMENTS_DIR)
186
+ index = status_mod.build_index(args.documents_dir)
176
187
  if args.format == "json":
177
188
  text = status_mod.render_json(index)
178
189
  elif args.format == "html":
@@ -181,7 +192,7 @@ def cmd_status(args: argparse.Namespace) -> int:
181
192
  text = status_mod.render_index_markdown(index)
182
193
  tag = index["overall"]["rag"]
183
194
  else:
184
- model = status_mod.build_status(DOCUMENTS_DIR, project=args.project)
195
+ model = status_mod.build_status(args.documents_dir, project=args.project)
185
196
  if args.project and not model["documents"]:
186
197
  print(f"docassert: no documents for project {args.project!r}", file=sys.stderr)
187
198
  return 2
@@ -206,16 +217,17 @@ def cmd_pages(args: argparse.Namespace) -> int:
206
217
  from . import status as status_mod
207
218
  out = Path(args.out)
208
219
  out.mkdir(parents=True, exist_ok=True)
220
+ docs_dir = args.documents_dir
209
221
 
210
- index = status_mod.build_index(DOCUMENTS_DIR)
222
+ index = status_mod.build_index(docs_dir)
211
223
  (out / "index.html").write_text(status_mod.render_index_html(index))
212
224
 
213
- plist = projects_mod.load_projects(DOCUMENTS_DIR)
225
+ plist = projects_mod.load_projects(docs_dir)
214
226
  for p in plist:
215
- model = status_mod.build_status(DOCUMENTS_DIR, project=p["id"])
227
+ model = status_mod.build_status(docs_dir, project=p["id"])
216
228
  (out / f"{p['id']}.html").write_text(status_mod.render_html(model))
217
229
 
218
- (out / "RTM.md").write_text(rtm.render_markdown(build_graph(DOCUMENTS_DIR)))
230
+ (out / "RTM.md").write_text(rtm.render_markdown(build_graph(docs_dir)))
219
231
  print(f"docassert: wrote {out}/ — index + {len(plist)} project page(s) + RTM.md "
220
232
  f"(portfolio: {index['overall']['rag']})")
221
233
  return 0
@@ -256,10 +268,15 @@ def main(argv: list[str] | None = None) -> int:
256
268
  parser.add_argument("--version", action="version", version=f"docassert {__version__}")
257
269
  sub = parser.add_subparsers(dest="command", required=True)
258
270
 
271
+ def docs_dir_opt(sp: argparse.ArgumentParser) -> None:
272
+ sp.add_argument("--documents-dir", default=DEFAULT_DOCUMENTS_DIR,
273
+ help=f"Documents tree to read (default: {DEFAULT_DOCUMENTS_DIR}/).")
274
+
259
275
  v = sub.add_parser("validate", help="Validate documents against their criteria.")
260
276
  v.add_argument("paths", nargs="+", help="Markdown files or globs.")
261
277
  v.add_argument("--junit", help="Write a JUnit XML report to this path.")
262
278
  v.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
279
+ docs_dir_opt(v)
263
280
  v.set_defaults(func=cmd_validate)
264
281
 
265
282
  c = sub.add_parser("consistency", help="Check cross-document traceability.")
@@ -267,12 +284,14 @@ def main(argv: list[str] | None = None) -> int:
267
284
  c.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
268
285
  c.add_argument("--no-semantic", action="store_true",
269
286
  help="Skip AI alignment (structural consistency only).")
287
+ docs_dir_opt(c)
270
288
  c.set_defaults(func=cmd_consistency)
271
289
 
272
290
  r = sub.add_parser("rtm", help="Generate the requirements traceability matrix.")
273
291
  r.add_argument("--out", help="Write to this path instead of stdout.")
274
292
  r.add_argument("--csv", action="store_true", help="Emit CSV instead of Markdown.")
275
293
  r.add_argument("--project", help="Scope to one project (PRJ-NNN-CODE id or CODE).")
294
+ docs_dir_opt(r)
276
295
  r.set_defaults(func=cmd_rtm)
277
296
 
278
297
  s = sub.add_parser("status", help="Derive a project status page from the documents.")
@@ -284,16 +303,19 @@ def main(argv: list[str] | None = None) -> int:
284
303
  s.add_argument("--index", action="store_true",
285
304
  help="Render the multi-project portfolio index instead of one status.")
286
305
  s.add_argument("--out", help="Write to this path instead of stdout.")
306
+ docs_dir_opt(s)
287
307
  s.set_defaults(func=cmd_status)
288
308
 
289
309
  pg = sub.add_parser("pages", help="Build the full Pages site (portfolio index + a page per project).")
290
310
  pg.add_argument("--out", default="_site", help="Output directory (default: _site).")
311
+ docs_dir_opt(pg)
291
312
  pg.set_defaults(func=cmd_pages)
292
313
 
293
314
  p = sub.add_parser("projects", help="Generate the project registry from the project.md anchors.")
294
315
  p.add_argument("--out", help="Write to this path instead of stdout (e.g. projects.yaml).")
295
316
  p.add_argument("--check", action="store_true",
296
317
  help="Exit non-zero if the registry file is stale (CI freshness gate).")
318
+ docs_dir_opt(p)
297
319
  p.set_defaults(func=cmd_projects)
298
320
 
299
321
  ini = sub.add_parser("init", help="Scaffold the default criteria/schema/profiles/templates into a repo.")
@@ -130,6 +130,12 @@ def check_profile_completeness(documents_dir: str | Path = "documents") -> Check
130
130
 
131
131
 
132
132
  # ── semantic (advisory) ────────────────────────────────────────────────────
133
+ # Each alignment edge costs one API call, so a large graph could otherwise run
134
+ # away on cost. Cap per run; tune with `alignment_limit` in consistency.yaml
135
+ # (0 disables the cap).
136
+ DEFAULT_ALIGNMENT_LIMIT = 25
137
+
138
+
133
139
  def run_alignment_checks(graph, config) -> list[CheckResult]:
134
140
  edges = [] # (prompt, parent, child, relation)
135
141
  for rule in config.get("alignment", []):
@@ -142,12 +148,26 @@ def run_alignment_checks(graph, config) -> list[CheckResult]:
142
148
 
143
149
  if not edges:
144
150
  return []
151
+
152
+ limit = int(config.get("alignment_limit", DEFAULT_ALIGNMENT_LIMIT) or 0)
153
+ note: CheckResult | None = None
154
+ if limit and len(edges) > limit:
155
+ note = CheckResult(
156
+ "alignment-limit", True, False,
157
+ f"graded {limit} of {len(edges)} link(s) — raise `alignment_limit` "
158
+ f"in consistency.yaml to grade more per run",
159
+ kind="semantic", score=None)
160
+ edges = edges[:limit]
161
+
145
162
  if not os.environ.get("ANTHROPIC_API_KEY"):
146
163
  return [CheckResult("alignment", True, False,
147
164
  f"skipped — no ANTHROPIC_API_KEY ({len(edges)} link(s) to grade)",
148
165
  kind="semantic", score=None)]
149
- return [run_alignment(f"align:{c.id}-{rel}-{p.id}", prompt, p.text, c.text)
150
- for prompt, p, c, rel in edges]
166
+ results = [run_alignment(f"align:{c.id}-{rel}-{p.id}", prompt, p.text, c.text)
167
+ for prompt, p, c, rel in edges]
168
+ if note is not None:
169
+ results.append(note)
170
+ return results
151
171
 
152
172
 
153
173
  def run_consistency(documents_dir: str | Path = "documents",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
5
5
  Author: C4G Enterprises Inc.
6
6
  License: Apache-2.0
@@ -80,7 +80,7 @@ you can customize them.
80
80
 
81
81
  | Command | What it does |
82
82
  |---|---|
83
- | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
83
+ | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
84
84
  | `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
85
85
  | `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
86
86
  | `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
@@ -89,6 +89,10 @@ you can customize them.
89
89
  | `docassert init [DIR]` | Scaffold the default config into a repo. |
90
90
  | `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
91
91
 
92
+ Every document-reading command accepts `--documents-dir` (default `documents/`).
93
+ AI alignment grades at most `alignment_limit` links per run (default 25; set it
94
+ in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
95
+
92
96
  ## Document kinds
93
97
 
94
98
  Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
@@ -90,6 +90,7 @@ docassert/_data/templates/test-cases.template.md
90
90
  docassert/_data/templates/user-story.template.md
91
91
  tests/test_config.py
92
92
  tests/test_consistency.py
93
+ tests/test_defects.py
93
94
  tests/test_extract.py
94
95
  tests/test_graph.py
95
96
  tests/test_kinds_delivery.py
@@ -0,0 +1,85 @@
1
+ """Tests for the 0.2.1 defect fixes: exit-code cap, --documents-dir, alignment cap."""
2
+ from docassert import consistency as C
3
+ from docassert.cli import _capped, main
4
+ from docassert.graph import Graph
5
+ from docassert.models import CheckResult, Item
6
+
7
+ PROJECT_MD = """---
8
+ kind: project
9
+ id: PRJ-009-TST
10
+ code: TST
11
+ name: Test Project
12
+ sponsor: jane.doe
13
+ status: proposed
14
+ ---
15
+
16
+ ## Overview
17
+ A test project.
18
+
19
+ ## Scope
20
+ Everything.
21
+ """
22
+
23
+
24
+ # ── exit-code cap ────────────────────────────────────────────────────────────
25
+ def test_exit_code_capped_below_wraparound():
26
+ assert _capped(0) == 0
27
+ assert _capped(3) == 3
28
+ assert _capped(125) == 125
29
+ assert _capped(256) == 125 # would otherwise wrap to exit status 0
30
+ assert _capped(1000) == 125
31
+
32
+
33
+ # ── --documents-dir ──────────────────────────────────────────────────────────
34
+ def test_projects_reads_documents_dir_flag(tmp_path, monkeypatch, capsys):
35
+ docs = tmp_path / "elsewhere"
36
+ (docs / "PRJ-009-TST").mkdir(parents=True)
37
+ (docs / "PRJ-009-TST" / "project.md").write_text(PROJECT_MD, encoding="utf-8")
38
+ monkeypatch.chdir(tmp_path) # cwd has no documents/ at all
39
+ assert main(["projects", "--documents-dir", str(docs)]) == 0
40
+ assert "PRJ-009-TST" in capsys.readouterr().out
41
+
42
+
43
+ def test_status_reads_documents_dir_flag(tmp_path, monkeypatch, capsys):
44
+ docs = tmp_path / "elsewhere"
45
+ (docs / "PRJ-009-TST").mkdir(parents=True)
46
+ (docs / "PRJ-009-TST" / "project.md").write_text(PROJECT_MD, encoding="utf-8")
47
+ monkeypatch.chdir(tmp_path)
48
+ assert main(["status", "--documents-dir", str(docs), "--summary"]) == 0
49
+ assert "Derived from 1 documents" in capsys.readouterr().out
50
+
51
+
52
+ # ── alignment call cap ───────────────────────────────────────────────────────
53
+ def _graph_with_edges(n):
54
+ g = Graph()
55
+ g.add(Item("TST-BR-001", "TST", "BR", "parent", {}, "d.md", "k", "approved", "S"))
56
+ for i in range(n):
57
+ g.add(Item(f"TST-PR-{i:03d}", "TST", "PR", "child",
58
+ {"traces": ["TST-BR-001"]}, "d.md", "k", "approved", "S"))
59
+ return g
60
+
61
+
62
+ def _stub_calls(monkeypatch):
63
+ calls = []
64
+ monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
65
+ monkeypatch.setattr(C, "run_alignment",
66
+ lambda cid, *a: calls.append(cid) or CheckResult(
67
+ cid, True, False, "ok", kind="semantic", score=1.0))
68
+ return calls
69
+
70
+
71
+ def test_alignment_capped(monkeypatch):
72
+ calls = _stub_calls(monkeypatch)
73
+ cfg = {"alignment": [{"relation": "traces", "prompt": "judge"}], "alignment_limit": 2}
74
+ results = C.run_alignment_checks(_graph_with_edges(4), cfg)
75
+ assert len(calls) == 2
76
+ note = next(r for r in results if r.check_id == "alignment-limit")
77
+ assert "graded 2 of 4" in note.detail and not note.blocking
78
+
79
+
80
+ def test_alignment_cap_disabled_with_zero(monkeypatch):
81
+ calls = _stub_calls(monkeypatch)
82
+ cfg = {"alignment": [{"relation": "traces", "prompt": "judge"}], "alignment_limit": 0}
83
+ results = C.run_alignment_checks(_graph_with_edges(4), cfg)
84
+ assert len(calls) == 4
85
+ assert not any(r.check_id == "alignment-limit" for r in results)
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