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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/liyi/src/markers.rs.liyi.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"source_anchor": "const ALIAS_TABLE: &[(&str, &str)] = &[",
"related": {
"marker-normalization": "sha256:f1786541ac9be4533a36924a80c578a749024a4980fbc323417c2d5640ed6306",
"quine-escape-in-source": null
"quine-escape-in-source": "sha256:b929750d0d3c9ce24a6f814d7a1db15f2fc03983efa2647cfcf6a7462d2c8b81"
}
},
{
Expand Down Expand Up @@ -125,7 +125,7 @@
"source_anchor": "fn is_fence_delimiter(line: &str) -> bool {",
"confidence": 0.95,
"related": {
"markdown-fenced-block-skip": null
"markdown-fenced-block-skip": "sha256:0bdd7d1ad747e2a826adfe2eca5652c7a9f382e1089ff12dd2c9b8d88f3920b7"
}
},
{
Expand Down Expand Up @@ -164,8 +164,8 @@
"source_hash": "sha256:882dc8ffe044cfd565123f41aa19ad94b6ec641f829f96a3c7b02565beb43de3",
"source_anchor": "pub fn scan_markers(content: &str) -> Vec<SourceMarker> {",
"related": {
"markdown-fenced-block-skip": null,
"quine-escape-in-source": null
"markdown-fenced-block-skip": "sha256:0bdd7d1ad747e2a826adfe2eca5652c7a9f382e1089ff12dd2c9b8d88f3920b7",
"quine-escape-in-source": "sha256:b929750d0d3c9ce24a6f814d7a1db15f2fc03983efa2647cfcf6a7462d2c8b81"
}
}
]
Expand Down
33 changes: 31 additions & 2 deletions crates/liyi/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,21 @@
reviewed spec, unset \"reviewed\".";

const TMPL_SHIFTED_SPAN: &str = "Run `liyi check --fix` to auto-correct the \
span for \"{item}\" in {expected_sidecar} from [{old_start}, \
{old_end}] to [{new_start}, {new_end}].";
span for \"{item}\" in {expected_sidecar} from {old_span} \
to {new_span}.";

const TMPL_UNREVIEWED_SPEC: &str = "Verify that the intent for \"{item}\" \
in {expected_sidecar} matches the source at {source_file}:{source_line}, \
then run `liyi approve {expected_sidecar} --item {item}` or set \
\"reviewed\": true in the sidecar.";

const TMPL_REQ_CHANGED: &str = "Requirement \"{requirement}\" has changed. \
Run `liyi approve {expected_sidecar} --item {item}` to update the \
stored hash for the related edge, or manually set \"related\": \
{{\"{requirement}\": null}} in the sidecar to trigger re-approval.";

