Skip to content

Commit 957e095

Browse files
Arch review P0.3: link_pairs + canon_segments view-models
The warm-up that proves the review's core pattern — move shared derivation out of the shells into the core, shells only paint/route. Two additive endpoints retire duplication items 1 (weave link dedup) and 5 (canon bands). Core: - pure_core::weave::link_pairs(weaves) — the one canonical-pair dedup, with a unit test. GTK's build_links is deleted; both call sites call this. FFI (extern surface grew → bindings regenerated, surface check passes): - pure_engine_link_pairs_json: deduped pairs, each endpoint located (refKey + book/chapter/verse) + a per-pair `resolved` flag (same calc as weaves_to_wire), so no shell dedupes or parses ref keys. - pure_engine_canon_segments_json: the 8 bands + OT/NT divide straight from core::reference — the WinUI hardcode that had drifted is gone. - Wire DTOs + a golden round-trip test folded into parity_endpoints_via_abi (resolved vs dangling pair; 8 segments + divide 39). Release DLL rebuilt. WinUI: - RefreshStudyData consumes link_pairs (filtering resolved) — the shell-side HashSet dedup + ParseRef are gone; the xref gutter set falls out of the same pairs. - A single `Canon` holder, loaded once from the engine, feeds both the strip and the chord/constellation popups (which also read the old static bands). CanonStrip no longer hardcodes segments. dotnet build: 0 errors. Docs: manifest connectors + canon-strip sections, endpoints table, and the Compose delta; architecture review gains an execution-status log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 928afd1 commit 957e095

14 files changed

Lines changed: 374 additions & 61 deletions

File tree

ARCHITECTURE-REVIEW-2026-07-16.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,35 @@ Start with the cheap `link_pairs`/`canon_segments` extraction to prove the
250250
pattern, then the popup view-models, then the panel content model; split
251251
`main.rs` along the seams that opens up; and replace the prose parity contract
252252
with golden view-model tests and a compile-checked block enum.
253+
254+
---
255+
256+
## Execution status (updated 2026-07-16)
257+
258+
Being executed in the review's own order. Each shell-touching phase is landed,
259+
then handed to Glendon for manual UI testing before the next (GTK builds only
260+
in CI; WinUI is compile-checked here with `dotnet build`, never auto-driven).
261+
262+
- **In-flight rendering-lens/authority-tier work** — committed `928afd1`
263+
(was left uncommitted by a prior session; verified via cargo tests first).
264+
-**P0.3 warm-up (`link_pairs` + `canon_segments`, items 1 & 5)** — DONE.
265+
- Core: `pure_core::weave::link_pairs` (the shared dedup) + unit test.
266+
- FFI: `pure_engine_link_pairs_json` (deduped pairs, each endpoint located,
267+
`resolved` flag) + `pure_engine_canon_segments_json` (the frozen bands +
268+
OT/NT divide); wire DTOs; golden round-trip test in `parity_endpoints_via_abi`;
269+
bindings regenerated (48 fns, surface check passes); release DLL rebuilt.
270+
- GTK: `build_links` deleted → both call sites use `weave::link_pairs`.
271+
- WinUI: `RefreshStudyData` consumes `link_pairs` (no shell dedup / `ParseRef`);
272+
the `CanonStrip` hardcode is gone — a single `Canon` holder loaded once from
273+
the engine now feeds both the strip **and** the chord/constellation popups
274+
(which also statically read the old hardcode). `dotnet build`: 0 errors.
275+
- Docs: manifest connectors + canon-strip sections, endpoints table, and
276+
Compose delta updated.
277+
- **Awaiting**: Glendon's manual WinUI check that connectors + the canon strip
278+
+ the two map popups still render correctly.
279+
-**P0.2 popup view-models (chord / constellation / concept map, items 2–4)**
280+
NEXT. Note the `Canon` holder + `link_pairs` already removed two of the popups'
281+
shell-side derivations, so this phase now starts from a smaller surface.
282+
-**P0.1 panel content-model (items 6–8)** — the big one; de-risk per the plan.
283+
-**P1.4 link-router single source**, **P1.5 split `main.rs`**,
284+
**P2.6/7 cross-shell golden tests + shrink the manifest** — pending.

