From 2e9a12998bc2379983e95e9831b57bfba512be22 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 10:49:04 +0800 Subject: [PATCH 01/44] docs(design): add =self-doc sentinel and derivative-item exclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate two claims that were both being carried by `=trivial`: a *risk* claim ("too small to hide behavior") and an *epistemic* claim ("intent is intrinsic to the surface form"). In practice `=trivial` was stretched onto substantial code — an 11-line context struct, a 163-line test module — where the item is not small but its intent is self-evident. This is the review-action a reviewer actually wanted but had no label for. Add `=self-doc` for the epistemic case, with a strict test that resists the "self-documenting" excuse: a competent-but-domain-stranger reader holding only name, signature, and durable co-located context must recover every property any observer could rely on, including non-value-domain effects (timing, resources, termination, information flow). The test is an open observer counterfactual, not a finite effect checklist, and side effects are presumptively disqualifying with a discharge (not assert) obligation. Also document the derivative-item rule: items whose whole purpose is to exercise/assert another specced item (chiefly tests) carry no independent intent and should be excluded from inference entirely — no entry, not a lighter sentinel — since specifying test intent is circular with adversarial test generation. Record the threat-model boundary as an explicit non-goal: 立意 surfaces hidden intent but does not verify the honesty of intent deliberately misrepresented via non-co-located or runtime-controlled state. Original prompt: > [multi-turn discussion, see session] Assess integrating 立意 into agentic > scaffolds; then: whether `=per-se`/`=self-doc` should complement `=trivial`; > survey the repo's `=trivial` uses; settle the reference reader, durable > context, observer/effect closure, and threat-model boundary; then make each > change to the spec, the AGENTS.md edits, and the implementation, in commits. AI-assisted-by: Claude Opus 4.8 (OpenCode) Signed-off-by: WANG Xuerui --- docs/liyi-design.md | 50 +++++++++++++++++++++++++++++++++- docs/liyi-design.md.liyi.jsonc | 28 +++++++++---------- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/docs/liyi-design.md b/docs/liyi-design.md index e36ca7c..1b834b3 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -974,6 +974,7 @@ This gives agents a DRY option: well-documented code gets `"intent": "=doc"` ins |---|---|---| | Agent | `"intent": "=doc"` in sidecar | "The docstring captures it" | | Agent | `"intent": "=trivial"` in sidecar | “This item is trivial — no behavioral spec warranted” | +| Agent | `"intent": "=self-doc"` in sidecar | “The code documents itself — a spec would tautologically restate the surface form” | | Agent | `"intent": ""` in sidecar | "Here’s what I infer it should do" | | Human | `@liyi:intent=doc` in source | "I confirm the docstring is my intent" | | Human | `@liyi:intent ` in source | "Here’s what I say it should do" | @@ -992,7 +993,9 @@ The `@liyi:trivial` source annotation tells agents and the linter to skip an ite } ``` -Meaning: “I evaluated this item and it’s self-evident — a spec would add no value.” +Meaning: “I evaluated this item and it’s trivial — small enough to carry no hidden behavior, so a spec would add no value.” + +`"=trivial"` is a **risk claim**: the item is too small and simple for anything to go subtly wrong, so a reviewer proves triviality by reading the body. It is *not* a claim that the item is self-evident despite being non-trivial — that is what `"=self-doc"` is for (see below). Do not stretch `"=trivial"` to cover substantial code whose intent happens to be recoverable from its surface form. **When to use which:** @@ -1009,6 +1012,51 @@ The second case matters for the scaffold workflow (see *Tree-sitter item discove **No source-annotation counterpart.** Unlike `=doc`, there is no `@liyi:intent =trivial` source marker. If you’re in the source, use `@liyi:trivial` directly — it’s shorter and the intent is the same. `"=trivial"` is specifically for sidecar-only workflows (scaffold, batch init, third-party code). +### `"=self-doc"` in the sidecar — the self-documenting sentinel + +`"=trivial"` answers "is this small enough that nothing can hide?" — a *risk* claim about size. But some items are **not** small yet still carry no intent a spec could usefully add: a plain data struct whose fields *are* its meaning, a recursive tree-search whose name fully states its job, a container module whose purpose is the sum of its members. Forcing prose onto these produces a tautology; marking them `"=trivial"` is a lie about their size (the 11-line context struct, the 163-line test module). `"=self-doc"` is the honest label for this class: + +```jsonc +{ + "item": "RecoveryMethod", + "intent": "=self-doc", + "source_span": [6, 10], + "tree_path": "enum.RecoveryMethod" +} +``` + +Meaning: “The item documents itself — any prose intent I could write would tautologically restate its surface form. There is nothing implicit hidden behind that form.” + +`"=self-doc"` is an **epistemic claim**, distinct from `"=trivial"`'s risk claim. The two are complementary, not interchangeable: + +| Sentinel | Claim | Reviewer action | +|---|---|---| +| `"=trivial"` | Too small to carry hidden behavior | Read the code, prove triviality | +| `"=self-doc"` | Intent is intrinsic to the surface form | Read the code, prove nothing implicit hides behind the form | +| `"=doc"` | Intent is in the docstring | Read the docstring | +| `""` | Intent stated by the agent | Read intent, read code, prove equivalence | + +An item can be non-trivial yet self-documenting (a recursive search), or trivial yet not needing the self-doc claim at all (a one-line getter — just use `"=trivial"`). The boundary: **`"=trivial"` = correctness proven by *reading the body*; `"=self-doc"` = correctness proven by *trusting the name/signature without tracing the body*.** + +**The self-documenting test (the discipline that keeps the sentinel honest).** "Self-documenting" is a phrase programmers reach for to excuse skipping documentation, so the claim carries a strict bar. `"=self-doc"` holds **only if** a reader who is *competent in the language and paradigm but a stranger to this codebase, its domain, and the authoring session* — holding only the item's name, signature, and **durable co-located context** (its enclosing module, in-scope types, any governing `@liyi:note`) — can recover **every property any observer could correctly rely on**, including properties observable *outside the value domain*: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Concretely: + +- **The tautology test, over the behavioral surface — not the syntactic one.** A sequence `A; B; C` of side-effecting steps is *not* self-documenting merely because prose would restate the sequence: the *reason for the order*, the *cost of repetition*, and the *partial-failure semantics* are load-bearing and invisible in the sequence. If any true constraint would be *news* to a signature-only reader, prose is required. +- **The observer counterfactual (the general form — there is no finite effect checklist).** Ask: *is there any observer — a caller, an attacker, a scheduler, a profiler, a regulator — who could distinguish a correct implementation from a plausible-but-wrong one, where the distinction is not visible in the surface?* If yes, it is not self-documenting. A constant-time comparison is pure, deterministic, idempotent, and stateless, yet **fails** this test: the attacker is the observer, and the timing invariant is domain knowledge the stranger-reader does not hold. The listed effect kinds (timing, resources, termination, information flow, nondeterminism, ordering, idempotency) are a **non-exhaustive prompt**, never a boundary — malice moved one dimension over still fails the counterfactual. +- **Durable context only.** Context counts toward self-documentation only if it travels with the code (same file/module/subtree, or a `@liyi:note`). Context that lives only in the current session, the author's memory, the present set of callers, or a PR discussion does **not** count. If the item is self-evident only to someone holding ephemeral context, it is not self-documenting — promote the premise to a `@liyi:note` (so it *becomes* durable) or write the intent. +- **Side effects are presumptively disqualifying.** `"=self-doc"` is available by default to pure, total items (data types, pure helpers). A side-effecting function *may* still qualify, but only after the agent has explicitly checked it for hidden ordering, non-idempotency, and extrinsic constraints — and found none. + +**Discharge, don't just assert.** Unlike `"=trivial"` (a glance suffices), a `"=self-doc"` claim obliges the agent to have *checked for the disqualifiers* — no observable state change, no order-dependence, no non-idempotency, no cross-call invariant, no non-value-domain effect, no extrinsic constraint it could find. A false `"=self-doc"` is then a lie about work done, not a defensible judgment call — the same standard that stops silent self-approval elsewhere in this design. + +**Threat-model boundary (a documented non-goal).** `"=self-doc"` — and 立意 as a whole — guarantees that *hidden intent cannot be silently filed as visible*: every load-bearing property either lives in reviewed prose or is conspicuously absent. It does **not** guarantee that reviewed prose is *true*. When malice is exported out of the item's observable, co-located surface — a remainder swept to a distant or runtime-controlled constant, masked by a plausible-but-false comment ("virtual sink for internal accounting") — the code matches its stated intent and the deception survives every structural check. Catching that requires a security-versed reasoner with a threat model running *on top of* 立意's routing, and even then is best-effort, prior-dependent flagging, never a guarantee. 立意 surfaces hidden intent; it does not verify the honesty of intent deliberately misrepresented via non-co-located or runtime state. This boundary is where the convention ends and adversarial security review begins. + +**Linter behavior.** `liyi check` treats `"=self-doc"` exactly like `"=trivial"` for tracking purposes: an info-level diagnostic, not stale, not counted as unreviewed, and skipped by the adversarial testing agent (its intent is intrinsic — there is nothing for a second model to attack). `@liyi:nontrivial` in source conflicts with a sidecar `"=self-doc"` the same way it conflicts with `"=trivial"`. Because a `"=self-doc"` claim on a security- or money-sensitive item is exactly the case where all value-domain checks pass yet a constraint hides in an omitted dimension, reviewers should treat `"=self-doc"` on such items as a conspicuous smell to challenge, never a rubber stamp. + +### Derivative items — exclude from inference, do not spec + +Some items carry **no independent intent at all**: their entire purpose is to exercise or assert another already-specced item, adding no invariant of their own. The overwhelming case is tests — a test named `see_none_suppresses_directory_scope` asserts a behavioral claim about `resolve`, so its intent *is* `resolve`'s spec. Inferring a per-item entry for each such test produces sidecar noise, a fake review burden (a reviewer has nothing to approve *on*), a hash-tracking liability (every test edit reads as staleness), and circularity (adversarial tests are *generated from* reviewed intent — specifying the tests' own intent is the pipeline eating its tail). + +The correct treatment is **exclusion from inference, not a lighter spec**: agents should not write a sidecar entry for clearly-derivative forms at all. This targets the derivative *shape* — "an item whose whole purpose is to exercise or assert another specced item, adding no invariant of its own" — not "tests" by name. A test that encodes a *novel* invariant the code does not otherwise state (a property test asserting a business rule) *does* carry independent intent and should be specced normally. The end state for a derivative item is **no entry**; do not reach for `"=trivial"` or `"=self-doc"` to file it away, as both still leave a row to review and rehash. + ### Why two review paths The source-level path (`@liyi:intent`) and the sidecar path (`"reviewed": true`) serve different needs: diff --git a/docs/liyi-design.md.liyi.jsonc b/docs/liyi-design.md.liyi.jsonc index 47c86cd..112f76a 100644 --- a/docs/liyi-design.md.liyi.jsonc +++ b/docs/liyi-design.md.liyi.jsonc @@ -105,8 +105,8 @@ { "requirement": "requirement-discovery-global", "source_span": [ - 1144, - 1146 + 1192, + 1194 ], "source_hash": "sha256:8fdcf5001d02d4622dd7c8a5bd89090119e35cd2aedf7b8d9d7243d5f4cff359", "source_anchor": "" @@ -114,8 +114,8 @@ { "requirement": "liyi-check-exit-code", "source_span": [ - 1269, - 1275 + 1317, + 1323 ], "source_hash": "sha256:2cb6ab36efd4724e3f7e7021bf36233633357e9fddbd1a231bc7b7ed07137d65", "source_anchor": "" @@ -123,8 +123,8 @@ { "requirement": "marker-normalization", "source_span": [ - 1302, - 1304 + 1350, + 1352 ], "source_hash": "sha256:f1786541ac9be4533a36924a80c578a749024a4980fbc323417c2d5640ed6306", "source_anchor": "" @@ -132,8 +132,8 @@ { "requirement": "quine-escape-in-source", "source_span": [ - 1316, - 1318 + 1364, + 1366 ], "source_hash": "sha256:b929750d0d3c9ce24a6f814d7a1db15f2fc03983efa2647cfcf6a7462d2c8b81", "source_anchor": "" @@ -141,8 +141,8 @@ { "requirement": "markdown-fenced-block-skip", "source_span": [ - 1322, - 1330 + 1370, + 1378 ], "source_hash": "sha256:d75bbcedd7757ae66541f99c329df0460e21a6ac02c056e06156d4aacc8dd749", "source_anchor": "" @@ -150,8 +150,8 @@ { "requirement": "fix-never-modifies-human-fields", "source_span": [ - 1539, - 1541 + 1587, + 1589 ], "source_hash": "sha256:2101e1597493adb25b7743b3b2f983f672f967e2e24ae18ef07ece70ccfb76bb", "source_anchor": "" @@ -159,8 +159,8 @@ { "requirement": "fix-semantic-drift-protection", "source_span": [ - 1543, - 1545 + 1591, + 1593 ], "source_hash": "sha256:7c7bcb74ddf40462f9162ada24feeffa60e080adae3f4a69d37dde5fb23ac81b", "source_anchor": "" From 272e7f332ad90aca4e18e3383d544b4acae89e94 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 10:49:49 +0800 Subject: [PATCH 02/44] docs(template): teach agents =self-doc and derivative-item exclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the AGENTS.md intent-spec template (the deliverable downstream repos adopt) to match the design doc. Rule 1 gains the `=self-doc` sentinel with its strict test — a competent-but-domain-stranger reader, durable co-located context only, an open observer counterfactual over non-value-domain effects, side effects presumptively disqualifying, and a discharge (not assert) obligation. Rule 8 gains the derivative-item exclusion (no entry for items whose whole purpose is to exercise another specced item) and extends the `@liyi:nontrivial` conflict to `=self-doc`. The embedded schema `intent` description lists the new sentinel. Original prompt: > [multi-turn discussion, see session] ... then make each change to the spec, > the AGENTS.md edits, and the implementation, in commits. AI-assisted-by: Claude Opus 4.8 (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6d53315..43fc102 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,14 +74,14 @@ See the contributing guide for full details and examples. When writing or modifying code: -1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. +1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. 2. When module-level invariants or governing context are apparent, write a `@liyi:note` 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, and close embedded blocks with `@liyi:end-note`. A note is a **context primitive, not a tracked spec**: it is marker-only — never write it to a `.liyi.jsonc`, never hash it, never add a `"reviewed"` flag. Notes apply to their directory subtree (shadowed by deeper notes); to pull a named note into a specific item's context regardless of location, annotate the item with `@liyi:see `. (Optionally name a note: `@liyi:note `; same-named blocks aggregate.) 3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. 4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), 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. 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`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial`. Honor `@liyi:file language=` as the authoritative language for a file when present (it overrides extension-based detection). +8. Skip items annotated with `@liyi:ignore` or `@liyi:trivial`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial` or `"=self-doc"`. Honor `@liyi:file language=` as the authoritative language for a file when present (it overrides extension-based detection). **Exclude clearly-derivative items from inference entirely** — write no sidecar entry for an item whose whole purpose is to exercise or assert another already-specced item, adding no invariant of its own (chiefly tests: a test named for the behavior it checks derives its intent from that behavior's spec). This targets the derivative *shape*, not "tests" by name — a test that encodes a novel invariant the code does not otherwise state does carry independent intent and should be specced. The end state for a derivative item is no entry; do not reach for `"=trivial"` or `"=self-doc"` to file it away. 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. @@ -151,7 +151,7 @@ Sidecar files must conform to the following JSON Schema. The top-level object ha }, "intent": { "type": "string", - "description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, or '=trivial' meaning the item is trivial and needs no intent spec." + "description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, '=trivial' meaning the item is trivial and needs no intent spec, or '=self-doc' meaning the item is self-documenting (intent intrinsic to its surface form) and needs no intent spec." }, "source_span": { "$ref": "#/$defs/sourceSpan" }, "tree_path": { From 6fe268763c71f42cc3451e7358828160491fd1ca Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 10:56:08 +0800 Subject: [PATCH 03/44] feat(linter): track =self-doc sentinel identically to =trivial Implement the =self-doc sentinel defined in the design doc and template. check_trivial_ignore now recognizes both "=trivial" and "=self-doc" in the sidecar: each emits an info-level Trivial diagnostic, is not flagged stale, is not counted unreviewed on its own, and is skipped by adversarial testing; an in-span @liyi:nontrivial conflicts with either (ConflictingTriviality error), which is what forbids a side-effecting item from being filed as self-documenting. format_github_actions treats =self-doc as a sentinel so its value is not appended as intent prose. The schema intent description lists the new sentinel. A self_doc_sidecar fixture and golden test cover the three cases: a plain enum, a non-trivial-but-self-doc recursive helper (the case =trivial could not honestly cover), and a @liyi:nontrivial conflict on side-effecting remainder logic. The check.rs and diagnostics.rs behavior changes touch two reviewed items (check_trivial_ignore, format_github_actions); their intent text is updated to mention =self-doc, "reviewed" is unset, and source_hash dropped per the staleness rules, leaving them for human re-approval. Original prompt: > [multi-turn discussion, see session] ... then make each change to the spec, > the AGENTS.md edits, and the implementation, in commits. AI-assisted-by: Claude Opus 4.8 (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi/src/check.rs | 19 +++++-- crates/liyi/src/check.rs.liyi.jsonc | 22 ++++---- crates/liyi/src/diagnostics.rs | 2 +- crates/liyi/src/diagnostics.rs.liyi.jsonc | 6 +- .../fixtures/self_doc_sidecar/example.rs | 23 ++++++++ .../self_doc_sidecar/example.rs.liyi.jsonc | 25 +++++++++ crates/liyi/tests/golden.rs | 55 +++++++++++++++++++ schema/liyi.schema.json | 2 +- 8 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 crates/liyi/tests/fixtures/self_doc_sidecar/example.rs create mode 100644 crates/liyi/tests/fixtures/self_doc_sidecar/example.rs.liyi.jsonc diff --git a/crates/liyi/src/check.rs b/crates/liyi/src/check.rs index a6cc8af..3ef310a 100644 --- a/crates/liyi/src/check.rs +++ b/crates/liyi/src/check.rs @@ -1579,7 +1579,8 @@ fn check_review_status( } } -/// Check trivial/ignore source markers and the sidecar `=trivial` sentinel. +/// Check trivial/ignore source markers and the sidecar `=trivial` / +/// `=self-doc` sentinels. fn check_trivial_ignore( file: &Path, label: &str, @@ -1630,8 +1631,12 @@ fn check_trivial_ignore( } } - // Sidecar "=trivial" sentinel - if item.intent == "=trivial" { + // Sidecar "=trivial" / "=self-doc" sentinels — both mark the item as + // needing no behavioral spec (trivial = too small to hide behavior; + // self-doc = intent intrinsic to the surface form) and are tracked + // identically: info-level, not stale, skipped by adversarial testing, + // and in conflict with an in-span @liyi:nontrivial. + if item.intent == "=trivial" || item.intent == "=self-doc" { let has_nontrivial = source_markers.iter().any(|m| { matches!(m, SourceMarker::Nontrivial { line } if *line >= span_start.saturating_sub(1) && *line <= span_end) @@ -1642,8 +1647,10 @@ fn check_trivial_ignore( item_or_req: label.to_string(), kind: DiagnosticKind::ConflictingTriviality, severity: Severity::Error, - message: "\x40liyi:nontrivial in source conflicts with \"=trivial\" in sidecar" - .into(), + message: format!( + "\x40liyi:nontrivial in source conflicts with \"{}\" in sidecar", + item.intent + ), fix_hint: None, fixed: false, span_start: Some(span_start), @@ -1657,7 +1664,7 @@ fn check_trivial_ignore( item_or_req: label.to_string(), kind: DiagnosticKind::Trivial, severity: Severity::Info, - message: "intent \"=trivial\"".into(), + message: format!("intent \"{}\"", item.intent), fix_hint: None, fixed: false, span_start: Some(span_start), diff --git a/crates/liyi/src/check.rs.liyi.jsonc b/crates/liyi/src/check.rs.liyi.jsonc index dac7d7b..8db990c 100644 --- a/crates/liyi/src/check.rs.liyi.jsonc +++ b/crates/liyi/src/check.rs.liyi.jsonc @@ -311,14 +311,14 @@ }, { "item": "check_trivial_ignore", - "reviewed": true, - "intent": "Emit Trivial or Ignored info diagnostics for @liyi:trivial and @liyi:ignore markers within the item's span. For the sidecar =trivial sentinel, emit Trivial info unless @liyi:nontrivial is present in the span, in which case emit ConflictingTriviality error.", + "reviewed": false, + "intent": "Emit Trivial or Ignored info diagnostics for @liyi:trivial and @liyi:ignore markers within the item's span. For the sidecar =trivial and =self-doc sentinels, emit Trivial info unless @liyi:nontrivial is present in the span, in which case emit ConflictingTriviality error. Both sentinels are treated identically for tracking.", "source_span": [ - 1583, - 1670 + 1584, + 1677 ], "tree_path": "fn.check_trivial_ignore", - "source_hash": "sha256:434e82e6c548233c1113313ec013030b349da1f922c4f5e8b9a5c5f2a6f4feb5", + "source_hash": "sha256:5d17d6d16f123247119356ecf46bd318c1fc9971c64575213ca18fd368781bc6", "source_anchor": "fn check_trivial_ignore(" }, { @@ -326,8 +326,8 @@ "reviewed": true, "intent": "Validate existing related edges (emit UnknownRequirement or ReqChanged using computed_hash for cascading staleness), sync source @liyi:related markers into sidecar as MissingRelatedEdge (auto-fix inserts edge with current hash), and fill null hashes on existing edges when --fix is active. Return true if the item was modified.", "source_span": [ - 1675, - 1788 + 1682, + 1795 ], "tree_path": "fn.check_related_edges", "source_hash": "sha256:66f6a7ee8df06a18e9a4e29e19851b775ee70717bc5b01a179afc8738a00aa12", @@ -342,8 +342,8 @@ "reviewed": true, "intent": "=doc", "source_span": [ - 1798, - 1850 + 1805, + 1857 ], "tree_path": "fn.detect_requirement_cycles", "source_hash": "sha256:c7124f27dcaade014657e1b571274470d11511922c508ca2a3836d7df20e808d", @@ -354,8 +354,8 @@ "reviewed": true, "intent": "Read a file's contents with caching: return the cached string if already loaded, otherwise read from disk, store in the cache, and return. Return None on I/O failure.", "source_span": [ - 1853, - 1864 + 1860, + 1871 ], "tree_path": "fn.read_cached", "source_hash": "sha256:c5e98a29f80d683b44e3c883feff6cc1eee817296cfa669df07d5c7175deb94f", diff --git a/crates/liyi/src/diagnostics.rs b/crates/liyi/src/diagnostics.rs index bd875f6..0419427 100644 --- a/crates/liyi/src/diagnostics.rs +++ b/crates/liyi/src/diagnostics.rs @@ -411,7 +411,7 @@ pub fn format_github_actions(d: &Diagnostic, root: &std::path::Path) -> String { // Append intent text to the message when available and non-sentinel. let message = match &d.intent { - Some(intent) if intent != "=doc" && intent != "=trivial" => { + Some(intent) if intent != "=doc" && intent != "=trivial" && intent != "=self-doc" => { format!("{message}%0AIntent: {}", escape(intent)) } _ => message, diff --git a/crates/liyi/src/diagnostics.rs.liyi.jsonc b/crates/liyi/src/diagnostics.rs.liyi.jsonc index 3d37d6a..13566f8 100644 --- a/crates/liyi/src/diagnostics.rs.liyi.jsonc +++ b/crates/liyi/src/diagnostics.rs.liyi.jsonc @@ -107,14 +107,14 @@ }, { "item": "format_github_actions", - "reviewed": true, - "intent": "Format a single diagnostic as a GitHub Actions workflow command (::notice, ::warning, or ::error) with file, line, and title parameters. Maps severity to annotation level, strips the repo root from file paths, escapes %, \\r, \\n per the Actions toolkit spec, uses the item name with a severity icon as the annotation title, and appends the intent text to the message body when present and non-sentinel (not =doc or =trivial).", + "reviewed": false, + "intent": "Format a single diagnostic as a GitHub Actions workflow command (::notice, ::warning, or ::error) with file, line, and title parameters. Maps severity to annotation level, strips the repo root from file paths, escapes %, \\r, \\n per the Actions toolkit spec, uses the item name with a severity icon as the annotation title, and appends the intent text to the message body when present and non-sentinel (not =doc, =trivial, or =self-doc).", "source_span": [ 394, 440 ], "tree_path": "fn.format_github_actions", - "source_hash": "sha256:4b40f8d57fb98a81e056599bccfca5433e9329071c6f3b39d9b521075a191533", + "source_hash": "sha256:b10ff1b5515b6e23424a14058dadff259d21ab8da81b0b28951850064b74434c", "source_anchor": "pub fn format_github_actions(d: &Diagnostic, root: &std::path::Path) -> String {" } ] diff --git a/crates/liyi/tests/fixtures/self_doc_sidecar/example.rs b/crates/liyi/tests/fixtures/self_doc_sidecar/example.rs new file mode 100644 index 0000000..5691328 --- /dev/null +++ b/crates/liyi/tests/fixtures/self_doc_sidecar/example.rs @@ -0,0 +1,23 @@ +pub enum RecoveryMethod { + TreePath, + SiblingScan, + ShiftHeuristic, +} + +fn find_descendant_by_kind(nodes: &[u32], kind: u32) -> Option { + for n in nodes { + if *n == kind { + return Some(*n); + } + } + None +} + +// @liyi:nontrivial +fn settle_remainder(total: u64, shares: &mut [u64]) -> u64 { + let each = total / shares.len() as u64; + for s in shares.iter_mut() { + *s = each; + } + total - each * shares.len() as u64 +} diff --git a/crates/liyi/tests/fixtures/self_doc_sidecar/example.rs.liyi.jsonc b/crates/liyi/tests/fixtures/self_doc_sidecar/example.rs.liyi.jsonc new file mode 100644 index 0000000..ba22002 --- /dev/null +++ b/crates/liyi/tests/fixtures/self_doc_sidecar/example.rs.liyi.jsonc @@ -0,0 +1,25 @@ +// liyi v0.1 spec file +{ + "version": "0.1", + "source": "example.rs", + "specs": [ + { + "item": "RecoveryMethod", + "reviewed": true, + "intent": "=self-doc", + "source_span": [1, 5] + }, + { + "item": "find_descendant_by_kind", + "reviewed": true, + "intent": "=self-doc", + "source_span": [7, 14] + }, + { + "item": "settle_remainder", + "reviewed": true, + "intent": "=self-doc", + "source_span": [17, 23] + } + ] +} diff --git a/crates/liyi/tests/golden.rs b/crates/liyi/tests/golden.rs index 7803e6b..40921bf 100644 --- a/crates/liyi/tests/golden.rs +++ b/crates/liyi/tests/golden.rs @@ -869,6 +869,61 @@ fn trivial_sidecar_sentinel() { assert_eq!(exit_code, LiyiExitCode::CheckFailure); } +// --------------------------------------------------------------------------- +// =self-doc sidecar sentinel tests +// --------------------------------------------------------------------------- + +#[test] +fn self_doc_sidecar_sentinel() { + let (_tmp, root) = fixture_in_tmp("self_doc_sidecar"); + let flags = CheckFlags { + fail_on_stale: false, + fail_on_unreviewed: false, + fail_on_req_changed: false, + fail_on_untracked: false, + }; + + // First pass: fix to fill in source_hash / source_anchor. + let _ = run_check(&root, &[], true, false, &flags); + + // Second pass: check diagnostics. + let (diagnostics, exit_code) = run_check(&root, &[], false, false, &flags); + + // RecoveryMethod: plain enum, =self-doc, no source annotation → Trivial info + let enum_self_doc = diagnostics + .iter() + .any(|d| d.item_or_req == "RecoveryMethod" && matches!(d.kind, DiagnosticKind::Trivial)); + assert!( + enum_self_doc, + "expected Trivial diagnostic for RecoveryMethod (=self-doc intent), got: {diagnostics:#?}" + ); + + // find_descendant_by_kind: non-trivial (has a loop) but self-doc → Trivial info, + // never flagged stale. This is the case =trivial could not honestly cover. + let helper_self_doc = diagnostics.iter().any(|d| { + d.item_or_req == "find_descendant_by_kind" && matches!(d.kind, DiagnosticKind::Trivial) + }); + assert!( + helper_self_doc, + "expected Trivial diagnostic for find_descendant_by_kind (=self-doc intent), got: {diagnostics:#?}" + ); + + // settle_remainder: @liyi:nontrivial in source + =self-doc in sidecar + // → ConflictingTriviality error (side-effecting remainder logic must not + // be filed as self-documenting). + let has_conflict = diagnostics.iter().any(|d| { + d.item_or_req == "settle_remainder" + && matches!(d.kind, DiagnosticKind::ConflictingTriviality) + }); + assert!( + has_conflict, + "expected ConflictingTriviality for settle_remainder, got: {diagnostics:#?}" + ); + + // ConflictingTriviality should cause check failure + assert_eq!(exit_code, LiyiExitCode::CheckFailure); +} + // --------------------------------------------------------------------------- // Init discovery tests // --------------------------------------------------------------------------- diff --git a/schema/liyi.schema.json b/schema/liyi.schema.json index fa9000b..f674400 100644 --- a/schema/liyi.schema.json +++ b/schema/liyi.schema.json @@ -54,7 +54,7 @@ }, "intent": { "type": "string", - "description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, or '=trivial' meaning the item is trivial and needs no intent spec." + "description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, '=trivial' meaning the item is trivial and needs no intent spec, or '=self-doc' meaning the item is self-documenting (intent intrinsic to its surface form) and needs no intent spec." }, "source_span": { "$ref": "#/$defs/sourceSpan" }, "tree_path": { From b170981e57d54e027afe2747cd5fc658161a2c59 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:19:17 +0800 Subject: [PATCH 04/44] docs(linter): reclassify stretched =trivial specs as =self-doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The =self-doc sentinel (added in 5d95f88c31bc and the follow-up commits afb0a8e, f19e585, 38f207b) separates two claims that =trivial previously conflated: a risk claim ("too small to hide behavior") from an epistemic claim ("intent is intrinsic to the surface form"). Nine pre-existing =trivial entries had been stretched onto substantial items — data structs and a context bundle, an enum, and multi-line search helpers — where =trivial was a lie about size and =self-doc is the honest label. Each affected spec sets "reviewed" to false (intent changed, source unchanged), leaving it for human re-approval per the staleness rules. Genuinely tiny items keep =trivial: one-line consts/statics, a one-line function body, and the declarative CI trigger block. Derivative test specs are dropped in a follow-up commit. Original prompt: > please survey the pre-existing `=trivial` intents and change to `=self-doc` or re-infer each, for any such intent no longer conforming to the current revised standard (read `git diff 5d95f88c31bc` for the exact changes). > please commit. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi/src/approve.rs.liyi.jsonc | 8 ++++---- crates/liyi/src/context.rs.liyi.jsonc | 8 ++++---- crates/liyi/src/prompt.rs.liyi.jsonc | 4 ++-- crates/liyi/src/recovery.rs.liyi.jsonc | 4 ++-- crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc | 8 ++++---- crates/liyi/src/tree_path/mod.rs.liyi.jsonc | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/liyi/src/approve.rs.liyi.jsonc b/crates/liyi/src/approve.rs.liyi.jsonc index 8a3a1a9..07348e5 100644 --- a/crates/liyi/src/approve.rs.liyi.jsonc +++ b/crates/liyi/src/approve.rs.liyi.jsonc @@ -101,8 +101,8 @@ }, { "item": "RequirementInfo", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 232, 238 @@ -113,8 +113,8 @@ }, { "item": "CandidateFileCtx", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 240, 250 diff --git a/crates/liyi/src/context.rs.liyi.jsonc b/crates/liyi/src/context.rs.liyi.jsonc index da0625d..f4ccb6c 100644 --- a/crates/liyi/src/context.rs.liyi.jsonc +++ b/crates/liyi/src/context.rs.liyi.jsonc @@ -53,8 +53,8 @@ }, { "item": "ResolvedNote", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 52, 55 @@ -80,8 +80,8 @@ }, { "item": "parent_dir", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 67, 72 diff --git a/crates/liyi/src/prompt.rs.liyi.jsonc b/crates/liyi/src/prompt.rs.liyi.jsonc index bcaa12b..0e55935 100644 --- a/crates/liyi/src/prompt.rs.liyi.jsonc +++ b/crates/liyi/src/prompt.rs.liyi.jsonc @@ -29,8 +29,8 @@ }, { "item": "GroupMeta", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 73, 77 diff --git a/crates/liyi/src/recovery.rs.liyi.jsonc b/crates/liyi/src/recovery.rs.liyi.jsonc index 81ef1c1..b02de2c 100644 --- a/crates/liyi/src/recovery.rs.liyi.jsonc +++ b/crates/liyi/src/recovery.rs.liyi.jsonc @@ -5,8 +5,8 @@ "specs": [ { "item": "RecoveryMethod", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 6, 10 diff --git a/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc b/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc index 9f3e5e0..4bf286f 100644 --- a/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc +++ b/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc @@ -165,8 +165,8 @@ }, { "item": "find_descendant_by_kind", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 222, 234 @@ -177,8 +177,8 @@ }, { "item": "find_flow_scalar", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 237, 250 diff --git a/crates/liyi/src/tree_path/mod.rs.liyi.jsonc b/crates/liyi/src/tree_path/mod.rs.liyi.jsonc index 9f37ef4..4ccc5f5 100644 --- a/crates/liyi/src/tree_path/mod.rs.liyi.jsonc +++ b/crates/liyi/src/tree_path/mod.rs.liyi.jsonc @@ -268,8 +268,8 @@ }, { "item": "find_ancestor_pair", - "reviewed": true, - "intent": "=trivial", + "reviewed": false, + "intent": "=self-doc", "source_span": [ 934, 943 From a187cc309923300bdb3c008c7e0e4c1271aa64a1 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:19:19 +0800 Subject: [PATCH 05/44] docs(linter): drop derivative test specs from context sidecar The revised standard excludes derivative items from inference entirely: an item whose whole purpose is to exercise or assert an already-specced item adds no invariant of its own, so a per-item entry creates a fake review burden, a hash-tracking liability, and circularity (adversarial tests are generated from reviewed intent). Remove the sidecar entries for the context.rs `mod tests` module, its `write` helper, and all nine test functions; the end state for a derivative item is no entry, not a lighter =trivial or =self-doc row. Original prompt: > please survey the pre-existing `=trivial` intents and change to `=self-doc` or re-infer each, for any such intent no longer conforming to the current revised standard (read `git diff 5d95f88c31bc` for the exact changes). > please commit. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi/src/context.rs.liyi.jsonc | 132 -------------------------- 1 file changed, 132 deletions(-) diff --git a/crates/liyi/src/context.rs.liyi.jsonc b/crates/liyi/src/context.rs.liyi.jsonc index f4ccb6c..565fb1f 100644 --- a/crates/liyi/src/context.rs.liyi.jsonc +++ b/crates/liyi/src/context.rs.liyi.jsonc @@ -180,138 +180,6 @@ "tree_path": "fn.render", "source_hash": "sha256:50a5c675ffb1288c9ca19c6557db00549a264bb141cb30c8dfdab21fe667ce38", "source_anchor": "pub fn render(notes: &[ResolvedNote]) -> String {" - }, - { - "item": "tests", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 314, - 477 - ], - "tree_path": "mod.tests", - "source_hash": "sha256:d6b92ac9e4dadc18d78fbe0cc6e4555b592ae99d39d6a6c8eb7e0f449da602e9", - "source_anchor": "mod tests {" - }, - { - "item": "tests::write", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 319, - 325 - ], - "tree_path": "mod.tests::fn.write", - "source_hash": "sha256:82b96522cb4cc15ba6f48d5bbbe23a405de2cd9a807b61a8c94cf04146452d7a", - "source_anchor": " fn write(root: &Path, rel: &str, content: &str) {" - }, - { - "item": "tests::ancestor_dirs_walks_to_root", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 328, - 334 - ], - "tree_path": "mod.tests::fn.ancestor_dirs_walks_to_root", - "source_hash": "sha256:22202a8e9ab3c24a797902e3de7d0373c040e1038af8737e2b188a3f0edaf65f", - "source_anchor": " fn ancestor_dirs_walks_to_root() {" - }, - { - "item": "tests::extract_named_block", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 337, - 344 - ], - "tree_path": "mod.tests::fn.extract_named_block", - "source_hash": "sha256:3195f3031b76e2938ee0007e040b625bbec78be6cebef62b870203e40bfd3288", - "source_anchor": " fn extract_named_block() {" - }, - { - "item": "tests::unclosed_block_extends_to_eof", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 347, - 354 - ], - "tree_path": "mod.tests::fn.unclosed_block_extends_to_eof", - "source_hash": "sha256:85d909eb318bec8693733b629fb69d640486da782fff38c5826160166a1acc33", - "source_anchor": " fn unclosed_block_extends_to_eof() {" - }, - { - "item": "tests::directory_scope_includes_ancestor_note", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 357, - 371 - ], - "tree_path": "mod.tests::fn.directory_scope_includes_ancestor_note", - "source_hash": "sha256:d6e6de7681791f1ce33c9eea85b2ce58686421a6f63ffd3a1840ae667d1d6533", - "source_anchor": " fn directory_scope_includes_ancestor_note() {" - }, - { - "item": "tests::nearer_note_shadows_same_name", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 374, - 393 - ], - "tree_path": "mod.tests::fn.nearer_note_shadows_same_name", - "source_hash": "sha256:bdd669b5072564d6e8b1e65a474552b395ee244468664fbc38f461b87d6f02d6", - "source_anchor": " fn nearer_note_shadows_same_name() {" - }, - { - "item": "tests::see_pulls_remote_note", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 396, - 414 - ], - "tree_path": "mod.tests::fn.see_pulls_remote_note", - "source_hash": "sha256:b6d52e04b45a2c946a5a192bdfac8d281b91e2210bbdd2bc2da6d78322d731ef", - "source_anchor": " fn see_pulls_remote_note() {" - }, - { - "item": "tests::see_none_suppresses_directory_scope", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 417, - 432 - ], - "tree_path": "mod.tests::fn.see_none_suppresses_directory_scope", - "source_hash": "sha256:f94b1e57167a7d9fb302b8fb67d9f86b1000b900a6f55ea0a582e328bf06026c", - "source_anchor": " fn see_none_suppresses_directory_scope() {" - }, - { - "item": "tests::see_survives_opt_out", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 435, - 458 - ], - "tree_path": "mod.tests::fn.see_survives_opt_out", - "source_hash": "sha256:7155f36a1e3bbef73f93e06d3494988760883eeed7c8a2e7a2d2415b0a851dc0", - "source_anchor": " fn see_survives_opt_out() {" - }, - { - "item": "tests::unmarked_readme_is_invisible", - "reviewed": true, - "intent": "=trivial", - "source_span": [ - 461, - 476 - ], - "tree_path": "mod.tests::fn.unmarked_readme_is_invisible", - "source_hash": "sha256:1d90fe171cb951bbae9616ed448af93d6eab2fbb306cffa16324d61ff6c300e4", - "source_anchor": " fn unmarked_readme_is_invisible() {" } ] } From c2ec28ecf6f8063954da63f798b19fbc9ea7a72a Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:33:09 +0800 Subject: [PATCH 06/44] docs(design): treat sentinel specs as reviewed and staleness-tracked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two "Linter behavior" paragraphs claimed sidecar `=trivial`/`=self-doc` specs are "not stale" and "not counted as unreviewed". Both claims are false: `check_item_hash` and `check_review_status` have no sentinel case, so a sentinel spec is an ordinary tracked spec — unsetting `reviewed` surfaces it as unreviewed, and a source change marks it stale. The rest of the design already agrees (the reviewer-responsibility table gives a review action per sentinel, `=doc` is explicitly staleness-tracked, and the level `all` summary lists trivial/unreviewed/stale together). Correct the doc, not the code: everything gets reviewed, sentinels included. Original prompt: > actually isn't the point "every intent should get reviewed" i.e. "trivial/self-doc sentinels should be reviewed too"? the doc has a table describing reviewer responsibilities -- what to prove when encountering different sentinels, so the implication has always been that they are to human-approve EVERYTHING. so any text implying otherwise should get corrected. and regarding triviality of "single expressions", maybe modify to "simple expressions", to disqualify complex ones like `machine == EM_FOO && (bar_count <= X || should_baz && quux (state->xxx, yyy) && ...`. what do you think? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/liyi-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 1b834b3..4b93534 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1006,7 +1006,7 @@ Meaning: “I evaluated this item and it’s trivial — small enough to carry n The second case matters for the scaffold workflow (see *Tree-sitter item discovery in scaffold* below): `liyi init` pre-populates every discovered item, the agent judges triviality and writes `"=trivial"` instead of deleting the entry. The full inventory is preserved — reviewers can see what was evaluated and judged trivial, not just what was specced. -**Linter behavior.** `liyi check` treats `"intent": "=trivial"` like `@liyi:trivial` in source — emits an info-level `Trivial` diagnostic, does not flag as stale, does not count the item as unreviewed. The adversarial testing agent skips items with `"=trivial"` (same as `@liyi:trivial`). +**Linter behavior.** A sidecar `"intent": "=trivial"` is an ordinary tracked spec: `liyi check` emits an info-level `Trivial` diagnostic, and the item is still reviewed and staleness-tracked like any other — a human approves the triviality claim, and any source change re-opens it. Only the source `@liyi:trivial` marker is lighter, because it implies no spec exists to review. The adversarial testing agent skips items with `"=trivial"` (same as `@liyi:trivial`). **Review interaction.** `liyi approve` displays `=trivial` items with a “trivial” tag. A reviewer who disagrees can override — clearing `"=trivial"` and either writing explicit intent or adding `@liyi:nontrivial` in source to prevent the agent from re-classifying. @@ -1049,7 +1049,7 @@ An item can be non-trivial yet self-documenting (a recursive search), or trivial **Threat-model boundary (a documented non-goal).** `"=self-doc"` — and 立意 as a whole — guarantees that *hidden intent cannot be silently filed as visible*: every load-bearing property either lives in reviewed prose or is conspicuously absent. It does **not** guarantee that reviewed prose is *true*. When malice is exported out of the item's observable, co-located surface — a remainder swept to a distant or runtime-controlled constant, masked by a plausible-but-false comment ("virtual sink for internal accounting") — the code matches its stated intent and the deception survives every structural check. Catching that requires a security-versed reasoner with a threat model running *on top of* 立意's routing, and even then is best-effort, prior-dependent flagging, never a guarantee. 立意 surfaces hidden intent; it does not verify the honesty of intent deliberately misrepresented via non-co-located or runtime state. This boundary is where the convention ends and adversarial security review begins. -**Linter behavior.** `liyi check` treats `"=self-doc"` exactly like `"=trivial"` for tracking purposes: an info-level diagnostic, not stale, not counted as unreviewed, and skipped by the adversarial testing agent (its intent is intrinsic — there is nothing for a second model to attack). `@liyi:nontrivial` in source conflicts with a sidecar `"=self-doc"` the same way it conflicts with `"=trivial"`. Because a `"=self-doc"` claim on a security- or money-sensitive item is exactly the case where all value-domain checks pass yet a constraint hides in an omitted dimension, reviewers should treat `"=self-doc"` on such items as a conspicuous smell to challenge, never a rubber stamp. +**Linter behavior.** `liyi check` treats `"=self-doc"` exactly like `"=trivial"` for tracking purposes: an info-level diagnostic on a spec that is still human-reviewed and staleness-tracked like any other (a source change re-opens the self-doc claim for re-approval), and it is skipped by the adversarial testing agent (its intent is intrinsic — there is nothing for a second model to attack). `@liyi:nontrivial` in source conflicts with a sidecar `"=self-doc"` the same way it conflicts with `"=trivial"`. Because a `"=self-doc"` claim on a security- or money-sensitive item is exactly the case where all value-domain checks pass yet a constraint hides in an omitted dimension, reviewers should treat `"=self-doc"` on such items as a conspicuous smell to challenge, never a rubber stamp. ### Derivative items — exclude from inference, do not spec From 00661de29bd4675841f5042bd46c83bf47386528 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:33:21 +0800 Subject: [PATCH 07/44] docs(design): define =trivial as a claim about simple expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "=trivial" is a risk claim (too small to hide behavior), but the docs defined the boundary only as "simple getters, one-line wrappers" — which conflates size with syntax. A body that is a single expression is not automatically trivial: a compound boolean guard whose operand order, short-circuit evaluation, nested calls, or cost are load-bearing hides real behavior and belongs in prose. State that a body must be a *simple* expression, not merely one expression, and note that body line count is a heuristic rather than the criterion. Also fix the `@liyi:trivial` marker definition, which described the =self-doc epistemic claim ("intent is self-evident from its signature") rather than the =trivial risk claim. Original prompt: > actually isn't the point "every intent should get reviewed" i.e. "trivial/self-doc sentinels should be reviewed too"? the doc has a table describing reviewer responsibilities -- what to prove when encountering different sentinels, so the implication has always been that they are to human-approve EVERYTHING. so any text implying otherwise should get corrected. and regarding triviality of "single expressions", maybe modify to "simple expressions", to disqualify complex ones like `machine == EM_FOO && (bar_count <= X || should_baz && quux (state->xxx, yyy) && ...`. what do you think? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- docs/liyi-design.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 43fc102..5a2aaf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,7 @@ See the contributing guide for full details and examples. When writing or modifying code: -1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. +1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. 2. When module-level invariants or governing context are apparent, write a `@liyi:note` 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, and close embedded blocks with `@liyi:end-note`. A note is a **context primitive, not a tracked spec**: it is marker-only — never write it to a `.liyi.jsonc`, never hash it, never add a `"reviewed"` flag. Notes apply to their directory subtree (shadowed by deeper notes); to pull a named note into a specific item's context regardless of location, annotate the item with `@liyi:see `. (Optionally name a note: `@liyi:note `; same-named blocks aggregate.) 3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. 4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), ensure it has a corresponding entry in the co-located `.liyi.jsonc` with `"requirement"` and `"source_span"`. (The tool fills in `"source_hash"`.) diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 4b93534..5f58f0d 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -995,7 +995,7 @@ The `@liyi:trivial` source annotation tells agents and the linter to skip an ite Meaning: “I evaluated this item and it’s trivial — small enough to carry no hidden behavior, so a spec would add no value.” -`"=trivial"` is a **risk claim**: the item is too small and simple for anything to go subtly wrong, so a reviewer proves triviality by reading the body. It is *not* a claim that the item is self-evident despite being non-trivial — that is what `"=self-doc"` is for (see below). Do not stretch `"=trivial"` to cover substantial code whose intent happens to be recoverable from its surface form. +`"=trivial"` is a **risk claim**: the item is too small and simple for anything to go subtly wrong, so a reviewer proves triviality by reading the body. It is *not* a claim that the item is self-evident despite being non-trivial — that is what `"=self-doc"` is for (see below). Do not stretch `"=trivial"` to cover substantial code whose intent happens to be recoverable from its surface form. "Simple" means more than line count: a body that is a single expression is still not trivial if its operand order, short-circuit evaluation, nested calls, or cost are load-bearing — a compound boolean guard with side-effecting operands belongs in prose. **When to use which:** @@ -1112,7 +1112,7 @@ Every reviewed item that uses `@liyi:intent` gets a comment annotation. For a fi Three annotations, on the line before the item definition: - **`@liyi:ignore`** — the function is deliberately excluded from the convention. Don’t infer intent, don’t report it. Use for internal helpers, legacy functions that won’t be touched. -- **`@liyi:trivial`** — the function’s intent is self-evident from its signature. A spec would add no value. Use for simple getters, setters, one-line wrappers. Applied by the agent during inference. +- **`@liyi:trivial`** — the item is too small to carry hidden behavior; a spec would add no value. Use for simple getters/setters, one-line wrappers, and *simple* expressions only — a body is not trivial if its operand order, short-circuit evaluation, nested calls, or cost are load-bearing. Applied by the agent during inference. - **`@liyi:nontrivial`** — a human override: “this looks trivial but I want a spec.” The agent must infer a spec and not re-classify as trivial. The linter treats it the same as an unannotated item. ```python @@ -1672,7 +1672,7 @@ The full AGENTS.md section is ~300 lines: 10 behavioral rules (the part a human ## 立意 (Intent Specs) When writing or modifying code: -1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. +1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. 2. When module-level invariants or governing context are apparent, write a `@liyi:note` block — in the directory's existing module doc (`README.md`, `doc.go`, `mod.rs` doc comment, etc.) or in a dedicated `LIYI.md`, closing embedded blocks with `@liyi:end-note`. A note is a context primitive, not a tracked spec: marker-only, never written to a `.liyi.jsonc` or hashed. Pull a named note into a specific item's context with `@liyi:see `. 3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. 4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), ensure it has a corresponding entry in the co-located `.liyi.jsonc` with `"requirement"` and `"source_span"`. (The tool fills in `"source_hash"`.) @@ -1885,7 +1885,7 @@ The scaffold uses two deterministic AST signals to help the agent prioritize whi Items with doc comments get `"_has_doc": true` in their `_hints`, signaling the agent that `"intent": "=doc"` may be appropriate if the docstring contains behavioral requirements. -**2. Item size (body line count).** Available directly from `source_span`: `end - start + 1`. Small items (≤ 3–5 lines of body, depending on language idiom) are likely trivial — simple getters, one-line wrappers, delegating constructors. The scaffold emits `"_body_lines": N` in hints. Items below a threshold get `"_likely_trivial": true`. +**2. Item size (body line count).** Available directly from `source_span`: `end - start + 1`. Small items (≤ 3–5 lines of body, depending on language idiom) are likely trivial — simple getters, one-line wrappers, delegating constructors, and simple expressions. This is a heuristic, not the criterion: a single dense expression can still be non-trivial. The scaffold emits `"_body_lines": N` in hints. Items below a threshold get `"_likely_trivial": true`. **Combined heuristic:** From c43279d9f6707573b311a8f42119847f1e98d8fd Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:33:26 +0800 Subject: [PATCH 08/44] docs(design): drop test module from =self-doc examples The =self-doc section cited "the 163-line test module" as its canonical example of a substantial-but-self-evident item. That contradicts the derivative-item rule added in the same change: a test whose whole purpose is to exercise or assert an already-specced item gets no sidecar entry at all, so it is never filed as =self-doc. Replace the example with a data-only enum (RecoveryMethod), which =self-doc legitimately covers. Original prompt: > actually isn't the point "every intent should get reviewed" i.e. "trivial/self-doc sentinels should be reviewed too"? the doc has a table describing reviewer responsibilities -- what to prove when encountering different sentinels, so the implication has always been that they are to human-approve EVERYTHING. so any text implying otherwise should get corrected. and regarding triviality of "single expressions", maybe modify to "simple expressions", to disqualify complex ones like `machine == EM_FOO && (bar_count <= X || should_baz && quux (state->xxx, yyy) && ...`. what do you think? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/liyi-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 5f58f0d..319683e 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1014,7 +1014,7 @@ The second case matters for the scaffold workflow (see *Tree-sitter item discove ### `"=self-doc"` in the sidecar — the self-documenting sentinel -`"=trivial"` answers "is this small enough that nothing can hide?" — a *risk* claim about size. But some items are **not** small yet still carry no intent a spec could usefully add: a plain data struct whose fields *are* its meaning, a recursive tree-search whose name fully states its job, a container module whose purpose is the sum of its members. Forcing prose onto these produces a tautology; marking them `"=trivial"` is a lie about their size (the 11-line context struct, the 163-line test module). `"=self-doc"` is the honest label for this class: +`"=trivial"` answers "is this small enough that nothing can hide?" — a *risk* claim about size. But some items are **not** small yet still carry no intent a spec could usefully add: a plain data struct whose fields *are* its meaning, a recursive tree-search whose name fully states its job, a container module whose purpose is the sum of its members. Forcing prose onto these produces a tautology; marking them `"=trivial"` is a lie about their size (the 11-line context struct, a data-only enum like `RecoveryMethod`). `"=self-doc"` is the honest label for this class: ```jsonc { From 0149cbf621e6e7448254ee6f578a3384d23a0ae6 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:33:32 +0800 Subject: [PATCH 09/44] docs(template): exclude sentinels from adversarial test generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rule 6 authorized adversarial test generation from any item with `"reviewed": true`. Once sentinel specs are properly reviewed (they are ordinary tracked specs — only their attack surface differs), that clause would sweep `=trivial`/`=self-doc` items into test generation, contradicting the design's "skipped by the adversarial testing agent". State the exclusion explicitly: sentinels carry no agent-authored intent for a second model to attack. Applied to the AGENTS.md template and the design doc's minimal-instruction mirror. Original prompt: > actually isn't the point "every intent should get reviewed" i.e. "trivial/self-doc sentinels should be reviewed too"? the doc has a table describing reviewer responsibilities -- what to prove when encountering different sentinels, so the implication has always been that they are to human-approve EVERYTHING. so any text implying otherwise should get corrected. and regarding triviality of "single expressions", maybe modify to "simple expressions", to disqualify complex ones like `machine == EM_FOO && (bar_count <= X || should_baz && quux (state->xxx, yyy) && ...`. what do you think? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- docs/liyi-design.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5a2aaf3..e068762 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,7 @@ When writing or modifying code: 3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. 4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), 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. 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. +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), excluding sentinel intents (`=trivial`, `=self-doc`), which carry no agent-authored intent for a second model to attack. 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`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial` or `"=self-doc"`. Honor `@liyi:file language=` as the authoritative language for a file when present (it overrides extension-based detection). **Exclude clearly-derivative items from inference entirely** — write no sidecar entry for an item whose whole purpose is to exercise or assert another already-specced item, adding no invariant of its own (chiefly tests: a test named for the behavior it checks derives its intent from that behavior's spec). This targets the derivative *shape*, not "tests" by name — a test that encodes a novel invariant the code does not otherwise state does carry independent intent and should be specced. The end state for a derivative item is no entry; do not reach for `"=trivial"` or `"=self-doc"` to file it away. 9. Use a different model for test generation than the one that wrote the code, when possible. diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 319683e..55c6f98 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1677,7 +1677,7 @@ When writing or modifying code: 3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. 4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), 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. 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. +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), excluding sentinel intents (`=trivial`, `=self-doc`), which carry no agent-authored intent for a second model to attack. 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`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial`. Honor `@liyi:file language=` as the authoritative language for a file when present. 9. Use a different model for test generation than the one that wrote the code, when possible. From 45b564deb69066103c31ccbc2c247a8fb40c6ec9 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:33:42 +0800 Subject: [PATCH 10/44] docs(template): add rule-conflict escalation to agent instructions Agents can hit genuine conflicts between normative rules (or ambiguities the convention leaves open) where guessing silently produces the wrong deliverable. Add an explicit escalation preference: name the conflict, don't guess; ask once with candidate resolutions if a human is reachable; otherwise take the conservative reading, leave affected specs unreviewed, and surface the conflict. Scope it so it is not a license to stall on judgment the standard delegates (e.g. the =self-doc observer test). Added to the AGENTS.md template and the design doc's agent-skill section, both after the tracked requirement blocks. Original prompt: > okay, and do you think it would be worth it to add conflict resolution preference (when several liyi agent rules appear to be in conflict, don't ever guess, ask the user) and whatever else to the AGENTS.md instructions, per your experience in this session? > ok, make the changes, batched by semantic change i.e. each commit makes one semantic change to the docs & related code. the conflict rule belongs to its own commit as well. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 9 +++++++++ docs/liyi-design.md | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index e068762..6ba7a5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,6 +90,15 @@ When writing or modifying code: In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve --rehash` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. 11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. +### Resolving rule conflicts + +When two normative instructions here appear to conflict, or a task's correct completion turns on an ambiguity these instructions do not resolve: + +1. **Do not guess.** Name the conflicting rules (and any design-doc sections they reference). +2. **If a human is reachable:** ask once — state the candidate resolutions and your recommendation. Consolidate into a single question rather than a series. +3. **If no human is reachable** (batch, CI): take the most conservative reading, leave the affected specs `"reviewed": false`, complete the unambiguous parts, and report the unresolved conflict prominently. +4. **Do not over-apply this.** Where the standard delegates judgment to you — e.g. whether an item qualifies for `=self-doc` under the observer test — reason it out and state your reasoning so a human can veto. That is not a conflict. + ### `.liyi.jsonc` Schema (v0.1) Sidecar files must conform to the following JSON Schema. The top-level object has three required fields: `"version"` (must be `"0.1"`), `"source"` (repo-relative path to the source file), and `"specs"` (array of item or requirement entries). Each spec entry is either an **item spec** or a **requirement spec**, distinguished by the presence of `"item"` vs `"requirement"`. diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 55c6f98..722946b 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1694,6 +1694,10 @@ When writing or modifying code: - **Model diversity.** Different model for tests than for code, when possible. - **Never modify source code logic** during the protocol. Only create/update `.liyi.jsonc` files, `@liyi:note` blocks (in docs or doc comments), test files, and annotation comments (`@liyi:trivial`, `@liyi:ignore`, `@liyi:requirement`, `@liyi:related`). Annotation comments are metadata, not logic — adding them does not change program behavior. +### Resolving rule conflicts + +When two normative rules appear to conflict, or a task's correct completion turns on an ambiguity the convention does not resolve, the agent must not guess. If a human is reachable, name the conflicting rules and ask once, presenting the candidate resolutions and a recommendation. If no human is reachable (batch or CI), take the most conservative reading, leave the affected specs `"reviewed": false`, finish the unambiguous parts, and surface the conflict prominently. This is not a license to stall on judgment the standard explicitly delegates — such as applying the `=self-doc` observer counterfactual — which the agent exercises and states outright for the human to veto. + ### The cognitive load inversion: tool-guided agents The 11 behavioral rules assume the agent internalizes a complex protocol and executes it reliably. Frontier models (Claude Opus, GPT-4.5) do this well. Budget models (Kimi K2.5, Gemini Flash, GPT-4o-mini) routinely forget steps — dropping AIGC trailers, skipping sidecar updates, ignoring `.liyiignore`. This is not a hypothetical concern; it is empirically observed in production use. From 1bd5146476a63b46c62911f80429d941faa8d40b Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:48:00 +0800 Subject: [PATCH 11/44] fix(template): correct nonexistent liyi approve flag Rule 10 told agents that after unsetting `reviewed` and deleting `source_hash`, the human would run `liyi check --fix` or `liyi approve --rehash`. There is no `--rehash` flag: the `approve` subcommand only defines `--yes`, `--dry-run`, `--item`, `--unreviewed-only`, `--stale-only`, and `--req-only`, and the design doc's `liyi approve` flag list has no such option either. `liyi approve` already rehashes a stale-reviewed item when the human confirms it, so drop the bogus flag and point at the real command. Root cause: the never-compute-hashes paragraph was written directly into AGENTS.md without a matching design-doc review, so the invented flag was never checked against the CLI. Original prompt: > check if the current `liyi:template` section in `AGENTS.md` differs > from `liyi-design.md` prescription, fix `AGENTS.md` and commit if any > inconsistency is found. > Bidirectional sync -- fix `--rehash` in a commit, then update the > design doc in another syncing commit. I was implying the design doc > leads AGENTS.md but apparently this is false -- fixing the trailing doc > is the right call. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 6ba7a5f..18da007 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,7 +87,7 @@ When writing or modifying code: - **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-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. - In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve --rehash` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. + In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. 11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. ### Resolving rule conflicts From f934f38a93f023d1ecadc0d1017d713d151a9a89 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:48:41 +0800 Subject: [PATCH 12/44] docs(design): sync agent-skill rules and schema with AGENTS.md The design doc reproduces the AGENTS.md behavioral rules and carries the canonical `.liyi.jsonc` JSON Schema, but both had fallen behind the template: - Reproduced rules 1, 2, 8, 10, and 11 were the pre-`=self-doc` text. Rule 1 lacked the `=self-doc` sentinel and its observer test plus the reviewed-unset warning; rule 8 lacked the `@liyi:nontrivial` / `"=self-doc"` conflict and the derivative-item exclusion; rule 10 lacked the never-compute-hashes paragraph; rule 11 still said "consume the `--prompt` output" instead of "prefer `--prompt` for structured JSON". Rule 2 was the pre-`@liyi:note` v0.2 wording. - The Appendix schema's `intent` description enumerated only `=doc` and `=trivial`, omitting `=self-doc`, so it disagreed with both the template's embedded schema and `schema/liyi.schema.json`. Root cause: later changes landed in AGENTS.md (rule 1/8 in f19e585, the never-compute-hashes paragraph in c4fb25c, rule 11 in 72db3ff) and in `schema/liyi.schema.json` (38f207b) without the design doc's reproduced copies being updated, because only the changelog narrative was treated as the spec while the reproduced rules and appendix were not. Original prompt: > check if the current `liyi:template` section in `AGENTS.md` differs > from `liyi-design.md` prescription, fix `AGENTS.md` and commit if any > inconsistency is found. > Bidirectional sync -- fix `--rehash` in a commit, then update the > design doc in another syncing commit. I was implying the design doc > leads AGENTS.md but apparently this is false -- fixing the trailing doc > is the right call. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/liyi-design.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 722946b..53e8b10 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1672,19 +1672,21 @@ The full AGENTS.md section is ~300 lines: 10 behavioral rules (the part a human ## 立意 (Intent Specs) When writing or modifying code: -1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. -2. When module-level invariants or governing context are apparent, write a `@liyi:note` block — in the directory's existing module doc (`README.md`, `doc.go`, `mod.rs` doc comment, etc.) or in a dedicated `LIYI.md`, closing embedded blocks with `@liyi:end-note`. A note is a context primitive, not a tracked spec: marker-only, never written to a `.liyi.jsonc` or hashed. Pull a named note into a specific item's context with `@liyi:see `. +1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. +2. When module-level invariants or governing context are apparent, write a `@liyi:note` 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, and close embedded blocks with `@liyi:end-note`. A note is a **context primitive, not a tracked spec**: it is marker-only — never write it to a `.liyi.jsonc`, never hash it, never add a `"reviewed"` flag. Notes apply to their directory subtree (shadowed by deeper notes); to pull a named note into a specific item's context regardless of location, annotate the item with `@liyi:see `. (Optionally name a note: `@liyi:note `; same-named blocks aggregate.) 3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. 4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), 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. 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), excluding sentinel intents (`=trivial`, `=self-doc`), which carry no agent-authored intent for a second model to attack. 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`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial`. Honor `@liyi:file language=` as the authoritative language for a file when present. +8. Skip items annotated with `@liyi:ignore` or `@liyi:trivial`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial` or `"=self-doc"`. Honor `@liyi:file language=` as the authoritative language for a file when present (it overrides extension-based detection). **Exclude clearly-derivative items from inference entirely** — write no sidecar entry for an item whose whole purpose is to exercise or assert another already-specced item, adding no invariant of its own (chiefly tests: a test named for the behavior it checks derives its intent from that behavior's spec). This targets the derivative *shape*, not "tests" by name — a test that encodes a novel invariant the code does not otherwise state does carry independent intent and should be specced. The end state for a derivative item is no entry; do not reach for `"=trivial"` or `"=self-doc"` to file it away. 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-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. When running in agent mode, consume the `liyi check --prompt` output and apply its instructions. Do not commit with unresolved coverage gaps — CI will reject it. + + In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. +11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. ``` ### Key principles @@ -2724,7 +2726,7 @@ The agent re-infers (updating `source_span`; the tool recomputes `source_hash`), }, "intent": { "type": "string", - "description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, or '=trivial' meaning the item is trivial and needs no intent spec." + "description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, '=trivial' meaning the item is trivial and needs no intent spec, or '=self-doc' meaning the item is self-documenting (intent intrinsic to its surface form) and needs no intent spec." }, "source_span": { "$ref": "#/$defs/sourceSpan" }, "tree_path": { From 98b73eba713f40d4b5734f13d0612c54b700de0c Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:51:21 +0800 Subject: [PATCH 13/44] docs(design): stop hard-coding the behavioral-rule count The design doc described the AGENTS.md payload as "10 behavioral rules" in four places, but the reproduced instruction has eleven rules and the same document elsewhere says "the 11 behavioral rules" / "the full 11-rule protocol". The count is incidental, so drop the exact numbers rather than renumber (and rewrite the MUST/SHOULD split's "these 3 things" / "these 8 things" to avoid implying a count). Original prompt: > please fix the rule count mismatch (preferably don't spell out exact > numbers if at all possible), and also the "key principles" -- make the > subtitle more specific, then sync it to AGENTS.md. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/liyi-design.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 53e8b10..42f34dd 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1666,7 +1666,7 @@ The agent instructions define the protocol. The CI linter enforces it. ### Minimal instruction (for any AGENTS.md) -The full AGENTS.md section is ~300 lines: 10 behavioral rules (the part a human reads once) followed by two JSON schemas (machine-consumed reference that the agent uses to produce valid `.liyi.jsonc` and `triage.json` files). The schemas are not adoption cost — no human needs to memorize them — but they are part of the payload. +The full AGENTS.md section is ~300 lines: the behavioral rules (the part a human reads once) followed by two JSON schemas (machine-consumed reference that the agent uses to produce valid `.liyi.jsonc` and `triage.json` files). The schemas are not adoption cost — no human needs to memorize them — but they are part of the payload. ```markdown ## 立意 (Intent Specs) @@ -1702,7 +1702,7 @@ When two normative rules appear to conflict, or a task's correct completion turn ### The cognitive load inversion: tool-guided agents -The 11 behavioral rules assume the agent internalizes a complex protocol and executes it reliably. Frontier models (Claude Opus, GPT-4.5) do this well. Budget models (Kimi K2.5, Gemini Flash, GPT-4o-mini) routinely forget steps — dropping AIGC trailers, skipping sidecar updates, ignoring `.liyiignore`. This is not a hypothetical concern; it is empirically observed in production use. +The behavioral rules assume the agent internalizes a complex protocol and executes it reliably. Frontier models (Claude Opus, GPT-4.5) do this well. Budget models (Kimi K2.5, Gemini Flash, GPT-4o-mini) routinely forget steps — dropping AIGC trailers, skipping sidecar updates, ignoring `.liyiignore`. This is not a hypothetical concern; it is empirically observed in production use. The conventional response is "write better instructions." This is necessary but insufficient. The deeper architectural response is to **move protocol knowledge from the agent's context window into the tool's output**, so that the agent's job shifts from "internalize and follow rules" to "run command, parse output, execute instructions." @@ -1844,7 +1844,7 @@ liyi init # scaffold AGENTS.md with the 立意 instruction paragrap liyi init --force # overwrite existing AGENTS.md ``` -Appends the 立意 agent instruction section (~300 lines: 10 rules + two JSON schemas) to `AGENTS.md` (creates the file if absent). Does not overwrite existing content unless `--force` is given. +Appends the 立意 agent instruction section (~300 lines: behavioral rules + two JSON schemas) to `AGENTS.md` (creates the file if absent). Does not overwrite existing content unless `--force` is given. **File initialization:** @@ -2205,7 +2205,7 @@ This section estimates the effort to *build* 立意 itself — the linter, the c - A **spec convention** — `@liyi:note` blocks (module intent) + `@liyi:requirement` blocks (named requirements) + `.liyi.jsonc` (item-level intent and requirement tracking, JSONC). - A **dependency model** — `@liyi:related` edges from code items to named requirements, with transitive staleness. - A **triage protocol** (deferred to 0.2.0) — `liyi check --json` provides rich stale-item context; an agent (using whatever model it already has) assesses each item and writes a structured report; `liyi triage --apply` acts on the report. The binary stays deterministic and offline (see *`liyi` is infrastructure; the agent is the brain*). -- **Agent instructions** — 10 behavioral rules + two JSON schemas in AGENTS.md (~300 lines; the schemas are machine-consumed reference, not human-read). +- **Agent instructions** — behavioral rules + two JSON schemas in AGENTS.md (~300 lines; the schemas are machine-consumed reference, not human-read). - A **practice** — establish intent before (or alongside) execution. - A **challenge mechanism** (deferred to 0.2.0) — on-demand semantic verification of code against intent, or intent against requirement, driven by the agent. @@ -2242,7 +2242,7 @@ Each level is independently valuable. Stop wherever the cost outweighs the benef | Level | What you do | What you get | Human cost | |---|---|---|---| -| **0. The instruction** | Copy the 立意 section into AGENTS.md (~300 lines: 10 rules the human reads once, two JSON schemas the agent consumes) | Agent writes `.liyi.jsonc` alongside code. You have a persistent record of what each item is meant to do. | 15 minutes | +| **0. The instruction** | Copy the 立意 section into AGENTS.md (~300 lines: behavioral rules the human reads once, two JSON schemas the agent consumes) | Agent writes `.liyi.jsonc` alongside code. You have a persistent record of what each item is meant to do. | 15 minutes | | **1. The review** | Review inferred intent in PRs — set `"reviewed": true` in sidecar (quick) or add `@liyi:intent` in source (explicit) | The review surface for intent is typically ~10% of the code surface — a few lines of spec per item instead of the full implementation. You catch wrong intent before wrong code gets tested. Careless review undermines adversarial testing quality — see *Why careless review is self-limiting* in the Security Model. | Seconds per item | | **2. The docs** | Add `## 立意` sections to READMEs / doc comments | Module-level invariants are documented, visible in rendered docs, discoverable by agents and humans. This is just good documentation practice. | 5 min per module | | **3. The linter** | Run `liyi check` in CI | Stale specs fail the build. You know which items changed since their intent was written. Deterministic enforcement. | Install a binary | @@ -2518,7 +2518,7 @@ The risk that absorption *prevents* the open convention from thriving — by pul ### 6. Agent reliability spectrum -The project's value proposition relies on agents following the AGENTS.md instruction. Empirical evidence shows a wide reliability spectrum: frontier models (Claude Opus 4.6) follow the full 11-rule protocol with high fidelity; budget models (Kimi K2.5, Gemini Flash) routinely forget AIGC trailers, skip sidecar updates, and ignore edge-case rules. This is not a positioning gap or a tooling gap — it is a fundamental characteristic of the current model landscape. +The project's value proposition relies on agents following the AGENTS.md instruction. Empirical evidence shows a wide reliability spectrum: frontier models (Claude Opus 4.6) follow the full behavioral-rule protocol with high fidelity; budget models (Kimi K2.5, Gemini Flash) routinely forget AIGC trailers, skip sidecar updates, and ignore edge-case rules. This is not a positioning gap or a tooling gap — it is a fundamental characteristic of the current model landscape. **This observation validates the project rather than undermining it.** The entire tooling layer (schema validation, staleness detection, coverage gap enforcement) exists precisely because agent output cannot be blindly trusted — `liyi check` catches what the agent forgets, so unreliable agents make the linter *more* valuable, not less (argued in full in *The cognitive load inversion: tool-guided agents*). @@ -2528,7 +2528,7 @@ The project's value proposition relies on agents following the AGENTS.md instruc 2. **Indirection.** The first actionable instruction is "go read another file" (the contributing guide), which itself says "go read the AIGC policy." Multi-hop retrieval fails silently on weaker models. 3. **Inlined schemas.** ~100 lines of JSON Schema in the middle of the instruction flow wastes context window and confuses models about whether it's an instruction or an example. 4. **Decision trees.** Rule 10 presents a nuanced choice (direct re-inference vs. triage) with criteria that require sophisticated judgment. Budget models need a default path, not a decision matrix. -5. **No minimum viable compliance tier.** All 11 rules read as equally mandatory. There is no separation between "you MUST do these 3 things or CI rejects your commit" and "you SHOULD do these 8 things for best results." +5. **No minimum viable compliance tier.** All the rules read as equally mandatory. There is no separation between "you MUST do the CI-gating essentials or CI rejects your commit" and "you SHOULD do the rest for best results." **The architectural response** is the cognitive load inversion described in *The cognitive load inversion: tool-guided agents*: move protocol knowledge from the agent's context window into the tool's `--prompt` output, and restructure AGENTS.md into tiers (MUST / SHOULD / REFERENCE) that degrade gracefully across model capability. A budget model that only follows the MUST tier — run `liyi check`, add AIGC trailer, use `--prompt` for fix instructions — still produces commits that pass CI. From 27146e93e8463175a17c5c77572edddcffb2339f Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:51:37 +0800 Subject: [PATCH 14/44] docs(template): sync key protocol principles into agent instructions The design doc's `### Key principles` section (adversarial not confirmatory, spec is the referee, model diversity, never modify source logic) had no counterpart in the AGENTS.md template, even though its sibling `### Resolving rule conflicts` was already synced there. Rename the heading to `### Key principles of the intent protocol` so it is specific about what it governs, and copy the section verbatim into the AGENTS.md template between the behavioral rules and `### Resolving rule conflicts`, matching the design doc's ordering. Original prompt: > please fix the rule count mismatch (preferably don't spell out exact > numbers if at all possible), and also the "key principles" -- make the > subtitle more specific, then sync it to AGENTS.md. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 7 +++++++ docs/liyi-design.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 18da007..546806b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,6 +90,13 @@ When writing or modifying code: In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. 11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. +### Key principles of the intent protocol + +- **Adversarial, not confirmatory.** Find bugs, not confirm correctness. +- **Spec is the referee.** If the spec says one thing and the code does another, the test exposes the gap. The human decides who's right. +- **Model diversity.** Different model for tests than for code, when possible. +- **Never modify source code logic** during the protocol. Only create/update `.liyi.jsonc` files, `@liyi:note` blocks (in docs or doc comments), test files, and annotation comments (`@liyi:trivial`, `@liyi:ignore`, `@liyi:requirement`, `@liyi:related`). Annotation comments are metadata, not logic — adding them does not change program behavior. + ### Resolving rule conflicts When two normative instructions here appear to conflict, or a task's correct completion turns on an ambiguity these instructions do not resolve: diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 42f34dd..975af03 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1689,7 +1689,7 @@ When writing or modifying code: 11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. ``` -### Key principles +### Key principles of the intent protocol - **Adversarial, not confirmatory.** Find bugs, not confirm correctness. - **Spec is the referee.** If the spec says one thing and the code does another, the test exposes the gap. The human decides who's right. From 76c3ba1206a78b1decf196132a8c9c32d32f0a2a Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:55:05 +0800 Subject: [PATCH 15/44] build(meta): add template-sync gate to make lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md is the source of truth for the agent instruction — `liyi init` ships the block between the `` and `` markers — while docs/liyi-design.md reproduces the behavioral rules, key principles, and the `.liyi.jsonc` schema appendix. Nothing kept the two copies in sync, so they have silently drifted before: the `=self-doc` sentinel reached the template and `schema/liyi.schema.json` but not the design doc's reproduced rules or appendix. Add scripts/check-template-sync.py, which extracts the reproduced regions from both files and fails with a unified diff when they diverge. The sidecar schema is compared as parsed JSON, so the template's `\u0040` quine escape and the doc's literal `@` are treated as equal. "Resolving rule conflicts" is intentionally skipped (the doc states it as prose, the template as a numbered list) as is the triage schema (no design-doc counterpart). Wire the check into `make lint`, and mirror it in `make lint-short` so the commit gate enforces it. Original prompt: > please add that consistency check to both the commit gate and CI! AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- Makefile | 13 ++- scripts/check-template-sync.py | 168 +++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 scripts/check-template-sync.py diff --git a/Makefile b/Makefile index 9c0d067..6612413 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT -.PHONY: verify lint fmt-check clippy liyi-check test \ +.PHONY: verify lint fmt-check clippy liyi-check template-sync test \ verify-short lint-short test-short # Run every pre-commit gate (lint + tests) in one shot. @@ -14,7 +14,7 @@ verify: lint test @echo "verify: all gates passed" # Run every pre-commit lint gate (matches CI). Ends with a success banner. -lint: fmt-check clippy liyi-check +lint: fmt-check clippy liyi-check template-sync @echo "lint: all gates passed" fmt-check: @@ -27,6 +27,11 @@ clippy: liyi-check: cargo run -p liyi-cli -- check --root . +# Verify docs/liyi-design.md's reproduced agent content matches the +# AGENTS.md template (the source of truth `liyi init` ships downstream). +template-sync: + python3 scripts/check-template-sync.py + test: cargo test --workspace @echo "test: passed" @@ -58,6 +63,10 @@ lint-short: tail -n 20 $$LOG; rm -f $$LOG; \ echo "lint-short: liyi check FAILED — re-run \`make liyi-check\` for the full report"; exit 1; \ fi; \ + if ! python3 scripts/check-template-sync.py >$$LOG 2>&1; then \ + tail -n 20 $$LOG; rm -f $$LOG; \ + echo "lint-short: template-sync FAILED — re-run \`make template-sync\` for the full output"; exit 1; \ + fi; \ rm -f $$LOG; \ echo "lint-short: all gates passed" diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py new file mode 100644 index 0000000..016d217 --- /dev/null +++ b/scripts/check-template-sync.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 OR MIT +"""Fail if docs/liyi-design.md's reproduced agent content drifts from the +portable template in AGENTS.md. + +`liyi init` extracts the block between `` and +`` from AGENTS.md and ships it to downstream +repositories, so AGENTS.md is the source of truth for the agent +instruction. The design doc reproduces parts of that block for human +readers, but nothing else keeps the two copies in sync, and they have +silently diverged before (a sentinel added to the template but not the +doc). This check fails when they drift again. + +Compared regions: + +* the numbered behavioral rules (verbatim), +* the "Key principles of the intent protocol" section (verbatim), +* the `.liyi.jsonc` JSON Schema appendix (compared as parsed JSON, so the + template's `\\u0040` quine escape and the doc's literal `@` agree). + +Intentionally not compared: + +* "Resolving rule conflicts" — the doc states it as prose, the template as + a numbered list; the two are semantically equivalent, not textually + identical. +* the triage report schema — it has no design-doc counterpart. + +Usage: + python3 scripts/check-template-sync.py [ROOT] + +ROOT defaults to the repository root inferred from this script's location. +""" + +from __future__ import annotations + +import difflib +import json +import re +import sys +from pathlib import Path + +TEMPLATE_START = "" +TEMPLATE_END = "" +RULES_INTRO = "When writing or modifying code:" +KEY_PRINCIPLES = "### Key principles of the intent protocol" +MINIMAL_INSTRUCTION = "### Minimal instruction (for any AGENTS.md)" +APPENDIX_HEADING = "## Appendix: JSON Schema for `.liyi.jsonc` (v0.1)" + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def template_block(agents: str) -> str: + """The portable instruction block `liyi init` ships downstream.""" + start = agents.index(TEMPLATE_START) + len(TEMPLATE_START) + end = agents.index(TEMPLATE_END, start) + return agents[start:end] + + +def normalize(text: str) -> str: + """Strip trailing whitespace and leading/trailing blank lines.""" + lines = [line.rstrip() for line in text.strip().splitlines()] + return "\n".join(lines).strip() + + +def rules_region(text: str, stop: str | None = None) -> str: + """The numbered rules, from the intro line up to *stop* (or the end).""" + start = text.index(RULES_INTRO) + len(RULES_INTRO) + end = text.index(stop, start) if stop else len(text) + return normalize(text[start:end]) + + +def section(text: str, heading: str) -> str: + """The body of a level-2/3 section, up to the next same-level heading.""" + start = text.index(heading) + len(heading) + rest = text[start:] + match = re.search(r"\n#{2,3} ", rest) + body = rest[: match.start()] if match else rest + return normalize(body) + + +def fenced(text: str, info: str) -> list[str]: + """Bodies of ``` fenced code blocks (info must match exactly).""" + return re.findall(rf"```{info}\n(.*?)\n```", text, re.DOTALL) + + +def design_rules(design: str) -> str: + after = design.index(MINIMAL_INSTRUCTION) + return rules_region(fenced(design[after:], "markdown")[0]) + + +def design_schema(design: str) -> object: + after = design.index(APPENDIX_HEADING) + return json.loads(fenced(design[after:], "json")[0]) + + +def agents_schema(template: str) -> object: + # The first ```json block in the template is the sidecar schema. + return json.loads(fenced(template, "json")[0]) + + +def diff(name: str, expected: str, actual: str) -> str: + out = [f"## {name}\n"] + out.extend( + difflib.unified_diff( + expected.splitlines(keepends=True), + actual.splitlines(keepends=True), + fromfile="AGENTS.md (source of truth)", + tofile="docs/liyi-design.md (reproduced copy)", + ) + ) + return "".join(out) + + +def main() -> int: + if len(sys.argv) > 1: + root = Path(sys.argv[1]).resolve() + else: + root = Path(__file__).resolve().parent.parent + + agents = read(root / "AGENTS.md") + design = read(root / "docs" / "liyi-design.md") + + try: + template = template_block(agents) + failures: list[str] = [] + + expected_rules = rules_region(template, KEY_PRINCIPLES) + actual_rules = design_rules(design) + if expected_rules != actual_rules: + failures.append(diff("behavioral rules", expected_rules, actual_rules)) + + expected_principles = section(template, KEY_PRINCIPLES) + actual_principles = section(design, KEY_PRINCIPLES) + if expected_principles != actual_principles: + failures.append( + diff("key principles", expected_principles, actual_principles) + ) + + expected_schema = json.dumps(agents_schema(template), indent=2, sort_keys=True) + actual_schema = json.dumps(design_schema(design), indent=2, sort_keys=True) + if expected_schema != actual_schema: + failures.append(diff("sidecar schema", expected_schema, actual_schema)) + except (ValueError, IndexError, json.JSONDecodeError) as exc: + print( + "template-sync: could not locate a reproduced region — " + f"the docs layout changed ({exc}).", + file=sys.stderr, + ) + return 1 + + if failures: + print( + "template-sync: AGENTS.md and docs/liyi-design.md have diverged.\n\n" + + "\n".join(failures) + + "\nAGENTS.md is the source of truth; update the design doc's " + "reproduced copies to match.\n", + end="", + ) + return 1 + + print("template-sync: design doc reproduces the AGENTS.md template") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From dd3f0e17c207bcaf3a091e6a07b2af2ef909a180 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:55:05 +0800 Subject: [PATCH 16/44] docs(meta): document the template-sync gate List the new agent-template sync check among the `make lint` gates in AGENTS.md, CONTRIBUTING.md, and both contributing guides (EN/ZH). Original prompt: > please add that consistency check to both the commit gate and CI! AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 12 ++++++------ CONTRIBUTING.md | 3 ++- docs/contributing-guide.en.md | 12 ++++++------ docs/contributing-guide.zh.md | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 546806b..af0fd47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,12 +20,12 @@ For Rust code: - Conform to official rustfmt style. - Keep free from Clippy lints. -Before committing, run `make lint` (rustfmt check, Clippy, and the linter's -own self-check) and ensure it passes, or `make verify` to also run the test -suite. Both gates abort at the first failure and end with a success banner -(`lint: all gates passed` / `verify: all gates passed`), so you can confirm -success by the final line — no need to capture the exit code or redirect -output to a log. +Before committing, run `make lint` (rustfmt check, Clippy, the linter's own +self-check, and the agent-template sync check) and ensure it passes, or +`make verify` to also run the test suite. Both gates abort at the first +failure and end with a success banner (`lint: all gates passed` / +`verify: all gates passed`), so you can confirm success by the final line — +no need to capture the exit code or redirect output to a log. **If you are an AI agent, prefer the quiet variants `make lint-short` and `make verify-short`.** They run the exact same gates but suppress normal diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a89a04..734c2b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,8 @@ structure, code style, commit conventions, and the AIGC policy. 1. **Open an issue first** for non-trivial changes so we can discuss scope. 2. Keep PRs small and focused — one logical change per PR. 3. Ensure `make verify` passes — it runs the lint gates (rustfmt check, - Clippy, and the project's own linter, which dogfoods itself) plus + Clippy, the project's own linter, which dogfoods itself, and the + agent-template sync check) plus `cargo test --workspace`, aborting at the first failure and ending with a `verify: all gates passed` banner. (`make lint` runs just the lint gates.) AI agents should prefer the quiet `make verify-short` / `make lint-short` diff --git a/docs/contributing-guide.en.md b/docs/contributing-guide.en.md index 6a95f29..ab5c4e0 100644 --- a/docs/contributing-guide.en.md +++ b/docs/contributing-guide.en.md @@ -203,12 +203,12 @@ Before finalizing a commit, verify: For Rust code, additionally verify: -- `make lint` passes (rustfmt check, Clippy, and the linter's own self-check), - or `make verify` to run the lint gates plus the test suite. Both abort at the - first failing gate and print a success banner on completion - (`lint: all gates passed` / `verify: all gates passed`); the banner is the - signal that every gate passed, so there is no need to inspect the exit code - or redirect output to a log. +- `make lint` passes (rustfmt check, Clippy, the linter's own self-check, and + the agent-template sync check), or `make verify` to run the lint gates plus + the test suite. Both abort at the first failing gate and print a success + banner on completion (`lint: all gates passed` / `verify: all gates passed`); + the banner is the signal that every gate passed, so there is no need to + inspect the exit code or redirect output to a log. > **For AI agents:** prefer the quiet variants `make lint-short` and > `make verify-short`. They run the same gates but suppress normal output to diff --git a/docs/contributing-guide.zh.md b/docs/contributing-guide.zh.md index af85011..3910266 100644 --- a/docs/contributing-guide.zh.md +++ b/docs/contributing-guide.zh.md @@ -200,8 +200,8 @@ Signed-off-by: Contributor Name 对于 Rust 代码,请额外检查: -- `make lint` 通过(rustfmt 检查、Clippy 以及 linter 自身的自检),或运行 - `make verify` 在 lint 各项之外再跑测试套件。两者都会在第一个失败的环节中止, +- `make lint` 通过(rustfmt 检查、Clippy、linter 自身的自检,以及 agent 模板同步检查), + 或运行 `make verify` 在 lint 各项之外再跑测试套件。两者都会在第一个失败的环节中止, 并在全部通过时打印成功横幅(`lint: all gates passed` / `verify: all gates passed`);这一行横幅即为全部通过的标志,无需再去查看退出码或把输出重定向到 日志中。 From 903ba958d50715fb113d4bcb10c869c842b3310b Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 11:55:05 +0800 Subject: [PATCH 17/44] ci(meta): run template-sync in CI Add a `template-sync` job that runs scripts/check-template-sync.py, and record the job in the workflow's sidecar so `liyi check` stays green. Original prompt: > please add that consistency check to both the commit gate and CI! AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- .github/workflows/ci.yml | 7 +++++++ .github/workflows/ci.yml.liyi.jsonc | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96b1913..40df811 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,3 +71,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo run -p liyi-cli -- check --root . + + template-sync: + name: Template sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: python3 scripts/check-template-sync.py diff --git a/.github/workflows/ci.yml.liyi.jsonc b/.github/workflows/ci.yml.liyi.jsonc index 0ca1fba..8674886 100644 --- a/.github/workflows/ci.yml.liyi.jsonc +++ b/.github/workflows/ci.yml.liyi.jsonc @@ -86,6 +86,18 @@ "tree_path": "key.jobs::key.\"liyi-check\"", "source_hash": "sha256:991baeb9c5382d8fab9e2c777bd2af27a7c6c9fcc08b64263f7b5963f7ba0909", "source_anchor": " liyi-check:" + }, + { + "item": "jobs::template-sync", + "reviewed": false, + "intent": "Run scripts/check-template-sync.py to verify that docs/liyi-design.md reproduces the AGENTS.md agent template (behavioral rules, key principles, and the .liyi.jsonc schema appendix) without drift. Must fail the PR if any reproduced region diverges from the template.", + "source_span": [ + 75, + 80 + ], + "tree_path": "key.jobs::key.\"template-sync\"", + "source_hash": "sha256:6ef01eceeb294b6c907d6e1da02c1b40d1b557b2fe07a634d6a1f88b2ed9567d", + "source_anchor": " template-sync:" } ] } From 18c0934cf63c0322214ec6de1f4419d6801c20fd Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 12:02:13 +0800 Subject: [PATCH 18/44] refactor(meta): share one liyi:template block between the docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template-sync check had to parse regions (fenced Markdown block, headings) and special-case the JSON schema, because the AGENTS.md template block — behavioral rules, key principles, conflict resolution, and both JSON schemas — was not byte-identical to the design doc's condensation: prose conflicts instead of a list, the sidecar schema in a separate appendix, the triage schema missing, and the marker escaped as `\u0040` in AGENTS.md. Make docs/liyi-design.md carry the same `` block verbatim: replace the inline reproductions with a pointer to the new section and fold the schema appendix into the block (which also brings in the previously absent triage schema). Unescape the marker in AGENTS.md's embedded schema so the blocks match byte-for-byte; it sits in a fenced code block, which the scanner already skips. The check is now a plain string comparison, and `--sync` (surfaced as `make template-sync-fix`) copies the AGENTS.md block over the design doc's, so drift can be fixed instead of just reported. Sidecar consequences of the design-doc edit, resolved in the same commit to keep `liyi check` green: the `version-field-required` requirement's hash changed (its cross-reference now points at the verbatim block), and `validate_version`'s related edge was re-filled with `liyi check --fix`. Original prompt: > are you over-engineering the template check? why not reframe the > design doc so that it also features a `` block, > then we can simply extract, compare and even offer to sync that? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- Makefile | 11 +- crates/liyi/src/schema.rs.liyi.jsonc | 2 +- docs/liyi-design.md | 247 +++++++++++++++++++++++---- docs/liyi-design.md.liyi.jsonc | 2 +- scripts/check-template-sync.py | 198 +++++++-------------- 6 files changed, 286 insertions(+), 176 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index af0fd47..053ea96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,7 +163,7 @@ Sidecar files must conform to the following JSON Schema. The top-level object ha "reviewed": { "type": "boolean", "default": false, - "description": "Optional. Whether a human has reviewed and accepted this intent via sidecar approval. Defaults to false when absent. The linter also considers an item reviewed if \u0040liyi:intent is present in source." + "description": "Optional. Whether a human has reviewed and accepted this intent via sidecar approval. Defaults to false when absent. The linter also considers an item reviewed if @liyi:intent is present in source." }, "intent": { "type": "string", diff --git a/Makefile b/Makefile index 6612413..449dd43 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT -.PHONY: verify lint fmt-check clippy liyi-check template-sync test \ +.PHONY: verify lint fmt-check clippy liyi-check template-sync template-sync-fix test \ verify-short lint-short test-short # Run every pre-commit gate (lint + tests) in one shot. @@ -27,11 +27,16 @@ clippy: liyi-check: cargo run -p liyi-cli -- check --root . -# Verify docs/liyi-design.md's reproduced agent content matches the -# AGENTS.md template (the source of truth `liyi init` ships downstream). +# Verify docs/liyi-design.md carries the same liyi:template block as +# AGENTS.md (the source of truth `liyi init` ships downstream). template-sync: python3 scripts/check-template-sync.py +# Copy the AGENTS.md template block over the design doc's, after a +# template-sync failure. Review the resulting diff before committing. +template-sync-fix: + python3 scripts/check-template-sync.py --sync + test: cargo test --workspace @echo "test: passed" diff --git a/crates/liyi/src/schema.rs.liyi.jsonc b/crates/liyi/src/schema.rs.liyi.jsonc index 6424065..2a0fc3a 100644 --- a/crates/liyi/src/schema.rs.liyi.jsonc +++ b/crates/liyi/src/schema.rs.liyi.jsonc @@ -15,7 +15,7 @@ "source_hash": "sha256:eb13a8e5c419c2fd0a1bc441fe4eaa5b8cd52ec411cb425c9fb0ca622d5ee96c", "source_anchor": "pub fn validate_version(version: &str) -> Result<(), String> {", "related": { - "version-field-required": "sha256:361fd9e237617e9cf0e80df99dd31b06274eca55e15e33384fb4a8f745d07fb9" + "version-field-required": "sha256:74f718a44797a3d0c2b81b4ccdd12e8ad488a0df6890015ad457daca802c5dd1" } }, { diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 975af03..c90fac4 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -404,7 +404,7 @@ The `source` path is relative to the repository root — the same path you'd pas `source_hash`, `source_anchor`, and `tree_path` are tool-managed — the agent writes only `source_span` and the tool fills in the rest (see *Per-item staleness* and *Structural identity via `tree_path`* below). Agents MAY write `tree_path` if they can infer the AST path, but the tool will overwrite it with the canonical form on the next `liyi check --fix`. `"intent": "=doc"` is a reserved sentinel meaning "the docstring already captures intent" — the agent uses it when the source docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary (see *`"=doc"` in the sidecar* below). -`"version"` is required. The linter checks it and rejects unknown versions. This costs nothing now and prevents painful migration when the schema evolves (e.g., adding `"related"` edges, or deferred structured fields). A JSON Schema definition ships alongside the linter for editor validation and autocompletion (see *Appendix: JSON Schema* below). When the schema changes, the linter will accept both `"0.1"` and the new version during a transition window, and `liyi migrate` will upgrade sidecar files in place. +`"version"` is required. The linter checks it and rejects unknown versions. This costs nothing now and prevents painful migration when the schema evolves (e.g., adding `"related"` edges, or deferred structured fields). A JSON Schema definition ships alongside the linter for editor validation and autocompletion (see *立意 (Intent Specs)* below). When the schema changes, the linter will accept both `"0.1"` and the new version during a transition window, and `liyi migrate` will upgrade sidecar files in place. **`liyi migrate` behavior.** When the schema version changes (e.g., 0.1 → 0.2), `--migrate` reads each `.liyi.jsonc`, adds any newly required fields with default values, removes deprecated fields, updates `"version"` to the new version, and writes the file back. It is idempotent — running it twice produces the same output. It does not re-hash spans or re-infer intent; it only transforms the schema envelope. Migration is always additive in 0.x: no field present in 0.1 will change meaning, only new fields may appear. @@ -1668,37 +1668,7 @@ The agent instructions define the protocol. The CI linter enforces it. The full AGENTS.md section is ~300 lines: the behavioral rules (the part a human reads once) followed by two JSON schemas (machine-consumed reference that the agent uses to produce valid `.liyi.jsonc` and `triage.json` files). The schemas are not adoption cost — no human needs to memorize them — but they are part of the payload. -```markdown -## 立意 (Intent Specs) - -When writing or modifying code: -1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. -2. When module-level invariants or governing context are apparent, write a `@liyi:note` 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, and close embedded blocks with `@liyi:end-note`. A note is a **context primitive, not a tracked spec**: it is marker-only — never write it to a `.liyi.jsonc`, never hash it, never add a `"reviewed"` flag. Notes apply to their directory subtree (shadowed by deeper notes); to pull a named note into a specific item's context regardless of location, annotate the item with `@liyi:see `. (Optionally name a note: `@liyi:note `; same-named blocks aggregate.) -3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. -4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), 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. 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), excluding sentinel intents (`=trivial`, `=self-doc`), which carry no agent-authored intent for a second model to attack. 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`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial` or `"=self-doc"`. Honor `@liyi:file language=` as the authoritative language for a file when present (it overrides extension-based detection). **Exclude clearly-derivative items from inference entirely** — write no sidecar entry for an item whose whole purpose is to exercise or assert another already-specced item, adding no invariant of its own (chiefly tests: a test named for the behavior it checks derives its intent from that behavior's spec). This targets the derivative *shape*, not "tests" by name — a test that encodes a novel invariant the code does not otherwise state does carry independent intent and should be specced. The end state for a derivative item is no entry; do not reach for `"=trivial"` or `"=self-doc"` to file it away. -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-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. - - In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. -11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. -``` - -### Key principles of the intent protocol - -- **Adversarial, not confirmatory.** Find bugs, not confirm correctness. -- **Spec is the referee.** If the spec says one thing and the code does another, the test exposes the gap. The human decides who's right. -- **Model diversity.** Different model for tests than for code, when possible. -- **Never modify source code logic** during the protocol. Only create/update `.liyi.jsonc` files, `@liyi:note` blocks (in docs or doc comments), test files, and annotation comments (`@liyi:trivial`, `@liyi:ignore`, `@liyi:requirement`, `@liyi:related`). Annotation comments are metadata, not logic — adding them does not change program behavior. - -### Resolving rule conflicts - -When two normative rules appear to conflict, or a task's correct completion turns on an ambiguity the convention does not resolve, the agent must not guess. If a human is reachable, name the conflicting rules and ask once, presenting the candidate resolutions and a recommendation. If no human is reachable (batch or CI), take the most conservative reading, leave the affected specs `"reviewed": false`, finish the unambiguous parts, and surface the conflict prominently. This is not a license to stall on judgment the standard explicitly delegates — such as applying the `=self-doc` observer counterfactual — which the agent exercises and states outright for the human to veto. +The section is reproduced verbatim in *立意 (Intent Specs)* below; `scripts/check-template-sync.py` (run by `make lint`) fails if the two copies drift, and `make template-sync-fix` syncs them. ### The cognitive load inversion: tool-guided agents @@ -2667,7 +2637,46 @@ The agent re-infers (updating `source_span`; the tool recomputes `source_hash`), --- -## Appendix: JSON Schema for `.liyi.jsonc` (v0.1) + +## 立意 (Intent Specs) + +When writing or modifying code: + +1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. +2. When module-level invariants or governing context are apparent, write a `@liyi:note` 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, and close embedded blocks with `@liyi:end-note`. A note is a **context primitive, not a tracked spec**: it is marker-only — never write it to a `.liyi.jsonc`, never hash it, never add a `"reviewed"` flag. Notes apply to their directory subtree (shadowed by deeper notes); to pull a named note into a specific item's context regardless of location, annotate the item with `@liyi:see `. (Optionally name a note: `@liyi:note `; same-named blocks aggregate.) +3. If a source item has a `@liyi:related ` annotation, record the dependency in `.liyi.jsonc` as `"related": {"": null}`. The tool fills in the requirement's current hash. +4. For each `@liyi:requirement ` block encountered (closed by `@liyi:end-requirement `), 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. 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), excluding sentinel intents (`=trivial`, `=self-doc`), which carry no agent-authored intent for a second model to attack. 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`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial` or `"=self-doc"`. Honor `@liyi:file language=` as the authoritative language for a file when present (it overrides extension-based detection). **Exclude clearly-derivative items from inference entirely** — write no sidecar entry for an item whose whole purpose is to exercise or assert another already-specced item, adding no invariant of its own (chiefly tests: a test named for the behavior it checks derives its intent from that behavior's spec). This targets the derivative *shape*, not "tests" by name — a test that encodes a novel invariant the code does not otherwise state does carry independent intent and should be specced. The end state for a derivative item is no entry; do not reach for `"=trivial"` or `"=self-doc"` to file it away. +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-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. + + In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority. +11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it. + +### Key principles of the intent protocol + +- **Adversarial, not confirmatory.** Find bugs, not confirm correctness. +- **Spec is the referee.** If the spec says one thing and the code does another, the test exposes the gap. The human decides who's right. +- **Model diversity.** Different model for tests than for code, when possible. +- **Never modify source code logic** during the protocol. Only create/update `.liyi.jsonc` files, `@liyi:note` blocks (in docs or doc comments), test files, and annotation comments (`@liyi:trivial`, `@liyi:ignore`, `@liyi:requirement`, `@liyi:related`). Annotation comments are metadata, not logic — adding them does not change program behavior. + +### Resolving rule conflicts + +When two normative instructions here appear to conflict, or a task's correct completion turns on an ambiguity these instructions do not resolve: + +1. **Do not guess.** Name the conflicting rules (and any design-doc sections they reference). +2. **If a human is reachable:** ask once — state the candidate resolutions and your recommendation. Consolidate into a single question rather than a series. +3. **If no human is reachable** (batch, CI): take the most conservative reading, leave the affected specs `"reviewed": false`, complete the unambiguous parts, and report the unresolved conflict prominently. +4. **Do not over-apply this.** Where the standard delegates judgment to you — e.g. whether an item qualifies for `=self-doc` under the observer test — reason it out and state your reasoning so a human can veto. That is not a conflict. + +### `.liyi.jsonc` Schema (v0.1) + +Sidecar files must conform to the following JSON Schema. The top-level object has three required fields: `"version"` (must be `"0.1"`), `"source"` (repo-relative path to the source file), and `"specs"` (array of item or requirement entries). Each spec entry is either an **item spec** or a **requirement spec**, distinguished by the presence of `"item"` vs `"requirement"`. ```json { @@ -2793,6 +2802,178 @@ The agent re-infers (updating `source_span`; the tool recomputes `source_hash`), } ``` +### Triage Report Schema (v0.1) + +When `liyi check` reports stale items, the agent assesses each and writes the result to `.liyi/triage.json`. The report must conform to the following JSON Schema (also available at `schema/triage.schema.json`): + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://liyi.run/schema/0.1/triage.schema.json", + "title": "立意 triage report", + "description": "Agent-produced assessment of stale intent specs, validated and consumed by liyi triage subcommands.", + "type": "object", + "required": ["version", "generated", "model", "summary", "items"], + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "const": "0.1", + "description": "Schema version. Must match the version understood by the liyi binary." + }, + "generated": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of when the triage report was produced." + }, + "model": { + "type": "string", + "description": "Identifier of the model that produced the assessments (e.g. 'claude-sonnet-4-20260514')." + }, + "root": { + "type": "string", + "default": ".", + "description": "Repository root relative to the working directory. Usually '.'." + }, + "summary": { "$ref": "#/$defs/summary" }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/triageItem" } + } + }, + "$defs": { + "sourceSpan": { + "type": "array", + "items": { "type": "integer", "minimum": 1 }, + "minItems": 2, + "maxItems": 2, + "description": "Closed interval of 1-indexed line numbers [start, end]. start must be <= end." + }, + "verdict": { + "type": "string", + "enum": ["cosmetic", "semantic", "intent-violation", "unclear"], + "description": "cosmetic: no behavioral change (rename, reformat). semantic: code evolved, intent is stale but code is correct. intent-violation: code contradicts declared intent. unclear: model cannot determine with sufficient confidence." + }, + "action": { + "type": "string", + "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", + "required": ["total_stale", "cosmetic", "semantic", "intent_violation", "unassessed"], + "additionalProperties": false, + "properties": { + "total_stale": { + "type": "integer", + "minimum": 0, + "description": "Total number of stale items assessed." + }, + "cosmetic": { + "type": "integer", + "minimum": 0, + "description": "Items with verdict 'cosmetic'." + }, + "semantic": { + "type": "integer", + "minimum": 0, + "description": "Items with verdict 'semantic'." + }, + "intent_violation": { + "type": "integer", + "minimum": 0, + "description": "Items with verdict 'intent-violation'." + }, + "unassessed": { + "type": "integer", + "minimum": 0, + "description": "Stale items not yet assessed (should be 0 in a complete report)." + }, + "impacted_transitively": { + "type": "integer", + "minimum": 0, + "description": "Items affected transitively via the related graph." + } + } + }, + "impactEntry": { + "type": "object", + "required": ["source", "item", "relationship", "impact_summary"], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "description": "Repo-relative path of the transitively affected source file." + }, + "item": { + "type": "string", + "description": "Name of the transitively affected item." + }, + "relationship": { + "type": "string", + "description": "The related edge that propagates the impact (e.g. 'related:multi-currency-addition')." + }, + "impact_summary": { + "type": "string", + "description": "Why this item is affected (1–2 sentences)." + } + } + }, + "triageItem": { + "type": "object", + "required": ["source", "item", "source_span", "verdict", "confidence", "change_summary", "invariant_summary", "reasoning", "action"], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "description": "Repo-relative source path." + }, + "item": { + "type": "string", + "description": "Item name (matches the sidecar spec)." + }, + "source_span": { + "$ref": "#/$defs/sourceSpan", + "description": "Current span from the sidecar." + }, + "verdict": { "$ref": "#/$defs/verdict" }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Model's self-assessed confidence in the verdict (0–1)." + }, + "change_summary": { + "type": "string", + "description": "What changed in the code (1–2 sentences)." + }, + "invariant_summary": { + "type": "string", + "description": "What stayed the same (1–2 sentences)." + }, + "reasoning": { + "type": "string", + "description": "Why the verdict was assigned (2–3 sentences, citable in reviews)." + }, + "action": { "$ref": "#/$defs/action" }, + "suggested_intent": { + "type": ["string", "null"], + "default": null, + "description": "Proposed new intent text. Expected for 'semantic' verdicts; null otherwise." + }, + "impact": { + "type": "array", + "items": { "$ref": "#/$defs/impactEntry" }, + "default": [], + "description": "Transitively affected items via the related graph." + } + } + } + } +} +``` + + This schema ships as `liyi.schema.json` in the linter's release artifacts and is published at the `$id` URL. Editors that support JSON Schema (VSCode, IntelliJ, Neovim with `SchemaStore`) will provide validation and autocompletion for `.liyi.jsonc` files when configured with `"$schema": "https://liyi.run/schema/0.1/liyi.schema.json"` at the top of the sidecar, or via a workspace-level `json.schemas` setting. --- diff --git a/docs/liyi-design.md.liyi.jsonc b/docs/liyi-design.md.liyi.jsonc index 112f76a..4e5331f 100644 --- a/docs/liyi-design.md.liyi.jsonc +++ b/docs/liyi-design.md.liyi.jsonc @@ -18,7 +18,7 @@ 406, 408 ], - "source_hash": "sha256:361fd9e237617e9cf0e80df99dd31b06274eca55e15e33384fb4a8f745d07fb9", + "source_hash": "sha256:74f718a44797a3d0c2b81b4ccdd12e8ad488a0df6890015ad457daca802c5dd1", "source_anchor": "" }, { diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index 016d217..4070086 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -1,167 +1,91 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 OR MIT -"""Fail if docs/liyi-design.md's reproduced agent content drifts from the -portable template in AGENTS.md. +"""Keep the `liyi:template` block identical in AGENTS.md and the design doc. -`liyi init` extracts the block between `` and -`` from AGENTS.md and ships it to downstream -repositories, so AGENTS.md is the source of truth for the agent -instruction. The design doc reproduces parts of that block for human -readers, but nothing else keeps the two copies in sync, and they have -silently diverged before (a sentinel added to the template but not the -doc). This check fails when they drift again. - -Compared regions: - -* the numbered behavioral rules (verbatim), -* the "Key principles of the intent protocol" section (verbatim), -* the `.liyi.jsonc` JSON Schema appendix (compared as parsed JSON, so the - template's `\\u0040` quine escape and the doc's literal `@` agree). - -Intentionally not compared: - -* "Resolving rule conflicts" — the doc states it as prose, the template as - a numbered list; the two are semantically equivalent, not textually - identical. -* the triage report schema — it has no design-doc counterpart. +`liyi init` ships the block between `` and +`` from AGENTS.md, so AGENTS.md is the source of +truth. docs/liyi-design.md carries the same block verbatim for human +readers. Because the two copies are byte-identical, checking them is a +plain string comparison — no per-section extraction, no schema-aware JSON +diff — and `--sync` can copy AGENTS.md's block over the design doc's. Usage: - python3 scripts/check-template-sync.py [ROOT] + python3 scripts/check-template-sync.py [--sync] [ROOT] -ROOT defaults to the repository root inferred from this script's location. +Without `--sync` the script exits non-zero and prints a unified diff when +the blocks differ. With `--sync` it rewrites the design doc's block from +AGENTS.md and exits zero. ROOT defaults to the repository root inferred +from this script's location. """ from __future__ import annotations import difflib -import json -import re import sys from pathlib import Path -TEMPLATE_START = "" -TEMPLATE_END = "" -RULES_INTRO = "When writing or modifying code:" -KEY_PRINCIPLES = "### Key principles of the intent protocol" -MINIMAL_INSTRUCTION = "### Minimal instruction (for any AGENTS.md)" -APPENDIX_HEADING = "## Appendix: JSON Schema for `.liyi.jsonc` (v0.1)" +START = "" +END = "" def read(path: Path) -> str: return path.read_text(encoding="utf-8") -def template_block(agents: str) -> str: - """The portable instruction block `liyi init` ships downstream.""" - start = agents.index(TEMPLATE_START) + len(TEMPLATE_START) - end = agents.index(TEMPLATE_END, start) - return agents[start:end] - - -def normalize(text: str) -> str: - """Strip trailing whitespace and leading/trailing blank lines.""" - lines = [line.rstrip() for line in text.strip().splitlines()] - return "\n".join(lines).strip() - - -def rules_region(text: str, stop: str | None = None) -> str: - """The numbered rules, from the intro line up to *stop* (or the end).""" - start = text.index(RULES_INTRO) + len(RULES_INTRO) - end = text.index(stop, start) if stop else len(text) - return normalize(text[start:end]) - - -def section(text: str, heading: str) -> str: - """The body of a level-2/3 section, up to the next same-level heading.""" - start = text.index(heading) + len(heading) - rest = text[start:] - match = re.search(r"\n#{2,3} ", rest) - body = rest[: match.start()] if match else rest - return normalize(body) - - -def fenced(text: str, info: str) -> list[str]: - """Bodies of ``` fenced code blocks (info must match exactly).""" - return re.findall(rf"```{info}\n(.*?)\n```", text, re.DOTALL) - - -def design_rules(design: str) -> str: - after = design.index(MINIMAL_INSTRUCTION) - return rules_region(fenced(design[after:], "markdown")[0]) - - -def design_schema(design: str) -> object: - after = design.index(APPENDIX_HEADING) - return json.loads(fenced(design[after:], "json")[0]) +def extract(text: str, where: str) -> str: + """The template block without the surrounding markers or newlines.""" + try: + start = text.index(START) + len(START) + end = text.index(END, start) + except ValueError: + raise SystemExit(f"template-sync: {where} has no {START} / {END} block") + return text[start:end].strip("\n") -def agents_schema(template: str) -> object: - # The first ```json block in the template is the sidecar schema. - return json.loads(fenced(template, "json")[0]) +def replace(text: str, block: str) -> str: + """Rewrite the existing template block in *text* to *block*.""" + start = text.index(START) + end = text.index(END, start) + len(END) + return text[:start] + f"{START}\n{block}\n{END}" + text[end:] -def diff(name: str, expected: str, actual: str) -> str: - out = [f"## {name}\n"] - out.extend( +def main() -> int: + args = sys.argv[1:] + sync = "--sync" in args + positional = [arg for arg in args if arg != "--sync"] + if len(positional) > 1: + raise SystemExit("template-sync: at most one ROOT argument is expected") + root = Path(positional[0]).resolve() if positional else Path(__file__).resolve().parent.parent + + agents_path = root / "AGENTS.md" + design_path = root / "docs" / "liyi-design.md" + + source = extract(read(agents_path), "AGENTS.md") + copy = extract(read(design_path), "docs/liyi-design.md") + + if source == copy: + print("template-sync: the design doc carries the same template as AGENTS.md") + return 0 + + if sync: + design_path.write_text(replace(read(design_path), source), encoding="utf-8") + print("template-sync: copied the AGENTS.md template into docs/liyi-design.md") + return 0 + + print("template-sync: the liyi:template blocks differ (AGENTS.md is the source of truth).\n") + sys.stdout.writelines( difflib.unified_diff( - expected.splitlines(keepends=True), - actual.splitlines(keepends=True), - fromfile="AGENTS.md (source of truth)", - tofile="docs/liyi-design.md (reproduced copy)", + source.splitlines(keepends=True), + copy.splitlines(keepends=True), + fromfile="AGENTS.md", + tofile="docs/liyi-design.md", ) ) - return "".join(out) - - -def main() -> int: - if len(sys.argv) > 1: - root = Path(sys.argv[1]).resolve() - else: - root = Path(__file__).resolve().parent.parent - - agents = read(root / "AGENTS.md") - design = read(root / "docs" / "liyi-design.md") - - try: - template = template_block(agents) - failures: list[str] = [] - - expected_rules = rules_region(template, KEY_PRINCIPLES) - actual_rules = design_rules(design) - if expected_rules != actual_rules: - failures.append(diff("behavioral rules", expected_rules, actual_rules)) - - expected_principles = section(template, KEY_PRINCIPLES) - actual_principles = section(design, KEY_PRINCIPLES) - if expected_principles != actual_principles: - failures.append( - diff("key principles", expected_principles, actual_principles) - ) - - expected_schema = json.dumps(agents_schema(template), indent=2, sort_keys=True) - actual_schema = json.dumps(design_schema(design), indent=2, sort_keys=True) - if expected_schema != actual_schema: - failures.append(diff("sidecar schema", expected_schema, actual_schema)) - except (ValueError, IndexError, json.JSONDecodeError) as exc: - print( - "template-sync: could not locate a reproduced region — " - f"the docs layout changed ({exc}).", - file=sys.stderr, - ) - return 1 - - if failures: - print( - "template-sync: AGENTS.md and docs/liyi-design.md have diverged.\n\n" - + "\n".join(failures) - + "\nAGENTS.md is the source of truth; update the design doc's " - "reproduced copies to match.\n", - end="", - ) - return 1 - - print("template-sync: design doc reproduces the AGENTS.md template") - return 0 + print( + "\nRe-run with `--sync` (or `make template-sync-fix`) to copy the " + "AGENTS.md block over the design doc's." + ) + return 1 if __name__ == "__main__": From 668a7b039eee9e12e83e015044445011c7c5f049 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 12:06:58 +0800 Subject: [PATCH 19/44] refactor(template): move the section heading outside the template block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `liyi:template` block started with its own `## 立意 (Intent Specs)` heading, so the design doc could not choose where the block sits without that heading dictating the outline. Move the heading outside the `` markers in both files; the block now begins at "When writing or modifying code:". `liyi init` must still ship the heading, so `init_agents_md` now prepends `## 立意 (Intent Specs)` before the block when appending to or creating AGENTS.md. Its intent was updated accordingly and left unreviewed for a human to re-approve; the `init_sidecar` span shift was rehashed in the same commit. Original prompt: > Hmm, maybe the block should be *minus the section heading* to preserve > the design doc's outline. Also in another commit please make the job > `sync-template` and `check-template-sync` for better readability. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- crates/liyi/src/init.rs | 16 +++++++++------- crates/liyi/src/init.rs.liyi.jsonc | 12 ++++++------ docs/liyi-design.md | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 053ea96..325a9ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,9 +69,9 @@ See the contributing guide for full details and examples. --- - ## 立意 (Intent Specs) + When writing or modifying code: 1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index e9361f6..1ef7aea 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -62,27 +62,29 @@ impl From for InitError { /// `liyi init` — append agent instruction to `AGENTS.md`. /// -/// Creates the file if it doesn't exist; appends the block if it does. -/// If `force` is true, overwrites the agent instruction block even if -/// a `## 立意` section already exists. +/// Creates the file if it doesn't exist; appends the section heading and the +/// block if it does. If `force` is true, overwrites the agent instruction block +/// even if a `## 立意` section already exists. pub fn init_agents_md(root: &Path, force: bool) -> Result { let agents_path = root.join("AGENTS.md"); + let section_heading = "## 立意 (Intent Specs)\n\n"; if agents_path.is_file() { let content = fs::read_to_string(&agents_path)?; if content.contains("## 立意") && !force { return Err(InitError::AlreadyExists(agents_path)); } - // Append the block + // Append the section heading and the block let block = agents_md_block(); let mut new_content = content; new_content.push('\n'); + new_content.push_str(section_heading); new_content.push_str(block); fs::write(&agents_path, new_content)?; } else { // Create new file let block = agents_md_block(); - let content = format!("# AGENTS.md\n\n{block}"); + let content = format!("# AGENTS.md\n\n{section_heading}{block}"); fs::write(&agents_path, content)?; } @@ -216,8 +218,8 @@ mod tests { // Must start with the section heading. assert!( - block.starts_with("## 立意"), - "extracted block must start with ## 立意 heading" + block.starts_with("When writing or modifying code:"), + "extracted block must start with the instruction intro" ); // Key invariants: the block contains the sidecar schema and diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index dce24cb..ca9297f 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -65,14 +65,14 @@ }, { "item": "init_agents_md", - "reviewed": true, - "intent": "If AGENTS.md does not exist, create it with a heading and the agent block extracted from the repo's own AGENTS.md via agents_md_block(). If it exists but does not contain '## 立意', append the block. If '## 立意' is present and force is false, return AlreadyExists. If force is true, append regardless.", + "reviewed": false, + "intent": "If AGENTS.md does not exist, create it with a '# AGENTS.md' heading followed by the agent instruction section — the '## 立意 (Intent Specs)' heading and the portable block from agents_md_block(). If it exists but does not contain '## 立意', append the section heading and the block. If '## 立意' is present and force is false, return AlreadyExists. If force is true, append regardless.", "source_span": [ 68, - 90 + 92 ], "tree_path": "fn.init_agents_md", - "source_hash": "sha256:71c4198c9e3243a50ecc0e95de56c2d05dc1d6829747f5071859468533b557f7", + "source_hash": "sha256:516858aacb50b051aa1470f0abb6d57b75696f09a623b269fd7f00e621e28b90", "source_anchor": "pub fn init_agents_md(root: &Path, force: bool) -> Result {" }, { @@ -80,8 +80,8 @@ "reviewed": true, "intent": "Create a .liyi.jsonc sidecar next to the source file. When discover is true and the language is tree-sitter-supported, pre-populate specs with discovered items including _hints (_has_doc, _body_lines, _likely_trivial based on trivial_threshold). Always scan the source file for @liyi:requirement markers and emit corresponding RequirementSpec entries regardless of file type or discover flag. When discover is false, emit only requirement specs (no items). If the sidecar exists and force is false, return AlreadyExists. Sidecar file name must be .liyi.jsonc.", "source_span": [ - 111, - 207 + 113, + 209 ], "tree_path": "fn.init_sidecar", "source_hash": "sha256:bc2c80e5d18b5780374676c330ec4ff2cc2304ed41be1cb350529fd56d8b3eb6", diff --git a/docs/liyi-design.md b/docs/liyi-design.md index c90fac4..fa64b13 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -2637,9 +2637,9 @@ The agent re-infers (updating `source_span`; the tool recomputes `source_hash`), --- - ## 立意 (Intent Specs) + When writing or modifying code: 1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. From 7635a5255c6adfbc12a3dee2995127ccd2b7a6f4 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 12:07:33 +0800 Subject: [PATCH 20/44] refactor(meta): give the template-sync targets verb-first names Rename the gate from `template-sync` to `check-template-sync` and the fixer from `template-sync-fix` to `sync-template`, matching the verb-first style of `fmt-check`, `liyi-check`, and the `check-template-sync.py` script. The CI job (and its sidecar spec) is renamed to `check-template-sync` as well, and the failure hint now points at `make sync-template`. Original prompt: > Hmm, maybe the block should be *minus the section heading* to preserve > the design doc's outline. Also in another commit please make the job > `sync-template` and `check-template-sync` for better readability. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- .github/workflows/ci.yml | 4 ++-- .github/workflows/ci.yml.liyi.jsonc | 10 +++++----- Makefile | 12 ++++++------ docs/liyi-design.md | 2 +- scripts/check-template-sync.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40df811..c988783 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,8 +72,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo run -p liyi-cli -- check --root . - template-sync: - name: Template sync + check-template-sync: + name: Check template sync runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/ci.yml.liyi.jsonc b/.github/workflows/ci.yml.liyi.jsonc index 8674886..31d0c8a 100644 --- a/.github/workflows/ci.yml.liyi.jsonc +++ b/.github/workflows/ci.yml.liyi.jsonc @@ -88,16 +88,16 @@ "source_anchor": " liyi-check:" }, { - "item": "jobs::template-sync", + "item": "jobs::check-template-sync", "reviewed": false, - "intent": "Run scripts/check-template-sync.py to verify that docs/liyi-design.md reproduces the AGENTS.md agent template (behavioral rules, key principles, and the .liyi.jsonc schema appendix) without drift. Must fail the PR if any reproduced region diverges from the template.", + "intent": "Run scripts/check-template-sync.py to verify that docs/liyi-design.md carries the same liyi:template block as AGENTS.md (the block `liyi init` ships downstream). Must fail the PR if the blocks differ.", "source_span": [ 75, 80 ], - "tree_path": "key.jobs::key.\"template-sync\"", - "source_hash": "sha256:6ef01eceeb294b6c907d6e1da02c1b40d1b557b2fe07a634d6a1f88b2ed9567d", - "source_anchor": " template-sync:" + "tree_path": "key.jobs::key.\"check-template-sync\"", + "source_hash": "sha256:fa834b9241a1a982101e82894486ecafd3aae730ca0021efcd91925205d87182", + "source_anchor": " check-template-sync:" } ] } diff --git a/Makefile b/Makefile index 449dd43..faf0129 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT -.PHONY: verify lint fmt-check clippy liyi-check template-sync template-sync-fix test \ +.PHONY: verify lint fmt-check clippy liyi-check check-template-sync sync-template test \ verify-short lint-short test-short # Run every pre-commit gate (lint + tests) in one shot. @@ -14,7 +14,7 @@ verify: lint test @echo "verify: all gates passed" # Run every pre-commit lint gate (matches CI). Ends with a success banner. -lint: fmt-check clippy liyi-check template-sync +lint: fmt-check clippy liyi-check check-template-sync @echo "lint: all gates passed" fmt-check: @@ -29,12 +29,12 @@ liyi-check: # Verify docs/liyi-design.md carries the same liyi:template block as # AGENTS.md (the source of truth `liyi init` ships downstream). -template-sync: +check-template-sync: python3 scripts/check-template-sync.py # Copy the AGENTS.md template block over the design doc's, after a -# template-sync failure. Review the resulting diff before committing. -template-sync-fix: +# check-template-sync failure. Review the resulting diff before committing. +sync-template: python3 scripts/check-template-sync.py --sync test: @@ -70,7 +70,7 @@ lint-short: fi; \ if ! python3 scripts/check-template-sync.py >$$LOG 2>&1; then \ tail -n 20 $$LOG; rm -f $$LOG; \ - echo "lint-short: template-sync FAILED — re-run \`make template-sync\` for the full output"; exit 1; \ + echo "lint-short: check-template-sync FAILED — re-run \`make check-template-sync\` for the full output"; exit 1; \ fi; \ rm -f $$LOG; \ echo "lint-short: all gates passed" diff --git a/docs/liyi-design.md b/docs/liyi-design.md index fa64b13..ed8424e 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1668,7 +1668,7 @@ The agent instructions define the protocol. The CI linter enforces it. The full AGENTS.md section is ~300 lines: the behavioral rules (the part a human reads once) followed by two JSON schemas (machine-consumed reference that the agent uses to produce valid `.liyi.jsonc` and `triage.json` files). The schemas are not adoption cost — no human needs to memorize them — but they are part of the payload. -The section is reproduced verbatim in *立意 (Intent Specs)* below; `scripts/check-template-sync.py` (run by `make lint`) fails if the two copies drift, and `make template-sync-fix` syncs them. +The section is reproduced verbatim in *立意 (Intent Specs)* below; `scripts/check-template-sync.py` (run by `make lint`) fails if the two copies drift, and `make sync-template` syncs them. ### The cognitive load inversion: tool-guided agents diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index 4070086..8882f19 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -82,7 +82,7 @@ def main() -> int: ) ) print( - "\nRe-run with `--sync` (or `make template-sync-fix`) to copy the " + "\nRe-run with `--sync` (or `make sync-template`) to copy the " "AGENTS.md block over the design doc's." ) return 1 From 2e315643323e3cfbdfe9e5dc531d12e02da5aa5d Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 12:11:04 +0800 Subject: [PATCH 21/44] refactor(template): keep the heading in AGENTS.md, bridge it in tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconsider the previous commit's approach: instead of stripping the section heading out of both blocks and teaching `liyi init` to re-add it, keep the `## 立意 (Intent Specs)` heading inside AGENTS.md's template block — the artifact `liyi init` actually ships — and leave only the design doc's block heading-less, so the design doc's own heading still governs its outline. This reverts the `init_agents_md` special-casing (and its sidecar churn): `agents_md_block` is a pure extractor again and the shipped block stays self-contained. The template-sync check now accounts for the heading by comparing AGENTS.md's block against the design doc's block prefixed with `## 立意 (Intent Specs)`, and `make sync-template` materializes AGENTS.md from the design doc, adding the heading — the design doc is the source. Original prompt: > Oh, actually I meant: when syncing the template block from the design > doc to `AGENTS.md`, add the desired section heading in this step, so the > `liyi init` logic stays the same. When checking sync status, account for > the section heading handling. Do you think this to be better? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- .github/workflows/ci.yml.liyi.jsonc | 2 +- AGENTS.md | 2 +- Makefile | 9 +++-- crates/liyi/src/init.rs | 16 ++++---- crates/liyi/src/init.rs.liyi.jsonc | 12 +++--- docs/liyi-design.md | 2 +- scripts/check-template-sync.py | 58 ++++++++++++++++++----------- 7 files changed, 57 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml.liyi.jsonc b/.github/workflows/ci.yml.liyi.jsonc index 31d0c8a..e18c3e9 100644 --- a/.github/workflows/ci.yml.liyi.jsonc +++ b/.github/workflows/ci.yml.liyi.jsonc @@ -90,7 +90,7 @@ { "item": "jobs::check-template-sync", "reviewed": false, - "intent": "Run scripts/check-template-sync.py to verify that docs/liyi-design.md carries the same liyi:template block as AGENTS.md (the block `liyi init` ships downstream). Must fail the PR if the blocks differ.", + "intent": "Run scripts/check-template-sync.py to verify that AGENTS.md materializes docs/liyi-design.md's liyi:template block, with the section heading added. Must fail the PR if the two diverge.", "source_span": [ 75, 80 diff --git a/AGENTS.md b/AGENTS.md index 325a9ea..053ea96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,9 +69,9 @@ See the contributing guide for full details and examples. --- + ## 立意 (Intent Specs) - When writing or modifying code: 1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. diff --git a/Makefile b/Makefile index faf0129..98f2847 100644 --- a/Makefile +++ b/Makefile @@ -27,13 +27,14 @@ clippy: liyi-check: cargo run -p liyi-cli -- check --root . -# Verify docs/liyi-design.md carries the same liyi:template block as -# AGENTS.md (the source of truth `liyi init` ships downstream). +# Verify AGENTS.md materializes docs/liyi-design.md's liyi:template block. +# The design doc is the source; AGENTS.md adds the section heading so +# `liyi init` can ship the block unchanged. check-template-sync: python3 scripts/check-template-sync.py -# Copy the AGENTS.md template block over the design doc's, after a -# check-template-sync failure. Review the resulting diff before committing. +# Rewrite AGENTS.md's liyi:template block from the design doc's, adding +# the section heading. Review the resulting diff before committing. sync-template: python3 scripts/check-template-sync.py --sync diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index 1ef7aea..e9361f6 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -62,29 +62,27 @@ impl From for InitError { /// `liyi init` — append agent instruction to `AGENTS.md`. /// -/// Creates the file if it doesn't exist; appends the section heading and the -/// block if it does. If `force` is true, overwrites the agent instruction block -/// even if a `## 立意` section already exists. +/// Creates the file if it doesn't exist; appends the block if it does. +/// If `force` is true, overwrites the agent instruction block even if +/// a `## 立意` section already exists. pub fn init_agents_md(root: &Path, force: bool) -> Result { let agents_path = root.join("AGENTS.md"); - let section_heading = "## 立意 (Intent Specs)\n\n"; if agents_path.is_file() { let content = fs::read_to_string(&agents_path)?; if content.contains("## 立意") && !force { return Err(InitError::AlreadyExists(agents_path)); } - // Append the section heading and the block + // Append the block let block = agents_md_block(); let mut new_content = content; new_content.push('\n'); - new_content.push_str(section_heading); new_content.push_str(block); fs::write(&agents_path, new_content)?; } else { // Create new file let block = agents_md_block(); - let content = format!("# AGENTS.md\n\n{section_heading}{block}"); + let content = format!("# AGENTS.md\n\n{block}"); fs::write(&agents_path, content)?; } @@ -218,8 +216,8 @@ mod tests { // Must start with the section heading. assert!( - block.starts_with("When writing or modifying code:"), - "extracted block must start with the instruction intro" + block.starts_with("## 立意"), + "extracted block must start with ## 立意 heading" ); // Key invariants: the block contains the sidecar schema and diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index ca9297f..dce24cb 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -65,14 +65,14 @@ }, { "item": "init_agents_md", - "reviewed": false, - "intent": "If AGENTS.md does not exist, create it with a '# AGENTS.md' heading followed by the agent instruction section — the '## 立意 (Intent Specs)' heading and the portable block from agents_md_block(). If it exists but does not contain '## 立意', append the section heading and the block. If '## 立意' is present and force is false, return AlreadyExists. If force is true, append regardless.", + "reviewed": true, + "intent": "If AGENTS.md does not exist, create it with a heading and the agent block extracted from the repo's own AGENTS.md via agents_md_block(). If it exists but does not contain '## 立意', append the block. If '## 立意' is present and force is false, return AlreadyExists. If force is true, append regardless.", "source_span": [ 68, - 92 + 90 ], "tree_path": "fn.init_agents_md", - "source_hash": "sha256:516858aacb50b051aa1470f0abb6d57b75696f09a623b269fd7f00e621e28b90", + "source_hash": "sha256:71c4198c9e3243a50ecc0e95de56c2d05dc1d6829747f5071859468533b557f7", "source_anchor": "pub fn init_agents_md(root: &Path, force: bool) -> Result {" }, { @@ -80,8 +80,8 @@ "reviewed": true, "intent": "Create a .liyi.jsonc sidecar next to the source file. When discover is true and the language is tree-sitter-supported, pre-populate specs with discovered items including _hints (_has_doc, _body_lines, _likely_trivial based on trivial_threshold). Always scan the source file for @liyi:requirement markers and emit corresponding RequirementSpec entries regardless of file type or discover flag. When discover is false, emit only requirement specs (no items). If the sidecar exists and force is false, return AlreadyExists. Sidecar file name must be .liyi.jsonc.", "source_span": [ - 113, - 209 + 111, + 207 ], "tree_path": "fn.init_sidecar", "source_hash": "sha256:bc2c80e5d18b5780374676c330ec4ff2cc2304ed41be1cb350529fd56d8b3eb6", diff --git a/docs/liyi-design.md b/docs/liyi-design.md index ed8424e..a35085c 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1668,7 +1668,7 @@ The agent instructions define the protocol. The CI linter enforces it. The full AGENTS.md section is ~300 lines: the behavioral rules (the part a human reads once) followed by two JSON schemas (machine-consumed reference that the agent uses to produce valid `.liyi.jsonc` and `triage.json` files). The schemas are not adoption cost — no human needs to memorize them — but they are part of the payload. -The section is reproduced verbatim in *立意 (Intent Specs)* below; `scripts/check-template-sync.py` (run by `make lint`) fails if the two copies drift, and `make sync-template` syncs them. +The canonical instruction is in *立意 (Intent Specs)* below; `scripts/check-template-sync.py` (run by `make lint`) fails if AGENTS.md's copy drifts, and `make sync-template` rewrites AGENTS.md from this block, adding the `## 立意 (Intent Specs)` heading. ### The cognitive load inversion: tool-guided agents diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index 8882f19..143e368 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -1,20 +1,25 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 OR MIT -"""Keep the `liyi:template` block identical in AGENTS.md and the design doc. +"""Keep AGENTS.md's portable template in sync with the design doc's block. -`liyi init` ships the block between `` and -`` from AGENTS.md, so AGENTS.md is the source of -truth. docs/liyi-design.md carries the same block verbatim for human -readers. Because the two copies are byte-identical, checking them is a -plain string comparison — no per-section extraction, no schema-aware JSON -diff — and `--sync` can copy AGENTS.md's block over the design doc's. +`liyi init` ships the ` ... end -->` block from +AGENTS.md unchanged, so that block keeps its `## 立意 (Intent Specs)` +section heading and `liyi init` needs no special-casing. +docs/liyi-design.md carries the same instruction body *without* the +heading, so the design doc's own section heading and outline stay in +control. This check compares the two, accounting for the heading: +AGENTS.md's block must equal the design doc's block prefixed with the +heading. + +`--sync` materializes AGENTS.md from the design doc's block, adding the +heading, so the design doc is the source of truth. Usage: python3 scripts/check-template-sync.py [--sync] [ROOT] Without `--sync` the script exits non-zero and prints a unified diff when -the blocks differ. With `--sync` it rewrites the design doc's block from -AGENTS.md and exits zero. ROOT defaults to the repository root inferred +the copies differ. With `--sync` it rewrites AGENTS.md's block from the +design doc's and exits zero. ROOT defaults to the repository root inferred from this script's location. """ @@ -26,6 +31,7 @@ START = "" END = "" +HEADING = "## 立意 (Intent Specs)" def read(path: Path) -> str: @@ -60,30 +66,38 @@ def main() -> int: agents_path = root / "AGENTS.md" design_path = root / "docs" / "liyi-design.md" - source = extract(read(agents_path), "AGENTS.md") - copy = extract(read(design_path), "docs/liyi-design.md") + agents = read(agents_path) + body = extract(read(design_path), "docs/liyi-design.md") + expected = f"{HEADING}\n\n{body}" + actual = extract(agents, "AGENTS.md") - if source == copy: - print("template-sync: the design doc carries the same template as AGENTS.md") + if actual == expected: + print("template-sync: AGENTS.md matches the design doc's template block") return 0 if sync: - design_path.write_text(replace(read(design_path), source), encoding="utf-8") - print("template-sync: copied the AGENTS.md template into docs/liyi-design.md") + agents_path.write_text(replace(agents, expected), encoding="utf-8") + print( + "template-sync: wrote the design doc's block into AGENTS.md " + "(section heading added)" + ) return 0 - print("template-sync: the liyi:template blocks differ (AGENTS.md is the source of truth).\n") + print( + "template-sync: AGENTS.md does not match the design doc's template " + "block (the design doc is the source of truth).\n" + ) sys.stdout.writelines( difflib.unified_diff( - source.splitlines(keepends=True), - copy.splitlines(keepends=True), - fromfile="AGENTS.md", - tofile="docs/liyi-design.md", + expected.splitlines(keepends=True), + actual.splitlines(keepends=True), + fromfile="docs/liyi-design.md (expected, heading added)", + tofile="AGENTS.md", ) ) print( - "\nRe-run with `--sync` (or `make sync-template`) to copy the " - "AGENTS.md block over the design doc's." + "\nRe-run with `--sync` (or `make sync-template`) to copy the design " + "doc's block into AGENTS.md." ) return 1 From b14681e113587ff3b46407134b983d706eca58c7 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 12:21:35 +0800 Subject: [PATCH 22/44] refactor: tweak the AGENTS.md section heading and related wording This commit was authored entirely manually by me, and the sidecar for `scripts/check-template-sync.py` is also manually crafted (`liyi init` didn't surface the top-level constant I desired). Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- crates/liyi/src/schema.rs.liyi.jsonc | 2 +- docs/liyi-design.md | 35 ++++++++--- docs/liyi-design.md.liyi.jsonc | 75 +++++++++++++---------- scripts/check-template-sync.py | 14 ++--- scripts/check-template-sync.py.liyi.jsonc | 22 +++++++ 6 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 scripts/check-template-sync.py.liyi.jsonc diff --git a/AGENTS.md b/AGENTS.md index 053ea96..5dc20da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ See the contributing guide for full details and examples. --- -## 立意 (Intent Specs) +## The 立意 (Intent Specs) design pattern for agents When writing or modifying code: diff --git a/crates/liyi/src/schema.rs.liyi.jsonc b/crates/liyi/src/schema.rs.liyi.jsonc index 2a0fc3a..8b47db0 100644 --- a/crates/liyi/src/schema.rs.liyi.jsonc +++ b/crates/liyi/src/schema.rs.liyi.jsonc @@ -15,7 +15,7 @@ "source_hash": "sha256:eb13a8e5c419c2fd0a1bc441fe4eaa5b8cd52ec411cb425c9fb0ca622d5ee96c", "source_anchor": "pub fn validate_version(version: &str) -> Result<(), String> {", "related": { - "version-field-required": "sha256:74f718a44797a3d0c2b81b4ccdd12e8ad488a0df6890015ad457daca802c5dd1" + "version-field-required": "sha256:0dc645decb5df864c85904dbc09dbfbaaea627945632d6e83696bce196c8b7e8" } }, { diff --git a/docs/liyi-design.md b/docs/liyi-design.md index a35085c..0cc17d5 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -404,9 +404,13 @@ The `source` path is relative to the repository root — the same path you'd pas `source_hash`, `source_anchor`, and `tree_path` are tool-managed — the agent writes only `source_span` and the tool fills in the rest (see *Per-item staleness* and *Structural identity via `tree_path`* below). Agents MAY write `tree_path` if they can infer the AST path, but the tool will overwrite it with the canonical form on the next `liyi check --fix`. `"intent": "=doc"` is a reserved sentinel meaning "the docstring already captures intent" — the agent uses it when the source docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary (see *`"=doc"` in the sidecar* below). -`"version"` is required. The linter checks it and rejects unknown versions. This costs nothing now and prevents painful migration when the schema evolves (e.g., adding `"related"` edges, or deferred structured fields). A JSON Schema definition ships alongside the linter for editor validation and autocompletion (see *立意 (Intent Specs)* below). When the schema changes, the linter will accept both `"0.1"` and the new version during a transition window, and `liyi migrate` will upgrade sidecar files in place. +`"version"` is required. The linter checks it and rejects unknown versions. +This costs nothing now and prevents painful migration when the schema evolves, e.g. adding `related` edges, or deferred structured fields. +A JSON Schema definition is to be shipped alongside the linter for editor validation and autocompletion. +When the schema changes, a new release of `liyi` is to accept both the currently supported version(s) and the new version during a transition window, and `liyi migrate` will upgrade sidecar files in place. + **`liyi migrate` behavior.** When the schema version changes (e.g., 0.1 → 0.2), `--migrate` reads each `.liyi.jsonc`, adds any newly required fields with default values, removes deprecated fields, updates `"version"` to the new version, and writes the file back. It is idempotent — running it twice produces the same output. It does not re-hash spans or re-infer intent; it only transforms the schema envelope. Migration is always additive in 0.x: no field present in 0.1 will change meaning, only new fields may appear. After human review — either the human adds `@liyi:intent` in the source file (see *Source-level intent* below), or sets `"reviewed": true` in the sidecar via CLI or IDE code action. Both paths mark the item as reviewed. When `"reviewed"` is set to `true`, `"confidence"` is removed — a human voucher replaces agent self-assessment. If the source later changes and the agent re-infers (producing a new unreviewed spec), `"confidence"` reappears: @@ -1660,15 +1664,19 @@ The linter is tested at three levels: --- -## The Agent Skill - -The agent instructions define the protocol. The CI linter enforces it. +## The agent-side design -### Minimal instruction (for any AGENTS.md) +Half of the 立意 paradigm relies on cooperation from agents' side: instructions, protocols, prompt and harness engineering. +The deterministic linter and related CI setup serves to both complement and constrain the agents, to provide a unified 立意 workflow as a whole. -The full AGENTS.md section is ~300 lines: the behavioral rules (the part a human reads once) followed by two JSON schemas (machine-consumed reference that the agent uses to produce valid `.liyi.jsonc` and `triage.json` files). The schemas are not adoption cost — no human needs to memorize them — but they are part of the payload. +### Agent instructions: `AGENTS.md` for adopters -The canonical instruction is in *立意 (Intent Specs)* below; `scripts/check-template-sync.py` (run by `make lint`) fails if AGENTS.md's copy drifts, and `make sync-template` rewrites AGENTS.md from this block, adding the `## 立意 (Intent Specs)` heading. +The canonical instructions for inclusion in 立意-adopting repositories are +defined in a dedicated appendix section in this design documentation, to be +reproduced by `liyi init` for easy adoption. +It consists of the practical instruction for agents (the part a human reads once) +followed by two JSON schemas (machine-consumed reference that the agent uses to +produce valid `.liyi.jsonc` and `triage.json` files). ### The cognitive load inversion: tool-guided agents @@ -2635,9 +2643,15 @@ The agent re-infers (updating `source_span`; the tool recomputes `source_hash`), --- ---- -## 立意 (Intent Specs) +## Appendix: The prescribed `AGENTS.md` content for Liyi-adopting repositories + + +The content is to be placed in a dedicated section named +"The 立意 (Intent Specs) design pattern for agents", in a 立意-adopting repo's +`AGENTS.md` (the default location supported by `liyi init`) or the repo's +preferred location. + When writing or modifying code: @@ -2974,7 +2988,8 @@ When `liyi check` reports stale items, the agent assesses each and writes the re ``` -This schema ships as `liyi.schema.json` in the linter's release artifacts and is published at the `$id` URL. Editors that support JSON Schema (VSCode, IntelliJ, Neovim with `SchemaStore`) will provide validation and autocompletion for `.liyi.jsonc` files when configured with `"$schema": "https://liyi.run/schema/0.1/liyi.schema.json"` at the top of the sidecar, or via a workspace-level `json.schemas` setting. +The schemas ship as `liyi.schema.json` and `triage.schema.json` in the linter's release artifacts and are to be published at the `$id` URL. +Editors that support JSON Schema (VSCode, IntelliJ, Neovim with `SchemaStore`) will provide validation and autocompletion for `.liyi.jsonc` files when configured with `"$schema": "https://liyi.run/schema/0.1/liyi.schema.json"` at the top of the sidecar, or via a workspace-level `json.schemas` setting. --- diff --git a/docs/liyi-design.md.liyi.jsonc b/docs/liyi-design.md.liyi.jsonc index 4e5331f..d3d8513 100644 --- a/docs/liyi-design.md.liyi.jsonc +++ b/docs/liyi-design.md.liyi.jsonc @@ -18,14 +18,14 @@ 406, 408 ], - "source_hash": "sha256:74f718a44797a3d0c2b81b4ccdd12e8ad488a0df6890015ad457daca802c5dd1", + "source_hash": "sha256:0dc645decb5df864c85904dbc09dbfbaaea627945632d6e83696bce196c8b7e8", "source_anchor": "" }, { "requirement": "reviewed-semantics", "source_span": [ - 432, - 434 + 436, + 438 ], "source_hash": "sha256:eabc16cfe83d7ed5b52779d4771a04f9ba6fee37c24e4493f2577d907afbd9e0", "source_anchor": "" @@ -33,8 +33,8 @@ { "requirement": "source-span-semantics", "source_span": [ - 456, - 458 + 460, + 462 ], "source_hash": "sha256:879d23ea45fef5ded8141bbaf784171a02ab9be65cf003c6be11c08fa735d20b", "source_anchor": "" @@ -42,8 +42,8 @@ { "requirement": "tool-managed-fields", "source_span": [ - 460, - 462 + 464, + 466 ], "source_hash": "sha256:b1b5caaebc89ce62a3b2e59f7468b813a67caf784a6fe03209c46ab67a2f7dbc", "source_anchor": "" @@ -51,8 +51,8 @@ { "requirement": "span-shift-heuristic", "source_span": [ - 472, - 474 + 476, + 478 ], "source_hash": "sha256:f51dc7ced1fa6597e05d2c82b7878a20babcbf31d09960978a68918fcc1075cf", "source_anchor": "" @@ -60,8 +60,8 @@ { "requirement": "tree-path-fix-behavior", "source_span": [ - 498, - 501 + 502, + 505 ], "source_hash": "sha256:1f5c710fae63a2d2466bcc01a45b30b1f85ab4a22bfb19f961958b1167d47d0c", "source_anchor": "" @@ -69,8 +69,8 @@ { "requirement": "sibling-scan-recovery", "source_span": [ - 503, - 526 + 507, + 530 ], "source_hash": "sha256:9f3c1ac03a88188f97c3ea850988d5f55f7a2f78c34b28104f47da67ba0f64e7", "source_anchor": "" @@ -78,8 +78,8 @@ { "requirement": "tree-path-empty-fallback", "source_span": [ - 530, - 538 + 534, + 542 ], "source_hash": "sha256:93c0520e982193e67ecef00b351b6967775bfcb9d989290fe51795b747decceb", "source_anchor": "" @@ -87,8 +87,8 @@ { "requirement": "requirement-name-uniqueness", "source_span": [ - 690, - 692 + 694, + 696 ], "source_hash": "sha256:fc329b7eb4016b20e9baa89b14470f2953aa6bad5311aa5d4867702994d94f32", "source_anchor": "" @@ -96,8 +96,8 @@ { "requirement": "cycle-detection", "source_span": [ - 820, - 822 + 824, + 826 ], "source_hash": "sha256:e8af30a5974fcccf038c0d07eb98c279b57c9671dad032123424aa041d1262ec", "source_anchor": "" @@ -105,8 +105,8 @@ { "requirement": "requirement-discovery-global", "source_span": [ - 1192, - 1194 + 1196, + 1198 ], "source_hash": "sha256:8fdcf5001d02d4622dd7c8a5bd89090119e35cd2aedf7b8d9d7243d5f4cff359", "source_anchor": "" @@ -114,8 +114,8 @@ { "requirement": "liyi-check-exit-code", "source_span": [ - 1317, - 1323 + 1321, + 1327 ], "source_hash": "sha256:2cb6ab36efd4724e3f7e7021bf36233633357e9fddbd1a231bc7b7ed07137d65", "source_anchor": "" @@ -123,8 +123,8 @@ { "requirement": "marker-normalization", "source_span": [ - 1350, - 1352 + 1354, + 1356 ], "source_hash": "sha256:f1786541ac9be4533a36924a80c578a749024a4980fbc323417c2d5640ed6306", "source_anchor": "" @@ -132,8 +132,8 @@ { "requirement": "quine-escape-in-source", "source_span": [ - 1364, - 1366 + 1368, + 1370 ], "source_hash": "sha256:b929750d0d3c9ce24a6f814d7a1db15f2fc03983efa2647cfcf6a7462d2c8b81", "source_anchor": "" @@ -141,8 +141,8 @@ { "requirement": "markdown-fenced-block-skip", "source_span": [ - 1370, - 1378 + 1374, + 1382 ], "source_hash": "sha256:d75bbcedd7757ae66541f99c329df0460e21a6ac02c056e06156d4aacc8dd749", "source_anchor": "" @@ -150,8 +150,8 @@ { "requirement": "fix-never-modifies-human-fields", "source_span": [ - 1587, - 1589 + 1591, + 1593 ], "source_hash": "sha256:2101e1597493adb25b7743b3b2f983f672f967e2e24ae18ef07ece70ccfb76bb", "source_anchor": "" @@ -159,11 +159,20 @@ { "requirement": "fix-semantic-drift-protection", "source_span": [ - 1591, - 1593 + 1595, + 1597 ], "source_hash": "sha256:7c7bcb74ddf40462f9162ada24feeffa60e080adae3f4a69d37dde5fb23ac81b", "source_anchor": "" + }, + { + "requirement": "agents-md-instructions-section-naming", + "source_span": [ + 2649, + 2654 + ], + "source_hash": "sha256:d5c4bb590a65e0196814a1b5a6ec83959f165824f730f77f7621be4a5ed2cb4b", + "source_anchor": "" } ] } diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index 143e368..866322b 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -2,12 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT """Keep AGENTS.md's portable template in sync with the design doc's block. -`liyi init` ships the ` ... end -->` block from -AGENTS.md unchanged, so that block keeps its `## 立意 (Intent Specs)` -section heading and `liyi init` needs no special-casing. -docs/liyi-design.md carries the same instruction body *without* the -heading, so the design doc's own section heading and outline stay in -control. This check compares the two, accounting for the heading: +`liyi init` ships the `liyi:template:start ... end` block from `AGENTS.md` +unchanged, so that block keeps its section heading and `liyi init` needs no +special-casing. `docs/liyi-design.md` carries the same instruction body +*without* the heading, so the design doc's own section heading and outline +stay in control. This check compares the two, accounting for the heading: AGENTS.md's block must equal the design doc's block prefixed with the heading. @@ -31,7 +30,8 @@ START = "" END = "" -HEADING = "## 立意 (Intent Specs)" +HEADING = "## The 立意 (Intent Specs) design pattern for agents" +'''.. @liyi:related agents-md-instructions-section-naming''' def read(path: Path) -> str: diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc new file mode 100644 index 0000000..6175c58 --- /dev/null +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -0,0 +1,22 @@ +// liyi v0.1 spec file +{ + "version": "0.1", + "source": "check-template-sync.py", + "specs": [ + { + "item": "HEADING", + "reviewed": false, + "intent": "=doc", + "source_span": [ + 33, + 34 + ], + "tree_path": "HEADING", + "source_hash": "sha256:33e11b229bb002341da7dbdda738980bcab6bf25a7c1e0f9486a2f50f9d3d0b7", + "source_anchor": "HEADING = \"## The 立意 (Intent Specs) design pattern for agents\"", + "related": { + "agents-md-instructions-section-naming'''": null + } + } + ] +} From 33ff7272ef2f1e146c1fe7b8346b0b5280e05bc1 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 13:29:09 +0800 Subject: [PATCH 23/44] fix(linter): update stale AGENTS.md heading assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agents_md_block_extracts_valid_template` still expected the old `## 立意` section heading, but the heading was reworded to `## The 立意 (Intent Specs) design pattern for agents`. Update the assertion so the test matches the extracted block again. This is a pre-existing failure left by the manual heading refactor; it is unrelated to the Python tree_path work but blocks `make verify`. Original prompt: > Please check `git diff --cached` and add test cases for the added > items, and commit. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi/src/init.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index e9361f6..1927b83 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -216,8 +216,8 @@ mod tests { // Must start with the section heading. assert!( - block.starts_with("## 立意"), - "extracted block must start with ## 立意 heading" + block.starts_with("## The 立意"), + "extracted block must start with the section heading" ); // Key invariants: the block contains the sidecar schema and From 7ad723099b75274373646c26a7e7e9a84e187955 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 13:29:10 +0800 Subject: [PATCH 24/44] fix(linter): point template-sync related marker at the real requirement The `@liyi:related` marker for HEADING was written inside a triple-quoted string, so the scanner read the closing `'''` as part of the requirement name (`agents-md-instructions-section-naming'''`), and `liyi check` failed with "related requirement not found". Make it a plain comment and correct the sidecar's related edge, then let `liyi check --fix` rehash. This is a pre-existing failure from the manually crafted sidecar; it is unrelated to the Python tree_path work but blocks `make verify`. Original prompt: > Please check `git diff --cached` and add test cases for the added > items, and commit. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- scripts/check-template-sync.py | 2 +- scripts/check-template-sync.py.liyi.jsonc | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index 866322b..ce9a09f 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -31,7 +31,7 @@ START = "" END = "" HEADING = "## The 立意 (Intent Specs) design pattern for agents" -'''.. @liyi:related agents-md-instructions-section-naming''' +# @liyi:related agents-md-instructions-section-naming def read(path: Path) -> str: diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc index 6175c58..3f54eb5 100644 --- a/scripts/check-template-sync.py.liyi.jsonc +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -9,13 +9,13 @@ "intent": "=doc", "source_span": [ 33, - 34 + 33 ], - "tree_path": "HEADING", - "source_hash": "sha256:33e11b229bb002341da7dbdda738980bcab6bf25a7c1e0f9486a2f50f9d3d0b7", + "tree_path": "const.HEADING", + "source_hash": "sha256:b2d628369d56e7a4f2c9cde920a380f6845dc0ef9a4de913b2e90c68e4b13146", "source_anchor": "HEADING = \"## The 立意 (Intent Specs) design pattern for agents\"", "related": { - "agents-md-instructions-section-naming'''": null + "agents-md-instructions-section-naming": "sha256:d5c4bb590a65e0196814a1b5a6ec83959f165824f730f77f7621be4a5ed2cb4b" } } ] From 970e871ac02ae0cc51223760937ba440a99f7779 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 13:29:11 +0800 Subject: [PATCH 25/44] feat(linter): resolve Python module-level constants in tree_path Python's tree_path config only mapped functions and classes, so module-level constants were invisible to `liyi init` and to the resolver. Register `const` for module-level `assignment` nodes, extract the name via a `python_node_name` callback that requires a plain-identifier target directly under the module root (excluding locals and attributes), and traverse the wrapping `expression_statement` as a transparent kind. Add resolve/compute/roundtrip tests for the newly sampled `TOPLEVEL_CONST`, plus a discovery test pinning that only module-level assignments become `const` items. Original prompt: > Please check `git diff --cached` and add test cases for the added > items, and commit. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi/src/tree_path/lang_python.rs | 131 +++++++++++++++++- .../src/tree_path/lang_python.rs.liyi.jsonc | 22 ++- 2 files changed, 145 insertions(+), 8 deletions(-) diff --git a/crates/liyi/src/tree_path/lang_python.rs b/crates/liyi/src/tree_path/lang_python.rs index cbdae40..3430124 100644 --- a/crates/liyi/src/tree_path/lang_python.rs +++ b/crates/liyi/src/tree_path/lang_python.rs @@ -31,17 +31,52 @@ fn python_has_doc_comment(node: &Node, source: &str) -> bool { false } +/// Custom name extraction for Python module-level constants. +/// +/// A module-level assignment (`NAME = ...` or `NAME: Final = ...`) declares a +/// module constant and maps to the `const` shorthand. The +/// `expression_statement` wrapper is traversed via `transparent_kinds`. +/// +/// Requiring the module scope and a plain-identifier target excludes local +/// bindings (`x = 0`, `x: int = 0`) and attribute assignments (`self.x = 0`) +/// inside function or class bodies, which are not constants. +fn python_node_name(node: &Node, source: &str) -> Option { + if node.kind() != "assignment" { + return None; + } + + // Only assignments directly under the module root qualify. + let statement = node.parent()?; + if statement.kind() != "expression_statement" || statement.parent()?.kind() != "module" { + return None; + } + + let name = node.child_by_field_name("left")?; + if name.kind() != "identifier" { + return None; + } + + Some(source[name.byte_range()].to_string()) +} + /// Python language configuration. pub(super) static CONFIG: LanguageConfig = LanguageConfig { ts_language: || tree_sitter_python::LANGUAGE.into(), extensions: &["py", "pyi"], - kind_map: &[("fn", "function_definition"), ("class", "class_definition")], + kind_map: &[ + ("fn", "function_definition"), + ("class", "class_definition"), + ("const", "assignment"), + ], name_field: "name", name_overrides: &[], body_fields: &["body"], - custom_name: None, + custom_name: Some(python_node_name), doc_comment_detector: Some(python_has_doc_comment), - transparent_kinds: &[], + // Module-level assignments are wrapped in an `expression_statement`, so + // the resolver must look through it to reach the underlying `assignment` + // node. + transparent_kinds: &["expression_statement"], }; #[cfg(test)] @@ -50,6 +85,11 @@ mod tests { const SAMPLE_PYTHON: &str = r#"# A simple order processing module +from typing import Final + +TOPLEVEL_CONST: Final = [1, 2, 3] + + class Order: def __init__(self, amount): self.amount = amount @@ -109,6 +149,91 @@ def calculate_total(items): ); } + #[test] + fn resolve_python_toplevel_const() { + let span = resolve_tree_path(SAMPLE_PYTHON, "const.TOPLEVEL_CONST", Language::Python); + assert!(span.is_some(), "should resolve const.TOPLEVEL_CONST"); + let [start, end] = span.unwrap(); + let lines: Vec<&str> = SAMPLE_PYTHON.lines().collect(); + assert!( + lines[start - 1].contains("TOPLEVEL_CONST: Final"), + "span should point to TOPLEVEL_CONST, got: {}", + lines[start - 1] + ); + assert_eq!(start, end, "constant span should be a single line"); + } + + #[test] + fn compute_python_toplevel_const_path() { + let lines: Vec<&str> = SAMPLE_PYTHON.lines().collect(); + let start = lines + .iter() + .position(|l| l.contains("TOPLEVEL_CONST: Final")) + .unwrap() + + 1; + + let path = compute_tree_path(SAMPLE_PYTHON, [start, start], Language::Python); + assert_eq!(path, "const.TOPLEVEL_CONST"); + } + + #[test] + fn roundtrip_python_toplevel_const() { + let resolved_span = + resolve_tree_path(SAMPLE_PYTHON, "const.TOPLEVEL_CONST", Language::Python).unwrap(); + + let computed_path = compute_tree_path(SAMPLE_PYTHON, resolved_span, Language::Python); + assert_eq!(computed_path, "const.TOPLEVEL_CONST"); + + let re_resolved = + resolve_tree_path(SAMPLE_PYTHON, &computed_path, Language::Python).unwrap(); + assert_eq!(re_resolved, resolved_span); + } + + /// Module-level assignments are constants whether annotated or not; + /// assignments inside class/function bodies are not. + const SAMPLE_PYTHON_SCOPES: &str = r#"MAX = 10 +LIMIT: Final = 20 + + +class Config: + retries: int = 3 + + def load(self): + local: int = 1 + self.value: int = 2 + return local +"#; + + #[test] + fn discover_python_only_module_level_consts() { + let items = discover_items(SAMPLE_PYTHON_SCOPES, Language::Python); + let names: Vec<&str> = items.iter().map(|i| i.name.as_str()).collect(); + + for (name, tree_path) in [ + ("MAX", "const.MAX"), + ("LIMIT", "const.LIMIT"), + ("Config", "class.Config"), + ("Config::load", "class.Config::fn.load"), + ] { + let item = items + .iter() + .find(|i| i.name == name) + .unwrap_or_else(|| panic!("{name} should be discovered")); + assert_eq!(item.tree_path, tree_path); + } + + for absent in [ + "Config::retries", + "Config::load::local", + "Config::load::self.value", + ] { + assert!( + !names.contains(&absent), + "unexpected item discovered: {absent}" + ); + } + } + #[test] fn compute_python_function_path() { let lines: Vec<&str> = SAMPLE_PYTHON.lines().collect(); diff --git a/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc b/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc index f39f1fb..19674c3 100644 --- a/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc +++ b/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc @@ -3,16 +3,28 @@ "version": "0.1", "source": "crates/liyi/src/tree_path/lang_python.rs", "specs": [ + { + "item": "python_node_name", + "reviewed": false, + "intent": "Custom name extractor for Python module-level constants: for an assignment whose parent expression_statement sits directly under the module root and whose target is a plain identifier, return that identifier; return None for local bindings and attribute assignments inside function or class bodies, and for non-assignment nodes.", + "source_span": [ + 43, + 60 + ], + "tree_path": "fn.python_node_name", + "source_hash": "sha256:39f4a4f7c22f0ee5657e227e8a58d79f35b432ce04fc4411d2842aceb0a64a3b", + "source_anchor": "fn python_node_name(node: &Node, source: &str) -> Option {" + }, { "item": "CONFIG", - "reviewed": true, - "intent": "Define the Python language configuration for tree_path resolution: register tree-sitter-python grammar, .py/.pyi extensions, kind mappings for fn (function_definition) and class (class_definition), using standard name field with no custom name extraction, and python_has_doc_comment detector for triple-quoted docstrings.", + "reviewed": false, + "intent": "Define the Python language configuration for tree_path resolution: register tree-sitter-python grammar, .py/.pyi extensions, kind mappings for fn (function_definition), class (class_definition), and const (assignment) for module-level constants, use the standard name field for fn/class and the python_node_name callback for const, the python_has_doc_comment detector for triple-quoted docstrings, and expression_statement as a transparent kind so the resolver reaches module-level assignment nodes.", "source_span": [ - 35, - 45 + 63, + 80 ], "tree_path": "static.CONFIG", - "source_hash": "sha256:db4a475937b5f454b05d37957988bcaa60f5acfb74c31f86169109b0db1ee398", + "source_hash": "sha256:a03265d4a3af69f431ba826d64694c88cd79018d1bcf1bfcfb2714a96bf092f7", "source_anchor": "pub(super) static CONFIG: LanguageConfig = LanguageConfig {" } ] From 7e82b654c5739d9f7a965f4daa484b257badd26b Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 13:38:59 +0800 Subject: [PATCH 26/44] refactor: manually fix scripts/check-template-sync.py.liyi.jsonc Signed-off-by: WANG Xuerui --- scripts/check-template-sync.py.liyi.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc index 3f54eb5..a62262d 100644 --- a/scripts/check-template-sync.py.liyi.jsonc +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -1,12 +1,12 @@ // liyi v0.1 spec file { "version": "0.1", - "source": "check-template-sync.py", + "source": "scripts/check-template-sync.py", "specs": [ { "item": "HEADING", "reviewed": false, - "intent": "=doc", + "intent": "=self-doc", "source_span": [ 33, 33 From 97f9071152b1fd08b589ac8c624ded0b15489438 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 13:50:09 +0800 Subject: [PATCH 27/44] refactor(template): adopt the versioned liyi agent-instructions pragma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the bare `` markers with a DocToc-style pragma that names the region and carries a monotonic revision, so a downstream `AGENTS.md` can be refreshed in place by `liyi migrate` instead of relying on `init_agents_md` matching the section heading: ... Unlike DocToc — whose reminder tells users to re-run it after every outline change — this block changes only when a newer Liyi release ships a different template, so the reminder says so explicitly. Docs: the `agents-md-instructions-section-naming` requirement now defines the pragma, and the `liyi migrate` behavior describes refreshing the `AGENTS.md` region. The pragma literal is shared by the design-doc appendix, `AGENTS.md`, `check-template-sync.py`, and `init.rs`, so this commit carries the mechanical constant/message/docstring updates to keep extraction working. Behavior changes follow: `liyi init` shipping the pragma, the prefix-based guard, and the `liyi migrate` Markdown path. Sidecar churn: the requirement was rehashed and the script's `HEADING` related edge re-acknowledged; `TEMPLATE_START`, `TEMPLATE_END`, and `agents_md_block` were re-opened for review. Original prompt: > let's use the legacy style but let's make it `START Liyi agent > instructions rev. 1` and `END liyi agent instructions`, that feels > better doesn't it? > Actually this block updates much less frequently than DocToc's case -- > only a Liyi release could possibly bump that, and not for every Liyi > release, while a DocToc is to be refreshed after every document outline > change by their user. Let's reword the reminder and start with doc > changes then implementation. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- .github/workflows/ci.yml.liyi.jsonc | 2 +- AGENTS.md | 5 +- crates/liyi/src/init.rs | 20 +++---- crates/liyi/src/init.rs.liyi.jsonc | 18 +++--- docs/liyi-design.md | 12 +++- docs/liyi-design.md.liyi.jsonc | 70 +++++++++++------------ scripts/check-template-sync.py | 6 +- scripts/check-template-sync.py.liyi.jsonc | 2 +- 8 files changed, 71 insertions(+), 64 deletions(-) diff --git a/.github/workflows/ci.yml.liyi.jsonc b/.github/workflows/ci.yml.liyi.jsonc index e18c3e9..cc23ad3 100644 --- a/.github/workflows/ci.yml.liyi.jsonc +++ b/.github/workflows/ci.yml.liyi.jsonc @@ -90,7 +90,7 @@ { "item": "jobs::check-template-sync", "reviewed": false, - "intent": "Run scripts/check-template-sync.py to verify that AGENTS.md materializes docs/liyi-design.md's liyi:template block, with the section heading added. Must fail the PR if the two diverge.", + "intent": "Run scripts/check-template-sync.py to verify that AGENTS.md materializes docs/liyi-design.md's liyi agent-instructions block, with the section heading added. Must fail the PR if the two diverge.", "source_span": [ 75, 80 diff --git a/AGENTS.md b/AGENTS.md index 5dc20da..03bba2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,8 @@ See the contributing guide for full details and examples. --- - + + ## The 立意 (Intent Specs) design pattern for agents When writing or modifying code: @@ -404,4 +405,4 @@ When `liyi check` reports stale items, the agent assesses each and writes the re } } ``` - + diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index 1927b83..163f533 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -9,26 +9,26 @@ use crate::tree_path::{detect_language, discover_items}; /// so that `liyi init` can extract the portable template block. const AGENTS_MD_FULL: &str = include_str!("../../../AGENTS.md"); -const TEMPLATE_START: &str = "\n"; -const TEMPLATE_END: &str = "\n"; +const TEMPLATE_START: &str = "\n\n"; +const TEMPLATE_END: &str = "\n"; /// Extract the portable agent instruction block from the repo's AGENTS.md. /// -/// The block is delimited by `` and -/// `` HTML comment markers. Panics at -/// runtime (not compile time) if the markers are missing — but since -/// the content is baked in via `include_str!`, this is effectively a -/// build-time guarantee: any AGENTS.md without markers won't produce -/// a working binary. +/// The block is delimited by the `START liyi agent instructions rev. N` +/// pragma and its `DON'T EDIT THIS SECTION` reminder, and closed by +/// ``. Panics at runtime (not compile +/// time) if the pragma is missing — but since the content is baked in via +/// `include_str!`, this is effectively a build-time guarantee: any +/// AGENTS.md without the pragma won't produce a working binary. fn agents_md_block() -> &'static str { let start = AGENTS_MD_FULL .find(TEMPLATE_START) - .expect("AGENTS.md missing marker") + .expect("AGENTS.md missing START liyi agent instructions marker") + TEMPLATE_START.len(); let end = start + AGENTS_MD_FULL[start..] .find(TEMPLATE_END) - .expect("AGENTS.md missing marker"); + .expect("AGENTS.md missing END liyi agent instructions marker"); &AGENTS_MD_FULL[start..end] } diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index dce24cb..bef06c7 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -17,38 +17,38 @@ }, { "item": "TEMPLATE_START", - "reviewed": true, + "reviewed": false, "intent": "=trivial", "source_span": [ 12, 12 ], "tree_path": "const.TEMPLATE_START", - "source_hash": "sha256:a2bb1acca95830bc995d5334ba98ad79cb29cf0cbeadb78bf5276b6fff08250b", - "source_anchor": "const TEMPLATE_START: &str = \"\\n\";" + "source_hash": "sha256:9293d9ba71f3ec171bb9632f9f989f0c36e58ea11dbc57e33351deb5b7c8792c", + "source_anchor": "const TEMPLATE_START: &str = \"\\n\\n\";" }, { "item": "TEMPLATE_END", - "reviewed": true, + "reviewed": false, "intent": "=trivial", "source_span": [ 13, 13 ], "tree_path": "const.TEMPLATE_END", - "source_hash": "sha256:b27d30cac4bea8226105bc507779eb865acb57718a13e9f72bd7d834147c942e", - "source_anchor": "const TEMPLATE_END: &str = \"\\n\";" + "source_hash": "sha256:f9ac799232fb18d7906b86b9d555a851db02f06bcc5c8492b6f62689f9f40e91", + "source_anchor": "const TEMPLATE_END: &str = \"\\n\";" }, { "item": "agents_md_block", - "reviewed": true, - "intent": "Extract the portable agent instruction block from the compile-time-included AGENTS.md by finding text between and markers. Must panic if either marker is missing.", + "reviewed": false, + "intent": "Extract the portable agent instruction block from the compile-time-included AGENTS.md by finding the text after the `START liyi agent instructions rev. N` pragma (and its DON'T EDIT reminder) and before the `END liyi agent instructions` marker. Must panic if the pragma or end marker is missing.", "source_span": [ 23, 33 ], "tree_path": "fn.agents_md_block", - "source_hash": "sha256:d872315f683788bef2aa149d652ee2b1ee73dc57b0d0920fcab6233fbcc6c9e9", + "source_hash": "sha256:c74950abdb98df0773c9f3d8e398b650838beba7b204f86ef748aaadb777ab2c", "source_anchor": "fn agents_md_block() -> &'static str {" }, { diff --git a/docs/liyi-design.md b/docs/liyi-design.md index 0cc17d5..a0bc138 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -413,6 +413,8 @@ When the schema changes, a new release of `liyi` is to accept both the currently **`liyi migrate` behavior.** When the schema version changes (e.g., 0.1 → 0.2), `--migrate` reads each `.liyi.jsonc`, adds any newly required fields with default values, removes deprecated fields, updates `"version"` to the new version, and writes the file back. It is idempotent — running it twice produces the same output. It does not re-hash spans or re-infer intent; it only transforms the schema envelope. Migration is always additive in 0.x: no field present in 0.1 will change meaning, only new fields may appear. +It also refreshes the prescribed agent instructions in a repository's `AGENTS.md`: it locates the region delimited by the `START liyi agent instructions rev. N` / `END liyi agent instructions` pragma and rewrites it from the template embedded in the running `liyi`, bumping the revision. The rest of the file is left untouched and the operation is idempotent. The revision changes only when a newer `liyi` release ships a different instruction template — not on every release. + After human review — either the human adds `@liyi:intent` in the source file (see *Source-level intent* below), or sets `"reviewed": true` in the sidecar via CLI or IDE code action. Both paths mark the item as reviewed. When `"reviewed"` is set to `true`, `"confidence"` is removed — a human voucher replaces agent self-assessment. If the source later changes and the agent re-infers (producing a new unreviewed spec), `"confidence"` reappears: ```jsonc @@ -2650,10 +2652,14 @@ The agent re-infers (updating `source_span`; the tool recomputes `source_hash`), The content is to be placed in a dedicated section named "The 立意 (Intent Specs) design pattern for agents", in a 立意-adopting repo's `AGENTS.md` (the default location supported by `liyi init`) or the repo's -preferred location. +preferred location. The section body is delimited by a DocToc-style pragma — +`START liyi agent instructions rev. N`, where N is a monotonic revision, with a +`DON'T EDIT THIS SECTION` reminder — so that `liyi migrate` can refresh it in +place without duplicating it. - + + When writing or modifying code: 1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. @@ -2986,7 +2992,7 @@ When `liyi check` reports stale items, the agent assesses each and writes the re } } ``` - + The schemas ship as `liyi.schema.json` and `triage.schema.json` in the linter's release artifacts and are to be published at the `$id` URL. Editors that support JSON Schema (VSCode, IntelliJ, Neovim with `SchemaStore`) will provide validation and autocompletion for `.liyi.jsonc` files when configured with `"$schema": "https://liyi.run/schema/0.1/liyi.schema.json"` at the top of the sidecar, or via a workspace-level `json.schemas` setting. diff --git a/docs/liyi-design.md.liyi.jsonc b/docs/liyi-design.md.liyi.jsonc index d3d8513..76a7a18 100644 --- a/docs/liyi-design.md.liyi.jsonc +++ b/docs/liyi-design.md.liyi.jsonc @@ -24,8 +24,8 @@ { "requirement": "reviewed-semantics", "source_span": [ - 436, - 438 + 438, + 440 ], "source_hash": "sha256:eabc16cfe83d7ed5b52779d4771a04f9ba6fee37c24e4493f2577d907afbd9e0", "source_anchor": "" @@ -33,8 +33,8 @@ { "requirement": "source-span-semantics", "source_span": [ - 460, - 462 + 462, + 464 ], "source_hash": "sha256:879d23ea45fef5ded8141bbaf784171a02ab9be65cf003c6be11c08fa735d20b", "source_anchor": "" @@ -42,8 +42,8 @@ { "requirement": "tool-managed-fields", "source_span": [ - 464, - 466 + 466, + 468 ], "source_hash": "sha256:b1b5caaebc89ce62a3b2e59f7468b813a67caf784a6fe03209c46ab67a2f7dbc", "source_anchor": "" @@ -51,8 +51,8 @@ { "requirement": "span-shift-heuristic", "source_span": [ - 476, - 478 + 478, + 480 ], "source_hash": "sha256:f51dc7ced1fa6597e05d2c82b7878a20babcbf31d09960978a68918fcc1075cf", "source_anchor": "" @@ -60,8 +60,8 @@ { "requirement": "tree-path-fix-behavior", "source_span": [ - 502, - 505 + 504, + 507 ], "source_hash": "sha256:1f5c710fae63a2d2466bcc01a45b30b1f85ab4a22bfb19f961958b1167d47d0c", "source_anchor": "" @@ -69,8 +69,8 @@ { "requirement": "sibling-scan-recovery", "source_span": [ - 507, - 530 + 509, + 532 ], "source_hash": "sha256:9f3c1ac03a88188f97c3ea850988d5f55f7a2f78c34b28104f47da67ba0f64e7", "source_anchor": "" @@ -78,8 +78,8 @@ { "requirement": "tree-path-empty-fallback", "source_span": [ - 534, - 542 + 536, + 544 ], "source_hash": "sha256:93c0520e982193e67ecef00b351b6967775bfcb9d989290fe51795b747decceb", "source_anchor": "" @@ -87,8 +87,8 @@ { "requirement": "requirement-name-uniqueness", "source_span": [ - 694, - 696 + 696, + 698 ], "source_hash": "sha256:fc329b7eb4016b20e9baa89b14470f2953aa6bad5311aa5d4867702994d94f32", "source_anchor": "" @@ -96,8 +96,8 @@ { "requirement": "cycle-detection", "source_span": [ - 824, - 826 + 826, + 828 ], "source_hash": "sha256:e8af30a5974fcccf038c0d07eb98c279b57c9671dad032123424aa041d1262ec", "source_anchor": "" @@ -105,8 +105,8 @@ { "requirement": "requirement-discovery-global", "source_span": [ - 1196, - 1198 + 1198, + 1200 ], "source_hash": "sha256:8fdcf5001d02d4622dd7c8a5bd89090119e35cd2aedf7b8d9d7243d5f4cff359", "source_anchor": "" @@ -114,8 +114,8 @@ { "requirement": "liyi-check-exit-code", "source_span": [ - 1321, - 1327 + 1323, + 1329 ], "source_hash": "sha256:2cb6ab36efd4724e3f7e7021bf36233633357e9fddbd1a231bc7b7ed07137d65", "source_anchor": "" @@ -123,8 +123,8 @@ { "requirement": "marker-normalization", "source_span": [ - 1354, - 1356 + 1356, + 1358 ], "source_hash": "sha256:f1786541ac9be4533a36924a80c578a749024a4980fbc323417c2d5640ed6306", "source_anchor": "" @@ -132,8 +132,8 @@ { "requirement": "quine-escape-in-source", "source_span": [ - 1368, - 1370 + 1370, + 1372 ], "source_hash": "sha256:b929750d0d3c9ce24a6f814d7a1db15f2fc03983efa2647cfcf6a7462d2c8b81", "source_anchor": "" @@ -141,8 +141,8 @@ { "requirement": "markdown-fenced-block-skip", "source_span": [ - 1374, - 1382 + 1376, + 1384 ], "source_hash": "sha256:d75bbcedd7757ae66541f99c329df0460e21a6ac02c056e06156d4aacc8dd749", "source_anchor": "" @@ -150,8 +150,8 @@ { "requirement": "fix-never-modifies-human-fields", "source_span": [ - 1591, - 1593 + 1593, + 1595 ], "source_hash": "sha256:2101e1597493adb25b7743b3b2f983f672f967e2e24ae18ef07ece70ccfb76bb", "source_anchor": "" @@ -159,8 +159,8 @@ { "requirement": "fix-semantic-drift-protection", "source_span": [ - 1595, - 1597 + 1597, + 1599 ], "source_hash": "sha256:7c7bcb74ddf40462f9162ada24feeffa60e080adae3f4a69d37dde5fb23ac81b", "source_anchor": "" @@ -168,10 +168,10 @@ { "requirement": "agents-md-instructions-section-naming", "source_span": [ - 2649, - 2654 + 2651, + 2659 ], - "source_hash": "sha256:d5c4bb590a65e0196814a1b5a6ec83959f165824f730f77f7621be4a5ed2cb4b", + "source_hash": "sha256:1e2df100d62f48b36f26ebfe43485734b81be9432aa109995d617e40ef051df8", "source_anchor": "" } ] diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index ce9a09f..90a73b1 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT """Keep AGENTS.md's portable template in sync with the design doc's block. -`liyi init` ships the `liyi:template:start ... end` block from `AGENTS.md` +`liyi init` ships the `START liyi agent instructions` block from `AGENTS.md` unchanged, so that block keeps its section heading and `liyi init` needs no special-casing. `docs/liyi-design.md` carries the same instruction body *without* the heading, so the design doc's own section heading and outline @@ -28,8 +28,8 @@ import sys from pathlib import Path -START = "" -END = "" +START = "\n" +END = "" HEADING = "## The 立意 (Intent Specs) design pattern for agents" # @liyi:related agents-md-instructions-section-naming diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc index a62262d..cc3a19a 100644 --- a/scripts/check-template-sync.py.liyi.jsonc +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -15,7 +15,7 @@ "source_hash": "sha256:b2d628369d56e7a4f2c9cde920a380f6845dc0ef9a4de913b2e90c68e4b13146", "source_anchor": "HEADING = \"## The 立意 (Intent Specs) design pattern for agents\"", "related": { - "agents-md-instructions-section-naming": "sha256:d5c4bb590a65e0196814a1b5a6ec83959f165824f730f77f7621be4a5ed2cb4b" + "agents-md-instructions-section-naming": "sha256:1e2df100d62f48b36f26ebfe43485734b81be9432aa109995d617e40ef051df8" } } ] From c33fca52b3fbdf99fd5666901dc75b721c2d9678 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 13:56:29 +0800 Subject: [PATCH 28/44] docs(template): shorten the pragma reminder to one 80-column line Swap the verbose two-clause reminder for the 78-column form, so the pragma stays readable without wrapping. Used `FROM` rather than a second `WITH` ("REFRESH WITH `liyi migrate` FROM A NEWER LIYI"), and reworded the `agents-md-instructions-section-naming` requirement's reference to match (`BLOCK`, not `SECTION`). Sidecar churn in the same commit: `TEMPLATE_START` rehashed and the script's `HEADING` related edge re-acknowledged. Original prompt: > Let's agree on a shorter wording, ideally below 80 columns, I'd suggest: > > ```markdown > > ``` > > This is 78 characters. Is this better? AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- AGENTS.md | 2 +- crates/liyi/src/init.rs | 2 +- crates/liyi/src/init.rs.liyi.jsonc | 4 ++-- docs/liyi-design.md | 4 ++-- docs/liyi-design.md.liyi.jsonc | 2 +- scripts/check-template-sync.py | 2 +- scripts/check-template-sync.py.liyi.jsonc | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 03bba2d..584d3ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ See the contributing guide for full details and examples. --- - + ## The 立意 (Intent Specs) design pattern for agents When writing or modifying code: diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index 163f533..ac85160 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -9,7 +9,7 @@ use crate::tree_path::{detect_language, discover_items}; /// so that `liyi init` can extract the portable template block. const AGENTS_MD_FULL: &str = include_str!("../../../AGENTS.md"); -const TEMPLATE_START: &str = "\n\n"; +const TEMPLATE_START: &str = "\n\n"; const TEMPLATE_END: &str = "\n"; /// Extract the portable agent instruction block from the repo's AGENTS.md. diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index bef06c7..b04815b 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -24,8 +24,8 @@ 12 ], "tree_path": "const.TEMPLATE_START", - "source_hash": "sha256:9293d9ba71f3ec171bb9632f9f989f0c36e58ea11dbc57e33351deb5b7c8792c", - "source_anchor": "const TEMPLATE_START: &str = \"\\n\\n\";" + "source_hash": "sha256:8d34ecdc56fb1fc11f75843c0e4d88a17c1a6df74fe29fdc091c422984331a91", + "source_anchor": "const TEMPLATE_START: &str = \"\\n\\n\";" }, { "item": "TEMPLATE_END", diff --git a/docs/liyi-design.md b/docs/liyi-design.md index a0bc138..c93bdd2 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -2654,12 +2654,12 @@ The content is to be placed in a dedicated section named `AGENTS.md` (the default location supported by `liyi init`) or the repo's preferred location. The section body is delimited by a DocToc-style pragma — `START liyi agent instructions rev. N`, where N is a monotonic revision, with a -`DON'T EDIT THIS SECTION` reminder — so that `liyi migrate` can refresh it in +`DON'T EDIT THIS BLOCK` reminder — so that `liyi migrate` can refresh it in place without duplicating it. - + When writing or modifying code: 1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone. diff --git a/docs/liyi-design.md.liyi.jsonc b/docs/liyi-design.md.liyi.jsonc index 76a7a18..232820d 100644 --- a/docs/liyi-design.md.liyi.jsonc +++ b/docs/liyi-design.md.liyi.jsonc @@ -171,7 +171,7 @@ 2651, 2659 ], - "source_hash": "sha256:1e2df100d62f48b36f26ebfe43485734b81be9432aa109995d617e40ef051df8", + "source_hash": "sha256:ba9f88f5edb0b338487e493235b49269cfc4ec74d03ebcfe33e1ec7bdc39d298", "source_anchor": "" } ] diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index 90a73b1..b83df0e 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -28,7 +28,7 @@ import sys from pathlib import Path -START = "\n" +START = "\n" END = "" HEADING = "## The 立意 (Intent Specs) design pattern for agents" # @liyi:related agents-md-instructions-section-naming diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc index cc3a19a..11f2dd1 100644 --- a/scripts/check-template-sync.py.liyi.jsonc +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -15,7 +15,7 @@ "source_hash": "sha256:b2d628369d56e7a4f2c9cde920a380f6845dc0ef9a4de913b2e90c68e4b13146", "source_anchor": "HEADING = \"## The 立意 (Intent Specs) design pattern for agents\"", "related": { - "agents-md-instructions-section-naming": "sha256:1e2df100d62f48b36f26ebfe43485734b81be9432aa109995d617e40ef051df8" + "agents-md-instructions-section-naming": "sha256:ba9f88f5edb0b338487e493235b49269cfc4ec74d03ebcfe33e1ec7bdc39d298" } } ] From 395b4cfcefc968f1e9926f019fb87eba2ed4542a Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:02:06 +0800 Subject: [PATCH 29/44] feat(linter): ship the versioned agent-instructions pragma from liyi init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The portable agent instruction block is now wrapped in a DocToc-style, version-tracked pragma instead of internal markers that `liyi init` stripped: ... - `TEMPLATE_REVISION` is the single source for the current revision; `agents_md_block()` re-renders the block from the repo's AGENTS.md at that revision, pragma, reminder, and end marker included. - `find_agents_md_block()` locates a block in arbitrary text without inspecting the revision, so a block written by an older `liyi` is found. - `migrate_agents_md()` rewrites a file's block to `TEMPLATE_REVISION`, preserving surrounding content and skipping the write when already current (idempotent). - `init_agents_md()` now keys off the pragma prefix rather than the `## 立意` heading — which also fixes the duplicate-append bug where a reworded heading made the guard miss an existing block. `--force` replaces the existing block instead of appending a second one. Sidecar churn: init.rs specs were re-generated for the new constants and functions; `init_agents_md`'s intent was rewritten and left unreviewed. Original prompt: > let's go. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi/src/init.rs | 186 +++++++++++++++++++++++------ crates/liyi/src/init.rs.liyi.jsonc | 124 +++++++++++++++---- docs/liyi-design.md | 6 +- 3 files changed, 252 insertions(+), 64 deletions(-) diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index ac85160..6eb984f 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -9,27 +9,83 @@ use crate::tree_path::{detect_language, discover_items}; /// so that `liyi init` can extract the portable template block. const AGENTS_MD_FULL: &str = include_str!("../../../AGENTS.md"); -const TEMPLATE_START: &str = "\n\n"; -const TEMPLATE_END: &str = "\n"; +/// Current revision of the portable agent-instruction template. +/// +/// Bumped when the template content changes; `liyi migrate` rewrites an +/// adopter's block to this revision. +pub const TEMPLATE_REVISION: u32 = 1; + +/// Opening words of the `START` pragma; the revision number follows. +const PRAGMA_START_PREFIX: &str = ""; +/// Reminder line that follows the `START` pragma. +const REMINDER: &str = + ""; +/// Closing marker line. +const END_MARKER: &str = ""; -/// Extract the portable agent instruction block from the repo's AGENTS.md. +/// The `START liyi agent instructions rev. N` pragma line for `rev`. +fn pragma_line(rev: u32) -> String { + format!("{PRAGMA_START_PREFIX}{rev}{PRAGMA_SUFFIX}") +} + +/// Extract the portable agent instruction block from the repo's AGENTS.md, +/// re-rendered at [`TEMPLATE_REVISION`] and including the pragma, reminder, +/// and closing marker — i.e. exactly what `liyi init` ships and `liyi migrate` +/// writes. /// -/// The block is delimited by the `START liyi agent instructions rev. N` -/// pragma and its `DON'T EDIT THIS SECTION` reminder, and closed by -/// ``. Panics at runtime (not compile -/// time) if the pragma is missing — but since the content is baked in via -/// `include_str!`, this is effectively a build-time guarantee: any -/// AGENTS.md without the pragma won't produce a working binary. -fn agents_md_block() -> &'static str { - let start = AGENTS_MD_FULL - .find(TEMPLATE_START) - .expect("AGENTS.md missing START liyi agent instructions marker") - + TEMPLATE_START.len(); - let end = start - + AGENTS_MD_FULL[start..] - .find(TEMPLATE_END) +/// Panics if the pragma or end marker is missing — but since the content is +/// baked in via `include_str!`, this is effectively a build-time guarantee: +/// any AGENTS.md without the pragma won't produce a working binary. +fn agents_md_block() -> String { + let start = format!("{}\n{REMINDER}\n", pragma_line(TEMPLATE_REVISION)); + let start_idx = AGENTS_MD_FULL + .find(&start) + .expect("AGENTS.md missing START liyi agent instructions pragma") + + start.len(); + let end_idx = start_idx + + AGENTS_MD_FULL[start_idx..] + .find(&format!("\n{END_MARKER}")) .expect("AGENTS.md missing END liyi agent instructions marker"); - &AGENTS_MD_FULL[start..end] + format!( + "{start}{}\n{END_MARKER}", + &AGENTS_MD_FULL[start_idx..end_idx] + ) +} + +/// Locate the portable agent-instruction block in `text`. +/// +/// Returns the byte range from the `START` pragma through the `END` marker, +/// or `None` if no block is present. The revision is not inspected, so a block +/// written by an older `liyi` is still found. +pub fn find_agents_md_block(text: &str) -> Option<(usize, usize)> { + let start = text.find(PRAGMA_START_PREFIX)?; + let end = start + text[start..].find(END_MARKER)? + END_MARKER.len(); + Some((start, end)) +} + +/// Rewrite the portable agent-instruction block in `path` to +/// [`TEMPLATE_REVISION`]. +/// +/// Returns `Ok(true)` when the file contains a block (rewritten in place, +/// idempotently) and `Ok(false)` when it does not. +pub fn migrate_agents_md(path: &Path) -> Result { + let content = + fs::read_to_string(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?; + let Some((start, end)) = find_agents_md_block(&content) else { + return Ok(false); + }; + let block = agents_md_block(); + if content[start..end] == block { + return Ok(true); + } + let mut new_content = String::with_capacity(content.len()); + new_content.push_str(&content[..start]); + new_content.push_str(&block); + new_content.push_str(&content[end..]); + fs::write(path, new_content).map_err(|e| format!("cannot write {}: {e}", path.display()))?; + Ok(true) } /// Error type for init operations. @@ -60,28 +116,34 @@ impl From for InitError { } } -/// `liyi init` — append agent instruction to `AGENTS.md`. +/// `liyi init` — create or update the agent instruction section in `AGENTS.md`. /// -/// Creates the file if it doesn't exist; appends the block if it does. -/// If `force` is true, overwrites the agent instruction block even if -/// a `## 立意` section already exists. +/// Creates the file if it doesn't exist and appends the block if it does. If a +/// portable block is already present, returns [`InitError::AlreadyExists`] +/// unless `force` is set, in which case the existing block is replaced with the +/// current revision. pub fn init_agents_md(root: &Path, force: bool) -> Result { let agents_path = root.join("AGENTS.md"); + let block = agents_md_block(); if agents_path.is_file() { let content = fs::read_to_string(&agents_path)?; - if content.contains("## 立意") && !force { - return Err(InitError::AlreadyExists(agents_path)); + if let Some((start, end)) = find_agents_md_block(&content) { + if !force { + return Err(InitError::AlreadyExists(agents_path)); + } + let mut new_content = String::with_capacity(content.len()); + new_content.push_str(&content[..start]); + new_content.push_str(&block); + new_content.push_str(&content[end..]); + fs::write(&agents_path, new_content)?; + } else { + let mut new_content = content; + new_content.push('\n'); + new_content.push_str(&block); + fs::write(&agents_path, new_content)?; } - // Append the block - let block = agents_md_block(); - let mut new_content = content; - new_content.push('\n'); - new_content.push_str(block); - fs::write(&agents_path, new_content)?; } else { - // Create new file - let block = agents_md_block(); let content = format!("# AGENTS.md\n\n{block}"); fs::write(&agents_path, content)?; } @@ -209,15 +271,29 @@ pub fn init_sidecar( #[cfg(test)] mod tests { use super::*; + use tempfile::TempDir; #[test] fn agents_md_block_extracts_valid_template() { let block = agents_md_block(); - // Must start with the section heading. + // The shipped block carries the pragma, reminder, content, and marker. + assert!( + block.starts_with(PRAGMA_START_PREFIX), + "block must start with the START pragma" + ); assert!( - block.starts_with("## The 立意"), - "extracted block must start with the section heading" + block.contains(&pragma_line(TEMPLATE_REVISION)), + "block must carry the current revision" + ); + assert!(block.contains(REMINDER), "block must carry the reminder"); + assert!( + block.ends_with(END_MARKER), + "block must end with the END marker" + ); + assert!( + block.contains("## The 立意"), + "block must include the section heading" ); // Key invariants: the block contains the sidecar schema and @@ -240,4 +316,44 @@ mod tests { "template must include the triage JSON schema" ); } + + #[test] + fn find_agents_md_block_locates_a_block_at_any_revision() { + let text = "before\n\n\nbody\n\nafter\n"; + let (start, end) = find_agents_md_block(text).expect("block must be found"); + assert_eq!( + &text[start..end], + "\n\nbody\n" + ); + assert!(find_agents_md_block("no pragma here").is_none()); + } + + #[test] + fn migrate_agents_md_rewrites_an_older_block_idempotently() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("AGENTS.md"); + fs::write( + &path, + "# AGENTS.md\n\n\n\nstale body\n\n", + ) + .unwrap(); + + assert!(migrate_agents_md(&path).unwrap()); + let migrated = fs::read_to_string(&path).unwrap(); + assert!(migrated.contains(&pragma_line(TEMPLATE_REVISION))); + assert!(migrated.contains("## The 立意")); + assert!(!migrated.contains("stale body")); + + // Idempotent: a second run leaves the file byte-identical. + assert!(migrate_agents_md(&path).unwrap()); + assert_eq!(fs::read_to_string(&path).unwrap(), migrated); + } + + #[test] + fn migrate_agents_md_reports_absent_blocks() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("AGENTS.md"); + fs::write(&path, "# AGENTS.md\n\nNo pragma here.\n").unwrap(); + assert!(!migrate_agents_md(&path).unwrap()); + } } diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index b04815b..6dc7737 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -16,48 +16,120 @@ "source_anchor": "const AGENTS_MD_FULL: &str = include_str!(\"../../../AGENTS.md\");" }, { - "item": "TEMPLATE_START", + "item": "TEMPLATE_REVISION", + "reviewed": false, + "intent": "=self-doc", + "source_span": [ + 16, + 16 + ], + "tree_path": "const.TEMPLATE_REVISION", + "source_hash": "sha256:d7c0b12409c8d457e6d0465410f2ef6fde03b25a04f28ade7f4fc3ac7a5f4bc6", + "source_anchor": "pub const TEMPLATE_REVISION: u32 = 1;" + }, + { + "item": "PRAGMA_START_PREFIX", "reviewed": false, "intent": "=trivial", "source_span": [ - 12, - 12 + 19, + 19 ], - "tree_path": "const.TEMPLATE_START", - "source_hash": "sha256:8d34ecdc56fb1fc11f75843c0e4d88a17c1a6df74fe29fdc091c422984331a91", - "source_anchor": "const TEMPLATE_START: &str = \"\\n\\n\";" + "tree_path": "const.PRAGMA_START_PREFIX", + "source_hash": "sha256:5d8a40c558d26e13d92efd83f8debd82257865100f7273e69c9ca8136dd26ce7", + "source_anchor": "const PRAGMA_START_PREFIX: &str = \"\";" + "tree_path": "const.PRAGMA_SUFFIX", + "source_hash": "sha256:60bb6d7545669e0bc92e5f2e24c8d6889188c43a98f502b88532b4808566c0e0", + "source_anchor": "const PRAGMA_SUFFIX: &str = \" -->\";" }, { - "item": "agents_md_block", + "item": "REMINDER", "reviewed": false, - "intent": "Extract the portable agent instruction block from the compile-time-included AGENTS.md by finding the text after the `START liyi agent instructions rev. N` pragma (and its DON'T EDIT reminder) and before the `END liyi agent instructions` marker. Must panic if the pragma or end marker is missing.", + "intent": "=trivial", "source_span": [ 23, - 33 + 24 + ], + "tree_path": "const.REMINDER", + "source_hash": "sha256:2797c357b6d27729c53c87772900ee1fa84dcd61aee7f6482bd1919e8345de86", + "source_anchor": "const REMINDER: &str =" + }, + { + "item": "END_MARKER", + "reviewed": false, + "intent": "=trivial", + "source_span": [ + 26, + 26 + ], + "tree_path": "const.END_MARKER", + "source_hash": "sha256:ef40ba603cf9fdf6ad6916acaea2c30fcb80a5c184d400b55b0a1cde221f823e", + "source_anchor": "const END_MARKER: &str = \"\";" + }, + { + "item": "pragma_line", + "reviewed": false, + "intent": "=trivial", + "source_span": [ + 29, + 31 + ], + "tree_path": "fn.pragma_line", + "source_hash": "sha256:5ee49009ad56ef46c546c72292f8c96500f537d5476f23890bb9c266cf5f77bf", + "source_anchor": "fn pragma_line(rev: u32) -> String {" + }, + { + "item": "agents_md_block", + "reviewed": false, + "intent": "Extract the portable agent instruction block from the compile-time-included AGENTS.md, re-rendered at TEMPLATE_REVISION and including the pragma, reminder, and END marker. Must panic if the pragma or end marker is missing.", + "source_span": [ + 41, + 55 ], "tree_path": "fn.agents_md_block", - "source_hash": "sha256:c74950abdb98df0773c9f3d8e398b650838beba7b204f86ef748aaadb777ab2c", - "source_anchor": "fn agents_md_block() -> &'static str {" + "source_hash": "sha256:a5d5d49815883b0619f35017aa79ccc1d693fab6e84e0b94254000c4a9ee3d6b", + "source_anchor": "fn agents_md_block() -> String {" + }, + { + "item": "find_agents_md_block", + "reviewed": false, + "intent": "Return the byte range from the START pragma through the END marker in text, or None when no block is present. Must not inspect the revision, so a block written by an older liyi is still found.", + "source_span": [ + 62, + 66 + ], + "tree_path": "fn.find_agents_md_block", + "source_hash": "sha256:43f12077d2c8da65aee38604f315653d789c09d9ec76366ee431271e51a08772", + "source_anchor": "pub fn find_agents_md_block(text: &str) -> Option<(usize, usize)> {" + }, + { + "item": "migrate_agents_md", + "reviewed": false, + "intent": "Rewrite the agent-instruction block in path to the current TEMPLATE_REVISION, preserving the surrounding content, and return Ok(false) when the file has no block. Must be idempotent.", + "source_span": [ + 73, + 89 + ], + "tree_path": "fn.migrate_agents_md", + "source_hash": "sha256:4108930e2ab51f6fbb91a3ce5c066f9c9fbefbf07049883a968326645c17310e", + "source_anchor": "pub fn migrate_agents_md(path: &Path) -> Result {" }, { "item": "InitError", "reviewed": true, "intent": "Error type with two variants: AlreadyExists (target file exists and --force was not set) and Io (underlying I/O error). Must implement Display and From.", "source_span": [ - 37, - 42 + 93, + 98 ], "tree_path": "enum.InitError", "source_hash": "sha256:90a3bd7254f7d5a74ce2d55135699a5d8a0caedb42a3866f1ab9d5629676210e", @@ -65,14 +137,14 @@ }, { "item": "init_agents_md", - "reviewed": true, - "intent": "If AGENTS.md does not exist, create it with a heading and the agent block extracted from the repo's own AGENTS.md via agents_md_block(). If it exists but does not contain '## 立意', append the block. If '## 立意' is present and force is false, return AlreadyExists. If force is true, append regardless.", + "reviewed": false, + "intent": "Create AGENTS.md when absent, writing `# AGENTS.md` plus the portable agent-instruction block. When the file already contains such a block, return AlreadyExists unless force is set, in which case replace the existing block with the current revision; when the file exists without a block, append it after a blank line. Must key off the block pragma, never the section heading.", "source_span": [ - 68, - 90 + 125, + 152 ], "tree_path": "fn.init_agents_md", - "source_hash": "sha256:71c4198c9e3243a50ecc0e95de56c2d05dc1d6829747f5071859468533b557f7", + "source_hash": "sha256:2c815666a33e3001f38c27c303e582e1fae5dfa1d0cad55d5770b5339c6b1d28", "source_anchor": "pub fn init_agents_md(root: &Path, force: bool) -> Result {" }, { @@ -80,8 +152,8 @@ "reviewed": true, "intent": "Create a .liyi.jsonc sidecar next to the source file. When discover is true and the language is tree-sitter-supported, pre-populate specs with discovered items including _hints (_has_doc, _body_lines, _likely_trivial based on trivial_threshold). Always scan the source file for @liyi:requirement markers and emit corresponding RequirementSpec entries regardless of file type or discover flag. When discover is false, emit only requirement specs (no items). If the sidecar exists and force is false, return AlreadyExists. Sidecar file name must be .liyi.jsonc.", "source_span": [ - 111, - 207 + 173, + 269 ], "tree_path": "fn.init_sidecar", "source_hash": "sha256:bc2c80e5d18b5780374676c330ec4ff2cc2304ed41be1cb350529fd56d8b3eb6", diff --git a/docs/liyi-design.md b/docs/liyi-design.md index c93bdd2..a4708bd 100644 --- a/docs/liyi-design.md +++ b/docs/liyi-design.md @@ -1820,11 +1820,11 @@ Sets `"reviewed": true` and fixes hashes without prompting. **Repository initialization:** ```bash -liyi init # scaffold AGENTS.md with the 立意 instruction paragraph -liyi init --force # overwrite existing AGENTS.md +liyi init # scaffold AGENTS.md with the 立意 instruction section +liyi init --force # replace the agent instruction block with the current revision ``` -Appends the 立意 agent instruction section (~300 lines: behavioral rules + two JSON schemas) to `AGENTS.md` (creates the file if absent). Does not overwrite existing content unless `--force` is given. +Writes the portable agent instruction section (~300 lines: behavioral rules + two JSON schemas) into `AGENTS.md`, creating the file if absent, wrapped in the `START liyi agent instructions rev. N` pragma and its `DON'T EDIT` reminder. The pragma makes the block idempotent and migratable rather than heading-dependent: a second `liyi init` refuses instead of duplicating, and `liyi init --force` replaces an existing block with the current revision. **File initialization:** From 463d5a59378c41df94824df2521cda42952ecca1 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:02:12 +0800 Subject: [PATCH 30/44] feat(linter): migrate agent-instruction blocks with liyi migrate `liyi migrate` previously walked only `.liyi.jsonc` sidecars. It now also refreshes the versioned agent-instruction block in an `AGENTS.md`: - `resolve_agents_md_targets()` finds files carrying the `START liyi agent instructions` pragma: in directory mode it collects files named `AGENTS.md`, and an explicitly-passed file is accepted wherever it lives so a repository's preferred location can be migrated directly. Targeting `AGENTS.md` by name keeps the design doc's heading-less source block from ever being rewritten. - The CLI dispatches sidecar targets to schema migration and agent files to `migrate_agents_md()`, and succeeds when either kind is found (instead of requiring a sidecar). Covered by new discovery unit tests and CLI tests for a stale block, a directory containing both kinds of target, and init idempotency. Original prompt: > let's go. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi-cli/src/cli.rs | 2 +- crates/liyi-cli/src/cli.rs.liyi.jsonc | 4 +- crates/liyi-cli/src/main.rs | 41 ++++++++++-- crates/liyi-cli/src/main.rs.liyi.jsonc | 20 +++--- crates/liyi-cli/tests/cli.rs | 78 +++++++++++++++++++++++ crates/liyi/src/discovery.rs | 85 +++++++++++++++++++++++++ crates/liyi/src/discovery.rs.liyi.jsonc | 24 +++++++ 7 files changed, 237 insertions(+), 17 deletions(-) diff --git a/crates/liyi-cli/src/cli.rs b/crates/liyi-cli/src/cli.rs index 6dba0ae..9fe2120 100644 --- a/crates/liyi-cli/src/cli.rs +++ b/crates/liyi-cli/src/cli.rs @@ -72,7 +72,7 @@ pub enum Commands { prompt: bool, }, - /// Migrate sidecar files to the current schema version + /// Migrate sidecar files and agent instruction blocks to the current versions Migrate { /// Sidecar files or directories to migrate (recursive) files: Vec, diff --git a/crates/liyi-cli/src/cli.rs.liyi.jsonc b/crates/liyi-cli/src/cli.rs.liyi.jsonc index 8eca112..b3b8b0b 100644 --- a/crates/liyi-cli/src/cli.rs.liyi.jsonc +++ b/crates/liyi-cli/src/cli.rs.liyi.jsonc @@ -29,14 +29,14 @@ }, { "item": "Commands", - "reviewed": true, + "reviewed": false, "intent": "Enumerate all CLI subcommands (Check, Migrate, Init, Context, Approve) with their full set of flags and arguments, providing defaults and mutual constraints. Init includes --no-discover and --trivial-threshold. Context takes a `` or `:` target and an optional --root override. Approve includes mutually exclusive kind filters: --unreviewed-only, --stale-only, --req-only.", "source_span": [ 28, 139 ], "tree_path": "enum.Commands", - "source_hash": "sha256:e89ae347b2740462601beeece7d4ec5e24135706e545e8ec19904ec8bcb4c13e", + "source_hash": "sha256:9bd42fe639060abbdc278d30ae577ab9958456be4b0147e43f8b391bbead7cc2", "source_anchor": "pub enum Commands {" } ] diff --git a/crates/liyi-cli/src/main.rs b/crates/liyi-cli/src/main.rs index 3f2f549..09e3b0e 100644 --- a/crates/liyi-cli/src/main.rs +++ b/crates/liyi-cli/src/main.rs @@ -1,5 +1,6 @@ use std::env; use std::io::{self, IsTerminal}; +use std::path::PathBuf; use std::process; use clap::Parser; @@ -115,21 +116,42 @@ fn main() { process::exit(2); } - let targets = match liyi::discovery::resolve_sidecar_targets(&files) { + let agents_files = match liyi::discovery::resolve_agents_md_targets(&files) { Ok(t) => t, Err(e) => { eprintln!("Error: {e}"); process::exit(2); } }; + // Explicit agent-instruction files are not source files, so keep + // them out of sidecar resolution (which would otherwise demand a + // co-located `.liyi.jsonc`). + let sidecar_inputs: Vec = files + .iter() + .filter(|p| !agents_files.contains(p)) + .cloned() + .collect(); + let sidecar_targets = if sidecar_inputs.is_empty() { + Vec::new() + } else { + match liyi::discovery::resolve_sidecar_targets(&sidecar_inputs) { + Ok(t) => t, + Err(e) => { + eprintln!("Error: {e}"); + process::exit(2); + } + } + }; - if targets.is_empty() { - eprintln!("no .liyi.jsonc files found in the given paths"); + if sidecar_targets.is_empty() && agents_files.is_empty() { + eprintln!( + "no .liyi.jsonc sidecars or agent-instruction blocks found in the given paths" + ); process::exit(2); } let mut errors = 0; - for sidecar_path in &targets { + for sidecar_path in &sidecar_targets { match liyi::reanchor::run_reanchor(sidecar_path, None, None, true) { Ok(()) => { println!("Migrated: {}", sidecar_path.display()); @@ -140,6 +162,17 @@ fn main() { } } } + for agents_path in &agents_files { + match liyi::init::migrate_agents_md(agents_path) { + Ok(_) => { + println!("Migrated: {}", agents_path.display()); + } + Err(e) => { + eprintln!("Error ({}): {e}", agents_path.display()); + errors += 1; + } + } + } if errors > 0 { process::exit(2); diff --git a/crates/liyi-cli/src/main.rs.liyi.jsonc b/crates/liyi-cli/src/main.rs.liyi.jsonc index 37646fb..eaab8a1 100644 --- a/crates/liyi-cli/src/main.rs.liyi.jsonc +++ b/crates/liyi-cli/src/main.rs.liyi.jsonc @@ -8,8 +8,8 @@ "reviewed": true, "intent": "Reset SIGPIPE to SIG_DFL so that piping output into programs like `head` causes silent termination instead of a panic.", "source_span": [ - 15, - 19 + 16, + 20 ], "tree_path": "fn.reset_sigpipe", "source_hash": "sha256:88c175f1caa9ce830cd8d1f4e54d8942fa5f709dbc2589d8291a7dbe3dcffbd0", @@ -17,14 +17,14 @@ }, { "item": "main", - "reviewed": true, + "reviewed": false, "intent": "=doc", "source_span": [ - 26, - 294 + 27, + 327 ], "tree_path": "fn.main", - "source_hash": "sha256:b4e0b493e99a15f9fd1cca0946da94ddcf8c800f4b0b4cfce6fadb031de9f39f", + "source_hash": "sha256:4ecab6146b1fd1629edd6d58e9a49561d3cccdd166842ec77c3d93f55642ce92", "source_anchor": "fn main() {" }, { @@ -32,8 +32,8 @@ "reviewed": true, "intent": "Parse the target into a path (and optional trailing :line), locate the repo root, resolve applicable notes via the live note scan, print the rendered report to stdout, and exit 0. Exit 2 on usage or resolution errors (bad line number, path outside the repo, missing root).", "source_span": [ - 304, - 355 + 337, + 388 ], "tree_path": "fn.run_context", "source_hash": "sha256:4cf6f64065c246654eea0d69ba8cc555065004c96785ae9022f8722bc9eed0da", @@ -44,8 +44,8 @@ "reviewed": true, "intent": "=doc", "source_span": [ - 361, - 363 + 394, + 396 ], "tree_path": "fn.is_tty", "source_hash": "sha256:021dc096ffc5aa501d5fe90e8fd1835c4ce784a7de59434995a9be221f97e40f", diff --git a/crates/liyi-cli/tests/cli.rs b/crates/liyi-cli/tests/cli.rs index 7558a06..3aa6494 100644 --- a/crates/liyi-cli/tests/cli.rs +++ b/crates/liyi-cli/tests/cli.rs @@ -121,6 +121,84 @@ fn init_creates_agents_md_when_absent() { content.contains("立意"), "generated AGENTS.md should contain the liyi template block" ); + assert!( + content.contains("START liyi agent instructions"), + "generated AGENTS.md should carry the pragma, got: {content}" + ); +} + +#[test] +fn init_is_idempotent_for_agents_md() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + + let first = run_in(root, &["init"]); + assert!(first.status.success(), "first init: {:?}", stderr(&first)); + + let second = run_in(root, &["init"]); + assert!( + !second.status.success(), + "a second init should refuse rather than duplicate the block" + ); + assert!(stderr(&second).contains("already exists")); + + let content = std::fs::read_to_string(root.join("AGENTS.md")).unwrap(); + assert_eq!( + content.matches("START liyi agent instructions").count(), + 1, + "init must not append a duplicate block" + ); +} + +#[test] +fn migrate_rewrites_an_older_agents_md_block() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let agents = root.join("AGENTS.md"); + std::fs::write( + &agents, + "# AGENTS.md\n\n\n\nstale body\n\n", + ) + .unwrap(); + + let out = run_in(root, &["migrate", "AGENTS.md"]); + assert!(out.status.success(), "migrate: {:?}", stderr(&out)); + + let content = std::fs::read_to_string(&agents).unwrap(); + assert!( + content.contains("rev. 1"), + "block should be at the current revision, got: {content}" + ); + assert!( + content.contains("## The 立意"), + "block should carry the template, got: {content}" + ); + assert!( + !content.contains("stale body"), + "the stale body should be replaced" + ); +} + +#[test] +fn migrate_directory_updates_agents_md_and_sidecars() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + std::fs::write( + root.join("AGENTS.md"), + "# AGENTS.md\n\n\n\nstale body\n\n", + ) + .unwrap(); + + let src = root.join("sample.rs"); + std::fs::write(&src, "pub fn f() {}\n").unwrap(); + run_in(root, &["init", "sample.rs"]); + + let out = run_in(root, &["migrate", "."]); + assert!(out.status.success(), "migrate: {:?}", stderr(&out)); + + let content = std::fs::read_to_string(root.join("AGENTS.md")).unwrap(); + assert!(content.contains("rev. 1")); + assert!(!content.contains("stale body")); } #[test] diff --git a/crates/liyi/src/discovery.rs b/crates/liyi/src/discovery.rs index ddd4bed..6490187 100644 --- a/crates/liyi/src/discovery.rs +++ b/crates/liyi/src/discovery.rs @@ -286,6 +286,52 @@ fn sidecar_path_for_source(source: &Path) -> PathBuf { source.with_file_name(format!("{name}{SIDECAR_SUFFIX}")) } +/// Expand a list of file/directory paths into agent-instruction files. +/// +/// A file qualifies when it contains the portable +/// `START liyi agent instructions` pragma. Directories are walked for files +/// named `AGENTS.md` (the `liyi init` default); an explicitly-passed file is +/// included whenever it carries the pragma, so a repository's preferred +/// location can be migrated directly. +pub fn resolve_agents_md_targets(paths: &[PathBuf]) -> Result, String> { + let mut result: Vec = Vec::new(); + for p in paths { + if p.is_dir() { + let walker = WalkBuilder::new(p) + .hidden(false) + .add_custom_ignore_filename(".liyiignore") + .build(); + for entry in walker { + let entry = entry.map_err(|e| format!("walk error: {e}"))?; + if entry.file_type().is_some_and(|ft| ft.is_file()) + && entry.path().file_name().and_then(|n| n.to_str()) == Some("AGENTS.md") + && has_agents_md_block(entry.path()) + { + result.push(entry.into_path()); + } + } + } else if p.is_file() { + if has_agents_md_block(p) { + result.push(p.clone()); + } + } else { + return Err(format!("path does not exist: {}", p.display())); + } + } + result.sort(); + result.dedup(); + Ok(result) +} + +/// Whether `path` contains a portable `AGENTS.md` instruction block. +/// +/// Unreadable or binary files simply do not qualify. +fn has_agents_md_block(path: &Path) -> bool { + std::fs::read_to_string(path) + .map(|text| crate::init::find_agents_md_block(&text).is_some()) + .unwrap_or(false) +} + /// Compute `path` relative to `base` using pure lexical processing. fn pathdiff(path: &Path, base: &Path) -> Option { path.strip_prefix(base) @@ -459,4 +505,43 @@ mod tests { assert!(err.contains("no sidecar found"), "got: {err}"); assert!(err.contains("foo.rs.liyi.jsonc"), "got: {err}"); } + + #[test] + fn resolve_agents_md_targets_finds_agents_md_in_a_directory() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::write( + root.join("AGENTS.md"), + "\n\nbody\n\n", + ) + .unwrap(); + fs::write(root.join("OTHER.md"), "no pragma here").unwrap(); + + let targets = resolve_agents_md_targets(&[root.to_path_buf()]).unwrap(); + assert_eq!(targets, vec![root.join("AGENTS.md")]); + } + + #[test] + fn resolve_agents_md_targets_includes_explicit_files_with_pragma() { + let tmp = TempDir::new().unwrap(); + let custom = tmp.path().join("agent-instructions.md"); + fs::write( + &custom, + "\n\nbody\n\n", + ) + .unwrap(); + + let targets = resolve_agents_md_targets(std::slice::from_ref(&custom)).unwrap(); + assert_eq!(targets, vec![custom]); + } + + #[test] + fn resolve_agents_md_targets_skips_files_without_pragma() { + let tmp = TempDir::new().unwrap(); + let agents = tmp.path().join("AGENTS.md"); + fs::write(&agents, "# AGENTS.md\n\nno pragma\n").unwrap(); + + let targets = resolve_agents_md_targets(std::slice::from_ref(&agents)).unwrap(); + assert!(targets.is_empty()); + } } diff --git a/crates/liyi/src/discovery.rs.liyi.jsonc b/crates/liyi/src/discovery.rs.liyi.jsonc index 6706ed0..55c8b45 100644 --- a/crates/liyi/src/discovery.rs.liyi.jsonc +++ b/crates/liyi/src/discovery.rs.liyi.jsonc @@ -95,6 +95,30 @@ "tree_path": "fn.sidecar_path_for_source", "source_hash": "sha256:552d2551c532e7542d3d3bce735fcc3715b9af100b67481f282a74df37e49b2c", "source_anchor": "fn sidecar_path_for_source(source: &Path) -> PathBuf {" + }, + { + "item": "resolve_agents_md_targets", + "reviewed": false, + "intent": "Expand a list of file/directory paths into agent-instruction files. A file qualifies when it contains the portable `START liyi agent instructions` pragma. For a directory, walk it (respecting .gitignore and .liyiignore) and include files named AGENTS.md that carry the pragma; an explicitly-passed file is included whenever it carries the pragma, so a repository's preferred location can be migrated directly. Error if a path does not exist. Return the deduplicated, sorted target list.", + "source_span": [ + 296, + 324 + ], + "tree_path": "fn.resolve_agents_md_targets", + "source_hash": "sha256:b507dd7b8df16e8f06c7800741564e439cb5d0c3780049756e85b0756939659f", + "source_anchor": "pub fn resolve_agents_md_targets(paths: &[PathBuf]) -> Result, String> {" + }, + { + "item": "has_agents_md_block", + "reviewed": false, + "intent": "=self-doc", + "source_span": [ + 329, + 333 + ], + "tree_path": "fn.has_agents_md_block", + "source_hash": "sha256:7bf55e6bd57ba5e329ac41f4a57f1186c447fe939b03a3aabc6b9c0132e6dea9", + "source_anchor": "fn has_agents_md_block(path: &Path) -> bool {" } ] } From ae61c75639f88a5330b846e892abc1228aa62099 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:22:05 +0800 Subject: [PATCH 31/44] docs: liyi approve Signed-off-by: WANG Xuerui --- .github/workflows/ci.yml.liyi.jsonc | 4 ++-- crates/liyi-cli/src/cli.rs.liyi.jsonc | 2 +- crates/liyi-cli/src/main.rs.liyi.jsonc | 2 +- crates/liyi/src/approve.rs.liyi.jsonc | 4 ++-- crates/liyi/src/check.rs.liyi.jsonc | 2 +- crates/liyi/src/context.rs.liyi.jsonc | 4 ++-- crates/liyi/src/diagnostics.rs.liyi.jsonc | 2 +- crates/liyi/src/discovery.rs.liyi.jsonc | 4 ++-- crates/liyi/src/init.rs.liyi.jsonc | 24 +++++++++---------- crates/liyi/src/prompt.rs.liyi.jsonc | 2 +- crates/liyi/src/recovery.rs.liyi.jsonc | 2 +- .../src/tree_path/inject/mod.rs.liyi.jsonc | 4 ++-- .../src/tree_path/lang_python.rs.liyi.jsonc | 4 ++-- crates/liyi/src/tree_path/mod.rs.liyi.jsonc | 2 +- scripts/check-template-sync.py.liyi.jsonc | 2 +- 15 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml.liyi.jsonc b/.github/workflows/ci.yml.liyi.jsonc index cc23ad3..c29118c 100644 --- a/.github/workflows/ci.yml.liyi.jsonc +++ b/.github/workflows/ci.yml.liyi.jsonc @@ -89,8 +89,8 @@ }, { "item": "jobs::check-template-sync", - "reviewed": false, - "intent": "Run scripts/check-template-sync.py to verify that AGENTS.md materializes docs/liyi-design.md's liyi agent-instructions block, with the section heading added. Must fail the PR if the two diverge.", + "reviewed": true, + "intent": "Verify that AGENTS.md is derived from docs/liyi-design.md's liyi agent-instructions block, with the section heading added. Must fail the PR if the two diverge.", "source_span": [ 75, 80 diff --git a/crates/liyi-cli/src/cli.rs.liyi.jsonc b/crates/liyi-cli/src/cli.rs.liyi.jsonc index b3b8b0b..8de9c6c 100644 --- a/crates/liyi-cli/src/cli.rs.liyi.jsonc +++ b/crates/liyi-cli/src/cli.rs.liyi.jsonc @@ -29,7 +29,7 @@ }, { "item": "Commands", - "reviewed": false, + "reviewed": true, "intent": "Enumerate all CLI subcommands (Check, Migrate, Init, Context, Approve) with their full set of flags and arguments, providing defaults and mutual constraints. Init includes --no-discover and --trivial-threshold. Context takes a `` or `:` target and an optional --root override. Approve includes mutually exclusive kind filters: --unreviewed-only, --stale-only, --req-only.", "source_span": [ 28, diff --git a/crates/liyi-cli/src/main.rs.liyi.jsonc b/crates/liyi-cli/src/main.rs.liyi.jsonc index eaab8a1..2fe1e3b 100644 --- a/crates/liyi-cli/src/main.rs.liyi.jsonc +++ b/crates/liyi-cli/src/main.rs.liyi.jsonc @@ -17,7 +17,7 @@ }, { "item": "main", - "reviewed": false, + "reviewed": true, "intent": "=doc", "source_span": [ 27, diff --git a/crates/liyi/src/approve.rs.liyi.jsonc b/crates/liyi/src/approve.rs.liyi.jsonc index 07348e5..028203a 100644 --- a/crates/liyi/src/approve.rs.liyi.jsonc +++ b/crates/liyi/src/approve.rs.liyi.jsonc @@ -101,7 +101,7 @@ }, { "item": "RequirementInfo", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 232, @@ -113,7 +113,7 @@ }, { "item": "CandidateFileCtx", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 240, diff --git a/crates/liyi/src/check.rs.liyi.jsonc b/crates/liyi/src/check.rs.liyi.jsonc index 8db990c..6b7c67a 100644 --- a/crates/liyi/src/check.rs.liyi.jsonc +++ b/crates/liyi/src/check.rs.liyi.jsonc @@ -311,7 +311,7 @@ }, { "item": "check_trivial_ignore", - "reviewed": false, + "reviewed": true, "intent": "Emit Trivial or Ignored info diagnostics for @liyi:trivial and @liyi:ignore markers within the item's span. For the sidecar =trivial and =self-doc sentinels, emit Trivial info unless @liyi:nontrivial is present in the span, in which case emit ConflictingTriviality error. Both sentinels are treated identically for tracking.", "source_span": [ 1584, diff --git a/crates/liyi/src/context.rs.liyi.jsonc b/crates/liyi/src/context.rs.liyi.jsonc index 565fb1f..d0ee125 100644 --- a/crates/liyi/src/context.rs.liyi.jsonc +++ b/crates/liyi/src/context.rs.liyi.jsonc @@ -53,7 +53,7 @@ }, { "item": "ResolvedNote", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 52, @@ -80,7 +80,7 @@ }, { "item": "parent_dir", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 67, diff --git a/crates/liyi/src/diagnostics.rs.liyi.jsonc b/crates/liyi/src/diagnostics.rs.liyi.jsonc index 13566f8..b5aa544 100644 --- a/crates/liyi/src/diagnostics.rs.liyi.jsonc +++ b/crates/liyi/src/diagnostics.rs.liyi.jsonc @@ -107,7 +107,7 @@ }, { "item": "format_github_actions", - "reviewed": false, + "reviewed": true, "intent": "Format a single diagnostic as a GitHub Actions workflow command (::notice, ::warning, or ::error) with file, line, and title parameters. Maps severity to annotation level, strips the repo root from file paths, escapes %, \\r, \\n per the Actions toolkit spec, uses the item name with a severity icon as the annotation title, and appends the intent text to the message body when present and non-sentinel (not =doc, =trivial, or =self-doc).", "source_span": [ 394, diff --git a/crates/liyi/src/discovery.rs.liyi.jsonc b/crates/liyi/src/discovery.rs.liyi.jsonc index 55c8b45..3422715 100644 --- a/crates/liyi/src/discovery.rs.liyi.jsonc +++ b/crates/liyi/src/discovery.rs.liyi.jsonc @@ -98,7 +98,7 @@ }, { "item": "resolve_agents_md_targets", - "reviewed": false, + "reviewed": true, "intent": "Expand a list of file/directory paths into agent-instruction files. A file qualifies when it contains the portable `START liyi agent instructions` pragma. For a directory, walk it (respecting .gitignore and .liyiignore) and include files named AGENTS.md that carry the pragma; an explicitly-passed file is included whenever it carries the pragma, so a repository's preferred location can be migrated directly. Error if a path does not exist. Return the deduplicated, sorted target list.", "source_span": [ 296, @@ -110,7 +110,7 @@ }, { "item": "has_agents_md_block", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 329, diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index 6dc7737..0172fa5 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -17,8 +17,8 @@ }, { "item": "TEMPLATE_REVISION", - "reviewed": false, - "intent": "=self-doc", + "reviewed": true, + "intent": "=doc", "source_span": [ 16, 16 @@ -29,7 +29,7 @@ }, { "item": "PRAGMA_START_PREFIX", - "reviewed": false, + "reviewed": true, "intent": "=trivial", "source_span": [ 19, @@ -41,7 +41,7 @@ }, { "item": "PRAGMA_SUFFIX", - "reviewed": false, + "reviewed": true, "intent": "=trivial", "source_span": [ 21, @@ -53,7 +53,7 @@ }, { "item": "REMINDER", - "reviewed": false, + "reviewed": true, "intent": "=trivial", "source_span": [ 23, @@ -65,7 +65,7 @@ }, { "item": "END_MARKER", - "reviewed": false, + "reviewed": true, "intent": "=trivial", "source_span": [ 26, @@ -77,8 +77,8 @@ }, { "item": "pragma_line", - "reviewed": false, - "intent": "=trivial", + "reviewed": true, + "intent": "=doc", "source_span": [ 29, 31 @@ -89,7 +89,7 @@ }, { "item": "agents_md_block", - "reviewed": false, + "reviewed": true, "intent": "Extract the portable agent instruction block from the compile-time-included AGENTS.md, re-rendered at TEMPLATE_REVISION and including the pragma, reminder, and END marker. Must panic if the pragma or end marker is missing.", "source_span": [ 41, @@ -101,7 +101,7 @@ }, { "item": "find_agents_md_block", - "reviewed": false, + "reviewed": true, "intent": "Return the byte range from the START pragma through the END marker in text, or None when no block is present. Must not inspect the revision, so a block written by an older liyi is still found.", "source_span": [ 62, @@ -113,7 +113,7 @@ }, { "item": "migrate_agents_md", - "reviewed": false, + "reviewed": true, "intent": "Rewrite the agent-instruction block in path to the current TEMPLATE_REVISION, preserving the surrounding content, and return Ok(false) when the file has no block. Must be idempotent.", "source_span": [ 73, @@ -137,7 +137,7 @@ }, { "item": "init_agents_md", - "reviewed": false, + "reviewed": true, "intent": "Create AGENTS.md when absent, writing `# AGENTS.md` plus the portable agent-instruction block. When the file already contains such a block, return AlreadyExists unless force is set, in which case replace the existing block with the current revision; when the file exists without a block, append it after a blank line. Must key off the block pragma, never the section heading.", "source_span": [ 125, diff --git a/crates/liyi/src/prompt.rs.liyi.jsonc b/crates/liyi/src/prompt.rs.liyi.jsonc index 0e55935..5ab3a3f 100644 --- a/crates/liyi/src/prompt.rs.liyi.jsonc +++ b/crates/liyi/src/prompt.rs.liyi.jsonc @@ -29,7 +29,7 @@ }, { "item": "GroupMeta", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 73, diff --git a/crates/liyi/src/recovery.rs.liyi.jsonc b/crates/liyi/src/recovery.rs.liyi.jsonc index b02de2c..a1c994a 100644 --- a/crates/liyi/src/recovery.rs.liyi.jsonc +++ b/crates/liyi/src/recovery.rs.liyi.jsonc @@ -5,7 +5,7 @@ "specs": [ { "item": "RecoveryMethod", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 6, diff --git a/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc b/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc index 4bf286f..87d4adf 100644 --- a/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc +++ b/crates/liyi/src/tree_path/inject/mod.rs.liyi.jsonc @@ -165,7 +165,7 @@ }, { "item": "find_descendant_by_kind", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 222, @@ -177,7 +177,7 @@ }, { "item": "find_flow_scalar", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 237, diff --git a/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc b/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc index 19674c3..f670c9d 100644 --- a/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc +++ b/crates/liyi/src/tree_path/lang_python.rs.liyi.jsonc @@ -5,7 +5,7 @@ "specs": [ { "item": "python_node_name", - "reviewed": false, + "reviewed": true, "intent": "Custom name extractor for Python module-level constants: for an assignment whose parent expression_statement sits directly under the module root and whose target is a plain identifier, return that identifier; return None for local bindings and attribute assignments inside function or class bodies, and for non-assignment nodes.", "source_span": [ 43, @@ -17,7 +17,7 @@ }, { "item": "CONFIG", - "reviewed": false, + "reviewed": true, "intent": "Define the Python language configuration for tree_path resolution: register tree-sitter-python grammar, .py/.pyi extensions, kind mappings for fn (function_definition), class (class_definition), and const (assignment) for module-level constants, use the standard name field for fn/class and the python_node_name callback for const, the python_has_doc_comment detector for triple-quoted docstrings, and expression_statement as a transparent kind so the resolver reaches module-level assignment nodes.", "source_span": [ 63, diff --git a/crates/liyi/src/tree_path/mod.rs.liyi.jsonc b/crates/liyi/src/tree_path/mod.rs.liyi.jsonc index 4ccc5f5..f65b2ff 100644 --- a/crates/liyi/src/tree_path/mod.rs.liyi.jsonc +++ b/crates/liyi/src/tree_path/mod.rs.liyi.jsonc @@ -268,7 +268,7 @@ }, { "item": "find_ancestor_pair", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 934, diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc index 11f2dd1..8e929a1 100644 --- a/scripts/check-template-sync.py.liyi.jsonc +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -5,7 +5,7 @@ "specs": [ { "item": "HEADING", - "reviewed": false, + "reviewed": true, "intent": "=self-doc", "source_span": [ 33, From 5e5e00be2d4e9a46b6cb48d78fa59c42d0a19c94 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:26:50 +0800 Subject: [PATCH 32/44] docs: link the agent instruction block delimiters to the requirement too Signed-off-by: WANG Xuerui --- scripts/check-template-sync.py | 4 ++- scripts/check-template-sync.py.liyi.jsonc | 34 +++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/scripts/check-template-sync.py b/scripts/check-template-sync.py index b83df0e..68f90ee 100644 --- a/scripts/check-template-sync.py +++ b/scripts/check-template-sync.py @@ -28,10 +28,12 @@ import sys from pathlib import Path +# @liyi:related agents-md-instructions-section-naming START = "\n" +# @liyi:related agents-md-instructions-section-naming END = "" -HEADING = "## The 立意 (Intent Specs) design pattern for agents" # @liyi:related agents-md-instructions-section-naming +HEADING = "## The 立意 (Intent Specs) design pattern for agents" def read(path: Path) -> str: diff --git a/scripts/check-template-sync.py.liyi.jsonc b/scripts/check-template-sync.py.liyi.jsonc index 8e929a1..83dcdda 100644 --- a/scripts/check-template-sync.py.liyi.jsonc +++ b/scripts/check-template-sync.py.liyi.jsonc @@ -3,13 +3,43 @@ "version": "0.1", "source": "scripts/check-template-sync.py", "specs": [ + { + "item": "START", + "reviewed": true, + "intent": "=self-doc", + "source_span": [ + 32, + 32 + ], + "tree_path": "const.START", + "source_hash": "sha256:2a5069ba2b819cef252dde6ec221dd83160f319fe07dff10a6a24b1300e84aac", + "source_anchor": "START = \"\\n\"", + "related": { + "agents-md-instructions-section-naming": "sha256:ba9f88f5edb0b338487e493235b49269cfc4ec74d03ebcfe33e1ec7bdc39d298" + } + }, + { + "item": "END", + "reviewed": true, + "intent": "=self-doc", + "source_span": [ + 34, + 34 + ], + "tree_path": "const.END", + "source_hash": "sha256:5c2d6107a704fe852e8434e1932942623fada1d315683e0bb7be968ff740eb8a", + "source_anchor": "END = \"\"", + "related": { + "agents-md-instructions-section-naming": "sha256:ba9f88f5edb0b338487e493235b49269cfc4ec74d03ebcfe33e1ec7bdc39d298" + } + }, { "item": "HEADING", "reviewed": true, "intent": "=self-doc", "source_span": [ - 33, - 33 + 36, + 36 ], "tree_path": "const.HEADING", "source_hash": "sha256:b2d628369d56e7a4f2c9cde920a380f6845dc0ef9a4de913b2e90c68e4b13146", From 44a550037b59495cd3c14cf80cf8bcd836495f2d Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:30:40 +0800 Subject: [PATCH 33/44] test(linter): cover liyi init agent-instructions behavior Close the gaps left by the pragma work. Unit (`init.rs`): - create ships a pragma-wrapped block (pragma, reminder, closing marker, exactly one block), - an existing file without a block gets one appended, prefix preserved, - an existing block is refused without `--force` and replaced in place with `--force` (no duplicate). End-to-end (`liyi-cli/tests/cli.rs`, via the compiled binary): - `init` output now also asserts the reminder and closing marker, - `init --force` replaces a stale `rev. 0` block with `rev. 1` and leaves exactly one block. Original prompt: > Okay add the unit tests and E2E tests, one commit for each feature. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi-cli/tests/cli.rs | 45 +++++++++++++++++++++++++++++ crates/liyi/src/init.rs | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/crates/liyi-cli/tests/cli.rs b/crates/liyi-cli/tests/cli.rs index 3aa6494..0d59558 100644 --- a/crates/liyi-cli/tests/cli.rs +++ b/crates/liyi-cli/tests/cli.rs @@ -125,6 +125,51 @@ fn init_creates_agents_md_when_absent() { content.contains("START liyi agent instructions"), "generated AGENTS.md should carry the pragma, got: {content}" ); + assert!( + content.contains("DON'T EDIT THIS BLOCK"), + "generated AGENTS.md should carry the reminder, got: {content}" + ); + assert!( + content + .trim_end() + .ends_with(""), + "generated AGENTS.md should close the block, got: {content}" + ); +} + +#[test] +fn init_force_replaces_the_agents_md_block() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let agents = root.join("AGENTS.md"); + std::fs::write( + &agents, + "# AGENTS.md\n\n\n\nstale body\n\n", + ) + .unwrap(); + + let refused = run_in(root, &["init"]); + assert!( + !refused.status.success(), + "init should refuse an existing block without --force" + ); + + let forced = run_in(root, &["init", "--force"]); + assert!( + forced.status.success(), + "init --force: {:?}", + stderr(&forced) + ); + + let content = std::fs::read_to_string(&agents).unwrap(); + assert!(content.contains("rev. 1"), "got: {content}"); + assert!(content.contains("## The 立意"), "got: {content}"); + assert!(!content.contains("stale body"), "got: {content}"); + assert_eq!( + content.matches("START liyi agent instructions").count(), + 1, + "force must replace, not append" + ); } #[test] diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index 6eb984f..baa166a 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -356,4 +356,60 @@ mod tests { fs::write(&path, "# AGENTS.md\n\nNo pragma here.\n").unwrap(); assert!(!migrate_agents_md(&path).unwrap()); } + + #[test] + fn init_agents_md_creates_a_pragma_wrapped_block() { + let tmp = TempDir::new().unwrap(); + let path = init_agents_md(tmp.path(), false).unwrap(); + let content = fs::read_to_string(&path).unwrap(); + + assert!( + content.starts_with("# AGENTS.md\n\n\n\nstale body\n\n", + ) + .unwrap(); + + // Without --force an existing block is refused. + assert!(matches!( + init_agents_md(tmp.path(), false), + Err(InitError::AlreadyExists(_)) + )); + + // With --force the stale block is replaced in place. + init_agents_md(tmp.path(), true).unwrap(); + let content = fs::read_to_string(&path).unwrap(); + assert!(content.contains(&pragma_line(TEMPLATE_REVISION))); + assert!(content.contains("## The 立意")); + assert!(!content.contains("stale body")); + assert_eq!(content.matches(PRAGMA_START_PREFIX).count(), 1); + } } From 1d021a1e9deb558b6d23437c05d570bb0821dbb1 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:31:29 +0800 Subject: [PATCH 34/44] test(linter): cover liyi migrate target discovery and rewrites Close the remaining gaps for the migrate feature. Unit (`init.rs`, `discovery.rs`): - `migrate_agents_md` preserves the content before and after the block, - a directory walk ignores a pragma-carrying file that is not named `AGENTS.md` (the design doc's heading-less source is never targeted). End-to-end (`liyi-cli/tests/cli.rs`, via the compiled binary): - `migrate` is idempotent (a second run leaves the file byte-identical), - a directory containing only an `AGENTS.md` still succeeds, - an explicitly-passed file at a custom location is migrated, - `migrate .` updates `AGENTS.md` but leaves a heading-less `liyi-design.md`-style block untouched. Original prompt: > Okay add the unit tests and E2E tests, one commit for each feature. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi-cli/tests/cli.rs | 83 ++++++++++++++++++++++++++++++++++++ crates/liyi/src/discovery.rs | 14 ++++++ crates/liyi/src/init.rs | 17 ++++++++ 3 files changed, 114 insertions(+) diff --git a/crates/liyi-cli/tests/cli.rs b/crates/liyi-cli/tests/cli.rs index 0d59558..3c4291b 100644 --- a/crates/liyi-cli/tests/cli.rs +++ b/crates/liyi-cli/tests/cli.rs @@ -246,6 +246,89 @@ fn migrate_directory_updates_agents_md_and_sidecars() { assert!(!content.contains("stale body")); } +#[test] +fn migrate_is_idempotent_at_the_cli() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let init = run_in(root, &["init"]); + assert!(init.status.success(), "init: {:?}", stderr(&init)); + + let first = run_in(root, &["migrate", "AGENTS.md"]); + assert!(first.status.success(), "migrate: {:?}", stderr(&first)); + let after_first = std::fs::read_to_string(root.join("AGENTS.md")).unwrap(); + + let second = run_in(root, &["migrate", "AGENTS.md"]); + assert!(second.status.success(), "migrate: {:?}", stderr(&second)); + assert_eq!( + std::fs::read_to_string(root.join("AGENTS.md")).unwrap(), + after_first, + "a second migrate must leave the file byte-identical" + ); +} + +#[test] +fn migrate_directory_with_only_agents_md_succeeds() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + std::fs::write( + root.join("AGENTS.md"), + "# AGENTS.md\n\n\n\nstale\n\n", + ) + .unwrap(); + + let out = run_in(root, &["migrate", "."]); + assert!( + out.status.success(), + "a directory with only an AGENTS.md must still migrate: {:?}", + stderr(&out) + ); + let content = std::fs::read_to_string(root.join("AGENTS.md")).unwrap(); + assert!(content.contains("rev. 1"), "got: {content}"); +} + +#[test] +fn migrate_accepts_an_explicit_custom_location_file() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let custom = root.join("agent-instructions.md"); + std::fs::write( + &custom, + "# Instructions\n\n\n\n## The 立意 (Intent Specs) design pattern for agents\nstale\n\n", + ) + .unwrap(); + + let out = run_in(root, &["migrate", "agent-instructions.md"]); + assert!(out.status.success(), "migrate: {:?}", stderr(&out)); + let content = std::fs::read_to_string(&custom).unwrap(); + assert!(content.contains("rev. 1"), "got: {content}"); + assert!(content.starts_with("# Instructions\n\n"), "got: {content}"); +} + +#[test] +fn migrate_directory_leaves_heading_less_blocks_alone() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let design = root.join("liyi-design.md"); + let design_body = "\nWhen writing or modifying code:\nstale\n\n"; + std::fs::write(&design, design_body).unwrap(); + std::fs::write( + root.join("AGENTS.md"), + "# AGENTS.md\n\n\n\nstale\n\n", + ) + .unwrap(); + + let out = run_in(root, &["migrate", "."]); + assert!(out.status.success(), "migrate: {:?}", stderr(&out)); + + let agents = std::fs::read_to_string(root.join("AGENTS.md")).unwrap(); + assert!(agents.contains("rev. 1"), "got: {agents}"); + assert_eq!( + std::fs::read_to_string(&design).unwrap(), + design_body, + "a block that is not named AGENTS.md must be left untouched" + ); +} + #[test] fn init_creates_sidecar_for_source_file() { let tmp = tempfile::TempDir::new().unwrap(); diff --git a/crates/liyi/src/discovery.rs b/crates/liyi/src/discovery.rs index 6490187..b88ee83 100644 --- a/crates/liyi/src/discovery.rs +++ b/crates/liyi/src/discovery.rs @@ -544,4 +544,18 @@ mod tests { let targets = resolve_agents_md_targets(std::slice::from_ref(&agents)).unwrap(); assert!(targets.is_empty()); } + + #[test] + fn resolve_agents_md_targets_ignores_custom_named_files_in_directories() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + let block = "\nbody\n\n"; + // A block under a non-default name (e.g. the design doc's heading-less + // source) must not be picked up by a directory walk. + fs::write(root.join("instructions.md"), block).unwrap(); + fs::write(root.join("AGENTS.md"), block).unwrap(); + + let targets = resolve_agents_md_targets(&[root.to_path_buf()]).unwrap(); + assert_eq!(targets, vec![root.join("AGENTS.md")]); + } } diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index baa166a..c0263a6 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -412,4 +412,21 @@ mod tests { assert!(!content.contains("stale body")); assert_eq!(content.matches(PRAGMA_START_PREFIX).count(), 1); } + + #[test] + fn migrate_agents_md_preserves_surrounding_content() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("AGENTS.md"); + fs::write( + &path, + "# Before\n\n\nORIGINAL-BODY-MARKER\n\n\n# After\n", + ) + .unwrap(); + + migrate_agents_md(&path).unwrap(); + let content = fs::read_to_string(&path).unwrap(); + assert!(content.starts_with("# Before\n\n"), "got: {content}"); + assert!(content.ends_with("\n\n# After\n"), "got: {content}"); + assert!(!content.contains("ORIGINAL-BODY-MARKER"), "got: {content}"); + } } From 747b0095299df3f92b518ea2f3e64c1f617f260b Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 14:41:07 +0800 Subject: [PATCH 35/44] fix(linter): target migrate strictly like init, without recursion A directory argument to `liyi migrate` used to walk the whole subtree for `AGENTS.md` files. In a monorepo that cannot tell own-controlled subtrees (`subprojects/*`) from ungoverned ones (`3rdparty/*`, `vendor/*`), so recursive migration could rewrite another project's agent instruction block to our revision. Make discovery strictly mirror `liyi init`: a directory argument contributes exactly its own `AGENTS.md` and never walks. Moving through a tree is now the caller's explicit choice, one governed directory at a time, and a file argument still migrates exactly that file for a preferred location. Both commands now share `AGENTS_MD_FILENAME`, so they cannot disagree about which file they manage. Covered by a unit test (nested `AGENTS.md` is reachable only by naming its directory) and an end-to-end test (`migrate .` leaves `3rdparty/vendored/AGENTS.md` byte-identical). Original prompt: > I actually expect strict equivalence because we're not in a position to > recursively update other projects not under our governance, and right > now we lack the ability to sketch out that boundary in a monorepo. Say > repo root and `subprojects/*` are own-controlled but `3rdparty/*` and > `vendor/*` are NOT. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi-cli/tests/cli.rs | 29 ++++++++++++ crates/liyi/src/discovery.rs | 46 +++++++++++------- crates/liyi/src/discovery.rs.liyi.jsonc | 14 +++--- crates/liyi/src/init.rs | 8 +++- crates/liyi/src/init.rs.liyi.jsonc | 62 +++++++++++++++---------- docs/liyi-design.md | 2 +- 6 files changed, 110 insertions(+), 51 deletions(-) diff --git a/crates/liyi-cli/tests/cli.rs b/crates/liyi-cli/tests/cli.rs index 3c4291b..1970579 100644 --- a/crates/liyi-cli/tests/cli.rs +++ b/crates/liyi-cli/tests/cli.rs @@ -329,6 +329,35 @@ fn migrate_directory_leaves_heading_less_blocks_alone() { ); } +#[test] +fn migrate_directory_does_not_recurse_into_ungoverned_subtrees() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let vendored = root.join("3rdparty/vendored"); + std::fs::create_dir_all(&vendored).unwrap(); + let old_block = "\n\nstale\n\n"; + std::fs::write(vendored.join("AGENTS.md"), old_block).unwrap(); + std::fs::write( + root.join("AGENTS.md"), + format!("# AGENTS.md\n\n{old_block}"), + ) + .unwrap(); + + let out = run_in(root, &["migrate", "."]); + assert!(out.status.success(), "migrate: {:?}", stderr(&out)); + + assert!( + std::fs::read_to_string(root.join("AGENTS.md")) + .unwrap() + .contains("rev. 1") + ); + assert_eq!( + std::fs::read_to_string(vendored.join("AGENTS.md")).unwrap(), + old_block, + "a vendored/ungoverned subtree must not be rewritten" + ); +} + #[test] fn init_creates_sidecar_for_source_file() { let tmp = tempfile::TempDir::new().unwrap(); diff --git a/crates/liyi/src/discovery.rs b/crates/liyi/src/discovery.rs index b88ee83..e79e86e 100644 --- a/crates/liyi/src/discovery.rs +++ b/crates/liyi/src/discovery.rs @@ -288,27 +288,19 @@ fn sidecar_path_for_source(source: &Path) -> PathBuf { /// Expand a list of file/directory paths into agent-instruction files. /// -/// A file qualifies when it contains the portable -/// `START liyi agent instructions` pragma. Directories are walked for files -/// named `AGENTS.md` (the `liyi init` default); an explicitly-passed file is -/// included whenever it carries the pragma, so a repository's preferred -/// location can be migrated directly. +/// Targeting strictly mirrors `liyi init`: a directory contributes exactly its +/// own `AGENTS.md` (never a recursive walk, so ungoverned subtrees such as +/// `3rdparty/` or `vendor/` are never touched), and an explicitly-passed file +/// is included so a repository's preferred location can be migrated directly. +/// A file qualifies only when it carries the portable +/// `START liyi agent instructions` pragma. pub fn resolve_agents_md_targets(paths: &[PathBuf]) -> Result, String> { let mut result: Vec = Vec::new(); for p in paths { if p.is_dir() { - let walker = WalkBuilder::new(p) - .hidden(false) - .add_custom_ignore_filename(".liyiignore") - .build(); - for entry in walker { - let entry = entry.map_err(|e| format!("walk error: {e}"))?; - if entry.file_type().is_some_and(|ft| ft.is_file()) - && entry.path().file_name().and_then(|n| n.to_str()) == Some("AGENTS.md") - && has_agents_md_block(entry.path()) - { - result.push(entry.into_path()); - } + let candidate = p.join(crate::init::AGENTS_MD_FILENAME); + if has_agents_md_block(&candidate) { + result.push(candidate); } } else if p.is_file() { if has_agents_md_block(p) { @@ -558,4 +550,24 @@ mod tests { let targets = resolve_agents_md_targets(&[root.to_path_buf()]).unwrap(); assert_eq!(targets, vec![root.join("AGENTS.md")]); } + + #[test] + fn resolve_agents_md_targets_does_not_recurse() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + let sub = root.join("subprojects/a"); + fs::create_dir_all(&sub).unwrap(); + let block = "\nbody\n\n"; + fs::write(root.join("AGENTS.md"), block).unwrap(); + fs::write(sub.join("AGENTS.md"), block).unwrap(); + + // A directory contributes only its own AGENTS.md; nested ones are + // reachable only by naming their directory, so ungoverned subtrees + // (3rdparty/, vendor/) can never be rewritten by a parent walk. + let top = resolve_agents_md_targets(&[root.to_path_buf()]).unwrap(); + assert_eq!(top, vec![root.join("AGENTS.md")]); + + let nested = resolve_agents_md_targets(&[sub.clone()]).unwrap(); + assert_eq!(nested, vec![sub.join("AGENTS.md")]); + } } diff --git a/crates/liyi/src/discovery.rs.liyi.jsonc b/crates/liyi/src/discovery.rs.liyi.jsonc index 3422715..4f75dd5 100644 --- a/crates/liyi/src/discovery.rs.liyi.jsonc +++ b/crates/liyi/src/discovery.rs.liyi.jsonc @@ -98,14 +98,14 @@ }, { "item": "resolve_agents_md_targets", - "reviewed": true, - "intent": "Expand a list of file/directory paths into agent-instruction files. A file qualifies when it contains the portable `START liyi agent instructions` pragma. For a directory, walk it (respecting .gitignore and .liyiignore) and include files named AGENTS.md that carry the pragma; an explicitly-passed file is included whenever it carries the pragma, so a repository's preferred location can be migrated directly. Error if a path does not exist. Return the deduplicated, sorted target list.", + "reviewed": false, + "intent": "Expand a list of file/directory paths into agent-instruction files, strictly mirroring `liyi init`'s targeting. A directory contributes exactly its own AGENTS.md (never a recursive walk, so ungoverned subtrees like 3rdparty/ or vendor/ are never touched); an explicitly-passed file is included when it carries the pragma, so a repository's preferred location can be migrated directly. A file qualifies only when it contains the portable `START liyi agent instructions` pragma. Error if a path does not exist. Return the deduplicated, sorted target list.", "source_span": [ - 296, - 324 + 297, + 316 ], "tree_path": "fn.resolve_agents_md_targets", - "source_hash": "sha256:b507dd7b8df16e8f06c7800741564e439cb5d0c3780049756e85b0756939659f", + "source_hash": "sha256:e080976caa652aeef085c70cc5038bc72efb5ae22e28ef5f0b8e93f705699287", "source_anchor": "pub fn resolve_agents_md_targets(paths: &[PathBuf]) -> Result, String> {" }, { @@ -113,8 +113,8 @@ "reviewed": true, "intent": "=self-doc", "source_span": [ - 329, - 333 + 321, + 325 ], "tree_path": "fn.has_agents_md_block", "source_hash": "sha256:7bf55e6bd57ba5e329ac41f4a57f1186c447fe939b03a3aabc6b9c0132e6dea9", diff --git a/crates/liyi/src/init.rs b/crates/liyi/src/init.rs index c0263a6..007c270 100644 --- a/crates/liyi/src/init.rs +++ b/crates/liyi/src/init.rs @@ -15,6 +15,12 @@ const AGENTS_MD_FULL: &str = include_str!("../../../AGENTS.md"); /// adopter's block to this revision. pub const TEMPLATE_REVISION: u32 = 1; +/// File name `liyi init` scaffolds the agent instruction block into. +/// +/// `liyi migrate` uses this same constant for directory targets, so the two +/// commands can never disagree about which file they manage. +pub const AGENTS_MD_FILENAME: &str = "AGENTS.md"; + /// Opening words of the `START` pragma; the revision number follows. const PRAGMA_START_PREFIX: &str = " + +# Monorepo governance boundary — backlog + +**Status:** Backlog. Recorded so the gap is not lost; not yet designed in +detail, and no requirement is declared yet. + +## The gap + +立意's recursive operations cannot tell directories a repository *owns* +from directories it merely *vendors*. In a layout such as + +```text +repo/ # owned +subprojects/* # owned +3rdparty/* # NOT owned +vendor/* # NOT owned +``` + +`liyi migrate` (sidecar schema migration) and `liyi check` / +`liyi check --fix` walk the whole tree. They will therefore read and +rewrite artifacts under `3rdparty/` and `vendor/`, including a vendored +project's `.liyi.jsonc` sidecars and its `AGENTS.md` agent-instruction +block. Rewriting another project's intent artifacts to this repository's +schema/instruction revision is out of bounds. + +## Current state + +- **Agent instructions are already safe.** `liyi migrate` targeting + strictly mirrors `liyi init`: a directory argument contributes exactly + its own `AGENTS.md`, never a recursive walk, so a tree is traversed only + by naming each governed directory. See *`liyi migrate` behavior* in + `docs/liyi-design.md`. +- **Sidecars are not.** `resolve_sidecar_targets` still walks directories + for `.liyi.jsonc`, so `liyi migrate .` can rewrite vendored sidecars. +- **`liyi check` is not.** It walks the whole tree (and `--fix` writes), + so it reports and repairs vendored sidecars too. +- **Ignore files are the wrong shape.** `.liyiignore` / `.gitignore` can + exclude paths, but the desired boundary is "which subtrees are under my + governance", which is an *include* concept, not an ignore list — and a + vendored tree may be committed, hence not gitignored. + +## Open design questions + +1. **What expresses the boundary?** Candidates: + - an explicit *include* list of governed roots (CLI flag and/or a + repo manifest), rather than an exclude list; + - a marker at each governed root (a `.liyi/` directory or a + file-scoped directive) so the boundary travels with the subtree; + - a workspace manifest (`liyi.toml`?) at the repo root that enumerates + or globs governed subtrees. +2. **Which operations become scoped?** Discovery, `check`, `check --fix`, + `migrate`, `init`. Mutating paths matter most; read-only `check` is + lower risk but still noisy across a vendored tree. +3. **Nested VCS boundaries.** The walker already skips VCS internals + (`.git/`, `.hg/`, …), but a nested working tree or submodule is still + walked. Should a nested `.git/` terminate the walk by default? +4. **Relationship to existing work.** How does this interact with the + deferred "workspace-aware requirement queries (monorepo)" item and the + shared requirement namespace? + +## Minimum viable direction + +An opt-in, explicit governed-root list (CLI flag and/or manifest) that +scopes discovery, `check`, `check --fix`, and `migrate` to the listed +subtrees, with the safe default remaining non-recursive for mutating +operations until that boundary exists. + +## References + +- `docs/liyi-design.md` — *`liyi migrate` behavior* (strict agent-file + targeting), `.liyiignore`, and the sidecar discovery rules. +- `docs/next-steps.md` — Tier 5, "Workspace-aware requirement queries + (monorepo)". +- Commit `3f451fcc38f7` — made `liyi migrate` agent-instruction targeting + mirror `liyi init` (no recursion) for exactly this reason. From c473d57fdd1981f3a0258fd4d3dd869866752c97 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:16:42 +0800 Subject: [PATCH 37/44] docs(monorepo-governance): fix commit id after human DCO Signed-off-by: WANG Xuerui --- docs/monorepo-governance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/monorepo-governance.md b/docs/monorepo-governance.md index 71ae1c2..eee73e6 100644 --- a/docs/monorepo-governance.md +++ b/docs/monorepo-governance.md @@ -72,5 +72,5 @@ operations until that boundary exists. targeting), `.liyiignore`, and the sidecar discovery rules. - `docs/next-steps.md` — Tier 5, "Workspace-aware requirement queries (monorepo)". -- Commit `3f451fcc38f7` — made `liyi migrate` agent-instruction targeting +- Commit `747b0095299d` — made `liyi migrate` agent-instruction targeting mirror `liyi init` (no recursion) for exactly this reason. From 1252d2bddb24652cbd46c01f8d3ece798b01254f Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:02:29 +0800 Subject: [PATCH 38/44] docs(meta): state the AGENTS.md harness convention in the README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 立意 writes its agent instructions to `AGENTS.md`, and any harness that reads `AGENTS.md` works out of the box. Make that an explicit, advertised commitment by adding a "Harness compatibility" section to both READMEs. Rather than special-casing vendor instruction files (e.g. Claude Code's `CLAUDE.md`), the README now follows the AGENTS.md convention itself: a harness that does not read `AGENTS.md` is the adopter's responsibility to bridge by placing the block where that harness will read it. This keeps one neutral instruction file instead of a per-harness matrix, and it matches the convention's own backward-compatibility guidance. Original prompt: > Can we sidestep this and call out the discrepancy loud by announcing > somewhere that we explicitly adhere to the https://agents.md > convention? > let's avoid singling out Claude Code and simply claim "If your harness > doesn't read AGENTS.md, it is your responsibility to find another > suitable place for the instruction block and make sure it is being > read by your harness", likewise for the `liyi init` prompt. The latter > can be terse because it's CLI output and the reader might be an LLM > (recent Claude models can be expected to know that and adapt for the > user). No need to mention "liyi commands stay single-file". > "harness" and "harness engineering" tend to be translated as > "驾驭框架/驾驭工程" in zh_CN, not "智能体工具" (that's just "agentic > tools"). fix that and we're good to commit. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- README.md | 8 ++++++++ README.zh.md | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index 620e5bd..b6a3dd8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,14 @@ liyi check --root . 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. +## Harness compatibility + +立意 writes its agent instructions to `AGENTS.md`, following the +[AGENTS.md convention](https://agents.md). Any harness that reads `AGENTS.md` +works out of the box. If your harness does not read `AGENTS.md`, it is your +responsibility to find another suitable place for the instruction block and +make sure it is actually read by your harness. + ## Progressive Adoption | Level | What you do | What you get | diff --git a/README.zh.md b/README.zh.md index 7910606..6a7a36c 100644 --- a/README.zh.md +++ b/README.zh.md @@ -36,6 +36,12 @@ liyi check --root . 3. **`liyi migrate`** — 当 schema 版本变更时升级 sidecar 文件。幂等。 4. **由人类复核** — 在 `.liyi.jsonc` 中设置 `"reviewed": true` 以批准,或在源码中添加 `@liyi:intent` 以明确给出人类版本。 +## 驾驭框架兼容性 + +《立意》将智能体指令写入 `AGENTS.md`,并遵循 [AGENTS.md 约定](https://agents.md)。 +凡能读取 `AGENTS.md` 的驾驭框架,均可直接使用。如果你的驾驭框架不读取 +`AGENTS.md`,则由你负责为该指令块寻找合适的存放位置,并确保驾驭框架能够读取它。 + ## 渐进式采用 | 级别 | 操作 | 收益 | From 77d716e28a99bac1a9cbd427a843c82ca98e88e1 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:02:30 +0800 Subject: [PATCH 39/44] feat(linter): announce the AGENTS.md convention from liyi init `liyi init` now prints a one-line note after scaffolding `AGENTS.md`, stating that the block follows the AGENTS.md convention and that a harness which does not read `AGENTS.md` is responsible for placing the block where it will be read. The note is unconditional -- no detection, no vendor filename in the code -- so an agent-driven adoption gets the rule at the moment it matters. Editing inside `main()` shifted its span and changed its hash, so the main.rs sidecar is re-anchored and `main` is left `"reviewed": false` for human re-confirmation. Original prompt: > let's avoid singling out Claude Code and simply claim "If your harness > doesn't read AGENTS.md, it is your responsibility to find another > suitable place for the instruction block and make sure it is being > read by your harness", likewise for the `liyi init` prompt. The latter > can be terse because it's CLI output and the reader might be an LLM > (recent Claude models can be expected to know that and adapt for the > user). No need to mention "liyi commands stay single-file". AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- crates/liyi-cli/src/main.rs | 9 ++++++++- crates/liyi-cli/src/main.rs.liyi.jsonc | 14 +++++++------- crates/liyi-cli/tests/cli.rs | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/liyi-cli/src/main.rs b/crates/liyi-cli/src/main.rs index 09e3b0e..a64c277 100644 --- a/crates/liyi-cli/src/main.rs +++ b/crates/liyi-cli/src/main.rs @@ -213,7 +213,14 @@ fn main() { None => { let root = env::current_dir().unwrap_or_default(); match liyi::init::init_agents_md(&root, force) { - Ok(path) => println!("Initialized: {}", path.display()), + Ok(path) => { + println!("Initialized: {}", path.display()); + println!( + "note: these instructions follow the AGENTS.md convention \ + (https://agents.md); if your harness does not read AGENTS.md, it is \ + your responsibility to place the block where your harness will read it." + ); + } Err(e) => { eprintln!("Error: {e}"); process::exit(1); diff --git a/crates/liyi-cli/src/main.rs.liyi.jsonc b/crates/liyi-cli/src/main.rs.liyi.jsonc index 2fe1e3b..031e391 100644 --- a/crates/liyi-cli/src/main.rs.liyi.jsonc +++ b/crates/liyi-cli/src/main.rs.liyi.jsonc @@ -17,14 +17,14 @@ }, { "item": "main", - "reviewed": true, + "reviewed": false, "intent": "=doc", "source_span": [ 27, - 327 + 334 ], "tree_path": "fn.main", - "source_hash": "sha256:4ecab6146b1fd1629edd6d58e9a49561d3cccdd166842ec77c3d93f55642ce92", + "source_hash": "sha256:9370c0ab92fba386fdcdc65a989afe5726b8ab693927a12b7682803e01ac5dfd", "source_anchor": "fn main() {" }, { @@ -32,8 +32,8 @@ "reviewed": true, "intent": "Parse the target into a path (and optional trailing :line), locate the repo root, resolve applicable notes via the live note scan, print the rendered report to stdout, and exit 0. Exit 2 on usage or resolution errors (bad line number, path outside the repo, missing root).", "source_span": [ - 337, - 388 + 344, + 395 ], "tree_path": "fn.run_context", "source_hash": "sha256:4cf6f64065c246654eea0d69ba8cc555065004c96785ae9022f8722bc9eed0da", @@ -44,8 +44,8 @@ "reviewed": true, "intent": "=doc", "source_span": [ - 394, - 396 + 401, + 403 ], "tree_path": "fn.is_tty", "source_hash": "sha256:021dc096ffc5aa501d5fe90e8fd1835c4ce784a7de59434995a9be221f97e40f", diff --git a/crates/liyi-cli/tests/cli.rs b/crates/liyi-cli/tests/cli.rs index 1970579..7c8951c 100644 --- a/crates/liyi-cli/tests/cli.rs +++ b/crates/liyi-cli/tests/cli.rs @@ -137,6 +137,30 @@ fn init_creates_agents_md_when_absent() { ); } +#[test] +fn init_announces_agents_md_convention() { + let tmp = tempfile::TempDir::new().unwrap(); + let out = run_in(tmp.path(), &["init"]); + assert!( + out.status.success(), + "init should exit 0: {:?}", + stderr(&out) + ); + let text = stdout(&out); + assert!( + text.contains("AGENTS.md convention"), + "init should announce the AGENTS.md convention, got: {text}" + ); + assert!( + text.contains("https://agents.md"), + "init note should link the convention, got: {text}" + ); + assert!( + text.contains("your responsibility"), + "init note should state the harness-placement responsibility, got: {text}" + ); +} + #[test] fn init_force_replaces_the_agents_md_block() { let tmp = tempfile::TempDir::new().unwrap(); From 621a8a6b578bb5e658effdd720923c6145b1bfae Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:23:13 +0800 Subject: [PATCH 40/44] docs(README): manually tweak the Harness Compatibility section Signed-off-by: WANG Xuerui --- README.md | 11 ++++++----- README.zh.md | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b6a3dd8..63d207e 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,12 @@ liyi check --root . ## Harness compatibility -立意 writes its agent instructions to `AGENTS.md`, following the -[AGENTS.md convention](https://agents.md). Any harness that reads `AGENTS.md` -works out of the box. If your harness does not read `AGENTS.md`, it is your -responsibility to find another suitable place for the instruction block and -make sure it is actually read by your harness. +立意 adopts the [AGENTS.md convention](https://agents.md) and writes its agent +instructions to `AGENTS.md` by default. +Any harness that reads `AGENTS.md` should work out of the box. +If your harness does not read `AGENTS.md`, it is your responsibility to find +another suitable place for the instruction block and make sure it is actually +read by your harness. ## Progressive Adoption diff --git a/README.zh.md b/README.zh.md index 6a7a36c..bbc7180 100644 --- a/README.zh.md +++ b/README.zh.md @@ -38,9 +38,9 @@ liyi check --root . ## 驾驭框架兼容性 -《立意》将智能体指令写入 `AGENTS.md`,并遵循 [AGENTS.md 约定](https://agents.md)。 -凡能读取 `AGENTS.md` 的驾驭框架,均可直接使用。如果你的驾驭框架不读取 -`AGENTS.md`,则由你负责为该指令块寻找合适的存放位置,并确保驾驭框架能够读取它。 +《立意》遵循 [AGENTS.md 约定](https://agents.md),默认将其所用的智能体指令写入 `AGENTS.md`。 +凡是能读取 `AGENTS.md` 的驾驭框架都应当直接工作。如果您所用的驾驭框架不读取 +`AGENTS.md`,则您需要负责为《立意》的指令块寻找合适的存放位置,并确保您的驾驭框架能够读取它。 ## 渐进式采用 From d4b47ef7e2b4b3307c7c5266f482d17482205ab9 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:26:26 +0800 Subject: [PATCH 41/44] docs: liyi approve Signed-off-by: WANG Xuerui --- crates/liyi-cli/src/main.rs.liyi.jsonc | 2 +- crates/liyi/src/discovery.rs.liyi.jsonc | 2 +- crates/liyi/src/init.rs.liyi.jsonc | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/liyi-cli/src/main.rs.liyi.jsonc b/crates/liyi-cli/src/main.rs.liyi.jsonc index 031e391..fddb185 100644 --- a/crates/liyi-cli/src/main.rs.liyi.jsonc +++ b/crates/liyi-cli/src/main.rs.liyi.jsonc @@ -17,7 +17,7 @@ }, { "item": "main", - "reviewed": false, + "reviewed": true, "intent": "=doc", "source_span": [ 27, diff --git a/crates/liyi/src/discovery.rs.liyi.jsonc b/crates/liyi/src/discovery.rs.liyi.jsonc index 4f75dd5..779f742 100644 --- a/crates/liyi/src/discovery.rs.liyi.jsonc +++ b/crates/liyi/src/discovery.rs.liyi.jsonc @@ -98,7 +98,7 @@ }, { "item": "resolve_agents_md_targets", - "reviewed": false, + "reviewed": true, "intent": "Expand a list of file/directory paths into agent-instruction files, strictly mirroring `liyi init`'s targeting. A directory contributes exactly its own AGENTS.md (never a recursive walk, so ungoverned subtrees like 3rdparty/ or vendor/ are never touched); an explicitly-passed file is included when it carries the pragma, so a repository's preferred location can be migrated directly. A file qualifies only when it contains the portable `START liyi agent instructions` pragma. Error if a path does not exist. Return the deduplicated, sorted target list.", "source_span": [ 297, diff --git a/crates/liyi/src/init.rs.liyi.jsonc b/crates/liyi/src/init.rs.liyi.jsonc index e9593ea..a7f48a6 100644 --- a/crates/liyi/src/init.rs.liyi.jsonc +++ b/crates/liyi/src/init.rs.liyi.jsonc @@ -29,7 +29,7 @@ }, { "item": "AGENTS_MD_FILENAME", - "reviewed": false, + "reviewed": true, "intent": "=trivial", "source_span": [ 22, @@ -149,7 +149,7 @@ }, { "item": "init_agents_md", - "reviewed": false, + "reviewed": true, "intent": "Create AGENTS.md when absent, writing `# AGENTS.md` plus the portable agent-instruction block. When the file already contains such a block, return AlreadyExists unless force is set, in which case replace the existing block with the current revision; when the file exists without a block, append it after a blank line. Must key off the block pragma, never the section heading, and target AGENTS_MD_FILENAME so `liyi migrate` uses the same file.", "source_span": [ 131, From ad18fe35dd3eff6c9305b0a63875d9914946c723 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:31:29 +0800 Subject: [PATCH 42/44] docs(docs): drop shipped items from the next-steps roadmap Remove the completed Tier 1 section and the finished 2A (approval follow-through), 2B (LSP library refactor), and 2F (note context) sections so the backlog lists only outstanding work. Drop the now-dangling reference to item 1.3. Original prompt: > Let's remove the already-done parts from it, refresh the stale info and > drop the explicit mention of number of untracked requirements, and > finally update the banner, in 3 commits. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/next-steps.md | 65 +--------------------------------------------- 1 file changed, 1 insertion(+), 64 deletions(-) diff --git a/docs/next-steps.md b/docs/next-steps.md index accf96c..3008b83 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -13,16 +13,6 @@ readiness. --- -## Tier 1 — Low-hanging fruit (small effort, high polish) - -These can each be done in a single focused session without new design work. - -| # | Item | Source | Why now | -|---|------|--------|---------| -| 1.1 | ~~**Extend `--prompt` to stale/shifted/unreviewed diagnostics**~~ | prompt-mode-design.md | ✅ Done — `liyi check --prompt` now groups actionable diagnostics for `Stale`, `Shifted`, `Unreviewed`, and `ReqChanged` in addition to the original coverage-gap kinds. | -| 1.2 | ~~**Doc-comment detection for remaining languages**~~ | init-discover-impl.md (Phase 2 gap) | ✅ Done — 15/21 languages now have `doc_comment_detector`. Remaining 5 (Bash, Ruby not feasible; JSON/TOML/YAML not applicable). | -| 1.3 | ~~**Disambiguate Rust trait impls & ObjC categories in tree_path**~~ | tree_path resolver audit | ✅ Done — `impl Trait for Foo` now encodes as `impl."Trait for Foo"` (distinct from inherent `impl.Foo`); ObjC `@interface Foo (Cat)` encodes as `class."Foo (Cat)"`. Root cause of a mislabeled sidecar spec where two distinct impl blocks shared one tree_path. See item 2.8 for the remaining collision classes. | - > **Note on the 14 unreferenced requirements:** `liyi check` currently reports > 7 requirements from lsp-design.md and 7 from sidecar-merge-design.md with no > referencing item specs. This is expected — the code that would carry @@ -32,39 +22,6 @@ These can each be done in a single focused session without new design work. ## Tier 2 — Next milestones (moderate effort, unlocks downstream value) -### 2A. Approval workflow follow-through (v0.1.x scope) - -| # | Item | Source | -|---|------|--------| -| 2.1 | ~~**StaleReviewed approval flow**~~ | approve-impl.md | -| 2.2 | ~~**ReqChanged approval flow**~~ | approve-impl.md | - -**Status**: ✅ Done — `liyi approve` now surfaces unreviewed, stale-reviewed, -and requirement-changed items; the TUI shows source diffs for stale-reviewed -items and requirement diffs for req-changed items, and the CLI exposes -`--unreviewed-only`, `--stale-only`, and `--req-only` filters. - -**Rationale**: This milestone is complete; keep it here as a shipped v0.1.x -checkpoint because later roadmap items build on the same review model. - -### 2B. Library refactoring for LSP (Step 0) - -| # | Item | Source | -|---|------|--------| -| 2.3 | ~~**Extract `build_requirement_registry()` as public API**~~ | lsp-design.md Step 0 | -| 2.4 | ~~**Export `RequirementRegistry` / `RequirementRecord`**~~ | lsp-design.md Step 0 | - -**Status**: ✅ Done — `liyi::check` now exposes public `RequirementRegistry` -and `RequirementRecord`, plus `build_requirement_registry()` (pass 1) and -`check_sidecars()` (pass 2) that accept a prebuilt registry. `run_check()` -remains the CLI wrapper and is behavior-compatible. The -`lsp-check-refactor-exposes-registry` requirement is now referenced by -`build_requirement_registry`. - -**Rationale**: This was the *prerequisite* gate for all LSP work — a pure -refactor with no behavioral change. With it landed, the LSP crate can cache -the registry across edits instead of re-scanning the project. - ### 2C. VCS hints (Phase 3 of init-discover) | # | Item | Source | @@ -89,7 +46,7 @@ git2 dependency. Can be worked in parallel with Tier 2B/2D. indistinguishable — the second is unaddressable and silently resolves to the first. This was the root cause of a mislabeled sidecar spec (an inherent `impl Diagnostic` carrying a trait impl's intent). The Rust trait-impl and -Objective-C category cases are fixed (item 1.3); a full audit of all 20 language +Objective-C category cases are already fixed; a full audit of all 20 language configs found these remaining collision classes: | Language(s) | Collision | Frequency | @@ -135,26 +92,6 @@ retire the stale `.h`→C doc comment), and a discovery hook alongside the authority: `docs/file-directive-design.md` (requirements `file-directive-namespace`, `file-language-precedence`, `file-ignore-additive`). -### 2F. `@liyi:note` context primitive + `liyi context` CLI - -| # | Item | Source | -|---|------|--------| -| 2.10 | ~~**Hard-rename `@liyi:module` → `@liyi:note`; add `@liyi:end-note`, `@liyi:see`; `liyi context ` MVP**~~ | `docs/note-context-design.md` — ✅ Done — `@liyi:module` removed from the scanner; `Note`/`EndNote`/`See` markers land in `markers.rs`; the live retrieval-graph resolver (directory scope + shadowing, `@liyi:see`, `=none` opt-out) ships in `context.rs`; `liyi context [:line]` is wired in the CLI. Item-precise `@liyi:see` (tree-sitter) stays file-scoped for now; LSP/MCP context surfaces (Tier 3) and challenge mode (Tier 4.4) remain deferred. | - -Replaces the presence-only `@liyi:module` marker with a marker-only, untracked -**context primitive**: notes carry governing prose injected into a reader's -context, separated from the hash-anchored staleness graph (the "two-graph" -model). Adds `@liyi:end-note` (block bounding) and `@liyi:see ` (item-side -membership). The near-term consumer is a read-only `liyi context ` -command that resolves and prints applicable notes; the LSP/MCP context API -(Tier 3) is the eventual home, and challenge-mode verification (Tier 4.4) is the -deferred semantic check — notes never seed adversarial unit tests. The staleness -engine (`check.rs`) is untouched; the JSON schema is unchanged. This is a -pre-1.0 breaking rename (no external adopters). Design authority: -`docs/note-context-design.md` (requirements `note-is-untracked`, -`note-directory-scope`, `note-see-membership`, `two-graph-separation`, -`context-resolution`). - ## Tier 3 — v0.2 headline: LSP server Depends on Tier 2B completion. From 4e72120950bb40fce53d8388b849b5913aaca777 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:31:47 +0800 Subject: [PATCH 43/44] docs(docs): refresh stale next-steps context Update the roadmap's cross-references after the shipped Tier 2 work: add the newer design docs to the synthesis list, drop the explicit count of unreferenced requirements, note that the LSP library refactor unblocks Tier 3, and reflect 2D/2E in the sequencing. Original prompt: > Let's remove the already-done parts from it, refresh the stale info and > drop the explicit mention of number of untracked requirements, and > finally update the banner, in 3 commits. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/next-steps.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/docs/next-steps.md b/docs/next-steps.md index 3008b83..aa63692 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -6,19 +6,20 @@ **As of**: 2026-05-30 · **Baseline**: v0.1.0, prompt-mode expansion and approval workflow shipped; doc-comment detection complete for all feasible languages. This document synthesizes the existing roadmaps (liyi-design.md, lsp-design.md, -approve-impl.md, init-discover-impl.md, prompt-mode-design.md, -injection-impl.md, sidecar-merge-design.md) and the repo's current state into a -single prioritized backlog. Items are grouped into tiers by impact and -readiness. +init-discover-impl.md, prompt-mode-design.md, injection-impl.md, +file-directive-design.md, note-context-design.md, sidecar-merge-design.md) and +the repo's current state into a single prioritized backlog. Items are grouped +into tiers by impact and readiness. --- -> **Note on the 14 unreferenced requirements:** `liyi check` currently reports -> 7 requirements from lsp-design.md and 7 from sidecar-merge-design.md with no -> referencing item specs. This is expected — the code that would carry -> `@liyi:related` edges pointing to these requirements doesn't exist yet. -> They will be resolved naturally when the LSP (Tier 3) and sidecar merge -> (Tier 4.2) features are implemented. +> **Note on unreferenced requirements:** `liyi check` currently reports +> requirements in several design documents (notably lsp-design.md, +> sidecar-merge-design.md, file-directive-design.md, and +> note-context-design.md) that have no referencing item specs. This is expected +> — the code that would carry `@liyi:related` edges pointing to these +> requirements doesn't exist yet. They will be resolved naturally when the +> corresponding features (Tiers 2–4) are implemented. ## Tier 2 — Next milestones (moderate effort, unlocks downstream value) @@ -33,7 +34,7 @@ readiness. **Rationale**: Phase 3 is fully designed and was explicitly "deferred, not cancelled." VCS hints significantly improve cold-start triage by telling agents which items have churn or bug-fix history. The `git log -L` approach avoids the -git2 dependency. Can be worked in parallel with Tier 2B/2D. +git2 dependency. Can be worked in parallel with Tier 2D/2E. ### 2D. Resolve remaining tree_path name collisions @@ -94,7 +95,8 @@ authority: `docs/file-directive-design.md` (requirements `file-directive-namespa ## Tier 3 — v0.2 headline: LSP server -Depends on Tier 2B completion. +Builds on the already-landed `liyi::check` library surface, so the server can +cache the requirement registry across edits. | # | Item | Source | Phase | |---|------|--------|-------| @@ -140,14 +142,15 @@ Not designed in detail; captured for completeness. ## Suggested sequencing ``` -Now Tier 2B (LSP library refactor) - Tier 2C (VCS hints) ─── can parallelize ─── Tier 2D (tree_path collision work) +Now Tier 2C (VCS hints) ─── can parallelize ─── Tier 2D (tree_path collision work) + Tier 2E (@liyi:file directives) │ -v0.2 Tier 3.1 → 3.2 → 3.3 (LSP) ← resolves 7 unreferenced lsp-design requirements +v0.2 Tier 3.1 → 3.2 → 3.3 (LSP) │ -Post-MVP Tier 4 items by opportunity ← 4.2 resolves 7 unreferenced merge-design requirements +Post-MVP Tier 4 items by opportunity ``` -Tier 1 is complete. Within Tier 2, 2B must precede Tier 3, while 2C and 2D are -independent of the LSP refactor and can be scheduled in parallel. Within Tier -4, items 4.1–4.3 are independent; 4.4 depends on Tier 3. +Tier 1 is complete and the Tier 2 LSP library refactor has landed, unblocking +Tier 3. The remaining Tier 2 items (2C, 2D, 2E) are independent of the LSP +refactor and can be scheduled in parallel. Within Tier 4, items 4.1–4.3 are +independent; 4.4 depends on Tier 3. From 971125b2e5ed38e6f6cef3be08aec3729dd2627e Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sat, 19 Sep 2026 15:31:52 +0800 Subject: [PATCH 44/44] docs(docs): refresh the next-steps baseline banner Move the "as of" date forward and list the capabilities shipped since the previous baseline. Original prompt: > Let's remove the already-done parts from it, refresh the stale info and > drop the explicit mention of number of untracked requirements, and > finally update the banner, in 3 commits. AI-assisted-by: DeepSeek V4.1 Flash (OpenCode) Signed-off-by: WANG Xuerui --- docs/next-steps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/next-steps.md b/docs/next-steps.md index aa63692..bbcb1f7 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -3,7 +3,7 @@ # Prioritized Next Steps -**As of**: 2026-05-30 · **Baseline**: v0.1.0, prompt-mode expansion and approval workflow shipped; doc-comment detection complete for all feasible languages. +**As of**: 2026-09-19 · **Baseline**: v0.1.0; prompt-mode expansion, approval workflow, doc-comment detection, the LSP library refactor, and the `@liyi:note` context MVP shipped. This document synthesizes the existing roadmaps (liyi-design.md, lsp-design.md, init-discover-impl.md, prompt-mode-design.md, injection-impl.md,