282 lines
6.1 KiB
Markdown
282 lines
6.1 KiB
Markdown
# wp-materialize – Agents Specification
|
||
|
||
## Purpose
|
||
|
||
`wp-materialize` is an **automation compiler** that materializes specified Markdown files in Git repositories (or local directories) into WordPress posts. Git / filesystem state is the **single source of truth**; WordPress is a **derived, materialized view**.
|
||
|
||
The system is **declarative, atomic, incremental, and fail-fast**. It never guesses intent, never partially updates WordPress, and never mutates state unless correctness is proven ahead of time.
|
||
|
||
This document is the **authoritative agent-facing spec** to be fed to Codex or other build agents.
|
||
|
||
---
|
||
|
||
## Core Principles (Non-Negotiable)
|
||
|
||
1. **Git / Filesystem as Source of Truth**
|
||
WordPress content must exactly reflect declared Markdown sources and manifests.
|
||
|
||
2. **Declarative Configuration Only**
|
||
No implicit discovery, no heuristics, no inference.
|
||
|
||
3. **Atomic Execution**
|
||
A full dry-run validation must succeed before *any* WordPress mutation occurs.
|
||
|
||
4. **Incremental Updates**
|
||
Only content whose source timestamp is newer than its cached materialization timestamp may be updated.
|
||
|
||
5. **Fail Fast, Fail Loud**
|
||
Any configuration, validation, or conversion error aborts the entire run.
|
||
|
||
---
|
||
|
||
## High-Level Architecture
|
||
|
||
The system operates in two strictly separated phases:
|
||
|
||
### Phase 1: Pure Evaluation (Dry Run)
|
||
|
||
* Read global config
|
||
* Discover and load repositories / directories
|
||
* Load and validate all `.wp-materialize.json` manifests
|
||
* Resolve inheritance (categories, tags, subdirectories)
|
||
* Convert Markdown → HTML (in-memory only)
|
||
* Resolve titles, timestamps, taxonomy
|
||
* Determine incremental update set
|
||
* Detect *all* errors
|
||
|
||
**If any error occurs, execution stops here.**
|
||
|
||
### Phase 2: Side-Effect Application
|
||
|
||
* Create missing WordPress categories
|
||
* Create or update WordPress posts
|
||
* Update cached timestamps *only for successfully applied posts*
|
||
|
||
---
|
||
|
||
## Global Configuration
|
||
|
||
Location:
|
||
|
||
```
|
||
.config/wp-materialize/config.json
|
||
```
|
||
|
||
Responsibilities:
|
||
|
||
* WordPress root directory (where `wp` CLI is executed)
|
||
* Repository storage directory
|
||
* List of Git repositories to clone / pull
|
||
* List of non-git directories to manage
|
||
|
||
### Runtime State (Separate from Config)
|
||
|
||
Mutable runtime state must be stored separately, e.g.:
|
||
|
||
```
|
||
.config/wp-materialize/state.json
|
||
```
|
||
|
||
State includes:
|
||
|
||
* Last successful materialization timestamp per post
|
||
* Cached per-post source timestamps
|
||
|
||
Config must remain declarative and diffable.
|
||
|
||
---
|
||
|
||
## Repository and Directory Rules
|
||
|
||
* Each managed directory **must** contain a `.wp-materialize.json` manifest.
|
||
* Any directory listed under `subdirectories` **must** contain its own manifest.
|
||
* Missing manifests are **hard errors**.
|
||
* No implicit recursion is allowed.
|
||
|
||
---
|
||
|
||
## Per-Directory Manifest: `.wp-materialize.json`
|
||
|
||
Each manifest defines a **scope boundary**.
|
||
|
||
### Top-Level Fields
|
||
|
||
#### `categories`
|
||
|
||
```json
|
||
{
|
||
"content": ["Systems", "Infrastructure"],
|
||
"inherit": true
|
||
}
|
||
```
|
||
|
||
* `content`: array of category paths
|
||
* `inherit: true` → append to parent effective categories
|
||
* `inherit: false` → override parent categories entirely
|
||
|
||
#### `tags`
|
||
|
||
```json
|
||
{
|
||
"content": ["automation", "wordpress"],
|
||
"inherit": true
|
||
}
|
||
```
|
||
|
||
Semantics identical to `categories`.
|
||
|
||
#### `subdirectories`
|
||
|
||
```json
|
||
{
|
||
"content": ["design", "notes"],
|
||
"inherit": true
|
||
}
|
||
```
|
||
|
||
* Controls traversal explicitly
|
||
* Included subdirectories **must** have their own manifest
|
||
* `inherit: false` cuts traversal
|
||
|
||
---
|
||
|
||
## File-Level Configuration
|
||
|
||
Each file listed under `files` represents a WordPress post.
|
||
|
||
```json
|
||
"files": {
|
||
"post.md": {
|
||
"title": "Explicit Title",
|
||
"categories": { "content": ["Overrides"], "inherit": false },
|
||
"tags": { "content": ["extra"], "inherit": true }
|
||
},
|
||
|
||
"essay.md": {
|
||
"use_heading_as_title": {
|
||
"level": 1,
|
||
"strict": true
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Title Rules
|
||
|
||
* If `use_heading_as_title` is specified:
|
||
|
||
* Extract the specified heading level
|
||
* Use it as the WordPress post title
|
||
* Remove that heading from the body
|
||
* Promote remaining headings by one level
|
||
* If `strict: true`, exactly one matching heading must exist
|
||
|
||
* Otherwise, `title` **must** be provided
|
||
|
||
---
|
||
|
||
## Markdown → HTML Conversion
|
||
|
||
* Conversion occurs **only during dry run**
|
||
* No HTML is written or sent to WordPress during evaluation
|
||
* Conversion errors are fatal
|
||
|
||
---
|
||
|
||
## Category Materialization
|
||
|
||
* Categories are treated as **hierarchical paths**
|
||
* If a declared category path does not exist in WordPress:
|
||
|
||
* It is **automatically created** during the apply phase
|
||
* Category creation:
|
||
|
||
* Must be planned during dry run
|
||
* Must occur before post updates
|
||
|
||
Tags are **not** auto-created.
|
||
|
||
---
|
||
|
||
## Timestamps and Incremental Updates
|
||
|
||
### Timestamp Sources
|
||
|
||
* Git repository:
|
||
|
||
* Use Git commit timestamps
|
||
* Non-git directory:
|
||
|
||
* Use filesystem timestamps
|
||
|
||
The source of timestamps must be **deterministic per repository**.
|
||
|
||
### Cached Metadata
|
||
|
||
* Each post stores a cached source timestamp representing the last **successful** materialization
|
||
* Failed runs **must not** update cached timestamps
|
||
|
||
### Incremental Rule
|
||
|
||
On each run:
|
||
|
||
* Compare current source timestamp vs cached timestamp
|
||
* Only posts where `source_timestamp > cached_timestamp` are eligible for update
|
||
* Unchanged posts are treated as no-ops
|
||
|
||
---
|
||
|
||
## Post Identity
|
||
|
||
Each WordPress post must store stable metadata:
|
||
|
||
```
|
||
_wp_materialize_source = <repo_name>:<relative_path>
|
||
```
|
||
|
||
This identity is used for:
|
||
|
||
* Idempotent updates
|
||
* Safe renames
|
||
* Incremental comparison
|
||
|
||
---
|
||
|
||
## Atomicity Guarantee
|
||
|
||
* If **any** dry-run validation fails:
|
||
|
||
* No WordPress calls are executed
|
||
* No categories are created
|
||
* No cached timestamps are updated
|
||
|
||
* Apply phase executes only after full validation succeeds
|
||
|
||
---
|
||
|
||
## Error Handling
|
||
|
||
All errors are fatal:
|
||
|
||
* Missing manifests
|
||
* Invalid inheritance
|
||
* Invalid Markdown
|
||
* Missing or ambiguous titles
|
||
* Invalid category/tag resolution
|
||
* Timestamp resolution failures
|
||
|
||
No warnings. No partial success.
|
||
|
||
---
|
||
|
||
## Implementation Notes
|
||
|
||
* Language: **Python**
|
||
* The implementation must prioritize:
|
||
|
||
* Determinism
|
||
* Readable error messages
|
||
* Testable pure functions for evaluation phase
|
||
|
||
This document is the **contract**. Implementation must not relax or reinterpret it.
|