@appchy/jarvis 0.1.104 → 0.1.105

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.
package/dist/bin.js CHANGED
@@ -10260,7 +10260,7 @@ import { createRequire as createRequire2 } from "module";
10260
10260
  var _require = createRequire2(import.meta.url);
10261
10261
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
10262
10262
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
10263
- var SHA = "9f128de";
10263
+ var SHA = "1bf7cfd";
10264
10264
  var BUILT = "2026-09-11";
10265
10265
  var BUILD = SHA ?? "source";
10266
10266
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -80,6 +80,26 @@ def _roots(owner: str) -> list:
80
80
  return ["", here, "work", "apps/cli"]
81
81
 
82
82
 
83
+ def _moved_to(path: str, by_base: dict) -> str:
84
+ """Where this file went, or "" when that cannot be said with confidence.
85
+
86
+ **A basename alone is far too weak to propose a move from.** `epic.md` matches
87
+ seventeen files here, `task.md` hundreds — and picking the first of them named a
88
+ completely unrelated epic's plan as the new home of a released one, which a
89
+ reader who trusts it would have edited their brief to point at. Worse than
90
+ silence. So a candidate must also carry the named path's own parent directory,
91
+ and if more than one still qualifies, none is offered: "seventeen candidates" is
92
+ not an answer, and neither is the first of them sorted. Reported by jarvis-14
93
+ after `check` did exactly this on their sweep item, 2026-09-12.
94
+ """
95
+ parts = Path(path).parts
96
+ if len(parts) < 2:
97
+ return "" # a bare filename names no place it could have moved FROM
98
+ tail = str(Path(*parts[-2:]))
99
+ fits = [c for c in by_base.get(parts[-1], []) if c == tail or c.endswith("/" + tail)]
100
+ return fits[0] if len(fits) == 1 else ""
101
+
102
+
83
103
  def _resolve(path: str, owner: str, tracked: set) -> str:
84
104
  for root in _roots(owner):
85
105
  candidate = str(Path(root) / path) if root else path
@@ -95,21 +115,51 @@ def _resolve(path: str, owner: str, tracked: set) -> str:
95
115
  BUILT = {"dist", "build", ".next", "out", "node_modules", ".data", ".work"}
96
116
 
97
117
 
98
- def _unjudgeable(path: str) -> bool:
118
+ def _siblings(repo: Path) -> set:
119
+ """Repos sitting beside this one. A brief writes `gotcha/apps/web/src/...` or
120
+ "gotcha's `apps/backoffice/src/lib/experiment.ts`" as often as it writes
121
+ `../gotcha/...`, and none of those are this checkout's to judge — but only the
122
+ `../` spelling announces itself. Found by hand-checking a random twelve of this
123
+ checker's own findings, where two of the twelve were a neighbour's file.
124
+ """
125
+ try:
126
+ return {d.name for d in repo.parent.iterdir()
127
+ if d.is_dir() and (d / ".git").exists() and d.name != repo.name}
128
+ except OSError:
129
+ return set()
130
+
131
+
132
+ def _unjudgeable(path: str, siblings=frozenset()) -> bool:
99
133
  """Paths this cannot have an opinion about, kept apart from paths it says are
100
134
  fine — silence here means *not my question*, not *checked and good*.
101
135
 
102
- Two kinds. Build output, which git never tracks and which is present anyway on
103
- any machine that has built. And anything climbing out of the repo: a brief that
104
- names `../docdoc/.claude/skills/work/SKILL.md` is talking about a sibling repo,
105
- whose contents this checkout cannot see and must not pronounce on.
136
+ Three kinds. Build output, which git never tracks and which is present anyway on
137
+ any machine that has built. Anything climbing out of the repo with `../`. And
138
+ anything rooted at a sibling repo's name, which is the same thing said without
139
+ the `../` and is otherwise indistinguishable from one of ours.
106
140
  """
107
- return path.startswith("../") or bool(BUILT & set(Path(path).parts))
141
+ parts = Path(path).parts
142
+ if path.startswith("../") or (BUILT & set(parts)):
143
+ return True
144
+ return bool(parts) and parts[0] in siblings
145
+
146
+
147
+ #: A sentence asserting that the path is NOT there. The absence is then the claim,
148
+ #: and flagging it pushes somebody into deleting the most informative half of a
149
+ #: correct statement — J-41's `enforced_by` reads "`apps/ws/src/storage.ts` does not
150
+ #: exist", which is the rule's whole point. Reported by jarvis-14, 2026-09-12.
151
+ ABSENT = re.compile(
152
+ r"\b(does not exist|no longer exists?|never existed|is gone|was (deleted|removed)|"
153
+ r"deleted|removed|retired|there is no such|nothing (at|called)|no longer there)\b", re.I)
108
154
 
109
155
 
110
156
  def named_paths(text: str):
111
- """Every path a document names, with the line it is on. Deduplicated per line so
112
- one path written twice in a sentence is one finding, not two."""
157
+ """Every path a document names, with the line it is on and that line's text.
158
+
159
+ Deduplicated per line so one path written twice in a sentence is one finding.
160
+ The line rides along because whether a path is a CLAIM depends on the sentence
161
+ around it: a brief saying a file does not exist is right, not stale.
162
+ """
113
163
  seen = set()
