coordinator.extraction¶
coordinator.extraction ¶
Grounded-extraction pipeline helper.
This module backs the create_extraction_graph coordinator tool (registered
only when the zettelkasten capability is enabled). It performs the synchronous,
tool-side prep a grounded-extraction run needs -- ingesting each source into
the zettelkasten and seeding a per-source hub note -- and then emits the fixed
extractor -> scribe -> auditor task graph (plus a trailing memory task).
Prep writes notes directly by importing the zettelkasten library functions. This
runs inside the coordinator process: the zettelkasten code is always importable
(it is a core feature) and GRAPHS_DIR resolves from ANGELO_WORKSPACE, so
the coordinator writes into the same .zettelkasten/ tree the zettelkasten MCP
server uses. Doing prep here -- not as a graph task -- is what lets the extractor
(a planner) run first: a planner cannot depend on an implementer, so the
ingest + hub-seeding cannot be a graph task.
The trio agents/personas and the schema registry live in the zettelkasten capability; this module only orchestrates them.
ExtractionError ¶
StructurePersistError ¶
Bases: ExtractionError
Raised when an ENFORCING keyed structure/rubric write failed for EVERY
source -- a per-source PERSIST failure (the quarantine case), NOT a genuine
config/scaffold error (an empty resolved synthesis-graph name, a wrapped
SpineError, etc).
A subclass of :class:ExtractionError so existing except ExtractionError
handlers (e.g. reconcile_structures) keep catching it. Callers that must
distinguish "could not persist the rubric for any source" (DEMOTE the run to
flat -- the sources still extract context-less into the flat graph) from
"the run is mis-configured" (PROPAGATE as a visible error) narrow on this
subtype: only a persist failure demotes; a plain ExtractionError raises.
Source code in coordinator/extraction.py
describe_sources ¶
Human-readable summary of the sources in an extraction run.
Lists up to limit source titles (each truncated to title_max chars);
any beyond the limit collapse into a +N more suffix. Used to give a
coordinator run's goal/title a source-specific label instead of the generic
Grounded extraction: <project> (schema: <schema>), so a dashboard full of
extraction runs is distinguishable at a glance. Returns "" when no source
carries a usable title/name (callers omit the source clause in that case).
Source code in coordinator/extraction.py
prep_sources ¶
prep_sources(sources: list[dict], schema_obj: dict, project: str, hub_per_source: bool = True, projects: list[str] | None = None) -> list[dict]
Ingest each source and seed its hub note; return per-source prep records.
Each returned record: {name, title, hub_id, content_hash, source_path,
existing} where name is the source's zettelkasten graph (folder) name.
A source dict accepts: name (required), optional title, authors,
year, venue, doi, doc_type, and either path (the tool
ingests it) or a precomputed content_hash (+ optional source_path).
EXTRACT-ONCE, ATTACH-TO-MANY-PROJECTS: projects (when given) is the FULL
set of projects the source is registered into; the source is still
ingested/created ONCE (hash-deduped) and the hub seeded once -- only project
MEMBERSHIP fans out. The positional project stays the back-compat
single-target shim and the primary used for the hub body text; absent
projects it is the sole target, so single-project callers are byte-identical.
Only the LEGACY FLAT extraction block is persisted here (schema-level rubric),
once per source and project-agnostic. The per-context (project,
synthesis_graph) keyed entry is written later by :func:prep_spine /
:func:reconcile_meta_tags, once structure resolution knows the REAL synthesis
graph -- prep cannot know it yet (see the flat-write note below), so pre-seeding
a keyed entry here only orphans it.
Source code in coordinator/extraction.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 | |
prep_spine ¶
prep_spine(schema_obj: dict, project: str, prepped: list[dict], synthesis_label: str = '', parent_label: str = '') -> dict | None
Pre-create the schema's materialized structure (the spine), if declared.
When the schema carries a synthesis block this creates, idempotently:
a dedicated synthesis graph (registered as a project source), the apex
synthesis node, an optional spec stub, and one node per materialized
dimension. It wires dimension nodes -> apex, spec -> apex, and apex -> each
per-source hub, so per-source claims (which the scribe later attaches to
their dimension node) reach the hub through the spine rather than flat.
parent_label (build-time sub-spine wiring, design §3) declares THIS spine a
sub-spine of another: after the child apex exists, the parent spine's synthesis
graph + apex are resolved and a primary child-apex --component-of-->
parent-apex cross-graph edge is written as the child materializes. The parent
graph is resolved via the parent's ORG spine_ref (schema-independent, so a
HETEROGENEOUS tier stack whose parent uses a different synthesis.graph
template still resolves correctly), falling back to this schema's template only
for the homogeneous same-schema case. A parent resolved through a real spine org
that has no apex is a HARD ERROR (a broken parent, never silently skipped); a
template-fallback parent that is not built yet is logged and skipped so the
child still materializes. STABILITY RULE: a hand-set / hand-moved parent edge
WINS — if the child apex already carries a primary parent (e.g. a dashboard
connect), a re-seed does NOT overwrite it.
Returns the structure record threaded into task context, or None when the
schema declares no synthesis block (the run stays a flat checklist).
Source code in coordinator/extraction.py
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 | |
prep_spine_from_org ¶
prep_spine_from_org(org: dict, project: str, prepped: list[dict], *, get_graph: Any, materialize: bool = True) -> dict | None
Source the extraction STRUCTURE from a PROMOTED spine org (design §6-7).
Unlike :func:prep_spine -- which mints a FRESH scaffold from a named
schema's synthesis block -- this reads an already materialized spine: a
state=='spine' org whose spine_ref names the synthesis graph and whose
schema carries the EMBEDDED expanded rubric (both landed in phase 2). The
spine graph is read DIRECTLY (promoted spines are intentionally NOT project
sources, so discover_spines never sees them): the apex + dimension nodes
are found by a tag walk over spine.APEX_TAGS / spine.DIMENSION_TAG and
mapped back to the embedded schema's dimension tags.
Returns the SAME dict shape :func:prep_spine returns (and that
:func:build_extraction_tasks / :func:_structure_context consume):
{synthesis_graph, apex_id, spec_id, dimension_nodes, attach_relation,
apex_relation}. CRITICAL: attach_relation is the org's EMBEDDED relation
(spine-member for a promoted v2 spine), NOT input-to. The v2 readback
(_spine_membership_index / spine_readback_matrix) only sees spine-member
edges, so attaching extracted claims via input-to would make them invisible
in matrix/outline/graph -- this is correctness, not preference.
Degrades to None (caller skips this spine) when the org is not a promoted
spine, its spine_ref graph is missing/empty, its embedded schema has no
synthesis block, or no materialized dimension node matches a schema tag.
materialize (default True) gates the SOLE write this function performs:
the apex -> per-source-hub rollup edges (:func:link_spine_hubs). The tag-walk
/ dimension detection above is ALWAYS read-only. Scope resolution must evaluate
every candidate without side effects, so it calls with materialize=False
(detection only) and then invokes :func:link_spine_hubs ONCE for the chosen
spine. The default True preserves any direct/legacy caller.
Source code in coordinator/extraction.py
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 | |
link_spine_hubs ¶
Write the apex -> per-source-hub rollup edges for a resolved spine.
Factored out of :func:prep_spine_from_org so SCOPE RESOLUTION can stay a
PURE READ: detection runs with materialize=False (zero graph writes) for
EVERY candidate, the caller picks exactly ONE, then materializes it ONCE. The
spine graph is NOT (re)registered as a project source -- promoted spines are
deliberately not project sources. Best-effort: never abort.
No-op without an apex id / synthesis graph / prepared sources, so it is safe on any structure dict (including the named-schema fallback, which has no apex of this form).
Source code in coordinator/extraction.py
materialize_structure ¶
Perform the deferred WRITES for the CHOSEN spine structure.
:func:prep_spine_from_org defers BOTH of its side effects -- the apex ->
per-source-hub rollup edges AND the per-source extraction-meta -- behind its
materialize flag so scope resolution can detect every candidate as a pure
read. Once the caller picks ONE structure it calls this ONCE, so exactly the
chosen spine is materialized; ignored/broken candidates leave ZERO writes.
project (when known) keys the persisted structure meta by
(project, synthesis_graph) so the chosen spine's structure does not
clobber another context's; absent it, the legacy flat block is written. A
per-structure project (set by the multi-project resolver) takes precedence
over the passed project argument, so each spine is keyed under ITS OWN
project even when materialized in a multi-project run.
Returns the quarantine list from :func:_persist_structure_meta (empty here:
this structure-only write passes no strict, so it is never enforcing -- the
spine path's enforcing rubric write is :func:reconcile_meta_tags).
Source code in coordinator/extraction.py
resolve_extraction_structure ¶
resolve_extraction_structure(project: str, prepped: list[dict], spines: list[str], *, get_graph: Any, present_fn: 'Callable[[str, str], bool] | None' = None, multi: bool = False) -> dict
Resolve the spine scope to a structure resolution dict.
Shared by the coordinator create_extraction_graph tool AND the stream /
daemon path (phase 3b): both turn a spines scope into the
STRUCTURE(s) the trio tasks fill. get_graph is injected (the zettelkasten
_get_graph) so this module never imports the zettelkasten MCP server, and
present_fn (defaulting to :func:_explicit_spine_present) is injectable
purely so the coordinator wrapper can keep its existing monkeypatch seam.
Builds the ordered, de-duped scope and returns
{"structure", "structures", "ignored_spines", "broken_spines",
"unknown_spines"}:
structure-- the chosen STRUCTURE dict (Noneto fall back to the named-schema path). Back-compat: equalsstructures[0](orNone).structures-- the list of materialized STRUCTURE dicts. Withmulti=False(default) this is[structure](or[]) -- the single chosen spine, byte-identical to the historical behavior. Withmulti=Trueit holds EVERY in-scope spine that yielded a valid structure (the opt-in full multi-fill).ignored_spines-- other in-scope org ids that ALSO yielded a valid structure but were NOT used. In single mode (multi=False) this run fills exactly ONE structure, so every OTHER valid in-scope spine is deferred here (to a laterresync). In multi mode every valid spine is filled, so this shrinks to only entries beyond any optional cap (none today -- there is no cap, so it stays empty undermulti=True).broken_spines-- scoped org ids that CLAIM to be a promoted spine (state=='spine'with aspine_ref) but yielded no structure (their spine graph is missing/empty / no materialized dimension matches).unknown_spines-- EXPLICIT opt-ins (caller-named inspines) that resolved to no structure and are NOT broken-promoted: a nonexistent org id, a saved-but-UN-promoted lens, or astate=='spine'org with a BLANKspine_ref. Surfaced so an explicit opt-in never silently vanishes into the default. The auto-included DEFAULT falling through is NOT flagged (legacy no-scope behavior).
No-silent-loss for an EXPLICIT demand: if a caller-named org is PRESENT on
disk (present_fn) but cannot be loaded/normalized -- a corrupt org file,
or load_organization raising / returning None via a caught
ValueError -- this function RAISES ExtractionError rather than
bucketing it unknown and silently degrading to the named-schema
fallback. The caller explicitly demanded THAT spine; extracting into a
DIFFERENT structure would be a silent wrong-structure run. Only a genuinely
ABSENT explicit id (a typo) stays graceful unknown. Default/auto spine
resolution failures CONTINUE to degrade gracefully (broken/silent) --
only EXPLICIT opt-ins fail loud.
Scope resolution is a PURE READ: each candidate is detected via
:func:prep_spine_from_org with materialize=False (zero graph
writes), and only the CHOSEN structure is materialized (apex->hub edges +
source meta).
Precedence: EXPLICIT opt-ins take precedence over the project default. The
scope is the de-duped opt-in org ids in caller order FIRST, then the project
default appended last (still auto-included). The FIRST scoped org that yields
a valid structure is CHOSEN; every OTHER scoped org that ALSO yields a valid
structure is recorded in ignored_spines. So spines=['B'] with project
default A uses B and reports A as ignored. With no opt-ins the default is
the sole candidate (design §7). De-duping by org id means an org that is BOTH
the default and an explicit opt-in is processed once (no double-extraction).
MULTI-SPINE (multi=True, opt-in): fill EVERY in-scope spine that yields a
valid structure rather than collapsing to the first. The scope is already
LIST-shaped and detection already evaluates EVERY candidate as a pure read,
so this just collects ALL valid structures and materializes each
(materialize_structure per structure -- each writes its own keyed
(project, synthesis_graph) extraction-meta, so the contexts stay
independent). Downstream (build_extraction_tasks / _structure_context
/ the scribe persona) then carries MULTIPLE numbered STRUCTURE blocks. The
semantics: a claim attaches to the matching dimension node of EVERY in-scope
spine whose dimension it fits; a spine whose dimensions it does NOT fit gets
no membership edge and the claim stays connected to those spines only through
its organic inter-note edges (membership is per-dimension and opt-in, so
non-match == no edge, automatically -- heterogeneous schemas are handled for
free). Default multi=False keeps "first wins" and returns exactly ONE
structure (and structures=[structure]), byte-identical to today.
Source code in coordinator/extraction.py
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 | |
resolve_extraction_structures_multi ¶
resolve_extraction_structures_multi(projects: list[str], prepped: list[dict], spines: list[str], *, get_graph: Any, present_fn: 'Callable[[str, str], bool] | None' = None, multi: bool = False) -> dict
Resolve the spine scope across SEVERAL projects and union the structures.
The "extract-once, attach-to-many-projects" entrypoint. A source is prepped /
ingested ONCE (the caller already ran :func:prep_sources for the full project
set); this then calls :func:resolve_extraction_structure ONCE PER PROJECT --
each resolves and materializes that project's own spine scope under its OWN
(project, synthesis_graph) key, so the contexts stay independent (no
clobber) -- stamps structure["project"] = <that project> on every returned
structure so the downstream render / reconcile keys each spine under ITS OWN
project, and concatenates the per-project structure lists. The per-project
diagnostics (ignored_spines / broken_spines / unknown_spines) are
UNIONed. An explicit spine id that does not resolve in a given project's own
org tree simply lands in that project's unknown_spines (best-effort).
With a single project this is exactly :func:resolve_extraction_structure
plus a project stamp -- the multi-project codepaths stay inert.
Source code in coordinator/extraction.py
reconcile_meta_tags ¶
reconcile_meta_tags(prepped: list[dict], structure: dict, project: str = '', *, strict: bool | None = None, grounded: bool | None = None, schema: str = '') -> list[dict]
Reconcile each source's persisted strict-tag VOCABULARY to a chosen spine.
prep_sources persists the NAMED registry schema's tags (and strict
intent) onto every source BEFORE structure resolution. When a spines=-sourced
structure wins, its dimension_nodes are keyed by the SPINE's EMBEDDED
dimension tags -- an independent vocabulary. add_note strict enforcement
checks a note's tags against the persisted tags, so a scribe tagging claims
with the spine's tag (e.g. claim) would be REJECTED whenever the spine tags
are not a subset of the named schema's (e.g. {finding, method}) -- silently
breaking the spines= feature.
Re-write each prepared source's persisted tags to the CHOSEN structure's
dimension tags so the scribe's spine-tagged claims pass enforcement. No-op for
a structure with no dimension tags (so the named-schema fallback, which never
calls this, is unaffected).
Returns the list of QUARANTINED sources -- [{"source", "reason"}] for each
source whose ENFORCING keyed write failed -- so the caller can EXCLUDE them
from the run and report them. Empty for a clean / non-enforcing run.
The per-source write is best-effort EXCEPT for an ENFORCING keyed run (a keyed
write whose effective strict is True). There a failed write is NOT silently
swallowed -- otherwise the scribe's full (project, synthesis_graph) key
would miss against a populated contexts list and reject every claim later
(the no-silent-loss P1). But to avoid stranding the other healthy sources (the
P2 availability regression), a single enforcing source's write failure
QUARANTINES just that source (recorded in the returned list, loudly logged) and
the rest continue; only when NO source survives does this raise
:class:ExtractionError. Non-enforcing / flat / non-strict writes stay
best-effort, as does a missing-_meta.yaml (FileNotFoundError) -- a
structural "not a real prepped source" condition, not the transient write
failure the escalation targets.
When project is known the reconcile targets the per-context
(project, synthesis_graph) entry (the chosen spine's synthesis_graph);
without a project the legacy FLAT block is reconciled (back-compat) and ONLY
tags is written there (preserving whatever strict/grounded prep
wrote flat).
In the KEYED branch strict/grounded/schema (the run's rubric) are
written INTO the keyed entry alongside the spine tags. This is the spine
path's keyed rubric write: the keyed entry is no longer seeded from the flat
block, so the run's strict intent must be written here explicitly or
write-time enforcement would silently stay off for the spine context.
The keyed strict/grounded/schema PREFER the chosen spine's EMBEDDED rubric
carried on structure (embedded_strict / embedded_grounded /
embedded_schema_name, set by :func:prep_spine_from_org) over the
strict/grounded/schema arguments. A scoped spine's embedded
strict/grounded can differ from the named registry schema the caller passes;
persisting the named value would write the WRONG keyed rubric. The argument is
the FALLBACK, used only when the embedded field is absent (None / empty).
Source code in coordinator/extraction.py
1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 | |
reconcile_structures ¶
reconcile_structures(prepped: list[dict], structures: 'list[dict | None]', *, project: str, schema: str, strict: bool, grounded: bool) -> 'tuple[dict[str, list[dict]], list[dict], list[dict]]'
Reconcile each structure's keyed extraction-meta into a PER-SOURCE map.
The flat semantic graph is the always-present substrate; a spine is a SEPARATE additive overlay. So this NEVER aborts a run and NEVER drops a source: a source whose keyed rubric write FAILED for a structure simply does not have that structure available (it is DEMOTED to flat for that spine, and still extracts into the flat graph). A spine that no source could join is merely unused (still materialized -- cleanup deferred).
Runs :func:reconcile_meta_tags once per non-empty structure so every
(project, synthesis_graph) context gets its OWN keyed strict-tag meta.
For each structure, a source is AVAILABLE when its enforcing keyed write
SUCCEEDED. reconcile_meta_tags still surfaces failures the two historical
ways -- a per-source quarantined list (partial failure) and an
:class:ExtractionError (EVERY source failed) -- but here BOTH are absorbed
as demotions instead of exclusion/abort: a partial failure demotes just the
failing source(s) for that structure, and a total failure demotes ALL sources
for that structure (leaving it unused). Nothing propagates.
Returns (source_structures, demoted, unused):
source_structures--{source_name -> [available structure, ...]}for EVERY prepped source (preserving the input structure order; a source that joined no spine maps to[]and renders flat).demoted-- lightweight diagnostics: one{"source", "synthesis_graph", "reason"}per (source, structure) pair that fell back to flat because the keyed write failed.unused-- the structure dicts that NO source could join (zero members), so the caller can drop them from the rendered/surviving set and surface them as unused spines.
Source code in coordinator/extraction.py
1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 | |
finalize_source_structures ¶
finalize_source_structures(structures: 'list[dict | None]', source_structures: 'dict[str, list[dict]]') -> 'tuple[list[dict], dict[str, list[dict]] | None]'
Collapse a per-source availability map to (surviving, per_source).
surviving is the structures with >=1 member source, in the input order
(used for the run JSON / prep record + the back-compat singular structure
+ the shared structures= arg). per_source is the map to thread into
:func:build_extraction_tasks ONLY when sources have HETEROGENEOUS available
sets (some source was demoted from a SURVIVING spine); when every source sees
the SAME full surviving set -- the common no-demotion case, and also the case
where the only demotions were from spines that NO source joined -- it returns
None so the caller renders the shared (byte-identical) path.
Source code in coordinator/extraction.py
projects_touching ¶
Every project any prepped source belongs to (launch project first).
The synthesizer's connectivity work is per-project: a source registered into
projects B and C (not just the launch project A) should have its claims placed
on B's and C's spines and synthesized into their _cross hubs too. This
reverse-lookups source -> projects by scanning the project manifests (there is
no dedicated index): a project is "touched" when its sources list contains
any of this run's source graph names. The launch_project is always included
and placed first (it is the run's primary even if its manifest write lags).
Best-effort: a manifest that cannot be listed just means fewer touched
projects, never an error -- returns at least [launch_project].
Source code in coordinator/extraction.py
build_synthesis_context ¶
build_synthesis_context(prepped: list[dict], launch_project: str, *, get_graph: Any, present_fn: 'Callable[[str, str], bool] | None' = None, return_structures: bool = False) -> 'str | tuple[str, list[dict]]'
Materialize every spine layout of every touched project + render the block.
Two jobs, both feeding the trailing synthesizer task:
- MATERIALIZE. Reuse the multi-project resolver with
multi=Trueso EVERY promoted spine of EVERY project the sources touch gets its structure meta persisted under its own(project, synthesis_graph)key (and its apex -> hub rollup edges written). This is the prerequisite that lets the synthesizer'snote(action="attach")resolve a dimension for a spine the per-source scribe never chose. It is idempotent with the main run's own materialization (identical edges/meta dedup), so re-doing the launch project's chosen spine here is harmless. - RENDER. Emit one
SPINESblock per (project, spine) giving the project, the synthesis graph, and the dimension MAP -- each dimension'stag, node id, and a human description read from the dimension node itself -- so the synthesizer can judge, semantically, which dimension each claim falls under.
Returns the rendered SPINES: ... context string (empty "" when no
project in scope has a promoted spine, which tells the caller to skip the
synthesizer's placement step). Fully best-effort: any failure returns ""
rather than aborting graph creation -- synthesis is additive, never load-bearing.
return_structures (default False) is an additive opt-in for the
STANDALONE synthesis-prep path (prep_synthesis_contexts): when True the
function returns (context_str, structures) where structures is the list
of resolved+materialized spine structure dicts (each carrying project and
synthesis_graph), so a caller can report exactly which
(project, synthesis_graph) contexts it just registered. The default keeps
the historical bare-string return byte-identical for create_extraction_graph.
Source code in coordinator/extraction.py
2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 | |
plan_slices ¶
plan_slices(prep: dict, *, window: int = SLICE_WINDOW_DEFAULT, overlap: int = SLICE_OVERLAP_DEFAULT, threshold: int = SLICE_CHAR_THRESHOLD_DEFAULT, page_threshold: int = SLICE_PAGE_THRESHOLD_DEFAULT, explicit: 'list[dict] | None' = None) -> list[dict]
Plan the SLICE fan-out for one prepared source.
Returns a list of slice dicts, each {index, total, kind, label, char_start,
char_end, page_start, page_end} where kind is full | chars |
pages | chapter and a None char_end means "to the end".
A source at or below the size threshold (chars <= threshold and pages
<= page_threshold) yields a SINGLE full slice -- byte-identical to the
historical single-trio run. Above it: caller explicit slices verbatim,
else chapter-grouped windows when the source carries a cached outline, else
plain char windows.
Source code in coordinator/extraction.py
slice_read_call ¶
Render the source(action="fulltext", ...) call a slice's extractor uses.
Source code in coordinator/extraction.py
slice_label ¶
Human-readable SLICE line for a slice's task context.
Source code in coordinator/extraction.py
build_extraction_tasks ¶
build_extraction_tasks(prepped: list[dict], schema_name: str, project: str, batch_size: int = 0, include_memory: bool = True, structure: dict | None = None, structures: 'list[dict | None] | None' = None, source_structures: 'dict[str, list[dict | None]] | None' = None, synthesis_context: str = '', include_synthesis: bool = False, connection_context: str = '', slice_window: int = SLICE_WINDOW_DEFAULT, slice_overlap: int = SLICE_OVERLAP_DEFAULT, slice_threshold: int = SLICE_CHAR_THRESHOLD_DEFAULT, slice_page_threshold: int = SLICE_PAGE_THRESHOLD_DEFAULT, reextract: bool = False, slice_report: 'dict[str, dict] | None' = None) -> list[dict]
Emit the per-source extractor -> scribe -> auditor DAG (+ memory task).
BOOK-SCALE FAN-OUT: a source is sliced by :func:plan_slices (char windows,
chapter-snapped when the source carries a cached PDF outline, or a caller's
explicit slices list). A source at/below the size threshold yields ONE
full slice and its trio is BYTE-IDENTICAL to the historical single-trio
run. A large source emits one extractor -> scribe pair PER slice (all
sharing the per-source hub + structure), then ONE auditor depending on
ALL that source's scribes -- so the coverage matrix stays per-source and every
slice is committed before the audit.
INCREMENTAL SKIP: planned slices already covered by the source's
completed-slice ledger (stamped by a prior run's auditor) are dropped, so a
re-run only mines the not-yet-extracted regions. A source whose EVERY slice is
already done emits NO trio and is reported as skipped in slice_report.
reextract=True bypasses the filter (re-mines everything). When
slice_report is a dict it is populated with {source_name: {"done":
[labels], "todo": [labels]}} so the caller can surface what was skipped vs
planned; it never affects the emitted tasks.
Each scribe/data-scribe declares writes: [".zettelkasten/<src>"] — a
write-lease scoped to its SOURCE BOX. Scribes on DIFFERENT sources get
disjoint lease keys and run in parallel; scribes on the SAME box (e.g. two
per-schema scribes over one source in a union run) serialize at claim time,
since their search-then-add is not atomic across calls and would otherwise
duplicate notes. The trailing synthesizer keeps writes: [] (it
legitimately touches many boxes). The extractor (planner) and auditor
(checker) take no write lease.
When include_synthesis is set (the create_extraction_graph tool opts
in via its synthesize flag; other callers such as the stream daemon keep
the default False so their graph shape is unchanged), a single trailing
synthesizer task is appended after ALL auditors: it places the run's
claims onto every touched project's other spine layouts (using
synthesis_context) and wires cross-source themes into _cross hubs. The
memory task then trails the synthesizer so the run is recorded last.
Each task carries schema=schema_name so the coordinator flows the
expanded rubric into its context. Extractors are independent (one wave)
unless batch_size > 0, in which case each batch's extractors chain to the
previous batch's extractors (planner -> planner, which the validator allows)
so the orchestrator fans out one batch at a time.
The flat semantic graph is the always-present substrate; a spine is an additive overlay. Rendering is therefore PER SOURCE, keyed on the structures AVAILABLE to each source:
source_structures(preferred) mapssource_name -> [structure, ...]-- the structures whose keyed extraction-meta write SUCCEEDED for that source. A source whose write failed for a given spine simply does NOT have it in the list (demoted to flat for that spine), and is NEVER dropped from the run. Sources absent from the map (or mapped to[]) render FLAT.- When
source_structuresisNone(callers not in spine mode, or a clean run with NO per-source demotions) the run falls back to the sharedstructures/structurelist applied uniformly to every source -- byte-identical to the historical behavior.
structures carries the MULTI-SPINE list (one entry per in-scope spine);
structure (singular) is the back-compat scalar (defaults to
[structure] when structures is not given). With ZERO or ONE valid
structure (per source) the rendered STRUCTURE block + scribe/auditor
instructions are BYTE-IDENTICAL to the historical single-/no-structure
behavior; with MORE than one the task context carries numbered STRUCTURE
blocks and the scribe attaches each claim to EVERY spine whose
dimension_nodes contains the claim's tag.
Source code in coordinator/extraction.py
2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 | |
build_union_extraction_tasks ¶
build_union_extraction_tasks(prepped: list[dict], schema_specs: list[dict], project: str, *, batch_size: int = 0, include_memory: bool = False, synthesis_context: str = '', include_synthesis: bool = False, connection_context: str = '', slice_window: int = SLICE_WINDOW_DEFAULT, slice_overlap: int = SLICE_OVERLAP_DEFAULT, slice_threshold: int = SLICE_CHAR_THRESHOLD_DEFAULT, slice_page_threshold: int = SLICE_PAGE_THRESHOLD_DEFAULT, reextract: bool = False, slice_report: 'dict[str, dict] | None' = None) -> list[dict]
Emit a UNION extractor -> per-schema scribe -> per-schema auditor DAG.
The extract-once fan-out for a MULTI-SCHEMA run: instead of one extractor per
schema re-reading the same source, each source/slice gets ONE shared extractor
carrying ALL the schemas' rubrics (a SCHEMAS list). It tags each candidate
with the schema: it belongs to; then ONE scribe PER schema (each depending
on the shared extractor, each carrying its own schema so the persona
filters candidates to that schema) commits its claims, and ONE auditor PER
schema audits + stamps that schema's slices.
schema_specs is a list of per-schema dicts, each {name, structure,
structures, source_structures, prefix} where prefix is the alias
namespace the caller derived (its existing per-schema uniqueness logic). The
shared extractor takes NO prefix (it is one node across schemas); the scribes
and auditors take their schema's prefix.
A SINGLE-element schema_specs is delegated verbatim to
:func:build_extraction_tasks (with the spec's prefix applied), so a
one-schema run stays BYTE-IDENTICAL to today: one extractor, one scribe, one
auditor per slice.
Incremental skip, reextract, slice_report, and the trailing
synthesizer/memory tasks behave exactly as in :func:build_extraction_tasks.
The per-schema skip is honored independently: a slice already done for schema
A but not schema B is still read (for B), and only B's scribe processes it.
Source code in coordinator/extraction.py
3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 | |