collect: Collections (N documents, and the references between them)#

model defines the typed Collection and its DocEntry members; resolve is the pass that turns cross-document references into concrete ones before the degradation engine or any writer runs. Nothing here is imported by myform/__init__.py: a collection is opt-in, so the package import budget (spec section 10) is unaffected by its existence.

Import it directly::

from myform.collect import Collection, resolve

Collections (N documents, and the references between them).

model defines the typed Collection and its DocEntry members; resolve is the pass that turns cross-document references into concrete ones before the degradation engine or any writer runs. Nothing here is imported by myform/__init__.py: a collection is opt-in, so the package import budget (spec section 10) is unaffected by its existence.

Import it directly:

from myform.collect import Collection, resolve
class myform.collect.Collection(*, root: str = '', entries: Mapping[DocId, DocEntry] = {}, order: tuple[DocId, ...] = ())#

An identified set of documents, and the index that resolves references between them.

root is operator-supplied configuration rather than member data, so it sits outside the member-path validation boundary on purpose: it may be absolute (discover passes the scan directory through verbatim) or empty, and it is trusted. The guarantee this model makes is the other direction – every validated member path is join-safe against whatever root the operator supplies.

model_copy(*, update: Mapping[str, object] | None = None, deep: bool = False) → Collection#

Copy through validation so an updated collection cannot retain tainted entries.

classmethod model_construct(_fields_set: set[str] | None = None, **values: object) → Collection#

Validate construction rather than exposing an unsafe collection escape hatch.

classmethod of(entries: Iterable[DocEntry], *, root: str = '') → Collection#

Build a collection from explicit entries, preserving their order as the nav order.

Revalidate each public DocEntry at this trust boundary. Canonical entries retain their value identity; an entry created through a non-validating Pydantic surface is replaced by its canonical validated form, or rejected when its member path is unsafe.

classmethod discover(root: str, *, formats: Mapping[str, str] | None = None, excludes: Iterable[str] | None = None) → Collection#

Walk root and register every file whose suffix maps to a registered reader.

Parameters:
  • root – The directory to scan; every doc_id is relative to it.

  • formats – Suffix -> reader-name map, defaulting to DEFAULT_FORMATS.

  • excludes – Directory names to skip, defaulting to DEFAULT_EXCLUDES.

Returns:

A collection ordered by source path, which is a stable ordering rather than a meaningful one – a site generator is expected to supply its own.

get(doc_id: DocId) → DocEntry | None#

Return the entry registered under doc_id, or None.

locate(ref: str, *, origin: DocId = '') → DocEntry | None#

Resolve one reference string against this collection.

Accepts decoded filesystem spellings: a relative source path (../notes/sibling.md), a relative document id (../notes/sibling), and either of those rooted at the collection (/notes/sibling). The resolution pass strips URI query and fragment components and decodes its URI path exactly once before calling.

Parameters:
  • ref – The decoded member path, without URI query or fragment components.

  • origin – The doc id the reference was written in, which relative paths resolve against; the empty string treats ref as collection-rooted.

Returns:

The referenced entry, or None when the reference names no member – which is a dangling reference and is reported rather than repaired.

locate_basename(ref: str) → tuple[DocEntry, ...]#

Return every member matching one unqualified document basename.

This is the deterministic primitive Obsidian’s shortest wikilinks need. Returning all matches rather than one arbitrary entry makes ambiguity explicit to the resolution pass.

Parameters:

ref – An unqualified document id or source filename.

Returns:

Matching entries ordered by stable document identity, or an empty tuple when the reference is qualified or unmatched. URI classification belongs to ordinary-link resolution; vault basenames may contain scheme-shaped colons.

relative_output(entry: DocEntry, *, origin: DocId, suffix: str = '') → str#

Render entry’s output relative to the origin member’s output directory.

Parameters:
  • entry – The referenced member.

  • origin – The doc id doing the referencing. An unregistered id retains the historical identity-mapped fallback so partial collections can still render a relative path.

  • suffix – Extension to append to the output stem ('.html', '', …).

