Skip to content

feat(session): edit-state introspection — EditSummary / RemainingPlaceholders and Save-time diff #166

@JSv4

Description

@JSv4

Summary

Every agent that uses DocxSession ends up writing two near-identical pieces of code:

  1. A verification pass that re-projects the document and asserts no placeholders remain, no commentary text survives, expected fill values appear in plausible positions. This is a regex zoo every time — the rules don't change between agents, only the values.
  2. A "show me what I changed" output that diffs before-and-after projections so the human (or another agent) can review the work. Today this requires the caller to project twice (once at session start, once at end) and run a shell diff.

Both are common enough that a first-class API would let agents write declarative verification and declarative review output.

Surfaced from the NVCA work — see ~/Code/Docxodus-Agents/AGENT_WORKFLOW.md Phase 5 (verification) and bootstrap/Verify.cs.skeleton.

Proposed Solution

1. session.GetEditSummary()

public EditSummary GetEditSummary();

public sealed record EditSummary
{
    public int TotalAnchors { get; init; }
    public IReadOnlyList<TemplatePlaceholder> RemainingPlaceholders { get; init; } = Array.Empty<TemplatePlaceholder>();
    public IReadOnlyList<TextMatch> BareUnderscoreRuns { get; init; } = Array.Empty<TextMatch>();
    public IReadOnlyList<TextMatch> UnbalancedBrackets { get; init; } = Array.Empty<TextMatch>();
    public int FootnoteCount { get; init; }   // excluding Word-reserved separators
    public int InlineFootnoteRefCount { get; init; }
    public int CommentCount { get; init; }
}

Lets verification be one call instead of multiple regex passes:

var summary = session.GetEditSummary();
Assert.Empty(summary.RemainingPlaceholders);
Assert.Empty(summary.BareUnderscoreRuns);
Assert.Equal(0, summary.FootnoteCount);

2. session.GetDiff() — markdown diff vs. initial state

public string GetDiff(DiffFormat format = DiffFormat.Unified);

public enum DiffFormat
{
    Unified,    // standard unified diff (git-style)
    SideBySide, // two-column for human review
    Json,       // structured: [{ op: "delete"|"insert", anchorId, before?, after? }]
}

Compares the projection at construction time against the current projection. Stored cheaply — we already hold the initial bytes in the session's memory stream; project once at session construction time and cache.

The Json format is the agentic-friendly variant: an LLM can read the structured list and decide whether changes look right, without parsing diff syntax.

3. Convenience: session.RemainingPlaceholders()

Pure delegation to FindPlaceholders but a shorter, more discoverable name for the "am I done yet?" use case:

public IReadOnlyList<TemplatePlaceholder> RemainingPlaceholders(PlaceholderKinds kinds = PlaceholderKinds.All)
    => FindPlaceholders(kinds);

Mainly a discoverability win — agents grep for "Remaining" / "Done" / "Pending" more naturally than "FindPlaceholders."

Implementation Approach

GetEditSummary is mostly composition: it calls existing FindPlaceholders + Grep (for underscore runs and unbalanced brackets) and combines the results. The FootnoteCount / CommentCount reads come from the AnchorIndex.

GetDiff is the heavier piece. Cache the initial-state markdown projection once at DocxSession construction (cheap — ~200ms for a 100-page DOCX). On GetDiff() re-project and run a line-based diff (existing .NET libraries: DiffMatchPatch, or hand-rolled — diff against ~200KB markdown is fast).

For DiffFormat.Json, group hunks by anchor id (extract from the {#kind:scope:unid} markers that lead each block). Output:

[
  { "op": "delete", "anchorId": "p:body:abc…", "before": "Drafting Note..." },
  { "op": "modify", "anchorId": "p:body:def…", "before": "[___]", "after": "ACME, INC." },
  { "op": "insert", "anchorId": "p:body:ghi…", "after": "New paragraph text" }
]

Acceptance Criteria

  • GetEditSummary returns the same counts on an unedited DOCX as on a freshly-opened session of the same bytes.
  • After running the NVCA fill workflow, GetEditSummary().RemainingPlaceholders.Count == 0 and all other counts are zero.
  • GetDiff(Unified) produces a parseable unified diff that patch(1) can apply against the original projection.
  • GetDiff(Json) produces one entry per modified anchor, ordered by document position.
  • Memory: caching the initial projection adds < 5% to session memory footprint for typical (5MB) DOCX files.
  • Docs: docx_mutation_api.md adds a verification recipe; Docxodus-Agents/bootstrap/Verify.cs.skeleton shrinks to Assert.Empty(session.GetEditSummary().RemainingPlaceholders).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions