cjm-transcript-correction-core 0.0.2__tar.gz → 0.0.4__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 (28) hide show
  1. {cjm_transcript_correction_core-0.0.2/cjm_transcript_correction_core.egg-info → cjm_transcript_correction_core-0.0.4}/PKG-INFO +11 -6
  2. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/README.md +7 -2
  3. cjm_transcript_correction_core-0.0.4/cjm_transcript_correction_core/__init__.py +1 -0
  4. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core/cli.py +33 -6
  5. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core/graph.py +191 -5
  6. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core/journal.py +12 -23
  7. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core/models.py +28 -3
  8. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core/pipeline.py +6 -2
  9. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4/cjm_transcript_correction_core.egg-info}/PKG-INFO +11 -6
  10. cjm_transcript_correction_core-0.0.4/cjm_transcript_correction_core.egg-info/entry_points.txt +5 -0
  11. cjm_transcript_correction_core-0.0.4/cjm_transcript_correction_core.egg-info/requires.txt +4 -0
  12. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/pyproject.toml +4 -1
  13. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/tests/test_graph.py +94 -0
  14. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/tests/test_journal.py +10 -3
  15. cjm_transcript_correction_core-0.0.2/cjm_transcript_correction_core/__init__.py +0 -1
  16. cjm_transcript_correction_core-0.0.2/cjm_transcript_correction_core.egg-info/entry_points.txt +0 -2
  17. cjm_transcript_correction_core-0.0.2/cjm_transcript_correction_core.egg-info/requires.txt +0 -4
  18. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/LICENSE +0 -0
  19. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/MANIFEST.in +0 -0
  20. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core/signals.py +0 -0
  21. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core.egg-info/SOURCES.txt +0 -0
  22. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core.egg-info/dependency_links.txt +0 -0
  23. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/cjm_transcript_correction_core.egg-info/top_level.txt +0 -0
  24. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/setup.cfg +0 -0
  25. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/tests/test_cli.py +0 -0
  26. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/tests/test_models.py +0 -0
  27. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/tests/test_pipeline.py +0 -0
  28. {cjm_transcript_correction_core-0.0.2 → cjm_transcript_correction_core-0.0.4}/tests/test_signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-correction-core
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: A frontend-agnostic core for the transcript correction workflow — the first downstream graph-extension core; composes the graph-storage capability worker into a headless pipeline that applies unified, non-destructive corrections (text, punctuation, segmentation) as a supersede-able overlay on a committed decomposition spine, recomputes the review worklist from deterministic signals, and exposes 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
@@ -10,9 +10,9 @@ Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.12
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: cjm_substrate>=0.0.51
14
- Requires-Dist: cjm_context_graph_primitives>=0.0.14
15
- Requires-Dist: cjm-context-graph-layer>=0.0.8
13
+ Requires-Dist: cjm_substrate>=0.0.57
14
+ Requires-Dist: cjm_context_graph_primitives>=0.0.15
15
+ Requires-Dist: cjm-context-graph-layer>=0.0.11
16
16
  Requires-Dist: cjm-transcript-graph-schema>=0.0.7
17
17
  Dynamic: license-file
18
18
 
@@ -26,7 +26,7 @@ A frontend-agnostic core for the transcript correction workflow — the first do
26
26
 
27
27
  - **`cjm_transcript_correction_core.cli`** — The CLI driver — the correction core's first (and currently only) frontend. run <decomp-manifest> corrects the committed spine in the decomp graph DB, pointing the graph worker at that shared DB via load-time config, with optional session resume/reopen; review runs the interactive text-correction loop (the cross-transcriber diff is intra-graph since stage 5).
28
28
  - **`cjm_transcript_correction_core.graph`** — The correction overlay's graph I/O: targeted (scale-shaped) reads of a committed spine via the graph-storage query action, construction of Correction / CorrectionSession nodes + CORRECTS / SUPERSEDES / DERIVED_FROM / REVIEWED edges, the in-core effective-spine projection (layer-0 + applied corrections), and commit through the job queue. Hand-rolled (revolution-1) = direct CR-18 spec material; append-only on layer-0 (never update/delete a Segment).
29
- - **`cjm_transcript_correction_core.journal`**
29
+ - **`cjm_transcript_correction_core.journal`** — Live append-through for the correction verbs — the workflow journal's domain half.
30
30
  - **`cjm_transcript_correction_core.models`** — Overlay data shapes for the transcript-correction workflow: the Correction / CorrectionSession graph nodes + their relation registry, the read view of a committed spine segment, the worklist item, run configuration, and the correction run manifest (proto-bundle that chains decomp -> correction).
31
31
  - **`cjm_transcript_correction_core.pipeline`** — The headless correction workflow: load a decomp run manifest, resolve the shared graph DB, start/resume/reopen a CorrectionSession, recompute the worklist from deterministic signals + persisted review state, run the D14 empty-segment prune (first operation), and record a chainable correction run manifest — with a cheapest-form HITL approval seam.
32
32
  - **`cjm_transcript_correction_core.signals`** — Pure deterministic Tier-1 signal functions (no capability calls): empty-segment detection, bidirectional boundary punctuation/capitalization heuristics, forced-alignment coverage flags, positional cross-transcriber diff, and phonetic + edit-distance variant clustering. The worklist is recomputed from these each session; revolution-1 builds ZERO new capabilities.