114
164
  for n, line in enumerate(text.splitlines(), 1):
115
165
  for m in NAMED.finditer(line):
@@ -117,7 +167,7 @@ def named_paths(text: str):
117
167
  if key in seen:
118
168
  continue
119
169
  seen.add(key)
120
- yield m.group(1), n
170
+ yield m.group(1), n, line
121
171
 
122
172
 
123
173
  def drift(repo: Path, docs, tracked=None) -> list:
@@ -130,6 +180,7 @@ def drift(repo: Path, docs, tracked=None) -> list:
130
180
  that question.
131
181
  """
132
182
  tracked = _tracked(repo) if tracked is None else tracked
183
+ siblings = _siblings(Path(repo))
133
184
  by_base = defaultdict(list)
134
185
  for t in tracked:
135
186
  by_base[Path(t).name].append(t)
@@ -141,16 +192,15 @@ def drift(repo: Path, docs, tracked=None) -> list:
141
192
  text = (repo / rel).read_text(errors="ignore")
142
193
  except OSError:
143
194
  continue
144
- for path, line in named_paths(text):
145
- if "/" not in path or _unjudgeable(path):
195
+ for path, line, text_of_line in named_paths(text):
196
+ if "/" not in path or _unjudgeable(path, siblings):
146
197
  continue # no location to check, or not this repo's to answer for
147
198
  if _resolve(path, rel, tracked):
148
199
  continue
149
- elsewhere = sorted(by_base.get(Path(path).name, []))
150
- if elsewhere:
151
- found.append((rel, line, path, "moved", elsewhere[0]))
152
- else:
153
- found.append((rel, line, path, "gone", ""))
200
+ if ABSENT.search(text_of_line):
201
+ continue # the sentence says it is not there; that is the claim, not a stale one
202
+ where = _moved_to(path, by_base)
203
+ found.append((rel, line, path, "moved" if where else "gone", where))
154
204
  return found
155
205
 
156
206
 
@@ -7501,9 +7501,11 @@ def test_a_brief_naming_a_file_that_is_gone_is_reported():
7501
7501
  assert [(f[2], f[3]) for f in found] == [("src/vanished.ts", "gone")]
7502
7502
 
7503
7503
 
7504
- def test_a_file_that_merely_moved_is_reported_as_moved_and_says_where():
7505
- # A link to fix, not a brief to rethink burying one in the other makes the
7506
- # rarer and more important finding invisible.
7504
+ def test_a_basename_match_under_a_different_parent_is_gone_not_moved():
7505
+ # This test used to assert the opposite, and asserted a bug: a file sharing only
7506
+ # its name with something elsewhere is not evidence of where it went. Proposing
7507
+ # a destination from that is a guess a reader would act on. Only a candidate
7508
+ # carrying the named path's own parent earns the word "moved".
7507
7509
  from harness.drift import drift
7508
7510
  with tempfile.TemporaryDirectory() as tmp:
7509
7511
  repo = _git_repo(tmp, push=False)
@@ -7516,8 +7518,8 @@ def test_a_file_that_merely_moved_is_reported_as_moved_and_says_where():
7516
7518
 
7517
7519
  found = drift(repo, ["work/brief.md"])
7518
7520
  assert len(found) == 1
7519
- assert found[0][3] == "moved"
7520
- assert found[0][4] == "packages/deep/moved.ts"
7521
+ assert found[0][3] == "gone"
7522
+ assert found[0][4] == ""
7521
7523
 
7522
7524
 
7523
7525
  def test_a_path_written_the_way_a_writer_means_it_is_not_a_finding():
@@ -7671,6 +7673,89 @@ def test_the_check_reads_the_epic_plan_too_not_only_the_brief():
7671
7673
  assert [f[2] for f in found] == ["src/long-gone.ts"]
7672
7674
 
7673
7675
 
7676
+ def test_a_bare_basename_never_proposes_a_destination():
7677
+ # `epic.md` matches seventeen files here and `task.md` hundreds. Picking the
7678
+ # first of them named an unrelated epic's plan as the new home of a released
7679
+ # one — which a reader who trusted it would have edited their brief to point at.
7680
+ # Worse than silence. Reported by jarvis-14 after `check` did exactly this.
7681
+ from harness.drift import drift
7682
+ with tempfile.TemporaryDirectory() as tmp:
7683
+ repo = _git_repo(tmp, push=False)
7684
+ for d in ("alpha", "beta"):
7685
+ (repo / "epics" / d).mkdir(parents=True, exist_ok=True)
7686
+ (repo / "epics" / d / "epic.md").write_text("# an epic\n")
7687
+ brief = repo / "work" / "brief.md"
7688
+ brief.parent.mkdir(parents=True, exist_ok=True)
7689
+ brief.write_text("the released `released-cut/epic.md`\n")
7690
+ _git(repo, "add", "-A"); _git(repo, "commit", "-m", "seed")
7691
+
7692
+ found = drift(repo, ["work/brief.md"])
7693
+ assert [(f[2], f[3], f[4]) for f in found] == [("released-cut/epic.md", "gone", "")]
7694
+
7695
+
7696
+ def test_several_candidates_suppress_the_suggestion_rather_than_picking_one():
7697
+ from harness.drift import drift
7698
+ with tempfile.TemporaryDirectory() as tmp:
7699
+ repo = _git_repo(tmp, push=False)
7700
+ for d in ("one", "two"):
7701
+ (repo / d / "clients").mkdir(parents=True, exist_ok=True)
7702
+ (repo / d / "clients" / "reach.ts").write_text("export const a = 1;\n")
7703
+ brief = repo / "work" / "brief.md"
7704
+ brief.parent.mkdir(parents=True, exist_ok=True)
7705
+ brief.write_text("see `src/clients/reach.ts`\n")
7706
+ _git(repo, "add", "-A"); _git(repo, "commit", "-m", "seed")
7707
+
7708
+ found = drift(repo, ["work/brief.md"])
7709
+ assert found[0][3] == "gone" and found[0][4] == ""
7710
+
7711
+
7712
+ def test_one_candidate_carrying_the_named_directory_is_a_confident_move():
7713
+ from harness.drift import drift
7714
+ with tempfile.TemporaryDirectory() as tmp:
7715
+ repo = _git_repo(tmp, push=False)
7716
+ (repo / "packages" / "data" / "src" / "mcp" / "tools").mkdir(parents=True)
7717
+ (repo / "packages" / "data" / "src" / "mcp" / "tools" / "schema.ts").write_text("x\n")
7718
+ brief = repo / "work" / "brief.md"
7719
+ brief.parent.mkdir(parents=True, exist_ok=True)
7720
+ brief.write_text("see `packages/data/src/tools/schema.ts`\n")
7721
+ _git(repo, "add", "-A"); _git(repo, "commit", "-m", "seed")
7722
+
7723
+ found = drift(repo, ["work/brief.md"])
7724
+ assert found[0][3] == "moved"
7725
+ assert found[0][4] == "packages/data/src/mcp/tools/schema.ts"
7726
+
7727
+
7728
+ def test_a_sentence_saying_the_file_is_not_there_is_a_claim_not_a_stale_one():
7729
+ # J-41's enforced_by reads "`apps/ws/src/storage.ts` does not exist" — the
7730
+ # absence IS the rule. Flagging it pushes somebody into deleting the most
7731
+ # informative half of a correct statement.
7732
+ from harness.drift import drift
7733
+ with tempfile.TemporaryDirectory() as tmp:
7734
+ repo = _git_repo(tmp, push=False)
7735
+ brief = repo / "work" / "brief.md"
7736
+ brief.parent.mkdir(parents=True, exist_ok=True)
7737
+ brief.write_text("the only resolver, and `apps/ws/src/storage.ts` does not exist\n"
7738
+ "`packages/old/gone.ts` was deleted on purpose\n")
7739
+ _git(repo, "add", "-A"); _git(repo, "commit", "-m", "seed")
7740
+
7741
+ assert drift(repo, ["work/brief.md"]) == []
7742
+
7743
+
7744
+ def test_a_sibling_repos_path_is_not_this_checkouts_to_judge_however_it_is_spelled():
7745
+ # `../gotcha/x.ts` announces itself; `gotcha/x.ts` does not, and is how a brief
7746
+ # usually writes it. Two of a random twelve findings were a neighbour's file.
7747
+ from harness.drift import drift
7748
+ with tempfile.TemporaryDirectory() as tmp:
7749
+ repo = _git_repo(tmp, push=False) # lands at <tmp>/repo
7750
+ (repo.parent / "gotcha" / ".git").mkdir(parents=True) # beside it
7751
+ brief = repo / "work" / "brief.md"
7752
+ brief.parent.mkdir(parents=True, exist_ok=True)
7753
+ brief.write_text("gotcha's `gotcha/apps/web/src/rate-limit.ts` and ours `src/mine.ts`\n")
7754
+ _git(repo, "add", "-A"); _git(repo, "commit", "-m", "seed")
7755
+
7756
+ assert [f[2] for f in drift(repo, ["work/brief.md"])] == ["src/mine.ts"]
7757
+
7758
+
7674
7759
  if __name__ == "__main__":
7675
7760
  tests = [v for k, v in sorted(globals().items())
7676
7761
  if k.startswith("test_") and callable(v)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -61,9 +61,9 @@
61
61
  "@jarvis/board": "0.1.0",
62
62
  "@jarvis/data": "0.1.0",
63
63
  "@jarvis/errors": "1.0.0",
64
+ "@jarvis/logger": "1.0.0",
64
65
  "@jarvis/rpc": "1.0.0",
65
66
  "@jarvis/types": "1.0.0",
66
- "@jarvis/logger": "1.0.0",
67
67
  "@jarvis/typescript-config": "1.0.0",
68
68
  "@jarvis/ui": "0.1.0",
69
69
  "@jarvis/vitest-config": "1.0.0"