@expo/code-review-cli 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ Exit 0 = every ref holds. Exit 1 = at least one is broken.
22
22
 
23
23
  Options:
24
24
  --root <dir> Repository root to check (default: the current git repo).
25
- --json Emit {ok, problems:[{file, line, kind, problem}]} on stdout.
25
+ --json Emit {ok, problems:[{file, line, kind, problem, target?}]} on stdout.
26
26
  `;
27
27
  const KIND_LABEL = {
28
28
  "broken-ref": "broken ref",
@@ -7,6 +7,7 @@ import { CONFIG_DIRNAME, stripJsonComments, stripTrailingCommas } from "../confi
7
7
  import { ROUTING_FILENAME } from "../config/routing.js";
8
8
  import { git, pathInside } from "./exec.js";
9
9
  import { matchesIgnore } from "./noise.js";
10
+ import { isAmbientRuntimeConfig } from "./scrub.js";
10
11
  /**
11
12
  * Built by concatenation so this module's own regexes and doc comments are not
12
13
  * themselves collected as refs by a scanner (the repo's `ref-check` does the same).
@@ -40,6 +41,7 @@ const CITATION_EXTENSIONS = new Set([
40
41
  ".css",
41
42
  ".go",
42
43
  ".graphql",
44
+ ".hcl",
43
45
  ".h",
44
46
  ".hpp",
45
47
  ".html",
@@ -63,6 +65,8 @@ const CITATION_EXTENSIONS = new Set([
63
65
  ".sh",
64
66
  ".sql",
65
67
  ".swift",
68
+ ".tf",
69
+ ".tfvars",
66
70
  ".toml",
67
71
  ".ts",
68
72
  ".tsx",
@@ -608,10 +612,11 @@ export async function checkConfigRefs(options) {
608
612
  // Does an extensionless token name something real? Cached: prompts repeat the same
609
613
  // paths, and each miss would otherwise cost a stat per occurrence.
610
614
  const resolvable = new Map();
611
- /** The root-relative path an extensionless token names, or null if it names nothing. */
615
+ /** The root-relative path a token names, or null. Scope first: a scoped prompt
616
+ * citing `alerts/` means its own tree even when the root has one too. */
612
617
  const namedPath = async (token, scopeRoot) => {
613
618
  for (const candidate of pathishCandidates(token)) {
614
- for (const base of [root, scopeRoot]) {
619
+ for (const base of [scopeRoot, root]) {
615
620
  const absolute = path.resolve(base, candidate);
616
621
  let resolved = resolvable.get(absolute);
617
622
  if (resolved === undefined) {
@@ -630,6 +635,18 @@ export async function checkConfigRefs(options) {
630
635
  }
631
636
  return null;
632
637
  };
638
+ /** The glob a wildcard token names, rebased onto the scope when that is what
639
+ * matches, so the suggested `glob:` ref actually resolves. */
640
+ const namedGlob = (token, scopeRoot) => {
641
+ const scopeRel = path.relative(root, scopeRoot).split(path.sep).join("/");
642
+ const candidates = [...(scopeRel && scopeRel !== "." ? [`${scopeRel}/${token}`] : []), token];
643
+ for (const candidate of candidates) {
644
+ if (index.files.some((file) => matchesIgnore(file, candidate))) {
645
+ return candidate;
646
+ }
647
+ }
648
+ return null;
649
+ };
633
650
  for (const dir of dirs) {
634
651
  const scopeRoot = path.dirname(dir);
635
652
  for (const file of await setupFiles(dir)) {
@@ -658,6 +675,7 @@ export async function checkConfigRefs(options) {
658
675
  line: ref.line,
659
676
  kind: problem.startsWith("cites a line number") ? "line-number-ref" : "broken-ref",
660
677
  problem,
678
+ target: ref.target,
661
679
  });
662
680
  continue;
663
681
  }
@@ -674,18 +692,15 @@ export async function checkConfigRefs(options) {
674
692
  }
675
693
  }
676
694
  for (const { line, token } of findProseCitations(text)) {
677
- // Extension or wildcard tail ⇒ a citation on shape alone. Otherwise it only
678
- // counts if it names something real: `eas-build-worker/terraform` and
679
- // `general-central/{module,production}` are paths, `anthropic/claude-opus-5`
680
- // is shaped identically and is not.
681
- const named = isCodeCitation(token) ? null : await namedPath(token, scopeRoot);
695
+ const isWild = token.includes("*");
696
+ const named = isWild ? namedGlob(token, scopeRoot) : await namedPath(token, scopeRoot);
697
+ // Shape alone makes a citation; otherwise it must name something real, or
698
+ // `anthropic/claude-opus-5` would read as a path.
682
699
  if (!isCodeCitation(token) && !named) {
683
700
  continue;
684
701
  }
685
- // Coverage must accept the path the token RESOLVED to, not just the token as
686
- // written: a prompt says `cert-manager` and the ref that pins it is
687
- // `infrastructure/cert-manager/`. Without this, the fix the message suggests
688
- // does not silence the citation it was suggested for.
702
+ // Coverage accepts the resolved path too, else the only ref that silences a
703
+ // scope-relative citation is an over-broad `glob:**/<basename>`.
689
704
  const forms = [...citationForms(token), ...(named ? coveringForms(named) : [])];
690
705
  if (ignored.has(token) ||
691
706
  forms.some((form) => covered.has(form)) ||
@@ -693,9 +708,12 @@ export async function checkConfigRefs(options) {
693
708
  continue;
694
709
  }
695
710
  const lineCitation = LINE_CITATION_RE.exec(token);
696
- // For an extensionless token the suggestion is the path that actually resolved,
697
- // which is also how a scope-relative citation learns its root-relative form.
698
- const suggestion = named ?? suggestedRef(token);
711
+ // A wildcard token means the family; anything else gets the precise resolved path.
712
+ const suggestion = isWild
713
+ ? named
714
+ ? `${GLOB_PREFIX}${named}`
715
+ : suggestedRef(token)
716
+ : (named ?? suggestedRef(token));
699
717
  problems.push({
700
718
  file: relative,
701
719
  line,
@@ -703,6 +721,7 @@ export async function checkConfigRefs(options) {
703
721
  problem: lineCitation
704
722
  ? `\`${token}\` pins a line number; cite the file or a \`#symbol\` instead, as \`${REF_MARK} ${suggestedRef(lineCitation[1])}\``
