@indigoai-us/hq-cli 5.103.27 → 5.103.28

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.
@@ -0,0 +1,130 @@
1
+ // src/utils/qmd-module-missing-error.ts
2
+ //
3
+ // Classify a qmd failure caused by a MISSING JAVASCRIPT MODULE in qmd's own
4
+ // bundled dependency tree — Node could not load a file qmd needs — as the
5
+ // caller's INCOMPLETE INSTALL, not an hq-cli code defect. hq-cli ships qmd and
6
+ // its dependencies, so a `Cannot find module …` from the qmd child means a
7
+ // partial or interrupted global install (`npm i -g` / `pnpm add -g`) left some
8
+ // files unwritten. That is the caller's install shape, not a bug HQ can fix in
9
+ // code, so the CLI surfaces an actionable reinstall remedy and SKIPS Sentry
10
+ // capture. Sibling of qmd-native-binding-error.ts (HQ-CLI-J, unbuilt
11
+ // better-sqlite3), qmd-collection-missing-error.ts (HQ-CLI-S),
12
+ // qmd-llm-disabled-error.ts (HQ-CLI 7688850003) and qmd-terminated-error.ts
13
+ // (HQ-CLI 7677702704): a failure that is NOT an hq-cli defect is printed with an
14
+ // actionable message and never filed as a crash.
15
+ //
16
+ // HQ-CLI-Y (Sentry 7690356976): a qmd child died with `Cannot find module
17
+ // './stringifyComment.js'`, its `Require stack:` rooted at
18
+ // /usr/lib/node_modules/@indigoai-us/hq-cli/node_modules/yaml/dist/stringify/… .
19
+ // Two compounding defects filed it as a high-priority crash AND minted a NEW
20
+ // permanent issue per child failure: finishRunQmd's generic message
21
+ // interpolated the caller's whole argv (query, -c value, absolute paths) AND
22
+ // embedded the child's RAW multi-line stderr, whose ` at <fn> (<file>:<line>)`
23
+ // lines the Sentry Node SDK then parsed as REAL frames of the hq-cli process —
24
+ // so grouping rode foreign frames and every distinct child failure split into
25
+ // its own issue. Bounding the message (search-index/index.ts) fixes the
26
+ // cardinality; this classifier closes the boundary so the broken-install
27
+ // condition reaches the operator as an actionable reinstall, not an unfixable
28
+ // crash.
29
+ //
30
+ // The gate is deliberately narrow on TWO axes, read ONLY from qmd's OWN captured
31
+ // streams (never the synthesized message — even now that it names the subcommand
32
+ // only, the message must never be the classification substrate), so a user
33
+ // searching for the phrase "Cannot find module" can never trip it:
34
+ // 1. RESOLUTION TOKEN: the module-resolution failure itself — `Cannot find
35
+ // module`, or the CJS/ESM `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` code.
36
+ // 2. CORROBORATOR: a Node module-loader artifact a bare query cannot
37
+ // synthesize — a `Require stack:` header, a `node_modules/` path, or the
38
+ // ESM `imported from` clause. Either axis alone would be too loose.
39
+ // The better-sqlite3 native-binding failure (HQ-CLI-J) carries neither
40
+ // resolution token, so it does not match here and keeps its own narrower remedy
41
+ // — and the top-level handler checks that classifier FIRST regardless of this
42
+ // one, so the more-actionable approve-builds remedy always wins when both could.
43
+ /** The module-resolution failure token. `MODULE_NOT_FOUND` covers the ESM
44
+ * `ERR_MODULE_NOT_FOUND` code as a substring; `Cannot find module` is the CJS
45
+ * loader's message text. */
46
+ const MODULE_RESOLUTION_TOKEN = /cannot find module|MODULE_NOT_FOUND/i;
47
+ /** A Node module-loader artifact a user's free-text query cannot synthesize. */
48
+ const MODULE_LOADER_ARTIFACT = /Require stack:|node_modules[\\/]|imported from/i;
49
+ /** qmd subcommands whose collection/query is chosen by the CALLER (read surface). */
50
+ const CALLER_READS = new Set(["search", "vsearch", "query", "get"]);
51
+ /**
52
+ * The actionable remedy shown to the operator. Input-free — nothing is
53
+ * interpolated from the error, the argv, or qmd's output — so there is no
54
+ * injection surface, matching the bounded-fingerprint discipline of its
55
+ * siblings. Names the concrete reinstall for both a global npm and a global
56
+ * pnpm install.
57
+ *
58
+ * Covers BOTH qmd origins deliberately, because they cannot be told apart at
59
+ * runtime: HQ_QMD_BIN is honoured verbatim, and it is also the seam a dev build
60
+ * (and this repo's e2e harness) uses to inject the bundled qmd, so a truthy
61
+ * HQ_QMD_BIN is NOT a reliable "external qmd" signal. A missing module is not an
62
+ * hq-cli code defect on either path, so the remedy — not the classification —
63
+ * carries the origin nuance: it names the bundled reinstall first, then the
64
+ * HQ_QMD_BIN case so an operator running their own qmd is pointed at that
65
+ * install rather than only at hq.
66
+ */
67
+ export const QMD_MODULE_MISSING_REMEDY = "hq's local search index can't start: one of its bundled modules is missing, " +
68
+ "which means the hq install tree is incomplete — a partial or interrupted " +
69
+ "install can leave some dependencies unwritten. Reinstall hq and run the " +
70
+ "command again: for a global install run `npm i -g @indigoai-us/hq-cli` (or " +
71
+ "the pnpm equivalent, `pnpm add -g @indigoai-us/hq-cli`). If you point " +
72
+ "HQ_QMD_BIN at your own qmd, that install is missing the module instead — " +
73
+ "reinstall its dependencies, or unset HQ_QMD_BIN to use the bundled copy.";
74
+ /**
75
+ * The qmd process's OWN captured diagnostic — stderr then stdout. Unlike
76
+ * qmd-native-binding-error.ts this NEVER falls back to the synthesized
77
+ * `message`: the message can carry the caller's query, so reading it would let
78
+ * free text trip the classifier. A value with no captured streams yields "". A
79
+ * bare string is treated as captured text directly (test convenience).
80
+ */
81
+ function capturedStreams(err) {
82
+ if (typeof err === "string")
83
+ return err;
84
+ if (err === null || typeof err !== "object")
85
+ return "";
86
+ const record = err;
87
+ const stderr = typeof record.stderr === "string" ? record.stderr : "";
88
+ const stdout = typeof record.stdout === "string" ? record.stdout : "";
89
+ return stderr || stdout ? `${stderr}\n${stdout}` : "";
90
+ }
91
+ /**
92
+ * True when `err` carries, in qmd's OWN captured streams, a module-resolution
93
+ * failure corroborated by a Node module-loader artifact — i.e. qmd's dependency
94
+ * tree is incomplete on this machine. finishRunQmd calls this on the raw streams
95
+ * to type the condition {@link import('../lib/search-index/index.js').QmdModuleMissingError};
96
+ * a true result there means the caller should reinstall.
97
+ */
98
+ export function isQmdModuleMissingError(err) {
99
+ const text = capturedStreams(err);
100
+ if (!text)
101
+ return false;
102
+ return MODULE_RESOLUTION_TOKEN.test(text) && MODULE_LOADER_ARTIFACT.test(text);
103
+ }
104
+ /**
105
+ * If `err` is a caller-driven qmd module-missing failure (a QmdModuleMissingError
106
+ * from `hq search` or its `vsearch`/`query`/`get` siblings), return the
107
+ * actionable, input-free reinstall remedy; otherwise return `null`. Mirrors
108
+ * qmdNativeBindingErrorMessage / qmdMissingCollectionMessage so the top-level
109
+ * handler branches the same way: a non-null result means print-and-skip-Sentry,
110
+ * null means "handle as usual (capture to Sentry)".
111
+ *
112
+ * Gated on the TYPED CLASS plus a caller-reads invocation allow-list: a
113
+ * module-missing raised by hq's OWN reconciliation (`collection list`, `context
114
+ * add`, …) points at a packaging fault hq itself could be responsible for, so it
115
+ * stays a captured internal error rather than being silenced as the user's
116
+ * install.
117
+ */
118
+ export function qmdModuleMissingMessage(err) {
119
+ if (err === null || typeof err !== "object")
120
+ return null;
121
+ const record = err;
122
+ if (record.name !== "QmdModuleMissingError")
123
+ return null;
124
+ const args = Array.isArray(record.args) ? record.args : [];
125
+ const subcommand = args[0];
126
+ if (typeof subcommand !== "string" || !CALLER_READS.has(subcommand))
127
+ return null;
128
+ return QMD_MODULE_MISSING_REMEDY;
129
+ }
130
+ //# sourceMappingURL=qmd-module-missing-error.js.map
@@ -65,13 +65,84 @@ const DEFAULT_GROUPING = "{{ default }}";
65
65
  * request identifier spliced into the name — could otherwise mint an unbounded
