sdr-grader 1.1.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 (146) hide show
  1. sdr_grader-1.1.1/.claude-plugin/plugin.json +13 -0
  2. sdr_grader-1.1.1/.github/ISSUE_TEMPLATE/bug_report.yml +73 -0
  3. sdr_grader-1.1.1/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. sdr_grader-1.1.1/.github/ISSUE_TEMPLATE/feature_request.yml +65 -0
  5. sdr_grader-1.1.1/.github/PULL_REQUEST_TEMPLATE.md +30 -0
  6. sdr_grader-1.1.1/.github/workflows/release.yml +79 -0
  7. sdr_grader-1.1.1/.github/workflows/test.yml +82 -0
  8. sdr_grader-1.1.1/.gitignore +70 -0
  9. sdr_grader-1.1.1/CHANGELOG.md +237 -0
  10. sdr_grader-1.1.1/CLAUDE.md +39 -0
  11. sdr_grader-1.1.1/CONTRIBUTING.md +92 -0
  12. sdr_grader-1.1.1/LICENSE +21 -0
  13. sdr_grader-1.1.1/PKG-INFO +253 -0
  14. sdr_grader-1.1.1/README.md +227 -0
  15. sdr_grader-1.1.1/SECURITY.md +51 -0
  16. sdr_grader-1.1.1/docs/ADAPTER_GUIDE.md +124 -0
  17. sdr_grader-1.1.1/docs/CALIBRATION_CORPUS.md +145 -0
  18. sdr_grader-1.1.1/docs/CHECK_FUNCTION_GUIDE.md +117 -0
  19. sdr_grader-1.1.1/docs/CI_INTEGRATION.md +104 -0
  20. sdr_grader-1.1.1/docs/CUSTOMIZATION.md +126 -0
  21. sdr_grader-1.1.1/docs/LEADERBOARDS.md +58 -0
  22. sdr_grader-1.1.1/docs/PLATFORM_COVERAGE.md +47 -0
  23. sdr_grader-1.1.1/docs/PROJECT_CONFIG.md +82 -0
  24. sdr_grader-1.1.1/docs/RUBRIC_AUDIT.md +315 -0
  25. sdr_grader-1.1.1/docs/RUBRIC_FORMAT.md +128 -0
  26. sdr_grader-1.1.1/docs/SUPPLEMENTARY_INPUTS.md +84 -0
  27. sdr_grader-1.1.1/docs/TREND_REPORTS.md +57 -0
  28. sdr_grader-1.1.1/docs/calibration_manifest.example.yaml +33 -0
  29. sdr_grader-1.1.1/docs/threshold_calibration.md +266 -0
  30. sdr_grader-1.1.1/examples/grade-aa-clean.html +542 -0
  31. sdr_grader-1.1.1/examples/grade-aa-messy.html +793 -0
  32. sdr_grader-1.1.1/examples/grade-cja-clean.html +542 -0
  33. sdr_grader-1.1.1/examples/grade-cja-messy.html +988 -0
  34. sdr_grader-1.1.1/examples/sample-report.html +882 -0
  35. sdr_grader-1.1.1/examples/templated-report.html +883 -0
  36. sdr_grader-1.1.1/examples/trend-example.html +618 -0
  37. sdr_grader-1.1.1/pyproject.toml +131 -0
  38. sdr_grader-1.1.1/scripts/aggregate_distributions.py +129 -0
  39. sdr_grader-1.1.1/scripts/build_aa_fixtures.py +322 -0
  40. sdr_grader-1.1.1/scripts/build_cja_fixtures.py +614 -0
  41. sdr_grader-1.1.1/scripts/calibrate_thresholds.py +578 -0
  42. sdr_grader-1.1.1/scripts/file_remaining_design_issues.sh +74 -0
  43. sdr_grader-1.1.1/scripts/generate_examples.py +32 -0
  44. sdr_grader-1.1.1/scripts/generate_grade_examples.py +62 -0
  45. sdr_grader-1.1.1/scripts/generate_trend_example.py +55 -0
  46. sdr_grader-1.1.1/scripts/sanitize_sdr.py +169 -0
  47. sdr_grader-1.1.1/skills/sdr-grader/README.md +47 -0
  48. sdr_grader-1.1.1/skills/sdr-grader/SKILL.md +83 -0
  49. sdr_grader-1.1.1/skills/sdr-grader/scripts/query_grade.py +237 -0
  50. sdr_grader-1.1.1/src/sdr_grader/__init__.py +3 -0
  51. sdr_grader-1.1.1/src/sdr_grader/__main__.py +6 -0
  52. sdr_grader-1.1.1/src/sdr_grader/adapters/__init__.py +0 -0
  53. sdr_grader-1.1.1/src/sdr_grader/adapters/aa.py +417 -0
  54. sdr_grader-1.1.1/src/sdr_grader/adapters/cja.py +569 -0
  55. sdr_grader-1.1.1/src/sdr_grader/cli/__init__.py +0 -0
  56. sdr_grader-1.1.1/src/sdr_grader/cli/exit_codes.py +6 -0
  57. sdr_grader-1.1.1/src/sdr_grader/cli/main.py +478 -0
  58. sdr_grader-1.1.1/src/sdr_grader/core/__init__.py +0 -0
  59. sdr_grader-1.1.1/src/sdr_grader/core/exceptions.py +17 -0
  60. sdr_grader-1.1.1/src/sdr_grader/core/grade_calc.py +102 -0
  61. sdr_grader-1.1.1/src/sdr_grader/core/grader.py +245 -0
  62. sdr_grader-1.1.1/src/sdr_grader/core/models.py +100 -0
  63. sdr_grader-1.1.1/src/sdr_grader/core/timeparse.py +31 -0
  64. sdr_grader-1.1.1/src/sdr_grader/data/distribution.json +18 -0
  65. sdr_grader-1.1.1/src/sdr_grader/input/__init__.py +0 -0
  66. sdr_grader-1.1.1/src/sdr_grader/input/detect.py +39 -0
  67. sdr_grader-1.1.1/src/sdr_grader/input/loader.py +142 -0
  68. sdr_grader-1.1.1/src/sdr_grader/input/shell_out.py +104 -0
  69. sdr_grader-1.1.1/src/sdr_grader/render/__init__.py +46 -0
  70. sdr_grader-1.1.1/src/sdr_grader/render/distribution.py +96 -0
  71. sdr_grader-1.1.1/src/sdr_grader/render/json_output.py +33 -0
  72. sdr_grader-1.1.1/src/sdr_grader/render/renderer.py +207 -0
  73. sdr_grader-1.1.1/src/sdr_grader/render/static/report.css +355 -0
  74. sdr_grader-1.1.1/src/sdr_grader/render/svg.py +76 -0
  75. sdr_grader-1.1.1/src/sdr_grader/render/templates/report.html.j2 +191 -0
  76. sdr_grader-1.1.1/src/sdr_grader/render/truncation.py +54 -0
  77. sdr_grader-1.1.1/src/sdr_grader/rules/__init__.py +0 -0
  78. sdr_grader-1.1.1/src/sdr_grader/rules/checks/__init__.py +0 -0
  79. sdr_grader-1.1.1/src/sdr_grader/rules/checks/_helpers.py +165 -0
  80. sdr_grader-1.1.1/src/sdr_grader/rules/checks/attribution.py +237 -0
  81. sdr_grader-1.1.1/src/sdr_grader/rules/checks/calc_metrics.py +305 -0
  82. sdr_grader-1.1.1/src/sdr_grader/rules/checks/governance.py +439 -0
  83. sdr_grader-1.1.1/src/sdr_grader/rules/checks/naming.py +356 -0
  84. sdr_grader-1.1.1/src/sdr_grader/rules/checks/schema_hygiene.py +677 -0
  85. sdr_grader-1.1.1/src/sdr_grader/rules/checks/segments.py +260 -0
  86. sdr_grader-1.1.1/src/sdr_grader/rules/checks/supplementary.py +84 -0
  87. sdr_grader-1.1.1/src/sdr_grader/rules/engine.py +62 -0
  88. sdr_grader-1.1.1/src/sdr_grader/rules/packs/__init__.py +0 -0
  89. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/_meta.yaml +35 -0
  90. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/attribution.yaml +26 -0
  91. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/calc_metrics.yaml +55 -0
  92. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/governance.yaml +66 -0
  93. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/naming.yaml +53 -0
  94. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/schema_hygiene.yaml +83 -0
  95. sdr_grader-1.1.1/src/sdr_grader/rules/packs/pragmatic/segments.yaml +54 -0
  96. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/_meta.yaml +38 -0
  97. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/attribution.yaml +52 -0
  98. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/calc_metrics.yaml +86 -0
  99. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/governance.yaml +117 -0
  100. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/naming.yaml +90 -0
  101. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/schema_hygiene.yaml +139 -0
  102. sdr_grader-1.1.1/src/sdr_grader/rules/packs/strict/segments.yaml +89 -0
  103. sdr_grader-1.1.1/src/sdr_grader/rules/registry.py +74 -0
  104. sdr_grader-1.1.1/src/sdr_grader/rules/rubric.py +329 -0
  105. sdr_grader-1.1.1/src/sdr_grader/rules/suppression.py +204 -0
  106. sdr_grader-1.1.1/src/sdr_grader/trend/__init__.py +19 -0
  107. sdr_grader-1.1.1/src/sdr_grader/trend/renderer.py +286 -0
  108. sdr_grader-1.1.1/src/sdr_grader/trend/runner.py +138 -0
  109. sdr_grader-1.1.1/src/sdr_grader/trend/templates/trend.html.j2 +119 -0
  110. sdr_grader-1.1.1/tests/_rule_test_helpers.py +107 -0
  111. sdr_grader-1.1.1/tests/conftest.py +1 -0
  112. sdr_grader-1.1.1/tests/fixtures/__init__.py +0 -0
  113. sdr_grader-1.1.1/tests/fixtures/aa_snapshot_clean.json +694 -0
  114. sdr_grader-1.1.1/tests/fixtures/aa_snapshot_messy.json +1236 -0
  115. sdr_grader-1.1.1/tests/fixtures/cja_snapshot_clean.json +1290 -0
  116. sdr_grader-1.1.1/tests/fixtures/cja_snapshot_messy.json +10434 -0
  117. sdr_grader-1.1.1/tests/fixtures/demo_report.py +318 -0
  118. sdr_grader-1.1.1/tests/test_adapter_fuzz.py +170 -0
  119. sdr_grader-1.1.1/tests/test_adapters_aa.py +225 -0
  120. sdr_grader-1.1.1/tests/test_adapters_cja.py +351 -0
  121. sdr_grader-1.1.1/tests/test_calibration_attr_measurements.py +170 -0
  122. sdr_grader-1.1.1/tests/test_cli.py +302 -0
  123. sdr_grader-1.1.1/tests/test_cross_platform_parity.py +173 -0
  124. sdr_grader-1.1.1/tests/test_cycle_groups.py +38 -0
  125. sdr_grader-1.1.1/tests/test_detect_and_registry.py +98 -0
  126. sdr_grader-1.1.1/tests/test_distribution.py +172 -0
  127. sdr_grader-1.1.1/tests/test_grade_calc.py +174 -0
  128. sdr_grader-1.1.1/tests/test_input_modes.py +322 -0
  129. sdr_grader-1.1.1/tests/test_render_cap.py +87 -0
  130. sdr_grader-1.1.1/tests/test_renderer.py +107 -0
  131. sdr_grader-1.1.1/tests/test_rubric_validation.py +63 -0
  132. sdr_grader-1.1.1/tests/test_rules_attribution.py +180 -0
  133. sdr_grader-1.1.1/tests/test_rules_calc_metrics.py +141 -0
  134. sdr_grader-1.1.1/tests/test_rules_governance.py +338 -0
  135. sdr_grader-1.1.1/tests/test_rules_naming.py +235 -0
  136. sdr_grader-1.1.1/tests/test_rules_schema_hygiene.py +148 -0
  137. sdr_grader-1.1.1/tests/test_rules_schema_hygiene_extras.py +543 -0
  138. sdr_grader-1.1.1/tests/test_rules_segments.py +127 -0
  139. sdr_grader-1.1.1/tests/test_rules_supplementary.py +98 -0
  140. sdr_grader-1.1.1/tests/test_skill_helper.py +119 -0
  141. sdr_grader-1.1.1/tests/test_smoke.py +5 -0
  142. sdr_grader-1.1.1/tests/test_suppression.py +178 -0
  143. sdr_grader-1.1.1/tests/test_svg_charts.py +32 -0
  144. sdr_grader-1.1.1/tests/test_timeparse.py +47 -0
  145. sdr_grader-1.1.1/tests/test_trend.py +178 -0
  146. sdr_grader-1.1.1/uv.lock +602 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://claude.ai/schemas/plugin.json",
