// Paragraph identity provenance (paraId R1).
//
// `w14:paraId` is IMPORTED, IMMUTABLE provenance: reads never stamp or
// repair it, duplicated ids never resolve first-wins, and invalid
// spellings are reported, never normalized in place. This layer turns
// the scanner's as-spelled capture into per-paragraph anchor judgments;
// it is built from the retained projection and its scan — never from
// the annotation index, which stays presentation data, not identity
// authority.
///|
/// One logical paragraph's stable-anchor judgment.
///
/// `path` is where the paragraph is in THIS snapshot; the anchor is what
/// the paragraph IS across structural edits. The two are deliberately
/// separate dimensions — and both are separate from planner
/// editability (`actionable`), which this layer never touches.
pub struct DocxParagraphAnchor {
/// `unique`, `missing`, `invalid`, `duplicate`, or `multi_physical`.
priv status : String
/// The canonical (uppercase) value when the spelling is a valid
/// paraId — present for `unique` AND `duplicate` (both carriers
/// report the value; neither duplicate is an addressable anchor).
/// Absent for `missing`, `invalid`, and `multi_physical`.
priv para_id : String?
/// For `multi_physical` only: the valid canonical ids of the
/// PARTICIPATING physical paragraphs, in document order, bounded — a
/// diagnostic inventory, never a singular anchor. One element when a
/// single physical paragraph is shared by several logical ones.
priv physical_para_ids : Array[String]
}
///|
/// The anchor status: `unique`, `missing`, `invalid`, `duplicate`, or
/// `multi_physical`.
pub fn DocxParagraphAnchor::status(self : DocxParagraphAnchor) -> String {
self.status
}
///|
/// The canonical (uppercase) paraId when the spelling is valid and the
/// anchor is a carrier (`unique` or `duplicate`); `None` otherwise.
pub fn DocxParagraphAnchor::para_id(self : DocxParagraphAnchor) -> String? {
self.para_id
}
///|
/// For `multi_physical` anchors only: the valid canonical ids of the
/// participating physical paragraphs, in document order, bounded.
pub fn DocxParagraphAnchor::physical_para_ids(
self : DocxParagraphAnchor,
) -> Array[String] {
// A COPY: arrays are reference-backed, and a caller's mutation must
// not rewrite the anchor's judgment.
self.physical_para_ids.copy()
}
///|
/// How many joined physical ids a `multi_physical` anchor reports
/// before truncating: the inventory is diagnostic, and a revision-heavy
/// join must not turn an anchor record into a payload.
let max_reported_physical_para_ids : Int = 8
///|
/// Validate one as-spelled paraId and return its canonical uppercase
/// form: exactly eight ASCII hex digits (either case), nonzero, and
/// below 0x80000000 (MS-DOCX: greater than zero, high bit clear).
/// Anything else is not an identity.
///
/// Distinct from the comments layer's `canonical_para_id`, which only
/// case-normalizes conforming spellings for comparison and passes
/// everything else through verbatim — this is the ADDRESSING judgment,
/// and it rejects what it cannot vouch for.
pub fn validated_para_id(raw : String) -> String? {
guard raw.length() == 8 else { return None }
let builder = StringBuilder(size_hint=8)
let mut all_zero = true
for index, character in raw {
let code = character.to_int()
let upper = if code >= 'a'.to_int() && code <= 'f'.to_int() {
code - 32
} else {
code
}
let is_hex = (upper >= '0'.to_int() && upper <= '9'.to_int()) ||
(upper >= 'A'.to_int() && upper <= 'F'.to_int())
guard is_hex else { return None }
if upper != '0'.to_int() {
all_zero = false
}
// The high bit lives in the FIRST digit: 8-F there means a value
// at or above 0x80000000, which the format reserves.
if index == 0 && upper >= '8'.to_int() && upper <= '9'.to_int() {
return None
}
if index == 0 && upper >= 'A'.to_int() && upper <= 'F'.to_int() {
return None
}
builder.write_char(upper.unsafe_to_char())
}
guard !all_zero else { return None }
Some(builder.to_string())
}
///|
/// Per-story anchor index. The AUTHORITATIVE key is the logical
/// paragraph's projection index — paths are NOT unique (a tolerated
/// nested paragraph makes two logical paragraphs share one physical
/// head, hence one path), so a path-keyed lookup refuses collided
/// paths rather than answering for either claimant.
pub struct DocxParagraphAnchorIndex {
priv anchors : Array[DocxParagraphAnchor?]
priv by_path : Map[String, DocxParagraphAnchor?]
// The scanner paragraph path per projection paragraph — the address a
// resolved identity CURRENTLY answers to.
priv paths : Array[String?]
// canonical id -> projection indices of ADDRESSABLE carriers (single
// physical, not shared; status unique or duplicate). A carrier buried
// inside a multi-physical join is deliberately NOT here — it counts
// in `buried_para_ids` so resolution can refuse with the truth.
priv by_para_id : Map[String, Array[Int]]
priv buried_para_ids : Map[String, Int]
}
///|
/// The judgment for one projected logical paragraph, by its index in
/// the projection — the join `find` and the planners already hold.
pub fn DocxParagraphAnchorIndex::anchor_of_paragraph(
self : DocxParagraphAnchorIndex,
paragraph_index : Int,
) -> DocxParagraphAnchor? {
match self.anchors.get(paragraph_index) {
Some(anchor) => anchor
None => None
}
}
///|
/// The scanner paragraph path a projection paragraph answers to in
/// THIS snapshot.
pub fn DocxParagraphAnchorIndex::scan_path_of_paragraph(
self : DocxParagraphAnchorIndex,
paragraph_index : Int,
) -> String? {
match self.paths.get(paragraph_index) {
Some(path) => path
None => None
}
}
///|
/// Projection indices of the ADDRESSABLE carriers of a canonical id.
pub fn DocxParagraphAnchorIndex::paragraphs_with_para_id(
self : DocxParagraphAnchorIndex,
canonical : String,
) -> Array[Int] {
// A COPY: the internal carrier list is the resolver's authority, and
// a caller's mutation must not change later resolution answers.
match self.by_para_id.get(canonical) {
Some(list) => list.copy()
None => []
}
}
///|
/// Path-keyed lookup for callers that only hold a path. A path claimed
/// by MORE than one logical paragraph returns None: an ambiguous name
/// must not resolve to either claimant's judgment.
pub fn DocxParagraphAnchorIndex::anchor_at(
self : DocxParagraphAnchorIndex,
path : String,
) -> DocxParagraphAnchor? {
match self.by_path.get(path) {
Some(entry) => entry
None => None
}
}
///|
/// Build the anchor index for one story.
///
/// Duplicate detection is PART-SCOPED over every WML `w:p` the scan
/// retained (the reader-selected view of the part) — not merely the
/// projected logical paragraphs — because Word's uniqueness rule is
/// per part, and a collision hiding in suppressed content still makes
/// the id unaddressable. Only VALID spellings can collide: an invalid
/// value is not an identity, so it neither claims nor contests one.
pub fn docx_paragraph_anchor_index(
annotated : DocxAnnotatedResult,
story : DocxStoryPartSource,
) -> DocxParagraphAnchorIndex raise DocxError {
let part = story.part()
guard annotated.reader_projections.get(part) is Some(projection) else {
raise Unsupported(
message="paragraph identity requires a mutation-safe read with a retained projection for '\{part}'",
)
}
let elements = projection.scan.elements()
// Pass 1: the part-scoped multiset of valid canonical ids.
let occupancy : Map[String, Int] = Map([])
for element in elements {
if is_wml_uri(element.uri) && element.local_name == "p" {
match element.para_id_raw {
Some(raw) =>
match validated_para_id(raw) {
Some(canonical) =>
occupancy[canonical] = occupancy.get(canonical).unwrap_or(0) + 1
None => ()
}
None => ()
}
}
}
// Pass 2: how many logical paragraphs each physical source
// participates in. A stable anchor requires a one-physical-to-
// one-logical mapping, so participation above one — a tolerated
// nested paragraph splitting its host — voids singular anchoring
// for EVERY logical paragraph that shares the physical.
let participation : Map[Int, Int] = Map([])
for paragraph in projection.paragraphs {
for source in paragraph.sources {
let SourceElementId(index) = source.source
participation[index] = participation.get(index).unwrap_or(0) + 1
}
}
// Pass 3: one judgment per projected logical paragraph, keyed by
// projection index; the path map is a secondary name that refuses
// collisions.
let paragraph_paths = find_path_map(projection.scan, "p")
let anchors : Array[DocxParagraphAnchor?] = []
let by_path : Map[String, DocxParagraphAnchor?] = Map([])
let paths : Array[String?] = []
let by_para_id : Map[String, Array[Int]] = Map([])
let buried_para_ids : Map[String, Int] = Map([])
for paragraph_index, paragraph in projection.paragraphs {
guard paragraph.sources.length() > 0 else {
anchors.push(None)
paths.push(None)
continue
}
let SourceElementId(head) = paragraph.sources[0].source
let mut shared = false
for source in paragraph.sources {
let SourceElementId(index) = source.source
if participation.get(index).unwrap_or(0) > 1 {
shared = true
}
}
let anchor : DocxParagraphAnchor = if paragraph.sources.length() > 1 ||
shared {
// No one-to-one mapping — joined from several physicals, or
// sharing a physical with another logical paragraph. Either way
// a singular id would be a promise resolution cannot keep.
let physical : Array[String] = []
for source in paragraph.sources {
let SourceElementId(index) = source.source
match elements[index].para_id_raw {
Some(raw) =>
match validated_para_id(raw) {
Some(canonical) => {
// The buried INVENTORY is complete — resolution's
// judgment must not depend on the diagnostic cap,
// which bounds only the reported array below.
buried_para_ids[canonical] = buried_para_ids
.get(canonical)
.unwrap_or(0) +
1
if physical.length() < max_reported_physical_para_ids {
physical.push(canonical)
}
}
None => ()
}
None => ()
}
}
{ status: "multi_physical", para_id: None, physical_para_ids: physical, }
} else {
match elements[head].para_id_raw {
None => { status: "missing", para_id: None, physical_para_ids: [], }
Some(raw) =>
match validated_para_id(raw) {
None => { status: "invalid", para_id: None, physical_para_ids: [], }
Some(canonical) =>
if occupancy.get(canonical).unwrap_or(0) > 1 {
{
status: "duplicate",
para_id: Some(canonical),
physical_para_ids: [],
}
} else {
{
status: "unique",
para_id: Some(canonical),
physical_para_ids: [],
}
}
}
}
}
anchors.push(Some(anchor))
match anchor.para_id {
Some(canonical) =>
match by_para_id.get(canonical) {
Some(list) => list.push(paragraph_index)
None => by_para_id[canonical] = [paragraph_index]
}
None => ()
}
match paragraph_paths.get(elements[head].byte_start) {
Some(path) => {
paths.push(Some(path))
if by_path.contains(path) {
// Second claimant: the path no longer names one paragraph.
by_path[path] = None
} else {
by_path[path] = Some(anchor)
}
}
None => paths.push(None)
}
}
{ anchors, by_path, paths, by_para_id, buried_para_ids, }
}
///|
/// The identity DELTA judgment for a mutation's candidate against its
/// source, part-scoped per Word's uniqueness rule. Existing dirt may
/// SURVIVE — real documents carry duplicate and invalid ids, and a text
/// edit must not be hostage to them — and removals are always fine
/// (consumed content takes its ids with it). What a write may never do:
///
/// - introduce an invalid spelling the part did not already carry
/// (per raw spelling, count-bounded by the source), or
/// - increase a valid id's per-part occupancy beyond one — creating a
/// NEW collision or deepening an existing one. Case-insensitive:
/// `1a…` and `1A…` are the same identity.
///
/// Returns the first violation's description, or None when the delta is
/// acceptable. Cross-part equality is NOT a violation — identity scope
/// is the part.
pub fn para_id_state_delta_violation(
source : Map[String, Array[String]],
candidate : Map[String, Array[String]],
) -> String? {
for part, candidate_values in candidate {
let source_values = source.get(part).unwrap_or([])
let source_invalid : Map[String, Int] = Map([])
let source_valid : Map[String, Int] = Map([])
for value in source_values {
match validated_para_id(value) {
Some(canonical) =>
source_valid[canonical] = source_valid.get(canonical).unwrap_or(0) + 1
None =>
source_invalid[value] = source_invalid.get(value).unwrap_or(0) + 1
}
}
let candidate_invalid : Map[String, Int] = Map([])
let candidate_valid : Map[String, Int] = Map([])
for value in candidate_values {
match validated_para_id(value) {
Some(canonical) =>
candidate_valid[canonical] = candidate_valid
.get(canonical)
.unwrap_or(0) +
1
None =>
candidate_invalid[value] = candidate_invalid.get(value).unwrap_or(0) +
1
}
}
for value, count in candidate_invalid {
if count > source_invalid.get(value).unwrap_or(0) {
return Some(
"the operation introduces an invalid w14:paraId spelling in '\{part}'",
)
}
}
for canonical, count in candidate_valid {
if count > 1 && count > source_valid.get(canonical).unwrap_or(0) {
return Some(
"the operation makes w14:paraId '\{canonical}' collide in '\{part}'",
)
}
}
}
None
}
///|
/// One tree paragraph occurrence's relationship to the projection: the
/// judgment travels ONLY across a proven bijection. `Unjoined` is a
/// JOIN outcome, not a sixth anchor status — the engine vocabulary is
/// unchanged, and a tree surface reporting it says "no sound
/// correspondence", never a borrowed judgment.
pub enum DocxParagraphJoin {
Joined(Int, DocxParagraphAnchor)
Unjoined(String)
}
///|
/// The tree/projection join for one story (paraId R1b): tree paragraph
/// occurrences in erase order, each mapped to at most one projection
/// paragraph by source-vector bijection.
pub struct DocxParagraphAnchorJoinIndex {
priv joins : Array[DocxParagraphJoin]
priv reverse : Map[Int, Int]
priv anchor_index : DocxParagraphAnchorIndex
}
///|
/// The join for the Nth tree Paragraph occurrence, in the depth-first
/// order the erased tree presents them. None when the occurrence is out
/// of range — a caller whose own enumeration disagrees with the
/// provenance channel must treat EVERY paragraph as unjoined.
pub fn DocxParagraphAnchorJoinIndex::join_of_occurrence(
self : DocxParagraphAnchorJoinIndex,
occurrence : Int,
) -> DocxParagraphJoin? {
self.joins.get(occurrence)
}
///|
/// Number of paragraph occurrences represented by this story join.
pub fn DocxParagraphAnchorJoinIndex::occurrence_count(
self : DocxParagraphAnchorJoinIndex,
) -> Int {
self.joins.length()
}
///|
/// The R2a seam: the unique tree occurrence a projection paragraph
/// joined to, if the bijection held in both directions.
pub fn DocxParagraphAnchorJoinIndex::occurrence_of_paragraph(
self : DocxParagraphAnchorJoinIndex,
paragraph_index : Int,
) -> Int? {
self.reverse.get(paragraph_index)
}
///|
/// Build the story's tree/projection join.
///
/// A tree occurrence joins a projection paragraph only when ALL hold:
/// the source vectors are exactly equal; exactly one occurrence and one
/// projection paragraph claim that vector; and every physical source in
/// the vector participates in exactly one projection paragraph. The
/// last condition kills the false match the nested-paragraph shape
/// offers (the outer tree paragraph's [host] superficially equals the
/// post-half projection paragraph's [host], but the host also
/// participates in the joined head) — refusing beats first-wins.
pub fn docx_paragraph_anchor_join_index(
annotated : DocxAnnotatedResult,
story : DocxStoryPartSource,
) -> DocxParagraphAnchorJoinIndex raise DocxError {
let part = story.part()
guard annotated.reader_paragraph_provenance.get(part) is Some(tree_vectors) else {
raise Unsupported(
message="paragraph identity join requires a mutation-safe read with retained provenance for '\{part}'",
)
}
guard annotated.reader_projections.get(part) is Some(projection) else {
raise Unsupported(
message="paragraph identity join requires a retained projection for '\{part}'",
)
}
let anchors = docx_paragraph_anchor_index(annotated, story)
// Projection-side claims and participation.
let projection_claims : Map[String, Array[Int]] = Map([])
let participation : Map[Int, Int] = Map([])
for paragraph_index, paragraph in projection.paragraphs {
let key = StringBuilder()
for source in paragraph.sources {
let SourceElementId(index) = source.source
participation[index] = participation.get(index).unwrap_or(0) + 1
key.write_string("\{index},")
}
let rendered = key.to_string()
match projection_claims.get(rendered) {
Some(list) => list.push(paragraph_index)
None => projection_claims[rendered] = [paragraph_index]
}
}
// Tree-side claims.
let tree_claims : Map[String, Int] = Map([])
let tree_keys : Array[String?] = []
for vector in tree_vectors {
let mut incomplete = false
let key = StringBuilder()
for entry in vector {
match entry {
Some(SourceElementId(index)) => key.write_string("\{index},")
None => incomplete = true
}
}
if incomplete || vector.length() == 0 {
tree_keys.push(None)
} else {
let rendered = key.to_string()
tree_claims[rendered] = tree_claims.get(rendered).unwrap_or(0) + 1
tree_keys.push(Some(rendered))
}
}
let joins : Array[DocxParagraphJoin] = []
let reverse : Map[Int, Int] = Map([])
for occurrence, entry in tree_keys {
match entry {
None => joins.push(Unjoined("missing-source-identity"))
Some(key) =>
match projection_claims.get(key) {
None => joins.push(Unjoined("no-projection-counterpart"))
Some(claimants) =>
if claimants.length() != 1 {
joins.push(Unjoined("ambiguous-projection-claim"))
} else if tree_claims.get(key).unwrap_or(0) != 1 {
joins.push(Unjoined("ambiguous-tree-claim"))
} else {
let paragraph_index = claimants[0]
let mut shared = false
for source in projection.paragraphs[paragraph_index].sources {
let SourceElementId(index) = source.source
if participation.get(index).unwrap_or(0) > 1 {
shared = true
}
}
if shared {
joins.push(Unjoined("shared-physical-source"))
} else {
match anchors.anchor_of_paragraph(paragraph_index) {
Some(anchor) => {
joins.push(Joined(paragraph_index, anchor))
reverse[paragraph_index] = occurrence
}
None => joins.push(Unjoined("unjudged-paragraph"))
}
}
}
}
}
}
{ joins, reverse, anchor_index: anchors, }
}
///|
/// One stable-selector resolution outcome. `p[id="…"]` resolves to a
/// TREE OCCURRENCE only through the whole chain — validated id, exactly
/// one addressable carrier, sound tree join — and every other state is
/// its own typed refusal. There is no first-wins and no ordinal
/// fallback anywhere in this enum.
pub enum DocxParaIdResolution {
/// The unique carrier's tree occurrence (erase order) and its
/// projection index.
ResolvedOccurrence(Int, Int)
/// Not eight case-insensitive hex digits in 1..0x7FFFFFFF.
ParaIdInvalid
/// No paragraph in the story carries the id.
ParaIdNotFound
/// More than one carrier claims the id (part-scoped, case-insensitive)
/// — the ADDRESSABLE carriers' projection indices, for diagnostics.
ParaIdAmbiguous(Array[Int])
/// The id's only carriers are buried inside multi-physical joins —
/// the identity exists but names no addressable paragraph.
ParaIdInMultiPhysical
/// A single addressable carrier exists but its tree join does not —
/// the paragraph cannot be named in the tree without guessing.
ParaIdUnjoined(Int)
}
///|
/// A carrier-level resolution: the identity judgment WITHOUT the tree
/// step. Its own type, so no consumer can mistake a carrier answer for
/// a tree occurrence.
pub enum DocxParaIdCarrierResolution {
/// The single addressable carrier's projection index.
ResolvedCarrier(Int)
CarrierInvalid
CarrierNotFound
CarrierAmbiguous(Array[Int])
CarrierInMultiPhysical
}
///|
/// The identity judgment WITHOUT the tree step: resolve an as-spelled
/// paraId to its single addressable carrier's projection index, or the
/// typed reason it never resolves. Scan-path consumers (find scopes,
/// the write verbs) build on this; tree surfaces add the join step.
pub fn DocxParagraphAnchorIndex::resolve_para_id_carrier(
self : DocxParagraphAnchorIndex,
raw : String,
) -> DocxParaIdCarrierResolution {
guard validated_para_id(raw) is Some(canonical) else { return CarrierInvalid }
let carriers = self.paragraphs_with_para_id(canonical)
if carriers.length() > 1 {
return CarrierAmbiguous(carriers)
}
guard carriers is [paragraph_index] else {
if self.buried_para_ids.get(canonical).unwrap_or(0) > 0 {
return CarrierInMultiPhysical
}
return CarrierNotFound
}
// A single addressable carrier whose PART-SCOPED occupancy is still
// plural means the other claimants are buried or suppressed — the
// identity is contested and never resolves. The carrier's own anchor
// status carries that occupancy judgment.
match self.anchor_of_paragraph(paragraph_index) {
Some(anchor) =>
if anchor.status() != "unique" {
return CarrierAmbiguous(carriers)
}
None => return CarrierAmbiguous(carriers)
}
ResolvedCarrier(paragraph_index)
}
///|
/// Resolve one as-spelled paraId to a tree occurrence.
pub fn DocxParagraphAnchorJoinIndex::resolve_para_id(
self : DocxParagraphAnchorJoinIndex,
raw : String,
) -> DocxParaIdResolution {
match self.anchor_index.resolve_para_id_carrier(raw) {
ResolvedCarrier(paragraph_index) =>
match self.reverse.get(paragraph_index) {
Some(occurrence) => ResolvedOccurrence(occurrence, paragraph_index)
None => ParaIdUnjoined(paragraph_index)
}
CarrierInvalid => ParaIdInvalid
CarrierNotFound => ParaIdNotFound
CarrierAmbiguous(carriers) => ParaIdAmbiguous(carriers)
CarrierInMultiPhysical => ParaIdInMultiPhysical
}
}
///|
/// The anchor index this join was judged against, for callers that
/// need scan paths or per-paragraph anchors beside the resolution.
pub fn DocxParagraphAnchorJoinIndex::anchor_index(
self : DocxParagraphAnchorJoinIndex,
) -> DocxParagraphAnchorIndex {
self.anchor_index
}