705
723
  : `\`${token}\` cites code without a ref; add \`${REF_MARK} ${suggestion} — why it matters\` (or \`${IGNORE_MARK} ${token}\` if it is not a path)`,
724
+ target: token,
706
725
  });
707
726
  }
708
727
  }
@@ -724,6 +743,12 @@ export async function checkConfigRefs(options) {
724
743
  citedPaths: [...citedPaths].sort(),
725
744
  };
726
745
  }
746
+ /** The file a target names, with any `glob:` prefix, `#anchor` or `:line` stripped. */
747
+ function citedBasename(target) {
748
+ const withoutPrefix = target.startsWith(GLOB_PREFIX) ? target.slice(GLOB_PREFIX.length) : target;
749
+ const withoutLine = LINE_CITATION_RE.exec(withoutPrefix)?.[1] ?? withoutPrefix;
750
+ return path.basename(splitAnchor(withoutLine)[0]);
751
+ }
727
752
  /** How many examples a review-side note names before saying "and N more". */
728
753
  const NOTE_EXAMPLES = 5;
729
754
  function andMore(items) {
@@ -746,7 +771,11 @@ export async function reviewSetupRefNotes(options) {
746
771
  return []; // a check that cannot run must never degrade the review
747
772
  }
748
773
  const notes = [];
749
- const broken = report.problems.filter((problem) => problem.kind !== "unannotated-citation");
774
+ // The read root is scrubbed of ambient runtime config (AGENTS.md, CLAUDE.md, .env…),
775
+ // so a ref to one is missing by design here, not stale. The template itself cites
776
+ // AGENTS.md, which would make this note cry wolf on every PR.
777
+ const broken = report.problems.filter((problem) => problem.kind !== "unannotated-citation" &&
778
+ !(problem.target && isAmbientRuntimeConfig(citedBasename(problem.target))));
750
779
  if (broken.length > 0) {
751
780
  notes.push(`The reviewer setup cites code that no longer resolves (${broken.length} ref(s)): ` +
752
781
  `${andMore(broken.map((problem) => `${problem.file}:${problem.line}`))}. ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/code-review-cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Generic, config-driven AI code reviewer engine. Repos supply their agents via .expo-code-review/.",
5
5
  "license": "MIT",
6
6
  "repository": {