/// Metadata for each diagnostic type: `(type_name, template, untrusted_fields)`.
struct GroupMeta {

Check notice on line 73 in crates/liyi/src/prompt.rs

View workflow job for this annotation

GitHub Actions / Liyi self-check

立意 · GroupMeta

intent "=trivial"
type_name: &'static str,
template: &'static str,
untrusted_fields: &'static [&'static str],
Expand Down Expand Up @@ -233,6 +238,30 @@
item,
));
}
DiagnosticKind::ReqChanged { requirement } => {
let Some(source_line) = d.span_start else {
continue;
};
let item_name = &d.item_or_req;
let mut item = serde_json::json!({
"item": item_name,
"requirement": requirement,
"source_file": source_rel,
"source_line": source_line,
"expected_sidecar": expected_sidecar,
});
if let Some(text) = &d.intent {
item["intent_text"] = serde_json::Value::String(text.clone());
}
raw.push((
GroupMeta {
type_name: "req_changed",
template: TMPL_REQ_CHANGED,
untrusted_fields: &["item", "requirement", "intent_text"],
},
item,
));
}
_ => {}
}
}
Expand Down
20 changes: 10 additions & 10 deletions crates/liyi/src/prompt.rs.liyi.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"reviewed": true,
"intent": "=trivial",
"source_span": [
68,
72
73,
77
],
"tree_path": "struct.GroupMeta",
"source_hash": "sha256:949b255735ad374171b0742fb9981747e0c5adb8cabf1ec4fca07078696579f2",
Expand All @@ -42,25 +42,25 @@
{
"item": "build_prompt_output",
"reviewed": true,
"intent": "Convert a diagnostic slice into PromptOutput by filtering to actionable kinds (coverage gaps plus Stale, Shifted, and Unreviewed), skipping fixed diagnostics, and building (GroupMeta, serde_json::Value) pairs. Each pair captures the diagnostic type, its template, untrusted field names, and a JSON object of per-item data. A final group_items() pass groups pairs by (type_name, template pointer) preserving insertion order. Error-only diagnostics remain excluded from groups but still influence exit_code.",
"intent": "Convert a diagnostic slice into PromptOutput by filtering to actionable kinds (coverage gaps plus Stale, Shifted, Unreviewed, and ReqChanged), skipping fixed diagnostics, and building (GroupMeta, serde_json::Value) pairs. Each pair captures the diagnostic type, its template, untrusted field names, and a JSON object of per-item data. A final group_items() pass groups pairs by (type_name, template pointer) preserving insertion order. Error-only diagnostics remain excluded from groups but still influence exit_code.",
"source_span": [
81,
253
86,
282
],
"tree_path": "fn.build_prompt_output",
"source_hash": "sha256:4b58abb88e8267fb636eae6727230d6f4c90fdaab97f0125c916e5a55985851e",
"source_hash": "sha256:496e2e08e42fff85a266aef7e978d4a53deb461a9b183cc680c620f7202ab014",
"source_anchor": "pub fn build_prompt_output(",
"related": {
"quine-escape-in-source": null
"quine-escape-in-source": "sha256:b929750d0d3c9ce24a6f814d7a1db15f2fc03983efa2647cfcf6a7462d2c8b81"
}
},
{
"item": "group_items",
"reviewed": true,
"intent": "Group raw (GroupMeta, serde_json::Value) pairs by (type_name, template) into PromptGroups, preserving insertion order. Uses linear scan since the number of distinct groups is small (≤ 7). Two items share a group when they have the same type_name and the same template pointer (ptr::eq).",
"intent": "Group raw (GroupMeta, serde_json::Value) pairs by (type_name, template) into PromptGroups, preserving insertion order. Uses linear scan since the number of distinct groups is small (≤ 8). Two items share a group when they have the same type_name and the same template pointer (ptr::eq).",
"source_span": [
256,
281
285,
310
],
"tree_path": "fn.group_items",
"source_hash": "sha256:62e8c05770642c453f9bbfe2e62d1981f3b8b6542d9cf9018a98feeae47988cb",
Expand Down
28 changes: 28 additions & 0 deletions crates/liyi/tests/golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,34 @@ fn prompt_stale_unreviewed_spec_is_fixable() {
assert_eq!(item["fix_command"], "liyi check --fix");
}

#[test]
fn prompt_req_changed() {
let (_tmp, root) = fixture_in_tmp("req_changed");
let flags = CheckFlags {
fail_on_stale: false,
fail_on_unreviewed: false,
fail_on_req_changed: false,
fail_on_untracked: false,
};

let (diagnostics, exit_code) = run_check(&root, &[], false, false, &flags);
let output = liyi::prompt::build_prompt_output(&diagnostics, exit_code, &root);

let group = output
.groups
.iter()
.find(|g| g.prompt_type == "req_changed")
.expect("expected req_changed group");

assert!(group.template.contains("liyi approve"));
assert_eq!(group.count, group.items.len());

let item = &group.items[0];
assert_eq!(item["item"], "add");
assert_eq!(item["requirement"], "no-overflow");
assert_eq!(item["source_line"], 2);
}

// ---------------------------------------------------------------------------
// =trivial sidecar sentinel tests
// ---------------------------------------------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions docs/prompt-mode-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Option B is cleaner — keeps formatting concerns out of the core check logic.
- `DiagnosticKind::Stale` → `stale_spec` (two templates: fixable vs manual)
- `DiagnosticKind::Shifted` → `shifted_span`
- `DiagnosticKind::Unreviewed` → `unreviewed_spec`
- `DiagnosticKind::ReqChanged` → `req_changed`

A final `group_items()` pass groups them by `(type_name, template pointer)`, preserving insertion order. Error-class diagnostics (`ParseError`, `OrphanedSource`, etc.) are excluded from groups but still affect `exit_code`.

Expand All @@ -209,7 +210,7 @@ A final `group_items()` pass groups them by `(type_name, template pointer)`, pre

1. `liyi check --prompt` on a fixture with gaps produces valid JSON matching `schema/prompt.schema.json`.
2. `liyi check --prompt` on a clean repo produces `{"groups": [], "exit_code": 0, ...}`.
3. The JSON includes groups for all six diagnostic types: `missing_requirement_spec`, `missing_related_edge`, `req_no_related`, `stale_spec`, `shifted_span`, and `unreviewed_spec`.
3. The JSON includes groups for all seven diagnostic types: `missing_requirement_spec`, `missing_related_edge`, `req_no_related`, `stale_spec`, `shifted_span`, `unreviewed_spec`, and `req_changed`.
4. `--prompt` will be mutually exclusive with `--json` (when implemented).
5. Exit code behavior is identical to default mode (respects all `--fail-on-*` flags).
6. When error-class diagnostics are present, `exit_code` is `2` even if `groups` is empty.
Expand All @@ -232,7 +233,7 @@ A final `group_items()` pass groups them by `(type_name, template pointer)`, pre

## Resolved Questions

1. **Should `--prompt` include non-coverage diagnostics?** Yes — all actionable diagnostic types (Untracked, MissingRelatedEdge, ReqNoRelated, Stale, Shifted, Unreviewed) are included as groups. Error-class diagnostics are excluded but affect `exit_code`.
1. **Should `--prompt` include non-coverage diagnostics?** Yes — all actionable diagnostic types (Untracked, MissingRelatedEdge, ReqNoRelated, Stale, Shifted, Unreviewed, ReqChanged) are included as groups. Error-class diagnostics are excluded but affect `exit_code`.

2. **Should we include the actual requirement text in `missing_requirement_spec`?** Yes. The cognitive load inversion principle argues for including all context the tool already has, so agents need not re-read files. Added as optional `requirement_text` field.

Expand Down
39 changes: 38 additions & 1 deletion schema/prompt.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"req_no_related",
"stale_spec",
"shifted_span",
"unreviewed_spec"
"unreviewed_spec",
"req_changed"
],
"description": "Diagnostic type shared by all items in this group."
},
Expand Down Expand Up @@ -103,9 +104,45 @@
{
"if": { "properties": { "type": { "const": "unreviewed_spec" } }, "required": ["type"] },
"then": { "properties": { "items": { "items": { "$ref": "#/$defs/unreviewedSpecItem" } } } }
},
{
"if": { "properties": { "type": { "const": "req_changed" } }, "required": ["type"] },
"then": { "properties": { "items": { "items": { "$ref": "#/$defs/reqChangedItem" } } } }
}
]
},
"reqChangedItem": {
"type": "object",
"required": ["item", "requirement", "source_file", "source_line", "expected_sidecar"],
"additionalProperties": false,
"properties": {
"item": {
"type": "string",
"description": "Name of the item whose related edge has a stale requirement hash (untrusted)."
},
"requirement": {
"type": "string",
"description": "Name of the requirement that changed (untrusted)."
},
"source_file": {
"type": "string",
"description": "Repo-relative path to the source file containing the item."
},
"source_line": {
"type": "integer",
"minimum": 1,
"description": "1-indexed start line of the item in the source file."
},
"expected_sidecar": {
"type": "string",
"description": "Repo-relative path to the sidecar file containing the itemSpec with the stale related edge."
},
"intent_text": {
"type": "string",
"description": "Current intent text recorded in the sidecar, when available (untrusted)."
}
}
},
"missingRequirementSpecItem": {
"type": "object",
"required": ["requirement", "source_file", "annotation_line", "expected_sidecar"],
Expand Down
Loading