gitnexus 1.6.4-rc.99 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shared/index.d.ts +8 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +9 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/integrations/circuit-breaker.d.ts +183 -0
- package/dist/_shared/integrations/circuit-breaker.d.ts.map +1 -0
- package/dist/_shared/integrations/circuit-breaker.js +236 -0
- package/dist/_shared/integrations/circuit-breaker.js.map +1 -0
- package/dist/_shared/integrations/resilient-fetch.d.ts +99 -0
- package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -0
- package/dist/_shared/integrations/resilient-fetch.js +204 -0
- package/dist/_shared/integrations/resilient-fetch.js.map +1 -0
- package/dist/_shared/integrations/retry.d.ts +60 -0
- package/dist/_shared/integrations/retry.d.ts.map +1 -0
- package/dist/_shared/integrations/retry.js +67 -0
- package/dist/_shared/integrations/retry.js.map +1 -0
- package/dist/_shared/integrations/understand-quickly.d.ts +79 -0
- package/dist/_shared/integrations/understand-quickly.d.ts.map +1 -0
- package/dist/_shared/integrations/understand-quickly.js +139 -0
- package/dist/_shared/integrations/understand-quickly.js.map +1 -0
- package/dist/_shared/test-helpers.d.ts +13 -0
- package/dist/_shared/test-helpers.d.ts.map +1 -0
- package/dist/_shared/test-helpers.js +13 -0
- package/dist/_shared/test-helpers.js.map +1 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/publish.d.ts +29 -0
- package/dist/cli/publish.js +174 -0
- package/dist/config/ignore-service.js +2 -0
- package/dist/core/embeddings/hf-env.d.ts +9 -37
- package/dist/core/embeddings/hf-env.js +65 -83
- package/dist/core/embeddings/http-client.d.ts +6 -0
- package/dist/core/embeddings/http-client.js +22 -18
- package/dist/core/group/bridge-db.js +7 -6
- package/dist/core/group/config-parser.js +19 -1
- package/dist/core/group/extractors/include-extractor.d.ts +39 -0
- package/dist/core/group/extractors/include-extractor.js +565 -0
- package/dist/core/group/extractors/manifest-extractor.js +8 -0
- package/dist/core/group/extractors/thrift-extractor.js +8 -0
- package/dist/core/group/matching.js +2 -0
- package/dist/core/group/storage.js +8 -1
- package/dist/core/group/sync.js +33 -0
- package/dist/core/group/types.d.ts +2 -1
- package/dist/core/ingestion/call-processor.js +6 -4
- package/dist/core/ingestion/cpp-ue-preprocessor.d.ts +12 -0
- package/dist/core/ingestion/cpp-ue-preprocessor.js +260 -0
- package/dist/core/ingestion/heritage-processor.js +9 -4
- package/dist/core/ingestion/import-processor.js +3 -2
- package/dist/core/ingestion/language-provider.d.ts +32 -0
- package/dist/core/ingestion/languages/c-cpp.js +2 -0
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/workers/parse-worker.js +4 -0
- package/dist/core/wiki/llm-client.d.ts +16 -0
- package/dist/core/wiki/llm-client.js +101 -63
- package/dist/server/api.js +7 -2
- package/package.json +2 -2
- package/vendor/node_modules/node-addon-api/node_addon_api.target.mk +16 -14
- package/vendor/node_modules/node-addon-api/node_addon_api_except.target.mk +16 -14
- package/vendor/node_modules/node-addon-api/node_addon_api_except_all.target.mk +16 -14
- package/vendor/node_modules/node-addon-api/node_addon_api_maybe.target.mk +16 -14
- package/web/assets/{agent-Dl2d-mDi.js → agent-BpHQleHE.js} +17 -17
- package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BUzkWgDl.js +1 -0
- package/web/assets/{architectureDiagram-EMZXCZ2Q-uiiIna83.js → architectureDiagram-EMZXCZ2Q-CXbzok1G.js} +3 -3
- package/web/assets/{blockDiagram-IGV67L2C-uBKdDzXA.js → blockDiagram-IGV67L2C-DA1QJ4ZB.js} +4 -4
- package/web/assets/{c4Diagram-DFAF54RM-DTxaX43T.js → c4Diagram-DFAF54RM-DeJV1rCn.js} +2 -2
- package/web/assets/{chunk-3GS5O3IE-BxKKu7d7.js → chunk-3GS5O3IE-B8hpnia8.js} +2 -2
- package/web/assets/{chunk-3YCYZ6SJ-BJZnGFjg.js → chunk-3YCYZ6SJ-B-BVu21C.js} +1 -1
- package/web/assets/{chunk-6NTNNK5N-CIL_eO2d.js → chunk-6NTNNK5N-DW-jYSDr.js} +1 -1
- package/web/assets/{chunk-A34GCYZU-Dv4Vkem9.js → chunk-A34GCYZU-BbZl-IKy.js} +1 -1
- package/web/assets/{chunk-DJ7UZH7F-Cskl-nb3.js → chunk-DJ7UZH7F-B5tMfF87.js} +1 -1
- package/web/assets/{chunk-DKKBVRCY-D3S2BD56.js → chunk-DKKBVRCY-Djm-6K_e.js} +2 -2
- package/web/assets/{chunk-DU5LTGQ6-B8Zh51LG.js → chunk-DU5LTGQ6-QPoynYcp.js} +1 -1
- package/web/assets/{chunk-FXACKDTF-Cb4xHVUz.js → chunk-FXACKDTF-jieSmbe2.js} +3 -3
- package/web/assets/chunk-H3VCZNTA-8EMHZ3SV.js +1 -0
- package/web/assets/chunk-HN6EAY2L-CgE_agsD.js +1 -0
- package/web/assets/{chunk-O5ABG6QK-V3sk7Bps.js → chunk-O5ABG6QK-DEZV1O19.js} +1 -1
- package/web/assets/{chunk-PK6DOVAG-FvG7aKfZ.js → chunk-PK6DOVAG-BXv0JnsJ.js} +3 -3
- package/web/assets/{chunk-RNJOYNJ4-BPANsy8V.js → chunk-RNJOYNJ4-BiBkF9dN.js} +1 -1
- package/web/assets/chunk-RWUO3TPN-DTWzAhQ1.js +1 -0
- package/web/assets/chunk-TBF5ZNIQ-BSl6s9lT.js +1 -0
- package/web/assets/{chunk-TYMNRAUI-BnMe6HfP.js → chunk-TYMNRAUI-D1NDLIMG.js} +1 -1
- package/web/assets/{chunk-W7ZLLLMY-BqZFKM5j.js → chunk-W7ZLLLMY-Bu-cdLzs.js} +1 -1
- package/web/assets/{chunk-WSB5WSVC-EKS7-Nnc.js → chunk-WSB5WSVC-Dor71SOz.js} +1 -1
- package/web/assets/{chunk-XGPFEOL4-BBab-enK.js → chunk-XGPFEOL4-CUonLRle.js} +1 -1
- package/web/assets/classDiagram-PPOCWD7C-Cdd6UdVv.js +1 -0
- package/web/assets/classDiagram-v2-23LJLIIU-DNhxY2Ib.js +1 -0
- package/web/assets/{cose-bilkent-PNC4W37J-DjEQkTZ4.js → cose-bilkent-PNC4W37J-DdZq7Qz_.js} +1 -1
- package/web/assets/dagre-E77IOHMT-Cfj_6e17.js +4 -0
- package/web/assets/{diagram-H7BISOXX-CMuISoJL.js → diagram-H7BISOXX-BNvpsrBv.js} +2 -2
- package/web/assets/{diagram-JC5VWROH-DHq_eoeM.js → diagram-JC5VWROH-Cn0rtDbJ.js} +1 -1
- package/web/assets/{diagram-LXUTUG65-C0NczIgW.js → diagram-LXUTUG65-CHfoVKNm.js} +2 -2
- package/web/assets/{diagram-WEHSV5V5-WsBnpSuc.js → diagram-WEHSV5V5-BLxHksnu.js} +1 -1
- package/web/assets/{erDiagram-GCSMX5X6-DxHFHFR6.js → erDiagram-GCSMX5X6-DfdN6iqF.js} +2 -2
- package/web/assets/flowDiagram-OTCZ4VVT-aAZw4c_M.js +162 -0
- package/web/assets/ganttDiagram-MUNLMDZQ-BEfC_7Or.js +292 -0
- package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-B6ZeRLP0.js +1 -0
- package/web/assets/gitGraphDiagram-3HKGZ4G3-CG0kC8Q5.js +106 -0
- package/web/assets/{index-DDcYO1IJ.js → index-3NUCUW2s.js} +119 -119
- package/web/assets/info-OMHHGYJF-BF2H5H6G-BIOsZy0W.js +1 -0
- package/web/assets/infoDiagram-MN7RKWGX-DUoMH-pq.js +2 -0
- package/web/assets/{ishikawaDiagram-YMYX4NHK-C-JI2Ih7.js → ishikawaDiagram-YMYX4NHK-BF8AtDaj.js} +2 -2
- package/web/assets/{journeyDiagram-SO5T7YLQ-IPX4h3iD.js → journeyDiagram-SO5T7YLQ-BW8oKa3N.js} +3 -3
- package/web/assets/{kanban-definition-LJHFXRCJ-B23LtshQ.js → kanban-definition-LJHFXRCJ-DzbOsR-2.js} +6 -6
- package/web/assets/{mindmap-definition-2EUWGEK5-DJKUsrCo.js → mindmap-definition-2EUWGEK5-BIXGQALq.js} +25 -25
- package/web/assets/packet-4T2RLAQJ-EV4IVRXR-Bs7ysNMU.js +1 -0
- package/web/assets/pie-ZZUOXDRM-N23DN5KN-Csi8nnbH.js +1 -0
- package/web/assets/{pieDiagram-3IATQBI2-Bj26KTKk.js → pieDiagram-3IATQBI2-QY-u7P6a.js} +3 -3
- package/web/assets/{quadrantDiagram-E256RVCF-VAk5oIRr.js → quadrantDiagram-E256RVCF-Di4aNiU8.js} +3 -3
- package/web/assets/radar-PYXPWWZC-P6TP7ZYP-DJlyXh2-.js +1 -0
- package/web/assets/{requirementDiagram-M5DCFWZL-CmlTJ_vQ.js → requirementDiagram-M5DCFWZL-CzUUrhQD.js} +4 -4
- package/web/assets/{sankeyDiagram-L3NBLAOT-C-f_DdcX.js → sankeyDiagram-L3NBLAOT-D2fbMiIU.js} +3 -3
- package/web/assets/{sequenceDiagram-ZOUHS735-DmtNf1NF.js → sequenceDiagram-ZOUHS735-BNdkqguh.js} +6 -6
- package/web/assets/stateDiagram-MLPALWAM-51_Q2w2L.js +1 -0
- package/web/assets/stateDiagram-v2-B5LQ5ZB2-8nHs2aNE.js +1 -0
- package/web/assets/{timeline-definition-5SPVSISX-BkpqZxt2.js → timeline-definition-5SPVSISX-DkMiGj4Z.js} +9 -9
- package/web/assets/treeView-SZITEDCU-5DXDK3XO-Cy6P0_CZ.js +1 -0
- package/web/assets/treemap-W4RFUUIX-WYLRDWKO-C_pp1R7A.js +1 -0
- package/web/assets/{vennDiagram-IE5QUKF5-iAJ08JBe.js → vennDiagram-IE5QUKF5-BCxMmf9q.js} +3 -3
- package/web/assets/wardley-RL74JXVD-BCRCBASE-CMStJhpn.js +1 -0
- package/web/assets/{wardleyDiagram-XU3VSMPF-BDiKTkL4.js → wardleyDiagram-XU3VSMPF-ijlLF77s.js} +4 -4
- package/web/assets/{xychartDiagram-ZHJ5623Y-CknyZyQK.js → xychartDiagram-ZHJ5623Y-DZ7fP1_3.js} +3 -3
- package/web/index.html +1 -1
- package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BYXnTu07.js +0 -1
- package/web/assets/chunk-H3VCZNTA-BOGhqvxW.js +0 -1
- package/web/assets/chunk-HN6EAY2L-CitKFy1e.js +0 -1
- package/web/assets/chunk-RWUO3TPN-BHusg2jI.js +0 -1
- package/web/assets/chunk-TBF5ZNIQ-CXamrpH3.js +0 -1
- package/web/assets/classDiagram-PPOCWD7C-B-LkKU0P.js +0 -1
- package/web/assets/classDiagram-v2-23LJLIIU-_NGI4VEh.js +0 -1
- package/web/assets/dagre-E77IOHMT-D6Xacg1x.js +0 -4
- package/web/assets/flowDiagram-OTCZ4VVT-_G1VxyQS.js +0 -162
- package/web/assets/ganttDiagram-MUNLMDZQ-BIHObj0t.js +0 -292
- package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-BXJXsixL.js +0 -1
- package/web/assets/gitGraphDiagram-3HKGZ4G3-COZUkHTD.js +0 -106
- package/web/assets/info-OMHHGYJF-BF2H5H6G-C820LK0J.js +0 -1
- package/web/assets/infoDiagram-MN7RKWGX-BeOjfPc_.js +0 -2
- package/web/assets/packet-4T2RLAQJ-EV4IVRXR-BvWtM5jq.js +0 -1
- package/web/assets/pie-ZZUOXDRM-N23DN5KN-BnDgj0TQ.js +0 -1
- package/web/assets/radar-PYXPWWZC-P6TP7ZYP-2IuDlZ6r.js +0 -1
- package/web/assets/stateDiagram-MLPALWAM-Lq7UVrkj.js +0 -1
- package/web/assets/stateDiagram-v2-B5LQ5ZB2-DCiXBPeS.js +0 -1
- package/web/assets/treeView-SZITEDCU-5DXDK3XO-Ipq1EIaF.js +0 -1
- package/web/assets/treemap-W4RFUUIX-WYLRDWKO-DESFxovu.js +0 -1
- package/web/assets/wardley-RL74JXVD-BCRCBASE-BVkhckBY.js +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gitnexus publish` — opt-in ping to the understand-quickly registry.
|
|
3
|
+
*
|
|
4
|
+
* Fires a single `repository_dispatch` event at
|
|
5
|
+
* `looptech-ai/understand-quickly` so the registry knows to refresh its
|
|
6
|
+
* entry for the current repo. Does NOT upload anything: per the
|
|
7
|
+
* understand-quickly protocol, the registry pulls the graph from a
|
|
8
|
+
* raw-GitHub URL the user controls.
|
|
9
|
+
*
|
|
10
|
+
* https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md
|
|
11
|
+
*
|
|
12
|
+
* Defaults:
|
|
13
|
+
* - Without `UNDERSTAND_QUICKLY_TOKEN` in the env, this is a no-op
|
|
14
|
+
* (prints one informational line, exit 0). Same shape as the
|
|
15
|
+
* `--publish` patterns in sibling tools.
|
|
16
|
+
* - With the token, fires the dispatch and reports the response code.
|
|
17
|
+
*
|
|
18
|
+
* The `id` is derived from the repo's `origin` remote unless the caller
|
|
19
|
+
* passes `--id <owner/repo>` explicitly. We deliberately do NOT auto-add
|
|
20
|
+
* the repo to the registry — registration is one-time and uses the
|
|
21
|
+
* `npx @understand-quickly/cli add` path documented in the protocol.
|
|
22
|
+
*/
|
|
23
|
+
export interface PublishOptions {
|
|
24
|
+
/** Override the auto-derived `owner/repo` id. */
|
|
25
|
+
id?: string;
|
|
26
|
+
/** Treat the cwd as the repo root (skip git-root walk). */
|
|
27
|
+
skipGit?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const publishCommand: (inputPath?: string, options?: PublishOptions) => Promise<void>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gitnexus publish` — opt-in ping to the understand-quickly registry.
|
|
3
|
+
*
|
|
4
|
+
* Fires a single `repository_dispatch` event at
|
|
5
|
+
* `looptech-ai/understand-quickly` so the registry knows to refresh its
|
|
6
|
+
* entry for the current repo. Does NOT upload anything: per the
|
|
7
|
+
* understand-quickly protocol, the registry pulls the graph from a
|
|
8
|
+
* raw-GitHub URL the user controls.
|
|
9
|
+
*
|
|
10
|
+
* https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md
|
|
11
|
+
*
|
|
12
|
+
* Defaults:
|
|
13
|
+
* - Without `UNDERSTAND_QUICKLY_TOKEN` in the env, this is a no-op
|
|
14
|
+
* (prints one informational line, exit 0). Same shape as the
|
|
15
|
+
* `--publish` patterns in sibling tools.
|
|
16
|
+
* - With the token, fires the dispatch and reports the response code.
|
|
17
|
+
*
|
|
18
|
+
* The `id` is derived from the repo's `origin` remote unless the caller
|
|
19
|
+
* passes `--id <owner/repo>` explicitly. We deliberately do NOT auto-add
|
|
20
|
+
* the repo to the registry — registration is one-time and uses the
|
|
21
|
+
* `npx @understand-quickly/cli add` path documented in the protocol.
|
|
22
|
+
*/
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { UNDERSTAND_QUICKLY_DISPATCH_URL, UNDERSTAND_QUICKLY_TOKEN_ENV, buildUqDispatchPayload, isValidOwnerRepo, parseOwnerRepoFromRemote, } from '../_shared/index.js';
|
|
25
|
+
import { getGitRoot, getRemoteOriginUrl, getCurrentCommit } from '../storage/git.js';
|
|
26
|
+
import { hasIndex } from '../storage/repo-manager.js';
|
|
27
|
+
import { cliInfo, cliError } from './cli-message.js';
|
|
28
|
+
const REGISTER_HINT = 'Register your repo once with: npx @understand-quickly/cli add\n' +
|
|
29
|
+
'Or use the wizard: https://looptech-ai.github.io/understand-quickly/add.html';
|
|
30
|
+
/**
|
|
31
|
+
* Hard cap on the dispatch fetch to keep CI publish steps from stalling
|
|
32
|
+
* for the OS TCP timeout (~2 min) when api.github.com is unreachable.
|
|
33
|
+
* Matches the pattern used in `src/core/embeddings/http-client.ts`.
|
|
34
|
+
*/
|
|
35
|
+
const DISPATCH_TIMEOUT_MS = 15_000;
|
|
36
|
+
export const publishCommand = async (inputPath, options = {}) => {
|
|
37
|
+
// ── 0. Token gate FIRST — guarantees true no-op without the token. ──
|
|
38
|
+
// The README, CLI --help, and PR body all promise "exit 0 without
|
|
39
|
+
// UNDERSTAND_QUICKLY_TOKEN". Doing the index/repo-root checks before
|
|
40
|
+
// the token gate would make those promises false for users who haven't
|
|
41
|
+
// run `gitnexus analyze` yet but want to verify the command is wired.
|
|
42
|
+
const token = process.env[UNDERSTAND_QUICKLY_TOKEN_ENV];
|
|
43
|
+
if (!token) {
|
|
44
|
+
cliInfo(`[understand-quickly] ${UNDERSTAND_QUICKLY_TOKEN_ENV} is not set — skipping dispatch.\n` +
|
|
45
|
+
`Set it to a fine-grained PAT with "Repository dispatches: write" on ` +
|
|
46
|
+
`looptech-ai/understand-quickly to enable instant resync.\n` +
|
|
47
|
+
`(Without the token, the registry's nightly sync still picks up your entry.)`, { skipped: 'no-token' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// ── 1. Resolve the repo root (same precedence as `analyze`) ──────────
|
|
51
|
+
let repoPath;
|
|
52
|
+
if (inputPath) {
|
|
53
|
+
repoPath = path.resolve(inputPath);
|
|
54
|
+
}
|
|
55
|
+
else if (options.skipGit) {
|
|
56
|
+
repoPath = path.resolve(process.cwd());
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const gitRoot = getGitRoot(process.cwd());
|
|
60
|
+
if (!gitRoot) {
|
|
61
|
+
cliError('[understand-quickly] not inside a git repository.\n' +
|
|
62
|
+
'Run from a repo, or pass --skip-git to publish from the current directory.');
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
repoPath = gitRoot;
|
|
67
|
+
}
|
|
68
|
+
// ── 2. Confirm a GitNexus index exists ───────────────────────────────
|
|
69
|
+
// Publishing without an index is almost always a mistake — the
|
|
70
|
+
// registry's nightly sync would fetch a stale or missing graph file
|
|
71
|
+
// and mark the entry `missing`. Refuse loudly with a fix-it hint.
|
|
72
|
+
if (!(await hasIndex(repoPath))) {
|
|
73
|
+
cliError(`[understand-quickly] no GitNexus index found at ${repoPath}/.gitnexus.\n` +
|
|
74
|
+
'Run `gitnexus analyze` first, then re-run `gitnexus publish`.');
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// ── 3. Derive the registry id ─────────────────────────────────────────
|
|
79
|
+
const id = options.id ?? parseOwnerRepoFromRemote(getRemoteOriginUrl(repoPath) ?? undefined) ?? null;
|
|
80
|
+
if (!id || !isValidOwnerRepo(id)) {
|
|
81
|
+
cliError(`[understand-quickly] could not derive a registry id from this repo.\n` +
|
|
82
|
+
`Pass --id <owner/repo> explicitly (e.g. --id looptech-ai/${path.basename(repoPath)}).\n` +
|
|
83
|
+
REGISTER_HINT);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// ── 4. Fire the dispatch ─────────────────────────────────────────────
|
|
88
|
+
const payload = buildUqDispatchPayload(id);
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await fetch(UNDERSTAND_QUICKLY_DISPATCH_URL, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
Accept: 'application/vnd.github+json',
|
|
95
|
+
Authorization: `Bearer ${token}`,
|
|
96
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
'User-Agent': 'gitnexus-cli',
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(payload),
|
|
101
|
+
signal: AbortSignal.timeout(DISPATCH_TIMEOUT_MS),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// `AbortSignal.timeout()` throws a `DOMException` with `name ===
|
|
106
|
+
// 'TimeoutError'` on Node 18.14+ (and on browsers/Bun). It is NOT
|
|
107
|
+
// a plain `AbortError`. Match the pattern used in
|
|
108
|
+
// gitnexus/src/core/embeddings/http-client.ts so the user sees the
|
|
109
|
+
// targeted "timed out" message instead of a generic "operation
|
|
110
|
+
// was aborted".
|
|
111
|
+
const isTimeout = err instanceof DOMException && err.name === 'TimeoutError';
|
|
112
|
+
if (isTimeout) {
|
|
113
|
+
cliError(`[understand-quickly] dispatch timed out after ${DISPATCH_TIMEOUT_MS}ms. ` +
|
|
114
|
+
`Check network access to api.github.com and retry.`, { id });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
cliError(`[understand-quickly] dispatch network error: ${msg}`, { id });
|
|
119
|
+
}
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// GitHub returns 204 on success. Distinct branches for 401/403/404/422
|
|
124
|
+
// so users debug without checking the docs.
|
|
125
|
+
if (response.status === 204) {
|
|
126
|
+
await response.body?.cancel().catch(() => { });
|
|
127
|
+
// `getCurrentCommit` is only meaningful in the success path — moving
|
|
128
|
+
// it inside this branch removes a wasted child-process spawn on every
|
|
129
|
+
// error response (LOW 7).
|
|
130
|
+
const commit = getCurrentCommit(repoPath);
|
|
131
|
+
cliInfo(`[understand-quickly] dispatched sync-entry for ${id}` +
|
|
132
|
+
(commit ? ` @ ${commit.slice(0, 7)}` : '') +
|
|
133
|
+
'.\n' +
|
|
134
|
+
`Note: a 204 only confirms GitHub accepted the dispatch. Whether the ` +
|
|
135
|
+
`registry workflow finds an entry for "${id}" is logged at ` +
|
|
136
|
+
`https://github.com/looptech-ai/understand-quickly/actions/workflows/sync.yml`, { id, commit, status: response.status });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (response.status === 401) {
|
|
140
|
+
cliError(`[understand-quickly] dispatch returned 401 — the ${UNDERSTAND_QUICKLY_TOKEN_ENV} value is invalid or expired.\n` +
|
|
141
|
+
`Regenerate a fine-grained PAT at https://github.com/settings/personal-access-tokens ` +
|
|
142
|
+
`with Repository access scoped to looptech-ai/understand-quickly and the ` +
|
|
143
|
+
`"Repository dispatches: write" permission, then retry.`, { id, status: response.status });
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (response.status === 403) {
|
|
148
|
+
cliError(`[understand-quickly] dispatch returned 403 — the token authenticated but ` +
|
|
149
|
+
`lacks the "Repository dispatches: write" permission on ` +
|
|
150
|
+
`looptech-ai/understand-quickly. Edit the PAT scopes and retry.`, { id, status: response.status });
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (response.status === 404) {
|
|
155
|
+
cliError(`[understand-quickly] dispatch returned 404 — the token cannot reach ` +
|
|
156
|
+
`looptech-ai/understand-quickly. Verify the PAT has Repository access to ` +
|
|
157
|
+
`that exact repo (not just your own org).`, { id, status: response.status });
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (response.status === 422) {
|
|
162
|
+
// Malformed event_type / client_payload — a code bug in this CLI,
|
|
163
|
+
// not a user mistake. Surface so we get bug reports.
|
|
164
|
+
const body422 = await response.text().catch(() => '');
|
|
165
|
+
cliError(`[understand-quickly] dispatch returned 422 (this is a CLI bug; please report).\n` +
|
|
166
|
+
`Body: ${body422 || '(empty)'}`, { id, status: response.status });
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// 5xx and anything else → bubble the body so the user has something to act on.
|
|
171
|
+
const body = await response.text().catch(() => '');
|
|
172
|
+
cliError(`[understand-quickly] dispatch failed with HTTP ${response.status}: ${body || '(empty body)'}`, { id, status: response.status });
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
};
|
|
@@ -21,6 +21,8 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|
|
21
21
|
'bower_components',
|
|
22
22
|
'jspm_packages',
|
|
23
23
|
'vendor', // PHP/Go
|
|
24
|
+
'third_party', // C/C++ (Google-style vendored dependencies)
|
|
25
|
+
'3rdparty', // C/C++ (alternate spelling, also Qt convention)
|
|
24
26
|
// 'packages' removed - commonly used for monorepo source code (lerna, pnpm, yarn workspaces)
|
|
25
27
|
'venv',
|
|
26
28
|
'.venv',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CircuitBreaker } from '../../_shared/index.js';
|
|
1
2
|
/** Per-attempt timeout for the full model download (5 minutes). */
|
|
2
3
|
export declare const HF_DOWNLOAD_TIMEOUT_MS: number;
|
|
3
4
|
/** Maximum total download attempts (1 initial + N-1 retries). */
|
|
@@ -72,41 +73,15 @@ export declare function applyHfEnvOverrides(env: HfEnvSubset): void;
|
|
|
72
73
|
export declare function isNetworkFetchError(message: string): boolean;
|
|
73
74
|
/** @internal Used by `withHfDownloadRetry` to mark a circuit-open rejection. */
|
|
74
75
|
export declare const CIRCUIT_OPEN_TAG = "hf-circuit-open";
|
|
75
|
-
/** Circuit-breaker states. */
|
|
76
|
-
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
77
76
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* closes again; if it fails the circuit re-opens.
|
|
85
|
-
*
|
|
86
|
-
* Exported for unit-testing; production code should use the module-level
|
|
87
|
-
* `hfDownloadCircuit` singleton.
|
|
77
|
+
* Module-level singleton shared by both embedder entry points
|
|
78
|
+
* (`core/embeddings/embedder.ts` + `mcp/core/embedder.ts`). Per-process
|
|
79
|
+
* only — not persisted across restarts. Backed by the shared
|
|
80
|
+
* `CircuitBreaker` from `gitnexus-shared` (same state machine, same
|
|
81
|
+
* semantics, plus the single-permit half-open gate that prevents
|
|
82
|
+
* recovery-time stampedes).
|
|
88
83
|
*/
|
|
89
|
-
export declare
|
|
90
|
-
readonly failureThreshold: number;
|
|
91
|
-
readonly resetTimeoutMs: number;
|
|
92
|
-
private _state;
|
|
93
|
-
private _failures;
|
|
94
|
-
/** Timestamp of the last recorded failure (ms since epoch). */
|
|
95
|
-
lastFailureAt: number;
|
|
96
|
-
constructor(failureThreshold?: number, resetTimeoutMs?: number);
|
|
97
|
-
/** Effective state, factoring in the reset-timeout transition. */
|
|
98
|
-
get state(): CircuitState;
|
|
99
|
-
/** Returns true when the circuit is open and calls should be rejected. */
|
|
100
|
-
isOpen(): boolean;
|
|
101
|
-
/** Record a successful call — resets the failure counter and closes the circuit. */
|
|
102
|
-
recordSuccess(): void;
|
|
103
|
-
/** Record a failed call — increments the counter and opens the circuit when the threshold is reached. */
|
|
104
|
-
recordFailure(): void;
|
|
105
|
-
/** @internal Reset to initial state (used in tests). */
|
|
106
|
-
reset(): void;
|
|
107
|
-
}
|
|
108
|
-
/** Module-level singleton shared by both embedder entry points. */
|
|
109
|
-
export declare const hfDownloadCircuit: HfDownloadCircuitBreaker;
|
|
84
|
+
export declare const hfDownloadCircuit: CircuitBreaker;
|
|
110
85
|
/** @internal Returns true for errors that should abort without retry (circuit-open). */
|
|
111
86
|
export declare function isHfCircuitOpenError(message: string): boolean;
|
|
112
87
|
/**
|
|
@@ -119,8 +94,6 @@ export declare function isHfDownloadFailure(message: string): boolean;
|
|
|
119
94
|
* `ETIMEDOUT` so that `isNetworkFetchError` classifies it correctly.
|
|
120
95
|
*/
|
|
121
96
|
export declare function withDownloadTimeout<T>(fn: () => Promise<T>, timeoutMs: number): Promise<T>;
|
|
122
|
-
/** @internal Async sleep (exposed for testing). */
|
|
123
|
-
export declare function sleep(ms: number): Promise<void>;
|
|
124
97
|
export interface HfRetryOptions {
|
|
125
98
|
/** Maximum total attempts including the initial one (default: `HF_MAX_ATTEMPTS`). */
|
|
126
99
|
maxAttempts?: number;
|
|
@@ -132,7 +105,7 @@ export interface HfRetryOptions {
|
|
|
132
105
|
* Circuit-breaker instance to use. Defaults to the module-level
|
|
133
106
|
* `hfDownloadCircuit` singleton. Pass a fresh instance in tests.
|
|
134
107
|
*/
|
|
135
|
-
circuit?:
|
|
108
|
+
circuit?: CircuitBreaker;
|
|
136
109
|
/**
|
|
137
110
|
* Optional callback invoked before each retry (not the initial attempt).
|
|
138
111
|
* @param attempt - 1-based retry number
|
|
@@ -160,4 +133,3 @@ export interface HfRetryOptions {
|
|
|
160
133
|
* the calling code still fire.
|
|
161
134
|
*/
|
|
162
135
|
export declare function withHfDownloadRetry<T>(fn: () => Promise<T>, options?: HfRetryOptions): Promise<T>;
|
|
163
|
-
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { CircuitBreaker, withRetry } from '../../_shared/index.js';
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Download resilience defaults
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -85,61 +86,18 @@ export function isNetworkFetchError(message) {
|
|
|
85
86
|
/** @internal Used by `withHfDownloadRetry` to mark a circuit-open rejection. */
|
|
86
87
|
export const CIRCUIT_OPEN_TAG = 'hf-circuit-open';
|
|
87
88
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* closes again; if it fails the circuit re-opens.
|
|
95
|
-
*
|
|
96
|
-
* Exported for unit-testing; production code should use the module-level
|
|
97
|
-
* `hfDownloadCircuit` singleton.
|
|
89
|
+
* Module-level singleton shared by both embedder entry points
|
|
90
|
+
* (`core/embeddings/embedder.ts` + `mcp/core/embedder.ts`). Per-process
|
|
91
|
+
* only — not persisted across restarts. Backed by the shared
|
|
92
|
+
* `CircuitBreaker` from `gitnexus-shared` (same state machine, same
|
|
93
|
+
* semantics, plus the single-permit half-open gate that prevents
|
|
94
|
+
* recovery-time stampedes).
|
|
98
95
|
*/
|
|
99
|
-
export
|
|
100
|
-
failureThreshold
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/** Timestamp of the last recorded failure (ms since epoch). */
|
|
105
|
-
lastFailureAt = 0;
|
|
106
|
-
constructor(failureThreshold = CB_FAILURE_THRESHOLD, resetTimeoutMs = CB_RESET_TIMEOUT_MS) {
|
|
107
|
-
this.failureThreshold = failureThreshold;
|
|
108
|
-
this.resetTimeoutMs = resetTimeoutMs;
|
|
109
|
-
}
|
|
110
|
-
/** Effective state, factoring in the reset-timeout transition. */
|
|
111
|
-
get state() {
|
|
112
|
-
if (this._state === 'open' && Date.now() - this.lastFailureAt > this.resetTimeoutMs) {
|
|
113
|
-
this._state = 'half-open';
|
|
114
|
-
}
|
|
115
|
-
return this._state;
|
|
116
|
-
}
|
|
117
|
-
/** Returns true when the circuit is open and calls should be rejected. */
|
|
118
|
-
isOpen() {
|
|
119
|
-
return this.state === 'open';
|
|
120
|
-
}
|
|
121
|
-
/** Record a successful call — resets the failure counter and closes the circuit. */
|
|
122
|
-
recordSuccess() {
|
|
123
|
-
this._failures = 0;
|
|
124
|
-
this._state = 'closed';
|
|
125
|
-
}
|
|
126
|
-
/** Record a failed call — increments the counter and opens the circuit when the threshold is reached. */
|
|
127
|
-
recordFailure() {
|
|
128
|
-
this._failures++;
|
|
129
|
-
this.lastFailureAt = Date.now();
|
|
130
|
-
if (this._failures >= this.failureThreshold) {
|
|
131
|
-
this._state = 'open';
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** @internal Reset to initial state (used in tests). */
|
|
135
|
-
reset() {
|
|
136
|
-
this._failures = 0;
|
|
137
|
-
this._state = 'closed';
|
|
138
|
-
this.lastFailureAt = 0;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/** Module-level singleton shared by both embedder entry points. */
|
|
142
|
-
export const hfDownloadCircuit = new HfDownloadCircuitBreaker();
|
|
96
|
+
export const hfDownloadCircuit = new CircuitBreaker({
|
|
97
|
+
failureThreshold: CB_FAILURE_THRESHOLD,
|
|
98
|
+
cooldownMs: CB_RESET_TIMEOUT_MS,
|
|
99
|
+
key: 'hf-download',
|
|
100
|
+
});
|
|
143
101
|
// ---------------------------------------------------------------------------
|
|
144
102
|
// Retry + timeout wrapper
|
|
145
103
|
// ---------------------------------------------------------------------------
|
|
@@ -171,10 +129,6 @@ export function withDownloadTimeout(fn, timeoutMs) {
|
|
|
171
129
|
});
|
|
172
130
|
});
|
|
173
131
|
}
|
|
174
|
-
/** @internal Async sleep (exposed for testing). */
|
|
175
|
-
export function sleep(ms) {
|
|
176
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
177
|
-
}
|
|
178
132
|
/**
|
|
179
133
|
* Retry wrapper for HuggingFace model downloads with per-attempt timeout and
|
|
180
134
|
* circuit-breaker protection.
|
|
@@ -214,37 +168,65 @@ export async function withHfDownloadRetry(fn, options = {}) {
|
|
|
214
168
|
? Math.min(Math.floor(envMaxAttempts), HF_MAX_ATTEMPTS_CAP)
|
|
215
169
|
: HF_MAX_ATTEMPTS;
|
|
216
170
|
const { maxAttempts = resolvedMaxAttempts, baseDelayMs = HF_BASE_DELAY_MS, timeoutMs = resolvedTimeout, circuit = hfDownloadCircuit, onRetry, } = options;
|
|
217
|
-
if (circuit.
|
|
218
|
-
|
|
171
|
+
if (circuit.getState() === 'open') {
|
|
172
|
+
// Compute remaining cooldown without consuming a probe permit.
|
|
173
|
+
const openedAt = circuit.getOpenedAt();
|
|
174
|
+
const secsUntilReset = openedAt !== null ? Math.ceil((circuit.getCooldownMs() - (Date.now() - openedAt)) / 1000) : 0;
|
|
219
175
|
throw new Error(`${CIRCUIT_OPEN_TAG}: HuggingFace download circuit is open after repeated network failures` +
|
|
220
176
|
(secsUntilReset > 0 ? ` — will reset in ~${secsUntilReset}s` : ''));
|
|
221
177
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
178
|
+
// Retry budget delegated to `withRetry` from gitnexus-shared. The
|
|
179
|
+
// HF-specific bits — per-attempt timeout, network-vs-non-network
|
|
180
|
+
// classification, circuit-breaker recording, onRetry callback — wire
|
|
181
|
+
// through the `isRetryable` callback. `circuitTripped` is the
|
|
182
|
+
// sentinel that lets us replace the final thrown error with a
|
|
183
|
+
// CIRCUIT_OPEN_TAG message when the breaker tripped mid-loop.
|
|
184
|
+
let circuitTripped = false;
|
|
185
|
+
try {
|
|
186
|
+
return await withRetry(async () => {
|
|
225
187
|
const result = await withDownloadTimeout(fn, timeoutMs);
|
|
226
188
|
circuit.recordSuccess();
|
|
227
189
|
return result;
|
|
190
|
+
}, {
|
|
191
|
+
maxAttempts,
|
|
192
|
+
baseDelayMs,
|
|
193
|
+
// Disable the cap to match the bespoke pure-exponential
|
|
194
|
+
// progression. With the default `HF_MAX_ATTEMPTS_CAP = 10` and
|
|
195
|
+
// `baseDelayMs = 2000`, the largest possible delay is
|
|
196
|
+
// `2000 * 2^9 = ~17 minutes` — bounded enough not to need a cap.
|
|
197
|
+
capDelayMs: Number.MAX_SAFE_INTEGER,
|
|
198
|
+
isRetryable: (err, attempt) => {
|
|
199
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
200
|
+
if (!isNetworkFetchError(error.message)) {
|
|
201
|
+
// Non-network error (e.g. CUDA unavailable) — propagate
|
|
202
|
+
// without retry. Use recordNeutral so the breaker's existing
|
|
203
|
+
// failure-count progress isn't reset by a non-network failure
|
|
204
|
+
// that says nothing about the CDN's health.
|
|
205
|
+
circuit.recordNeutral();
|
|
206
|
+
return { retry: false };
|
|
207
|
+
}
|
|
208
|
+
circuit.recordFailure();
|
|
209
|
+
if (circuit.getState() === 'open') {
|
|
210
|
+
// Circuit just tripped — fail fast, no more retries.
|
|
211
|
+
circuitTripped = true;
|
|
212
|
+
return { retry: false };
|
|
213
|
+
}
|
|
214
|
+
// Mirror the bespoke onRetry contract: fire only when there's
|
|
215
|
+
// actually a next attempt.
|
|
216
|
+
if (attempt + 1 < maxAttempts) {
|
|
217
|
+
onRetry?.(attempt + 1, maxAttempts, error);
|
|
218
|
+
}
|
|
219
|
+
return { retry: true };
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (circuitTripped) {
|
|
225
|
+
throw new Error(`${CIRCUIT_OPEN_TAG}: HuggingFace download circuit opened after ${CB_FAILURE_THRESHOLD} consecutive failures`);
|
|
228
226
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
throw lastError;
|
|
234
|
-
}
|
|
235
|
-
circuit.recordFailure();
|
|
236
|
-
if (circuit.isOpen()) {
|
|
237
|
-
// Circuit just tripped — fail fast, no more retries
|
|
238
|
-
throw new Error(`${CIRCUIT_OPEN_TAG}: HuggingFace download circuit opened after ${circuit.failureThreshold} consecutive failures`);
|
|
239
|
-
}
|
|
240
|
-
if (attempt < maxAttempts - 1) {
|
|
241
|
-
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
242
|
-
onRetry?.(attempt + 1, maxAttempts, lastError);
|
|
243
|
-
await sleep(delay);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
227
|
+
// All retries exhausted — rethrow the last network error so
|
|
228
|
+
// isNetworkFetchError patterns in the calling code still match and
|
|
229
|
+
// surface HF_ENDPOINT guidance.
|
|
230
|
+
throw err;
|
|
246
231
|
}
|
|
247
|
-
// All retries exhausted — throw the last network error so isNetworkFetchError
|
|
248
|
-
// patterns in the calling code still match and surface HF_ENDPOINT guidance.
|
|
249
|
-
throw lastError;
|
|
250
232
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shared fetch+retry logic for OpenAI-compatible /v1/embeddings endpoints.
|
|
5
5
|
* Imported by both the core embedder (batch) and MCP embedder (query).
|
|
6
|
+
*
|
|
7
|
+
* Network resilience is delegated to `resilientFetch` from
|
|
8
|
+
* `gitnexus-shared` — bounded retries with exponential-backoff jitter,
|
|
9
|
+
* `Retry-After` honored on 429, and an in-process circuit breaker that
|
|
10
|
+
* fails fast on a flapping endpoint. Per-attempt timeout is enforced
|
|
11
|
+
* via `AbortSignal.timeout` on the underlying fetch.
|
|
6
12
|
*/
|
|
7
13
|
/**
|
|
8
14
|
* Check whether HTTP embedding mode is active (env vars are set).
|
|
@@ -3,12 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shared fetch+retry logic for OpenAI-compatible /v1/embeddings endpoints.
|
|
5
5
|
* Imported by both the core embedder (batch) and MCP embedder (query).
|
|
6
|
+
*
|
|
7
|
+
* Network resilience is delegated to `resilientFetch` from
|
|
8
|
+
* `gitnexus-shared` — bounded retries with exponential-backoff jitter,
|
|
9
|
+
* `Retry-After` honored on 429, and an in-process circuit breaker that
|
|
10
|
+
* fails fast on a flapping endpoint. Per-attempt timeout is enforced
|
|
11
|
+
* via `AbortSignal.timeout` on the underlying fetch.
|
|
6
12
|
*/
|
|
13
|
+
import { CircuitOpenError, ResilientFetchExhaustedError, resilientFetch } from '../../_shared/index.js';
|
|
7
14
|
const HTTP_TIMEOUT_MS = 30_000;
|
|
8
15
|
const HTTP_MAX_RETRIES = 2;
|
|
9
16
|
const HTTP_RETRY_BACKOFF_MS = 1_000;
|
|
10
17
|
const HTTP_BATCH_SIZE = 64;
|
|
11
18
|
const DEFAULT_DIMS = 384;
|
|
19
|
+
const HTTP_BREAKER_KEY = 'embeddings-http';
|
|
12
20
|
/**
|
|
13
21
|
* Build config from the current process.env snapshot.
|
|
14
22
|
* Returns null when GITNEXUS_EMBEDDING_URL + GITNEXUS_EMBEDDING_MODEL are unset.
|
|
@@ -67,10 +75,10 @@ const safeUrl = (url) => {
|
|
|
67
75
|
* @param batchIndex - Logical batch number (for error context)
|
|
68
76
|
* @param attempt - Current retry attempt (internal)
|
|
69
77
|
*/
|
|
70
|
-
const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0
|
|
78
|
+
const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0) => {
|
|
71
79
|
let resp;
|
|
72
80
|
try {
|
|
73
|
-
resp = await
|
|
81
|
+
resp = await resilientFetch(url, {
|
|
74
82
|
method: 'POST',
|
|
75
83
|
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
76
84
|
headers: {
|
|
@@ -78,32 +86,28 @@ const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, attempt
|
|
|
78
86
|
Authorization: `Bearer ${apiKey}`,
|
|
79
87
|
},
|
|
80
88
|
body: JSON.stringify({ input: batch, model }),
|
|
89
|
+
}, {
|
|
90
|
+
breakerKey: HTTP_BREAKER_KEY,
|
|
91
|
+
retry: { maxAttempts: HTTP_MAX_RETRIES + 1, baseDelayMs: HTTP_RETRY_BACKOFF_MS },
|
|
81
92
|
});
|
|
82
93
|
}
|
|
83
94
|
catch (err) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
95
|
+
if (err instanceof CircuitOpenError) {
|
|
96
|
+
throw new Error(`Embedding endpoint circuit open (${safeUrl(url)}, batch ${batchIndex}): retry in ${Math.ceil(err.retryAfterMs / 1000)}s`);
|
|
97
|
+
}
|
|
98
|
+
if (err instanceof DOMException && err.name === 'TimeoutError') {
|
|
88
99
|
throw new Error(`Embedding request timed out after ${HTTP_TIMEOUT_MS}ms (${safeUrl(url)}, batch ${batchIndex})`);
|
|
89
100
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const delay = HTTP_RETRY_BACKOFF_MS * (attempt + 1);
|
|
93
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
94
|
-
return httpEmbedBatch(url, batch, model, apiKey, batchIndex, attempt + 1);
|
|
101
|
+
if (err instanceof ResilientFetchExhaustedError) {
|
|
102
|
+
throw new Error(`Embedding endpoint returned ${err.response.status} (${safeUrl(url)}, batch ${batchIndex})`);
|
|
95
103
|
}
|
|
96
104
|
const reason = err instanceof Error ? err.message : String(err);
|
|
97
105
|
throw new Error(`Embedding request failed (${safeUrl(url)}, batch ${batchIndex}): ${reason}`);
|
|
98
106
|
}
|
|
99
107
|
if (!resp.ok) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
104
|
-
return httpEmbedBatch(url, batch, model, apiKey, batchIndex, attempt + 1);
|
|
105
|
-
}
|
|
106
|
-
throw new Error(`Embedding endpoint returned ${status} (${safeUrl(url)}, batch ${batchIndex})`);
|
|
108
|
+
// resilientFetch already retried 5xx/429; any non-OK response here is
|
|
109
|
+
// a terminal client error (4xx other than 429).
|
|
110
|
+
throw new Error(`Embedding endpoint returned ${resp.status} (${safeUrl(url)}, batch ${batchIndex})`);
|
|
107
111
|
}
|
|
108
112
|
const data = (await resp.json());
|
|
109
113
|
return data.data;
|
|
@@ -589,12 +589,13 @@ export async function openBridgeDbReadOnly(groupDir) {
|
|
|
589
589
|
await new Promise((r) => setTimeout(r, delay));
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
592
|
+
// Strip CRLF from user-controlled strings before logging to close
|
|
593
|
+
// CodeQL js/log-injection. Pino's NDJSON serialization already
|
|
594
|
+
// JSON-escapes all values, but we sanitize here as a defence-in-depth
|
|
595
|
+
// measure so CodeQL can see the taint flow is broken.
|
|
596
|
+
const safeGroupDir = String(groupDir).replace(/[\r\n]/g, ' ');
|
|
597
|
+
const safeErrMsg = lastErr instanceof Error ? String(lastErr.message).replace(/[\r\n]/g, ' ') : undefined;
|
|
598
|
+
bridgeLogger.debug({ groupDir: safeGroupDir, errMsg: safeErrMsg, attempts: LBUG_OPEN_RETRY_ATTEMPTS }, 'openBridgeDbReadOnly gave up');
|
|
598
599
|
return null;
|
|
599
600
|
}
|
|
600
601
|
/* ------------------------------------------------------------------ */
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
const _require = createRequire(import.meta.url);
|
|
3
3
|
const yaml = _require('js-yaml');
|
|
4
|
-
const VALID_CONTRACT_TYPES = [
|
|
4
|
+
const VALID_CONTRACT_TYPES = [
|
|
5
|
+
'http',
|
|
6
|
+
'grpc',
|
|
7
|
+
'thrift',
|
|
8
|
+
'topic',
|
|
9
|
+
'lib',
|
|
10
|
+
'custom',
|
|
11
|
+
'include',
|
|
12
|
+
];
|
|
5
13
|
const VALID_ROLES = ['provider', 'consumer'];
|
|
14
|
+
// Defaults matter for backward compatibility: any group.yaml that omits a
|
|
15
|
+
// `detect.<type>` key inherits its value from this constant. Adding a new
|
|
16
|
+
// extractor that defaults to `true` silently changes the behavior of every
|
|
17
|
+
// existing group on the next sync. New extractors must default to `false`
|
|
18
|
+
// (opt-in) so operators consciously enable them via group.yaml.
|
|
19
|
+
//
|
|
20
|
+
// `includes`: opt-in. The C/C++ IncludeExtractor (PR #1156) ships disabled by
|
|
21
|
+
// default; enable with `detect.includes: true` for groups containing C/C++
|
|
22
|
+
// repos that need cross-repo header tracking.
|
|
6
23
|
const DEFAULT_DETECT = {
|
|
7
24
|
http: true,
|
|
8
25
|
grpc: true,
|
|
@@ -10,6 +27,7 @@ const DEFAULT_DETECT = {
|
|
|
10
27
|
topics: true,
|
|
11
28
|
shared_libs: true,
|
|
12
29
|
embedding_fallback: true,
|
|
30
|
+
includes: false,
|
|
13
31
|
workspace_deps: false,
|
|
14
32
|
};
|
|
15
33
|
const DEFAULT_MATCHING = {
|