Skip to content
Merged
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
156 changes: 151 additions & 5 deletions HPRSCRIPT.md

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions skills/hprscript-search/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Invoke the binary through Bash as `hprscript`. Use one call per reasoning stage
## Non-negotiable defaults

- Put distinguishable terms in separate `-p` or `-pi` flags so every hit retains its pattern ID.
- Prefer `-llm` when reading results, `-f` for paths, `-c` for counts, and `-limit N` for existence checks.
- Prefer `-llm` when reading results, `-f` for paths, `-c` for counts, and `-limit N` for existence checks. In `-llm`/`-elide`/`-rollup` output, patterns with zero matches are named in a trailing `--- no matches: … ---` footer — treat that as explicit evidence of absence, qualified with "scan stopped early" when a limit cut the scan. With ≥2 matching patterns a `--- files: … ---` footer gives per-pattern file counts and the overlap (`both:`/`multi-pattern:`) — read the correlation from there instead of joining by hand.
- Trust per-match role tags instead of re-deriving them: `[comment]`/`[string]`/`[import]` in `-llm` (a `role` field in JSONL, `$ROLE` in `-format`) classify each hit lexically, and with `-scope` active a hit on a signature line reads `[def func X]` while body hits read `[in func X]`. Untagged hits in a recognized language are plain code.
- Use an absolute path or glob when the effective cwd is uncertain. Inspect the first emitted path and stop if it escapes the intended tree.
- Add `-summary -require-complete` when a broad sweep must be exhaustive. Do not present a partial scan as complete.
- Restructure unsupported lookarounds or backreferences, or express the relationship with `query` or script phases. Do not fall back to grep or rg.
Expand All @@ -31,6 +32,8 @@ Invoke the binary through Bash as `hprscript`. Use one call per reasoning stage
| Files containing / missing a pattern | `-f` / `-absent` |
| File outline / enclosing function | `-list-scopes` / `-scope auto` |
| Representative usages | `-sample N` |
| Which functions are involved, and how heavily | `-rollup` (one line per scope with per-pattern counts) |
| Full function around a known hit | `hprscript expand file:line[@hash]` (batch refs in one call) |
| Ranked files / packed evidence | `-hotspots N` / `-budget N` |
| Compact scope excerpts | `-elide` |
| Cross-run chunk deduplication | `-elide` or `-budget` with `-seen <path>` |
Expand All @@ -49,7 +52,8 @@ hprscript -ident 'parse config' -glob '**/*.go'
- `-p` / `-pi`: case-sensitive / case-insensitive regex, repeatable.
- `-F` / `-Fi`: literal fixed strings.
- `-name <id>`: name the preceding pattern for output, relations, and `-file-where`.
- `-patterns-from <file>`: load a JSONL rule pack.
- `-desc <text>`: describe the preceding pattern; `-llm`/`-elide` output then opens with a query legend, keeping the result block self-describing for later readers.
- `-patterns-from <file>`: load a JSONL rule pack (entries may carry a `description`).
- `-ident '<terms>'`: find identifier variants such as `parseConfig`, `parse_config`, and `ConfigParser`.
- `-w`: wrap all patterns in word boundaries; use inline `\b` for per-pattern control.

Expand Down Expand Up @@ -78,6 +82,8 @@ hprscript -p 'func\s+(\w+)\(([^)]*)\)' -extract name,args \

Prefer `-in-scope` over line numbers when code may move. Balanced block tracking is lexical, not language-aware; delimiters inside strings or comments can skew it.

To read the whole function around a hit, run `hprscript expand <file>:<line>` on the hit's location instead of re-searching with block flags; batch several refs in one call. Add `-refs` to the search so hits carry `file:line@hash` — expand then verifies the line, recovers it by content if it moved, and reports `stale` (exit 3) instead of rendering the wrong code after an edit.

Relate named patterns by distance or enclosing scope:

