@appchy/jarvis 0.1.116 → 0.1.117
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 +1 -1
- package/harness/harness/links.py +36 -17
- package/harness/harness/report.py +8 -0
- package/harness/test_work.py +37 -0
- package/harness/work.py +7 -1
- package/package.json +4 -4
package/dist/bin.js
CHANGED
|
@@ -10284,7 +10284,7 @@ import { createRequire as createRequire2 } from "module";
|
|
|
10284
10284
|
var _require = createRequire2(import.meta.url);
|
|
10285
10285
|
var VERSION2 = _require("../package.json").version ?? "0.0.0";
|
|
10286
10286
|
var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
|
|
10287
|
-
var SHA = "
|
|
10287
|
+
var SHA = "d379214";
|
|
10288
10288
|
var BUILT = "2026-09-12";
|
|
10289
10289
|
var BUILD = SHA ?? "source";
|
|
10290
10290
|
var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
|
package/harness/harness/links.py
CHANGED
|
@@ -199,13 +199,22 @@ def repairing(repo: Path, indent: str = " "):
|
|
|
199
199
|
_ITEM_FILES = ("task.md", "epic.md", "version.md")
|
|
200
200
|
|
|
201
201
|
|
|
202
|
-
def
|
|
202
|
+
def _prose(text: str) -> str:
|
|
203
|
+
"""`text` with inline code spans blanked, offsets preserved.
|
|
204
|
+
|
|
205
|
+
Blanked rather than removed so a position found here is the same position in the
|
|
206
|
+
real text — which is what lets the WRITE path edit exactly the links the READ path
|
|
207
|
+
found, and nothing else.
|
|
208
|
+
"""
|
|
209
|
+
return CODE_SPAN.sub(lambda m: " " * len(m.group(0)), text)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _index(docs) -> dict:
|
|
203
213
|
"""Board item folders by name. A name with two homes is left out rather than guessed."""
|
|
204
214
|
homes: dict = {}
|
|
205
|
-
for doc in
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
homes.setdefault(doc.parent.name, set()).add(doc.parent.resolve())
|
|
215
|
+
for doc in docs:
|
|
216
|
+
if doc.name in _ITEM_FILES:
|
|
217
|
+
homes.setdefault(doc.parent.name, set()).add(doc.parent.resolve())
|
|
209
218
|
return {name: next(iter(dirs)) for name, dirs in homes.items() if len(dirs) == 1}
|
|
210
219
|
|
|
211
220
|
|
|
@@ -261,15 +270,15 @@ def dangling(base: Path) -> list:
|
|
|
261
270
|
before-state to be repaired by identity, so it is settled from its own text or
|
|
262
271
|
not at all.
|
|
263
272
|
"""
|
|
264
|
-
|
|
273
|
+
docs = sorted(_docs(base))
|
|
274
|
+
index = _index(docs)
|
|
265
275
|
out = []
|
|
266
|
-
for doc in
|
|
276
|
+
for doc in docs:
|
|
267
277
|
try:
|
|
268
278
|
text = doc.read_text(errors="ignore")
|
|
269
279
|
except OSError:
|
|
270
280
|
continue
|
|
271
|
-
|
|
272
|
-
for href in dict.fromkeys(m.group(1) for m in LINK.finditer(text)):
|
|
281
|
+
for href in dict.fromkeys(m.group(1) for m in LINK.finditer(_prose(text))):
|
|
273
282
|
if "://" in href or href.startswith(("#", "mailto:", "/")):
|
|
274
283
|
continue
|
|
275
284
|
if (doc.parent / href.split("#")[0]).exists():
|
|
@@ -281,9 +290,12 @@ def dangling(base: Path) -> list:
|
|
|
281
290
|
def repair_dangling(base: Path, apply: bool = False) -> tuple:
|
|
282
291
|
"""Point every settled broken link at what it names. Returns (fixed, stuck).
|
|
283
292
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
293
|
+
**Edits by position, never by `str.replace`.** The same href can appear twice in
|
|
294
|
+
one file — once as the broken link and once inside a code span quoting it, which
|
|
295
|
+
is exactly what a brief warning about the mistake looks like. A blanket replace
|
|
296
|
+
rewrites the quotation too, so the document that documents the error silently
|
|
297
|
+
stops showing it. Only the positions the read path found, which is prose, are
|
|
298
|
+
touched.
|
|
287
299
|
"""
|
|
288
300
|
fixed, stuck = 0, []
|
|
289
301
|
edits: dict = {}
|
|
@@ -296,12 +308,19 @@ def repair_dangling(base: Path, apply: bool = False) -> tuple:
|
|
|
296
308
|
if new == href:
|
|
297
309
|
stuck.append((doc, href))
|
|
298
310
|
continue
|
|
299
|
-
edits.setdefault(doc, []
|
|
311
|
+
edits.setdefault(doc, {})[href] = new
|
|
300
312
|
fixed += 1
|
|
301
313
|
if apply:
|
|
302
|
-
for doc,
|
|
314
|
+
for doc, mapping in edits.items():
|
|
303
315
|
text = doc.read_text(errors="ignore")
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
316
|
+
pieces, last = [], 0
|
|
317
|
+
for m in LINK.finditer(_prose(text)):
|
|
318
|
+
replacement = mapping.get(m.group(1))
|
|
319
|
+
if replacement is None:
|
|
320
|
+
continue
|
|
321
|
+
pieces.append(text[last:m.start(1)])
|
|
322
|
+
pieces.append(replacement)
|
|
323
|
+
last = m.end(1)
|
|
324
|
+
pieces.append(text[last:])
|
|
325
|
+
doc.write_text("".join(pieces))
|
|
307
326
|
return fixed, stuck
|
|
@@ -170,6 +170,14 @@ def misalignments(root: Path) -> list:
|
|
|
170
170
|
#: `handoff.md` that `archive` stripped, is an accurate record of a world that is
|
|
171
171
|
#: gone. Demanding those resolve would make the gate unsatisfiable for a reason
|
|
172
172
|
#: nobody could act on.
|
|
173
|
+
#:
|
|
174
|
+
#: **`--fix` deliberately does not honour this, and the asymmetry is the point.** The
|
|
175
|
+
#: exemption says a shipped cut cannot be REQUIRED to resolve, not that it should be
|
|
176
|
+
#: left broken where it can be mended. A repair only ever moves a link onto the file
|
|
177
|
+
#: it already named — `repair_dangling` refuses anything two candidates answer — so a
|
|
178
|
+
#: shipped plan still points at the same document, which is what `links.repairing`
|
|
179
|
+
#: does to archived docs on every folder move anyway. What survives untouched is the
|
|
180
|
+
#: set that cannot be settled, which is where the record actually lives.
|
|
173
181
|
_SHIPPED = ("versions/complete/", "archive/")
|
|
174
182
|
|
|
175
183
|
|
package/harness/test_work.py
CHANGED
|
@@ -2106,6 +2106,30 @@ def test_a_standing_broken_link_is_settled_by_depth_or_by_board_name():
|
|
|
2106
2106
|
assert links.dangling(base) == []
|
|
2107
2107
|
|
|
2108
2108
|
|
|
2109
|
+
def test_repairing_a_link_never_touches_the_same_href_quoted_in_prose():
|
|
2110
|
+
# A brief warning about a mistake quotes the wrong path in backticks AND may hold
|
|
2111
|
+
# the real broken link further down. `str.replace` over the raw text rewrites the
|
|
2112
|
+
# quotation too, so the document that documents the error silently stops showing
|
|
2113
|
+
# it — the read path stripped code spans and the write path did not. Reproduced
|
|
2114
|
+
# before it was fixed.
|
|
2115
|
+
from harness import links
|
|
2116
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2117
|
+
base = Path(tmp) / "work"
|
|
2118
|
+
(base / "product").mkdir(parents=True)
|
|
2119
|
+
(base / "product" / "board.md").write_text("# board\n")
|
|
2120
|
+
doc = base / "notes" / "deep" / "doc.md"
|
|
2121
|
+
doc.parent.mkdir(parents=True)
|
|
2122
|
+
doc.write_text(
|
|
2123
|
+
"Getting the depth wrong looks like `[board](../product/board.md)`.\n\n"
|
|
2124
|
+
"A real one: [board](../product/board.md).\n")
|
|
2125
|
+
|
|
2126
|
+
fixed, stuck = links.repair_dangling(base, apply=True)
|
|
2127
|
+
body = doc.read_text()
|
|
2128
|
+
assert (fixed, stuck) == (1, [])
|
|
2129
|
+
assert "`[board](../product/board.md)`" in body, "the quotation was rewritten"
|
|
2130
|
+
assert "A real one: [board](../../product/board.md)." in body, body
|
|
2131
|
+
|
|
2132
|
+
|
|
2109
2133
|
def test_a_broken_link_two_resolvers_disagree_about_is_left_for_a_person():
|
|
2110
2134
|
# A link repaired to the wrong real file is worse than one that is visibly
|
|
2111
2135
|
# broken: nothing afterwards can tell it was ever wrong.
|
|
@@ -4984,6 +5008,19 @@ def test_a_write_aimed_at_another_branch_is_refused():
|
|
|
4984
5008
|
raise AssertionError("a write with --branch must be refused")
|
|
4985
5009
|
|
|
4986
5010
|
|
|
5011
|
+
def test_links_is_refused_a_branch_because_fix_writes_this_one():
|
|
5012
|
+
# It reports far more often than it fixes, which is what made listing it as a read
|
|
5013
|
+
# look right. `--fix` writes, and it writes to the tree that is CHECKED OUT — so
|
|
5014
|
+
# being listed would have let `links --fix --branch other` past this gate and
|
|
5015
|
+
# edited the wrong branch while the caller believed otherwise.
|
|
5016
|
+
try:
|
|
5017
|
+
entry._reads_only("links", {"branch": "theirs"})
|
|
5018
|
+
except SystemExit as e:
|
|
5019
|
+
assert e.code != 0
|
|
5020
|
+
else:
|
|
5021
|
+
raise AssertionError("`links` must not accept --branch: --fix writes")
|
|
5022
|
+
|
|
5023
|
+
|
|
4987
5024
|
def test_a_read_may_name_a_branch():
|
|
4988
5025
|
# The same gate must not refuse the commands the flag exists for.
|
|
4989
5026
|
entry._reads_only("list", {"branch": "theirs"})
|
package/harness/work.py
CHANGED
|
@@ -351,9 +351,15 @@ def _say(note: str) -> None:
|
|
|
351
351
|
#: Commands that only LOOK. `--branch` is meaningful on these and refused on every
|
|
352
352
|
#: other, which is the safe default: a command added later is covered without anyone
|
|
353
353
|
#: remembering to come back here.
|
|
354
|
+
#:
|
|
355
|
+
#: `links` is deliberately NOT here even though it usually only reports. `--fix`
|
|
356
|
+
#: writes, and it writes to the tree that is checked out — so listing it would let
|
|
357
|
+
#: `links --fix --branch other` past the refusal and edit this branch while the caller
|
|
358
|
+
#: believed they were touching another. It reads the working tree in both modes, so
|
|
359
|
+
#: `--branch` could never have meant anything for it.
|
|
354
360
|
_READS = {"list", "find", "path", "code", "where", "rules", "status", "digest",
|
|
355
361
|
"log", "needs", "align", "coverage", "readme", "config", "context",
|
|
356
|
-
"doctor", "kickoff", "remind", "applies", "wrap"
|
|
362
|
+
"doctor", "kickoff", "remind", "applies", "wrap"}
|
|
357
363
|
|
|
358
364
|
|
|
359
365
|
def _reads_only(cmd: str, flags: dict) -> None:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.117",
|
|
4
4
|
"description": "Jarvis — local AI coding assistant CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"tsup": "^8.5.1",
|
|
57
57
|
"typescript": "^5.7.0",
|
|
58
58
|
"vitest": "^2.1.0",
|
|
59
|
-
"@jarvis/agents": "1.0.0",
|
|
60
|
-
"@jarvis/anthropic": "1.0.0",
|
|
61
59
|
"@jarvis/board": "0.1.0",
|
|
62
60
|
"@jarvis/data": "0.1.0",
|
|
61
|
+
"@jarvis/errors": "1.0.0",
|
|
63
62
|
"@jarvis/logger": "1.0.0",
|
|
64
63
|
"@jarvis/rpc": "1.0.0",
|
|
65
|
-
"@jarvis/errors": "1.0.0",
|
|
66
64
|
"@jarvis/types": "1.0.0",
|
|
65
|
+
"@jarvis/agents": "1.0.0",
|
|
66
|
+
"@jarvis/anthropic": "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"
|