jupytermind 0.3.0

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 (193) hide show
  1. package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
  2. package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
  3. package/.github/skills/ai-data-scientist/SKILL.md +330 -0
  4. package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
  5. package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
  6. package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
  7. package/.github/skills/ai-materials-scientist/manifest.json +58 -0
  8. package/.github/skills/ai-scientist/SKILL.md +69 -0
  9. package/.github/skills/ai-scientist/manifest.json +61 -0
  10. package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
  11. package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
  12. package/.github/skills/japanese-prose/NOTICE.md +17 -0
  13. package/.github/skills/japanese-prose/SKILL.md +111 -0
  14. package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
  15. package/.github/skills/japanese-prose/references/scoring.md +24 -0
  16. package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
  17. package/.github/skills/japanese-prose/scripts/core.py +192 -0
  18. package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
  19. package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
  20. package/.github/skills/japanese-prose/scripts/lint.py +378 -0
  21. package/.github/skills/japanese-prose/scripts/outline.py +68 -0
  22. package/.github/skills/japanese-prose/scripts/terms.py +112 -0
  23. package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
  24. package/.github/skills/presentation-planner/SKILL.md +257 -0
  25. package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
  26. package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
  27. package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
  28. package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
  29. package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
  30. package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
  31. package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
  32. package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
  33. package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
  34. package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
  35. package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
  36. package/.github/skills/tech-writer/SKILL.md +434 -0
  37. package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
  38. package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
  39. package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
  40. package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
  41. package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
  42. package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
  43. package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
  44. package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
  45. package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
  46. package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
  47. package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
  48. package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
  49. package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
  50. package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
  51. package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
  52. package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
  53. package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
  54. package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
  55. package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
  56. package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
  57. package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
  58. package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
  59. package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
  60. package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
  61. package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
  62. package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
  63. package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
  64. package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
  65. package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
  66. package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
  67. package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
  68. package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
  69. package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
  70. package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
  71. package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
  72. package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
  73. package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
  74. package/.github/skills/tech-writer/references/style-constitution.md +104 -0
  75. package/.github/skills/tech-writer/scripts/lint.py +412 -0
  76. package/LICENSE +21 -0
  77. package/README.md +92 -0
  78. package/bin/ai-data-scientist.js +123 -0
  79. package/package.json +41 -0
  80. package/pyproject.toml +45 -0
  81. package/src/ai_chemistry_scientist/__init__.py +0 -0
  82. package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
  83. package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
  84. package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
  85. package/src/ai_chemistry_scientist/dispatch.py +369 -0
  86. package/src/ai_chemistry_scientist/docking_score.py +97 -0
  87. package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
  88. package/src/ai_chemistry_scientist/evidence.py +41 -0
  89. package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
  90. package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
  91. package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
  92. package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
  93. package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
  94. package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
  95. package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
  96. package/src/ai_chemistry_scientist/validation.py +70 -0
  97. package/src/ai_data_scientist/__init__.py +0 -0
  98. package/src/ai_data_scientist/analysis_assumptions.py +121 -0
  99. package/src/ai_data_scientist/anomaly_detection.py +39 -0
  100. package/src/ai_data_scientist/automl.py +109 -0
  101. package/src/ai_data_scientist/cleaning.py +56 -0
  102. package/src/ai_data_scientist/cli.py +90 -0
  103. package/src/ai_data_scientist/clustering.py +54 -0
  104. package/src/ai_data_scientist/dashboard.py +33 -0
  105. package/src/ai_data_scientist/data_definition.py +100 -0
  106. package/src/ai_data_scientist/data_quality.py +164 -0
  107. package/src/ai_data_scientist/dataset_validation.py +135 -0
  108. package/src/ai_data_scientist/dependency_pins.py +60 -0
  109. package/src/ai_data_scientist/eda.py +82 -0
  110. package/src/ai_data_scientist/experiment_evaluation.py +635 -0
  111. package/src/ai_data_scientist/explainability.py +340 -0
  112. package/src/ai_data_scientist/feature_engineering.py +163 -0
  113. package/src/ai_data_scientist/gate_config.py +32 -0
  114. package/src/ai_data_scientist/ingestion.py +127 -0
  115. package/src/ai_data_scientist/insight_engine.py +180 -0
  116. package/src/ai_data_scientist/japanese_nlp.py +43 -0
  117. package/src/ai_data_scientist/jupyter_launcher.py +137 -0
  118. package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
  119. package/src/ai_data_scientist/language_router.py +28 -0
  120. package/src/ai_data_scientist/lifecycle.py +221 -0
  121. package/src/ai_data_scientist/mcp_gateway.py +113 -0
  122. package/src/ai_data_scientist/mcp_runtime.py +194 -0
  123. package/src/ai_data_scientist/mcp_transport.py +53 -0
  124. package/src/ai_data_scientist/ml_modeling.py +451 -0
  125. package/src/ai_data_scientist/model_tuning.py +104 -0
  126. package/src/ai_data_scientist/notebook_audit.py +574 -0
  127. package/src/ai_data_scientist/project_manager.py +243 -0
  128. package/src/ai_data_scientist/report_export.py +73 -0
  129. package/src/ai_data_scientist/sensitivity.py +445 -0
  130. package/src/ai_data_scientist/signal_analysis.py +201 -0
  131. package/src/ai_data_scientist/skill_packaging.py +40 -0
  132. package/src/ai_data_scientist/stats_analysis.py +88 -0
  133. package/src/ai_data_scientist/text_nlp.py +44 -0
  134. package/src/ai_data_scientist/timeseries.py +68 -0
  135. package/src/ai_data_scientist/visualization.py +708 -0
  136. package/src/ai_genomics_scientist/__init__.py +1 -0
  137. package/src/ai_genomics_scientist/differential_expression.py +147 -0
  138. package/src/ai_genomics_scientist/dispatch.py +267 -0
  139. package/src/ai_genomics_scientist/evidence.py +45 -0
  140. package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
  141. package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
  142. package/src/ai_genomics_scientist/sequence_features.py +111 -0
  143. package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
  144. package/src/ai_genomics_scientist/validation.py +83 -0
  145. package/src/ai_genomics_scientist/variant_effect.py +147 -0
  146. package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
  147. package/src/ai_materials_scientist/__init__.py +0 -0
  148. package/src/ai_materials_scientist/calphad.py +117 -0
  149. package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
  150. package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
  151. package/src/ai_materials_scientist/dispatch.py +100 -0
  152. package/src/ai_materials_scientist/evidence.py +84 -0
  153. package/src/ai_materials_scientist/fem.py +279 -0
  154. package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
  155. package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
  156. package/src/ai_materials_scientist/phase_field.py +167 -0
  157. package/src/ai_materials_scientist/validation.py +70 -0
  158. package/src/ai_scientist/__init__.py +1 -0
  159. package/src/ai_scientist/completion_gate.py +15 -0
  160. package/src/ai_scientist/data_analysis.py +46 -0
  161. package/src/ai_scientist/evidence_registry.py +99 -0
  162. package/src/ai_scientist/experimental_design.py +20 -0
  163. package/src/ai_scientist/language.py +14 -0
  164. package/src/ai_scientist/latex_renderer.py +41 -0
  165. package/src/ai_scientist/literature_review.py +37 -0
  166. package/src/ai_scientist/manifest.py +87 -0
  167. package/src/ai_scientist/manuscript.py +94 -0
  168. package/src/ai_scientist/mcp_config.py +76 -0
  169. package/src/ai_scientist/mcp_external.py +42 -0
  170. package/src/ai_scientist/mcp_failures.py +23 -0
  171. package/src/ai_scientist/mcp_gateway.py +38 -0
  172. package/src/ai_scientist/mcp_managed.py +180 -0
  173. package/src/ai_scientist/npm_packaging.py +49 -0
  174. package/src/ai_scientist/orchestrator.py +133 -0
  175. package/src/ai_scientist/peer_review.py +60 -0
  176. package/src/ai_scientist/phase_gate.py +74 -0
  177. package/src/ai_scientist/phase_state.py +230 -0
  178. package/src/ai_scientist/presentation.py +56 -0
  179. package/src/ai_scientist/project_config.py +31 -0
  180. package/src/ai_scientist/project_handle.py +74 -0
  181. package/src/ai_scientist/reproducibility.py +20 -0
  182. package/src/ai_scientist/research_planning.py +20 -0
  183. package/src/ai_scientist/skill_invocation.py +21 -0
  184. package/src/ai_scientist/tdd_gate.py +99 -0
  185. package/src/ai_structural_biology_scientist/__init__.py +0 -0
  186. package/src/ai_structural_biology_scientist/contact_map.py +87 -0
  187. package/src/ai_structural_biology_scientist/dispatch.py +269 -0
  188. package/src/ai_structural_biology_scientist/evidence.py +43 -0
  189. package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
  190. package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
  191. package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
  192. package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
  193. package/src/ai_structural_biology_scientist/validation.py +100 -0
