Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/dead-code-scan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: dead-code-scan

# Unused-export / unused-file / unused-dependency detection (knip), reported to the PR job
# summary. The gap this fills: eslint's no-unused-vars only sees WITHIN a file, and the
# duplication scan only sees copy/paste — neither catches an export nobody imports, an
# orphaned file, or a dependency whose last import was removed.
#
# Report-only (warn mode): knip.jsonc sets every rule to "warn" or "off", so `knip` exits 0,
# and this job additionally forces `exit 0` so it can never gate CI regardless of future rule
# changes. Findings are for a human to judge; promote a rule to "error" in knip.jsonc (and drop
# the forced exit 0) if a class of finding becomes trustworthy enough to block.
#
# Config lives in knip.jsonc; run it locally with `yarn knip`.

on:
pull_request:
branches: [main]
# Dead code can only change when an analyzed input changes. Keep in sync with knip.jsonc's
# project globs and tsconfig (module resolution). package.json and yarn.lock are included
# too: knip auto-detects entry points from package.json / index.html and resolves imports
# through the installed dependency graph.
paths:
- "**/*.ts"
- "**/*.vue"
- "**/tsconfig*.json"
- "knip.jsonc"
- "package.json"
- "yarn.lock"
- ".github/workflows/dead-code-scan.yaml"
workflow_dispatch:

permissions:
contents: read

jobs:
dead-code-scan:
name: Dead Code Scan (knip)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- uses: actions/setup-node@v6
with:
node-version: 22

# knip resolves imports through the real dependency graph, so node_modules has to be
# installed. --frozen-lockfile keeps CI honest against yarn.lock.
- run: yarn install --frozen-lockfile --network-timeout 120000

# Capture knip's output and write it to the job summary EITHER WAY, then exit 0. A
# `{ ... } >> $GITHUB_STEP_SUMMARY` block ends with printf's status, so the trailing
# `exit 0` is what actually guarantees report-only.
- name: Run knip
run: |
set -uo pipefail
output=$(yarn --silent knip --reporter compact 2>&1) || true
{
printf '## Dead code scan (knip)\n\n'
printf 'Report-only (warn mode): every finding below is for a human to judge; this\n'
printf 'job never fails. See .github/workflows/dead-code-scan.yaml.\n\n'
printf '```\n'
printf '%s\n' "$output"
printf '```\n'
} >> "$GITHUB_STEP_SUMMARY"
exit 0
66 changes: 66 additions & 0 deletions .github/workflows/duplication-scan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: duplication-scan

# Copy/paste duplication detection (jscpd), reported to GitHub code scanning.
# Report-only: no --threshold is set, so jscpd never fails the job. The findings
# surface as SARIF annotations for a human to judge, not as a gate.

on:
pull_request:
branches: [main]
paths-ignore:
- "docs/**"
- "plans/**"
- "**/*.md"
push:
branches: [main]

permissions:
contents: read

jobs:
duplication-scan:
name: Duplication Scan (jscpd)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
security-events: write # required to upload SARIF to code scanning
actions: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install jscpd
env:
JSCPD_VERSION: 5.0.12
# SHA256 of jscpd-linux-x64-gnu.tar.gz (jscpd ships no checksums.txt, so this is
# computed by hand). Update it together with JSCPD_VERSION on any bump.
JSCPD_SHA256: c1107547ee52bc83131d6e62d1fc9c156d194c593a4532876cdb1584b4e1dc3b
run: |
set -euo pipefail
ARCHIVE="jscpd-linux-x64-gnu.tar.gz"
curl -sSfL -o "$ARCHIVE" \
"https://github.com/kucherenko/jscpd/releases/download/v${JSCPD_VERSION}/${ARCHIVE}"
echo "${JSCPD_SHA256} ${ARCHIVE}" | sha256sum -c -
sudo tar -xz -C /usr/local/bin -f "$ARCHIVE" jscpd
rm -f "$ARCHIVE"
jscpd --version

- name: Run jscpd
run: |
set -euo pipefail
# dist/ is Vite/tsc build output (a duplicate of src by construction) and test/ + tests/
# hold standalone runner scripts, so all are excluded to keep the scan on real source.
jscpd . \
--format "typescript,vue" \
--ignore "**/node_modules/**,**/dist/**,**/*.d.ts,**/test/**,**/tests/**" \
--reporters console,sarif \
--output report

- name: Upload SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
continue-on-error: true
with:
sarif_file: report/jscpd-report.sarif
category: jscpd
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ output
logs
benchmark/results
*~*.tgz

# jscpd duplication-scan output (see .github/workflows/duplication-scan.yaml)
report/
25 changes: 25 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
// Vite + Vue app. The entry is index.html -> src/main.ts; knip's built-in vite plugin
// auto-detects index.html as an entry, so it is not listed here (listing it triggers a
// "redundant entry" hint). project is the client source under analysis; the Express
// backend (server/**) and benchmark/** live outside src and are not analyzed here.
"project": ["src/**/*.{ts,vue}"],
"ignoreExportsUsedInFile": true,
"includeEntryExports": false,
"rules": {
// Warn mode: every rule is "warn" (reported, exit 0) or "off". Nothing gates CI here.
"files": "warn",
"dependencies": "warn",
"devDependencies": "warn",
"unlisted": "warn",
"exports": "warn",
"types": "warn",
"unresolved": "off",
"binaries": "off",
"nsExports": "off",
"nsTypes": "off",
"enumMembers": "off",
"duplicates": "off",
},
}
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"benchmark:test": "tsx benchmark/src/test-runner.ts",
"benchmark:check": "tsx benchmark/src/cli.ts check",
"benchmark:list": "tsx benchmark/src/cli.ts list",
"benchmark:re-evaluate": "tsx benchmark/src/re-evaluate.ts"
"benchmark:re-evaluate": "tsx benchmark/src/re-evaluate.ts",
"knip": "knip"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.115.0",
Expand Down Expand Up @@ -81,7 +82,6 @@
"material-icons": "^1.13.14",
"uuid": "^14.0.0",
"vue": "^3.5.40",
"vue-drawing-canvas": "^1.0.14",
"vue-router": "^5.2.0",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
Expand All @@ -97,10 +97,8 @@
"@tailwindcss/postcss": "^4.3.3",
"@tailwindcss/vite": "^4.3.3",
"@types/cors": "^2.8.19",
"@types/dotenv": "^8.2.3",
"@types/express": "^5.0.6",
"@types/node": "^26.1.2",
"@types/uuid": "^11.0.0",
"@vitejs/plugin-vue": "^6.0.8",
"autoprefixer": "^10.5.4",
"concurrently": "^10.0.4",
Expand All @@ -109,6 +107,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-sonarjs": "^4.2.0",
"globals": "^17.8.0",
"knip": "^6",
"postcss": "^8.5.25",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.3",
Expand Down
Loading
Loading