66
66
  * number of groups DESPITE the bounded discriminator. The finite-cardinality
67
67
  * promise must hold for the WHOLE key, so any name outside this set collapses to
68
- * FALLBACK_ERROR_NAME. The only first-party error carrying `rpcCode`/`status`
69
- * is IntegrationsCliError; add a name here only when a new bounded carrier
68
+ * FALLBACK_ERROR_NAME. IntegrationsCliError carries `rpcCode`/`status`; the Qmd*
69
+ * errors carry a process EXIT CODE in `status` and are fingerprinted on the
70
+ * dedicated qmd branch below (allow-listed here so their names appear verbatim
71
+ * rather than collapsing). Add a name here only when a new bounded carrier
70
72
  * genuinely warrants its own family of groups.
71
73
  */
72
- const KNOWN_ERROR_NAMES = new Set(["IntegrationsCliError"]);
74
+ const KNOWN_ERROR_NAMES = new Set([
75
+ "IntegrationsCliError",
76
+ // qmd process failures — all extend QmdExitError except QmdBinaryMissingError,
77
+ // which shares the `Qmd` prefix the qmd branch keys on (HQ-CLI-Y, 7690356976).
78
+ "QmdExitError",
79
+ "QmdBinaryMissingError",
80
+ "QmdCollectionMissingError",
81
+ "QmdCollectionExistsError",
82
+ "QmdTerminatedError",
83
+ "QmdLlmDisabledError",
84
+ "QmdModuleMissingError",
85
+ ]);
73
86
  /** Fixed bucket for any error name outside the closed allowlist. */