@@ -46,11 +46,14 @@ A frontend-agnostic core for the transcript correction workflow — the first do
46
46
  - `active_corrections` _function_ — Filter to the effective correction set (the layer's resolve_active over a read superseded set).
47
47
  - `build_boundary_shift_correction` _function_ — Build a grouping Correction that moves text across one segment boundary.
48
48
  - `build_correction_node` _function_ — Construct a Correction overlay node (pure; commit happens separately).
49
+ - `build_mark_correction` _function_ — Build a NON-MUTATING mark Correction (DEC 2a231843: routed attention).
49
50
  - `build_prune_amendment` _function_ — Build a grouping Correction that supersedes a prune with a REDUCED set (unprune).
50
51
  - `build_prune_correction` _function_ — Build one batch grouping Correction that prunes empty segments (D14).
51
52
  - `build_reject_review` _function_ — Build a review Correction that REJECTS a prior correction (reject-as-supersede).
52
53
  - `build_text_correction` _function_ — Build a text_content Correction + its CORRECTS (+ optional SUPERSEDES) edges.
53
54
  - `commit_boundary_shift_correction` _function_ — Commit a boundary-shift correction (node + CORRECTS x2 [+ SUPERSEDES]) + REVIEWED markers on both segments.
55
+ - `commit_mark_correction` _function_ — Commit a mark (node + CORRECTS per anchored segment [+ SUPERSEDES]).
56
+ - `commit_mark_dismissal` _function_ — Dismiss an open mark WITHOUT a correction (reject-as-supersede).
54
57
  - `commit_nodes_edges` _function_ — Commit overlay nodes/edges through the layer's idempotent extend_graph.
55
58
  - `commit_prune_amendment` _function_ — Commit an unprune amendment (node + DERIVED_FROM edges + SUPERSEDES).
56
59
  - `commit_text_correction` _function_ — Commit a text_content correction (node + CORRECTS [+ SUPERSEDES]) + a REVIEWED marker.
@@ -66,7 +69,10 @@ A frontend-agnostic core for the transcript correction workflow — the first do
66
69
  - `load_source_corrections` _function_ — Load every Correction targeting a Source (across sessions) + the superseded-id set.
67
70
  - `load_source_segments` _function_ — Load a Source's fine Segment spine under its chosen rendition (typed query surface).
68
71
  - `load_variant_texts` _function_ — Resolve per-transcriber chunk texts from the segments' CharSlice refs.
72
+ - `mark_anchor_segments` _function_ — Validate a mark anchor and list the Segment ids it touches.
73
+ - `open_marks` _function_ — Filter to the OPEN marks — the pass-2 worklist ('query open marks, walk them').
69
74
  - `project_effective_spine` _function_ — Project the effective spine = layer-0 + applied corrections.
75
+ - `reanchor_span` _function_ — Re-locate a span anchor in text that may have been edited since mark time.
70
76
  - `record_review_markers` _function_ — Persist per-(session, segment) review markers as REVIEWED edges.
71
77
  - `resolve_source_renditions` _function_ — Pick the AudioRendition set whose fine Segment spine correction operates on.
72
78
  - `set_session_status` _function_ — Update a session's status + updated_at.
@@ -79,7 +85,6 @@ A frontend-agnostic core for the transcript correction workflow — the first do
79
85
  - `correction_replay_handlers` _function_ — The correction core's registered replay vocabulary (replay stays DOMAIN-OWNED).
80
86
  - `journal_correction_op` _function_ — Append one correction op — envelope + semantic args + the EXACT wires committed.
81
87
  - `segment_anchor` _function_ — The run-independent anchor stamped on every correction op (DEC ccbab9f5 point 5).
82
- - `sidecar_journal_path` _function_ — The db's sidecar journal path (DEC ccbab9f5 point 3: placement is per-workflow,
83
88
 
84
89
  ### `cjm_transcript_correction_core.models`
85
90
 
@@ -8,7 +8,7 @@ A frontend-agnostic core for the transcript correction workflow — the first do
8
8
 
9
9
  - **`cjm_transcript_correction_core.cli`** — The CLI driver — the correction core's first (and currently only) frontend. run <decomp-manifest> corrects the committed spine in the decomp graph DB, pointing the graph worker at that shared DB via load-time config, with optional session resume/reopen; review runs the interactive text-correction loop (the cross-transcriber diff is intra-graph since stage 5).
10
10
  - **`cjm_transcript_correction_core.graph`** — The correction overlay's graph I/O: targeted (scale-shaped) reads of a committed spine via the graph-storage query action, construction of Correction / CorrectionSession nodes + CORRECTS / SUPERSEDES / DERIVED_FROM / REVIEWED edges, the in-core effective-spine projection (layer-0 + applied corrections), and commit through the job queue. Hand-rolled (revolution-1) = direct CR-18 spec material; append-only on layer-0 (never update/delete a Segment).
11
- - **`cjm_transcript_correction_core.journal`**
11
+ - **`cjm_transcript_correction_core.journal`** — Live append-through for the correction verbs — the workflow journal's domain half.
12
12
  - **`cjm_transcript_correction_core.models`** — Overlay data shapes for the transcript-correction workflow: the Correction / CorrectionSession graph nodes + their relation registry, the read view of a committed spine segment, the worklist item, run configuration, and the correction run manifest (proto-bundle that chains decomp -> correction).
13
13
  - **`cjm_transcript_correction_core.pipeline`** — The headless correction workflow: load a decomp run manifest, resolve the shared graph DB, start/resume/reopen a CorrectionSession, recompute the worklist from deterministic signals + persisted review state, run the D14 empty-segment prune (first operation), and record a chainable correction run manifest — with a cheapest-form HITL approval seam.
14
14
  - **`cjm_transcript_correction_core.signals`** — Pure deterministic Tier-1 signal functions (no capability calls): empty-segment detection, bidirectional boundary punctuation/capitalization heuristics, forced-alignment coverage flags, positional cross-transcriber diff, and phonetic + edit-distance variant clustering. The worklist is recomputed from these each session; revolution-1 builds ZERO new capabilities.
@@ -28,11 +28,14 @@ A frontend-agnostic core for the transcript correction workflow — the first do
28
28
  - `active_corrections` _function_ — Filter to the effective correction set (the layer's resolve_active over a read superseded set).
29
29
  - `build_boundary_shift_correction` _function_ — Build a grouping Correction that moves text across one segment boundary.
30
30
  - `build_correction_node` _function_ — Construct a Correction overlay node (pure; commit happens separately).
31
+ - `build_mark_correction` _function_ — Build a NON-MUTATING mark Correction (DEC 2a231843: routed attention).
31
32
  - `build_prune_amendment` _function_ — Build a grouping Correction that supersedes a prune with a REDUCED set (unprune).
32
33
  - `build_prune_correction` _function_ — Build one batch grouping Correction that prunes empty segments (D14).
33
34
  - `build_reject_review` _function_ — Build a review Correction that REJECTS a prior correction (reject-as-supersede).
34
35
  - `build_text_correction` _function_ — Build a text_content Correction + its CORRECTS (+ optional SUPERSEDES) edges.
35
36
  - `commit_boundary_shift_correction` _function_ — Commit a boundary-shift correction (node + CORRECTS x2 [+ SUPERSEDES]) + REVIEWED markers on both segments.
37
+ - `commit_mark_correction` _function_ — Commit a mark (node + CORRECTS per anchored segment [+ SUPERSEDES]).
38
+ - `commit_mark_dismissal` _function_ — Dismiss an open mark WITHOUT a correction (reject-as-supersede).
36
39
  - `commit_nodes_edges` _function_ — Commit overlay nodes/edges through the layer's idempotent extend_graph.
37
40
  - `commit_prune_amendment` _function_ — Commit an unprune amendment (node + DERIVED_FROM edges + SUPERSEDES).
38
41
  - `commit_text_correction` _function_ — Commit a text_content correction (node + CORRECTS [+ SUPERSEDES]) + a REVIEWED marker.
@@ -48,7 +51,10 @@ A frontend-agnostic core for the transcript correction workflow — the first do
48
51
  - `load_source_corrections` _function_ — Load every Correction targeting a Source (across sessions) + the superseded-id set.
49
52
  - `load_source_segments` _function_ — Load a Source's fine Segment spine under its chosen rendition (typed query surface).
50
53
  - `load_variant_texts` _function_ — Resolve per-transcriber chunk texts from the segments' CharSlice refs.
54
+ - `mark_anchor_segments` _function_ — Validate a mark anchor and list the Segment ids it touches.
55
+ - `open_marks` _function_ — Filter to the OPEN marks — the pass-2 worklist ('query open marks, walk them').
51
56
  - `project_effective_spine` _function_ — Project the effective spine = layer-0 + applied corrections.
57
+ - `reanchor_span` _function_ — Re-locate a span anchor in text that may have been edited since mark time.
52
58
  - `record_review_markers` _function_ — Persist per-(session, segment) review markers as REVIEWED edges.
53
59
  - `resolve_source_renditions` _function_ — Pick the AudioRendition set whose fine Segment spine correction operates on.
54
60
  - `set_session_status` _function_ — Update a session's status + updated_at.
@@ -61,7 +67,6 @@ A frontend-agnostic core for the transcript correction workflow — the first do
61
67
  - `correction_replay_handlers` _function_ — The correction core's registered replay vocabulary (replay stays DOMAIN-OWNED).
62
68
  - `journal_correction_op` _function_ — Append one correction op — envelope + semantic args + the EXACT wires committed.
63
69
  - `segment_anchor` _function_ — The run-independent anchor stamped on every correction op (DEC ccbab9f5 point 5).
64
- - `sidecar_journal_path` _function_ — The db's sidecar journal path (DEC ccbab9f5 point 3: placement is per-workflow,
65
70
 
66
71
  ### `cjm_transcript_correction_core.models`
67
72
 
@@ -3,11 +3,13 @@
3
3
  import argparse
4
4
  import asyncio
5
5
  import logging
6
+ import os
6
7
  from pathlib import Path
7
8
  from typing import Dict, List, Optional
8
9
 
9
10
  from cjm_substrate.core.manager import CapabilityManager
10
11
  from cjm_substrate.core.queue import JobQueue
12
+ from cjm_substrate.core.workspace import resolve_workspace
11
13
  from cjm_transcript_correction_core.models import CorrectionConfig
12
14
  from cjm_transcript_correction_core.pipeline import (load_decomp_manifest, resolve_graph_db_path,
13
15
  run_correction, run_review)
@@ -18,7 +20,13 @@ logger = logging.getLogger(__name__)
18
20
  def _add_common_run_args(p: argparse.ArgumentParser) -> None: # Shared run/review arguments
19
21
  """Attach the capability / session / output arguments shared by `run` and `review`."""
20
22
  p.add_argument("manifest", help="Decomp-core run manifest JSON (the committed spine)")
21
- p.add_argument("--manifests-dir", default=".cjm/manifests", help="Capability manifests directory")
23
+ p.add_argument("--manifests-dir", default=None,
24
+ help="Capability manifests directory (default: the workspace's .cjm/manifests "
25
+ "when one is active, else .cjm/manifests under the cwd)")
26
+ p.add_argument("--workspace", default=None,
27
+ help="Workspace root (5daadfc4; default: CJM_WORKSPACE env, else upward walk "
28
+ "from cwd). Supplies manifests/output defaults and is exported so "
29
+ "capability workers resolve workspace-scoped paths")
22
30
  p.add_argument("--graph-capability", default="cjm-capability-graph-sqlite", help="Graph-storage capability name")
23
31
  p.add_argument("--graph-db-path", default=None,
24
32
  help="Override graph DB path (default: the decomp manifest's recorded db_path)")
@@ -31,7 +39,9 @@ def _add_common_run_args(p: argparse.ArgumentParser) -> None: # Shared run/revi
31
39
  p.add_argument("--session", default=None, help="Resume an existing CorrectionSession id")
32
40
  p.add_argument("--reopen", action="store_true", help="Reopen a completed session (with --session)")
33
41
  p.add_argument("--actor", default="human", help="Actor recorded on corrections + review markers")
34
- p.add_argument("--output", default=None, help="Correction-manifest output path (default: runs/<run_id>.json)")
42
+ p.add_argument("--output", default=None,
43
+ help="Correction-manifest output path (default: <workspace>/runs/<run_id>.json "
44
+ "when a workspace is active, else runs/<run_id>.json under the cwd)")
35
45
  p.add_argument("-v", "--verbose", action="store_true", help="DEBUG-level logging")
36
46
 
37
47
 
@@ -84,6 +94,14 @@ async def run_command(
84
94
  args: argparse.Namespace, # Parsed args for the `run` subcommand
85
95
  ) -> int: # Process exit code
86
96
  """Execute the `run` subcommand: correct a decomp manifest's committed spine."""
97
+ # 5daadfc4 workspace: resolve BEFORE any substrate config loads; export so
98
+ # the process tree (substrate config, capability workers) is workspace-scoped.
99
+ ws = resolve_workspace(explicit=getattr(args, "workspace", None))
100
+ if ws is not None:
101
+ os.environ["CJM_WORKSPACE"] = str(ws.root)
102
+ if args.manifests_dir is None:
103
+ args.manifests_dir = (str(ws.substrate_data_dir / "manifests")
104
+ if ws is not None else ".cjm/manifests")
87
105
  manifest_path = str(Path(args.manifest).resolve())
88
106
  if not Path(manifest_path).exists():
89
107
  raise SystemExit(f"decomp manifest not found: {manifest_path}")
@@ -122,8 +140,9 @@ async def run_command(
122
140
  except Exception as e: # Best-effort teardown; never mask the run's outcome
123
141
  logger.warning(f"unload {iid} failed: {e}")
124
142
 
125
- out = Path(args.output) if args.output else Path("runs") / f"{manifest.run_id}.json"
126
- manifest.save(out)
143
+ out = (Path(args.output) if args.output
144
+ else (ws.runs_dir if ws is not None else Path("runs")) / f"{manifest.run_id}.json")
145
+ manifest.save(out, workspace=ws)
127
146
  n_sources = len(manifest.sources)
128
147
  n_pruned = sum(s.get("pruned", 0) for s in manifest.sources)
129
148
  n_flagged = sum(s.get("worklist_flagged", 0) for s in manifest.sources)
@@ -137,6 +156,13 @@ async def review_command(
137
156
  args: argparse.Namespace, # Parsed args for the `review` subcommand
138
157
  ) -> int: # Process exit code
139
158
  """Execute the `review` subcommand: interactive text corrections over the flagged worklist."""
159
+ # 5daadfc4 workspace: same early resolution + export as run_command.
160
+ ws = resolve_workspace(explicit=getattr(args, "workspace", None))
161
+ if ws is not None:
162
+ os.environ["CJM_WORKSPACE"] = str(ws.root)
163
+ if args.manifests_dir is None:
164
+ args.manifests_dir = (str(ws.substrate_data_dir / "manifests")
165
+ if ws is not None else ".cjm/manifests")
140
166
  manifest_path = str(Path(args.manifest).resolve())
141
167
  if not Path(manifest_path).exists():
142
168
  raise SystemExit(f"decomp manifest not found: {manifest_path}")
@@ -167,8 +193,9 @@ async def review_command(
167
193
  except Exception as e: # Best-effort teardown; never mask the run's outcome
168
194
  logger.warning(f"unload {iid} failed: {e}")
169
195
 
170
- out = Path(args.output) if args.output else Path("runs") / f"{manifest.run_id}.json"
171
- manifest.save(out)
196
+ out = (Path(args.output) if args.output
197
+ else (ws.runs_dir if ws is not None else Path("runs")) / f"{manifest.run_id}.json")
198
+ manifest.save(out, workspace=ws)
172
199
  n_corr = sum(s.get("corrected", 0) for s in manifest.sources)
173
200
  n_active = sum(s.get("active_corrections", 0) for s in manifest.sources)
174
201
  print(f"correction manifest: {out}")
@@ -259,7 +259,7 @@ def _edge(
259
259
 
260
260
 
261
261
  def build_correction_node(
262
- correction_type: str, # "text_content" | "punctuation" | "grouping" | "review"
262
+ correction_type: str, # "text_content" | "punctuation" | "grouping" | "review" | "mark"
263
263
  session_id: str, # Owning session id
264
264
  payload: Dict[str, Any], # Type-specific payload
265
265
  actor: str = "human", # Actor
@@ -376,7 +376,8 @@ async def record_review_markers(
376
376
  journal_path: Optional[str] = None, # Sidecar journal — append the op on success (None = unjournaled)
377
377
  ) -> int: # Number of REVIEWED edges committed
378
378
  """Persist per-(session, segment) review markers as REVIEWED edges."""
379
- edges = [_edge(session_id, seg_id, CorrectionRelations.REVIEWED, {"decision": dec})
379
+ edges = [_edge(session_id, seg_id, CorrectionRelations.REVIEWED,
380
+ {"decision": dec, "ts": time.time()})
380
381
  for seg_id, dec in decisions]
381
382
  n = (await commit_nodes_edges(queue, graph_id, [], edges))["edges"]
382
383
  if journal_path:
@@ -395,11 +396,21 @@ async def load_review_markers(
395
396
  graph_id: str, # Graph-storage capability id
396
397
  session_id: str, # Owning session id
397
398
  ) -> Dict[str, str]: # segment_id -> decision for the session
398
- """Load a session's review markers (typed edge projection over REVIEWED edges)."""
399
+ """Load a session's review markers (typed edge projection over REVIEWED edges).
400
+
401
+ Markers are EVENTS (a re-decision appends — the _edge docstring's contract),
402
+ so the read is LATEST-WINS by the marker's ts, and a latest decision of
403
+ "unreviewed" means UNDECIDED: the segment drops out of the returned map
404
+ entirely (the space-undo gesture; compute_worklist then re-surfaces it).
405
+ Historical edges without ts sort first, so any re-decision beats them.
406
+ """
399
407
  q = EdgeQuery(source_id=session_id, relation_type=CorrectionRelations.REVIEWED,
400
- project=["decision"])
408
+ project=["decision", "ts"])
401
409
  res = await graph_task(queue, graph_id, "query_edges", query=q.to_dict())
402
- return {r["target_id"]: r["decision"] for r in (res.rows or [])}
410
+ latest: Dict[str, str] = {}
411
+ for r in sorted((res.rows or []), key=lambda r: float(r.get("ts") or 0.0)):
412
+ latest[r["target_id"]] = r["decision"]
413
+ return {sid: dec for sid, dec in latest.items() if dec != "unreviewed"}
403
414
 
404
415
 
405
416
  def _node_to_correction_dict(
@@ -836,3 +847,178 @@ async def commit_prune_amendment(
836
847
  anchor=await segment_anchor(queue, graph_id, unprune_ids),
837
848
  nodes=[node], edges=edges, op_id=node["id"])
838
849
  return _node_to_correction_dict(node)
850
+
851
+
852
+ def mark_anchor_segments(
853
+ anchor: Dict[str, Any], # Mark anchor: {"kind": "segment" | "boundary" | "span", ...}
854
+ ) -> List[str]: # The layer-0 Segment ids the anchor touches (CORRECTS targets)
855
+ """Validate a mark anchor and list the Segment ids it touches.
856
+
857
+ The three anchor shapes (DEC 2a231843): `segment` {segment_id} ·
858
+ `boundary` {boundary_after, right_segment_id} (the boundary AFTER a
859
+ segment — the shift gesture's coordinates) · `span` {segment_id,
860
+ char_start, char_end, text_snapshot} (offsets into the segment's effective
861
+ text AT MARK TIME; the snapshot is what re-anchoring trusts — see
862
+ `reanchor_span` — the offsets are only its hint).
863
+ """
864
+ kind = anchor.get("kind")
865
+ if kind == "segment":
866
+ ids = [anchor.get("segment_id")]
867
+ elif kind == "boundary":
868
+ ids = [anchor.get("boundary_after"), anchor.get("right_segment_id")]
869
+ elif kind == "span":
870
+ ids = [anchor.get("segment_id")]
871
+ if not anchor.get("text_snapshot") or anchor.get("char_start") is None \
872
+ or anchor.get("char_end") is None:
873
+ raise ValueError("span mark anchor needs char_start, char_end and a text_snapshot")
874
+ else:
875
+ raise ValueError(f"mark anchor kind must be 'segment' | 'boundary' | 'span', got {kind!r}")
876
+ if any(not sid for sid in ids):
877
+ raise ValueError(f"{kind} mark anchor is missing its segment id(s): {anchor!r}")
878
+ return [str(sid) for sid in ids]
879
+
880
+
881
+ def build_mark_correction(
882
+ source_id: str, # Source the marked segment(s) belong to
883
+ anchor: Dict[str, Any], # Anchor: segment | boundary | span (see mark_anchor_segments)
884
+ mark_class: str, # Open-vocabulary class (RECOMMENDED_MARK_CLASSES is the slate)
885
+ session_id: str, # Owning session id
886
+ supersedes_id: Optional[str] = None, # Prior mark this one replaces (re-mark)
887
+ actor: str = "human", # Actor ("human" | "capability:<name>" for pass-2 assists)
888
+ note: Optional[str] = None, # Optional free-text note
889
+ ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: # (correction node dict, edge dicts)
890
+ """Build a NON-MUTATING mark Correction (DEC 2a231843: routed attention).
891
+
892
+ A mark is a placeholder correction: it records that a slot needs judgment
893
+ (an omission-entangled boundary, a suspect entity, ...) WITHOUT resolving
894
+ anything — `corrections_to_edits` has no arm for it, so it can never touch
895
+ the effective view. It closes by SUPERSESSION: the real correction that
896
+ discharges it passes `supersedes_id=<mark id>`, or a dismissal supersedes
897
+ it (`commit_mark_dismissal`, riding the reject-review shape). CORRECTS
898
+ edges anchor every touched segment so marks surface in segment-scoped
899
+ reads.
900
+ """
901
+ mc = (mark_class or "").strip()
902
+ if not mc:
903
+ raise ValueError("mark_class must be a non-empty string")
904
+ if not mc[:1].isalnum():
905
+ raise ValueError(f"mark_class must start with a letter or digit, got {mark_class!r}"
906
+ " — punctuation-led tokens are reserved for gestures ('-' dismissal)")
907
+ seg_ids = mark_anchor_segments(anchor)
908
+ payload = {"operation": "mark", "source_id": source_id,
909
+ "anchor": dict(anchor), "mark_class": mc}
910
+ node = build_correction_node("mark", session_id, payload, actor=actor,
911
+ rationale=note).to_graph_node()
912
+ edges = [make_edge(node.id, sid, CorrectionRelations.CORRECTS) for sid in seg_ids]
913
+ if supersedes_id:
914
+ edges.append(make_edge(node.id, supersedes_id, CorrectionRelations.SUPERSEDES))
915
+ return node.to_dict(), edges
916
+
917
+
918
+ async def commit_mark_correction(
919
+ queue: JobQueue, # Started job queue
920
+ graph_id: str, # Graph-storage capability id
921
+ source_id: str, # Source the marked segment(s) belong to
922
+ anchor: Dict[str, Any], # Anchor: segment | boundary | span
923
+ mark_class: str, # Open-vocabulary class
924
+ session_id: str, # Owning session id
925
+ supersedes_id: Optional[str] = None, # Prior mark to supersede (re-mark)
926
+ actor: str = "human", # Actor
927
+ note: Optional[str] = None, # Optional free-text note
928
+ journal_path: Optional[str] = None, # Sidecar journal — append the op on success (None = unjournaled)
929
+ ) -> str: # The new mark Correction node id
930
+ """Commit a mark (node + CORRECTS per anchored segment [+ SUPERSEDES]).
931
+
932
+ NO review marker: a mark is routed attention, not a review decision — the
933
+ walked-past state stays exactly as the operator left it.
934
+ """
935
+ node, edges = build_mark_correction(source_id, anchor, mark_class, session_id,
936
+ supersedes_id=supersedes_id, actor=actor, note=note)
937
+ await commit_nodes_edges(queue, graph_id, [node], edges)
938
+ if journal_path:
939
+ journal_correction_op(journal_path, "mark", actor=actor, session_id=session_id,
940
+ args={"source_id": source_id, "anchor": dict(anchor),
941
+ "mark_class": mark_class, "note": note,
942
+ "supersedes_id": supersedes_id},
943
+ anchor=await segment_anchor(queue, graph_id,
944
+ mark_anchor_segments(anchor)),
945
+ nodes=[node], edges=edges, op_id=node["id"])
946
+ return node["id"]
947
+
948
+
949
+ async def commit_mark_dismissal(
950
+ queue: JobQueue, # Started job queue
951
+ graph_id: str, # Graph-storage capability id
952
+ source_id: str, # Source the mark belongs to
953
+ mark_id: str, # The open mark being dismissed
954
+ session_id: str, # Owning session id
955
+ actor: str = "human", # Actor (the dismisser)
956
+ note: Optional[str] = None, # Optional note (why dismissed)
957
+ journal_path: Optional[str] = None, # Sidecar journal — append the op on success (None = unjournaled)
958
+ ) -> str: # The review node id that superseded the mark
959
+ """Dismiss an open mark WITHOUT a correction (reject-as-supersede).
960
+
961
+ Rides the review verdict shape (`build_reject_review`): dismissal is a
962
+ SUPERSEDES edge from a small review node — append-only, carrying
963
+ actor/note/timestamp — and `open_marks` then excludes the mark. No anchor
964
+ on the journal op: dismissal is derivative session state, like review
965
+ markers (the ccbab9f5 anchor scope).
966
+ """
967
+ node, edges = build_reject_review(source_id, mark_id, session_id,
968
+ actor=actor, rationale=note)
969
+ await commit_nodes_edges(queue, graph_id, [node], edges)
970
+ if journal_path:
971
+ journal_correction_op(journal_path, "mark-dismiss", actor=actor,
972
+ session_id=session_id,
973
+ args={"source_id": source_id, "mark_id": mark_id, "note": note},
974
+ nodes=[node], edges=edges, op_id=node["id"])
975
+ return node["id"]
976
+
977
+
978
+ def open_marks(
979
+ corrections: List[Dict[str, Any]], # Corrections (e.g. from load_source_corrections)
980
+ superseded_ids: set, # Ids that are SUPERSEDES targets
981
+ ) -> List[Dict[str, Any]]: # The OPEN marks (not yet discharged/dismissed), oldest first
982
+ """Filter to the OPEN marks — the pass-2 worklist ('query open marks, walk them').
983
+
984
+ A mark leaves this set the moment anything supersedes it (the discharging
985
+ correction or a dismissal review) — resolution is read from SUPERSEDES
986
+ edges, never a status mutation.
987
+ """
988
+ marks = [c for c in corrections
989
+ if c.get("correction_type") == "mark"
990
+ and (c.get("payload") or {}).get("operation") == "mark"
991
+ and c.get("id") not in superseded_ids]
992
+ marks.sort(key=lambda c: c.get("created_at") or 0.0)
993
+ return marks
994
+
995
+
996
+ def reanchor_span(
997
+ anchor: Dict[str, Any], # A span mark anchor (char_start / char_end / text_snapshot)
998
+ effective_text: str, # The segment's CURRENT effective text
999
+ ) -> Optional[Tuple[int, int]]: # Current (char_start, char_end); None = degrade to segment level
1000
+ """Re-locate a span anchor in text that may have been edited since mark time.
1001
+
1002
+ The SNAPSHOT is the anchor's truth; the recorded offsets are only its hint
1003
+ (DEC 2a231843): exact offsets verified first, then the snapshot occurrence
1004
+ NEAREST the recorded start, else None — the caller degrades the mark to
1005
+ segment level (which usually means the marked text was addressed).
1006
+ """
1007
+ snap = anchor.get("text_snapshot")
1008
+ if not snap:
1009
+ return None
1010
+ try:
1011
+ cs, ce = int(anchor.get("char_start")), int(anchor.get("char_end"))
1012
+ except (TypeError, ValueError):
1013
+ cs, ce = -1, -1
1014
+ if 0 <= cs <= ce <= len(effective_text) and effective_text[cs:ce] == snap:
1015
+ return cs, ce
1016
+ hits: List[int] = []
1017
+ at = effective_text.find(snap)
1018
+ while at != -1:
1019
+ hits.append(at)
1020
+ at = effective_text.find(snap, at + 1)
1021
+ if not hits:
1022
+ return None
1023
+ best = min(hits, key=lambda h: abs(h - cs) if cs >= 0 else h)
1024
+ return best, best + len(snap)
@@ -11,22 +11,12 @@ append discipline lives in `cjm_context_graph_primitives.journal`.
11
11
 
12
12
  from typing import Any, Dict, List, Optional
13
13
 
14
- from cjm_context_graph_layer.ops import extend_graph, graph_task
14
+ from cjm_context_graph_layer.journal import wires_handlers
15
+ from cjm_context_graph_layer.ops import graph_task
15
16
  from cjm_context_graph_primitives.journal import append_op
16
17
  from cjm_context_graph_primitives.query import NodeQuery
17
18
 
18
19
 
19
- def sidecar_journal_path(
20
- db_path: str, # The workflow graph db path (e.g. .../context_graph.db)
21
- ) -> str: # The sidecar write-journal path next to it (.../context_graph.writes.jsonl)
22
- """The db's sidecar journal path (DEC ccbab9f5 point 3: placement is per-workflow,
23
- NEXT TO the db it is the source of truth for). Derived, never configured — one
24
- less ambient default (the explicit-db-path guardrail extended to the journal)."""
25
- if db_path.endswith(".db"):
26
- return db_path[: -len(".db")] + ".writes.jsonl"
27
- return db_path + ".writes.jsonl"
28
-
29
-
30
20
  async def segment_anchor(
31
21
  queue: Any, # Started job queue
32
22
  graph_id: str, # Graph-storage capability id
@@ -92,22 +82,21 @@ def journal_correction_op(
92
82
  def correction_replay_handlers() -> Dict[str, Any]: # verb -> async handler(queue, graph_id, op)
93
83
  """The correction core's registered replay vocabulary (replay stays DOMAIN-OWNED).
94
84
 
95
- Pass to `cjm_context_graph_layer.journal.replay_journal(handlers=...)`: wire-carrying
96
- ops re-apply through the layer's idempotent extend (replayed wires collide into
97
- verified no-ops); `session-status` is the core's only update_node write, replayed
98
- last-wins in append order."""
99
- async def _apply_wires(queue: Any, graph_id: str, op: Dict[str, Any]) -> None:
100
- w = op.get("wires") or {}
101
- await extend_graph(queue, graph_id, w.get("nodes") or [], w.get("edges") or [])
102
-
85
+ Exported through the `cjm_context_graph_layer.replay` entry-point group (DEC
86
+ 426658f1) and unioned by `composed_replay_handlers`: wire-carrying verbs register
87
+ the layer's shared `apply_wires` — the single wires-replay authority, identity-
88
+ comparable across cores, which keeps cross-core verb collisions checkable;
89
+ `session-status` is the core's only update_node write, replayed last-wins in
90
+ append order."""
103
91
  async def _apply_session_status(queue: Any, graph_id: str, op: Dict[str, Any]) -> None:
104
92
  a = op["args"]
105
93
  await graph_task(queue, graph_id, "update_node", node_id=a["session_id"],
106
94
  properties={"status": a["status"], "updated_at": a["updated_at"]})
107
95
 
108
- return {"session-start": _apply_wires, "boundary-shift": _apply_wires,
109
- "text-correction": _apply_wires, "prune-amendment": _apply_wires,
110
- "review-markers": _apply_wires, "session-status": _apply_session_status}
96
+ handlers = wires_handlers("session-start", "boundary-shift", "text-correction",
97
+ "prune-amendment", "mark", "mark-dismiss", "review-markers")
98
+ handlers["session-status"] = _apply_session_status
99
+ return handlers
111
100
 
112
101
 
113
102
  # A Source's content hash is IMMUTABLE — read once per process, not per commit
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Union
9
9
  from uuid import uuid4
10
10
 
11
11
  from cjm_context_graph_primitives.graph import GraphNode
12
+ from cjm_substrate.core.workspace import relativize_recorded
12
13
 
13
14
 
14
15
  class CorrectionRelations:
@@ -35,7 +36,7 @@ class Correction:
35
36
  generic GraphNode. Corrections are DECISIONS (asserted events) — they keep
36
37
  GENERATED ids, the FLIP-TRIGGER-protected class.
37
38
  """
38
- correction_type: str # "text_content" | "punctuation" | "grouping" | "review"
39
+ correction_type: str # "text_content" | "punctuation" | "grouping" | "review" | "mark"
39
40
  status: str = "applied" # "proposed" | "applied" | "superseded"
40
41
  session_id: str = "" # Owning CorrectionSession id
41
42
  payload: Dict[str, Any] = field(default_factory=dict) # Type-specific data (new text, prune set, ...)
@@ -158,14 +159,38 @@ class CorrectionManifest:
158
159
  def save(
159
160
  self,
160
161
  path: Union[str, Path], # Destination JSON file (parent dirs created)
162
+ workspace=None, # Active Workspace; owned paths record as ${WS}/<rel> (5daadfc4 rung f)
161
163
  ) -> Path: # The written path
162
- """Write the manifest as pretty-printed JSON."""
164
+ """Write the manifest as pretty-printed JSON.
165
+
166
+ With `workspace`, recorded paths under its root take the ${WS}/ token
167
+ form (relativize_recorded), so the manifest relocates with the
168
+ workspace; readers resolve via resolve_recorded_tree at load."""
163
169
  out = Path(path)
164
170
  out.parent.mkdir(parents=True, exist_ok=True)
165
- out.write_text(json.dumps(self.to_dict(), indent=2))
171
+ out.write_text(json.dumps(relativize_recorded(self.to_dict(), workspace), indent=2))
166
172
  return out
167
173
 
168
174
 
169
175
  def new_run_id() -> str: # e.g. "correct_20260608_153000_1a2b3c4d"
170
176
  """Generate a unique, sortable correction run id."""
171
177
  return f"correct_{time.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
178
+
179
+
180
+ # The RECOMMENDED mark-class slate (DEC 2a231843) — an OPEN vocabulary: classes are
181
+ # DATA, not schema. A mark commits with any non-empty class string; this tuple is
182
+ # the evidence-derived starting set (census dc31c33c + the drive evidence chain)
183
+ # that pickers and status hints surface, so a new class found mid-walk is a journal
184
+ # entry, never a core release.
185
+ RECOMMENDED_MARK_CLASSES = (
186
+ "hesitation-omission", # single Um / 'you know' slots dropped (fill dominates)
187
+ "repeat-omission", # dropped repeated words/stutters — the omission-entangled boundary case
188
+ "meta-speech-omission", # spoken 'Quote' / 'End Quote' markers dropped
189
+ "meta-speech-executed", # meta-speech rendered AS punctuation instead
190
+ "homophone-substitution", # context-vs-acoustics substitution (where/were)
191
+ "proper-noun-suspect", # entity spelling suspect (Hiroo/Hiro/Hero)
192
+ "orthographic-drift", # decoder-state capitalization/orthography decay
193
+ "granularity-mismatch", # VAD split lands mid-word/mid-token
194
+ "foreign-speech", # non-English speech garbled into English (montage/quote cases; drive-minted 2026-07-19)
195
+ "suspect", # free-note catch-all — flag now, judge later
196
+ )
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional
9
9
  from cjm_substrate.core.journal_store import JournalEvent, SubstrateEventType
10
10
  from cjm_substrate.core.manager import CapabilityManager
11
11
  from cjm_substrate.core.queue import JobQueue
12
+ from cjm_substrate.core.workspace import resolve_recorded_tree
12
13
  from cjm_transcript_correction_core.graph import (active_corrections, build_prune_correction,
13
14
  commit_nodes_edges, commit_text_correction,
14
15
  count_source_segments,
@@ -31,8 +32,11 @@ logger = logging.getLogger(__name__)
31
32
  def load_decomp_manifest(
32
33
  path: str, # Path to a decomp-core run manifest JSON
33
34
  ) -> Dict[str, Any]: # Parsed manifest dict
34
- """Load + lightly validate a decomp-core run manifest (untyped JSON; CR-20 interchange)."""
35
- data = json.loads(Path(path).read_text())
35
+ """Load + lightly validate a decomp-core run manifest (untyped JSON; CR-20 interchange).
36
+
37
+ ${WS}/ recorded paths (5daadfc4 rung f) resolve to absolute here, anchored
38
+ at the manifest's own location (active workspace as fallback)."""
39
+ data = resolve_recorded_tree(json.loads(Path(path).read_text()), Path(path))
36
40
  fmt = data.get("format", "")
37
41
  if "decomp-core" not in fmt:
38
42
  logger.warning(f"unexpected decomp manifest format: {fmt!r} (continuing)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-correction-core
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: A frontend-agnostic core for the transcript correction workflow — the first downstream graph-extension core; composes the graph-storage capability worker into a headless pipeline that applies unified, non-destructive corrections (text, punctuation, segmentation) as a supersede-able overlay on a committed decomposition spine, recomputes the review worklist from deterministic signals, and exposes 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
@@ -10,9 +10,9 @@ Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.12
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
- Requires-Dist: cjm_substrate>=0.0.51
14
- Requires-Dist: cjm_context_graph_primitives>=0.0.14
15
- Requires-Dist: cjm-context-graph-layer>=0.0.8
13
+ Requires-Dist: cjm_substrate>=0.0.57
14
+ Requires-Dist: cjm_context_graph_primitives>=0.0.15
15
+ Requires-Dist: cjm-context-graph-layer>=0.0.11
16
16
  Requires-Dist: cjm-transcript-graph-schema>=0.0.7
17
17
  Dynamic: license-file
18
18
 
@@ -26,7 +26,7 @@ A frontend-agnostic core for the transcript correction workflow — the first do
26
26
 
27
27
  - **`cjm_transcript_correction_core.cli`** — The CLI driver — the correction core's first (and currently only) frontend. run <decomp-manifest> corrects the committed spine in the decomp graph DB, pointing the graph worker at that shared DB via load-time config, with optional session resume/reopen; review runs the interactive text-correction loop (the cross-transcriber diff is intra-graph since stage 5).
28
28
  - **`cjm_transcript_correction_core.graph`** — The correction overlay's graph I/O: targeted (scale-shaped) reads of a committed spine via the graph-storage query action, construction of Correction / CorrectionSession nodes + CORRECTS / SUPERSEDES / DERIVED_FROM / REVIEWED edges, the in-core effective-spine projection (layer-0 + applied corrections), and commit through the job queue. Hand-rolled (revolution-1) = direct CR-18 spec material; append-only on layer-0 (never update/delete a Segment).
29
- - **`cjm_transcript_correction_core.journal`**
29
+ - **`cjm_transcript_correction_core.journal`** — Live append-through for the correction verbs — the workflow journal's domain half.
30
30
  - **`cjm_transcript_correction_core.models`** — Overlay data shapes for the transcript-correction workflow: the Correction / CorrectionSession graph nodes + their relation registry, the read view of a committed spine segment, the worklist item, run configuration, and the correction run manifest (proto-bundle that chains decomp -> correction).
31
31
  - **`cjm_transcript_correction_core.pipeline`** — The headless correction workflow: load a decomp run manifest, resolve the shared graph DB, start/resume/reopen a CorrectionSession, recompute the worklist from deterministic signals + persisted review state, run the D14 empty-segment prune (first operation), and record a chainable correction run manifest — with a cheapest-form HITL approval seam.
32
32
  - **`cjm_transcript_correction_core.signals`** — Pure deterministic Tier-1 signal functions (no capability calls): empty-segment detection, bidirectional boundary punctuation/capitalization heuristics, forced-alignment coverage flags, positional cross-transcriber diff, and phonetic + edit-distance variant clustering. The worklist is recomputed from these each session; revolution-1 builds ZERO new capabilities.
@@ -46,11 +46,14 @@ A frontend-agnostic core for the transcript correction workflow — the first do
46
46
  - `active_corrections` _function_ — Filter to the effective correction set (the layer's resolve_active over a read superseded set).
47
47
  - `build_boundary_shift_correction` _function_ — Build a grouping Correction that moves text across one segment boundary.
48
48
  - `build_correction_node` _function_ — Construct a Correction overlay node (pure; commit happens separately).
49
+ - `build_mark_correction` _function_ — Build a NON-MUTATING mark Correction (DEC 2a231843: routed attention).
49
50
  - `build_prune_amendment` _function_ — Build a grouping Correction that supersedes a prune with a REDUCED set (unprune).
50
51
  - `build_prune_correction` _function_ — Build one batch grouping Correction that prunes empty segments (D14).
51
52
  - `build_reject_review` _function_ — Build a review Correction that REJECTS a prior correction (reject-as-supersede).
52
53
  - `build_text_correction` _function_ — Build a text_content Correction + its CORRECTS (+ optional SUPERSEDES) edges.
53
54
  - `commit_boundary_shift_correction` _function_ — Commit a boundary-shift correction (node + CORRECTS x2 [+ SUPERSEDES]) + REVIEWED markers on both segments.
55
+ - `commit_mark_correction` _function_ — Commit a mark (node + CORRECTS per anchored segment [+ SUPERSEDES]).
56
+ - `commit_mark_dismissal` _function_ — Dismiss an open mark WITHOUT a correction (reject-as-supersede).
54
57
  - `commit_nodes_edges` _function_ — Commit overlay nodes/edges through the layer's idempotent extend_graph.
55
58
  - `commit_prune_amendment` _function_ — Commit an unprune amendment (node + DERIVED_FROM edges + SUPERSEDES).
56
59
  - `commit_text_correction` _function_ — Commit a text_content correction (node + CORRECTS [+ SUPERSEDES]) + a REVIEWED marker.
@@ -66,7 +69,10 @@ A frontend-agnostic core for the transcript correction workflow — the first do
66
69
  - `load_source_corrections` _function_ — Load every Correction targeting a Source (across sessions) + the superseded-id set.
67
70
  - `load_source_segments` _function_ — Load a Source's fine Segment spine under its chosen rendition (typed query surface).
68
71
  - `load_variant_texts` _function_ — Resolve per-transcriber chunk texts from the segments' CharSlice refs.
72
+ - `mark_anchor_segments` _function_ — Validate a mark anchor and list the Segment ids it touches.
73
+ - `open_marks` _function_ — Filter to the OPEN marks — the pass-2 worklist ('query open marks, walk them').
69
74
  - `project_effective_spine` _function_ — Project the effective spine = layer-0 + applied corrections.
75
+ - `reanchor_span` _function_ — Re-locate a span anchor in text that may have been edited since mark time.
70
76
  - `record_review_markers` _function_ — Persist per-(session, segment) review markers as REVIEWED edges.
71
77
  - `resolve_source_renditions` _function_ — Pick the AudioRendition set whose fine Segment spine correction operates on.
72
78
  - `set_session_status` _function_ — Update a session's status + updated_at.
@@ -79,7 +85,6 @@ A frontend-agnostic core for the transcript correction workflow — the first do
79
85
  - `correction_replay_handlers` _function_ — The correction core's registered replay vocabulary (replay stays DOMAIN-OWNED).
80
86
  - `journal_correction_op` _function_ — Append one correction op — envelope + semantic args + the EXACT wires committed.
81
87
  - `segment_anchor` _function_ — The run-independent anchor stamped on every correction op (DEC ccbab9f5 point 5).
82
- - `sidecar_journal_path` _function_ — The db's sidecar journal path (DEC ccbab9f5 point 3: placement is per-workflow,
83
88
 
84
89
  ### `cjm_transcript_correction_core.models`
85
90
 
@@ -0,0 +1,5 @@
1
+ [cjm_context_graph_layer.replay]
2
+ correction = cjm_transcript_correction_core.journal:correction_replay_handlers
3
+
4
+ [console_scripts]
5
+ cjm-transcript-correction-core = cjm_transcript_correction_core.cli:main
@@ -0,0 +1,4 @@
1
+ cjm_substrate>=0.0.57
2
+ cjm_context_graph_primitives>=0.0.15
3
+ cjm-context-graph-layer>=0.0.11
4
+ cjm-transcript-graph-schema>=0.0.7
@@ -14,7 +14,7 @@ classifiers = [
14
14
  "Programming Language :: Python :: 3",
15
15
  "Programming Language :: Python :: 3 :: Only",
16
16
  ]
17
- dependencies = ['cjm_substrate>=0.0.51', 'cjm_context_graph_primitives>=0.0.14', 'cjm-context-graph-layer>=0.0.8', 'cjm-transcript-graph-schema>=0.0.7']
17
+ dependencies = ['cjm_substrate>=0.0.57', 'cjm_context_graph_primitives>=0.0.15', 'cjm-context-graph-layer>=0.0.11', 'cjm-transcript-graph-schema>=0.0.7']
18
18
 
19
19
  [project.urls]
20
20
  Repository = "https://github.com/cj-mills/cjm-transcript-correction-core"
@@ -22,6 +22,9 @@ Repository = "https://github.com/cj-mills/cjm-transcript-correction-core"
22
22
  [project.scripts]
23
23
  cjm-transcript-correction-core = "cjm_transcript_correction_core.cli:main"
24
24
 
25
+ [project.entry-points."cjm_context_graph_layer.replay"]
26
+ correction = "cjm_transcript_correction_core.journal:correction_replay_handlers"
27
+
25
28
  [tool.setuptools.dynamic]
26
29
  version = {attr = "cjm_transcript_correction_core.__version__"}
27
30
 
@@ -5,12 +5,15 @@ Projected from the graph notebook's two pure-check cells at the golden-reference
5
5
  from cjm_transcript_correction_core.graph import (
6
6
  active_corrections,
7
7
  build_boundary_shift_correction,
8
+ build_mark_correction,
8
9
  build_prune_amendment,
9
10
  build_prune_correction,
10
11
  build_reject_review,
11
12
  build_text_correction,
12
13
  corrections_to_edits,
14
+ open_marks,
13
15
  project_effective_spine,
16
+ reanchor_span,
14
17
  )
15
18
  from cjm_transcript_correction_core.models import SpineSegment
16
19
 
@@ -178,3 +181,94 @@ def test_prune_amendment_rescues_boundary_shift_target():
178
181
  eff2 = project_effective_spine(segs, active)
179
182
  assert [s.id for s in eff2] == ["a", "b", "c"]
180
183
  assert eff2[0].text == "largest naval battle" and eff2[1].text == "in history"
184
+
185
+
186
+ def test_build_mark_correction_anchor_shapes():
187
+ # segment: one CORRECTS edge, non-mutating payload vocabulary
188
+ node, edges = build_mark_correction("src1", {"kind": "segment", "segment_id": "a"},
189
+ "suspect", session_id="s1", note="check later")
190
+ props = node["properties"]
191
+ assert props["correction_type"] == "mark"
192
+ assert props["payload"]["operation"] == "mark"
193
+ assert props["payload"]["mark_class"] == "suspect"
194
+ assert props["rationale"] == "check later"
195
+ assert [(e["relation_type"], e["target_id"]) for e in edges] == [("CORRECTS", "a")]
196
+ # boundary: CORRECTS x2 (the shift gesture's coordinates; seams allowed)
197
+ _, edges = build_mark_correction(
198
+ "src1", {"kind": "boundary", "boundary_after": "a", "right_segment_id": "b"},
199
+ "repeat-omission", session_id="s1")
200
+ assert [(e["relation_type"], e["target_id"]) for e in edges] == [
201
+ ("CORRECTS", "a"), ("CORRECTS", "b")]
202
+ # span: offsets + verbatim snapshot ride the anchor; supersession is an edge
203
+ node, edges = build_mark_correction(
204
+ "src1", {"kind": "span", "segment_id": "c", "char_start": 0, "char_end": 5,
205
+ "text_snapshot": "world"},
206
+ "homophone-substitution", session_id="s1", supersedes_id="m0")
207
+ assert node["properties"]["payload"]["anchor"]["text_snapshot"] == "world"
208
+ assert ("SUPERSEDES", "m0") in [(e["relation_type"], e["target_id"]) for e in edges]
209
+
210
+
211
+ def test_build_mark_correction_validation():
212
+ def rejects(anchor, mark_class="suspect"):
213
+ try:
214
+ build_mark_correction("src1", anchor, mark_class, session_id="s1")
215
+ except ValueError:
216
+ return True
217
+ return False
218
+ assert rejects({"kind": "segment", "segment_id": "a"}, mark_class=" ")
219
+ # punctuation-led classes are reserved for gestures (the '`-`' junk-mark drive find)
220
+ assert rejects({"kind": "segment", "segment_id": "a"}, mark_class="-")
221
+ assert rejects({"kind": "segment", "segment_id": "a"}, mark_class="`-`")
222
+ assert rejects({"kind": "sentence", "segment_id": "a"})
223
+ assert rejects({"kind": "segment"})
224
+ assert rejects({"kind": "boundary", "boundary_after": "a"})
225
+ # a span without its snapshot could never re-anchor — refused at build time
226
+ assert rejects({"kind": "span", "segment_id": "a", "char_start": 0, "char_end": 2})
227
+
228
+
229
+ def test_mark_never_touches_projection():
230
+ """The DEC 2a231843 invariant: a mark is invisible to the effective view BY
231
+ CONSTRUCTION — corrections_to_edits has no arm for correction_type "mark"."""
232
+ node, _ = build_mark_correction("src1", {"kind": "segment", "segment_id": "b"},
233
+ "hesitation-omission", session_id="s1")
234
+ props = dict(node["properties"])
235
+ props["id"] = node["id"]
236
+ assert corrections_to_edits([props]) == []
237
+ out = project_effective_spine(SEGS, [props])
238
+ assert [(s.id, s.text) for s in out] == [(s.id, s.text) for s in SEGS]
239
+
240
+
241
+ def test_open_marks_lifecycle():
242
+ m1, _ = build_mark_correction("src1", {"kind": "segment", "segment_id": "a"},
243
+ "suspect", session_id="s1")
244
+ m2, _ = build_mark_correction("src1", {"kind": "boundary", "boundary_after": "a",
245
+ "right_segment_id": "b"},
246
+ "repeat-omission", session_id="s1")
247
+ fix, _ = build_text_correction("src1", "a", "Hello", session_id="s1",
248
+ supersedes_id=m1["id"])
249
+ rows = []
250
+ for n in (m1, m2, fix):
251
+ p = dict(n["properties"])
252
+ p["id"] = n["id"]
253
+ rows.append(p)
254
+ # open until something supersedes: the discharging correction closes m1
255
+ assert [m["id"] for m in open_marks(rows, set())] == [m1["id"], m2["id"]]
256
+ assert [m["id"] for m in open_marks(rows, {m1["id"]})] == [m2["id"]]
257
+ # a dismissal review (reject-as-supersede) closes m2 and is not itself a mark
258
+ rej, _ = build_reject_review("src1", m2["id"], session_id="s1")
259
+ p = dict(rej["properties"])
260
+ p["id"] = rej["id"]
261
+ assert open_marks(rows + [p], {m1["id"], m2["id"]}) == []
262
+
263
+
264
+ def test_reanchor_span():
265
+ a = {"char_start": 6, "char_end": 11, "text_snapshot": "world"}
266
+ assert reanchor_span(a, "hello world") == (6, 11) # exact offsets verified
267
+ assert reanchor_span(a, "well, hello world") == (12, 17) # edited text -> snapshot re-found
268
+ assert reanchor_span(a, "goodbye moon") is None # gone -> degrade to segment level
269
+ # multiple occurrences: the one nearest the recorded start wins
270
+ assert reanchor_span({"char_start": 0, "char_end": 2, "text_snapshot": "ab"},
271
+ "ab ab") == (0, 2)
272
+ assert reanchor_span({"char_start": 3, "char_end": 5, "text_snapshot": "ab"},
273
+ "ab ab") == (3, 5)
274
+ assert reanchor_span({"text_snapshot": ""}, "x") is None
@@ -1,8 +1,8 @@
1
1
  """The correction journal: sidecar path + op envelope (pure half; replay is e2e-proven)."""
2
2
 
3
3
  from cjm_context_graph_primitives.journal import read_journal
4
- from cjm_transcript_correction_core.journal import (correction_replay_handlers,
5
- journal_correction_op, sidecar_journal_path)
4
+ from cjm_transcript_correction_core.journal import correction_replay_handlers, journal_correction_op
5
+ from cjm_context_graph_layer.journal import sidecar_journal_path
6
6
 
7
7
 
8
8
  def test_sidecar_journal_path():
@@ -33,4 +33,11 @@ def test_journal_correction_op_envelope(tmp_path):
33
33
  assert len(read_journal(j)) == 2
34
34
  # every registered replay verb has a handler
35
35
  assert set(correction_replay_handlers()) == {"session-start", "boundary-shift", "text-correction",
36
- "prune-amendment", "review-markers", "session-status"}
36
+ "prune-amendment", "mark", "mark-dismiss",
37
+ "review-markers", "session-status"}
38
+
39
+
40
+ def test_replay_handlers_cover_mark_verbs():
41
+ """Marks are BORN JOURNALED (DEC 2a231843): both verbs replay as wire ops."""
42
+ handlers = correction_replay_handlers()
43
+ assert "mark" in handlers and "mark-dismiss" in handlers
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- cjm-transcript-correction-core = cjm_transcript_correction_core.cli:main
@@ -1,4 +0,0 @@
1
- cjm_substrate>=0.0.51
2
- cjm_context_graph_primitives>=0.0.14
3
- cjm-context-graph-layer>=0.0.8
4
- cjm-transcript-graph-schema>=0.0.7