pipulate 2.41__tar.gz → 2.43__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {pipulate-2.41/pipulate.egg-info → pipulate-2.43}/PKG-INFO +34 -1
  2. {pipulate-2.41 → pipulate-2.43}/README.md +33 -0
  3. {pipulate-2.41 → pipulate-2.43}/__init__.py +12 -2
  4. {pipulate-2.41 → pipulate-2.43}/cli.py +13 -2
  5. {pipulate-2.41 → pipulate-2.43}/imports/ascii_displays.py +28 -21
  6. {pipulate-2.41 → pipulate-2.43}/imports/durable_backup_system.py +28 -2
  7. {pipulate-2.41 → pipulate-2.43/pipulate.egg-info}/PKG-INFO +34 -1
  8. {pipulate-2.41 → pipulate-2.43}/pyproject.toml +1 -1
  9. {pipulate-2.41 → pipulate-2.43}/tools/scraper_tools.py +45 -2
  10. {pipulate-2.41 → pipulate-2.43}/LICENSE +0 -0
  11. {pipulate-2.41 → pipulate-2.43}/MANIFEST.in +0 -0
  12. {pipulate-2.41 → pipulate-2.43}/apps/010_introduction.py +0 -0
  13. {pipulate-2.41 → pipulate-2.43}/apps/015_config.py +0 -0
  14. {pipulate-2.41 → pipulate-2.43}/apps/020_profiles.py +0 -0
  15. {pipulate-2.41 → pipulate-2.43}/apps/025_aspect.py +0 -0
  16. {pipulate-2.41 → pipulate-2.43}/apps/030_roles.py +0 -0
  17. {pipulate-2.41 → pipulate-2.43}/apps/040_hello_workflow.py +0 -0
  18. {pipulate-2.41 → pipulate-2.43}/apps/050_documentation.py +0 -0
  19. {pipulate-2.41 → pipulate-2.43}/apps/060_tasks.py +0 -0
  20. {pipulate-2.41 → pipulate-2.43}/apps/070_history.py +0 -0
  21. {pipulate-2.41 → pipulate-2.43}/apps/110_parameter_buster.py +0 -0
  22. {pipulate-2.41 → pipulate-2.43}/apps/120_link_graph.py +0 -0
  23. {pipulate-2.41 → pipulate-2.43}/apps/130_gap_analysis.py +0 -0
  24. {pipulate-2.41 → pipulate-2.43}/apps/200_workflow_genesis.py +0 -0
  25. {pipulate-2.41 → pipulate-2.43}/apps/210_widget_examples.py +0 -0
  26. {pipulate-2.41 → pipulate-2.43}/apps/220_roadmap.py +0 -0
  27. {pipulate-2.41 → pipulate-2.43}/apps/230_dev_assistant.py +0 -0
  28. {pipulate-2.41 → pipulate-2.43}/apps/240_simon_mcp.py +0 -0
  29. {pipulate-2.41 → pipulate-2.43}/apps/300_blank_placeholder.py +0 -0
  30. {pipulate-2.41 → pipulate-2.43}/apps/400_botify_trifecta.py +0 -0
  31. {pipulate-2.41 → pipulate-2.43}/apps/440_browser_automation.py +0 -0
  32. {pipulate-2.41 → pipulate-2.43}/apps/450_stream_simulator.py +0 -0
  33. {pipulate-2.41 → pipulate-2.43}/apps/510_text_field.py +0 -0
  34. {pipulate-2.41 → pipulate-2.43}/apps/520_text_area.py +0 -0
  35. {pipulate-2.41 → pipulate-2.43}/apps/530_dropdown.py +0 -0
  36. {pipulate-2.41 → pipulate-2.43}/apps/540_checkboxes.py +0 -0
  37. {pipulate-2.41 → pipulate-2.43}/apps/550_radios.py +0 -0
  38. {pipulate-2.41 → pipulate-2.43}/apps/560_range.py +0 -0
  39. {pipulate-2.41 → pipulate-2.43}/apps/570_switch.py +0 -0
  40. {pipulate-2.41 → pipulate-2.43}/apps/580_upload.py +0 -0
  41. {pipulate-2.41 → pipulate-2.43}/apps/610_markdown.py +0 -0
  42. {pipulate-2.41 → pipulate-2.43}/apps/620_mermaid.py +0 -0
  43. {pipulate-2.41 → pipulate-2.43}/apps/630_prism.py +0 -0
  44. {pipulate-2.41 → pipulate-2.43}/apps/640_javascript.py +0 -0
  45. {pipulate-2.41 → pipulate-2.43}/apps/710_pandas.py +0 -0
  46. {pipulate-2.41 → pipulate-2.43}/apps/720_rich.py +0 -0
  47. {pipulate-2.41 → pipulate-2.43}/apps/730_matplotlib.py +0 -0
  48. {pipulate-2.41 → pipulate-2.43}/apps/810_webbrowser.py +0 -0
  49. {pipulate-2.41 → pipulate-2.43}/apps/820_selenium.py +0 -0
  50. {pipulate-2.41 → pipulate-2.43}/apps/830_pico_slider.py +0 -0
  51. {pipulate-2.41 → pipulate-2.43}/config.py +0 -0
  52. {pipulate-2.41 → pipulate-2.43}/imports/__init__.py +0 -0
  53. {pipulate-2.41 → pipulate-2.43}/imports/ai_dictdb.py +0 -0
  54. {pipulate-2.41 → pipulate-2.43}/imports/ai_tool_discovery_simple_parser.py +0 -0
  55. {pipulate-2.41 → pipulate-2.43}/imports/append_only_conversation.py +0 -0
  56. {pipulate-2.41 → pipulate-2.43}/imports/botify/__init__.py +0 -0
  57. {pipulate-2.41 → pipulate-2.43}/imports/botify/code_generators.py +0 -0
  58. {pipulate-2.41 → pipulate-2.43}/imports/botify/true_schema_discoverer.py +0 -0
  59. {pipulate-2.41 → pipulate-2.43}/imports/botify_code_generation.py +0 -0
  60. {pipulate-2.41 → pipulate-2.43}/imports/crud.py +0 -0
  61. {pipulate-2.41 → pipulate-2.43}/imports/database_safety_wrapper.py +0 -0
  62. {pipulate-2.41 → pipulate-2.43}/imports/dom_processing/__init__.py +0 -0
  63. {pipulate-2.41 → pipulate-2.43}/imports/dom_processing/ai_dom_beautifier.py +0 -0
  64. {pipulate-2.41 → pipulate-2.43}/imports/dom_processing/enhanced_dom_processor.py +0 -0
  65. {pipulate-2.41 → pipulate-2.43}/imports/mcp_orchestrator.py +0 -0
  66. {pipulate-2.41 → pipulate-2.43}/imports/server_logging.py +0 -0
  67. {pipulate-2.41 → pipulate-2.43}/imports/stream_orchestrator.py +0 -0
  68. {pipulate-2.41 → pipulate-2.43}/imports/voice_synthesis.py +0 -0
  69. {pipulate-2.41 → pipulate-2.43}/pipulate/__init__.py +0 -0
  70. {pipulate-2.41 → pipulate-2.43}/pipulate/core.py +0 -0
  71. {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/SOURCES.txt +0 -0
  72. {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/dependency_links.txt +0 -0
  73. {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/entry_points.txt +0 -0
  74. {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/requires.txt +0 -0
  75. {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/top_level.txt +0 -0
  76. {pipulate-2.41 → pipulate-2.43}/server.py +0 -0
  77. {pipulate-2.41 → pipulate-2.43}/setup.cfg +0 -0
  78. {pipulate-2.41 → pipulate-2.43}/tests/test_mck_rep2.py +0 -0
  79. {pipulate-2.41 → pipulate-2.43}/tools/__init__.py +0 -0
  80. {pipulate-2.41 → pipulate-2.43}/tools/advanced_automation_tools.py +0 -0
  81. {pipulate-2.41 → pipulate-2.43}/tools/botify_tools.py +0 -0
  82. {pipulate-2.41 → pipulate-2.43}/tools/conversation_tools.py +0 -0
  83. {pipulate-2.41 → pipulate-2.43}/tools/dom_tools.py +0 -0
  84. {pipulate-2.41 → pipulate-2.43}/tools/keychain_tools.py +0 -0
  85. {pipulate-2.41 → pipulate-2.43}/tools/llm_optics.py +0 -0
  86. {pipulate-2.41 → pipulate-2.43}/tools/mcp_tools.py +0 -0
  87. {pipulate-2.41 → pipulate-2.43}/tools/system_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 2.41
3
+ Version: 2.43
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,8 +12,18 @@ Usage:
12
12
  pipulate
13
13
  """
14
14
 
15
- __version__ = "2.41"
16
- __version_description__ = "So wI chu"
15
+ __version__ = "2.43"
16
+ # APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
17
+ # flake.nix's descMatch regex, whose character class excluded ' from the
18
+ # CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
19
+ # ride, and this line is now the only thing that exercises the fix: with the
20
+ # apostrophes gone, the old regex and the new one print an IDENTICAL string, so
21
+ # the straddle could not discriminate between a landed patch and a missing one.
22
+ # A convention the author has to remember ("no apostrophes in version names")
23
+ # is not a property of the system; the regex is. Blast radius is one banner:
24
+ # nothing but flake.nix reads this name -- version_sync.py syncs __version__
25
+ # and __description__, never this. So'wI' chu' -- "engage the cloaking device."
26
+ __version_description__ = "Planning Kata Walks"
17
27
  # SPDX expression, single source of truth, synced into pyproject.toml by
18
28
  # scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
19
29
  # deprecated SPDX) because the header below grants "any later version".
@@ -428,9 +428,20 @@ def main():
428
428
  uninstall_pipulate(args.app_name)
429
429
 
430
430
  elif args.command == 'db-inspect':
431
+ # SAME FOSSIL, SECOND FILE (2026-08-04). db-inspect opened
432
+ # data/botifython_dev.db -- last written May 3 -- and printed its tables
433
+ # under the heading "Tables in botifython_dev.db", so an inspector built
434
+ # to show live state was confidently reporting a dead file's contents.
435
+ # Derived from the same identity value as the backup roster next door.
436
+ try:
437
+ from config import APP_NAME
438
+ app_stem = APP_NAME.lower()
439
+ except Exception as e:
440
+ console.print(f"⚠️ Could not resolve app name from config ({e}); assuming 'pipulate'.")
441
+ app_stem = 'pipulate'
431
442
  db_map = {
432
- 'main_dev': 'data/botifython_dev.db',
433
- 'main_prod': 'data/botifython.db',
443
+ 'main_dev': f'data/{app_stem}_dev.db',
444
+ 'main_prod': f'data/{app_stem}.db',
434
445
  'discussion': 'data/discussion.db',
435
446
  'keychain': 'data/ai_keychain.db'
436
447
  }
@@ -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": 1744376308, # CRC32 no-strip (leading newline preserved, like honeybot_pipeline)
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 three-tier Notebooks workspace: Corporate / Personal / Shared.
794
-
795
- Self-documenting governance: read-only canon, a private sandbox, and a
796
- single-writer outbound-exchange partition per username. Authored with no
797
- .strip() so the leading newline is preserved (the honeybot_pipeline
798
- convention), which the FIGURATE_LEDGER seal must match. No [[[color bits]]]
799
- and no <angle tags>, so _expand_color_bits_ai leaves it untouched and
800
- ai_art == art.
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
- │ every level advertises its own AGENTS.md + OKF index.md
811
+ │ FLAT siblings. Nothing nests. Nothing to get wrong.
805
812
  │
806
- ├── Corporate/ read-only canon · auto-pulled · git wins on collision
807
- │ ├── AGENTS.md
808
- │ ├── .agents/skills/
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
- ├── Personal/ your sandbox · gitignored · vibe-code freely
812
- │ ├── AGENTS.md
813
- │ └── Playground/ NOTHING here is ever shared
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/ outbound exchange · one folder per name
816
- ├── alice/ you write ONLY your own folder;
817
- └── bob/ single-writer partitions = zero merge conflicts
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 — Corporate / Personal / Shared", border_style="white")
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
 
@@ -60,6 +60,32 @@ class DurableBackupManager:
60
60
  logger.debug(f"Could not set backup directory permissions: {e}")
61
61
 
62
62
  logger.info(f"🗃️ Rolling backup root: {self.backup_root}")
63
+ # THE ROSTER IS DERIVED, NOT LITERAL (2026-08-04, fossil-convicted on
64
+ # the maintainer's own box). The app database filename is a pure
65
+ # function of whitelabel.txt: config.py builds data/{APP_NAME.lower()}.db
66
+ # and server.py:322 independently builds the same string. This roster
67
+ # hardcoded 'botifython'. whitelabel.txt here says Pipulate, so the live
68
+ # databases are data/pipulate*.db (mtime Jul 30) while the roster kept
69
+ # copying data/botifython_dev.db (mtime MAY 3) once per server start,
70
+ # forever. The distinct backup stems were ai_keychain / botifython /
71
+ # botifython_dev / discussion: the live Profiles/Tasks database had NEVER
72
+ # been backed up, and the two stale files are exactly what hid it --
73
+ # os.path.exists returned True, no warning fired, and the summary table
74
+ # printed a green checkmark beside a file nobody had touched in three
75
+ # months. A stranger with only one family at least gets a "not found"
76
+ # line in a log. THE DERIVED-PATH RULE: compute the target from an
77
+ # identity value you read and cannot author, so the mismatch becomes
78
+ # unrepresentable instead of merely unlikely.
79
+ try:
80
+ from config import get_app_name
81
+ app_stem = get_app_name().lower()
82
+ except Exception as e:
83
+ # Fail LOUD, then mirror config.py's own basename rule. A silent
84
+ # fallback would be the same disease this patch is curing.
85
+ logger.warning(f"⚠️ Could not resolve app name from config ({e}); "
86
+ f"deriving the backup roster from the install directory name.")
87
+ root_name = Path(__file__).resolve().parent.parent.name
88
+ app_stem = (root_name[:-5] if root_name.endswith('-main') else root_name).lower()
63
89
 
64
90
  # 🎯 CRITICAL DATABASES TO PROTECT
65
91
  self.critical_databases = {
@@ -76,13 +102,13 @@ class DurableBackupManager:
76
102
  'cross_cutting': True
77
103
  },
78
104
  'app_prod': {
79
- 'source_path': 'data/botifython.db',
105
+ 'source_path': f'data/{app_stem}.db',
80
106
  'description': 'Production Profiles/Tasks',
81
107
  'critical': True,
82
108
  'cross_cutting': False
83
109
  },
84
110
  'app_dev': {
85
- 'source_path': 'data/botifython_dev.db',
111
+ 'source_path': f'data/{app_stem}_dev.db',
86
112
  'description': 'Development Profiles/Tasks',
87
113
  'critical': False,
88
114
  'cross_cutting': False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 2.41
3
+ Version: 2.43
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.41"
8
+ version = "2.43"
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