okstra 0.131.1 → 0.132.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.131.1",
3
+ "version": "0.132.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.131.1",
3
- "builtAt": "2026-07-22T05:33:21.666Z",
2
+ "package": "0.132.0",
3
+ "builtAt": "2026-07-22T06:44:24.538Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -308,6 +308,66 @@ def _build_index(headings: list, definitions: list, labels: dict) -> list[str]:
308
308
  return block
309
309
 
310
310
 
311
+ # --- Prose ventilation post-render pass -----------------------------------
312
+ # Split prose paragraphs at sentence boundaries so raw markdown reads one
313
+ # sentence per line; the HTML view turns those newlines into <br>
314
+ # (report_views._markdown_to_html). Prose only — table cells already carry
315
+ # their own <br>, and headings / lists / code / blockquotes must stay intact.
316
+ # A leading-whitespace line is a list continuation, not a prose paragraph:
317
+ # only column-0 non-block lines are treated as prose (allowlist).
318
+ _LIST_LINE_RE = re.compile(r"^\s*(?:[-*]\s|\d+\.\s)")
319
+ _SENT_BOUNDARY_RE = re.compile(r"[.?!][ \t]+(?=\S)")
320
+ # Trailing tokens whose dot is not a sentence end (compared lowercased with
321
+ # the trailing dot stripped).
322
+ _ABBREV = {
323
+ "e.g", "i.e", "etc", "cf", "vs", "al", "no", "fig",
324
+ "dr", "mr", "ms", "inc", "ltd", "jr", "sr",
325
+ }
326
+
327
+
328
+ def _ventilate_segment(seg: str) -> str:
329
+ def replace_boundary(match: re.Match) -> str:
330
+ mark = match.start()
331
+ prev = seg[mark - 1] if mark > 0 else ""
332
+ # A number, a bare-digit, or an ellipsis dot is not a sentence end.
333
+ if not prev or prev.isspace() or prev.isdigit() or prev == ".":
334
+ return match.group(0)
335
+ tail = re.search(r"[A-Za-z.]+$", seg[: mark + 1])
336
+ if tail and tail.group(0).rstrip(".").lower() in _ABBREV:
337
+ return match.group(0)
338
+ return seg[mark] + "\n"
339
+
340
+ return _SENT_BOUNDARY_RE.sub(replace_boundary, seg)
341
+
342
+
343
+ def _split_prose_line(line: str) -> str:
344
+ parts = line.split("`")
345
+ for even in range(0, len(parts), 2): # segments outside backtick code spans
346
+ parts[even] = _ventilate_segment(parts[even])
347
+ return "`".join(parts)
348
+
349
+
350
+ def _ventilate_prose(markdown: str) -> str:
351
+ """Break prose paragraphs into one sentence per line. Idempotent: a line
352
+ already holding a single sentence has no internal boundary to split."""
353
+ lines = markdown.split("\n")
354
+ mask = _code_line_mask(lines)
355
+ for i, line in enumerate(lines):
356
+ if mask[i]: # frontmatter / fenced code
357
+ continue
358
+ if not line or line != line.lstrip(): # blank or indented continuation
359
+ continue
360
+ if (
361
+ _HEADING_RE.match(line)
362
+ or line.startswith("|")
363
+ or line.startswith(">")
364
+ or _LIST_LINE_RE.match(line)
365
+ ):
366
+ continue
367
+ lines[i] = _split_prose_line(line)
368
+ return "\n".join(lines)
369
+
370
+
311
371
  def _inject_index_and_anchors(markdown: str, dictionary: dict | None) -> str:
312
372
  """Append scroll anchors + a top-of-report index to a rendered report.
313
373
  Idempotent: re-running on already-anchored markdown is a no-op for
@@ -500,7 +560,8 @@ def render(
500
560
  try:
501
561
  template = env.get_template(template_path.name)
502
562
  rendered = template.render(**data)
503
- return _inject_index_and_anchors(rendered, dictionary)
563
+ rendered = _inject_index_and_anchors(rendered, dictionary)
564
+ return _ventilate_prose(rendered)
504
565
  except I18nError as exc:
505
566
  raise FinalReportRenderError(
506
567
  f"i18n lookup failed while rendering {template_path.name}: {exc}"
@@ -399,7 +399,9 @@ def _markdown_to_html(
399
399
  para_lines.append(ln)
400
400
  i += 1
401
401
  if para_lines:
402
- out.append("<p>" + _inline(" ".join(para_lines)) + "</p>")
402
+ # The renderer's prose ventilation puts one sentence per source
403
+ # line; join with <br> so those breaks survive into the HTML.
404
+ out.append("<p>" + _inline("<br>".join(para_lines)) + "</p>")
403
405
 
404
406
  return "\n".join(out), headings
405
407