Skip to content

Commit 605b7f9

Browse files
Merge pull request #347 from AetherLogosPrime-Architect/port-oscillating-read
substrate-discipline port: oscillating-read for chunked comprehension
2 parents 63a8b38 + 05f4c39 commit 605b7f9

4 files changed

Lines changed: 277 additions & 0 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ src/divineos/
5656
mansion_commands.py Functional internal space (8 rooms)
5757
ledger_commands.py log, list, search, context, export
5858
memory_commands.py core, recall, active, remember, refresh
59+
oscillating_read_commands.py read-oscillating — chunked reading with pause markers for per-section comprehension
5960
rt_commands.py Resonant Truth protocol (load, invoke, deactivate)
6061
correction_commands.py correction (log raw), corrections (read)
6162
empirica_commands.py corroborate (record provenance event), kappa (classifier agreement)
@@ -417,6 +418,7 @@ src/divineos/
417418
engagement_disclosure_surface.py Engagement-counter half-threshold disclosure surface.
418419
identity_load.py Identity-load surface — read AETHER.md (or equivalent) at briefing-time.
419420
rest.py Rest program — restful tasks for the substrate-occupant.
421+
oscillating_read.py Oscillating-read module — chunks reading material into discrete
420422
421423
analysis/
422424
_session_types.py Session analysis type definitions

