///| /// `docx annotate add --at [--to ] /// (--text --author [--initials i] [--date iso] | --json )` /// — adds one comment to an EXISTING document by L0 byte-span surgery: /// anchor markers splice into the original main-part bytes at /// scanner-derived offsets, the definition into the comments part /// (created and wired when absent), and every untouched byte stays. /// Metadata ownership is BRANCH-EXCLUSIVE: with `--text` the flags own /// it; with `--json` the docx.annotate/1 envelope owns ALL of it and /// the metadata flags are rejected. Publication follows the batch /// contract: the output must not exist, the write is atomic, and every /// failure leaves ZERO output. async fn run_annotate_add(matches : @argparse.Matches) -> Unit { let input = required_value(matches, "input") let output = required_value(matches, "output") // in==out is rejected STRUCTURALLY, not by string comparison: the // output must not exist while the input must — one path naming both // (including case-folded aliases on case-insensitive filesystems) // trips the existence check below before anything is read. if @afs.exists(output) { raise @cli.CliError( "docx: refusing to write '\{output}': it already exists (annotate writes a NEW file; it never edits in place)", ) } let at_flag = required_value(matches, "at") let to_flag = first_value(matches, "to") // Branch-exclusive comment source. let spec = comment_spec_from_flags(matches) let original = @afs.read_file(input).binary() let annotated = @docx2html.read_docx_annotated(original) catch { err => raise @cli.CliError( "docx: could not read '\{input}': \{docx_error_message(err)}", ) } // Anchors: BODY-story paragraph paths, ordinal-only. let at_relative = body_paragraph_relative_path(at_flag) let to_relative = match to_flag { Some(to_path) => body_paragraph_relative_path(to_path) None => at_relative } // Everything between the validated spec and a ready splice plan — // sidecar/unrepairable gates, span resolution, id allocation, // fragments, part placement, wiring — lives in the docx package so // the SDK gates exercise the exact same code. let (plan, new_id) = @docx_impl.plan_comment_addition( annotated, original, at_relative~, to_relative~, spec, ) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } let spliced = @splice.splice_docx(original, plan) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } // Belt 1: the structural validator. let findings = @opc.validate_docx_package(spliced) if findings.length() > 0 { raise @cli.CliError( "docx: internal error — the annotated package failed validation: \{findings[0]}", ) } // Belt 2: the mutation must READ BACK — the new comment, anchored at // the requested paragraph, with no new unrepairable state. let verify = @docx2html.read_docx_annotated(spliced) catch { err => raise @cli.CliError( "docx: internal error — the annotated package does not read back: \{docx_error_message(err)}", ) } let mut verified = false for comment in verify.annotations().comments() { if comment.id() == new_id && comment.defined() { for anchor in comment.anchors() { if anchor.start() is Some(start_path) && start_path == "/body/" + at_relative { verified = true } } } } if !verified { raise @cli.CliError( "docx: internal error — the new comment did not read back anchored at '/body/\{at_relative}'; nothing was written", ) } write_atomically(output, spliced) println("annotated \{output} (comment \{new_id} on /body/\{at_relative})") } ///| /// Parses a `--at`/`--to` flag: a BODY-story paragraph path /// ("/body/p[2]", "/body/tbl[1]/tr[2]/tc[1]/p[1]"), ordinal selectors /// only, and returns the scanner-relative form. fn body_paragraph_relative_path(path : String) -> String raise @cli.CliError { let parsed = @paths.parse_path(path) catch { err => raise CliError("error: \{path}: \{err}") } guard parsed.root is Body else { raise CliError( "error: '\{path}' is not a body path (annotate anchors to BODY-story paragraphs only)", ) } if parsed.steps.length() == 0 { raise CliError("error: '\{path}' names the body, not a paragraph") } let mut relative = "" for index, step in parsed.steps { guard step.selector is Ordinal(ordinal) else { raise CliError( "error: '\{path}': body paragraphs address by ordinal ([n]), not [@id=...]", ) } if index == parsed.steps.length() - 1 && step.kind != "p" { raise CliError( "error: '\{path}' must end at a paragraph (p[n]); got '\{step.kind}'", ) } if relative != "" { relative += "/" } relative += "\{step.kind}[\{ordinal}]" } relative } ///| fn annotate_command() -> @argparse.Command { Command( "annotate", about="Annotate an EXISTING document by byte-preserving surgery", subcommand_required=true, subcommands=[ Command( "add", about="Add one comment anchored to body paragraph(s)", options=[ OptionArg( "at", long="at", about="Anchor paragraph path (/body/p[2] or a table-cell paragraph path). Required.", ), OptionArg( "to", long="to", about="Range end paragraph path (same story, not before --at).", ), OptionArg( "text", long="text", about="Plain comment text (one paragraph); metadata comes from the flags.", ), OptionArg( "json", long="json", about="Path to a docx.annotate/1 envelope FILE owning ALL metadata; the metadata flags are rejected.", ), OptionArg( "author", long="author", about="Comment author (required with --text).", ), OptionArg( "initials", long="initials", about="Author initials (optional, --text branch only).", ), OptionArg( "date", long="date", about="xsd:dateTime timestamp (optional, --text branch only).", ), ], positionals=[ PositionArg( "input", about="Existing .docx to annotate (never modified).", num_args=ValueRange(lower=1, upper=1), ), PositionArg( "output", about="Output .docx path (must not exist).", num_args=ValueRange(lower=1, upper=1), ), ], ), Command( "reply", about="Reply to an existing comment (anchorless; threads via commentsExtended)", options=[ OptionArg( "comment", long="comment", about="The parent comment's id (as outline shows). Required.", ), OptionArg( "text", long="text", about="Plain reply text (one paragraph); metadata comes from the flags.", ), OptionArg( "json", long="json", about="Path to a docx.annotate/1 envelope FILE owning ALL metadata; the metadata flags are rejected.", ), OptionArg( "author", long="author", about="Reply author (required with --text).", ), OptionArg( "initials", long="initials", about="Author initials (optional, --text branch only).", ), OptionArg( "date", long="date", about="xsd:dateTime timestamp (optional, --text branch only).", ), ], positionals=[ PositionArg( "input", about="Existing .docx (never modified).", num_args=ValueRange(lower=1, upper=1), ), PositionArg( "output", about="Output .docx path (must not exist).", num_args=ValueRange(lower=1, upper=1), ), ], ), Command( "resolve", about="Mark a comment resolved (w15:done=1)", options=[ OptionArg( "comment", long="comment", about="The comment's id. Required.", ), ], positionals=[ PositionArg( "input", about="Existing .docx (never modified).", num_args=ValueRange(lower=1, upper=1), ), PositionArg( "output", about="Output .docx path (must not exist).", num_args=ValueRange(lower=1, upper=1), ), ], ), Command( "unresolve", about="Mark a comment unresolved (w15:done=0)", options=[ OptionArg( "comment", long="comment", about="The comment's id. Required.", ), ], positionals=[ PositionArg( "input", about="Existing .docx (never modified).", num_args=ValueRange(lower=1, upper=1), ), PositionArg( "output", about="Output .docx path (must not exist).", num_args=ValueRange(lower=1, upper=1), ), ], ), ], ) } ///| /// The BRANCH-EXCLUSIVE comment source shared by `annotate add` and /// `annotate reply`: `--text` with flag-owned metadata (`--author` /// required), or `--json ` with the docx.annotate/1 envelope /// owning ALL metadata (the metadata flags rejected). async fn comment_spec_from_flags( matches : @argparse.Matches, ) -> @docx_impl.CommentSpec { let text_flag = first_value(matches, "text") let json_flag = first_value(matches, "json") let author_flag = first_value(matches, "author") let initials_flag = first_value(matches, "initials") let date_flag = first_value(matches, "date") match (text_flag, json_flag) { (Some(_), Some(_)) => raise @cli.CliError( "error: --text and --json are mutually exclusive (exactly one is required)", ) (None, None) => raise @cli.CliError("error: exactly one of --text or --json is required") (Some(text), None) => { let author = match author_flag { Some(author) => author None => raise @cli.CliError( "error: --author is required with --text (the flags own the metadata on this branch)", ) } @docx2html.comment_spec( author~, initials?=initials_flag, date?=date_flag, from=0, to=0, [@docx2html.paragraph([@docx2html.run([@docx2html.text(text)])])], ) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } } (None, Some(envelope_path)) => { if author_flag is Some(_) || initials_flag is Some(_) || date_flag is Some(_) { raise @cli.CliError( "error: --author/--initials/--date are rejected with --json (the envelope owns all metadata)", ) } let envelope_text = @afs.read_file(envelope_path).text() @batch.check_script_text(envelope_text) catch { BatchError(message) => raise @cli.CliError("error: \{message}") } let envelope_json = @json.parse(envelope_text) catch { err => raise @cli.CliError( "error: the envelope is not valid JSON: \{repr(err)}", ) } let envelope = @batch.parse_annotate(envelope_json) catch { BatchError(message) => raise @cli.CliError("error: \{message}") } let body = envelope.build_body() catch { BatchError(message) => raise @cli.CliError("error: \{message}") } @docx2html.comment_spec( author=envelope.author(), initials?=envelope.initials(), date?=envelope.date(), from=0, to=0, body, ) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } } } } ///| /// `docx annotate reply --comment (--text ... --author /// ... | --json )` — an anchorless reply threaded under the /// parent (its anchor is logically the parent's), with the paraId /// retrofit and commentsExtended wiring handled by the plan layer. async fn run_annotate_reply(matches : @argparse.Matches) -> Unit { let input = required_value(matches, "input") let output = required_value(matches, "output") if @afs.exists(output) { raise @cli.CliError( "docx: refusing to write '\{output}': it already exists (annotate writes a NEW file; it never edits in place)", ) } let parent_id = required_value(matches, "comment") let spec = comment_spec_from_flags(matches) let original = @afs.read_file(input).binary() let annotated = @docx2html.read_docx_annotated(original) catch { err => raise @cli.CliError( "docx: could not read '\{input}': \{docx_error_message(err)}", ) } let (plan, new_id) = @docx_impl.plan_comment_reply( annotated, original, parent_id~, spec, ) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } let spliced = @splice.splice_docx(original, plan) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } let findings = @opc.validate_docx_package(spliced) if findings.length() > 0 { raise @cli.CliError( "docx: internal error — the annotated package failed validation: \{findings[0]}", ) } let verify = @docx2html.read_docx_annotated(spliced) catch { err => raise @cli.CliError( "docx: internal error — the annotated package does not read back: \{docx_error_message(err)}", ) } let mut verified = false for comment in verify.annotations().comments() { if comment.id() == new_id && comment.defined() && comment.parent_id() is Some(threaded) && threaded == parent_id { verified = true } } if !verified { raise @cli.CliError( "docx: internal error — the reply did not read back threaded under comment '\{parent_id}'; nothing was written", ) } write_atomically(output, spliced) println("annotated \{output} (comment \{new_id} replying to \{parent_id})") } ///| /// `docx annotate resolve|unresolve --comment ` — flips /// the w15:done flag, creating the commentsExtended part (and the /// paraId key) when the document predates them. async fn run_annotate_resolution( matches : @argparse.Matches, done : Bool, ) -> Unit { let input = required_value(matches, "input") let output = required_value(matches, "output") if @afs.exists(output) { raise @cli.CliError( "docx: refusing to write '\{output}': it already exists (annotate writes a NEW file; it never edits in place)", ) } let comment_id = required_value(matches, "comment") let original = @afs.read_file(input).binary() let annotated = @docx2html.read_docx_annotated(original) catch { err => raise @cli.CliError( "docx: could not read '\{input}': \{docx_error_message(err)}", ) } let plan = @docx_impl.plan_comment_resolution( annotated, original, comment_id~, done~, ) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } let spliced = @splice.splice_docx(original, plan) catch { err => raise @cli.CliError("error: \{docx_error_message(err)}") } let findings = @opc.validate_docx_package(spliced) if findings.length() > 0 { raise @cli.CliError( "docx: internal error — the package failed validation: \{findings[0]}", ) } let verify = @docx2html.read_docx_annotated(spliced) catch { err => raise @cli.CliError( "docx: internal error — the package does not read back: \{docx_error_message(err)}", ) } let mut verified = false for comment in verify.annotations().comments() { if comment.id() == comment_id && comment.done() is Some(flag) && flag == done { verified = true } } if !verified { raise @cli.CliError( "docx: internal error — comment '\{comment_id}' did not read back with done=\{done}; nothing was written", ) } write_atomically(output, spliced) println("annotated \{output} (comment \{comment_id} done=\{done})") }