Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
510 changes: 506 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

38 changes: 30 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[package]
name = "rust-template"
name = "tinydocs"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
rust-version = "1.88"
license = "GPL-3.0-only"
description = "A production-ready Rust library template."
repository = "https://github.com/tinyhumansai/rust-template"
documentation = "https://docs.rs/rust-template"
description = "Agent-friendly document synthesis and text extraction (DOCX, PPTX, PDF) in Rust."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Package description and keywords claim PPTX/PDF support this PR does not add

The package metadata advertises PPTX, PDF, and text extraction, but this PR adds only docx-rs (DOCX synthesis) and serde (JSON spec types). No PPTX or PDF dependency is introduced, and the only feature gate is docx. The pdf keyword compounds the mismatch. A consumer or docs.rs reader would be misled into believing capabilities that this crate does not provide. Either narrow the description/keywords to what is actually shipped (DOCX synthesis + spec types), or add the PPTX/PDF/extraction dependencies and features in this same PR.

existing_code: description = "Agent-friendly document synthesis and text extraction (DOCX, PPTX, PDF) in Rust."

[RULE] Package metadata must accurately reflect shipped capabilities ·

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security likely

Description claims PPTX, PDF, and text extraction with no dependency for them

The package description claims capabilities the dependencies cannot back.

description = "Agent-friendly document synthesis and text extraction (DOCX, PPTX, PDF) in Rust."

The only document-format dependency added is docx-rs (DOCX only). There is no production dependency for PPTX or PDF — zip is a dev-dependency used to inspect test output, and serde/serde_json handle the JSON wire contract. A crate advertising PPTX and PDF extraction without any library to parse those formats will either fail at runtime or mislead consumers (and agents that read crate metadata) into relying on support that does not exist.

[RULE] Package metadata must reflect actual capabilities ·

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium tests confident

Package metadata claims PPTX, PDF, and text extraction not implemented here

The package description claims text extraction (DOCX, PPTX, PDF) but this pull request implements only .docx synthesis — no PPTX, no PDF, no text extraction. The keywords list includes "pdf" for the same reason. crates.io metadata is user-facing and these claims will surface in search results and docs.rs as supported features that do not exist.

description = "Agent-friendly document synthesis and text extraction (DOCX, PPTX, PDF) in Rust."
keywords = ["docx", "ooxml", "pdf", "document", "agent"]

Drop PPTX/PDF/extraction from the description and pdf from the keywords, or scope them to what this PR actually ships.

[RULE] Prefer small, typed APIs over stringly-typed ones; accept &str/impl Into<String> at boundaries and return owned, concrete types. ·

