docassert 0.14.1__tar.gz → 0.16.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.
- {docassert-0.14.1/docassert.egg-info → docassert-0.16.0}/PKG-INFO +1 -1
- {docassert-0.14.1 → docassert-0.16.0}/docassert/__init__.py +1 -1
- docassert-0.16.0/docassert/_data/criteria/operations.criteria.yaml +40 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/risk-register.criteria.yaml +5 -0
- docassert-0.16.0/docassert/_data/profiles/operations.yaml +18 -0
- docassert-0.16.0/docassert/_data/schema/operations.schema.json +50 -0
- docassert-0.16.0/docassert/_data/templates/operations.template.md +17 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/status/derive.py +32 -3
- {docassert-0.14.1 → docassert-0.16.0}/docassert/status/render.py +29 -6
- {docassert-0.14.1 → docassert-0.16.0}/docassert/structural.py +47 -0
- {docassert-0.14.1 → docassert-0.16.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.14.1 → docassert-0.16.0}/docassert.egg-info/SOURCES.txt +4 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_profiles.py +3 -2
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_status.py +111 -0
- {docassert-0.14.1 → docassert-0.16.0}/LICENSE +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/NOTICE +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/README.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/__main__.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/bridge/board.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/bridge/ops.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/bridge/plan.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/cli.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/config.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/consistency.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/extract.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/graph.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/loader.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/models.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/profiles.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/projects.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/report.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/rtm.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/scaffold.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/semantic.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert/status/__init__.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/pyproject.toml +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/setup.cfg +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_badge.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_bridge.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_config.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_consistency.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_defects.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_extract.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_graph.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_json_report.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_projects.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_scaffold.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_semantic.py +0 -0
- {docassert-0.14.1 → docassert-0.16.0}/tests/test_structural.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Audit criteria for the Operations kind (spec §13.21).
|
|
2
|
+
kind: operations
|
|
3
|
+
|
|
4
|
+
required_sections:
|
|
5
|
+
- Overview
|
|
6
|
+
- Services
|
|
7
|
+
|
|
8
|
+
item_sections:
|
|
9
|
+
- section: Services
|
|
10
|
+
prefix: SVC
|
|
11
|
+
|
|
12
|
+
checks:
|
|
13
|
+
- id: frontmatter-schema
|
|
14
|
+
type: structural
|
|
15
|
+
blocking: true
|
|
16
|
+
description: Frontmatter is valid against schema/operations.schema.json.
|
|
17
|
+
- id: frontmatter-complete
|
|
18
|
+
type: structural
|
|
19
|
+
blocking: once-proposed
|
|
20
|
+
description: Every schema-required frontmatter field is present (advisory while draft).
|
|
21
|
+
- id: required-sections
|
|
22
|
+
type: structural
|
|
23
|
+
blocking: once-proposed
|
|
24
|
+
description: Every required section is present and non-empty.
|
|
25
|
+
- id: items-well-formed
|
|
26
|
+
type: structural
|
|
27
|
+
blocking: true
|
|
28
|
+
description: Every SVC item follows the item grammar in its declared section.
|
|
29
|
+
- id: svc-items-complete
|
|
30
|
+
type: structural
|
|
31
|
+
blocking: once-proposed
|
|
32
|
+
description: Every SVC item states a Level (the commitment) and a Measure (how attainment is read).
|
|
33
|
+
- id: ops-review-fresh
|
|
34
|
+
type: structural
|
|
35
|
+
blocking: false
|
|
36
|
+
description: review_by is today or later; a past date is an amber input to derived status, never a merge blocker.
|
|
37
|
+
- id: unique-id
|
|
38
|
+
type: structural
|
|
39
|
+
blocking: true
|
|
40
|
+
description: The document id is unique across the repository.
|
|
@@ -30,6 +30,11 @@ checks:
|
|
|
30
30
|
type: structural
|
|
31
31
|
blocking: once-proposed
|
|
32
32
|
description: Every risk states a Probability, Impact, Owner, and Response.
|
|
33
|
+
- id: risk-disposition-valid
|
|
34
|
+
type: structural
|
|
35
|
+
blocking: true
|
|
36
|
+
description: Any Status field on a RISK item names a valid disposition (open/mitigated/accepted/closed).
|
|
37
|
+
|
|
33
38
|
- id: unique-id
|
|
34
39
|
type: structural
|
|
35
40
|
blocking: true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Profile: operations
|
|
2
|
+
#
|
|
3
|
+
# For a repository (or project) whose work is a standing service catalog
|
|
4
|
+
# rather than a delivery arc. The catalog itself is the one required kind;
|
|
5
|
+
# the register and runbook are strongly recommended companions.
|
|
6
|
+
name: operations
|
|
7
|
+
description: >
|
|
8
|
+
A governed service catalog: the operations document is required, with the
|
|
9
|
+
risk register and runbook recommended alongside it.
|
|
10
|
+
|
|
11
|
+
enforce_when: active
|
|
12
|
+
|
|
13
|
+
expects:
|
|
14
|
+
required:
|
|
15
|
+
- operations
|
|
16
|
+
recommended:
|
|
17
|
+
- risk-register
|
|
18
|
+
- runbook
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Operations frontmatter",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"kind",
|
|
7
|
+
"id",
|
|
8
|
+
"project",
|
|
9
|
+
"title",
|
|
10
|
+
"owner",
|
|
11
|
+
"status",
|
|
12
|
+
"review_by"
|
|
13
|
+
],
|
|
14
|
+
"additionalProperties": true,
|
|
15
|
+
"properties": {
|
|
16
|
+
"kind": {
|
|
17
|
+
"const": "operations"
|
|
18
|
+
},
|
|
19
|
+
"id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^[A-Z]{2,6}-[a-z0-9][a-z0-9-]*$",
|
|
22
|
+
"description": "Stable id, <CODE>-<slug>. Unique across documents/."
|
|
23
|
+
},
|
|
24
|
+
"title": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 3
|
|
27
|
+
},
|
|
28
|
+
"owner": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"minLength": 2
|
|
31
|
+
},
|
|
32
|
+
"status": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"draft",
|
|
35
|
+
"proposed",
|
|
36
|
+
"approved",
|
|
37
|
+
"baselined"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"project": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"pattern": "^PRJ-\\d{3,}-[A-Z]{2,6}$"
|
|
43
|
+
},
|
|
44
|
+
"review_by": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"format": "date",
|
|
47
|
+
"description": "The next operations review; a past date is an amber input to derived status."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: operations
|
|
3
|
+
project: PRJ-000-XXX # the owning project's id
|
|
4
|
+
id: XXX-operations # <CODE>-<slug>; the project code namespaces it
|
|
5
|
+
title: My Operations
|
|
6
|
+
owner: jane.doe
|
|
7
|
+
status: draft
|
|
8
|
+
review_by: 2026-12-31 # the next operations review; stale = amber
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
<!-- The operating period, the envelope, and what this catalog covers. -->
|
|
14
|
+
|
|
15
|
+
## Services
|
|
16
|
+
|
|
17
|
+
<!-- - **XXX-SVC-001**: What the service is. Level: the commitment, measurably stated. Measure: how attainment is read. -->
|
|
@@ -82,6 +82,9 @@ def _risks(graph, code=None):
|
|
|
82
82
|
description = (item.text[:m.start()] if m else item.text).strip()
|
|
83
83
|
rm = re.search(r"Response\s*:\s*(.+)$", item.text,
|
|
84
84
|
re.IGNORECASE | re.DOTALL)
|
|
85
|
+
raw = (_field_value(item.text, "status") or "open").lower()
|
|
86
|
+
disposition = raw if raw in {"open", "mitigated", "accepted",
|
|
87
|
+
"closed"} else "open"
|
|
85
88
|
risks.append({
|
|
86
89
|
"id": item.id,
|
|
87
90
|
"description": description,
|
|
@@ -90,9 +93,12 @@ def _risks(graph, code=None):
|
|
|
90
93
|
"impact": impact or "?",
|
|
91
94
|
"owner": _field_value(item.text, "owner") or "?",
|
|
92
95
|
"response": rm.group(1).strip() if rm else "",
|
|
96
|
+
"disposition": disposition,
|
|
93
97
|
"score": _SEVERITY.get(prob, 0) * _SEVERITY.get(impact, 0),
|
|
94
98
|
})
|
|
95
|
-
|
|
99
|
+
# open risks first, then by severity; dispositioned risks stay as record
|
|
100
|
+
return sorted(risks, key=lambda r: (r["disposition"] != "open",
|
|
101
|
+
-r["score"]))
|
|
96
102
|
|
|
97
103
|
|
|
98
104
|
def _broken_references(graph, code=None):
|
|
@@ -107,6 +113,25 @@ def _broken_references(graph, code=None):
|
|
|
107
113
|
return broken
|
|
108
114
|
|
|
109
115
|
|
|
116
|
+
def _operations(docs):
|
|
117
|
+
"""Operations documents with their review state."""
|
|
118
|
+
import datetime
|
|
119
|
+
out = []
|
|
120
|
+
for d in docs:
|
|
121
|
+
if d.kind != "operations":
|
|
122
|
+
continue
|
|
123
|
+
raw = d.frontmatter.get("review_by")
|
|
124
|
+
try:
|
|
125
|
+
due = (raw if isinstance(raw, datetime.date)
|
|
126
|
+
else datetime.date.fromisoformat(str(raw)))
|
|
127
|
+
fresh = due >= datetime.date.today()
|
|
128
|
+
due_s = due.isoformat()
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
fresh, due_s = False, str(raw)
|
|
131
|
+
out.append({"id": d.id, "review_by": due_s, "fresh": fresh})
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
110
135
|
def _latest_report(docs):
|
|
111
136
|
reports = [d for d in docs if d.kind == "status-report"]
|
|
112
137
|
if not reports:
|
|
@@ -173,6 +198,7 @@ def build_status(documents_dir=DOCUMENTS_DIR, project: str | None = None) -> dic
|
|
|
173
198
|
},
|
|
174
199
|
"coverage": _coverage(graph, cfg, code),
|
|
175
200
|
"risks": _risks(graph, code),
|
|
201
|
+
"operations": _operations(docs),
|
|
176
202
|
"broken_references": _broken_references(graph, code),
|
|
177
203
|
"latest_report": _latest_report(docs),
|
|
178
204
|
"completeness": completeness,
|
|
@@ -218,7 +244,7 @@ def build_index(documents_dir=DOCUMENTS_DIR) -> dict:
|
|
|
218
244
|
"rag": m["rag"],
|
|
219
245
|
"total": m["counts"]["total"],
|
|
220
246
|
"failing": m["counts"]["failing"],
|
|
221
|
-
"risks": len(m["risks"]),
|
|
247
|
+
"risks": len([r for r in m["risks"] if r.get("disposition", "open") == "open"]),
|
|
222
248
|
"coverage_gaps": sum(len(c["gaps"]) for c in m["coverage"]),
|
|
223
249
|
"broken": len(m["broken_references"]),
|
|
224
250
|
"completeness": m.get("completeness"),
|
|
@@ -238,7 +264,10 @@ def derive_rag(model) -> str:
|
|
|
238
264
|
reported = (model["latest_report"] or {}).get("rag")
|
|
239
265
|
completeness_gap = bool(comp and (comp["missing_required"] or comp["incomplete_required"]
|
|
240
266
|
or comp["recommended_gaps"] or comp.get("unknown")))
|
|
241
|
-
|
|
267
|
+
open_risks = [r for r in model["risks"] if r.get("disposition", "open") == "open"]
|
|
268
|
+
stale_ops = [o for o in model.get("operations", []) if not o["fresh"]]
|
|
269
|
+
if (coverage_gap or open_risks or stale_ops
|
|
270
|
+
or reported in {"amber", "red"} or completeness_gap):
|
|
242
271
|
return "amber"
|
|
243
272
|
return "green"
|
|
244
273
|
|
|
@@ -54,9 +54,17 @@ def render_markdown(model, summary: bool = False) -> str:
|
|
|
54
54
|
if failing:
|
|
55
55
|
out.append(f"- 🔴 **Documents failing audit**: {', '.join(failing)}")
|
|
56
56
|
|
|
57
|
+
for o in model.get("operations", []):
|
|
58
|
+
mark = "🟢" if o["fresh"] else "🟠"
|
|
59
|
+
verb = "next" if o["fresh"] else "OVERDUE since"
|
|
60
|
+
out.append(f"- {mark} **Operations review** (`{o['id']}`): {verb} {o['review_by']}")
|
|
57
61
|
if model["risks"]:
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
open_r = [r for r in model["risks"] if r.get("disposition", "open") == "open"]
|
|
63
|
+
rest = len(model["risks"]) - len(open_r)
|
|
64
|
+
mark = "🟠" if open_r else "🟢"
|
|
65
|
+
out.append(f"- {mark} **Open risks**: {len(open_r)}"
|
|
66
|
+
+ (f" ({rest} dispositioned)" if rest else ""))
|
|
67
|
+
for r in open_r[:5]:
|
|
60
68
|
out.append(f" - `{r['id']}` — {r['probability']}/{r['impact']} · owner: {r['owner']}")
|
|
61
69
|
|
|
62
70
|
lr = model["latest_report"]
|
|
@@ -149,6 +157,12 @@ def render_html(model) -> str:
|
|
|
149
157
|
value, "var(--muted)")
|
|
150
158
|
return f'<td style="color:{color};font-weight:600;">{esc(value)}</td>'
|
|
151
159
|
|
|
160
|
+
def disp(value):
|
|
161
|
+
color = {"open": "var(--amber)", "mitigated": "var(--ok)",
|
|
162
|
+
"accepted": "var(--muted)", "closed": "var(--muted)"}.get(
|
|
163
|
+
value, "var(--amber)")
|
|
164
|
+
return f'<td style="color:{color};font-weight:600;">{esc(value)}</td>'
|
|
165
|
+
|
|
152
166
|
if model["risks"]:
|
|
153
167
|
risk_rows = "".join(
|
|
154
168
|
f'<tr><td><code>{esc(r["id"])}</code></td>'
|
|
@@ -156,15 +170,24 @@ def render_html(model) -> str:
|
|
|
156
170
|
f'<td>{", ".join(f"<code>{esc(b)}</code>" for b in r["threatens"]) or "—"}</td>'
|
|
157
171
|
+ sev(r["probability"]) + sev(r["impact"])
|
|
158
172
|
+ f'<td>{esc(r["owner"])}</td>'
|
|
159
|
-
|
|
173
|
+
+ disp(r.get("disposition", "open"))
|
|
174
|
+
+ f'<td>{esc(r["response"]) or "—"}</td></tr>'
|
|
160
175
|
for r in model["risks"])
|
|
161
176
|
risks = ('<table><thead><tr><th>ID</th><th>Risk</th><th>Threatens</th>'
|
|
162
177
|
'<th>Probability</th><th>Impact</th><th>Owner</th>'
|
|
163
|
-
'<th>Response</th></tr></thead>'
|
|
178
|
+
'<th>Status</th><th>Response</th></tr></thead>'
|
|
164
179
|
f'<tbody>{risk_rows}</tbody></table>')
|
|
165
180
|
else:
|
|
166
181
|
risks = "<p>None open.</p>"
|
|
167
182
|
|
|
183
|
+
ops_html = "".join(
|
|
184
|
+
f'<div class="sig"><div class="sig-h"><span>Operations review · '
|
|
185
|
+
f'<code>{esc(o["id"])}</code></span><span style="color:'
|
|
186
|
+
+ ("var(--ok)" if o["fresh"] else "var(--amber)") + ';font-weight:600;">'
|
|
187
|
+
+ ("next " if o["fresh"] else "overdue since ") + f'{esc(o["review_by"])}'
|
|
188
|
+
'</span></div></div>'
|
|
189
|
+
for o in model.get("operations", []))
|
|
190
|
+
|
|
168
191
|
problems = ""
|
|
169
192
|
if model["broken_references"]:
|
|
170
193
|
problems += ("<section><h2>Broken references</h2><ul>"
|
|
@@ -276,11 +299,11 @@ def render_html(model) -> str:
|
|
|
276
299
|
</header>
|
|
277
300
|
<p class="meta">Derived from {c['total']} documents · {c['kinds']} kinds ·
|
|
278
301
|
{c['approved']} approved{scope} · generated {gen}. Do not edit — regenerated from the documents.</p>
|
|
279
|
-
<section><h2>Traceability coverage</h2>{coverage}</section>
|
|
302
|
+
<section><h2>Traceability coverage</h2>{coverage}{ops_html}</section>
|
|
280
303
|
{document_set}
|
|
281
304
|
{execution_html}
|
|
282
305
|
{problems}
|
|
283
|
-
<section><h2>
|
|
306
|
+
<section><h2>Risks ({len([r for r in model['risks'] if r.get('disposition', 'open') == 'open'])} open of {len(model['risks'])})</h2>{risks}</section>
|
|
284
307
|
<section><h2>Latest status report</h2><p>{report}</p></section>
|
|
285
308
|
<section><h2>Documents ({c['total']})</h2>
|
|
286
309
|
<table><thead><tr><th>Kind</th><th>ID</th><th>Status</th><th>Audit</th></tr></thead>
|
|
@@ -226,6 +226,7 @@ def check_project_id_format(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
|
226
226
|
|
|
227
227
|
|
|
228
228
|
_RISK_FIELDS = ["Probability", "Impact", "Owner", "Response"]
|
|
229
|
+
_DISPOSITIONS = {"open", "mitigated", "accepted", "closed"}
|
|
229
230
|
_ADR_STATES = {"proposed", "accepted", "superseded", "deprecated", "rejected"}
|
|
230
231
|
|
|
231
232
|
|
|
@@ -255,6 +256,49 @@ def check_risk_items_complete(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
|
255
256
|
return True, f"All {total} risk(s) state probability, impact, owner, and response."
|
|
256
257
|
|
|
257
258
|
|
|
259
|
+
def check_risk_disposition_valid(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
260
|
+
"""Where a RISK item yields Status, it names a valid disposition."""
|
|
261
|
+
bad, total = [], 0
|
|
262
|
+
for iid, text in _items_of_prefix(doc, ctx, "RISK"):
|
|
263
|
+
total += 1
|
|
264
|
+
value = _field_value(text, "Status")
|
|
265
|
+
if value is not None and value.lower() not in _DISPOSITIONS:
|
|
266
|
+
bad.append(f"{iid}: {value!r}")
|
|
267
|
+
if bad:
|
|
268
|
+
return False, ("invalid disposition (open|mitigated|accepted|closed): "
|
|
269
|
+
+ "; ".join(bad))
|
|
270
|
+
return True, f"All {total} risk disposition(s) valid (absent means open)."
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def check_svc_items_complete(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
274
|
+
"""Every SVC item states a Level and a Measure."""
|
|
275
|
+
incomplete, total = [], 0
|
|
276
|
+
for iid, text in _items_of_prefix(doc, ctx, "SVC"):
|
|
277
|
+
total += 1
|
|
278
|
+
missing = [f for f in ("Level", "Measure") if _field_value(text, f) is None]
|
|
279
|
+
if missing:
|
|
280
|
+
incomplete.append(f'{iid} missing {", ".join(missing)}')
|
|
281
|
+
if incomplete:
|
|
282
|
+
return False, "; ".join(incomplete)
|
|
283
|
+
return True, f"All {total} service(s) state a level and a measure."
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def check_ops_review_fresh(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
287
|
+
"""review_by is today or later (advisory; staleness ambers derived status)."""
|
|
288
|
+
import datetime
|
|
289
|
+
raw = doc.frontmatter.get("review_by")
|
|
290
|
+
if raw is None:
|
|
291
|
+
return False, "No review_by date."
|
|
292
|
+
try:
|
|
293
|
+
due = raw if isinstance(raw, datetime.date) else datetime.date.fromisoformat(str(raw))
|
|
294
|
+
except ValueError:
|
|
295
|
+
return False, f"review_by is not a date: {raw!r}"
|
|
296
|
+
today = datetime.date.today()
|
|
297
|
+
if due < today:
|
|
298
|
+
return False, f"operations review overdue since {due.isoformat()}"
|
|
299
|
+
return True, f"Next operations review {due.isoformat()}."
|
|
300
|
+
|
|
301
|
+
|
|
258
302
|
def check_adr_items_have_status(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
259
303
|
"""Every ADR item declares a valid Status."""
|
|
260
304
|
bad, total = [], 0
|
|
@@ -402,6 +446,9 @@ CHECKS: dict[str, Callable[[Document, dict], tuple[bool, str]]] = {
|
|
|
402
446
|
"unique-id": check_unique_id,
|
|
403
447
|
"items-well-formed": check_items_well_formed,
|
|
404
448
|
"risk-items-complete": check_risk_items_complete,
|
|
449
|
+
"risk-disposition-valid": check_risk_disposition_valid,
|
|
450
|
+
"svc-items-complete": check_svc_items_complete,
|
|
451
|
+
"ops-review-fresh": check_ops_review_fresh,
|
|
405
452
|
"adr-items-have-status": check_adr_items_have_status,
|
|
406
453
|
"raci-one-accountable": check_raci_one_accountable,
|
|
407
454
|
"story-format": check_story_format,
|
|
@@ -33,6 +33,7 @@ docassert/_data/criteria/charter.criteria.yaml
|
|
|
33
33
|
docassert/_data/criteria/data-migration-plan.criteria.yaml
|
|
34
34
|
docassert/_data/criteria/frnfr.criteria.yaml
|
|
35
35
|
docassert/_data/criteria/hypercare-plan.criteria.yaml
|
|
36
|
+
docassert/_data/criteria/operations.criteria.yaml
|
|
36
37
|
docassert/_data/criteria/post-implementation-review.criteria.yaml
|
|
37
38
|
docassert/_data/criteria/prd.criteria.yaml
|
|
38
39
|
docassert/_data/criteria/project.criteria.yaml
|
|
@@ -47,6 +48,7 @@ docassert/_data/criteria/test-cases.criteria.yaml
|
|
|
47
48
|
docassert/_data/criteria/user-story.criteria.yaml
|
|
48
49
|
docassert/_data/profiles/agile-delivery.yaml
|
|
49
50
|
docassert/_data/profiles/lean-startup.yaml
|
|
51
|
+
docassert/_data/profiles/operations.yaml
|
|
50
52
|
docassert/_data/profiles/regulated-industry.yaml
|
|
51
53
|
docassert/_data/schema/adr.schema.json
|
|
52
54
|
docassert/_data/schema/benefits-realization.schema.json
|
|
@@ -56,6 +58,7 @@ docassert/_data/schema/charter.schema.json
|
|
|
56
58
|
docassert/_data/schema/data-migration-plan.schema.json
|
|
57
59
|
docassert/_data/schema/frnfr.schema.json
|
|
58
60
|
docassert/_data/schema/hypercare-plan.schema.json
|
|
61
|
+
docassert/_data/schema/operations.schema.json
|
|
59
62
|
docassert/_data/schema/post-implementation-review.schema.json
|
|
60
63
|
docassert/_data/schema/prd.schema.json
|
|
61
64
|
docassert/_data/schema/project.schema.json
|
|
@@ -77,6 +80,7 @@ docassert/_data/templates/charter.template.md
|
|
|
77
80
|
docassert/_data/templates/data-migration-plan.template.md
|
|
78
81
|
docassert/_data/templates/frnfr.template.md
|
|
79
82
|
docassert/_data/templates/hypercare-plan.template.md
|
|
83
|
+
docassert/_data/templates/operations.template.md
|
|
80
84
|
docassert/_data/templates/post-implementation-review.template.md
|
|
81
85
|
docassert/_data/templates/prd.template.md
|
|
82
86
|
docassert/_data/templates/project.template.md
|
|
@@ -60,8 +60,9 @@ def test_load_profile_and_unknown():
|
|
|
60
60
|
assert P.load_profile("does-not-exist", PROFILES) is None
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
64
|
-
assert set(P.available(PROFILES)) == {"regulated-industry", "lean-startup",
|
|
63
|
+
def test_available_lists_the_shipped_profiles():
|
|
64
|
+
assert set(P.available(PROFILES)) == {"regulated-industry", "lean-startup",
|
|
65
|
+
"agile-delivery", "operations"}
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
# ── integration with the real repo ──────────────────────────────────────────
|
|
@@ -162,3 +162,114 @@ def test_render_html_risk_table():
|
|
|
162
162
|
# severity cells are colored
|
|
163
163
|
assert 'style="color:var(--bad);font-weight:600;">high</td>' in out \
|
|
164
164
|
or 'style="color:var(--amber);font-weight:600;">medium</td>' in out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── risk disposition (ENG-PR-002) ────────────────────────────────────────────
|
|
168
|
+
def _risk_graph(*status_clauses):
|
|
169
|
+
from docassert.graph import Graph
|
|
170
|
+
from docassert.models import Item
|
|
171
|
+
g = Graph()
|
|
172
|
+
for i, clause in enumerate(status_clauses, 1):
|
|
173
|
+
text = (f"Risk {i} description. Probability: low. Impact: high. "
|
|
174
|
+
f"Owner: o.{i}.{clause} Response: watch.")
|
|
175
|
+
g.add(Item(f"XX-RISK-{i:03d}", "XX", "RISK", text, {}, "d.md",
|
|
176
|
+
"risk-register", "approved", "S"))
|
|
177
|
+
return g
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_disposition_parsed_and_defaulted():
|
|
181
|
+
from docassert.status.derive import _risks
|
|
182
|
+
g = _risk_graph(" Status: Mitigated.", " Status: accepted.", "")
|
|
183
|
+
rs = _risks(g)
|
|
184
|
+
assert [r["disposition"] for r in rs] == ["open", "mitigated", "accepted"]
|
|
185
|
+
assert rs[0]["id"] == "XX-RISK-003" # open risks sort first
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_rag_green_when_all_risks_dispositioned():
|
|
189
|
+
base = {"documents": [], "coverage": [], "broken_references": [],
|
|
190
|
+
"latest_report": None, "completeness": None}
|
|
191
|
+
open_r = dict(base, risks=[{"id": "R", "score": 2, "disposition": "open"}])
|
|
192
|
+
done_r = dict(base, risks=[{"id": "R", "score": 2, "disposition": "accepted"}])
|
|
193
|
+
assert S.derive_rag(open_r) == "amber"
|
|
194
|
+
assert S.derive_rag(done_r) == "green"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_disposition_check_blocks_invalid(tmp_path):
|
|
198
|
+
from docassert.loader import load
|
|
199
|
+
from docassert.structural import check_risk_disposition_valid
|
|
200
|
+
p = tmp_path / "r.md"
|
|
201
|
+
p.write_text(
|
|
202
|
+
"---\nkind: risk-register\nid: XX-risk-register\n"
|
|
203
|
+
"project: PRJ-001-XX\ntitle: T\nowner: o\nstatus: approved\n---\n\n"
|
|
204
|
+
"## Overview\nx\n\n## Risks\n\n"
|
|
205
|
+
"- **XX-RISK-001**: Bad. Probability: low. Impact: low. Owner: o. "
|
|
206
|
+
"Status: wontfix. Response: r.\n", encoding="utf-8")
|
|
207
|
+
doc = load(p)
|
|
208
|
+
ok, detail = check_risk_disposition_valid(
|
|
209
|
+
doc, {"item_sections": [{"section": "Risks", "prefix": "RISK"}]})
|
|
210
|
+
assert not ok and "wontfix" in detail
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_render_html_shows_disposition_column():
|
|
214
|
+
_cd_root()
|
|
215
|
+
out = S.render_html(S.build_status(ROOT / "documents"))
|
|
216
|
+
assert "<th>Status</th>" in out and "open of" in out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── operations kind (ENG-PR-003) ─────────────────────────────────────────────
|
|
220
|
+
OPS_DOC = """---
|
|
221
|
+
kind: operations
|
|
222
|
+
id: XX-operations
|
|
223
|
+
project: PRJ-001-XX
|
|
224
|
+
title: Ops fixture
|
|
225
|
+
owner: ops.owner
|
|
226
|
+
status: approved
|
|
227
|
+
review_by: {due}
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Overview
|
|
231
|
+
x
|
|
232
|
+
|
|
233
|
+
## Services
|
|
234
|
+
|
|
235
|
+
- **XX-SVC-001**: Triage. Level: within 14 days. Measure: monthly report.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _ops_tree(tmp_path, due):
|
|
240
|
+
d = tmp_path / "documents" / "PRJ-001-XX"
|
|
241
|
+
d.mkdir(parents=True)
|
|
242
|
+
(d / "project.md").write_text(
|
|
243
|
+
"---\nkind: project\nid: PRJ-001-XX\ncode: XX\nname: Xy Ops\n"
|
|
244
|
+
"sponsor: sp.owner\nstatus: active\n---\n\n## Overview\nx\n\n## Scope\nx\n",
|
|
245
|
+
encoding="utf-8")
|
|
246
|
+
(d / "operations.md").write_text(OPS_DOC.format(due=due), encoding="utf-8")
|
|
247
|
+
return tmp_path / "documents"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_stale_operations_review_ambers_status(tmp_path):
|
|
251
|
+
_cd_root()
|
|
252
|
+
m = S.build_status(_ops_tree(tmp_path, "2020-01-01"), project="PRJ-001-XX")
|
|
253
|
+
assert m["operations"][0]["fresh"] is False
|
|
254
|
+
assert m["rag"] == "amber"
|
|
255
|
+
assert "OVERDUE" in S.render_markdown(m)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_fresh_operations_review_stays_green(tmp_path):
|
|
259
|
+
_cd_root()
|
|
260
|
+
m = S.build_status(_ops_tree(tmp_path, "2099-12-31"), project="PRJ-001-XX")
|
|
261
|
+
assert m["operations"][0]["fresh"] is True
|
|
262
|
+
assert m["rag"] == "green"
|
|
263
|
+
assert "Operations review" in S.render_html(m)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_svc_and_freshness_checks(tmp_path):
|
|
267
|
+
from docassert.loader import load
|
|
268
|
+
from docassert.structural import check_ops_review_fresh, check_svc_items_complete
|
|
269
|
+
docs = _ops_tree(tmp_path, "2020-01-01")
|
|
270
|
+
doc = load(docs / "PRJ-001-XX" / "operations.md")
|
|
271
|
+
ctx = {"item_sections": [{"section": "Services", "prefix": "SVC"}]}
|
|
272
|
+
ok, _ = check_svc_items_complete(doc, ctx)
|
|
273
|
+
assert ok
|
|
274
|
+
fresh_ok, detail = check_ops_review_fresh(doc, ctx)
|
|
275
|
+
assert not fresh_ok and "overdue" in detail
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml
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
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/benefits-realization.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/data-migration-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/post-implementation-review.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/schema/release-cutover-plan.schema.json
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
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.14.1 → docassert-0.16.0}/docassert/_data/templates/release-cutover-plan.template.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
|
|
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
|
|
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
|