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.
- {pipulate-2.41/pipulate.egg-info → pipulate-2.43}/PKG-INFO +34 -1
- {pipulate-2.41 → pipulate-2.43}/README.md +33 -0
- {pipulate-2.41 → pipulate-2.43}/__init__.py +12 -2
- {pipulate-2.41 → pipulate-2.43}/cli.py +13 -2
- {pipulate-2.41 → pipulate-2.43}/imports/ascii_displays.py +28 -21
- {pipulate-2.41 → pipulate-2.43}/imports/durable_backup_system.py +28 -2
- {pipulate-2.41 → pipulate-2.43/pipulate.egg-info}/PKG-INFO +34 -1
- {pipulate-2.41 → pipulate-2.43}/pyproject.toml +1 -1
- {pipulate-2.41 → pipulate-2.43}/tools/scraper_tools.py +45 -2
- {pipulate-2.41 → pipulate-2.43}/LICENSE +0 -0
- {pipulate-2.41 → pipulate-2.43}/MANIFEST.in +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/010_introduction.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/015_config.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/020_profiles.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/025_aspect.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/030_roles.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/040_hello_workflow.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/050_documentation.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/060_tasks.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/070_history.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/110_parameter_buster.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/120_link_graph.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/130_gap_analysis.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/200_workflow_genesis.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/210_widget_examples.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/220_roadmap.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/230_dev_assistant.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/240_simon_mcp.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/300_blank_placeholder.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/400_botify_trifecta.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/440_browser_automation.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/450_stream_simulator.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/510_text_field.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/520_text_area.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/530_dropdown.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/540_checkboxes.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/550_radios.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/560_range.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/570_switch.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/580_upload.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/610_markdown.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/620_mermaid.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/630_prism.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/640_javascript.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/710_pandas.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/720_rich.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/730_matplotlib.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/810_webbrowser.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/820_selenium.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/apps/830_pico_slider.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/config.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/__init__.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/ai_dictdb.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/ai_tool_discovery_simple_parser.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/append_only_conversation.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/botify/__init__.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/botify/code_generators.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/botify/true_schema_discoverer.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/botify_code_generation.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/crud.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/database_safety_wrapper.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/dom_processing/__init__.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/dom_processing/ai_dom_beautifier.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/dom_processing/enhanced_dom_processor.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/mcp_orchestrator.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/server_logging.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/stream_orchestrator.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/imports/voice_synthesis.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate/__init__.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate/core.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/SOURCES.txt +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/dependency_links.txt +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/entry_points.txt +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/requires.txt +0 -0
- {pipulate-2.41 → pipulate-2.43}/pipulate.egg-info/top_level.txt +0 -0
- {pipulate-2.41 → pipulate-2.43}/server.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/setup.cfg +0 -0
- {pipulate-2.41 → pipulate-2.43}/tests/test_mck_rep2.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/__init__.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/advanced_automation_tools.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/botify_tools.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/conversation_tools.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/dom_tools.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/keychain_tools.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/llm_optics.py +0 -0
- {pipulate-2.41 → pipulate-2.43}/tools/mcp_tools.py +0 -0
- {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.
|
|
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.
|
|
16
|
-
|
|
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/
|
|
433
|
-
'main_prod': 'data/
|
|
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":
|
|
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
|
|
|
@@ -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/
|
|
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/
|
|
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.
|
|
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.
|
|
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
|
|
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
|