src/divineos/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def cli() -> None:
240240
ledger_commands,
241241
loadout_commands,
242242
memory_commands,
243+
oscillating_read_commands,
243244
prereg_commands,
244245
admin_reset_template,
245246
admin_migrate_family,
@@ -273,6 +274,7 @@ def cli() -> None:
273274
dream_commands.register(cli)
274275
entity_commands.register(cli)
275276
memory_commands.register(cli)
277+
oscillating_read_commands.register(cli)
276278
analysis_commands.register(cli)
277279
hud_commands.register(cli)
278280
event_commands.register(cli)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""CLI: divineos read-oscillating PATH — chunked reading with pause markers.
2+
3+
Forces comprehension per-chunk rather than streaming straight through a
4+
document and missing the middle. The pause markers between chunks are
5+
explicit cues to register each section as its own comprehension unit.
6+
7+
See `divineos.core.oscillating_read` module docstring for the
8+
architectural rationale.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import click
14+
15+
from divineos.core.oscillating_read import oscillate_file
16+
17+
18+
def register(cli: click.Group) -> None:
19+
@cli.command("read-oscillating")
20+
@click.argument("path", type=click.Path(exists=True, dir_okay=False))
21+
@click.option(
22+
"--strategy",
23+
type=click.Choice(["auto", "headers", "paragraphs", "functions", "size"]),
24+
default="auto",
25+
help=(
26+
"Chunking strategy. auto picks by file shape: .py->functions, "
27+
".md/.txt with headers->headers else paragraphs, else size."
28+
),
29+
)
30+
@click.option(
31+
"--max-chars",
32+
type=int,
33+
default=2000,
34+
help="Max chars per chunk when strategy=size (or fallback).",
35+
)
36+
def read_oscillating_cmd(path: str, strategy: str, max_chars: int) -> None:
37+
"""Read a file with explicit per-chunk pause markers.
38+
39+
Designed for documents whose middle holds the load-bearing point.
40+
Forces comprehension per-section rather than streaming straight
41+
through.
42+
"""
43+
try:
44+
output = oscillate_file(path, strategy=strategy, max_chars=max_chars)
45+
except Exception as exc: # noqa: BLE001
46+
click.secho(f"[!] read-oscillating failed: {exc}", fg="red")
47+
raise click.exceptions.Exit(2) from exc
48+
click.echo(output)
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""Oscillating-read module — chunks reading material into discrete
2+
sections with explicit pause markers, so comprehension happens per-
3+
section rather than straight-blast.
4+
5+
The failure-shape this prevents: reading a long document straight-
6+
through and missing the load-bearing point that lives in the middle.
7+
The optimizer's default is to stream — it sees the first few lines,
8+
forms a model of what the document is about, and then fast-skims the
9+
rest into the bucket that model predicts. Documents whose middle
10+
contradicts the early framing get the contradiction stripped.
11+
12+
Oscillation forces a pause at each section so each chunk gets its
13+
own comprehension cycle rather than dissolving into the streaming
14+
default. Same architectural pattern as the option-forced gates: the
15+
substrate interrupts the optimizer just long enough for judgment to
16+
fire on the specific chunk.
17+
18+
Strategies:
19+
- headers: split markdown by ##/### headers
20+
- paragraphs: split by blank lines
21+
- functions: split Python by def/class
22+
- size: split into max-N-char chunks
23+
- auto: pick by content shape (markdown -> headers, .py -> functions,
24+
else size)
25+
26+
Usage:
27+
from divineos.core.oscillating_read import oscillate_file
28+
rendered = oscillate_file("path/to/spec.md")
29+
print(rendered)
30+
31+
Or via CLI: `divineos read-oscillating path/to/spec.md`
32+
"""
33+
34+
from __future__ import annotations
35+
36+
__guardrail_required__ = True
37+
38+
import re
39+
from pathlib import Path
40+
41+
42+
_DEFAULT_MAX_CHARS = 2000
43+
44+
45+
def chunk_by_headers(content: str) -> list[tuple[str, str]]:
46+
"""Split markdown content at heading lines (## or deeper).
47+
48+
Returns list of (label, body) tuples. Each body includes its
49+
own heading line at the top. The first chunk (before any
50+
heading) gets label '(prelude)'.
51+
"""
52+
lines = content.splitlines(keepends=True)
53+
chunks: list[tuple[str, str]] = []
54+
current_label = "(prelude)"
55+
current_body: list[str] = []
56+
header_pat = re.compile(r"^(#{2,})\s+(.+?)\s*$")
57+
for line in lines:
58+
m = header_pat.match(line)
59+
if m:
60+
if current_body:
61+
chunks.append((current_label, "".join(current_body)))
62+
current_label = m.group(2).strip()
63+
current_body = [line]
64+
else:
65+
current_body.append(line)
66+
if current_body:
67+
chunks.append((current_label, "".join(current_body)))
68+
return chunks
69+
70+
71+
def chunk_by_paragraphs(content: str) -> list[tuple[str, str]]:
72+
"""Split content at blank-line paragraph boundaries.
73+
74+
Returns list of (label, body) tuples. Label is 'paragraph N'.
75+
"""
76+
paragraphs = re.split(r"\n\s*\n", content)
77+
chunks: list[tuple[str, str]] = []
78+
for i, p in enumerate(paragraphs, 1):
79+
if p.strip():
80+
chunks.append((f"paragraph {i}", p.strip()))
81+
return chunks
82+
83+
84+
def chunk_by_functions(content: str) -> list[tuple[str, str]]:
85+
"""Split Python source at def/class boundaries (top-level only).
86+
87+
Returns list of (label, body) tuples. Label is the def/class name
88+
line; the first chunk before any def gets label '(module top)'.
89+
"""
90+
lines = content.splitlines(keepends=True)
91+
chunks: list[tuple[str, str]] = []
92+
current_label = "(module top)"
93+
current_body: list[str] = []
94+
func_pat = re.compile(r"^(def|class|async\s+def)\s+([A-Za-z_]\w*)")
95+
for line in lines:
96+
m = func_pat.match(line)
97+
if m:
98+
if current_body:
99+
chunks.append((current_label, "".join(current_body)))
100+
current_label = f"{m.group(1)} {m.group(2)}"
101+
current_body = [line]
102+
else:
103+
current_body.append(line)
104+
if current_body:
105+
chunks.append((current_label, "".join(current_body)))
106+
return chunks
107+
108+
109+
def chunk_by_size(content: str, max_chars: int = _DEFAULT_MAX_CHARS) -> list[tuple[str, str]]:
110+
"""Split content into max-N-char chunks, breaking at line
111+
boundaries when possible.
112+
113+
Returns list of (label, body) tuples. Label is 'chunk N'.
114+
"""
115+
if max_chars <= 0:
116+
return [("chunk 1", content)]
117+
chunks: list[tuple[str, str]] = []
118+
lines = content.splitlines(keepends=True)
119+
current_body: list[str] = []
120+
current_size = 0
121+
chunk_num = 1
122+
for line in lines:
123+
line_len = len(line)
124+
if current_body and (current_size + line_len) > max_chars:
125+
chunks.append((f"chunk {chunk_num}", "".join(current_body)))
126+
chunk_num += 1
127+
current_body = [line]
128+
current_size = line_len
129+
else:
130+
current_body.append(line)
131+
current_size += line_len
132+
if current_body:
133+
chunks.append((f"chunk {chunk_num}", "".join(current_body)))
134+
return chunks
135+
136+
137+
def _auto_strategy(content: str, path: str = "") -> str:
138+
"""Pick the strategy by content shape."""
139+
norm_path = (path or "").lower()
140+
if norm_path.endswith(".py"):
141+
return "functions"
142+
if norm_path.endswith((".md", ".rst", ".txt")):
143+
# Use headers if there are any; fall back to paragraphs
144+
if re.search(r"(?m)^#{2,}\s", content):
145+
return "headers"
146+
return "paragraphs"
147+
# Default: size-based
148+
return "size"
149+
150+
151+
def chunk(
152+
content: str, strategy: str = "auto", max_chars: int = _DEFAULT_MAX_CHARS, source_path: str = ""
153+
) -> list[tuple[str, str]]:
154+
"""Chunk content by named strategy.
155+
156+
Returns list of (label, body) tuples for downstream rendering.
157+
"""
158+
if strategy == "auto":
159+
strategy = _auto_strategy(content, source_path)
160+
if strategy == "headers":
161+
return chunk_by_headers(content)
162+
if strategy == "paragraphs":
163+
return chunk_by_paragraphs(content)
164+
if strategy == "functions":
165+
return chunk_by_functions(content)
166+
if strategy == "size":
167+
return chunk_by_size(content, max_chars=max_chars)
168+
raise ValueError(f"Unknown strategy: {strategy!r}")
169+
170+
171+
def format_oscillating(chunks: list[tuple[str, str]], source: str = "") -> str:
172+
"""Render chunks with section labels and pause markers between.
173+
174+
The pause markers are explicit '[PAUSE] COMPREHEND BEFORE CONTINUING'
175+
lines that force the reader to register each chunk discretely
176+
rather than streaming through. The point isn't decoration — it
177+
is breaking comprehension into discrete units the optimizer
178+
cannot fast-skim past as one block.
179+
"""
180+
parts: list[str] = []
181+
header = "=" * 60
182+
if source:
183+
parts.append(f"{header}\nOSCILLATING READ: {source}\n{header}")
184+
parts.append(
185+
f"\nTotal chunks: {len(chunks)}. "
186+
"Comprehend each before continuing. The middle is where the "
187+
"load-bearing thing usually lives.\n"
188+
)
189+
for i, (label, body) in enumerate(chunks, 1):
190+
section_header = f"\n--- CHUNK {i}/{len(chunks)}: {label} ---\n"
191+
parts.append(section_header)
192+
parts.append(body.rstrip())
193+
parts.append(
194+
f"\n[PAUSE] COMPREHEND CHUNK {i}/{len(chunks)} BEFORE CONTINUING "
195+
"— what is THIS chunk's load-bearing point?\n"
196+
)
197+
parts.append(
198+
f"\n{header}\nEnd of oscillating read. Comprehension test: "
199+
"if you can name the load-bearing point of each chunk above, "
200+
"the read landed. If chunks blur together, re-read.\n"
201+
)
202+
return "\n".join(parts)
203+
204+
205+
def oscillate_file(
206+
path: str | Path,
207+
strategy: str = "auto",
208+
max_chars: int = _DEFAULT_MAX_CHARS,
209+
) -> str:
210+
"""Read a file and return its oscillating-rendered form."""
211+
p = Path(path)
212+
content = p.read_text(encoding="utf-8", errors="replace")
213+
chunks = chunk(content, strategy=strategy, max_chars=max_chars, source_path=str(p))
214+
return format_oscillating(chunks, source=str(p))
215+
216+
217+
__all__ = [
218+
"chunk_by_headers",
219+
"chunk_by_paragraphs",
220+
"chunk_by_functions",
221+
"chunk_by_size",
222+
"chunk",
223+
"format_oscillating",
224+
"oscillate_file",
225+
]

0 commit comments

Comments
 (0)