@appchy/jarvis 0.1.74 → 0.1.75

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.
@@ -17,6 +17,10 @@ from .lint import _feature_ac_ids, feature_ac_levels
17
17
  #: exists to remove.
18
18
  FOREIGN_REPOS: tuple = ()
19
19
 
20
+ #: This repo's own standards file, relative to the repo root. Set from config
21
+ #: (`spine.standards`); empty when a repo keeps no second ledger.
22
+ STANDARDS: str = ""
23
+
20
24
  #: Directories under `work/` that are NOT durable governance. `product/` and
21
25
  #: `architecture/` are handled on their own (they are the two sides the
22
26
  #: single-feature check compares); the rest here hold work-in-flight or history,
@@ -156,6 +160,29 @@ def _foreign_qualifier() -> re.Pattern:
156
160
  return re.compile(r"(?!)")
157
161
  alt = "|".join(re.escape(r) for r in FOREIGN_REPOS)
158
162
  return re.compile(rf"(?:{alt})\s*[:=]?\s*[`*\[(\"']{{0,3}}$", re.IGNORECASE)
163
+ def _standards_ids(root: Path) -> set:
164
+ """Ids this repo declares in its standards file, which does not live under `work/`.
165
+
166
+ Without this the check walked the board alone and reported every one of them as
167
+ resolving to nothing: twenty in this repo, twelve in a sibling, all false, all
168
+ cited by live briefs that were reading the rules correctly. They resolve
169
+ perfectly — they are declared at the repo root, in the file the config already
170
+ names.
171
+
172
+ Twenty false errors on top of three real warnings is worse than a wrong number:
173
+ it is how somebody learns the report is noise and stops reading the part that
174
+ was telling them something.
175
+
176
+ `doctor`'s id check reaches for the same pointer, so the two cannot disagree
177
+ about what counts as declared here.
178
+ """
179
+ from . import config # deferred — `config.apply` imports this module
180
+ if not STANDARDS:
181
+ return set()
182
+ named = config.standards_index({"spine": {"standards": STANDARDS}}, root.parent)
183
+ return {ids.normalise(i) for i, _ in named}
184
+
185
+
159
186
  def _align_citations(root: Path) -> list:
160
187
  """A cited id that resolves to nothing."""
161
188
  out = []
@@ -165,6 +192,7 @@ def _align_citations(root: Path) -> list:
165
192
  defined = set(definition_sites(root))
166
193
  for _, _, rules, _ in hosts(root):
167
194
  defined |= set(rules)
195
+ defined |= _standards_ids(root)
168
196
  foreign = _foreign_qualifier()
169
197
  dangling: dict = {}
170
198
  for p in live_surfaces(root):
@@ -623,6 +623,7 @@ def apply(cfg: dict) -> None:
623
623
  kickoff.SPINE = [s for s in ([cfg["spine"]["standards"]] if cfg["spine"]["standards"]
624
624
  else []) + list(cfg["spine"]["conventions"]) if s]
625
625
  align.FOREIGN_REPOS = tuple(cfg["ids"]["foreign"])
626
+ align.STANDARDS = cfg["spine"]["standards"] or ""
626
627
  registry.DOMAIN_ORDER = tuple(cfg["domains"]["order"])
627
628
  # From the SHIPPED set each time, never from whatever the last `apply` left
628
629
  # behind. Unioning into the live value made a second call in one process keep the
@@ -284,6 +284,29 @@ def test_a_doc_narrating_a_retirement_is_not_a_dangling_citation():
284
284
  assert not [f for f in align._align_citations(root) if f[1] == "dangling-citation"]
285
285
 
286
286
 
287
+ def test_a_standard_declared_at_the_repo_root_is_not_a_dangling_citation():
288
+ # `align` walked `work/` alone, so every id declared in the standards file at
289
+ # the repo ROOT read as resolving to nothing — twenty here, twelve in a
290
+ # sibling, all false and all cited by briefs that were reading the rules
291
+ # correctly. Twenty false errors on three real warnings is how somebody
292
+ # learns the report is noise.
293
+ with tempfile.TemporaryDirectory() as tmp:
294
+ root = _citing_repo(tmp, "Written to `S-1`, and also `S-9`.\n")
295
+ (Path(tmp) / "STANDARDS.md").write_text(
296
+ "# Standards\n\n## S-1. Function signatures\n\ntext\n")
297
+ ids.configure("D", ("S",))
298
+ align.STANDARDS = "STANDARDS.md"
299
+ try:
300
+ found = [m for sev, c, m in align._align_citations(root)
301
+ if c == "dangling-citation"]
302
+ finally:
303
+ align.STANDARDS = ""
304
+ assert not any("S-1" in m for m in found), found
305
+ # And the guard: a standards-shaped id the file does not declare is still
306
+ # reported, or this would be an amnesty rather than a lookup.
307
+ assert any("S-9" in m for m in found), found
308
+
309
+
287
310
  def test_a_real_dangling_id_still_reports_in_a_file_that_retires_another():
