@holmes-lab/holmes-kit 0.23.3 → 0.24.1
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/CHANGELOG.md +61 -0
- package/README.md +45 -42
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/index.js +41 -1
- package/dist/holmes/cli/open-url.d.ts +30 -0
- package/dist/holmes/cli/open-url.js +38 -0
- package/dist/holmes/cli/report.d.ts +20 -0
- package/dist/holmes/cli/report.js +115 -0
- package/dist/holmes/cli/reported-log.d.ts +23 -0
- package/dist/holmes/cli/reported-log.js +31 -0
- package/dist/holmes/hooks/stop.d.ts +7 -0
- package/dist/holmes/hooks/stop.js +35 -1
- package/dist/holmes/project/analysis-currency.d.ts +22 -0
- package/dist/holmes/project/analysis-currency.js +96 -0
- package/dist/holmes/project/field-report-url.d.ts +47 -0
- package/dist/holmes/project/field-report-url.js +63 -0
- package/dist/holmes/project/field-report.d.ts +54 -0
- package/dist/holmes/project/field-report.js +107 -0
- package/dist/holmes/update/workspaces.d.ts +53 -2
- package/dist/holmes/update/workspaces.js +77 -4
- package/package.json +1 -1
|
@@ -34,12 +34,33 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.EMPTY_REGISTRY = void 0;
|
|
37
|
+
exports.holmesHome = holmesHome;
|
|
37
38
|
exports.readRegistry = readRegistry;
|
|
38
39
|
exports.mergeWorkspaceEntry = mergeWorkspaceEntry;
|
|
40
|
+
exports.isTempTarget = isTempTarget;
|
|
41
|
+
exports.pruneRegistry = pruneRegistry;
|
|
39
42
|
exports.recordWorkspace = recordWorkspace;
|
|
40
43
|
// @implements A-SPEC-543.1
|
|
41
44
|
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const os = __importStar(require("node:os"));
|
|
42
46
|
const path = __importStar(require("node:path"));
|
|
47
|
+
/**
|
|
48
|
+
* @implements A-SPEC-678
|
|
49
|
+
* Where the registry lives — with one seam a test can reach.
|
|
50
|
+
*
|
|
51
|
+
* Measured 2026-09-18: inside jest, `process.env` is a plain object copy in the sandbox, so
|
|
52
|
+
* assigning `HOME` never reaches `setenv` and `os.homedir()` keeps answering the developer's real
|
|
53
|
+
* home. Plain node follows `$HOME`; jest does not. An in-process test therefore cannot isolate a
|
|
54
|
+
* module that calls `os.homedir()` directly — which is how 599 of this machine's 603 registry
|
|
55
|
+
* entries came to be test temp directories.
|
|
56
|
+
*
|
|
57
|
+
* jest DOES control `process.env`, so one lookup there is the smallest seam that works. Consumers
|
|
58
|
+
* set nothing and get `os.homedir()`; an empty value is not a setting.
|
|
59
|
+
*/
|
|
60
|
+
function holmesHome(env, fallback) {
|
|
61
|
+
const override = env.HOLMES_HOME;
|
|
62
|
+
return typeof override === 'string' && override.trim() !== '' ? override : fallback;
|
|
63
|
+
}
|
|
43
64
|
exports.EMPTY_REGISTRY = { version: 1, workspaces: [] };
|
|
44
65
|
/**
|
|
45
66
|
* @implements A-SPEC-543.1
|
|
@@ -79,16 +100,59 @@ function mergeWorkspaceEntry(reg, entry) {
|
|
|
79
100
|
};
|
|
80
101
|
}
|
|
81
102
|
/**
|
|
82
|
-
* @implements A-SPEC-
|
|
83
|
-
*
|
|
84
|
-
*
|
|
103
|
+
* @implements A-SPEC-677
|
|
104
|
+
* Whether a target lives under the OS temp root — and therefore is ephemeral by construction.
|
|
105
|
+
*
|
|
106
|
+
* macOS spells that root two ways: `os.tmpdir()` answers `/var/folders/...` while a realpath gives
|
|
107
|
+
* `/private/var/folders/...`. Both appear in this machine's registry, so both are judged. A `/tmp`
|
|
108
|
+
* literal would have missed all 599 of them.
|
|
109
|
+
*
|
|
110
|
+
* An empty root filters NOTHING. When the judgement cannot be made, keeping an entry costs a line
|
|
111
|
+
* and dropping one the user wanted costs them something they cannot get back.
|
|
85
112
|
*/
|
|
113
|
+
function isTempTarget(target, tmpdir) {
|
|
114
|
+
if (typeof target !== 'string' || typeof tmpdir !== 'string' || tmpdir.trim() === '')
|
|
115
|
+
return false;
|
|
116
|
+
const roots = new Set([tmpdir, tmpdir.startsWith('/private') ? tmpdir.slice('/private'.length) : `/private${tmpdir}`]);
|
|
117
|
+
return [...roots].some((r) => r !== '' && (target === r || target.startsWith(r.endsWith('/') ? r : `${r}/`)));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* @implements A-SPEC-677
|
|
121
|
+
* Drop what has certainly gone; COUNT what merely cannot be seen.
|
|
122
|
+
*
|
|
123
|
+
* The asymmetry is the whole judgement. A vanished temp path is gone by construction — nothing
|
|
124
|
+
* re-creates a `mkdtemp` directory. A vanished normal path may be an unmounted volume, an external
|
|
125
|
+
* disk or a network share, and `upgrade` already skips it harmlessly with one line. Deleting it is
|
|
126
|
+
* the one outcome the user cannot undo, so absence outside the temp root is reported, never acted on.
|
|
127
|
+
*/
|
|
128
|
+
function pruneRegistry(reg, p) {
|
|
129
|
+
let dropped = 0, absent = 0;
|
|
130
|
+
const workspaces = reg.workspaces.filter((w) => {
|
|
131
|
+
if (p.exists(w.target))
|
|
132
|
+
return true;
|
|
133
|
+
if (p.isTemp(w.target)) {
|
|
134
|
+
dropped += 1;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
absent += 1;
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
return { registry: { version: 1, workspaces }, dropped, absent };
|
|
141
|
+
}
|
|
86
142
|
function recordWorkspace(home, entry, io = {
|
|
87
143
|
read: (p) => fs.readFileSync(p, 'utf8'),
|
|
88
144
|
write: (p, c) => fs.writeFileSync(p, c),
|
|
89
145
|
mkdir: (p) => { fs.mkdirSync(p, { recursive: true }); },
|
|
146
|
+
exists: (p) => fs.existsSync(p),
|
|
147
|
+
tmpdir: () => os.tmpdir(),
|
|
90
148
|
}) {
|
|
91
149
|
try {
|
|
150
|
+
// @implements A-SPEC-677 — an ephemeral target is not recorded at all. What never enters cannot
|
|
151
|
+
// pile up, and 599 of this machine's 603 entries entered exactly this way. Returning true is
|
|
152
|
+
// honest: nothing failed, there was simply nothing worth remembering.
|
|
153
|
+
const tmpdir = io.tmpdir?.() ?? '';
|
|
154
|
+
if (isTempTarget(entry.target, tmpdir))
|
|
155
|
+
return true;
|
|
92
156
|
const dir = path.join(home, '.holmes');
|
|
93
157
|
const file = path.join(dir, 'workspaces.json');
|
|
94
158
|
let raw = null;
|
|
@@ -98,9 +162,18 @@ function recordWorkspace(home, entry, io = {
|
|
|
98
162
|
catch {
|
|
99
163
|
raw = null;
|
|
100
164
|
}
|
|
101
|
-
const
|
|
165
|
+
const exists = io.exists;
|
|
166
|
+
// @implements A-SPEC-677 — prune while we are already here. Without an `exists` probe there is
|
|
167
|
+
// no way to tell gone from present, so the registry is merged unpruned rather than guessed at.
|
|
168
|
+
const base = exists
|
|
169
|
+
? pruneRegistry(readRegistry(raw), { exists, isTemp: (t) => isTempTarget(t, tmpdir) })
|
|
170
|
+
: { registry: readRegistry(raw), dropped: 0, absent: 0 };
|
|
171
|
+
const merged = mergeWorkspaceEntry(base.registry, entry);
|
|
102
172
|
io.mkdir(dir);
|
|
103
173
|
io.write(file, `${JSON.stringify(merged, null, 2)}\n`);
|
|
174
|
+
// Never a silent deletion: whoever asked can learn how many entries went.
|
|
175
|
+
if (base.dropped > 0)
|
|
176
|
+
io.onPrune?.(base.dropped);
|
|
104
177
|
return true;
|
|
105
178
|
}
|
|
106
179
|
catch {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.24.1",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|