repository = "https://github.com/tinyhumansai/tinydocs"
documentation = "https://docs.rs/tinydocs"
readme = "README.md"
keywords = ["template"]
categories = ["development-tools"]
keywords = ["docx", "ooxml", "pdf", "document", "agent"]
categories = ["text-processing"]
# Keep the published package to what a consumer actually needs.
exclude = [
".github/",
Expand All @@ -25,11 +25,33 @@ exclude = [
# Derive macros for the crate-wide error type in `src/error/mod.rs`. Every
# dependency entry should carry a comment like this one saying why it is here.
thiserror = "2"
# The document spec types are the wire contract a host exposes to an LLM as a
# JSON tool schema, so they derive Serialize/Deserialize here rather than
# forcing every host to re-declare them.
serde = { version = "1", features = ["derive"] }
# OOXML `.docx` synthesis. Optional: exclusive to the `docx` feature so a host
# that only needs extraction does not pull the writer stack.
docx-rs = { version = "0.4.20", optional = true }

[dev-dependencies]
# `.docx` output is a zip container; the tests re-open the produced bytes and
# assert on the OOXML parts inside.
zip = { version = "2", default-features = false, features = ["deflate"] }
# The spec types are a JSON wire contract; the tests assert they round-trip and
# that unknown keys are rejected.
serde_json = "1"

# The example generates a `.docx`, so it only builds when that gate is on.
# Without this, `--no-default-features` fails on the example rather than
# reporting the (correct) fact that the crate itself compiles fine.
[[example]]
name = "basic"
required-features = ["docx"]

[features]
default = []
default = ["docx"]
# `.docx` generation via `docx-rs`.
docx = ["dep:docx-rs"]

# Lints apply to the whole crate and to every target. CI runs clippy with
# `-D warnings`, so anything set to "warn" here fails the build in CI.
Expand Down
136 changes: 75 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,78 @@
# Rust Template
# TinyDocs

Agent-friendly document synthesis and text extraction in Rust.

`tinydocs` turns a typed, validated document spec into real office-format
bytes. It is built for hosts that let a language model produce documents: the
spec types double as the JSON tool schema, validation rejects a malformed spec
with a structured error naming the exact offending field so the model can
self-correct, and synthesis hands back a plain byte buffer.

```rust
use tinydocs::docx::{self, DocumentSection, DocumentSpec};

let spec = DocumentSpec {
title: "Weekly Report".to_string(),
author: Some("Ferris".to_string()),
sections: vec![DocumentSection {
heading: Some("Highlights".to_string()),
paragraphs: vec!["Throughput doubled.".to_string()],
bullets: vec!["Shipped the parser".to_string()],
}],
};

let bytes = docx::generate(&spec)?;
std::fs::write("report.docx", bytes)?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

## What it does not do

No filesystem access, no subprocesses, no async runtime, no deadline handling.
`docx::generate` is synchronous and CPU-bound.

That is a deliberate seam, not an omission. A host running on an async executor
owns the blocking-pool hop and the timeout, because only the host knows its own
executor and deadline policy — a crate that guessed at either would be wrong
for every host that guessed differently. The typical async caller looks like:

```rust,ignore
let spec = spec.clone();
let bytes = tokio::time::timeout(
deadline,
tokio::task::spawn_blocking(move || tinydocs::docx::generate(&spec)),
)
.await???;
```

A production-ready Rust 2024 library template used by TinyHumans AI. It ships
the module layout, lint configuration, error handling, testing, documentation,
CI, and release workflow that every new crate in this organization starts from —
plus one small feature module that demonstrates the conventions end to end.
## Validation

## Use This Template
Every limit is a public constant, so a host can quote the exact number in its
own tool description and stay in lockstep with what validation enforces.

Choose **Use this template** on GitHub, create a repository, then work through
the checklist at the top of [`AGENTS.md`](AGENTS.md):
| Limit | Value | Bounds |
| --- | --- | --- |
| `MAX_SECTIONS` | 128 | sections per document |
| `MAX_TEXT_CHARS` | 2,000 | title, author, section heading |
| `MAX_PARAGRAPH_CHARS` | 20,000 | one paragraph or bullet |
| `MAX_PARAGRAPHS_PER_SECTION` | 200 | paragraphs per section |
| `MAX_BULLETS_PER_SECTION` | 200 | bullets per section |
| `MAX_TOTAL_CHARS` | 2,000,000 | all text in the document |

- update the package name, description, repository, keywords, and categories in
`Cargo.toml`;
- update this README and the crate documentation in `src/lib.rs`;
- replace the placeholder `greeting` module with the first real feature area;
- update the security contact and repository links in the community files;
- replace `ROADMAP.md` with the real plan, or delete it;
- change the license if GPL-3.0-only is not appropriate.
The aggregate cap is the load-bearing one. The per-field limits bound each
individual piece but not their product — `MAX_SECTIONS ×
MAX_PARAGRAPHS_PER_SECTION × MAX_PARAGRAPH_CHARS` alone is over 500M
characters, so a spec satisfying every other limit could still build a
multi-hundred-megabyte document in memory.

Search for `rust-template` and `rust_template` to find every remaining
template-specific value.
`DocumentSpec::validate` is public and runs before any synthesis, so a host can
reject a bad tool call at its own boundary without paying for a blocking hop.

## What You Get
## Feature flags

| Area | What is configured |
| --- | --- |
| Layout | Directory modules with `mod.rs` / `types.rs` / `test.rs`, a crate-wide error type, integration tests, and a runnable example |
| Lints | `unsafe_code` forbidden, `missing_docs`, clippy `all` + `pedantic`, no `unwrap`/`expect`/`panic`/`todo` in library code — all declared in `[lints]` so local and CI runs agree |
| CI | Format, clippy, build, test (default and all features), rustdoc with `-D warnings`, an MSRV build, and a `cargo-deny` supply-chain check |
| Release | Manual `workflow_dispatch` bump that validates, versions, tags, and publishes to crates.io |
| Community | Issue and pull request templates, Dependabot, contributing, security, support, and code of conduct docs |
| Agents | [`AGENTS.md`](AGENTS.md) as the single source of truth, symlinked as `CLAUDE.md`, plus a `.claude/settings.json` allowlist for the standard commands |
| Vendor | TinyBus pinned as the `vendor/tinybus` submodule, initialized by CI and release workflows |
| Feature | Default | Gates |
| --- | --- | --- |
| `docx` | on | `.docx` synthesis via `docx-rs` |

## Layout

Expand All @@ -41,65 +82,38 @@ src/
├── error/
│ ├── mod.rs # crate-wide `Error` and `Result<T>`
│ └── test.rs
└── greeting/ # one directory per feature area
├── mod.rs # module docs, wiring, smallest useful public API
└── test.rs # module-local unit tests
└── docx/
├── mod.rs # `generate` + spec validation
├── types.rs # `DocumentSpec`, `DocumentSection`, limits
└── test.rs
tests/
└── public_api.rs # integration tests against the public API only
examples/
└── basic.rs # compiled and linted in CI
vendor/
└── tinybus/ # pinned TinyBus git submodule
docs/
├── README.md # documentation index and conventions
├── specs/ # behavior and architecture specifications
├── plans/ # implementation-ordered delivery plans
└── adr/ # immutable architecture decision records
```

Feature areas use directory modules: implementation and exports live in
`mod.rs`, substantial types move to `types.rs`, and unit tests live in
`test.rs`. [`AGENTS.md`](AGENTS.md) holds the complete repository guidance, and
`CLAUDE.md` is a symlink to it so every coding agent reads one source of truth.

## Development

Clone with submodules, or initialize them before building:

```sh
git submodule update --init --recursive
```

```sh
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo build --all-targets --all-features
cargo test --all-features
cargo run --example basic
```

Those four checks are exactly what CI runs. Optional extras:
Run the gated build too — it is the only thing that catches a feature that
compiles only when it is turned on:

```sh
cargo doc --no-deps --all-features # CI builds this with RUSTDOCFLAGS="-D warnings"
cargo deny check all # supply-chain check; see deny.toml
cargo clippy --all-targets --no-default-features -- -D warnings
```

## Releasing

Run the **Release** workflow from the Actions tab with a `patch`, `minor`, or
`major` bump. It revalidates the crate, bumps the version, commits, tags
`vX.Y.Z`, and publishes to crates.io. Do not hand-edit the version in
`Cargo.toml`.

## Documentation

- [`AGENTS.md`](AGENTS.md) — repository guidelines for humans and agents
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to propose a change
- [`docs/specs/`](docs/specs/README.md) — behavior and architecture specs
- [`docs/plans/`](docs/plans/README.md) — test-first implementation plans
- [`docs/adr/`](docs/adr/0001-record-architecture-decisions.md) — architecture
decision records
- [`SECURITY.md`](SECURITY.md) — how to report a vulnerability

## License
Expand Down
44 changes: 35 additions & 9 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Minimal end-to-end usage of the crate.
//! Generate a small `.docx` and report its size.
//!
//! Examples are compiled and linted in CI, so they cannot drift from the API.
//! Run it with:
Expand All @@ -7,16 +7,42 @@
//! cargo run --example basic
//! ```

use rust_template::{Result, greet};
use tinydocs::docx::{self, DocumentSection, DocumentSpec};

fn main() -> Result<()> {
println!("{}", greet("Rust")?);
fn main() {
let spec = DocumentSpec {
title: "Quarterly Review".to_string(),
author: Some("Ferris".to_string()),
sections: vec![
DocumentSection {
heading: Some("Summary".to_string()),
paragraphs: vec!["Throughput doubled while error rates fell.".to_string()],
bullets: vec![],
},
DocumentSection {
heading: Some("Next Quarter".to_string()),
paragraphs: vec![],
bullets: vec![
"Ship the streaming parser".to_string(),
"Halve p99 latency".to_string(),
],
},
],
};

// Failure modes are part of the public contract; show them too.
match greet(" ") {
Ok(greeting) => println!("{greeting}"),
Err(error) => println!("expected failure: {error}"),
match docx::generate(&spec) {
Ok(bytes) => println!("generated {} bytes of .docx", bytes.len()),
Err(error) => println!("generation failed: {error}"),
}

Ok(())
// Failure modes are part of the public contract, so show one too. An empty
// title is rejected before any synthesis happens.
let invalid = DocumentSpec {
title: " ".to_string(),
..spec
};
match docx::generate(&invalid) {
Ok(bytes) => println!("unexpectedly generated {} bytes", bytes.len()),
Err(error) => println!("expected failure: {error}"),
}
}
Loading