@@ -0,0 +1,574 @@
1
+ """Read-only notebook execution/evidence audit.
2
+
3
+ Implements DES-AIDS-033 (REQ-AIDS-045): inspects a project notebook without
4
+ writing it back, reporting nbformat validity, unexecuted/error code cells,
5
+ chart outputs, and whether every insight-like markdown cell carries a
6
+ well-formed evidence manifest that resolves to a real executed cell output
7
+ (reusing insight_engine's cited-value matching rule).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import json
14
+ import re
15
+ import struct
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ import nbformat
20
+
21
+ from ai_data_scientist import project_manager
22
+ from ai_data_scientist.insight_engine import AmbiguousEvidenceError, _find_evidence_cell
23
+
24
+ _EVIDENCE_FENCE_PATTERN = re.compile(r"```evidence\n(.*?)\n```", re.DOTALL)
25
+ _REQUIRED_MANIFEST_KEYS = {"execution_count", "cited_value", "claim_type"}
26
+ # A chart whose compressed PNG payload holds fewer than this many bytes per
27
+ # pixel is treated as suspiciously uniform/near-empty (DES-AIDS-041): a real
28
+ # rendered chart (axes, ticks, text, data) compresses far less efficiently
29
+ # than a solid-fill or all-white canvas of the same dimensions. Approximate
30
+ # by design (no imaging dependency is added for an exact pixel scan).
31
+ _NEAR_EMPTY_BYTES_PER_PIXEL_THRESHOLD = 0.02
32
+ _PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class NotebookAuditFinding:
37
+ """A single audit observation tied to an optional cell index."""
38
+
39
+ severity: str # "error" | "warning"
40
+ message: str
41
+ cell_index: int | None = None
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class VisualAuditFinding:
46
+ """A single visual-readability observation for one chart output."""
47
+
48
+ chart_cell_index: int
49
+ code: str # e.g. "missing_glyphs", "near_empty_image", "missing_label"
50
+ severity: str # "error" | "warning"
51
+ details: dict = field(default_factory=dict)
52
+ output_index: int | None = None
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class NotebookAuditReport:
57
+ """Aggregated, read-only audit result for one notebook."""
58
+
59
+ path: str
60
+ nbformat_valid: bool
61
+ code_cell_count: int
62
+ executed_code_cell_count: int
63
+ unexecuted_cell_indices: tuple[int, ...]
64
+ error_cell_indices: tuple[int, ...]
65
+ chart_cell_indices: tuple[int, ...]
66
+ insight_cell_count: int
67
+ findings: tuple[NotebookAuditFinding, ...] = field(default_factory=tuple)
68
+ visual_findings: tuple[VisualAuditFinding, ...] = field(default_factory=tuple)
69
+
70
+ @property
71
+ def ok(self) -> bool:
72
+ """``True`` iff no error-level finding was recorded."""
73
+ return not any(finding.severity == "error" for finding in self.findings)
74
+
75
+
76
+ # @id CODE-AIDS-089
77
+ # @implements REQ-AIDS-069
78
+ # @design DES-AIDS-057
79
+ def _extract_evidence_manifests(markdown_source: str) -> list[dict | None]:
80
+ """Return every ```evidence fenced block's parsed JSON payload, in
81
+ appearance order. A block that fails to parse as a JSON object yields
82
+ ``None`` (not silently omitted), so malformed blocks are still reported.
83
+ """
84
+ manifests: list[dict | None] = []
85
+ for match in _EVIDENCE_FENCE_PATTERN.finditer(markdown_source):
86
+ try:
87
+ payload = json.loads(match.group(1))
88
+ except json.JSONDecodeError:
89
+ manifests.append(None)
90
+ continue
91
+ manifests.append(payload if isinstance(payload, dict) else None)
92
+ return manifests
93
+
94
+
95
+ # @id CODE-AIDS-086
96
+ # @implements REQ-AIDS-066
97
+ # @design DES-AIDS-054
98
+ def _strip_evidence_fences(markdown_source: str) -> str:
99
+ """Return ``markdown_source`` with every ```evidence fenced block removed."""
100
+ return _EVIDENCE_FENCE_PATTERN.sub("", markdown_source)
101
+
102
+
103
+ def _body_mentions_value(body_text: str, cited_value: str) -> bool:
104
+ """``True`` iff ``body_text`` mentions ``cited_value``, verbatim or as a
105
+ rounding-equivalent decimal (REQ-AIDS-066).
106
+
107
+ Non-numeric ``cited_value``s (e.g. categorical/"OK" claim types) fall
108
+ back to the plain substring check only, per DES-AIDS-054.
109
+ """
110
+ if cited_value in body_text:
111
+ return True
112
+ try:
113
+ cited_float = float(cited_value)
114
+ except ValueError:
115
+ return False
116
+ for candidate in re.findall(r"-?\d+\.\d+", body_text):
117
+ decimals = len(candidate.split(".")[1])
118
+ if round(float(candidate), decimals) == round(cited_float, decimals):
119
+ return True
120
+ return False
121
+
122
+
123
+ # @id CODE-AIDS-090
124
+ # @implements REQ-AIDS-070
125
+ # @design DES-AIDS-058
126
+ def _looks_like_insight_candidate(markdown_source: str) -> bool:
127
+ stripped = markdown_source.strip()
128
+ if not stripped:
129
+ return False
130
+ # GitHub #30: a heading-prefixed cell ("# ...") was unconditionally
131
+ # excluded, letting malformed evidence in such cells bypass validation.
132
+ # A cell that genuinely carries an evidence manifest is a candidate
133
+ # regardless of a leading heading.
134
+ if _EVIDENCE_FENCE_PATTERN.search(stripped):
135
+ return True
136
+ if not stripped.startswith("#"):
137
+ return True
138
+ # GitHub #43: a heading-prefixed cell with no evidence fence was
139
+ # unconditionally excluded even when it carries a genuine result
140
+ # paragraph after the heading. Strip leading heading line(s) (and any
141
+ # blank lines directly between them) and re-apply the same candidacy
142
+ # decision to whatever non-heading text remains.
143
+ remainder_lines = stripped.splitlines()
144
+ while remainder_lines and (
145
+ remainder_lines[0].lstrip().startswith("#") or not remainder_lines[0].strip()
146
+ ):
147
+ remainder_lines.pop(0)
148
+ remainder = "\n".join(remainder_lines).strip()
149
+ if not remainder:
150
+ return False
151
+ return _looks_like_insight_candidate(remainder)
152
+
153
+
154
+ def _png_dimensions(png_bytes: bytes) -> tuple[int, int] | None:
155
+ """Parse (width, height) from a PNG's IHDR chunk; ``None`` if malformed."""
156
+ if not png_bytes.startswith(_PNG_SIGNATURE) or len(png_bytes) < 24:
157
+ return None
158
+ width, height = struct.unpack(">II", png_bytes[16:24])
159
+ return width, height
160
+
161
+
162
+ # @id CODE-AIDS-073
163
+ # @implements REQ-AIDS-053
164
+ # @design DES-AIDS-041
165
+ # @id CODE-AIDS-092
166
+ # @implements REQ-AIDS-072
167
+ # @design DES-AIDS-060
168
+ def audit_visual_outputs(
169
+ notebook, chart_cell_indices: tuple[int, ...]
170
+ ) -> tuple[VisualAuditFinding, ...]:
171
+ """Inspect each chart cell's image outputs individually.
172
+
173
+ Each image output's own ``output["metadata"]["chart"]`` (written by
174
+ ``visualization.build_image_output``, REQ-AIDS-071) is authoritative.
175
+ Only when a cell has exactly one qualifying image output AND that
176
+ output has no ``"chart"`` key at all (not merely empty/invalid) does
177
+ this fall back to the cell-level ``cell["metadata"]["chart"]``
178
+ (preserving ``record_chart``'s pre-REQ-AIDS-071 contract). Every
179
+ finding is tagged with the specific ``output_index`` it concerns
180
+ (DES-AIDS-060), so one image's metadata is never applied to a sibling.
181
+ """
182
+ findings: list[VisualAuditFinding] = []
183
+ for index in chart_cell_indices:
184
+ cell = notebook.cells[index]
185
+ image_outputs = [
186
+ (output_index, output)
187
+ for output_index, output in enumerate(cell.get("outputs", []))
188
+ if output.get("data", {}).get("image/png")
189
+ ]
190
+ cell_metadata = cell.get("metadata", {}).get("chart")
191
+
192
+ for output_index, output in image_outputs:
193
+ output_metadata = output.get("metadata", {})
194
+ has_output_chart_key = "chart" in output_metadata
195
+ chart_metadata = output_metadata.get("chart")
196
+ if (
197
+ not has_output_chart_key
198
+ and len(image_outputs) == 1
199
+ and isinstance(cell_metadata, dict)
200
+ and cell_metadata
201
+ ):
202
+ chart_metadata = cell_metadata
203
+
204
+ # GitHub #28/#40: an image output whose resolved authoring
205
+ # metadata is absent, empty, or not a mapping must be flagged
206
+ # "unaudited" rather than silently treated as passing the
207
+ # glyph/label checks below, which require a usable mapping.
208
+ if not (isinstance(chart_metadata, dict) and chart_metadata):
209
+ findings.append(
210
+ VisualAuditFinding(
211
+ chart_cell_index=index,
212
+ code="unaudited",
213
+ severity="warning",
214
+ details={},
215
+ output_index=output_index,
216
+ )
217
+ )
218
+ if not isinstance(chart_metadata, dict):
219
+ chart_metadata = {}
220
+
221
+ missing_glyphs = chart_metadata.get("missing_glyphs")
222
+ if missing_glyphs:
223
+ findings.append(
224
+ VisualAuditFinding(
225
+ chart_cell_index=index,
226
+ code="missing_glyphs",
227
+ severity="error",
228
+ details={"codepoints": missing_glyphs},
229
+ output_index=output_index,
230
+ )
231
+ )
232
+
233
+ for label_field in ("title", "xlabel", "ylabel", "legend"):
234
+ if chart_metadata and not chart_metadata.get(label_field):
235
+ findings.append(
236
+ VisualAuditFinding(
237
+ chart_cell_index=index,
238
+ code="missing_label",
239
+ severity="warning",
240
+ details={"field": label_field},
241
+ output_index=output_index,
242
+ )
243
+ )
244
+
245
+ encoded = output.get("data", {}).get("image/png")
246
+ png_bytes = base64.b64decode(encoded)
247
+ dimensions = _png_dimensions(png_bytes)
248
+ if dimensions is None:
249
+ continue
250
+ width, height = dimensions
251
+ pixel_count = max(width * height, 1)
252
+ bytes_per_pixel = len(png_bytes) / pixel_count
253
+ if bytes_per_pixel < _NEAR_EMPTY_BYTES_PER_PIXEL_THRESHOLD:
254
+ findings.append(
255
+ VisualAuditFinding(
256
+ chart_cell_index=index,
257
+ code="near_empty_image",
258
+ severity="error",
259
+ details={"bytes_per_pixel": bytes_per_pixel},
260
+ output_index=output_index,
261
+ )
262
+ )
263
+
264
+ return tuple(findings)
265
+
266
+
267
+ # @id CODE-AIDS-053
268
+ # @implements REQ-AIDS-045
269
+ # @design DES-AIDS-033
270
+ # @id CODE-AIDS-057
271
+ # @implements REQ-AIDS-047
272
+ # @design DES-AIDS-035
273
+ def audit_notebook(path: Path | str, visual_audit: bool = False) -> NotebookAuditReport:
274
+ """Audit ``path`` read-only; never writes the notebook back to disk.
275
+
276
+ When ``visual_audit`` is ``True`` (default ``False``, fully backward
277
+ compatible), also runs ``audit_visual_outputs`` over every detected
278
+ chart cell and appends any readability finding as an error-severity
279
+ ``NotebookAuditFinding`` so it affects ``report.ok`` (REQ-AIDS-053).
280
+ """
281
+ path = Path(path)
282
+ try:
283
+ resolved_path = project_manager.resolve_stable_path(path)
284
+ except project_manager.StablePathResolutionError as exc:
285
+ return NotebookAuditReport(
286
+ path=str(path),
287
+ nbformat_valid=False,
288
+ code_cell_count=0,
289
+ executed_code_cell_count=0,
290
+ unexecuted_cell_indices=(),
291
+ error_cell_indices=(),
292
+ chart_cell_indices=(),
293
+ insight_cell_count=0,
294
+ findings=(
295
+ NotebookAuditFinding("error", f"Notebook path could not be resolved: {exc}", None),
296
+ ),
297
+ )
298
+
299
+ try:
300
+ notebook = nbformat.read(str(resolved_path), as_version=4)
301
+ nbformat.validate(notebook)
302
+ except Exception as exc: # noqa: BLE001 - surface any parse/validate failure as a finding
303
+ return NotebookAuditReport(
304
+ path=str(path),
305
+ nbformat_valid=False,
306
+ code_cell_count=0,
307
+ executed_code_cell_count=0,
308
+ unexecuted_cell_indices=(),
309
+ error_cell_indices=(),
310
+ chart_cell_indices=(),
311
+ insight_cell_count=0,
312
+ findings=(
313
+ NotebookAuditFinding("error", f"Notebook failed to parse or validate: {exc}", None),
314
+ ),
315
+ )
316
+
317
+ findings: list[NotebookAuditFinding] = []
318
+ code_cell_count = 0
319
+ executed_code_cell_count = 0
320
+ unexecuted_indices: list[int] = []
321
+ error_indices: list[int] = []
322
+ chart_indices: list[int] = []
323
+ # GitHub #54: execution_count is reused across a kernel restart or when
324
+ # a notebook is appended to in a new session; track every index sharing
325
+ # a value so duplicates can be reported instead of silently ignored.
326
+ execution_count_indices: dict[int, list[int]] = {}
327
+
328
+ # @id CODE-AIDS-058
329
+ # @implements REQ-AIDS-048
330
+ # @design DES-AIDS-036
331
+ # Detect the one documented self-audit pattern: the notebook's own last
332
+ # cell, still running (no execution_count yet), whose source invokes
333
+ # audit_notebook. That cell cannot have an execution_count by definition
334
+ # (it is the audit call itself), so it must not be flagged as a failure.
335
+ last_index = len(notebook.cells) - 1
336
+ self_audit_index: int | None = None
337
+ if last_index >= 0:
338
+ last_cell = notebook.cells[last_index]
339
+ if (
340
+ last_cell.get("cell_type") == "code"
341
+ and last_cell.get("execution_count") is None
342
+ and "audit_notebook" in last_cell.get("source", "")
343
+ ):
344
+ self_audit_index = last_index
345
+
346
+ for index, cell in enumerate(notebook.cells):
347
+ if cell.get("cell_type") != "code":
348
+ continue
349
+ code_cell_count += 1
350
+ execution_count = cell.get("execution_count")
351
+ if index == self_audit_index:
352
+ findings.append(
353
+ NotebookAuditFinding(
354
+ "warning",
355
+ "Trailing cell invokes audit_notebook and has not finished "
356
+ "executing yet; excluded from unexecuted-cell findings.",
357
+ index,
358
+ )
359
+ )
360
+ elif execution_count is None:
361
+ unexecuted_indices.append(index)
362
+ findings.append(
363
+ NotebookAuditFinding(
364
+ "error", "Code cell has no execution_count (not executed).", index
365
+ )
366
+ )
367
+ else:
368
+ executed_code_cell_count += 1
369
+ execution_count_indices.setdefault(execution_count, []).append(index)
370
+
371
+ has_error = False
372
+ has_chart = False
373
+ for output in cell.get("outputs", []):
374
+ if output.get("output_type") == "error":
375
+ has_error = True
376
+ data = output.get("data", {})
377
+ if "image/png" in data:
378
+ has_chart = True
379
+ if has_error:
380
+ error_indices.append(index)
381
+ findings.append(NotebookAuditFinding("error", "Code cell has an error output.", index))
382
+ if has_chart:
383
+ chart_indices.append(index)
384
+
385
+ # @id CODE-AIDS-126
386
+ # @implements REQ-AIDS-045
387
+ # @design DES-AIDS-033
388
+ # GitHub #54: report every execution_count shared by more than one code
389
+ # cell so a stale/reused count can't silently resolve an insight's
390
+ # evidence to the wrong cell without the audit flagging it.
391
+ for execution_count, indices in sorted(execution_count_indices.items()):
392
+ if len(indices) > 1:
393
+ findings.append(
394
+ NotebookAuditFinding(
395
+ "warning",
396
+ f"execution_count={execution_count!r} is shared by {len(indices)} "
397
+ f"code cells at indices {indices}; evidence resolution for this "
398
+ "execution_count is ambiguous (e.g. after a kernel restart or "
399
+ "appending to the notebook in a new session).",
400
+ indices[0],
401
+ )
402
+ )
403
+
404
+ insight_cell_count = 0
405
+ for index, cell in enumerate(notebook.cells):
406
+ if cell.get("cell_type") != "markdown":
407
+ continue
408
+ source = cell.get("source", "")
409
+ if not _looks_like_insight_candidate(source):
410
+ continue
411
+
412
+ manifests = _extract_evidence_manifests(source)
413
+ if not manifests:
414
+ findings.append(
415
+ NotebookAuditFinding(
416
+ "error",
417
+ "Markdown cell looks like an insight but has no evidence "
418
+ "manifest (missing ```evidence fenced JSON block).",
419
+ index,
420
+ )
421
+ )
422
+ continue
423
+
424
+ insight_cell_count += 1
425
+ for block_index, manifest in enumerate(manifests):
426
+ block_tag = f" (block {block_index})" if len(manifests) > 1 else ""
427
+ if manifest is None:
428
+ findings.append(
429
+ NotebookAuditFinding(
430
+ "error",
431
+ f"Malformed evidence block{block_tag}: the ```evidence fenced "
432
+ "block is not well-formed JSON object.",
433
+ index,
434
+ )
435
+ )
436
+ continue
437
+
438
+ missing_keys = _REQUIRED_MANIFEST_KEYS - manifest.keys()
439
+ if missing_keys:
440
+ findings.append(
441
+ NotebookAuditFinding(
442
+ "error",
443
+ f"Evidence manifest{block_tag} is missing required keys: "
444
+ f"{sorted(missing_keys)}.",
445
+ index,
446
+ )
447
+ )
448
+ continue
449
+
450
+ execution_count = manifest["execution_count"]
451
+ cited_value = str(manifest["cited_value"])
452
+ try:
453
+ evidence_cell = _find_evidence_cell(notebook, execution_count, cited_value)
454
+ except AmbiguousEvidenceError:
455
+ findings.append(
456
+ NotebookAuditFinding(
457
+ "error",
458
+ f"Evidence manifest{block_tag} references "
459
+ f"execution_count={execution_count!r} with "
460
+ f"cited_value={cited_value!r}, but more than one executed "
461
+ "cell matches it; the evidentiary cell is ambiguous "
462
+ "(GitHub #54).",
463
+ index,
464
+ )
465
+ )
466
+ continue
467
+ if evidence_cell is None:
468
+ findings.append(
469
+ NotebookAuditFinding(
470
+ "error",
471
+ f"Evidence manifest{block_tag} references "
472
+ f"execution_count={execution_count!r} with "
473
+ f"cited_value={cited_value!r}, but no executed cell output "
474
+ "contains it (missing or stale evidence).",
475
+ index,
476
+ )
477
+ )
478
+ elif not _body_mentions_value(_strip_evidence_fences(source), cited_value):
479
+ findings.append(
480
+ NotebookAuditFinding(
481
+ "warning",
482
+ f"Insight body text does not mention the cited value "
483
+ f"{cited_value!r}{block_tag} (verbatim or as a "
484
+ "rounding-equivalent number); verify the claim still "
485
+ "matches the evidence.",
486
+ index,
487
+ )
488
+ )
489
+
490
+ supporting_evidence = manifest.get("supporting_evidence")
491
+ if supporting_evidence is None:
492
+ continue
493
+ if not isinstance(supporting_evidence, list):
494
+ findings.append(
495
+ NotebookAuditFinding(
496
+ "error",
497
+ f"Malformed supporting_evidence{block_tag}: expected a list.",
498
+ index,
499
+ )
500
+ )
501
+ continue
502
+ for entry_index, entry in enumerate(supporting_evidence):
503
+ entry_tag = f"{block_tag} supporting_evidence[{entry_index}]"
504
+ if (
505
+ not isinstance(entry, dict)
506
+ or not isinstance(entry.get("execution_count"), int)
507
+ or isinstance(entry.get("execution_count"), bool)
508
+ or not isinstance(entry.get("cited_value"), str)
509
+ or not entry.get("cited_value")
510
+ ):
511
+ findings.append(
512
+ NotebookAuditFinding(
513
+ "error",
514
+ f"Malformed supporting_evidence entry{entry_tag}: "
515
+ "expected a mapping with an int execution_count and "
516
+ "a non-empty str cited_value.",
517
+ index,
518
+ )
519
+ )
520
+ continue
521
+ entry_cited_value = entry["cited_value"]
522
+ try:
523
+ entry_evidence_cell = _find_evidence_cell(
524
+ notebook, entry["execution_count"], entry_cited_value
525
+ )
526
+ except AmbiguousEvidenceError:
527
+ findings.append(
528
+ NotebookAuditFinding(
529
+ "error",
530
+ f"supporting_evidence entry{entry_tag} references "
531
+ f"execution_count={entry['execution_count']!r} with "
532
+ f"cited_value={entry_cited_value!r}, but more than one "
533
+ "executed cell matches it; the evidentiary cell is "
534
+ "ambiguous (GitHub #54).",
535
+ index,
536
+ )
537
+ )
538
+ continue
539
+ if entry_evidence_cell is None:
540
+ findings.append(
541
+ NotebookAuditFinding(
542
+ "error",
543
+ f"supporting_evidence entry{entry_tag} references "
544
+ f"execution_count={entry['execution_count']!r} with "
545
+ f"cited_value={entry_cited_value!r}, but no executed "
546
+ "cell output contains it (missing or stale evidence).",
547
+ index,
548
+ )
549
+ )
550
+
551
+ visual_findings: tuple[VisualAuditFinding, ...] = ()
552
+ if visual_audit:
553
+ visual_findings = audit_visual_outputs(notebook, tuple(chart_indices))
554
+ for visual_finding in visual_findings:
555
+ findings.append(
556
+ NotebookAuditFinding(
557
+ visual_finding.severity,
558
+ f"Visual readability issue ({visual_finding.code}): {visual_finding.details}",
559
+ visual_finding.chart_cell_index,
560
+ )
561
+ )
562
+
563
+ return NotebookAuditReport(
564
+ path=str(path),
565
+ nbformat_valid=True,
566
+ code_cell_count=code_cell_count,
567
+ executed_code_cell_count=executed_code_cell_count,
568
+ unexecuted_cell_indices=tuple(unexecuted_indices),
569
+ error_cell_indices=tuple(error_indices),
570
+ chart_cell_indices=tuple(chart_indices),
571
+ insight_cell_count=insight_cell_count,
572
+ findings=tuple(findings),
573
+ visual_findings=visual_findings,
574
+ )