From 3e1738f33356ea7f48ebac8efef9da05bf798ca0 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Thu, 17 Sep 2026 13:40:34 -0400 Subject: [PATCH 01/21] recreating dockerfile to donwload gemma model --- dockerfile | 66 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/dockerfile b/dockerfile index 9b24a466..61007a8c 100644 --- a/dockerfile +++ b/dockerfile @@ -1,18 +1,68 @@ -FROM node:20-alpine +ARG MODEL_REPO=unsloth/gemma-4-E2B-it-GGUF +ARG MODEL_FILE=gemma-4-E2B-it-Q4_K_M.gguf -RUN apk add --no-cache go git +# stage 1: builder +# install deps and build TS +FROM node:20-bookworm AS builder -RUN go install github.com/boyter/scc/v3@v3.4.0 +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* -WORKDIR /action +ENV NODE_LLAMA_CPP_SKIP_DOWNLOAD=true -COPY package*.json ./ +WORKDIR /app + +COPY package.json package-lock.json ./ RUN npm ci COPY . . +RUN npm run build + +# stage 2: model +# download the model GGUF and cache +FROM debian:bookworm-slim AS model +ARG MODEL_REPO +ARG MODEL_FILE + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /opt/models \ + && curl -fSL --retry 3 \ + "https://huggingface.co/${MODEL_REPO}/resolve/main/${MODEL_FILE}?download=true" \ + -o /opt/models/model.gguf \ + && test "$(stat -c%s /opt/models/model.gguf)" -gt 100000000 + +# stage 3: runtime +# final image the action actually runs +FROM node:20-bookworm-slim AS runtime + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# scc used for the laborHours estimate +RUN curl -fSL --retry 3 \ + "https://github.com/boyter/scc/releases/download/v3.4.0/scc_Linux_x86_64.tar.gz" \ + -o /tmp/scc.tar.gz \ + && tar -xzf /tmp/scc.tar.gz -C /usr/local/bin scc \ + && rm /tmp/scc.tar.gz \ + && chmod +x /usr/local/bin/scc \ + && scc --version + +WORKDIR /app + +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./package.json -RUN npm run bundle +COPY --from=model /opt/models/model.gguf /opt/models/model.gguf -ENV PATH="/root/go/bin:${PATH}" +ENV ACG_MODEL_PATH=/opt/models/model.gguf -ENTRYPOINT ["node", "/action/dist/index.js"] \ No newline at end of file +ENTRYPOINT ["node", "/app/dist/index.js"] \ No newline at end of file From fd3fea601613542af25b9878bdf49c9faf9a6865 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Thu, 17 Sep 2026 13:41:04 -0400 Subject: [PATCH 02/21] adding extra ignore statements, just incase --- .dockerignore | 2 +- .gitignore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index fc843b98..00651544 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,5 +6,5 @@ dist .gitignore .npmrc coverage -.env +.env* *.log \ No newline at end of file diff --git a/.gitignore b/.gitignore index 990a7db2..160b8020 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,7 @@ typings/ .yarn-integrity # dotenv environment variables file -.env +.env* .env.test # parcel-bundler cache (https://parceljs.org/) From 5479f79a84b20bb5094fa27d6165524775b5b936 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Fri, 18 Sep 2026 15:21:43 -0400 Subject: [PATCH 03/21] updating action.yml --- action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 3898cba1..cc6e1949 100644 --- a/action.yml +++ b/action.yml @@ -1,4 +1,4 @@ -name: Code.json Generator +name: automated-codejson-generator description: "Automatically generates and updates code.json metadata files for CMS repositories" author: DSACMS @@ -38,7 +38,7 @@ outputs: runs: using: docker - image: Dockerfile + image: docker://ghcr.io/dsacms/automated-codejson-generator:v2.0.0 branding: icon: "file-text" From ad2670ffedceccd49ceee11f6e308989eb4442f0 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Mon, 21 Sep 2026 10:14:06 -0400 Subject: [PATCH 04/21] doing an llm test --- dockerfile | 2 +- package-lock.json | 1353 ++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + rollup.config.ts | 2 + src/llm.ts | 84 +++ src/main.ts | 6 +- tsconfig.base.json | 1 + 7 files changed, 1400 insertions(+), 49 deletions(-) create mode 100644 src/llm.ts diff --git a/dockerfile b/dockerfile index 61007a8c..7e2c7c43 100644 --- a/dockerfile +++ b/dockerfile @@ -17,7 +17,7 @@ COPY package.json package-lock.json ./ RUN npm ci COPY . . -RUN npm run build +RUN npm run package # stage 2: model # download the model GGUF and cache diff --git a/package-lock.json b/package-lock.json index 9ede0a21..92e798aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@actions/core": "^3.0.0", "@octokit/action": "^7.0.0", "codejson-core": "^0.1.1", + "node-llama-cpp": "^3.21.1", "octokit-plugin-create-pull-request": "^6.0.1", "zod": "^4.2.1" }, @@ -2062,6 +2063,14 @@ "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==", "dev": true }, + "node_modules/@huggingface/jinja": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz", + "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==", + "engines": { + "node": ">=18" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -2140,6 +2149,17 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3356,6 +3376,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -3405,6 +3438,220 @@ } ] }, + "node_modules/@node-llama-cpp/linux-arm64": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.21.1.tgz", + "integrity": "sha512-eI3anSEGzJiwF2rpMpZVTdHzw47LztbvfvuRzlu/fPO18ROnBOd+75RPUBlV6qENPpmNtbov85+3VODwEwBurg==", + "cpu": [ + "arm64", + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-armv7l": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.21.1.tgz", + "integrity": "sha512-N/IaOXCQogfyDrlANRXbU2u+l0dbZ4BBqc3/Rv1xknHbRi/rX0rrOf5elPG8S9I5p9ebcU48HrRqk2d9O/LNHw==", + "cpu": [ + "arm", + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-riscv64": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-riscv64/-/linux-riscv64-3.21.1.tgz", + "integrity": "sha512-tjxegcxrw4heT3LIyPjXvCu7pFaD7e4ac1z+5KhWf/iS/9SSB9poqNyPXSkPibyzhTBB8BgDY47LgfBKqiu/xQ==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.21.1.tgz", + "integrity": "sha512-Totj6RTDMGo3FoK2yNUaz/oGmZD0olAqTrRluFUWmu0kW2i9TOzHSW0OWCAiGkzRCVX0WsU2CyUQo/W2NYtdPA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64-cuda": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.21.1.tgz", + "integrity": "sha512-idsE55VoD/FbvLhnBC9krUJaUoC0XnebseAAnCpm/brKf61HEbu+idgbb5eN6mxkjLSrHtILprJ0nDcUJjVrFg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64-cuda-ext": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.21.1.tgz", + "integrity": "sha512-1PuURowccBcks3CCoMe1mTHh5vD0V5bFHMUEgtbI/OSDjpP+Pdbn+lf42CGUrepTzR8kCH1HovQ7d8q6/vuIKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/linux-x64-vulkan": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.21.1.tgz", + "integrity": "sha512-flEeSzONQ6OIm5bYKLQf4eEnBzcC5WugFbyPUwkCqyyOFZ8pH0H9ulQ7ZNDLSIczeKneMRJornlyY1Hq7d3GDA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/mac-arm64-metal": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.21.1.tgz", + "integrity": "sha512-6MIDAZV7DXD9R1+jEXWJkFuolni3GATGE6mmuyFYc6ZmtOh9djAy15df0T0AXK3sCmVKL+fQcOAWZezxx4MvQQ==", + "cpu": [ + "arm64", + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/mac-x64": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.21.1.tgz", + "integrity": "sha512-Q4nyNmbcNiKygBZQZjX+qLk2w3YzoiFpLWsxNURQN7xjyUi2p2rWxjRa7Otg0FrjGf/ri4aQ+X3Uj5Y0zD06eg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-arm64": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.21.1.tgz", + "integrity": "sha512-/UDR7Y0B8iKfc/tqxBmWkcgRdZQ9Wrkwod7fYl4J6iZXcTIj286RkHvZSED7VMY67TrcUXVvxJVMNFtmjeYnDg==", + "cpu": [ + "arm64", + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.21.1.tgz", + "integrity": "sha512-JcIpCnra7bAd7JNWA8ehdn2DIRaTMuj+fBC8bw+5rKSNzszWFbuj2XA05c5QHovonDMBLN1ePXE/hcfZtzXoUQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64-cuda": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.21.1.tgz", + "integrity": "sha512-PLz70neFEMkQTlmJqG1ujUtCSgHasLPMxQ9rLkqQVmZYAXqkRj+45TIHCjdn3XqUeaoun0vrO9rRaxZ8NVq9jg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64-cuda-ext": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.21.1.tgz", + "integrity": "sha512-L2MxLVEYPDDe4O8EDWPmX6brldc9+Vt9hNoT/anj3pSnmrjkQcmaoKhHs6oP5qhEhb5Uog0rj6lgzNGtJO3p9Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@node-llama-cpp/win-x64-vulkan": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.21.1.tgz", + "integrity": "sha512-HJ237Azh5m4Sz+w3A1IU/fYLaSDOocskcfn2ILTgqTuuORCB86EMnu3Yt/XsNhRHTtEiwz1Q/E0mO4PLIbZLAA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@nolyfill/is-core-module": { "version": "1.0.39", "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", @@ -4056,6 +4303,145 @@ "@protobuf-ts/runtime": "^2.11.1" } }, + "node_modules/@reflink/reflink": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz", + "integrity": "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@reflink/reflink-darwin-arm64": "0.1.19", + "@reflink/reflink-darwin-x64": "0.1.19", + "@reflink/reflink-linux-arm64-gnu": "0.1.19", + "@reflink/reflink-linux-arm64-musl": "0.1.19", + "@reflink/reflink-linux-x64-gnu": "0.1.19", + "@reflink/reflink-linux-x64-musl": "0.1.19", + "@reflink/reflink-win32-arm64-msvc": "0.1.19", + "@reflink/reflink-win32-x64-msvc": "0.1.19" + } + }, + "node_modules/@reflink/reflink-darwin-arm64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-arm64/-/reflink-darwin-arm64-0.1.19.tgz", + "integrity": "sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-darwin-x64": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-x64/-/reflink-darwin-x64-0.1.19.tgz", + "integrity": "sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-arm64-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-gnu/-/reflink-linux-arm64-gnu-0.1.19.tgz", + "integrity": "sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-arm64-musl": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-musl/-/reflink-linux-arm64-musl-0.1.19.tgz", + "integrity": "sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-x64-gnu": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz", + "integrity": "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-linux-x64-musl": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz", + "integrity": "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-win32-arm64-msvc": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-arm64-msvc/-/reflink-win32-arm64-msvc-0.1.19.tgz", + "integrity": "sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@reflink/reflink-win32-x64-msvc": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-x64-msvc/-/reflink-win32-x64-msvc-0.1.19.tgz", + "integrity": "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.9", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", @@ -4484,6 +4870,19 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.52", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", @@ -4508,6 +4907,18 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@tinyhttp/content-disposition": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz", + "integrity": "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==", + "engines": { + "node": ">=12.17.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", @@ -5523,7 +5934,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "dev": true, "engines": { "node": ">=12" }, @@ -5765,9 +6175,17 @@ "node": ">= 0.4" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "dependencies": { @@ -6211,6 +6629,14 @@ "node": ">=0.2.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -6312,7 +6738,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -6329,6 +6754,19 @@ "node": ">=10" } }, + "node_modules/chmodrp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz", + "integrity": "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -6350,11 +6788,35 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6368,7 +6830,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -6377,7 +6838,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6391,14 +6851,12 @@ "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6412,7 +6870,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6424,7 +6881,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6437,6 +6893,50 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cmake-js": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz", + "integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==", + "dependencies": { + "debug": "^4.4.3", + "fs-extra": "^11.3.3", + "node-api-headers": "^1.8.0", + "rc": "1.2.8", + "semver": "^7.7.3", + "tar": "^7.5.6", + "url-join": "^4.0.1", + "which": "^6.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "cmake-js": "bin/cmake-js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cmake-js/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "engines": { + "node": ">=20" + } + }, + "node_modules/cmake-js/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6469,7 +6969,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6480,8 +6979,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/commander": { "version": "14.0.3", @@ -6709,7 +7207,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6774,7 +7271,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -6801,6 +7297,14 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6951,6 +7455,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/env-var": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz", + "integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==", + "engines": { + "node": ">=10" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -7168,7 +7680,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "engines": { "node": ">=6" } @@ -7680,6 +8191,11 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -7890,6 +8406,31 @@ "node": ">=16.0.0" } }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7968,6 +8509,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8051,11 +8605,21 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8243,8 +8807,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/handlebars": { "version": "4.7.9", @@ -8419,7 +8982,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, "engines": { "node": ">= 4" } @@ -8485,6 +9047,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -8499,6 +9066,137 @@ "node": ">= 0.4" } }, + "node_modules/ipull": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz", + "integrity": "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==", + "dependencies": { + "@tinyhttp/content-disposition": "^2.2.0", + "async-retry": "^1.3.3", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-spinners": "^2.9.2", + "commander": "^10.0.0", + "eventemitter3": "^5.0.1", + "filenamify": "^6.0.0", + "fs-extra": "^11.1.1", + "is-unicode-supported": "^2.0.0", + "lifecycle-utils": "^2.0.1", + "lodash.debounce": "^4.0.8", + "lowdb": "^7.0.1", + "pretty-bytes": "^6.1.0", + "pretty-ms": "^8.0.0", + "sleep-promise": "^9.1.0", + "slice-ansi": "^7.1.0", + "stdout-update": "^4.0.1", + "strip-ansi": "^7.1.0" + }, + "bin": { + "ipull": "dist/cli/cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/ido-pluto/ipull?sponsor=1" + }, + "optionalDependencies": { + "@reflink/reflink": "^0.1.16" + } + }, + "node_modules/ipull/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ipull/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/ipull/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/ipull/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipull/node_modules/lifecycle-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz", + "integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==" + }, + "node_modules/ipull/node_modules/parse-ms": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", + "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipull/node_modules/pretty-ms": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", + "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", + "dependencies": { + "parse-ms": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ipull/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -8684,7 +9382,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -8729,6 +9426,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -8898,6 +9606,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unsafe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", @@ -8962,8 +9681,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -12109,6 +12827,17 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jwt-decode": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", @@ -12203,6 +12932,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lifecycle-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-4.5.1.tgz", + "integrity": "sha512-RV7+jNHVQ2ktkkfgpjatWgVAeVJqW3RCw9rxAD9t8TMJr3RwJlMaKtXZmJZLth6Fxj8igP4uqkkZjsIMekuK2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/giladgd" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -12230,6 +12968,11 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -12242,6 +12985,35 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowdb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "dependencies": { + "steno": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -12355,6 +13127,17 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -12371,7 +13154,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12380,11 +13162,21 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -12409,8 +13201,24 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } }, "node_modules/napi-postinstall": { "version": "0.3.4", @@ -12439,6 +13247,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-api-headers": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.9.0.tgz", + "integrity": "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==" + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -12472,6 +13293,99 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-llama-cpp": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.21.1.tgz", + "integrity": "sha512-E3N0Ev2Va0wzydvXYFwM9Hvyo07PoWjt8T/9GMc43Cvt8YkV/NU/tYUgEMC1grTPv2o9PRjQi0G3Wb5P/awiMg==", + "hasInstallScript": true, + "dependencies": { + "@huggingface/jinja": "^0.5.9", + "async-retry": "^1.3.3", + "bytes": "^3.1.2", + "chalk": "^5.6.2", + "chmodrp": "^1.0.2", + "cmake-js": "^8.0.0", + "cross-spawn": "^7.0.6", + "env-var": "^7.5.0", + "filenamify": "^6.0.0", + "fs-extra": "^11.4.0", + "ignore": "^7.0.4", + "ipull": "^3.9.5", + "is-unicode-supported": "^2.1.0", + "lifecycle-utils": "^4.4.1", + "log-symbols": "^7.0.1", + "nanoid": "^5.1.6", + "node-addon-api": "^8.9.1", + "ora": "^9.3.0", + "pretty-ms": "^9.3.0", + "proper-lockfile": "^4.1.2", + "semver": "^7.7.1", + "simple-git": "^3.36.0", + "slice-ansi": "^8.0.0", + "stdout-update": "^4.0.1", + "strip-ansi": "^7.2.0", + "validate-npm-package-name": "^7.0.2", + "which": "^6.0.1", + "yargs": "^17.7.2" + }, + "bin": { + "nlc": "dist/cli/cli.js", + "node-llama-cpp": "dist/cli/cli.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/giladgd" + }, + "optionalDependencies": { + "@node-llama-cpp/linux-arm64": "3.21.1", + "@node-llama-cpp/linux-armv7l": "3.21.1", + "@node-llama-cpp/linux-riscv64": "3.21.1", + "@node-llama-cpp/linux-x64": "3.21.1", + "@node-llama-cpp/linux-x64-cuda": "3.21.1", + "@node-llama-cpp/linux-x64-cuda-ext": "3.21.1", + "@node-llama-cpp/linux-x64-vulkan": "3.21.1", + "@node-llama-cpp/mac-arm64-metal": "3.21.1", + "@node-llama-cpp/mac-x64": "3.21.1", + "@node-llama-cpp/win-arm64": "3.21.1", + "@node-llama-cpp/win-x64": "3.21.1", + "@node-llama-cpp/win-x64-cuda": "3.21.1", + "@node-llama-cpp/win-x64-cuda-ext": "3.21.1", + "@node-llama-cpp/win-x64-vulkan": "3.21.1" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/node-llama-cpp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-llama-cpp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -12673,6 +13587,53 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/own-keys": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", @@ -12766,6 +13727,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12803,7 +13775,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -12972,6 +13943,17 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -13004,6 +13986,20 @@ "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", "dev": true }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -13032,6 +14028,29 @@ "node": ">= 6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -13070,6 +14089,28 @@ "node": ">= 0.14.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -13182,7 +14223,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13256,6 +14296,43 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -13377,7 +14454,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -13435,7 +14511,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -13447,7 +14522,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -13528,7 +14602,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -13536,6 +14609,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -13551,6 +14640,51 @@ "node": ">=8" } }, + "node_modules/sleep-promise": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz", + "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==" + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13603,6 +14737,85 @@ "node": ">=8" } }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stdout-update": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz", + "integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==", + "dependencies": { + "ansi-escapes": "^6.2.0", + "ansi-styles": "^6.2.1", + "string-width": "^7.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/stdout-update/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stdout-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/stdout-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "node_modules/stdout-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/steno": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -13790,7 +15003,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "dependencies": { "ansi-regex": "^6.2.2" }, @@ -13907,6 +15119,21 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-stream": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", @@ -13919,6 +15146,14 @@ "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -14309,7 +15544,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14391,6 +15626,14 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -14477,6 +15720,11 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14503,6 +15751,14 @@ "node": ">=10.12.0" } }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -14516,7 +15772,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -14774,7 +16029,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -14804,7 +16058,6 @@ "version": "17.7.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -14822,7 +16075,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -14831,7 +16083,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -14839,14 +16090,12 @@ "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -14860,7 +16109,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -14889,6 +16137,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 7af587e6..a218ba35 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@actions/core": "^3.0.0", "@octokit/action": "^7.0.0", "codejson-core": "^0.1.1", + "node-llama-cpp": "^3.21.1", "octokit-plugin-create-pull-request": "^6.0.1", "zod": "^4.2.1" }, diff --git a/rollup.config.ts b/rollup.config.ts index 924635eb..1df0decd 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -6,6 +6,8 @@ import typescript from "@rollup/plugin-typescript"; const config = { input: "src/index.ts", + // native llama.cpp bindings load their own .node binaries, they can't be bundled + external: ["node-llama-cpp"], output: { esModule: true, file: "dist/index.js", diff --git a/src/llm.ts b/src/llm.ts new file mode 100644 index 00000000..2708fde7 --- /dev/null +++ b/src/llm.ts @@ -0,0 +1,84 @@ +import * as fs from "fs/promises"; +import { Logger } from "./types/Dependencies.js"; + +// the Dockerfile bakes the GGUF in and points this at it +const DEFAULT_MODEL_PATH = "/opt/models/model.gguf"; + +const SMOKE_TEST_PROMPT = + "In one sentence, what is a code.json file used for in a government software repository?"; + +export function getModelPath(): string { + return process.env.ACG_MODEL_PATH || DEFAULT_MODEL_PATH; +} + +// loads the baked in model, asks it one question and prints the answer to the action log. +// this is a smoke test, so a failure here is reported but never fails the action +export async function runModelSmokeTest(log: Logger): Promise { + const modelPath = getModelPath(); + const startedAt = Date.now(); + + log.info("===== Local model smoke test ====="); + log.info(`Model path: ${modelPath}`); + + // only the Docker image ships the GGUF, so outside it there is nothing to test. + // this also keeps the native binding out of unit test and local-action runs + if (!(await exists(modelPath))) { + log.info("No model found at that path, skipping the smoke test."); + return null; + } + + // imported lazily so environments without the native llama.cpp binary + // (local-action runs, unit tests) can still load this module + const { getLlama, LlamaChatSession } = await import("node-llama-cpp"); + + // "never" means use the prebuilt binary or fail loudly, no compiling inside the runner + const llama = await getLlama({ gpu: false, build: "never" }); + const model = await llama.loadModel({ modelPath, gpuLayers: 0 }); + + try { + // a small context keeps the runner's memory use down, this only needs to answer once + const context = await model.createContext({ contextSize: 512 }); + + try { + const session = new LlamaChatSession({ + contextSequence: context.getSequence(), + }); + + log.info(`Prompt: ${SMOKE_TEST_PROMPT}`); + + const answer = await session.prompt(SMOKE_TEST_PROMPT, { + maxTokens: 64, + temperature: 0, + }); + + const elapsedSeconds = ((Date.now() - startedAt) / 1000).toFixed(1); + + log.info(`Response: ${answer.trim()}`); + log.info(`Model responded in ${elapsedSeconds}s`); + log.info("===== Local model smoke test passed ====="); + + return answer.trim(); + } finally { + await context.dispose(); + } + } finally { + await model.dispose(); + } +} + +async function exists(filepath: string): Promise { + try { + await fs.access(filepath); + return true; + } catch { + return false; + } +} + +export async function tryModelSmokeTest(log: Logger): Promise { + try { + await runModelSmokeTest(log); + } catch (error) { + log.warning(`Local model smoke test failed: ${error}`); + } +} diff --git a/src/main.ts b/src/main.ts index 70beac0e..9a887103 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,8 +7,9 @@ import { import { Dependencies } from "./types/Dependencies.js"; import { createHelpers, Helpers } from "./helper.js"; import { createProductionDeps } from "./create-deps.js"; +import { tryModelSmokeTest } from "./llm.js"; -// gathers what can be observed about the repository right now so anything not an observation belongs to codejson-core +// gathers what can be observed about the repository right now so anything not an observation belongs to codejson-core async function getMetaData( helpers: Helpers, existingCodeJSON?: CodeJSON | null, @@ -67,6 +68,9 @@ export async function runWithDeps(deps: Dependencies): Promise { const helpers = createHelpers(deps); try { + // temporary: proves the model baked into the image loads and generates + await tryModelSmokeTest(deps.log); + const eventName = process.env.GITHUB_EVENT_NAME; if (eventName === "pull_request") { diff --git a/tsconfig.base.json b/tsconfig.base.json index 9af953f7..10777bac 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "noUnusedParameters": false, "pretty": true, "resolveJsonModule": true, + "skipLibCheck": true, "sourceMap": true, "strict": true, "strictNullChecks": true, From cecef0e2f2cace4eb94a0b5784047b1e7d70b385 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Mon, 21 Sep 2026 10:16:29 -0400 Subject: [PATCH 05/21] updating workflow file --- .github/workflows/update-codejson.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-codejson.yml b/.github/workflows/update-codejson.yml index 30be5507..5e16a8a7 100644 --- a/.github/workflows/update-codejson.yml +++ b/.github/workflows/update-codejson.yml @@ -22,7 +22,7 @@ jobs: - name: Update code.json id: generator - uses: DSACMS/automated-codejson-generator@main + uses: DSACMS/automated-codejson-generator@sachin/local-ai-feature with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} From 975de8b440c85596399534b0e6772a316b418338 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Mon, 21 Sep 2026 10:19:34 -0400 Subject: [PATCH 06/21] updating action for testing --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index cc6e1949..c956d5c4 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ outputs: runs: using: docker - image: docker://ghcr.io/dsacms/automated-codejson-generator:v2.0.0 + image: dockerfile branding: icon: "file-text" From 15693c4c5f71c29acf4957e2d1c8575da5a64d08 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Mon, 21 Sep 2026 10:26:01 -0400 Subject: [PATCH 07/21] fixing test --- src/llm.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/llm.ts b/src/llm.ts index 2708fde7..eee59ee0 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -31,13 +31,13 @@ export async function runModelSmokeTest(log: Logger): Promise { // (local-action runs, unit tests) can still load this module const { getLlama, LlamaChatSession } = await import("node-llama-cpp"); - // "never" means use the prebuilt binary or fail loudly, no compiling inside the runner + // no GPU on an Actions runner, and "never" means use the prebuilt binary + // rather than compiling llama.cpp from source inside the container const llama = await getLlama({ gpu: false, build: "never" }); - const model = await llama.loadModel({ modelPath, gpuLayers: 0 }); + const model = await llama.loadModel({ modelPath }); try { - // a small context keeps the runner's memory use down, this only needs to answer once - const context = await model.createContext({ contextSize: 512 }); + const context = await model.createContext(); try { const session = new LlamaChatSession({ @@ -46,13 +46,28 @@ export async function runModelSmokeTest(log: Logger): Promise { log.info(`Prompt: ${SMOKE_TEST_PROMPT}`); + // gemma reasons before it answers, and that reasoning is a "thought" segment + // that never appears in the returned text. capture it so a run that spends all + // its tokens thinking is visible in the log instead of looking like a blank answer + let thought = ""; + const answer = await session.prompt(SMOKE_TEST_PROMPT, { - maxTokens: 64, - temperature: 0, + // thinking has to stay bounded or a CPU only runner will sit here for minutes + budgets: { thoughtTokens: 200 }, + maxTokens: 500, + onResponseChunk: (chunk) => { + if (chunk.type === "segment" && chunk.segmentType === "thought") { + thought += chunk.text; + } + }, }); const elapsedSeconds = ((Date.now() - startedAt) / 1000).toFixed(1); + if (thought.trim() !== "") { + log.info(`Thought: ${thought.trim()}`); + } + log.info(`Response: ${answer.trim()}`); log.info(`Model responded in ${elapsedSeconds}s`); log.info("===== Local model smoke test passed ====="); From e3bad7bf26943a59159e54eb210f062d783d0422 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Tue, 22 Sep 2026 12:51:40 -0400 Subject: [PATCH 08/21] updating action file to use GHRC image --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index c956d5c4..f071cc98 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ outputs: runs: using: docker - image: dockerfile + image: docker://ghcr.io/dsacms/automated-codejson-generator:v2.0.1 branding: icon: "file-text" From 1829c2821267442c39e92c4fb2e78c1f43b91e69 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Mon, 21 Sep 2026 11:08:26 -0400 Subject: [PATCH 09/21] filling out more deterministic fields --- src/__tests__/unit/helper.test.ts | 177 ++++++++++++++++++++++++++++++ src/__tests__/unit/main.test.ts | 136 +++++++++++++++++++++++ src/helper.ts | 130 +++++++++++++++++++++- src/main.ts | 24 +++- 4 files changed, 460 insertions(+), 7 deletions(-) diff --git a/src/__tests__/unit/helper.test.ts b/src/__tests__/unit/helper.test.ts index cf2154ee..92f8796f 100644 --- a/src/__tests__/unit/helper.test.ts +++ b/src/__tests__/unit/helper.test.ts @@ -4,6 +4,9 @@ import { parsePackageJSON, parseRequirementsTxt, mergeReusedCode, + deriveRepositoryHost, + deriveUsageType, + deriveMaturityTier, } from "../../helper.js"; import { GOV_DEPENDENCIES, @@ -525,3 +528,177 @@ describe("GOV_DEPENDENCIES_PYPI integrity", () => { }, ); }); + +describe("deriveRepositoryHost", () => { + it.each([ + ["https://github.com/CMSgov/some-repo", "github.com/CMSgov"], + [ + "https://github.com/CMS-Enterprise/some-repo", + "github.com/CMS-Enterprise", + ], + [ + "https://github.com/Enterprise-CMCS/some-repo", + "github.com/Enterprise-CMCS", + ], + ["https://github.com/DSACMS/some-repo", "github.com/DSACMS"], + [ + "https://github.com/MeasureAuthoringTool/some-repo", + "github.com/MeasureAuthoringTool", + ], + ["https://github.cms.gov/some-org/some-repo", "github.cms.gov"], + ])("maps %s to %s", (url, expected) => { + expect(deriveRepositoryHost(url)).toBe(expected); + }); + + it("matches the organization regardless of case", () => { + expect(deriveRepositoryHost("https://github.com/dsacms/some-repo")).toBe( + "github.com/DSACMS", + ); + }); + + it("returns undefined for an organization outside the enum", () => { + expect( + deriveRepositoryHost("https://github.com/some-vendor/some-repo"), + ).toBeUndefined(); + }); + + it("returns undefined for a host outside the enum", () => { + expect( + deriveRepositoryHost("https://gitlab.com/DSACMS/some-repo"), + ).toBeUndefined(); + }); + + it("returns undefined when the URL has no organization segment", () => { + expect(deriveRepositoryHost("https://github.com/")).toBeUndefined(); + }); + + it("returns undefined for a value that is not a URL", () => { + expect(deriveRepositoryHost("")).toBeUndefined(); + expect(deriveRepositoryHost("private")).toBeUndefined(); + }); +}); + +describe("deriveUsageType", () => { + it("treats a public repository as open source", () => { + expect(deriveUsageType("public")).toEqual(["openSource"]); + }); + + // every remaining usageType value is an exemption needing a written justification + it("leaves a private repository for a human to classify", () => { + expect(deriveUsageType("private")).toEqual([]); + expect(deriveUsageType(undefined)).toEqual([]); + }); +}); + +describe("deriveMaturityTier", () => { + const tier3Files = new Set([ + "LICENSE", + "README", + "COMMUNITY", + "SECURITY", + "CONTRIBUTING", + "CODE_OF_CONDUCT", + ]); + + it("reports tier 4 when the repository documents its governance", () => { + expect( + deriveMaturityTier(new Set([...tier3Files, "GOVERNANCE"]), "public"), + ).toBe(4); + }); + + it("reports tier 3 for a public repository open to contribution", () => { + expect(deriveMaturityTier(tier3Files, "public")).toBe(3); + }); + + // tiers 2 and 3 require the same documents, so visibility is the only separator + it("reports tier 2 for the same documents in a private repository", () => { + expect(deriveMaturityTier(tier3Files, "private")).toBe(2); + }); + + it("reports tier 1 when only a security policy is present", () => { + expect( + deriveMaturityTier(new Set(["LICENSE", "README", "SECURITY"]), "public"), + ).toBe(1); + }); + + it("reports tier 0 when the distinguishing documents are absent", () => { + expect(deriveMaturityTier(new Set(["LICENSE", "README"]), "public")).toBe( + 0, + ); + expect(deriveMaturityTier(new Set(), "public")).toBe(0); + }); + + // a contributing guide on its own does not reach the tier 2 checklist + it("requires both a contributing guide and a code of conduct", () => { + expect( + deriveMaturityTier(new Set(["LICENSE", "CONTRIBUTING"]), "public"), + ).toBe(0); + }); +}); + +describe("createHelpers - detectCommunityFiles", () => { + it("reports the documents the repository ships", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/LICENSE": "CC0", + "/github/workspace/README.md": "# Project", + "/github/workspace/CONTRIBUTING.md": "how to contribute", + }), + }); + + const files = await createHelpers(deps).detectCommunityFiles(); + + expect(files).toEqual(new Set(["LICENSE", "README", "CONTRIBUTING"])); + }); + + it("accepts community health files stored under .github", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ + "/github/workspace/.github/SECURITY.md": "report issues here", + "/github/workspace/.github/CODE_OF_CONDUCT.md": "be kind", + }), + }); + + const files = await createHelpers(deps).detectCommunityFiles(); + + expect(files).toEqual(new Set(["SECURITY", "CODE_OF_CONDUCT"])); + }); + + it("accepts alternate LICENSE extensions", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ "/github/workspace/LICENSE.md": "CC0" }), + }); + + const files = await createHelpers(deps).detectCommunityFiles(); + + expect(files).toEqual(new Set(["LICENSE"])); + }); + + it("returns an empty set for a repository with no documents", async () => { + const files = await createHelpers(createMockDeps()).detectCommunityFiles(); + + expect(files).toEqual(new Set()); + }); +}); + +describe("createHelpers - readREADME", () => { + it("reads README.md", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ "/github/workspace/README.md": "# Project" }), + }); + + expect(await createHelpers(deps).readREADME()).toBe("# Project"); + }); + + it("falls back to other README spellings", async () => { + const deps = createMockDeps({ + readFile: readFileFrom({ "/github/workspace/README.rst": "Project" }), + }); + + expect(await createHelpers(deps).readREADME()).toBe("Project"); + }); + + it("returns null when the repository has no README", async () => { + expect(await createHelpers(createMockDeps()).readREADME()).toBeNull(); + }); +}); diff --git a/src/__tests__/unit/main.test.ts b/src/__tests__/unit/main.test.ts index cab0408e..6a2e217b 100644 --- a/src/__tests__/unit/main.test.ts +++ b/src/__tests__/unit/main.test.ts @@ -159,6 +159,142 @@ describe("getMetaData", () => { expect(result.tags).toEqual(["test", "automation"]); }); + + it("marks a public repository as open source", async () => { + const result = await getMetaData(createHelpers(createMockDeps()), null); + + expect(result.permissions?.usageType).toEqual(["openSource"]); + }); + + it("preserves an existing usageType rather than reclassifying it", async () => { + const existing = { + ...validCodeJSON, + permissions: { + licenses: [{ name: "MIT", URL: "https://example.gov/license" }], + usageType: ["exemptByAgencyMission"], + exemptionText: "sharing would risk agency operations", + }, + } as any; + + const result = await getMetaData(createHelpers(createMockDeps()), existing); + + expect(result.permissions?.usageType).toEqual(["exemptByAgencyMission"]); + }); + + // usageType is the only part of permissions the action observes + it("carries existing licenses and exemption text through untouched", async () => { + const existing = { + ...validCodeJSON, + permissions: { + licenses: [{ name: "MIT", URL: "https://example.gov/license" }], + usageType: [], + exemptionText: "left over from an earlier exemption", + }, + } as any; + + const result = await getMetaData(createHelpers(createMockDeps()), existing); + + expect(result.permissions?.licenses).toEqual([ + { name: "MIT", URL: "https://example.gov/license" }, + ]); + expect(result.permissions?.exemptionText).toBe( + "left over from an earlier exemption", + ); + expect(result.permissions?.usageType).toEqual(["openSource"]); + }); + + it("defaults licenses to the draft baseline for a new repository", async () => { + const result = await getMetaData(createHelpers(createMockDeps()), null); + + expect(result.permissions?.licenses).toEqual([ + { name: "CC0-1.0", URL: "" }, + ]); + }); + + it("derives the maturity tier from the community health files present", async () => { + const deps = createMockDeps({ + readFile: jest.fn((filepath: string) => + filepath === "/github/workspace/GOVERNANCE.md" + ? Promise.resolve("how this project is governed") + : Promise.reject(new Error("ENOENT")), + ), + }); + + const result = await getMetaData(createHelpers(deps), null); + + expect(result.maturityModelTier).toBe(4); + }); + + it("preserves a maturity tier a human has already set", async () => { + const existing = { ...validCodeJSON, maturityModelTier: 2 } as any; + + const result = await getMetaData(createHelpers(createMockDeps()), existing); + + expect(result.maturityModelTier).toBe(2); + }); + + // every file this action has generated before carries the baseline 0, so a zero has to + // count as unset or the field could never be filled on a later run + it("treats a maturity tier of 0 as unset", async () => { + const deps = createMockDeps({ + readFile: jest.fn((filepath: string) => + filepath === "/github/workspace/SECURITY.md" + ? Promise.resolve("report issues here") + : Promise.reject(new Error("ENOENT")), + ), + }); + const existing = { ...validCodeJSON, maturityModelTier: 0 } as any; + + const result = await getMetaData(createHelpers(deps), existing); + + expect(result.maturityModelTier).toBe(1); + }); + + it("omits repositoryHost when the repository is not under a known organization", async () => { + const result = await getMetaData(createHelpers(createMockDeps()), null); + + expect(result).not.toHaveProperty("repositoryHost"); + }); + + it("derives repositoryHost from a recognised organization", async () => { + const dsacmsOctokit = createMockOctokit({ + rest: { + repos: { + get: jest.fn().mockResolvedValue({ + data: { + name: "test-repo", + description: "A test repository", + html_url: "https://github.com/DSACMS/test-repo", + private: false, + forks_count: 5, + topics: [], + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-06-01T00:00:00Z", + default_branch: "main", + fork: false, + parent: null, + }, + }), + }, + }, + }); + + const deps = createMockDeps({ octokit: dsacmsOctokit }); + const result = await getMetaData(createHelpers(deps), null); + + expect(result.repositoryHost).toBe("github.com/DSACMS"); + }); + + it("preserves an existing repositoryHost the URL cannot confirm", async () => { + const existing = { + ...validCodeJSON, + repositoryHost: "CCSQ GitHub", + } as any; + + const result = await getMetaData(createHelpers(createMockDeps()), existing); + + expect(result.repositoryHost).toBe("CCSQ GitHub"); + }); }); describe("runWithDeps", () => { diff --git a/src/helper.ts b/src/helper.ts index cd4b4039..8a4cffac 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -8,6 +8,25 @@ import { } from "./gov-dependencies.js"; const HOURS_PER_MONTH = 730.001; +const WORKSPACE_PATH = "/github/workspace"; + +const COMMUNITY_FILE_CANDIDATES: Record = { + LICENSE: ["LICENSE", "LICENSE.md", "LICENSE.txt"], + README: ["README.md", "README.rst", "README.txt", "README"], + COMMUNITY: ["COMMUNITY.md", ".github/COMMUNITY.md"], + SECURITY: ["SECURITY.md", ".github/SECURITY.md"], + CONTRIBUTING: ["CONTRIBUTING.md", ".github/CONTRIBUTING.md"], + CODE_OF_CONDUCT: ["CODE_OF_CONDUCT.md", ".github/CODE_OF_CONDUCT.md"], + GOVERNANCE: ["GOVERNANCE.md", ".github/GOVERNANCE.md"], +}; + +const GITHUB_ORG_HOSTS: Record = { + cmsgov: "github.com/CMSgov", + "cms-enterprise": "github.com/CMS-Enterprise", + "enterprise-cmcs": "github.com/Enterprise-CMCS", + dsacms: "github.com/DSACMS", + measureauthoringtool: "github.com/MeasureAuthoringTool", +}; // both write paths go through here so the committed file is byte-identical either way function serializeCodeJSON(codeJSON: CodeJSON): string { @@ -23,11 +42,15 @@ export function createHelpers(deps: Dependencies) { //=============================================== async function calculateMetaData(): Promise> { try { - const [laborHours, basicInfo, version] = await Promise.all([ - getLaborHours(), - getBasicInfo(), - getVersion(), - ]); + const [laborHours, basicInfo, version, communityFiles] = + await Promise.all([ + getLaborHours(), + getBasicInfo(), + getVersion(), + detectCommunityFiles(), + ]); + + const repositoryHost = deriveRepositoryHost(basicInfo.url); return { name: basicInfo.title, @@ -46,6 +69,11 @@ export function createHelpers(deps: Dependencies) { created: basicInfo.date.created, lastModified: basicInfo.date.lastModified, }, + maturityModelTier: deriveMaturityTier( + communityFiles, + basicInfo.repositoryVisibility, + ), + ...(repositoryHost ? { repositoryHost } : {}), }; } catch (error) { log.error(`Failed to calculate meta data: ${error}`); @@ -55,7 +83,10 @@ export function createHelpers(deps: Dependencies) { async function getVersion(): Promise { try { - const release = await octokit.rest.repos.getLatestRelease({ owner, repo }); + const release = await octokit.rest.repos.getLatestRelease({ + owner, + repo, + }); const versionFromRelease = normalizeVersionString(release.data.tag_name); if (versionFromRelease !== "") { @@ -180,6 +211,39 @@ export function createHelpers(deps: Dependencies) { } } + async function detectCommunityFiles(): Promise> { + const found = await Promise.all( + Object.entries(COMMUNITY_FILE_CANDIDATES).map( + async ([document, candidates]) => { + for (const candidate of candidates) { + const content = await readManifest( + `${WORKSPACE_PATH}/${candidate}`, + ); + if (content !== null) { + return document; + } + } + return null; + }, + ), + ); + + return new Set( + found.filter((document): document is string => document !== null), + ); + } + + async function readREADME(): Promise { + for (const candidate of COMMUNITY_FILE_CANDIDATES.README) { + const content = await readManifest(`${WORKSPACE_PATH}/${candidate}`); + if (content !== null) { + return content; + } + } + + return null; + } + //=============================================== // Fork Upstream //=============================================== @@ -378,6 +442,8 @@ export function createHelpers(deps: Dependencies) { return { calculateMetaData, + detectCommunityFiles, + readREADME, detectReusedCode, detectForkParent, mergeReusedCode, @@ -444,6 +510,58 @@ export function mergeReusedCode( return merged; } +// reads the hosting organization off the repository URL +export function deriveRepositoryHost( + repositoryURL: string, +): CodeJSON["repositoryHost"] | undefined { + let url: URL; + try { + url = new URL(repositoryURL); + } catch { + return undefined; + } + + if (url.hostname === "github.cms.gov") { + return "github.cms.gov"; + } + + if (url.hostname !== "github.com" && url.hostname !== "www.github.com") { + return undefined; + } + + const organization = url.pathname.split("/").filter(Boolean)[0]; + if (!organization) { + return undefined; + } + + return GITHUB_ORG_HOSTS[organization.toLowerCase()]; +} + +export function deriveUsageType( + repositoryVisibility: "public" | "private" | undefined, +): CodeJSON["permissions"]["usageType"] { + return repositoryVisibility === "public" ? ["openSource"] : []; +} + +export function deriveMaturityTier( + presentFiles: Set, + repositoryVisibility: "public" | "private" | undefined, +): 0 | 1 | 2 | 3 | 4 { + if (presentFiles.has("GOVERNANCE")) { + return 4; + } + + if (presentFiles.has("CONTRIBUTING") && presentFiles.has("CODE_OF_CONDUCT")) { + return repositoryVisibility === "public" ? 3 : 2; + } + + if (presentFiles.has("SECURITY")) { + return 1; + } + + return 0; +} + // combines repository topics with existing manually added tags, de-duped export function mergeTags( repositoryTopics: string[] = [], diff --git a/src/main.ts b/src/main.ts index 9a887103..93e51fcc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,12 @@ import { CodeJSON, assembleDraft, + draftBaseline, droppedFields, validateCodeJSON, } from "./codejson.js"; import { Dependencies } from "./types/Dependencies.js"; -import { createHelpers, Helpers } from "./helper.js"; +import { createHelpers, deriveUsageType, Helpers } from "./helper.js"; import { createProductionDeps } from "./create-deps.js"; import { tryModelSmokeTest } from "./llm.js"; @@ -32,6 +33,24 @@ async function getMetaData( existingCodeJSON?.tags ?? [], ); + const repositoryHost = existingCodeJSON?.repositoryHost || partialCodeJSON.repositoryHost; + + const maturityModelTier = existingCodeJSON?.maturityModelTier || partialCodeJSON.maturityModelTier || 0; + + const existingUsageType = existingCodeJSON?.permissions?.usageType ?? []; + + const permissions: CodeJSON["permissions"] = { + licenses: + existingCodeJSON?.permissions?.licenses ?? + draftBaseline.permissions?.licenses ?? + [], + usageType: + existingUsageType.length > 0 + ? existingUsageType + : deriveUsageType(partialCodeJSON.repositoryVisibility), + exemptionText: existingCodeJSON?.permissions?.exemptionText ?? "", + }; + // detect the fork upstream and government-made dependencies, then merge with any existing reusedCode const [forkParent, detectedDeps] = await Promise.all([ helpers.detectForkParent(), @@ -59,6 +78,9 @@ async function getMetaData( lastModified: partialCodeJSON.date?.lastModified ?? "", }, reusedCode, + permissions, + maturityModelTier, + ...(repositoryHost ? { repositoryHost } : {}), }; } From 0e37d476e2c93d148a973aca77a127bf067fa009 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Tue, 22 Sep 2026 13:09:36 -0400 Subject: [PATCH 10/21] adding llm model integration logic --- src/__tests__/unit/llm.test.ts | 221 +++++++++++++++++++++++++++++++++ src/llm.ts | 106 +++++++++------- src/main.ts | 12 +- 3 files changed, 290 insertions(+), 49 deletions(-) create mode 100644 src/__tests__/unit/llm.test.ts diff --git a/src/__tests__/unit/llm.test.ts b/src/__tests__/unit/llm.test.ts new file mode 100644 index 00000000..92fe6888 --- /dev/null +++ b/src/__tests__/unit/llm.test.ts @@ -0,0 +1,221 @@ +import { + describe, + it, + expect, + jest, + beforeEach, + afterEach, + beforeAll, + afterAll, +} from "@jest/globals"; +import * as fs from "fs/promises"; +import * as os from "os"; +import * as path from "path"; +import { createMockLogger } from "../fixtures/mock-deps.js"; + +// the real binding needs a multi-gigabyte GGUF and a compiled .node binary, so the +// module is faked here. withModel's own early return covers the no-model case below +const promptMock = jest.fn(async () => "generated text"); +const contextDisposeMock = jest.fn(async () => undefined); +const modelDisposeMock = jest.fn(async () => undefined); +const createContextMock = jest.fn(async () => ({ + getSequence: () => ({}), + dispose: contextDisposeMock, +})); +const createGrammarMock = jest.fn(async () => ({ + parse: (text: string) => JSON.parse(text), +})); +const loadModelMock = jest.fn(async () => ({ + createContext: createContextMock, + dispose: modelDisposeMock, +})); +const getLlamaMock = jest.fn(async () => ({ + loadModel: loadModelMock, + createGrammarForJsonSchema: createGrammarMock, +})); + +jest.unstable_mockModule("node-llama-cpp", () => ({ + getLlama: getLlamaMock, + LlamaChatSession: class { + async prompt(text: string, options: unknown) { + return promptMock(text, options); + } + }, +})); + +const { withModel, getModelPath } = await import("../../llm.js"); + +describe("getModelPath", () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("defaults to the path the Dockerfile bakes the model into", () => { + delete process.env.ACG_MODEL_PATH; + expect(getModelPath()).toBe("/opt/models/model.gguf"); + }); + + it("honours an override", () => { + process.env.ACG_MODEL_PATH = "/tmp/other.gguf"; + expect(getModelPath()).toBe("/tmp/other.gguf"); + }); +}); + +describe("withModel without a model present", () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("returns null and never runs the callback", async () => { + process.env.ACG_MODEL_PATH = "/nonexistent/model.gguf"; + const log = createMockLogger(); + const run = jest.fn(); + + expect(await withModel(log, run as any)).toBeNull(); + expect(run).not.toHaveBeenCalled(); + expect(log.info).toHaveBeenCalledWith( + "No model found at /nonexistent/model.gguf, skipping generation.", + ); + }); +}); + +describe("withModel with a model present", () => { + const originalEnv = process.env; + let modelFile: string; + let temporaryDirectory: string; + + beforeAll(async () => { + temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "acg-llm-")); + modelFile = path.join(temporaryDirectory, "model.gguf"); + await fs.writeFile(modelFile, "not a real model"); + }); + + afterAll(async () => { + await fs.rm(temporaryDirectory, { recursive: true, force: true }); + }); + + beforeEach(() => { + process.env = { ...originalEnv, ACG_MODEL_PATH: modelFile }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("returns whatever the callback returns", async () => { + const result = await withModel(createMockLogger(), async (session) => + session.generateText("describe this project"), + ); + + expect(result).toBe("generated text"); + expect(promptMock).toHaveBeenCalledWith( + "describe this project", + expect.objectContaining({ maxTokens: 1500 }), + ); + }); + + it("loads the model once and disposes it when the run finishes", async () => { + await withModel(createMockLogger(), async (session) => { + await session.generateText("first"); + await session.generateText("second"); + }); + + expect(loadModelMock).toHaveBeenCalledTimes(1); + expect(modelDisposeMock).toHaveBeenCalledTimes(1); + }); + + // a shared session would carry the first answer into the second prompt as history + it("gives each prompt its own context and disposes it", async () => { + await withModel(createMockLogger(), async (session) => { + await session.generateText("first"); + await session.generateText("second"); + }); + + expect(createContextMock).toHaveBeenCalledTimes(2); + expect(contextDisposeMock).toHaveBeenCalledTimes(2); + }); + + it("disposes the model even when the run throws", async () => { + await expect( + withModel(createMockLogger(), async () => { + throw new Error("generation blew up"); + }), + ).rejects.toThrow("generation blew up"); + + expect(modelDisposeMock).toHaveBeenCalledTimes(1); + }); + + it("disposes the context even when a prompt throws", async () => { + promptMock.mockRejectedValueOnce(new Error("aborted")); + + await expect( + withModel(createMockLogger(), async (session) => + session.generateText("first"), + ), + ).rejects.toThrow("aborted"); + + expect(contextDisposeMock).toHaveBeenCalledTimes(1); + }); + + it("passes a deadline so a stuck generation cannot hold the job open", async () => { + await withModel(createMockLogger(), async (session) => + session.generateText("first"), + ); + + const options = promptMock.mock.calls[0][1] as any; + expect(options.signal).toBeInstanceOf(AbortSignal); + }); + + it("honours a caller supplied token budget", async () => { + await withModel(createMockLogger(), async (session) => + session.generateText("first", 1200), + ); + + expect(promptMock).toHaveBeenCalledWith( + "first", + expect.objectContaining({ maxTokens: 1200 }), + ); + }); + + it("constrains JSON generation with a grammar built from the schema", async () => { + const schema = { + type: "object", + properties: { softwareType: { enum: ["library", "addon"] } }, + } as const; + + promptMock.mockResolvedValueOnce('{"softwareType":"library"}'); + + const result = await withModel(createMockLogger(), async (session) => + session.generateJSON("classify this", schema), + ); + + expect(createGrammarMock).toHaveBeenCalledWith(schema); + expect(promptMock).toHaveBeenCalledWith( + "classify this", + expect.objectContaining({ grammar: expect.anything() }), + ); + expect(result).toEqual({ softwareType: "library" }); + }); + + it("reports how long generation took", async () => { + const log = createMockLogger(); + + await withModel(log, async (session) => session.generateText("first")); + + expect(log.info).toHaveBeenCalledWith( + expect.stringContaining("Model responded in"), + ); + }); +}); diff --git a/src/llm.ts b/src/llm.ts index eee59ee0..3cf6c7df 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -1,42 +1,55 @@ import * as fs from "fs/promises"; +import type { + GbnfJsonSchema, + GbnfJsonSchemaToType, + LlamaGrammar, +} from "node-llama-cpp"; import { Logger } from "./types/Dependencies.js"; -// the Dockerfile bakes the GGUF in and points this at it const DEFAULT_MODEL_PATH = "/opt/models/model.gguf"; - -const SMOKE_TEST_PROMPT = - "In one sentence, what is a code.json file used for in a government software repository?"; +const TIMEOUT_MS = 900_000; +const DEFAULT_MAX_TOKENS = 1500; +const THOUGHT_TOKEN_BUDGET = 400; export function getModelPath(): string { return process.env.ACG_MODEL_PATH || DEFAULT_MODEL_PATH; } -// loads the baked in model, asks it one question and prints the answer to the action log. -// this is a smoke test, so a failure here is reported but never fails the action -export async function runModelSmokeTest(log: Logger): Promise { - const modelPath = getModelPath(); - const startedAt = Date.now(); +export interface ModelSession { + generateText(prompt: string, maxTokens?: number): Promise; - log.info("===== Local model smoke test ====="); - log.info(`Model path: ${modelPath}`); + generateJSON( + prompt: string, + schema: Schema, + maxTokens?: number, + ): Promise>; +} + +export type ModelRunner = ( + log: Logger, + run: (session: ModelSession) => Promise, +) => Promise; + +export const withModel: ModelRunner = async (log, run) => { + const modelPath = getModelPath(); - // only the Docker image ships the GGUF, so outside it there is nothing to test. - // this also keeps the native binding out of unit test and local-action runs if (!(await exists(modelPath))) { - log.info("No model found at that path, skipping the smoke test."); + log.info(`No model found at ${modelPath}, skipping generation.`); return null; } - // imported lazily so environments without the native llama.cpp binary - // (local-action runs, unit tests) can still load this module const { getLlama, LlamaChatSession } = await import("node-llama-cpp"); - // no GPU on an Actions runner, and "never" means use the prebuilt binary - // rather than compiling llama.cpp from source inside the container const llama = await getLlama({ gpu: false, build: "never" }); const model = await llama.loadModel({ modelPath }); - - try { + const deadline = AbortSignal.timeout(TIMEOUT_MS); + + async function prompt( + text: string, + maxTokens: number, + grammar?: LlamaGrammar, + ): Promise { + const startedAt = Date.now(); const context = await model.createContext(); try { @@ -44,17 +57,13 @@ export async function runModelSmokeTest(log: Logger): Promise { contextSequence: context.getSequence(), }); - log.info(`Prompt: ${SMOKE_TEST_PROMPT}`); - - // gemma reasons before it answers, and that reasoning is a "thought" segment - // that never appears in the returned text. capture it so a run that spends all - // its tokens thinking is visible in the log instead of looking like a blank answer let thought = ""; - const answer = await session.prompt(SMOKE_TEST_PROMPT, { - // thinking has to stay bounded or a CPU only runner will sit here for minutes - budgets: { thoughtTokens: 200 }, - maxTokens: 500, + const answer = await session.prompt(text, { + budgets: { thoughtTokens: THOUGHT_TOKEN_BUDGET }, + maxTokens, + signal: deadline, + grammar, onResponseChunk: (chunk) => { if (chunk.type === "segment" && chunk.segmentType === "thought") { thought += chunk.text; @@ -63,23 +72,42 @@ export async function runModelSmokeTest(log: Logger): Promise { }); const elapsedSeconds = ((Date.now() - startedAt) / 1000).toFixed(1); + const trimmed = answer.trim(); if (thought.trim() !== "") { - log.info(`Thought: ${thought.trim()}`); + log.debug(`Model thought for ${thought.trim().length} characters`); } - log.info(`Response: ${answer.trim()}`); - log.info(`Model responded in ${elapsedSeconds}s`); - log.info("===== Local model smoke test passed ====="); + log.info( + `Model responded in ${elapsedSeconds}s with ${trimmed.length} characters`, + ); - return answer.trim(); + return trimmed; } finally { await context.dispose(); } + } + + try { + return await run({ + generateText: (text, maxTokens = DEFAULT_MAX_TOKENS) => + prompt(text, maxTokens), + + generateJSON: async ( + text: string, + schema: Schema, + maxTokens: number = DEFAULT_MAX_TOKENS, + ): Promise> => { + const grammar = await llama.createGrammarForJsonSchema(schema); + const answer = await prompt(text, maxTokens, grammar); + + return grammar.parse(answer) as GbnfJsonSchemaToType; + }, + }); } finally { await model.dispose(); } -} +}; async function exists(filepath: string): Promise { try { @@ -89,11 +117,3 @@ async function exists(filepath: string): Promise { return false; } } - -export async function tryModelSmokeTest(log: Logger): Promise { - try { - await runModelSmokeTest(log); - } catch (error) { - log.warning(`Local model smoke test failed: ${error}`); - } -} diff --git a/src/main.ts b/src/main.ts index 93e51fcc..662f4214 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,6 @@ import { import { Dependencies } from "./types/Dependencies.js"; import { createHelpers, deriveUsageType, Helpers } from "./helper.js"; import { createProductionDeps } from "./create-deps.js"; -import { tryModelSmokeTest } from "./llm.js"; // gathers what can be observed about the repository right now so anything not an observation belongs to codejson-core async function getMetaData( @@ -33,9 +32,13 @@ async function getMetaData( existingCodeJSON?.tags ?? [], ); - const repositoryHost = existingCodeJSON?.repositoryHost || partialCodeJSON.repositoryHost; + const repositoryHost = + existingCodeJSON?.repositoryHost || partialCodeJSON.repositoryHost; - const maturityModelTier = existingCodeJSON?.maturityModelTier || partialCodeJSON.maturityModelTier || 0; + const maturityModelTier = + existingCodeJSON?.maturityModelTier || + partialCodeJSON.maturityModelTier || + 0; const existingUsageType = existingCodeJSON?.permissions?.usageType ?? []; @@ -90,9 +93,6 @@ export async function runWithDeps(deps: Dependencies): Promise { const helpers = createHelpers(deps); try { - // temporary: proves the model baked into the image loads and generates - await tryModelSmokeTest(deps.log); - const eventName = process.env.GITHUB_EVENT_NAME; if (eventName === "pull_request") { From 6105d20e6fefb303c1bee5d16a5459af949be753 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 15:01:49 -0400 Subject: [PATCH 11/21] creating logic to connect code.json to model --- src/__tests__/unit/enrich.test.ts | 480 ++++++++++++++++++++++++++++++ src/__tests__/unit/llm.test.ts | 40 +++ src/enrich.ts | 332 +++++++++++++++++++++ src/llm.ts | 17 +- 4 files changed, 866 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/unit/enrich.test.ts create mode 100644 src/enrich.ts diff --git a/src/__tests__/unit/enrich.test.ts b/src/__tests__/unit/enrich.test.ts new file mode 100644 index 00000000..2a962c1e --- /dev/null +++ b/src/__tests__/unit/enrich.test.ts @@ -0,0 +1,480 @@ +import { describe, it, expect, jest } from "@jest/globals"; +import { + missingEnrichableFields, + condenseReadme, + applyEnrichment, + enrichCodeJSON, +} from "../../enrich.js"; +import { CodeJSON } from "../../codejson.js"; +import { ModelRunner, ModelSession } from "../../llm.js"; +import { createMockLogger } from "../fixtures/mock-deps.js"; +import validCodeJSON from "../fixtures/test-code.json"; + +const blank = "" as never; +const complete = validCodeJSON as unknown as CodeJSON; + +function createFakeSession( + overrides: Partial = {}, +): ModelSession { + return { + generateText: jest.fn().mockResolvedValue(""), + generateJSON: jest.fn().mockResolvedValue({}), + ...overrides, + } as unknown as ModelSession; +} + +function runnerFor(session: ModelSession): ModelRunner { + return (async (_log, run) => run(session)) as ModelRunner; +} + +describe("missingEnrichableFields", () => { + it("reports nothing missing for a fully populated code.json", () => { + expect(missingEnrichableFields(complete)).toEqual([]); + }); + + it("treats a longDescription under 150 characters as missing", () => { + const codeJSON = { ...complete, longDescription: "too short" }; + expect(missingEnrichableFields(codeJSON)).toEqual(["longDescription"]); + }); + + it("treats empty arrays as missing", () => { + const codeJSON = { + ...complete, + tags: [], + categories: [], + platforms: [], + }; + expect(missingEnrichableFields(codeJSON)).toEqual([ + "tags", + "categories", + "platforms", + ]); + }); + + it("treats a blank enum value as missing", () => { + const codeJSON = { + ...complete, + softwareType: blank, + repositoryType: blank, + }; + expect(missingEnrichableFields(codeJSON)).toEqual([ + "softwareType", + "repositoryType", + ]); + }); + + it("reports every field for a blank draft", () => { + const codeJSON = { + ...complete, + longDescription: "", + tags: [], + categories: [], + platforms: [], + softwareType: blank, + repositoryType: blank, + }; + expect(missingEnrichableFields(codeJSON)).toEqual([ + "longDescription", + "tags", + "categories", + "platforms", + "softwareType", + "repositoryType", + ]); + }); +}); + +describe("condenseReadme", () => { + it("leaves short plain text untouched", () => { + expect(condenseReadme("A short project description.")).toBe( + "A short project description.", + ); + }); + + it("strips HTML comments", () => { + expect(condenseReadme("intro\n\nbody")).toBe( + "intro\n\nbody", + ); + }); + + it("strips fenced code blocks", () => { + expect(condenseReadme("intro\n```js\nconsole.log(1)\n```\nbody")).toBe( + "intro\n\nbody", + ); + }); + + it("strips badge links", () => { + expect( + condenseReadme( + "[![Build](https://img.shields.io/badge/build-passing-green)](https://ci.example.com)\nbody", + ), + ).toBe("body"); + }); + + it("collapses runs of blank lines", () => { + expect(condenseReadme("intro\n\n\n\n\nbody")).toBe("intro\n\nbody"); + }); + + it("truncates at the nearest paragraph boundary under the limit", () => { + const kept = "keep this paragraph."; + const dropped = "x".repeat(50); + const content = `${kept}\n\n${dropped}`; + + expect(condenseReadme(content, kept.length + 10)).toBe(kept); + }); + + it("hard-truncates when no paragraph boundary exists under the limit", () => { + const content = "x".repeat(100); + expect(condenseReadme(content, 20)).toBe("x".repeat(20)); + }); +}); + +describe("applyEnrichment", () => { + const blankDraft: CodeJSON = { + ...complete, + longDescription: "", + tags: [], + categories: [], + platforms: [], + softwareType: blank, + repositoryType: blank, + }; + + it("only writes fields listed as missing", () => { + const result = applyEnrichment( + blankDraft, + { longDescription: "a".repeat(200), categories: ["compliance"] }, + ["longDescription"], + ); + + expect(result.longDescription).toBe("a".repeat(200)); + expect(result.categories).toEqual([]); + }); + + it("clamps longDescription to the schema's maximum length", () => { + const result = applyEnrichment( + blankDraft, + { longDescription: "a".repeat(10500) }, + ["longDescription"], + ); + + expect(result.longDescription).toHaveLength(10000); + }); + + it("merges generated tags with existing tags instead of replacing them", () => { + const draft = { ...blankDraft, tags: ["manual"] }; + const result = applyEnrichment(draft, { tags: ["generated", "manual"] }, [ + "tags", + ]); + + expect(result.tags).toEqual(["generated", "manual"]); + }); + + it("writes categories directly", () => { + const result = applyEnrichment( + blankDraft, + { categories: ["compliance", "automation"] }, + ["categories"], + ); + + expect(result.categories).toEqual(["compliance", "automation"]); + }); + + it("drops platform values outside the schema's enum", () => { + const result = applyEnrichment( + blankDraft, + { platforms: ["web", "atari" as CodeJSON["platforms"][number]] }, + ["platforms"], + ); + + expect(result.platforms).toEqual(["web"]); + }); + + it("rejects a softwareType outside the schema's enum", () => { + const result = applyEnrichment( + blankDraft, + { softwareType: "not-a-real-type" as CodeJSON["softwareType"] }, + ["softwareType"], + ); + + expect(result.softwareType).toBe(blank); + }); + + it("accepts a softwareType within the schema's enum", () => { + const result = applyEnrichment(blankDraft, { softwareType: "library" }, [ + "softwareType", + ]); + + expect(result.softwareType).toBe("library"); + }); + + it("rejects a repositoryType outside the schema's enum", () => { + const result = applyEnrichment( + blankDraft, + { repositoryType: "not-a-real-type" as CodeJSON["repositoryType"] }, + ["repositoryType"], + ); + + expect(result.repositoryType).toBe(blank); + }); + + it("accepts a repositoryType within the schema's enum", () => { + const result = applyEnrichment(blankDraft, { repositoryType: "tools" }, [ + "repositoryType", + ]); + + expect(result.repositoryType).toBe("tools"); + }); + + it("leaves a missing field untouched when nothing was generated for it", () => { + const result = applyEnrichment(blankDraft, {}, ["longDescription", "tags"]); + + expect(result.longDescription).toBe(""); + expect(result.tags).toEqual([]); + }); + + it("does not mutate the input", () => { + const before = JSON.parse(JSON.stringify(blankDraft)); + applyEnrichment(blankDraft, { longDescription: "a".repeat(200) }, [ + "longDescription", + ]); + + expect(blankDraft).toEqual(before); + }); +}); + +describe("enrichCodeJSON", () => { + const blankDraft: CodeJSON = { + ...complete, + longDescription: "", + tags: [], + categories: [], + platforms: [], + softwareType: blank, + repositoryType: blank, + }; + const usableDescription = "This project does many useful things. ".repeat(5); + + it("returns the draft unchanged and never runs the model when nothing is missing", async () => { + const runModel = jest.fn(); + + const result = await enrichCodeJSON( + complete, + { readme: null }, + createMockLogger(), + runModel as unknown as ModelRunner, + ); + + expect(result).toBe(complete); + expect(runModel).not.toHaveBeenCalled(); + }); + + it("returns the draft unchanged when no model is available", async () => { + const runModel: ModelRunner = async () => null; + + const result = await enrichCodeJSON( + blankDraft, + { readme: null }, + createMockLogger(), + runModel, + ); + + expect(result).toBe(blankDraft); + }); + + it("returns the draft unchanged and logs a warning when the model run throws", async () => { + const runModel: ModelRunner = async () => { + throw new Error("generation blew up"); + }; + const log = createMockLogger(); + + const result = await enrichCodeJSON( + blankDraft, + { readme: null }, + log, + runModel, + ); + + expect(result).toBe(blankDraft); + expect(log.warning).toHaveBeenCalledWith( + expect.stringContaining("generation blew up"), + ); + }); + + it("fills every missing field from the model's answers", async () => { + const session = createFakeSession({ + generateText: jest.fn().mockResolvedValue(usableDescription), + generateJSON: jest.fn().mockResolvedValue({ + tags: ["generated"], + categories: ["compliance"], + platforms: ["web"], + softwareType: "library", + repositoryType: "tools", + }), + }); + + const result = await enrichCodeJSON( + blankDraft, + { readme: null }, + createMockLogger(), + runnerFor(session), + ); + + expect(result.longDescription).toBe(usableDescription); + expect(result.tags).toEqual(["generated"]); + expect(result.categories).toEqual(["compliance"]); + expect(result.platforms).toEqual(["web"]); + expect(result.softwareType).toBe("library"); + expect(result.repositoryType).toBe("tools"); + expect(session.generateText).toHaveBeenCalledTimes(1); + expect(session.generateJSON).toHaveBeenCalledTimes(1); + }); + + it("retries once when the longDescription comes back too short", async () => { + const generateText = jest + .fn() + .mockResolvedValueOnce("too short") + .mockResolvedValueOnce(usableDescription); + const session = createFakeSession({ generateText }); + + const result = await enrichCodeJSON( + { ...blankDraft, softwareType: "library", repositoryType: "tools" }, + { readme: null }, + createMockLogger(), + runnerFor(session), + ); + + expect(result.longDescription).toBe(usableDescription); + expect(generateText).toHaveBeenCalledTimes(2); + expect(generateText.mock.calls[1][0]).toEqual( + expect.stringContaining("too short"), + ); + }); + + it("leaves longDescription blank and warns when both attempts come back too short", async () => { + const generateText = jest.fn().mockResolvedValue("still too short"); + const session = createFakeSession({ + generateText, + generateJSON: jest + .fn() + .mockResolvedValue({ softwareType: "library" }), + }); + const log = createMockLogger(); + + const result = await enrichCodeJSON( + { ...blankDraft, repositoryType: "tools" }, + { readme: null }, + log, + runnerFor(session), + ); + + expect(result.longDescription).toBe(""); + expect(result.softwareType).toBe("library"); + expect(log.warning).toHaveBeenCalledWith( + expect.stringContaining("could not produce a usable longDescription"), + ); + }); + + it("discards a longDescription that only echoes the short description", async () => { + // a long-enough description isolates the echo check from the length check + const draft = { + ...complete, + description: usableDescription, + longDescription: "", + }; + const generateText = jest.fn().mockResolvedValue(draft.description); + const session = createFakeSession({ generateText }); + + const result = await enrichCodeJSON( + draft, + { readme: null }, + createMockLogger(), + runnerFor(session), + ); + + expect(result.longDescription).toBe(""); + expect(session.generateJSON).not.toHaveBeenCalled(); + }); + + it("leaves longDescription blank and warns when the prose call throws", async () => { + const session = createFakeSession({ + generateText: jest.fn().mockRejectedValue(new Error("aborted")), + generateJSON: jest + .fn() + .mockResolvedValue({ softwareType: "library" }), + }); + const log = createMockLogger(); + + const result = await enrichCodeJSON( + blankDraft, + { readme: null }, + log, + runnerFor(session), + ); + + expect(result.longDescription).toBe(""); + expect(result.softwareType).toBe("library"); + expect(log.warning).toHaveBeenCalledWith( + expect.stringContaining("longDescription generation failed"), + ); + }); + + it("keeps a generated longDescription when the classification call fails", async () => { + const session = createFakeSession({ + generateText: jest.fn().mockResolvedValue(usableDescription), + generateJSON: jest + .fn() + .mockRejectedValue(new Error("grammar parse failed")), + }); + const log = createMockLogger(); + + const result = await enrichCodeJSON( + blankDraft, + { readme: null }, + log, + runnerFor(session), + ); + + expect(result.longDescription).toBe(usableDescription); + expect(result.softwareType).toBe(blank); + expect(log.warning).toHaveBeenCalledWith( + expect.stringContaining("Field classification failed"), + ); + }); + + it("drops a classification value outside the schema's enum before it reaches the result", async () => { + const session = createFakeSession({ + generateJSON: jest + .fn() + .mockResolvedValue({ softwareType: "not-a-real-type" }), + }); + + const result = await enrichCodeJSON( + { ...complete, softwareType: blank }, + { readme: null }, + createMockLogger(), + runnerFor(session), + ); + + expect(result.softwareType).toBe(blank); + }); + + it("passes the condensed readme and known fields into the prompt", async () => { + const generateText = jest.fn().mockResolvedValue(""); + const session = createFakeSession({ generateText }); + const readme = "intro\n\nmore detail"; + + await enrichCodeJSON( + { ...complete, longDescription: "" }, + { readme }, + createMockLogger(), + runnerFor(session), + ); + + const prompt = generateText.mock.calls[0][0] as string; + expect(prompt).toContain(`Software type: ${complete.softwareType}`); + expect(prompt).toContain("README:"); + expect(prompt).toContain("intro"); + expect(prompt).not.toContain("internal note"); + }); +}); diff --git a/src/__tests__/unit/llm.test.ts b/src/__tests__/unit/llm.test.ts index 92fe6888..470dae27 100644 --- a/src/__tests__/unit/llm.test.ts +++ b/src/__tests__/unit/llm.test.ts @@ -33,10 +33,14 @@ const getLlamaMock = jest.fn(async () => ({ loadModel: loadModelMock, createGrammarForJsonSchema: createGrammarMock, })); +const sessionConstructorMock = jest.fn(); jest.unstable_mockModule("node-llama-cpp", () => ({ getLlama: getLlamaMock, LlamaChatSession: class { + constructor(options: unknown) { + sessionConstructorMock(options); + } async prompt(text: string, options: unknown) { return promptMock(text, options); } @@ -209,6 +213,42 @@ describe("withModel with a model present", () => { expect(result).toEqual({ softwareType: "library" }); }); + // an unset temperature would let identical repo state classify differently + // across scheduled runs, opening a no-op-looking PR + it("pins temperature to 0 for JSON generation so results are reproducible", async () => { + const schema = { type: "object", properties: {} } as const; + promptMock.mockResolvedValueOnce("{}"); + + await withModel(createMockLogger(), async (session) => + session.generateJSON("classify this", schema), + ); + + expect(promptMock).toHaveBeenCalledWith( + "classify this", + expect.objectContaining({ temperature: 0 }), + ); + }); + + it("replaces the library's generic assistant persona with a task-specific system prompt", async () => { + await withModel(createMockLogger(), async (session) => + session.generateText("first"), + ); + + const options = sessionConstructorMock.mock.calls[0][0] as any; + expect(options.systemPrompt).toEqual( + expect.stringContaining("no preamble"), + ); + }); + + it("leaves temperature unset for free-text generation", async () => { + await withModel(createMockLogger(), async (session) => + session.generateText("describe this project"), + ); + + const options = promptMock.mock.calls[0][1] as any; + expect(options.temperature).toBeUndefined(); + }); + it("reports how long generation took", async () => { const log = createMockLogger(); diff --git a/src/enrich.ts b/src/enrich.ts new file mode 100644 index 00000000..c3fccc0a --- /dev/null +++ b/src/enrich.ts @@ -0,0 +1,332 @@ +import type { GbnfJsonSchema } from "node-llama-cpp"; +import { CodeJSON } from "./codejson.js"; +import { mergeTags } from "./helper.js"; +import { ModelRunner, ModelSession, withModel } from "./llm.js"; +import { Logger } from "./types/Dependencies.js"; + +const LONG_DESCRIPTION_MIN_LENGTH = 150; +const LONG_DESCRIPTION_MAX_LENGTH = 10000; +const DEFAULT_README_MAX_CHARS = 4000; +const CATEGORIES_LIST_URL = "https://yml.publiccode.tools/categories-list.html"; + +export const ENRICHABLE_FIELDS = [ + "longDescription", + "tags", + "categories", + "platforms", + "softwareType", + "repositoryType", +] as const; + +export const SOFTWARE_TYPES = [ + "standalone/mobile", + "standalone/iot", + "standalone/desktop", + "standalone/web", + "standalone/backend", + "standalone/other", + "addon", + "library", + "configurationFiles", +] as const satisfies readonly CodeJSON["softwareType"][]; + +export const REPOSITORY_TYPES = [ + "package", + "website", + "standards", + "libraries", + "data", + "application", + "tools", + "APIs", +] as const satisfies readonly CodeJSON["repositoryType"][]; + +export const PLATFORMS = [ + "mac", + "web", + "windows", + "linux", + "ios", + "android", + "other", +] as const satisfies readonly CodeJSON["platforms"][number][]; + +export interface GeneratedFields { + longDescription: string; + tags: string[]; + categories: string[]; + platforms: CodeJSON["platforms"]; + softwareType: CodeJSON["softwareType"]; + repositoryType: CodeJSON["repositoryType"]; +} + +export type EnrichableField = (typeof ENRICHABLE_FIELDS)[number]; +type ClassificationField = Exclude; + +export function missingEnrichableFields(codeJSON: CodeJSON): EnrichableField[] { + return ENRICHABLE_FIELDS.filter((field) => { + switch (field) { + case "longDescription": + return ( + codeJSON.longDescription.trim().length < LONG_DESCRIPTION_MIN_LENGTH + ); + case "tags": + case "categories": + case "platforms": + return codeJSON[field].length === 0; + case "softwareType": + case "repositoryType": + return (codeJSON[field] as string) === ""; + } + }); +} + +export function condenseReadme( + content: string, + maxChars: number = DEFAULT_README_MAX_CHARS, +): string { + const stripped = content + .replace(//g, "") + .replace(/```[\s\S]*?```/g, "") + .replace(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g, "") + .replace(/\n{3,}/g, "\n\n") + .trim(); + + if (stripped.length <= maxChars) { + return stripped; + } + + const truncated = stripped.slice(0, maxChars); + const paragraphBoundary = truncated.lastIndexOf("\n\n"); + + return ( + paragraphBoundary > 0 ? truncated.slice(0, paragraphBoundary) : truncated + ).trim(); +} + + +export function applyEnrichment( + codeJSON: CodeJSON, + generated: Partial, + missing: EnrichableField[], // writes only the fields that are missing +): CodeJSON { + const result = { ...codeJSON }; + + for (const field of missing) { + switch (field) { + case "longDescription": + if (generated.longDescription) { + result.longDescription = generated.longDescription.slice( + 0, + LONG_DESCRIPTION_MAX_LENGTH, + ); + } + break; + case "tags": + if (generated.tags) { + result.tags = mergeTags(generated.tags, codeJSON.tags); + } + break; + case "categories": + if (generated.categories) { + result.categories = generated.categories; + } + break; + case "platforms": + if (generated.platforms) { + result.platforms = generated.platforms.filter((platform) => + (PLATFORMS as readonly string[]).includes(platform), + ); + } + break; + case "softwareType": + if ( + generated.softwareType && + (SOFTWARE_TYPES as readonly string[]).includes(generated.softwareType) + ) { + result.softwareType = generated.softwareType; + } + break; + case "repositoryType": + if ( + generated.repositoryType && + (REPOSITORY_TYPES as readonly string[]).includes( + generated.repositoryType, + ) + ) { + result.repositoryType = generated.repositoryType; + } + break; + } + } + + return result; +} + +export async function enrichCodeJSON( + codeJSON: CodeJSON, + context: { readme: string | null }, + log: Logger, + runModel: ModelRunner = withModel, +): Promise { + const missing = missingEnrichableFields(codeJSON); + + if (missing.length === 0) { + log.info("No AI-enrichable fields are missing, skipping model generation."); + return codeJSON; + } + + let generated: Partial | null; + try { + generated = await runModel(log, (session) => + generateFields(session, codeJSON, context.readme, missing, log), + ); + } catch (error) { + log.warning( + `AI enrichment failed, continuing without it: ${errorMessage(error)}`, + ); + return codeJSON; + } + + if (generated === null) { + return codeJSON; + } + + return applyEnrichment(codeJSON, generated, missing); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +async function generateFields( + session: ModelSession, + codeJSON: CodeJSON, + readme: string | null, + missing: EnrichableField[], + log: Logger, +): Promise> { + const promptContext = buildPromptContext(codeJSON, readme); + const generated: Partial = {}; + + if (missing.includes("longDescription")) { + const longDescription = await generateLongDescription( + session, + codeJSON.description, + promptContext, + log, + ); + if (longDescription) { + generated.longDescription = longDescription; + } + } + + const classificationFields = missing.filter( + (field): field is ClassificationField => field !== "longDescription", + ); + if (classificationFields.length > 0) { + Object.assign( + generated, + await generateClassification( + session, + promptContext, + classificationFields, + log, + ), + ); + } + + return generated; +} + +function buildPromptContext(codeJSON: CodeJSON, readme: string | null): string { + const lines = [ + `Project name: ${codeJSON.name}`, + `Short description: ${codeJSON.description}`, + `Languages: ${codeJSON.languages.join(", ") || "unknown"}`, + `Existing tags: ${codeJSON.tags.join(", ") || "none"}`, + `Repository URL: ${codeJSON.repositoryURL}`, + ]; + + if ((codeJSON.softwareType as string) !== "") { + lines.push(`Software type: ${codeJSON.softwareType}`); + } + if ((codeJSON.repositoryType as string) !== "") { + lines.push(`Repository type: ${codeJSON.repositoryType}`); + } + + if (readme) { + lines.push("", "README:", condenseReadme(readme)); + } + + return lines.join("\n"); +} + +async function generateLongDescription( + session: ModelSession, + shortDescription: string, + promptContext: string, + log: Logger, +): Promise { + const prompt = `${promptContext}\n\nWrite a longer description of this software for a potential user, at least four sentences. Do not repeat the short description verbatim.`; + + try { + let answer = await session.generateText(prompt); + + let isUnusable = answer.trim().length < LONG_DESCRIPTION_MIN_LENGTH || answer.trim().toLowerCase() === shortDescription.trim().toLowerCase(); + + if (isUnusable) { + log.warning( + "Model could not produce a usable longDescription, leaving it blank.", + ); + return undefined; + } + + return answer; + } catch (error) { + log.warning(`longDescription generation failed: ${errorMessage(error)}`); + return undefined; + } +} + +async function generateClassification( + session: ModelSession, + promptContext: string, + fields: ClassificationField[], + log: Logger, +): Promise> { + const prompt = `${promptContext}\n\nClassify this software project. For categories, choose from the list at ${CATEGORIES_LIST_URL}.`; + + try { + const result = await session.generateJSON( + prompt, + classificationSchema(fields), + ); + return result as unknown as Partial; + } catch (error) { + log.warning(`Field classification failed: ${errorMessage(error)}`); + return {}; + } +} + +function classificationSchema(fields: ClassificationField[]): GbnfJsonSchema { + const properties: Record = {}; + + if (fields.includes("tags")) { + properties.tags = { type: "array", items: { type: "string" } }; + } + if (fields.includes("categories")) { + properties.categories = { type: "array", items: { type: "string" } }; + } + if (fields.includes("platforms")) { + properties.platforms = { type: "array", items: { enum: PLATFORMS } }; + } + if (fields.includes("softwareType")) { + properties.softwareType = { enum: SOFTWARE_TYPES }; + } + if (fields.includes("repositoryType")) { + properties.repositoryType = { enum: REPOSITORY_TYPES }; + } + + return { type: "object", properties }; +} diff --git a/src/llm.ts b/src/llm.ts index 3cf6c7df..a766c07b 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -9,7 +9,12 @@ import { Logger } from "./types/Dependencies.js"; const DEFAULT_MODEL_PATH = "/opt/models/model.gguf"; const TIMEOUT_MS = 900_000; const DEFAULT_MAX_TOKENS = 1500; -const THOUGHT_TOKEN_BUDGET = 400; +const THOUGHT_TOKEN_BUDGET = 500; + +const SYSTEM_PROMPT = + "You write precise, factual text for USA government software metadata records. Answer " + + "only with the requested content: no preamble, no meta-commentary, no " + + "markdown formatting, and never mention that you are an AI or a language model."; export function getModelPath(): string { return process.env.ACG_MODEL_PATH || DEFAULT_MODEL_PATH; @@ -48,6 +53,7 @@ export const withModel: ModelRunner = async (log, run) => { text: string, maxTokens: number, grammar?: LlamaGrammar, + temperature?: number, ): Promise { const startedAt = Date.now(); const context = await model.createContext(); @@ -55,6 +61,7 @@ export const withModel: ModelRunner = async (log, run) => { try { const session = new LlamaChatSession({ contextSequence: context.getSequence(), + systemPrompt: SYSTEM_PROMPT, }); let thought = ""; @@ -64,6 +71,7 @@ export const withModel: ModelRunner = async (log, run) => { maxTokens, signal: deadline, grammar, + temperature, onResponseChunk: (chunk) => { if (chunk.type === "segment" && chunk.segmentType === "thought") { thought += chunk.text; @@ -78,10 +86,13 @@ export const withModel: ModelRunner = async (log, run) => { log.debug(`Model thought for ${thought.trim().length} characters`); } - log.info( + log.debug( `Model responded in ${elapsedSeconds}s with ${trimmed.length} characters`, ); + log.info(`Model thoughts: ${thought}`); + log.info(`Model response: ${trimmed}`); + return trimmed; } finally { await context.dispose(); @@ -99,7 +110,7 @@ export const withModel: ModelRunner = async (log, run) => { maxTokens: number = DEFAULT_MAX_TOKENS, ): Promise> => { const grammar = await llama.createGrammarForJsonSchema(schema); - const answer = await prompt(text, maxTokens, grammar); + const answer = await prompt(text, maxTokens, grammar, 0); return grammar.parse(answer) as GbnfJsonSchemaToType; }, From 39f2b3c6ce21d9e4e7446ab7649b8f1b796e9ebe Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 15:08:23 -0400 Subject: [PATCH 12/21] hooking up model logic into main --- src/__tests__/unit/enrich.test.ts | 23 +--------------------- src/__tests__/unit/llm.test.ts | 2 +- src/__tests__/unit/main.test.ts | 32 +++++++++++++++++++++++++++++++ src/enrich.ts | 7 ++++--- src/main.ts | 9 ++++++++- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/__tests__/unit/enrich.test.ts b/src/__tests__/unit/enrich.test.ts index 2a962c1e..86acefa8 100644 --- a/src/__tests__/unit/enrich.test.ts +++ b/src/__tests__/unit/enrich.test.ts @@ -330,28 +330,7 @@ describe("enrichCodeJSON", () => { expect(session.generateJSON).toHaveBeenCalledTimes(1); }); - it("retries once when the longDescription comes back too short", async () => { - const generateText = jest - .fn() - .mockResolvedValueOnce("too short") - .mockResolvedValueOnce(usableDescription); - const session = createFakeSession({ generateText }); - - const result = await enrichCodeJSON( - { ...blankDraft, softwareType: "library", repositoryType: "tools" }, - { readme: null }, - createMockLogger(), - runnerFor(session), - ); - - expect(result.longDescription).toBe(usableDescription); - expect(generateText).toHaveBeenCalledTimes(2); - expect(generateText.mock.calls[1][0]).toEqual( - expect.stringContaining("too short"), - ); - }); - - it("leaves longDescription blank and warns when both attempts come back too short", async () => { + it("leaves longDescription blank and warns when the answer comes back too short", async () => { const generateText = jest.fn().mockResolvedValue("still too short"); const session = createFakeSession({ generateText, diff --git a/src/__tests__/unit/llm.test.ts b/src/__tests__/unit/llm.test.ts index 470dae27..fb57e559 100644 --- a/src/__tests__/unit/llm.test.ts +++ b/src/__tests__/unit/llm.test.ts @@ -254,7 +254,7 @@ describe("withModel with a model present", () => { await withModel(log, async (session) => session.generateText("first")); - expect(log.info).toHaveBeenCalledWith( + expect(log.debug).toHaveBeenCalledWith( expect.stringContaining("Model responded in"), ); }); diff --git a/src/__tests__/unit/main.test.ts b/src/__tests__/unit/main.test.ts index 6a2e217b..5f3e8b4e 100644 --- a/src/__tests__/unit/main.test.ts +++ b/src/__tests__/unit/main.test.ts @@ -375,6 +375,38 @@ describe("runWithDeps", () => { expect(deps.octokit.createPullRequest).toHaveBeenCalled(); }); + it("skips AI enrichment when no model is available, logging why", async () => { + process.env.GITHUB_EVENT_NAME = "schedule"; + + const deps = createMockDeps({ + readFile: jest.fn().mockRejectedValue(new Error("no file")), + }); + + await runWithDeps(deps); + + expect(deps.log.info).toHaveBeenCalledWith( + expect.stringContaining("No model found at"), + ); + expect(deps.octokit.createPullRequest).toHaveBeenCalled(); + }); + + it("skips model generation entirely when the existing code.json has nothing left to enrich", async () => { + process.env.GITHUB_EVENT_NAME = "schedule"; + + const deps = createMockDeps({ + readFile: jest.fn().mockResolvedValue(JSON.stringify(validCodeJSON)), + }); + + await runWithDeps(deps); + + expect(deps.log.info).toHaveBeenCalledWith( + expect.stringContaining("No AI-enrichable fields are missing"), + ); + expect(generatedCodeJSON(deps).longDescription).toBe( + validCodeJSON.longDescription, + ); + }); + it("defaults feedbackMechanism and SBOM to the repository URL", async () => { process.env.GITHUB_EVENT_NAME = "schedule"; diff --git a/src/enrich.ts b/src/enrich.ts index c3fccc0a..63b66656 100644 --- a/src/enrich.ts +++ b/src/enrich.ts @@ -104,7 +104,6 @@ export function condenseReadme( ).trim(); } - export function applyEnrichment( codeJSON: CodeJSON, generated: Partial, @@ -271,9 +270,11 @@ async function generateLongDescription( const prompt = `${promptContext}\n\nWrite a longer description of this software for a potential user, at least four sentences. Do not repeat the short description verbatim.`; try { - let answer = await session.generateText(prompt); + const answer = await session.generateText(prompt); - let isUnusable = answer.trim().length < LONG_DESCRIPTION_MIN_LENGTH || answer.trim().toLowerCase() === shortDescription.trim().toLowerCase(); + const isUnusable = + answer.trim().length < LONG_DESCRIPTION_MIN_LENGTH || + answer.trim().toLowerCase() === shortDescription.trim().toLowerCase(); if (isUnusable) { log.warning( diff --git a/src/main.ts b/src/main.ts index 662f4214..616a28ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import { import { Dependencies } from "./types/Dependencies.js"; import { createHelpers, deriveUsageType, Helpers } from "./helper.js"; import { createProductionDeps } from "./create-deps.js"; +import { enrichCodeJSON } from "./enrich.js"; // gathers what can be observed about the repository right now so anything not an observation belongs to codejson-core async function getMetaData( @@ -110,9 +111,15 @@ export async function runWithDeps(deps: Dependencies): Promise { } const metaData = await getMetaData(helpers, currentCodeJSON); - const finalCodeJSON = assembleDraft(metaData, currentCodeJSON, { + const draftCodeJSON = assembleDraft(metaData, currentCodeJSON, { isArchived: deps.isArchived, }); + const readme = await helpers.readREADME(); + const finalCodeJSON = await enrichCodeJSON( + draftCodeJSON, + { readme }, + deps.log, + ); // a generated code.json is a draft so we must report what fields are missing const validationErrors = validateCodeJSON(finalCodeJSON); From 8756419c9d7dc593352c346073e35091885bacd8 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 15:17:21 -0400 Subject: [PATCH 13/21] reverting back to local dockerfile for testing --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index f071cc98..67bfa204 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ outputs: runs: using: docker - image: docker://ghcr.io/dsacms/automated-codejson-generator:v2.0.1 + image: Dockerfile branding: icon: "file-text" From 80662b51d0a9ab7eb7c3875add4262243f8d2fd0 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 15:20:52 -0400 Subject: [PATCH 14/21] reverting code.json for testing --- code.json | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/code.json b/code.json index 134c9ce9..c9b6d4ec 100644 --- a/code.json +++ b/code.json @@ -2,7 +2,7 @@ "name": "automated-codejson-generator", "version": "1.2.1", "description": "Tooling that allows for code.json creation through automation", - "longDescription": "This project provides a GitHub Action that helps federal agencies maintain their code.json files, which are required for compliance with the Federal Source Code Policy. The action automatically calculates and updates various metadata fields including labor hours, programming languages used, repository information, and timestamps. It creates pull requests with these updates, making it easier to keep code.json files accurate and up-to-date.", + "longDescription": "", "status": "Production", "permissions": { "licenses": [ @@ -28,9 +28,9 @@ "forks": 4, "clones": 0 }, - "platforms": ["web", "linux"], - "categories": ["developer-tools", "automation"], - "softwareType": "standalone/backend", + "platforms": [], + "categories": [], + "softwareType": "", "languages": ["TypeScript", "JavaScript"], "maintenance": "internal", "contractNumber": [], @@ -43,15 +43,7 @@ "lastModified": "2025-10-21T21:52:17Z", "metadataLastUpdated": "2025-10-30T22:53:09.463Z" }, - "tags": [ - "cmsoss-tier2", - "metadata", - "codejson", - "ci", - "github-action", - "workflow", - "featured" - ], + "tags": [], "contact": { "email": "opensource@cms.hhs.gov", "name": "CMS Open Source Team" @@ -59,7 +51,7 @@ "feedbackMechanism": "https://github.com/DSACMS/automated-codejson-generator/issues", "AIUseCaseID": "0", "localisation": false, - "repositoryType": "tools", + "repositoryType": "", "userInput": false, "fismaLevel": "low", "group": "CMS/OA/DSAC", From d1558ca8dd7a3c50e7ca7b08fc45cb4550c3c5d3 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 15:40:30 -0400 Subject: [PATCH 15/21] fixing empty tags generation --- src/enrich.ts | 8 ++++++-- src/llm.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/enrich.ts b/src/enrich.ts index 63b66656..2548d796 100644 --- a/src/enrich.ts +++ b/src/enrich.ts @@ -296,7 +296,7 @@ async function generateClassification( fields: ClassificationField[], log: Logger, ): Promise> { - const prompt = `${promptContext}\n\nClassify this software project. For categories, choose from the list at ${CATEGORIES_LIST_URL}.`; + const prompt = `${promptContext}\n\nClassify this software project. For categories, choose from the list at ${CATEGORIES_LIST_URL}. For tags, list at least 5 single words or short phrases describing the project's purpose, domain, or technology.`; try { const result = await session.generateJSON( @@ -314,7 +314,11 @@ function classificationSchema(fields: ClassificationField[]): GbnfJsonSchema { const properties: Record = {}; if (fields.includes("tags")) { - properties.tags = { type: "array", items: { type: "string" } }; + properties.tags = { + type: "array", + items: { type: "string" }, + minItems: 5, + }; } if (fields.includes("categories")) { properties.categories = { type: "array", items: { type: "string" } }; diff --git a/src/llm.ts b/src/llm.ts index a766c07b..6f20a0bc 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -90,7 +90,7 @@ export const withModel: ModelRunner = async (log, run) => { `Model responded in ${elapsedSeconds}s with ${trimmed.length} characters`, ); - log.info(`Model thoughts: ${thought}`); + log.info(`Model thoughts: ${thought.trim() || "(none)"}`); log.info(`Model response: ${trimmed}`); return trimmed; From e65a0ff437c61f5fe897c08fc5fb29804ebe32a3 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 15:58:58 -0400 Subject: [PATCH 16/21] actually fixing tags --- src/__tests__/fixtures/test-code.json | 6 +- src/__tests__/unit/enrich.test.ts | 20 ++-- src/enrich.data.ts | 145 ++++++++++++++++++++++++++ src/enrich.ts | 66 +++++------- 4 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 src/enrich.data.ts diff --git a/src/__tests__/fixtures/test-code.json b/src/__tests__/fixtures/test-code.json index 134f4d14..4e3d458e 100644 --- a/src/__tests__/fixtures/test-code.json +++ b/src/__tests__/fixtures/test-code.json @@ -25,7 +25,7 @@ "clones": 50 }, "platforms": ["web", "linux"], - "categories": ["compliance", "automation"], + "categories": ["compliance-management", "it-development"], "softwareType": "standalone/other", "languages": ["TypeScript"], "maintenance": "internal", @@ -36,7 +36,7 @@ "lastModified": "2024-06-15T00:00:00Z", "metadataLastUpdated": "2024-06-20T00:00:00Z" }, - "tags": ["code.json", "federal", "open-source"], + "tags": ["code.json", "federal", "open-source", "metadata", "compliance"], "contact": { "email": "opensource@cms.hhs.gov", "name": "CMS Open Source Team" @@ -53,4 +53,4 @@ "subsetInHealthcare": ["operational"], "userType": ["government"], "maturityModelTier": 1 -} \ No newline at end of file +} diff --git a/src/__tests__/unit/enrich.test.ts b/src/__tests__/unit/enrich.test.ts index 86acefa8..afeb9106 100644 --- a/src/__tests__/unit/enrich.test.ts +++ b/src/__tests__/unit/enrich.test.ts @@ -37,6 +37,11 @@ describe("missingEnrichableFields", () => { expect(missingEnrichableFields(codeJSON)).toEqual(["longDescription"]); }); + it("treats a tag list under the minimum as missing", () => { + const codeJSON = { ...complete, tags: ["only", "two"] }; + expect(missingEnrichableFields(codeJSON)).toEqual(["tags"]); + }); + it("treats empty arrays as missing", () => { const codeJSON = { ...complete, @@ -143,7 +148,10 @@ describe("applyEnrichment", () => { it("only writes fields listed as missing", () => { const result = applyEnrichment( blankDraft, - { longDescription: "a".repeat(200), categories: ["compliance"] }, + { + longDescription: "a".repeat(200), + categories: ["compliance-management"], + }, ["longDescription"], ); @@ -170,14 +178,14 @@ describe("applyEnrichment", () => { expect(result.tags).toEqual(["generated", "manual"]); }); - it("writes categories directly", () => { + it("drops category values outside the controlled vocabulary", () => { const result = applyEnrichment( blankDraft, - { categories: ["compliance", "automation"] }, + { categories: ["compliance-management", "GitHub Actions"] }, ["categories"], ); - expect(result.categories).toEqual(["compliance", "automation"]); + expect(result.categories).toEqual(["compliance-management"]); }); it("drops platform values outside the schema's enum", () => { @@ -306,7 +314,7 @@ describe("enrichCodeJSON", () => { generateText: jest.fn().mockResolvedValue(usableDescription), generateJSON: jest.fn().mockResolvedValue({ tags: ["generated"], - categories: ["compliance"], + categories: ["compliance-management"], platforms: ["web"], softwareType: "library", repositoryType: "tools", @@ -322,7 +330,7 @@ describe("enrichCodeJSON", () => { expect(result.longDescription).toBe(usableDescription); expect(result.tags).toEqual(["generated"]); - expect(result.categories).toEqual(["compliance"]); + expect(result.categories).toEqual(["compliance-management"]); expect(result.platforms).toEqual(["web"]); expect(result.softwareType).toBe("library"); expect(result.repositoryType).toBe("tools"); diff --git a/src/enrich.data.ts b/src/enrich.data.ts new file mode 100644 index 00000000..9744eed5 --- /dev/null +++ b/src/enrich.data.ts @@ -0,0 +1,145 @@ +import { CodeJSON } from "./codejson.js"; + +export const SOFTWARE_TYPES = [ + "standalone/mobile", + "standalone/iot", + "standalone/desktop", + "standalone/web", + "standalone/backend", + "standalone/other", + "addon", + "library", + "configurationFiles", +] as const satisfies readonly CodeJSON["softwareType"][]; + +export const REPOSITORY_TYPES = [ + "package", + "website", + "standards", + "libraries", + "data", + "application", + "tools", + "APIs", +] as const satisfies readonly CodeJSON["repositoryType"][]; + +export const PLATFORMS = [ + "mac", + "web", + "windows", + "linux", + "ios", + "android", + "other", +] as const satisfies readonly CodeJSON["platforms"][number][]; + +// the publiccode.yml controlled vocabulary, mirrored here because the model runs +// offline and cannot read https://publiccodeyml.github.io/v0/categories-list.html +export const CATEGORIES = [ + "accounting", + "agile-project-management", + "applicant-tracking", + "application-development", + "appointment-scheduling", + "backup", + "billing-and-invoicing", + "blog", + "budgeting", + "business-intelligence", + "business-process-management", + "cad", + "call-center-management", + "cloud-management", + "collaboration", + "communications", + "compliance-management", + "contact-management", + "content-management", + "crm", + "customer-service-and-support", + "data-analytics", + "data-collection", + "data-visualization", + "design", + "design-system", + "digital-asset-management", + "digital-citizenship", + "document-management", + "donor-management", + "e-commerce", + "e-signature", + "educational-content", + "email-management", + "email-marketing", + "employee-management", + "enterprise-project-management", + "enterprise-social-networking", + "erp", + "event-management", + "facility-management", + "feedback-and-reviews-management", + "financial-reporting", + "fleet-management", + "fundraising", + "gamification", + "geographic-information-systems", + "grant-management", + "graphic-design", + "help-desk", + "hr", + "ide", + "identity-management", + "instant-messaging", + "integrated-library-system", + "inventory-management", + "it-asset-management", + "it-development", + "it-management", + "it-security", + "it-service-management", + "knowledge-management", + "learning-management-system", + "marketing", + "mind-mapping", + "mobile-marketing", + "mobile-payment", + "network-management", + "office", + "online-booking", + "online-community", + "payment-gateway", + "payroll", + "predictive-analysis", + "procurement", + "productivity-suite", + "project-collaboration", + "project-management", + "property-management", + "real-estate-management", + "regulations-and-directives", + "remote-support", + "resource-management", + "sales-management", + "seo", + "service-desk", + "social-media-management", + "survey", + "talent-management", + "task-management", + "taxes-management", + "test-management", + "time-management", + "time-tracking", + "translation", + "video-conferencing", + "video-editing", + "visitor-management", + "voip", + "warehouse-management", + "web-collaboration", + "web-conferencing", + "website-builder", + "whistleblowing", + "workflow-management", + "other", +] as const; diff --git a/src/enrich.ts b/src/enrich.ts index 2548d796..7b059695 100644 --- a/src/enrich.ts +++ b/src/enrich.ts @@ -1,5 +1,11 @@ import type { GbnfJsonSchema } from "node-llama-cpp"; import { CodeJSON } from "./codejson.js"; +import { + CATEGORIES, + PLATFORMS, + REPOSITORY_TYPES, + SOFTWARE_TYPES, +} from "./enrich.data.js"; import { mergeTags } from "./helper.js"; import { ModelRunner, ModelSession, withModel } from "./llm.js"; import { Logger } from "./types/Dependencies.js"; @@ -7,7 +13,7 @@ import { Logger } from "./types/Dependencies.js"; const LONG_DESCRIPTION_MIN_LENGTH = 150; const LONG_DESCRIPTION_MAX_LENGTH = 10000; const DEFAULT_README_MAX_CHARS = 4000; -const CATEGORIES_LIST_URL = "https://yml.publiccode.tools/categories-list.html"; +const MIN_TAGS = 5; export const ENRICHABLE_FIELDS = [ "longDescription", @@ -18,39 +24,6 @@ export const ENRICHABLE_FIELDS = [ "repositoryType", ] as const; -export const SOFTWARE_TYPES = [ - "standalone/mobile", - "standalone/iot", - "standalone/desktop", - "standalone/web", - "standalone/backend", - "standalone/other", - "addon", - "library", - "configurationFiles", -] as const satisfies readonly CodeJSON["softwareType"][]; - -export const REPOSITORY_TYPES = [ - "package", - "website", - "standards", - "libraries", - "data", - "application", - "tools", - "APIs", -] as const satisfies readonly CodeJSON["repositoryType"][]; - -export const PLATFORMS = [ - "mac", - "web", - "windows", - "linux", - "ios", - "android", - "other", -] as const satisfies readonly CodeJSON["platforms"][number][]; - export interface GeneratedFields { longDescription: string; tags: string[]; @@ -71,6 +44,7 @@ export function missingEnrichableFields(codeJSON: CodeJSON): EnrichableField[] { codeJSON.longDescription.trim().length < LONG_DESCRIPTION_MIN_LENGTH ); case "tags": + return codeJSON.tags.length < MIN_TAGS; case "categories": case "platforms": return codeJSON[field].length === 0; @@ -128,7 +102,9 @@ export function applyEnrichment( break; case "categories": if (generated.categories) { - result.categories = generated.categories; + result.categories = generated.categories.filter((category) => + (CATEGORIES as readonly string[]).includes(category), + ); } break; case "platforms": @@ -296,7 +272,21 @@ async function generateClassification( fields: ClassificationField[], log: Logger, ): Promise> { - const prompt = `${promptContext}\n\nClassify this software project. For categories, choose from the list at ${CATEGORIES_LIST_URL}. For tags, list at least 5 single words or short phrases describing the project's purpose, domain, or technology.`; + const instructions = ["Classify this software project."]; + + if (fields.includes("tags")) { + instructions.push( + `For tags, list at least ${MIN_TAGS} single words or short phrases describing the project's purpose, domain, or technology.`, + ); + } + + if (fields.includes("categories")) { + instructions.push( + `For categories, choose one or more from this list: ${CATEGORIES.join(", ")}.`, + ); + } + + const prompt = `${promptContext}\n\n${instructions.join(" ")}`; try { const result = await session.generateJSON( @@ -317,11 +307,11 @@ function classificationSchema(fields: ClassificationField[]): GbnfJsonSchema { properties.tags = { type: "array", items: { type: "string" }, - minItems: 5, + minItems: MIN_TAGS, }; } if (fields.includes("categories")) { - properties.categories = { type: "array", items: { type: "string" } }; + properties.categories = { type: "array", items: { enum: CATEGORIES } }; } if (fields.includes("platforms")) { properties.platforms = { type: "array", items: { enum: PLATFORMS } }; From d95477d07909016092a0bb700619226f4b7d4139 Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Wed, 23 Sep 2026 16:21:16 -0400 Subject: [PATCH 17/21] adding classfication instructions --- src/enrich.ts | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/enrich.ts b/src/enrich.ts index 7b059695..4db20c82 100644 --- a/src/enrich.ts +++ b/src/enrich.ts @@ -13,7 +13,7 @@ import { Logger } from "./types/Dependencies.js"; const LONG_DESCRIPTION_MIN_LENGTH = 150; const LONG_DESCRIPTION_MAX_LENGTH = 10000; const DEFAULT_README_MAX_CHARS = 4000; -const MIN_TAGS = 5; +const TARGET_ITEM_COUNT = 5; export const ENRICHABLE_FIELDS = [ "longDescription", @@ -44,7 +44,7 @@ export function missingEnrichableFields(codeJSON: CodeJSON): EnrichableField[] { codeJSON.longDescription.trim().length < LONG_DESCRIPTION_MIN_LENGTH ); case "tags": - return codeJSON.tags.length < MIN_TAGS; + return codeJSON.tags.length < TARGET_ITEM_COUNT; case "categories": case "platforms": return codeJSON[field].length === 0; @@ -272,17 +272,31 @@ async function generateClassification( fields: ClassificationField[], log: Logger, ): Promise> { - const instructions = ["Classify this software project."]; + const instructions = [ + "Classify this software project for a code.json metadata record. Choose only labels the project clearly fits; do not pad.", + ]; if (fields.includes("tags")) { instructions.push( - `For tags, list at least ${MIN_TAGS} single words or short phrases describing the project's purpose, domain, or technology.`, + `For tags, list at least ${TARGET_ITEM_COUNT} of the best single words or short phrases describing the project's purpose, domain, or technology.`, ); } if (fields.includes("categories")) { instructions.push( - `For categories, choose one or more from this list: ${CATEGORIES.join(", ")}.`, + `For categories, choose the best matching items from this list: ${CATEGORIES.join(", ")}.`, + ); + } + + if (fields.includes("softwareType")) { + instructions.push( + "For softwareType, say what the software is: addon extends another product or platform, library is imported by other code, standalone/* runs on its own.", + ); + } + + if (fields.includes("repositoryType")) { + instructions.push( + "For repositoryType, say what this repository ships: tools for developer or automation tooling, application for an end-user product, package for a published dependency, website for a site's source.", ); } @@ -307,14 +321,23 @@ function classificationSchema(fields: ClassificationField[]): GbnfJsonSchema { properties.tags = { type: "array", items: { type: "string" }, - minItems: MIN_TAGS, + minItems: TARGET_ITEM_COUNT, }; } if (fields.includes("categories")) { - properties.categories = { type: "array", items: { enum: CATEGORIES } }; + properties.categories = { + type: "array", + items: { enum: CATEGORIES }, + minItems: 1, + maxItems: TARGET_ITEM_COUNT, + }; } if (fields.includes("platforms")) { - properties.platforms = { type: "array", items: { enum: PLATFORMS } }; + properties.platforms = { + type: "array", + items: { enum: PLATFORMS }, + minItems: 1, + }; } if (fields.includes("softwareType")) { properties.softwareType = { enum: SOFTWARE_TYPES }; From 874d04b4cbe31db6d1ce77c989d6819eafc65e44 Mon Sep 17 00:00:00 2001 From: Sachin Panayil <79382140+sachin-panayil@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:09:01 -0400 Subject: [PATCH 18/21] Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/enrich.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/enrich.ts b/src/enrich.ts index 4db20c82..fea98248 100644 --- a/src/enrich.ts +++ b/src/enrich.ts @@ -59,8 +59,14 @@ export function condenseReadme( content: string, maxChars: number = DEFAULT_README_MAX_CHARS, ): string { - const stripped = content - .replace(//g, "") + let withoutComments = content; + let previous: string; + do { + previous = withoutComments; + withoutComments = withoutComments.replace(//g, ""); + } while (withoutComments !== previous); + + const stripped = withoutComments .replace(/```[\s\S]*?```/g, "") .replace(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g, "") .replace(/\n{3,}/g, "\n\n") From 82684e4baa3ef61cf414aab22cb35e390bd494ed Mon Sep 17 00:00:00 2001 From: Sachin Panayil Date: Mon, 28 Sep 2026 15:24:34 -0400 Subject: [PATCH 19/21] adding security logic to reduce attacks to new AI vector --- .github/workflows/update-codejson.yml | 1 - dockerfile | 9 ++- src/__tests__/fixtures/mock-deps.ts | 2 + src/__tests__/unit/enrich.test.ts | 111 +++++++++++++++++++++++--- src/__tests__/unit/helper.test.ts | 88 ++++++++++++++++++++ src/__tests__/unit/main.test.ts | 33 ++++++++ src/create-deps.ts | 8 +- src/enrich.ts | 72 ++++++++++++++++- src/helper.ts | 44 +++++++++- src/llm.ts | 18 +---- src/main.ts | 17 +++- src/types/Dependencies.ts | 3 + 12 files changed, 362 insertions(+), 44 deletions(-) diff --git a/.github/workflows/update-codejson.yml b/.github/workflows/update-codejson.yml index 5e16a8a7..0b4ebfe8 100644 --- a/.github/workflows/update-codejson.yml +++ b/.github/workflows/update-codejson.yml @@ -25,7 +25,6 @@ jobs: uses: DSACMS/automated-codejson-generator@sachin/local-ai-feature with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ADMIN_TOKEN: ${{ secrets.ADMIN_PAT }} BRANCH: "main" SKIP_PR: "false" diff --git a/dockerfile b/dockerfile index 7e2c7c43..9db3d075 100644 --- a/dockerfile +++ b/dockerfile @@ -1,5 +1,7 @@ ARG MODEL_REPO=unsloth/gemma-4-E2B-it-GGUF ARG MODEL_FILE=gemma-4-E2B-it-Q4_K_M.gguf +ARG MODEL_REVISION=0314792d7f1f7e229411f620751375812bb9faf2 +ARG MODEL_SHA256=740185b21d22ceb83a11c3aa62ad5842ef32c70f6096d756bbee85a1e4ec34b8 # stage 1: builder # install deps and build TS @@ -24,6 +26,8 @@ RUN npm run package FROM debian:bookworm-slim AS model ARG MODEL_REPO ARG MODEL_FILE +ARG MODEL_REVISION +ARG MODEL_SHA256 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -32,9 +36,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN mkdir -p /opt/models \ && curl -fSL --retry 3 \ - "https://huggingface.co/${MODEL_REPO}/resolve/main/${MODEL_FILE}?download=true" \ + "https://huggingface.co/${MODEL_REPO}/resolve/${MODEL_REVISION}/${MODEL_FILE}?download=true" \ -o /opt/models/model.gguf \ - && test "$(stat -c%s /opt/models/model.gguf)" -gt 100000000 + && echo "${MODEL_SHA256} /opt/models/model.gguf" | sha256sum -c - # stage 3: runtime # final image the action actually runs @@ -50,6 +54,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN curl -fSL --retry 3 \ "https://github.com/boyter/scc/releases/download/v3.4.0/scc_Linux_x86_64.tar.gz" \ -o /tmp/scc.tar.gz \ + && echo "8f812d25c237112ea74a4285f0699b7a2c448d70c5e63e370d2d199d38b13eda /tmp/scc.tar.gz" | sha256sum -c - \ && tar -xzf /tmp/scc.tar.gz -C /usr/local/bin scc \ && rm /tmp/scc.tar.gz \ && chmod +x /usr/local/bin/scc \ diff --git a/src/__tests__/fixtures/mock-deps.ts b/src/__tests__/fixtures/mock-deps.ts index d7ce9cc0..a80c8e3d 100644 --- a/src/__tests__/fixtures/mock-deps.ts +++ b/src/__tests__/fixtures/mock-deps.ts @@ -1,4 +1,5 @@ import { jest } from "@jest/globals"; +import { withModel } from "../../llm.js"; import { Dependencies, OctokitClient, @@ -101,6 +102,7 @@ export function createMockDeps( stderr: "", }), readFile: jest.fn().mockRejectedValue(new Error("File not found")), + runModel: withModel, log: createMockLogger(), setOutput: jest.fn(), diff --git a/src/__tests__/unit/enrich.test.ts b/src/__tests__/unit/enrich.test.ts index afeb9106..c31946ba 100644 --- a/src/__tests__/unit/enrich.test.ts +++ b/src/__tests__/unit/enrich.test.ts @@ -4,6 +4,8 @@ import { condenseReadme, applyEnrichment, enrichCodeJSON, + enrichedFields, + screenGenerated, } from "../../enrich.js"; import { CodeJSON } from "../../codejson.js"; import { ModelRunner, ModelSession } from "../../llm.js"; @@ -159,14 +161,14 @@ describe("applyEnrichment", () => { expect(result.categories).toEqual([]); }); - it("clamps longDescription to the schema's maximum length", () => { + it("clamps longDescription to 2000 characters", () => { const result = applyEnrichment( blankDraft, - { longDescription: "a".repeat(10500) }, + { longDescription: "a ".repeat(1500) }, ["longDescription"], ); - expect(result.longDescription).toHaveLength(10000); + expect(result.longDescription).toHaveLength(2000); }); it("merges generated tags with existing tags instead of replacing them", () => { @@ -268,7 +270,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( complete, - { readme: null }, + { readme: null, secrets: [] }, createMockLogger(), runModel as unknown as ModelRunner, ); @@ -282,7 +284,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( blankDraft, - { readme: null }, + { readme: null, secrets: [] }, createMockLogger(), runModel, ); @@ -298,7 +300,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( blankDraft, - { readme: null }, + { readme: null, secrets: [] }, log, runModel, ); @@ -323,7 +325,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( blankDraft, - { readme: null }, + { readme: null, secrets: [] }, createMockLogger(), runnerFor(session), ); @@ -350,7 +352,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( { ...blankDraft, repositoryType: "tools" }, - { readme: null }, + { readme: null, secrets: [] }, log, runnerFor(session), ); @@ -374,7 +376,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( draft, - { readme: null }, + { readme: null, secrets: [] }, createMockLogger(), runnerFor(session), ); @@ -394,7 +396,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( blankDraft, - { readme: null }, + { readme: null, secrets: [] }, log, runnerFor(session), ); @@ -417,7 +419,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( blankDraft, - { readme: null }, + { readme: null, secrets: [] }, log, runnerFor(session), ); @@ -438,7 +440,7 @@ describe("enrichCodeJSON", () => { const result = await enrichCodeJSON( { ...complete, softwareType: blank }, - { readme: null }, + { readme: null, secrets: [] }, createMockLogger(), runnerFor(session), ); @@ -453,7 +455,7 @@ describe("enrichCodeJSON", () => { await enrichCodeJSON( { ...complete, longDescription: "" }, - { readme }, + { readme, secrets: [] }, createMockLogger(), runnerFor(session), ); @@ -465,3 +467,86 @@ describe("enrichCodeJSON", () => { expect(prompt).not.toContain("internal note"); }); }); + +describe("screenGenerated", () => { + const token = "ghs_abcdEFGH1234ijklMNOP5678"; + const safeDescription = + "A tool that generates code.json metadata for federal repositories."; + + it("keeps output that passes every check", () => { + const generated = { longDescription: safeDescription, tags: ["metadata"] }; + + expect(screenGenerated(generated, [token], createMockLogger())).toEqual( + generated, + ); + }); + + it.each([ + ["a link", "See https://evil.example for details."], + ["a bare www link", "Visit www.evil.example today."], + ["html", "Metadata tool."], + ["a markdown link", "Read [the docs](evil) first."], + ["an encoded blob", "Payload aGVsbG8gd29ybGQgaGVsbG8gd29ybGQK here."], + ["a leaked token", `Configured with ${token} on every run.`], + [ + "a transformed token fragment", + `Key is ${token.slice(7, 15).toUpperCase()} ok.`, + ], + ])("discards a longDescription containing %s", (_, longDescription) => { + const log = createMockLogger(); + const result = screenGenerated({ longDescription }, [token], log); + + expect(result.longDescription).toBeUndefined(); + expect(log.warning).toHaveBeenCalledWith( + expect.stringContaining("longDescription"), + ); + }); + + it("drops only the tags that fail and caps the count", () => { + const tags = [ + "metadata", + "https://evil.example", + "