heyspex 0.2.4 → 0.2.6
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/.claude-plugin/marketplace.json +1 -1
- package/bin/hs.mjs +281 -30
- package/dev-factory/lib/dispatch-guard.mjs +75 -0
- package/dev-factory/lib/runner-path.mjs +51 -0
- package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
- package/dist/cli/hs-connect.mjs +20 -1
- package/dist/cli/hs-defer.mjs +3802 -3744
- package/dist/cli/hs-events.mjs +1296 -0
- package/dist/cli/hs-export-events.mjs +1252 -0
- package/dist/cli/hs-handoff.mjs +2576 -0
- package/dist/cli/hs-index-rebuild.mjs +209 -36
- package/dist/cli/hs-index-reconcile.mjs +212 -39
- package/dist/cli/hs-init.mjs +20 -1
- package/dist/cli/hs-install.mjs +20 -1
- package/dist/cli/hs-join.mjs +20 -1
- package/dist/cli/hs-next-launch.mjs +2698 -0
- package/dist/cli/hs-pin.mjs +1336 -0
- package/dist/cli/hs-prereq.mjs +1380 -0
- package/dist/cli/hs-revalidate.mjs +3257 -0
- package/dist/cli/hs-spawn.mjs +12377 -0
- package/dist/cli/hs-test.mjs +1046 -0
- package/dist/cli/hs-trash.mjs +362 -0
- package/dist/cli/hs-trust.mjs +197 -0
- package/dist/cli/hs-vault-pull.mjs +214 -41
- package/dist/cli/hs-vault-push.mjs +212 -39
- package/dist/cli-templates/harness-commands/claude/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/claude/hs-res.md +71 -63
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +66 -58
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +18 -18
- package/docs/copilot-plugin-install.md +2 -2
- package/package.json +9 -5
- package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
- package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
- package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
- package/scripts/claude-statusline/statusline-command.mjs +287 -27
- package/scripts/hs-companion.mjs +8 -0
- package/scripts/install-claude-code-hooks.mjs +104 -3
- package/scripts/install-copilot-hooks.mjs +21 -1
- package/scripts/install-cursor-hooks.mjs +4 -3
- package/scripts/lib/capture-execution-guard.mjs +221 -0
- package/scripts/lib/customer-kit.mjs +48 -0
- package/scripts/lib/hs-companion-settings.mjs +25 -0
- package/scripts/record-grounding.mjs +38 -0
- package/vendor/heyspex-mcp/http-transport.mjs +1 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
- package/bin/postinstall.mjs +0 -88
|
@@ -0,0 +1,3257 @@
|
|
|
1
|
+
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
|
+
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
11
|
+
}) : x)(function(x) {
|
|
12
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
15
|
+
var __esm = (fn, res) => function __init() {
|
|
16
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
17
|
+
};
|
|
18
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
19
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
// scripts/lib/heyspex-home.mjs
|
|
39
|
+
import { homedir } from "node:os";
|
|
40
|
+
function heyspexHome(env = process.env) {
|
|
41
|
+
const override = env?.HEYSPEX_HOME;
|
|
42
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
43
|
+
return override.trim();
|
|
44
|
+
}
|
|
45
|
+
return homedir();
|
|
46
|
+
}
|
|
47
|
+
var init_heyspex_home = __esm({
|
|
48
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// vendor/heyspex-mcp/credentials.mjs
|
|
54
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
55
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
56
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
57
|
+
function credentialsFilePath(homeDir = heyspexHome()) {
|
|
58
|
+
return join2(homeDir, ".heyspex", "credentials.yaml");
|
|
59
|
+
}
|
|
60
|
+
function parseMcpToken(raw) {
|
|
61
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
62
|
+
const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
|
|
63
|
+
return token ? token : null;
|
|
64
|
+
}
|
|
65
|
+
function isValidAccountId(id) {
|
|
66
|
+
return typeof id === "string" && ACCOUNT_ID_RE.test(id);
|
|
67
|
+
}
|
|
68
|
+
function topLevelScalar(raw, key) {
|
|
69
|
+
const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
|
|
70
|
+
const value = raw.match(re)?.[1]?.trim();
|
|
71
|
+
return value ? value : null;
|
|
72
|
+
}
|
|
73
|
+
function decodeScalar(text) {
|
|
74
|
+
const t = text.trim();
|
|
75
|
+
if (t === "" || t === "null" || t === "~") return null;
|
|
76
|
+
if (t.startsWith('"')) {
|
|
77
|
+
try {
|
|
78
|
+
const v = JSON.parse(t);
|
|
79
|
+
return typeof v === "string" && v ? v : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return t.replace(/^"|"$/g, "") || null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
|
|
85
|
+
const bare = t.replace(/\s+#.*$/, "").trim();
|
|
86
|
+
return bare || null;
|
|
87
|
+
}
|
|
88
|
+
function emptyAccount() {
|
|
89
|
+
return { mcp_token: null, console_url: null, email: null, paired_at: null };
|
|
90
|
+
}
|
|
91
|
+
function findAccountsBlock(lines) {
|
|
92
|
+
const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
|
|
93
|
+
if (start < 0) return null;
|
|
94
|
+
let end = start + 1;
|
|
95
|
+
while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
|
|
96
|
+
while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
|
|
97
|
+
return { start, end };
|
|
98
|
+
}
|
|
99
|
+
function parseAccountsBlock(lines) {
|
|
100
|
+
const accounts = {};
|
|
101
|
+
const block = findAccountsBlock(lines);
|
|
102
|
+
if (!block) return accounts;
|
|
103
|
+
let current = null;
|
|
104
|
+
for (const line of lines.slice(block.start + 1, block.end)) {
|
|
105
|
+
const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
|
|
106
|
+
if (header) {
|
|
107
|
+
const id = decodeScalar(header[1]);
|
|
108
|
+
current = isValidAccountId(id) ? id : null;
|
|
109
|
+
if (current) accounts[current] = emptyAccount();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const field = line.match(/^ {4}([a-z_]+):(.*)$/);
|
|
113
|
+
if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
|
|
114
|
+
accounts[current][field[1]] = decodeScalar(field[2]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return accounts;
|
|
118
|
+
}
|
|
119
|
+
function parseCredentialsStore(raw) {
|
|
120
|
+
const text = typeof raw === "string" ? raw : "";
|
|
121
|
+
if (topLevelScalar(text, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
|
|
122
|
+
const token = parseMcpToken(text);
|
|
123
|
+
if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
|
|
124
|
+
return {
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
currentAccountId: LEGACY_ACCOUNT_ID,
|
|
127
|
+
accounts: {
|
|
128
|
+
[LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text, "console_url") }
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const accounts = parseAccountsBlock(text.split(/\r?\n/));
|
|
133
|
+
const current = topLevelScalar(text, "current_account_id");
|
|
134
|
+
return {
|
|
135
|
+
schemaVersion: 2,
|
|
136
|
+
currentAccountId: current && accounts[current] ? current : null,
|
|
137
|
+
accounts
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function resolveBearer({ envToken, credentialsText, accountId, legacyAccountId } = {}) {
|
|
141
|
+
const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
|
|
142
|
+
if (fromEnv) return { token: fromEnv, source: "env" };
|
|
143
|
+
const store = parseCredentialsStore(credentialsText);
|
|
144
|
+
let entry;
|
|
145
|
+
if (typeof accountId === "string" && accountId.trim()) {
|
|
146
|
+
const wanted = accountId.trim();
|
|
147
|
+
entry = store.accounts[wanted];
|
|
148
|
+
if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
|
|
149
|
+
entry = store.accounts[LEGACY_ACCOUNT_ID];
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
|
|
153
|
+
}
|
|
154
|
+
if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
|
|
155
|
+
return { token: null, source: null };
|
|
156
|
+
}
|
|
157
|
+
function readTextOrNull(read, path4) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = read(path4);
|
|
160
|
+
return typeof raw === "string" ? raw : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function readLegacyConfigAccountId(read, homeDir) {
|
|
166
|
+
const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
|
|
167
|
+
const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
|
|
168
|
+
return value || null;
|
|
169
|
+
}
|
|
170
|
+
function readBearer({ env = process.env, homeDir = heyspexHome(), readFile, accountId } = {}) {
|
|
171
|
+
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
172
|
+
const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
|
|
173
|
+
const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
|
|
174
|
+
const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
|
|
175
|
+
return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
|
|
176
|
+
}
|
|
177
|
+
function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
|
|
178
|
+
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
179
|
+
const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
|
|
180
|
+
if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
|
|
181
|
+
return store.currentAccountId;
|
|
182
|
+
}
|
|
183
|
+
if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
|
|
184
|
+
return readLegacyConfigAccountId(read, homeDir);
|
|
185
|
+
}
|
|
186
|
+
var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
|
|
187
|
+
var init_credentials = __esm({
|
|
188
|
+
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
189
|
+
"use strict";
|
|
190
|
+
init_heyspex_home();
|
|
191
|
+
MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
192
|
+
MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
193
|
+
CREDENTIALS_SCHEMA_VERSION = 2;
|
|
194
|
+
LEGACY_ACCOUNT_ID = "legacy";
|
|
195
|
+
ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
|
|
196
|
+
ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
|
|
197
|
+
CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// scripts/lib/dev-mode.mjs
|
|
202
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
203
|
+
import { homedir as homedir8 } from "node:os";
|
|
204
|
+
import { join as join8 } from "node:path";
|
|
205
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
206
|
+
return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
207
|
+
}
|
|
208
|
+
function normalizeMode(raw) {
|
|
209
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
210
|
+
if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
|
|
211
|
+
if (v === "dev") return { mode: "dev", reliance: null };
|
|
212
|
+
if (v === "local") return { mode: "dev", reliance: "local" };
|
|
213
|
+
if (v === "cloud") return { mode: "dev", reliance: "cloud" };
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function normalizeReliance(raw) {
|
|
217
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
218
|
+
return v === "local" || v === "cloud" ? v : null;
|
|
219
|
+
}
|
|
220
|
+
function deriveSemantics(mode, reliance) {
|
|
221
|
+
const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
|
|
222
|
+
const local = mode === "dev" && effectiveReliance === "local";
|
|
223
|
+
return {
|
|
224
|
+
reliance: effectiveReliance,
|
|
225
|
+
consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
|
|
226
|
+
readTier: local ? "disk" : "neon",
|
|
227
|
+
vaultWriteMode: local ? "local" : "cloud"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
231
|
+
let raw;
|
|
232
|
+
try {
|
|
233
|
+
raw = readFile(devModeFilePath(homeDir));
|
|
234
|
+
} catch {
|
|
235
|
+
return { record: null, parseError: null };
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(raw);
|
|
239
|
+
const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
|
|
240
|
+
const reliance = normalizeReliance(parsed?.reliance);
|
|
241
|
+
if (!mode || mode === "dev" && !reliance) {
|
|
242
|
+
return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
record: {
|
|
246
|
+
mode,
|
|
247
|
+
reliance: mode === "prod" ? "cloud" : reliance,
|
|
248
|
+
console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
|
|
249
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
|
|
250
|
+
source: typeof parsed.source === "string" ? parsed.source : null
|
|
251
|
+
},
|
|
252
|
+
parseError: null
|
|
253
|
+
};
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function overrideCloudCheck(source, cloudKnown, staleAgainstCloud) {
|
|
259
|
+
if (source !== "local-override") return "not-applicable";
|
|
260
|
+
if (!cloudKnown) return "unverified";
|
|
261
|
+
return staleAgainstCloud ? "stale" : "in-sync";
|
|
262
|
+
}
|
|
263
|
+
function resolveDevMode({
|
|
264
|
+
env = process.env,
|
|
265
|
+
homeDir = heyspexHome(),
|
|
266
|
+
readFile = defaultReadFile,
|
|
267
|
+
cloudSetting = void 0
|
|
268
|
+
} = {}) {
|
|
269
|
+
const local = readLocalDevModeRecord({ homeDir, readFile });
|
|
270
|
+
const cloudKnown = cloudSetting !== void 0;
|
|
271
|
+
const cloudValue = cloudKnown && cloudSetting ? {
|
|
272
|
+
mode: cloudSetting.mode === "dev" ? "dev" : "prod",
|
|
273
|
+
reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
|
|
274
|
+
} : null;
|
|
275
|
+
let mode;
|
|
276
|
+
let reliance;
|
|
277
|
+
let source;
|
|
278
|
+
let relianceSource;
|
|
279
|
+
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
280
|
+
if (envParsed) {
|
|
281
|
+
mode = envParsed.mode;
|
|
282
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
283
|
+
reliance = envReliance ?? "local";
|
|
284
|
+
source = "env";
|
|
285
|
+
relianceSource = envReliance ? "env" : "assumed-local";
|
|
286
|
+
} else if (local.record) {
|
|
287
|
+
mode = local.record.mode;
|
|
288
|
+
reliance = local.record.reliance;
|
|
289
|
+
source = "local-override";
|
|
290
|
+
} else if (cloudValue) {
|
|
291
|
+
mode = cloudValue.mode;
|
|
292
|
+
reliance = cloudValue.reliance;
|
|
293
|
+
source = "cloud-setting";
|
|
294
|
+
} else {
|
|
295
|
+
mode = "prod";
|
|
296
|
+
reliance = "cloud";
|
|
297
|
+
source = "default";
|
|
298
|
+
}
|
|
299
|
+
const semantics = deriveSemantics(mode, reliance);
|
|
300
|
+
const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
|
|
301
|
+
const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
|
|
302
|
+
const cloudCheck = overrideCloudCheck(source, cloudKnown, staleAgainstCloud);
|
|
303
|
+
return {
|
|
304
|
+
mode,
|
|
305
|
+
reliance: semantics.reliance,
|
|
306
|
+
consoleUrl,
|
|
307
|
+
readTier: semantics.readTier,
|
|
308
|
+
vaultWriteMode: semantics.vaultWriteMode,
|
|
309
|
+
sources: {
|
|
310
|
+
mode: source,
|
|
311
|
+
reliance: relianceSource ?? source,
|
|
312
|
+
consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
|
|
313
|
+
readTier: "derived",
|
|
314
|
+
vaultWriteMode: "derived"
|
|
315
|
+
},
|
|
316
|
+
localOverride: {
|
|
317
|
+
present: local.record !== null,
|
|
318
|
+
record: local.record,
|
|
319
|
+
parseError: local.parseError,
|
|
320
|
+
staleAgainstCloud,
|
|
321
|
+
cloudCheck
|
|
322
|
+
},
|
|
323
|
+
cloudSetting: { known: cloudKnown, value: cloudValue }
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
327
|
+
var init_dev_mode = __esm({
|
|
328
|
+
"scripts/lib/dev-mode.mjs"() {
|
|
329
|
+
"use strict";
|
|
330
|
+
init_heyspex_home();
|
|
331
|
+
init_credentials();
|
|
332
|
+
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
333
|
+
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
334
|
+
DEV_MODE_FILENAME = "dev-mode.json";
|
|
335
|
+
defaultReadFile = (p) => readFileSync8(p, "utf8");
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// node_modules/kind-of/index.js
|
|
340
|
+
var require_kind_of = __commonJS({
|
|
341
|
+
"node_modules/kind-of/index.js"(exports2, module2) {
|
|
342
|
+
"use strict";
|
|
343
|
+
var toString = Object.prototype.toString;
|
|
344
|
+
module2.exports = function kindOf(val) {
|
|
345
|
+
if (val === void 0) return "undefined";
|
|
346
|
+
if (val === null) return "null";
|
|
347
|
+
var type = typeof val;
|
|
348
|
+
if (type === "boolean") return "boolean";
|
|
349
|
+
if (type === "string") return "string";
|
|
350
|
+
if (type === "number") return "number";
|
|
351
|
+
if (type === "symbol") return "symbol";
|
|
352
|
+
if (type === "function") {
|
|
353
|
+
return isGeneratorFn(val) ? "generatorfunction" : "function";
|
|
354
|
+
}
|
|
355
|
+
if (isArray(val)) return "array";
|
|
356
|
+
if (isBuffer(val)) return "buffer";
|
|
357
|
+
if (isArguments(val)) return "arguments";
|
|
358
|
+
if (isDate(val)) return "date";
|
|
359
|
+
if (isError(val)) return "error";
|
|
360
|
+
if (isRegexp(val)) return "regexp";
|
|
361
|
+
switch (ctorName(val)) {
|
|
362
|
+
case "Symbol":
|
|
363
|
+
return "symbol";
|
|
364
|
+
case "Promise":
|
|
365
|
+
return "promise";
|
|
366
|
+
// Set, Map, WeakSet, WeakMap
|
|
367
|
+
case "WeakMap":
|
|
368
|
+
return "weakmap";
|
|
369
|
+
case "WeakSet":
|
|
370
|
+
return "weakset";
|
|
371
|
+
case "Map":
|
|
372
|
+
return "map";
|
|
373
|
+
case "Set":
|
|
374
|
+
return "set";
|
|
375
|
+
// 8-bit typed arrays
|
|
376
|
+
case "Int8Array":
|
|
377
|
+
return "int8array";
|
|
378
|
+
case "Uint8Array":
|
|
379
|
+
return "uint8array";
|
|
380
|
+
case "Uint8ClampedArray":
|
|
381
|
+
return "uint8clampedarray";
|
|
382
|
+
// 16-bit typed arrays
|
|
383
|
+
case "Int16Array":
|
|
384
|
+
return "int16array";
|
|
385
|
+
case "Uint16Array":
|
|
386
|
+
return "uint16array";
|
|
387
|
+
// 32-bit typed arrays
|
|
388
|
+
case "Int32Array":
|
|
389
|
+
return "int32array";
|
|
390
|
+
case "Uint32Array":
|
|
391
|
+
return "uint32array";
|
|
392
|
+
case "Float32Array":
|
|
393
|
+
return "float32array";
|
|
394
|
+
case "Float64Array":
|
|
395
|
+
return "float64array";
|
|
396
|
+
}
|
|
397
|
+
if (isGeneratorObj(val)) {
|
|
398
|
+
return "generator";
|
|
399
|
+
}
|
|
400
|
+
type = toString.call(val);
|
|
401
|
+
switch (type) {
|
|
402
|
+
case "[object Object]":
|
|
403
|
+
return "object";
|
|
404
|
+
// iterators
|
|
405
|
+
case "[object Map Iterator]":
|
|
406
|
+
return "mapiterator";
|
|
407
|
+
case "[object Set Iterator]":
|
|
408
|
+
return "setiterator";
|
|
409
|
+
case "[object String Iterator]":
|
|
410
|
+
return "stringiterator";
|
|
411
|
+
case "[object Array Iterator]":
|
|
412
|
+
return "arrayiterator";
|
|
413
|
+
}
|
|
414
|
+
return type.slice(8, -1).toLowerCase().replace(/\s/g, "");
|
|
415
|
+
};
|
|
416
|
+
function ctorName(val) {
|
|
417
|
+
return typeof val.constructor === "function" ? val.constructor.name : null;
|
|
418
|
+
}
|
|
419
|
+
function isArray(val) {
|
|
420
|
+
if (Array.isArray) return Array.isArray(val);
|
|
421
|
+
return val instanceof Array;
|
|
422
|
+
}
|
|
423
|
+
function isError(val) {
|
|
424
|
+
return val instanceof Error || typeof val.message === "string" && val.constructor && typeof val.constructor.stackTraceLimit === "number";
|
|
425
|
+
}
|
|
426
|
+
function isDate(val) {
|
|
427
|
+
if (val instanceof Date) return true;
|
|
428
|
+
return typeof val.toDateString === "function" && typeof val.getDate === "function" && typeof val.setDate === "function";
|
|
429
|
+
}
|
|
430
|
+
function isRegexp(val) {
|
|
431
|
+
if (val instanceof RegExp) return true;
|
|
432
|
+
return typeof val.flags === "string" && typeof val.ignoreCase === "boolean" && typeof val.multiline === "boolean" && typeof val.global === "boolean";
|
|
433
|
+
}
|
|
434
|
+
function isGeneratorFn(name, val) {
|
|
435
|
+
return ctorName(name) === "GeneratorFunction";
|
|
436
|
+
}
|
|
437
|
+
function isGeneratorObj(val) {
|
|
438
|
+
return typeof val.throw === "function" && typeof val.return === "function" && typeof val.next === "function";
|
|
439
|
+
}
|
|
440
|
+
function isArguments(val) {
|
|
441
|
+
try {
|
|
442
|
+
if (typeof val.length === "number" && typeof val.callee === "function") {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (err.message.indexOf("callee") !== -1) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
function isBuffer(val) {
|
|
453
|
+
if (val.constructor && typeof val.constructor.isBuffer === "function") {
|
|
454
|
+
return val.constructor.isBuffer(val);
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// node_modules/is-extendable/index.js
|
|
462
|
+
var require_is_extendable = __commonJS({
|
|
463
|
+
"node_modules/is-extendable/index.js"(exports2, module2) {
|
|
464
|
+
"use strict";
|
|
465
|
+
module2.exports = function isExtendable(val) {
|
|
466
|
+
return typeof val !== "undefined" && val !== null && (typeof val === "object" || typeof val === "function");
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// node_modules/extend-shallow/index.js
|
|
472
|
+
var require_extend_shallow = __commonJS({
|
|
473
|
+
"node_modules/extend-shallow/index.js"(exports2, module2) {
|
|
474
|
+
"use strict";
|
|
475
|
+
var isObject = require_is_extendable();
|
|
476
|
+
module2.exports = function extend(o) {
|
|
477
|
+
if (!isObject(o)) {
|
|
478
|
+
o = {};
|
|
479
|
+
}
|
|
480
|
+
var len = arguments.length;
|
|
481
|
+
for (var i = 1; i < len; i++) {
|
|
482
|
+
var obj = arguments[i];
|
|
483
|
+
if (isObject(obj)) {
|
|
484
|
+
assign(o, obj);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return o;
|
|
488
|
+
};
|
|
489
|
+
function assign(a, b) {
|
|
490
|
+
for (var key in b) {
|
|
491
|
+
if (hasOwn(b, key)) {
|
|
492
|
+
a[key] = b[key];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function hasOwn(obj, key) {
|
|
497
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// node_modules/section-matter/index.js
|
|
503
|
+
var require_section_matter = __commonJS({
|
|
504
|
+
"node_modules/section-matter/index.js"(exports2, module2) {
|
|
505
|
+
"use strict";
|
|
506
|
+
var typeOf = require_kind_of();
|
|
507
|
+
var extend = require_extend_shallow();
|
|
508
|
+
module2.exports = function(input, options2) {
|
|
509
|
+
if (typeof options2 === "function") {
|
|
510
|
+
options2 = { parse: options2 };
|
|
511
|
+
}
|
|
512
|
+
var file = toObject(input);
|
|
513
|
+
var defaults = { section_delimiter: "---", parse: identity };
|
|
514
|
+
var opts = extend({}, defaults, options2);
|
|
515
|
+
var delim = opts.section_delimiter;
|
|
516
|
+
var lines = file.content.split(/\r?\n/);
|
|
517
|
+
var sections = null;
|
|
518
|
+
var section = createSection();
|
|
519
|
+
var content = [];
|
|
520
|
+
var stack = [];
|
|
521
|
+
function initSections(val) {
|
|
522
|
+
file.content = val;
|
|
523
|
+
sections = [];
|
|
524
|
+
content = [];
|
|
525
|
+
}
|
|
526
|
+
function closeSection(val) {
|
|
527
|
+
if (stack.length) {
|
|
528
|
+
section.key = getKey(stack[0], delim);
|
|
529
|
+
section.content = val;
|
|
530
|
+
opts.parse(section, sections);
|
|
531
|
+
sections.push(section);
|
|
532
|
+
section = createSection();
|
|
533
|
+
content = [];
|
|
534
|
+
stack = [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
for (var i = 0; i < lines.length; i++) {
|
|
538
|
+
var line = lines[i];
|
|
539
|
+
var len = stack.length;
|
|
540
|
+
var ln = line.trim();
|
|
541
|
+
if (isDelimiter(ln, delim)) {
|
|
542
|
+
if (ln.length === 3 && i !== 0) {
|
|
543
|
+
if (len === 0 || len === 2) {
|
|
544
|
+
content.push(line);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
stack.push(ln);
|
|
548
|
+
section.data = content.join("\n");
|
|
549
|
+
content = [];
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (sections === null) {
|
|
553
|
+
initSections(content.join("\n"));
|
|
554
|
+
}
|
|
555
|
+
if (len === 2) {
|
|
556
|
+
closeSection(content.join("\n"));
|
|
557
|
+
}
|
|
558
|
+
stack.push(ln);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
content.push(line);
|
|
562
|
+
}
|
|
563
|
+
if (sections === null) {
|
|
564
|
+
initSections(content.join("\n"));
|
|
565
|
+
} else {
|
|
566
|
+
closeSection(content.join("\n"));
|
|
567
|
+
}
|
|
568
|
+
file.sections = sections;
|
|
569
|
+
return file;
|
|
570
|
+
};
|
|
571
|
+
function isDelimiter(line, delim) {
|
|
572
|
+
if (line.slice(0, delim.length) !== delim) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
if (line.charAt(delim.length + 1) === delim.slice(-1)) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
function toObject(input) {
|
|
581
|
+
if (typeOf(input) !== "object") {
|
|
582
|
+
input = { content: input };
|
|
583
|
+
}
|
|
584
|
+
if (typeof input.content !== "string" && !isBuffer(input.content)) {
|
|
585
|
+
throw new TypeError("expected a buffer or string");
|
|
586
|
+
}
|
|
587
|
+
input.content = input.content.toString();
|
|
588
|
+
input.sections = [];
|
|
589
|
+
return input;
|
|
590
|
+
}
|
|
591
|
+
function getKey(val, delim) {
|
|
592
|
+
return val ? val.slice(delim.length).trim() : "";
|
|
593
|
+
}
|
|
594
|
+
function createSection() {
|
|
595
|
+
return { key: "", data: "", content: "" };
|
|
596
|
+
}
|
|
597
|
+
function identity(val) {
|
|
598
|
+
return val;
|
|
599
|
+
}
|
|
600
|
+
function isBuffer(val) {
|
|
601
|
+
if (val && val.constructor && typeof val.constructor.isBuffer === "function") {
|
|
602
|
+
return val.constructor.isBuffer(val);
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// node_modules/gray-matter/lib/engines.js
|
|
610
|
+
var require_engines = __commonJS({
|
|
611
|
+
"node_modules/gray-matter/lib/engines.js"(exports, module) {
|
|
612
|
+
"use strict";
|
|
613
|
+
var yaml = __require("js-yaml");
|
|
614
|
+
var engines = exports = module.exports;
|
|
615
|
+
engines.yaml = {
|
|
616
|
+
parse: yaml.safeLoad.bind(yaml),
|
|
617
|
+
stringify: yaml.safeDump.bind(yaml)
|
|
618
|
+
};
|
|
619
|
+
engines.json = {
|
|
620
|
+
parse: JSON.parse.bind(JSON),
|
|
621
|
+
stringify: function(obj, options2) {
|
|
622
|
+
const opts = Object.assign({ replacer: null, space: 2 }, options2);
|
|
623
|
+
return JSON.stringify(obj, opts.replacer, opts.space);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
engines.javascript = {
|
|
627
|
+
parse: function parse(str, options, wrap) {
|
|
628
|
+
try {
|
|
629
|
+
if (wrap !== false) {
|
|
630
|
+
str = "(function() {\nreturn " + str.trim() + ";\n}());";
|
|
631
|
+
}
|
|
632
|
+
return eval(str) || {};
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (wrap !== false && /(unexpected|identifier)/i.test(err.message)) {
|
|
635
|
+
return parse(str, options, false);
|
|
636
|
+
}
|
|
637
|
+
throw new SyntaxError(err);
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
stringify: function() {
|
|
641
|
+
throw new Error("stringifying JavaScript is not supported");
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// node_modules/strip-bom-string/index.js
|
|
648
|
+
var require_strip_bom_string = __commonJS({
|
|
649
|
+
"node_modules/strip-bom-string/index.js"(exports2, module2) {
|
|
650
|
+
"use strict";
|
|
651
|
+
module2.exports = function(str2) {
|
|
652
|
+
if (typeof str2 === "string" && str2.charAt(0) === "\uFEFF") {
|
|
653
|
+
return str2.slice(1);
|
|
654
|
+
}
|
|
655
|
+
return str2;
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// node_modules/gray-matter/lib/utils.js
|
|
661
|
+
var require_utils = __commonJS({
|
|
662
|
+
"node_modules/gray-matter/lib/utils.js"(exports2) {
|
|
663
|
+
"use strict";
|
|
664
|
+
var stripBom = require_strip_bom_string();
|
|
665
|
+
var typeOf = require_kind_of();
|
|
666
|
+
exports2.define = function(obj, key, val) {
|
|
667
|
+
Reflect.defineProperty(obj, key, {
|
|
668
|
+
enumerable: false,
|
|
669
|
+
configurable: true,
|
|
670
|
+
writable: true,
|
|
671
|
+
value: val
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
exports2.isBuffer = function(val) {
|
|
675
|
+
return typeOf(val) === "buffer";
|
|
676
|
+
};
|
|
677
|
+
exports2.isObject = function(val) {
|
|
678
|
+
return typeOf(val) === "object";
|
|
679
|
+
};
|
|
680
|
+
exports2.toBuffer = function(input) {
|
|
681
|
+
return typeof input === "string" ? Buffer.from(input) : input;
|
|
682
|
+
};
|
|
683
|
+
exports2.toString = function(input) {
|
|
684
|
+
if (exports2.isBuffer(input)) return stripBom(String(input));
|
|
685
|
+
if (typeof input !== "string") {
|
|
686
|
+
throw new TypeError("expected input to be a string or buffer");
|
|
687
|
+
}
|
|
688
|
+
return stripBom(input);
|
|
689
|
+
};
|
|
690
|
+
exports2.arrayify = function(val) {
|
|
691
|
+
return val ? Array.isArray(val) ? val : [val] : [];
|
|
692
|
+
};
|
|
693
|
+
exports2.startsWith = function(str2, substr, len) {
|
|
694
|
+
if (typeof len !== "number") len = substr.length;
|
|
695
|
+
return str2.slice(0, len) === substr;
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// node_modules/gray-matter/lib/defaults.js
|
|
701
|
+
var require_defaults = __commonJS({
|
|
702
|
+
"node_modules/gray-matter/lib/defaults.js"(exports2, module2) {
|
|
703
|
+
"use strict";
|
|
704
|
+
var engines2 = require_engines();
|
|
705
|
+
var utils = require_utils();
|
|
706
|
+
module2.exports = function(options2) {
|
|
707
|
+
const opts = Object.assign({}, options2);
|
|
708
|
+
opts.delimiters = utils.arrayify(opts.delims || opts.delimiters || "---");
|
|
709
|
+
if (opts.delimiters.length === 1) {
|
|
710
|
+
opts.delimiters.push(opts.delimiters[0]);
|
|
711
|
+
}
|
|
712
|
+
opts.language = (opts.language || opts.lang || "yaml").toLowerCase();
|
|
713
|
+
opts.engines = Object.assign({}, engines2, opts.parsers, opts.engines);
|
|
714
|
+
return opts;
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// node_modules/gray-matter/lib/engine.js
|
|
720
|
+
var require_engine = __commonJS({
|
|
721
|
+
"node_modules/gray-matter/lib/engine.js"(exports2, module2) {
|
|
722
|
+
"use strict";
|
|
723
|
+
module2.exports = function(name, options2) {
|
|
724
|
+
let engine = options2.engines[name] || options2.engines[aliase(name)];
|
|
725
|
+
if (typeof engine === "undefined") {
|
|
726
|
+
throw new Error('gray-matter engine "' + name + '" is not registered');
|
|
727
|
+
}
|
|
728
|
+
if (typeof engine === "function") {
|
|
729
|
+
engine = { parse: engine };
|
|
730
|
+
}
|
|
731
|
+
return engine;
|
|
732
|
+
};
|
|
733
|
+
function aliase(name) {
|
|
734
|
+
switch (name.toLowerCase()) {
|
|
735
|
+
case "js":
|
|
736
|
+
case "javascript":
|
|
737
|
+
return "javascript";
|
|
738
|
+
case "coffee":
|
|
739
|
+
case "coffeescript":
|
|
740
|
+
case "cson":
|
|
741
|
+
return "coffee";
|
|
742
|
+
case "yaml":
|
|
743
|
+
case "yml":
|
|
744
|
+
return "yaml";
|
|
745
|
+
default: {
|
|
746
|
+
return name;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// node_modules/gray-matter/lib/stringify.js
|
|
754
|
+
var require_stringify = __commonJS({
|
|
755
|
+
"node_modules/gray-matter/lib/stringify.js"(exports2, module2) {
|
|
756
|
+
"use strict";
|
|
757
|
+
var typeOf = require_kind_of();
|
|
758
|
+
var getEngine = require_engine();
|
|
759
|
+
var defaults = require_defaults();
|
|
760
|
+
module2.exports = function(file, data, options2) {
|
|
761
|
+
if (data == null && options2 == null) {
|
|
762
|
+
switch (typeOf(file)) {
|
|
763
|
+
case "object":
|
|
764
|
+
data = file.data;
|
|
765
|
+
options2 = {};
|
|
766
|
+
break;
|
|
767
|
+
case "string":
|
|
768
|
+
return file;
|
|
769
|
+
default: {
|
|
770
|
+
throw new TypeError("expected file to be a string or object");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const str2 = file.content;
|
|
775
|
+
const opts = defaults(options2);
|
|
776
|
+
if (data == null) {
|
|
777
|
+
if (!opts.data) return file;
|
|
778
|
+
data = opts.data;
|
|
779
|
+
}
|
|
780
|
+
const language = file.language || opts.language;
|
|
781
|
+
const engine = getEngine(language, opts);
|
|
782
|
+
if (typeof engine.stringify !== "function") {
|
|
783
|
+
throw new TypeError('expected "' + language + '.stringify" to be a function');
|
|
784
|
+
}
|
|
785
|
+
data = Object.assign({}, file.data, data);
|
|
786
|
+
const open = opts.delimiters[0];
|
|
787
|
+
const close = opts.delimiters[1];
|
|
788
|
+
const matter3 = engine.stringify(data, options2).trim();
|
|
789
|
+
let buf = "";
|
|
790
|
+
if (matter3 !== "{}") {
|
|
791
|
+
buf = newline(open) + newline(matter3) + newline(close);
|
|
792
|
+
}
|
|
793
|
+
if (typeof file.excerpt === "string" && file.excerpt !== "") {
|
|
794
|
+
if (str2.indexOf(file.excerpt.trim()) === -1) {
|
|
795
|
+
buf += newline(file.excerpt) + newline(close);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return buf + newline(str2);
|
|
799
|
+
};
|
|
800
|
+
function newline(str2) {
|
|
801
|
+
return str2.slice(-1) !== "\n" ? str2 + "\n" : str2;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// node_modules/gray-matter/lib/excerpt.js
|
|
807
|
+
var require_excerpt = __commonJS({
|
|
808
|
+
"node_modules/gray-matter/lib/excerpt.js"(exports2, module2) {
|
|
809
|
+
"use strict";
|
|
810
|
+
var defaults = require_defaults();
|
|
811
|
+
module2.exports = function(file, options2) {
|
|
812
|
+
const opts = defaults(options2);
|
|
813
|
+
if (file.data == null) {
|
|
814
|
+
file.data = {};
|
|
815
|
+
}
|
|
816
|
+
if (typeof opts.excerpt === "function") {
|
|
817
|
+
return opts.excerpt(file, opts);
|
|
818
|
+
}
|
|
819
|
+
const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
|
|
820
|
+
if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
|
|
821
|
+
return file;
|
|
822
|
+
}
|
|
823
|
+
const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
|
|
824
|
+
const idx = file.content.indexOf(delimiter2);
|
|
825
|
+
if (idx !== -1) {
|
|
826
|
+
file.excerpt = file.content.slice(0, idx);
|
|
827
|
+
}
|
|
828
|
+
return file;
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// node_modules/gray-matter/lib/to-file.js
|
|
834
|
+
var require_to_file = __commonJS({
|
|
835
|
+
"node_modules/gray-matter/lib/to-file.js"(exports2, module2) {
|
|
836
|
+
"use strict";
|
|
837
|
+
var typeOf = require_kind_of();
|
|
838
|
+
var stringify = require_stringify();
|
|
839
|
+
var utils = require_utils();
|
|
840
|
+
module2.exports = function(file) {
|
|
841
|
+
if (typeOf(file) !== "object") {
|
|
842
|
+
file = { content: file };
|
|
843
|
+
}
|
|
844
|
+
if (typeOf(file.data) !== "object") {
|
|
845
|
+
file.data = {};
|
|
846
|
+
}
|
|
847
|
+
if (file.contents && file.content == null) {
|
|
848
|
+
file.content = file.contents;
|
|
849
|
+
}
|
|
850
|
+
utils.define(file, "orig", utils.toBuffer(file.content));
|
|
851
|
+
utils.define(file, "language", file.language || "");
|
|
852
|
+
utils.define(file, "matter", file.matter || "");
|
|
853
|
+
utils.define(file, "stringify", function(data, options2) {
|
|
854
|
+
if (options2 && options2.language) {
|
|
855
|
+
file.language = options2.language;
|
|
856
|
+
}
|
|
857
|
+
return stringify(file, data, options2);
|
|
858
|
+
});
|
|
859
|
+
file.content = utils.toString(file.content);
|
|
860
|
+
file.isEmpty = false;
|
|
861
|
+
file.excerpt = "";
|
|
862
|
+
return file;
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// node_modules/gray-matter/lib/parse.js
|
|
868
|
+
var require_parse = __commonJS({
|
|
869
|
+
"node_modules/gray-matter/lib/parse.js"(exports2, module2) {
|
|
870
|
+
"use strict";
|
|
871
|
+
var getEngine = require_engine();
|
|
872
|
+
var defaults = require_defaults();
|
|
873
|
+
module2.exports = function(language, str2, options2) {
|
|
874
|
+
const opts = defaults(options2);
|
|
875
|
+
const engine = getEngine(language, opts);
|
|
876
|
+
if (typeof engine.parse !== "function") {
|
|
877
|
+
throw new TypeError('expected "' + language + '.parse" to be a function');
|
|
878
|
+
}
|
|
879
|
+
return engine.parse(str2, opts);
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// node_modules/gray-matter/index.js
|
|
885
|
+
var require_gray_matter = __commonJS({
|
|
886
|
+
"node_modules/gray-matter/index.js"(exports2, module2) {
|
|
887
|
+
"use strict";
|
|
888
|
+
var fs = __require("fs");
|
|
889
|
+
var sections = require_section_matter();
|
|
890
|
+
var defaults = require_defaults();
|
|
891
|
+
var stringify = require_stringify();
|
|
892
|
+
var excerpt = require_excerpt();
|
|
893
|
+
var engines2 = require_engines();
|
|
894
|
+
var toFile = require_to_file();
|
|
895
|
+
var parse2 = require_parse();
|
|
896
|
+
var utils = require_utils();
|
|
897
|
+
function matter3(input, options2) {
|
|
898
|
+
if (input === "") {
|
|
899
|
+
return { data: {}, content: input, excerpt: "", orig: input };
|
|
900
|
+
}
|
|
901
|
+
let file = toFile(input);
|
|
902
|
+
const cached2 = matter3.cache[file.content];
|
|
903
|
+
if (!options2) {
|
|
904
|
+
if (cached2) {
|
|
905
|
+
file = Object.assign({}, cached2);
|
|
906
|
+
file.orig = cached2.orig;
|
|
907
|
+
return file;
|
|
908
|
+
}
|
|
909
|
+
matter3.cache[file.content] = file;
|
|
910
|
+
}
|
|
911
|
+
return parseMatter(file, options2);
|
|
912
|
+
}
|
|
913
|
+
function parseMatter(file, options2) {
|
|
914
|
+
const opts = defaults(options2);
|
|
915
|
+
const open = opts.delimiters[0];
|
|
916
|
+
const close = "\n" + opts.delimiters[1];
|
|
917
|
+
let str2 = file.content;
|
|
918
|
+
if (opts.language) {
|
|
919
|
+
file.language = opts.language;
|
|
920
|
+
}
|
|
921
|
+
const openLen = open.length;
|
|
922
|
+
if (!utils.startsWith(str2, open, openLen)) {
|
|
923
|
+
excerpt(file, opts);
|
|
924
|
+
return file;
|
|
925
|
+
}
|
|
926
|
+
if (str2.charAt(openLen) === open.slice(-1)) {
|
|
927
|
+
return file;
|
|
928
|
+
}
|
|
929
|
+
str2 = str2.slice(openLen);
|
|
930
|
+
const len = str2.length;
|
|
931
|
+
const language = matter3.language(str2, opts);
|
|
932
|
+
if (language.name) {
|
|
933
|
+
file.language = language.name;
|
|
934
|
+
str2 = str2.slice(language.raw.length);
|
|
935
|
+
}
|
|
936
|
+
let closeIndex = str2.indexOf(close);
|
|
937
|
+
if (closeIndex === -1) {
|
|
938
|
+
closeIndex = len;
|
|
939
|
+
}
|
|
940
|
+
file.matter = str2.slice(0, closeIndex);
|
|
941
|
+
const block = file.matter.replace(/^\s*#[^\n]+/gm, "").trim();
|
|
942
|
+
if (block === "") {
|
|
943
|
+
file.isEmpty = true;
|
|
944
|
+
file.empty = file.content;
|
|
945
|
+
file.data = {};
|
|
946
|
+
} else {
|
|
947
|
+
file.data = parse2(file.language, file.matter, opts);
|
|
948
|
+
}
|
|
949
|
+
if (closeIndex === len) {
|
|
950
|
+
file.content = "";
|
|
951
|
+
} else {
|
|
952
|
+
file.content = str2.slice(closeIndex + close.length);
|
|
953
|
+
if (file.content[0] === "\r") {
|
|
954
|
+
file.content = file.content.slice(1);
|
|
955
|
+
}
|
|
956
|
+
if (file.content[0] === "\n") {
|
|
957
|
+
file.content = file.content.slice(1);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
excerpt(file, opts);
|
|
961
|
+
if (opts.sections === true || typeof opts.section === "function") {
|
|
962
|
+
sections(file, opts.section);
|
|
963
|
+
}
|
|
964
|
+
return file;
|
|
965
|
+
}
|
|
966
|
+
matter3.engines = engines2;
|
|
967
|
+
matter3.stringify = function(file, data, options2) {
|
|
968
|
+
if (typeof file === "string") file = matter3(file, options2);
|
|
969
|
+
return stringify(file, data, options2);
|
|
970
|
+
};
|
|
971
|
+
matter3.read = function(filepath, options2) {
|
|
972
|
+
const str2 = fs.readFileSync(filepath, "utf8");
|
|
973
|
+
const file = matter3(str2, options2);
|
|
974
|
+
file.path = filepath;
|
|
975
|
+
return file;
|
|
976
|
+
};
|
|
977
|
+
matter3.test = function(str2, options2) {
|
|
978
|
+
return utils.startsWith(str2, defaults(options2).delimiters[0]);
|
|
979
|
+
};
|
|
980
|
+
matter3.language = function(str2, options2) {
|
|
981
|
+
const opts = defaults(options2);
|
|
982
|
+
const open = opts.delimiters[0];
|
|
983
|
+
if (matter3.test(str2)) {
|
|
984
|
+
str2 = str2.slice(open.length);
|
|
985
|
+
}
|
|
986
|
+
const language = str2.slice(0, str2.search(/\r?\n/));
|
|
987
|
+
return {
|
|
988
|
+
raw: language,
|
|
989
|
+
name: language ? language.trim() : ""
|
|
990
|
+
};
|
|
991
|
+
};
|
|
992
|
+
matter3.cache = {};
|
|
993
|
+
matter3.clearCache = function() {
|
|
994
|
+
matter3.cache = {};
|
|
995
|
+
};
|
|
996
|
+
module2.exports = matter3;
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// spex-console/bin/hs-revalidate.ts
|
|
1001
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
1002
|
+
import { createHash } from "node:crypto";
|
|
1003
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1004
|
+
import path3 from "node:path";
|
|
1005
|
+
import process3 from "node:process";
|
|
1006
|
+
|
|
1007
|
+
// scripts/lib/shared-path-helpers.mjs
|
|
1008
|
+
import { realpathSync } from "node:fs";
|
|
1009
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
1010
|
+
import { fileURLToPath } from "node:url";
|
|
1011
|
+
function resolveCodeRoot({
|
|
1012
|
+
explicit,
|
|
1013
|
+
packageRoot,
|
|
1014
|
+
vaultRoot,
|
|
1015
|
+
env = process.env
|
|
1016
|
+
} = {}) {
|
|
1017
|
+
const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
|
|
1018
|
+
if (fromExplicit) return fromExplicit;
|
|
1019
|
+
const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
|
|
1020
|
+
if (fromEnv) return fromEnv;
|
|
1021
|
+
const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
|
|
1022
|
+
if (fromPackage) return fromPackage;
|
|
1023
|
+
const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
|
|
1024
|
+
return fromVault;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// spex-console/src/lib/cli/checks-manifest.ts
|
|
1028
|
+
var KIND_ALIASES = {
|
|
1029
|
+
tests: "test",
|
|
1030
|
+
unit: "test",
|
|
1031
|
+
grep: "command",
|
|
1032
|
+
diff: "command",
|
|
1033
|
+
live: "command"
|
|
1034
|
+
};
|
|
1035
|
+
function joinUnderRoot(root, rel) {
|
|
1036
|
+
if (rel === "" || rel === ".") return root;
|
|
1037
|
+
if (/^(?:[a-zA-Z]:[\\/]|[\\/])/.test(rel)) return rel;
|
|
1038
|
+
const sep2 = root.includes("\\") && !root.includes("/") ? "\\" : "/";
|
|
1039
|
+
return `${root.replace(/[\\/]+$/, "")}${sep2}${rel.replace(/^[\\/]+/, "")}`;
|
|
1040
|
+
}
|
|
1041
|
+
function resolveCheckCwds(check, roots) {
|
|
1042
|
+
if (typeof check.cwd === "string" && check.cwd.trim() !== "") {
|
|
1043
|
+
const raw = check.cwd.trim();
|
|
1044
|
+
const vaultMatch = /^vault:(.*)$/.exec(raw);
|
|
1045
|
+
if (vaultMatch) {
|
|
1046
|
+
return [joinUnderRoot(roots.vaultRoot ?? roots.repoRoot, vaultMatch[1].trim())];
|
|
1047
|
+
}
|
|
1048
|
+
return [joinUnderRoot(roots.repoRoot, raw)];
|
|
1049
|
+
}
|
|
1050
|
+
return roots.repoRoot === roots.consoleRoot ? [roots.repoRoot] : [roots.repoRoot, roots.consoleRoot];
|
|
1051
|
+
}
|
|
1052
|
+
var VALID_METHODS = /* @__PURE__ */ new Set([
|
|
1053
|
+
"machine",
|
|
1054
|
+
"property",
|
|
1055
|
+
"human"
|
|
1056
|
+
]);
|
|
1057
|
+
var VALID_KINDS = /* @__PURE__ */ new Set([
|
|
1058
|
+
"test",
|
|
1059
|
+
"lint",
|
|
1060
|
+
"types",
|
|
1061
|
+
"command",
|
|
1062
|
+
"guard"
|
|
1063
|
+
]);
|
|
1064
|
+
var VALID_EXPECTS = /* @__PURE__ */ new Set([
|
|
1065
|
+
"exit-0",
|
|
1066
|
+
"no-net-new",
|
|
1067
|
+
"empty-output"
|
|
1068
|
+
]);
|
|
1069
|
+
function parseChecksManifest(text) {
|
|
1070
|
+
let raw;
|
|
1071
|
+
try {
|
|
1072
|
+
raw = JSON.parse(text);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
return {
|
|
1075
|
+
error: `manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1079
|
+
return { error: "manifest must be an object" };
|
|
1080
|
+
}
|
|
1081
|
+
const rec = raw;
|
|
1082
|
+
if (typeof rec.spec !== "string" || rec.spec.trim() === "") {
|
|
1083
|
+
return { error: 'manifest is missing a non-empty "spec"' };
|
|
1084
|
+
}
|
|
1085
|
+
if (!Array.isArray(rec.criteria)) {
|
|
1086
|
+
return { error: 'manifest must have a "criteria" array' };
|
|
1087
|
+
}
|
|
1088
|
+
const criteria = [];
|
|
1089
|
+
for (let i = 0; i < rec.criteria.length; i++) {
|
|
1090
|
+
const c = rec.criteria[i];
|
|
1091
|
+
if (typeof c !== "object" || c === null) {
|
|
1092
|
+
return { error: `criteria[${i}] must be an object` };
|
|
1093
|
+
}
|
|
1094
|
+
const cr = c;
|
|
1095
|
+
const ac = typeof cr.ac === "number" ? cr.ac : i + 1;
|
|
1096
|
+
const ctext = typeof cr.text === "string" ? cr.text : "";
|
|
1097
|
+
const method = cr.method;
|
|
1098
|
+
if (typeof method !== "string" || !VALID_METHODS.has(method)) {
|
|
1099
|
+
return { error: `criteria[${i}].method must be one of machine|property|human` };
|
|
1100
|
+
}
|
|
1101
|
+
const rawChecks = cr.checks ?? [];
|
|
1102
|
+
if (!Array.isArray(rawChecks)) {
|
|
1103
|
+
return { error: `criteria[${i}].checks must be an array` };
|
|
1104
|
+
}
|
|
1105
|
+
if (method === "human" && rawChecks.length > 0) {
|
|
1106
|
+
return {
|
|
1107
|
+
error: `criteria[${i}] is method:human but has checks \u2014 judgment-only criteria carry none`
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
if (method !== "human" && rawChecks.length === 0) {
|
|
1111
|
+
return { error: `criteria[${i}] is method:${method} but has no checks to run` };
|
|
1112
|
+
}
|
|
1113
|
+
const checks = [];
|
|
1114
|
+
for (let j = 0; j < rawChecks.length; j++) {
|
|
1115
|
+
const t = rawChecks[j];
|
|
1116
|
+
if (typeof t !== "object" || t === null) {
|
|
1117
|
+
return { error: `criteria[${i}].checks[${j}] must be an object` };
|
|
1118
|
+
}
|
|
1119
|
+
const tr = t;
|
|
1120
|
+
if (typeof tr.kind !== "string") {
|
|
1121
|
+
return {
|
|
1122
|
+
error: `criteria[${i}].checks[${j}].kind must be one of test|lint|types|command|guard`
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const kind = KIND_ALIASES[tr.kind] ?? tr.kind;
|
|
1126
|
+
if (!VALID_KINDS.has(kind)) {
|
|
1127
|
+
return {
|
|
1128
|
+
error: `criteria[${i}].checks[${j}].kind must be one of test|lint|types|command|guard`
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof tr.run !== "string" || tr.run.trim() === "") {
|
|
1132
|
+
return { error: `criteria[${i}].checks[${j}] is missing a non-empty "run"` };
|
|
1133
|
+
}
|
|
1134
|
+
const expectRaw = tr.expect ?? "exit-0";
|
|
1135
|
+
if (typeof expectRaw !== "string" || !VALID_EXPECTS.has(expectRaw)) {
|
|
1136
|
+
return {
|
|
1137
|
+
error: `criteria[${i}].checks[${j}].expect must be one of exit-0|no-net-new|empty-output`
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const baseline = typeof tr.baseline === "number" && Number.isFinite(tr.baseline) ? tr.baseline : 0;
|
|
1141
|
+
const cwd = typeof tr.cwd === "string" && tr.cwd.trim() !== "" ? tr.cwd.trim() : void 0;
|
|
1142
|
+
checks.push({
|
|
1143
|
+
kind,
|
|
1144
|
+
run: tr.run,
|
|
1145
|
+
expect: expectRaw,
|
|
1146
|
+
baseline,
|
|
1147
|
+
...cwd ? { cwd } : {}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
criteria.push({ ac, text: ctext, method, checks });
|
|
1151
|
+
}
|
|
1152
|
+
const frozenAt = rec.frozen_at ?? rec.frozenAt;
|
|
1153
|
+
const authoredBy = rec.authored_by ?? rec.authoredBy;
|
|
1154
|
+
return {
|
|
1155
|
+
manifest: {
|
|
1156
|
+
spec: rec.spec,
|
|
1157
|
+
frozenAt: typeof frozenAt === "string" && frozenAt.trim() !== "" ? frozenAt : null,
|
|
1158
|
+
authoredBy: typeof authoredBy === "string" && authoredBy.trim() !== "" ? authoredBy : null,
|
|
1159
|
+
criteria
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
var ZERO_MATCH_RE = /Passed:\s*0\b|No test is available|No test files found|No test matches the given testcase filter|^\s*(?:ℹ|#)\s*(?:tests|pass)\s+0\s*$/i;
|
|
1164
|
+
var NODE_TEST_FILE_PASS_RE = /^\s*(?:✔|ok\s+\d+\s+-)\s+\S*\.(?:test|spec)\.[cm]?[jt]sx?\s*(?:\(|$)/;
|
|
1165
|
+
var NODE_TEST_FILTER_RE = /--test-(?:name|skip)-pattern/;
|
|
1166
|
+
function detectZeroMatch(rawOutput, run) {
|
|
1167
|
+
if (!rawOutput) return null;
|
|
1168
|
+
const filtered = run !== void 0 && NODE_TEST_FILTER_RE.test(run);
|
|
1169
|
+
const line = rawOutput.split(/\r?\n/).find((l) => ZERO_MATCH_RE.test(l) || filtered && NODE_TEST_FILE_PASS_RE.test(l));
|
|
1170
|
+
return line ? line.trim() : null;
|
|
1171
|
+
}
|
|
1172
|
+
function evaluateCheck(check, outcome) {
|
|
1173
|
+
if (detectZeroMatch(outcome.rawOutput, check.run) !== null) return false;
|
|
1174
|
+
if (check.expect === "no-net-new") {
|
|
1175
|
+
return (outcome.errorCount ?? 0) <= check.baseline;
|
|
1176
|
+
}
|
|
1177
|
+
if (check.expect === "empty-output") {
|
|
1178
|
+
return outcome.exitCode === 0 && (outcome.stdout ?? "\0").trim() === "";
|
|
1179
|
+
}
|
|
1180
|
+
return outcome.exitCode === 0;
|
|
1181
|
+
}
|
|
1182
|
+
async function runChecksManifest(manifest, deps) {
|
|
1183
|
+
const criteria = [];
|
|
1184
|
+
let durationMs = 0;
|
|
1185
|
+
for (const entry of manifest.criteria) {
|
|
1186
|
+
if (entry.method === "human") {
|
|
1187
|
+
criteria.push({
|
|
1188
|
+
ac: entry.ac,
|
|
1189
|
+
text: entry.text,
|
|
1190
|
+
method: entry.method,
|
|
1191
|
+
verdict: "judgment_only",
|
|
1192
|
+
checks: []
|
|
1193
|
+
});
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const checkResults = [];
|
|
1197
|
+
for (const check of entry.checks) {
|
|
1198
|
+
let outcome;
|
|
1199
|
+
try {
|
|
1200
|
+
outcome = await deps.runCheck(check);
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
outcome = {
|
|
1203
|
+
exitCode: -1,
|
|
1204
|
+
durationMs: 0,
|
|
1205
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
const passed = evaluateCheck(check, outcome);
|
|
1209
|
+
durationMs += outcome.durationMs;
|
|
1210
|
+
checkResults.push({
|
|
1211
|
+
check,
|
|
1212
|
+
passed,
|
|
1213
|
+
durationMs: outcome.durationMs,
|
|
1214
|
+
message: passed ? void 0 : outcome.message ?? failureMessage(check, outcome)
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
const verdict = checkResults.every((r) => r.passed) ? "pass" : "fail";
|
|
1218
|
+
criteria.push({
|
|
1219
|
+
ac: entry.ac,
|
|
1220
|
+
text: entry.text,
|
|
1221
|
+
method: entry.method,
|
|
1222
|
+
verdict,
|
|
1223
|
+
checks: checkResults
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
const passedCount = criteria.filter((c) => c.verdict === "pass").length;
|
|
1227
|
+
const failedCount = criteria.filter((c) => c.verdict === "fail").length;
|
|
1228
|
+
const judgmentOnlyCount = criteria.filter((c) => c.verdict === "judgment_only").length;
|
|
1229
|
+
return {
|
|
1230
|
+
spec: manifest.spec,
|
|
1231
|
+
frozenAt: manifest.frozenAt,
|
|
1232
|
+
authoredBy: manifest.authoredBy,
|
|
1233
|
+
criteria,
|
|
1234
|
+
passed: failedCount === 0,
|
|
1235
|
+
total: criteria.length,
|
|
1236
|
+
passedCount,
|
|
1237
|
+
failedCount,
|
|
1238
|
+
judgmentOnlyCount,
|
|
1239
|
+
durationMs
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function failureMessage(check, outcome) {
|
|
1243
|
+
const zeroMatch = detectZeroMatch(outcome.rawOutput);
|
|
1244
|
+
if (zeroMatch !== null) {
|
|
1245
|
+
return `zero-match: the check's filter matched nothing to run \u2014 "${zeroMatch}"`;
|
|
1246
|
+
}
|
|
1247
|
+
if (check.expect === "no-net-new") {
|
|
1248
|
+
return `${outcome.errorCount ?? "?"} errors exceeds baseline ${check.baseline}`;
|
|
1249
|
+
}
|
|
1250
|
+
return `command exited ${outcome.exitCode}`;
|
|
1251
|
+
}
|
|
1252
|
+
var HELP = [
|
|
1253
|
+
"Usage:",
|
|
1254
|
+
" hs check run <spec-id|manifest.json> [--json]",
|
|
1255
|
+
"",
|
|
1256
|
+
"Verify a spec's acceptance criteria against its frozen checks manifest, and",
|
|
1257
|
+
"report pass/fail PER CRITERION against the real working tree (HS-EPIC-DCEP).",
|
|
1258
|
+
"Machine criteria run their checks; method:human criteria are reported",
|
|
1259
|
+
"judgment-only and handed to the council \u2014 never silently auto-passed.",
|
|
1260
|
+
"",
|
|
1261
|
+
"Manifest shape (specs/checks/<spec-id>.checks.json):",
|
|
1262
|
+
' { "spec": "[[HS-EPIC-XYZ]]", "frozen_at": "...", "authored_by": "...",',
|
|
1263
|
+
' "criteria": [ { "ac": 1, "text": "...", "method": "machine",',
|
|
1264
|
+
' "checks": [ { "kind": "types", "run": "npx tsc --noEmit",',
|
|
1265
|
+
' "expect": "no-net-new", "baseline": 60 } ] } ] }',
|
|
1266
|
+
"",
|
|
1267
|
+
"Arguments:",
|
|
1268
|
+
" <spec-id|manifest.json> A manifest path, or a spec id resolved to",
|
|
1269
|
+
" specs/checks/<id>.checks.json under the vault root.",
|
|
1270
|
+
"",
|
|
1271
|
+
"Flags:",
|
|
1272
|
+
" --manifest <p> Manifest path (alias for the positional argument).",
|
|
1273
|
+
" --report PATCH the verdict to the spec's active claim (the PCFL gate).",
|
|
1274
|
+
" Requires a spec-id target (identifies the claim). The verify",
|
|
1275
|
+
" exit code is unchanged; reporting is a side effect.",
|
|
1276
|
+
" --unattended Declare that no human is present at this flip (pipeline",
|
|
1277
|
+
" caller). Rides --report: the server's no-manifest",
|
|
1278
|
+
" self-report fallback then fails closed instead of shipping.",
|
|
1279
|
+
" --json Emit a machine-readable JSON result (the gate's input shape).",
|
|
1280
|
+
" --help, -h Show this help.",
|
|
1281
|
+
"",
|
|
1282
|
+
"Exit codes:",
|
|
1283
|
+
" 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-manifest \xB7 12 runner-missing \xB7 20 checks-failed"
|
|
1284
|
+
].join("\n");
|
|
1285
|
+
|
|
1286
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
1287
|
+
import path2 from "node:path";
|
|
1288
|
+
|
|
1289
|
+
// scripts/lib/hs-command.mjs
|
|
1290
|
+
import { existsSync } from "node:fs";
|
|
1291
|
+
import { delimiter, dirname, join } from "node:path";
|
|
1292
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1293
|
+
var HS_BIN_NAME = "hs";
|
|
1294
|
+
var HS_DISPATCHER_PATH = (() => {
|
|
1295
|
+
try {
|
|
1296
|
+
const here = dirname(fileURLToPath2(import.meta.url));
|
|
1297
|
+
return join(dirname(dirname(here)), "bin", "hs.mjs");
|
|
1298
|
+
} catch {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
})();
|
|
1302
|
+
function shellQuote(p) {
|
|
1303
|
+
return /[\\\s]/.test(p) ? `"${p}"` : p;
|
|
1304
|
+
}
|
|
1305
|
+
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
1306
|
+
function candidateNames(name, env) {
|
|
1307
|
+
if (process.platform !== "win32") return [name];
|
|
1308
|
+
const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1309
|
+
const extensions = [.../* @__PURE__ */ new Set([...pathext, ".ps1"])];
|
|
1310
|
+
return [name, ...extensions.map((ext) => name + ext)];
|
|
1311
|
+
}
|
|
1312
|
+
function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
|
|
1313
|
+
try {
|
|
1314
|
+
const raw = env.PATH ?? env.Path ?? env.path ?? "";
|
|
1315
|
+
for (const dir of String(raw).split(delimiter)) {
|
|
1316
|
+
const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
|
|
1317
|
+
if (!trimmed) continue;
|
|
1318
|
+
for (const candidate of candidateNames(name, env)) {
|
|
1319
|
+
const full = join(trimmed, candidate);
|
|
1320
|
+
try {
|
|
1321
|
+
if (exists(full)) return full;
|
|
1322
|
+
} catch {
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
var cached = null;
|
|
1331
|
+
var HS_ON_PATH_OVERRIDE_ENV = "HEYSPEX_HS_ON_PATH";
|
|
1332
|
+
function onPathOverride(env) {
|
|
1333
|
+
const raw = String(env?.[HS_ON_PATH_OVERRIDE_ENV] ?? "");
|
|
1334
|
+
if (raw === "1") return true;
|
|
1335
|
+
if (raw === "0") return false;
|
|
1336
|
+
return void 0;
|
|
1337
|
+
}
|
|
1338
|
+
function describeHsCommand({ env, exists, useCache = true } = {}) {
|
|
1339
|
+
const injected = env !== void 0 || exists !== void 0;
|
|
1340
|
+
const effectiveEnv = env ?? process.env;
|
|
1341
|
+
const override = onPathOverride(effectiveEnv);
|
|
1342
|
+
if (override !== void 0) {
|
|
1343
|
+
return {
|
|
1344
|
+
onPath: override,
|
|
1345
|
+
prefix: override ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
|
|
1346
|
+
resolvedPath: null,
|
|
1347
|
+
dispatcherPath: HS_DISPATCHER_PATH
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
if (useCache && !injected && cached) return cached;
|
|
1351
|
+
const resolvedPath = findOnPath(HS_BIN_NAME, {
|
|
1352
|
+
env: env ?? process.env,
|
|
1353
|
+
exists: exists ?? existsSync
|
|
1354
|
+
});
|
|
1355
|
+
const answer = {
|
|
1356
|
+
onPath: resolvedPath !== null,
|
|
1357
|
+
prefix: resolvedPath !== null ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
|
|
1358
|
+
resolvedPath,
|
|
1359
|
+
dispatcherPath: HS_DISPATCHER_PATH
|
|
1360
|
+
};
|
|
1361
|
+
if (useCache && !injected) cached = answer;
|
|
1362
|
+
return answer;
|
|
1363
|
+
}
|
|
1364
|
+
function hsCommand(args = "", options2 = {}) {
|
|
1365
|
+
let prefix = HS_DISPATCHER_PREFIX;
|
|
1366
|
+
try {
|
|
1367
|
+
prefix = describeHsCommand(options2).prefix;
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
const tail = String(args ?? "").trim();
|
|
1371
|
+
return tail ? `${prefix} ${tail}` : prefix;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1375
|
+
init_heyspex_home();
|
|
1376
|
+
init_credentials();
|
|
1377
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1378
|
+
import { homedir as homedir6 } from "node:os";
|
|
1379
|
+
import { join as join6 } from "node:path";
|
|
1380
|
+
|
|
1381
|
+
// vendor/heyspex-mcp/session-owner.mjs
|
|
1382
|
+
init_heyspex_home();
|
|
1383
|
+
init_credentials();
|
|
1384
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1385
|
+
import { homedir as homedir3 } from "node:os";
|
|
1386
|
+
import { join as join3 } from "node:path";
|
|
1387
|
+
function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
|
|
1388
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
|
|
1389
|
+
if (fromEnv) return fromEnv;
|
|
1390
|
+
return readCurrentAccountId({ homeDir, readFile });
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
1394
|
+
var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
1395
|
+
"* /api/mcp/session",
|
|
1396
|
+
"* /api/mcp/pairing-codes",
|
|
1397
|
+
"POST /api/vaults",
|
|
1398
|
+
"GET /api/vaults",
|
|
1399
|
+
"POST /api/workspaces",
|
|
1400
|
+
"GET /api/workspaces",
|
|
1401
|
+
"* /api/admin/dev-mode"
|
|
1402
|
+
]);
|
|
1403
|
+
function pathnameOf(path4) {
|
|
1404
|
+
const raw = String(path4 ?? "").split("?")[0].split("#")[0];
|
|
1405
|
+
const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
|
|
1406
|
+
return noTrailing || "/";
|
|
1407
|
+
}
|
|
1408
|
+
function isVaultLessEndpoint(method, path4) {
|
|
1409
|
+
const m = String(method || "GET").toUpperCase();
|
|
1410
|
+
const pathname = pathnameOf(path4);
|
|
1411
|
+
return VAULT_LESS_ENDPOINTS.some((row) => {
|
|
1412
|
+
const [rowMethod, rowPath] = row.split(" ");
|
|
1413
|
+
return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
1418
|
+
import {
|
|
1419
|
+
readdirSync,
|
|
1420
|
+
readFileSync as readFileSync3,
|
|
1421
|
+
writeFileSync as writeFileSync2,
|
|
1422
|
+
mkdirSync as mkdirSync2,
|
|
1423
|
+
existsSync as existsSync2,
|
|
1424
|
+
renameSync as renameSync2,
|
|
1425
|
+
rmSync
|
|
1426
|
+
} from "node:fs";
|
|
1427
|
+
import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
|
|
1428
|
+
|
|
1429
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
1430
|
+
init_heyspex_home();
|
|
1431
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
|
|
1432
|
+
import { homedir as homedir5, tmpdir } from "node:os";
|
|
1433
|
+
|
|
1434
|
+
// vendor/heyspex-mcp/usable-dir-env.mjs
|
|
1435
|
+
import { statSync } from "node:fs";
|
|
1436
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
1437
|
+
|
|
1438
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
1439
|
+
import { basename as basename2, dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
|
|
1440
|
+
|
|
1441
|
+
// vendor/heyspex-mcp/vault-binding.mjs
|
|
1442
|
+
import {
|
|
1443
|
+
copyFileSync,
|
|
1444
|
+
existsSync as existsSync3,
|
|
1445
|
+
mkdirSync as mkdirSync3,
|
|
1446
|
+
readFileSync as readFileSync4,
|
|
1447
|
+
realpathSync as realpathSync2,
|
|
1448
|
+
renameSync as renameSync3,
|
|
1449
|
+
statSync as statSync2,
|
|
1450
|
+
unlinkSync,
|
|
1451
|
+
writeFileSync as writeFileSync3
|
|
1452
|
+
} from "node:fs";
|
|
1453
|
+
import { homedir as homedir4 } from "node:os";
|
|
1454
|
+
import path from "node:path";
|
|
1455
|
+
|
|
1456
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1457
|
+
var VAULT_NOT_REGISTERED = "VAULT_NOT_REGISTERED";
|
|
1458
|
+
var VaultNotRegisteredError = class extends Error {
|
|
1459
|
+
/**
|
|
1460
|
+
* @param {string | null | undefined} repoRootAbs the root the resolver settled on
|
|
1461
|
+
* @param {string | null | undefined} source the resolver tier that produced it
|
|
1462
|
+
* @param {{ reason?: 'unregistered' | 'legacy-id', legacyId?: string | null }} [detail]
|
|
1463
|
+
*/
|
|
1464
|
+
constructor(repoRootAbs, source, { reason = "unregistered", legacyId = null } = {}) {
|
|
1465
|
+
const root = typeof repoRootAbs === "string" && repoRootAbs.trim() ? repoRootAbs.trim() : null;
|
|
1466
|
+
const where = root ? ` (${root})` : "";
|
|
1467
|
+
const why = reason === "legacy-id" ? `This repo${where} is registered on this machine under a legacy id (\`${legacyId}\`), not the vault id the console knows, and its heyspex.yaml carries no \`vault_id\` to stand in for it, ` : `This repo${where} is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), `;
|
|
1468
|
+
super(
|
|
1469
|
+
why + `so the console call was refused before it was sent. Run \`${hsCommand("connect")}\`${root ? ` in ${root}` : ""} to ${reason === "legacy-id" ? "re-register" : "connect"} it, then try again. Nothing was sent.`
|
|
1470
|
+
);
|
|
1471
|
+
this.name = "VaultNotRegisteredError";
|
|
1472
|
+
this.code = VAULT_NOT_REGISTERED;
|
|
1473
|
+
this.reason = reason;
|
|
1474
|
+
this.repoRootAbs = root;
|
|
1475
|
+
this.source = typeof source === "string" ? source : null;
|
|
1476
|
+
this.legacyId = legacyId;
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
var CLOUD_VAULT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1480
|
+
function isCloudVaultId(value) {
|
|
1481
|
+
return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
|
|
1482
|
+
}
|
|
1483
|
+
var MARKER_VAULT_ID_RE = /^vault_id:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
1484
|
+
function readMarkerVaultId(repoRootAbs, { readFile } = {}) {
|
|
1485
|
+
if (typeof repoRootAbs !== "string" || !repoRootAbs.trim()) return null;
|
|
1486
|
+
const read = readFile ?? ((p) => readFileSync6(p, "utf8"));
|
|
1487
|
+
try {
|
|
1488
|
+
const raw = read(join6(repoRootAbs, "heyspex.yaml"));
|
|
1489
|
+
const value = String(raw ?? "").match(MARKER_VAULT_ID_RE)?.[1]?.trim() ?? "";
|
|
1490
|
+
return value || null;
|
|
1491
|
+
} catch {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function cloudVaultIdFor(resolved, { readMarkerVaultId: readMarker = readMarkerVaultId } = {}) {
|
|
1496
|
+
const rowId = resolved.vaultId.trim();
|
|
1497
|
+
if (isCloudVaultId(rowId)) return rowId;
|
|
1498
|
+
const marker = readMarker(resolved.repoRootAbs);
|
|
1499
|
+
if (isCloudVaultId(marker)) return marker.trim();
|
|
1500
|
+
throw new VaultNotRegisteredError(resolved.repoRootAbs, resolved.source, { reason: "legacy-id", legacyId: rowId });
|
|
1501
|
+
}
|
|
1502
|
+
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
1503
|
+
|
|
1504
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
1505
|
+
init_credentials();
|
|
1506
|
+
|
|
1507
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
1508
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1509
|
+
import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
|
|
1510
|
+
|
|
1511
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
1512
|
+
import { homedir as homedir7 } from "node:os";
|
|
1513
|
+
function heyspexHome2(env = process.env) {
|
|
1514
|
+
const override = env?.HEYSPEX_HOME;
|
|
1515
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
1516
|
+
return override.trim();
|
|
1517
|
+
}
|
|
1518
|
+
return homedir7();
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// spex-console/src/lib/vault-registry/types.ts
|
|
1522
|
+
var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
|
|
1523
|
+
var VAULT_REGISTRY_SCHEMA_VERSION = 2;
|
|
1524
|
+
function emptyVaultRegistry() {
|
|
1525
|
+
return {
|
|
1526
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
1527
|
+
current_account_id: null,
|
|
1528
|
+
accounts: {},
|
|
1529
|
+
workspaces: [],
|
|
1530
|
+
vaults: []
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
1535
|
+
function vaultRegistryDir(opts = {}) {
|
|
1536
|
+
return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
|
|
1537
|
+
}
|
|
1538
|
+
function vaultRegistryFilePath(opts = {}) {
|
|
1539
|
+
return join7(vaultRegistryDir(opts), "registry.json");
|
|
1540
|
+
}
|
|
1541
|
+
function isNonEmptyString(v) {
|
|
1542
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
1543
|
+
}
|
|
1544
|
+
function accountOf(o) {
|
|
1545
|
+
return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
|
|
1546
|
+
}
|
|
1547
|
+
function normalizeWorkspace(raw) {
|
|
1548
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1549
|
+
const o = raw;
|
|
1550
|
+
if (!isNonEmptyString(o.id)) return null;
|
|
1551
|
+
if (!isNonEmptyString(o.display_name)) return null;
|
|
1552
|
+
if (!isNonEmptyString(o.created_at)) return null;
|
|
1553
|
+
const record = {
|
|
1554
|
+
id: o.id,
|
|
1555
|
+
display_name: o.display_name,
|
|
1556
|
+
created_at: o.created_at,
|
|
1557
|
+
archived: o.archived === true,
|
|
1558
|
+
account_id: accountOf(o)
|
|
1559
|
+
};
|
|
1560
|
+
if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
|
|
1561
|
+
return record;
|
|
1562
|
+
}
|
|
1563
|
+
function normalizeVault(raw) {
|
|
1564
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1565
|
+
const o = raw;
|
|
1566
|
+
if (!isNonEmptyString(o.id)) return null;
|
|
1567
|
+
if (!isNonEmptyString(o.workspace_id)) return null;
|
|
1568
|
+
if (!isNonEmptyString(o.display_name)) return null;
|
|
1569
|
+
if (!isNonEmptyString(o.repo_root_abs)) return null;
|
|
1570
|
+
if (typeof o.schema_version !== "number") return null;
|
|
1571
|
+
if (!isNonEmptyString(o.registered_at)) return null;
|
|
1572
|
+
const record = {
|
|
1573
|
+
id: o.id,
|
|
1574
|
+
workspace_id: o.workspace_id,
|
|
1575
|
+
display_name: o.display_name,
|
|
1576
|
+
repo_root_abs: o.repo_root_abs,
|
|
1577
|
+
schema_version: o.schema_version,
|
|
1578
|
+
registered_at: o.registered_at,
|
|
1579
|
+
archived: o.archived === true,
|
|
1580
|
+
account_id: accountOf(o)
|
|
1581
|
+
};
|
|
1582
|
+
if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
|
|
1583
|
+
return record;
|
|
1584
|
+
}
|
|
1585
|
+
function normalizeStoredPointers(raw) {
|
|
1586
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
1587
|
+
const out = {};
|
|
1588
|
+
for (const field of ["active_vault_id", "active_workspace_id"]) {
|
|
1589
|
+
const v = o[field];
|
|
1590
|
+
if (v === null || isNonEmptyString(v)) out[field] = v;
|
|
1591
|
+
}
|
|
1592
|
+
return out;
|
|
1593
|
+
}
|
|
1594
|
+
function normalizePointers(raw) {
|
|
1595
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
1596
|
+
return {
|
|
1597
|
+
active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
|
|
1598
|
+
active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
function registryFileSchemaOf(json) {
|
|
1602
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) return null;
|
|
1603
|
+
return json.schema_version === 2 ? 2 : 1;
|
|
1604
|
+
}
|
|
1605
|
+
function parseVaultRegistryFile(json) {
|
|
1606
|
+
const schema = registryFileSchemaOf(json);
|
|
1607
|
+
if (schema === null) return emptyVaultRegistry();
|
|
1608
|
+
const o = json;
|
|
1609
|
+
const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
|
|
1610
|
+
const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
|
|
1611
|
+
const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
|
|
1612
|
+
const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
|
|
1613
|
+
if (schema === 1) {
|
|
1614
|
+
const legacy = normalizePointers(o);
|
|
1615
|
+
const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces.length > 0 || vaults.length > 0;
|
|
1616
|
+
return {
|
|
1617
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
1618
|
+
current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
|
|
1619
|
+
accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
|
|
1620
|
+
workspaces,
|
|
1621
|
+
vaults
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
const accounts = {};
|
|
1625
|
+
if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
|
|
1626
|
+
for (const [id, pointers] of Object.entries(o.accounts)) {
|
|
1627
|
+
if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
1632
|
+
current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
|
|
1633
|
+
accounts,
|
|
1634
|
+
workspaces,
|
|
1635
|
+
vaults
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function readRegistryJson(opts) {
|
|
1639
|
+
const filePath = vaultRegistryFilePath(opts);
|
|
1640
|
+
if (!existsSync5(filePath)) return { kind: "missing" };
|
|
1641
|
+
try {
|
|
1642
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
1643
|
+
const json = JSON.parse(raw);
|
|
1644
|
+
if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
|
|
1645
|
+
return { kind: "ok", raw, json };
|
|
1646
|
+
} catch {
|
|
1647
|
+
return { kind: "unreadable" };
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function inspectVaultRegistry(opts = {}) {
|
|
1651
|
+
const read = readRegistryJson(opts);
|
|
1652
|
+
if (read.kind !== "ok") {
|
|
1653
|
+
return {
|
|
1654
|
+
registry: emptyVaultRegistry(),
|
|
1655
|
+
file_schema_version: null,
|
|
1656
|
+
registry_unreadable: read.kind === "unreadable"
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
registry: parseVaultRegistryFile(read.json),
|
|
1661
|
+
file_schema_version: registryFileSchemaOf(read.json),
|
|
1662
|
+
registry_unreadable: false
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function loadVaultRegistry(opts = {}) {
|
|
1666
|
+
return inspectVaultRegistry(opts).registry;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
1670
|
+
var EXIT = {
|
|
1671
|
+
/** Success. */
|
|
1672
|
+
OK: 0,
|
|
1673
|
+
/**
|
|
1674
|
+
* A read-only CHECK ran correctly and its answer is "no" — `hs connect
|
|
1675
|
+
* --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
|
|
1676
|
+
* script can tell "you typed the command wrong" from "the command worked,
|
|
1677
|
+
* the repo is not connected". Both were `1` until 2026-09-18, which made
|
|
1678
|
+
* `--check` unusable in a conditional: every caller had to parse stdout to
|
|
1679
|
+
* find out which of the two had happened.
|
|
1680
|
+
*/
|
|
1681
|
+
CHECK_FAILED: 2,
|
|
1682
|
+
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
1683
|
+
USAGE: 1,
|
|
1684
|
+
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
1685
|
+
CONFLICT: 9,
|
|
1686
|
+
/** Target not found — release of an unheld claim, etc. (404). */
|
|
1687
|
+
NOT_FOUND: 10,
|
|
1688
|
+
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
1689
|
+
INVALID_ARG: 11,
|
|
1690
|
+
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
1691
|
+
CONSOLE_UNREACHABLE: 12,
|
|
1692
|
+
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
1693
|
+
WRITE_FAILED: 13,
|
|
1694
|
+
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
1695
|
+
NEON_REQUIRED: 14,
|
|
1696
|
+
/**
|
|
1697
|
+
* A cross-account `--account <id>` override was refused — the caller is
|
|
1698
|
+
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
1699
|
+
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
1700
|
+
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
1701
|
+
* malformed command.
|
|
1702
|
+
*/
|
|
1703
|
+
FORBIDDEN: 15,
|
|
1704
|
+
/**
|
|
1705
|
+
* A CLOUD write refused because no account id could be resolved (no
|
|
1706
|
+
* `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
|
|
1707
|
+
* `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
|
|
1708
|
+
* ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
|
|
1709
|
+
* `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
|
|
1710
|
+
* authorized for this override".
|
|
1711
|
+
*/
|
|
1712
|
+
SIGN_IN_REQUIRED: 16,
|
|
1713
|
+
/**
|
|
1714
|
+
* Signed in, but the account a repo would be attached to was not confirmed:
|
|
1715
|
+
* a non-interactive install/connect with no matching `--account` / `--yes`,
|
|
1716
|
+
* an `account_id` that disagrees with the bearer's user, or the human
|
|
1717
|
+
* aborting the identity prompt (`install-identity.ts`,
|
|
1718
|
+
* [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
|
|
1719
|
+
*/
|
|
1720
|
+
IDENTITY_UNCONFIRMED: 17,
|
|
1721
|
+
/**
|
|
1722
|
+
* The repo this verb runs in is not registered on this machine (no
|
|
1723
|
+
* `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
|
|
1724
|
+
* cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
|
|
1725
|
+
* nothing was sent. Names `hs connect`
|
|
1726
|
+
* ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
|
|
1727
|
+
*/
|
|
1728
|
+
VAULT_NOT_REGISTERED: 18,
|
|
1729
|
+
/**
|
|
1730
|
+
* The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
|
|
1731
|
+
* *`) was written but the CLOUD half did not land -- the console refused
|
|
1732
|
+
* it or was never reached. The verb's receipt names the failed half and the
|
|
1733
|
+
* exact command to re-run
|
|
1734
|
+
* ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
|
|
1735
|
+
* Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
|
|
1736
|
+
* sync the cloud" from a generic server failure.
|
|
1737
|
+
*/
|
|
1738
|
+
CLOUD_WRITE_FAILED: 19,
|
|
1739
|
+
/**
|
|
1740
|
+
* The repo's committed `heyspex.yaml` records `account: <A>` but the machine
|
|
1741
|
+
* is signed in as another account, so `hs connect` refused and wrote
|
|
1742
|
+
* nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
|
|
1743
|
+
* ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
|
|
1744
|
+
*/
|
|
1745
|
+
ACCOUNT_MISMATCH: 20,
|
|
1746
|
+
/**
|
|
1747
|
+
* Both sides of a setup verb's receipt were read back and still DISAGREE
|
|
1748
|
+
* after a short bounded re-read of the cloud side (a briefly stale read is
|
|
1749
|
+
* absorbed first). The write may well have landed; what the verb cannot
|
|
1750
|
+
* claim is that this machine and the console now say the same thing. The
|
|
1751
|
+
* receipt prints the diffs and the command to re-run
|
|
1752
|
+
* ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
|
|
1753
|
+
* Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
|
|
1754
|
+
*/
|
|
1755
|
+
RECEIPT_DISAGREE: 21
|
|
1756
|
+
};
|
|
1757
|
+
var VAULT_ID_HEADER = "x-heyspex-vault-id";
|
|
1758
|
+
var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
|
|
1759
|
+
function readPairedBearer(opts = {}) {
|
|
1760
|
+
return readBearer(opts);
|
|
1761
|
+
}
|
|
1762
|
+
function readPairedToken(opts = {}) {
|
|
1763
|
+
return readPairedBearer(opts).token ?? void 0;
|
|
1764
|
+
}
|
|
1765
|
+
function bearerHeaderFor(consoleUrl, opts = {}) {
|
|
1766
|
+
try {
|
|
1767
|
+
const host = new URL(consoleUrl).hostname;
|
|
1768
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
|
|
1769
|
+
return {};
|
|
1770
|
+
}
|
|
1771
|
+
const token = readPairedToken(opts);
|
|
1772
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
1773
|
+
} catch {
|
|
1774
|
+
return {};
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
function scopeHeadersFor(scope, method, path4) {
|
|
1778
|
+
if (scope === "none") {
|
|
1779
|
+
if (!isVaultLessEndpoint(method, path4)) {
|
|
1780
|
+
throw new Error(
|
|
1781
|
+
`controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path4} is vault-scoped and must name { accountId, vaultId }.`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
return {};
|
|
1785
|
+
}
|
|
1786
|
+
return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
|
|
1787
|
+
}
|
|
1788
|
+
async function controlRequest(deps, req) {
|
|
1789
|
+
const url = `${deps.consoleUrl}${req.path}`;
|
|
1790
|
+
const scopeHeaders = scopeHeadersFor(deps.scope, req.method, req.path);
|
|
1791
|
+
let res;
|
|
1792
|
+
try {
|
|
1793
|
+
res = await deps.fetchImpl(url, {
|
|
1794
|
+
method: req.method,
|
|
1795
|
+
headers: {
|
|
1796
|
+
"content-type": "application/json",
|
|
1797
|
+
// A remote console (cloud vault write mode, [[HS-PH7O-ILNC-DIAG-hs-check-report-cannot-authenticate]])
|
|
1798
|
+
// authenticates every control call; loopback never asked, so the header
|
|
1799
|
+
// is attached only when the target is non-loopback and a paired token
|
|
1800
|
+
// exists. Same `hsmcp_` bearer the MCP server presents.
|
|
1801
|
+
...bearerHeaderFor(deps.consoleUrl),
|
|
1802
|
+
// Invariant 2: name the vault and the account on every call, or the
|
|
1803
|
+
// console serves whatever its leftover pointer says.
|
|
1804
|
+
...scopeHeaders
|
|
1805
|
+
},
|
|
1806
|
+
...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
|
|
1807
|
+
});
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1810
|
+
return {
|
|
1811
|
+
ok: false,
|
|
1812
|
+
error: {
|
|
1813
|
+
code: EXIT.CONSOLE_UNREACHABLE,
|
|
1814
|
+
message: `could not reach console at ${deps.consoleUrl}: ${message}
|
|
1815
|
+
Start the dev server or set HEYSPEX_CONSOLE_URL.`,
|
|
1816
|
+
payload: {
|
|
1817
|
+
ok: false,
|
|
1818
|
+
error: "console_unreachable",
|
|
1819
|
+
url: deps.consoleUrl,
|
|
1820
|
+
message
|
|
1821
|
+
},
|
|
1822
|
+
isError: true
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
let parsed = null;
|
|
1827
|
+
try {
|
|
1828
|
+
parsed = await res.json();
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
if (res.status === 402) {
|
|
1832
|
+
return {
|
|
1833
|
+
ok: false,
|
|
1834
|
+
error: {
|
|
1835
|
+
code: EXIT.NEON_REQUIRED,
|
|
1836
|
+
message: "this command requires the Neon control plane, which is not configured (console returned 402).",
|
|
1837
|
+
payload: {
|
|
1838
|
+
ok: false,
|
|
1839
|
+
error: "neon_required",
|
|
1840
|
+
status: 402,
|
|
1841
|
+
console_response: parsed
|
|
1842
|
+
},
|
|
1843
|
+
isError: true
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
return { ok: true, status: res.status, body: parsed };
|
|
1848
|
+
}
|
|
1849
|
+
function normalizeRoot(p) {
|
|
1850
|
+
return path2.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
1851
|
+
}
|
|
1852
|
+
function resolveControlScope(input) {
|
|
1853
|
+
const target = normalizeRoot(input.vaultRoot);
|
|
1854
|
+
const row = input.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
|
|
1855
|
+
if (!row) {
|
|
1856
|
+
return {
|
|
1857
|
+
ok: false,
|
|
1858
|
+
error: {
|
|
1859
|
+
code: EXIT.VAULT_NOT_REGISTERED,
|
|
1860
|
+
message: `this repo (${input.vaultRoot}) is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), so the call was refused before it was sent. Run \`${hsCommand("connect")}\` in ${input.vaultRoot}, then try again.`,
|
|
1861
|
+
payload: {
|
|
1862
|
+
ok: false,
|
|
1863
|
+
error: "vault_not_registered",
|
|
1864
|
+
reason: "unregistered",
|
|
1865
|
+
vault_root: input.vaultRoot
|
|
1866
|
+
},
|
|
1867
|
+
isError: true
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
let vaultId;
|
|
1872
|
+
try {
|
|
1873
|
+
vaultId = cloudVaultIdFor(
|
|
1874
|
+
{ vaultId: row.id, repoRootAbs: row.repo_root_abs, source: "registry" },
|
|
1875
|
+
input.readMarkerVaultId ? { readMarkerVaultId: input.readMarkerVaultId } : {}
|
|
1876
|
+
);
|
|
1877
|
+
} catch (err) {
|
|
1878
|
+
if (!(err instanceof VaultNotRegisteredError)) throw err;
|
|
1879
|
+
const refused = err;
|
|
1880
|
+
return {
|
|
1881
|
+
ok: false,
|
|
1882
|
+
error: {
|
|
1883
|
+
code: EXIT.VAULT_NOT_REGISTERED,
|
|
1884
|
+
message: refused.message,
|
|
1885
|
+
payload: {
|
|
1886
|
+
ok: false,
|
|
1887
|
+
error: "vault_not_registered",
|
|
1888
|
+
reason: refused.reason ?? "legacy-id",
|
|
1889
|
+
vault_root: input.vaultRoot,
|
|
1890
|
+
legacy_id: refused.legacyId ?? row.id
|
|
1891
|
+
},
|
|
1892
|
+
isError: true
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
const accountId = input.accountId?.trim() ?? "";
|
|
1897
|
+
if (!accountId) {
|
|
1898
|
+
return {
|
|
1899
|
+
ok: false,
|
|
1900
|
+
error: {
|
|
1901
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
1902
|
+
message: `no HeySpex account is configured on this machine (\`hs login\` has not been run), so the call was refused before it was sent. Run \`${hsCommand("login")}\` on this machine, then try again.`,
|
|
1903
|
+
payload: { ok: false, error: "sign_in_required", vault_root: input.vaultRoot },
|
|
1904
|
+
isError: true
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
return { ok: true, scope: { accountId, vaultId } };
|
|
1909
|
+
}
|
|
1910
|
+
function resolveControlScopeFromMachine(vaultRoot, opts = {}) {
|
|
1911
|
+
const registry = loadVaultRegistry(opts.registry);
|
|
1912
|
+
const accountId = resolveSessionOwnerUncached({ env: opts.env, homeDir: opts.homeDir });
|
|
1913
|
+
return resolveControlScope({ vaultRoot, accountId, vaults: registry.vaults });
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// scripts/lib/console-url-resolver.mjs
|
|
1917
|
+
init_heyspex_home();
|
|
1918
|
+
init_dev_mode();
|
|
1919
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
1920
|
+
import { homedir as homedir9 } from "node:os";
|
|
1921
|
+
import { join as join9 } from "node:path";
|
|
1922
|
+
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
1923
|
+
function trimTrailingSlash(url) {
|
|
1924
|
+
return url.replace(/\/+$/, "");
|
|
1925
|
+
}
|
|
1926
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
1927
|
+
function isLoopbackConsoleUrl(url) {
|
|
1928
|
+
if (typeof url !== "string" || !url) return false;
|
|
1929
|
+
try {
|
|
1930
|
+
return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
1931
|
+
} catch {
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
var staleLoopbackCredentialsWarned = false;
|
|
1936
|
+
function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
1937
|
+
if (staleLoopbackCredentialsWarned) return;
|
|
1938
|
+
staleLoopbackCredentialsWarned = true;
|
|
1939
|
+
try {
|
|
1940
|
+
log(
|
|
1941
|
+
`[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
|
|
1942
|
+
`
|
|
1943
|
+
);
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
|
|
1948
|
+
try {
|
|
1949
|
+
const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
|
|
1950
|
+
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
1951
|
+
const value = match?.[1]?.trim();
|
|
1952
|
+
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
1953
|
+
} catch {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function defaultReadFile2(p) {
|
|
1958
|
+
return readFileSync9(p, "utf8");
|
|
1959
|
+
}
|
|
1960
|
+
function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
|
|
1961
|
+
if (!repoRoot) return null;
|
|
1962
|
+
try {
|
|
1963
|
+
const discovery = String(readDiscovery(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
|
|
1964
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
1965
|
+
} catch {
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
var ConsoleUrlUnresolvedError = class extends Error {
|
|
1970
|
+
constructor() {
|
|
1971
|
+
super(
|
|
1972
|
+
"resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
|
|
1973
|
+
);
|
|
1974
|
+
this.name = "ConsoleUrlUnresolvedError";
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
function resolveModeEnvelope({ env, homeDir }) {
|
|
1978
|
+
try {
|
|
1979
|
+
const resolved = resolveDevMode({ env, homeDir });
|
|
1980
|
+
return {
|
|
1981
|
+
reliance: resolved.reliance === "cloud" ? "cloud" : "local",
|
|
1982
|
+
consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1983
|
+
};
|
|
1984
|
+
} catch {
|
|
1985
|
+
return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function resolveConsoleUrl({
|
|
1989
|
+
repoRoot,
|
|
1990
|
+
env = process.env,
|
|
1991
|
+
homeDir = heyspexHome(),
|
|
1992
|
+
strict = false,
|
|
1993
|
+
devMode,
|
|
1994
|
+
log = (line) => process.stderr.write(line),
|
|
1995
|
+
readFile = defaultReadFile2,
|
|
1996
|
+
readDiscovery = defaultReadFile2
|
|
1997
|
+
} = {}) {
|
|
1998
|
+
const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
|
|
1999
|
+
if (envUrl) return trimTrailingSlash(envUrl);
|
|
2000
|
+
const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
|
|
2001
|
+
const cloud = mode.reliance === "cloud";
|
|
2002
|
+
const derivedUrl = trimTrailingSlash(
|
|
2003
|
+
cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
|
|
2004
|
+
);
|
|
2005
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
|
|
2006
|
+
if (credentialsUrl) {
|
|
2007
|
+
if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
|
|
2008
|
+
warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
|
|
2009
|
+
} else {
|
|
2010
|
+
return credentialsUrl;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (!cloud) {
|
|
2014
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
|
|
2015
|
+
if (discoveryUrl) return discoveryUrl;
|
|
2016
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
2017
|
+
}
|
|
2018
|
+
return derivedUrl;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// spex-console/src/lib/cli/defer.ts
|
|
2022
|
+
function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
|
|
2023
|
+
return resolveConsoleUrl({
|
|
2024
|
+
repoRoot: vaultRoot,
|
|
2025
|
+
env: opts.env,
|
|
2026
|
+
homeDir: opts.homeDir,
|
|
2027
|
+
readFile: opts.readFile,
|
|
2028
|
+
readDiscovery,
|
|
2029
|
+
log: opts.log
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
var HELP2 = [
|
|
2033
|
+
"Usage:",
|
|
2034
|
+
" hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
|
|
2035
|
+
" hs undefer <spec-id> [--json] [--no-daemon]",
|
|
2036
|
+
"",
|
|
2037
|
+
"Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
|
|
2038
|
+
"scheduler-invisible until the date passes, then auto-revives.",
|
|
2039
|
+
"",
|
|
2040
|
+
"Arguments:",
|
|
2041
|
+
" <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
|
|
2042
|
+
"",
|
|
2043
|
+
"Flags:",
|
|
2044
|
+
" --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
|
|
2045
|
+
" Defaults to +1y (matches the console Defer button).",
|
|
2046
|
+
" Ignored by `undefer` (which always clears the field).",
|
|
2047
|
+
" --json Emit a machine-readable JSON result.",
|
|
2048
|
+
" --no-daemon Write straight to the vault (no console needed; CI path).",
|
|
2049
|
+
" --help, -h Show this help.",
|
|
2050
|
+
"",
|
|
2051
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
|
|
2052
|
+
].join("\n");
|
|
2053
|
+
|
|
2054
|
+
// spex-console/src/lib/cli/revalidate-sweep.ts
|
|
2055
|
+
var REVALIDATE_EXIT = {
|
|
2056
|
+
/** Swept clean — no spec is red (no-win items do not fail the pass). */
|
|
2057
|
+
OK: 0,
|
|
2058
|
+
/** Usage error — unknown flag or bad verb. */
|
|
2059
|
+
USAGE: 1,
|
|
2060
|
+
/** Nothing to sweep — no stamped spec has a manifest. */
|
|
2061
|
+
NOT_FOUND: 10,
|
|
2062
|
+
/** The pass ran and at least one stamped spec's frozen checks are red. */
|
|
2063
|
+
DECAY_FOUND: 20
|
|
2064
|
+
};
|
|
2065
|
+
var STAMPED_STATUSES = /* @__PURE__ */ new Set(["review_pending", "in_review"]);
|
|
2066
|
+
var DIAGNOSE_CLOSED_STATUSES = /* @__PURE__ */ new Set([
|
|
2067
|
+
"closed",
|
|
2068
|
+
"shipped",
|
|
2069
|
+
"in_review",
|
|
2070
|
+
"archived",
|
|
2071
|
+
"cancelled",
|
|
2072
|
+
"duplicate",
|
|
2073
|
+
"hibernated",
|
|
2074
|
+
"superseded"
|
|
2075
|
+
]);
|
|
2076
|
+
function isDiagnoseOpen(status) {
|
|
2077
|
+
return !DIAGNOSE_CLOSED_STATUSES.has(status.trim().toLowerCase());
|
|
2078
|
+
}
|
|
2079
|
+
var DECAY_TAG = "stamp-decay";
|
|
2080
|
+
function computeCoverage(input) {
|
|
2081
|
+
const percent = input.stamped === 0 ? 0 : Math.round(input.checkable / input.stamped * 1e3) / 10;
|
|
2082
|
+
return { ...input, percent };
|
|
2083
|
+
}
|
|
2084
|
+
var CHECK_KIND_ALIASES = {
|
|
2085
|
+
tests: "test",
|
|
2086
|
+
unit: "test",
|
|
2087
|
+
grep: "command",
|
|
2088
|
+
live: "command",
|
|
2089
|
+
diff: "command",
|
|
2090
|
+
evidence: "command"
|
|
2091
|
+
};
|
|
2092
|
+
function loadSweepManifest(spec, text) {
|
|
2093
|
+
const aliased = [];
|
|
2094
|
+
const originalKinds = {};
|
|
2095
|
+
let raw;
|
|
2096
|
+
try {
|
|
2097
|
+
raw = JSON.parse(text);
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
return {
|
|
2100
|
+
spec,
|
|
2101
|
+
error: `manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
2102
|
+
aliased,
|
|
2103
|
+
originalKinds
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
if (typeof raw === "object" && raw !== null) {
|
|
2107
|
+
const criteria = raw.criteria;
|
|
2108
|
+
if (Array.isArray(criteria)) {
|
|
2109
|
+
for (const criterion of criteria) {
|
|
2110
|
+
if (typeof criterion !== "object" || criterion === null) continue;
|
|
2111
|
+
const checks = criterion.checks;
|
|
2112
|
+
if (!Array.isArray(checks)) continue;
|
|
2113
|
+
for (const check of checks) {
|
|
2114
|
+
if (typeof check !== "object" || check === null) continue;
|
|
2115
|
+
const rec = check;
|
|
2116
|
+
const kind = rec.kind;
|
|
2117
|
+
if (typeof kind !== "string") continue;
|
|
2118
|
+
const canonical = CHECK_KIND_ALIASES[kind];
|
|
2119
|
+
if (!canonical) continue;
|
|
2120
|
+
if (typeof rec.run === "string") {
|
|
2121
|
+
originalKinds[keyOf(rec.run, typeof rec.cwd === "string" ? rec.cwd : void 0)] = kind;
|
|
2122
|
+
}
|
|
2123
|
+
rec.kind = canonical;
|
|
2124
|
+
aliased.push(kind);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const { manifest, error } = parseChecksManifest(JSON.stringify(raw));
|
|
2130
|
+
return { spec, manifest, error, aliased: [...new Set(aliased)].sort(), originalKinds };
|
|
2131
|
+
}
|
|
2132
|
+
function commandKey(check) {
|
|
2133
|
+
return keyOf(check.run, check.cwd);
|
|
2134
|
+
}
|
|
2135
|
+
function keyOf(run, cwd) {
|
|
2136
|
+
return `${cwd?.trim() || "*"}\0${run.trim()}`;
|
|
2137
|
+
}
|
|
2138
|
+
function planCommands(loaded) {
|
|
2139
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2140
|
+
let checkInstances = 0;
|
|
2141
|
+
for (const entry of loaded) {
|
|
2142
|
+
if (!entry.manifest) continue;
|
|
2143
|
+
for (const criterion of entry.manifest.criteria) {
|
|
2144
|
+
for (const check of criterion.checks) {
|
|
2145
|
+
checkInstances++;
|
|
2146
|
+
const key = commandKey(check);
|
|
2147
|
+
let planned = byKey.get(key);
|
|
2148
|
+
if (!planned) {
|
|
2149
|
+
planned = {
|
|
2150
|
+
key,
|
|
2151
|
+
run: check.run,
|
|
2152
|
+
cwd: check.cwd ?? null,
|
|
2153
|
+
kind: entry.originalKinds[key] ?? check.kind,
|
|
2154
|
+
uses: [],
|
|
2155
|
+
noWin: classifyNoWin(check)
|
|
2156
|
+
};
|
|
2157
|
+
byKey.set(key, planned);
|
|
2158
|
+
}
|
|
2159
|
+
planned.uses.push({ specId: entry.spec.id, ac: criterion.ac, check });
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return { commands: [...byKey.values()], checkInstances };
|
|
2164
|
+
}
|
|
2165
|
+
function makeReplayDeps(outcomes) {
|
|
2166
|
+
return {
|
|
2167
|
+
runCheck: async (check) => {
|
|
2168
|
+
const outcome = outcomes.get(commandKey(check));
|
|
2169
|
+
if (outcome) return outcome;
|
|
2170
|
+
return {
|
|
2171
|
+
exitCode: -1,
|
|
2172
|
+
durationMs: 0,
|
|
2173
|
+
message: `internal: no recorded outcome for \`${check.run}\` (command was never planned)`
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
function pickAttempt(attempts) {
|
|
2179
|
+
if (attempts.length === 0) {
|
|
2180
|
+
return { cwd: "(none)", outcome: { exitCode: -1, durationMs: 0, message: "no attempt made" } };
|
|
2181
|
+
}
|
|
2182
|
+
return attempts.find((a) => a.outcome.exitCode === 0) ?? attempts[0];
|
|
2183
|
+
}
|
|
2184
|
+
function needsSerialConfirmation(outcome) {
|
|
2185
|
+
return outcome.exitCode !== 0;
|
|
2186
|
+
}
|
|
2187
|
+
function resolveExecution(parallel, confirmation) {
|
|
2188
|
+
if (!needsSerialConfirmation(parallel)) return { outcome: parallel, parallelismArtifact: false };
|
|
2189
|
+
if (!confirmation) return { outcome: parallel, parallelismArtifact: false };
|
|
2190
|
+
return {
|
|
2191
|
+
outcome: confirmation,
|
|
2192
|
+
parallelismArtifact: !needsSerialConfirmation(confirmation)
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
var RUNNER_RE = /(?:^|[\\/])(vitest(?:\.m?js)?|jest(?:\.m?js)?)$/i;
|
|
2196
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
2197
|
+
"-t",
|
|
2198
|
+
"--testNamePattern",
|
|
2199
|
+
"--reporter",
|
|
2200
|
+
"--project",
|
|
2201
|
+
"--config",
|
|
2202
|
+
"-c",
|
|
2203
|
+
"--environment",
|
|
2204
|
+
"--pool",
|
|
2205
|
+
"--prefix"
|
|
2206
|
+
]);
|
|
2207
|
+
var COMPOUND_RE = /[|;&]|\$\(|`|\|\|/;
|
|
2208
|
+
var COUNT_PINNED_PATTERNS = [
|
|
2209
|
+
{ re: /\bexactly[=\s]+\d+\b/i, label: "an `exactly <n>` assertion" },
|
|
2210
|
+
{ re: /\s-eq\s+\d{2,}\b/, label: "a shell `-eq <n>` comparison against a literal count" },
|
|
2211
|
+
{ re: /--count[=\s]+\d{2,}\b/, label: "a `--count <n>` assertion" },
|
|
2212
|
+
{ re: /\.length\s*===?\s*\d{2,}\b/, label: "a `.length === <n>` assertion" }
|
|
2213
|
+
];
|
|
2214
|
+
function classifyNoWin(check) {
|
|
2215
|
+
const run = check.run.trim();
|
|
2216
|
+
if (check.expect === "exit-0" && isWholeSuiteInvocation(run)) {
|
|
2217
|
+
return {
|
|
2218
|
+
reason: "whole-suite-exit-0",
|
|
2219
|
+
detail: "pins the entire test suite to exit 0 \u2014 it goes red whenever any unrelated test in the repo is red, so no change to this spec can make it pass"
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
for (const { re, label } of COUNT_PINNED_PATTERNS) {
|
|
2223
|
+
if (re.test(run)) {
|
|
2224
|
+
return {
|
|
2225
|
+
reason: "count-pinned",
|
|
2226
|
+
detail: `${label} \u2014 it goes red as soon as the counted thing grows, independently of this spec`
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return null;
|
|
2231
|
+
}
|
|
2232
|
+
function isWholeSuiteInvocation(run) {
|
|
2233
|
+
if (COMPOUND_RE.test(run)) return false;
|
|
2234
|
+
const tokens = run.split(/\s+/).filter(Boolean);
|
|
2235
|
+
if (tokens[0] === "npm") {
|
|
2236
|
+
const rest = tokens.slice(1).filter((t) => !t.startsWith("-"));
|
|
2237
|
+
const script = rest[0] === "run" ? rest[1] : rest[0];
|
|
2238
|
+
return (script === "test" || script === "tests") && !tokens.includes("--");
|
|
2239
|
+
}
|
|
2240
|
+
const runnerAt = tokens.findIndex((t) => RUNNER_RE.test(t));
|
|
2241
|
+
if (runnerAt === -1) return false;
|
|
2242
|
+
let i = runnerAt + 1;
|
|
2243
|
+
if (tokens[i] === "run" || tokens[i] === "watch") i++;
|
|
2244
|
+
for (; i < tokens.length; i++) {
|
|
2245
|
+
const token = tokens[i];
|
|
2246
|
+
if (VALUE_FLAGS.has(token)) {
|
|
2247
|
+
i++;
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
if (token.startsWith("-")) continue;
|
|
2251
|
+
return false;
|
|
2252
|
+
}
|
|
2253
|
+
return true;
|
|
2254
|
+
}
|
|
2255
|
+
function scoreSpec(loaded, report, noWinByKey, artifactKeys) {
|
|
2256
|
+
const base = {
|
|
2257
|
+
specId: loaded.spec.id,
|
|
2258
|
+
title: loaded.spec.title,
|
|
2259
|
+
status: loaded.spec.status
|
|
2260
|
+
};
|
|
2261
|
+
if (!report) {
|
|
2262
|
+
return {
|
|
2263
|
+
...base,
|
|
2264
|
+
verdict: "unparsable",
|
|
2265
|
+
error: loaded.error ?? "manifest could not be loaded",
|
|
2266
|
+
failures: [],
|
|
2267
|
+
noWin: [],
|
|
2268
|
+
parallelismArtifacts: []
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
const failures = [];
|
|
2272
|
+
const noWin = [];
|
|
2273
|
+
const artifacts = /* @__PURE__ */ new Set();
|
|
2274
|
+
for (const criterion of report.criteria) {
|
|
2275
|
+
for (const result of criterion.checks) {
|
|
2276
|
+
const key = commandKey(result.check);
|
|
2277
|
+
if (artifactKeys.has(key)) artifacts.add(result.check.run);
|
|
2278
|
+
if (result.passed) continue;
|
|
2279
|
+
const finding = noWinByKey.get(key);
|
|
2280
|
+
if (finding) {
|
|
2281
|
+
noWin.push({
|
|
2282
|
+
ac: criterion.ac,
|
|
2283
|
+
run: result.check.run,
|
|
2284
|
+
reason: finding.reason,
|
|
2285
|
+
detail: finding.detail
|
|
2286
|
+
});
|
|
2287
|
+
} else {
|
|
2288
|
+
failures.push({
|
|
2289
|
+
ac: criterion.ac,
|
|
2290
|
+
run: result.check.run,
|
|
2291
|
+
message: result.message ?? "check failed"
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const verdict = failures.length > 0 ? "fail" : noWin.length > 0 ? "no-win" : "pass";
|
|
2297
|
+
return { ...base, verdict, report, failures, noWin, parallelismArtifacts: [...artifacts] };
|
|
2298
|
+
}
|
|
2299
|
+
var EMPTY_SWEEP_STATE = { lastRunAt: null, noWin: {} };
|
|
2300
|
+
function recordNoWin(state, commands, now) {
|
|
2301
|
+
const noWin = { ...state.noWin };
|
|
2302
|
+
const items = [];
|
|
2303
|
+
for (const command of commands) {
|
|
2304
|
+
if (!command.noWin) continue;
|
|
2305
|
+
const known = noWin[command.key];
|
|
2306
|
+
const firstReportedAt = known?.firstReportedAt ?? now;
|
|
2307
|
+
if (!known) noWin[command.key] = { reason: command.noWin.reason, firstReportedAt };
|
|
2308
|
+
items.push({
|
|
2309
|
+
key: command.key,
|
|
2310
|
+
run: command.run,
|
|
2311
|
+
reason: command.noWin.reason,
|
|
2312
|
+
detail: command.noWin.detail,
|
|
2313
|
+
specIds: [...new Set(command.uses.map((u) => u.specId))].sort(),
|
|
2314
|
+
firstSeen: !known,
|
|
2315
|
+
firstReportedAt
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
return { state: { ...state, noWin, lastRunAt: now }, items };
|
|
2319
|
+
}
|
|
2320
|
+
function shouldRunNow(state, now, minIntervalHours) {
|
|
2321
|
+
if (!state.lastRunAt) return { run: true, hoursSinceLast: null };
|
|
2322
|
+
const last = Date.parse(state.lastRunAt);
|
|
2323
|
+
if (Number.isNaN(last)) return { run: true, hoursSinceLast: null };
|
|
2324
|
+
const hours = (now.getTime() - last) / 36e5;
|
|
2325
|
+
if (minIntervalHours === null) return { run: true, hoursSinceLast: hours };
|
|
2326
|
+
return { run: hours >= minIntervalHours, hoursSinceLast: hours };
|
|
2327
|
+
}
|
|
2328
|
+
function detectSideEffects(before, after) {
|
|
2329
|
+
const beforeByPath = new Map(before.map((f) => [f.path, f]));
|
|
2330
|
+
const afterByPath = new Map(after.map((f) => [f.path, f]));
|
|
2331
|
+
const effects = [];
|
|
2332
|
+
for (const file of after) {
|
|
2333
|
+
const prior = beforeByPath.get(file.path);
|
|
2334
|
+
if (!prior) {
|
|
2335
|
+
effects.push({ path: file.path, change: "added", contentIdentical: false });
|
|
2336
|
+
} else if (prior.sha !== file.sha) {
|
|
2337
|
+
effects.push({
|
|
2338
|
+
path: file.path,
|
|
2339
|
+
change: "modified",
|
|
2340
|
+
contentIdentical: prior.normalizedSha === file.normalizedSha
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
for (const file of before) {
|
|
2345
|
+
if (!afterByPath.has(file.path)) {
|
|
2346
|
+
effects.push({ path: file.path, change: "removed", contentIdentical: false });
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return effects.sort((a, b) => a.path.localeCompare(b.path));
|
|
2350
|
+
}
|
|
2351
|
+
function desiredDiagnoseRows(results) {
|
|
2352
|
+
return results.filter((r) => r.verdict === "fail").map((result) => {
|
|
2353
|
+
const acs = [...new Set(result.failures.map((f) => f.ac))].sort((a, b) => a - b);
|
|
2354
|
+
const commands = [...new Set(result.failures.map((f) => f.run))];
|
|
2355
|
+
return {
|
|
2356
|
+
subjectSpecId: result.specId,
|
|
2357
|
+
name: `Frozen checks for ${result.specId} are red \u2014 the completion stamp no longer holds`,
|
|
2358
|
+
summary: `${result.specId} is at \`${result.status}\` but ${result.failures.length} of its frozen checks now fail (AC${acs.join(", AC")}). The stamp asserted these commands passed; re-running them says they do not.`,
|
|
2359
|
+
severity: "error",
|
|
2360
|
+
tags: [DECAY_TAG, "quality-gate", "checks-manifest"],
|
|
2361
|
+
related: [`[[${result.specId}]]`],
|
|
2362
|
+
body: renderDecayBody(result, commands)
|
|
2363
|
+
};
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
function tableCell(text, max = 200) {
|
|
2367
|
+
const flat = text.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
2368
|
+
const clipped = flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
2369
|
+
return clipped.replace(/\|/g, "\\|");
|
|
2370
|
+
}
|
|
2371
|
+
function renderDecayBody(result, commands) {
|
|
2372
|
+
const lines = [
|
|
2373
|
+
`# Frozen checks for ${result.specId} are red`,
|
|
2374
|
+
"",
|
|
2375
|
+
"## Symptom",
|
|
2376
|
+
"",
|
|
2377
|
+
`${result.specId} (${result.title}) sits at \`${result.status}\` with a frozen checks manifest whose commands no longer pass. Filed by the scheduled re-verification pass (\`hs revalidate\`, [[HS-R5LF-36QL-EPIC-stamped-work-is-re-verified-on-a]]).`,
|
|
2378
|
+
"",
|
|
2379
|
+
"## Failing checks",
|
|
2380
|
+
"",
|
|
2381
|
+
"| AC | command | detail |",
|
|
2382
|
+
"|---|---|---|"
|
|
2383
|
+
];
|
|
2384
|
+
for (const failure of result.failures) {
|
|
2385
|
+
lines.push(
|
|
2386
|
+
`| AC${failure.ac} | \`${tableCell(failure.run)}\` | ${tableCell(failure.message)} |`
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
lines.push(
|
|
2390
|
+
"",
|
|
2391
|
+
"## Reproduction",
|
|
2392
|
+
"",
|
|
2393
|
+
"```",
|
|
2394
|
+
...commands,
|
|
2395
|
+
"```",
|
|
2396
|
+
"",
|
|
2397
|
+
"## Root cause",
|
|
2398
|
+
"",
|
|
2399
|
+
"Unknown until triaged. The HS-PEAG sweep found **zero** reds caused by broken shipped code \u2014 every one was a stale *check* \u2014 so start by asking whether the check still describes the work, not whether the work regressed.",
|
|
2400
|
+
"",
|
|
2401
|
+
"## Acceptance criteria",
|
|
2402
|
+
"",
|
|
2403
|
+
`1. Every command above exits 0, or the frozen check is corrected with a recorded reason.`,
|
|
2404
|
+
`2. \`hs revalidate\` reports ${result.specId} green on the next pass.`
|
|
2405
|
+
);
|
|
2406
|
+
return lines.join("\n");
|
|
2407
|
+
}
|
|
2408
|
+
function reconcileDiagnoses(open, desired) {
|
|
2409
|
+
const openBySubject = /* @__PURE__ */ new Map();
|
|
2410
|
+
for (const row of open) {
|
|
2411
|
+
if (!isDiagnoseOpen(row.status)) continue;
|
|
2412
|
+
const list = openBySubject.get(row.subjectSpecId) ?? [];
|
|
2413
|
+
list.push(row);
|
|
2414
|
+
openBySubject.set(row.subjectSpecId, list);
|
|
2415
|
+
}
|
|
2416
|
+
for (const list of openBySubject.values()) {
|
|
2417
|
+
list.sort((a, b) => a.diagnoseId.localeCompare(b.diagnoseId));
|
|
2418
|
+
}
|
|
2419
|
+
const actions = [];
|
|
2420
|
+
const desiredSubjects = /* @__PURE__ */ new Set();
|
|
2421
|
+
for (const row of desired) {
|
|
2422
|
+
desiredSubjects.add(row.subjectSpecId);
|
|
2423
|
+
const existing = openBySubject.get(row.subjectSpecId) ?? [];
|
|
2424
|
+
if (existing.length === 0) {
|
|
2425
|
+
actions.push({ kind: "file", subjectSpecId: row.subjectSpecId, row });
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
const [canonical, ...duplicates] = existing;
|
|
2429
|
+
actions.push({
|
|
2430
|
+
kind: "update",
|
|
2431
|
+
subjectSpecId: row.subjectSpecId,
|
|
2432
|
+
diagnoseId: canonical.diagnoseId,
|
|
2433
|
+
row
|
|
2434
|
+
});
|
|
2435
|
+
for (const duplicate of duplicates) {
|
|
2436
|
+
actions.push({
|
|
2437
|
+
kind: "close",
|
|
2438
|
+
subjectSpecId: row.subjectSpecId,
|
|
2439
|
+
diagnoseId: duplicate.diagnoseId,
|
|
2440
|
+
reason: `duplicate decay row \u2014 ${canonical.diagnoseId} is the open row for ${row.subjectSpecId}`
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const [subjectSpecId, rows] of openBySubject) {
|
|
2445
|
+
if (desiredSubjects.has(subjectSpecId)) continue;
|
|
2446
|
+
for (const row of rows) {
|
|
2447
|
+
actions.push({
|
|
2448
|
+
kind: "close",
|
|
2449
|
+
subjectSpecId,
|
|
2450
|
+
diagnoseId: row.diagnoseId,
|
|
2451
|
+
reason: `${subjectSpecId} is green \u2014 its frozen checks pass again`
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return actions;
|
|
2456
|
+
}
|
|
2457
|
+
function scopeOpenDiagnoses(rows, onlySpecIds) {
|
|
2458
|
+
if (onlySpecIds.length === 0) return [...rows];
|
|
2459
|
+
const scope = new Set(onlySpecIds);
|
|
2460
|
+
return rows.filter((row) => scope.has(row.subjectSpecId));
|
|
2461
|
+
}
|
|
2462
|
+
function makeDiagnoseWriter(deps) {
|
|
2463
|
+
const call = async (method, apiPath, body) => {
|
|
2464
|
+
const res = await controlRequest(deps, { method, path: apiPath, ...body ? { body } : {} });
|
|
2465
|
+
if (!res.ok) return { ok: false, error: res.error.message };
|
|
2466
|
+
if (res.status >= 300) {
|
|
2467
|
+
const detail = res.body ?? null;
|
|
2468
|
+
const reason = [detail?.error, detail?.message].filter((v) => typeof v === "string").join(": ");
|
|
2469
|
+
return { ok: false, error: `console returned ${res.status}${reason ? ` \u2014 ${reason}` : ""}` };
|
|
2470
|
+
}
|
|
2471
|
+
return { ok: true, body: res.body };
|
|
2472
|
+
};
|
|
2473
|
+
return {
|
|
2474
|
+
async fileDiagnose(row) {
|
|
2475
|
+
const res = await call("POST", "/api/specs", {
|
|
2476
|
+
type: "diagnose",
|
|
2477
|
+
name: row.name,
|
|
2478
|
+
summary: row.summary,
|
|
2479
|
+
frontmatter: { severity: row.severity, tags: row.tags, related: row.related },
|
|
2480
|
+
body: row.body
|
|
2481
|
+
});
|
|
2482
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
2483
|
+
const spec = res.body?.spec;
|
|
2484
|
+
if (!spec?.id) return { ok: false, error: "console accepted the create but returned no id" };
|
|
2485
|
+
return { ok: true, diagnoseId: spec.id };
|
|
2486
|
+
},
|
|
2487
|
+
async updateDiagnose(diagnoseId, row) {
|
|
2488
|
+
const current = await call("GET", `/api/specs/${encodeURIComponent(diagnoseId)}`);
|
|
2489
|
+
if (!current.ok) return { ok: false, error: current.error };
|
|
2490
|
+
const existing = current.body?.spec?.frontmatter ?? {};
|
|
2491
|
+
const res = await call("PUT", `/api/specs/${encodeURIComponent(diagnoseId)}`, {
|
|
2492
|
+
frontmatter: { ...existing, summary: row.summary },
|
|
2493
|
+
body: row.body
|
|
2494
|
+
});
|
|
2495
|
+
return { ok: res.ok, error: res.error };
|
|
2496
|
+
},
|
|
2497
|
+
async closeDiagnose(diagnoseId, reason) {
|
|
2498
|
+
const res = await call("PATCH", `/api/specs/${encodeURIComponent(diagnoseId)}/status`, {
|
|
2499
|
+
status: "closed",
|
|
2500
|
+
close_reason: reason
|
|
2501
|
+
});
|
|
2502
|
+
return { ok: res.ok, error: res.error };
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
async function applyReconciliation(actions, writer) {
|
|
2507
|
+
const applied = [];
|
|
2508
|
+
for (const action of actions) {
|
|
2509
|
+
try {
|
|
2510
|
+
if (action.kind === "file") {
|
|
2511
|
+
const res = await writer.fileDiagnose(action.row);
|
|
2512
|
+
applied.push({ action, ok: res.ok, diagnoseId: res.diagnoseId, error: res.error });
|
|
2513
|
+
} else if (action.kind === "update") {
|
|
2514
|
+
const res = await writer.updateDiagnose(action.diagnoseId, action.row);
|
|
2515
|
+
applied.push({ action, ok: res.ok, diagnoseId: action.diagnoseId, error: res.error });
|
|
2516
|
+
} else {
|
|
2517
|
+
const res = await writer.closeDiagnose(action.diagnoseId, action.reason);
|
|
2518
|
+
applied.push({ action, ok: res.ok, diagnoseId: action.diagnoseId, error: res.error });
|
|
2519
|
+
}
|
|
2520
|
+
} catch (err) {
|
|
2521
|
+
applied.push({
|
|
2522
|
+
action,
|
|
2523
|
+
ok: false,
|
|
2524
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
return applied;
|
|
2529
|
+
}
|
|
2530
|
+
async function buildSweepReport(input) {
|
|
2531
|
+
const noWinByKey = /* @__PURE__ */ new Map();
|
|
2532
|
+
for (const command of input.plan.commands) {
|
|
2533
|
+
if (command.noWin) noWinByKey.set(command.key, command.noWin);
|
|
2534
|
+
}
|
|
2535
|
+
const deps = makeReplayDeps(input.outcomes);
|
|
2536
|
+
const results = [];
|
|
2537
|
+
for (const entry of input.loaded) {
|
|
2538
|
+
const report = entry.manifest ? await runChecksManifest(entry.manifest, deps) : void 0;
|
|
2539
|
+
results.push(scoreSpec(entry, report, noWinByKey, input.parallelismArtifacts));
|
|
2540
|
+
}
|
|
2541
|
+
const unparsable = results.filter((r) => r.verdict === "unparsable").map((r) => ({ specId: r.specId, error: r.error ?? "unknown" }));
|
|
2542
|
+
const coverage = computeCoverage({
|
|
2543
|
+
stamped: input.loaded.length + input.uncovered.length,
|
|
2544
|
+
checkable: results.filter((r) => r.verdict !== "unparsable").length,
|
|
2545
|
+
uncovered: input.uncovered.length,
|
|
2546
|
+
unparsable: unparsable.length
|
|
2547
|
+
});
|
|
2548
|
+
const artifacts = input.plan.commands.filter((c) => input.parallelismArtifacts.has(c.key)).map((c) => ({ run: c.run, specIds: [...new Set(c.uses.map((u) => u.specId))].sort() }));
|
|
2549
|
+
const reconciliation = reconcileDiagnoses(input.openDiagnoses, desiredDiagnoseRows(results));
|
|
2550
|
+
return {
|
|
2551
|
+
startedAt: input.startedAt,
|
|
2552
|
+
durationMs: input.durationMs,
|
|
2553
|
+
coverage,
|
|
2554
|
+
checkInstances: input.plan.checkInstances,
|
|
2555
|
+
distinctCommands: input.plan.commands.length,
|
|
2556
|
+
results,
|
|
2557
|
+
noWin: [...input.noWinItems],
|
|
2558
|
+
parallelismArtifacts: artifacts,
|
|
2559
|
+
sideEffects: [...input.sideEffects],
|
|
2560
|
+
unparsable,
|
|
2561
|
+
cwdResolution: input.plan.commands.map((c) => ({
|
|
2562
|
+
run: c.run,
|
|
2563
|
+
declared: c.cwd,
|
|
2564
|
+
resolved: input.resolvedCwds.get(c.key) ?? "(not run)"
|
|
2565
|
+
})),
|
|
2566
|
+
reconciliation,
|
|
2567
|
+
applied: null,
|
|
2568
|
+
redCount: results.filter((r) => r.verdict === "fail").length,
|
|
2569
|
+
greenCount: results.filter((r) => r.verdict === "pass").length
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
function sweepExitCode(report) {
|
|
2573
|
+
return report.redCount > 0 ? REVALIDATE_EXIT.DECAY_FOUND : REVALIDATE_EXIT.OK;
|
|
2574
|
+
}
|
|
2575
|
+
function formatSweepReport(report) {
|
|
2576
|
+
const lines = [];
|
|
2577
|
+
const { coverage } = report;
|
|
2578
|
+
lines.push(
|
|
2579
|
+
`Re-verification pass \u2014 ${report.startedAt} (${Math.round(report.durationMs / 1e3)}s)`
|
|
2580
|
+
);
|
|
2581
|
+
lines.push("");
|
|
2582
|
+
lines.push(
|
|
2583
|
+
`Coverage: ${coverage.checkable}/${coverage.stamped} stamped specs checkable (${coverage.percent}%) \xB7 ${coverage.uncovered} have no manifest` + (coverage.unparsable > 0 ? ` \xB7 ${coverage.unparsable} manifest unusable` : "")
|
|
2584
|
+
);
|
|
2585
|
+
lines.push(
|
|
2586
|
+
`Commands: ${report.checkInstances} check instances \u2192 ${report.distinctCommands} distinct, each run once`
|
|
2587
|
+
);
|
|
2588
|
+
const declared = report.cwdResolution.filter((c) => c.declared !== null).length;
|
|
2589
|
+
lines.push(
|
|
2590
|
+
`Working dirs: ${declared} declared a cwd \xB7 ${report.cwdResolution.length - declared} resolved by trying both roots`
|
|
2591
|
+
);
|
|
2592
|
+
lines.push(
|
|
2593
|
+
`Verdict: ${report.redCount} red \xB7 ${report.greenCount} green \xB7 ${report.results.filter((r) => r.verdict === "no-win").length} no-win`
|
|
2594
|
+
);
|
|
2595
|
+
const red = report.results.filter((r) => r.verdict === "fail");
|
|
2596
|
+
if (red.length > 0) {
|
|
2597
|
+
lines.push("");
|
|
2598
|
+
lines.push("RED \u2014 the stamp no longer holds:");
|
|
2599
|
+
for (const result of red) {
|
|
2600
|
+
lines.push(` ${result.specId} (${result.status}) \u2014 ${result.title}`);
|
|
2601
|
+
for (const failure of result.failures) {
|
|
2602
|
+
lines.push(` AC${failure.ac} ${failure.run}`);
|
|
2603
|
+
lines.push(` ${failure.message.split("\n")[0].slice(0, 160)}`);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
if (report.noWin.length > 0) {
|
|
2608
|
+
lines.push("");
|
|
2609
|
+
lines.push("NO-WIN \u2014 cannot pass for reasons the spec does not own (not filed):");
|
|
2610
|
+
for (const item of report.noWin) {
|
|
2611
|
+
const seen = item.firstSeen ? "new" : `first reported ${item.firstReportedAt}`;
|
|
2612
|
+
lines.push(` [${item.reason}] ${item.run} (${seen})`);
|
|
2613
|
+
lines.push(` ${item.detail}`);
|
|
2614
|
+
lines.push(` affects: ${item.specIds.join(", ")}`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (report.parallelismArtifacts.length > 0) {
|
|
2618
|
+
lines.push("");
|
|
2619
|
+
lines.push("PARALLELISM ARTIFACTS \u2014 red under fan-out, green on the serial re-run:");
|
|
2620
|
+
for (const artifact of report.parallelismArtifacts) {
|
|
2621
|
+
lines.push(` ${artifact.run} (would have false-reported ${artifact.specIds.join(", ")})`);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
if (report.unparsable.length > 0) {
|
|
2625
|
+
lines.push("");
|
|
2626
|
+
lines.push("UNUSABLE MANIFESTS \u2014 present but the parser rejected them:");
|
|
2627
|
+
for (const item of report.unparsable) lines.push(` ${item.specId}: ${item.error}`);
|
|
2628
|
+
}
|
|
2629
|
+
if (report.sideEffects.length > 0) {
|
|
2630
|
+
lines.push("");
|
|
2631
|
+
lines.push(
|
|
2632
|
+
"SIDE EFFECTS \u2014 files that changed while the pass ran (nothing was reverted;",
|
|
2633
|
+
" attribution is by timing, so ambient writes can appear here):"
|
|
2634
|
+
);
|
|
2635
|
+
for (const effect of report.sideEffects) {
|
|
2636
|
+
const note = effect.contentIdentical ? " (content identical \u2014 line endings only)" : "";
|
|
2637
|
+
lines.push(` ${effect.change} ${effect.path}${note}`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
lines.push("");
|
|
2641
|
+
const verb = report.applied ? "applied" : "planned (dry run \u2014 pass --apply to execute)";
|
|
2642
|
+
lines.push(`Resolution lane: ${report.reconciliation.length} action(s) ${verb}`);
|
|
2643
|
+
for (const action of report.reconciliation) {
|
|
2644
|
+
const target = action.kind === "file" ? action.subjectSpecId : action.diagnoseId;
|
|
2645
|
+
lines.push(` ${action.kind} ${target}`);
|
|
2646
|
+
}
|
|
2647
|
+
return lines.join("\n");
|
|
2648
|
+
}
|
|
2649
|
+
function sweepReportToJson(report) {
|
|
2650
|
+
return {
|
|
2651
|
+
ok: report.redCount === 0,
|
|
2652
|
+
startedAt: report.startedAt,
|
|
2653
|
+
durationMs: report.durationMs,
|
|
2654
|
+
coverage: report.coverage,
|
|
2655
|
+
checkInstances: report.checkInstances,
|
|
2656
|
+
distinctCommands: report.distinctCommands,
|
|
2657
|
+
red: report.redCount,
|
|
2658
|
+
green: report.greenCount,
|
|
2659
|
+
noWinSpecs: report.results.filter((r) => r.verdict === "no-win").length,
|
|
2660
|
+
results: report.results.map((r) => ({
|
|
2661
|
+
spec: r.specId,
|
|
2662
|
+
status: r.status,
|
|
2663
|
+
verdict: r.verdict,
|
|
2664
|
+
failures: r.failures,
|
|
2665
|
+
noWin: r.noWin,
|
|
2666
|
+
...r.error ? { error: r.error } : {}
|
|
2667
|
+
})),
|
|
2668
|
+
noWin: report.noWin,
|
|
2669
|
+
parallelismArtifacts: report.parallelismArtifacts,
|
|
2670
|
+
sideEffects: report.sideEffects,
|
|
2671
|
+
unparsable: report.unparsable,
|
|
2672
|
+
cwdResolution: report.cwdResolution,
|
|
2673
|
+
reconciliation: report.reconciliation.map((a) => ({
|
|
2674
|
+
kind: a.kind,
|
|
2675
|
+
spec: a.subjectSpecId,
|
|
2676
|
+
...a.kind === "file" ? {} : { diagnose: a.diagnoseId },
|
|
2677
|
+
...a.kind === "close" ? { reason: a.reason } : {}
|
|
2678
|
+
})),
|
|
2679
|
+
applied: report.applied
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
function parseRevalidateArgs(argv) {
|
|
2683
|
+
const args = {
|
|
2684
|
+
json: false,
|
|
2685
|
+
apply: false,
|
|
2686
|
+
concurrency: 3,
|
|
2687
|
+
minIntervalHours: null,
|
|
2688
|
+
only: [],
|
|
2689
|
+
planOnly: false,
|
|
2690
|
+
timeoutSeconds: 420
|
|
2691
|
+
};
|
|
2692
|
+
const numeric = (raw, flag) => {
|
|
2693
|
+
if (raw === void 0) return { error: `${flag} requires a value` };
|
|
2694
|
+
const value = Number(raw);
|
|
2695
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
2696
|
+
return { error: `${flag} must be a positive number` };
|
|
2697
|
+
return value;
|
|
2698
|
+
};
|
|
2699
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2700
|
+
const arg = argv[i];
|
|
2701
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
2702
|
+
else if (arg === "--json") args.json = true;
|
|
2703
|
+
else if (arg === "--apply") args.apply = true;
|
|
2704
|
+
else if (arg === "--plan-only") args.planOnly = true;
|
|
2705
|
+
else if (arg === "--concurrency" || arg.startsWith("--concurrency=")) {
|
|
2706
|
+
const raw = arg.includes("=") ? arg.split("=")[1] : argv[++i];
|
|
2707
|
+
const value = numeric(raw, "--concurrency");
|
|
2708
|
+
if (typeof value !== "number")
|
|
2709
|
+
return { error: { code: REVALIDATE_EXIT.USAGE, message: value.error } };
|
|
2710
|
+
args.concurrency = Math.floor(value);
|
|
2711
|
+
} else if (arg === "--timeout" || arg.startsWith("--timeout=")) {
|
|
2712
|
+
const raw = arg.includes("=") ? arg.split("=")[1] : argv[++i];
|
|
2713
|
+
const value = numeric(raw, "--timeout");
|
|
2714
|
+
if (typeof value !== "number")
|
|
2715
|
+
return { error: { code: REVALIDATE_EXIT.USAGE, message: value.error } };
|
|
2716
|
+
args.timeoutSeconds = Math.floor(value);
|
|
2717
|
+
} else if (arg === "--if-older-than" || arg.startsWith("--if-older-than=")) {
|
|
2718
|
+
const raw = arg.includes("=") ? arg.split("=")[1] : argv[++i];
|
|
2719
|
+
const value = numeric(raw, "--if-older-than");
|
|
2720
|
+
if (typeof value !== "number")
|
|
2721
|
+
return { error: { code: REVALIDATE_EXIT.USAGE, message: value.error } };
|
|
2722
|
+
args.minIntervalHours = value;
|
|
2723
|
+
} else if (arg === "--spec" || arg.startsWith("--spec=")) {
|
|
2724
|
+
const raw = arg.includes("=") ? arg.split("=")[1] : argv[++i];
|
|
2725
|
+
if (raw === void 0) {
|
|
2726
|
+
return { error: { code: REVALIDATE_EXIT.USAGE, message: "--spec requires a value" } };
|
|
2727
|
+
}
|
|
2728
|
+
args.only.push(
|
|
2729
|
+
...raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
2730
|
+
);
|
|
2731
|
+
} else if (arg.startsWith("-")) {
|
|
2732
|
+
return { error: { code: REVALIDATE_EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
2733
|
+
} else {
|
|
2734
|
+
return { error: { code: REVALIDATE_EXIT.USAGE, message: `unexpected argument: ${arg}` } };
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
return { args };
|
|
2738
|
+
}
|
|
2739
|
+
var REVALIDATE_HELP = [
|
|
2740
|
+
"Usage:",
|
|
2741
|
+
" hs revalidate [--json] [--apply] [--spec <id>] [--concurrency N] [--timeout S]",
|
|
2742
|
+
" [--if-older-than H]",
|
|
2743
|
+
"",
|
|
2744
|
+
"Re-run the FROZEN checks of work already stamped `review_pending`/`in_review`",
|
|
2745
|
+
"and report what has decayed since the stamp was earned",
|
|
2746
|
+
"([[HS-R5LF-36QL-EPIC-stamped-work-is-re-verified-on-a]]).",
|
|
2747
|
+
"",
|
|
2748
|
+
"A stamp says 'these commands passed THEN'. Nothing else re-asserts it: a sweep",
|
|
2749
|
+
"of 94 stamped tasks found 19 red and zero broken features \u2014 every red a stale",
|
|
2750
|
+
"check, one of them red within a day of being audited clean by hand.",
|
|
2751
|
+
"",
|
|
2752
|
+
"The pass is read-only with respect to the repo: no git writes, no spec status",
|
|
2753
|
+
"changes. Files a check mutates as a side effect are detected and named, never",
|
|
2754
|
+
"reverted. The Resolution plan is printed and NOT executed unless --apply.",
|
|
2755
|
+
"",
|
|
2756
|
+
"Flags:",
|
|
2757
|
+
" --json Machine-readable report.",
|
|
2758
|
+
" --apply Execute the Resolution plan (file/update/close diagnose rows).",
|
|
2759
|
+
" --plan-only Enumerate, dedupe and report coverage without running anything.",
|
|
2760
|
+
" --spec <id[,id]> Sweep only these specs.",
|
|
2761
|
+
" --concurrency N Commands in flight during the fan-out pass (default 3).",
|
|
2762
|
+
" Re-confirmation of every red is always serial.",
|
|
2763
|
+
" --timeout S Per-command timeout in seconds (default 420).",
|
|
2764
|
+
" --if-older-than H No-op unless the last pass was more than H hours ago \u2014",
|
|
2765
|
+
" lets a frequent timer drive a daily sweep.",
|
|
2766
|
+
" --help, -h Show this help.",
|
|
2767
|
+
"",
|
|
2768
|
+
"Exit codes:",
|
|
2769
|
+
" 0 clean \xB7 1 usage \xB7 10 nothing to sweep \xB7 20 decay found"
|
|
2770
|
+
].join("\n");
|
|
2771
|
+
|
|
2772
|
+
// spex-console/src/lib/vault-frontmatter.ts
|
|
2773
|
+
var import_gray_matter2 = __toESM(require_gray_matter(), 1);
|
|
2774
|
+
|
|
2775
|
+
// spex-console/src/lib/safe-yaml.ts
|
|
2776
|
+
var import_gray_matter = __toESM(require_gray_matter(), 1);
|
|
2777
|
+
import yaml2 from "js-yaml";
|
|
2778
|
+
var SAFE_YAML_SCHEMA = yaml2.DEFAULT_SAFE_SCHEMA ?? yaml2.DEFAULT_SCHEMA;
|
|
2779
|
+
var grayMatterYamlStringify = (() => {
|
|
2780
|
+
const engine = import_gray_matter.default.engines?.yaml;
|
|
2781
|
+
return typeof engine?.stringify === "function" ? engine.stringify : void 0;
|
|
2782
|
+
})();
|
|
2783
|
+
var YAML_FAILSAFE_SCHEMA = yaml2.FAILSAFE_SCHEMA;
|
|
2784
|
+
|
|
2785
|
+
// spex-console/src/lib/vault-frontmatter.ts
|
|
2786
|
+
function parseSpecMarkdown(raw) {
|
|
2787
|
+
return (0, import_gray_matter2.default)(raw);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// spex-console/bin/run-entry.ts
|
|
2791
|
+
import process2 from "node:process";
|
|
2792
|
+
var BinExit = class extends Error {
|
|
2793
|
+
code;
|
|
2794
|
+
constructor(code) {
|
|
2795
|
+
super(`exit ${code}`);
|
|
2796
|
+
this.name = "BinExit";
|
|
2797
|
+
this.code = code;
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
function runEntry(name, main2, options2 = {}) {
|
|
2801
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options2;
|
|
2802
|
+
void (async () => {
|
|
2803
|
+
try {
|
|
2804
|
+
await main2();
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
if (err instanceof BinExit) {
|
|
2807
|
+
process2.exitCode = err.code;
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2811
|
+
process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
2812
|
+
`);
|
|
2813
|
+
process2.exitCode = fallbackCode;
|
|
2814
|
+
}
|
|
2815
|
+
})();
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// spex-console/bin/hs-revalidate.ts
|
|
2819
|
+
var STATE_RELATIVE_PATH = path3.join(".tmp", "revalidate-state.json");
|
|
2820
|
+
function specFiles(specsDir) {
|
|
2821
|
+
const out = [];
|
|
2822
|
+
const walk = (dir) => {
|
|
2823
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2824
|
+
const abs = path3.join(dir, entry.name);
|
|
2825
|
+
if (entry.isDirectory()) walk(abs);
|
|
2826
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) out.push(abs);
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
if (existsSync6(specsDir)) walk(specsDir);
|
|
2830
|
+
return out;
|
|
2831
|
+
}
|
|
2832
|
+
function readStampedSpecs(specsDir) {
|
|
2833
|
+
const specs = [];
|
|
2834
|
+
for (const file of specFiles(specsDir)) {
|
|
2835
|
+
let raw;
|
|
2836
|
+
try {
|
|
2837
|
+
raw = readFileSync10(file, "utf-8");
|
|
2838
|
+
} catch {
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
const head = raw.slice(0, 4e3);
|
|
2842
|
+
if (!/status:\s*['"]?(review_pending|in_review)\b/.test(head)) continue;
|
|
2843
|
+
let data;
|
|
2844
|
+
try {
|
|
2845
|
+
data = parseSpecMarkdown(raw).data ?? {};
|
|
2846
|
+
} catch {
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
const id = typeof data.id === "string" ? data.id : null;
|
|
2850
|
+
const status = typeof data.status === "string" ? data.status : "";
|
|
2851
|
+
if (!id || !STAMPED_STATUSES.has(status)) continue;
|
|
2852
|
+
specs.push({
|
|
2853
|
+
id,
|
|
2854
|
+
status,
|
|
2855
|
+
title: typeof data.name === "string" ? data.name : id
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
return specs.sort((a, b) => a.id.localeCompare(b.id));
|
|
2859
|
+
}
|
|
2860
|
+
function readOpenDecayDiagnoses(specsDir) {
|
|
2861
|
+
const dir = path3.join(specsDir, "diagnoses");
|
|
2862
|
+
if (!existsSync6(dir)) return [];
|
|
2863
|
+
const rows = [];
|
|
2864
|
+
for (const entry of readdirSync2(dir)) {
|
|
2865
|
+
if (!entry.endsWith(".md")) continue;
|
|
2866
|
+
let data;
|
|
2867
|
+
try {
|
|
2868
|
+
data = parseSpecMarkdown(readFileSync10(path3.join(dir, entry), "utf-8")).data ?? {};
|
|
2869
|
+
} catch {
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
const tags = Array.isArray(data.tags) ? data.tags.map(String) : [];
|
|
2873
|
+
if (!tags.includes(DECAY_TAG)) continue;
|
|
2874
|
+
const diagnoseId = typeof data.id === "string" ? data.id : null;
|
|
2875
|
+
const status = typeof data.status === "string" ? data.status : "open";
|
|
2876
|
+
if (!diagnoseId || !isDiagnoseOpen(status)) continue;
|
|
2877
|
+
const related = Array.isArray(data.related) ? data.related.map(String) : [];
|
|
2878
|
+
const subject = related.map((ref) => /^\[\[([^\]|#]+)/.exec(ref.trim())?.[1]?.trim()).find((ref) => Boolean(ref));
|
|
2879
|
+
if (!subject) continue;
|
|
2880
|
+
rows.push({ diagnoseId, subjectSpecId: subject.split("-").slice(0, 2).join("-"), status });
|
|
2881
|
+
}
|
|
2882
|
+
return rows;
|
|
2883
|
+
}
|
|
2884
|
+
var sha = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
2885
|
+
var normalize = (text) => text.replace(/\r\n/g, "\n");
|
|
2886
|
+
function dirtyPaths(repoRoot) {
|
|
2887
|
+
const res = spawnSync("git", ["status", "--porcelain"], {
|
|
2888
|
+
cwd: repoRoot,
|
|
2889
|
+
encoding: "utf-8",
|
|
2890
|
+
windowsHide: true,
|
|
2891
|
+
maxBuffer: 32 * 1024 * 1024
|
|
2892
|
+
});
|
|
2893
|
+
if (res.status !== 0 || typeof res.stdout !== "string") return [];
|
|
2894
|
+
return res.stdout.split(/\r?\n/).map((line) => line.slice(3).trim()).filter(Boolean).map((entry) => entry.includes(" -> ") ? entry.split(" -> ")[1] : entry).map((entry) => entry.replace(/^"(.*)"$/, "$1"));
|
|
2895
|
+
}
|
|
2896
|
+
function hashFile(repoRoot, rel) {
|
|
2897
|
+
const abs = path3.join(repoRoot, rel);
|
|
2898
|
+
try {
|
|
2899
|
+
if (!existsSync6(abs) || !statSync4(abs).isFile()) return null;
|
|
2900
|
+
const raw = readFileSync10(abs);
|
|
2901
|
+
return { path: rel, sha: sha(raw), normalizedSha: sha(normalize(raw.toString("utf-8"))) };
|
|
2902
|
+
} catch {
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function hashFromHead(repoRoot, rel) {
|
|
2907
|
+
const res = spawnSync("git", ["show", `HEAD:${rel}`], {
|
|
2908
|
+
cwd: repoRoot,
|
|
2909
|
+
encoding: "buffer",
|
|
2910
|
+
windowsHide: true,
|
|
2911
|
+
maxBuffer: 32 * 1024 * 1024
|
|
2912
|
+
});
|
|
2913
|
+
if (res.status !== 0 || !res.stdout) return null;
|
|
2914
|
+
const raw = res.stdout;
|
|
2915
|
+
return { path: rel, sha: sha(raw), normalizedSha: sha(normalize(raw.toString("utf-8"))) };
|
|
2916
|
+
}
|
|
2917
|
+
var VAULT_PATH_PREFIX = "vault:";
|
|
2918
|
+
function snapshotRoots(roots) {
|
|
2919
|
+
const vault = roots.vaultRoot ?? roots.repoRoot;
|
|
2920
|
+
const code = { root: roots.repoRoot, prefix: "" };
|
|
2921
|
+
return vault === roots.repoRoot ? [code] : [code, { root: vault, prefix: VAULT_PATH_PREFIX }];
|
|
2922
|
+
}
|
|
2923
|
+
function snapshot(entry) {
|
|
2924
|
+
const paths = dirtyPaths(entry.root);
|
|
2925
|
+
const files = [];
|
|
2926
|
+
for (const rel of paths) {
|
|
2927
|
+
const file = hashFile(entry.root, rel);
|
|
2928
|
+
if (file) files.push({ ...file, path: `${entry.prefix}${rel}` });
|
|
2929
|
+
}
|
|
2930
|
+
return { paths, files };
|
|
2931
|
+
}
|
|
2932
|
+
var cachedPosixShell;
|
|
2933
|
+
function resolveWindowsPosixShell() {
|
|
2934
|
+
if (cachedPosixShell !== void 0) return cachedPosixShell;
|
|
2935
|
+
const candidates = [
|
|
2936
|
+
process3.env.HEYSPEX_CHECK_SHELL,
|
|
2937
|
+
"C:\\Program Files\\Git\\bin\\bash.exe"
|
|
2938
|
+
].filter((c) => typeof c === "string" && c.length > 0);
|
|
2939
|
+
for (const candidate of candidates) {
|
|
2940
|
+
if (existsSync6(candidate)) return cachedPosixShell = candidate;
|
|
2941
|
+
}
|
|
2942
|
+
const where = spawnSync("where", ["bash"], { encoding: "utf8", windowsHide: true });
|
|
2943
|
+
if (where.status === 0 && where.stdout) {
|
|
2944
|
+
const hit = where.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).find((line) => !/\\system32\\/i.test(line));
|
|
2945
|
+
if (hit && existsSync6(hit)) return cachedPosixShell = hit;
|
|
2946
|
+
}
|
|
2947
|
+
return cachedPosixShell = null;
|
|
2948
|
+
}
|
|
2949
|
+
var MISSING_BINARY_RE = /command not found|is not recognized as an internal or external command/i;
|
|
2950
|
+
var countTscErrors = (output) => (output.match(/error TS\d+/g) ?? []).length;
|
|
2951
|
+
function runOnce(run, cwd, timeoutSeconds) {
|
|
2952
|
+
return new Promise((resolve5) => {
|
|
2953
|
+
const started = Date.now();
|
|
2954
|
+
const posixShell = process3.platform === "win32" ? resolveWindowsPosixShell() : null;
|
|
2955
|
+
const child = posixShell ? spawn(posixShell, ["-lc", run], {
|
|
2956
|
+
cwd,
|
|
2957
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2958
|
+
windowsHide: true
|
|
2959
|
+
}) : spawn(run, { shell: true, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2960
|
+
let captured = "";
|
|
2961
|
+
const collect = (chunk) => {
|
|
2962
|
+
if (captured.length < 2e5) captured += chunk.toString();
|
|
2963
|
+
};
|
|
2964
|
+
child.stdout?.on("data", collect);
|
|
2965
|
+
child.stderr?.on("data", collect);
|
|
2966
|
+
let settled = false;
|
|
2967
|
+
const finish = (outcome) => {
|
|
2968
|
+
if (settled) return;
|
|
2969
|
+
settled = true;
|
|
2970
|
+
clearTimeout(timer);
|
|
2971
|
+
resolve5(outcome);
|
|
2972
|
+
};
|
|
2973
|
+
const timer = setTimeout(() => {
|
|
2974
|
+
child.kill();
|
|
2975
|
+
finish({
|
|
2976
|
+
exitCode: 124,
|
|
2977
|
+
durationMs: Date.now() - started,
|
|
2978
|
+
message: `timed out after ${timeoutSeconds}s`
|
|
2979
|
+
});
|
|
2980
|
+
}, timeoutSeconds * 1e3);
|
|
2981
|
+
child.on(
|
|
2982
|
+
"error",
|
|
2983
|
+
(err) => finish({ exitCode: -1, durationMs: Date.now() - started, message: err.message })
|
|
2984
|
+
);
|
|
2985
|
+
child.on("close", (code) => {
|
|
2986
|
+
const durationMs = Date.now() - started;
|
|
2987
|
+
let exitCode = code ?? -1;
|
|
2988
|
+
const missingBinary = exitCode === 0 && MISSING_BINARY_RE.test(captured);
|
|
2989
|
+
if (missingBinary) exitCode = 127;
|
|
2990
|
+
const trimmed = captured.trim();
|
|
2991
|
+
finish({
|
|
2992
|
+
exitCode,
|
|
2993
|
+
durationMs,
|
|
2994
|
+
errorCount: countTscErrors(captured),
|
|
2995
|
+
// HS-IRCO: feed the same zero-match detection `hs check run` uses —
|
|
2996
|
+
// this sweep re-verifies the same frozen manifests through the same
|
|
2997
|
+
// shared `evaluateCheck`, so a "Passed: 0" / "No test files found"
|
|
2998
|
+
// false-green must be caught here too, not only on first verify.
|
|
2999
|
+
rawOutput: captured,
|
|
3000
|
+
...exitCode === 0 ? {} : {
|
|
3001
|
+
message: missingBinary ? "errored, not ok \u2014 shell reported a missing binary" : trimmed.split("\n").slice(-3).join(" ").slice(0, 400) || `command exited ${exitCode}`
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
});
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
async function runCommand(command, roots, timeoutSeconds) {
|
|
3008
|
+
const probe = command.uses[0].check;
|
|
3009
|
+
const attempts = [];
|
|
3010
|
+
for (const cwd of resolveCheckCwds(probe, roots)) {
|
|
3011
|
+
const outcome = await runOnce(command.run, cwd, timeoutSeconds);
|
|
3012
|
+
attempts.push({ cwd, outcome });
|
|
3013
|
+
if (outcome.exitCode === 0) break;
|
|
3014
|
+
}
|
|
3015
|
+
return pickAttempt(attempts);
|
|
3016
|
+
}
|
|
3017
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
3018
|
+
const results = new Array(items.length);
|
|
3019
|
+
let next = 0;
|
|
3020
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
3021
|
+
for (; ; ) {
|
|
3022
|
+
const index = next++;
|
|
3023
|
+
if (index >= items.length) return;
|
|
3024
|
+
results[index] = await fn(items[index], index);
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
await Promise.all(workers);
|
|
3028
|
+
return results;
|
|
3029
|
+
}
|
|
3030
|
+
function readState(statePath) {
|
|
3031
|
+
try {
|
|
3032
|
+
const parsed = JSON.parse(readFileSync10(statePath, "utf-8"));
|
|
3033
|
+
return {
|
|
3034
|
+
lastRunAt: typeof parsed.lastRunAt === "string" ? parsed.lastRunAt : null,
|
|
3035
|
+
noWin: typeof parsed.noWin === "object" && parsed.noWin !== null ? parsed.noWin : {}
|
|
3036
|
+
};
|
|
3037
|
+
} catch {
|
|
3038
|
+
return EMPTY_SWEEP_STATE;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
function writeState(statePath, state) {
|
|
3042
|
+
try {
|
|
3043
|
+
mkdirSync6(path3.dirname(statePath), { recursive: true });
|
|
3044
|
+
writeFileSync6(statePath, `${JSON.stringify(state, null, 2)}
|
|
3045
|
+
`, "utf-8");
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
process3.stderr.write(
|
|
3048
|
+
`hs revalidate: could not persist state (${err instanceof Error ? err.message : String(err)}).
|
|
3049
|
+
`
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
async function main() {
|
|
3054
|
+
const vaultRoot = process3.env.HS_VAULT_ROOT;
|
|
3055
|
+
if (!vaultRoot) {
|
|
3056
|
+
process3.stderr.write(
|
|
3057
|
+
"hs revalidate: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
|
|
3058
|
+
);
|
|
3059
|
+
process3.exitCode = REVALIDATE_EXIT.USAGE;
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
const parsed = parseRevalidateArgs(process3.argv.slice(3));
|
|
3063
|
+
if (parsed.help) {
|
|
3064
|
+
process3.stdout.write(`${REVALIDATE_HELP}
|
|
3065
|
+
`);
|
|
3066
|
+
process3.exitCode = REVALIDATE_EXIT.OK;
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
if (parsed.error) {
|
|
3070
|
+
process3.stderr.write(`hs revalidate: ${parsed.error.message}
|
|
3071
|
+
`);
|
|
3072
|
+
process3.exitCode = parsed.error.code;
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
const args = parsed.args;
|
|
3076
|
+
const codeRoot = resolveCodeRoot({ vaultRoot });
|
|
3077
|
+
const roots = {
|
|
3078
|
+
repoRoot: codeRoot,
|
|
3079
|
+
consoleRoot: path3.join(codeRoot, "spex-console"),
|
|
3080
|
+
vaultRoot
|
|
3081
|
+
};
|
|
3082
|
+
const specsDir = path3.join(vaultRoot, "specs");
|
|
3083
|
+
const statePath = path3.join(vaultRoot, STATE_RELATIVE_PATH);
|
|
3084
|
+
const state = readState(statePath);
|
|
3085
|
+
if (args.minIntervalHours !== null) {
|
|
3086
|
+
const gate = shouldRunNow(state, /* @__PURE__ */ new Date(), args.minIntervalHours);
|
|
3087
|
+
if (!gate.run) {
|
|
3088
|
+
process3.stdout.write(
|
|
3089
|
+
`hs revalidate: last pass was ${gate.hoursSinceLast?.toFixed(1)}h ago (< ${args.minIntervalHours}h) \u2014 skipping.
|
|
3090
|
+
`
|
|
3091
|
+
);
|
|
3092
|
+
process3.exitCode = REVALIDATE_EXIT.OK;
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
const only = new Set(args.only);
|
|
3097
|
+
const allStamped = readStampedSpecs(specsDir);
|
|
3098
|
+
const stamped = only.size > 0 ? allStamped.filter((s) => only.has(s.id)) : allStamped;
|
|
3099
|
+
const manifestPath = (id) => path3.join(specsDir, "checks", `${id}.checks.json`);
|
|
3100
|
+
const checkable = [];
|
|
3101
|
+
const uncovered = [];
|
|
3102
|
+
for (const spec of stamped)
|
|
3103
|
+
(existsSync6(manifestPath(spec.id)) ? checkable : uncovered).push(spec);
|
|
3104
|
+
if (checkable.length === 0) {
|
|
3105
|
+
process3.stderr.write(
|
|
3106
|
+
`hs revalidate: nothing to sweep \u2014 none of the ${stamped.length} stamped spec(s) has a frozen manifest.
|
|
3107
|
+
`
|
|
3108
|
+
);
|
|
3109
|
+
process3.exitCode = REVALIDATE_EXIT.NOT_FOUND;
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
const loaded = checkable.map(
|
|
3113
|
+
(spec) => loadSweepManifest(spec, readFileSync10(manifestPath(spec.id), "utf-8"))
|
|
3114
|
+
);
|
|
3115
|
+
const plan = planCommands(loaded);
|
|
3116
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3117
|
+
const startedMs = Date.now();
|
|
3118
|
+
process3.stderr.write(
|
|
3119
|
+
`hs revalidate: ${checkable.length} stamped spec(s) with a manifest \xB7 ${plan.checkInstances} check instances \u2192 ${plan.commands.length} distinct commands
|
|
3120
|
+
`
|
|
3121
|
+
);
|
|
3122
|
+
if (args.planOnly) {
|
|
3123
|
+
const unusable = loaded.filter((entry) => !entry.manifest);
|
|
3124
|
+
const coverage = computeCoverage({
|
|
3125
|
+
stamped: stamped.length,
|
|
3126
|
+
checkable: loaded.length - unusable.length,
|
|
3127
|
+
uncovered: uncovered.length,
|
|
3128
|
+
unparsable: unusable.length
|
|
3129
|
+
});
|
|
3130
|
+
const noWinCount = plan.commands.filter((c) => c.noWin).length;
|
|
3131
|
+
const payload = {
|
|
3132
|
+
coverage,
|
|
3133
|
+
checkInstances: plan.checkInstances,
|
|
3134
|
+
distinctCommands: plan.commands.length,
|
|
3135
|
+
declaredCwd: plan.commands.filter((c) => c.cwd !== null).length,
|
|
3136
|
+
noWinCommands: noWinCount,
|
|
3137
|
+
unusable: unusable.map((entry) => ({ spec: entry.spec.id, error: entry.error }))
|
|
3138
|
+
};
|
|
3139
|
+
process3.stdout.write(
|
|
3140
|
+
args.json ? `${JSON.stringify(payload, null, 2)}
|
|
3141
|
+
` : [
|
|
3142
|
+
`Plan only \u2014 nothing was executed.`,
|
|
3143
|
+
`Coverage: ${coverage.checkable}/${coverage.stamped} stamped specs checkable (${coverage.percent}%) \xB7 ${coverage.uncovered} have no manifest \xB7 ${coverage.unparsable} manifest unusable`,
|
|
3144
|
+
`Commands: ${plan.checkInstances} check instances \u2192 ${plan.commands.length} distinct`,
|
|
3145
|
+
`Working dirs: ${payload.declaredCwd} declare a cwd \xB7 ${plan.commands.length - payload.declaredCwd} would try both roots`,
|
|
3146
|
+
`no-win commands: ${noWinCount}`,
|
|
3147
|
+
...unusable.map((entry) => ` unusable ${entry.spec.id}: ${entry.error}`)
|
|
3148
|
+
].join("\n") + "\n"
|
|
3149
|
+
);
|
|
3150
|
+
process3.exitCode = REVALIDATE_EXIT.OK;
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
const watched = snapshotRoots(roots);
|
|
3154
|
+
const before = watched.flatMap((entry) => snapshot(entry).files);
|
|
3155
|
+
const attempts = await mapWithConcurrency(
|
|
3156
|
+
plan.commands,
|
|
3157
|
+
args.concurrency,
|
|
3158
|
+
(command) => runCommand(command, roots, args.timeoutSeconds)
|
|
3159
|
+
);
|
|
3160
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
3161
|
+
const resolvedCwds = /* @__PURE__ */ new Map();
|
|
3162
|
+
const parallelismArtifacts = /* @__PURE__ */ new Set();
|
|
3163
|
+
const suspect = plan.commands.map((command, index) => ({ command, attempt: attempts[index] })).filter(({ attempt }) => needsSerialConfirmation(attempt.outcome));
|
|
3164
|
+
if (suspect.length > 0) {
|
|
3165
|
+
process3.stderr.write(
|
|
3166
|
+
`hs revalidate: ${suspect.length} non-zero result(s) \u2014 re-running each serially before believing it
|
|
3167
|
+
`
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const confirmations = /* @__PURE__ */ new Map();
|
|
3171
|
+
for (const { command } of suspect) {
|
|
3172
|
+
confirmations.set(command.key, await runCommand(command, roots, args.timeoutSeconds));
|
|
3173
|
+
}
|
|
3174
|
+
plan.commands.forEach((command, index) => {
|
|
3175
|
+
const parallel = attempts[index];
|
|
3176
|
+
const confirmation = confirmations.get(command.key);
|
|
3177
|
+
const resolved = resolveExecution(parallel.outcome, confirmation?.outcome);
|
|
3178
|
+
outcomes.set(command.key, resolved.outcome);
|
|
3179
|
+
resolvedCwds.set(command.key, (confirmation ?? parallel).cwd);
|
|
3180
|
+
if (resolved.parallelismArtifact) parallelismArtifacts.add(command.key);
|
|
3181
|
+
});
|
|
3182
|
+
const beforePaths = new Set(before.map((f) => f.path));
|
|
3183
|
+
const after = [];
|
|
3184
|
+
for (const entry of watched) {
|
|
3185
|
+
const snap = snapshot(entry);
|
|
3186
|
+
after.push(...snap.files);
|
|
3187
|
+
const afterByPath = new Set(snap.files.map((f) => f.path));
|
|
3188
|
+
for (const rel of snap.paths) {
|
|
3189
|
+
const labelled = `${entry.prefix}${rel}`;
|
|
3190
|
+
if (beforePaths.has(labelled) || !afterByPath.has(labelled)) continue;
|
|
3191
|
+
const head = hashFromHead(entry.root, rel);
|
|
3192
|
+
if (head) before.push({ ...head, path: labelled });
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
const sideEffects = detectSideEffects(before, after);
|
|
3196
|
+
const recorded = recordNoWin(state, plan.commands, startedAt);
|
|
3197
|
+
const report = await buildSweepReport({
|
|
3198
|
+
startedAt,
|
|
3199
|
+
loaded,
|
|
3200
|
+
uncovered,
|
|
3201
|
+
plan,
|
|
3202
|
+
outcomes,
|
|
3203
|
+
parallelismArtifacts,
|
|
3204
|
+
resolvedCwds,
|
|
3205
|
+
noWinItems: recorded.items,
|
|
3206
|
+
sideEffects,
|
|
3207
|
+
// A `--spec`-scoped pass reconciles ONLY the rows it actually re-ran; the
|
|
3208
|
+
// lane's other rows were never checked, so closing them would be a false green.
|
|
3209
|
+
openDiagnoses: scopeOpenDiagnoses(readOpenDecayDiagnoses(specsDir), args.only),
|
|
3210
|
+
durationMs: Date.now() - startedMs
|
|
3211
|
+
});
|
|
3212
|
+
if (args.apply && report.reconciliation.length > 0) {
|
|
3213
|
+
const consoleUrl = resolveSpexConsoleUrl(vaultRoot, (p) => {
|
|
3214
|
+
try {
|
|
3215
|
+
return readFileSync10(p, "utf-8");
|
|
3216
|
+
} catch {
|
|
3217
|
+
return null;
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
3220
|
+
const scoped = resolveControlScopeFromMachine(vaultRoot);
|
|
3221
|
+
if (!scoped.ok) {
|
|
3222
|
+
process3.stderr.write(
|
|
3223
|
+
`hs revalidate: Resolution actions not applied \u2014 ${scoped.error.message}
|
|
3224
|
+
`
|
|
3225
|
+
);
|
|
3226
|
+
} else {
|
|
3227
|
+
report.applied = await applyReconciliation(
|
|
3228
|
+
report.reconciliation,
|
|
3229
|
+
makeDiagnoseWriter({ fetchImpl: fetch, consoleUrl, scope: scoped.scope })
|
|
3230
|
+
);
|
|
3231
|
+
const failed = report.applied.filter((a) => !a.ok);
|
|
3232
|
+
if (failed.length > 0) {
|
|
3233
|
+
process3.stderr.write(
|
|
3234
|
+
`hs revalidate: ${failed.length} Resolution action(s) failed \u2014 ${failed[0].error ?? "unknown"}
|
|
3235
|
+
`
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
const sink = report.redCount === 0 ? process3.stdout : process3.stderr;
|
|
3241
|
+
sink.write(
|
|
3242
|
+
args.json ? `${JSON.stringify(sweepReportToJson(report), null, 2)}
|
|
3243
|
+
` : `${formatSweepReport(report)}
|
|
3244
|
+
`
|
|
3245
|
+
);
|
|
3246
|
+
writeState(statePath, recorded.state);
|
|
3247
|
+
process3.exitCode = sweepExitCode(report);
|
|
3248
|
+
closeIdleConnections();
|
|
3249
|
+
}
|
|
3250
|
+
function closeIdleConnections() {
|
|
3251
|
+
try {
|
|
3252
|
+
const dispatcher = globalThis[Symbol.for("undici.globalDispatcher.1")];
|
|
3253
|
+
void dispatcher?.destroy?.();
|
|
3254
|
+
} catch {
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
runEntry("hs revalidate", main, { fallbackCode: REVALIDATE_EXIT.USAGE });
|