```bash
Expand Down
81 changes: 76 additions & 5 deletions src/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ bool parse_nonneg(const char *s, int64_t &out) {
bool set_output_mode(Cli &cli, OutputMode mode) {
if (cli.out_mode_set) {
cli.error = true;
cli.error_message = "output modes -j/-f/-c/-o/-format/-absent/-llm/-elide are mutually exclusive";
cli.error_message = "output modes -j/-f/-c/-o/-format/-absent/-llm/-elide/-rollup are mutually exclusive";
return false;
}
cli.out_mode = mode;
Expand Down Expand Up @@ -182,6 +182,7 @@ void print_help(FILE *out) {
" hprscript investigate -p <seed> [options] [inputs]\n"
" hprscript query -q '<json>' [input overrides]\n"
" hprscript query -query <path> [input overrides]\n"
" hprscript expand <file:line[@hash]> [...refs]\n"
" hprscript edit -p <pat> <edit flags> [files...]\n"
" hprscript apply <plan.json> [apply flags]\n"
"\n"
Expand All @@ -192,9 +193,11 @@ void print_help(FILE *out) {
" -Fi <string> Case-insensitive fixed-string pattern\n"
" -name <id> Name the preceding pattern (shown as pat/$PAT_ID, usable\n"
" as the A/B side of relations and in -file-where)\n"
" -desc <text> Describe the preceding pattern; descriptions print once\n"
" as a query-legend header in -llm/-elide output\n"
" -patterns-from <f> Load patterns from a JSONL rule file: one object per\n"
" line {id, regexp|literal, case_insensitive, word_boundary,\n"
" utf8}; '#' comment lines allowed (repeatable)\n"
" line {id, regexp|literal, description, case_insensitive,\n"
" word_boundary, utf8}; '#' comment lines allowed (repeatable)\n"
" -extract n1,n2,… Re-extract capture groups from the preceding -p/-pi\n"
" -ident 't1 t2 …' Match identifiers whose subtokens include ALL given\n"
" terms, regardless of casing/separator (parseConfig ~\n"
Expand All @@ -213,6 +216,12 @@ void print_help(FILE *out) {
" -git-added-lines Restrict matches to lines ADDED by the selected diffs\n"
" (needs -git-changed/-staged/-range; -p mode only)\n"
" -w Whole-word match (\\b…\\b)\n"
" -no-roles Disable per-match role classification (the `role` JSONL\n"
" field, [def]/[comment]/[string]/[import] tags in -llm,\n"
" and $ROLE in -format)\n"
" -refs Append a @hash content check to line numbers in\n"
" -llm/-rollup output; the resulting file:line@hash refs\n"
" are verified by 'hprscript expand'\n"
" -no-utf8 Disable UTF-8 mode (byte-level matching)\n"
" -ucp Enable Unicode \\w/\\d/\\s (may reject some patterns)\n"
" -limit <n> Max global results\n"
Expand All @@ -234,10 +243,16 @@ void print_help(FILE *out) {
" each pattern (record-level absence, e.g. JSONL fields)\n"
" -llm Token-efficient text for LLM consumption (auto-detects\n"
" block/scope; dedupes file paths; prints a 'limit reached'\n"
" footer when -limit or -max-output-bytes truncates output)\n"
" footer when -limit or -max-output-bytes truncates output;\n"
" ends with a 'no matches' footer naming patterns that\n"
" matched nothing)\n"
" -elide Scope-aware chunks: signature + matched lines with -A/-B\n"
" context; untouched interior lines fold as \"… (+N lines)\"\n"
" (implies -scope auto when no -scope config is given)\n"
" -rollup One line per enclosing scope: line range, name, match\n"
" count with per-pattern breakdown, and one representative\n"
" line; scopeless matches group as \"(top level)\" (implies\n"
" -scope auto when no -scope config is given)\n"
"\n"
"Block extraction (with -p):\n"
" -block-open <s> Opening delimiter (e.g. \"{\")\n"
Expand Down Expand Up @@ -303,6 +318,12 @@ void print_help(FILE *out) {
" record per function/class (JSONL, or -llm flat).\n"
" Honors -in-scope/-in-scope-kind; takes no patterns\n"
"\n"
"Expand mode (hprscript expand <file:line[@hash]> ...):\n"
" Print the enclosing scope of each ref (a search hit's file:line).\n"
" With @hash (from -refs) the line is verified first: a moved line is\n"
" found again by content; a vanished one reports 'stale' (exit 3).\n"
" Honors -scope*/-C (context for scopeless lines)/-max-block-bytes.\n"
"\n"
"Script mode:\n"
" -s <json> Inline script\n"
" -script <path> Script file\n"
Expand Down Expand Up @@ -396,6 +417,9 @@ Cli parse_cli(int argc, char **argv) {
} else if (argc > 1 && eq(argv[1], "query")) {
cli.query.active = true;
first = 2;
} else if (argc > 1 && eq(argv[1], "expand")) {
cli.expand.active = true;
first = 2;
}
for (int i = first; i < argc; ++i) {
const char *a = argv[i];
Expand Down Expand Up @@ -624,6 +648,34 @@ Cli parse_cli(int argc, char **argv) {
cli.patterns.back().name = v;
continue;
}
if (eq(a, "-desc")) {
const char *v = take(i, argc, argv, a, cli); if (!v) return cli;
if (cli.patterns.empty()) {
cli.error = true;
cli.error_message = "-desc must follow a -p/-pi/-F/-Fi";
return cli;
}
if (v[0] == '\0') {
cli.error = true;
cli.error_message = "-desc: empty description";
return cli;
}
if (!cli.patterns.back().desc.empty()) {
cli.error = true;
cli.error_message = "-desc repeated for the same pattern";
return cli;
}
cli.patterns.back().desc = v;
continue;
}
if (eq(a, "-no-roles") || eq(a, "--no-roles")) {
cli.no_roles = true;
continue;
}
if (eq(a, "-refs") || eq(a, "--refs")) {
cli.refs = true;
continue;
}
if (eq(a, "-glob")) {
const char *v = take(i, argc, argv, a, cli); if (!v) return cli;
cli.globs.emplace_back(v);
Expand Down Expand Up @@ -730,6 +782,7 @@ Cli parse_cli(int argc, char **argv) {
if (eq(a, "-absent")) { if (!set_output_mode(cli, OutputMode::Absent)) return cli; continue; }
if (eq(a, "-llm")) { if (!set_output_mode(cli, OutputMode::Llm)) return cli; continue; }
if (eq(a, "-elide")) { if (!set_output_mode(cli, OutputMode::Elide)) return cli; continue; }
if (eq(a, "-rollup")) { if (!set_output_mode(cli, OutputMode::Rollup)) return cli; continue; }
if (eq(a, "-format")) {
const char *v = take(i, argc, argv, a, cli); if (!v) return cli;
if (!set_output_mode(cli, OutputMode::Custom)) return cli;
Expand Down Expand Up @@ -1196,6 +1249,19 @@ Cli parse_cli(int argc, char **argv) {
cli.error_message = "query cannot combine with quick-search output selectors";
}
}
if (cli.expand.active && !cli.error && !cli.show_help &&
!cli.show_version) {
if (!cli.patterns.empty() || !cli.patterns_from.empty() ||
!cli.script_inline.empty() || !cli.script_path.empty()) {
cli.error = true;
cli.error_message =
"expand takes <file:line[@hash]> refs, not patterns/scripts";
} else if (cli.out_mode_set) {
cli.error = true;
cli.error_message =
"expand output is plain text; output-mode flags do not apply";
}
}
return cli;
}

Expand Down Expand Up @@ -1229,7 +1295,7 @@ bool load_patterns_from(Cli &cli) {
if (kv.first != "id" && kv.first != "regexp" &&
kv.first != "literal" && kv.first != "case_insensitive" &&
kv.first != "word_boundary" && kv.first != "utf8" &&
kv.first != "ref")
kv.first != "ref" && kv.first != "description")
return fail("unknown field '" + kv.first + "'");
}
const json::Value *re = pr.value.find("regexp");
Expand Down Expand Up @@ -1270,6 +1336,11 @@ bool load_patterns_from(Cli &cli) {
if (!v->is_bool()) return fail("'ref' must be a boolean");
p.ref = v->as_bool();
}
if (const json::Value *v = pr.value.find("description")) {
if (!v->is_string())
return fail("'description' must be a string");
p.desc = v->as_string();
}
cli.patterns.push_back(std::move(p));
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/cli.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ struct CliPattern {
// `-p`/`-pi`, or by `id` in a -patterns-from file). Empty → auto `p<i>`.
// Shown as `pat`/$PAT_ID and usable as the A/B side of relations.
std::string name;
// Free-text meaning (set by `-desc` on the most recently declared
// pattern, or by `description` in a -patterns-from file). When any
// pattern has one, -llm/-elide output opens with a query-legend header.
std::string desc;
// Comma-separated capture group names (set by `-extract` on the most
// recently declared `-p`/`-pi`). Maps capture group i+1 → names[i].
std::vector<std::string> extract_names;
Expand Down Expand Up @@ -118,6 +122,13 @@ struct QueryOptions {
std::string path;
};

// `hprscript expand <file:line[@hash]> …` — print the enclosing scope of a
// search hit. Refs travel in Cli::positional; scope/-C/-max-block-bytes
// flags are shared with search mode. See src/expand.hpp.
struct ExpandOptions {
bool active = false;
};

struct Cli {
// Original argv, retained for immutable edit-plan provenance.
std::vector<std::string> command;
Expand Down Expand Up @@ -175,6 +186,15 @@ struct Cli {
std::string seen_path;
bool word_boundary = false;
bool no_utf8 = false; // -no-utf8: byte-mode matching
// -no-roles: disable per-match role classification (def/comment/string/
// import — the `role` JSONL field, -llm bracket tags, and $ROLE). On by
// default for per-match output modes; roles are computed lazily per
// matched file, so the cost only exists where output is produced.
bool no_roles = false;
// -refs: append a @hash content check to line numbers in -llm/-rollup
// output, making each hit a verified `file:line@hash` ref that
// `hprscript expand` can check for drift before expanding.
bool refs = false;
bool ucp = false; // -ucp: enable Unicode \w/\d/\s (opt-in)
int64_t limit = -1; // global match cap (-limit)
int64_t per_file_limit = -1; // per-file cap (-m)
Expand Down Expand Up @@ -311,6 +331,7 @@ struct Cli {
ApplyOptions apply;
InvestigateOptions investigate;
QueryOptions query;
ExpandOptions expand;

// Misc.
bool show_version = false;
Expand Down
4 changes: 4 additions & 0 deletions src/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ struct Pattern {
bool ucp = false;
double weight = 1.0; // reserved for ranking mode (post-MVP)

// Free-text meaning of the pattern (-desc / rule-file `description`).
// Surfaced once as a query-legend line in -llm/-elide output.
std::string desc;

// Reference-only pattern (edit mode's -ref): participates in matching —
// relations and -file-where see its matches — but never produces edit
// sites. The qualifier in `-far hit:allow:0` needs this, or its own
Expand Down
Loading
Loading