3
+ "name": "sdr-grader",
4
+ "version": "1.0.0",
5
+ "description": "Follow-up question support for sdr-grader's --json grade output. Filter findings, look up remediations, and compare two grades from inside Claude Code.",
6
+ "author": {
7
+ "name": "Brian Au",
8
+ "url": "https://github.com/brian-a-au/sdr-grader"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/brian-a-au/sdr-grader",
12
+ "skills": ["skills/sdr-grader"]
13
+ }
@@ -0,0 +1,73 @@
1
+ name: Bug report
2
+ description: A rule grades incorrectly, the CLI crashes, or output is wrong.
3
+ title: "[bug] "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for taking the time to file a bug. A reproducible snapshot
10
+ is the single most useful thing you can attach.
11
+
12
+ **Do not paste a real production snapshot.** Use
13
+ `scripts/sanitize_sdr.py` to scrub tenant identifiers, then trim
14
+ to the smallest shape that still reproduces the bug. If the
15
+ snapshot is sensitive even after scrubbing, open a
16
+ [private security advisory](https://github.com/brian-a-au/sdr-grader/security/advisories/new)
17
+ instead.
18
+
19
+ - type: textarea
20
+ id: what-happened
21
+ attributes:
22
+ label: What happened
23
+ description: What did the grader do? What did you expect it to do?
24
+ placeholder: |
25
+ Ran `sdr-grader snapshot.json --output grade.html`. SCH-003 fired
26
+ with "170 components lack descriptions" but the snapshot has
27
+ descriptions on every component.
28
+ validations:
29
+ required: true
30
+
31
+ - type: textarea
32
+ id: reproduction
33
+ attributes:
34
+ label: Reproduction
35
+ description: |
36
+ Minimal command + minimal snapshot. Inline the snapshot in a code
37
+ block if it's small, or attach it.
38
+ render: shell
39
+ validations:
40
+ required: true
41
+
42
+ - type: input
43
+ id: version
44
+ attributes:
45
+ label: sdr-grader version
46
+ description: Output of `sdr-grader --version` (or commit SHA if running from source).
47
+ placeholder: "1.0.0"
48
+ validations:
49
+ required: true
50
+
51
+ - type: dropdown
52
+ id: platform
53
+ attributes:
54
+ label: Platform
55
+ options:
56
+ - CJA (cja_auto_sdr snapshot)
57
+ - AA (aa_auto_sdr snapshot)
58
+ - Both / not platform-specific
59
+ validations:
60
+ required: true
61
+
62
+ - type: input
63
+ id: pack
64
+ attributes:
65
+ label: Rubric pack
66
+ description: Which pack was active? Default is `strict`.
67
+ placeholder: "strict"
68
+
69
+ - type: textarea
70
+ id: extra
71
+ attributes:
72
+ label: Anything else
73
+ description: Logs, screenshots of the rendered report, related issues.
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Security vulnerability
4
+ url: https://github.com/brian-a-au/sdr-grader/security/advisories/new
5
+ about: Please report security issues privately via a GitHub Security Advisory, not a public issue.
6
+ - name: False positive on a private snapshot
7
+ url: https://github.com/brian-a-au/sdr-grader/security/advisories/new
8
+ about: If a rule fires incorrectly on a snapshot you cannot share publicly, open a private advisory and we'll work out a sanitized reduction together. See SECURITY.md.
@@ -0,0 +1,65 @@
1
+ name: Feature or rule proposal
2
+ description: Propose a new rule, new check kind, or other enhancement.
3
+ title: "[feature] "
4
+ labels: ["enhancement"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Open an issue **before** opening a PR for new rules or structural
10
+ changes — it saves us both time vs. arriving at the same
11
+ conclusion through review feedback. See `CONTRIBUTING.md` for
12
+ what's PR-able directly vs. maintainer-gated.
13
+
14
+ - type: textarea
15
+ id: proposal
16
+ attributes:
17
+ label: What are you proposing
18
+ description: One paragraph. New rule? New check kind? New CLI flag? Something else?
19
+ validations:
20
+ required: true
21
+
22
+ - type: textarea
23
+ id: motivation
24
+ attributes:
25
+ label: Motivation
26
+ description: |
27
+ What broken-implementation pattern does this catch? Why does the
28
+ current rule set miss it?
29
+ validations:
30
+ required: true
31
+
32
+ - type: dropdown
33
+ id: shape
34
+ attributes:
35
+ label: If this is a new rule, what does it measure?
36
+ description: |
37
+ See `CONTRIBUTING.md` invariants. Cardinality rules ("you have
38
+ more than N segments") are rejected by default — the same number
39
+ is healthy for one tenant and pathological for another.
40
+ options:
41
+ - Shape (structure of a definition, e.g. nesting depth)
42
+ - Ratio (e.g. % of components missing descriptions)
43
+ - Correctness (e.g. formula references a deleted component)
44
+ - Cardinality (raw count — likely to be rejected)
45
+ - Not applicable (not a new rule)
46
+ validations:
47
+ required: true
48
+
49
+ - type: textarea
50
+ id: example
51
+ attributes:
52
+ label: Example
53
+ description: |
54
+ Synthetic fixture or sketch of what the firing condition looks
55
+ like. If this needs calibration data, say so — provisional
56
+ thresholds are fine in an initial PR.
57
+ render: shell
58
+
59
+ - type: textarea
60
+ id: alternatives
61
+ attributes:
62
+ label: Alternatives considered
63
+ description: |
64
+ Existing rules that almost cover this? Other ways to surface the
65
+ same signal (e.g. supplementary input vs. core rule)?
@@ -0,0 +1,30 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR do, in 1-3 sentences? Link the issue it closes. -->
4
+
5
+ ## Type of change
6
+
7
+ - [ ] Bug fix (incorrect grading, adapter crash, renderer regression)
8
+ - [ ] New rule (also opened an issue first per CONTRIBUTING.md)
9
+ - [ ] Threshold / severity tweak (requires maintainer recalibration — explained in the issue)
10
+ - [ ] Docs / examples / CI
11
+ - [ ] Other (describe)
12
+
13
+ ## Invariant check
14
+
15
+ These are non-negotiable per CONTRIBUTING.md. Confirm:
16
+
17
+ - [ ] No randomness or `datetime.now()` in graded output (determinism contract)
18
+ - [ ] No new cardinality rules (`len(X) > k` shapes are rejected)
19
+ - [ ] `render/` does not import from `rules/` or `core/grader.py`
20
+
21
+ ## Tests + examples
22
+
23
+ - [ ] `uv run pytest` passes locally
24
+ - [ ] `uv run ruff check` passes
25
+ - [ ] If rules or renderer changed, examples regenerated via `scripts/generate_*.py` and the diff is committed (the `examples-drift` CI gate will fail otherwise)
26
+
27
+ ## Notes for the reviewer
28
+
29
+ <!-- Anything non-obvious: a tricky calibration choice, a subtle invariant
30
+ relied on, a fixture that needed updating, a follow-up issue you opened. -->
@@ -0,0 +1,79 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ # Minimal default; each job opts into exactly the scopes it needs.
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: write # create the GitHub release and attach the dist assets
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v3
22
+
23
+ - name: Set up Python
24
+ run: uv python install 3.12
25
+
26
+ - name: Sync dependencies
27
+ run: uv sync --all-extras --dev
28
+
29
+ - name: Lint
30
+ run: uv run ruff check
31
+
32
+ - name: Test
33
+ run: uv run pytest --cov=src/sdr_grader --cov-report=term
34
+
35
+ - name: Build distributions
36
+ run: uv build
37
+
38
+ - name: Drop uv's stray dist/.gitignore
39
+ # `uv build` writes a `.gitignore` (containing `*`) into dist/. Left
40
+ # in place it rides along into the uploaded artifact and shows up as
41
+ # a `default.gitignore` release asset.
42
+ run: rm -f dist/.gitignore
43
+
44
+ - name: Upload built distributions as a workflow artifact
45
+ uses: actions/upload-artifact@v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+ if-no-files-found: error
50
+
51
+ - name: Upload distributions to GitHub release
52
+ uses: softprops/action-gh-release@v2
53
+ with:
54
+ files: |
55
+ dist/*.whl
56
+ dist/*.tar.gz
57
+ generate_release_notes: true
58
+
59
+ publish:
60
+ needs: build
61
+ runs-on: ubuntu-latest
62
+ # The `pypi` environment gates this job. Configure it on GitHub
63
+ # (Settings -> Environments -> pypi) with a required reviewer so the
64
+ # irreversible PyPI upload waits for a manual approval, and keep the
65
+ # matching "Environment name: pypi" on the PyPI trusted publisher.
66
+ environment:
67
+ name: pypi
68
+ url: https://pypi.org/p/sdr-grader
69
+ permissions:
70
+ id-token: write # OIDC token for PyPI trusted publishing; nothing else
71
+ steps:
72
+ - name: Download built distributions
73
+ uses: actions/download-artifact@v4
74
+ with:
75
+ name: dist
76
+ path: dist/
77
+
78
+ - name: Publish to PyPI
79
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,82 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v3
19
+ with:
20
+ enable-cache: true
21
+ # The default prune strips pre-built wheels from the saved cache,
22
+ # so every run re-downloaded them from PyPI despite a "cache hit".
23
+ prune-cache: false
24
+
25
+ - name: Set up Python ${{ matrix.python-version }}
26
+ run: uv python install ${{ matrix.python-version }}
27
+
28
+ - name: Sync dependencies
29
+ run: uv sync --all-extras --dev
30
+
31
+ - name: Lint
32
+ run: uv run ruff check
33
+
34
+ - name: Test
35
+ run: uv run pytest --cov=src/sdr_grader --cov-report=term --cov-report=xml
36
+
37
+ - name: Upload coverage XML
38
+ if: matrix.python-version == '3.12'
39
+ uses: actions/upload-artifact@v4
40
+ with:
41
+ name: coverage-xml
42
+ path: coverage.xml
43
+ if-no-files-found: warn
44
+
45
+ examples-drift:
46
+ # Regenerate every example artifact and fail if anything changed.
47
+ # Catches commits that change rules / fixtures / renderer without
48
+ # refreshing the committed examples/ directory.
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - name: Install uv
54
+ uses: astral-sh/setup-uv@v3
55
+ with:
56
+ enable-cache: true
57
+ # The default prune strips pre-built wheels from the saved cache,
58
+ # so every run re-downloaded them from PyPI despite a "cache hit".
59
+ prune-cache: false
60
+
61
+ - name: Set up Python
62
+ run: uv python install 3.12
63
+
64
+ - name: Sync dependencies
65
+ run: uv sync --all-extras --dev
66
+
67
+ - name: Regenerate fixtures
68
+ run: uv run python scripts/build_cja_fixtures.py
69
+
70
+ - name: Regenerate examples
71
+ run: |
72
+ uv run python scripts/generate_examples.py
73
+ uv run python scripts/generate_grade_examples.py
74
+ uv run python scripts/generate_trend_example.py
75
+
76
+ - name: Fail on diff
77
+ run: |
78
+ if ! git diff --exit-code -- examples/ tests/fixtures/; then
79
+ echo "::error::Generated examples or fixtures drifted from committed copies."
80
+ echo "Run the relevant generator scripts locally and commit the result."
81
+ exit 1
82
+ fi
@@ -0,0 +1,70 @@
1
+ # Local reference artifacts (kept in working directory, not tracked).
2
+ # These are the inputs the SPEC describes — the visual contract reference and
3
+ # the spec itself. Phase 1 will copy them into src/sdr_grader/render/ paths.
4
+ /render.py
5
+ /report.css
6
+ /report.html.j2
7
+ /sample-report.html
8
+ /templated-report.html
9
+ /SPEC.md
10
+
11
+ # macOS
12
+ .DS_Store
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.py[cod]
17
+ *$py.class
18
+ *.so
19
+ .Python
20
+ build/
21
+ dist/
22
+ *.egg-info/
23
+ .eggs/
24
+ *.egg
25
+ MANIFEST
26
+
27
+ # uv / virtualenvs
28
+ .venv/
29
+ venv/
30
+ env/
31
+
32
+ # Testing
33
+ .pytest_cache/
34
+ .hypothesis/
35
+ .coverage
36
+ .coverage.*
37
+ htmlcov/
38
+ coverage.xml
39
+ .tox/
40
+
41
+ # Linters / type checkers
42
+ .ruff_cache/
43
+ .mypy_cache/
44
+ .pyre/
45
+
46
+ # IDE
47
+ .idea/
48
+ .vscode/
49
+ *.swp
50
+ *.swo
51
+
52
+ # Local-only generated artifacts
53
+ /grade-*.html
54
+ /grade-*.json
55
+
56
+ # Environment
57
+ .env
58
+ .env.local
59
+
60
+ # Claude Code "superpowers" plugin artifacts and plans — local tooling only,
61
+ # never committed. Matches any `superpowers/` directory at any depth
62
+ # (e.g. `/superpowers/`, `docs/superpowers/plans/`), including the
63
+ # dot-prefixed `.superpowers/` working directory the plugin writes to.
64
+ **/superpowers/
65
+ .superpowers/
66
+
67
+ # Private production SDR snapshots used for rubric calibration.
68
+ # Real customer data; never committed. See the calibration plan in
69
+ # `docs/superpowers/plans/` for the loader contract.
70
+ /tests/fixtures/private/
@@ -0,0 +1,237 @@
1
+ # Changelog
2
+
3
+ All notable changes follow the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
4
+ spirit. The version numbers follow [Semantic Versioning](https://semver.org/).
5
+
6
+ ## 1.1.1 — 2026-07-13
7
+
8
+ A packaging-only release. No rules, checks, or grades change, so grades
9
+ are identical to 1.1.0.
10
+
11
+ ### Fixed
12
+
13
+ - **Local working artifacts no longer ship in the source distribution.**
14
+ The `.superpowers/` and `.hypothesis/` directories were bundled into the
15
+ sdist because `.gitignore` did not cover them, and hatchling reads
16
+ `.gitignore` to decide what the sdist contains. They are now ignored,
17
+ and `pyproject.toml` excludes them from the sdist target as a second
18
+ guard. The sdist is uploaded to PyPI and is world-readable regardless of
19
+ the git repository's visibility, so these never belonged in it. The
20
+ wheel was already clean and is unchanged.
21
+
22
+ ### Changed
23
+
24
+ - **Richer PyPI metadata.** Added trove classifiers and keywords to
25
+ `pyproject.toml`. No runtime change.
26
+
27
+ ## 1.1.0 — 2026-07-11
28
+
29
+ This release adds six rules, removes two rules that a corpus audit showed
30
+ could not tell a good implementation from a bad one, and fixes a wide set
31
+ of bugs in the adapters, the input pipeline, and the renderer. The default
32
+ packs now hold 30 rules across the same 6 categories. Grades can shift
33
+ from 1.0.0 because the default packs changed.
34
+
35
+ ### Added
36
+
37
+ - **Six new rules** in both default packs:
38
+ - `SCH-007` and `ATTR-004` grade CJA Data View settings.
39
+ - `SCH-008` finds cycles between CJA derived fields.
40
+ - `SCH-009` finds CJA derived fields that reference components that do
41
+ not exist.
42
+ - `GOV-007` and `GOV-008` flag calculated metrics and segments that are
43
+ shared but never approved.
44
+ - **Default output filenames keyed to the report id.** When you do not
45
+ pass `--output`, the CLI names the file `grade-<report id>.html`. The
46
+ old default used a timestamp, so a batch run that graded several
47
+ instances within the same second wrote them all to one file. (#2)
48
+ - **Capped component lists in the report.** A finding now shows at most
49
+ 50 affected components plus a line with the count of the hidden ones.
50
+ The `--json` output keeps the full list. (#5)
51
+ - **Full inventory in shell-out mode.** Runs started with `--dataview` or
52
+ `--rsid` now pass `--include-all-inventory` to the snapshot tool.
53
+
54
+ ### Changed
55
+
56
+ - **`ATTR-001` and `ATTR-002` are out of the default packs.** A corpus
57
+ audit (`docs/RUBRIC_AUDIT.md`) showed that `ATTR-001` fired on 100% of
58
+ the metrics it watched, and that every fixture scored the same on
59
+ `ATTR-002`, so its threshold could not separate anything. Both check
60
+ functions stay registered for custom packs.
61
+ - **`NAME-001` and `NAME-003` grade again.** The CJA adapter now parses
62
+ tags that arrive as JSON-encoded strings, which is what real snapshots
63
+ contain. Before this fix the two rules matched nothing.
64
+ - **Faster rendering of large reports.** The renderer caches compiled
65
+ templates and CSS, and the report skips layout work for findings that
66
+ are off-screen.
67
+ - **Docs reorganized.** Reference material moved out of the README into
68
+ `docs/`, including a customization hub, platform coverage, supplementary
69
+ inputs, leaderboards, and the rubric audit.
70
+
71
+ ### Fixed
72
+
73
+ - **HTML escaping.** The report and trend templates rendered values
74
+ without escaping, and the comparison chart did not escape category
75
+ labels in its SVG. Both are fixed, so snapshot content can no longer
76
+ inject markup into a report.
77
+ - **Input handling.** Snapshot files with a UTF-8 BOM load. Timestamps
78
+ with UTC offsets or fractional seconds parse. A snapshot whose platform
79
+ cannot be determined raises a clear error instead of being guessed.
80
+ Directory mode ranks all snapshots on one timestamp scale.
81
+ - **Shell-out mode.** The subprocess call has a timeout, decodes output
82
+ as UTF-8, surfaces warnings from the snapshot tool, and reports decode
83
+ failures as the standard invalid-snapshot error.
84
+ - **Rule engine.** Pattern and target params are validated when the
85
+ rubric loads instead of failing mid-run. Cycle detection for `SCH-008`
86
+ and `SEG-004` is iterative and deterministic. Expiration blocks with
87
+ NaN or Infinity `numPeriods` no longer crash.
88
+ - **AA adapter.** It rejects exports whose dimension, metric, calculated
89
+ metric, or segment sections are missing or not lists. It guards against
90
+ wrongly typed tags, reference lists, and numbers. It counts nesting
91
+ depth the same way the CJA adapter does. It renders nested formula text
92
+ readably and skips blank classification tags.
93
+ - **CJA adapter.** Derived-field deduplication normalizes IDs before
94
+ comparing them.
95
+ - **CLI.** `--fail-below` works in trend mode, and trend mode rejects
96
+ flags it does not support.
97
+
98
+ ### Discipline
99
+
100
+ - 402 tests pass and ruff is clean.
101
+ - Determinism holds. The same snapshot and rubric still produce
102
+ byte-identical HTML and JSON.
103
+
104
+ ## 1.0.0 — 2026-05-20
105
+
106
+ First public release. The grader covers the full surface the design SPEC
107
+ laid out, plus a Claude Code skill bundle for follow-up question support.
108
+ Default rubric thresholds are calibrated against a 108-snapshot corpus of
109
+ real CJA + AA production implementations — see
110
+ [`docs/threshold_calibration.md`](docs/threshold_calibration.md) for the
111
+ distributions and the per-rule confidence rating behind each threshold.
112
+
113
+ ### Added
114
+
115
+ - **Deterministic, rule-based linter** for Adobe CJA and AA implementations.
116
+ Reads `cja_auto_sdr` / `aa_auto_sdr` JSON snapshots; emits a single
117
+ self-contained HTML report card and a parallel machine-readable JSON.
118
+ - **26 rules across 6 categories**: schema hygiene, naming consistency,
119
+ segment complexity, calc metric maintainability, attribution coverage,
120
+ governance posture. Every rule in the default packs grades against
121
+ data the snapshot itself carries; check functions that need external
122
+ evidence (Launch exports, cardinality, ownership, downstream usage,
123
+ SDR docs) ship registered but unwired so operators can include them
124
+ in a forked pack.
125
+ - **No cardinality rules.** Rules measure shape, ratio, or correctness —
126
+ never raw counts. See SPEC §11 for the principle and rationale.
127
+ - **Two bundled rubric packs (v1.0)**: `strict` (master-cert-grade,
128
+ calibrated p75–p90 thresholds) and `pragmatic` (sanity-check,
129
+ calibrated p90–p95 thresholds, same rule IDs).
130
+ - **Four input modes**: file path, snapshot directory (with
131
+ `--at TIMESTAMP` and `--trend`), shell-out via `--dataview` /
132
+ `--rsid`, and stdin.
133
+ - **Trend reports**: `--trend` walks a snapshot directory chronologically
134
+ and renders an HTML trajectory with sparklines, per-category deltas,
135
+ and a findings churn summary.
136
+ - **Distribution context**: `--distribution-data PATH` (or `bundled`)
137
+ populates the report's overall histogram and category comparison
138
+ charts from a percentile data file.
139
+ - **Project-level suppression**: `.sdr-grader.yaml` lets operators
140
+ suppress rules, override severities, and rebalance category weights.
141
+ Skipped rules surface in the rendered methodology section.
142
+ - **Supplementary inputs**: `--extra-input KEY=PATH` attaches arbitrary
143
+ JSON files under `Implementation.supplementary_data[KEY]`. The shape
144
+ and key are entirely the rule's contract; the default packs don't
145
+ consume them. Operators who want to grade external evidence write a
146
+ custom rubric pack that targets the registered check functions.
147
+ - **JSON output**: `--json PATH` writes the full Report data structure
148
+ for CI dashboards and downstream tooling. Datetimes normalize to UTC
149
+ ISO-8601 with `Z` suffix.
150
+ - **CI integration**: `--fail-below GRADE` exits 2 when the grade drops
151
+ below the threshold letter, suitable for PR gates.
152
+ - **Claude Code skill bundle** at `skills/sdr-grader/`: SKILL.md plus a
153
+ bundled `query_grade.py` helper for filtering findings, looking up
154
+ remediations, and comparing two grade JSONs from inside Claude Code.
155
+ - **Documentation set**: `docs/RUBRIC_FORMAT.md`, `docs/CHECK_FUNCTION_GUIDE.md`,
156
+ `docs/ADAPTER_GUIDE.md`, `docs/CI_INTEGRATION.md`, plus README quickstart.
157
+ - **Examples**: clean and messy grade cards per platform
158
+ (`examples/grade-cja-clean.html` A 100%, `examples/grade-cja-messy.html`
159
+ F 44%, `examples/grade-aa-clean.html` A 100%, `examples/grade-aa-messy.html`
160
+ F 39%), a multi-snapshot `examples/trend-example.html`, the original
161
+ visual-contract reference `examples/sample-report.html`, and the
162
+ renderer-output golden `examples/templated-report.html`.
163
+
164
+ ### Discipline
165
+
166
+ - **Determinism is testable.** The same snapshot + rubric produces
167
+ byte-identical HTML and JSON; tests assert this across two runs.
168
+ - **261 tests** cover adapters, rules, engine, grader, CLI, trend
169
+ pipeline, distribution context, supplementary inputs, and the skill
170
+ helper. All pass; ruff clean.
171
+ - **Phase discipline preserved**: each commit produced a working,
172
+ reviewable artifact. The full commit graph from scaffold to 1.0.0 is
173
+ visible in the git history.
174
+
175
+ ### Calibration-driven design decisions
176
+
177
+ Calibration against a 108-snapshot corpus of real CJA + AA production
178
+ implementations exposed three rule design problems that this release
179
+ addresses:
180
+
181
+ - **Adobe APIs don't expose `owner` on inline dimensions / metrics.**
182
+ Every tenant in the corpus measured 100% missing owners even after
183
+ normalizing the `"Unknown User"` sentinel. `GOV-004 missing_owners`
184
+ has been moved out of the default packs (still registered for
185
+ operators with their own ownership data).
186
+ - **SDR snapshots don't carry downstream usage.** "Orphan" detection
187
+ for segments and calculated metrics in the corpus showed every
188
+ tenant at 100% — segments and calc metrics in production are
189
+ referenced by Workspace projects, dashboards, and alerts, none of
190
+ which the SDR captures. `SEG-003 orphan_segments` and
191
+ `CALC-005 orphan_calc_metrics` have been moved out of the default
192
+ packs.
193
+ - **Description discipline is universally poor.** ≥90% of real
194
+ segment-bearing tenants and ≈100% of calc-metric-bearing tenants
195
+ have 100% of those components missing descriptions.
196
+ `SEG-005 segments_missing_descriptions` and
197
+ `CALC-001 calc_metrics_missing_descriptions` are kept in the default
198
+ packs at low severity with near-saturation thresholds (0.95 strict,
199
+ 1.0 pragmatic) — they flag the most egregious tenants without
200
+ swamping the grade.
201
+
202
+ ### Registered but not in the default packs
203
+
204
+ These check functions are registered and tested, but the default
205
+ `strict` / `pragmatic` packs don't reference them — they require
206
+ evidence the snapshot itself doesn't carry. Operators with that
207
+ evidence can include them in a custom rubric pack:
208
+
209
+ - `orphan_segments` (slot `SEG-003`) and `orphan_calc_metrics` (slot
210
+ `CALC-005`) — require downstream Workspace / dashboard usage data.
211
+ - `missing_owners` (slot `GOV-004`) — requires owner attribution data
212
+ that Adobe's APIs don't expose on dims/metrics.
213
+ - `doc_drift` (slot `GOV-006`) — reads `last_sdr_update_at` (param),
214
+ `supplementary_data['sdr']['last_updated_at']`, or
215
+ `metadata['SDR Last Updated']`.
216
+ - `cardinality_concerns` (slot `SCH-006`) — reads
217
+ `supplementary_data['cardinality']` (a `component_id -> int` map).
218
+ - `launch_required_data_elements` (slot `LAUNCH-001`) — reads
219
+ `supplementary_data['launch']`; the canonical worked example for the
220
+ `--extra-input` extension pattern.
221
+ - `calc_deprecated_allocations` (slot `CALC-022`) — needs a concrete
222
+ `deprecated_allocations` set via params; default ships placeholder
223
+ values.
224
+
225
+ ### Known limitations
226
+
227
+ - `cja_auto_sdr` / `aa_auto_sdr` are required to produce the JSON
228
+ snapshots the grader consumes; they are separate projects.
229
+ - The bundled `data/distribution.json` ships seed percentile data.
230
+ `scripts/aggregate_distributions.py` lets teams build their own
231
+ internal leaderboards from a directory of grade JSONs;
232
+ `--distribution-data PATH` plugs the result into the report. A
233
+ centralized opt-in submission service is out of scope for this repo.
234
+ - README screenshots require manual capture (open
235
+ `examples/grade-cja-messy.html` in a browser and screenshot the page).
236
+ Embedded SVG sparklines + the inlined CSS make the report itself a
237
+ high-fidelity preview when rendered.