Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
15f201d
feat(tree_path): add Bash language support
xen0n Mar 11, 2026
45887a1
feat(tree_path): add Ruby language support
xen0n Mar 11, 2026
801d541
feat(tree_path): add Zig language support
xen0n Mar 11, 2026
e0a7617
build(deps): add nom 8 for tree_path parsing
xen0n Mar 11, 2026
9d21f8a
feat(tree_path): add nom parser for tree_path grammar v0.2
xen0n Mar 11, 2026
40bcc56
docs(roadmap): update M7 status and add tree_path grammar spec
xen0n Mar 11, 2026
fa70138
fix(tree_path): address clippy warnings and format code
xen0n Mar 11, 2026
9e8fe48
docs(roadmap): sync status for implemented M7 languages and parser
xen0n Mar 11, 2026
353f3d7
fix(tree_path): attach injection marker to preceding segment without ::
xen0n Mar 11, 2026
9ee324e
docs(design): document tree_path quoting and injection syntax
xen0n Mar 11, 2026
7111050
fix(design): rebuild design doc sidecar with correct hashes and anchors
xen0n Mar 11, 2026
f64c127
feat(linter): add EndRequirement marker variant and scanner support
xen0n Mar 11, 2026
bdd6a5f
feat(linter): use marker-pair spans in check and reanchor
xen0n Mar 11, 2026
b1cbe68
docs(design): specify end-requirement marker and migrate blocks
xen0n Mar 11, 2026
2564749
style: cargo fmt
xen0n Mar 11, 2026
f887428
feat(linter): compute tree_path in check --fix
xen0n Mar 11, 2026
e01808f
refactor: move sidecar file resolution to discovery module
xen0n Mar 11, 2026
c208316
feat(cli): replace reanchor subcommand with migrate
xen0n Mar 11, 2026
253c75c
docs: replace reanchor references with check --fix and migrate
xen0n Mar 11, 2026
fbb5b76
fix(linter): deterministic related edge output order
xen0n Mar 11, 2026
cff9698
style: cargo fmt && liyi check --fix
xen0n Mar 11, 2026
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
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ When writing or modifying code:
2. When module-level invariants are apparent, write an `@liyi:module` block — in the directory's existing module doc (`README.md`, `doc.go`, `mod.rs` doc comment, etc.) or in a dedicated `LIYI.md`. Use the doc markup language's comment syntax for the marker.
3. If a source item has a `@liyi:related <name>` annotation, record the dependency in `.liyi.jsonc` as `"related": {"<name>": null}`. The tool fills in the requirement's current hash.
4. For each `@liyi:requirement <name>` block encountered, ensure it has a corresponding entry in the co-located `.liyi.jsonc` with `"requirement"` and `"source_span"`. (The tool fills in `"source_hash"`.)
5. If a spec has `"related"` edges referencing a requirement, do not overwrite the requirement text during inference. Re-anchor the spec (update `source_span`) but preserve the `"related"` edges. Do not write `source_hash` — the tool fills it in.
5. If a spec has `"related"` edges referencing a requirement, do not overwrite the requirement text during inference. Update the spec (update `source_span`) but preserve the `"related"` edges. Do not write `source_hash` — the tool fills it in.
6. Only generate adversarial tests from items that have a `@liyi:intent` annotation in source or `"reviewed": true` in the sidecar (i.e., human-reviewed intent). When `@liyi:intent` is present in source, use its prose (or the docstring for `=doc`) as the authoritative intent for test generation.
7. Tests should target boundary conditions, error-handling gaps, property violations, and semantic mismatches. Prioritize tests a subtly wrong implementation would fail.
8. Skip items annotated with `@liyi:ignore` or `@liyi:trivial`, and files matched by `.liyiignore`. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial`.
9. Use a different model for test generation than the one that wrote the code, when possible.
10. When `liyi check` reports stale items, choose one of two paths:
- **Direct re-inference** (preferred during interactive editing with few stale items): re-read the source, update `source_span` and `intent` in the sidecar, leave `"reviewed"` unset. Appropriate when you are the agent that just made the change, the number of stale items is small, and the changes are straightforward.
- **Triage** (preferred for batch workflows, CI, or when many items are stale): assess each item — is the change cosmetic, semantic, or an intent violation? Write the assessment to `.liyi/triage.json` following the triage report schema. For cosmetic changes, run `liyi triage --apply` to auto-reanchor. For semantic changes, propose updated intent in `suggested_intent`. For intent violations, flag for human review. Prefer triage when stale items have `"reviewed": true` or `@liyi:intent` in source — these carry human-vouched intent that deserves explicit assessment, not silent re-inference.
- **Triage** (preferred for batch workflows, CI, or when many items are stale): assess each item — is the change cosmetic, semantic, or an intent violation? Write the assessment to `.liyi/triage.json` following the triage report schema. For cosmetic changes, run `liyi triage --apply` to auto-fix. For semantic changes, propose updated intent in `suggested_intent`. For intent violations, flag for human review. Prefer triage when stale items have `"reviewed": true` or `@liyi:intent` in source — these carry human-vouched intent that deserves explicit assessment, not silent re-inference.
11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. Do not commit with unresolved coverage gaps — CI will reject it.

### `.liyi.jsonc` Schema (v0.1)
Expand Down Expand Up @@ -104,7 +104,7 @@ Sidecar files must conform to the following JSON Schema. The top-level object ha
},
"source_hash": {
"$ref": "#/$defs/sourceHash",
"description": "Tool-managed. SHA-256 hex digest of the source lines in the span. Computed by liyi reanchor or the linter — agents should not produce this."
"description": "Tool-managed. SHA-256 hex digest of the source lines in the span. Computed by liyi check --fix — agents should not produce this."
},
"source_anchor": {
"type": "string",
Expand All @@ -128,7 +128,7 @@ Sidecar files must conform to the following JSON Schema. The top-level object ha
},
"_hints": {
"type": "object",
"description": "Transient inference aids emitted by liyi init for cold-start scenarios. LLM-readable, intentionally unstructured. Stripped by liyi reanchor after initial review. Tools MUST NOT rely on any specific shape."
"description": "Transient inference aids emitted by liyi init for cold-start scenarios. LLM-readable, intentionally unstructured. Stripped by liyi check --fix after initial review. Tools MUST NOT rely on any specific shape."
}
}
},
Expand All @@ -149,7 +149,7 @@ Sidecar files must conform to the following JSON Schema. The top-level object ha
},
"source_hash": {
"$ref": "#/$defs/sourceHash",
"description": "Tool-managed. Computed by liyi reanchor or the linter."
"description": "Tool-managed. Computed by liyi check --fix."
},
"source_anchor": {
"type": "string",
Expand Down Expand Up @@ -215,8 +215,8 @@ When `liyi check` reports stale items, the agent assesses each and writes the re
},
"action": {
"type": "string",
"enum": ["auto-reanchor", "update-intent", "fix-code-or-update-intent", "manual-review"],
"description": "Recommended action. auto-reanchor for cosmetic, update-intent for semantic, fix-code-or-update-intent for intent-violation, manual-review for unclear."
"enum": ["auto-fix", "update-intent", "fix-code-or-update-intent", "manual-review"],
"description": "Recommended action. auto-fix for cosmetic, update-intent for semantic, fix-code-or-update-intent for intent-violation, manual-review for unclear."
},
"summary": {
"type": "object",
Expand Down
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ liyi check --root .
## How It Works

1. **Agent infers intent** — today's agents automatically read `AGENTS.md`, which teaches them the 立意 pattern. During normal development they maintain `.liyi.jsonc` sidecar files for each code item, with `source_span` and natural-language `intent`. If they don't do it automatically, you can always tell them to.
2. **`liyi check`** — hashes source spans, detects staleness and shifts, checks review status, tracks requirement edges. Zero network, zero LLM, fully deterministic.
3. **`liyi reanchor`** — re-hashes spans after intentional code changes. Never modifies intent or review state.
2. **`liyi check`** — hashes source spans, detects staleness and shifts, checks review status, tracks requirement edges. Zero network, zero LLM, fully deterministic. With `--fix`, auto-corrects shifted spans, fills missing hashes, and computes `tree_path`.
3. **`liyi migrate`** — upgrades sidecar files when the schema version changes. Idempotent.
4. **Human reviews** — sets `"reviewed": true` in the sidecar to approve, or adds `@liyi:intent` in source to provide the authoritative human version.

## Progressive Adoption
Expand All @@ -55,10 +55,8 @@ liyi check [OPTIONS] [PATHS]...
--fail-on-req-changed <true|false> Fail on changed requirements (default: true)
--root <PATH> Override repo root

liyi reanchor [FILE]
--item <NAME> Target a specific item
--span <S,E> Override span (1-indexed, inclusive)
--migrate Schema version migration
liyi migrate [FILE|DIR]...
Upgrade sidecar schema version
```

