claude-dev-env 8.89.0 → 8.90.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/.agents/skills/pr-visual/README.md +29 -0
- package/.agents/skills/pr-visual/SKILL.md +62 -0
- package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
- package/.agents/skills/pr-visual/assets/viewer.html +147 -0
- package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
- package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
- package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
- package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
- package/.agents/skills/pr-visual/references/evidence.md +9 -0
- package/.agents/skills/pr-visual/references/formats.md +30 -0
- package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
- package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
- package/.agents/skills/pr-visual/scripts/collect.py +111 -0
- package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
- package/.agents/skills/pr-visual/scripts/render.py +133 -0
- package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
- package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
- package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
- package/.agents/skills/pr-visual/scripts/verify.py +218 -0
- package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
- package/bin/codex-skill-load-block.mjs +0 -25
- package/bin/codex-skill-load-block.test.mjs +5 -17
- package/bin/codex-trimmed-sol.test.mjs +0 -1
- package/bin/cursor-native-hooks.test.mjs +3 -3
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-deepstack-plugin.mjs +42 -25
- package/bin/install-deepstack-plugin.test.mjs +37 -11
- package/bin/install-pstack-plugin.mjs +58 -11
- package/bin/install-pstack-plugin.test.mjs +54 -21
- package/bin/install.agents-home.test.mjs +0 -1
- package/bin/install.codex-rules.test.mjs +0 -2
- package/bin/install.cursor-rules.test.mjs +90 -26
- package/bin/install.mjs +30 -36
- package/bin/install.pr-visual.test.mjs +40 -0
- package/bin/install.prompt-context.test.mjs +3 -4
- package/bin/install.prune.test.mjs +0 -1
- package/bin/install.question-presentation.test.mjs +0 -4
- package/bin/install.settings-defaults.test.mjs +0 -1
- package/bin/install.shared-settings.test.mjs +0 -1
- package/bin/install.test.mjs +14 -44
- package/bin/install.transaction.test.mjs +0 -8
- package/bin/install.uninstall-transaction.test.mjs +0 -2
- package/bin/install.update-keeps-hooks.test.mjs +0 -1
- package/docs/BDD_SCENARIO_QUALITY.md +1 -1
- package/docs/BDD_TEST_LAYOUT.md +1 -1
- package/docs/CODE_RULES.md +1 -1
- package/docs/rule-guides/deepstack-wording.md +3 -5
- package/docs/rule-guides/session-title.md +5 -4
- package/hooks/blocking/session_title_turn_gate.py +287 -0
- package/hooks/blocking/test_session_title_format_gate.py +5 -0
- package/hooks/blocking/test_session_title_turn_gate.py +264 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/features/session-context.md +0 -4
- package/hooks/hooks.json +5 -5
- package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
- package/hooks/hooks_constants/session_title_constants.py +41 -3
- package/hooks/lifecycle/nested_project_hooks.py +2 -2
- package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
- package/hooks/session/compaction_user_messages.py +2 -2
- package/package.json +2 -2
- package/rules/deepstack-wording.md +3 -3
- package/scripts/ci/install-playtest.mjs +58 -314
- package/scripts/ci/install-playtest.test.mjs +24 -6
- package/scripts/policy_lint/adapter_configuration.py +1 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_deepstack_review_criterion.py +2 -3
- package/scripts/test_deepstack_wording_rule.py +6 -4
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/.agents/skills/deepen/SKILL.md +0 -26
- package/commands/deepen.md +0 -14
- package/docs/decisions/deepstack-gate.md +0 -32
- package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
- package/hooks/session/deepen_default_rewrite.py +0 -128
- package/hooks/session/deepen_first_prompt.py +0 -383
- package/hooks/session/deepen_role_gate.py +0 -171
- package/hooks/session/deepen_turn_marker.py +0 -75
- package/hooks/session/fixtures/deepstack_replay.json +0 -15
- package/hooks/session/grade_deepstack_replay.py +0 -78
- package/hooks/session/test_deepen_default_rewrite.py +0 -90
- package/hooks/session/test_deepen_first_prompt.py +0 -452
- package/hooks/session/test_deepen_role_gate.py +0 -83
- package/hooks/session/test_deepen_turn_marker.py +0 -59
- package/hooks/session/test_grade_deepstack_replay.py +0 -47
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Validate evidence-backed diagram scenes and render a portable HTML visual."""
|
|
2
|
+
import argparse
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from metrics import prepare_chart, prepare_embedded_charts
|
|
10
|
+
from visual_config.config import constants as c
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RenderRunFatal(ValueError):
|
|
14
|
+
"""Stop rendering when scene structure or evidence is invalid."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_drilldown(node: dict) -> None:
|
|
18
|
+
flow = node.get("drilldown")
|
|
19
|
+
if not flow or not flow.get("title") or not c.MINIMUM_DRILLDOWN_STEPS <= len(flow.get("steps", [])) <= c.MAXIMUM_DRILLDOWN_STEPS:
|
|
20
|
+
raise RenderRunFatal("Every node needs a titled drilldown with two to six steps.")
|
|
21
|
+
for each_step in flow["steps"]:
|
|
22
|
+
if not each_step.get("label") or not each_step.get("icon"):
|
|
23
|
+
raise RenderRunFatal("Every drilldown step needs a label and icon.")
|
|
24
|
+
for each_edge in flow.get("edges", []):
|
|
25
|
+
if not 0 <= each_edge["from"] < len(flow["steps"]) or not 0 <= each_edge["to"] < len(flow["steps"]) or each_edge["from"] == each_edge["to"]:
|
|
26
|
+
raise RenderRunFatal("Invalid drilldown edge.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_graph(scene: dict) -> None:
|
|
30
|
+
node_ids = [each_node["id"] for each_node in scene["nodes"]]
|
|
31
|
+
if len(set(node_ids)) != len(node_ids):
|
|
32
|
+
raise RenderRunFatal("Duplicate diagram node IDs.")
|
|
33
|
+
for each_edge in scene["edges"]:
|
|
34
|
+
if each_edge["from"] not in node_ids or each_edge["to"] not in node_ids:
|
|
35
|
+
raise RenderRunFatal("Edge refers to a missing node.")
|
|
36
|
+
if not re.fullmatch(c.SVG_PATH_PATTERN, each_edge["path"]):
|
|
37
|
+
raise RenderRunFatal("Invalid SVG path.")
|
|
38
|
+
for each_node in scene["nodes"]:
|
|
39
|
+
if not scene.get("chart") and (not c.MINIMUM_NODE_X <= each_node["x"] <= c.MAXIMUM_NODE_X or not c.MINIMUM_NODE_Y <= each_node["y"] <= c.MAXIMUM_NODE_Y):
|
|
40
|
+
raise RenderRunFatal("Node lies outside diagram bounds.")
|
|
41
|
+
if not each_node.get("sources"):
|
|
42
|
+
raise RenderRunFatal("Every node needs source evidence.")
|
|
43
|
+
_validate_drilldown(each_node)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _source_evidence(source: dict, snapshot: dict, snapshot_path: Path) -> None:
|
|
47
|
+
side = source.get("side", c.HEAD_SIDE)
|
|
48
|
+
if side not in c.ALL_SOURCE_SIDES:
|
|
49
|
+
raise RenderRunFatal("Source side must be head or base.")
|
|
50
|
+
source_root = snapshot_path.parent / side
|
|
51
|
+
source_path = (source_root / source["path"]).resolve()
|
|
52
|
+
if not source_path.is_relative_to(source_root):
|
|
53
|
+
raise RenderRunFatal("Source path leaves snapshot.")
|
|
54
|
+
all_lines = source_path.read_text(encoding="utf-8-sig").splitlines()
|
|
55
|
+
first, last = source["line"], source.get("end", source["line"])
|
|
56
|
+
if not 1 <= first <= last <= len(all_lines):
|
|
57
|
+
raise RenderRunFatal("Invalid source line range.")
|
|
58
|
+
source["excerpt"] = c.NEWLINE.join(c.SOURCE_LINE.format(number=each_index + first, line=each_line) for each_index, each_line in enumerate(all_lines[first - 1:last]))
|
|
59
|
+
revision = snapshot.get("diff_base", snapshot[c.BASE_SIDE]) if side == c.BASE_SIDE else snapshot[c.HEAD_SIDE]
|
|
60
|
+
source["url"] = c.SOURCE_LINK.format(repo=snapshot["repo"], revision=revision, path=quote(source["path"], safe="/"), first=first, last=last)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _prepare_sources(scene: dict, snapshot: dict, snapshot_path: Path) -> None:
|
|
64
|
+
for each_node in scene["nodes"]:
|
|
65
|
+
for each_source in each_node["sources"]:
|
|
66
|
+
_source_evidence(each_source, snapshot, snapshot_path)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def prepare(path: Path) -> dict:
|
|
70
|
+
"""Bind a diagram to captured source and populate immutable evidence links.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
path: Scene JSON path with a relative snapshot reference.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The scene with PR identity, original summary, and source excerpts.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
RenderRunFatal: The scene or an evidence reference is invalid.
|
|
80
|
+
"""
|
|
81
|
+
scene = json.loads(path.read_text(encoding="utf-8"))
|
|
82
|
+
snapshot_path = (path.parent / scene["snapshot"]).resolve()
|
|
83
|
+
snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
|
|
84
|
+
scene["pr"] = {each_key: snapshot[each_key] for each_key in ("number", "url", "repo", "state", "head", "base", "observed_at")}
|
|
85
|
+
scene["pr"].update(draft=snapshot["metadata"]["draft"], merged=snapshot["metadata"].get("merged", False), changed_files=len(snapshot["files"]), title=snapshot["metadata"]["title"])
|
|
86
|
+
scene["summary"] = snapshot["metadata"]["body"] or "No PR description."
|
|
87
|
+
_validate_graph(scene)
|
|
88
|
+
_prepare_sources(scene, snapshot, snapshot_path)
|
|
89
|
+
prepare_chart(scene, snapshot_path)
|
|
90
|
+
prepare_embedded_charts(scene, snapshot_path)
|
|
91
|
+
return scene
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _omit_private_payload(scene: dict) -> None:
|
|
95
|
+
scene.pop("summary", None)
|
|
96
|
+
for each_node in scene["nodes"]:
|
|
97
|
+
for each_source in each_node["sources"]:
|
|
98
|
+
each_source.pop("excerpt", None)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def render(all_scenes: list[dict], artifact_path: Path, is_links_only: bool = False) -> None:
|
|
102
|
+
"""Write a standalone interactive diagram or tabbed gallery.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
all_scenes: Prepared diagrams with captured source evidence.
|
|
106
|
+
artifact_path: Destination HTML file.
|
|
107
|
+
is_links_only: Omit original descriptions and code excerpts.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
RenderRunFatal: The HTML exceeds the host size limit.
|
|
111
|
+
"""
|
|
112
|
+
all_scenes = copy.deepcopy(all_scenes)
|
|
113
|
+
if is_links_only:
|
|
114
|
+
for each_scene in all_scenes:
|
|
115
|
+
_omit_private_payload(each_scene)
|
|
116
|
+
payload = json.dumps(all_scenes, ensure_ascii=False).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
|
|
117
|
+
template = c.VIEWER_PATH.read_text(encoding="utf-8")
|
|
118
|
+
icons = c.ICONS_PATH.read_text(encoding="utf-8")
|
|
119
|
+
rendered_html = template.replace("__ICONS__", icons).replace("__SCENES__", payload)
|
|
120
|
+
if len(rendered_html.encode()) > c.MAXIMUM_HTML_BYTES:
|
|
121
|
+
raise RenderRunFatal("HTML exceeds the host visualization limit.")
|
|
122
|
+
artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
artifact_path.write_text(rendered_html, encoding="utf-8")
|
|
124
|
+
print(f"{artifact_path}: {len(all_scenes)} diagrams, {len(rendered_html.encode())} bytes")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
129
|
+
parser.add_argument("scenes", type=Path, nargs="+")
|
|
130
|
+
parser.add_argument("--out", type=Path, required=True)
|
|
131
|
+
parser.add_argument("--links-only", action="store_true", help="Omit original descriptions and code excerpts for hosted display.")
|
|
132
|
+
arguments = parser.parse_args()
|
|
133
|
+
render([prepare(each_path) for each_path in arguments.scenes], arguments.out, arguments.links_only)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Check immutable capture and stale-head rejection with synthetic GitHub data."""
|
|
2
|
+
import base64
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import Mock
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
13
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
import collect
|
|
16
|
+
|
|
17
|
+
REPOSITORY = "example/project"
|
|
18
|
+
PR_URL = "https://github.com/example/project/pull/1"
|
|
19
|
+
HEAD_SHA = "b" * 40
|
|
20
|
+
BASE_SHA = "c" * 40
|
|
21
|
+
MERGE_BASE_SHA = "a" * 40
|
|
22
|
+
CONTENT = b"print('captured')\n"
|
|
23
|
+
METADATA = {"head": {"sha": HEAD_SHA, "repo": {"full_name": REPOSITORY}}, "base": {"sha": BASE_SHA, "repo": {"full_name": REPOSITORY}}, "changed_files": 1, "state": "open"}
|
|
24
|
+
CHANGED_FILES = [{"filename": "handler.py", "status": "modified", "patch": "fixture patch"}]
|
|
25
|
+
BLOB = {"encoding": "base64", "content": base64.b64encode(CONTENT).decode()}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_capture_uses_merge_base_and_hashes_saved_bytes(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
29
|
+
"""Both saved sides match the immutable revisions used by the diff."""
|
|
30
|
+
github = Mock(side_effect=[METADATA, {"merge_base_commit": {"sha": MERGE_BASE_SHA}}, CHANGED_FILES, BLOB, BLOB, METADATA])
|
|
31
|
+
monkeypatch.setattr(collect, "_api", github)
|
|
32
|
+
collect.collect(PR_URL, tmp_path)
|
|
33
|
+
destination = tmp_path / "pr-1" / HEAD_SHA
|
|
34
|
+
snapshot = json.loads((destination / "snapshot.json").read_text())
|
|
35
|
+
assert snapshot["diff_base"] == MERGE_BASE_SHA
|
|
36
|
+
for each_side in ("base", "head"):
|
|
37
|
+
assert (destination / each_side / "handler.py").read_bytes() == CONTENT
|
|
38
|
+
assert snapshot["files"][0]["captures"][each_side]["sha256"] == hashlib.sha256(CONTENT).hexdigest()
|
|
39
|
+
all_endpoints = [each_call.args[0] for each_call in github.call_args_list]
|
|
40
|
+
assert any(each_endpoint.endswith(f"?ref={MERGE_BASE_SHA}") for each_endpoint in all_endpoints)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_moved_head_never_saves_a_complete_snapshot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
44
|
+
"""A changed PR head requires another capture before rendering."""
|
|
45
|
+
moved = copy.deepcopy(METADATA)
|
|
46
|
+
moved["head"]["sha"] = "d" * 40
|
|
47
|
+
github = Mock(side_effect=[METADATA, {"merge_base_commit": {"sha": MERGE_BASE_SHA}}, CHANGED_FILES, BLOB, BLOB, moved])
|
|
48
|
+
monkeypatch.setattr(collect, "_api", github)
|
|
49
|
+
with pytest.raises(collect.CaptureRunFatal, match="changed during capture"):
|
|
50
|
+
collect.collect(PR_URL, tmp_path)
|
|
51
|
+
assert not list(tmp_path.rglob("snapshot.json"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_incomplete_changed_file_list_fails(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
55
|
+
"""A truncated file listing cannot imply complete coverage."""
|
|
56
|
+
monkeypatch.setattr(collect, "_api", Mock(return_value=[]))
|
|
57
|
+
with pytest.raises(collect.CaptureRunFatal, match="incomplete"):
|
|
58
|
+
collect._changed_files(REPOSITORY, "1", 1)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Verify embedded chart provenance and passive Python literal extraction."""
|
|
2
|
+
import copy
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
9
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
10
|
+
|
|
11
|
+
from metrics import MetricRunFatal, _coordinate, prepare_embedded_charts
|
|
12
|
+
from render import prepare
|
|
13
|
+
|
|
14
|
+
FIXTURES = SCRIPTS_DIRECTORY.parents[0] / "fixtures"
|
|
15
|
+
ALL_LITERAL_CASES = (("assert metric == 2.0", {}, 2.0), ("Pair(90, 180)", {"argument": 1}, 180), ("peaks = (1, 2, 10)", {"element": 2}, 10))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.parametrize("source_text,selector,expected", ALL_LITERAL_CASES)
|
|
19
|
+
def test_numeric_literals_are_read_without_executing_source(source_text: str, selector: dict, expected: float, tmp_path: Path) -> None:
|
|
20
|
+
"""Executable statements in the source remain inert during chart preparation."""
|
|
21
|
+
source_root = tmp_path / "head"
|
|
22
|
+
source_root.mkdir()
|
|
23
|
+
(source_root / "example.py").write_text('raise RuntimeError("Source must remain inert")\n' + source_text + "\n")
|
|
24
|
+
node = {"sources": [{"path": "example.py", "line": 2}]}
|
|
25
|
+
assert _coordinate({"source": 0, "python_line": 2, **selector}, node, tmp_path / "snapshot.json") == expected
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_step_chart_uses_reviewed_values() -> None:
|
|
29
|
+
"""Step cards and expansions use captured fixture measurements."""
|
|
30
|
+
scene = prepare(FIXTURES / "integrated.scene.json")
|
|
31
|
+
assert [each_point["values"]["y"] for each_point in scene["nodes"][0]["chart"]["points"]] == [12, 20, 17]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_ungrounded_embedded_numbers_are_rejected() -> None:
|
|
35
|
+
"""Author-entered chart coordinates cannot replace source references."""
|
|
36
|
+
scene = copy.deepcopy(prepare(FIXTURES / "integrated.scene.json"))
|
|
37
|
+
scene["nodes"][0]["chart"]["points"][0].pop("metrics")
|
|
38
|
+
with pytest.raises(MetricRunFatal):
|
|
39
|
+
prepare_embedded_charts(scene, FIXTURES / "snapshot.json")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_python_numbers_outside_cited_lines_are_rejected(tmp_path: Path) -> None:
|
|
43
|
+
"""A measurement must be covered by its displayed evidence link."""
|
|
44
|
+
(tmp_path / "head").mkdir()
|
|
45
|
+
(tmp_path / "head/example.py").write_text("assert metric == 2\nassert metric == 10\n")
|
|
46
|
+
with pytest.raises(MetricRunFatal, match="outside"):
|
|
47
|
+
_coordinate({"source": 0, "python_line": 2}, {"sources": [{"path": "example.py", "line": 1}]}, tmp_path / "snapshot.json")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Exercise numerical provenance, evidence boundaries, and portable rendering."""
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
10
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
11
|
+
from metrics import MetricRunFatal, prepare_chart
|
|
12
|
+
from render import RenderRunFatal, _source_evidence, prepare, render
|
|
13
|
+
|
|
14
|
+
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures"
|
|
15
|
+
FORMATS = ("flow", "bar", "line", "scatter")
|
|
16
|
+
EXPECTED_Y = [12, 20, 17]
|
|
17
|
+
INVALID_VALUES = (True, None, "12", float("nan"), float("inf"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize("kind", FORMATS)
|
|
21
|
+
def test_render_source_backed_format(kind: str, tmp_path: Path) -> None:
|
|
22
|
+
"""Every format retains immutable evidence and interactive detail data."""
|
|
23
|
+
scene = prepare(FIXTURES / f"{kind}.scene.json")
|
|
24
|
+
artifact_path = tmp_path / "visual.html"
|
|
25
|
+
render([scene], artifact_path)
|
|
26
|
+
assert "__SCENES__" not in artifact_path.read_text()
|
|
27
|
+
assert scene["nodes"][0]["sources"][0]["url"].endswith("/metrics.json#L4-L5")
|
|
28
|
+
if kind != "flow":
|
|
29
|
+
assert [each_node["values"]["y"] for each_node in scene["nodes"]] == EXPECTED_Y
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("value", INVALID_VALUES)
|
|
33
|
+
def test_chart_rejects_nonnumeric_measurements(value: object, tmp_path: Path) -> None:
|
|
34
|
+
"""Missing and nonnumeric observations cannot become plotted values."""
|
|
35
|
+
scene = json.loads((FIXTURES / "bar.scene.json").read_text())
|
|
36
|
+
(tmp_path / "head").mkdir()
|
|
37
|
+
(tmp_path / "head/metrics.json").write_text(json.dumps({"samples": [{"duration_ms": value}]}))
|
|
38
|
+
with pytest.raises(MetricRunFatal):
|
|
39
|
+
prepare_chart(scene, tmp_path / "snapshot.json")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_missing_pointer_fails_instead_of_zero() -> None:
|
|
43
|
+
"""A missing observation cannot imply a zero measurement."""
|
|
44
|
+
scene = prepare(FIXTURES / "bar.scene.json")
|
|
45
|
+
scene["nodes"][0]["metrics"]["y"]["pointer"] = "/missing"
|
|
46
|
+
with pytest.raises(MetricRunFatal):
|
|
47
|
+
prepare_chart(scene, FIXTURES / "snapshot.json")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize("source", [{"path": "../../outside", "line": 1}, {"path": "metrics.json", "line": 0}, {"path": "metrics.json", "line": 1, "side": "other"}])
|
|
51
|
+
def test_source_boundary_rejects_invalid_references(source: dict) -> None:
|
|
52
|
+
"""Evidence must remain inside a captured side and a valid line range."""
|
|
53
|
+
snapshot = json.loads((FIXTURES / "snapshot.json").read_text())
|
|
54
|
+
with pytest.raises(RenderRunFatal):
|
|
55
|
+
_source_evidence(source, snapshot, FIXTURES / "snapshot.json")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_links_only_is_private_and_does_not_mutate_scene(tmp_path: Path) -> None:
|
|
59
|
+
"""Hosted output drops payload excerpts without modifying the prepared scene."""
|
|
60
|
+
scene = prepare(FIXTURES / "flow.scene.json")
|
|
61
|
+
original = copy.deepcopy(scene)
|
|
62
|
+
artifact_path = tmp_path / "visual.html"
|
|
63
|
+
render([scene], artifact_path, is_links_only=True)
|
|
64
|
+
payload = artifact_path.read_text().split("data-scenes>", 1)[1].split("</script>", 1)[0]
|
|
65
|
+
public = json.loads(payload)[0]
|
|
66
|
+
assert "summary" not in public
|
|
67
|
+
assert all("excerpt" not in each_source for each_node in public["nodes"] for each_source in each_node["sources"])
|
|
68
|
+
assert scene == original
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_embedded_text_cannot_close_script(tmp_path: Path) -> None:
|
|
72
|
+
"""Captured prose remains data even when it contains HTML closing tags."""
|
|
73
|
+
scene = prepare(FIXTURES / "flow.scene.json")
|
|
74
|
+
scene["summary"] = "</script><script>window.untrusted=1</script>"
|
|
75
|
+
artifact_path = tmp_path / "visual.html"
|
|
76
|
+
render([scene], artifact_path)
|
|
77
|
+
assert scene["summary"] not in artifact_path.read_text()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Exercise docked panels, mini-diagrams, source disclosures, and isolation."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from playwright.sync_api import Locator, Page, sync_playwright
|
|
9
|
+
from visual_config.config import constants as c
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _panel_layout(root: Locator, is_mobile: bool = False) -> None:
|
|
13
|
+
stage_box = root.locator(c.STAGE_SELECTOR).bounding_box()
|
|
14
|
+
panel_box = root.locator(c.DRAWER_SELECTOR).bounding_box()
|
|
15
|
+
assert stage_box is not None and panel_box is not None
|
|
16
|
+
if is_mobile:
|
|
17
|
+
assert panel_box["y"] + panel_box["height"] <= stage_box["y"] + 1
|
|
18
|
+
else:
|
|
19
|
+
assert stage_box["x"] + stage_box["width"] <= panel_box["x"] + 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _check_node(root: Locator, node: dict) -> None:
|
|
23
|
+
assert root.locator(c.DRAWER_SELECTOR).is_visible()
|
|
24
|
+
assert root.locator(".drawer .detail-map").is_visible()
|
|
25
|
+
assert root.locator(".drawer .detail-map use").count() == len(node["drilldown"]["steps"])
|
|
26
|
+
assert not root.locator(c.EXACT_SELECTOR).evaluate(c.DISCLOSURE_OPEN_EXPRESSION)
|
|
27
|
+
_panel_layout(root)
|
|
28
|
+
assert root.locator(".drawer [data-embedded-chart]").count() == int(bool(node.get("chart")))
|
|
29
|
+
root.locator(c.DISCLOSURE_SELECTOR).click()
|
|
30
|
+
assert root.locator(".exact-source a").count() == len(node["sources"])
|
|
31
|
+
assert root.locator(".exact-source p").count() == len(node["detail"])
|
|
32
|
+
assert (root.locator(".exact-source pre").count() > 0) == bool(node["sources"][0].get("excerpt"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _navigate_related(root: Locator) -> None:
|
|
36
|
+
jumps = root.locator(c.JUMP_SELECTOR)
|
|
37
|
+
if jumps.count():
|
|
38
|
+
target = jumps.first.get_attribute("data-detail-target")
|
|
39
|
+
jumps.first.click()
|
|
40
|
+
assert root.locator(c.NODE_TARGET_SELECTOR.format(target=target)).get_attribute("aria-expanded") == "true"
|
|
41
|
+
assert not root.locator(c.EXACT_SELECTOR).evaluate(c.DISCLOSURE_OPEN_EXPRESSION)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _toolbar_details(root: Locator, node_count: int) -> None:
|
|
45
|
+
for each_label in c.ALL_TOOLBAR_DETAIL_LABELS:
|
|
46
|
+
root.get_by_role("button", name=each_label, exact=True).click()
|
|
47
|
+
assert root.locator(".drawer .detail-map").is_visible()
|
|
48
|
+
assert root.locator(c.JUMP_SELECTOR).count() == node_count
|
|
49
|
+
root.locator(c.DISCLOSURE_SELECTOR).click()
|
|
50
|
+
assert root.locator(".exact-source p").count() > 0
|
|
51
|
+
root.get_by_role("button", name=c.CLOSE_BUTTON_LABEL).click()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _keyboard_and_zoom(page: Page, root: Locator) -> None:
|
|
55
|
+
first_node = root.locator(c.NODE_SELECTOR).first
|
|
56
|
+
first_node.focus()
|
|
57
|
+
page.keyboard.press("Enter")
|
|
58
|
+
page.keyboard.press("Escape")
|
|
59
|
+
assert first_node.evaluate("(n)=>n===document.activeElement")
|
|
60
|
+
root.get_by_role("button", name=c.ZOOM_BUTTON_LABEL, exact=True).click()
|
|
61
|
+
assert root.locator(c.CANVAS_SELECTOR).evaluate(c.CANVAS_WIDTH_EXPRESSION)
|
|
62
|
+
root.get_by_role("button", name="Fit", exact=True).click()
|
|
63
|
+
assert root.locator(c.CANVAS_SELECTOR).evaluate(c.CANVAS_WIDTH_EXPRESSION) == ""
|
|
64
|
+
assert not root.locator(".canvas svg text").evaluate_all(c.OVERFLOW_EXPRESSION)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _scene_screenshots(page: Page, root: Locator, scene: dict, proof_directory: Path, scene_index: int) -> None:
|
|
68
|
+
page.screenshot(path=str(proof_directory / c.PR_SCREENSHOT_NAME.format(number=scene["pr"]["number"], index=scene_index)), full_page=True)
|
|
69
|
+
all_nodes = root.locator(c.NODE_SELECTOR)
|
|
70
|
+
all_nodes.first.click()
|
|
71
|
+
page.screenshot(path=str(proof_directory / c.DETAIL_SCREENSHOT_NAME.format(number=scene["pr"]["number"], index=scene_index)), full_page=True)
|
|
72
|
+
if all_nodes.count() > 1:
|
|
73
|
+
all_nodes.nth(1).click()
|
|
74
|
+
assert root.locator(c.DETAIL_TITLE_SELECTOR).inner_text() == c.SPACE_SEPARATOR.join(scene["nodes"][1]["label"])
|
|
75
|
+
page.keyboard.press("Escape")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _verify_scene(page: Page, root: Locator, scene: dict, scene_index: int, proof_directory: Path) -> dict:
|
|
79
|
+
root.get_by_role("tab").nth(scene_index).click()
|
|
80
|
+
all_nodes = root.locator(c.NODE_SELECTOR)
|
|
81
|
+
for each_index, each_node in enumerate(scene["nodes"]):
|
|
82
|
+
all_nodes.nth(each_index).click()
|
|
83
|
+
_check_node(root, each_node)
|
|
84
|
+
_navigate_related(root)
|
|
85
|
+
page.keyboard.press("Escape")
|
|
86
|
+
assert root.locator(c.DRAWER_SELECTOR).is_hidden()
|
|
87
|
+
assert root.locator(c.SELECTED_NODE_SELECTOR).count() == 0
|
|
88
|
+
_toolbar_details(root, len(scene["nodes"]))
|
|
89
|
+
_keyboard_and_zoom(page, root)
|
|
90
|
+
_scene_screenshots(page, root, scene, proof_directory, scene_index)
|
|
91
|
+
return {"pr": scene["pr"]["number"], "nodes_checked": all_nodes.count(), "docked_panel": "pass", "mini_diagrams": "pass", "related_navigation": "pass", "collapsed_sources": "pass", "keyboard_and_zoom": "pass"}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _dashboard(page: Page, root: Locator, all_scenes: list[dict], proof_directory: Path) -> None:
|
|
95
|
+
tabs = root.get_by_role("tab")
|
|
96
|
+
assert tabs.count() == len(all_scenes)
|
|
97
|
+
assert root.locator(".tabs svg").count() == 0
|
|
98
|
+
assert root.locator(c.DASHBOARD_SELECTOR).bounding_box()["height"] <= c.MAXIMUM_DASHBOARD_HEIGHT
|
|
99
|
+
for each_index, each_scene in enumerate(all_scenes):
|
|
100
|
+
tabs.nth(each_index).click()
|
|
101
|
+
assert tabs.nth(each_index).get_attribute("aria-selected") == "true"
|
|
102
|
+
assert root.locator(c.DRAWER_SELECTOR).is_hidden()
|
|
103
|
+
assert root.locator("[data-step-chart]").count() == (0 if each_scene.get("chart") else sum(bool(each_node.get("chart")) for each_node in each_scene["nodes"]))
|
|
104
|
+
tabs.first.click()
|
|
105
|
+
root.screenshot(path=str(proof_directory / c.DASHBOARD_SCREENSHOT_NAME))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _verify_mobile(page: Page, root: Locator, proof_directory: Path) -> None:
|
|
109
|
+
all_tabs = root.get_by_role("tab")
|
|
110
|
+
all_tabs.first.focus()
|
|
111
|
+
if all_tabs.count() > 1:
|
|
112
|
+
page.keyboard.press("ArrowRight")
|
|
113
|
+
assert all_tabs.nth(1).get_attribute("aria-selected") == "true"
|
|
114
|
+
page.set_viewport_size(c.ALL_MOBILE_VIEWPORT)
|
|
115
|
+
root.locator(c.NODE_SELECTOR).first.click()
|
|
116
|
+
_panel_layout(root, is_mobile=True)
|
|
117
|
+
assert root.locator(c.STAGE_SELECTOR).evaluate(c.SCROLLABLE_EXPRESSION)
|
|
118
|
+
root.locator(c.STAGE_SELECTOR).evaluate(c.SCROLL_MAP_EXPRESSION)
|
|
119
|
+
assert root.locator(c.STAGE_SELECTOR).evaluate(c.READ_SCROLL_EXPRESSION) > 0
|
|
120
|
+
page.screenshot(path=str(proof_directory / c.MOBILE_SCREENSHOT_NAME), full_page=True)
|
|
121
|
+
page.keyboard.press("Escape")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _mount_two_widgets(page: Page, source: str) -> str:
|
|
125
|
+
page.set_viewport_size(c.ALL_DESKTOP_VIEWPORT)
|
|
126
|
+
page.set_content(c.HOST_FIXTURE_HTML)
|
|
127
|
+
background = page.locator(c.HOST_FIXTURE_SELECTOR).evaluate(c.BACKGROUND_EXPRESSION)
|
|
128
|
+
fragment = re.search(c.MAIN_FRAGMENT_PATTERN, source, re.DOTALL).group()
|
|
129
|
+
style = re.search(c.STYLE_FRAGMENT_PATTERN, source, re.DOTALL).group()
|
|
130
|
+
for each_instance in range(2):
|
|
131
|
+
page.evaluate(c.MOUNT_WIDGET_EXPRESSION, style + fragment)
|
|
132
|
+
return background
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _verify_isolation(page: Page, source: str) -> None:
|
|
136
|
+
background = _mount_two_widgets(page, source)
|
|
137
|
+
all_roots = page.locator(c.ROOT_SELECTOR)
|
|
138
|
+
all_roots.nth(0).locator(c.NODE_SELECTOR).first.click()
|
|
139
|
+
assert all_roots.nth(0).locator(c.DRAWER_SELECTOR).is_visible()
|
|
140
|
+
assert all_roots.nth(1).locator(c.DRAWER_SELECTOR).is_hidden()
|
|
141
|
+
all_roots.nth(1).get_by_role("button", name=c.ZOOM_BUTTON_LABEL, exact=True).click()
|
|
142
|
+
assert all_roots.nth(0).locator(c.CANVAS_SELECTOR).evaluate(c.CANVAS_WIDTH_EXPRESSION) == ""
|
|
143
|
+
assert all_roots.nth(0).get_by_role("tab").first.get_attribute("aria-selected") == "true"
|
|
144
|
+
assert page.locator(c.HOST_FIXTURE_SELECTOR).evaluate(c.BACKGROUND_EXPRESSION) == background
|
|
145
|
+
all_ids = page.locator("[id]").evaluate_all(c.READ_IDS_EXPRESSION)
|
|
146
|
+
assert len(all_ids) == len(set(all_ids)), "Widget instances share SVG or control IDs"
|
|
147
|
+
_verify_host_contrast(page, all_roots.nth(0))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _luminance(color: str) -> float:
|
|
151
|
+
channels = [int(each_component) / c.RGB_CHANNEL_MAXIMUM for each_component in re.findall(c.RGB_COMPONENT_PATTERN, color)]
|
|
152
|
+
linear = [each_channel / c.SRGB_LINEAR_DIVISOR if each_channel <= c.SRGB_LINEAR_THRESHOLD else ((each_channel + c.SRGB_OFFSET) / c.SRGB_SCALE) ** c.SRGB_EXPONENT for each_channel in channels]
|
|
153
|
+
return sum(each_channel * each_weight for each_channel, each_weight in zip(linear, c.ALL_LUMINANCE_WEIGHTS, strict=True))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _verify_host_contrast(page: Page, root: Locator) -> None:
|
|
157
|
+
page.add_style_tag(content=c.HOST_THEME_STYLES)
|
|
158
|
+
assert page.locator("body").evaluate(c.CONTROL_STYLES_EXPRESSION)["scheme"] == "dark"
|
|
159
|
+
assert page.locator(c.HOST_FIXTURE_SELECTOR).evaluate(c.CONTROL_STYLES_EXPRESSION)["color"] == c.WHITE_COLOR
|
|
160
|
+
assert root.evaluate(c.CONTROL_STYLES_EXPRESSION)["scheme"] == "light"
|
|
161
|
+
all_controls = root.locator("button")
|
|
162
|
+
for each_index in range(all_controls.count()):
|
|
163
|
+
control = all_controls.nth(each_index)
|
|
164
|
+
styles = control.evaluate(c.CONTROL_STYLES_EXPRESSION)
|
|
165
|
+
assert styles["opacity"] == "1" and styles["shadow"] == "none"
|
|
166
|
+
foreground, background = sorted((_luminance(styles["color"]), _luminance(styles["background"])))
|
|
167
|
+
assert (background + c.CONTRAST_OFFSET) / (foreground + c.CONTRAST_OFFSET) >= c.MINIMUM_TEXT_CONTRAST
|
|
168
|
+
children = control.locator("span")
|
|
169
|
+
for each_child in range(children.count()):
|
|
170
|
+
assert children.nth(each_child).evaluate(c.CONTROL_STYLES_EXPRESSION)["color"] == styles["color"]
|
|
171
|
+
close = root.get_by_role("button", name=c.CLOSE_BUTTON_LABEL)
|
|
172
|
+
assert close.evaluate(c.CONTROL_STYLES_EXPRESSION)["color"] == c.TEAL_COLOR
|
|
173
|
+
assert close.evaluate(c.CONTROL_STYLES_EXPRESSION)["border"] == c.CONTROL_BORDER_COLOR
|
|
174
|
+
page.keyboard.press("Tab")
|
|
175
|
+
close.focus()
|
|
176
|
+
assert close.evaluate(c.CONTROL_STYLES_EXPRESSION)["outline"] == c.TEAL_COLOR
|
|
177
|
+
root.get_by_role("button", name="Fit", exact=True).focus()
|
|
178
|
+
assert root.get_by_role("button", name="Fit", exact=True).evaluate(c.CONTROL_STYLES_EXPRESSION)["outline"] == c.LIME_COLOR
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def verify(path: Path, proof_directory: Path, browser_path: str | None = None, should_disable_sandbox: bool = False) -> None:
|
|
182
|
+
"""Exercise the rendered artifact in desktop, mobile, and shared-host layouts.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
path: Rendered HTML artifact.
|
|
186
|
+
proof_directory: Directory receiving screenshots and the verification report.
|
|
187
|
+
browser_path: Optional Chromium executable; otherwise detect or use Playwright.
|
|
188
|
+
should_disable_sandbox: Disable Chromium sandbox only in an isolated container.
|
|
189
|
+
"""
|
|
190
|
+
proof_directory.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
with sync_playwright() as driver:
|
|
192
|
+
browser_path = browser_path or shutil.which("chromium") or shutil.which("google-chrome")
|
|
193
|
+
browser = driver.chromium.launch(executable_path=browser_path, headless=True, args=["--no-sandbox"] if should_disable_sandbox else [])
|
|
194
|
+
page = browser.new_page(viewport=c.ALL_DESKTOP_VIEWPORT, device_scale_factor=1)
|
|
195
|
+
all_errors = []
|
|
196
|
+
page.on("pageerror", lambda each_error: all_errors.append(str(each_error)))
|
|
197
|
+
page.set_content(path.read_text(encoding="utf-8"))
|
|
198
|
+
root = page.locator(c.ROOT_SELECTOR)
|
|
199
|
+
all_scenes = root.locator(c.DATA_SCENES_SELECTOR).evaluate(c.READ_SCENES_EXPRESSION)
|
|
200
|
+
_dashboard(page, root, all_scenes, proof_directory)
|
|
201
|
+
all_verdicts = [_verify_scene(page, root, each_scene, each_index, proof_directory) for each_index, each_scene in enumerate(all_scenes)]
|
|
202
|
+
_verify_mobile(page, root, proof_directory)
|
|
203
|
+
_verify_isolation(page, path.read_text(encoding="utf-8"))
|
|
204
|
+
assert not all_errors, all_errors
|
|
205
|
+
browser.close()
|
|
206
|
+
report = {"input": str(path), "diagrams": all_verdicts, "browser_errors": all_errors, "mobile_stacked_panel": "pass", "multiple_widget_isolation": "pass", "dark_host_contrast": "pass"}
|
|
207
|
+
(proof_directory / c.REPORT_NAME).write_text(json.dumps(report, indent=c.JSON_INDENT), encoding="utf-8")
|
|
208
|
+
print(json.dumps(report, indent=c.JSON_INDENT))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
213
|
+
parser.add_argument("html", type=Path)
|
|
214
|
+
parser.add_argument("--out", type=Path, default=Path(c.DEFAULT_PROOF_DIRECTORY))
|
|
215
|
+
parser.add_argument("--browser-path", help="Chromium executable; defaults to a detected browser or Playwright Chromium.")
|
|
216
|
+
parser.add_argument("--no-sandbox", action="store_true", help="Use only when the browser runs in an isolated container.")
|
|
217
|
+
arguments = parser.parse_args()
|
|
218
|
+
verify(arguments.html, arguments.out, arguments.browser_path, arguments.no_sandbox)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Capture limits, artifact names, and diagram boundaries."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
SKILL_ROOT = Path(__file__).resolve().parents[3]
|
|
5
|
+
PAGE_SIZE = 100
|
|
6
|
+
SOURCE_WORKERS = 4
|
|
7
|
+
JSON_INDENT = 2
|
|
8
|
+
NEWLINE = "\n"
|
|
9
|
+
PARAGRAPH_BREAK = "\n\n"
|
|
10
|
+
ALL_SOURCE_SIDES = ("base", "head")
|
|
11
|
+
HEAD_SIDE = "head"
|
|
12
|
+
BASE_SIDE = "base"
|
|
13
|
+
PR_URL_PATTERN = r"https://github\.com/([\w.-]+/[\w.-]+)/pull/([1-9]\d*)/?"
|
|
14
|
+
PR_ENDPOINT = "repos/{repo}/pulls/{number}"
|
|
15
|
+
COMPARE_ENDPOINT = "repos/{repo}/compare/{base}...{head}"
|
|
16
|
+
FILES_ENDPOINT = "repos/{repo}/pulls/{number}/files?per_page={size}&page={page}"
|
|
17
|
+
CONTENTS_ENDPOINT = "repos/{repo}/contents/{path}?ref={revision}"
|
|
18
|
+
SOURCE_LINK = "https://github.com/{repo}/blob/{revision}/{path}#L{first}-L{last}"
|
|
19
|
+
DIFF_SECTION = "--- {filename}\n{patch}"
|
|
20
|
+
SOURCE_LINE = "{number:4} {line}"
|
|
21
|
+
SNAPSHOT_NAME = "snapshot.json"
|
|
22
|
+
DIFF_NAME = "changes.diff"
|
|
23
|
+
DEFAULT_EVIDENCE_DIRECTORY = "evidence"
|
|
24
|
+
SVG_PATH_PATTERN = r"[MmLlHhVvQqCcSsTtAaZz0-9.,\s-]+"
|
|
25
|
+
MINIMUM_NODE_X = 60
|
|
26
|
+
MAXIMUM_NODE_X = 1380
|
|
27
|
+
MINIMUM_NODE_Y = 200
|
|
28
|
+
MAXIMUM_NODE_Y = 760
|
|
29
|
+
MINIMUM_DRILLDOWN_STEPS = 2
|
|
30
|
+
MAXIMUM_DRILLDOWN_STEPS = 6
|
|
31
|
+
MAXIMUM_HTML_BYTES = 256 * 1024
|
|
32
|
+
VIEWER_PATH = SKILL_ROOT / "assets" / "viewer.html"
|
|
33
|
+
ICONS_PATH = SKILL_ROOT / "assets" / "icons.symbols"
|
|
34
|
+
ALL_DESKTOP_VIEWPORT = {"width": 1440, "height": 1200}
|
|
35
|
+
ALL_MOBILE_VIEWPORT = {"width": 390, "height": 844}
|
|
36
|
+
ROOT_SELECTOR = "[data-pr-visual]"
|
|
37
|
+
DRAWER_SELECTOR = ".drawer"
|
|
38
|
+
STAGE_SELECTOR = ".stage"
|
|
39
|
+
CANVAS_SELECTOR = ".canvas"
|
|
40
|
+
EXACT_SELECTOR = ".exact-source"
|
|
41
|
+
DISCLOSURE_SELECTOR = ".exact-source summary"
|
|
42
|
+
DETAIL_TITLE_SELECTOR = "[data-detail-title]"
|
|
43
|
+
NODE_SELECTOR = "[data-node]"
|
|
44
|
+
JUMP_SELECTOR = "[data-detail-target]"
|
|
45
|
+
DATA_SCENES_SELECTOR = "[data-scenes]"
|
|
46
|
+
CANVAS_WIDTH_EXPRESSION = "(n)=>n.style.width"
|
|
47
|
+
DISCLOSURE_OPEN_EXPRESSION = "(n)=>n.open"
|
|
48
|
+
SOURCE_HAS_EXCERPTS_EXPRESSION = "s=>s.nodes.some(n=>n.sources.some(r=>r.excerpt))"
|
|
49
|
+
BACKGROUND_EXPRESSION = "(n)=>getComputedStyle(n).backgroundColor"
|
|
50
|
+
SCROLLABLE_EXPRESSION = "(n)=>n.scrollWidth>n.clientWidth"
|
|
51
|
+
SCROLL_MAP_EXPRESSION = "(n)=>n.scrollLeft=600"
|
|
52
|
+
READ_SCROLL_EXPRESSION = "(n)=>n.scrollLeft"
|
|
53
|
+
READ_SCENES_EXPRESSION = "(n)=>JSON.parse(n.textContent)"
|
|
54
|
+
READ_IDS_EXPRESSION = "(nodes)=>nodes.map(n=>n.id)"
|
|
55
|
+
NODE_TARGET_SELECTOR = '[data-node="{target}"]'
|
|
56
|
+
SELECTED_NODE_SELECTOR = '[data-node][aria-expanded=true]'
|
|
57
|
+
PR_SCREENSHOT_NAME = "scene-{index}-pr-{number}.png"
|
|
58
|
+
DETAIL_SCREENSHOT_NAME = "scene-{index}-pr-{number}-detail.png"
|
|
59
|
+
MOBILE_SCREENSHOT_NAME = "mobile-detail.png"
|
|
60
|
+
REPORT_NAME = "verification.json"
|
|
61
|
+
DEFAULT_PROOF_DIRECTORY = "proof"
|
|
62
|
+
SPACE_SEPARATOR = " "
|
|
63
|
+
ZOOM_BUTTON_LABEL = "Zoom in"
|
|
64
|
+
CLOSE_BUTTON_LABEL = "Close details"
|
|
65
|
+
ALL_TOOLBAR_DETAIL_LABELS = ("Summary vs code", "Review notes")
|
|
66
|
+
OVERFLOW_EXPRESSION = "(nodes)=>nodes.filter(n=>{const b=n.getBBox(); return b.x<0||b.x+b.width>1440||b.y<0||b.y+b.height>1020}).map(n=>n.textContent)"
|
|
67
|
+
MAIN_FRAGMENT_PATTERN = r"<main data-pr-visual>.*?</main>"
|
|
68
|
+
STYLE_FRAGMENT_PATTERN = r"<style>.*?</style>"
|
|
69
|
+
HOST_FIXTURE_HTML = '<div class="toolbar" id="host-toolbar">Host toolbar</div>'
|
|
70
|
+
HOST_FIXTURE_SELECTOR = "#host-toolbar"
|
|
71
|
+
HOST_THEME_STYLES = 'body{color-scheme:dark;color:white;background:#121212}button{color:white;opacity:.45;text-shadow:0 0 2px white}span{color:#cccccc}'
|
|
72
|
+
CONTROL_STYLES_EXPRESSION = '(n)=>{const s=getComputedStyle(n);return {color:s.color,background:s.backgroundColor,opacity:s.opacity,shadow:s.textShadow,scheme:s.colorScheme,border:s.borderTopColor,outline:s.outlineColor}}'
|
|
73
|
+
TEAL_COLOR = "rgb(23, 53, 63)"
|
|
74
|
+
WHITE_COLOR = "rgb(255, 255, 255)"
|
|
75
|
+
LIME_COLOR = "rgb(216, 246, 135)"
|
|
76
|
+
CONTROL_BORDER_COLOR = "rgb(135, 155, 142)"
|
|
77
|
+
MINIMUM_TEXT_CONTRAST = 4.5
|
|
78
|
+
RGB_COMPONENT_PATTERN = r"\d+"
|
|
79
|
+
RGB_CHANNEL_MAXIMUM = 255
|
|
80
|
+
SRGB_LINEAR_THRESHOLD = 0.04045
|
|
81
|
+
SRGB_LINEAR_DIVISOR = 12.92
|
|
82
|
+
SRGB_OFFSET = 0.055
|
|
83
|
+
SRGB_SCALE = 1.055
|
|
84
|
+
SRGB_EXPONENT = 2.4
|
|
85
|
+
ALL_LUMINANCE_WEIGHTS = (0.2126, 0.7152, 0.0722)
|
|
86
|
+
CONTRAST_OFFSET = 0.05
|
|
87
|
+
MOUNT_WIDGET_EXPRESSION = 'html=>{const t=document.createElement("template");t.innerHTML=html;const scripts=[...t.content.querySelectorAll("script:not([type])")];scripts.forEach(n=>n.remove());document.body.append(t.content);scripts.forEach(old=>{const script=document.createElement("script");script.textContent=old.textContent;document.querySelectorAll("[data-pr-visual]").item(document.querySelectorAll("[data-pr-visual]").length-1).append(script)})}'
|
|
88
|
+
|
|
89
|
+
ALL_CHART_KINDS = ("bar", "line", "scatter")
|
|
90
|
+
ALL_CHART_AXES = ("x", "y")
|
|
91
|
+
CHART_Y_AXIS = "y"
|
|
92
|
+
METRIC_SOURCE_INDEX = "source"
|
|
93
|
+
|
|
94
|
+
PYTHON_LINE_FIELD = "python_line"
|
|
95
|
+
PYTHON_ARGUMENT_FIELD = "argument"
|
|
96
|
+
PYTHON_ELEMENT_FIELD = "element"
|
|
97
|
+
CHART_X_AXIS = "x"
|
|
98
|
+
ALL_CHART_BASES = ("captured", "source-example")
|
|
99
|
+
ALL_EMBEDDED_CHART_FIELDS = ("title", "caption", "x_label", "y_label", "points")
|
|
100
|
+
|
|
101
|
+
DASHBOARD_SCREENSHOT_NAME = "dashboard.png"
|
|
102
|
+
MAXIMUM_DASHBOARD_HEIGHT = 120
|
|
103
|
+
|
|
104
|
+
DASHBOARD_SELECTOR = ".tabs"
|