From cd1201024b651f1bcb244526380885b241372d03 Mon Sep 17 00:00:00 2001 From: che cheng Date: Sat, 2 May 2026 01:37:11 +0800 Subject: [PATCH] docs: propose r word-builder mvp Refs #88 --- .../changes/r-word-builder-mvp/.openspec.yaml | 4 + openspec/changes/r-word-builder-mvp/design.md | 70 ++++++++++++++ .../changes/r-word-builder-mvp/proposal.md | 47 ++++++++++ .../specs/r-word-builder-mvp/spec.md | 94 +++++++++++++++++++ openspec/changes/r-word-builder-mvp/tasks.md | 22 +++++ 5 files changed, 237 insertions(+) create mode 100644 openspec/changes/r-word-builder-mvp/.openspec.yaml create mode 100644 openspec/changes/r-word-builder-mvp/design.md create mode 100644 openspec/changes/r-word-builder-mvp/proposal.md create mode 100644 openspec/changes/r-word-builder-mvp/specs/r-word-builder-mvp/spec.md create mode 100644 openspec/changes/r-word-builder-mvp/tasks.md diff --git a/openspec/changes/r-word-builder-mvp/.openspec.yaml b/openspec/changes/r-word-builder-mvp/.openspec.yaml new file mode 100644 index 0000000..2e10e34 --- /dev/null +++ b/openspec/changes/r-word-builder-mvp/.openspec.yaml @@ -0,0 +1,4 @@ +schema: spec-driven +created: 2026-05-02 +created_by: che cheng +created_with: claude diff --git a/openspec/changes/r-word-builder-mvp/design.md b/openspec/changes/r-word-builder-mvp/design.md new file mode 100644 index 0000000..f0e1754 --- /dev/null +++ b/openspec/changes/r-word-builder-mvp/design.md @@ -0,0 +1,70 @@ +## Context + +`word-builder-swift` provides the Swift-side document builder, but R users still route analysis results through `officer`, `rmarkdown`, Pandoc, or manual copy/paste. Issue #88 proposes a different pipeline: R remains the analysis environment, and the R package generates a Swift script that imports `WordBuilderSwift` and writes the final `.docx`. The generated Swift script is a durable intermediate artifact, which is the main product difference from existing R-to-Word workflows. + +The MVP is best treated as a new repository with a macOS-first support statement. The proposal lives here because the issue and project direction are tracked in macdoc, but implementation belongs in a future R package repository. + +## Goals / Non-Goals + +**Goals:** + +- Define an MVP package named `wordbuilder` in a future PsychQuant repository. +- Support headings, paragraphs, text runs, and unstyled data-frame tables. +- Generate deterministic Swift source that imports `WordBuilderSwift` and writes `.docx`. +- Retain generated Swift by default. +- Use a persistent Swift package cache so repeated renders avoid repeated project setup. +- Establish golden-file and integration testing strategy. + +**Non-Goals:** + +- Support ggplot2 image embedding in Phase 1. +- Support APA table styling, citations, cross-references, headers, footers, numbering, or bookmarks in Phase 1. +- Support Windows in Phase 1. +- Commit to CRAN release in Phase 1. +- Modify macdoc CLI or `word-builder-swift` unless implementation reveals a blocker. + +## Decisions + +### Use a separate PsychQuant R package repository + +Create a future repository for the package rather than placing R source inside macdoc. The R package has its own lifecycle, tests, README, generated examples, and user-facing installation flow. The macdoc issue remains the planning tracker, and the implementation repository can link back to #88. + +Alternative considered: put the R package under `macdoc/packages/`. Rejected because R package structure, testing, and distribution conventions differ from SwiftPM packages and would add a second package ecosystem to this repo. + +### Name the package `wordbuilder` + +Use lowercase `wordbuilder` as the R package name and keep `WordBuilderSwift` as the generated Swift import. Lowercase package naming is easier to type in R code and avoids ambiguity with the Swift module name. + +Alternative considered: `wordBuilder`, `rswiftdocx`, and `docx.swift`. Rejected for the MVP because `wordbuilder` is direct, searchable, and maps to the existing Swift package without punctuation. + +### Generate Swift directly instead of routing through macdoc + +`render()` writes a Swift source file and executes Swift through a package cache that depends on `word-builder-swift`. It does not call `macdoc convert`, because this workflow is document construction rather than format conversion, and the generated Swift script is itself the reviewable artifact. + +Alternative considered: call a future `macdoc docx build` command. Rejected for the MVP because #88 can proceed independently of #92 once it has a pinned `WordBuilderSwift` dependency. + +### Retain generated Swift by default + +`render()` keeps the generated Swift file next to the output document or at a caller-provided path. The path is returned in the render result with the `.docx` path. Temporary cleanup can be an explicit option after the default durable artifact workflow is proven. + +Alternative considered: delete generated Swift after successful render. Rejected because the reviewable intermediate is the core differentiator. + +### Use a persistent Swift package cache + +The package creates or reuses a cache directory under the platform user cache location. The cache contains a SwiftPM package that pins a `word-builder-swift` version. Individual renders write a Swift source file into the cache or copy it into a runner target, execute Swift, and return output paths. The cache key includes the pinned `word-builder-swift` version. + +Alternative considered: create a fresh Swift package in a temporary directory for each render. Rejected because cold package setup would make iterative report work too slow. + +### Test generated Swift separately from end-to-end rendering + +Most automated tests compare generated Swift to golden files. End-to-end tests that require the Swift toolchain compile and run generated Swift, then read back `.docx` content, but they are marked as local or periodic integration tests. This keeps routine R tests fast while still proving the pipeline. + +Alternative considered: run Swift in every test. Rejected because Swift toolchain installation and compile time would make basic package checks brittle. + +## Risks / Trade-offs + +- [Risk] Swift toolchain detection fails for R users. → Mitigation: `render()` performs a preflight check for `swift` on PATH and returns an actionable error before generating output. +- [Risk] Cold compile latency is too high. → Mitigation: persistent cache keyed by `word-builder-swift` version, plus clear logging for first-run setup. +- [Risk] Phase 1 looks weaker than `officer`. → Mitigation: position the MVP around reviewable Swift artifacts and reproducibility, not feature parity. +- [Risk] Generated Swift golden files become noisy. → Mitigation: use stable formatting, deterministic object ordering, and minimal codegen helpers. +- [Risk] `word-builder-swift` lacks a needed Phase 1 primitive. → Mitigation: open a separate upstream issue and keep #88 MVP scope to supported primitives. diff --git a/openspec/changes/r-word-builder-mvp/proposal.md b/openspec/changes/r-word-builder-mvp/proposal.md new file mode 100644 index 0000000..2c231fe --- /dev/null +++ b/openspec/changes/r-word-builder-mvp/proposal.md @@ -0,0 +1,47 @@ +## Why + +Issue #88 proposes a reproducible R-to-Word workflow where R analysis objects emit a reviewable Swift script that calls `WordBuilderSwift` and produces `.docx`. The MVP needs a scoped SDD proposal before any repository is created because it crosses R APIs, Swift toolchain execution, cache layout, generated artifact retention, and test strategy. + +## What Changes + +- Define a new macOS-first R package, working name `wordbuilder`, in a future repository owned by PsychQuant. +- Provide an R API for the Phase 1 document model: + - `word_document()` + - `add_heading()` + - `add_paragraph()` + - `add_text_run()` + - `add_table()` for plain data frames + - `render()` for generating Swift, running Swift, and returning the `.docx` path +- Generate a runnable Swift source file that imports `WordBuilderSwift` and uses a pinned `word-builder-swift` release. +- Retain the generated `.swift` file by default so it can be reviewed, committed, edited, and rerun outside R. +- Execute Swift through a persistent package/cache directory under the user cache location so repeated renders avoid cold package setup. +- Add golden-file tests for generated Swift and a local/periodic integration test that runs Swift and reads back `.docx` content. + +## Non-Goals + +- No ggplot2 image embedding in Phase 1. +- No table styling, APA table formatting, citations, cross-references, headers, footers, or numbering in Phase 1. +- No Windows support in Phase 1. +- No CRAN release commitment in Phase 1; GitHub distribution is sufficient until toolchain requirements stabilize. +- No changes to macdoc CLI or `word-builder-swift` unless the MVP exposes a blocking upstream gap. + +## Capabilities + +### New Capabilities + +- `r-word-builder-mvp`: R package MVP that serializes R report objects into runnable WordBuilderSwift scripts and produces `.docx` output with retained intermediate Swift artifacts. + +### Modified Capabilities + +(none) + +## Impact + +- Affected specs: r-word-builder-mvp +- Affected code: + - New external repository: PsychQuant/r-word-builder + - Modified in this repository: none +- Related systems: + - Consumes `word-builder-swift` as the canonical `.docx` writer. + - Requires Swift toolchain discovery from R. + - Tracks GitHub issue #88. diff --git a/openspec/changes/r-word-builder-mvp/specs/r-word-builder-mvp/spec.md b/openspec/changes/r-word-builder-mvp/specs/r-word-builder-mvp/spec.md new file mode 100644 index 0000000..527d38f --- /dev/null +++ b/openspec/changes/r-word-builder-mvp/specs/r-word-builder-mvp/spec.md @@ -0,0 +1,94 @@ +## ADDED Requirements + +### Requirement: R document builder API + +The package SHALL expose an R API for Phase 1 document construction. The API SHALL include `word_document()`, `add_heading()`, `add_paragraph()`, `add_text_run()`, `add_table()`, and `render()`. Builder functions SHALL return updated document objects rather than mutating hidden global state. + +#### Scenario: Build a simple report object + +- **WHEN** an R user creates `doc <- word_document()`, then calls `add_heading(doc, "Q1 Report", level = 1)`, and then calls `add_paragraph(doc, "Revenue grew 15%")` +- **THEN** the returned document object contains one heading block and one paragraph block in order + +#### Scenario: Add a plain data frame table + +- **WHEN** an R user calls `add_table(doc, data.frame(group = c("A", "B"), mean = c(1.2, 3.4)))` +- **THEN** the returned document object contains a table block with two columns and two data rows + +### Requirement: Deterministic Swift code generation + +The package SHALL serialize a document object into deterministic Swift source that imports `WordBuilderSwift`, constructs equivalent `Document`, `Section`, `Paragraph`, `TextRun`, and `Table` values, and writes a `.docx` file through the WordBuilderSwift packer. The same R document object and options SHALL produce byte-identical Swift source. + +#### Scenario: Generate Swift for heading and paragraph + +- **WHEN** the package serializes a document containing heading `Q1 Report` and paragraph `Revenue grew 15%` +- **THEN** the generated Swift source imports `WordBuilderSwift` +- **AND** the generated Swift source contains the text literals `Q1 Report` and `Revenue grew 15%` + +#### Scenario: Stable generation + +- **WHEN** the same R document object is serialized twice with the same options +- **THEN** the two generated Swift files are byte-identical + +##### Example: repeated serialization + +- **GIVEN** a document object with one heading `Q1 Report`, one paragraph `Revenue grew 15%`, and one two-row table +- **WHEN** the package writes `report-a.swift` and `report-b.swift` from that object with identical options +- **THEN** `report-a.swift` and `report-b.swift` have identical bytes + +### Requirement: Render uses persistent Swift package cache + +The `render()` function SHALL create or reuse a persistent Swift package cache under the platform user cache directory. The cache key SHALL include the pinned `word-builder-swift` version. Rendering SHALL execute Swift from that cache, write the requested `.docx` output, and return a structured result containing output path, Swift source path, cache path, and process status. + +#### Scenario: First render initializes cache + +- **WHEN** an R user calls `render(doc, output = "q1-report.docx")` and no cache exists for the pinned `word-builder-swift` version +- **THEN** the package creates the cache package, writes generated Swift, executes Swift, and returns the output path and cache path + +#### Scenario: Second render reuses cache + +- **WHEN** a cache exists for the pinned `word-builder-swift` version +- **THEN** `render()` reuses that cache instead of creating a new Swift package directory + +### Requirement: Generated Swift artifact retention + +The `render()` function SHALL retain the generated Swift source file by default. The retained source path SHALL be visible in the render result. If the user supplies an explicit Swift source output path, the package SHALL write the generated Swift there. + +#### Scenario: Retain generated Swift next to output + +- **WHEN** an R user calls `render(doc, output = "q1-report.docx")` without a Swift source path +- **THEN** the package keeps a generated Swift source file and returns its path in the render result + +#### Scenario: User-specified Swift source path + +- **WHEN** an R user calls `render(doc, output = "q1-report.docx", swift_output = "q1-report.swift")` +- **THEN** the generated Swift source is written to `q1-report.swift` + +### Requirement: Swift toolchain preflight + +The package SHALL check for a usable `swift` executable before attempting render execution. If Swift is unavailable, `render()` SHALL fail with a clear error that identifies the missing executable and does not create a `.docx` output file. + +#### Scenario: Swift missing from PATH + +- **WHEN** `render()` runs in an environment where `swift` cannot be found on PATH +- **THEN** `render()` returns an error explaining that the Swift toolchain is required +- **AND** the requested `.docx` output file is not created + +### Requirement: MVP test strategy + +The package SHALL include golden-file tests for generated Swift and separate integration tests for Swift execution and `.docx` readback. Routine tests SHALL pass without compiling Swift. Integration tests SHALL be explicitly marked so they can run locally or periodically when the Swift toolchain is available. + +#### Scenario: Golden-file code generation test + +- **WHEN** the test suite serializes a known R document fixture +- **THEN** the generated Swift matches the checked-in golden Swift file + +##### Example: fixture and golden file + +- **GIVEN** `tests/fixtures/basic_report.rds` contains one heading, one paragraph, and one data-frame table +- **WHEN** the golden-file test serializes that fixture +- **THEN** the generated Swift matches `tests/testthat/_snaps/basic_report.swift` + +#### Scenario: Integration render test + +- **WHEN** integration tests are enabled and Swift is available +- **THEN** the test suite renders a `.docx` and verifies readback text for heading, paragraph, and table content diff --git a/openspec/changes/r-word-builder-mvp/tasks.md b/openspec/changes/r-word-builder-mvp/tasks.md new file mode 100644 index 0000000..09a5efa --- /dev/null +++ b/openspec/changes/r-word-builder-mvp/tasks.md @@ -0,0 +1,22 @@ +## 1. Repository and R Package Skeleton + +- [ ] 1.1 Create the future PsychQuant R package repository and standard R package skeleton, following Use a separate PsychQuant R package repository. +- [ ] 1.2 Configure DESCRIPTION, README, license, package namespace, and examples using package name `wordbuilder`, following Name the package `wordbuilder`. + +## 2. R API and Swift Generation + +- [ ] 2.1 Implement R document builder API with word_document, add_heading, add_paragraph, add_text_run, add_table, and immutable-return builder objects. +- [ ] 2.2 Implement Deterministic Swift code generation for headings, paragraphs, text runs, and plain data-frame tables, following Generate Swift directly instead of routing through macdoc. +- [ ] 2.3 Add escaping and stable formatting tests so identical R document objects produce byte-identical Swift source. + +## 3. Render Runner and Artifact Retention + +- [ ] 3.1 Implement Swift toolchain preflight that detects `swift` on PATH and fails before output creation when unavailable. +- [ ] 3.2 Implement Render uses persistent Swift package cache keyed by pinned word-builder-swift version, following Use a persistent Swift package cache. +- [ ] 3.3 Implement Generated Swift artifact retention with default retained source paths and caller-specified swift_output paths, following Retain generated Swift by default. + +## 4. Tests and Documentation + +- [ ] 4.1 Implement MVP test strategy with golden-file code generation tests and explicitly marked integration render/readback tests, following Test generated Swift separately from end-to-end rendering. +- [ ] 4.2 Document macOS-first SystemRequirements, first-run Swift cache behaviour, generated Swift review workflow, and Phase 1 non-goals. +EOF \ No newline at end of file