## Exit Codes
Expand Down
10 changes: 4 additions & 6 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ liyi check --root .
## 工作原理

1. **由智能体推断意图** — 当今的智能体会自动读取 `AGENTS.md`,于是便掌握了《立意》设计模式。在正常开发流程中,它们便会自动为每个代码条目维护 `.liyi.jsonc` sidecar 文件,包含 `source_span` 和自然语言 `intent`。如果没有自动维护,也总可以明确告诉它这么干。
2. **`liyi check`** — 为智能体提供的源码区间计算内容哈希,检测内容是否过时、行号是否偏移、是否被复核过,并追踪需求边。零网络访问、零 LLM 依赖、行为完全确定。
3. **`liyi reanchor`**在有意的代码变更后重新计算区间哈希。不修改意图或复核状态
2. **`liyi check`** — 为智能体提供的源码区间计算内容哈希,检测内容是否过时、行号是否偏移、是否被复核过,并追踪需求边。零网络访问、零 LLM 依赖、行为完全确定。`--fix` 可自动修正偏移区间、填充缺失哈希、计算 `tree_path`
3. **`liyi migrate`**当 schema 版本变更时升级 sidecar 文件。幂等
4. **由人类复核** — 在 `.liyi.jsonc` 中设置 `"reviewed": true` 以批准,或在源码中添加 `@liyi:intent` 以明确给出人类版本。

