cjm-transcript-decomp-core 0.0.9__tar.gz → 0.0.10__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 (31) hide show
  1. {cjm_transcript_decomp_core-0.0.9/cjm_transcript_decomp_core.egg-info → cjm_transcript_decomp_core-0.0.10}/PKG-INFO +6 -3
  2. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/README.md +5 -2
  3. cjm_transcript_decomp_core-0.0.10/cjm_transcript_decomp_core/__init__.py +1 -0
  4. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/cli.py +103 -0
  5. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/graph.py +57 -2
  6. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10/cjm_transcript_decomp_core.egg-info}/PKG-INFO +6 -3
  7. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_graph.py +32 -0
  8. cjm_transcript_decomp_core-0.0.9/cjm_transcript_decomp_core/__init__.py +0 -1
  9. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/LICENSE +0 -0
  10. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/MANIFEST.in +0 -0
  11. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/alignment.py +0 -0
  12. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/discovery.py +0 -0
  13. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/launch.py +0 -0
  14. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/models.py +0 -0
  15. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/pipeline.py +0 -0
  16. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/runs.py +0 -0
  17. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/segments.py +0 -0
  18. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/state.py +0 -0
  19. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/SOURCES.txt +0 -0
  20. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/dependency_links.txt +0 -0
  21. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/entry_points.txt +0 -0
  22. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/requires.txt +0 -0
  23. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/top_level.txt +0 -0
  24. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/pyproject.toml +0 -0
  25. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/setup.cfg +0 -0
  26. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_alignment.py +0 -0
  27. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_batch_logic.py +0 -0
  28. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_cli.py +0 -0
  29. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_models.py +0 -0
  30. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_pipeline.py +0 -0
  31. {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_segment_logic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-decomp-core
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: A frontend-agnostic core for the transcript decomposition workflow — composes isolated capability workers (forced alignment, VAD, graph storage) into a headless pipeline that decomposes transcription run manifests into a VAD-aligned context-graph spine with traceable provenance, and a CLI as its first driver.
5
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -26,7 +26,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
26
26
 
27
27
  ## Modules
28
28
 
29
- - **`cjm_transcript_decomp_core`**
29
+ - **`cjm_transcript_decomp_core.__init__`**
30
30
  - **`cjm_transcript_decomp_core.alignment`** — Pure forced-alignment logic (no capability calls): map FA words back to character spans in the original text, assign words to VAD chunks by timestamp, and build one text segment per VAD chunk. Extracted from the page-centric ForcedAlignmentService (Tier-1 logic).
31
31
  - **`cjm_transcript_decomp_core.cli`** — The CLI driver — the decomposition core's first (and currently only) frontend.
32
32
  - **`cjm_transcript_decomp_core.discovery`** — Capability-role discovery by manifest surface match — the journaling-by-
@@ -53,6 +53,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
53
53
 
54
54
  ### `cjm_transcript_decomp_core.cli`
55
55
 
56
+ - `backfill_provenance_command` _function_ — Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
56
57
  - `build_parser` _function_ — Build the CLI parser (subcommands: run).
57
58
  - `load_capabilities` _function_ — Discover manifests + load each requested capability (default instance).
58
59
  - `main` _function_ — CLI entry point (console script: `cjm-transcript-decomp-core`).
@@ -67,7 +68,9 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
67
68
 
68
69
  - `SourceVerification` _class_ — Skeptical-lens verification of one Source's fine-spine extension under a
69
70
  - `build_extension_payload` _function_ — Build the fine-spine EXTENSION payload (pure; no capability calls).
71
+ - `provenance_edges_from_segment_wire` _function_ — Derive a Segment's Transcript provenance edges from the node itself:
70
72
  - `resolve_root_ids` _function_ — Recompute the transcription-emitted root node ids from manifest data.
73
+ - `segment_provenance_edges` _function_ — Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
71
74
  - `verify_source` _function_ — Verify a Source's committed extension via server-side AGGREGATES (D13/D19).
72
75
 
73
76
  ### `cjm_transcript_decomp_core.launch`
@@ -141,4 +144,4 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
141
144
  ## Dependencies
142
145
 
143
146
  **Depends on:** `cjm-capability-primitives`, `cjm-context-graph-layer`, `cjm-context-graph-primitives`, `cjm-substrate`, `cjm-transcript-graph-schema`, `pyyaml`
144
- **Used by:** `cjm-transcript-decomp-qt`, `cjm-transcript-decomp-tui`
147
+ **Used by:** `cjm-transcript-decomp-qt`
@@ -6,7 +6,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
6
6
 
7
7
  ## Modules
8
8
 
9
- - **`cjm_transcript_decomp_core`**
9
+ - **`cjm_transcript_decomp_core.__init__`**
10
10
  - **`cjm_transcript_decomp_core.alignment`** — Pure forced-alignment logic (no capability calls): map FA words back to character spans in the original text, assign words to VAD chunks by timestamp, and build one text segment per VAD chunk. Extracted from the page-centric ForcedAlignmentService (Tier-1 logic).
11
11
  - **`cjm_transcript_decomp_core.cli`** — The CLI driver — the decomposition core's first (and currently only) frontend.
12
12
  - **`cjm_transcript_decomp_core.discovery`** — Capability-role discovery by manifest surface match — the journaling-by-
@@ -33,6 +33,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
33
33
 
34
34
  ### `cjm_transcript_decomp_core.cli`
35
35
 
36
+ - `backfill_provenance_command` _function_ — Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
36
37
  - `build_parser` _function_ — Build the CLI parser (subcommands: run).
37
38
  - `load_capabilities` _function_ — Discover manifests + load each requested capability (default instance).
38
39
  - `main` _function_ — CLI entry point (console script: `cjm-transcript-decomp-core`).
@@ -47,7 +48,9 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
47
48
 
48
49
  - `SourceVerification` _class_ — Skeptical-lens verification of one Source's fine-spine extension under a
49
50
  - `build_extension_payload` _function_ — Build the fine-spine EXTENSION payload (pure; no capability calls).
51
+ - `provenance_edges_from_segment_wire` _function_ — Derive a Segment's Transcript provenance edges from the node itself:
50
52
  - `resolve_root_ids` _function_ — Recompute the transcription-emitted root node ids from manifest data.
53
+ - `segment_provenance_edges` _function_ — Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
51
54
  - `verify_source` _function_ — Verify a Source's committed extension via server-side AGGREGATES (D13/D19).
52
55
 
53
56
  ### `cjm_transcript_decomp_core.launch`
@@ -121,4 +124,4 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
121
124
  ## Dependencies
122
125
 
123
126
  **Depends on:** `cjm-capability-primitives`, `cjm-context-graph-layer`, `cjm-context-graph-primitives`, `cjm-substrate`, `cjm-transcript-graph-schema`, `pyyaml`
124
- **Used by:** `cjm-transcript-decomp-qt`, `cjm-transcript-decomp-tui`
127
+ **Used by:** `cjm-transcript-decomp-qt`
@@ -0,0 +1 @@
1
+ __version__ = "0.0.10"
@@ -27,9 +27,13 @@ import os
27
27
  from pathlib import Path
28
28
  from typing import Any, Dict, List, Optional
29
29
 
30
+ from cjm_context_graph_layer.journal import journal_extend, sidecar_journal_path
31
+ from cjm_context_graph_layer.ops import graph_task
32
+ from cjm_context_graph_primitives.query import NodeQuery
30
33
  from cjm_substrate.core.manager import CapabilityManager
31
34
  from cjm_substrate.core.queue import JobQueue
32
35
  from cjm_substrate.core.workspace import resolve_workspace
36
+ from cjm_transcript_decomp_core.graph import provenance_edges_from_segment_wire
33
37
  from cjm_transcript_decomp_core.models import DecompConfig
34
38
  from cjm_transcript_decomp_core.pipeline import load_source_manifest, run_decomp
35
39
 
@@ -107,6 +111,29 @@ def build_parser() -> argparse.ArgumentParser: # Configured CLI parser
107
111
  run.add_argument("--actor", default=None,
108
112
  help="Journal attribution for who/what initiated this run (default: cli:<username>)")
109
113
  run.add_argument("-v", "--verbose", action="store_true", help="DEBUG-level logging")
114
+
115
+ # ---- backfill-provenance: Segment -> Transcript DERIVED_FROM edges (finding 89b16be6) ----
116
+ bp = sub.add_parser(
117
+ "backfill-provenance",
118
+ help="Mint Segment -> Transcript DERIVED_FROM edges for Segments already on the graph "
119
+ "(text provenance as an edge, not only text_from metadata); journaled per source "
120
+ "under spine-extension, idempotent on re-run")
121
+ bp.add_argument("--source", default=None,
122
+ help="Restrict to one Source node id (prefix ok; default: every Segment)")
123
+ bp.add_argument("--page-size", type=int, default=2000,
124
+ help="Segments per query page (default 2000)")
125
+ bp.add_argument("--manifests-dir", default=".cjm/manifests", help="Capability manifests directory")
126
+ bp.add_argument("--graph-capability", default="cjm-capability-graph-sqlite",
127
+ help="Graph-storage capability name")
128
+ bp.add_argument("--graph-db-path", default=None,
129
+ help="Graph db path (default: the workspace capability config)")
130
+ bp.add_argument("--workspace", default=None,
131
+ help="Workspace root (default: CJM_WORKSPACE env, else upward walk from cwd)")
132
+ bp.add_argument("--actor", default=None,
133
+ help="Journal attribution (default: cli:<username>)")
134
+ bp.add_argument("--dry-run", action="store_true",
135
+ help="Count the edges per source; touch neither graph nor journal")
136
+ bp.add_argument("-v", "--verbose", action="store_true", help="DEBUG-level logging")
110
137
  return parser
111
138
 
112
139
 
@@ -263,4 +290,80 @@ def main(
263
290
  )
264
291
  if args.command == "run":
265
292
  return asyncio.run(run_command(args))
293
+ if args.command == "backfill-provenance":
294
+ return asyncio.run(backfill_provenance_command(args))
266
295
  raise SystemExit(f"unknown command: {args.command}")
296
+
297
+
298
+ async def backfill_provenance_command(
299
+ args: argparse.Namespace, # Parsed CLI arguments for the `backfill-provenance` subcommand
300
+ ) -> int: # Process exit code
301
+ """Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
302
+ edges (finding 89b16be6) for Segments already on the graph, deriving each
303
+ segment's edges from its own refs (`provenance_edges_from_segment_wire`,
304
+ the same derivation a fresh decomposition uses). Journaled per SOURCE under
305
+ the existing `spine-extension` verb (edges-only wires; the replay handler
306
+ is already registered, and `journal_extend`'s added-delta trim keeps a
307
+ re-run silent), args carry act=provenance-backfill so the ops read as what
308
+ they are. Pages the Segment table so a 100k-segment graph never sits in one
309
+ query; --source narrows to one Source id (prefix ok); --dry-run counts."""
310
+ ws = resolve_workspace(explicit=getattr(args, "workspace", None))
311
+ if ws is not None:
312
+ os.environ["CJM_WORKSPACE"] = str(ws.root)
313
+ manager = CapabilityManager(search_paths=[Path(args.manifests_dir)])
314
+ configs = ({args.graph_capability: {"db_path": args.graph_db_path}}
315
+ if args.graph_db_path else None)
316
+ load_capabilities(manager, [args.graph_capability], configs=configs)
317
+ effective = args.graph_db_path or (
318
+ (manager.instances[args.graph_capability].config or {}).get("db_path"))
319
+ if not effective:
320
+ raise SystemExit("no graph db path: pass --graph-db-path, or persist one on the "
321
+ f"{args.graph_capability} instance in the active workspace's config store")
322
+ journal_path = sidecar_journal_path(str(effective))
323
+ actor = args.actor or f"cli:{getpass.getuser()}"
324
+ queue = JobQueue(deps=manager)
325
+ await queue.start()
326
+ try:
327
+ per_source: Dict[str, List[Dict[str, Any]]] = {}
328
+ seg_counts: Dict[str, int] = {}
329
+ offset = 0
330
+ while True:
331
+ nq = NodeQuery(label="Segment", project=["text_from", "source_id", "sources"],
332
+ limit=args.page_size, offset=offset)
333
+ res = await graph_task(queue, args.graph_capability, "query_nodes", query=nq.to_dict())
334
+ rows = list(res.rows or [])
335
+ for row in rows:
336
+ sid = row.get("source_id") or "?"
337
+ if args.source and not sid.startswith(args.source):
338
+ continue
339
+ seg_counts[sid] = seg_counts.get(sid, 0) + 1
340
+ per_source.setdefault(sid, []).extend(provenance_edges_from_segment_wire(row))
341
+ if len(rows) < args.page_size:
342
+ break
343
+ offset += len(rows)
344
+ total_edges = sum(len(v) for v in per_source.values())
345
+ if args.dry_run:
346
+ for sid in sorted(per_source):
347
+ print(f"{sid[:8]} segments={seg_counts[sid]} edges={len(per_source[sid])}")
348
+ print(f"dry run: {len(per_source)} sources, {sum(seg_counts.values())} segments, "
349
+ f"{total_edges} DERIVED_FROM edges (nothing written)")
350
+ return 0
351
+ added = 0
352
+ for sid in sorted(per_source):
353
+ edges = per_source[sid]
354
+ if not edges:
355
+ continue
356
+ r = await journal_extend(queue, args.graph_capability, [], edges,
357
+ journal_path=journal_path, verb="spine-extension",
358
+ actor=actor,
359
+ args={"act": "provenance-backfill", "source_id": sid,
360
+ "segments": seg_counts[sid], "edges": len(edges)})
361
+ added += r.edges_added
362
+ print(f"{sid[:8]} segments={seg_counts[sid]} edges={len(edges)} added={r.edges_added}")
363
+ finally:
364
+ await queue.stop()
365
+ manager.unload_capability(args.graph_capability)
366
+ print(f"backfilled DERIVED_FROM edges: {added} added of {total_edges} derived "
367
+ f"across {len(per_source)} sources (actor {actor})")
368
+ print(f"journal: {journal_path}")
369
+ return 0
@@ -3,7 +3,8 @@
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Any, Dict, List, Optional, Tuple
5
5
 
6
- from cjm_context_graph_layer.grammar import grouped_spine_edges, SpineRelations
6
+ from cjm_context_graph_layer.grammar import (grouped_spine_edges, make_edge, OverlayRelations,
7
+ SpineRelations)
7
8
  from cjm_context_graph_layer.ops import graph_task
8
9
  from cjm_context_graph_primitives.graph import GraphNode
9
10
  from cjm_context_graph_primitives.provenance import SourceRef
@@ -80,7 +81,9 @@ def build_extension_payload(
80
81
  transcriber variant; `text_from` is recorded per segment as provenance
81
82
  (which Transcript the layer-0 text came from), never as global config. Edges
82
83
  come from `grouped_spine_edges`: PART_OF to the OWNING AudioRendition,
83
- STARTS_WITH per rendition, NEXT chained source-wide across coarse boundaries.
84
+ STARTS_WITH per rendition, NEXT chained source-wide across coarse boundaries —
85
+ plus one DERIVED_FROM per sliced Transcript (text provenance as an edge,
86
+ finding 89b16be6; role=text_from marks the authoritative one).
84
87
  """
85
88
  roots = resolve_root_ids(source_entry, capabilities_info)
86
89
  source_id = roots["source"]
@@ -114,6 +117,13 @@ def build_extension_payload(
114
117
 
115
118
  # The fine spine hangs under each segment's RENDITION (PART_OF rendition).
116
119
  edges = grouped_spine_edges([(asegs[i]["rendition"], groups[i]) for i in sorted(groups)])
120
+ # Text provenance is an EDGE, not only a property (finding 89b16be6): each
121
+ # Segment DERIVED_FROM every Transcript it slices (role=text_from on the
122
+ # authoritative one), so Segments are reachable from their Transcript by
123
+ # traversal the way they already were from their AudioRendition. Derived
124
+ # from the node's own refs — the same derivation the backfill verb uses.
125
+ for node in nodes:
126
+ edges.extend(provenance_edges_from_segment_wire(node))
117
127
  ids = {"source": source_id, "segments": seg_ids,
118
128
  "audio_segments": [a["audio_segment"] for a in asegs],
119
129
  "renditions": [a["rendition"] for a in asegs],
@@ -273,3 +283,48 @@ async def verify_source(
273
283
  all_have_sources=missing_sources == 0,
274
284
  source_locators=sorted(locators),
275
285
  )
286
+
287
+
288
+ def segment_provenance_edges(
289
+ segment_id: str, # The Segment node id
290
+ transcript_ids: List[str], # Transcript node ids the segment slices text from (any order; de-duplicated)
291
+ text_from: Optional[str] = None, # The authoritative Transcript id (None when the segment carries no text)
292
+ ) -> List[Dict[str, Any]]: # DERIVED_FROM edge wires, Segment -> Transcript, one per distinct transcript
293
+ """Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
294
+ instance): a Segment's text comes from Transcript nodes, and until now that
295
+ link lived only in `text_from` metadata + the CharSlice source refs, so a
296
+ Segment was unreachable from its Transcript by traversal (the only
297
+ traceability edge went to the AudioRendition). One DERIVED_FROM per sliced
298
+ Transcript; the authoritative one carries role=text_from, the rest
299
+ role=variant. Ids are deterministic from the triple, so a backfill over
300
+ existing Segments and a fresh decomposition converge on the same edges."""
301
+ seen: set = set()
302
+ edges: List[Dict[str, Any]] = []
303
+ for tid in transcript_ids:
304
+ if not tid or tid in seen:
305
+ continue
306
+ seen.add(tid)
307
+ role = "text_from" if tid == text_from else "variant"
308
+ edges.append(make_edge(segment_id, tid, OverlayRelations.DERIVED_FROM, {"role": role}))
309
+ return edges
310
+
311
+
312
+ def provenance_edges_from_segment_wire(
313
+ node: Dict[str, Any], # A Segment node wire (or projected row): {"id", "properties"?, "sources", "text_from"?}
314
+ ) -> List[Dict[str, Any]]: # The segment's DERIVED_FROM edges, derived from its own refs
315
+ """Derive a Segment's Transcript provenance edges from the node itself:
316
+ every `char`-sliced source ref's graph-node locator is a Transcript the
317
+ segment slices text from, and `text_from` (a property on a wire, a
318
+ projected field on a query row) names the authoritative one. ONE derivation
319
+ for both callers — `build_extension_payload` at decomposition time and the
320
+ `backfill-provenance` verb over Segments already on the graph — so the
321
+ backfill reproduces exactly what a fresh run would emit."""
322
+ props = node.get("properties") or {}
323
+ text_from = props.get("text_from", node.get("text_from"))
324
+ tids: List[str] = []
325
+ for ref in node.get("sources") or []:
326
+ sl = ref.get("slice") or {}
327
+ loc = ref.get("locator") or {}
328
+ if sl.get("kind") == "char" and loc.get("node_id"):
329
+ tids.append(loc["node_id"])
330
+ return segment_provenance_edges(node["id"], tids, text_from)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-decomp-core
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: A frontend-agnostic core for the transcript decomposition workflow — composes isolated capability workers (forced alignment, VAD, graph storage) into a headless pipeline that decomposes transcription run manifests into a VAD-aligned context-graph spine with traceable provenance, and a CLI as its first driver.
5
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -26,7 +26,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
26
26
 
27
27
  ## Modules
28
28
 
29
- - **`cjm_transcript_decomp_core`**
29
+ - **`cjm_transcript_decomp_core.__init__`**
30
30
  - **`cjm_transcript_decomp_core.alignment`** — Pure forced-alignment logic (no capability calls): map FA words back to character spans in the original text, assign words to VAD chunks by timestamp, and build one text segment per VAD chunk. Extracted from the page-centric ForcedAlignmentService (Tier-1 logic).
31
31
  - **`cjm_transcript_decomp_core.cli`** — The CLI driver — the decomposition core's first (and currently only) frontend.
32
32
  - **`cjm_transcript_decomp_core.discovery`** — Capability-role discovery by manifest surface match — the journaling-by-
@@ -53,6 +53,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
53
53
 
54
54
  ### `cjm_transcript_decomp_core.cli`
55
55
 
56
+ - `backfill_provenance_command` _function_ — Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
56
57
  - `build_parser` _function_ — Build the CLI parser (subcommands: run).
57
58
  - `load_capabilities` _function_ — Discover manifests + load each requested capability (default instance).
58
59
  - `main` _function_ — CLI entry point (console script: `cjm-transcript-decomp-core`).
@@ -67,7 +68,9 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
67
68
 
68
69
  - `SourceVerification` _class_ — Skeptical-lens verification of one Source's fine-spine extension under a
69
70
  - `build_extension_payload` _function_ — Build the fine-spine EXTENSION payload (pure; no capability calls).
71
+ - `provenance_edges_from_segment_wire` _function_ — Derive a Segment's Transcript provenance edges from the node itself:
70
72
  - `resolve_root_ids` _function_ — Recompute the transcription-emitted root node ids from manifest data.
73
+ - `segment_provenance_edges` _function_ — Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
71
74
  - `verify_source` _function_ — Verify a Source's committed extension via server-side AGGREGATES (D13/D19).
72
75
 
73
76
  ### `cjm_transcript_decomp_core.launch`
@@ -141,4 +144,4 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
141
144
  ## Dependencies
142
145
 
143
146
  **Depends on:** `cjm-capability-primitives`, `cjm-context-graph-layer`, `cjm-context-graph-primitives`, `cjm-substrate`, `cjm-transcript-graph-schema`, `pyyaml`
144
- **Used by:** `cjm-transcript-decomp-qt`, `cjm-transcript-decomp-tui`
147
+ **Used by:** `cjm-transcript-decomp-qt`
@@ -123,3 +123,35 @@ def test_parallel_spines_disjoint_ids_and_split_metadata():
123
123
  assert all("split_policy" not in n["properties"] for n in base_nodes)
124
124
  assert all(n["properties"]["skeleton_hash"] == "sha256:skel-split" for n in split_nodes)
125
125
  assert all(n["properties"]["split_policy"] == "sentence-split/v1" for n in split_nodes)
126
+
127
+
128
+ def test_segment_transcript_provenance_edges():
129
+ # finding 89b16be6: a Segment's text provenance is an EDGE, not only metadata —
130
+ # DERIVED_FROM per sliced Transcript, the authoritative one role=text_from, the
131
+ # others role=variant; an empty (audio-only) segment derives no text edge; and the
132
+ # wire-derived form (the backfill's input) reproduces the builder's edges exactly.
133
+ from cjm_transcript_decomp_core.graph import (provenance_edges_from_segment_wire,
134
+ segment_provenance_edges)
135
+ roots = resolve_root_ids(SOURCE_ENTRY, CAPABILITIES)
136
+ tv = roots["audio_segments"][0]["transcripts"]["voxtral"]
137
+ tw = roots["audio_segments"][0]["transcripts"]["whisper"]
138
+ nodes, edges, ids = build_extension_payload(SOURCE_ENTRY, CAPABILITIES, "sha256:vad", "voxtral", _segments())
139
+ derived = [e for e in edges if e["relation_type"] == "DERIVED_FROM"]
140
+ # seg 0 slices voxtral + whisper (2 edges), seg 1 voxtral only (1), seg 2 empty (0)
141
+ assert len(derived) == 3
142
+ by_src = {}
143
+ for e in derived:
144
+ by_src.setdefault(e["source_id"], []).append(e)
145
+ s0 = {e["target_id"]: e["properties"]["role"] for e in by_src[ids["segments"][0]]}
146
+ assert s0 == {tv: "text_from", tw: "variant"}
147
+ assert [(e["target_id"], e["properties"]["role"]) for e in by_src[ids["segments"][1]]] == [(tv, "text_from")]
148
+ assert ids["segments"][2] not in by_src
149
+ # Deterministic ids from the triple: a rebuilt edge collides into the same id.
150
+ again = segment_provenance_edges(ids["segments"][0], [tw, tv], tv)
151
+ assert {e["id"] for e in again} == {e["id"] for e in by_src[ids["segments"][0]]}
152
+ # The backfill derivation (from the node's own refs) == the builder's edges.
153
+ for n in nodes:
154
+ assert provenance_edges_from_segment_wire(n) == by_src.get(n["id"], [])
155
+ # A projected query row carries text_from as a field, not under properties.
156
+ row = {"id": nodes[0]["id"], "text_from": tv, "sources": nodes[0]["sources"]}
157
+ assert provenance_edges_from_segment_wire(row) == by_src[nodes[0]["id"]]
@@ -1 +0,0 @@
1
- __version__ = "0.0.9"