pipulate 2.42__tar.gz → 2.44__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pipulate-2.42/pipulate.egg-info → pipulate-2.44}/PKG-INFO +34 -1
- {pipulate-2.42 → pipulate-2.44}/README.md +33 -0
- {pipulate-2.42 → pipulate-2.44}/__init__.py +2 -2
- {pipulate-2.42 → pipulate-2.44}/imports/ascii_displays.py +28 -21
- {pipulate-2.42 → pipulate-2.44/pipulate.egg-info}/PKG-INFO +34 -1
- {pipulate-2.42 → pipulate-2.44}/pyproject.toml +1 -1
- {pipulate-2.42 → pipulate-2.44}/tools/scraper_tools.py +45 -2
- {pipulate-2.42 → pipulate-2.44}/LICENSE +0 -0
- {pipulate-2.42 → pipulate-2.44}/MANIFEST.in +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/010_introduction.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/015_config.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/020_profiles.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/025_aspect.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/030_roles.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/040_hello_workflow.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/050_documentation.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/060_tasks.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/070_history.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/110_parameter_buster.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/120_link_graph.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/130_gap_analysis.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/200_workflow_genesis.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/210_widget_examples.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/220_roadmap.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/230_dev_assistant.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/240_simon_mcp.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/300_blank_placeholder.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/400_botify_trifecta.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/440_browser_automation.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/450_stream_simulator.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/510_text_field.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/520_text_area.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/530_dropdown.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/540_checkboxes.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/550_radios.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/560_range.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/570_switch.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/580_upload.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/610_markdown.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/620_mermaid.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/630_prism.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/640_javascript.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/710_pandas.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/720_rich.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/730_matplotlib.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/810_webbrowser.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/820_selenium.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/apps/830_pico_slider.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/cli.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/config.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/__init__.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/ai_dictdb.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/ai_tool_discovery_simple_parser.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/append_only_conversation.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/botify/__init__.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/botify/code_generators.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/botify/true_schema_discoverer.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/botify_code_generation.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/crud.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/database_safety_wrapper.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/dom_processing/__init__.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/dom_processing/ai_dom_beautifier.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/dom_processing/enhanced_dom_processor.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/durable_backup_system.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/mcp_orchestrator.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/server_logging.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/stream_orchestrator.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/imports/voice_synthesis.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate/__init__.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate/core.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate.egg-info/SOURCES.txt +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate.egg-info/dependency_links.txt +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate.egg-info/entry_points.txt +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate.egg-info/requires.txt +0 -0
- {pipulate-2.42 → pipulate-2.44}/pipulate.egg-info/top_level.txt +0 -0
- {pipulate-2.42 → pipulate-2.44}/server.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/setup.cfg +0 -0
- {pipulate-2.42 → pipulate-2.44}/tests/test_mck_rep2.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/__init__.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/advanced_automation_tools.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/botify_tools.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/conversation_tools.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/dom_tools.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/keychain_tools.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/llm_optics.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/mcp_tools.py +0 -0
- {pipulate-2.42 → pipulate-2.44}/tools/system_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipulate
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.44
|
|
4
4
|
Summary: AI-readiness for the agentic web — local-first, Nix-reproducible workflows. The successor to AI SEO software.
|
|
5
5
|
Author-email: Mike Levin <pipulate@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -108,6 +108,39 @@ cd ~/pipulate
|
|
|
108
108
|
nix develop
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
<!-- DO NOT MOVE THIS BLOCK ABOVE "## Quick Start". Two splicers own
|
|
112
|
+
parts of this file, with two different boundary schemes:
|
|
113
|
+
1. release.py run_waxascii_release_stamp() -- HEADER-BOUNDED.
|
|
114
|
+
It finds the "I HEREBY WILL NOT RE-GENERATE" line near the
|
|
115
|
+
top and replaces EVERYTHING between the "# Pipulate:" header
|
|
116
|
+
and the next header with the sealed white_rabbit art. Runs on
|
|
117
|
+
every `release`. Anything inside that region is destroyed.
|
|
118
|
+
2. prompt_foo.py update_readme_md_in_place() -- SENTINEL-BOUNDED,
|
|
119
|
+
the two HTML comments below. Runs on every compile, refuses on
|
|
120
|
+
CRC drift, writes only when the bytes change.
|
|
121
|
+
The sentinels below are safe ONLY because they sit after
|
|
122
|
+
"## Quick Start", outside splicer 1's blast radius. Verified from
|
|
123
|
+
line numbers, not from a release run. -->
|
|
124
|
+
### Where your work lives
|
|
125
|
+
<!-- --- START WORKSPACE TREE --- -->
|
|
126
|
+
```text
|
|
127
|
+
Notebooks/ — the JupyterLab root (NOT Pipulate's own root)
|
|
128
|
+
│ FLAT siblings. Nothing nests. Nothing to get wrong.
|
|
129
|
+
│
|
|
130
|
+
├── Advanced_Notebooks/ canon · flake-delivered, copy-if-absent
|
|
131
|
+
├── Educational_Notebooks/ canon · your edits survive, updates do not arrive
|
|
132
|
+
├── imports/ canon · the code-behind "sauce" modules
|
|
133
|
+
│
|
|
134
|
+
├── Playground/ personal · gitignored · your own git repo goes here
|
|
135
|
+
├── Client_Work/ personal · gitignored · never leaves this machine
|
|
136
|
+
├── Deliverables/ personal · gitignored
|
|
137
|
+
│
|
|
138
|
+
└── Shared/ the ONE folder for handing work to a teammate
|
|
139
|
+
├── alice/ one folder per person; you write ONLY your own
|
|
140
|
+
└── bob/ single-writer partitions = zero merge conflicts
|
|
141
|
+
```
|
|
142
|
+
<!-- --- END WORKSPACE TREE --- -->
|
|
143
|
+
|
|
111
144
|
## 💡 What Can You Actually Build?
|
|
112
145
|
|
|
113
146
|
**Real examples of what people create with Pipulate:**
|
|
@@ -51,6 +51,39 @@ cd ~/pipulate
|
|
|
51
51
|
nix develop
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
<!-- DO NOT MOVE THIS BLOCK ABOVE "## Quick Start". Two splicers own
|
|
55
|
+
parts of this file, with two different boundary schemes:
|
|
56
|
+
1. release.py run_waxascii_release_stamp() -- HEADER-BOUNDED.
|
|
57
|
+
It finds the "I HEREBY WILL NOT RE-GENERATE" line near the
|
|
58
|
+
top and replaces EVERYTHING between the "# Pipulate:" header
|
|
59
|
+
and the next header with the sealed white_rabbit art. Runs on
|
|
60
|
+
every `release`. Anything inside that region is destroyed.
|
|
61
|
+
2. prompt_foo.py update_readme_md_in_place() -- SENTINEL-BOUNDED,
|
|
62
|
+
the two HTML comments below. Runs on every compile, refuses on
|
|
63
|
+
CRC drift, writes only when the bytes change.
|
|
64
|
+
The sentinels below are safe ONLY because they sit after
|
|
65
|
+
"## Quick Start", outside splicer 1's blast radius. Verified from
|
|
66
|
+
line numbers, not from a release run. -->
|
|
67
|
+
### Where your work lives
|
|
68
|
+
<!-- --- START WORKSPACE TREE --- -->
|
|
69
|
+
```text
|
|
70
|
+
Notebooks/ — the JupyterLab root (NOT Pipulate's own root)
|
|
71
|
+
│ FLAT siblings. Nothing nests. Nothing to get wrong.
|
|
72
|
+
│
|
|
73
|
+
├── Advanced_Notebooks/ canon · flake-delivered, copy-if-absent
|
|
74
|
+
├── Educational_Notebooks/ canon · your edits survive, updates do not arrive
|
|
75
|
+
├── imports/ canon · the code-behind "sauce" modules
|
|
76
|
+
│
|
|
77
|
+
├── Playground/ personal · gitignored · your own git repo goes here
|
|
78
|
+
├── Client_Work/ personal · gitignored · never leaves this machine
|
|
79
|
+
├── Deliverables/ personal · gitignored
|
|
80
|
+
│
|
|
81
|
+
└── Shared/ the ONE folder for handing work to a teammate
|
|
82
|
+
├── alice/ one folder per person; you write ONLY your own
|
|
83
|
+
└── bob/ single-writer partitions = zero merge conflicts
|
|
84
|
+
```
|
|
85
|
+
<!-- --- END WORKSPACE TREE --- -->
|
|
86
|
+
|
|
54
87
|
## 💡 What Can You Actually Build?
|
|
55
88
|
|
|
56
89
|
**Real examples of what people create with Pipulate:**
|
|
@@ -12,7 +12,7 @@ Usage:
|
|
|
12
12
|
pipulate
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
__version__ = "2.
|
|
15
|
+
__version__ = "2.44"
|
|
16
16
|
# APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
|
|
17
17
|
# flake.nix's descMatch regex, whose character class excluded ' from the
|
|
18
18
|
# CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
|
|
@@ -23,7 +23,7 @@ __version__ = "2.42"
|
|
|
23
23
|
# is not a property of the system; the regex is. Blast radius is one banner:
|
|
24
24
|
# nothing but flake.nix reads this name -- version_sync.py syncs __version__
|
|
25
25
|
# and __description__, never this. So'wI' chu' -- "engage the cloaking device."
|
|
26
|
-
__version_description__ = "
|
|
26
|
+
__version_description__ = "Massaging Install Paths"
|
|
27
27
|
# SPDX expression, single source of truth, synced into pyproject.toml by
|
|
28
28
|
# scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
|
|
29
29
|
# deprecated SPDX) because the header below grants "any later version".
|
|
@@ -66,7 +66,7 @@ FIGURATE_LEDGER: dict = {
|
|
|
66
66
|
"canal_lock": 311868176,
|
|
67
67
|
"mechanical_man": 4142234675,
|
|
68
68
|
# CRC32 after _expand_color_bits_ai() + .strip()
|
|
69
|
-
"workspace_tree":
|
|
69
|
+
"workspace_tree": 2408173560, # CRC32 no-strip (leading newline preserved, like honeybot_pipeline)
|
|
70
70
|
"forcing_pair": 3594006631, # CRC32 no-strip; seal re-taken 2026-07-27 — 208394658 sealed a re-typed copy of the art, not the bytes on disk; the straddle caught it (drift 1→0)
|
|
71
71
|
# === FIGURATE_LEDGER_EXTRUDE_BOTTOM ===
|
|
72
72
|
# Add new artwork CRC32 entries immediately above this line
|
|
@@ -789,38 +789,45 @@ def _figurate_mechanical_man():
|
|
|
789
789
|
human = Panel(human_art, title="🗝️ Tik-Tok — Two Keys Wound, One on the Wall", border_style="white")
|
|
790
790
|
return human, ai_art
|
|
791
791
|
|
|
792
|
+
|
|
792
793
|
def _figurate_workspace_tree():
|
|
793
|
-
"""Render the
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
and
|
|
800
|
-
|
|
794
|
+
"""Render the Notebooks workspace as it ACTUALLY sits on disk: flat.
|
|
795
|
+
THE NESTED VERSION WAS THE ONLY AUTHORITY THAT SHIPPED, AND IT WAS WRONG.
|
|
796
|
+
update_agents_md_in_place() splices this art into AGENTS.md on every
|
|
797
|
+
compile, so it is what every external agent tool reads -- while flake.nix
|
|
798
|
+
argued for flat siblings and `ls Notebooks/` showed nine flat directories
|
|
799
|
+
and no Corporate/, Personal/, or Workshop/ at all. Three authorities, one
|
|
800
|
+
winner, and the winner disagreed with the disk. Fixed at the GENERATOR so
|
|
801
|
+
the output cannot drift back.
|
|
802
|
+
Three tiers, zero nesting: canon arrives by the flake's copy-if-absent
|
|
803
|
+
loop, personal is gitignored, and Shared/ is the single deliberate
|
|
804
|
+
outbound surface with one folder per person. Authored with no .strip() so
|
|
805
|
+
the leading newline is preserved (the honeybot_pipeline convention), which
|
|
806
|
+
the FIGURATE_LEDGER seal must match. No color-bit or angle-tag markers, so
|
|
807
|
+
_expand_color_bits_ai leaves it untouched and ai_art == art.
|
|
801
808
|
"""
|
|
802
809
|
art = r"""
|
|
803
810
|
Notebooks/ — the JupyterLab root (NOT Pipulate's own root)
|
|
804
|
-
│
|
|
811
|
+
│ FLAT siblings. Nothing nests. Nothing to get wrong.
|
|
805
812
|
│
|
|
806
|
-
├──
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
│ └── apps/ org plugins ride in — no core commit needed
|
|
813
|
+
├── Advanced_Notebooks/ canon · flake-delivered, copy-if-absent
|
|
814
|
+
├── Educational_Notebooks/ canon · your edits survive, updates do not arrive
|
|
815
|
+
├── imports/ canon · the code-behind "sauce" modules
|
|
810
816
|
│
|
|
811
|
-
├──
|
|
812
|
-
|
|
813
|
-
|
|
817
|
+
├── Playground/ personal · gitignored · your own git repo goes here
|
|
818
|
+
├── Client_Work/ personal · gitignored · never leaves this machine
|
|
819
|
+
├── Deliverables/ personal · gitignored
|
|
814
820
|
│
|
|
815
|
-
└── Shared/
|
|
816
|
-
├── alice/
|
|
817
|
-
└── bob/
|
|
821
|
+
└── Shared/ the ONE folder for handing work to a teammate
|
|
822
|
+
├── alice/ one folder per person; you write ONLY your own
|
|
823
|
+
└── bob/ single-writer partitions = zero merge conflicts
|
|
818
824
|
"""
|
|
819
825
|
ai_art = _expand_color_bits_ai(art)
|
|
820
826
|
human_art = _expand_color_bits_human(art)
|
|
821
|
-
human = Panel(human_art, title="🗂️ Notebooks Workspace —
|
|
827
|
+
human = Panel(human_art, title="🗂️ Notebooks Workspace — canon · personal · Shared", border_style="white")
|
|
822
828
|
return human, ai_art
|
|
823
829
|
|
|
830
|
+
|
|
824
831
|
def _figurate_forcing_pair():
|
|
825
832
|
"""Render the two creativity forcing functions as ONE composed frame.
|
|
826
833
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipulate
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.44
|
|
4
4
|
Summary: AI-readiness for the agentic web — local-first, Nix-reproducible workflows. The successor to AI SEO software.
|
|
5
5
|
Author-email: Mike Levin <pipulate@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -108,6 +108,39 @@ cd ~/pipulate
|
|
|
108
108
|
nix develop
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
<!-- DO NOT MOVE THIS BLOCK ABOVE "## Quick Start". Two splicers own
|
|
112
|
+
parts of this file, with two different boundary schemes:
|
|
113
|
+
1. release.py run_waxascii_release_stamp() -- HEADER-BOUNDED.
|
|
114
|
+
It finds the "I HEREBY WILL NOT RE-GENERATE" line near the
|
|
115
|
+
top and replaces EVERYTHING between the "# Pipulate:" header
|
|
116
|
+
and the next header with the sealed white_rabbit art. Runs on
|
|
117
|
+
every `release`. Anything inside that region is destroyed.
|
|
118
|
+
2. prompt_foo.py update_readme_md_in_place() -- SENTINEL-BOUNDED,
|
|
119
|
+
the two HTML comments below. Runs on every compile, refuses on
|
|
120
|
+
CRC drift, writes only when the bytes change.
|
|
121
|
+
The sentinels below are safe ONLY because they sit after
|
|
122
|
+
"## Quick Start", outside splicer 1's blast radius. Verified from
|
|
123
|
+
line numbers, not from a release run. -->
|
|
124
|
+
### Where your work lives
|
|
125
|
+
<!-- --- START WORKSPACE TREE --- -->
|
|
126
|
+
```text
|
|
127
|
+
Notebooks/ — the JupyterLab root (NOT Pipulate's own root)
|
|
128
|
+
│ FLAT siblings. Nothing nests. Nothing to get wrong.
|
|
129
|
+
│
|
|
130
|
+
├── Advanced_Notebooks/ canon · flake-delivered, copy-if-absent
|
|
131
|
+
├── Educational_Notebooks/ canon · your edits survive, updates do not arrive
|
|
132
|
+
├── imports/ canon · the code-behind "sauce" modules
|
|
133
|
+
│
|
|
134
|
+
├── Playground/ personal · gitignored · your own git repo goes here
|
|
135
|
+
├── Client_Work/ personal · gitignored · never leaves this machine
|
|
136
|
+
├── Deliverables/ personal · gitignored
|
|
137
|
+
│
|
|
138
|
+
└── Shared/ the ONE folder for handing work to a teammate
|
|
139
|
+
├── alice/ one folder per person; you write ONLY your own
|
|
140
|
+
└── bob/ single-writer partitions = zero merge conflicts
|
|
141
|
+
```
|
|
142
|
+
<!-- --- END WORKSPACE TREE --- -->
|
|
143
|
+
|
|
111
144
|
## 💡 What Can You Actually Build?
|
|
112
145
|
|
|
113
146
|
**Real examples of what people create with Pipulate:**
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pipulate"
|
|
7
7
|
# VERSION NOTE: Synced from pipulate/__init__.py.__version__ via python version_sync.py
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.44"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Mike Levin", email="pipulate@gmail.com" },
|
|
11
11
|
]
|
|
@@ -154,7 +154,7 @@ def _simplify_html_for_llm(html_content, default_title=""):
|
|
|
154
154
|
# selenium_automation. Nothing raised; ruff passed; the registry was simply
|
|
155
155
|
# wrong. DECORATOR STRADDLE RULE: decorators bind downward, so any insertion
|
|
156
156
|
# anchored on a def must include the line above it in the SEARCH.
|
|
157
|
-
def _document_candidates(cdp_events: list, domain: str) -> list:
|
|
157
|
+
def _document_candidates(cdp_events: list, domain: str, final_url: str = "") -> list:
|
|
158
158
|
"""Document responseReceived events that plausibly ARE the requested page.
|
|
159
159
|
|
|
160
160
|
Returned in ledger order; the caller takes [-1]. Pure and ledger-only on
|
|
@@ -173,6 +173,33 @@ def _document_candidates(cdp_events: list, domain: str) -> list:
|
|
|
173
173
|
if ev.get("method") == "Network.responseReceived"
|
|
174
174
|
and ev.get("params", {}).get("type") == "Document"
|
|
175
175
|
]
|
|
176
|
+
# THE SAME-ORIGIN IFRAME DEFEAT (convicted 2026-08-04). The host-exact
|
|
177
|
+
# filter below was hardened on 2026-07-24 against THIRD-PARTY frames that
|
|
178
|
+
# carry the target host in a query parameter. It is structurally blind to a
|
|
179
|
+
# SAME-ORIGIN frame: a Shop Pay buyer-recognition callback served from the
|
|
180
|
+
# storefront's own host passes host-exactness BY DEFINITION, fires late,
|
|
181
|
+
# and the caller then takes [-1] -- the LAST Document, which is the iframe.
|
|
182
|
+
# The frame tiebreaker below is only a tiebreaker, and it is permitted to
|
|
183
|
+
# no-op, so nothing downstream can tell a witnessed choice from a guess.
|
|
184
|
+
# ANCHOR ON A VALUE THE BROWSER AUTHORED, NOT ON AN INFERENCE. final_url is
|
|
185
|
+
# driver.current_url, read before this selection runs. A sub-frame URL is
|
|
186
|
+
# NEVER the browser's current URL, so an exact match cannot be a frame --
|
|
187
|
+
# which makes this correct regardless of which inferential step below was
|
|
188
|
+
# the one that broke.
|
|
189
|
+
# PURITY PRESERVED, deliberately: the caller passes final_url IN, no live
|
|
190
|
+
# CDP call appears here, and the whole selection is still replayable
|
|
191
|
+
# offline over any cached network_log.jsonl -- which is what makes the
|
|
192
|
+
# straddle for this very patch cost zero browser flights.
|
|
193
|
+
# FAILS OPEN: no exact match degrades to the host+frame path unchanged, so
|
|
194
|
+
# the worst case equals the behaviour shipped before this block existed.
|
|
195
|
+
if final_url:
|
|
196
|
+
target = final_url.split("#", 1)[0]
|
|
197
|
+
exact = [
|
|
198
|
+
ev for ev in docs
|
|
199
|
+
if ev.get("params", {}).get("response", {}).get("url", "").split("#", 1)[0] == target
|
|
200
|
+
]
|
|
201
|
+
if exact:
|
|
202
|
+
return exact
|
|
176
203
|
want = _norm(domain)
|
|
177
204
|
on_host = [
|
|
178
205
|
ev for ev in docs
|
|
@@ -722,7 +749,7 @@ async def _selenium_capture(params: dict, checkpoint=None) -> dict:
|
|
|
722
749
|
# Redirect chains do not threaten it either way; CDP surfaces those
|
|
723
750
|
# as requestWillBeSent with redirectResponse, not as extra
|
|
724
751
|
# responseReceived events.
|
|
725
|
-
doc_events = _document_candidates(cdp_events, domain)
|
|
752
|
+
doc_events = _document_candidates(cdp_events, domain, final_url)
|
|
726
753
|
if doc_events:
|
|
727
754
|
doc_params = doc_events[-1]["params"]
|
|
728
755
|
# The subject of the headers on the very next line. Captured
|
|
@@ -777,6 +804,22 @@ async def _selenium_capture(params: dict, checkpoint=None) -> dict:
|
|
|
777
804
|
# artifact that does not record its own subject cannot be audited
|
|
778
805
|
# against the ledger sitting next to it.
|
|
779
806
|
"header_source_url": doc_url,
|
|
807
|
+
# WHICH RULE CHOSE IT. header_source_url (2026-07-24) records the
|
|
808
|
+
# SUBJECT of these headers; this records HOW that subject was
|
|
809
|
+
# picked. The 2026-08-04 conviction cost two turns precisely
|
|
810
|
+
# because the artifact could name its subject but not its
|
|
811
|
+
# selection path. "final_url_exact" means the browser's own
|
|
812
|
+
# current URL matched a Document response; "host_fallback" means
|
|
813
|
+
# the selector guessed from host and frame order and the result
|
|
814
|
+
# did NOT match the final URL -- treat the whole triptych as
|
|
815
|
+
# suspect on that value; "none" means no Document was selected at
|
|
816
|
+
# all. One jq, not a two-turn investigation.
|
|
817
|
+
"header_selection": (
|
|
818
|
+
"none" if not doc_url
|
|
819
|
+
else "final_url_exact"
|
|
820
|
+
if final_url and doc_url.split("#", 1)[0] == final_url.split("#", 1)[0]
|
|
821
|
+
else "host_fallback"
|
|
822
|
+
),
|
|
780
823
|
# Consumers read this to decide whether source.html may be trusted
|
|
781
824
|
# as the LEFT PANEL of the triptych. Absent on captures taken
|
|
782
825
|
# before 2026-07-24, which consumers therefore treat as UNFLAGGED,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|