## 渐进式采用
Expand All @@ -56,10 +56,8 @@ liyi check [OPTIONS] [PATHS]...
--fail-on-req-changed <true|false> 对已变更需求报错(默认:true)
--root <PATH> 覆盖仓库根目录
liyi reanchor [FILE]
--item <NAME> 指定目标条目
--span <S,E> 覆盖区间(1 起始,闭区间)
--migrate 执行 schema 版本迁移
liyi migrate [FILE|DIR]...
升级 sidecar schema 版本
```

## 退出状态码
Expand Down
36 changes: 3 additions & 33 deletions crates/liyi-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,10 @@ pub enum Commands {
level: DiagnosticLevel,
},

/// Re-hash source spans in sidecar files
Reanchor {
/// Sidecar files or directories to reanchor (recursive)
#[arg(required_unless_present = "migrate")]
/// Migrate sidecar files to the current schema version
Migrate {
/// Sidecar files or directories to migrate (recursive)
files: Vec<PathBuf>,

/// Target a specific item by name
#[arg(long, requires = "span")]
item: Option<String>,

/// Override span (start,end)
#[arg(long, requires = "item", value_parser = parse_span)]
span: Option<[usize; 2]>,

/// Migrate sidecar to current schema version
#[arg(long)]
migrate: bool,
},

/// Scaffold AGENTS.md or skeleton .liyi.jsonc sidecar
Expand Down Expand Up @@ -116,20 +103,3 @@ pub enum Commands {
item: Option<String>,
},
}

/// Parse a "start,end" string into a [usize; 2] span.
fn parse_span(s: &str) -> Result<[usize; 2], String> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 2 {
return Err(format!("expected format 'start,end', got '{s}'"));
}
let start: usize = parts[0]
.trim()
.parse()
.map_err(|_| format!("invalid start: '{}'", parts[0].trim()))?;
let end: usize = parts[1]
.trim()
.parse()
.map_err(|_| format!("invalid end: '{}'", parts[1].trim()))?;
Ok([start, end])
}
18 changes: 3 additions & 15 deletions crates/liyi-cli/src/cli.rs.liyi.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,14 @@
{
"item": "Commands",
"reviewed": false,
"intent": "Enumerate all CLI subcommands (Check, Reanchor, Init, Approve) with their full set of flags and arguments, providing defaults and mutual constraints (e.g. --item requires --span, file is required unless --migrate, --level filters diagnostic output).",
"intent": "Enumerate all CLI subcommands (Check, Migrate, Init, Approve) with their full set of flags and arguments, providing defaults and mutual constraints (e.g. --level filters diagnostic output).",
"source_span": [
28,
118
105
],
"tree_path": "enum::Commands",
"source_hash": "sha256:383b23d5dbade566788d2673cb55fc05e8e7e33b70f4e2edd77df0245d09db65",
"source_hash": "sha256:94096c9cb7d64c3fd721c783da0c3f0482f6b548e317daac6b2c1d0ec902dd63",
"source_anchor": "pub enum Commands {"
},
{
"item": "parse_span",
"reviewed": true,
"intent": "Parse a 'start,end' string into a [usize; 2] span, rejecting inputs that are not exactly two comma-separated unsigned integers.",
"source_span": [
121,
135
],
"tree_path": "fn::parse_span",
"source_hash": "sha256:d57d01b6fb8d7fbefc54c62e3240b46d80cc2370a7d148811caad6d809b23977",
"source_anchor": "fn parse_span(s: &str) -> Result<[usize; 2], String> {"
}
]
}
22 changes: 4 additions & 18 deletions crates/liyi-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,13 @@ fn main() {

process::exit(exit_code as i32);
}
Commands::Reanchor {
files,
item,
span,
migrate,
} => {
if migrate && files.is_empty() {
eprintln!("--migrate requires at least one sidecar file path");
process::exit(2);
}

Commands::Migrate { files } => {
if files.is_empty() {
eprintln!("at least one sidecar file or directory required");
process::exit(2);
}

let targets = match liyi::reanchor::resolve_reanchor_targets(&files) {
let targets = match liyi::discovery::resolve_sidecar_targets(&files) {
Ok(t) => t,
Err(e) => {
eprintln!("Error: {e}");
Expand All @@ -120,13 +110,9 @@ fn main() {

let mut errors = 0;
for sidecar_path in &targets {
match liyi::reanchor::run_reanchor(sidecar_path, item.as_deref(), span, migrate) {
match liyi::reanchor::run_reanchor(sidecar_path, None, None, true) {
Ok(()) => {
if migrate {
println!("Migrated: {}", sidecar_path.display());
} else {
println!("Reanchored: {}", sidecar_path.display());
}
println!("Migrated: {}", sidecar_path.display());
}
Err(e) => {
eprintln!("Error ({}): {e}", sidecar_path.display());
Expand Down
8 changes: 4 additions & 4 deletions crates/liyi-cli/src/main.rs.liyi.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@
"intent": "=doc",
"source_span": [
26,
242
228
],
"tree_path": "fn::main",
"source_hash": "sha256:a11828b49d8e7c5b144423442c9bab41b1d84aff99c551e159e0c03558f70b06",
"source_hash": "sha256:5c447be7165dcba8bfd3d08fff588db7b2d95309ec9c474aa30487deeec02d49",
"source_anchor": "fn main() {"
},
{
"item": "is_tty",
"reviewed": true,
"intent": "=doc",
"source_span": [
248,
250
234,
236
],
"tree_path": "fn::is_tty",
"source_hash": "sha256:36dcd447c8fa9e666c6682395c3148c216b7c07dce8cc88f3d76f90714207ccd",
Expand Down
4 changes: 4 additions & 0 deletions crates/liyi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ tree-sitter-php = "0.24.2"
tree-sitter-objc = "3.0.2"
tree-sitter-kotlin-ng = "1.1.0"
tree-sitter-swift = "0.7.1"
tree-sitter-bash = "0.25.1"
tree-sitter-ruby = "0.23.1"
tree-sitter-zig = "1.1.2"
nom = "8"

[dev-dependencies]
proptest = "1"
Expand Down
4 changes: 2 additions & 2 deletions crates/liyi/src/approve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use crate::discovery::resolve_sidecar_targets;
use crate::hashing::hash_span;
use crate::reanchor::resolve_reanchor_targets;
use crate::sidecar::{Spec, parse_sidecar, write_sidecar};

/// Result of an approve operation on a single sidecar.
Expand Down Expand Up @@ -75,7 +75,7 @@ pub fn collect_approval_candidates(
paths: &[PathBuf],
item_filter: Option<&str>,
) -> Result<Vec<ApprovalCandidate>, ApproveError> {
let targets = resolve_reanchor_targets(paths).map_err(ApproveError::Parse)?;
let targets = resolve_sidecar_targets(paths).map_err(ApproveError::Parse)?;
if targets.is_empty() {
return Err(ApproveError::NoTargets);
}
Expand Down
4 changes: 2 additions & 2 deletions crates/liyi/src/approve.rs.liyi.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@
130
],
"tree_path": "fn::collect_approval_candidates",
"source_hash": "sha256:1fc98ea41edd3bed9c13d936635399d5e27a81f1f8acec326368b1eb532bda7c",
"source_hash": "sha256:f16f381eb67bf6126a0acfe022aac1ee575696b1d32a5cb46cee6bf2eb15a1d2",
"source_anchor": "pub fn collect_approval_candidates("
},
{
"item": "apply_approval_decisions",
"reviewed": false,
"intent": "Apply a parallel slice of Decision values to the candidates, grouped by sidecar file. For Yes: set reviewed=true and reanchor hashes. For No: set reviewed=false. For Skip: no mutation. Write back modified sidecars unless dry_run. Returns per-sidecar ApproveResult.",
"intent": "Apply a parallel slice of Decision values to the candidates, grouped by sidecar file. For Yes: set reviewed=true and fill hashes. For No: set reviewed=false. For Skip: no mutation. Write back modified sidecars unless dry_run. Returns per-sidecar ApproveResult.",
"source_span": [
137,
206
Expand Down
Loading
Loading