74
87
  const FALLBACK_ERROR_NAME = "other";
88
+ /**
89
+ * The CLOSED set of qmd subcommands worth their own group — the surfaces hq
90
+ * drives (caller reads plus reconciliation). Anything else, including a missing
91
+ * or non-string args[0], collapses to `qmd:other`, so the subcommand axis is
92
+ * finite no matter what argv qmd was handed (a query value can never reach it).
93
+ */
94
+ const KNOWN_QMD_SUBCOMMANDS = new Set([
95
+ "search", "vsearch", "query", "get",
96
+ "collection", "context", "embed", "status", "update", "cleanup",
97
+ ]);
98
+ /**
99
+ * The small set of qmd process EXIT CODES worth their own group. qmd exits 1 for
100
+ * a general error; any other numeric code collapses to `exit:other`, so the
101
+ * exit-code axis stays finite. A signalled child (`status` null) never reaches
102
+ * this set — it groups by its signal instead (see {@link qmdDispositionToken}).
103
+ */
104
+ const KNOWN_QMD_EXIT_CODES = new Set([1, 2]);
105
+ /**
106
+ * qmd child TERMINATION SIGNALS worth their own group. A signalled qmd child
107
+ * (QmdTerminatedError: `status` null, `signal` set) carries no exit code, so
108
+ * without a signal axis every native crash would collapse into one `exit:other`
109
+ * bucket — merging the distinct failures (SIGSEGV vs SIGABRT vs SIGBUS) the
110
+ * termination policy deliberately keeps on separate reporting paths. Bounded:
111
+ * any signal outside this closed set collapses to `signal:other`, so the axis
112
+ * stays finite no matter what killed the child.
113
+ */
114
+ const KNOWN_QMD_SIGNALS = new Set([
115
+ "SIGSEGV", "SIGABRT", "SIGBUS", "SIGILL", "SIGFPE",
116
+ "SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT",
117
+ ]);
118
+ /** The bounded qmd subcommand token (`qmd:<sub>`), read only from args[0]. */
119
+ function qmdSubcommandToken(args) {
120
+ const first = Array.isArray(args) ? args[0] : undefined;
121
+ return typeof first === "string" && KNOWN_QMD_SUBCOMMANDS.has(first)
122
+ ? `qmd:${first}`
123
+ : "qmd:other";
124
+ }
125
+ /** The bounded qmd exit-code token (`exit:<code>`), read only from `status`. */
126
+ function qmdExitToken(status) {
127
+ return typeof status === "number" &&
128
+ Number.isInteger(status) &&
129
+ KNOWN_QMD_EXIT_CODES.has(status)
130
+ ? `exit:${status}`
131
+ : "exit:other";
132
+ }
133
+ /**
134
+ * The bounded qmd DISPOSITION token — the fingerprint's fourth component. A
135
+ * signalled child (a non-empty `signal` string, `status` null) groups by
136
+ * `signal:<name>` from the closed allow-list so distinct native crashes stay in
137
+ * distinct groups; an ordinary exit groups by `exit:<code>`. Both inputs are
138
+ * bounded, so the axis stays finite regardless of what qmd reported.
139
+ */
140
+ function qmdDispositionToken(status, signal) {
141
+ if (typeof signal === "string" && signal.length > 0) {
142
+ return KNOWN_QMD_SIGNALS.has(signal) ? `signal:${signal}` : "signal:other";
143
+ }
144
+ return qmdExitToken(status);
145
+ }
75
146
  /**
76
147
  * Return a bounded `event.fingerprint` array for `err`, or `null` when `err`
77
148
  * carries no discriminator from the closed allowlist (in which case the caller
@@ -98,6 +169,28 @@ export function sentryFingerprintFor(err) {
98
169
  const rawName = typeof record.name === "string" && record.name.length > 0 ? record.name : null;
99
170
  if (rawName === null)
100
171
  return null;
172
+ // qmd process failures carry a PROCESS EXIT CODE in `status` (never an HTTP
173
+ // status) plus a subcommand in args[0]. Fingerprint them on a dedicated,
174
+ // fully-bounded key BEFORE the HTTP-status branch so an exit code is never read
175
+ // as an HTTP status, and so grouping is one group per (subcommand, disposition)
176
+ // instead of one per query — the cardinality blow-up that minted a new
177
+ // permanent issue per qmd child failure (HQ-CLI-Y, Sentry 7690356976). The
178
+ // fourth component is the DISPOSITION: an ordinary exit groups by `exit:<code>`,
179
+ // while a signalled child (QmdTerminatedError: status null, signal set) groups
180
+ // by `signal:<name>` so distinct native crashes (SIGSEGV/SIGABRT/SIGBUS) stay
181
+ // in distinct groups rather than collapsing into one `exit:other`. Keyed on the
182
+ // `Qmd` name prefix; the name is still bounded to the closed allowlist, so an
183
+ // unexpected Qmd* name collapses to FALLBACK_ERROR_NAME while keeping the
184
+ // bounded qmd discriminators.
185
+ if (rawName.startsWith("Qmd")) {
186
+ const qmdName = KNOWN_ERROR_NAMES.has(rawName) ? rawName : FALLBACK_ERROR_NAME;
187
+ return [
188
+ DEFAULT_GROUPING,
189
+ qmdName,
190
+ qmdSubcommandToken(record.args),
191
+ qmdDispositionToken(record.status, record.signal),
192
+ ];
193
+ }
101
194
  const name = KNOWN_ERROR_NAMES.has(rawName) ? rawName : FALLBACK_ERROR_NAME;
102
195
  const rpcCode = record.rpcCode;
103
196
  if (typeof rpcCode === "number" && Number.isInteger(rpcCode)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.27",
3
+ "version": "5.103.28",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {