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.
- package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
- package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
- package/.github/skills/ai-data-scientist/SKILL.md +330 -0
- package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
- package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
- package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
- package/.github/skills/ai-materials-scientist/manifest.json +58 -0
- package/.github/skills/ai-scientist/SKILL.md +69 -0
- package/.github/skills/ai-scientist/manifest.json +61 -0
- package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
- package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
- package/.github/skills/japanese-prose/NOTICE.md +17 -0
- package/.github/skills/japanese-prose/SKILL.md +111 -0
- package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
- package/.github/skills/japanese-prose/references/scoring.md +24 -0
- package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
- package/.github/skills/japanese-prose/scripts/core.py +192 -0
- package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
- package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
- package/.github/skills/japanese-prose/scripts/lint.py +378 -0
- package/.github/skills/japanese-prose/scripts/outline.py +68 -0
- package/.github/skills/japanese-prose/scripts/terms.py +112 -0
- package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
- package/.github/skills/presentation-planner/SKILL.md +257 -0
- package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
- package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
- package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
- package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
- package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
- package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
- package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
- package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
- package/.github/skills/tech-writer/SKILL.md +434 -0
- package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
- package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
- package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
- package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
- package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
- package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
- package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
- package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
- package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
- package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
- package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
- package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
- package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
- package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
- package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
- package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
- package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
- package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
- package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
- package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
- package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
- package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
- package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
- package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
- package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
- package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
- package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
- package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
- package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
- package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
- package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
- package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
- package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
- package/.github/skills/tech-writer/references/style-constitution.md +104 -0
- package/.github/skills/tech-writer/scripts/lint.py +412 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/ai-data-scientist.js +123 -0
- package/package.json +41 -0
- package/pyproject.toml +45 -0
- package/src/ai_chemistry_scientist/__init__.py +0 -0
- package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
- package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
- package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
- package/src/ai_chemistry_scientist/dispatch.py +369 -0
- package/src/ai_chemistry_scientist/docking_score.py +97 -0
- package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
- package/src/ai_chemistry_scientist/evidence.py +41 -0
- package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
- package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
- package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
- package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
- package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
- package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
- package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
- package/src/ai_chemistry_scientist/validation.py +70 -0
- package/src/ai_data_scientist/__init__.py +0 -0
- package/src/ai_data_scientist/analysis_assumptions.py +121 -0
- package/src/ai_data_scientist/anomaly_detection.py +39 -0
- package/src/ai_data_scientist/automl.py +109 -0
- package/src/ai_data_scientist/cleaning.py +56 -0
- package/src/ai_data_scientist/cli.py +90 -0
- package/src/ai_data_scientist/clustering.py +54 -0
- package/src/ai_data_scientist/dashboard.py +33 -0
- package/src/ai_data_scientist/data_definition.py +100 -0
- package/src/ai_data_scientist/data_quality.py +164 -0
- package/src/ai_data_scientist/dataset_validation.py +135 -0
- package/src/ai_data_scientist/dependency_pins.py +60 -0
- package/src/ai_data_scientist/eda.py +82 -0
- package/src/ai_data_scientist/experiment_evaluation.py +635 -0
- package/src/ai_data_scientist/explainability.py +340 -0
- package/src/ai_data_scientist/feature_engineering.py +163 -0
- package/src/ai_data_scientist/gate_config.py +32 -0
- package/src/ai_data_scientist/ingestion.py +127 -0
- package/src/ai_data_scientist/insight_engine.py +180 -0
- package/src/ai_data_scientist/japanese_nlp.py +43 -0
- package/src/ai_data_scientist/jupyter_launcher.py +137 -0
- package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
- package/src/ai_data_scientist/language_router.py +28 -0
- package/src/ai_data_scientist/lifecycle.py +221 -0
- package/src/ai_data_scientist/mcp_gateway.py +113 -0
- package/src/ai_data_scientist/mcp_runtime.py +194 -0
- package/src/ai_data_scientist/mcp_transport.py +53 -0
- package/src/ai_data_scientist/ml_modeling.py +451 -0
- package/src/ai_data_scientist/model_tuning.py +104 -0
- package/src/ai_data_scientist/notebook_audit.py +574 -0
- package/src/ai_data_scientist/project_manager.py +243 -0
- package/src/ai_data_scientist/report_export.py +73 -0
- package/src/ai_data_scientist/sensitivity.py +445 -0
- package/src/ai_data_scientist/signal_analysis.py +201 -0
- package/src/ai_data_scientist/skill_packaging.py +40 -0
- package/src/ai_data_scientist/stats_analysis.py +88 -0
- package/src/ai_data_scientist/text_nlp.py +44 -0
- package/src/ai_data_scientist/timeseries.py +68 -0
- package/src/ai_data_scientist/visualization.py +708 -0
- package/src/ai_genomics_scientist/__init__.py +1 -0
- package/src/ai_genomics_scientist/differential_expression.py +147 -0
- package/src/ai_genomics_scientist/dispatch.py +267 -0
- package/src/ai_genomics_scientist/evidence.py +45 -0
- package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
- package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
- package/src/ai_genomics_scientist/sequence_features.py +111 -0
- package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
- package/src/ai_genomics_scientist/validation.py +83 -0
- package/src/ai_genomics_scientist/variant_effect.py +147 -0
- package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
- package/src/ai_materials_scientist/__init__.py +0 -0
- package/src/ai_materials_scientist/calphad.py +117 -0
- package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
- package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
- package/src/ai_materials_scientist/dispatch.py +100 -0
- package/src/ai_materials_scientist/evidence.py +84 -0
- package/src/ai_materials_scientist/fem.py +279 -0
- package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
- package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
- package/src/ai_materials_scientist/phase_field.py +167 -0
- package/src/ai_materials_scientist/validation.py +70 -0
- package/src/ai_scientist/__init__.py +1 -0
- package/src/ai_scientist/completion_gate.py +15 -0
- package/src/ai_scientist/data_analysis.py +46 -0
- package/src/ai_scientist/evidence_registry.py +99 -0
- package/src/ai_scientist/experimental_design.py +20 -0
- package/src/ai_scientist/language.py +14 -0
- package/src/ai_scientist/latex_renderer.py +41 -0
- package/src/ai_scientist/literature_review.py +37 -0
- package/src/ai_scientist/manifest.py +87 -0
- package/src/ai_scientist/manuscript.py +94 -0
- package/src/ai_scientist/mcp_config.py +76 -0
- package/src/ai_scientist/mcp_external.py +42 -0
- package/src/ai_scientist/mcp_failures.py +23 -0
- package/src/ai_scientist/mcp_gateway.py +38 -0
- package/src/ai_scientist/mcp_managed.py +180 -0
- package/src/ai_scientist/npm_packaging.py +49 -0
- package/src/ai_scientist/orchestrator.py +133 -0
- package/src/ai_scientist/peer_review.py +60 -0
- package/src/ai_scientist/phase_gate.py +74 -0
- package/src/ai_scientist/phase_state.py +230 -0
- package/src/ai_scientist/presentation.py +56 -0
- package/src/ai_scientist/project_config.py +31 -0
- package/src/ai_scientist/project_handle.py +74 -0
- package/src/ai_scientist/reproducibility.py +20 -0
- package/src/ai_scientist/research_planning.py +20 -0
- package/src/ai_scientist/skill_invocation.py +21 -0
- package/src/ai_scientist/tdd_gate.py +99 -0
- package/src/ai_structural_biology_scientist/__init__.py +0 -0
- package/src/ai_structural_biology_scientist/contact_map.py +87 -0
- package/src/ai_structural_biology_scientist/dispatch.py +269 -0
- package/src/ai_structural_biology_scientist/evidence.py +43 -0
- package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
- package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
- package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
- package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
- 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
|
+
)
|