@expo/code-review-cli 0.9.0 → 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,24 +692,28 @@ export async function checkConfigRefs(options) {
674
692
  }
675
693
  }
676
694
  for (const { line, token } of findProseCitations(text)) {
677
- const forms = citationForms(token);
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.
699
+ if (!isCodeCitation(token) && !named) {
700
+ continue;
701
+ }
702
+ // Coverage accepts the resolved path too, else the only ref that silences a
703
+ // scope-relative citation is an over-broad `glob:**/<basename>`.
704
+ const forms = [...citationForms(token), ...(named ? coveringForms(named) : [])];
678
705
  if (ignored.has(token) ||
679
706
  forms.some((form) => covered.has(form)) ||
680
707
  coveringGlobs.some((pattern) => forms.some((form) => matchesIgnore(form.replace(/^\.\.\.\//, ""), pattern)))) {
681
708
  continue;
682
709
  }
683
- // Extension or wildcard tail ⇒ a citation on shape alone. Otherwise it only
684
- // counts if it names something real: `eas-build-worker/terraform` and
685
- // `general-central/{module,production}` are paths, `anthropic/claude-opus-5`
686
- // is shaped identically and is not.
687
- const named = isCodeCitation(token) ? null : await namedPath(token, scopeRoot);
688
- if (!isCodeCitation(token) && !named) {
689
- continue;
690
- }
691
710
  const lineCitation = LINE_CITATION_RE.exec(token);
692
- // For an extensionless token the suggestion is the path that actually resolved,
693
- // which is also how a scope-relative citation learns its root-relative form.
694
- 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));
695
717
  problems.push({
696
718
  file: relative,
697
719
  line,
@@ -699,6 +721,7 @@ export async function checkConfigRefs(options) {
699
721
  problem: lineCitation
700
722
  ? `\`${token}\` pins a line number; cite the file or a \`#symbol\` instead, as \`${REF_MARK} ${suggestedRef(lineCitation[1])}\``
701
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,
702
725
  });
703
726
  }
704
727
  }
@@ -720,6 +743,12 @@ export async function checkConfigRefs(options) {
720
743
  citedPaths: [...citedPaths].sort(),
721
744
  };
722
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
+ }
723
752
  /** How many examples a review-side note names before saying "and N more". */
724
753
  const NOTE_EXAMPLES = 5;
725
754
  function andMore(items) {
@@ -742,7 +771,11 @@ export async function reviewSetupRefNotes(options) {
742
771
  return []; // a check that cannot run must never degrade the review
743
772
  }
744
773
  const notes = [];
745
- 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))));
746
779
  if (broken.length > 0) {
747
780
  notes.push(`The reviewer setup cites code that no longer resolves (${broken.length} ref(s)): ` +
748
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.0",
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": {