Skip to content

Commit 618b701

Browse files
KSXGitHubclaude
andauthored
docs(ai): use templates, remove "if" (#362)
* feat(ai): add pdu-ai-instructions binary for flexible AI instruction generation Replace the rigid sync test (which required all AI instruction files to be identical) with a template-based generation system. Shared content lives in template/ai-instructions/shared.md, while target-specific fragments (claude.md, copilot.md, agents.md) allow per-tool customization. https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): address review feedback on ai-instructions binary - Move /template/ai-instructions after /LICENSE in Cargo.toml include - Merge src/ai_instructions.rs into cli/ai_instructions.rs (not part of public lib) - Use clap derive for --generate flag instead of manual arg parsing - Make ai-instructions feature depend on clap/derive - Log stderr before assertions in test (consistent with usual_cli.rs pattern) - List specific features in run.sh instead of --all-features - Simplify claude.md wording (remove "if" since it's Claude-exclusive) https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): simplify ai-instructions binary - Remove doc comments from self-documenting constants - Store fragments as static str slices instead of generating Strings - Use const array instead of Vec from a function - Use RuntimeError enum with derive_more::Display for error handling https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): eliminate allocations and polish ai-instructions binary - Use &[AiInstructionFile] slice instead of sized array for future-proof const - Derive Clone + Copy on AiInstructionFile (all fields are Copy) - Implement Display for zero-alloc file writing via write! macro - Add matches() method for zero-alloc content comparison - Import io::{self, Write} and use io::Error throughout - Collapse display_outdated into a single format expression in derive_more - Follow info:/warning:/error: message convention https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): extract Fragments type and clean up main - Extract Fragments newtype with Display and matches() implementations - Use if-let-Err pattern in main (matching lib.rs convention) - Change write message to "info: Generated file {}" https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): simplify AiInstructionFile to tuple struct Destructuring in loop bodies makes field access clean and removes repetitive `file.path` / `file.fragments` references. https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): simplify types and error reporting - Destructure Fragments in Display and matches() instead of self.0 - Replace AiInstructionFile type with plain (&str, Fragments) tuples - Print out-of-date errors directly to stderr instead of collecting - Simplify Outdated variant to unit (no data needed) - Remove unnecessary "info: ok" messages from check mode https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): improve readability of main and check_files - Extract nested if-else into a let binding in main - Use Result accumulator instead of bool flag in check_files https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor: fewer lines * refactor: remove unnecessary dereference * refactor: use `pipe` * refactor: replace qualified paths with `use` * feat: remove unnecessary command * docs: capitalize error messages * docs: re-add actual error message for outdated * feat: better hint system * docs: capitalize correctly * docs: separate rust docs from cli docs * feat(ai): add repository path argument to pdu-ai-instructions The binary no longer assumes it runs from the repository root. A positional `repository` argument specifies the top-level directory. https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): use exact repository path in hint, log stdout in test - hint() takes &Args and uses the actual repository path in the message - Test logs both stdout and stderr before assertions https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 * refactor(ai): inline full_path into pipe chains https://claude.ai/code/session_01H2MVRarXJZFyp4Z9WpqF46 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 50ba05e commit 618b701

12 files changed

Lines changed: 189 additions & 27 deletions

File tree

.github/copilot-instructions.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,4 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c
1515
- Custom errors: `#[derive(Debug, Display, Error)]`
1616
- Minimize `unwrap()` in non-test code — use proper error handling
1717
- Install toolchain before running tests: `rustup toolchain install "$(< rust-toolchain)" && rustup component add --toolchain "$(< rust-toolchain)" rustfmt clippy`
18-
- If the AI agent is Claude Code, `gh` (GitHub CLI) is not installed — do not attempt to use it
1918
- Run `FMT=true LINT=true BUILD=true TEST=true DOC=true ./test.sh` to validate changes

AGENTS.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,4 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c
1515
- Custom errors: `#[derive(Debug, Display, Error)]`
1616
- Minimize `unwrap()` in non-test code — use proper error handling
1717
- Install toolchain before running tests: `rustup toolchain install "$(< rust-toolchain)" && rustup component add --toolchain "$(< rust-toolchain)" rustfmt clippy`
18-
- If the AI agent is Claude Code, `gh` (GitHub CLI) is not installed — do not attempt to use it
1918
- Run `FMT=true LINT=true BUILD=true TEST=true DOC=true ./test.sh` to validate changes

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c
1515
- Custom errors: `#[derive(Debug, Display, Error)]`
1616
- Minimize `unwrap()` in non-test code — use proper error handling
1717
- Install toolchain before running tests: `rustup toolchain install "$(< rust-toolchain)" && rustup component add --toolchain "$(< rust-toolchain)" rustfmt clippy`
18-
- If the AI agent is Claude Code, `gh` (GitHub CLI) is not installed — do not attempt to use it
1918
- Run `FMT=true LINT=true BUILD=true TEST=true DOC=true ./test.sh` to validate changes
19+
- `gh` (GitHub CLI) is not installed — do not attempt to use it

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ include = [
2727
"/README.md",
2828
"/USAGE.md",
2929
"/LICENSE",
30+
"/template/ai-instructions",
3031
]
3132

3233
[lib]
@@ -48,11 +49,17 @@ name = "pdu-usage-md"
4849
path = "cli/usage_md.rs"
4950
required-features = ["cli"]
5051

52+
[[bin]]
53+
name = "pdu-ai-instructions"
54+
path = "cli/ai_instructions.rs"
55+
required-features = ["ai-instructions"]
56+
5157
[features]
5258
default = ["cli"]
5359
json = ["serde/derive", "serde_json"]
5460
cli = ["clap/derive", "clap_complete", "clap-utilities", "json"]
5561
cli-completions = ["cli"]
62+
ai-instructions = ["clap/derive"]
5663

5764
[dependencies]
5865
assert-cmp = "0.3.0"

cli/ai_instructions.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use clap::Parser;
2+
use derive_more::Display;
3+
use pipe_trait::Pipe;
4+
use std::{
5+
fmt,
6+
fs::{read_to_string, File},
7+
io::{self, Write},
8+
path::{Path, PathBuf},
9+
process::ExitCode,
10+
};
11+
12+
const SHARED: &str = include_str!("../template/ai-instructions/shared.md");
13+
const CLAUDE: &str = include_str!("../template/ai-instructions/claude.md");
14+
const COPILOT: &str = include_str!("../template/ai-instructions/copilot.md");
15+
const AGENTS: &str = include_str!("../template/ai-instructions/agents.md");
16+
17+
#[derive(Clone, Copy)]
18+
struct Fragments(&'static [&'static str]);
19+
20+
impl fmt::Display for Fragments {
21+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22+
let Fragments(fragments) = self;
23+
for fragment in *fragments {
24+
f.write_str(fragment)?;
25+
}
26+
Ok(())
27+
}
28+
}
29+
30+
impl Fragments {
31+
fn matches(&self, actual: &str) -> bool {
32+
let Fragments(fragments) = self;
33+
let mut remaining = actual;
34+
for fragment in *fragments {
35+
match remaining.strip_prefix(fragment) {
36+
Some(rest) => remaining = rest,
37+
None => return false,
38+
}
39+
}
40+
remaining.is_empty()
41+
}
42+
}
43+
44+
const FILES: &[(&str, Fragments)] = &[
45+
("CLAUDE.md", Fragments(&[SHARED, CLAUDE])),
46+
(
47+
".github/copilot-instructions.md",
48+
Fragments(&[SHARED, COPILOT]),
49+
),
50+
("AGENTS.md", Fragments(&[SHARED, AGENTS])),
51+
];
52+
53+
#[derive(Debug, Display)]
54+
enum RuntimeError {
55+
#[display("Failed to write {path}: {error}")]
56+
WriteFile {
57+
path: &'static str,
58+
error: io::Error,
59+
},
60+
#[display("Failed to read {path}: {error}")]
61+
ReadFile {
62+
path: &'static str,
63+
error: io::Error,
64+
},
65+
#[display("Some AI instruction files were outdated.")]
66+
Outdated,
67+
}
68+
69+
impl RuntimeError {
70+
fn hint(&self, args: &Args) -> Option<impl fmt::Display> {
71+
match self {
72+
RuntimeError::ReadFile { .. } | RuntimeError::WriteFile { .. } => None,
73+
RuntimeError::Outdated => Some(format!(
74+
"Run `./run.sh pdu-ai-instructions --generate {}` to update.",
75+
args.repository.display(),
76+
)),
77+
}
78+
}
79+
}
80+
81+
/// The CLI arguments.
82+
#[derive(Debug, Parser)]
83+
#[clap(about = "Check or generate AI instruction files from templates")]
84+
struct Args {
85+
/// Generate the AI instruction files instead of checking them.
86+
#[clap(long)]
87+
generate: bool,
88+
89+
/// Path to the top-level directory of the repository.
90+
repository: PathBuf,
91+
}
92+
93+
fn main() -> ExitCode {
94+
let args = Args::parse();
95+
let result = match args.generate {
96+
true => write_files(&args.repository),
97+
false => check_files(&args.repository),
98+
};
99+
if let Err(error) = result {
100+
eprintln!("error: {error}");
101+
if let Some(hint) = error.hint(&args) {
102+
eprintln!("hint: {hint}");
103+
}
104+
return ExitCode::FAILURE;
105+
}
106+
ExitCode::SUCCESS
107+
}
108+
109+
fn write_files(repository: &Path) -> Result<(), RuntimeError> {
110+
for (path, fragments) in FILES {
111+
let mut output = repository
112+
.join(path)
113+
.pipe(File::create)
114+
.map_err(|error| RuntimeError::WriteFile { path, error })?;
115+
write!(output, "{fragments}").map_err(|error| RuntimeError::WriteFile { path, error })?;
116+
eprintln!("info: Generated file {path}");
117+
}
118+
Ok(())
119+
}
120+
121+
fn check_files(repository: &Path) -> Result<(), RuntimeError> {
122+
let mut result: Result<(), RuntimeError> = Ok(());
123+
for &(path, fragments) in FILES {
124+
let actual = repository
125+
.join(path)
126+
.pipe(read_to_string)
127+
.map_err(|error| RuntimeError::ReadFile { path, error })?;
128+
if !fragments.matches(&actual) {
129+
eprintln!("error: File {path} is out-of-date");
130+
result = Err(RuntimeError::Outdated);
131+
}
132+
}
133+
result
134+
}

run.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#! /bin/bash
22
set -o errexit -o pipefail -o nounset
3-
exec cargo run --bin="$1" --features cli-completions -- "${@:2}"
3+
exec cargo run --bin="$1" --features cli-completions,ai-instructions -- "${@:2}"

template/ai-instructions/agents.md

Whitespace-only changes.

template/ai-instructions/claude.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `gh` (GitHub CLI) is not installed — do not attempt to use it

template/ai-instructions/copilot.md

Whitespace-only changes.

template/ai-instructions/shared.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# AI Instructions
2+
3+
Read and follow the CONTRIBUTING.md file in this repository for all code style conventions, commit message format, and development guidelines.
4+
5+
## Quick Reference
6+
7+
- Commit format: Conventional Commits — `type(scope): lowercase description`
8+
- Version releases are the only exception: just the version number (e.g. `0.21.1`)
9+
- Prefer merged imports
10+
- Use descriptive generic names (`Size`, `Report`), not single letters
11+
- Use descriptive variable and closure parameter names by default — single letters are only allowed in: conventional names (`n` for count, `f` for formatter), comparison closures (`|a, b|`), trivial single-expression closures, fold accumulators, index variables (`i`/`j`/`k` in short closures or index-based loops only), and test fixtures (identical roles only). Never use single letters in multi-line functions or closures
12+
- Use `pipe-trait` for chaining through unary functions (constructors, `Some`, `Ok`, free functions, etc.), avoiding nested calls, and continuing method chains — but not for simple standalone calls (prefer `foo(value)` over `value.pipe(foo)`)
13+
- Prefer `where` clauses for multiple trait bounds
14+
- Derive order: std traits → comparison traits → `Hash` → derive_more → feature-gated
15+
- Custom errors: `#[derive(Debug, Display, Error)]`
16+
- Minimize `unwrap()` in non-test code — use proper error handling
17+
- Install toolchain before running tests: `rustup toolchain install "$(< rust-toolchain)" && rustup component add --toolchain "$(< rust-toolchain)" rustfmt clippy`
18+
- Run `FMT=true LINT=true BUILD=true TEST=true DOC=true ./test.sh` to validate changes

0 commit comments

Comments
 (0)