Returns:

A POSIX relative path, always prefixed ./ when it would otherwise be a bare name, so the result is unambiguously a path rather than a scheme-less URL.

classmethod is_external(ref: str) → bool#

Whether an authored URI has a scheme or protocol-relative authority.

class myform.collect.DocEntry(*, doc_id: DocId, source: str, format: str, output: str = '', title: str | None = None)#

One document’s identity within a collection.

source is the collection-relative source path with its suffix, which is what an author writes in a relative link. doc_id is that path without its suffix, which is what Sphinx and every {doc}-style reference use. Keeping both is what lets one index answer both spellings of the same reference. doc_id, source, and output must all use normalized relative POSIX paths, so later filesystem consumers can join them without a lexical escape from the collection root.

model_copy(*, update: Mapping[str, object] | None = None, deep: bool = False) → DocEntry#

Copy through normal validation so an immutable entry cannot bypass its path invariant.

class myform.collect.LinkStyle(*values)#

How a resolved cross-document reference is spelled in the output.

PATH rewrites the reference relative to the source document, which is what a rendered site, an HTML fragment, or a plain-markdown bundle wants. ROOT_PATH uses the member’s collection-rooted output path without a leading slash, for syntaxes such as Obsidian wikilinks. DOC_ID uses the canonical collection-relative id with a leading slash, which is what a Sphinx-bound target wants.

class myform.collect.Outcome(*values)#

What became of one candidate cross-document reference.

class myform.collect.ResolutionEntry(*, path: Path = (), node_kind: str, ref: str, outcome: Outcome, doc_id: DocId | None = None, candidates: tuple[DocId, ...] = ())#

One reported reference-resolution act at a source-tree address.

class myform.collect.ResolutionReport(*, origin: DocId = '', entries: tuple[ResolutionEntry, ...] = ())#

The complete ordered record of a single resolution pass.

Deliberately not a ConversionReport. A degradation report records what a target could not express; a dangling or ambiguous reference is neither approximated, replaced, dropped, nor enclosed – it is a promise the collection could not keep uniquely, and collapsing the two would make ConversionReport.lossy mean two different things.

property dangling: tuple[ResolutionEntry, ...]#

Every reference that named no member of the collection.

property resolved: tuple[ResolutionEntry, ...]#

Every reference rewritten to a concrete member.

property ambiguous: tuple[ResolutionEntry, ...]#

Every unqualified reference that matched more than one collection member.

summary() → str#

Render one deterministic CLI-ready summary line.

myform.collect.dangling_refs(reports: Iterable[ResolutionReport]) → tuple[ResolutionEntry, ...]#

Collect every dangling reference across a collection-wide set of reports.

myform.collect.doc_id_for(source: str) → DocId#

Derive the conventional doc id for one collection-relative source path.

myform.collect.resolve(doc: Doc, collection: Collection | None = None, *, origin: DocId = '', style: LinkStyle = LinkStyle.PATH, wiki_style: LinkStyle | None = None, suffix: str = '') → tuple[Doc, ResolutionReport]#

Rewrite doc’s cross-document references against collection.

Ordinary link URI paths are decoded once for collection lookup, then resolved filesystem output paths are URI-encoded before they are written back. Query and fragment components remain intact. Obsidian wikilinks and embeds instead retain literal vault-path identity; ROOT_PATH output remains vault syntax rather than being URI-encoded.

Parameters:
  • doc – The document to resolve, left unmodified.

  • collection – The collection doc belongs to; None makes this the identity.

  • origin – doc’s own id within the collection, which relative references resolve against.

  • style – How an ordinary resolved reference is spelled (LinkStyle).

  • wiki_style – Optional spelling override for wikilinks and embeds. This lets a target preserve vault-root semantics without changing ordinary Markdown links.

  • suffix – Extension appended to output stems under LinkStyle.PATH ('.html', …). Root-path wiki spelling uses the entry’s output unchanged.

Returns:

The resolved document and the complete ResolutionReport. When collection is None the same Doc object is returned, unmodified.