288
311
  # The guard on the check above: file-scoped narration must not become a
289
312
  # blanket amnesty for every id the document happens to mention.
@@ -5482,6 +5505,30 @@ def test_an_epic_move_snapshots_the_repo_once_not_once_per_task():
5482
5505
  assert len(calls) == 1, f"scanned the repo {len(calls)} times for 3 tasks"
5483
5506
 
5484
5507
 
5508
+ def test_the_config_is_found_from_a_subdirectory_of_the_repo():
5509
+ # The tree search walks up for a `work/`; the config search stopped at the cwd.
5510
+ # So standing one directory inside a repo found no config and every check ran
5511
+ # in the shipped dialect against the right tree — `align` from the root
5512
+ # reported twenty citations and from `apps/cli/` reported thirteen entirely
5513
+ # different ones. One tree, two answers, decided by where you stood.
5514
+ with tempfile.TemporaryDirectory() as tmp:
5515
+ repo = Path(tmp) / "repo"
5516
+ (repo / "work" / "versions").mkdir(parents=True)
5517
+ deep = repo / "src" / "deep"
5518
+ deep.mkdir(parents=True)
5519
+
5520
+ was = Path.cwd()
5521
+ env = {k: os.environ.pop(k) for k in ("CLAUDE_PROJECT_DIR", "WORK_DIR")
5522
+ if k in os.environ}
5523
+ os.chdir(deep)
5524
+ try:
5525
+ assert entry._project_root({}) == repo.resolve(), \
5526
+ "the config search stopped at the cwd instead of finding the repo"
5527
+ finally:
5528
+ os.chdir(was)
5529
+ os.environ.update(env)
5530
+
5531
+
5485
5532
  def _migrate(tmp: str, **flags):
5486
5533
  """`migrate` against a fixture, with WORK_DIR pointing at it."""
5487
5534
  from harness import migrate as migrate_mod
package/harness/work.py CHANGED
@@ -193,7 +193,16 @@ def parse_argv(argv):
193
193
  def _project_root(flags) -> Path:
194
194
  """The repo the harness is acting on. `--project` wins, then the env, then the
195
195
  cwd — the same precedence `find_work_root` uses, so config and tree can never
196
- disagree about which repo this is."""
196
+ disagree about which repo this is.
197
+
198
+ The last step WALKS UP, and that is the half this claimed and did not do. The
199
+ tree search walks up for a `work/`; this stopped at the cwd, so standing one
200
+ directory into a repo found no config and every check ran in the shipped
201
+ dialect against the right tree. Measured here: `align` from the root reported
202
+ twenty citations, and from `apps/cli/` reported thirteen entirely different
203
+ ones, having stopped recognising this repo's own ids altogether. One tree, two
204
+ answers, decided by where the caller happened to be standing.
205
+ """
197
206
  import os
198
207
  p = flags.get("project") or os.environ.get("CLAUDE_PROJECT_DIR")
199
208
  if p:
@@ -201,7 +210,8 @@ def _project_root(flags) -> Path:
201
210
  wd = os.environ.get("WORK_DIR")
202
211
  if wd:
203
212
  return Path(wd).resolve().parent
204
- return Path.cwd().resolve()
213
+ root, _, cur = locate_work_root()
214
+ return root.parent if root else cur
205
215
 
206
216
 
207
217
  def main() -> int:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,17 +56,17 @@
56
56
  "tsup": "^8.5.1",
57
57
  "typescript": "^5.7.0",
58
58
  "vitest": "^2.1.0",
59
+ "@jarvis/agents": "1.0.0",
59
60
  "@jarvis/anthropic": "1.0.0",
60
- "@jarvis/board": "0.1.0",
61
61
  "@jarvis/data": "0.1.0",
62
+ "@jarvis/board": "0.1.0",
62
63
  "@jarvis/errors": "1.0.0",
63
64
  "@jarvis/logger": "1.0.0",
64
65
  "@jarvis/rpc": "1.0.0",
65
66
  "@jarvis/types": "1.0.0",
66
- "@jarvis/typescript-config": "1.0.0",
67
67
  "@jarvis/ui": "0.1.0",
68
68
  "@jarvis/vitest-config": "1.0.0",
69
- "@jarvis/agents": "1.0.0"
69
+ "@jarvis/typescript-config": "1.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",