apps/desktop/src/main.rs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -185,21 +185,6 @@ fn build_xrefs(weaves: &[LoadedWeave]) -> HashMap<VRef, Vec<Xref>> {
185185
map
186186
}
187187

188-
/// All weave links as canonical, deduped verse pairs (core stores each link
189-
/// with `a <= b` in reading order already), for the ambient connector lines.
190-
fn build_links(weaves: &[LoadedWeave]) -> Vec<(VRef, VRef)> {
191-
let mut seen = HashSet::new();
192-
let mut out = Vec::new();
193-
for lw in weaves {
194-
for l in &lw.weave.links {
195-
if seen.insert((l.a.clone(), l.b.clone())) {
196-
out.push((l.a.clone(), l.b.clone()));
197-
}
198-
}
199-
}
200-
out
201-
}
202-
203188
/// Register the bundled EB Garamond with fontconfig for this process only (no
204189
/// change to the user's installed fonts). Returns the family to render in — the
205190
/// bundled face if it registered, else a plain serif fallback.
@@ -362,7 +347,7 @@ fn load_state(cfg: &Config) -> Result<State, String> {
362347
// are reported but don't fail the reader.
363348
let (weaves, _weave_errs) = weave::load_weaves(&home);
364349
let xrefs = build_xrefs(&weaves);
365-
let links = build_links(&weaves);
350+
let links = weave::link_pairs(&weaves);
366351
let (threads, _thread_errs) = thread::load_threads(&home);
367352
let (tags, _tag_errs) = tag::load_tags(&home);
368353
// TSK cross-references (topical study tier) — optional, absent → empty.
@@ -2229,7 +2214,7 @@ fn reload_weaves(state: &Shared) {
22292214
let home = state.borrow().home.clone();
22302215
let (weaves, _) = weave::load_weaves(&home);
22312216
let xrefs = build_xrefs(&weaves);
2232-
let links = build_links(&weaves);
2217+
let links = weave::link_pairs(&weaves);
22332218
let mut st = state.borrow_mut();
22342219
st.weaves = weaves;
22352220
st.xrefs = xrefs;

apps/windows/PureStudyWin/CanonStrip.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,25 @@
1010

1111
namespace PureStudyWin;
1212

13-
public sealed class CanonStrip : Microsoft.UI.Xaml.Controls.UserControl
13+
/// The canon overview segmentation — the 8 bands + OT/NT divide — fed once from
14+
/// the core view-model (pure_engine_canon_segments_json), frozen in
15+
/// core::reference. The single app-wide source read by both the strip and the
16+
/// map popups; nothing hardcodes the bands anymore (the old copies had drifted).
17+
public static class Canon
1418
{
15-
public static readonly (string Name, int First, int Last)[] Segments =
19+
public static CanonSegment[] Segments { get; private set; } = Array.Empty<CanonSegment>();
20+
public static int OtNtDivide { get; private set; }
21+
22+
/// Populate once at startup from the engine's canon view-model.
23+
public static void Set(CanonSegments cs)
1624
{
17-
("Law", 0, 4), ("History", 5, 16), ("Wisdom", 17, 21), ("Prophets", 22, 38),
18-
("Gospels", 39, 42), ("Acts", 43, 43), ("Letters", 44, 64), ("Revelation", 65, 65),
19-
};
20-
public const int OtNtDivide = 39;
25+
Segments = cs.Segments.ToArray();
26+
OtNtDivide = cs.OtNtDivide;
27+
}
28+
}
2129

30+
public sealed class CanonStrip : Microsoft.UI.Xaml.Controls.UserControl
31+
{
2232
/// Book order → (pin per pane, active flag). Set by the shell.
2333
public List<(int bookOrder, bool active)> Pins = new();
2434
private List<TocBook> _books = new();
@@ -58,24 +68,24 @@ private void OnDraw(CanvasControl sender, CanvasDrawEventArgs args)
5868
{
5969
var ds = args.DrawingSession;
6070
float w = (float)sender.ActualWidth, h = (float)sender.ActualHeight;
61-
if (w < 10) return;
71+
if (w < 10 || Canon.Segments.Length == 0) return;
6272

6373
using var labelFmt = new CanvasTextFormat { FontSize = 11 };
64-
for (int s = 0; s < Segments.Length; s++)
74+
for (int s = 0; s < Canon.Segments.Length; s++)
6575
{
66-
var seg = Segments[s];
76+
var seg = Canon.Segments[s];
6777
float x0 = seg.First / 66f * w;
6878
float x1 = (seg.Last + 1) / 66f * w;
6979
if (s % 2 == 1)
7080
ds.FillRectangle(x0, 0, x1 - x0, h, Color.FromArgb(10, 0, 0, 0));
71-
using var tl = new CanvasTextLayout(sender, seg.Name, labelFmt, 1e6f, 1e6f);
81+
using var tl = new CanvasTextLayout(sender, seg.Label, labelFmt, 1e6f, 1e6f);
7282
float tw = (float)tl.LayoutBounds.Width;
7383
if (tw <= x1 - x0 - 6)
7484
ds.DrawTextLayout(tl, x0 + (x1 - x0 - tw) / 2, (h - (float)tl.LayoutBounds.Height) / 2,
7585
Color.FromArgb(230, 89, 77, 56));
7686
}
7787
// OT/NT divide.
78-
float dx = OtNtDivide / 66f * w;
88+
float dx = Canon.OtNtDivide / 66f * w;
7989
ds.DrawLine(dx, 0, dx, h, Color.FromArgb(128, 102, 77, 51), 1f);
8090

8191
foreach (var (order, active) in Pins)

apps/windows/PureStudyWin/MainWindow.cs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ private async Task LoadEngineCoreAsync()
193193
_status.Text = "";
194194
ApplyMode(persist: false);
195195
ApplyVersePerLine(persist: false);
196+
// Canon bands from the core view-model — the single app-wide source for
197+
// the strip and the map popups; no shell hardcode (item 5).
198+
if (_engine?.CanonSegmentsJson() is { } csj)
199+
Canon.Set(Wire.Parse<CanonSegments>(csj));
196200
RefreshStudyData();
197201
_strip.SetBooks(_books);
198202
_panes[_active].Reader.Focus(FocusState.Programmatic);
@@ -348,21 +352,19 @@ private void RefreshStudyData()
348352
_weaves = Wire.Parse<WeaveLib>(json);
349353
_panel.Weaves = _weaves;
350354

351-
// GTK build_links: dedupe canonical pairs; build_xrefs: per-verse set.
352-
var pairs = new HashSet<(string, string)>();
353-
var xrefVerses = new HashSet<string>();
355+
// Connector pairs come from the core view-model (link_pairs): deduped,
356+
// canonical, each endpoint located — no shell-side dedup or ref parsing.
357+
// Only resolved pairs are drawable; their endpoints seed the reader's
358+
// xref gutter set (the same set GTK's build_xrefs feeds the gutter).
354359
var links = new List<LinkPair>();
355-
foreach (var w in _weaves.Weaves)
356-
foreach (var l in w.Links)
360+
var xrefVerses = new HashSet<string>();
361+
if (_engine.LinkPairsJson() is { } lpj)
362+
foreach (var p in Wire.Parse<LinkPairs>(lpj).Pairs)
357363
{
358-
if (!l.Resolved) continue;
359-
xrefVerses.Add(l.A);
360-
xrefVerses.Add(l.B);
361-
var key = string.CompareOrdinal(l.A, l.B) <= 0 ? (l.A, l.B) : (l.B, l.A);
362-
if (!pairs.Add(key)) continue;
363-
if (ParseRef(key.Item1) is not { } ra || ParseRef(key.Item2) is not { } rb)
364-
continue;
365-
links.Add(new LinkPair(key.Item1, key.Item2, ra.book, ra.ch, rb.book, rb.ch));
364+
if (!p.Resolved) continue;
365+
links.Add(new LinkPair(p.A, p.B, p.ABook, p.AChapter, p.BBook, p.BChapter));
366+
xrefVerses.Add(p.A);
367+
xrefVerses.Add(p.B);
366368
}
367369
_connectors.Links = links;
368370
foreach (var p in _panes)
@@ -374,14 +376,6 @@ private void RefreshStudyData()
374376
UpdateLinkButton();
375377
}
376378

377-
private static (string book, uint ch)? ParseRef(string refKey)
378-
{
379-
int sp = refKey.LastIndexOf(' ');
380-
if (sp < 0) return null;
381-
var cv = refKey[(sp + 1)..].Split(':');
382-
return uint.TryParse(cv[0], out var ch) ? (refKey[..sp], ch) : null;
383-
}
384-
385379
// ── header wiring ──────────────────────────────────────────────────────
386380

387381
private void WireHeader(Grid root)

apps/windows/PureStudyWin/Popups.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,12 @@ public static void ChordMap(List<LinkPair> links, List<TocBook> books, Action<st
122122
float y0 = h - 26;
123123

124124
using var labelFmt = new CanvasTextFormat { FontSize = 10 };
125-
for (int i = 0; i < CanonStrip.Segments.Length; i++)
125+
for (int i = 0; i < Canon.Segments.Length; i++)
126126
{
127-
var seg = CanonStrip.Segments[i];
127+
var seg = Canon.Segments[i];
128128
float x0 = seg.First / 66f * w, x1 = (seg.Last + 1) / 66f * w;
129129
if (i % 2 == 1) ds.FillRectangle(x0, 0, x1 - x0, y0, Color.FromArgb(10, 0, 0, 0));
130-
ds.DrawText(seg.Name, new Vector2(x0 + 3, y0 + 6),
130+
ds.DrawText(seg.Label, new Vector2(x0 + 3, y0 + 6),
131131
Color.FromArgb(230, 89, 77, 56), labelFmt);
132132
}
133133
// Book ticks anchor the ribbon feet.
@@ -137,12 +137,12 @@ public static void ChordMap(List<LinkPair> links, List<TocBook> books, Action<st
137137
ds.DrawLine(tx, y0, tx, y0 - 4, Color.FromArgb(60, 89, 77, 56), 1f);
138138
}
139139
ds.DrawLine(0, y0, w, y0, Color.FromArgb(128, 158, 125, 56), 1.5f);
140-
float seam = CanonStrip.OtNtDivide / 66f * w;
140+
float seam = Canon.OtNtDivide / 66f * w;
141141
ds.DrawLine(seam, 0, seam, y0, Color.FromArgb(128, 102, 77, 51), 1f);
142142

143143
Color ArcColor((int ia, int ib) k, float frac, bool hot)
144144
{
145-
bool aOt = k.ia < CanonStrip.OtNtDivide, bOt = k.ib < CanonStrip.OtNtDivide;
145+
bool aOt = k.ia < Canon.OtNtDivide, bOt = k.ib < Canon.OtNtDivide;
146146
var (r, g, b2) = aOt && bOt ? (0.72f, 0.57f, 0.24f)
147147
: !aOt && !bOt ? (0.30f, 0.53f, 0.78f)
148148
: (0.58f, 0.38f, 0.70f);
@@ -319,13 +319,13 @@ public static void Constellation(StudyEngine engine, WeaveLib lib, List<TocBook>
319319
if (i % 2 == 0)
320320
ds.FillRectangle(0, ConstTopPad + i * laneH, w, laneH, Color.FromArgb(8, 0, 0, 0));
321321

322-
foreach (var seg in CanonStrip.Segments)
322+
foreach (var seg in Canon.Segments)
323323
{
324324
float x = PlotLeft + seg.First / 66f * (w - PlotLeft);
325325
ds.DrawLine(x, ConstTopPad, x, h, Color.FromArgb(26, 0, 0, 0), 1f);
326-
ds.DrawText(seg.Name, new Vector2(x + 2, 2), Color.FromArgb(180, 89, 77, 56), rulerFmt);
326+
ds.DrawText(seg.Label, new Vector2(x + 2, 2), Color.FromArgb(180, 89, 77, 56), rulerFmt);
327327
}
328-
float seam = PlotLeft + CanonStrip.OtNtDivide / 66f * (w - PlotLeft);
328+
float seam = PlotLeft + Canon.OtNtDivide / 66f * (w - PlotLeft);
329329
ds.DrawLine(seam, ConstTopPad, seam, h, Color.FromArgb(153, 158, 125, 56), 1f);
330330

331331
for (int lane = 0; lane < shown.Count; lane++)
@@ -611,7 +611,7 @@ string Label(string c)
611611
}
612612
bi++;
613613
}
614-
float seam = CanonStrip.OtNtDivide / 66f * w;
614+
float seam = Canon.OtNtDivide / 66f * w;
615615
ds.DrawLine(seam, y0, seam, h, Color.FromArgb(128, 102, 77, 51), 1f);
616616
}
617617
};

apps/windows/PureStudyWin/Wire.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ public sealed record WeaveLink1(
9797
string A, string ADisplay, string B, string BDisplay, string Label,
9898
bool Approved, ushort[]? SpanA, ushort[]? SpanB, bool Resolved);
9999

100+
// Deduped canonical weave pairs from the core view-model
101+
// (pure_engine_link_pairs_json): each endpoint located + a resolvability flag,
102+
// so the connector layer neither dedupes nor parses ref keys itself.
103+
public sealed record LinkPairs(List<WeaveLinkPair> Pairs);
104+
public sealed record WeaveLinkPair(
105+
string A, string ABook, ushort AChapter, ushort AVerse,
106+
string B, string BBook, ushort BChapter, ushort BVerse, bool Resolved);
107+
108+
// The canon overview segmentation (pure_engine_canon_segments_json), frozen in
109+
// core::reference — the strip reads it instead of hardcoding the 8 bands.
110+
public sealed record CanonSegments(List<CanonSegment> Segments, int OtNtDivide);
111+
public sealed record CanonSegment(string Label, int First, int Last);
112+
100113
public sealed record Concept1(
101114
string Code, uint Total, uint Ot, uint Nt, List<BookCount> TopBooks,
102115
Dictionary<string, uint> ByBook, List<Scored> Collocates,

crates/core/src/weave.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,29 @@ pub struct LoadedWeave {
479479
pub weave: Weave,
480480
}
481481

482+
/// All weave links across the library as canonical, deduped verse pairs. Each
483+
/// stored link is already ordered `a <= b` in reading order (see
484+
/// [`Link::canon_span`]), so the pair is taken as-is and a `HashSet` drops the
485+
/// duplicates a verse pair linked by several weaves would produce.
486+
///
487+
/// This is the one derivation behind the ambient connector lines and the chord
488+
/// map, shared by every shell: GTK calls it directly; the non-Rust shells
489+
/// receive the same pairs (with each endpoint resolved and located) through
490+
/// `pure_engine_link_pairs_json`. Resolvability against the corpus is a
491+
/// separate, drawing-time concern and is not filtered here.
492+
pub fn link_pairs(weaves: &[LoadedWeave]) -> Vec<(VRef, VRef)> {
493+
let mut seen = HashSet::new();
494+
let mut out = Vec::new();
495+
for lw in weaves {
496+
for l in &lw.weave.links {
497+
if seen.insert((l.a.clone(), l.b.clone())) {
498+
out.push((l.a.clone(), l.b.clone()));
499+
}
500+
}
501+
}
502+
out
503+
}
504+
482505
/// Slug a weave name into a JSON filename under `dir`. Ported from
483506
/// `weaveFileIn`.
484507
pub fn weave_file_in(dir: impl AsRef<Path>, name: &str) -> std::path::PathBuf {
@@ -802,6 +825,37 @@ mod tests {
802825
assert_eq!(w.approved_count(), 1);
803826
}
804827

828+
#[test]
829+
fn link_pairs_dedupe_canonically_across_weaves() {
830+
// Two weaves that share one link (Gen 1:1–John 1:1, endpoints given in
831+
// opposite order) plus one unique link each.
832+
let mut w1 = Weave::empty("a", WeaveKind::Retelling, "kjv1769-tok2", "now");
833+
w1.add_links([
834+
Link::canon(r("Gen", 1, 1), r("John", 1, 1)),
835+
Link::canon(r("Gen", 2, 4), r("Matt", 19, 4)),
836+
]);
837+
let mut w2 = Weave::empty("b", WeaveKind::Quotation, "kjv1769-tok2", "now");
838+
w2.add_links([
839+
Link::canon(r("John", 1, 1), r("Gen", 1, 1)), // same pair, reversed
840+
Link::canon(r("Exod", 12, 3), r("Rev", 5, 6)),
841+
]);
842+
let loaded = vec![
843+
LoadedWeave { file: "a.json".into(), weave: w1 },
844+
LoadedWeave { file: "b.json".into(), weave: w2 },
845+
];
846+
847+
let pairs = link_pairs(&loaded);
848+
// The shared pair appears once → three pairs total.
849+
assert_eq!(pairs.len(), 3);
850+
assert!(pairs.contains(&(r("Gen", 1, 1), r("John", 1, 1))));
851+
assert!(pairs.contains(&(r("Gen", 2, 4), r("Matt", 19, 4))));
852+
assert!(pairs.contains(&(r("Exod", 12, 3), r("Rev", 5, 6))));
853+
// Every pair is stored canonically (a <= b in reading order).
854+
for (a, b) in &pairs {
855+
assert!(a.reading_key() <= b.reading_key());
856+
}
857+
}
858+
805859
#[test]
806860
fn components_are_transitive() {
807861
// A-B and B-C should form one component {A,B,C}; D-E a second.

crates/ffi/bindings/csharp/PureStudy.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ public uint ChapterCount(string book)
123123
public string? SuggestedWeavesJson() => Utf8.Take(PureStudyNative.pure_engine_suggested_weaves_json(_handle));
124124
public string? WeavesJson() => Utf8.Take(PureStudyNative.pure_engine_weaves_json(_handle));
125125

126+
/// Deduped canonical weave pairs (the connector lines + chord map), each
127+
/// endpoint located and flagged resolved — the shell no longer dedupes or
128+
/// parses ref keys itself.
129+
public string? LinkPairsJson() => Utf8.Take(PureStudyNative.pure_engine_link_pairs_json(_handle));
130+
131+
/// The canon overview segmentation (8 bands + OT/NT divide), frozen in
132+
/// core::reference — the shell reads it instead of hardcoding the bands.
133+
public string? CanonSegmentsJson() => Utf8.Take(PureStudyNative.pure_engine_canon_segments_json(_handle));
134+
126135
public string? VerseXrefsJson(string refKey)
127136
{
128137
fixed (byte* r = Utf8.NulTerminated(refKey))

crates/ffi/bindings/csharp/PureStudyNative.g.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,32 @@ public static unsafe partial class PureStudyNative
469469
[DllImport(__DllName, EntryPoint = "pure_engine_weaves_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
470470
public static extern byte* pure_engine_weaves_json(PureEngine* engine);
471471

472+
/// <summary>
473+
/// Every weave link as a deduped canonical verse pair, each endpoint located
474+
/// (ref key + book/chapter/verse) and flagged `resolved` when both ends are in
475+
/// the corpus. The one derivation behind the ambient connector lines and the
476+
/// chord map, so a shell neither dedupes nor parses ref keys itself. Never null
477+
/// on a live engine (empty library → empty list).
478+
///
479+
/// # Safety
480+
/// `engine` is a live engine (or null → null).
481+
/// </summary>
482+
[DllImport(__DllName, EntryPoint = "pure_engine_link_pairs_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
483+
public static extern byte* pure_engine_link_pairs_json(PureEngine* engine);
484+
485+
/// <summary>
486+
/// The canon overview segmentation: the 8 sections as `(label, first, last)`
487+
/// book indices over the 66 books, plus the OT/NT divide (39). Static data
488+
/// frozen in `core::reference` — served here so a non-Rust shell consumes the
489+
/// one source instead of re-hardcoding the bands. Never null on a live engine.
490+
///
491+
/// # Safety
492+
/// `engine` is a live engine (or null → null); the payload does not depend on
493+
/// engine state, but the arg keeps the call shape uniform.
494+
/// </summary>
495+
[DllImport(__DllName, EntryPoint = "pure_engine_canon_segments_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
496+
public static extern byte* pure_engine_canon_segments_json(PureEngine* engine);
497+
472498
/// <summary>
473499
/// The symbolic concept engine's view of a Strong's code: occurrence total,
474500
/// testament split, concentrating books, per-book dispersion counts,

0 commit comments

Comments
 (0)