Merge origin/main into fix/hook-augment-startup-deadline
Conflicts resolved: - src/daemon/runtime.h: both sides add test seams at the end of the CBM_ENABLE_TEST_SEAMS block. Kept main's connection-cap and HELLO-order seams and placed this branch's fingerprint-cache seams after them; the branch's added lines are unchanged. - src/cli/hook_augment.c: kept main's re-arm guard, which opens the timeouts log only once and keeps that descriptor. Dropped main's early return and breadcrumb formatting after the open: this branch formats the breadcrumb before the log path is resolved, so stderr gets it even when the log cannot be opened. That formatting runs on every arm, so main's re-arm comment now says the text above is refreshed, and the #858 header comment now says the breadcrumb also goes to stderr. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "codebase-memory",
|
||||
"description": "The codebase-memory-mcp code knowledge graph server, as a Claude Code plugin.",
|
||||
"owner": {
|
||||
"name": "DeusData",
|
||||
"url": "https://github.com/DeusData"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "codebase-memory",
|
||||
"source": "./plugin",
|
||||
"description": "Codebase knowledge graph for AI agents: sub-ms queries, 99% fewer tokens."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"test_impact": {
|
||||
"version": 1,
|
||||
"tests": {
|
||||
"presets": {
|
||||
"c-cbm": true
|
||||
}
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"id": "harness",
|
||||
"paths": ["tests/test_framework.h", "tests/test_helpers.*", "tests/test_main.c"],
|
||||
"action": "run_all"
|
||||
},
|
||||
{
|
||||
"id": "engine",
|
||||
"paths": [
|
||||
".codebase-memory.json",
|
||||
"src/mcp/test_impact*",
|
||||
"src/store/store_impact.*",
|
||||
"scripts/test-impact/**",
|
||||
"scripts/ci/test-impact-*"
|
||||
],
|
||||
"action": "run_all"
|
||||
},
|
||||
{
|
||||
"id": "build",
|
||||
"paths": [
|
||||
"scripts/build.sh",
|
||||
"scripts/env.sh",
|
||||
"scripts/test.sh",
|
||||
"scripts/run-tests-parallel.sh",
|
||||
"scripts/run-test-wave.py",
|
||||
"scripts/path-safety.sh",
|
||||
"scripts/clean.sh",
|
||||
"scripts/suite-failure-report.sh",
|
||||
"scripts/ci/verify-shard-union.sh",
|
||||
"scripts/embed-frontend.sh",
|
||||
"/.gitattributes"
|
||||
],
|
||||
"action": "run_all"
|
||||
},
|
||||
{
|
||||
"id": "grammars",
|
||||
"paths": ["internal/cbm/vendored/**"],
|
||||
"action": "run_all"
|
||||
},
|
||||
{
|
||||
"id": "fixtures",
|
||||
"paths": ["tests/fixtures/*/**"],
|
||||
"action": "referencing_tests",
|
||||
"key": "path_segment:2"
|
||||
},
|
||||
{
|
||||
"id": "contracts",
|
||||
"paths": ["tests/**/*.{sh,py,json,txt,ps1}"],
|
||||
"action": "ignore"
|
||||
},
|
||||
{
|
||||
"id": "notices",
|
||||
"paths": ["NOTICE*", "CHANGELOG*", "THIRD_PARTY*"],
|
||||
"action": "ignore"
|
||||
},
|
||||
{
|
||||
"id": "repo-data",
|
||||
"paths": [
|
||||
"scripts/**",
|
||||
"pkg/**",
|
||||
"test-infrastructure/**",
|
||||
"docs/**",
|
||||
"**/*.md",
|
||||
"graph-ui/**",
|
||||
"tools/**",
|
||||
"tests/fuzz/corpus/**",
|
||||
"/{.clang-format,.clang-tidy,.cppcheck,.gitignore,.gitleaksignore,DCO,Formula}",
|
||||
"/{flake.lock,flake.nix,glama.json,install.ps1,install.sh,server.json}"
|
||||
],
|
||||
"action": "referencing_tests",
|
||||
"key": "path",
|
||||
"unreferenced": "ignore"
|
||||
}
|
||||
],
|
||||
"unmapped_files": "run_all",
|
||||
"coverage": {
|
||||
"compatibility_paths": [
|
||||
"tests/test_framework.h",
|
||||
"tests/test_main.c",
|
||||
"tests/test_helpers.h",
|
||||
"Makefile.cbm",
|
||||
"scripts/test-impact/coverage-map.py",
|
||||
"scripts/test-impact/coverage-merge.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,3 +15,10 @@ scripts/hooks/pre-commit text eol=lf
|
||||
# Chialisp grammar corpus: one case asserts CRLF comment termination, so this
|
||||
# file must keep its exact bytes on every platform (no eol conversion). (#919)
|
||||
tools/tree-sitter-chialisp/test/corpus/comments.txt -text
|
||||
|
||||
# llvm-profdata text captures: tests/test_profile_text_parser.py checks their
|
||||
# SHA-256, so they keep their exact bytes on every platform (no eol conversion).
|
||||
tests/fixtures/profile_text/*.proftext -text
|
||||
# Golden profile-map outputs: tests/test_profile_map_v2.py compares the
|
||||
# producer's LF bytes against them, so they keep their exact bytes too.
|
||||
tests/fixtures/profile_map_v2/*.tsv -text
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# CodeQL scope: the repository's first-party source.
|
||||
#
|
||||
# Read by .github/workflows/codeql.yml (every scan, full and PR) and by
|
||||
# scripts/ci/select-lanes.sh (which changed paths select which language's PR
|
||||
# scan). Keep the lists plain: one path prefix per line, no globs.
|
||||
#
|
||||
# paths / paths-ignore narrow what the INTERPRETED languages extract
|
||||
# (javascript-typescript, python, actions). For c-cpp they have no effect:
|
||||
# CodeQL extracts exactly what the traced build compiles. A PR's c-cpp scan
|
||||
# keeps vendored C out by compiling it before tracing starts (except vendored
|
||||
# files the PR changes); the full scan on main, the dry run and the release
|
||||
# traces everything, vendored C included.
|
||||
name: cbm first-party source
|
||||
paths:
|
||||
- src
|
||||
- internal
|
||||
- graph-ui
|
||||
- pkg
|
||||
- scripts
|
||||
- tests
|
||||
- .github
|
||||
paths-ignore:
|
||||
- internal/cbm/vendored
|
||||
- graph-ui/vendor
|
||||
- graph-ui/fixtures
|
||||
- graph-ui/verification
|
||||
- tests/fixtures
|
||||
@@ -4,3 +4,9 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
# init and analyze must run the same CodeQL Action version within a job, and
|
||||
# upload-sarif is kept in step with them, so one PR bumps all three.
|
||||
codeql-action:
|
||||
patterns:
|
||||
- "github/codeql-action*"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Constraints for the ScanCode install of the license gate (PIP_CONSTRAINT).
|
||||
# pluggy 1.7.0 (2026-10-15) no longer finds plugincode 32.0.0's class-level
|
||||
# hookspecs: scancode then crashes at import with "did not find any
|
||||
# 'pre_scan' hooks" and the gate fails on every run.
|
||||
pluggy==1.6.0
|
||||
@@ -41,7 +41,9 @@ jobs:
|
||||
goarch: arm64
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
- os: macos-14
|
||||
# The macOS minimum (14.0) is pinned in Makefile.cbm, not inherited
|
||||
# from this runner's SDK, so both darwin binaries still start on macOS 14.
|
||||
- os: macos-15
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
cc: cc
|
||||
@@ -64,9 +66,10 @@ jobs:
|
||||
|
||||
- name: Install deps (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
@@ -105,7 +108,7 @@ jobs:
|
||||
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/unstripped/*
|
||||
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/THIRD_PARTY_NOTICES.md
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-candidates-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
@@ -117,7 +120,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
@@ -129,7 +132,7 @@ jobs:
|
||||
unzip
|
||||
zip
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
@@ -163,7 +166,7 @@ jobs:
|
||||
release-candidates/windows-amd64/unstripped/*
|
||||
release-candidates/windows-amd64/THIRD_PARTY_NOTICES.md
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-candidates-windows-amd64
|
||||
path: release-candidates/windows-amd64
|
||||
@@ -179,7 +182,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANGARM64
|
||||
path-type: inherit
|
||||
@@ -191,7 +194,7 @@ jobs:
|
||||
unzip
|
||||
zip
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
@@ -225,7 +228,7 @@ jobs:
|
||||
release-candidates/windows-arm64/unstripped/*
|
||||
release-candidates/windows-arm64/THIRD_PARTY_NOTICES.md
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-candidates-windows-arm64
|
||||
path: release-candidates/windows-arm64
|
||||
@@ -248,9 +251,10 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
@@ -288,7 +292,7 @@ jobs:
|
||||
release-candidates/linux-${{ matrix.arch }}-portable/unstripped/*
|
||||
release-candidates/linux-${{ matrix.arch }}-portable/THIRD_PARTY_NOTICES.md
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-candidates-linux-${{ matrix.arch }}-portable
|
||||
path: release-candidates/linux-${{ matrix.arch }}-portable
|
||||
@@ -313,7 +317,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download all native candidate pairs
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: release-candidates-*
|
||||
path: ${{ runner.temp }}/release-candidate-artifacts
|
||||
@@ -442,7 +446,7 @@ jobs:
|
||||
|
||||
- name: Preserve candidate selection evidence
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-selection-evidence
|
||||
path: selection-evidence
|
||||
@@ -451,56 +455,56 @@ jobs:
|
||||
# Keep the established artifact names so the reusable smoke workflow and
|
||||
# all callers consume only the selected, byte-verified containers.
|
||||
- name: Upload linux-amd64 containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-linux-amd64
|
||||
path: release-containers/linux-amd64/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload linux-arm64 containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-linux-arm64
|
||||
path: release-containers/linux-arm64/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload linux-amd64-portable containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-linux-amd64-portable
|
||||
path: release-containers/linux-amd64-portable/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload linux-arm64-portable containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-linux-arm64-portable
|
||||
path: release-containers/linux-arm64-portable/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload darwin-amd64 containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-darwin-amd64
|
||||
path: release-containers/darwin-amd64/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload darwin-arm64 containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-darwin-arm64
|
||||
path: release-containers/darwin-arm64/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload windows-amd64 containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-windows-amd64
|
||||
path: release-containers/windows-amd64/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload windows-arm64 containers
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: binaries-windows-arm64
|
||||
path: release-containers/windows-arm64/*
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
# Reusable: lint only (cppcheck + clang-format).
|
||||
# Security-static and CodeQL gate are separate — see _security.yml.
|
||||
# Security-static is separate — see _security.yml; CodeQL — see codeql.yml.
|
||||
name: Lint & Security
|
||||
|
||||
on:
|
||||
workflow_call: {}
|
||||
workflow_call:
|
||||
inputs:
|
||||
lanes:
|
||||
description: 'PR lane selection (scripts/ci/select-lanes.sh JSON list); "all" = every job (dry run, release)'
|
||||
type: string
|
||||
default: all
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'lint') }}
|
||||
# Pinned, not ubuntu-latest: the LLVM install below adds an
|
||||
# apt.llvm.org llvm-toolchain-NOBLE repo, which is 24.04 by name. When
|
||||
# GitHub moves ubuntu-latest to the next LTS this job breaks on a day
|
||||
# nobody chose. Pin the runner to the release the repo line already names.
|
||||
runs-on: ubuntu-24.04
|
||||
# 15 min proved too tight on a slow runner day and a timed-out lint reads
|
||||
# as "cancelled", which the release graph must treat as a hard stop; keep
|
||||
# a bound, but one only a genuine hang can hit (normal runtime ~5 min).
|
||||
timeout-minutes: 30
|
||||
# a bound, but one only a genuine hang can hit. 30 min stopped being that
|
||||
# in 2026-10: single-process cppcheck over a src tree that grew ~60 % in
|
||||
# files took the lint step from ~8 to 22-29 min, and three runs on
|
||||
# 2026-10-13 were cancelled at 30 min with cppcheck still alive.
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -24,14 +37,16 @@ jobs:
|
||||
run: bash scripts/check-no-test-skips.sh
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev cmake
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev cmake
|
||||
|
||||
- name: Install LLVM 20
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-20
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
|
||||
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-format-20
|
||||
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: cppcheck-cache
|
||||
@@ -60,20 +75,24 @@ jobs:
|
||||
# provability (never suppressed — the NOLINT ban applies here too).
|
||||
# Vendored-tree diagnostics are path-filtered, mirroring .cppcheck.
|
||||
lint-mem:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'lint-mem') }}
|
||||
# Pinned for the same reason as `lint`: llvm-toolchain-noble-21 below.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
|
||||
- name: Install LLVM 21 (pinned analyzer toolchain)
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-tidy-21
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
|
||||
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-tidy-21
|
||||
|
||||
- name: Memory-analyzer gate
|
||||
run: scripts/ci/lint-mem.sh clang-tidy-21
|
||||
|
||||
@@ -37,7 +37,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
|
||||
- name: Event lane on this repository
|
||||
run: |
|
||||
# `cmd | tee` reports TEE's status: without this a failing lane
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
run: python3 scripts/memwaste-ratchet.py --out "$RUNNER_TEMP/memwaste-event" --venue ${{ inputs.mode }}-linux
|
||||
- name: Upload report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memwaste-event-linux-${{ inputs.mode }}
|
||||
path: |
|
||||
@@ -66,7 +67,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
|
||||
- name: CPU scaling lane (src/ at 2x against 4x)
|
||||
# Report-only: the script exits 1 when it flags a super-linear function.
|
||||
continue-on-error: true
|
||||
@@ -80,7 +82,7 @@ jobs:
|
||||
--out "$RUNNER_TEMP/memwaste-scaling" | tee "$RUNNER_TEMP/memwaste-scaling/report.txt"
|
||||
- name: Upload report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memwaste-scaling-linux-${{ inputs.mode }}
|
||||
path: ${{ runner.temp }}/memwaste-scaling/report.txt
|
||||
@@ -93,7 +95,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
|
||||
- name: Access lane on this repository
|
||||
run: |
|
||||
# `cmd | tee` reports TEE's status: without this a failing lane
|
||||
@@ -105,7 +108,7 @@ jobs:
|
||||
--out "$RUNNER_TEMP/memwaste-access" | tee "$RUNNER_TEMP/memwaste-access/report.txt"
|
||||
- name: Upload report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memwaste-access-linux-${{ inputs.mode }}
|
||||
path: |
|
||||
@@ -115,7 +118,7 @@ jobs:
|
||||
|
||||
event-macos:
|
||||
if: ${{ inputs.mode == 'full' }}
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -132,7 +135,7 @@ jobs:
|
||||
run: python3 scripts/memwaste-ratchet.py --out "$RUNNER_TEMP/memwaste-event" --venue full-macos
|
||||
- name: Upload report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memwaste-event-macos-${{ inputs.mode }}
|
||||
path: |
|
||||
@@ -146,7 +149,7 @@ jobs:
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
@@ -174,7 +177,7 @@ jobs:
|
||||
run: python3 scripts/memwaste-ratchet.py --out "$(cygpath -u "$RUNNER_TEMP")/memwaste-event" --venue full-windows
|
||||
- name: Upload report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: memwaste-event-windows-${{ inputs.mode }}
|
||||
path: |
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
# Reusable: security-static + CodeQL gate.
|
||||
# Reusable: security-static + license gate.
|
||||
# CodeQL is not here: PRs do not run it. It runs on push to main, weekly, and
|
||||
# inside dry-run.yml / release.yml, which call codeql.yml directly.
|
||||
# Runs independently from lint/test/build — does not block the main pipeline.
|
||||
# Affects overall workflow success status.
|
||||
name: Security Gate
|
||||
|
||||
on:
|
||||
workflow_call: {}
|
||||
workflow_call:
|
||||
inputs:
|
||||
lanes:
|
||||
description: 'PR lane selection (scripts/ci/select-lanes.sh JSON list); "all" = every job (dry run, release)'
|
||||
type: string
|
||||
default: all
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -16,19 +23,28 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: "Layer 1: Static allow-list audit"
|
||||
run: scripts/security-audit.sh
|
||||
run: |
|
||||
bash tests/test_security_audit_subprocess_calls.sh
|
||||
scripts/security-audit.sh
|
||||
- name: "Layer 6: UI security audit"
|
||||
run: scripts/security-ui.sh
|
||||
run: |
|
||||
bash tests/test_security_ui_urls.sh
|
||||
scripts/security-ui.sh
|
||||
- name: "Layer 8: Vendored dependency integrity"
|
||||
run: scripts/security-vendored.sh
|
||||
|
||||
license-gate:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'license-gate') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install ScanCode Toolkit
|
||||
run: pipx install scancode-toolkit
|
||||
# Pinned: an unpinned install picked up pluggy 1.7.0, which breaks
|
||||
# scancode at import (see .github/scancode-constraints.txt).
|
||||
env:
|
||||
PIP_CONSTRAINT: ${{ github.workspace }}/.github/scancode-constraints.txt
|
||||
run: pipx install scancode-toolkit==32.5.0
|
||||
- name: "Gate self-test (a planted violation must be detected)"
|
||||
run: scripts/license-gate.sh --selftest
|
||||
- name: "License compliance gate (one finding fails)"
|
||||
@@ -38,95 +54,3 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: scripts/audit-license-provenance.py
|
||||
|
||||
codeql-gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 240
|
||||
# Without security-events:read this job inherits the workflow's
|
||||
# contents:read, the code-scanning alert API answers 403, and the gate
|
||||
# below reported "0 alerts" on every run it has ever made. actions:read is
|
||||
# what the CodeQL-run poll above needs for the same reason.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
steps:
|
||||
- name: Wait for CodeQL on current commit (max 150 min)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# On pull_request events github.sha is the synthetic merge commit;
|
||||
# CodeQL runs are recorded against the PR head SHA.
|
||||
CURRENT_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
echo "Waiting for CodeQL to complete on $CURRENT_SHA..."
|
||||
# 300 attempts x 30s = 150 min. The previous budget was 90 x 30s = 45 min,
|
||||
# which is shorter than CodeQL actually takes on this repository: a measured
|
||||
# run on PR #1426 (head 7b72652a) completed with conclusion=success after
|
||||
# 124 min, 7 minutes AFTER this gate had already given up. That marked at
|
||||
# least five contributor PRs red for a scan that passed. The job's own
|
||||
# timeout-minutes is 240, so 150 still leaves headroom.
|
||||
for attempt in $(seq 1 300); do
|
||||
LATEST=$(gh api "repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?head_sha=$CURRENT_SHA&per_page=1" \
|
||||
--jq '.workflow_runs[] | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "")
|
||||
if [ -z "$LATEST" ]; then
|
||||
echo " $attempt/300: no run yet..."; sleep 30; continue
|
||||
fi
|
||||
CONCLUSION=$(echo "$LATEST" | cut -d' ' -f1)
|
||||
STATUS=$(echo "$LATEST" | cut -d' ' -f2)
|
||||
if [ "$STATUS" = "completed" ] && [ "$CONCLUSION" = "success" ]; then
|
||||
echo "=== CodeQL passed ==="; exit 0
|
||||
elif [ "$STATUS" = "completed" ]; then
|
||||
echo "BLOCKED: CodeQL $CONCLUSION"; exit 1
|
||||
fi
|
||||
echo " $attempt/300: $STATUS..."; sleep 30
|
||||
done
|
||||
echo "BLOCKED: CodeQL timeout"; exit 1
|
||||
|
||||
- name: Check for open code scanning alerts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# FAIL CLOSED. The previous form was `gh api ... 2>/dev/null || echo "0"`,
|
||||
# which looks defensive and is the opposite. gh writes the API error
|
||||
# BODY to stdout, so a 403 made ALERTS the string
|
||||
# {"message":"Resource not accessible by integration",...,"status":"403"}0
|
||||
# every `[` comparison then failed as non-integer, the `if` took its
|
||||
# false branch, and the step printed "CodeQL gate passed (0 alerts)"
|
||||
# and exited 0. That is exactly how a security gate should NEVER fail:
|
||||
# silently, in the direction of "everything is fine". An unreadable
|
||||
# alert list is now an error, not a zero.
|
||||
# NOTE: this runs in a command substitution, so it must `return`, not
|
||||
# `exit` -- an `exit` here would only end the subshell and let the
|
||||
# caller continue with an empty count, recreating the very bug this
|
||||
# replaces. The callers below check the status explicitly.
|
||||
count_open_alerts() {
|
||||
local body status
|
||||
body=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
|
||||
--jq 'length' 2>/dev/null)
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "BLOCKED: cannot read code scanning alerts (gh exit $status)." >&2
|
||||
echo " Refusing to treat an unreadable alert list as zero." >&2
|
||||
echo " Most likely cause: the job lacks security-events:read." >&2
|
||||
return 1
|
||||
fi
|
||||
case "$body" in
|
||||
''|*[!0-9]*)
|
||||
echo "BLOCKED: alert count was not a number: ${body:-<empty>}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$body"
|
||||
}
|
||||
echo "Waiting 60s for alert API to settle..."
|
||||
sleep 60
|
||||
ALERTS=$(count_open_alerts) || exit 1
|
||||
sleep 15
|
||||
ALERTS2=$(count_open_alerts) || exit 1
|
||||
[ "$ALERTS" -lt "$ALERTS2" ] && ALERTS=$ALERTS2
|
||||
if [ "$ALERTS" -gt 0 ]; then
|
||||
echo "BLOCKED: $ALERTS open alert(s)"
|
||||
gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
|
||||
--jq '.[] | " #\(.number) [\(.rule.security_severity_level // .rule.severity)] \(.rule.id) — \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "=== CodeQL gate passed (0 alerts) ==="
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
CORE_UNIX='[
|
||||
{"os":"ubuntu-latest","goos":"linux","goarch":"amd64"},
|
||||
{"os":"ubuntu-24.04-arm","goos":"linux","goarch":"arm64"},
|
||||
{"os":"macos-14","goos":"darwin","goarch":"arm64"},
|
||||
{"os":"macos-15","goos":"darwin","goarch":"arm64"},
|
||||
{"os":"macos-15-intel","goos":"darwin","goarch":"amd64"}
|
||||
]'
|
||||
# Broad legs reuse existing goos/goarch artifacts on additional OS
|
||||
@@ -47,9 +47,10 @@ jobs:
|
||||
# (ubuntu-22.04 / 22.04-arm). So the dynamic broad legs stay on
|
||||
# forward-compatible OSes only (macOS); running the dynamic binary on
|
||||
# ubuntu-22.04 would fail Phase 1 (glibc too old), not a real regression.
|
||||
BROAD_UNIX='[
|
||||
{"os":"macos-15","goos":"darwin","goarch":"arm64"}
|
||||
]'
|
||||
# Empty since the move off macos-14 (retired 2026-11-02): the darwin-arm64
|
||||
# artifact is now built AND smoked on macos-15, the core leg, which was
|
||||
# this broad leg.
|
||||
BROAD_UNIX='[]'
|
||||
CORE_WIN='[{"os":"windows-latest","arch":"amd64"}]'
|
||||
# windows-11-arm now runs the NATIVE arm64 binary (build-windows-arm64),
|
||||
# not the x86_64 binary under emulation — we smoke the artifact we ship.
|
||||
@@ -95,7 +96,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper; the
|
||||
# macOS runner images have none on PATH.
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: pkg/go/go.mod
|
||||
cache: false
|
||||
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
|
||||
@@ -118,6 +126,7 @@ jobs:
|
||||
env:
|
||||
CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact
|
||||
SMOKE_REQUIRE_UI: "1"
|
||||
SMOKE_REQUIRE_WRAPPERS: "1"
|
||||
|
||||
- name: Security audits
|
||||
run: |
|
||||
@@ -135,8 +144,9 @@ jobs:
|
||||
|
||||
- name: ClamAV scan (Linux)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update -qq && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y -qq clamav > /dev/null 2>&1
|
||||
# apt auto-starts the clamav-freshclam daemon, which holds a lock on
|
||||
# freshclam's log/db; stop it so the manual freshclam below can run
|
||||
# (else: "Failed to lock the log file ... Resource temporarily unavailable").
|
||||
@@ -175,7 +185,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper; the
|
||||
# MSYS2 shell below inherits it (path-type: inherit).
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: pkg/go/go.mod
|
||||
cache: false
|
||||
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: ${{ matrix.arch == 'arm64' && 'CLANGARM64' || 'CLANG64' }}
|
||||
path-type: inherit
|
||||
@@ -186,7 +203,7 @@ jobs:
|
||||
zip
|
||||
coreutils
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-windows-${{ matrix.arch }}
|
||||
|
||||
@@ -224,6 +241,7 @@ jobs:
|
||||
env:
|
||||
SMOKE_ARCH: ${{ matrix.arch }}
|
||||
SMOKE_REQUIRE_UI: "1"
|
||||
SMOKE_REQUIRE_WRAPPERS: "1"
|
||||
run: |
|
||||
export CBM_SMOKE_ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
|
||||
bash test-infrastructure/vm/vm-smoke.sh
|
||||
@@ -279,7 +297,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper.
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: pkg/go/go.mod
|
||||
cache: false
|
||||
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-linux-${{ matrix.arch }}-portable
|
||||
|
||||
@@ -301,6 +325,7 @@ jobs:
|
||||
env:
|
||||
CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact
|
||||
SMOKE_REQUIRE_UI: "1"
|
||||
SMOKE_REQUIRE_WRAPPERS: "1"
|
||||
|
||||
# The -portable binary is what all linux install/update paths now deliver;
|
||||
# it MUST start on old glibc (Debian 11 / RHEL 8 / Ubuntu 20.04). Runs it
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
goarch: arm64
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
- os: macos-14
|
||||
- os: macos-15
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
cc: cc
|
||||
@@ -66,13 +66,14 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps (Linux)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 git
|
||||
- name: Build
|
||||
if: ${{ !inputs.use_release_artifacts }}
|
||||
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
||||
- name: Download selected release container
|
||||
if: ${{ inputs.use_release_artifacts }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ${{ runner.temp }}/cbm-selected-container
|
||||
@@ -97,14 +98,14 @@ jobs:
|
||||
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
|
||||
- name: Upload metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-quick-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: soak-results/
|
||||
retention-days: 14
|
||||
- name: Upload query-leak metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-query-leak-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: soak-results-query-leak/
|
||||
@@ -118,7 +119,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
@@ -146,7 +147,7 @@ jobs:
|
||||
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
|
||||
- name: Download selected release container
|
||||
if: ${{ inputs.use_release_artifacts }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-windows-amd64
|
||||
path: ${{ runner.temp }}/cbm-selected-container
|
||||
@@ -174,14 +175,14 @@ jobs:
|
||||
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
|
||||
- name: Upload metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-quick-windows-amd64
|
||||
path: soak-results/
|
||||
retention-days: 14
|
||||
- name: Upload query-leak metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-query-leak-windows-amd64
|
||||
path: soak-results-query-leak/
|
||||
@@ -196,7 +197,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANGARM64
|
||||
path-type: inherit
|
||||
@@ -224,7 +225,7 @@ jobs:
|
||||
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
|
||||
- name: Download selected release container
|
||||
if: ${{ inputs.use_release_artifacts }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-windows-arm64
|
||||
path: ${{ runner.temp }}/cbm-selected-container
|
||||
@@ -252,14 +253,14 @@ jobs:
|
||||
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
|
||||
- name: Upload metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-quick-windows-arm64
|
||||
path: soak-results/
|
||||
retention-days: 14
|
||||
- name: Upload query-leak metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-query-leak-windows-arm64
|
||||
path: soak-results-query-leak/
|
||||
@@ -283,9 +284,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install soak dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y python3 git
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y python3 git
|
||||
- name: Download selected portable release container
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: binaries-linux-${{ matrix.arch }}-portable
|
||||
path: ${{ runner.temp }}/cbm-selected-container
|
||||
@@ -303,14 +305,14 @@ jobs:
|
||||
${{ inputs.duration_minutes }}
|
||||
- name: Upload metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-quick-linux-${{ matrix.arch }}-portable
|
||||
path: soak-results/
|
||||
retention-days: 14
|
||||
- name: Upload query-leak metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-query-leak-linux-${{ matrix.arch }}-portable
|
||||
path: soak-results-query-leak/
|
||||
@@ -335,7 +337,7 @@ jobs:
|
||||
goarch: arm64
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
- os: macos-14
|
||||
- os: macos-15
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
cc: cc
|
||||
@@ -356,7 +358,8 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps (Linux)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 git
|
||||
- name: Build (ASan)
|
||||
run: |
|
||||
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
|
||||
@@ -370,7 +373,7 @@ jobs:
|
||||
run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15
|
||||
- name: Upload metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-asan-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: soak-results/
|
||||
@@ -386,7 +389,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
@@ -410,7 +413,7 @@ jobs:
|
||||
run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15
|
||||
- name: Upload metrics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: soak-asan-windows-amd64
|
||||
path: soak-results/
|
||||
|
||||
@@ -16,6 +16,18 @@ on:
|
||||
description: 'Split each C-suite leg across parallel shard jobs (ubuntu x3, windows x2); off = one job per leg, byte-identical to the pre-shard topology'
|
||||
type: boolean
|
||||
default: false
|
||||
shard_profile:
|
||||
description: 'Shard counts when shard_suites is on: standard = ubuntu x3, windows x2 (dry run, release); pr = ubuntu x86 x1, ubuntu arm64 x2, windows x1 (fewer jobs re-paying setup)'
|
||||
type: string
|
||||
default: standard
|
||||
lanes:
|
||||
description: 'PR lane selection (scripts/ci/select-lanes.sh JSON list): only those legs and jobs run; "all" = every one (dry run, release)'
|
||||
type: string
|
||||
default: all
|
||||
test_selection:
|
||||
description: 'PR test selection (smart CI): the artifact the select-tests job uploaded; the Linux x86_64 test-unix leg runs only its tests, the arm64, macOS and Windows legs run every test. Empty = every test (dry run, release, and any PR the selection does not narrow)'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -30,25 +42,36 @@ jobs:
|
||||
outputs:
|
||||
unix: ${{ steps.set.outputs.unix }}
|
||||
windows: ${{ steps.set.outputs.windows }}
|
||||
tsan: ${{ steps.set.outputs.tsan }}
|
||||
has_unix: ${{ steps.set.outputs.has_unix }}
|
||||
has_windows: ${{ steps.set.outputs.has_windows }}
|
||||
has_tsan: ${{ steps.set.outputs.has_tsan }}
|
||||
steps:
|
||||
- name: Compute matrices
|
||||
id: set
|
||||
env:
|
||||
BROAD: ${{ inputs.broad_platforms }}
|
||||
SHARDS: ${{ inputs.shard_suites }}
|
||||
SHARD_PROFILE: ${{ inputs.shard_profile }}
|
||||
LANES: ${{ inputs.lanes }}
|
||||
run: |
|
||||
# Every CORE leg is tagged with the lane (scripts/ci/select-lanes.sh)
|
||||
# it serves; the tag is what lane selection filters on below.
|
||||
# The lane names unix-macos14 and tsan-mac are kept as lane keys
|
||||
# (select-lanes.sh, ci-ok and the lane contracts key on them); both
|
||||
# run on macos-15 because GitHub retires the macos-14 image on 2026-11-02.
|
||||
CORE_UNIX='[
|
||||
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++"},
|
||||
{"os":"ubuntu-24.04-arm","cc":"gcc","cxx":"g++"},
|
||||
{"os":"macos-14","cc":"cc","cxx":"c++"},
|
||||
{"os":"macos-15-intel","cc":"cc","cxx":"c++"}
|
||||
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++","lane":"unix-x86"},
|
||||
{"os":"ubuntu-24.04-arm","cc":"gcc","cxx":"g++","lane":"unix-arm64"},
|
||||
{"os":"macos-15","cc":"cc","cxx":"c++","lane":"unix-macos14"},
|
||||
{"os":"macos-15-intel","cc":"cc","cxx":"c++","lane":"unix-macos-intel"}
|
||||
]'
|
||||
# Broad matrix legs are REQUIRED gates (no optional/continue-on-error
|
||||
# escape hatches): the release dry run must be all-green, every platform.
|
||||
# macos-15 is the CORE macOS arm64 leg now, so it is not repeated here.
|
||||
BROAD_UNIX='[
|
||||
{"os":"ubuntu-22.04","cc":"gcc","cxx":"g++"},
|
||||
{"os":"ubuntu-22.04-arm","cc":"gcc","cxx":"g++"},
|
||||
{"os":"macos-15","cc":"cc","cxx":"c++"}
|
||||
{"os":"ubuntu-22.04-arm","cc":"gcc","cxx":"g++"}
|
||||
]'
|
||||
# Each Windows leg pins the msys2 environment + package arch to the
|
||||
# RUNNER architecture so the build is native, never emulated:
|
||||
@@ -59,7 +82,7 @@ jobs:
|
||||
# interception crashed (interception_win: unhandled instruction). With
|
||||
# the native ARM64 toolchain ASan instruments native ARM64 code, so it
|
||||
# is a real (non-optional) gate, not a tolerated emulated-flake.
|
||||
CORE_WIN='[{"os":"windows-latest","msystem":"CLANG64","pkg":"x86_64"}]'
|
||||
CORE_WIN='[{"os":"windows-latest","msystem":"CLANG64","pkg":"x86_64","lane":"windows"}]'
|
||||
BROAD_WIN='[{"os":"windows-2025","msystem":"CLANG64","pkg":"x86_64"},{"os":"windows-11-arm","msystem":"CLANGARM64","pkg":"aarch64"}]'
|
||||
if [ "$BROAD" = "true" ]; then
|
||||
UNIX=$(jq -cn --argjson a "$CORE_UNIX" --argjson b "$BROAD_UNIX" '$a + $b')
|
||||
@@ -68,6 +91,30 @@ jobs:
|
||||
UNIX=$(jq -cn --argjson a "$CORE_UNIX" '$a')
|
||||
WIN=$(jq -cn --argjson a "$CORE_WIN" '$a')
|
||||
fi
|
||||
# The ThreadSanitizer legs (test-tsan), tagged the same way.
|
||||
TSAN='[
|
||||
{"os":"ubuntu-latest","cc":"clang","cxx":"clang++","lane":"tsan-x86"},
|
||||
{"os":"ubuntu-24.04-arm","cc":"clang","cxx":"clang++","lane":"tsan-arm"},
|
||||
{"os":"macos-15","cc":"cc","cxx":"c++","lane":"tsan-mac"}
|
||||
]'
|
||||
# Lane selection. "all" (dry run, release) keeps every leg and drops
|
||||
# the tags, so those matrices -- and their job names -- stay exactly
|
||||
# as before lane selection. A PR keeps only its selected legs, and
|
||||
# the tag stays in the job name, where ci-ok reads it back. An
|
||||
# untagged leg could never be selected: that is a configuration
|
||||
# error here, never a leg silently dropped.
|
||||
if [ "$LANES" = "all" ]; then
|
||||
UNIX=$(jq -c 'map(del(.lane))' <<< "$UNIX")
|
||||
WIN=$(jq -c 'map(del(.lane))' <<< "$WIN")
|
||||
TSAN=$(jq -c 'map(del(.lane))' <<< "$TSAN")
|
||||
else
|
||||
for M in "$UNIX" "$WIN" "$TSAN"; do
|
||||
jq -e 'all(.[]; has("lane"))' <<< "$M" > /dev/null || { echo "::error::untagged test leg in $M"; exit 1; }
|
||||
done
|
||||
UNIX=$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$UNIX")
|
||||
WIN=$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$WIN")
|
||||
TSAN=$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$TSAN")
|
||||
fi
|
||||
# Suite sharding (opt-in via the shard_suites input): each platform's
|
||||
# C-suite run is split across N runner jobs. CBM_TEST_SHARD="i/N"
|
||||
# slices deterministically inside run-tests-parallel.sh; each
|
||||
@@ -78,20 +125,37 @@ jobs:
|
||||
# shards); Windows gets 2 (every extra shard re-pays ~5 min of
|
||||
# MSYS2 setup); macOS stays at 1 — mac legs are not the critical
|
||||
# path and mac runner concurrency ceilings are the tightest.
|
||||
# The pr profile shards less: a PR runs few legs and every shard
|
||||
# re-pays its setup, so x86 and Windows run whole and only arm64,
|
||||
# the slowest leg (~72 min unsharded), splits in two.
|
||||
case "$SHARD_PROFILE" in
|
||||
standard) N_X86=3; N_ARM=3; N_WIN=2 ;;
|
||||
pr) N_X86=1; N_ARM=2; N_WIN=1 ;;
|
||||
*) echo "::error::unknown shard_profile '$SHARD_PROFILE'"; exit 1 ;;
|
||||
esac
|
||||
if [ "$SHARDS" = "true" ]; then
|
||||
UNIX=$(jq -c '[.[] | . as $e
|
||||
| (if ($e.os | startswith("ubuntu")) then 3 else 1 end) as $n
|
||||
UNIX=$(jq -c --argjson x86 "$N_X86" --argjson arm "$N_ARM" '[.[] | . as $e
|
||||
| (if ($e.os | startswith("ubuntu")) then
|
||||
(if ($e.os | endswith("-arm")) then $arm else $x86 end)
|
||||
else 1 end) as $n
|
||||
| range(1; $n + 1) as $i
|
||||
| $e + {shard: "\($i)/\($n)"}]' <<< "$UNIX")
|
||||
WIN=$(jq -c '[.[] | . as $e
|
||||
| range(1; 3) as $i
|
||||
| $e + {shard: "\($i)/2"}]' <<< "$WIN")
|
||||
WIN=$(jq -c --argjson n "$N_WIN" '[.[] | . as $e
|
||||
| range(1; $n + 1) as $i
|
||||
| $e + {shard: "\($i)/\($n)"}]' <<< "$WIN")
|
||||
fi
|
||||
echo "unix={\"include\":$UNIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "windows={\"include\":$WIN}" >> "$GITHUB_OUTPUT"
|
||||
echo "tsan={\"include\":$TSAN}" >> "$GITHUB_OUTPUT"
|
||||
# GitHub rejects an empty matrix, so a job whose legs were all
|
||||
# deselected is skipped through these flags instead.
|
||||
echo "has_unix=$(jq 'length > 0' <<< "$UNIX")" >> "$GITHUB_OUTPUT"
|
||||
echo "has_windows=$(jq 'length > 0' <<< "$WIN")" >> "$GITHUB_OUTPUT"
|
||||
echo "has_tsan=$(jq 'length > 0' <<< "$TSAN")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
test-unix:
|
||||
needs: setup-matrix
|
||||
if: ${{ needs.setup-matrix.outputs.has_unix == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.setup-matrix.outputs.unix) }}
|
||||
@@ -105,7 +169,8 @@ jobs:
|
||||
|
||||
- name: Install deps (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
|
||||
|
||||
# Ubuntu 23.10+ ships kernel.apparmor_restrict_unprivileged_userns=1, which
|
||||
# blocks unshare(CLONE_NEWUSER) for unprivileged users. Without this, the
|
||||
@@ -160,12 +225,24 @@ jobs:
|
||||
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-
|
||||
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-
|
||||
|
||||
- name: Fetch the test selection
|
||||
# Only the Linux x86_64 leg narrows (user decision 2026-10-16): the
|
||||
# coverage that justifies an omission comes from the Linux x86_64
|
||||
# producer, so arm64, macOS and Windows code under #ifdef has static
|
||||
# evidence only and those legs run every test.
|
||||
if: ${{ inputs.test_selection != '' && runner.os == 'Linux' && runner.arch == 'X64' }}
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: ${{ inputs.test_selection }}
|
||||
path: ${{ runner.temp }}/test-selection
|
||||
|
||||
- name: Test
|
||||
run: scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
||||
env:
|
||||
CBM_TEST_SHARD: ${{ matrix.shard }}
|
||||
CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.cc }}
|
||||
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
|
||||
CBM_TEST_SELECTION_DIR: ${{ inputs.test_selection != '' && runner.os == 'Linux' && runner.arch == 'X64' && format('{0}/test-selection', runner.temp) || '' }}
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1500M
|
||||
|
||||
@@ -176,7 +253,7 @@ jobs:
|
||||
# too keeps red iterations warm.
|
||||
- name: Shard manifest (completeness proof)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: shard-manifest-${{ matrix.os }}-${{ matrix.cc }}-${{ strategy.job-index }}
|
||||
path: build/c/test-logs/shard-manifest.txt
|
||||
@@ -187,6 +264,23 @@ jobs:
|
||||
# shard-completeness job still gates the cross-shard union.
|
||||
if-no-files-found: warn
|
||||
|
||||
# The job log carries a red suite's SUMMARY; these are the complete logs
|
||||
# behind it, so a failure can be read instead of re-run. The harness
|
||||
# copies the log of every suite that did not end green, plus results.txt,
|
||||
# into test-logs/failed/ (scripts/suite-failure-report.sh) — typically a
|
||||
# few kilobytes for a red suite, not the ~25 MB of a whole run's logs.
|
||||
# Runs only on a failed job. warn, not error: a job that failed OUTSIDE
|
||||
# the suite wave (contract step, build, prod-binary guards) has no such
|
||||
# directory, and a second error would only bury the first.
|
||||
- name: Failing suite logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: suite-logs-${{ matrix.os }}-${{ matrix.cc }}-${{ strategy.job-index }}
|
||||
path: build/c/test-logs/failed/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Compiler cache (save)
|
||||
if: always() && (matrix.shard == null || startsWith(matrix.shard, '1/'))
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -205,6 +299,7 @@ jobs:
|
||||
# inode/flock behavior and Windows handle/link behavior are each gating,
|
||||
# without adding Node/Go provisioning to every sanitizer/container leg.
|
||||
test-package-wrappers:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'pkg-wrappers') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -219,11 +314,11 @@ jobs:
|
||||
go-version-file: pkg/go/go.mod
|
||||
cache: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -231,6 +326,28 @@ jobs:
|
||||
shell: bash
|
||||
run: scripts/ci/test-package-wrappers.sh
|
||||
|
||||
# CodeAtlasWeb (graph-ui) leg: the frontend's own vitest suite (2000+
|
||||
# tests; the render tests run under jsdom), its hardcoded-string and
|
||||
# promise scans, its portable frozen acceptance checks, and a production
|
||||
# build with the same npm ci the product build runs. One canonical script
|
||||
# (scripts/ci/test-ui.sh) is the whole leg, here and locally. No browser
|
||||
# is downloaded: the browser proofs (graph-ui/tools/smoke-*.mjs) need a
|
||||
# running server and Playwright browsers, and they stay a local, manually
|
||||
# dispatched venue documented in graph-ui/README.md rather than a hosted
|
||||
# gate.
|
||||
test-ui:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: CodeAtlasWeb leg (install, unit, style, promises, acceptance, build)
|
||||
run: scripts/ci/test-ui.sh
|
||||
|
||||
# ThreadSanitizer data-race gate. The threaded production surfaces (watcher,
|
||||
# subprocess, httpd, pipeline, mcp, worker pool, diagnostics, the runnable
|
||||
# daemon-coordination paths) plus allocator concurrency all run under TSan
|
||||
@@ -245,20 +362,25 @@ jobs:
|
||||
# parallel suite — catches what an older compiler-rt can miss. Same
|
||||
# canonical scripts/test.sh wave as every other leg.
|
||||
test-diag:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'diag') }}
|
||||
# Pinned for the same reason as the lint jobs: the pinned diagnostic
|
||||
# toolchain below installs from llvm-toolchain-noble-21.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
|
||||
|
||||
- name: Install LLVM 21 (pinned diagnostic toolchain)
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-21
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
|
||||
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-21
|
||||
|
||||
- name: Compiler cache (content-verified, restore)
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -280,6 +402,17 @@ jobs:
|
||||
scripts/test.sh CC=clang-21 CXX=clang++-21 \
|
||||
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls"
|
||||
|
||||
# Same step as in test-unix: the complete logs of the suites that did
|
||||
# not end green, only when the job failed.
|
||||
- name: Failing suite logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: suite-logs-diag
|
||||
path: build/c/test-logs/failed/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Compiler cache (save)
|
||||
if: always()
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -297,17 +430,21 @@ jobs:
|
||||
# artifact — the local ladder cannot answer that, having no faithful x86-64
|
||||
# emulation. It has now answered it, and the answer SPLIT the list:
|
||||
#
|
||||
# - the five deep-recursion suites overflow their thread stacks on x86-64
|
||||
# too, so that limit is NOT architectural;
|
||||
# - five suites aborted with "stack-overflow ... nested bug" on both
|
||||
# architectures. That was not deep recursion: it was MSan crashing while
|
||||
# it reported real uninitialized reads (reporting with the slow unwinder
|
||||
# exhausted the main thread's stack). scripts/msan.sh now unwinds with
|
||||
# frame pointers, the
|
||||
# reports print, the reads are fixed, and the five suites run again;
|
||||
# - `cli` does not overflow here at all — it fails in the install path for
|
||||
# an unrelated, still-undiagnosed reason;
|
||||
# - `incremental` was a shadow-memory RSS artifact, now fixed in the test
|
||||
# itself rather than skipped, so it stays in the lane.
|
||||
#
|
||||
# Per-cause detail lives in scripts/msan.sh, which is the single authoritative
|
||||
# list both venues now share. Zero use-of-uninitialized-value findings were
|
||||
# reported across the lane on either architecture.
|
||||
# list both venues now share.
|
||||
test-msan:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'msan') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
@@ -332,6 +469,7 @@ jobs:
|
||||
# so this leg is the ordinary ASan suite built with that toolchain and run
|
||||
# with detect_leaks=1. Mirrors the local `make -f Makefile.cbm test-lsan`.
|
||||
test-lsan-macos:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'lsan-macos') }}
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
@@ -362,6 +500,17 @@ jobs:
|
||||
LLVM_PREFIX="$(brew --prefix llvm@22)"
|
||||
scripts/test.sh CC="$LLVM_PREFIX/bin/clang" CXX="$LLVM_PREFIX/bin/clang++"
|
||||
|
||||
# Same step as in test-unix: the complete logs of the suites that did
|
||||
# not end green, only when the job failed.
|
||||
- name: Failing suite logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: suite-logs-lsan-macos
|
||||
path: build/c/test-logs/failed/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Compiler cache (save)
|
||||
if: always()
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -370,13 +519,11 @@ jobs:
|
||||
key: ccache-lsan-${{ github.ref }}-${{ github.sha }}
|
||||
|
||||
test-tsan:
|
||||
needs: setup-matrix
|
||||
if: ${{ needs.setup-matrix.outputs.has_tsan == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { os: ubuntu-latest, cc: clang, cxx: clang++ }
|
||||
- { os: ubuntu-24.04-arm, cc: clang, cxx: clang++ }
|
||||
- { os: macos-14, cc: cc, cxx: c++ }
|
||||
matrix: ${{ fromJSON(needs.setup-matrix.outputs.tsan) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
@@ -384,7 +531,8 @@ jobs:
|
||||
|
||||
- name: Install deps (Ubuntu)
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev ccache
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang zlib1g-dev ccache
|
||||
|
||||
- name: Install deps (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
@@ -429,6 +577,7 @@ jobs:
|
||||
|
||||
test-windows:
|
||||
needs: setup-matrix
|
||||
if: ${{ needs.setup-matrix.outputs.has_windows == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.setup-matrix.outputs.windows) }}
|
||||
@@ -445,7 +594,7 @@ jobs:
|
||||
# preflight script, so AV-interaction coverage lives there; runners run
|
||||
# without RTP by platform constraint.
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: ${{ matrix.msystem }}
|
||||
path-type: inherit
|
||||
@@ -506,6 +655,8 @@ jobs:
|
||||
CBM_TEST_SHARD: ${{ matrix.shard }}
|
||||
CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.msystem }}
|
||||
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
|
||||
# Windows legs run every test: see the Linux x86_64-only fetch in test-unix.
|
||||
CBM_TEST_SELECTION_DIR: ''
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1500M
|
||||
|
||||
@@ -518,7 +669,7 @@ jobs:
|
||||
|
||||
- name: Shard manifest (completeness proof)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: shard-manifest-${{ matrix.os }}-${{ matrix.msystem }}-${{ strategy.job-index }}
|
||||
path: build/c/test-logs/shard-manifest.txt
|
||||
@@ -529,6 +680,17 @@ jobs:
|
||||
# shard-completeness job still gates the cross-shard union.
|
||||
if-no-files-found: warn
|
||||
|
||||
# Same step as in test-unix: the complete logs of the suites that did
|
||||
# not end green, only when the job failed.
|
||||
- name: Failing suite logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: suite-logs-${{ matrix.os }}-${{ matrix.msystem }}-${{ strategy.job-index }}
|
||||
path: build/c/test-logs/failed/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
# Shard 1 (or the unsharded job) saves the compiler cache; every shard
|
||||
# builds the identical runner, so one cache carries the full object set.
|
||||
- name: Compiler cache (save)
|
||||
@@ -558,12 +720,16 @@ jobs:
|
||||
with:
|
||||
path: repo
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: shard-manifest-*
|
||||
path: manifests
|
||||
# A PR with no test leg selected uploads no manifest; the script
|
||||
# accepts that only when the selection really holds no leg.
|
||||
- name: Verify cross-shard union per leg
|
||||
run: repo/scripts/ci/verify-shard-union.sh manifests
|
||||
env:
|
||||
LANES: ${{ inputs.lanes }}
|
||||
run: repo/scripts/ci/verify-shard-union.sh manifests --lanes "$LANES"
|
||||
|
||||
# Windows product-surface regression guards. Distinct from test-windows above
|
||||
# (the sanitizer C suite): these drive a real product binary + external UI pack
|
||||
@@ -574,12 +740,13 @@ jobs:
|
||||
# CLI arguments being mangled by the narrow-argv main() (#423/#20, fixed by the
|
||||
# wide-argv entrypoint that reads GetCommandLineW).
|
||||
test-windows-guards:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'windows-guards') }}
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
@@ -590,7 +757,7 @@ jobs:
|
||||
make
|
||||
git
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
group: linux
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
- os: macos-14
|
||||
- os: macos-15
|
||||
group: macos
|
||||
cc: cc
|
||||
cxx: c++
|
||||
@@ -55,7 +55,8 @@ jobs:
|
||||
|
||||
- name: Install deps (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
|
||||
- name: Run bug-reproduction board
|
||||
if: ${{ github.event_name == 'push' || inputs.platforms == 'all' || inputs.platforms == matrix.group }}
|
||||
@@ -68,7 +69,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
include:
|
||||
- { os: ubuntu-latest, cc: gcc, cxx: g++ }
|
||||
- { os: ubuntu-24.04-arm, cc: gcc, cxx: g++ }
|
||||
- { os: macos-14, cc: cc, cxx: c++ }
|
||||
- { os: macos-15, cc: cc, cxx: c++ }
|
||||
- { os: macos-15-intel, cc: cc, cxx: c++ }
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
@@ -36,7 +36,8 @@ jobs:
|
||||
|
||||
- name: Install deps (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
|
||||
|
||||
- name: Install ccache (macOS)
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
@@ -81,7 +82,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
|
||||
@@ -1,43 +1,195 @@
|
||||
name: CodeQL SAST
|
||||
|
||||
# One analysis per language: c-cpp, javascript-typescript (graph-ui, pkg/npm),
|
||||
# python, actions. Scope: .github/codeql/codeql-config.yml (first-party source).
|
||||
#
|
||||
# FULL scan -- push to main, weekly, dry run, release: every language; c-cpp
|
||||
# traces the whole production build, vendored C included.
|
||||
# PR scan -- pr.yml's `codeql` job (light: true), needed by ci-ok. One leg per
|
||||
# language whose scope the PR changes (codeql-* lanes of
|
||||
# scripts/ci/select-lanes.sh; an undeterminable diff or a change to the
|
||||
# workflows, the build or the CodeQL machinery selects every language).
|
||||
# Each leg analyses its language's WHOLE first-party scope, so cross-file
|
||||
# data flow is kept; c-cpp compiles the vendored objects before tracing
|
||||
# starts, except vendored files the PR changes. Diff-informed analysis then
|
||||
# restricts the REPORTED alerts to the changed lines; what is extracted and
|
||||
# analysed is never narrowed by the diff.
|
||||
# There is no pull_request trigger and no job anywhere that polls for a CodeQL
|
||||
# run: the former PR gate waited up to 150 min per PR for a full analysis and,
|
||||
# with ~30 PRs pushed at once, starved PR CI for hours.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "23 5 * * 1"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
light:
|
||||
description: "PR-style c-cpp build (vendored objects compiled untraced) -- to time it"
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
light:
|
||||
description: "PR scan: vendored C compiled untraced except changed files; diff-informed results"
|
||||
type: boolean
|
||||
default: false
|
||||
matrix:
|
||||
description: 'The legs to run, {"include":[...]} (pr.yml: the selected codeql-* lanes); empty = every language'
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
# CodeQL needs security-events: write to upload results; scoped to this job so
|
||||
# the workflow's top-level token stays read-only (Scorecard TokenPermissions).
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# The same legs (and keys) everywhere, so a PR's analysis and main's
|
||||
# share one code-scanning configuration per language.
|
||||
matrix: ${{ fromJSON(inputs.matrix || '{"include":[{"language":"c-cpp","build-mode":"manual","lane":"codeql-cpp"},{"language":"javascript-typescript","build-mode":"none","lane":"codeql-js"},{"language":"python","build-mode":"none","lane":"codeql-py"},{"language":"actions","build-mode":"none","lane":"codeql-actions"}]}') }}
|
||||
# Superseded runs are always cancelled: a newer push to main cancels the
|
||||
# analysis of an older main commit (batched merges -> only the newest main
|
||||
# commit is scanned), a newer push to a PR cancels that PR's scan. Every
|
||||
# other trigger gets a group of its own and is never cancelled: a dry run
|
||||
# or a release tests ONE commit, and two dispatched together must not
|
||||
# cancel each other's gate. Per language, so legs never cancel each other.
|
||||
# In a called workflow `github` is the CALLER's context: event_name is
|
||||
# pull_request for pr.yml, github.workflow names the dry run or release.
|
||||
concurrency:
|
||||
group: codeql-${{ matrix.language }}-${{ github.event_name == 'push' && github.ref || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
# CodeQL needs security-events: write to upload results (and read for the
|
||||
# alert check); scoped to this job so the workflow's top-level token stays
|
||||
# read-only (Scorecard TokenPermissions).
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
env:
|
||||
# One code-scanning configuration for every trigger. The action derives
|
||||
# the key from the RUN's workflow file, which for a called workflow is
|
||||
# the caller: pr.yml / dry-run.yml / release.yml would each open a
|
||||
# configuration of their own, and on main an alert fixed by a later push
|
||||
# would stay open in a stale one. The value is the key the push-to-main
|
||||
# analysis has always used (the action documents this variable as
|
||||
# settable for non-standard setups).
|
||||
CODEQL_ACTION_ANALYSIS_KEY: .github/workflows/codeql.yml:analyze
|
||||
# Diff-informed analysis: codeql-action v4.38.1 (CLI 2.27.0) reports a
|
||||
# pull request's alerts only on its changed lines when the
|
||||
# diff_informed_queries feature is on (default on; this variable forces
|
||||
# it, src/feature-flags.ts). It applies only when the event payload is a
|
||||
# pull request -- pr.yml's call -- and is inert on main, dry run, release.
|
||||
CODEQL_ACTION_DIFF_INFORMED_QUERIES: ${{ inputs.light && 'true' || '' }}
|
||||
# No compiler cache: a cache hit skips the compiler, and CodeQL extracts
|
||||
# only what a traced compiler invocation actually compiles.
|
||||
CBM_NO_CCACHE: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# The PR scan reads the merge commit's first parent (the base) to
|
||||
# find the vendored files the PR changes.
|
||||
fetch-depth: ${{ inputs.light && 2 || 1 }}
|
||||
|
||||
- name: Install build dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
if: ${{ matrix.language == 'c-cpp' }}
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
|
||||
# PR c-cpp: compile the third-party objects (tree-sitter grammars,
|
||||
# sqlite, mimalloc, lz4, zstd, the tree-sitter runtime: ~1.3 GB of
|
||||
# vendored C against ~45 MB first-party) BEFORE tracing starts, so the
|
||||
# traced build below compiles first-party code only ...
|
||||
- name: Build third-party objects (PR scan, untraced)
|
||||
if: ${{ matrix.language == 'c-cpp' && inputs.light }}
|
||||
run: scripts/build.sh --vendored-only
|
||||
|
||||
# ... except the vendored files this PR changes: those are rebuilt
|
||||
# under tracing (an undeterminable diff leaves the list empty; the scan
|
||||
# still covers all first-party C).
|
||||
- name: Vendored files this PR changes (traced too)
|
||||
if: ${{ matrix.language == 'c-cpp' && inputs.light }}
|
||||
run: scripts/ci/codeql-retrace-vendored.sh "$RUNNER_TEMP/codeql-retrace.txt"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/init@33d7b85da1a04171e211befe2c739303b9a4c066 # v4
|
||||
with:
|
||||
languages: c-cpp
|
||||
build-mode: manual
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Build for CodeQL analysis
|
||||
- name: Build for CodeQL analysis (full)
|
||||
if: ${{ matrix.language == 'c-cpp' && !inputs.light }}
|
||||
run: scripts/build.sh
|
||||
|
||||
- name: Build for CodeQL analysis (PR scan, first-party + changed vendored)
|
||||
if: ${{ matrix.language == 'c-cpp' && inputs.light }}
|
||||
run: scripts/build.sh --reuse-vendored --retrace-from "$RUNNER_TEMP/codeql-retrace.txt"
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4
|
||||
with:
|
||||
category: "/language:c-cpp"
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
# Alert policy: the default branch carries ZERO open code-scanning
|
||||
# alerts. Any open alert on the analysed ref fails this leg: on main the
|
||||
# commit turns red and GitHub notifies whoever triggered the run; the
|
||||
# dry run and the release cannot pass; a PR (refs/pull/<n>/merge: what
|
||||
# its changed lines introduce) fails ci-ok. With a zero baseline "any
|
||||
# open alert" is exactly "a new alert"; a dismissal (with a reason, in
|
||||
# the Security tab) holds on every ref. Every leg checks after its own
|
||||
# upload, so each language's alerts are seen by at least its own leg.
|
||||
# Moved from _security.yml's former PR-CI codeql-gate job.
|
||||
- name: Check for open code scanning alerts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ALERT_REF: ${{ github.ref }}
|
||||
run: |
|
||||
# FAIL CLOSED. The previous form was `gh api ... 2>/dev/null || echo "0"`,
|
||||
# which looks defensive and is the opposite. gh writes the API error
|
||||
# BODY to stdout, so a 403 made ALERTS the string
|
||||
# {"message":"Resource not accessible by integration",...,"status":"403"}0
|
||||
# every `[` comparison then failed as non-integer, the `if` took its
|
||||
# false branch, and the step printed "CodeQL gate passed (0 alerts)"
|
||||
# and exited 0. That is exactly how a security gate should NEVER fail:
|
||||
# silently, in the direction of "everything is fine". An unreadable
|
||||
# alert list is now an error, not a zero.
|
||||
# NOTE: this runs in a command substitution, so it must `return`, not
|
||||
# `exit` -- an `exit` here would only end the subshell and let the
|
||||
# caller continue with an empty count, recreating the very bug this
|
||||
# replaces. The callers below check the status explicitly.
|
||||
count_open_alerts() {
|
||||
local body status
|
||||
body=$(gh api "repos/${{ github.repository }}/code-scanning/alerts?state=open&ref=$ALERT_REF" \
|
||||
--jq 'length' 2>/dev/null)
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "BLOCKED: cannot read code scanning alerts (gh exit $status)." >&2
|
||||
echo " Refusing to treat an unreadable alert list as zero." >&2
|
||||
echo " Most likely cause: the job lacks security-events:read." >&2
|
||||
return 1
|
||||
fi
|
||||
case "$body" in
|
||||
''|*[!0-9]*)
|
||||
echo "BLOCKED: alert count was not a number: ${body:-<empty>}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$body"
|
||||
}
|
||||
echo "Waiting 60s for alert API to settle..."
|
||||
sleep 60
|
||||
ALERTS=$(count_open_alerts) || exit 1
|
||||
sleep 15
|
||||
ALERTS2=$(count_open_alerts) || exit 1
|
||||
[ "$ALERTS" -lt "$ALERTS2" ] && ALERTS=$ALERTS2
|
||||
if [ "$ALERTS" -gt 0 ]; then
|
||||
echo "::error title=Open code scanning alerts::$ALERTS open alert(s) on $ALERT_REF -- fix, or dismiss with a reason in the Security tab"
|
||||
echo "BLOCKED: $ALERTS open alert(s)"
|
||||
gh api "repos/${{ github.repository }}/code-scanning/alerts?state=open&ref=$ALERT_REF" \
|
||||
--jq '.[] | " #\(.number) [\(.rule.security_severity_level // .rule.severity)] \(.rule.id) — \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "=== CodeQL gate passed (0 alerts) ==="
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
# Each step can be skipped for faster iteration.
|
||||
#
|
||||
# Pipeline: lint → test → build + candidate scan/select → smoke/soak
|
||||
# Security: security-static + codeql-gate (independent island)
|
||||
# Security: security-static + license-gate (independent island)
|
||||
# CodeQL: codeql.yml full scan of this commit + alert check (independent island)
|
||||
#
|
||||
# Security does NOT block lint/test/build/smoke/soak. All jobs must pass
|
||||
# Security and CodeQL do NOT block lint/test/build/smoke/soak. All jobs must pass
|
||||
# for the overall workflow to be green.
|
||||
#
|
||||
# Candidate VirusTotal scanning and tuple-local selection are part of the
|
||||
@@ -62,15 +63,24 @@ jobs:
|
||||
# ── Security (independent island — does not block main pipeline) ──
|
||||
security:
|
||||
uses: ./.github/workflows/_security.yml
|
||||
# The called job requests security-events:read to count code-scanning
|
||||
# alerts; a reusable workflow cannot request more than its caller grants,
|
||||
# so withholding this here fails the whole run at startup.
|
||||
# No job in _security.yml reads code scanning or the Actions API any more
|
||||
# (the CodeQL gate moved to codeql.yml), so the token stays read-only.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
secrets: inherit
|
||||
|
||||
# ── CodeQL: full scan of this exact commit + the open-alert check ──
|
||||
# Every language, vendored C included (PRs run the narrower PR scan).
|
||||
# codeql.yml uploads results and reads the alerts: a called workflow
|
||||
# cannot request more than its caller grants, so withholding either
|
||||
# permission fails the whole run at startup.
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
# ── Lint (cppcheck + clang-format) ────────────────────────────
|
||||
lint:
|
||||
if: ${{ inputs.skip_lint != true }}
|
||||
@@ -132,12 +142,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev clang llvm libclang-rt-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev clang llvm libclang-rt-dev
|
||||
- name: Fuzz smoke (extract, cypher, config)
|
||||
run: scripts/fuzz.sh all --runs 20000 --seed 1
|
||||
- name: Upload reproducers
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: fuzz-artifacts
|
||||
path: build/fuzz/artifacts-*/
|
||||
|
||||
@@ -26,7 +26,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
- name: test-repro (single platform, ASan; CBM_REPRO_ONLY filters suites)
|
||||
env:
|
||||
# Optionally narrow to specific suites for a fast targeted check, e.g.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# FreeBSD build, INFORMATIONAL ONLY. Builds the headless binary (the C core,
|
||||
# no embedded UI) inside a FreeBSD VM with the canonical scripts/build.sh,
|
||||
# then checks that the binary starts (--version). The UI build is not run here:
|
||||
# its npm dependency tree pulls in onnxruntime-node, which publishes no FreeBSD
|
||||
# build, so `npm ci` stops with EBADPLATFORM. The UI bundle itself is
|
||||
# platform-independent JavaScript and is built on every other leg.
|
||||
#
|
||||
# NON-GATING: workflow_dispatch only. This workflow is not part of the ci-ok
|
||||
# aggregate and is not called by release.yml or dry-run.yml, so a FreeBSD red
|
||||
# never blocks a pull request or a release. Nothing from it is uploaded or
|
||||
# published, and it runs with read-only permissions.
|
||||
name: FreeBSD build (informational)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-freebsd:
|
||||
name: build (freebsd-${{ matrix.release }}, ${{ matrix.arch }}, clang)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
release: ['15.1', '14.4']
|
||||
arch: ['x86_64']
|
||||
# arch: ['x86_64', 'aarch64']
|
||||
# aarch64 is disabled for now: it has not been proven with this
|
||||
# workflow. Its earlier blocker was the frontend build (lightningcss
|
||||
# publishes no freebsd-arm64 binary), which this headless job no
|
||||
# longer runs. Re-enable after a green aarch64 proof run.
|
||||
include:
|
||||
- arch: x86_64
|
||||
sync: rsync
|
||||
# - arch: aarch64
|
||||
# sync: scp
|
||||
# Must be an x86_64 host (ubuntu-latest). QEMU guest emulation on the arm
|
||||
# runners (ubuntu-*-arm) degrades so badly that runs hit 3+ hour timeouts.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and run inside a FreeBSD ${{ matrix.release }} (${{ matrix.arch }}) VM
|
||||
# https://github.com/vmactions/freebsd-vm
|
||||
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9
|
||||
with:
|
||||
release: ${{ matrix.release }}
|
||||
arch: ${{ matrix.arch }}
|
||||
# Sync options: rsync, sshfs, nfs, scp
|
||||
sync: ${{ matrix.sync }}
|
||||
# Nothing from the VM is used after the run, so skip the copy back.
|
||||
copyback: false
|
||||
mem: 6144
|
||||
cpu: 4
|
||||
prepare: |
|
||||
pkg update -f
|
||||
pkg install -y gmake sqlite3 pkgconf bash
|
||||
which python3 >/dev/null 2>&1 || ln -sf $(which python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 2>/dev/null | head -n 1) /usr/local/bin/python3
|
||||
run: |
|
||||
scripts/build.sh CC=clang CXX=clang++
|
||||
build/c/codebase-memory-mcp --version
|
||||
@@ -45,12 +45,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev clang llvm libclang-rt-dev
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev clang llvm libclang-rt-dev
|
||||
- name: Fuzz exploration (30 min per target)
|
||||
run: scripts/fuzz.sh all --max-total-time 1800 --seed 0
|
||||
- name: Upload reproducers
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: fuzz-artifacts-nightly
|
||||
path: build/fuzz/artifacts-*/
|
||||
|
||||
@@ -51,17 +51,25 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
MESSAGE_FILE=.github/pr-acknowledgement.md
|
||||
BODY_FILE="$RUNNER_TEMP/pr-acknowledgement-body.md"
|
||||
|
||||
if [ ! -f "$MESSAGE_FILE" ]; then
|
||||
echo "No $MESSAGE_FILE in the base tree — acknowledgements are off."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# An empty or whitespace-only file is the documented off switch.
|
||||
if [ -z "$(tr -d '[:space:]' < "$MESSAGE_FILE")" ]; then
|
||||
echo "$MESSAGE_FILE is blank — acknowledgements are off."
|
||||
# HTML comments in the template are notes for maintainers. Posted
|
||||
# verbatim they become text a reader cannot see but a tool can, and
|
||||
# our own PR screening holds every comment that carries hidden text.
|
||||
# Strip them (an unterminated one runs to the end, as GitHub renders
|
||||
# it) so the posted body is exactly what the contributor sees.
|
||||
perl -0777 -pe 's/<!--.*?(?:-->|\z)[ \t]*\n?//gs' "$MESSAGE_FILE" > "$BODY_FILE"
|
||||
|
||||
# A body with no visible text is the documented off switch.
|
||||
if [ -z "$(tr -d '[:space:]' < "$BODY_FILE")" ]; then
|
||||
echo "$MESSAGE_FILE has no visible text — acknowledgements are off."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "$MESSAGE_FILE"
|
||||
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "$BODY_FILE"
|
||||
echo "Acknowledged PR #${PR_NUMBER}."
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# PR validation: security gates + lint + full test suite. Builds, smoke and
|
||||
# soak stay maintainer-driven via the dry-run workflow_dispatch. Branch
|
||||
# PR validation: security gates + lint + the test legs this PR's changes need.
|
||||
# `changes` runs scripts/ci/select-lanes.sh on the PR's file list; every lane
|
||||
# job below runs only when selected (unknown paths, workflows, the harness,
|
||||
# Makefile.cbm and vendored code select everything). Builds, smoke and soak at
|
||||
# full breadth stay maintainer-driven via the dry-run workflow_dispatch. Branch
|
||||
# protection requires `dco` + `ci-ok` — a single stable summary context that
|
||||
# fails unless every PR stage succeeded.
|
||||
# fails unless every PR stage succeeded and every selected lane ran green.
|
||||
name: PR
|
||||
|
||||
on:
|
||||
@@ -21,42 +24,111 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
security:
|
||||
needs: [changes]
|
||||
uses: ./.github/workflows/_security.yml
|
||||
# The called job requests security-events:read to count code-scanning
|
||||
# alerts; a reusable workflow cannot request more than its caller grants,
|
||||
# so withholding this here fails the whole run at startup.
|
||||
# No job in _security.yml reads code scanning or the Actions API any more
|
||||
# (the CodeQL gate moved to codeql.yml), so the token stays read-only.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
secrets: inherit
|
||||
with:
|
||||
lanes: ${{ needs.changes.outputs.lanes }}
|
||||
|
||||
lint:
|
||||
needs: [changes]
|
||||
if: ${{ contains(fromJSON(needs.changes.outputs.lanes), 'lint') || contains(fromJSON(needs.changes.outputs.lanes), 'lint-mem') }}
|
||||
uses: ./.github/workflows/_lint.yml
|
||||
with:
|
||||
lanes: ${{ needs.changes.outputs.lanes }}
|
||||
|
||||
# ── Smart CI, PR CI only: run only the tests this change can reach.
|
||||
# Active only while the repository variable TEST_IMPACT_MODE is `gate`;
|
||||
# unset (shadow) every test runs and test-impact-shadow only reports.
|
||||
# test-impact-select.sh --gate narrows only on a well-formed answer: a
|
||||
# failure of this job, a run-all answer or a lane that selects nothing
|
||||
# leaves `test` without a selection, and every test runs. ci-ok does not
|
||||
# need this job: when it breaks, PRs cost more minutes, never coverage.
|
||||
# Dry runs and releases never pass a selection.
|
||||
select-tests:
|
||||
needs: [changes]
|
||||
if: ${{ vars.TEST_IMPACT_MODE == 'gate' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # the merge base's team-artifact bundle (test-impact-artifact.yml)
|
||||
outputs:
|
||||
selection: ${{ steps.select.outputs.selection }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Full history: the engine certifies the history it diffs and refuses
|
||||
# a shallow clone, so a depth-2 checkout always answered `all`.
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build this checkout's engine
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
scripts/build.sh CC=gcc CXX=g++
|
||||
- name: Select the tests
|
||||
id: select
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-mcp --out "$RUNNER_TEMP/test-impact" --gate "$RUNNER_TEMP/test-selection"
|
||||
- name: Upload the selection
|
||||
if: ${{ steps.select.outputs.selection == 'narrowed' }}
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: test-selection
|
||||
path: ${{ runner.temp }}/test-selection
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
# The selection a veto discarded (written only when one did): evidence
|
||||
# for the vetoes, never read by a test leg.
|
||||
- name: Upload the vetoed selection
|
||||
if: ${{ !cancelled() && steps.select.outcome != 'skipped' }}
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: test-selection-vetoed
|
||||
path: ${{ runner.temp }}/test-impact/selection-vetoed.json
|
||||
if-no-files-found: ignore
|
||||
overwrite: true
|
||||
|
||||
test:
|
||||
needs: [lint]
|
||||
needs: [changes, lint, select-tests]
|
||||
if: ${{ !cancelled() && needs.lint.result == 'success' }}
|
||||
uses: ./.github/workflows/_test.yml
|
||||
with:
|
||||
lanes: ${{ needs.changes.outputs.lanes }}
|
||||
# Smart CI: only a successful select-tests run that narrowed passes a
|
||||
# selection; skipped (shadow), failed, run-all and nothing-selected
|
||||
# answers all run every test.
|
||||
test_selection: ${{ needs.select-tests.result == 'success' && needs.select-tests.outputs.selection == 'narrowed' && 'test-selection' || '' }}
|
||||
# Perf assertions are timing-sensitive on shared runners; they stay in
|
||||
# dry runs and releases where a flaky red does not block a merge.
|
||||
skip_perf: true
|
||||
# Iteration speed: split each C-suite leg across parallel shard jobs
|
||||
# (ubuntu x3, windows x2; coverage re-proven every run by the
|
||||
# shard-completeness job). Dry runs and releases shard identically.
|
||||
# Iteration speed: split C-suite legs across parallel shard jobs
|
||||
# (coverage re-proven every run by the shard-completeness job). The PR
|
||||
# profile shards only arm64 (x2); x86 and Windows run whole, since each
|
||||
# shard re-pays its setup. Dry runs and releases keep x3 / x2.
|
||||
shard_suites: true
|
||||
shard_profile: pr
|
||||
|
||||
# ── Which files changed? Gate the (heavier) build+smoke on product code so
|
||||
# docs / CI / test-only PRs stay fast. ──
|
||||
# ── Which lanes does this PR need? The changed files go through
|
||||
# scripts/ci/select-lanes.sh (tier, lanes, full, reasons); docs / CI /
|
||||
# test-only PRs stay fast, anything it cannot place runs everything. ──
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
product: ${{ steps.f.outputs.product }}
|
||||
tier: ${{ steps.select.outputs.tier }}
|
||||
full: ${{ steps.select.outputs.full }}
|
||||
lanes: ${{ steps.select.outputs.lanes }}
|
||||
smoke: ${{ steps.select.outputs.smoke }}
|
||||
codeql: ${{ steps.select.outputs.codeql }}
|
||||
steps:
|
||||
- name: Detect product-code changes
|
||||
id: f
|
||||
- name: List the changed files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
@@ -64,7 +136,9 @@ jobs:
|
||||
run: |
|
||||
# The full .diff endpoint rejects large-but-valid PRs at 20k lines.
|
||||
# The paginated files endpoint remains filename-only for this gate.
|
||||
if FILES=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename'); then
|
||||
# A rename lists its old path too, as the fallback's --no-renames
|
||||
# diff does: moving a file away is a change to where it was.
|
||||
if FILES=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[] | .filename, (.previous_filename // empty)'); then
|
||||
echo "file list: pulls/files API"
|
||||
else
|
||||
# The files endpoint renders the diff server-side too, and answers
|
||||
@@ -81,45 +155,71 @@ jobs:
|
||||
"https://github.com/$REPO" "refs/pull/$PR/merge"
|
||||
FILES=$(git --git-dir="$CHANGES_GIT" diff --name-only --no-renames FETCH_HEAD^1 FETCH_HEAD)
|
||||
fi
|
||||
printf '%s\n' "$FILES"
|
||||
# A here-string, not a pipe: under this shell's pipefail, grep -q
|
||||
# exits on its first match, printf dies of SIGPIPE once the list
|
||||
# outgrows the pipe buffer, and the MATCH is then reported as a
|
||||
# failure -- product=false, smoke skipped, on the largest PRs.
|
||||
if grep -qE '^(src/|internal/|install\.(sh|ps1)|scripts/build\.sh|scripts/smoke-test\.sh|scripts/smoke-local\.sh|scripts/smoke-fixture-server\.py|scripts/gen-third-party-notices\.sh|scripts/env\.sh|test-infrastructure/vm/(vm-smoke\.sh|windows-user-path-guard\.ps1)|Makefile\.cbm)' <<<"$FILES"; then
|
||||
echo "product=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "product=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
printf '%s\n' "$FILES" > "$RUNNER_TEMP/changed-files.txt"
|
||||
cat "$RUNNER_TEMP/changed-files.txt"
|
||||
|
||||
# The selector is PR code: it runs in its own step, after the
|
||||
# token-bearing one, from a checkout that keeps no credentials.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# The pulls/files API lists at most 3000 files: the PR's own count lets
|
||||
# the selector notice a cut list and select everything instead.
|
||||
- name: Select lanes
|
||||
id: select
|
||||
env:
|
||||
CHANGED_FILES: ${{ github.event.pull_request.changed_files }}
|
||||
run: |
|
||||
scripts/ci/select-lanes.sh --format github-output --expect-files "$CHANGED_FILES" "$RUNNER_TEMP/changed-files.txt" > "$RUNNER_TEMP/selection.txt"
|
||||
cat "$RUNNER_TEMP/selection.txt"
|
||||
cat "$RUNNER_TEMP/selection.txt" >> "$GITHUB_OUTPUT"
|
||||
# pr-smoke's legs, each tagged with its lane (kept in the job name,
|
||||
# where ci-ok reads it back); the job gets the selected ones.
|
||||
SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-15","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]'
|
||||
LANES=$(sed -n 's/^lanes=//p' "$RUNNER_TEMP/selection.txt")
|
||||
echo "smoke={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$SMOKE")}" >> "$GITHUB_OUTPUT"
|
||||
# The CodeQL legs, tagged the same way (codeql.yml's full matrix has
|
||||
# the same entries, so PR and main share a configuration).
|
||||
CODEQL='[{"language":"c-cpp","build-mode":"manual","lane":"codeql-cpp"},{"language":"javascript-typescript","build-mode":"none","lane":"codeql-js"},{"language":"python","build-mode":"none","lane":"codeql-py"},{"language":"actions","build-mode":"none","lane":"codeql-actions"}]'
|
||||
echo "codeql={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$CODEQL")}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Light smoke: build the PRODUCTION binary and run the core smoke on the
|
||||
# three RELIABLE NATIVE platforms. Catches built-binary regressions at PR
|
||||
# time (e.g. the Windows CreateProcess argv-quoting class that previously
|
||||
# only surfaced in the release dry run). The full broad/emulated smoke stays
|
||||
# in the dry run. Only product-code PRs pay this; every leg gates.
|
||||
# in the dry run. Only the selected legs run (ubuntu from the extraction
|
||||
# tier up, all three for platform/packaging changes); every leg gates.
|
||||
# The maintained local wrappers add a race-free release fixture so the
|
||||
# download/checksum/install/update phases run here as well. ──
|
||||
pr-smoke:
|
||||
needs: [changes]
|
||||
if: ${{ !cancelled() && needs.changes.outputs.product == 'true' }}
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && (contains(fromJSON(needs.changes.outputs.lanes), 'smoke-ubuntu') || contains(fromJSON(needs.changes.outputs.lanes), 'smoke-mac') || contains(fromJSON(needs.changes.outputs.lanes), 'smoke-win')) }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-14, windows-latest]
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.smoke) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper; the
|
||||
# macOS runner images have none on PATH.
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: pkg/go/go.mod
|
||||
cache: false
|
||||
|
||||
- name: Install deps (Ubuntu)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
|
||||
|
||||
- name: Install ccache (macOS)
|
||||
if: matrix.os == 'macos-14'
|
||||
if: matrix.os == 'macos-15'
|
||||
run: command -v ccache >/dev/null 2>&1 || brew install ccache
|
||||
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
if: matrix.os == 'windows-latest'
|
||||
with:
|
||||
msystem: CLANG64
|
||||
@@ -153,9 +253,10 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1000M
|
||||
SMOKE_REQUIRE_WRAPPERS: "1"
|
||||
|
||||
- name: Build prod + smoke (macOS)
|
||||
if: matrix.os == 'macos-14'
|
||||
if: matrix.os == 'macos-15'
|
||||
run: |
|
||||
scripts/build.sh CC=cc CXX=c++
|
||||
codesign --sign - --force build/c/codebase-memory-mcp
|
||||
@@ -163,6 +264,7 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1000M
|
||||
SMOKE_REQUIRE_WRAPPERS: "1"
|
||||
|
||||
- name: Build prod + smoke (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -173,31 +275,146 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1000M
|
||||
SMOKE_REQUIRE_WRAPPERS: "1"
|
||||
|
||||
# ── Waste sanitizer, REPORT-ONLY: memory + CPU waste on this repository and
|
||||
# the per-function scaling lane (Linux). Deliberately NOT in ci-ok's needs:
|
||||
# it uploads evidence and prints the ratchet, it never blocks a merge. Only
|
||||
# product-code PRs pay for it. ──
|
||||
# PRs whose selection holds the memwaste lane (memory core, memwaste
|
||||
# tooling, full runs) pay for it. ──
|
||||
memwaste:
|
||||
needs: [changes]
|
||||
if: ${{ !cancelled() && needs.changes.outputs.product == 'true' }}
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'memwaste') }}
|
||||
uses: ./.github/workflows/_memwaste.yml
|
||||
with:
|
||||
mode: pr
|
||||
|
||||
# ── Test-impact SHADOW, REPORT-ONLY: predicts from the code graph which C
|
||||
# suites this change can affect (the latest released binary indexes the
|
||||
# PR checkout; detect_changes at depth 15 against the base, mapped to
|
||||
# suites, with the selector's run-all triggers) and uploads the
|
||||
# prediction, so graph-based selection can be judged against what really
|
||||
# failed before anything gates on it. continue-on-error and NOT in
|
||||
# ci-ok's needs: it can never block or pass a merge. ──
|
||||
test-impact-shadow:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # the merge base's team-artifact bundle (test-impact-artifact.yml)
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Full history: the engine (test-impact select) certifies the history
|
||||
# it diffs and refuses a shallow clone ("Graft or shallow state
|
||||
# prevents history certification"), which made every shadow answer
|
||||
# run_all (GIT_UNAVAILABLE). About 400 MiB, this job only.
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Fetch the latest released binary
|
||||
id: release
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: scripts/ci/test-impact-shadow.sh --fetch-release "$RUNNER_TEMP/cbm-bin"
|
||||
- name: Build the binary from this checkout (download failed)
|
||||
if: ${{ steps.release.outcome == 'failure' }}
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
scripts/build.sh CC=gcc CXX=g++
|
||||
mkdir -p "$RUNNER_TEMP/cbm-bin"
|
||||
cp build/c/codebase-memory-mcp "$RUNNER_TEMP/cbm-bin/"
|
||||
- name: Predict the impacted suites
|
||||
run: scripts/ci/test-impact-shadow.sh --binary "$RUNNER_TEMP/cbm-bin/codebase-memory-mcp" --out "$RUNNER_TEMP/test-impact"
|
||||
# The engine (test-impact select) is this checkout's own code: the
|
||||
# released binary predates it, so the checkout is built either way.
|
||||
- name: Build this checkout's engine
|
||||
if: ${{ steps.release.outcome != 'failure' }}
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
scripts/build.sh CC=gcc CXX=g++
|
||||
- name: Select with the team artifact (engine shadow)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-mcp --out "$RUNNER_TEMP/test-impact"
|
||||
- name: Upload the prediction
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: test-impact-prediction
|
||||
path: |
|
||||
${{ runner.temp }}/test-impact/prediction.json
|
||||
${{ runner.temp }}/test-impact/selection.json
|
||||
${{ runner.temp }}/test-impact/changed-files.txt
|
||||
${{ runner.temp }}/test-impact/selection-engine.json
|
||||
${{ runner.temp }}/test-impact/summary-engine.md
|
||||
if-no-files-found: warn
|
||||
|
||||
# ── Docs-only and non-product PRs (tiers T0a, T0b) run the static contract
|
||||
# steps of the canonical test leg on their own: they police docs
|
||||
# surfaces (language counts, version metadata), test-infrastructure/ and
|
||||
# lint config too. Every test leg runs the same steps. ──
|
||||
contracts:
|
||||
needs: [changes]
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'contracts') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Contract steps only
|
||||
run: scripts/test.sh --contracts-only
|
||||
|
||||
# ── CodeQL PR scan (lanes codeql-cpp / -js / -py / -actions): one leg per
|
||||
# language whose first-party scope this PR changes; each analyses that
|
||||
# language's whole first-party scope, c-cpp with the vendored objects
|
||||
# compiled before tracing (except vendored files the PR changes), and
|
||||
# reports alerts on the changed lines (diff-informed). A job of this run,
|
||||
# needed by ci-ok -- nothing waits for a separate CodeQL run. Push to
|
||||
# main, dry run and release run the full scan (codeql.yml). ──
|
||||
codeql:
|
||||
needs: [changes]
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && (contains(fromJSON(needs.changes.outputs.lanes), 'codeql-cpp') || contains(fromJSON(needs.changes.outputs.lanes), 'codeql-js') || contains(fromJSON(needs.changes.outputs.lanes), 'codeql-py') || contains(fromJSON(needs.changes.outputs.lanes), 'codeql-actions')) }}
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
# codeql.yml uploads results and reads alerts: a called workflow cannot
|
||||
# request more than its caller grants (startup failure otherwise).
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
with:
|
||||
light: true
|
||||
matrix: ${{ needs.changes.outputs.codeql }}
|
||||
|
||||
ci-ok:
|
||||
# The one required context (besides dco) — fails unless every PR stage
|
||||
# succeeded, so matrix renames can never silently deadlock merges. `skipped`
|
||||
# is OK: pr-smoke skips on docs/CI/test-only PRs (changes.product == false).
|
||||
needs: [security, lint, test, changes, pr-smoke]
|
||||
# succeeded, so matrix renames can never silently deadlock merges, and
|
||||
# every lane the selector chose ran green: `skipped` is OK only for a lane
|
||||
# nobody selected (a selected lane that skipped is a silent gate loss).
|
||||
# memwaste and the test-impact shadow are report-only and not needed here.
|
||||
needs: [changes, security, lint, test, pr-smoke, contracts, codeql]
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
# actions:read lists this run's jobs, the lane-level truth behind `needs`.
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
# Needs a checkout (unlike before): the gate logic lives in the canonical
|
||||
# scripts/ci entry, not inline YAML (venue-parity contract).
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: All PR stages must have succeeded
|
||||
- name: List this run's jobs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?filter=latest&per_page=100" \
|
||||
--jq '.jobs[] | {name, status, conclusion}' > "$RUNNER_TEMP/jobs.jsonl"
|
||||
- name: All PR stages and every selected lane must have succeeded
|
||||
env:
|
||||
RESULTS: ${{ toJSON(needs) }}
|
||||
LANES: ${{ needs.changes.outputs.lanes }}
|
||||
JOBS: ${{ runner.temp }}/jobs.jsonl
|
||||
run: scripts/ci/require-all-green.sh
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Release pipeline: lint → test → build → smoke/soak → draft → verify → publish
|
||||
#
|
||||
# Security (security-static + codeql-gate) starts immediately and runs in
|
||||
# parallel with lint/test/build/smoke/soak. It only blocks the final verify
|
||||
# Security (security-static + license-gate) and CodeQL (codeql.yml: full scan
|
||||
# of this commit + the open-alert check) start immediately and run in
|
||||
# parallel with lint/test/build/smoke/soak. They only block the final verify
|
||||
# step — everything else proceeds independently.
|
||||
name: Release
|
||||
|
||||
@@ -44,15 +45,24 @@ jobs:
|
||||
# ── Security (starts immediately, blocks only verify) ───────────
|
||||
security:
|
||||
uses: ./.github/workflows/_security.yml
|
||||
# The called job requests security-events:read to count code-scanning
|
||||
# alerts; a reusable workflow cannot request more than its caller grants,
|
||||
# so withholding this here fails the whole run at startup.
|
||||
# No job in _security.yml reads code scanning or the Actions API any more
|
||||
# (the CodeQL gate moved to codeql.yml), so the token stays read-only.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
secrets: inherit
|
||||
|
||||
# ── CodeQL (starts immediately, blocks only verify) ──────────────
|
||||
# Full scan of this exact commit (every language, vendored C included) +
|
||||
# the open-alert check. codeql.yml uploads results and reads the alerts:
|
||||
# a called workflow cannot request more than its caller grants, so
|
||||
# withholding either permission fails the whole run at startup.
|
||||
codeql:
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
# ── 0. Preflight: refuse malformed dispatch inputs ──────────────
|
||||
# The tag is inputs.version VERBATIM. A bare "0.10.7" publishes a release the
|
||||
# installers can never resolve (they fetch releases/download/v<version>/...),
|
||||
@@ -175,7 +185,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download only selected release containers
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: binaries-*
|
||||
merge-multiple: true
|
||||
@@ -184,7 +194,7 @@ jobs:
|
||||
# download above. Preserve only the three reviewable manifests that bind
|
||||
# candidate bytes, VT results, and the tuple-local decision.
|
||||
- name: Download candidate selection evidence separately
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: release-selection-evidence
|
||||
path: selection-evidence
|
||||
@@ -226,7 +236,7 @@ jobs:
|
||||
# same-run workflow artifact, not a draft-release download, so that
|
||||
# job keeps contents: read and no gh dependency.
|
||||
- name: Preserve checksums for the registry job
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-checksums
|
||||
path: checksums.txt
|
||||
@@ -236,14 +246,26 @@ jobs:
|
||||
- name: Generate SBOM
|
||||
run: python3 scripts/ci/generate-sbom.py "${{ inputs.version }}"
|
||||
|
||||
# actions/attest-sbom is deprecated. What it does now is emit
|
||||
# "::warning::actions/attest-sbom has been deprecated" and forward to
|
||||
# actions/attest with NODE_OPTIONS set, passing sbom-path straight
|
||||
# through. Calling attest directly produces the same attestation without
|
||||
# the wrapper, and sbom-path is a first-class input there (verified
|
||||
# against the action definition at this exact SHA, which is tag v4.1.0).
|
||||
#
|
||||
# This step deliberately carries no continue-on-error: a release must
|
||||
# fail if its SBOM cannot be attested. An unattested SBOM is worse than
|
||||
# an absent one, because the artifact still ships looking complete.
|
||||
- name: Attest SBOM
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
env:
|
||||
NODE_OPTIONS: '--max-http-header-size=32768'
|
||||
with:
|
||||
subject-path: '*.tar.gz'
|
||||
sbom-path: 'sbom.json'
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3
|
||||
|
||||
- name: Sign artifacts
|
||||
run: |
|
||||
@@ -271,9 +293,15 @@ jobs:
|
||||
# move the release tag. (A floating HEAD also broke the 0.8.0 release:
|
||||
# HEAD had become a commit touching .github/workflows/, and the App
|
||||
# token may not create refs pointing at workflow-modifying commits.)
|
||||
# The Go wrapper is a nested module: `go install
|
||||
# .../pkg/go/cmd/codebase-memory-mcp@vX.Y.Z` and @latest resolve its
|
||||
# versions from pkg/go/v* tags only (without one, Go fetches the whole
|
||||
# repository, which is over its 500 MB module limit).
|
||||
run: |
|
||||
git tag -f "$VERSION" "$GITHUB_SHA"
|
||||
git push origin "$VERSION" --force
|
||||
git tag -f "pkg/go/$VERSION" "$GITHUB_SHA"
|
||||
git push origin "pkg/go/$VERSION" --force
|
||||
|
||||
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
@@ -294,12 +322,12 @@ jobs:
|
||||
body: ${{ inputs.release_notes || '' }}
|
||||
generate_release_notes: ${{ inputs.release_notes == '' }}
|
||||
|
||||
# ── 7. Verify + Publish (requires security gate) ───────────────
|
||||
# ── 7. Verify + Publish (requires the security and CodeQL gates) ─
|
||||
verify:
|
||||
needs: [release-draft, security]
|
||||
needs: [release-draft, security, codeql]
|
||||
# Explicit condition: a deliberately skipped test ancestor must not skip
|
||||
# the publish chain; the draft itself proves smoke and soak both succeeded.
|
||||
if: ${{ !cancelled() && !failure() && needs.security.result == 'success' && needs.release-draft.result == 'success' }}
|
||||
if: ${{ !cancelled() && !failure() && needs.security.result == 'success' && needs.codeql.result == 'success' && needs.release-draft.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -401,7 +429,7 @@ jobs:
|
||||
|
||||
- name: Preserve extracted-object VirusTotal evidence
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: virustotal-evidence-release-${{ github.run_id }}
|
||||
path: |
|
||||
@@ -470,7 +498,7 @@ jobs:
|
||||
|
||||
# ── npm ──────────────────────────────────────────────────
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
@@ -498,10 +526,16 @@ jobs:
|
||||
else
|
||||
npm publish --access public --provenance
|
||||
fi
|
||||
# `next` is "the newest you can opt into": once a stable release
|
||||
# exists, an older RC must not stay behind it (0.9.1-rc.1 did until
|
||||
# v0.12.1). Re-runnable: dist-tag add is idempotent.
|
||||
if [[ "$RELEASE_VERSION" != *-* ]]; then
|
||||
npm dist-tag add "codebase-memory-mcp@$V" next
|
||||
fi
|
||||
|
||||
# ── PyPI ─────────────────────────────────────────────────
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
@@ -583,7 +617,7 @@ jobs:
|
||||
# the release-draft job as a same-run artifact (not a draft-release
|
||||
# download — that would need contents beyond read plus gh).
|
||||
- name: Fetch release checksums
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: release-checksums
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ on:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
# Only the newest main commit is scored: a newer push (or the weekly run)
|
||||
# cancels a pending or running score of an older commit.
|
||||
concurrency:
|
||||
group: scorecard-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
scorecard:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -20,13 +26,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run OpenSSF Scorecard
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
||||
uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
|
||||
- name: Upload SARIF results
|
||||
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
|
||||
uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -39,9 +39,6 @@ jobs:
|
||||
- os: ubuntu-24.04-arm
|
||||
cc: gcc
|
||||
cxx: g++
|
||||
- os: macos-14 # arm64
|
||||
cc: cc
|
||||
cxx: c++
|
||||
- os: macos-15 # arm64
|
||||
cc: cc
|
||||
cxx: c++
|
||||
@@ -54,7 +51,8 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install deps (Linux)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
|
||||
timeout-minutes: 10
|
||||
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 git
|
||||
- name: Build (production path with explicit test seams)
|
||||
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} TEST_SEAMS=1
|
||||
- name: Smoke invariants
|
||||
@@ -72,7 +70,7 @@ jobs:
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANG64
|
||||
path-type: inherit
|
||||
@@ -102,7 +100,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
|
||||
with:
|
||||
msystem: CLANGARM64
|
||||
path-type: inherit
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Team artifact for test selection (smart CI, report-only until the oracles
|
||||
# and the shadow comparison are green). Every push to main publishes the
|
||||
# commit's bundle — its frozen graph, built incrementally from the previous
|
||||
# bundle, and its per-test coverage map, re-run only for the suites the push
|
||||
# selects — as the Actions artifact test-impact-<sha>. A weekly run rebuilds
|
||||
# both from scratch and fails when the incremental graph differs from the full
|
||||
# one. PR selection fetches the bundle of its merge base
|
||||
# (scripts/ci/test-impact-bundle.sh) and never narrows on anything else.
|
||||
name: test-impact-artifact
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "23 3 * * 0" # weekly full refresh, Sunday 03:23 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: incremental or full
|
||||
type: choice
|
||||
options: [incremental, full]
|
||||
default: full
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # fetch the previous bundle
|
||||
|
||||
concurrency:
|
||||
# Bundles chain: each push builds on the previous one, so runs never overlap
|
||||
# and are never cancelled half-way.
|
||||
group: test-impact-artifact
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Full history: `test-impact select/publish` certify the history they
|
||||
# read and refuse a shallow clone.
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install build deps and LLVM 21 (coverage toolchain)
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
|
||||
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
|
||||
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-21 llvm-21 libclang-rt-21-dev
|
||||
|
||||
- name: Build the production binary and the coverage runner
|
||||
run: |
|
||||
scripts/build.sh CC=gcc CXX=g++
|
||||
make -f Makefile.cbm test-runner-cov COV_LLVM_BIN=/usr/lib/llvm-21/bin -j"$(nproc)"
|
||||
|
||||
- name: Publish the bundle
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
BEFORE: ${{ github.event.before }}
|
||||
REQUESTED: ${{ inputs.mode }}
|
||||
# The selection lane excludes @perf and PR CI skips perf, so their
|
||||
# (slowest, longest under coverage) suites are neither mapped nor
|
||||
# listed: the runner then neither lists nor runs them.
|
||||
CBM_SKIP_PERF: "1"
|
||||
run: |
|
||||
mode=incremental
|
||||
if [ "$EVENT" = schedule ] || [ "$REQUESTED" = full ]; then mode=full; fi
|
||||
before="$(git rev-parse HEAD^1)"
|
||||
if [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
|
||||
before="$BEFORE"
|
||||
fi
|
||||
scripts/ci/test-impact-publish.sh --binary build/c/codebase-memory-mcp \
|
||||
--runner build/cov/test-runner --llvm-bin /usr/lib/llvm-21/bin \
|
||||
--out "$RUNNER_TEMP/bundle" --mode "$mode" --before "$before"
|
||||
|
||||
- name: Upload the bundle
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: test-impact-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/bundle
|
||||
include-hidden-files: true # the graph lives in .codebase-memory/
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
@@ -33,6 +33,8 @@ Thumbs.db
|
||||
|
||||
# MCP config (user-local, generated by install command)
|
||||
.mcp.json
|
||||
# ...except the Claude Code plugin's server definition, which is published
|
||||
!plugin/.mcp.json
|
||||
|
||||
# MCP Registry auth tokens
|
||||
.mcpregistry_*
|
||||
@@ -51,6 +53,9 @@ graph-ui/dist/
|
||||
# Generated by scripts/embed-frontend.sh into the source tree (the linker needs
|
||||
# it beside the other UI sources). Removed by scripts/clean.sh; never committed.
|
||||
src/ui/embedded_assets.c
|
||||
# Raw coverage profiles. A coverage-instrumented binary (make test-runner-cov,
|
||||
# cbm-cov) run without LLVM_PROFILE_FILE drops default.profraw in its cwd.
|
||||
*.profraw
|
||||
|
||||
# Generated reports
|
||||
BENCHMARK_REPORT.md
|
||||
|
||||
@@ -6,7 +6,9 @@ Contributions are welcome. This guide covers setup, testing, and PR guidelines.
|
||||
|
||||
## Build from Source
|
||||
|
||||
**Prerequisites**: C compiler (gcc or clang), make, zlib, Git. Optional: Node.js 22+ (for graph UI).
|
||||
**Build prerequisites**: C compiler (gcc or clang), make, zlib, Git. Optional: Node.js 22+ (for graph UI).
|
||||
|
||||
**Contributor check prerequisites**: `scripts/test.sh` also requires `zip`, `python3`, and `curl`. `scripts/lint.sh` requires `cppcheck`, `clang-format`, and `clang-tidy`; `scripts/lint.sh --ci`, which is what CI runs, skips `clang-tidy`.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/DeusData/codebase-memory-mcp.git
|
||||
|
||||
@@ -52,7 +52,7 @@ Operational authority is stricter than code review authority.
|
||||
|
||||
| Operation | Current authority | Project rule |
|
||||
| --- | --- | --- |
|
||||
| PR validation (`pr.yml`, DCO, CodeQL) | Automatic | Anyone may trigger it by opening or updating a PR. Required checks must pass. |
|
||||
| PR validation (`pr.yml`, DCO, PR CodeQL scan) | Automatic | Anyone may trigger it by opening or updating a PR. Required checks must pass. |
|
||||
| Dry run (`dry-run.yml`) | `@DeusData` | Delegated release operators may run dry runs after promotion. Dry-run delegation does not imply release authority. |
|
||||
| Smoke/soak/repro manual runs | `@DeusData` | Area reviewers may operate these runs when delegated for diagnosis. Results are advisory. |
|
||||
| Release workflow (`release.yml`) | `@DeusData` only | Owner-only until a release operator is explicitly promoted. Publishing, replacing releases, and tag movement remain owner-gated. |
|
||||
|
||||
@@ -18,8 +18,24 @@
|
||||
# that use their own *_CFLAGS — and so it survives a command-line `CC=` override.
|
||||
# ARCHFLAGS is empty on Linux/Windows and for native direct `make` invocations,
|
||||
# leaving CC/CXX unchanged there.
|
||||
override CC := $(CC) $(ARCHFLAGS)
|
||||
override CXX := $(CXX) $(ARCHFLAGS)
|
||||
#
|
||||
# Minimum macOS version (Apple targets only). Without one, clang takes the
|
||||
# minimum from the SDK it compiles against (LC_BUILD_VERSION minos), so a
|
||||
# release built on a macos-15 runner would refuse to start on macOS 14. 14.0
|
||||
# keeps every macOS that could run a release built on the macos-14 runner
|
||||
# (retired 2026-11-02). A flag folded into the drivers like ARCHFLAGS, not an
|
||||
# exported MACOSX_DEPLOYMENT_TARGET: it reaches every compile AND link on every
|
||||
# build path (build.sh, test.sh, direct make) and is part of each command line.
|
||||
# Detected from the compiler's target, not the host, so a cross compiler run
|
||||
# on a Mac is left untouched.
|
||||
MACOS_MIN_VERSION := 14.0
|
||||
IS_APPLE_TARGET := $(shell echo | $(CC) $(ARCHFLAGS) -dM -E - 2>/dev/null | grep -q '__APPLE__' && echo yes || echo no)
|
||||
MACOS_MIN_FLAGS :=
|
||||
ifeq ($(IS_APPLE_TARGET),yes)
|
||||
MACOS_MIN_FLAGS := -mmacosx-version-min=$(MACOS_MIN_VERSION)
|
||||
endif
|
||||
override CC := $(CC) $(ARCHFLAGS) $(MACOS_MIN_FLAGS)
|
||||
override CXX := $(CXX) $(ARCHFLAGS) $(MACOS_MIN_FLAGS)
|
||||
|
||||
# ── Common flags ─────────────────────────────────────────────────
|
||||
|
||||
@@ -45,8 +61,9 @@ endif
|
||||
|
||||
# -Wdate-time: with -Werror, any use of __DATE__/__TIME__/__TIMESTAMP__ fails the
|
||||
# build. Build-time entropy is how one identical source tree produced a new hash
|
||||
# on every compile (see the local patch in vendored/mimalloc/src/options.c) —
|
||||
# once removed, it must not creep back through a new call site.
|
||||
# on every compile (mimalloc's version banner, patched locally until upstream
|
||||
# dropped the clock in v3.4.5; see vendored/mimalloc/PATCHES.md) — once
|
||||
# removed, it must not creep back through a new call site.
|
||||
CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
|
||||
-Wno-unused-parameter -Wno-sign-compare -Wdate-time \
|
||||
$(GCC_ONLY_FLAGS) \
|
||||
@@ -71,13 +88,33 @@ ifeq ($(TEST_SEAMS),1)
|
||||
TEST_SEAM_DEFINE := -DCBM_ENABLE_TEST_SEAMS=1
|
||||
endif
|
||||
|
||||
# Per-test coverage for the test-impact map. TEST_COVERAGE=1 compiles clang
|
||||
# source coverage into OUR code only -- the one-shot sources, the LSP unity
|
||||
# object and the preprocessor -- in the test runner and in the product binary,
|
||||
# and turns on the runner's per-test profile hooks (tests/test_framework.h).
|
||||
# Vendored objects stay uninstrumented: a vendored change selects tests by rule,
|
||||
# and every counter compiled in is written out again for every test. Like every
|
||||
# other flavour it needs its own BUILD_DIR; the test-runner-cov and cbm-cov
|
||||
# targets pin one.
|
||||
# -fprofile-continuous: a child process started with `%c` in LLVM_PROFILE_FILE
|
||||
# keeps its counters mapped onto its profile file, so a child that is killed
|
||||
# still leaves a complete profile. Linux needs the flag for `%c` to work at
|
||||
# all; with it macOS behaves the same way (tests/test_main.c, "Children").
|
||||
TEST_COVERAGE ?= 0
|
||||
TEST_COVERAGE_FLAGS :=
|
||||
TEST_COVERAGE_DEFINE :=
|
||||
ifeq ($(TEST_COVERAGE),1)
|
||||
TEST_COVERAGE_FLAGS := -fprofile-instr-generate -fcoverage-mapping -fprofile-continuous
|
||||
TEST_COVERAGE_DEFINE := -DCBM_TEST_COVERAGE=1
|
||||
endif
|
||||
|
||||
# Production flags (CFLAGS_EXTRA allows CI to inject -DCBM_VERSION)
|
||||
# CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only
|
||||
# the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where
|
||||
# binding would create an alloc/free mismatch, so the guard is prod-only.
|
||||
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) \
|
||||
$(GLOBAL_OVERRIDE_DEFINE) $(MEMWASTE_CFLAGS) $(CFLAGS_EXTRA)
|
||||
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2 $(MEMWASTE_VENDOR_CFLAGS)
|
||||
$(GLOBAL_OVERRIDE_DEFINE) $(MEMWASTE_CFLAGS) $(TEST_COVERAGE_FLAGS) $(CFLAGS_EXTRA)
|
||||
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2 $(MEMWASTE_VENDOR_CFLAGS) $(TEST_COVERAGE_FLAGS)
|
||||
|
||||
# Test flags: debug + sanitizers (override SANITIZE= to disable on Windows)
|
||||
SANITIZE = -fsanitize=address,undefined -fno-omit-frame-pointer
|
||||
@@ -85,7 +122,8 @@ EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
|
||||
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
|
||||
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
|
||||
-DCBM_CONFIG_EDIT_PATH_ENABLE_TEST_API=1 \
|
||||
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1 -DCBM_ENABLE_TEST_SEAMS=1
|
||||
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1 -DCBM_ENABLE_TEST_SEAMS=1 \
|
||||
-DCBM_SVN_STATE_ENABLE_TEST_API=1
|
||||
# The build system is the single source of truth for "is this binary
|
||||
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
|
||||
# clang's feature-check spelling and every non-ASan sanitizer, so the
|
||||
@@ -102,8 +140,10 @@ INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
|
||||
COVERAGE_MARKER_TEST_DEFINE = -DCBM_COVERAGE_MARKER_TEST_API=1
|
||||
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
|
||||
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
|
||||
$(INCREMENTAL_TEST_DEFINE) $(COVERAGE_MARKER_TEST_DEFINE) -g -O1 $(SANITIZE)
|
||||
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)
|
||||
$(INCREMENTAL_TEST_DEFINE) $(COVERAGE_MARKER_TEST_DEFINE) $(TEST_COVERAGE_DEFINE) \
|
||||
-g -O1 $(SANITIZE) $(TEST_COVERAGE_FLAGS)
|
||||
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(TEST_COVERAGE_FLAGS) \
|
||||
$(CXX_STDLIB_FLAGS)
|
||||
|
||||
# TSan (can't combine with ASan)
|
||||
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
|
||||
@@ -160,8 +200,9 @@ endif
|
||||
ifeq ($(IS_MINGW),yes)
|
||||
# --no-insert-timestamp: the PE header otherwise carries the link wall clock, so
|
||||
# two Windows builds of identical source are never byte-identical and every
|
||||
# release is a brand-new file to a reputation system. Same motivation as the
|
||||
# mimalloc __DATE__ patch; ELF and Mach-O have no equivalent field to clear.
|
||||
# release is a brand-new file to a reputation system. Same motivation as
|
||||
# dropping mimalloc's __DATE__ banner; ELF and Mach-O have no equivalent field
|
||||
# to clear.
|
||||
WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -lbcrypt -Wl,--allow-multiple-definition -Wl,--stack,8388608 -Wl,--no-insert-timestamp -static
|
||||
endif
|
||||
|
||||
@@ -309,6 +350,7 @@ FOUNDATION_SRCS = \
|
||||
src/foundation/str_util.c \
|
||||
src/foundation/workspace.c \
|
||||
src/foundation/platform.c \
|
||||
src/foundation/path_syntax_internal.c \
|
||||
src/foundation/system_info.c \
|
||||
src/foundation/slab_alloc.c \
|
||||
src/foundation/yaml.c \
|
||||
@@ -327,6 +369,7 @@ FOUNDATION_SRCS = \
|
||||
src/foundation/limits.c \
|
||||
src/foundation/index_policy.c \
|
||||
src/foundation/subprocess.c \
|
||||
src/foundation/git_env.c \
|
||||
src/foundation/sha256.c \
|
||||
src/foundation/secure_random.c \
|
||||
src/foundation/macos_acl.c \
|
||||
@@ -337,10 +380,12 @@ FOUNDATION_SRCS = \
|
||||
EXTRACTION_SRCS = \
|
||||
$(CBM_DIR)/cbm.c \
|
||||
$(CBM_DIR)/extract_defs.c \
|
||||
$(CBM_DIR)/callable_sig.c \
|
||||
$(CBM_DIR)/extract_calls.c \
|
||||
$(CBM_DIR)/extract_imports.c \
|
||||
$(CBM_DIR)/extract_usages.c \
|
||||
$(CBM_DIR)/extract_unified.c \
|
||||
$(CBM_DIR)/extract_url_fold.c \
|
||||
$(CBM_DIR)/extract_semantic.c \
|
||||
$(CBM_DIR)/extract_type_refs.c \
|
||||
$(CBM_DIR)/extract_type_assigns.c \
|
||||
@@ -348,14 +393,41 @@ EXTRACTION_SRCS = \
|
||||
$(CBM_DIR)/extract_channels.c \
|
||||
$(CBM_DIR)/extract_k8s.c \
|
||||
$(CBM_DIR)/extract_dbt.c \
|
||||
$(CBM_DIR)/doclink.c \
|
||||
$(CBM_DIR)/doclink_cs.c \
|
||||
$(CBM_DIR)/doclink_md.c \
|
||||
$(CBM_DIR)/doclink_rst.c \
|
||||
$(CBM_DIR)/doclink_py.c \
|
||||
$(CBM_DIR)/doc_adr.c \
|
||||
$(CBM_DIR)/doc_pdf.c \
|
||||
$(CBM_DIR)/doc_adoc.c \
|
||||
$(CBM_DIR)/pdf/pdf_obj.c \
|
||||
$(CBM_DIR)/pdf/pdf_font.c \
|
||||
$(CBM_DIR)/pdf/pdf_text.c \
|
||||
$(CBM_DIR)/pdf/pdf_tables.c \
|
||||
$(CBM_DIR)/pdf/pdf_unicode.c \
|
||||
$(CBM_DIR)/doclink_go.c \
|
||||
$(CBM_DIR)/doclink_jvm.c \
|
||||
$(CBM_DIR)/doclink_rs.c \
|
||||
$(CBM_DIR)/doclink_perl.c \
|
||||
$(CBM_DIR)/doclink_ts.c \
|
||||
$(CBM_DIR)/doclink_php.c \
|
||||
$(CBM_DIR)/doclink_c.c \
|
||||
$(CBM_DIR)/diagram.c \
|
||||
$(CBM_DIR)/route_path.c \
|
||||
$(CBM_DIR)/openapi.c \
|
||||
$(CBM_DIR)/extract_nextjs.c \
|
||||
$(CBM_DIR)/sql_values.c \
|
||||
$(CBM_DIR)/php_inline_html.c \
|
||||
$(CBM_DIR)/helpers.c \
|
||||
$(CBM_DIR)/cpp_branch_views.c \
|
||||
$(CBM_DIR)/result_compact.c \
|
||||
$(CBM_DIR)/result_spill.c \
|
||||
$(CBM_DIR)/lang_specs.c \
|
||||
$(CBM_DIR)/macro_table.c \
|
||||
$(CBM_DIR)/iris_export_xml.c \
|
||||
$(CBM_DIR)/service_patterns.c
|
||||
$(CBM_DIR)/service_patterns.c \
|
||||
$(CBM_DIR)/spawn_patterns.c
|
||||
|
||||
# LSP resolvers (compiled as one unit via lsp_all.c)
|
||||
LSP_SRCS = $(CBM_DIR)/lsp_all.c
|
||||
@@ -398,13 +470,15 @@ PREPROCESSOR_SRC = $(CBM_DIR)/preprocessor.cpp
|
||||
SQLITE_WRITER_SRC = $(CBM_DIR)/sqlite_writer.c
|
||||
|
||||
# Store module (new)
|
||||
STORE_SRCS = src/store/store.c
|
||||
STORE_SRCS = src/store/store.c src/store/graph_mem.c src/store/architecture_projection.c src/store/store_impact.c src/store/store_graph_digest.c src/store/store_content_digest.c
|
||||
|
||||
# Cypher module (new)
|
||||
CYPHER_SRCS = src/cypher/cypher.c
|
||||
|
||||
# MCP server module (new)
|
||||
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c
|
||||
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c src/mcp/architecture_jobs.c \
|
||||
src/mcp/test_impact.c src/mcp/test_impact_diff.c src/mcp/test_impact_classify.c src/mcp/test_impact_source.c src/mcp/test_impact_seed.c src/mcp/test_impact_engine.c src/mcp/test_impact_engine_snapshot.c src/mcp/test_impact_engine_reach.c src/mcp/test_impact_engine_artifact.c src/mcp/test_impact_cli.c src/mcp/test_impact_artifact.c src/mcp/test_impact_changes.c src/mcp/test_impact_origins.c src/mcp/test_impact_coverage.c src/mcp/test_impact_config.c src/mcp/test_impact_select.c src/mcp/test_impact_policy.c src/mcp/test_impact_git.c src/mcp/test_impact_inventory.c src/mcp/test_impact_inventory_input.c src/mcp/test_impact_tree.c src/mcp/test_impact_tree_audit.c src/mcp/test_impact_tree_read.c src/mcp/test_impact_tree_batch.c src/mcp/test_impact_tree_digest.c src/mcp/test_impact_tree_names.c src/mcp/test_impact_tree_plan.c src/mcp/test_impact_tree_posix.c src/mcp/test_impact_tree_posix_acl.c src/mcp/test_impact_tree_posix_io.c src/mcp/test_impact_tree_util.c src/mcp/test_impact_tree_windows.c src/mcp/test_impact_tree_windows_acl.c src/mcp/test_impact_tree_windows_io.c \
|
||||
src/mcp/test_impact_result.c src/mcp/test_impact_result_json.c src/mcp/test_impact_result_project.c src/mcp/test_impact_result_receipt.c src/mcp/test_impact_result_rows.c src/mcp/test_impact_result_util.c src/mcp/test_impact_result_validate.c src/mcp/test_impact_result_wire.c
|
||||
|
||||
# Shared MCP daemon coordination and local transport
|
||||
DAEMON_SRCS = \
|
||||
@@ -423,7 +497,18 @@ DAEMON_SRCS = \
|
||||
DISCOVER_SRCS = \
|
||||
src/discover/language.c \
|
||||
src/discover/userconfig.c \
|
||||
src/discover/test_conventions.c \
|
||||
src/discover/gitignore.c \
|
||||
src/discover/gitignore_checked.c \
|
||||
src/discover/inventory_filter.c \
|
||||
src/discover/inventory.c \
|
||||
src/discover/inventory_guard.c \
|
||||
src/discover/inventory_metadata.c \
|
||||
src/discover/inventory_directories.c \
|
||||
src/discover/inventory_controls.c \
|
||||
src/discover/gitignore_checked_parse.c \
|
||||
src/discover/gitignore_core.c \
|
||||
src/discover/gitignore_guard.c \
|
||||
src/discover/discover.c
|
||||
|
||||
# Graph buffer module (new)
|
||||
@@ -435,6 +520,7 @@ PIPELINE_SRCS = \
|
||||
src/pipeline/lsp_surface.c \
|
||||
src/pipeline/pipeline_delta.c \
|
||||
src/pipeline/path_alias.c \
|
||||
src/pipeline/laravel_routing.c \
|
||||
src/pipeline/registry.c \
|
||||
src/pipeline/pipeline.c \
|
||||
src/pipeline/pipeline_incremental.c \
|
||||
@@ -442,6 +528,7 @@ PIPELINE_SRCS = \
|
||||
src/pipeline/pass_parallel.c \
|
||||
src/pipeline/pass_definitions.c \
|
||||
src/pipeline/pass_calls.c \
|
||||
src/pipeline/pass_spawns.c \
|
||||
src/pipeline/pass_lsp_cross.c \
|
||||
src/pipeline/pass_usages.c \
|
||||
src/pipeline/pass_semantic.c \
|
||||
@@ -451,12 +538,34 @@ PIPELINE_SRCS = \
|
||||
src/pipeline/pass_configures.c \
|
||||
src/pipeline/pass_configlink.c \
|
||||
src/pipeline/pass_doclinks.c \
|
||||
src/pipeline/doc_links.c \
|
||||
src/pipeline/doc_links_cs.c \
|
||||
src/pipeline/doc_links_md.c \
|
||||
src/pipeline/doc_links_pdf.c \
|
||||
src/pipeline/doc_links_rst.c \
|
||||
src/pipeline/doc_links_adoc.c \
|
||||
src/pipeline/doc_links_go.c \
|
||||
src/pipeline/doc_links_jvm.c \
|
||||
src/pipeline/doc_links_rs.c \
|
||||
src/pipeline/doc_links_perl.c \
|
||||
src/pipeline/doc_links_ts.c \
|
||||
src/pipeline/doc_links_php.c \
|
||||
src/pipeline/doc_links_c.c \
|
||||
src/pipeline/pass_diagrams.c \
|
||||
src/pipeline/doc_links_msbuild.c \
|
||||
src/pipeline/pass_route_nodes.c \
|
||||
src/pipeline/pass_enrichment.c \
|
||||
src/pipeline/pass_envscan.c \
|
||||
src/pipeline/pass_compile_commands.c \
|
||||
src/pipeline/pass_infrascan.c \
|
||||
src/pipeline/pass_k8s.c \
|
||||
src/pipeline/helm_template.c \
|
||||
src/pipeline/pass_operator.c \
|
||||
src/pipeline/pass_shell.c \
|
||||
src/pipeline/pass_terraform.c \
|
||||
src/pipeline/pass_ansible.c \
|
||||
src/pipeline/pass_docker.c \
|
||||
src/pipeline/pass_envjoin.c \
|
||||
src/pipeline/pass_ci.c \
|
||||
src/pipeline/pass_similarity.c \
|
||||
src/pipeline/pass_semantic_edges.c \
|
||||
src/pipeline/pass_complexity.c \
|
||||
@@ -479,13 +588,13 @@ UNIXCODER_BLOB_SRC = vendored/nomic/code_vectors_blob.S
|
||||
TRACES_SRCS = src/traces/traces.c
|
||||
|
||||
# Watcher module (new)
|
||||
WATCHER_SRCS = src/watcher/watcher.c
|
||||
WATCHER_SRCS = src/watcher/watcher.c src/watcher/svn_state.c
|
||||
|
||||
# Git context module (new)
|
||||
GIT_SRCS = src/git/git_context.c
|
||||
|
||||
# CLI module (new)
|
||||
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
|
||||
CLI_SRCS = src/cli/cli.c src/cli/runtime_settings.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
|
||||
src/cli/agent_clients.c src/cli/agent_profiles.c \
|
||||
src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \
|
||||
src/cli/config_text_edit.c src/cli/config_edit_path.c src/cli/activation_transaction.c
|
||||
@@ -494,7 +603,16 @@ CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/
|
||||
UI_SRCS = \
|
||||
src/ui/config.c \
|
||||
src/ui/http_server.c \
|
||||
src/ui/activity.c \
|
||||
src/ui/atlas_repository.c \
|
||||
src/ui/layout3d.c \
|
||||
src/ui/layout_regions.c \
|
||||
src/ui/atlas_api.c \
|
||||
src/ui/atlas_flows.c \
|
||||
src/ui/atlas_handout.c \
|
||||
src/ui/atlas_why.c \
|
||||
src/ui/atlas_metrics.c \
|
||||
src/ui/atlas_impact.c \
|
||||
src/ui/httpd.c \
|
||||
src/ui/embedded_stub.c
|
||||
|
||||
@@ -547,6 +665,18 @@ ifneq ($(filter yes,$(IS_MINGW) $(IS_LINUX)),)
|
||||
MIMALLOC_OVERRIDE_DEFINE := -DMI_MALLOC_OVERRIDE=1
|
||||
GLOBAL_OVERRIDE_DEFINE := -DCBM_MEM_GLOBAL_OVERRIDE=1
|
||||
endif
|
||||
# MI_MINGW_UCRT64: since 3.5.3, mimalloc treats a MinGW build as having a
|
||||
# process-attach hook only when this is defined (src/prim/windows/prim.c);
|
||||
# upstream's CMake sets it, this Makefile must too. Without it src/prim/prim.c
|
||||
# also compiles its constructor, so _mi_auto_process_init runs twice (that
|
||||
# constructor plus the CRT TLS callback) and the second options flush writes a
|
||||
# stray newline to stderr before main: a blank first line on every run, which
|
||||
# failed smoke-win's one-line --version check. Every MinGW toolchain we build
|
||||
# with (MSYS2 CLANG64/CLANGARM64) is UCRT. 3.3.2 had no such condition.
|
||||
MIMALLOC_WIN_DEFINE :=
|
||||
ifeq ($(IS_MINGW),yes)
|
||||
MIMALLOC_WIN_DEFINE := -DMI_MINGW_UCRT64=1
|
||||
endif
|
||||
# MI_DEBUG=0: the production allocator is mimalloc's RELEASE build. Its header
|
||||
# defaults to MI_DEBUG=2 unless NDEBUG is defined, and nothing here defined it,
|
||||
# so every shipped binary up to v0.11.0 ran mimalloc's debug build: internal
|
||||
@@ -556,15 +686,39 @@ endif
|
||||
# work inside the allocator (2026-09-17). Committed-bytes accounting, which the
|
||||
# memory budget reads, is kept in every build (subproc stats, not MI_STAT).
|
||||
# The TEST build below keeps mimalloc's debug checks.
|
||||
# MI_PROFILE=0: mimalloc 3.5.2+ compiles its allocation-sampling profiler in by
|
||||
# default, which puts a sample countdown on the allocation path. The product
|
||||
# never uses it, so the production allocator leaves it out (the contract in
|
||||
# tests/test_prod_allocator_contract.sh checks the banner has no profiling line).
|
||||
MIMALLOC_CFLAGS = -std=c11 -O2 -w \
|
||||
-Ivendored/mimalloc/include \
|
||||
-Ivendored/mimalloc/src \
|
||||
-DMI_OVERRIDE=1 -DMI_DEBUG=0 \
|
||||
$(MIMALLOC_OVERRIDE_DEFINE)
|
||||
-DMI_OVERRIDE=1 -DMI_DEBUG=0 -DMI_PROFILE=0 \
|
||||
$(MIMALLOC_OVERRIDE_DEFINE) $(MIMALLOC_WIN_DEFINE)
|
||||
MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
|
||||
-Ivendored/mimalloc/include \
|
||||
-Ivendored/mimalloc/src \
|
||||
-DMI_OVERRIDE=0
|
||||
-DMI_OVERRIDE=0 $(MIMALLOC_WIN_DEFINE)
|
||||
# MI_WIN_INIT_USE_FLS (MinGW TEST build only) is an EXPERIMENT (R32) for the
|
||||
# Windows test leg's nondeterministic crash: 1-3 suites die in their first
|
||||
# test with 0xC0000005 and no sanitizer report, seen only on trees with
|
||||
# mimalloc 3.5.3. 3.5.3 is the first version whose loader hooks are live on
|
||||
# MinGW: TLS callbacks in .CRT$XLB/.CRT$XLY run _mi_auto_process_init under the
|
||||
# loader lock and _mi_thread_done on every thread detach. With FLS,
|
||||
# src/prim/windows/prim.c compiles none of those hooks; process init runs once,
|
||||
# from the constructor in src/prim/prim.c (no double init), and thread-done
|
||||
# goes through a fiber-local destructor. MI_DEBUG=1 is what FLS needs to run at
|
||||
# all: the first thread init (src/init.c) associates the main theap before the
|
||||
# FLS key exists, which trips an internal assertion in prim.c and aborts every
|
||||
# MI_DEBUG=2 process at startup. Level 1 keeps the fill patterns, padding and
|
||||
# double-free checks and drops only the internal assertions. The production
|
||||
# allocator (MIMALLOC_CFLAGS) is unchanged. Drop or promote this when the
|
||||
# experiment ends.
|
||||
MIMALLOC_WIN_TEST_DEFINE :=
|
||||
ifeq ($(IS_MINGW),yes)
|
||||
MIMALLOC_WIN_TEST_DEFINE := -DMI_WIN_INIT_USE_FLS=1 -DMI_DEBUG=1
|
||||
MIMALLOC_CFLAGS_TEST += $(MIMALLOC_WIN_TEST_DEFINE)
|
||||
endif
|
||||
|
||||
# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
|
||||
# SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the
|
||||
@@ -614,6 +768,8 @@ PROJECT_HDRS = $(wildcard src/*.h src/*/*.h $(CBM_DIR)/*.h $(CBM_DIR)/lsp/*.h te
|
||||
|
||||
TEST_FOUNDATION_SRCS = \
|
||||
tests/test_main.c \
|
||||
tests/test_coverage_children.c \
|
||||
tests/test_coverage_parent_failure.c \
|
||||
tests/test_arena.c \
|
||||
tests/test_hash_table.c \
|
||||
tests/test_dyn_array.c \
|
||||
@@ -632,6 +788,8 @@ TEST_FOUNDATION_SRCS = \
|
||||
|
||||
TEST_EXTRACTION_SRCS = \
|
||||
tests/test_extraction.c \
|
||||
tests/test_callable_sig.c \
|
||||
tests/test_test_conventions.c \
|
||||
tests/test_extraction_inheritance.c \
|
||||
tests/test_extraction_imports.c \
|
||||
tests/test_parse_coverage.c \
|
||||
@@ -643,8 +801,13 @@ TEST_EXTRACTION_SRCS = \
|
||||
TEST_STORE_SRCS = \
|
||||
tests/test_store_nodes.c \
|
||||
tests/test_store_edges.c \
|
||||
tests/test_store_impact.c \
|
||||
tests/test_store_scope.c \
|
||||
tests/test_store_graph_digest.c \
|
||||
tests/test_graph_mem.c \
|
||||
tests/test_store_search.c \
|
||||
tests/test_store_arch.c \
|
||||
tests/test_architecture_projection.c \
|
||||
tests/test_store_bulk.c \
|
||||
tests/test_store_pragmas.c \
|
||||
tests/test_store_checkpoint.c \
|
||||
@@ -655,6 +818,23 @@ TEST_CYPHER_SRCS = \
|
||||
|
||||
TEST_MCP_SRCS = \
|
||||
tests/test_mcp.c \
|
||||
tests/test_test_impact.c \
|
||||
tests/test_test_impact_profiles.c \
|
||||
tests/test_test_impact_changes.c \
|
||||
tests/test_test_impact_origins.c \
|
||||
tests/test_test_impact_git.c \
|
||||
tests/test_test_impact_tree.c \
|
||||
tests/test_test_impact_inventory.c \
|
||||
tests/test_test_impact_classify.c \
|
||||
tests/test_test_impact_source.c \
|
||||
tests/test_test_impact_seed.c \
|
||||
tests/test_test_impact_engine.c \
|
||||
tests/test_test_impact_artifact.c \
|
||||
tests/test_test_impact_inventory_errors.c \
|
||||
tests/test_test_impact_inventory_support.c \
|
||||
tests/test_test_impact_runner_filter.c \
|
||||
tests/test_test_impact_runner_filter_fixture.c \
|
||||
tests/test_test_impact_runner_filter_process.c \
|
||||
tests/test_index_supervisor.c
|
||||
|
||||
TEST_DAEMON_SRCS = \
|
||||
@@ -672,14 +852,25 @@ TEST_DISCOVER_SRCS = \
|
||||
tests/test_language.c \
|
||||
tests/test_userconfig.c \
|
||||
tests/test_gitignore.c \
|
||||
tests/test_gitignore_checked.c \
|
||||
tests/if_native_git_path.c \
|
||||
tests/test_inventory_filter.c \
|
||||
tests/test_inventory_filter_cases.c \
|
||||
tests/test_inventory_filter_errors.c \
|
||||
tests/test_inventory_filter_limits.c \
|
||||
tests/test_inventory_filter_native.c \
|
||||
tests/test_inventory_filter_native_fixture.c \
|
||||
tests/test_inventory_filter_native_platform.c \
|
||||
tests/test_inventory_filter_structure_extra.c \
|
||||
tests/test_inventory_filter_support.c \
|
||||
tests/test_git_context.c \
|
||||
tests/test_discover.c
|
||||
|
||||
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
|
||||
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_pipeline_conventions.c tests/test_pipeline_frozen.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_doc_mentions.c tests/test_doc_links_md.c tests/test_doc_links_pdf.c tests/test_doc_links_rst.c tests/test_doc_links_adoc.c tests/test_doc_mentions_go.c tests/test_doc_mentions_jvm.c tests/test_doc_mentions_rs.c tests/test_doc_mentions_perl.c tests/test_doc_mentions_ts.c tests/test_doc_mentions_php.c tests/test_doc_mentions_c.c tests/test_diagrams.c tests/test_openapi.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
|
||||
|
||||
TEST_WATCHER_SRCS = tests/test_watcher.c
|
||||
TEST_WATCHER_SRCS = tests/test_watcher.c tests/test_svn_state.c
|
||||
|
||||
TEST_LZ4_SRCS = tests/test_lz4.c
|
||||
TEST_ZSTD_SRCS = tests/test_zstd.c
|
||||
@@ -698,6 +889,7 @@ TEST_CS_LSP_SRCS = tests/test_cs_lsp.c
|
||||
TEST_CS_LSP_BENCH_SRCS = tests/test_cs_lsp_bench.c
|
||||
|
||||
TEST_PERL_LSP_SRCS = tests/test_perl_lsp.c
|
||||
TEST_RUBY_LSP_SRCS = tests/test_ruby_lsp.c
|
||||
|
||||
TEST_SCOPE_SRCS = tests/test_scope.c
|
||||
|
||||
@@ -712,16 +904,17 @@ TEST_PY_LSP_STRESS_SRCS = tests/test_py_lsp_stress.c
|
||||
TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c
|
||||
|
||||
TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
|
||||
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
|
||||
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
|
||||
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c \
|
||||
tests/test_java_typed_calls.c
|
||||
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c tests/test_kotlin_typed_calls.c
|
||||
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
|
||||
|
||||
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c
|
||||
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c tests/test_conditional_variants.c tests/test_spawns.c
|
||||
|
||||
TEST_TRACES_SRCS = tests/test_traces.c
|
||||
|
||||
|
||||
TEST_CLI_SRCS = tests/test_cli.c tests/test_agent_clients.c tests/test_agent_profiles.c \
|
||||
TEST_CLI_SRCS = tests/test_cli.c tests/test_runtime_settings.c tests/test_agent_clients.c tests/test_agent_profiles.c \
|
||||
tests/test_config_json_like.c \
|
||||
tests/test_config_toml_edit.c tests/test_config_yaml_edit.c tests/test_config_text_edit.c \
|
||||
tests/test_activation_transaction.c
|
||||
@@ -821,7 +1014,7 @@ TEST_REPRO_SRCS = \
|
||||
tests/repro/repro_lsp_java_cs.c \
|
||||
tests/repro/repro_lsp_kt_php_rust.c
|
||||
|
||||
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_RUBY_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
|
||||
# ── Build directories ────────────────────────────────────────────
|
||||
@@ -831,8 +1024,14 @@ BUILD_DIR = build/c
|
||||
# ── Object file compilation (grammars need relaxed warnings) ─────
|
||||
|
||||
# Grammar + tree-sitter runtime: compiled without -Werror (upstream code has warnings)
|
||||
# $(TEST_SEAM_DEFINE): these prod objects (grammars, ts_runtime, lsp_all) are
|
||||
# linked into the same binary as the CFLAGS_PROD sources, so both halves must
|
||||
# agree on the seam switch. lsp_all holds seam code of its own (ts_lsp.c's
|
||||
# budget probes, any seam counter a resolver defines): without the define a
|
||||
# TEST_SEAMS=1 binary silently lacks those seams, and a seam symbol that cbm.c
|
||||
# references fails to link. GRAMMAR_CFLAGS_TEST always carries the define.
|
||||
GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
|
||||
$(MEMWASTE_VENDOR_CFLAGS)
|
||||
$(TEST_SEAM_DEFINE) $(MEMWASTE_VENDOR_CFLAGS)
|
||||
GRAMMAR_CFLAGS_TEST = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
|
||||
-DCBM_ENABLE_TEST_SEAMS=1 $(SANITIZED_DEFINE) $(SANITIZE)
|
||||
GRAMMAR_CFLAGS_TSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
|
||||
@@ -850,7 +1049,7 @@ PP_OBJ_TSAN = $(BUILD_DIR)/tsan_preprocessor.o
|
||||
|
||||
# ── Targets ──────────────────────────────────────────────────────
|
||||
|
||||
.PHONY: test test-par test-repro test-foundation test-tsan test-daemon-smoke cbm cbm-with-ui frontend embed clean-c lint lint-tidy lint-tidy-diff lint-cppcheck lint-format security fuzz
|
||||
.PHONY: test-coverage-flush test test-par test-repro test-foundation test-tsan test-daemon-smoke test-runner-cov cbm-cov cbm cbm-with-ui frontend embed clean-c lint lint-tidy lint-tidy-diff lint-cppcheck lint-format security fuzz
|
||||
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
@@ -861,7 +1060,7 @@ $(BUILD_DIR)/test-foundation: $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(PROJE
|
||||
$(CC) $(CFLAGS_TEST) -o $@ $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(LDFLAGS_TEST)
|
||||
|
||||
test-foundation: $(BUILD_DIR)/test-foundation
|
||||
cd $(CURDIR) && $(BUILD_DIR)/test-foundation
|
||||
cd "$(CURDIR)" && $(BUILD_DIR)/test-foundation
|
||||
|
||||
# ── Grammar/TS/LSP object files (compiled with relaxed warnings) ─
|
||||
|
||||
@@ -889,7 +1088,7 @@ $(BUILD_DIR)/ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS_TEST) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS_TEST) $(SANITIZE) -c -o $@ $<
|
||||
$(CC) $(GRAMMAR_CFLAGS_TEST) $(SANITIZE) $(TEST_COVERAGE_FLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
|
||||
$(CXX) $(CXXFLAGS_TEST) -w -I$(CBM_DIR)/vendored -c -o $@ $<
|
||||
@@ -940,7 +1139,9 @@ $(BUILD_DIR)/sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/tsan_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
|
||||
$(CC) $(SQLITE3_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) Makefile.cbm | $(BUILD_DIR)
|
||||
# .build-config: SQLITE3_CFLAGS carries MEMWASTE_VENDOR_CFLAGS (see the stamp
|
||||
# under "Production binary").
|
||||
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) Makefile.cbm $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) $(SQLITE3_CFLAGS) -c -o $@ $<
|
||||
|
||||
# TRE regex (only compiled on Windows — POSIX uses system <regex.h>)
|
||||
@@ -970,7 +1171,7 @@ $(BUILD_DIR)/tre.o: $(TRE_SRC) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/tsan_tre.o: $(TRE_SRC) | $(BUILD_DIR)
|
||||
$(CC) $(TRE_CFLAGS) $(TSAN_SANITIZE) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_tre.o: $(TRE_SRC) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_tre.o: $(TRE_SRC) $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) $(TRE_CFLAGS) -O2 $(MEMWASTE_VENDOR_CFLAGS) -c -o $@ $<
|
||||
endif
|
||||
|
||||
@@ -1018,13 +1219,13 @@ $(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_
|
||||
$(LDFLAGS_TEST)
|
||||
|
||||
test: $(BUILD_DIR)/test-runner
|
||||
cd $(CURDIR) && $(BUILD_DIR)/test-runner
|
||||
cd "$(CURDIR)" && $(BUILD_DIR)/test-runner
|
||||
|
||||
# Parallel variant: every suite as its own process; identical gate quality
|
||||
# (see the ZERO-LOSS CONTRACT in scripts/run-tests-parallel.sh — union guard
|
||||
# against --list-suites + aggregated pass/fail/skip totals).
|
||||
test-par: $(BUILD_DIR)/test-runner
|
||||
cd $(CURDIR) && bash scripts/run-tests-parallel.sh $(BUILD_DIR)/test-runner
|
||||
cd "$(CURDIR)" && bash scripts/run-tests-parallel.sh $(BUILD_DIR)/test-runner
|
||||
|
||||
# Focused native development is intentionally a separate, explicit target so
|
||||
# an inherited TEST_SUITES value cannot silently narrow the gating test target.
|
||||
@@ -1032,7 +1233,7 @@ TEST_SUITES ?=
|
||||
test-focused: $(BUILD_DIR)/test-runner
|
||||
@test -n "$(strip $(TEST_SUITES))" || \
|
||||
(echo "TEST_SUITES is required for test-focused"; exit 2)
|
||||
cd $(CURDIR) && $(BUILD_DIR)/test-runner $(TEST_SUITES)
|
||||
cd "$(CURDIR)" && $(BUILD_DIR)/test-runner $(TEST_SUITES)
|
||||
|
||||
# ── Cumulative bug-reproduction runner (RED board by design, non-gating) ──
|
||||
# Mirrors test-runner's link line but uses repro_main.c (own main + counters)
|
||||
@@ -1124,32 +1325,49 @@ TS_RUNTIME_OBJ_PROD = $(BUILD_DIR)/prod_ts_runtime.o
|
||||
LSP_OBJ_PROD = $(BUILD_DIR)/prod_lsp_all.o
|
||||
PP_OBJ_PROD = $(BUILD_DIR)/prod_preprocessor.o
|
||||
|
||||
$(BUILD_DIR)/prod_%.o: $(CBM_DIR)/%.c $(GRAMMAR_UNITY_DEPS) | $(BUILD_DIR)
|
||||
# Every prod object whose flags carry a BUILD_CONFIG_SIG variable
|
||||
# (TEST_SEAM_DEFINE via GRAMMAR_CFLAGS, MEMWASTE_VENDOR_CFLAGS) depends on the
|
||||
# $(BUILD_DIR)/.build-config stamp. These objects are compiled once and linked
|
||||
# into every later binary, so without the stamp a TEST_SEAMS or MEMWASTE flip
|
||||
# relinks the PREVIOUS flavour: a release binary keeps seam code compiled into
|
||||
# lsp_all, or a seams binary misses it. prod_mimalloc.o and the unixcoder blob
|
||||
# take no config variable and stay off the stamp.
|
||||
$(BUILD_DIR)/prod_%.o: $(CBM_DIR)/%.c $(GRAMMAR_UNITY_DEPS) $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) $(TEST_COVERAGE_FLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_preprocessor.o: $(CBM_DIR)/preprocessor.cpp $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CXX) $(CXXFLAGS_PROD) -w -I$(CBM_DIR)/vendored -c -o $@ $<
|
||||
|
||||
# Vendored LZ4 (compiled separately, not unity-built via lz4_store.c)
|
||||
LZ4_OBJ_PROD = $(BUILD_DIR)/prod_lz4.o $(BUILD_DIR)/prod_lz4hc.o
|
||||
$(BUILD_DIR)/prod_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w $(MEMWASTE_VENDOR_CFLAGS) -I$(CBM_DIR) -c -o $@ $<
|
||||
$(BUILD_DIR)/prod_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w $(MEMWASTE_VENDOR_CFLAGS) -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
|
||||
|
||||
# Vendored zstd (compiled separately, not unity-built via zstd_store.c)
|
||||
ZSTD_OBJ_PROD = $(BUILD_DIR)/prod_zstd.o
|
||||
$(BUILD_DIR)/prod_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
|
||||
$(BUILD_DIR)/prod_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) -std=c11 -D_DEFAULT_SOURCE -O2 -w $(MEMWASTE_VENDOR_CFLAGS) -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
|
||||
|
||||
OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LSP_OBJ_PROD) $(PP_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ)
|
||||
|
||||
# The part of OBJS_VENDORED_PROD that holds no first-party code: everything but
|
||||
# prod_lsp_all.o (first-party LSP unity) and prod_preprocessor.o (first-party
|
||||
# code around vendored simplecpp). CodeQL's light PR scan builds these BEFORE
|
||||
# it starts tracing (scripts/build.sh --vendored-only), so the traced build
|
||||
# (--reuse-vendored) compiles first-party code only: the grammars alone are
|
||||
# most of the 1.3 GB of vendored C, against ~45 MB of first-party code.
|
||||
OBJS_THIRD_PARTY_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ)
|
||||
.PHONY: cbm-third-party
|
||||
cbm-third-party: $(OBJS_THIRD_PARTY_PROD)
|
||||
|
||||
MAIN_SRC = src/main.c
|
||||
|
||||
# Rebuild when the build CONFIGURATION changes, not only when sources do. The
|
||||
@@ -1161,14 +1379,15 @@ MAIN_SRC = src/main.c
|
||||
# configuration and it is rewritten only when that content changes, so ordinary
|
||||
# incremental builds are unaffected. (Single-quoted: the signature may contain
|
||||
# the escaped quotes of -DCBM_VERSION, which are literal inside '', but must not
|
||||
# contain a literal single quote.)
|
||||
# contain a literal single quote.) The config-flavoured prod objects above
|
||||
# depend on the stamp for the same reason as the binary.
|
||||
# SANITIZE belongs here: it changes the instrumentation compiled into every
|
||||
# object, so a build that only changes it MUST rebuild. Its absence made
|
||||
# `scripts/test.sh SANITIZE= ...` -- the documented way to get a plain build
|
||||
# for trap debugging -- silently re-run the previously instrumented binary,
|
||||
# which during this bisect produced a "crashes without sanitizers too" result
|
||||
# that was pure artifact and sent the investigation down the wrong path.
|
||||
BUILD_CONFIG_SIG := TEST_SEAMS=$(TEST_SEAMS)|CFLAGS_EXTRA=$(CFLAGS_EXTRA)|SANITIZE=$(SANITIZE)|MEMWASTE=$(MEMWASTE)|MEMWASTE_ACCESS=$(MEMWASTE_ACCESS)|MEMWASTE_PROFILE=$(MEMWASTE_PROFILE)
|
||||
BUILD_CONFIG_SIG := TEST_SEAMS=$(TEST_SEAMS)|CFLAGS_EXTRA=$(CFLAGS_EXTRA)|SANITIZE=$(SANITIZE)|MEMWASTE=$(MEMWASTE)|MEMWASTE_ACCESS=$(MEMWASTE_ACCESS)|MEMWASTE_PROFILE=$(MEMWASTE_PROFILE)|TEST_COVERAGE=$(TEST_COVERAGE)
|
||||
.PHONY: build-config-check
|
||||
build-config-check:
|
||||
$(BUILD_DIR)/.build-config: build-config-check | $(BUILD_DIR)
|
||||
@@ -1196,8 +1415,11 @@ PROD_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c,
|
||||
# Embedded asset object files (generated by embed script)
|
||||
EMBED_OBJS = $(wildcard $(BUILD_DIR)/embedded/embed_*.o)
|
||||
|
||||
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: the UI's dev dependencies include
|
||||
# playwright for its local browser proofs; a plain `npm ci` would otherwise
|
||||
# download browsers, which no build or gate needs.
|
||||
frontend:
|
||||
cd graph-ui && npm ci && npm run build
|
||||
cd graph-ui && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci && npm run build
|
||||
|
||||
embed: frontend
|
||||
scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded
|
||||
@@ -1223,6 +1445,7 @@ LLVM_BIN := $(shell brew --prefix llvm 2>/dev/null)/bin
|
||||
CLANG_TIDY ?= $(shell [ -x "$(LLVM_BIN)/clang-tidy" ] && echo "$(LLVM_BIN)/clang-tidy" || echo clang-tidy)
|
||||
CLANG_FORMAT ?= $(shell [ -x "$(LLVM_BIN)/clang-format" ] && echo "$(LLVM_BIN)/clang-format" || echo clang-format)
|
||||
CPPCHECK ?= cppcheck
|
||||
CPPCHECK_JOBS ?= 2
|
||||
|
||||
# macOS SDK sysroot (needed for Homebrew LLVM to find system headers)
|
||||
SYSROOT = $(shell xcrun --show-sdk-path 2>/dev/null)
|
||||
@@ -1247,10 +1470,16 @@ lint-tidy:
|
||||
lint-tidy-diff:
|
||||
@scripts/lint-tidy-diff.sh "$(CLANG_TIDY)" $(LINT_SRCS) -- $(CFLAGS_COMMON) $(SYSROOT_FLAG)
|
||||
|
||||
# cppcheck: complementary analysis (config in .cppcheck)
|
||||
# cppcheck: complementary analysis (config in .cppcheck). A build directory
|
||||
# retains whole-program analysis with parallel workers; each run owns its state.
|
||||
lint-cppcheck:
|
||||
@echo "=== cppcheck ==="
|
||||
@$(CPPCHECK) --enable=warning,style,performance,portability \
|
||||
@set -e; \
|
||||
cppcheck_dir=$$(mktemp -d "$${TMPDIR:-/tmp}/cbm-cppcheck.XXXXXX"); \
|
||||
trap 'status=$$?; rm -rf "$$cppcheck_dir" || :; exit "$$status"' EXIT; \
|
||||
trap 'exit 129' HUP; trap 'exit 130' INT; trap 'exit 143' TERM; \
|
||||
$(CPPCHECK) -j$(CPPCHECK_JOBS) --cppcheck-build-dir="$$cppcheck_dir" \
|
||||
--enable=warning,style,performance,portability \
|
||||
--std=c11 --language=c \
|
||||
--suppressions-list=.cppcheck \
|
||||
--error-exitcode=1 \
|
||||
@@ -1364,6 +1593,84 @@ test-lsan:
|
||||
cd $(CURDIR) && ASAN_OPTIONS="detect_leaks=1:halt_on_error=1" \
|
||||
./build/lsan/test-runner $(LSAN_SUITES)
|
||||
|
||||
# ── Per-test coverage build (test-impact map) ───────────────────
|
||||
# The instrumented runner and product binary (TEST_COVERAGE=1 above), in their
|
||||
# own build directory. Homebrew LLVM rather than Apple clang: the raw profile
|
||||
# format is versioned with the compiler, so llvm-profdata and llvm-cov have to
|
||||
# come from the toolchain that built the binary.
|
||||
# LLVM_PROFILE_FILE=/dev/null CBM_TEST_COVERAGE_DIR=<dir> \
|
||||
# build/cov/test-runner [suite...]
|
||||
# leaves one profile set per test (tests/test_main.c). The runner names every
|
||||
# profile itself, but the profile runtime creates its default file before main
|
||||
# runs: started without LLVM_PROFILE_FILE it leaves an empty default.profraw in
|
||||
# the working directory.
|
||||
# No C suite runs the product binary -- they re-exec the runner itself. The
|
||||
# shell process regressions do, and they find the instrumented one the way they
|
||||
# find any other: CBM_TEST_BINARY=build/cov/codebase-memory-mcp, with
|
||||
# LLVM_PROFILE_FILE set by whoever collects the profiles.
|
||||
COV_LLVM_BIN ?= /opt/homebrew/opt/llvm/bin
|
||||
COV_BUILD_DIR ?= build/cov
|
||||
COV_MAKE = $(MAKE) -f Makefile.cbm CC=$(COV_LLVM_BIN)/clang CXX=$(COV_LLVM_BIN)/clang++ \
|
||||
BUILD_DIR=$(COV_BUILD_DIR) SANITIZE= TEST_COVERAGE=1
|
||||
test-runner-cov:
|
||||
$(COV_MAKE) $(COV_BUILD_DIR)/test-runner
|
||||
cbm-cov:
|
||||
$(COV_MAKE) $(COV_BUILD_DIR)/codebase-memory-mcp TEST_SEAMS=1
|
||||
|
||||
# Genuine LLVM write failures must fail the parent coverage run.
|
||||
test-coverage-flush: test-runner-cov
|
||||
@evidence_dir=$$(mktemp -d "$(COV_BUILD_DIR)/coverage-flush.XXXXXX") && \
|
||||
printf 'Coverage flush evidence: %s/proof\n' "$$evidence_dir" && \
|
||||
python3 tests/test_coverage_flush.py \
|
||||
--runner "$(COV_BUILD_DIR)/test-runner" \
|
||||
--llvm-profdata "$(COV_LLVM_BIN)/llvm-profdata" \
|
||||
--output-dir "$$evidence_dir/proof"
|
||||
|
||||
.PHONY: test-coverage-children
|
||||
# Genuine child profile/marker and setup-reducer controls; evidence is retained.
|
||||
test-coverage-children: test-runner-cov
|
||||
@evidence_dir=$$(mktemp -d "$(COV_BUILD_DIR)/coverage-children.XXXXXX") && \
|
||||
printf 'Coverage child evidence: %s/proof\n' "$$evidence_dir" && \
|
||||
python3 tests/test_coverage_children.py \
|
||||
--runner "$(COV_BUILD_DIR)/test-runner" \
|
||||
--llvm-profdata "$(COV_LLVM_BIN)/llvm-profdata" \
|
||||
--reducer scripts/test-impact/coverage-map.py \
|
||||
--output-dir "$$evidence_dir/proof"
|
||||
|
||||
.PHONY: test-coverage-parent-failure
|
||||
# Positive default plus genuine owned EMFILE; all evidence is retained.
|
||||
test-coverage-parent-failure: test-runner-cov
|
||||
@evidence_dir=$$(mktemp -d "$(COV_BUILD_DIR)/coverage-parent-failure.XXXXXX") && \
|
||||
printf 'Coverage parent failure evidence: %s/proof\n' "$$evidence_dir" && \
|
||||
python3 tests/test_coverage_parent_failure.py \
|
||||
--runner "$(COV_BUILD_DIR)/test-runner" \
|
||||
--llvm-profdata "$(COV_LLVM_BIN)/llvm-profdata" \
|
||||
--reducer scripts/test-impact/coverage-map.py \
|
||||
--output-dir "$$evidence_dir/proof"
|
||||
|
||||
.PHONY: test-coverage-terminal
|
||||
# A readable stale terminal profile cannot hide a failed parent write.
|
||||
test-coverage-terminal: test-runner-cov
|
||||
@evidence_dir=$$(mktemp -d "$(COV_BUILD_DIR)/coverage-terminal.XXXXXX") && \
|
||||
printf 'Coverage terminal evidence: %s/proof\n' "$$evidence_dir" && \
|
||||
python3 tests/test_coverage_terminal.py \
|
||||
--runner "$(COV_BUILD_DIR)/test-runner" \
|
||||
--llvm-profdata "$(COV_LLVM_BIN)/llvm-profdata" \
|
||||
--helper tests/test_coverage_children.py \
|
||||
--reducer scripts/test-impact/coverage-map.py \
|
||||
--output-dir "$$evidence_dir/proof"
|
||||
|
||||
.PHONY: test-coverage-live-v2
|
||||
# Native full-identity transport remains explicitly unadmitted; evidence is retained.
|
||||
test-coverage-live-v2: test-runner-cov
|
||||
@evidence_dir=$$(mktemp -d "$(COV_BUILD_DIR)/coverage-live-v2.XXXXXX") && \
|
||||
printf 'Coverage live-v2 evidence: %s/proof\n' "$$evidence_dir" && \
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 tests/test_coverage_live_v2.py \
|
||||
--collector scripts/test-impact/coverage-map.py \
|
||||
--runner "$(COV_BUILD_DIR)/test-runner" \
|
||||
--llvm-profdata "$(COV_LLVM_BIN)/llvm-profdata" \
|
||||
--output "$$evidence_dir/proof" $(if $(COV_LIVE_V2_CASE),--case "$(COV_LIVE_V2_CASE)")
|
||||
|
||||
# ── Coverage-guided fuzzing (libFuzzer) ─────────────────────────
|
||||
# One binary, three targets (tests/fuzz/cbm_fuzz.c): the per-file extractor,
|
||||
# the Cypher parser, the TOML/YAML/JSON config editors. ASan+UBSan as in the
|
||||
@@ -1379,8 +1686,12 @@ fuzz:
|
||||
$(MAKE) -f Makefile.cbm build/fuzz/cbm-fuzz BUILD_DIR=build/fuzz \
|
||||
CC=$(FUZZ_CC) CXX=$(FUZZ_CXX) SANITIZE="$(FUZZ_SANITIZE)"
|
||||
|
||||
# -fsanitize-link-c++-runtime: the link runs through the C driver, which on
|
||||
# Linux leaves out UBSan's C++ runtime, but the C++ objects (preprocessor,
|
||||
# simplecpp) carry -fsanitize=vptr checks that need it
|
||||
# (__ubsan_vptr_type_cache, __ubsan_handle_dynamic_type_cache_miss).
|
||||
$(BUILD_DIR)/cbm-fuzz: $(FUZZ_SRC) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) $(PROJECT_HDRS) | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS_TEST) -fsanitize=fuzzer -o $@ \
|
||||
$(CC) $(CFLAGS_TEST) -fsanitize=fuzzer -fsanitize-link-c++-runtime -o $@ \
|
||||
$(FUZZ_SRC) $(PROD_SRCS) \
|
||||
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
|
||||
$(OBJS_VENDORED_TEST) \
|
||||
|
||||
@@ -111,7 +111,7 @@ This project implements multiple layers of security verification. Every release
|
||||
- **All dangerous function calls** require a reviewed entry in `scripts/security-allowlist.txt`
|
||||
- **Time-bomb pattern detection** — scans for `time()`/`sleep()` near dangerous calls (could indicate delayed activation)
|
||||
- **MCP tool handler file read audit** — tracks file read count in `mcp.c` against an expected maximum (detects added file reads that could exfiltrate data through tool responses)
|
||||
- **CodeQL SAST** — static application security testing on every push (taint analysis, CWE detection, data flow tracking). Any open alert blocks the release.
|
||||
- **CodeQL SAST** — static application security testing of the C/C++, JavaScript/TypeScript, Python and GitHub Actions code (taint analysis, CWE detection, data flow tracking): a full scan on every push to main, weekly, and in every dry run and release; a first-party scan of the languages a pull request changes, with alerts reported on its changed lines. Any open alert fails the run and blocks the release.
|
||||
- **Fuzz testing** — random/mutated inputs to MCP server and Cypher parser (60 seconds per build). Catches crashes, segfaults, and memory errors that structured tests miss.
|
||||
- **Native antivirus scanning** on every platform (any detection fails the build):
|
||||
- **Windows**: Windows Defender with ML heuristics — the same engine end users run
|
||||
|
||||
@@ -60,15 +60,26 @@ License summary:
|
||||
smart-coin language, written for this project because no usable public
|
||||
grammar exists; its source and corpus tests live in
|
||||
`tools/tree-sitter-chialisp/`.
|
||||
- Seven further grammars (`arkts`, `assembly`, `cfml`,
|
||||
`cfscript`, `dotenv`, `pine`, `qml`) are self-maintained forks that retain
|
||||
their original upstream authors' licenses — see the manifest for per-grammar
|
||||
provenance. `arkts` is a first-party derivative of
|
||||
- Ten further grammars (`arkts`, `assembly`, `cfml`,
|
||||
`cfscript`, `dotenv`, `javascript`, `pascal`, `pine`, `qml`, `tsx`) are
|
||||
self-maintained forks that retain their original upstream authors' licenses —
|
||||
see the manifest for per-grammar provenance. `arkts` is a first-party derivative of
|
||||
[tree-sitter/tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript)
|
||||
(MIT, (c) 2017 Max Brunsfeld; on the
|
||||
[tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript)
|
||||
base, MIT, (c) 2014 Max Brunsfeld) with (c) 2026 DeusData ArkTS additions;
|
||||
its grammar source lives in `tools/tree-sitter-arkts/`.
|
||||
its grammar source lives in `tools/tree-sitter-arkts/`. `javascript`
|
||||
([tree-sitter/tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript),
|
||||
MIT, (c) 2014 Max Brunsfeld) and `tsx`
|
||||
([tree-sitter/tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript)'s
|
||||
`tsx` dialect, MIT, (c) 2017 Max Brunsfeld, on the same javascript base) are
|
||||
the upstream grammars with one (c) 2026 DeusData patch that lets a lone `&`
|
||||
appear in JSX strings and text; their grammar sources live in
|
||||
`tools/tree-sitter-javascript/` and `tools/tree-sitter-tsx/`. `pascal`
|
||||
([Isopod/tree-sitter-pascal](https://github.com/Isopod/tree-sitter-pascal),
|
||||
MIT, (c) 2018 Benjamin Gray) is the upstream grammar with one (c) 2026
|
||||
DeusData patch that accepts non-ASCII letters in identifiers; its grammar
|
||||
source lives in `tools/tree-sitter-pascal/`.
|
||||
|
||||
### tree-sitter-plsql
|
||||
|
||||
@@ -110,10 +121,12 @@ License summary:
|
||||
| wyhash | `internal/cbm/vendored/wyhash/` | Unlicense (public domain) | [wangyi-fudan/wyhash](https://github.com/wangyi-fudan/wyhash) |
|
||||
|
||||
Local modifications to these libraries are documented next to the
|
||||
vendored sources (currently only SQLite: `vendored/sqlite3/PATCHES.md`,
|
||||
raising the Unix VFS `MAX_PATHNAME` ceiling from 512 to 4096 to match
|
||||
CBM's 4 KiB path support). Patches must be reapplied on every upstream
|
||||
refresh and are covered by `scripts/vendored-checksums.txt`.
|
||||
vendored sources: `vendored/sqlite3/PATCHES.md` (raising the Unix VFS
|
||||
`MAX_PATHNAME` ceiling from 512 to 4096 to match CBM's 4 KiB path support)
|
||||
and `vendored/mimalloc/PATCHES.md` (the upstream source pin, mimalloc v3.5.3
|
||||
at commit `d4881d33`, plus the macOS decommit-recommit change and one comment
|
||||
rewording). Patches must be reapplied on every upstream refresh and are
|
||||
covered by `scripts/vendored-checksums.txt`.
|
||||
|
||||
The graph-UI HTTP server is a first-party implementation
|
||||
(`src/ui/httpd.c` + `src/ui/http_server.c`) — no third-party HTTP library
|
||||
@@ -131,6 +144,109 @@ Semantic vector search uses static token embeddings derived from the
|
||||
See `vendored/nomic/NOTICE` for the exact derivation procedure
|
||||
(per-token inference + int8 quantization via `scripts/extract_nomic_vectors.py`).
|
||||
|
||||
## Embedded Character Data (PDF text layer)
|
||||
|
||||
The PDF text-layer extractor (`internal/cbm/pdf/`) carries static tables
|
||||
derived from the following sources. No upstream source code is included.
|
||||
|
||||
- **Adobe Glyph List** — the glyph-name subset in `internal/cbm/pdf/pdf_tables.c`
|
||||
(the names of the standard Latin encodings and common symbols, mapped to
|
||||
Unicode) follows [adobe-type-tools/agl-aglfn](https://github.com/adobe-type-tools/agl-aglfn)
|
||||
`glyphlist.txt`.
|
||||
- **License:** BSD-3-Clause
|
||||
- **Copyright:** Copyright 2002-2019 Adobe (http://www.adobe.com/).
|
||||
- **Adobe core-14 font metrics** — the advance widths of the printable ASCII
|
||||
range of Helvetica and Times-Roman in `pdf_tables.c` (used only for a
|
||||
standard-14 font that omits its `/Widths`) are the values of Adobe's
|
||||
core-14 AFM files. The AFM files themselves are not distributed.
|
||||
- **Copyright:** Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems
|
||||
Incorporated. All Rights Reserved.
|
||||
- **Unicode Character Database** — `internal/cbm/pdf/pdf_unicode.c` (NFKC of
|
||||
each code point, canonical compositions, character classes) is generated
|
||||
by `scripts/gen-pdf-unicode.py` from CPython's `unicodedata` (Unicode
|
||||
16.0.0).
|
||||
- **License:** Unicode License v3 (text below)
|
||||
- **Copyright:** Copyright © 1991-2026 Unicode, Inc.
|
||||
- The base encodings (Standard, WinAnsi, MacRoman, Symbol) are those of
|
||||
ISO 32000-1, Annex D.
|
||||
|
||||
Adobe Glyph List license (BSD-3-Clause):
|
||||
|
||||
```
|
||||
Copyright 2002-2019 Adobe (http://www.adobe.com/).
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
Neither the name of Adobe nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
Unicode License v3:
|
||||
|
||||
```
|
||||
UNICODE LICENSE V3
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright © 1991-2026 Unicode, Inc.
|
||||
|
||||
NOTICE TO USER: Carefully read the following legal agreement. BY
|
||||
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
||||
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
||||
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of data files and any associated documentation (the "Data Files") or
|
||||
software and any associated documentation (the "Software") to deal in the
|
||||
Data Files or Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
||||
copies of the Data Files or Software, and to permit persons to whom the
|
||||
Data Files or Software are furnished to do so, provided that either (a)
|
||||
this copyright and permission notice appear with all copies of the Data
|
||||
Files or Software, or (b) this copyright and permission notice appear in
|
||||
associated Documentation.
|
||||
|
||||
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
THIRD PARTY RIGHTS.
|
||||
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
||||
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
||||
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
||||
FILES OR SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in these Data Files or Software without prior written
|
||||
authorization of the copyright holder.
|
||||
```
|
||||
|
||||
## Hybrid LSP — Reference Language Servers
|
||||
|
||||
The Hybrid LSP layer (`internal/cbm/lsp/`) is an original C implementation
|
||||
|
||||
@@ -79,6 +79,7 @@ codebase-memory-mcp config set auto_index true
|
||||
codebase-memory-mcp config set auto_index_limit 50000
|
||||
codebase-memory-mcp config set watcher_enabled false
|
||||
codebase-memory-mcp config reset auto_index
|
||||
codebase-memory-mcp config get install_hooks
|
||||
```
|
||||
|
||||
Current keys:
|
||||
@@ -87,22 +88,44 @@ Current keys:
|
||||
|---|---|---|
|
||||
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
|
||||
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
|
||||
| `auto_watch` | `true` | Register the session's project with the background git watcher on connect. Set `false` to keep a session from registering its project (the watcher still runs for other projects). |
|
||||
| `watcher_enabled` | `true` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. |
|
||||
| `ignore_worktrees` | `false` | Skip linked git worktrees (`git worktree add`) when indexing automatically. |
|
||||
| `auto_watch` | `true` | Global switch for MCP session watch registration. On the next refresh or registration request, `true` allows session roots and successful `index_repository` targets to be watched; `false` removes all watches. The background watcher can still run with no registered projects. |
|
||||
| `doc_candidates` | `auto` | Score documentation sections against code to store doc↔code candidates (the possible targets of a section, and the sections that may describe a function or file, each with a judged probability). Unset (`auto`), the index mode decides: on for `full` indexes (the default mode), off for `moderate` and `fast`. Set `true` or `false` to choose for every mode; `config reset doc_candidates` returns to `auto`. The scoring grows with sections × functions: under a second up to ~100k functions, ~15 s at ~400k, ~3 minutes on the Linux kernel. Off stores no candidates and leaves the rest of the graph unchanged; on in `fast` mode stores candidates without adding `fast`'s skipped `SIMILAR_TO` / `SEMANTICALLY_RELATED` edges. `index_repository` also takes `doc_candidates` for a single index, which wins over this setting. Applies from the next index. |
|
||||
| `watcher_enabled` | `true` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. Active MCP sessions can register their root project and successful explicit index targets; under `daemon start` those watches outlive the session until the daemon stops, the project is deleted, its directory disappears, or `auto_watch` is set to `false`. `index_status` reports the current state in its `watch` object. |
|
||||
| `watch_non_git` | `false` | Also poll project roots that are **not git repositories**. By default the watcher only follows git projects, so a project indexed from a plain directory is never refreshed after its first index — reindex it manually with `index_repository`. Set `true` to poll such roots on the same adaptive cadence with a file-tree scan: the indexer's own discovery walk (same skip lists, `.gitignore` and `.cbmignore` rules) hashed over each file's path, size and mtime. Any change reindexes once; paths the indexer skips (including cbm's own `.codebase-memory/` output) never trigger. The first poll after the daemon starts reindexes each such project once, since nothing records which tree state the index holds. The scan walks the whole tree every poll, so it costs more than git polling on very large trees. Read once when the daemon starts, like `watcher_enabled`. |
|
||||
| `index_max_files` | `off` | Optional maximum number of accepted source files in one discovery run. |
|
||||
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |
|
||||
| `index_max_rss_mb` | `off` | Optional maximum charged memory in MiB for the complete contained index-worker process tree (`64..1048576`). On macOS this is phys_footprint, not resident size. |
|
||||
| `index_max_duration_seconds` | `off` | Optional maximum wall-clock duration in seconds for the whole index request, including crash/hang recovery (`1..86400`). Like `index_max_rss_mb`, it stops a worker only before it starts to publish the new index; see [INDEX_RESOURCE_LIMITS.md](INDEX_RESOURCE_LIMITS.md). |
|
||||
| `install_hooks` | `true` | Install CBM-managed client hooks. `install --no-hooks` persists `false`; `install --hooks` restores `true`. |
|
||||
|
||||
Bare installs and updates respect the stored hook preference. MCP registration, skills, instructions, and profiles remain installed when hooks are disabled.
|
||||
|
||||
For a watched project, `index_status` names the polling method in `watch.strategy`:
|
||||
`git` (HEAD and dirty-state polling), `tree` (a non-git root polled by the
|
||||
`watch_non_git` file-tree scan), `none` (a non-git root that is registered but never
|
||||
polled, the default for a plain directory) or `pending` (registered; the first poll
|
||||
has not run yet).
|
||||
|
||||
> **`watcher_enabled` vs `auto_watch`.** `watcher_enabled` controls whether the
|
||||
> watcher *subsystem* starts at all (the background poll thread). `auto_watch` is
|
||||
> narrower: it only controls whether a connecting session registers *its own*
|
||||
> project with an already-running watcher. When `watcher_enabled=false`,
|
||||
> narrower: it globally controls MCP session watch registration. On the next
|
||||
> refresh or registration request, `false` removes all watches; `true` allows
|
||||
> session roots and successful explicit `index_repository` targets to register.
|
||||
> Watcher refreshes retain `mode`, `persistence`, and the resolved
|
||||
> `doc_candidates` boolean from the latest successful explicit index registered
|
||||
> for that target. This boolean can come from the request or its global setting.
|
||||
> When neither supplied a boolean, refresh uses the current setting or mode default.
|
||||
> A watcher callback also observes `auto_watch=false`: it removes all watches
|
||||
> before admitting any new refresh work.
|
||||
> When `watcher_enabled=false`,
|
||||
> `auto_watch` has no effect — there is no watcher to register with.
|
||||
>
|
||||
> They also differ in **when they are read**, which matters because the watcher
|
||||
> lives in the background daemon, not in your MCP client:
|
||||
>
|
||||
> - `auto_watch` is consulted each time a session would register its project, so
|
||||
> a change applies to sessions that connect afterwards.
|
||||
> - `auto_watch` takes effect on the next refresh or registration request and
|
||||
> applies globally to session roots and successful `index_repository` targets.
|
||||
> - `watcher_enabled` is read **once, when the daemon starts**, because it decides
|
||||
> whether the watcher is built at all. The daemon is long-lived and outlives
|
||||
> individual MCP sessions, so **reconnecting your client is not enough** — retire
|
||||
@@ -118,14 +141,54 @@ Current keys:
|
||||
> `auto_index` still runs, and `index_repository` stays available for manual
|
||||
> reindexing.
|
||||
|
||||
The two `index_max_*` settings are independent and disabled by default. They
|
||||
The four `index_max_*` settings are independent and disabled by default. They
|
||||
apply to explicit indexing, automatic indexing, and watcher re-indexing, but not
|
||||
to `cross-repo-intelligence`, which does not scan repository source files.
|
||||
Equality is allowed; exceeding either setting fails the complete index request
|
||||
and preserves any previously serving database. See
|
||||
Equality is allowed; exceeding any setting fails the complete index request and
|
||||
preserves any previously serving database. Worker RSS covers descendants and is
|
||||
not the same as the internal `CBM_MEM_BUDGET_MB` allocation budget. Total
|
||||
duration is independent of the existing 15-minute no-log-progress timeout. See
|
||||
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
|
||||
error-response details.
|
||||
|
||||
### `ignore_worktrees`
|
||||
|
||||
Every indexed project is registered under its own absolute root path, so each
|
||||
linked worktree becomes a separate permanent index. On machines that create many
|
||||
short-lived worktrees, the automatic paths (`auto_index`, and the session hook's
|
||||
"index this project first" guidance) turn every throwaway checkout into another
|
||||
stored index of what is largely the same repository.
|
||||
|
||||
Enable the key to keep those checkouts out of the index:
|
||||
|
||||
```bash
|
||||
codebase-memory-mcp config set ignore_worktrees true
|
||||
```
|
||||
|
||||
With it enabled:
|
||||
|
||||
- automatic indexing skips a session whose root is a linked worktree;
|
||||
- the `hook-augment` context says the worktree is unindexed on purpose instead
|
||||
of telling the agent to run `index_repository`;
|
||||
- an explicit `index_repository` call on a linked worktree is refused, and names
|
||||
both ways forward — pass `index_worktree=true` for that one call, or turn the
|
||||
key back off.
|
||||
|
||||
The main checkout of the same repository is unaffected, as are ordinary clones
|
||||
and submodules. Detection is git plumbing only: a linked worktree's `.git` is a
|
||||
file pointing at a gitdir that contains a `commondir` entry.
|
||||
|
||||
A subdirectory counts as the worktree it sits in. Detection walks up from the
|
||||
given path to the nearest `.git` entry, so `index_repository` on any
|
||||
subdirectory of a linked worktree is refused the same way, and a session root or
|
||||
hook working directory inside one is treated as that worktree. The walk stops at
|
||||
the first `.git` it finds; a main checkout or a submodule ends it as "not a
|
||||
worktree". A path outside any git checkout is checked one directory at a time up
|
||||
to the filesystem root and is never treated as a worktree. None of this runs
|
||||
while the key is off.
|
||||
|
||||
The default is `false`, so indexing behavior is unchanged unless you opt in.
|
||||
|
||||
## 3. UI Settings
|
||||
|
||||
The optional built-in graph UI stores its settings in:
|
||||
@@ -146,6 +209,8 @@ Current format:
|
||||
Notes:
|
||||
|
||||
- If a UI-enabled binary finds its verified external asset pack and no UI config file exists yet, the UI auto-enables on first run. Missing or invalid assets leave the MCP/daemon service available and keep the UI disabled.
|
||||
- A bare `daemon start` honors the saved `ui_enabled` value, including `false`. Enable the UI with `codebase-memory-mcp config set ui_enabled true`.
|
||||
- On a cold start, `daemon start --open` or `daemon start --port=N` explicitly enables the UI and persists that choice. An already-active daemon keeps its current settings; stop it first to change ports with `--port=N`.
|
||||
- `CBM_CACHE_DIR` changes both the UI config location and the runtime settings database location.
|
||||
- CBM resolves `CBM_CACHE_DIR` to one canonical per-account cache root. A process configured with a different root fails while any CBM session or command is active; close them before switching roots.
|
||||
|
||||
@@ -200,7 +265,21 @@ process that should share one daemon must see the same value — set it in the
|
||||
environment of your MCP client and your shell alike, or a CLI invocation without
|
||||
it will coordinate through the default location instead.
|
||||
|
||||
Environment used by daemon-owned components—such as diagnostics, daemon logging, and process-wide indexing resource limits—is captured from the first daemon-backed session that starts the daemon. Later sessions join the existing process and cannot replace those values. To change them, close every daemon-backed session, update the relevant agent configurations consistently, and restart a session. `CBM_ALLOWED_ROOT` remains session-specific, a conflicting `CBM_CACHE_DIR` is rejected, and one-shot CLI commands use their own current environment without starting the daemon.
|
||||
**WSL2 and Windows drives (`/mnt/c`, `/mnt/e`, ...).** `CBM_CACHE_DIR` goes through
|
||||
the same private-directory check. With WSL's default automount options, DrvFs
|
||||
reports every directory as `0777` and ignores `chmod`, so a cache under
|
||||
`/mnt/<drive>` is refused, and the refusal names this remedy. Either turn on
|
||||
permission metadata in `/etc/wsl.conf`, then run `wsl --shutdown` and reopen WSL:
|
||||
|
||||
```ini
|
||||
[automount]
|
||||
options = "metadata,umask=22,fmask=11"
|
||||
```
|
||||
|
||||
or keep the cache on the Linux filesystem (the default `~/.cache/codebase-memory-mcp`),
|
||||
which is also much faster than a 9p-mounted Windows drive.
|
||||
|
||||
Environment used by daemon-owned components—such as diagnostics, daemon logging, and process-wide indexing resource limits—is captured from the first daemon-backed session that starts the daemon. Later sessions join the existing process and cannot replace those values. To change them, close every daemon-backed session, update the relevant agent configurations consistently, and restart a session. `CBM_ALLOWED_ROOT` remains session-specific, a conflicting `CBM_CACHE_DIR` is rejected, and a one-shot CLI command is not exempt from the rule above: it connects to the coordination daemon like any other session, starting one if none is running, so its own environment becomes the captured daemon-owned environment only when its invocation is the one that starts the daemon — joining an already-running daemon, it inherits that daemon's already-captured values instead.
|
||||
|
||||
|
||||
### Roots that are always refused
|
||||
@@ -215,12 +294,48 @@ root because they are too broad or too sensitive to index as a unit:
|
||||
- a credential directory at any depth — `.ssh`, `.aws`, `.gnupg`, `.kube`,
|
||||
`.docker`, `.netrc`, `.git-credentials`, `.password-store`, macOS `Keychains`.
|
||||
|
||||
A single top-level directory can still be indexed as one root on purpose, for
|
||||
example a container or build machine that keeps its project in `/work`, `/app` or
|
||||
`/src`, by approving that exact path in a terminal:
|
||||
`codebase-memory-mcp allow-root --approve-broad /work`. The approval is recorded
|
||||
like any other allowed root, listed as `(approved broad)` by `allow-root --list`,
|
||||
and covers no other top-level directory; as with any recorded root, indexing is
|
||||
then confined to the recorded roots. Filesystem, drive and share roots cannot be
|
||||
approved.
|
||||
|
||||
Two limits are worth stating plainly. This constrains *scope*, not
|
||||
*sensitivity*: inside a root that is allowed, every file the process can read may
|
||||
be indexed and later returned. And the credential list is a denylist, so it
|
||||
raises the cost of a mistake rather than closing the class — a directory it does
|
||||
not name is permitted.
|
||||
|
||||
### Shared / HPC filesystems
|
||||
|
||||
On clusters and other shared machines, home directories often live on a network
|
||||
filesystem (NFS, Lustre, GPFS, SMB/CIFS), and `~/.cache` is sometimes a symlink
|
||||
into a shared tree. Three rules keep CBM working there:
|
||||
|
||||
- **Put the cache and the rendezvous on local storage.** Set `CBM_CACHE_DIR` (indexes,
|
||||
`_config.db`) and, if needed, `CBM_RUNTIME_DIR` (the daemon rendezvous) to a
|
||||
local disk you own, for example `/local/scratch/$USER/cbm` or `/tmp/$USER-cbm`.
|
||||
Set the same values in your MCP client's environment and your shell.
|
||||
- **Do not route the cache into another account's tree.** A symlinked `~/.cache` is
|
||||
resolved and allowed, but every directory on the *resolved* path must be owned
|
||||
by you or by root. If a parent such as `/shared/fs` belongs to another account,
|
||||
that account can rename or replace anything below it, so CBM refuses the path
|
||||
and names the directory and its owner:
|
||||
|
||||
```text
|
||||
... (cache-private) - '/shared/fs' is owned by uid 1234 (alice), not by you (uid 1000) or root; ...
|
||||
```
|
||||
|
||||
This check has no override. Choose a location with a safe ancestry instead.
|
||||
- **Avoid network filesystems for the cache.** The indexes are SQLite databases in
|
||||
WAL mode, which needs shared-memory locking that network filesystems do not
|
||||
provide reliably. On Linux, CBM logs one `daemon.cache_root_network_fs` warning
|
||||
at startup when the cache root is on NFS, Lustre, GPFS, CIFS or SMB2. The cache
|
||||
still works there, but it can be slow, and a database can be corrupted.
|
||||
|
||||
## 5. Agent and Editor Integration Files
|
||||
|
||||
The `install` command can also write MCP entries and instruction blocks into agent/editor config files such as Claude Code, Codex, Gemini, VS Code, Cursor, Zed, and others.
|
||||
|
||||
@@ -274,8 +274,7 @@ then its index is **deleted** before the next language — so **only one project
|
||||
every index is genuinely cold. The skeleton:
|
||||
|
||||
```bash
|
||||
# Start clean: no leftover DBs, fresh results tree
|
||||
rm -f ~/.cache/codebase-memory-mcp/*.db
|
||||
# Start clean: fresh results tree
|
||||
mkdir -p /tmp/eval-results
|
||||
|
||||
for lang in $ALL_LANGS; do # ALL_LANGS = full 159-name list (§2 / §8)
|
||||
@@ -283,8 +282,13 @@ for lang in $ALL_LANGS; do # ALL_LANGS = full 159-name list
|
||||
|
||||
# --- step 2: cold index in the main channel, TIMED (key metric) ---
|
||||
t0=$(now_ms)
|
||||
CBM_BENCH_KEEP_RUNTIME=1 \
|
||||
scripts/benchmark-index.sh ~/.local/bin/codebase-memory-mcp "$lang" /tmp/bench/"$lang" /tmp/eval-results
|
||||
index_ms=$(( $(now_ms) - t0 )) # clone+index wall-clock → manifest + report (§5)
|
||||
# The harness indexes into a run-private root (#1696). CBM_BENCH_KEEP_RUNTIME leaves that
|
||||
# root behind on success and records its paths; the graph session in steps 4-7 must be
|
||||
# started with these CBM_RUNTIME_DIR / CBM_CACHE_DIR, and step 8 removes the root.
|
||||
set -a; . /tmp/eval-results/"$lang"/runtime-root.txt; set +a
|
||||
|
||||
# --- step 3: record per-type histograms (zeros back-filled) ---
|
||||
# node-types.json, edge-types.json (every label + all 32 edge types, zeros kept, §7 below)
|
||||
@@ -293,7 +297,14 @@ for lang in $ALL_LANGS; do # ALL_LANGS = full 159-name list
|
||||
# + per-language report + (deferred, blind) judge ---
|
||||
|
||||
# --- step 8: delete THIS language's index so the next is cold, then mark done ---
|
||||
rm -f ~/.cache/codebase-memory-mcp/*.db
|
||||
# The graph session (steps 4-7) may still have a daemon running under the kept
|
||||
# CBM_RUNTIME_DIR (exported above); stop it before its root goes away. A refused
|
||||
# stop (a session still attached) means the root is still in use: keep it.
|
||||
if ~/.local/bin/codebase-memory-mcp daemon stop; then
|
||||
rm -rf -- "$CBM_BENCH_RUNTIME_ROOT"
|
||||
else
|
||||
echo "$lang: daemon under $CBM_RUNTIME_DIR did not stop; keeping $CBM_BENCH_RUNTIME_ROOT" >&2
|
||||
fi
|
||||
manifest_mark_done "$lang" "$index_ms"
|
||||
done
|
||||
```
|
||||
@@ -311,15 +322,16 @@ SHA, and — **not just totals** — a **per-type breakdown**:
|
||||
- `node-types.json` — a histogram of **node count by label** (`Function`, `Method`, `Class`,
|
||||
`Interface`, `Type`/`Enum`/`Struct`, `Field`, `Variable`, `Route`, `Module`, `Section`, `Macro`,
|
||||
`File`, `Folder`) + total.
|
||||
- `edge-types.json` — a histogram of **edge count by type, with one entry for every one of the 32
|
||||
- `edge-types.json` — a histogram of **edge count by type, with one entry for every one of the 33
|
||||
edge types, including those that came back `0`**. The canonical set is the indexer's own
|
||||
`ALL_EDGE_TYPES[]` (26 intra-repo types — `tests/test_lang_contract.c`): `CALLS`, `ASYNC_CALLS`,
|
||||
`ALL_EDGE_TYPES[]` (27 intra-repo types — `tests/test_lang_contract.c`): `CALLS`, `ASYNC_CALLS`,
|
||||
`HTTP_CALLS`, `GRPC_CALLS`, `GRAPHQL_CALLS`, `TRPC_CALLS`, `DEFINES`, `DEFINES_METHOD`, `IMPLEMENTS`,
|
||||
`INHERITS`, `OVERRIDE`, `DECORATES`, `IMPORTS`, `HANDLES`, `CONFIGURES`, `DEPENDS_ON`, `USAGE`,
|
||||
`DATA_FLOWS`, `SEMANTICALLY_RELATED`, `SIMILAR_TO`, `TESTS`, `TESTS_FILE`, `INFRA_MAPS`,
|
||||
`FILE_CHANGES_WITH`, `CONTAINS_FILE`, `CONTAINS_FOLDER` — **plus the 6 cross-repo types** from the
|
||||
`FILE_CHANGES_WITH`, `CONTAINS_FILE`, `CONTAINS_FOLDER`, `MENTIONS` (doc comment -> referenced
|
||||
code) — **plus the 6 cross-repo types** from the
|
||||
cross-repo pass (`CROSS_HTTP_CALLS`, `CROSS_ASYNC_CALLS`, `CROSS_GRPC_CALLS`, `CROSS_GRAPHQL_CALLS`,
|
||||
`CROSS_TRPC_CALLS`, `CROSS_CHANNEL`) and a total. The writer **emits the full 32-type list and
|
||||
`CROSS_TRPC_CALLS`, `CROSS_CHANNEL`) and a total. The writer **emits the full 33-type list and
|
||||
back-fills missing types with `0`** rather than recording only the types that appeared.
|
||||
|
||||
These come straight from `query_graph` (`MATCH (n) RETURN labels(n), count(*)` and
|
||||
@@ -859,10 +871,13 @@ Deep-Dive section.
|
||||
# 1. Clone all 159 repos (shallow; skip existing)
|
||||
scripts/clone-bench-repos.sh /tmp/bench
|
||||
|
||||
# 2. Cold index all 159 (LSP cohort in full mode)
|
||||
rm -f ~/.cache/codebase-memory-mcp/*.db
|
||||
# 2. Cold index all 159 (LSP cohort in full mode). Each language is indexed into a run-private
|
||||
# runtime and cache (#1696); CBM_BENCH_KEEP_RUNTIME=1 keeps that root after a successful
|
||||
# index and records its paths in /tmp/eval-results/<lang>/runtime-root.txt. Source it
|
||||
# before the graph phase, then stop the daemon and remove the root, as in §7 step 8.
|
||||
mkdir -p /tmp/eval-results
|
||||
for lang in $ALL_LANGS; do
|
||||
CBM_BENCH_KEEP_RUNTIME=1 \
|
||||
scripts/benchmark-index.sh ~/.local/bin/codebase-memory-mcp "$lang" /tmp/bench/"$lang" /tmp/eval-results
|
||||
done
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Index resource limits
|
||||
|
||||
Index resource limits are optional operator controls for repositories whose
|
||||
discovery breadth is not known in advance. They are disabled by default so
|
||||
existing large-repository workloads retain their current behavior.
|
||||
discovery breadth or worker runtime is not known in advance. They are disabled
|
||||
by default so existing large-repository workloads retain their current behavior.
|
||||
|
||||
## Discovery settings
|
||||
|
||||
@@ -23,6 +23,61 @@ Values use base-10 integers. MiB means 1,048,576 bytes. Empty values, zero,
|
||||
negative values, suffixes, trailing characters, and values outside the stated
|
||||
ranges are rejected without changing the stored value.
|
||||
|
||||
## Worker settings
|
||||
|
||||
| Key | Default | Accepted value | Protects |
|
||||
|---|---:|---:|---|
|
||||
| `index_max_rss_mb` | `off` | `off` or `64..1048576` | Charged memory of the complete worker process tree |
|
||||
| `index_max_duration_seconds` | `off` | `off` or `1..86400` | Wall-clock duration of the whole index request |
|
||||
|
||||
```bash
|
||||
codebase-memory-mcp config set index_max_rss_mb 8192
|
||||
codebase-memory-mcp config set index_max_duration_seconds 3600
|
||||
```
|
||||
|
||||
RSS is the charged memory of the contained worker and every descendant, not
|
||||
the worker's allocation budget and not peak memory. On macOS that quantity is
|
||||
`phys_footprint` (the same number `cbm_mem_charged()` enforces), not
|
||||
`resident_size`, which still counts pages the allocator has already handed
|
||||
back. Linux and Windows use RSS / working set. This hard watchdog is
|
||||
separate from the internal `CBM_MEM_BUDGET_MB` soft budget. The supervisor
|
||||
samples the tree at most once every 250 milliseconds so the watchdog does not
|
||||
turn full process-table enumeration into a busy loop.
|
||||
|
||||
Duration is per request: the clock starts at the first worker spawn and is
|
||||
not reset when crash/hang recovery starts a later attempt. Continuous log
|
||||
progress does not reset it. It is independent of the existing 15-minute quiet
|
||||
timeout, which still identifies a worker that stops making progress. A
|
||||
duration limit shorter than that quiet timeout kills a hung worker before hang
|
||||
quarantine can name the in-flight file, so the next attempt may hang on the
|
||||
same file.
|
||||
|
||||
Equality is allowed. The first RSS or elapsed-duration observation above its
|
||||
limit starts the existing graceful-to-force process-tree shutdown. CBM reports
|
||||
terminal only after the tree is quiescent or a bounded containment failure is
|
||||
explicitly surfaced. Resource termination is not retried and does not
|
||||
quarantine a source file.
|
||||
|
||||
A worker limit never stops a worker after it has started to publish. The
|
||||
supervisor enforces the limits, but only the worker replaces the serving
|
||||
database, so the two coordinate through a per-attempt publication claim that
|
||||
exactly one of them can win. The worker claims it immediately before it
|
||||
replaces the serving database; the supervisor claims it before it stops the
|
||||
worker for a limit. If the limit is reached first, the worker can no longer
|
||||
publish and the previous index stays untouched. If the worker claimed first,
|
||||
the limits no longer apply to that attempt: the new index is published and the
|
||||
rest of the attempt, including the persistence artifact export, runs to
|
||||
completion. The supervisor then logs `index.supervisor.resource_limit_after_publication`
|
||||
instead of stopping it. With both worker limits `off` there is no claim.
|
||||
|
||||
If RSS is enabled and three consecutive probes cannot obtain any trustworthy
|
||||
tree measurement while the root worker is still running, CBM fails closed with
|
||||
`code=resource_probe_failed`. A probe is untrustworthy as soon as one member of
|
||||
the tree cannot be measured: only a process that exits between the listing and
|
||||
its measurement is skipped, because a total without a live member would
|
||||
under-report. On Linux the probe reads every `/proc/<pid>/stat` to find the
|
||||
tree's members, so a `/proc` mounted with `hidepid=1` makes it fail closed.
|
||||
|
||||
## Counting and failure semantics
|
||||
|
||||
`index_max_files` counts a file only after it passes directory pruning, ignore
|
||||
@@ -55,6 +110,15 @@ The previous database remains available because publication occurs only after a
|
||||
complete discovery and successful staged build. If no previous database exists,
|
||||
`serving_index_preserved` is false.
|
||||
|
||||
Worker limit failures use the same shape with `stage=worker`,
|
||||
`resource=rss_bytes` and `unit=bytes`, or `resource=duration_ms` and
|
||||
`unit=milliseconds`. RSS measurement failures use `code=resource_probe_failed`
|
||||
and omit `observed`, `limit`, and `unit` because no trustworthy observation was
|
||||
available. For a worker failure `serving_index_preserved` describes the whole
|
||||
request: it is true only when a servable index existed when the request began,
|
||||
no attempt of the request (including an earlier one that crashed and was
|
||||
recovered) claimed publication, and that index still serves.
|
||||
|
||||
## Trust and compatibility
|
||||
|
||||
Limits are read from the CLI-managed `_config.db`; they are not MCP request
|
||||
@@ -64,5 +128,6 @@ parent policy.
|
||||
|
||||
These settings do not replace or increase `auto_index_limit`, change the 512 MiB
|
||||
single-file cap, alter workspace-root authorization, or affect
|
||||
`cross-repo-intelligence`. With both settings `off`, discovery follows the
|
||||
existing unbounded path.
|
||||
`cross-repo-intelligence`. With all settings `off`, discovery follows the
|
||||
existing path and the supervisor performs no periodic RSS probe or total-duration
|
||||
termination.
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
# Smart CI: pull-request test selection on GitHub Actions
|
||||
|
||||
Smart CI runs, for each pull request, only the CI jobs and tests that the
|
||||
change can reach. This page explains how this repository does it and how
|
||||
another repository on GitHub Actions could do the same with the released
|
||||
`codebase-memory-mcp` binary.
|
||||
|
||||
Status: experimental. Every answer carries `"experimental": true`. The
|
||||
selection narrows real test runs only while the repository variable
|
||||
`TEST_IMPACT_MODE` is `gate`; otherwise it runs in report-only (shadow) mode.
|
||||
|
||||
> **Read the scope section before you start.** The lane layer is a pattern
|
||||
> any repository can copy. The test layer narrows only C/C++ code whose tests
|
||||
> follow this repository's test-framework conventions (or declared
|
||||
> equivalents), and only with a per-test coverage map that you must produce
|
||||
> yourself. In any other repository the engine answers "run everything". That
|
||||
> is safe, but it saves nothing.
|
||||
|
||||
## The guarantee
|
||||
|
||||
Smart CI never runs fewer tests unless it can show that the omitted tests
|
||||
cannot be affected. When it is unsure, it runs everything:
|
||||
|
||||
- a path that no rule knows, an empty or truncated file list, or a change to
|
||||
the selection's own configuration, tooling or workflows;
|
||||
- a shallow clone, a diff that cannot be read in full, or an invalid config;
|
||||
- no coverage map, or a coverage map that is not admitted (wrong commit, more
|
||||
than 7 days old, made for another platform or harness);
|
||||
- a change that reaches the test runner's `main`, or a test-runner role
|
||||
(below) that no test names;
|
||||
- a role table entry the engine cannot read, or a macro naming a role's
|
||||
literal that it cannot follow;
|
||||
- any failure of the selection job itself.
|
||||
|
||||
Each of these makes the run cost more CI minutes. None of them loses coverage.
|
||||
|
||||
## Two layers
|
||||
|
||||
```
|
||||
changed files ──► Layer 1: lane selector ──► which CI jobs run
|
||||
(scripts/ci/select-lanes.sh, your own rules)
|
||||
│
|
||||
▼ (the Linux x86_64 test leg only, in this repo)
|
||||
merge base + PR ─► Layer 2: test-impact engine ─► which suites / tests run
|
||||
(codebase-memory-mcp test-impact select
|
||||
+ team-artifact bundle from main)
|
||||
```
|
||||
|
||||
| | Layer 1: lanes | Layer 2: tests |
|
||||
|---|---|---|
|
||||
| Question | Which jobs (lint, test legs, smoke, sanitizers) does this PR need? | Inside a test job, which suites and tests? |
|
||||
| Input | The PR's changed-file list | Git history, the code graph, a per-test coverage map |
|
||||
| Implementation | `scripts/ci/select-lanes.sh` (repository-specific rules) | The binary's `test-impact select` subcommand |
|
||||
| Unknown input | Every lane runs | Every test runs (`run_all` with reasons) |
|
||||
| Portable? | As a pattern: you write your own rules | C/C++ with supported test conventions only (see below) |
|
||||
|
||||
## Can your repository use it?
|
||||
|
||||
| Piece | General? | What you provide |
|
||||
|---|---|---|
|
||||
| Lane selection (Layer 1) | Pattern only | Your own path → category → lanes rules. `select-lanes.sh` encodes this repository's paths. |
|
||||
| Engine (`test-impact select`) | Ships in the binary | Nothing to build. |
|
||||
| Source languages it can narrow | **C family only**: `.c .h .cc .cpp .hpp .m .mm .cxx .hh` | Every other changed file must match a config rule (`ignore`, `referencing_tests`, ...). Otherwise the answer is `run_all` (`UNMAPPED_FILE`). |
|
||||
| Test model | **C family only** | The `c-cbm` preset (this repository's `TEST`/`SUITE`/`RUN_TEST`/`RUN_SELECTED_SUITE` macros, see `tests/test_framework.h`), or custom `tests.conventions` records for equivalent C macros. |
|
||||
| Coverage map | **Required to narrow at all** | Format-1 files (`functions.tsv`, `tests.tsv`, `meta.json`). This repository produces them with an LLVM-instrumented runner and `scripts/test-impact/coverage-map.py`, which are both specific to its harness. |
|
||||
| Runner filter | Pattern | A test runner that can run a `suite` / `suite:test` list and fails when a listed test does not exist. |
|
||||
|
||||
The other presets (`pytest`, `go`, `junit`, `gtest`, `rust`, `jest`) are
|
||||
accepted switches, but the engine does not model them. An enabled `gtest`
|
||||
blocks narrowing for C++ files. Custom `tests.conventions` are covered by the
|
||||
model's unit tests. The engine's end-to-end tests use `c-cbm` only. Treat
|
||||
custom conventions as unproven until you have shadow evidence.
|
||||
|
||||
Without admitted coverage the engine marks every suite `whole`, and the lane
|
||||
reports `run_all`. Static reachability alone never omits a suite.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **The binary, pinned.** Use the same release in the producer and the PR
|
||||
jobs. Download that release's Linux archive and verify it against the
|
||||
release's `checksums.txt`, as this repository's shadow job does
|
||||
(`scripts/ci/test-impact-shadow.sh --fetch-release`):
|
||||
|
||||
```bash
|
||||
CBM_VERSION=vX.Y.Z # a release that contains the features you use (see below)
|
||||
asset=codebase-memory-mcp-linux-amd64.tar.gz
|
||||
gh release download "$CBM_VERSION" --repo DeusData/codebase-memory-mcp \
|
||||
--pattern "$asset" --pattern checksums.txt --dir "$RUNNER_TEMP/cbm"
|
||||
(cd "$RUNNER_TEMP/cbm" && grep " $asset\$" checksums.txt | sha256sum -c - &&
|
||||
tar -xzf "$asset" codebase-memory-mcp)
|
||||
"$RUNNER_TEMP/cbm/codebase-memory-mcp" --version
|
||||
```
|
||||
|
||||
For a quick start, `install.sh` installs the latest release with the same
|
||||
checks (HTTPS only, SHA-256 against `checksums.txt`, the archive's member
|
||||
list, `--version`); `--skip-config` skips agent configuration, which a CI
|
||||
runner does not need. It has no version flag, so it is not a pin.
|
||||
|
||||
**Version note.** `v0.12.0` and `v0.12.1` have `test-impact select`/`publish`.
|
||||
They do not have `key: "path"`, `unreferenced`, `--vetoed-out` or
|
||||
`run_all_rules`; those ship in a later release. With either version, a config
|
||||
that uses those keys is invalid, and every answer is `run_all`. Until that
|
||||
release, build the binary from source (`make -f Makefile.cbm cbm`), as this
|
||||
repository's producer and `select-tests` jobs do.
|
||||
|
||||
2. **Full git history.** Check out with `fetch-depth: 0` in every job that
|
||||
runs the engine. The engine verifies the history it diffs and refuses a
|
||||
shallow clone (`GIT_UNAVAILABLE`, everything runs).
|
||||
|
||||
3. **A runner that runs a named subset.** See "The test job" below.
|
||||
|
||||
4. **A coverage producer on the default branch** that writes the format-1 map
|
||||
for every test, child processes included. A test whose coverage is
|
||||
uncertain must be marked `incomplete`. An incomplete test is always
|
||||
selected.
|
||||
|
||||
5. **`gh`, `python3`, and `GH_TOKEN` with `actions: read`** in the PR job, to
|
||||
fetch the bundle. GitHub-hosted runners include `gh` and `python3`.
|
||||
|
||||
## Recipe
|
||||
|
||||
Copy these files from this repository and adapt the marked lines:
|
||||
|
||||
| File | Adapt |
|
||||
|---|---|
|
||||
| `scripts/ci/test-impact-bundle.sh` | `WORKFLOW=` (producer file name) and the hard-coded `main` branch filter. |
|
||||
| `scripts/ci/test-impact-select.sh` | `--lane unit` (your lane name) and the `PLATFORM` default (must equal the producer's). |
|
||||
| `scripts/test-impact/selection.py` | Nothing, if your runner accepts `suite` / `suite:test` lines. |
|
||||
|
||||
### (a) Producer: publish a bundle for every commit on main
|
||||
|
||||
The bundle holds the commit's frozen graph, its coverage map and a
|
||||
`receipt.json` that binds both by SHA-256. It is uploaded as the Actions
|
||||
artifact `test-impact-<sha>`. `test-impact-bundle.sh` accepts a bundle only
|
||||
from a successful `push`, `schedule` or `workflow_dispatch` run of the
|
||||
producer workflow, in the same repository, on `main`, whose head is exactly
|
||||
the requested commit.
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test-impact-artifact.yml (the name test-impact-bundle.sh looks for)
|
||||
name: test-impact-artifact
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "23 3 * * 0" # weekly full refresh
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # fetch the previous bundle (incremental mode)
|
||||
concurrency:
|
||||
group: test-impact-artifact # bundles chain: never overlap, never cancel
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@<pinned-sha>
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install codebase-memory-mcp
|
||||
run: <the pinned install from Prerequisites>
|
||||
- name: Per-test coverage map # YOURS: format 1, see coverage-map.py
|
||||
run: |
|
||||
echo "OBSERVED=$(date +%s)" >> "$GITHUB_ENV" # when the coverage run starts
|
||||
./ci/coverage-map.sh --out "$RUNNER_TEMP/coverage"
|
||||
- name: Publish the bundle
|
||||
run: |
|
||||
export CBM_CACHE_DIR="$RUNNER_TEMP/ti-cache"
|
||||
export CBM_RUNTIME_DIR="$(mktemp -d /tmp/cbm-rt.XXXXXX)" # short: sun_path
|
||||
mkdir -p "$RUNNER_TEMP/ti-work" && chmod 700 "$RUNNER_TEMP/ti-work"
|
||||
"$RUNNER_TEMP/cbm/codebase-memory-mcp" test-impact publish \
|
||||
--repo "$GITHUB_WORKSPACE" --out "$RUNNER_TEMP/bundle" \
|
||||
--work "$RUNNER_TEMP/ti-work" --coverage "$RUNNER_TEMP/coverage" \
|
||||
--observed-at "$OBSERVED" --platform linux-x86_64-clang21
|
||||
- uses: actions/upload-artifact@<pinned-sha>
|
||||
with:
|
||||
name: test-impact-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/bundle
|
||||
include-hidden-files: true # the graph lives in .codebase-memory/
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
```
|
||||
|
||||
This repository's producer (`.github/workflows/test-impact-artifact.yml` with
|
||||
`scripts/ci/test-impact-publish.sh`) adds an incremental mode. A push re-runs
|
||||
coverage only for the suites that its own selection picks and merges them into
|
||||
the previous map (`coverage-merge.py`). The graph is built incrementally from
|
||||
the previous bundle (`--previous`). The weekly run rebuilds everything and
|
||||
fails if the incremental graph topology differs from the full one. The
|
||||
incremental mode is an optimisation. A full coverage run on every push is
|
||||
simpler and just as correct.
|
||||
|
||||
The receipt keeps the time of the oldest coverage observation. An incremental
|
||||
publish carries it forward unchanged. A map whose oldest observation is more
|
||||
than 7 days old is refused (`TOO_OLD`), so a full refresh has to run more often
|
||||
than that.
|
||||
|
||||
### (b) PR job, shadow mode (report only)
|
||||
|
||||
```yaml
|
||||
test-impact-shadow:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true # can never block or pass a merge
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # the merge base's bundle
|
||||
steps:
|
||||
- uses: actions/checkout@<pinned-sha>
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install codebase-memory-mcp
|
||||
run: <the pinned install from Prerequisites>
|
||||
- name: Select with the team artifact (shadow)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: scripts/ci/test-impact-select.sh --binary "$RUNNER_TEMP/cbm/codebase-memory-mcp" --out "$RUNNER_TEMP/test-impact"
|
||||
- uses: actions/upload-artifact@<pinned-sha>
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: test-impact-prediction
|
||||
path: |
|
||||
${{ runner.temp }}/test-impact/selection-engine.json
|
||||
${{ runner.temp }}/test-impact/summary-engine.md
|
||||
```
|
||||
|
||||
Leave this job out of your aggregate required check. What
|
||||
`test-impact-select.sh` does:
|
||||
|
||||
1. Takes the base side of the PR merge commit (`HEAD^1`) and fetches that
|
||||
commit's bundle.
|
||||
2. If the producer skipped that commit (merge bursts: GitHub keeps one pending
|
||||
run per concurrency group), it uses the nearest first-parent ancestor that
|
||||
has a bundle, up to 20 commits back, and diffs against that ancestor. If the
|
||||
PR reverts a change that main made after the ancestor, the ancestor's map
|
||||
cannot speak for that file, so the script selects without coverage (the
|
||||
revert guard).
|
||||
3. Passes `--artifact-verified` only for a bundle that it fetched and checked
|
||||
itself. Nothing inside a bundle can assert where the bundle came from.
|
||||
4. Runs `test-impact select` and writes `selection-engine.json`. It also
|
||||
writes a Markdown summary to the job's step summary.
|
||||
|
||||
The engine reads `.codebase-memory.json` from the **selection base** (the merge
|
||||
base, or the ancestor whose bundle was admitted), not from the PR. A PR cannot
|
||||
loosen its own selection. A PR that changes the config runs
|
||||
everything (`builtin:test-impact-config`).
|
||||
|
||||
### (c) Gate job and the test job
|
||||
|
||||
```yaml
|
||||
select-tests:
|
||||
if: ${{ vars.TEST_IMPACT_MODE == 'gate' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
outputs:
|
||||
selection: ${{ steps.select.outputs.selection }}
|
||||
steps:
|
||||
- uses: actions/checkout@<pinned-sha>
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install codebase-memory-mcp
|
||||
run: <the pinned install from Prerequisites>
|
||||
- id: select
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: scripts/ci/test-impact-select.sh --binary "$RUNNER_TEMP/cbm/codebase-memory-mcp" --out "$RUNNER_TEMP/test-impact" --gate "$RUNNER_TEMP/test-selection"
|
||||
- if: ${{ steps.select.outputs.selection == 'narrowed' }}
|
||||
uses: actions/upload-artifact@<pinned-sha>
|
||||
with:
|
||||
name: test-selection
|
||||
path: ${{ runner.temp }}/test-selection
|
||||
if-no-files-found: error
|
||||
|
||||
test:
|
||||
needs: [select-tests]
|
||||
if: ${{ !cancelled() }} # a skipped or failed select-tests must NOT skip the tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@<pinned-sha>
|
||||
- if: ${{ needs.select-tests.result == 'success' && needs.select-tests.outputs.selection == 'narrowed' }}
|
||||
uses: actions/download-artifact@<pinned-sha>
|
||||
with:
|
||||
name: test-selection
|
||||
path: ${{ runner.temp }}/test-selection
|
||||
- run: ./ci/run-tests.sh # YOURS: narrows only if test-only.txt is present
|
||||
```
|
||||
|
||||
With `--gate DIR`, the script runs `selection.py filter … --lane unit` and
|
||||
writes `selection=narrowed` or `selection=all` to `$GITHUB_OUTPUT`. A narrowed
|
||||
answer writes:
|
||||
|
||||
- `DIR/test-only.txt`: one token per line, either `suite` (run the whole
|
||||
suite) or `suite:test`;
|
||||
- `DIR/optional-suites.txt`: selected suites that are registered only under a
|
||||
preprocessor condition. A build may lack them.
|
||||
|
||||
Everything else means `all`: a `run_all` answer, an unknown schema or filter
|
||||
encoding, a filter that disagrees with the lane's suite list, a crash, and a
|
||||
lane that selects nothing. The test job then gets no selection and runs every
|
||||
test.
|
||||
|
||||
**The test job's runner** has to keep the same contract as this repository's
|
||||
runner (`scripts/test.sh` → `scripts/run-tests-parallel.sh` →
|
||||
`CBM_TEST_ONLY_FILE`, read by `tests/test_main.c`):
|
||||
|
||||
- launch only the suites that the tokens name, and only the named tests inside
|
||||
a `suite:test` suite;
|
||||
- **fail** when the selection names a suite the build does not register,
|
||||
unless `optional-suites.txt` lists it;
|
||||
- **fail** when a selected test cannot be found;
|
||||
- **fail** when a selection was promised but the file is missing or empty.
|
||||
|
||||
A selection that is silently ignored, or partly ignored, would turn a narrowing
|
||||
into missed coverage.
|
||||
|
||||
**A runner that starts itself again.** Tests often spawn the test binary as a
|
||||
child (`<runner> __probe_x ...`) and its `main` hands over to a probe. A change
|
||||
that reaches `main` reaches every test, so the engine runs everything
|
||||
(`RUNNER_REACHED`). List the probes instead in a table in the runner's source,
|
||||
one entry per line, as `tests/test_main.c` does:
|
||||
|
||||
```c
|
||||
TF_ROLE(TF_PHASE_SEAMED, "__cbm_sockprobe", tf_maybe_run_socket_probe),
|
||||
TF_ROLE(TF_PHASE_WORKER, "--index-worker", tf_maybe_run_index_worker),
|
||||
TF_ROLE(TF_PHASE_SPAWNER, "CBM_INDEX_SUPERVISOR", tf_maybe_run_index_worker),
|
||||
```
|
||||
|
||||
The engine reads the table: a change that only a probe's handler reaches
|
||||
selects the tests whose source names one of that handler's literals (the argv
|
||||
the child gets, or a switch that lets production code start it), also through
|
||||
a `#define` that names the literal. The runner's own use of a literal names no
|
||||
spawner. A reached handler that no test names, a table entry the engine cannot
|
||||
read, and a change reaching `main` any other way still run everything; the
|
||||
engine note names the call chain into `main`.
|
||||
|
||||
**Only the legs the coverage speaks for should narrow.** Here the coverage
|
||||
comes from one Linux x86_64 clang-21 producer. Only the Linux x86_64 test leg
|
||||
downloads the selection (`runner.os == 'Linux' && runner.arch == 'X64'` in
|
||||
`_test.yml`). The arm64, macOS and Windows legs always run everything,
|
||||
because code under architecture or platform `#ifdef`s has static evidence
|
||||
only there. That leg builds with gcc, not clang: code under a compiler `#if`
|
||||
has static evidence only too, and `--platform` names the producer's platform,
|
||||
so the engine's platform check admits the map by construction.
|
||||
|
||||
### (d) Flip the variable
|
||||
|
||||
Shadow is the default: the variable is unset, `select-tests` is skipped, and
|
||||
every test runs. When the evidence is in, set the repository variable:
|
||||
|
||||
```bash
|
||||
gh variable set TEST_IMPACT_MODE --body gate # gate
|
||||
gh variable delete TEST_IMPACT_MODE # back to shadow
|
||||
```
|
||||
|
||||
Do not add `select-tests` to your required checks or to the aggregate gate.
|
||||
When it breaks, PRs cost more minutes. They do not lose coverage.
|
||||
|
||||
### Optional: Layer 1 (lanes)
|
||||
|
||||
`scripts/ci/select-lanes.sh` is this repository's lane rule set. A `changes`
|
||||
job lists the PR's files (the `pulls/{n}/files` API, with a git fallback),
|
||||
runs the selector, and writes `tier`, `full`, `lanes` (a JSON list) and
|
||||
`reasons` to `$GITHUB_OUTPUT`. Each lane job then runs
|
||||
`if: contains(fromJSON(needs.changes.outputs.lanes), '<lane>')`. A single
|
||||
aggregate job (`ci-ok`, `scripts/ci/require-all-green.sh`) is the only required
|
||||
check. It fails when a *selected* lane was skipped, cancelled or missing.
|
||||
|
||||
For your own version, keep these properties:
|
||||
|
||||
- An unknown path, an empty list, workflow and harness changes, and a list
|
||||
truncated at the API's 3000-file cap all select every lane.
|
||||
- The selector runs from a checkout without credentials, after the step that
|
||||
holds the token.
|
||||
- Replay your PR history through the selector and check that every real
|
||||
failure had its catching lane selected. This repository's equivalent is
|
||||
`scripts/test-impact/replay-selector.sh`.
|
||||
|
||||
## Configuration: `.codebase-memory.json` → `test_impact`
|
||||
|
||||
| Key | Values | Default |
|
||||
|---|---|---|
|
||||
| `version` | `1` (required) | — |
|
||||
| `tests.presets` | `{"c-cbm": true, ...}`. Unknown names are an error. | `c-cbm` off, the six others on |
|
||||
| `tests.conventions` | C-family macro records: `case`, `suite`, `registration`, `suite_registration` | none |
|
||||
| `rules` | ordered list, see below | none (built-ins only) |
|
||||
| `lanes` | list of `{name, suites, exclude_suites?, default: "run"\|"skip", narrow?}` | one lane `unit`: all suites except `@perf`, `narrow: true` |
|
||||
| `unmapped_files` | `"run_all"` (the only accepted value) | `run_all` |
|
||||
| `coverage.compatibility_paths` | repo-relative files that the coverage map depends on (harness, runner, build flags) | none |
|
||||
|
||||
**Rules.** Each rule is `{id, paths, action, ...}`. `paths` are gitignore-style
|
||||
globs with `{a,b}` alternatives. A pattern with a `/` is anchored at the
|
||||
repository root. Without a `/`, it matches at any depth. `**` matches across
|
||||
directories. Project rules come first, then the built-ins. For each changed
|
||||
path, **the first matching rule wins**, so order matters.
|
||||
|
||||
| `action` | Effect |
|
||||
|---|---|
|
||||
| `run_all` | Any match runs everything. The summary names the rule (`run_all_rules`). |
|
||||
| `ignore` | The path selects nothing. |
|
||||
| `referencing_tests` | Selects the tests whose source contains the path as text. The engine tries the path itself, then its parent directories down to the `key`, each with and without its first segment. `key: "path_segment:N"` stops at segment N (zero-based). `key: "path"` tries the exact path only. If no test names the path, everything runs, unless `unreferenced: "ignore"` is set. |
|
||||
| `suites` | Runs the named suites whole. |
|
||||
| `lanes` | Activates the named lanes (for lanes with `default: "skip"`). |
|
||||
|
||||
Built-in rules, applied after yours:
|
||||
|
||||
| id | paths | action |
|
||||
|---|---|---|
|
||||
| `builtin:test-impact-config` | `.codebase-memory.json` | `run_all` |
|
||||
| `builtin:build` | `Makefile*`, `CMakeLists.txt`, `*.cmake`, `meson.build`, `BUILD*`, `WORKSPACE`, `package*.json`, `go.mod`, `go.sum`, `Cargo.*`, `pom.xml`, `build.gradle*`, `pyproject.toml`, `setup.*`, `requirements*.txt` | `run_all` |
|
||||
| `builtin:ci` | `.github/**`, `.gitlab-ci.yml`, `.circleci/**`, `Jenkinsfile*`, `.buildkite/**`, `azure-pipelines*.yml` | `run_all` |
|
||||
| `builtin:vendor` | `vendor*/**`, `third_party/**` | `run_all` |
|
||||
| `builtin:conftest` | `conftest.py` | `run_all` |
|
||||
| `builtin:documentation` | `docs/**`, `**/*.md`, `LICENSE*` | `ignore` |
|
||||
|
||||
**Lanes.** A lane that you declare yourself does **not** narrow unless you set
|
||||
`"narrow": true`. `signals` is accepted but has no effect yet.
|
||||
|
||||
**This repository's file, explained:**
|
||||
|
||||
| Rule | Why |
|
||||
|---|---|
|
||||
| `harness` → `run_all` | The test framework and the runner's `main` touch every suite. |
|
||||
| `engine` → `run_all` | The selection's own code and config. A change to them could loosen what is selected. |
|
||||
| `build` → `run_all` | Build and runner scripts: the file list cannot tell a flag change from a harmless one. `/.gitattributes` is anchored at the root. |
|
||||
| `grammars` → `run_all` | Vendored parsers are not modelled per test. |
|
||||
| `fixtures` → `referencing_tests`, `path_segment:2` | `tests/fixtures/<name>/...` selects the tests that mention the fixture, down to `tests/fixtures/<name>`. |
|
||||
| `contracts` → `ignore` | Shell, Python and data contract tests run in every leg's contract step, which is never narrowed. |
|
||||
| `notices` → `ignore` | Release notices. |
|
||||
| `repo-data` → `referencing_tests`, `path`, `unreferenced: ignore` | Scripts, docs and packaging. A C test whose source names one of these files (its path, as text) is selected. A file that no test names selects nothing. |
|
||||
|
||||
`coverage.compatibility_paths` lists the harness and coverage tools. Changing
|
||||
the bytes of any of them, or the `--platform` label, makes the existing coverage
|
||||
map incompatible until the producer publishes a new one.
|
||||
|
||||
## Rollout and evidence
|
||||
|
||||
1. **Shadow.** Run (a) and (b). Leave `TEST_IMPACT_MODE` unset.
|
||||
2. **Measure.** For each PR run, compare `selection-engine.json` with what
|
||||
failed in that run's full test legs.
|
||||
- A **miss** is a test that failed in the full run but was not selected.
|
||||
One unexplained miss blocks the flip.
|
||||
- **Savings** are the suites and tests that were not selected.
|
||||
|
||||
Nothing in this repository computes misses for Layer 2 yet. Layer 1's
|
||||
history replay (`replay-selector.sh`) is the model to follow.
|
||||
3. **Gate.** Set `TEST_IMPACT_MODE=gate`. Keep the shadow job running, so that
|
||||
the evidence keeps coming.
|
||||
|
||||
**Reading the step summary** (`### test-impact engine`):
|
||||
|
||||
| Line | Meaning |
|
||||
|---|---|
|
||||
| `decision: \`selected\`` / `\`run_all\`` / `\`nothing\``, then `run-all reasons [...]` | The top-level answer and why everything runs. `decided by rule(s) [...]` names the matched `run_all` rule ids. |
|
||||
| `team artifact: fetched (ancestor …)` / `none`; `coverage \`admitted\`` / `\`rejected\` [reasons]` / `\`unavailable\`` | Whether a bundle was used, and whether its coverage was admitted. |
|
||||
| `engine note: …` | The engine's first diagnostic line, such as the path that caused `UNMAPPED_FILE`, or the call chain into the runner's `main` for `RUNNER_REACHED`. |
|
||||
| `lane \`unit\` (selected): N whole [REASON n, …], M narrowed (K tests)` | Per lane: suites that run whole (with a histogram of reasons), and suites narrowed to K tests. |
|
||||
| `the runner-main veto (RUNNER_REACHED) discarded this selection:` | The change reached the runner's `main`, so everything runs. The lines below show what would have been selected otherwise. |
|
||||
| `PR CI runs N selection tokens` / `PR CI runs every test (selection exit N)` | Gate mode only: what the test legs received. |
|
||||
|
||||
Common run-all reasons:
|
||||
|
||||
| Reason | Cause |
|
||||
|---|---|
|
||||
| `RULE_RUN_ALL` | A `run_all` rule matched (see `run_all_rules`). |
|
||||
| `UNMAPPED_FILE` | A changed file matched no rule and is not C-family source. |
|
||||
| `RULE_TARGET_UNKNOWN` | A `referencing_tests` path that no test names (without `unreferenced: "ignore"`). |
|
||||
| `RUNNER_REACHED` | The change reaches the test runner's `main` (or a child role none of whose literals a test names). |
|
||||
| `GIT_UNAVAILABLE` | No git, a shallow clone, or a diff that cannot be read in full. |
|
||||
| `CONFIG_INVALID` / `POLICY_UNAVAILABLE` | The merge base's config or rules do not parse. |
|
||||
|
||||
Reasons per suite: `ARTIFACT_REJECTED` (no admitted coverage), `SETUP_HIT` /
|
||||
`SETUP_UNKNOWN` (the suite's between-test setup ran changed code, or its
|
||||
coverage is incomplete), `INVENTORY_UNKNOWN` (the suite cannot be read with
|
||||
certainty), `CONDITIONAL` (`#if`-registered), `STATIC` / `CHANGED` / `COVERAGE`
|
||||
/ `RULE` (the evidence that selected it).
|
||||
|
||||
## Failure modes
|
||||
|
||||
Every row below costs CI minutes, not coverage.
|
||||
|
||||
| What happens | Result |
|
||||
|---|---|
|
||||
| No bundle for the merge base | Nearest bundled ancestor, up to 20 first-parent commits back. Otherwise no coverage, and everything runs. |
|
||||
| Bundle fetch fails (token, API) | Warning, no coverage, and everything runs. |
|
||||
| The PR reverts a change that main made after the ancestor | Revert guard: no coverage, and everything runs. |
|
||||
| Coverage older than 7 days, or a platform/compatibility mismatch | Map refused: every suite runs whole. |
|
||||
| Shallow checkout | `GIT_UNAVAILABLE`: everything runs. |
|
||||
| Engine deadline (default 1800 s) or crash | The job fails. The test job gets no selection, so everything runs. |
|
||||
| `select-tests` skipped (shadow) or failed | Everything runs (`!cancelled()` on the test job). |
|
||||
| The selection names a suite or test that the build lacks | The test run **fails**: a plumbing fault, reported, never skipped. |
|
||||
|
||||
## Known limits
|
||||
|
||||
- Narrowing covers C-family source and C test conventions only. Everything
|
||||
else is decided by rules or runs everything.
|
||||
- The coverage map format and its producer come from this repository's LLVM
|
||||
toolchain and harness. You need your own equivalent.
|
||||
- One producer platform. The selection narrows only the leg that matches the
|
||||
producer's platform; every other leg runs everything. Narrowing more legs
|
||||
needs a producer (and a bundle) per platform.
|
||||
- The weekly full refresh and the 7-day age limit are the same length. Each
|
||||
week, when the refresh runs late, there can be a short window in which maps
|
||||
are refused and every test runs.
|
||||
- In merge bursts the producer skips commits. The ancestor fallback covers up
|
||||
to 20 of them.
|
||||
- The answer schema is `cbm.test_impact.v0` and is marked experimental. It may
|
||||
change between releases. Pin the binary.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Windows Troubleshooting
|
||||
|
||||
This page collects known Windows-specific symptoms, with the version range each affects, confirmed workarounds, and links to the canonical tracking issues.
|
||||
|
||||
## Non-ASCII paths in CBM_CACHE_DIR or CBM_ALLOWED_ROOT
|
||||
|
||||
**Symptom:** Configuration appears not to persist, the config database cannot be opened (`error: cannot open config database`), or repositories inside `CBM_ALLOWED_ROOT` are rejected during indexing.
|
||||
|
||||
**Affected versions:** Affects v0.9.x and earlier. Fixed in v0.10.0 (environment variables are read as wide strings via `GetEnvironmentVariableW`).
|
||||
|
||||
**Workaround:** On an older build, use ASCII-only paths for these two variables or upgrade with `install.ps1`.
|
||||
|
||||
**Tracking:** #1165.
|
||||
|
||||
## Upgrade completes but binary version does not change
|
||||
|
||||
**Symptom:** `codebase-memory-mcp update` reports success and downloads files, but `codebase-memory-mcp --version` still reports the previous version (with `.exe.old` remaining alongside).
|
||||
|
||||
**Affected versions:** Affects v0.9.x and earlier (in-process self-update on Windows). From v0.10.0, `update` prints the `install.ps1` command instead of replacing itself.
|
||||
|
||||
**Workaround:** On an older build, run `install.ps1` once; it replaces the binary out-of-process and verifies the installed version.
|
||||
|
||||
**Tracking:** #1168 (closed; fixed in v0.10.0).
|
||||
|
||||
## Index worker hangs, crashes, or leaves an empty worker log
|
||||
|
||||
**Symptom:** `index_repository` reports worker hangs or crashes. (An *empty* worker log is a separate, fixed bug: v0.10.4 and earlier.)
|
||||
|
||||
**Affected versions:** Empty worker logs affect v0.10.4 and earlier (fixed in v0.10.5). Misleading "crashed on a file" reports for returned errors fixed in v0.10.0. From v0.11.0, the Windows worker is memory-capped. From v0.12.0, a worker that exits without returning a result is no longer counted as a success, and its log is kept. The underlying hang (#1130) and startup exit (#1145) remain open for investigation.
|
||||
|
||||
**Workaround:** For repositories containing large binary, media, or model directories, exclude those paths with `.cbmignore` before indexing (see [`.cbmignore` documentation](cbmignore.md)). From v0.10.5, the worker log names the in-flight file; if indexing still fails on v0.11.0 or newer, attach that log to #1130 or #1145.
|
||||
|
||||
**Tracking:** #1130, #1132, #1145.
|
||||
|
||||
## Antigravity 2.0 configuration not picked up
|
||||
|
||||
**Symptom:** Installation completes but Antigravity 2.0 does not see the MCP server or hooks, or the environment is detected as Gemini-CLI; duplicate large binaries may also be left on disk.
|
||||
|
||||
**Affected versions:** All releases through v0.12.1 when neither detection signal below is present (v0.9.x checks only for `~/.gemini/antigravity-cli`; from v0.10.0, `antigravity` on PATH also counts). The duplicate binary left behind by `install.ps1` affects v0.9.x and earlier (fixed in v0.10.0).
|
||||
|
||||
**Workaround:** Antigravity is configured only when `~/.gemini/antigravity-cli` exists or `antigravity` is on PATH; the target is `~/.gemini/config/mcp_config.json`, and no hooks are installed for it. If neither signal is present, the machine is treated as Gemini-CLI. From v0.10.0, `install.ps1` keeps a single binary; running `codebase-memory-mcp install` manually without `--dir` can still create a second copy in `~/.local/bin`.
|
||||
|
||||
**Tracking:** #1161.
|
||||
|
||||
## OpenCode installed but not detected
|
||||
|
||||
**Symptom:** `codebase-memory-mcp install` detects other clients but skips OpenCode on Windows.
|
||||
|
||||
**Affected versions:** v0.9.x and earlier detect OpenCode only through `opencode` on PATH. v0.10.0 also detects its configuration (the config file, `%USERPROFILE%\.config\opencode\`, or `OPENCODE_CONFIG_DIR`), and v0.12.0 also detects its data folder `%USERPROFILE%\.local\share\opencode\`, which the OpenCode CLI and the Desktop app both create, so detection no longer depends on PATH. In v0.12.1 and earlier, a PATH longer than 4 KB hides every CLI from detection, and npm, mise and scoop shim folders are not searched; both are fixed after v0.12.1 (#2381).
|
||||
|
||||
**Workaround:** On v0.12.0 or newer, start OpenCode once (CLI or Desktop app) so that its data folder exists, then re-run `codebase-memory-mcp install`. On v0.10.x or v0.11.0, make sure `%USERPROFILE%\.config\opencode\` exists (or point `OPENCODE_CONFIG` at an existing config file, or `OPENCODE_CONFIG_DIR` at an existing folder), then re-run `codebase-memory-mcp install`. On v0.9.x, `opencode` must be on PATH; upgrading with `install.ps1` is the simpler fix.
|
||||
|
||||
**Tracking:** #1167.
|
||||
|
||||
---
|
||||
|
||||
When a release resolves one of these issues, update or remove the corresponding entry rather than letting this page become a second issue tracker.
|
||||
@@ -70,7 +70,12 @@ For directories:
|
||||
|
||||
1. **Built-in skip list** — `.git`, `node_modules`, `dist`, `target`,
|
||||
`vendor`, tool caches, etc. (60+ names; the fast/moderate index modes add
|
||||
more, e.g. `docs`, `examples`, `testdata`). A `.cbmignore` negation
|
||||
more, e.g. `docs`, `examples`, `testdata`). Generated output whose
|
||||
folder name alone is too common to skip everywhere is matched by path
|
||||
suffix instead: Laravel's compiled Blade view cache
|
||||
`storage/framework/views/` (at any depth) is skipped even without
|
||||
Laravel's stock `.gitignore` there, while other `views/` folders stay
|
||||
indexed. A `.cbmignore` negation
|
||||
(for example `!target/`) can un-skip these directories, except the
|
||||
non-negatable safety core: `.git`, `node_modules`, `.worktrees`, and
|
||||
`.claude-worktrees`.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 279 KiB |
@@ -0,0 +1,53 @@
|
||||
# CBM Lens Beta README captures
|
||||
|
||||
These are screenshots and browser recordings of the real application using public source from this repository. The application data and text were not retouched. The GIFs were refreshed on 20 October 2026; the still images preserve the earlier captures from 12–13 October.
|
||||
|
||||
## High-resolution recordings: 20 October
|
||||
|
||||
- Repository: [DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)
|
||||
- Source commit: `4cf44147e639ae070ee44d9230244bfb9d9676e0`
|
||||
- Build: `make -f Makefile.cbm cbm-with-ui`, with the standard frontend and experimental agent activity disabled
|
||||
- Capture: built-in browser screenshots on macOS, English locale, native viewport **1600 × 1000**, **12 frames per second**
|
||||
- Encoding: JPEG screenshot frames converted to GIF with one 256-color palette per recording, no dithering, infinite looping; no upscaling, sharpening or pointer overlays
|
||||
|
||||
The `v0.0.1` label is the frontend package version, not the backend release version. Standard native releases include Lens; using a source build for these recordings is a capture-provenance detail, not an installation requirement.
|
||||
|
||||
### Data and interaction
|
||||
|
||||
The project `codebase-memory-mcp` is a clean `git archive` export of the source commit, indexed in `full` mode into an isolated local cache. No private repository was used. Because the export has no `.git` folder, the interface reports "Git unavailable" where applicable.
|
||||
|
||||
The index reported 81,190 nodes and 411,072 edges. These counts describe this source scope, not a benchmark or proof of complete repository coverage. The interface retains its coverage and display-limit notices. The clips join recorded interaction segments with pauses shortened between them; their displayed application states and answers are unchanged. Directional pulses represent indexed relationships, not observed execution.
|
||||
|
||||
| Recording | Duration | What it shows |
|
||||
|-----------|----------|---------------|
|
||||
| [lens-overview.gif](lens-overview.gif) | 12.5 seconds | Architecture overview, opening `src/foundation`, browsing its files, selecting `arena.c` and reading its source beside the map. |
|
||||
| [lens-galaxy.gif](lens-galaxy.gif) | 12 seconds | Expanding outgoing relationships around `cbm_arena_alloc`, switching to the hierarchy and tracing the two-hop call path `cbm_arena_alloc → arena_grow → cbm_alloc`. |
|
||||
| [lens-chat.gif](lens-chat.gif) | 11.5 seconds | `arena_grow` selected in Galaxy, with caller and callee questions answered directly from the indexed graph. |
|
||||
|
||||
All three GIFs are 1600 × 1000. Their frames and playback duration were decoded and checked after export. README and website use these same files.
|
||||
|
||||
### Local chat check
|
||||
|
||||
The optional Qwen2.5 Coder 0.5B model was downloaded and loaded through the application's own setup. Automatic explanations were disabled for this recording. Both displayed answers are deterministic graph answers, labelled "Listed from the indexed graph; not generated by the model." The clip shows one incoming `CALLS` relationship from `cbm_arena_alloc` and two outgoing `CALLS` relationships to `cbm_alloc` and `cbm_free`, alongside the other indexed relationship types. It does not demonstrate model-generated prose.
|
||||
|
||||
## Original still images: 12 October
|
||||
|
||||
Bernhard Jackiewicz captured the PNG stills on 12 October 2026, from commit `4d9592b5` on `main` after [PR #2068](https://github.com/DeusData/codebase-memory-mcp/pull/2068). The build used `scripts/build.sh --with-ui`; headless Chromium on macOS used English locale, a 1440 × 900 viewport and device scale factor 1. Experimental agent activity was disabled.
|
||||
|
||||
The indexed project was a public `git archive` export of that commit in `full` mode, with 75,037 nodes and 361,349 edges. The PNG stills are 1440 × 900, except the chat still at 1440 × 1200 so the explanation and answer fit together. These are historical application views, also used as reduced-motion alternatives on the website; they are not extracted from the new GIFs.
|
||||
|
||||
| Still | What it shows |
|
||||
|-------|---------------|
|
||||
| [lens-architecture.png](lens-architecture.png) | The whole repository on the architecture map. |
|
||||
| [lens-chat.png](lens-chat.png) | A local model explanation of `cbm_pipeline_pass_doclinks` and a caller answer listed from the graph. |
|
||||
| [lens-system-structure.png](lens-system-structure.png) | Component groups and their dependencies in System structure, with `src` selected. |
|
||||
| [lens-behavior.png](lens-behavior.png) | Possible direct calls of `compileCards`, with its declaration. |
|
||||
|
||||
## Refreshed still images: 13 October
|
||||
|
||||
`lens-explore.jpg` and `lens-galaxy.jpg` are unedited browser captures at 1280 × 720 from the locally served CBM Lens Beta UI. The running binary reports `dev`; its exact build commit is unavailable, so these captures do not certify an exact source revision. The local project is named `cbm-triage-main-20260919`; the screenshots show public CBM source and indexed relationships.
|
||||
|
||||
- **[Explore](lens-explore.jpg):** `src/foundation/limits.c`, its compact file impact summary and direct dependencies (24 nodes, 26 edges). The UI's freshness, sampling and coverage notices remain visible.
|
||||
- **[Galaxy](lens-galaxy.jpg):** `cbm_max_file_bytes`, both directions, all edge types, two layers (160 nodes, 218 edges).
|
||||
|
||||
These counts describe the displayed index snapshot, not a benchmark or proof of complete repository coverage.
|
||||
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 197 KiB |
@@ -1,26 +1,49 @@
|
||||
# codebase-memory-mcp
|
||||
|
||||
> An open-source Model Context Protocol (MCP) server that indexes a codebase into a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. AI coding agents query the graph instead of reading files one by one, answering structural questions with roughly 120x fewer tokens (~3,400 vs ~412,000 across five structural queries). Parses 162 languages via vendored tree-sitter grammars, with Hybrid LSP semantic type resolution for Python, TypeScript/JavaScript, PHP, C#, Go, C/C++, Java, Kotlin, and Rust. It also generates semantic graph edges (SEMANTICALLY_RELATED for vocabulary-mismatch matches, SIMILAR_TO for near-clone detection) and supports semantic vector search via bundled nomic-embed-code embeddings compiled into the native executable — no API key, no Ollama, no Docker. Ships a native executable with an authenticated integration asset; the UI variant adds one content-addressed frontend pack. All processing is local — the embeddings run on-device; there is no embedded LLM and no API key.
|
||||
> Codebase Memory is an open-source local MCP server for AI coding agents. It connects code, documentation, configuration, and infrastructure in a persistent knowledge graph, with 17 MCP tools and native CLI access. Search for a feature, follow callers and dependencies, trace services and deployments, and inspect change impact. Indexing, graph queries, and semantic embeddings run locally without a hosted service or API key. CBM Lens is the optional visual companion, currently in open beta (v0.0.1); its optional browser model supports on-device code chat after an opt-in download.
|
||||
|
||||
## Key facts
|
||||
## Capabilities
|
||||
|
||||
- License: MIT, open source.
|
||||
- Languages: 158 (158 vendored tree-sitter grammars compiled into the binary).
|
||||
- Hybrid LSP type resolution: 9 language families (Python, TypeScript/JavaScript/JSX/TSX, PHP, C#, Go, C/C++, Java, Kotlin, Rust) — a lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers including tsserver, pyright, gopls, Roslyn, Eclipse JDT, and rust-analyzer.
|
||||
- MCP tools: 15 (search_graph incl. semantic_query vector search, trace_path (alias: trace_call_path), check_index_coverage, query_graph (Cypher), detect_changes, get_architecture, get_code_snippet, manage_adr, and more).
|
||||
- Semantic search: natural-language code discovery via bundled nomic-embed-code embeddings (768-dim, compiled into the binary); 11-signal combined scoring; fully local, no API key.
|
||||
- Semantic & similarity edges: SEMANTICALLY_RELATED (vocabulary-mismatch matches) and SIMILAR_TO (MinHash + LSH near-clone / duplicate detection).
|
||||
- Cross-repo intelligence: CROSS_* edges link nodes across multiple repos indexed in one store; multi-galaxy 3D layout and cross-repo architecture summary.
|
||||
- Cross-service linking: HTTP route ↔ call-site matching, plus gRPC/GraphQL/tRPC detection and pub/sub channels (EMITS/LISTENS_ON for Socket.IO, EventEmitter, generic buses).
|
||||
- Supported agents: 45 automatic/conditional client surfaces (39 automatically detected + 6 conditional/explicit): Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Continue / cn, Visual Studio, TRAE, Roo Code, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob IDE, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), and Sourcegraph Cody.
|
||||
- Agent profiles: documented custom-agent formats receive Scout (fast/provisional), Verify (default/task-directed), and Auditor (bounded/full verification) definitions. Every direct tier checks exact path/scope coverage with check_index_coverage and falls back to source for flagged gaps; unsafe child-MCP formats use explicit parent handoff. Kiro and Junie use positive-allowlist Scout/Analysis server profiles (7/11 tools); Qoder combines named-server selection with exact tier-specific MCP tool IDs, while Factory uses exact registered IDs without additive whole-server exposure. Foreign Junie aliases are preserved and force parent handoff.
|
||||
- Performance: Linux kernel (28M LOC, 75K files) full index in 3 minutes → 4.81M nodes, 7.72M edges; Cypher queries in under 1ms.
|
||||
- Distribution: verified runtime set (native executable plus authenticated release-owned assets); fully static Linux builds are available in the `-portable` archives. Also available through npm, PyPI, Homebrew, Scoop, Winget, Chocolatey, AUR, and `go install`.
|
||||
- Code exploration: symbol and structural search, BM25 text search, semantic search, source snippets and file outlines, call paths, data flow, architecture summaries, hotspots, cycles, and coverage diagnostics. The server registers 17 MCP tools.
|
||||
- Semantic and similarity relationships: bundled embeddings support natural-language discovery; SEMANTICALLY_RELATED edges connect vocabulary-mismatch matches, and SIMILAR_TO edges expose near-clone candidates.
|
||||
- Languages and formats: vendored parsers cover source, documentation, and configuration formats. Hybrid LSP adds semantic type resolution for Python, TypeScript/JavaScript/JSX/TSX, PHP, C#, Go, C/C++, Java, Kotlin, Rust, and Perl. Support and resolution depth differ by language; use the README language matrix and check_index_coverage for the indexed project.
|
||||
- Documentation connected to code: Markdown, reStructuredText, and AsciiDoc sections; PDF text pages (no OCR); supported document and language doc-comment references to code; ADR metadata and supersession. Supported diagrams and OpenAPI descriptions contribute entities, with format-specific reference-resolution limits.
|
||||
- Infrastructure and deployment: Docker/Compose, Kubernetes, Helm, Kustomize, Terraform/OpenTofu, Terragrunt, Ansible, shell, Make, and supported CI relationships. Trace deployments, images, entrypoints, configuration keys, and environment-variable access back to source when inputs can be resolved statically.
|
||||
- Cross-service and cross-repository relationships: supported HTTP, asynchronous messaging, gRPC, GraphQL, and tRPC links. Cross-repo analysis explicitly links projects indexed in the same store; missing links do not prove that no runtime relationship exists.
|
||||
- Change impact: detect_changes maps diffs to affected symbols and investigates related tests with scope: "tests"; compare_graphs compares indexed snapshots.
|
||||
- Smart CI (experimental): repository-specific changed-file rules select CI jobs; test-impact select combines static reachability, changed tests, and admitted per-test coverage to select tests. Narrowing supports C/C++ family code with supported test-harness conventions and an explicitly configured runner. Missing or doubtful evidence broadens the selection or runs all tests. Other indexed languages are not automatically supported for test narrowing. test-impact publish creates the baseline graph-and-coverage bundle. Start in shadow mode; consult the guide for version requirements.
|
||||
- Architecture decisions and runtime evidence: manage_adr stores the project's decision record; ingest_traces adds separately collected observed call relationships alongside static analysis.
|
||||
- CBM Lens open beta (v0.0.1): optional visual companion to the MCP server, with a local browser interface with source exploration, Galaxy graph navigation, architecture and behavior views, ADR editing, index-coverage diagnostics, daemon/system information, and optional local code chat. Static graph animations are not live execution traces. Activity overlays are experimental source-build features, not a standard-release promise.
|
||||
- Agent integrations: 46 automatic or conditional client surfaces. Installation configures supported MCP entries, durable instructions, skills, Scout/Verify/Auditor profiles, and applicable session, compaction, search, and read hooks. Coverage and activation vary by client and platform. Hooks provide context without blocking or replacing the user's tool call; some clients require trust or use parent-agent handoff instead.
|
||||
- Persistent operation: a per-account daemon coordinates sessions and indexing. Existing indexes can stay synchronized during eligible active MCP sessions; initial auto-indexing is separately opt-in. Optional team graph artifacts share a baseline while each user keeps a local query database.
|
||||
- Token-efficient retrieval: targeted graph queries, focused source snippets, and compact, paged results can reduce token use and repeated file reads during code exploration.
|
||||
- Distribution: native C backend, MIT license, standard native releases for macOS, Linux, and Windows include CBM Lens; Linux portable archives provide static builds. Nix and development source builds can omit the UI. Package-manager options and release verification are documented in the README.
|
||||
|
||||
## Client coverage
|
||||
|
||||
- Supported agents: 46 automatic/conditional client surfaces (40 automatically detected + 6 conditional/explicit): Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Continue / cn, Visual Studio, TRAE, Roo Code, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob IDE, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), agentty, and Sourcegraph Cody.
|
||||
|
||||
## Measurement and limits
|
||||
|
||||
A March 2026 preprint across 31 real-world repositories reports 10× lower token use and 2.1× fewer tool calls compared with file-by-file exploration. These historical backend measurements cover the study’s workloads and agent setup. The paper documents the full evaluation; the README Performance section links to the research and earlier measurements.
|
||||
|
||||
Graph evidence depends on index generation, freshness, and exact path/scope coverage. Partial, skipped, excluded, stale, or unknown coverage requires source verification, especially for absence claims. Infrastructure relationships are static, and test-selection support is narrower than language-indexing support.
|
||||
|
||||
## Links
|
||||
|
||||
- Homepage: https://deusdata.github.io/codebase-memory-mcp/
|
||||
- Source code (GitHub): https://github.com/DeusData/codebase-memory-mcp
|
||||
- Source and feature reference: https://github.com/DeusData/codebase-memory-mcp
|
||||
- Latest release: https://github.com/DeusData/codebase-memory-mcp/releases/latest
|
||||
- Quick start and installation: https://github.com/DeusData/codebase-memory-mcp#quick-start
|
||||
- Documentation indexing: https://github.com/DeusData/codebase-memory-mcp#documentation-connected-to-code
|
||||
- Infrastructure and deployment: https://github.com/DeusData/codebase-memory-mcp#infrastructure-and-deployment
|
||||
- CBM Lens: https://github.com/DeusData/codebase-memory-mcp#cbm-lens-beta-code-visualization-and-source-navigation
|
||||
- Agent integrations: https://github.com/DeusData/codebase-memory-mcp#multi-agent-support
|
||||
- Smart CI guide: https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/SMART_CI.md
|
||||
- Configuration: https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/CONFIGURATION.md
|
||||
- MCP tools: https://github.com/DeusData/codebase-memory-mcp#mcp-tools
|
||||
- Token savings and evidence: https://deusdata.github.io/codebase-memory-mcp/#token-savings
|
||||
- Performance and research: https://github.com/DeusData/codebase-memory-mcp#performance
|
||||
- Benchmark report: https://github.com/DeusData/codebase-memory-mcp/blob/main/docs/BENCHMARK.md
|
||||
- Research paper: https://arxiv.org/abs/2603.27277
|
||||
- Security and release verification: https://github.com/DeusData/codebase-memory-mcp#security
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://deusdata.github.io/codebase-memory-mcp/</loc>
|
||||
<lastmod>2026-06-12</lastmod>
|
||||
<priority>1.0</priority>
|
||||
<lastmod>2026-10-20</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
packages = forAllSystems (pkgs: rec {
|
||||
default = pkgs.stdenv.mkDerivation {
|
||||
pname = "codebase-memory-mcp";
|
||||
version = "0.11.0";
|
||||
version = "0.12.1";
|
||||
|
||||
src = ./.;
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
.DS_Store
|
||||
cbm/
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.vite/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Die lokalen Modelle und die gebaute Inferenz-Runtime: zusammen ueber 11 GB
|
||||
# und beide reproduzierbar aus Pruefsummen und Baubefehl. Was committet wird,
|
||||
# ist die Herkunft und nicht das Gewicht.
|
||||
#
|
||||
# Die vier Zeilen je Verzeichnis sind kein Umweg, sondern das, was git
|
||||
# verlangt: ein ausgeschlossenes VERZEICHNIS wird gar nicht erst betreten, also
|
||||
# koennte eine Ausnahme darin nie greifen (git-scm, gitignore(5): "It is not
|
||||
# possible to re-include a file if a parent directory of that file is
|
||||
# excluded"). Die Verzeichnisregel steht trotzdem zuerst, weil sie die Absicht
|
||||
# nennt; die naechste hebt sie fuer das Verzeichnis selbst wieder auf, die
|
||||
# dritte schliesst seinen Inhalt aus, und erst die vierte holt die eine Datei
|
||||
# zurueck, die committet gehoert.
|
||||
models/
|
||||
!models/
|
||||
models/*
|
||||
!models/SHA256SUMS
|
||||
|
||||
vendor/
|
||||
!vendor/
|
||||
vendor/*
|
||||
!vendor/llama/
|
||||
vendor/llama/*
|
||||
!vendor/llama/HERKUNFT.md
|
||||
|
||||
# Die Laufspuren des Sidecars. Die PID-Datei gehoert dem laufenden Prozess,
|
||||
# das Log ist ohnehin schon durch *.log erfasst.
|
||||
llm/.pid
|
||||
|
||||
# Der Node-Bau des Context-Compilers fuer die Eval. Er wird bei jedem Lauf neu
|
||||
# erzeugt (tools/lib/compiler-bundle.mjs); ein committetes Bundle waere eine
|
||||
# zweite, alternde Fassung der Dateien daneben.
|
||||
.eval-build/
|
||||
|
||||
# Die Ereignisdatei der Agenten (W11a). Sie enthaelt keine Dateiinhalte, aber
|
||||
# sie verraet, an welchen Dateien jemand gearbeitet und welche Befehle er
|
||||
# gefahren hat. Das ist die Arbeitsspur einer Person und gehoert nicht in ein
|
||||
# Repository; was committet ist, ist die AUFZEICHNUNG des Beweislaufs unter
|
||||
# fixtures/agent-events/, und deren Herkunft steht dort daneben.
|
||||
.atlas-trace/
|
||||
events.jsonl
|
||||
events.jsonl.seq
|
||||
|
||||
# Local visual captures and development reports; JSON/JSONL fixtures stay tracked.
|
||||
/verification/**/*.png
|
||||
/verification/**/*.jpg
|
||||
/verification/**/*.jpeg
|
||||
/verification/**/*.gif
|
||||
/verification/**/*.webp
|
||||
/verification/**/*.svg
|
||||
/verification/**/*.webm
|
||||
/verification/**/*.mp4
|
||||
/verification/**/*.md
|
||||
@@ -1,48 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -1,64 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -1,21 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -1,56 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="relative flex-1 rounded-full bg-border"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,73 @@
|
||||
# Frontend configuration
|
||||
|
||||
Open **System**, then **Configuration**. Settings are grouped by indexing,
|
||||
resources, server, diagnostics, and browser. The **External inputs** reference
|
||||
covers repository files, installation, launch flags, integration paths, and
|
||||
development settings whose owner is outside the running daemon.
|
||||
|
||||
## Daemon overrides
|
||||
|
||||
Daemon edits are staged until **Save changes**. A batch is validated and committed
|
||||
atomically to `_config.db` in the CBM cache directory. Invalid keys or values reject
|
||||
the whole batch. Each row exposes its source, default, saved override, current
|
||||
process value, and application timing. **Reset** removes the saved override; it
|
||||
does not overwrite the inherited environment or replace it with a hardcoded default.
|
||||
Legacy environment values outside the registry's canonical format are shown as
|
||||
runtime-interpreted inputs, rather than guessed effective values. Saving an override
|
||||
requires a validated value.
|
||||
|
||||
Supported environment overrides are loaded once when a process starts, ahead of
|
||||
the inherited environment and built-in defaults. They do not mutate or export the
|
||||
process environment. Restart CBM to apply changes throughout the installation;
|
||||
newly launched index workers can load saved values before the existing daemon is
|
||||
restarted. Explicit command flags, per-index options, and supervisor recovery caps
|
||||
retain their documented precedence.
|
||||
|
||||
The existing `auto_index`, `auto_index_limit`, `auto_watch`, `watcher_enabled`, and
|
||||
`ui-lang` settings share their canonical SQLite values with CLI configuration.
|
||||
UI enablement and port overrides use the same transactional store, with legacy
|
||||
UI `config.json` as a fallback. Explicit CLI/UI saves update that store too.
|
||||
Concurrent edits use a revision check; a stale save fails without changing data.
|
||||
|
||||
Launch locations, filesystem access grants, arbitrary output destinations,
|
||||
installer settings, test injection, and supervisor controls are references rather
|
||||
than editable process overrides. Platform-specific or unlinked functionality is
|
||||
marked unavailable. No generic environment editor or secret enumeration is exposed.
|
||||
|
||||
## Browser preferences
|
||||
|
||||
Browser changes apply immediately. Graph display and edge motion use their existing
|
||||
preference stores. Galaxy budgets, coverage shadow, and architecture appearance
|
||||
share a versioned, per-project browser store with the controls in those views.
|
||||
Changing these values preserves the current selection. If storage is unavailable,
|
||||
preferences remain usable for the current session.
|
||||
|
||||
The top-bar agent status opens separate local model controls. Opening configuration never
|
||||
downloads weights or starts a model. Repository extension mappings and ignore files
|
||||
remain repository-owned and take effect on subsequent indexing.
|
||||
|
||||
## Local HTTP API
|
||||
|
||||
`GET /api/config` returns `{ revision, settings }`. Each setting declares its type,
|
||||
validation bounds or enum choices, lifecycle, provenance, and editability.
|
||||
The registry lives in `src/cli/runtime_settings.c`; consumer locations are included
|
||||
in its metadata. Browser response validation lives in `src/settings/config-model.ts`.
|
||||
|
||||
`POST /api/config` accepts JSON with the revision just read and a nonempty changes
|
||||
object. Values are canonical strings; `null` removes an override:
|
||||
|
||||
```json
|
||||
{
|
||||
"revision": "12",
|
||||
"changes": {
|
||||
"CBM_WORKERS": "4",
|
||||
"CBM_SEMANTIC_ENABLED": "false",
|
||||
"CBM_SQLITE_MMAP_SIZE": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success returns the updated snapshot. Invalid batches return 400, conflicting
|
||||
revisions return 409, and storage failures return 500. Existing loopback Host,
|
||||
same-origin, and JSON-content checks protect the endpoint. Frontend drafts survive
|
||||
errors so users can inspect the conflict before reloading or discarding them.
|
||||
@@ -0,0 +1,132 @@
|
||||
# CBM Lens Beta
|
||||
|
||||
CBM Lens is the local exploration frontend served by `codebase-memory-mcp --ui=true`.
|
||||
It combines source, the indexed graph and optional browser inference. Source files
|
||||
are read-only; project architecture decisions can be edited in the ADR workspace.
|
||||
The frontend was contributed by Bernhard Jackiewicz as CodeAtlasWeb.
|
||||
|
||||
## Explore a project
|
||||
|
||||
- **Explore** follows the current file or marked source. Its graph requests direct
|
||||
dependencies separately from the capped repository layout. File-level impact
|
||||
summaries use graph and local Git evidence, with details available on demand.
|
||||
- **Galaxy** searches nodes, files and folders. Select a scope and expand incoming
|
||||
or outgoing relationships by depth and edge type. Overview budgets limit drawing;
|
||||
a capped layout does not establish a complete neighborhood. Bounded caches reuse
|
||||
positions and verified adjacency, and fresh index generations invalidate evidence.
|
||||
- **Architecture** presents the repository map, routes, hotspots, system structure
|
||||
and behavior. Connectivity clusters are not proof of architectural boundaries.
|
||||
Moving arrows show edge direction, not observed execution. Reduced-motion settings
|
||||
retain stationary direction markers.
|
||||
- **Coverage** explains indexing gaps. **System** contains daemon status, indexes,
|
||||
diagnostics and MCP configuration. See [configuration and precedence](CONFIGURATION.md).
|
||||
- **ADR** stores one editable Markdown decision document per project through
|
||||
`GET/POST /api/adr`, shared with `manage_adr`. It does not edit repository files.
|
||||
|
||||
Use **Add project index** in the project dropdown (or Alt+P) to choose a local
|
||||
repository, name its index and follow daemon-reported progress. Files are indexed
|
||||
in place, not uploaded. Closing the dialog does not cancel indexing.
|
||||
|
||||
ADR drafts stay in this tab's session storage. Before saving, the UI compares the
|
||||
server document with the editing baseline and retains conflicting drafts. This is
|
||||
not an atomic document lock. Saves are limited to 8,000 UTF-8 bytes and a 16 KiB
|
||||
serialized request. The examples in `fixtures/adr/` are explicitly synthetic and
|
||||
are never inserted into real projects automatically.
|
||||
|
||||
## Local agent
|
||||
|
||||
The top-bar agent status opens model configuration; the adjacent chat control folds
|
||||
or unfolds the conversation. Model downloads require explicit activation. The chat
|
||||
uses browser-local inference; the selected source is not sent to a remote model.
|
||||
Model provenance and license limitations are in [THIRD_PARTY.md](THIRD_PARTY.md).
|
||||
|
||||
Explorer provides the exact marked text, or the loaded file when nothing is marked.
|
||||
A caret alone does not narrow the context. One replaceable source block precedes
|
||||
conversation history; each message captures the current selection. Navigation does
|
||||
not alter an in-flight request, and Retry uses its original snapshot. Missing or
|
||||
partial source is disclosed. Token counting uses the selected model's tokenizer;
|
||||
oversized requests are rejected rather than silently shortened.
|
||||
|
||||
Automatic explanations respond to new selections independently of follow-up chat.
|
||||
Browser history is separated by project, retained for 24 hours, and can be cleared
|
||||
in Agent configuration.
|
||||
|
||||
## Build and preview
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make -f Makefile.cbm cbm-with-ui
|
||||
```
|
||||
|
||||
This installs the locked frontend dependencies, builds `dist/` and embeds it in the
|
||||
native binary. For frontend development, run `npm ci` and `npm run dev` in this
|
||||
directory. Vite listens on 5173 and proxies `/api` and `/rpc` to the local server on
|
||||
9749. `npm run build` creates a production bundle.
|
||||
|
||||
The standalone test preview in `tools/lib/static-proxy.mjs` snapshots `dist/` when
|
||||
it starts. Restart that preview after building and compare the served `index.html`
|
||||
asset name with `dist/index.html` before reviewing changes.
|
||||
|
||||
The canonical frontend CI entry point is `scripts/ci/test-ui.sh` from the repository
|
||||
root. It runs unit tests, style and promise checks, acceptance checks and a production
|
||||
build. Browser smoke commands require a running server and Playwright Chromium;
|
||||
install that browser explicitly with `npx playwright install chromium` when needed.
|
||||
|
||||
## Experimental agent activity
|
||||
|
||||
Build with `VITE_EXPERIMENTAL_AGENTS=true` to expose recorded tool activity, hook
|
||||
setup and graph overlays. The value must be exactly `true`; URL parameters and saved
|
||||
preferences cannot enable it. Browser-local chat is independent of this flag.
|
||||
Activity remains off until enabled in the UI. It requires an event adapter and is
|
||||
not automatic observation of arbitrary coding agents.
|
||||
|
||||
The supported adapter is `agents/hooks/atlas-trace.py`. **Connect your agent**
|
||||
downloads it and offers a project-specific install command. Review and run it locally:
|
||||
|
||||
```sh
|
||||
python3 ~/Downloads/cbm-atlas-trace.py --install-claude --root /absolute/repository --project INDEXED_NAME --daemon-url http://127.0.0.1:9749
|
||||
```
|
||||
|
||||
The installer preserves unrelated settings and hooks, rejects conflicts, symlinks
|
||||
and malformed configuration, and writes only the selected repository's
|
||||
`.claude/settings.local.json` and `.claude/hooks/`.
|
||||
Start a new session in your configured client after installation. Other clients
|
||||
need their own adapter.
|
||||
|
||||
For manual hook setup, set `ATLAS_PROJECT` to the indexed name, optionally
|
||||
`ATLAS_AGENT_NAME` and `ATLAS_DAEMON_URL`, and pass the tool event on stdin. The hook
|
||||
records tool, run, sequence, timestamp, path and explicit line ranges. It omits source
|
||||
contents, tool output and free-form commands, search patterns and descriptions. Paths
|
||||
can still be sensitive. Destinations must be local; proxies and redirects are refused.
|
||||
|
||||
A transactional outbox at `~/.atlas-trace/events.jsonl.outbox.sqlite3` buffers up to
|
||||
10,000 pending events. `ATLAS_TRACE_FILE` changes the path prefix. Delivery resumes
|
||||
on the next hook or `python3 agents/hooks/atlas-trace.py --flush`; one flush sends up
|
||||
to 100 events from one project. The older JSONL/replay tools are fixture utilities.
|
||||
|
||||
## Activity and diagnostic APIs
|
||||
|
||||
`POST /api/agent-events` accepts `{project, events: [...]}` with at most 100 events.
|
||||
Each event needs `ts`, `agent`, `run`, integer `seq`, `phase: start|end` and `tool`.
|
||||
Optional fields include `path`, `lines`, `detail`, `source`, self-reported `intent`
|
||||
and explicit `replay`. Unknown fields are discarded. The UI distinguishes recorded
|
||||
facts from self-reports and leaves unmapped paths visible.
|
||||
|
||||
The daemon stores activity in `<cache>/activity.db`, independently of replaceable
|
||||
project indexes. Monotonic cursors, `(project, run, seq)` identities and retired
|
||||
sequence floors prevent duplicate or expired events from reappearing. Retention is
|
||||
bounded to 10,000 events; expired history is incomplete. Poll with
|
||||
`GET /api/agent-events?project=NAME&after=CURSOR&limit=200` on the graph's origin.
|
||||
|
||||
Daemon and frontend logs share the SQLite journal. `GET /api/logs` accepts `project`,
|
||||
`scope=unattributed`, `min_level`, literal `q`, `after` and `lines`; filters apply
|
||||
before counts and pagination. Unattributed records are not assigned to the selected
|
||||
project. Responses identify severity, source, receipt time, durable ID and database
|
||||
generation. A bounded memory fallback is explicitly marked `persistent:false`.
|
||||
|
||||
`POST /api/ui-log` batches frontend errors and request failures. Failed journal writes
|
||||
return 503 for retry. `GET /api/ui-log` reads the same journal; `path` names
|
||||
`activity.db`, while `export_path` names the rotating compatibility JSONL export.
|
||||
Changing that export does not change HTTP history. These APIs publish nothing
|
||||
externally.
|
||||
@@ -0,0 +1,149 @@
|
||||
# CBM Lens third-party notices
|
||||
|
||||
This document records imported source, package licenses and optional model provenance.
|
||||
Preserve the copyright and license headers in individual source files. The npm
|
||||
lockfile records the exact installed dependency versions.
|
||||
|
||||
## Galaxy source
|
||||
|
||||
The initial scene was adapted from [DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp),
|
||||
`feat/atlas-r1` at `bda46086`, on 2026-08-28. It is MIT licensed,
|
||||
Copyright (c) 2025 DeusData; the full license is in the repository's `LICENSE`.
|
||||
|
||||
| Lens file | Original under `graph-ui/src` |
|
||||
| --- | --- |
|
||||
| `src/galaxy/GraphScene.tsx` | `components/GraphScene.tsx` |
|
||||
| `src/galaxy/NodeCloud.tsx` | `components/NodeCloud.tsx` |
|
||||
| `src/galaxy/EdgeLines.tsx` | `components/EdgeLines.tsx` |
|
||||
| `src/galaxy/NodeLabels.tsx` | `components/NodeLabels.tsx` |
|
||||
| `src/galaxy/HaloLayer.tsx` | `components/HaloLayer.tsx` |
|
||||
| `src/galaxy/types.ts` | `lib/types.ts` |
|
||||
| `src/galaxy/density.ts` | `lib/density.ts` |
|
||||
|
||||
Adaptations include component-local canvas access, memoized scene measurements,
|
||||
frontend-specific props and types, and revised graph interaction/rendering.
|
||||
`NodeTooltipCard.tsx` is a separate implementation using Lens styling rather than
|
||||
the original Tailwind tooltip. File headers retain the initial import details;
|
||||
subsequent changes are recorded in Git history.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Recorded version | License |
|
||||
| --- | --- | --- |
|
||||
| `three` | 0.183.2 | MIT |
|
||||
| `@react-three/fiber` | 9.5.0 | MIT |
|
||||
| `@react-three/drei` | 10.7.7 | MIT |
|
||||
| `@react-three/postprocessing` | 3.0.4 | MIT |
|
||||
| `postprocessing` | 6.38.3 | Zlib |
|
||||
| `three-stdlib` | 2.36.1 | MIT |
|
||||
| `@types/three` | 0.183.1 | MIT |
|
||||
| `@huggingface/transformers` | 4.2.0 | Apache-2.0 |
|
||||
| `onnxruntime-web` | 1.26.0-dev.20260416-b7804b056c | MIT |
|
||||
| `sharp` | 0.35.0 | Apache-2.0 |
|
||||
| `react-markdown` | 10.1.0 | MIT |
|
||||
| `remark-gfm` | 4.0.1 | MIT |
|
||||
|
||||
These packages are used in their published form. `postprocessing` requires accurate
|
||||
origin attribution and identification of modified versions under its Zlib license.
|
||||
React, Vite, Vitest and Monaco use MIT; TypeScript and Playwright use Apache-2.0.
|
||||
`sharp` is a transitive Node dependency with a targeted override, not browser code.
|
||||
Full Markdown renderer license texts appear below.
|
||||
|
||||
## CodeAtlasIDE provenance
|
||||
|
||||
Provider, semantic IR, twin and semantic-search modules were ported from Bernhard
|
||||
Jackiewicz's CodeAtlasIDE project by the same author. Their source headers retain
|
||||
that origin. They are original contributions from Bernhard, not imports from an
|
||||
unrelated third party.
|
||||
|
||||
The concept of deriving an entry tour from project structure was inspired by
|
||||
Understand-Anything (MIT). `src/tours/tour-generator.ts` contains original Kahn
|
||||
traversal, role labels, wording and cycle handling; no source was copied from that
|
||||
project. Its direct code origin is CodeAtlasIDE.
|
||||
|
||||
## Optional browser models
|
||||
|
||||
Transformers.js and ONNX Runtime assets are served locally. Weights are not bundled;
|
||||
downloading them requires an explicit browser action. Source inference stays in the
|
||||
browser. Model policy pins revisions and permitted files; it does not execute remote
|
||||
model scripts.
|
||||
|
||||
The following metadata was recorded on 2026-09-08. Download sizes include tokenizer
|
||||
and configuration files and do not describe GPU memory requirements.
|
||||
|
||||
| Model | Fixed revision | Download bytes | Recorded license |
|
||||
| --- | --- | --- | --- |
|
||||
| [onnx-community/Qwen2.5-Coder-0.5B-Instruct](https://huggingface.co/onnx-community/Qwen2.5-Coder-0.5B-Instruct) | `f0292f665fd307846ff3c318a91a1bc29d091492` | 566366194 | Apache-2.0 for the base model |
|
||||
| [onnx-community/Qwen3-0.6B-ONNX](https://huggingface.co/onnx-community/Qwen3-0.6B-ONNX) | `da1453100cf3ff33ef56d17983fc7a8648706db6` | 578917626 | Apache-2.0 |
|
||||
| [LiquidAI/LFM2.5-1.2B-Instruct-ONNX](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-ONNX) | `10f72e70abf67ac0fd7ebf15bc5854726891d864` | 763763755 | Liquid AI license; review commercial terms above $10M annual revenue |
|
||||
| [onnx-community/Qwen3.5-2B-ONNX-OPT](https://huggingface.co/onnx-community/Qwen3.5-2B-ONNX-OPT) | `2ea7886f48b926aca97de8b0e041ffca7e3ebaa9` | 1402850762 | Apache-2.0 |
|
||||
|
||||
All use `q4f16`. Qwen3.5 loads the text decoder and embeddings only, not vision
|
||||
files. The Qwen2.5 ONNX weight file is 554935833 bytes; its published LFS SHA-256 is
|
||||
`60c076ac0d3910881fe0cad75997e803459a5f71bb74bcbcd82407ef83db7ba0`.
|
||||
The converter's model card does not declare a separate license; its
|
||||
[official base model](https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct)
|
||||
is Apache-2.0.
|
||||
|
||||
`src/browser-ai/model-policy.ts` retains complete file lists, published LFS hashes
|
||||
and allowed hosts, including `huggingface.co` and the recorded redirect host
|
||||
`us.aws.cdn.hf.co`. The loader does not recompute the published hashes.
|
||||
The browser context limit is 8192 tokens including 512 reserved output tokens;
|
||||
requests are counted with the selected template and tokenizer rather than silently
|
||||
truncated. Qwen3 and Qwen3.5 default to non-thinking mode. Runtime support is not a
|
||||
benchmark or a compatibility guarantee for every browser/GPU combination.
|
||||
|
||||
## Markdown renderer licenses
|
||||
|
||||
### react-markdown
|
||||
|
||||
```text
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Espen Hovlandsdal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
### remark-gfm
|
||||
|
||||
```text
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) Titus Wormer <tituswormer@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Die zweite Quelle: Schreibvorgaenge auf der Platte.
|
||||
*
|
||||
* Sie ist der Rueckfall fuer alles, was keine Werkzeug-Hooks kennt. Ein Agent,
|
||||
* der mit einem Editor oder einem Skript arbeitet, ruft keinen Hook auf; was
|
||||
* dabei trotzdem sichtbar ist, ist die Datei, die sich aendert.
|
||||
*
|
||||
* ## Was sie sieht, und was nicht
|
||||
*
|
||||
* Sie sieht **nur Schreibvorgaenge**. Kein Lesen, kein Suchen, kein Testlauf,
|
||||
* kein Befehl. Genau so ist jedes Ereignis gekennzeichnet (`source: "fs"`), und
|
||||
* das ist keine Formalie: wer spaeter eine Aufzeichnung ansieht, muss wissen,
|
||||
* dass die Ruhe zwischen zwei Schreibvorgaengen nicht Ruhe bedeutet, sondern
|
||||
* Arbeit, die hier nicht ankommt. Ein Protokoll, das seine eigene Blindheit
|
||||
* verschweigt, ist schlimmer als keines.
|
||||
*
|
||||
* Sie schreibt **keine Dateiinhalte**, so wenig wie der Hook daneben: ein
|
||||
* Ereignis nennt den Pfad und die Zeit, mehr nicht. Einen Zeilenbereich kann
|
||||
* sie nicht nennen, denn ein Dateisystem meldet keine Zeilen; das Feld fehlt
|
||||
* darum, statt eine Zahl zu erfinden.
|
||||
*
|
||||
* ## Aufruf
|
||||
*
|
||||
* node agents/hooks/atlas-trace-watch.mjs <verzeichnis>
|
||||
*
|
||||
* Wohin geschrieben wird, sagt ATLAS_TRACE_FILE; ohne die Variable ist es
|
||||
* ~/.atlas-trace/events.jsonl. Wie der Agent heisst, sagt ATLAS_AGENT_NAME;
|
||||
* ohne sie steht dort "file-writes", weil das die Wahrheit ueber diese Quelle
|
||||
* ist: sie kennt keinen Agenten, sie kennt eine Datei, die sich geaendert hat.
|
||||
*
|
||||
* Kein Paket, nur node:fs.
|
||||
*/
|
||||
import { appendFileSync, mkdirSync, watch } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const ROOT = process.argv[2] ?? process.cwd();
|
||||
const OUT = process.env['ATLAS_TRACE_FILE']
|
||||
?? join(homedir(), '.atlas-trace', 'events.jsonl');
|
||||
const AGENT = process.env['ATLAS_AGENT_NAME'] ?? 'file-writes';
|
||||
|
||||
/** Der Lauf. Acht Zeichen, wie die Kennung des Hooks daneben. */
|
||||
const RUN = String(Date.now()).slice(-8);
|
||||
|
||||
/** Was nie interessiert: Bauwerk, Fremdcode, Beweisbilder, das Protokoll selbst. */
|
||||
const IGNORE = /(^|\/)(node_modules|\.git|dist|cbm|vendor|models|verification|coverage)(\/|$)/;
|
||||
|
||||
/**
|
||||
* Derselbe Pfad zweimal in dieser Zeitspanne ist ein Ereignis, nicht zwei.
|
||||
*
|
||||
* Ein Editor schreibt gern mehrfach (Datei anlegen, Inhalt schreiben, Rechte
|
||||
* setzen), und ein Bild soll die Handlung zeigen und nicht die Schreibtechnik.
|
||||
*/
|
||||
const COALESCE_MS = 400;
|
||||
|
||||
const lastSeen = new Map();
|
||||
let seq = 0;
|
||||
|
||||
function record(path) {
|
||||
const now = Date.now();
|
||||
if (now - (lastSeen.get(path) ?? 0) < COALESCE_MS) {
|
||||
return;
|
||||
}
|
||||
lastSeen.set(path, now);
|
||||
seq += 1;
|
||||
const line = {
|
||||
ts: now,
|
||||
agent: AGENT,
|
||||
run: RUN,
|
||||
seq,
|
||||
phase: 'end',
|
||||
tool: 'Write',
|
||||
path,
|
||||
source: 'fs',
|
||||
detail: 'seen as a write on disk; reads, searches and test runs are invisible here',
|
||||
};
|
||||
try {
|
||||
appendFileSync(OUT, JSON.stringify(line) + '\n', 'utf8');
|
||||
process.stdout.write(`${new Date(now).toISOString()} ${path}\n`);
|
||||
} catch {
|
||||
/* Ein volles Dateisystem kostet das Protokoll, nicht die Arbeit. */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(dirname(OUT), { recursive: true });
|
||||
} catch {
|
||||
/* Steht es schon da, ist alles gut; steht es nicht, meldet der erste Schreibversuch es. */
|
||||
}
|
||||
|
||||
watch(ROOT, { recursive: true }, (_event, name) => {
|
||||
if (typeof name !== 'string' || IGNORE.test(name)) {
|
||||
return;
|
||||
}
|
||||
record(relative('.', name) || name);
|
||||
});
|
||||
|
||||
process.stdout.write(`watching ${ROOT}, appending to ${OUT}\n`);
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record tool metadata in a local outbox and POST it to the existing daemon.
|
||||
|
||||
Configure ATLAS_PROJECT to the indexed project name and ATLAS_DAEMON_URL when
|
||||
using a nondefault daemon port. No bridge, listener or external transmission.
|
||||
Pending events survive downtime and retry on the next hook or --flush.
|
||||
The hook always returns zero so observing work cannot break the tool call.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import sqlite3
|
||||
import uuid
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import argparse
|
||||
import pathlib
|
||||
import shlex
|
||||
import tempfile
|
||||
|
||||
DEFAULT_FILE = os.path.join(os.path.expanduser('~'), '.atlas-trace', 'events.jsonl')
|
||||
|
||||
def trace_file():
|
||||
return os.environ.get('ATLAS_TRACE_FILE') or DEFAULT_FILE
|
||||
|
||||
|
||||
def open_outbox():
|
||||
path = trace_file() + '.outbox.sqlite3'
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), mode=0o700, exist_ok=True)
|
||||
db = sqlite3.connect(path, timeout=0.5)
|
||||
os.chmod(path, 0o600)
|
||||
db.executescript("""
|
||||
PRAGMA journal_mode=WAL;
|
||||
CREATE TABLE IF NOT EXISTS counters(run TEXT PRIMARY KEY, seq INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS pending(id INTEGER PRIMARY KEY, project TEXT NOT NULL,
|
||||
payload TEXT NOT NULL);
|
||||
""")
|
||||
return db
|
||||
|
||||
|
||||
def enqueue(db, project, row):
|
||||
# Counter allocation and enqueue are one SQLite transaction: parallel hooks
|
||||
# cannot assign the same run/sequence pair and silently discard an event.
|
||||
with db:
|
||||
db.execute('BEGIN IMMEDIATE')
|
||||
db.execute('INSERT INTO counters VALUES(?,1) ON CONFLICT(run) DO UPDATE SET seq=seq+1',
|
||||
(row['run'],))
|
||||
row['seq'] = db.execute('SELECT seq FROM counters WHERE run=?', (row['run'],)).fetchone()[0]
|
||||
db.execute('INSERT INTO pending(project,payload) VALUES(?,?)',
|
||||
(project, json.dumps(row, ensure_ascii=False)))
|
||||
# A prolonged daemon outage cannot fill the disk without bound. Sequence
|
||||
# gaps remain visible when older undelivered metadata expires.
|
||||
db.execute('DELETE FROM pending WHERE id IN (SELECT id FROM pending ORDER BY id DESC LIMIT -1 OFFSET 10000)')
|
||||
|
||||
|
||||
def flush(db):
|
||||
origin = os.environ.get('ATLAS_DAEMON_URL', 'http://127.0.0.1:9749')
|
||||
parsed = urllib.parse.urlsplit(origin)
|
||||
if (parsed.scheme != 'http' or parsed.hostname not in ('127.0.0.1', 'localhost')
|
||||
or parsed.username or parsed.password or parsed.path not in ('', '/')
|
||||
or parsed.query or parsed.fragment):
|
||||
return # Never forward local metadata to an external endpoint.
|
||||
first = db.execute('SELECT project FROM pending ORDER BY id LIMIT 1').fetchone()
|
||||
if not first:
|
||||
return
|
||||
rows = db.execute('SELECT id,payload FROM pending WHERE project=? ORDER BY id LIMIT 100',
|
||||
first).fetchall()
|
||||
data = json.dumps({'project': first[0], 'events': [json.loads(row[1]) for row in rows]}).encode()
|
||||
request = urllib.request.Request(origin.rstrip('/') + '/api/agent-events', data=data,
|
||||
headers={'Content-Type': 'application/json'})
|
||||
# No environment proxy, redirects, or second server; post only to loopback.
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
|
||||
with opener.open(request, timeout=0.75) as response:
|
||||
result = json.load(response)
|
||||
if response.status != 200 or result.get('accepted', 0) + result.get('duplicates', 0) != len(rows):
|
||||
return
|
||||
with db:
|
||||
db.executemany('DELETE FROM pending WHERE id=?', [(row[0],) for row in rows])
|
||||
|
||||
|
||||
def line_span(file_path, args):
|
||||
"""Use an explicit requested source range, never infer it after an edit.
|
||||
|
||||
PostToolUse runs after mutation. Searching old_string could locate a different,
|
||||
unchanged occurrence and falsely attribute that line to the observed edit.
|
||||
"""
|
||||
offset = args.get('offset')
|
||||
limit = args.get('limit')
|
||||
if isinstance(offset, int) and not isinstance(offset, bool) and offset > 0:
|
||||
if isinstance(limit, int) and not isinstance(limit, bool) and limit > 0:
|
||||
return [offset, offset + limit - 1]
|
||||
return [offset, offset]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def daemon_origin(value):
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
if (parsed.scheme != 'http' or parsed.hostname not in ('127.0.0.1', 'localhost')
|
||||
or parsed.username or parsed.password or parsed.path not in ('', '/')
|
||||
or parsed.query or parsed.fragment):
|
||||
raise ValueError('The daemon URL must be a loopback HTTP origin.')
|
||||
return value.rstrip('/')
|
||||
|
||||
|
||||
def install_claude(root, project, origin):
|
||||
"""Explicit, project-local install; preserve unrelated settings and hooks.
|
||||
|
||||
No package downloads or network requests. Refuse symlinks, malformed settings,
|
||||
and conflicting older installations rather than replace user configuration.
|
||||
"""
|
||||
root = pathlib.Path(root).resolve(strict=True)
|
||||
if not root.is_dir() or not project or len(project.encode()) > 255:
|
||||
raise ValueError('Choose the indexed repository root and exact project name.')
|
||||
origin = daemon_origin(origin)
|
||||
directory = root / '.claude'
|
||||
hook_dir = directory / 'hooks'
|
||||
settings = directory / 'settings.local.json'
|
||||
target = hook_dir / 'cbm-atlas-trace.py'
|
||||
if any(path.is_symlink() for path in (directory, hook_dir, settings, target)):
|
||||
raise ValueError('Refusing symlinked hook or settings paths.')
|
||||
original = settings.read_bytes() if settings.exists() else None
|
||||
config = json.loads(original) if original is not None else {}
|
||||
if not isinstance(config, dict):
|
||||
raise ValueError('Existing settings must be a JSON object.')
|
||||
hooks = config.setdefault('hooks', {})
|
||||
if not isinstance(hooks, dict):
|
||||
raise ValueError('Existing hooks must be an object.')
|
||||
entries = hooks.setdefault('PostToolUse', [])
|
||||
if not isinstance(entries, list):
|
||||
raise ValueError('Existing PostToolUse hooks must be an array.')
|
||||
command = ('ATLAS_PROJECT=' + shlex.quote(project) + ' ATLAS_AGENT_NAME=' + shlex.quote('Claude Code')
|
||||
+ ' ATLAS_DAEMON_URL=' + shlex.quote(origin)
|
||||
+ ' python3 ' + shlex.quote(str(target)))
|
||||
proposed = {'matcher': '', 'hooks': [{'type': 'command', 'command': command, 'timeout': 3}]}
|
||||
installed = proposed in entries
|
||||
for entry in entries:
|
||||
for handler in entry.get('hooks', []) if isinstance(entry, dict) else []:
|
||||
if not isinstance(handler, dict):
|
||||
continue
|
||||
try:
|
||||
words = shlex.split(str(handler.get('command', '')))
|
||||
except ValueError:
|
||||
words = []
|
||||
references_target = any(word in (str(target), '.claude/hooks/cbm-atlas-trace.py',
|
||||
'./.claude/hooks/cbm-atlas-trace.py') for word in words)
|
||||
if references_target and entry != proposed:
|
||||
raise ValueError('An existing Atlas hook differs. Review its matcher, command and timeout before changing it.')
|
||||
source = pathlib.Path(__file__).read_bytes()
|
||||
if target.exists() and target.read_bytes() != source:
|
||||
raise ValueError('An existing Atlas hook file differs. Review it before replacing it.')
|
||||
if not installed:
|
||||
entries.append(proposed)
|
||||
encoded = (json.dumps(config, indent=2, ensure_ascii=False) + '\n').encode()
|
||||
# A changed file is never overwritten after our read. Directory operations
|
||||
# happen only after the complete, reviewable configuration was validated.
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
hook_dir.mkdir(mode=0o700, exist_ok=True)
|
||||
if (settings.read_bytes() if settings.exists() else None) != original:
|
||||
raise ValueError('Settings changed during setup; retry after reviewing them.')
|
||||
created_hook = not target.exists()
|
||||
if created_hook:
|
||||
with target.open('xb') as handle:
|
||||
handle.write(source)
|
||||
target.chmod(0o600)
|
||||
if not installed:
|
||||
descriptor, temporary = tempfile.mkstemp(prefix='.cbm-atlas-settings-', dir=directory)
|
||||
try:
|
||||
with os.fdopen(descriptor, 'wb') as handle:
|
||||
handle.write(encoded)
|
||||
if (settings.read_bytes() if settings.exists() else None) != original:
|
||||
raise ValueError('Settings changed during setup; nothing was replaced.')
|
||||
os.replace(temporary, settings)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
return {'installed': True, 'changed': not installed or created_hook,
|
||||
'settings': str(settings), 'hook': str(target), 'project': project,
|
||||
'daemon': origin, 'next': 'Start a new Claude Code session in this repository, perform a tool call, then load activity.'}
|
||||
|
||||
|
||||
def setup_main():
|
||||
parser = argparse.ArgumentParser(description='Install the local Code Atlas Claude Code tool hook.')
|
||||
parser.add_argument('--install-claude', action='store_true', required=True)
|
||||
parser.add_argument('--root', required=True, help='Exact indexed repository root')
|
||||
parser.add_argument('--project', required=True, help='Exact indexed project name')
|
||||
parser.add_argument('--daemon-url', default='http://127.0.0.1:9749')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
result = install_claude(args.root, args.project, args.daemon_url)
|
||||
except Exception as error:
|
||||
print('Atlas setup: ' + str(error), file=sys.stderr)
|
||||
return 1
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
if '--flush' in sys.argv:
|
||||
try:
|
||||
with open_outbox() as db:
|
||||
flush(db)
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
return 0
|
||||
try:
|
||||
path = trace_file()
|
||||
directory = os.path.dirname(path)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
tool = payload.get('tool_name') or ''
|
||||
args = payload.get('tool_input') or {}
|
||||
if not isinstance(args, dict):
|
||||
args = {}
|
||||
file_path = args.get('file_path') or args.get('notebook_path') or ''
|
||||
run = str(payload.get('session_id') or uuid.uuid4())
|
||||
root = str(payload.get('cwd') or os.getcwd())
|
||||
project = os.environ.get('ATLAS_PROJECT', os.path.basename(root.rstrip(os.sep)))
|
||||
|
||||
row = {
|
||||
'ts': int(time.time() * 1000),
|
||||
'agent': os.environ.get('ATLAS_AGENT_NAME', 'agent'),
|
||||
'run': run,
|
||||
'seq': 0,
|
||||
'phase': 'end',
|
||||
'tool': tool,
|
||||
'path': str(file_path or args.get('path') or ''),
|
||||
# Commands, patterns and descriptions may contain credentials.
|
||||
'detail': '',
|
||||
'source': 'tool-hook',
|
||||
}
|
||||
span = line_span(file_path, args) if tool == 'Read' else None
|
||||
if span is not None:
|
||||
row['lines'] = span
|
||||
|
||||
if os.path.isabs(row['path']):
|
||||
row['path'] = os.path.relpath(row['path'], root)
|
||||
with open_outbox() as db:
|
||||
enqueue(db, project, row)
|
||||
flush(db)
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(setup_main() if '--install-claude' in sys.argv else main())
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Reproducible local producer tests; no external network or agent accounts."""
|
||||
import concurrent.futures
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location('atlas_trace', Path(__file__).with_name('atlas-trace.py'))
|
||||
hook = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(hook)
|
||||
|
||||
|
||||
class TraceTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory()
|
||||
self.environment = patch.dict(os.environ, {
|
||||
'ATLAS_TRACE_FILE': os.path.join(self.directory.name, 'events'),
|
||||
'ATLAS_DAEMON_URL': 'http://127.0.0.1:9749',
|
||||
})
|
||||
self.environment.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.environment.stop()
|
||||
self.directory.cleanup()
|
||||
|
||||
def record(self, number):
|
||||
with hook.open_outbox() as db:
|
||||
hook.enqueue(db, 'fixture', {'run': 'concurrent-run', 'seq': 0, 'ts': number,
|
||||
'agent': 'fixture', 'tool': 'Read', 'phase': 'end'})
|
||||
|
||||
def assert_private_tool_input_is_omitted(self, tool, field, value, private_marker):
|
||||
root = Path(self.directory.name)
|
||||
source = root / 'src' / 'example.py'
|
||||
payload = {'tool_name': tool, 'session_id': 'privacy-run', 'cwd': str(root),
|
||||
'tool_input': {field: value, 'file_path': str(source)}}
|
||||
generated = []
|
||||
original_enqueue = hook.enqueue
|
||||
|
||||
def capture_enqueue(db, project, row):
|
||||
generated.append(json.loads(json.dumps(row)))
|
||||
original_enqueue(db, project, row)
|
||||
|
||||
with patch.dict(os.environ, {'ATLAS_PROJECT': 'fixture', 'ATLAS_AGENT_NAME': 'fixture'}), \
|
||||
patch.object(hook.sys, 'argv', ['atlas-trace.py']), \
|
||||
patch.object(hook.sys, 'stdin', io.StringIO(json.dumps(payload))), \
|
||||
patch.object(hook.time, 'time', return_value=1234.5), \
|
||||
patch.object(hook, 'enqueue', side_effect=capture_enqueue), \
|
||||
patch.object(hook.urllib.request, 'build_opener') as transport:
|
||||
transport.return_value.open.side_effect = OSError('offline fixture')
|
||||
self.assertEqual(hook.main(), 0)
|
||||
transport.return_value.open.assert_called_once()
|
||||
request = transport.return_value.open.call_args.args[0]
|
||||
|
||||
with hook.open_outbox() as db:
|
||||
pending = db.execute('SELECT project,payload FROM pending').fetchall()
|
||||
self.assertEqual(len(generated), 1)
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertEqual(pending[0][0], 'fixture')
|
||||
outbound = json.loads(request.data)
|
||||
self.assertEqual(outbound['project'], 'fixture')
|
||||
self.assertEqual(len(outbound['events']), 1)
|
||||
for surface, event, serialized in (
|
||||
('generated', generated[0], json.dumps(generated[0])),
|
||||
('persisted', json.loads(pending[0][1]), pending[0][1]),
|
||||
('outgoing', outbound['events'][0], request.data.decode())):
|
||||
with self.subTest(surface=surface):
|
||||
self.assertEqual(event['tool'], tool)
|
||||
self.assertEqual(event['path'], os.path.join('src', 'example.py'))
|
||||
self.assertEqual(event['agent'], 'fixture')
|
||||
self.assertEqual(event['run'], 'privacy-run')
|
||||
self.assertEqual(event['phase'], 'end')
|
||||
self.assertEqual(event['source'], 'tool-hook')
|
||||
self.assertEqual(event['ts'], 1234500)
|
||||
self.assertNotIn(private_marker, serialized)
|
||||
self.assertNotIn(value, serialized)
|
||||
|
||||
def test_command_private_values_never_enter_event_payloads(self):
|
||||
marker = 'SYNTHETIC_COMMAND_PRIVATE_VALUE_71E5'
|
||||
self.assert_private_tool_input_is_omitted(
|
||||
'Bash', 'command', 'AUTH_TOKEN=' + marker + ' example-tool --quiet', marker)
|
||||
|
||||
def test_search_patterns_never_enter_event_payloads(self):
|
||||
marker = 'SYNTHETIC_PATTERN_PRIVATE_VALUE_83A6'
|
||||
self.assert_private_tool_input_is_omitted('Grep', 'pattern', marker, marker)
|
||||
|
||||
def test_description_private_values_never_enter_event_payloads(self):
|
||||
marker = 'SYNTHETIC_DESCRIPTION_PRIVATE_VALUE_92B7'
|
||||
self.assert_private_tool_input_is_omitted(
|
||||
'Bash', 'description', 'Inspect the value ' + marker, marker)
|
||||
|
||||
def test_concurrent_hooks_allocate_unique_sequences_and_keep_pending_after_failure(self):
|
||||
with hook.open_outbox():
|
||||
pass
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
|
||||
list(pool.map(self.record, range(20)))
|
||||
with hook.open_outbox() as db:
|
||||
rows = db.execute('SELECT payload FROM pending').fetchall()
|
||||
self.assertEqual(sorted(json.loads(row[0])['seq'] for row in rows), list(range(1, 21)))
|
||||
with patch.object(hook.urllib.request, 'build_opener', side_effect=OSError('offline')):
|
||||
with self.assertRaises(OSError):
|
||||
hook.flush(db)
|
||||
self.assertEqual(db.execute('SELECT count(*) FROM pending').fetchone()[0], 20)
|
||||
|
||||
def test_external_destinations_and_credentials_are_never_contacted(self):
|
||||
self.record(1)
|
||||
for origin in ('https://example.com', 'http://example.com',
|
||||
'http://127.0.0.1.evil.test', 'http://secret@localhost:9749',
|
||||
'http://localhost:9749/redirect', 'http://localhost:9749?secret=1'):
|
||||
with patch.dict(os.environ, {'ATLAS_DAEMON_URL': origin}), \
|
||||
patch.object(hook.urllib.request, 'build_opener') as network, \
|
||||
hook.open_outbox() as db:
|
||||
hook.flush(db)
|
||||
network.assert_not_called()
|
||||
self.assertEqual(db.execute('SELECT count(*) FROM pending').fetchone()[0], 1)
|
||||
|
||||
def test_explicit_setup_preserves_settings_is_idempotent_and_escapes_names(self):
|
||||
root = Path(self.directory.name) / "repo with 'quote'"
|
||||
(root / '.claude').mkdir(parents=True)
|
||||
settings = root / '.claude/settings.local.json'
|
||||
original = {'permissions': {'allow': ['Read']}, 'hooks': {'PostToolUse': [
|
||||
{'matcher': 'Read', 'hooks': [{'type': 'command', 'command': 'existing-hook'}]}]}}
|
||||
settings.write_text(json.dumps(original))
|
||||
with patch.object(hook.urllib.request, 'build_opener') as network:
|
||||
result = hook.install_claude(root, "project'; echo bad", 'http://127.0.0.1:9749')
|
||||
again = hook.install_claude(root, "project'; echo bad", 'http://127.0.0.1:9749')
|
||||
network.assert_not_called()
|
||||
self.assertTrue(result['changed'])
|
||||
self.assertFalse(again['changed'])
|
||||
installed = json.loads(settings.read_text())
|
||||
self.assertEqual(installed['permissions'], original['permissions'])
|
||||
self.assertEqual(installed['hooks']['PostToolUse'][0], original['hooks']['PostToolUse'][0])
|
||||
self.assertEqual(len(installed['hooks']['PostToolUse']), 2)
|
||||
command = installed['hooks']['PostToolUse'][1]['hooks'][0]['command']
|
||||
self.assertEqual(hook.shlex.split(command)[0], "ATLAS_PROJECT=project'; echo bad")
|
||||
self.assertEqual(hook.shlex.split(command)[-1], str(root.resolve() / '.claude/hooks/cbm-atlas-trace.py'))
|
||||
before = settings.read_bytes()
|
||||
with self.assertRaises(ValueError):
|
||||
hook.install_claude(root, 'another-project', 'http://127.0.0.1:9749')
|
||||
self.assertEqual(settings.read_bytes(), before)
|
||||
|
||||
def test_setup_refuses_symlinks_malformed_settings_and_external_daemons_before_writes(self):
|
||||
root = Path(self.directory.name) / 'repo'
|
||||
root.mkdir()
|
||||
with self.assertRaises(ValueError):
|
||||
hook.install_claude(root, 'fixture', 'https://example.com')
|
||||
self.assertFalse((root / '.claude').exists())
|
||||
(root / '.claude').mkdir()
|
||||
settings = root / '.claude/settings.local.json'
|
||||
settings.write_text('malformed user data')
|
||||
with self.assertRaises(ValueError):
|
||||
hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749')
|
||||
self.assertEqual(settings.read_text(), 'malformed user data')
|
||||
settings.unlink()
|
||||
target = root / 'external-settings.json'
|
||||
target.write_text('{}')
|
||||
settings.symlink_to(target)
|
||||
with self.assertRaises(ValueError):
|
||||
hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749')
|
||||
self.assertEqual(target.read_text(), '{}')
|
||||
self.assertFalse((root / '.claude/hooks').exists())
|
||||
|
||||
def test_setup_refuses_a_second_execution_with_a_different_matcher_or_timeout(self):
|
||||
root = Path(self.directory.name) / 'TEST fixture'
|
||||
root.mkdir()
|
||||
hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749')
|
||||
settings = root / '.claude/settings.local.json'
|
||||
pristine = json.loads(settings.read_text())
|
||||
for change in ('matcher', 'timeout', 'relative-command'):
|
||||
config = json.loads(json.dumps(pristine))
|
||||
entry = config['hooks']['PostToolUse'][0]
|
||||
if change == 'matcher':
|
||||
entry['matcher'] = 'Read'
|
||||
elif change == 'timeout':
|
||||
entry['hooks'][0]['timeout'] = 5
|
||||
else:
|
||||
entry['hooks'][0]['command'] = 'python3 .claude/hooks/cbm-atlas-trace.py'
|
||||
settings.write_text(json.dumps(config))
|
||||
before = settings.read_bytes()
|
||||
with self.assertRaisesRegex(ValueError, 'existing Atlas hook differs'):
|
||||
hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749')
|
||||
self.assertEqual(settings.read_bytes(), before)
|
||||
self.assertEqual(len(json.loads(settings.read_text())['hooks']['PostToolUse']), 1)
|
||||
|
||||
def test_post_edit_range_is_unknown_instead_of_an_unmodified_duplicate_occurrence(self):
|
||||
source = Path(self.directory.name) / 'edited.txt'
|
||||
source.write_text('new value\nold value\n')
|
||||
with patch('builtins.open', side_effect=AssertionError('Metadata hook must not read source')):
|
||||
self.assertIsNone(hook.line_span(str(source), {'old_string': 'old value', 'new_string': 'new value'}))
|
||||
self.assertEqual(hook.line_span(str(source), {'offset': 8, 'limit': 3}), [8, 10])
|
||||
self.assertIsNone(hook.line_span(str(source), {'offset': True}))
|
||||
|
||||
def test_acknowledged_duplicate_retry_removes_only_acknowledged_rows(self):
|
||||
self.record(1)
|
||||
self.record(2)
|
||||
class Response:
|
||||
status = 200
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *args): return None
|
||||
def read(self): return b'{"accepted":0,"duplicates":2}'
|
||||
class Opener:
|
||||
def open(self, request, timeout):
|
||||
self_body = json.loads(request.data)
|
||||
assert request.full_url == 'http://127.0.0.1:9749/api/agent-events'
|
||||
assert self_body['project'] == 'fixture'
|
||||
assert len(self_body['events']) == 2
|
||||
return Response()
|
||||
with patch.object(hook.urllib.request, 'build_opener', return_value=Opener()), \
|
||||
hook.open_outbox() as db:
|
||||
hook.flush(db)
|
||||
self.assertEqual(db.execute('SELECT count(*) FROM pending').fetchone()[0], 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1 +0,0 @@
|
||||
{"$schema":"https://ui.shadcn.com/schema.json","style":"new-york","rsc":false,"tsx":true,"tailwind":{"config":"","css":"src/styles/globals.css","baseColor":"neutral","cssVariables":true},"aliases":{"components":"@/components","utils":"@/lib/utils","ui":"@/components/ui","lib":"@/lib","hooks":"@/hooks"},"iconLibrary":"lucide"}
|
||||
@@ -0,0 +1,70 @@
|
||||
# ADR 0001: Optional local inference and model evidence
|
||||
|
||||
## Decision
|
||||
|
||||
CBM Lens offers optional browser inference. Weights are downloaded only after the
|
||||
user opts in; they are not embedded in the executable. Model selection remains a
|
||||
user choice. Browser model IDs, fixed revisions, allowed files and licenses live in
|
||||
`src/browser-ai/model-policy.ts`; [THIRD_PARTY.md](../../THIRD_PARTY.md) documents them.
|
||||
|
||||
The older GGUF sidecar catalogue remains supported by local scripts and its recorded
|
||||
evaluation. It is separate from the browser ONNX catalogue. Its measurements do not
|
||||
predict browser performance, another machine's performance or a model's general rank.
|
||||
|
||||
## Sidecar model sources
|
||||
|
||||
The six Q4_K_M candidates below were recorded in August 2026. Class A used a 3072-token
|
||||
context and class B used 8192 tokens. Exact download sizes refer to those evaluated
|
||||
files, not runtime memory. Checksums are retained in `models/SHA256SUMS`.
|
||||
|
||||
| Model | Class | Download source | Recorded size | License recorded for the base model |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Qwen3.5-2B | A | [unsloth/Qwen3.5-2B-GGUF](https://huggingface.co/unsloth/Qwen3.5-2B-GGUF) | 1280835840 Bytes | Apache 2.0 |
|
||||
| LFM2.5-1.2B-Instruct | A | [LiquidAI/LFM2.5-1.2B-Instruct-GGUF](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-GGUF) | 730895168 Bytes | LFM Open License v1.0; commercial terms apply at $10M annual revenue |
|
||||
| MiniCPM5-1B | A | [openbmb/MiniCPM5-1B-GGUF](https://huggingface.co/openbmb/MiniCPM5-1B-GGUF) | 688065920 Bytes | Apache 2.0 |
|
||||
| Qwen2.5-Coder-1.5B-Instruct | A | [Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF](https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF) | 1117320768 Bytes | Apache 2.0 |
|
||||
| Qwen3.5-4B | B | [unsloth/Qwen3.5-4B-MTP-GGUF](https://huggingface.co/unsloth/Qwen3.5-4B-MTP-GGUF) | 2834975040 Bytes | Apache 2.0 |
|
||||
| Gemma 4 E4B-it | B | [unsloth/gemma-4-E4B-it-GGUF](https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF) | 4977171584 Bytes | Apache 2.0 |
|
||||
|
||||
Review current model-card terms before downloading or redistribution. The recorded
|
||||
LFM license allows free commercial use below $10M annual revenue; use beyond that
|
||||
threshold requires separate consideration. No evaluation result overrides licensing.
|
||||
|
||||
## Evaluation limits
|
||||
|
||||
`verification/w5/eval.json` records 44 fixture questions, including 23 in German,
|
||||
with temperature 0 and seed 42. The figures below summarize that saved run. Citation
|
||||
compliance measures the evaluation's citation rule, not factual correctness.
|
||||
|
||||
| Model | Class | Pass rate | Citation compliance | Mean tokens/s, rounded |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Qwen3.5-2B | A | 0.682 | 0.932 | 86 |
|
||||
| LFM2.5-1.2B | A | 0.295 | 0.432 | 110 |
|
||||
| MiniCPM5-1B | A | 0.250 | 0.545 | 171 |
|
||||
| Qwen2.5-Coder-1.5B | A | 0.227 | 0.705 | 84 |
|
||||
| Qwen3.5-4B | B | 0.818 | 0.955 | 38 |
|
||||
| gemma-4-E4B | B | 0.841 | 1.000 | 32 |
|
||||
|
||||
The recorded class recommendations were Qwen3.5-2B and gemma-4-E4B, with pass-rate
|
||||
and citation thresholds of 0.6 and 0.9. They are recommendations, not installed
|
||||
defaults. A reported repeat run has no separate saved artifact and is not independent
|
||||
reproduction evidence. The original record does not measure unscorable citations;
|
||||
missing measurements must not be displayed as zero.
|
||||
|
||||
## Engine boundary
|
||||
|
||||
The sidecar evaluation used llama.cpp on Apple Silicon with Metal and GGUF files.
|
||||
Its source/build record is in [runtime provenance](../../vendor/llama/HERKUNFT.md).
|
||||
`llm/start.sh` also supports a local model cache and OpenAI-compatible transport;
|
||||
the class-specific script options reproduce the recorded evaluation.
|
||||
|
||||
Browser inference uses a different runtime and format. Compare actual device and
|
||||
workload behavior before drawing performance conclusions, especially prompt prefill,
|
||||
GPU memory limits and large-file handling. Relevant engine sources retained from
|
||||
the original investigation are [llama.cpp](https://github.com/ggml-org/llama.cpp),
|
||||
[wllama](https://github.com/ngxson/wllama), [WebLLM](https://webllm.mlc.ai/docs/),
|
||||
[Transformers.js](https://github.com/huggingface/transformers.js),
|
||||
[WebGPU limits](https://developer.mozilla.org/en-US/docs/Web/API/GPUSupportedLimits),
|
||||
and the [ggml WebGPU study](https://arxiv.org/html/2605.20706v1).
|
||||
These references explain the formats and tradeoffs; the sidecar timings above are
|
||||
not browser benchmarks.
|
||||
@@ -0,0 +1,357 @@
|
||||
[
|
||||
{
|
||||
"id": "who-01-de",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Wer ruft createUser auf?",
|
||||
"expected": ["registerUserRoutes", "create"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-02-en",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.services.userService.listUsers",
|
||||
"question": "Who calls listUsers?",
|
||||
"expected": ["registerUserRoutes", "createUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-03-de",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.util.validate.validateUser",
|
||||
"question": "Wer ruft validateUser?",
|
||||
"expected": ["createUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-04-en",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.repo.db.insert",
|
||||
"question": "Who calls insert?",
|
||||
"expected": ["createUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-05-de",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.services.orderService.getOrder",
|
||||
"question": "Wer ruft getOrder?",
|
||||
"expected": ["registerOrderRoutes"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-06-en",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.repo.db.query",
|
||||
"question": "Who calls query?",
|
||||
"expected": ["listUsers"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-07-de",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.routes.users.registerUserRoutes",
|
||||
"question": "Wer ruft registerUserRoutes?",
|
||||
"expected": ["createApp"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "who-08-en",
|
||||
"class": "who-calls",
|
||||
"focusQn": "src.config.loadConfig",
|
||||
"question": "Who calls loadConfig?",
|
||||
"expected": ["main"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-01-de",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Was macht createUser?",
|
||||
"expected": ["validateUser", "insert"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-02-en",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.services.userService.listUsers",
|
||||
"question": "What does listUsers do?",
|
||||
"expected": ["query"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-03-de",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.services.orderService.getOrder",
|
||||
"question": "Was macht getOrder?",
|
||||
"expected": ["validateId", "query"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-04-en",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.repo.db.hotspotScan",
|
||||
"question": "What does hotspotScan do?",
|
||||
"expected": ["query"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-05-de",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Was macht main?",
|
||||
"expected": ["loadConfig", "createApp"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-06-en",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.server.createApp",
|
||||
"question": "What does createApp do?",
|
||||
"expected": ["registerUserRoutes"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-07-de",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.config.loadConfig",
|
||||
"question": "Was macht loadConfig?",
|
||||
"expected": ["loadConfig"],
|
||||
"depth": 0
|
||||
},
|
||||
{
|
||||
"id": "what-08-en",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.util.validate.ValidationError",
|
||||
"question": "What is ValidationError?",
|
||||
"expected": ["validate.ts"],
|
||||
"depth": 0
|
||||
},
|
||||
{
|
||||
"id": "what-09-de",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.services.userService.create",
|
||||
"question": "Was tut `userService.create`?",
|
||||
"expected": ["createUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "what-10-en",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.routes.orders.registerOrderRoutes",
|
||||
"question": "What does registerOrderRoutes do?",
|
||||
"expected": ["getOrder"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "err-01-de",
|
||||
"class": "why-error",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Welche Fehler kann createUser werfen?",
|
||||
"expected": ["ValidationError"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "err-02-en",
|
||||
"class": "why-error",
|
||||
"focusQn": "src.util.validate.validateUser",
|
||||
"question": "What errors can validateUser raise?",
|
||||
"expected": ["ValidationError"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "err-03-de",
|
||||
"class": "why-error",
|
||||
"focusQn": "src.util.validate.validateId",
|
||||
"question": "Welche Ausnahme wirft validateId?",
|
||||
"expected": ["ValidationError"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "err-04-en",
|
||||
"class": "why-error",
|
||||
"focusQn": "src.services.orderService.getOrder",
|
||||
"question": "What errors can getOrder raise?",
|
||||
"expected": ["ValidationError"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "err-05-de",
|
||||
"class": "why-error",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Warum schlaegt createUser fehl, wenn DB_URL nicht gesetzt ist?",
|
||||
"expected": ["ValidationError", "DB_URL"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "err-06-en",
|
||||
"class": "why-error",
|
||||
"focusQn": "src.repo.db.insert",
|
||||
"question": "What errors can insert raise?",
|
||||
"expected": ["insert"],
|
||||
"forbidden": ["ValidationError"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "if-01-de",
|
||||
"class": "what-if",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Was passiert, wenn ich createUser aendere?",
|
||||
"expected": ["registerUserRoutes", "create"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "if-02-en",
|
||||
"class": "what-if",
|
||||
"focusQn": "src.services.userService.listUsers",
|
||||
"question": "What happens if I change listUsers?",
|
||||
"expected": ["registerUserRoutes"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "if-03-de",
|
||||
"class": "what-if",
|
||||
"focusQn": "src.repo.db.query",
|
||||
"question": "Welche Auswirkung haette eine Aenderung an query?",
|
||||
"expected": ["listUsers"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "if-04-en",
|
||||
"class": "what-if",
|
||||
"focusQn": "src.util.validate.validateUser",
|
||||
"question": "What is the blast radius of validateUser?",
|
||||
"expected": ["createUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "if-05-de",
|
||||
"class": "what-if",
|
||||
"focusQn": "src.repo.db.insert",
|
||||
"question": "Was passiert, wenn ich insert aendere?",
|
||||
"expected": ["createUser"],
|
||||
"depth": 2
|
||||
},
|
||||
{
|
||||
"id": "if-06-en",
|
||||
"class": "what-if",
|
||||
"focusQn": "src.routes.users.registerUserRoutes",
|
||||
"question": "What happens if registerUserRoutes changes?",
|
||||
"expected": ["createApp"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "entry-01-de",
|
||||
"class": "where-entry",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Welche Routen gibt es in diesem Projekt?",
|
||||
"expected": ["/users"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "entry-02-en",
|
||||
"class": "where-entry",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Which HTTP routes does this project expose?",
|
||||
"expected": ["/users", "/orders"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "entry-03-de",
|
||||
"class": "where-entry",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Wo faengt dieses Projekt an?",
|
||||
"expected": ["src/server.ts"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "entry-04-en",
|
||||
"class": "where-entry",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Where do I start reading this project?",
|
||||
"expected": ["src/server.ts"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "entry-05-de",
|
||||
"class": "where-entry",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Welche Route fuehrt zu createUser?",
|
||||
"expected": ["/users"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "cmp-01-de",
|
||||
"class": "compare",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Was ist der Unterschied zwischen createUser und listUsers?",
|
||||
"expected": ["validateUser", "query"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "cmp-02-en",
|
||||
"class": "compare",
|
||||
"focusQn": "src.services.orderService.getOrder",
|
||||
"question": "What is the difference between getOrder and createUser?",
|
||||
"expected": ["validateId", "validateUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "cmp-03-de",
|
||||
"class": "compare",
|
||||
"focusQn": "src.services.userService.listUsers",
|
||||
"question": "Vergleiche listUsers und createUser.",
|
||||
"expected": ["query", "validateUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "cmp-04-en",
|
||||
"class": "compare",
|
||||
"focusQn": "src.repo.db.hotspotScan",
|
||||
"question": "What is the difference between hotspotScan and walk?",
|
||||
"expected": ["query"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "over-01-de",
|
||||
"class": "overview",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Gib mir einen Ueberblick ueber dieses Projekt.",
|
||||
"expected": ["TypeScript"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "over-02-en",
|
||||
"class": "overview",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "What is this project made of?",
|
||||
"expected": ["TypeScript"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "over-03-de",
|
||||
"class": "overview",
|
||||
"focusQn": "src.server.main",
|
||||
"question": "Wie ist die Architektur dieses Projekts aufgebaut?",
|
||||
"expected": ["symbol"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "honest-01-de",
|
||||
"class": "other",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "Hallo, alles klar bei dir?",
|
||||
"expected": ["no card"],
|
||||
"forbidden": ["createUser"],
|
||||
"depth": 1
|
||||
},
|
||||
{
|
||||
"id": "honest-02-en",
|
||||
"class": "what-is",
|
||||
"focusQn": "src.services.userService.createUser",
|
||||
"question": "What is @notASymbolAnywhereInThisRepo?",
|
||||
"expected": ["no card"],
|
||||
"forbidden": ["createUser"],
|
||||
"depth": 1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Architecture decisions
|
||||
|
||||
> Preview examples for cbm-preview. These editable notes capture frontend design choices discussed during development; they are not a historical ADR archive or graph-inferred facts. Status: proposed for review.
|
||||
|
||||
## 001: Explain architecture from evidence
|
||||
|
||||
**Status:** Proposed · **Scope:** Architecture workspace
|
||||
|
||||
### Context
|
||||
|
||||
A repository map explains where code lives. System structure and behavior also need to explain relationships, boundaries, and execution steps. A convincing picture should remain traceable to its inputs.
|
||||
|
||||
### Decision
|
||||
|
||||
Combine indexed graph relationships with source evidence. Keep the repository map, routes, hotspots, system structure, and behavior as distinct lenses. Mark inferred relationships as inferred, and make their evidence inspectable. Do not embed project-specific architectural explanations in the visualization code.
|
||||
|
||||
### Consequences
|
||||
|
||||
- The same analysis can be applied to other indexed repositories.
|
||||
- Missing evidence remains a visible limitation rather than a fabricated connection.
|
||||
- A source or graph change can alter the view; these written decisions change only when someone edits them.
|
||||
|
||||
**Implementation references:** `graph-ui/src/architecture/`, `src/store/architecture_projection.c`.
|
||||
|
||||
## 002: Keep browser AI optional
|
||||
|
||||
**Status:** Proposed · **Scope:** Local chat
|
||||
|
||||
### Context
|
||||
|
||||
Code exploration should work before any model is downloaded. Users may want to ask about a file or a literal code selection without sending source to a hosted model.
|
||||
|
||||
### Decision
|
||||
|
||||
Run the chat model locally in the browser after explicit setup. Download weights only after opt-in. Attach the selected code or graph context to the next prompt and render answers as Markdown.
|
||||
|
||||
### Consequences
|
||||
|
||||
| Benefit | Tradeoff |
|
||||
| --- | --- |
|
||||
| Exploration works without model setup | Chat is unavailable until a model is enabled |
|
||||
| Inference stays on the device | Model quality and speed depend on local resources |
|
||||
| The prompt can use exact marked code | A bounded context cannot include an entire large repository |
|
||||
|
||||
**Implementation references:** `graph-ui/src/browser-ai/`, `graph-ui/src/reader/MonacoReader.tsx`.
|
||||
|
||||
## 003: Keep agent activity experimental
|
||||
|
||||
**Status:** Proposed · **Scope:** Agents workspace
|
||||
|
||||
### Context
|
||||
|
||||
The current integration records events from explicitly configured agent hooks. The UI cannot infer every agent operation merely by observing the graph.
|
||||
|
||||
### Decision
|
||||
|
||||
Hide Agents unless the experimental build flag is enabled. Treat hook events as observed activity, and avoid implying complete task execution, tool outcomes, or duration data.
|
||||
|
||||
### Consequences
|
||||
|
||||
The default navigation remains focused. Further agent adapters and a richer event contract can be introduced before promoting the view to a standard workspace.
|
||||
|
||||
**Implementation references:** `graph-ui/src/app/feature-flags.ts`, `graph-ui/agents/hooks/atlas-trace.py`, `src/ui/activity.c`.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Architecture decisions
|
||||
|
||||
> Preview examples for the indexed Train Ticket repository. These are proposed review notes for exercising the ADR workspace, not decisions attributed to the upstream maintainers.
|
||||
|
||||
## 001: Review services as deployment boundaries
|
||||
|
||||
**Status:** Proposed · **Scope:** Routes review
|
||||
|
||||
### Context
|
||||
|
||||
A large service graph is difficult to understand when every source file has equal visual weight. Deployment declarations provide another useful level of organization.
|
||||
|
||||
### Decision
|
||||
|
||||
Use detected service and container boundaries to organize the routes review. Keep the declaration behind each boundary inspectable. Treat a deployment dependency and a source-level request as different kinds of evidence.
|
||||
|
||||
### Consequences
|
||||
|
||||
- The initial view can show relationships between services before drilling into handlers.
|
||||
- A declared dependency alone does not prove that a request is sent.
|
||||
- Services without discovered connections should remain visible.
|
||||
|
||||
## 002: Follow a request through its evidence
|
||||
|
||||
**Status:** Proposed · **Scope:** Cross-service investigation
|
||||
|
||||
### Context
|
||||
|
||||
Matching a caller to a route helps explain the system, but static analysis may not resolve dynamic hosts, route parameters, or runtime configuration.
|
||||
|
||||
### Decision
|
||||
|
||||
During review, follow a connection from its caller through the route evidence to the destination handler. Keep unresolved destinations visible as unknowns. Use source inspection to verify a suspected relationship before treating it as established.
|
||||
|
||||
### Review checklist
|
||||
|
||||
- [ ] Inspect an outgoing request in the selected service.
|
||||
- [ ] Compare its path and method with the destination route.
|
||||
- [ ] Check relevant configuration and service naming.
|
||||
- [ ] Record unresolved runtime assumptions here.
|
||||
|
||||
### Consequences
|
||||
|
||||
This creates a repeatable review path while avoiding claims that a static graph proves runtime behavior.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Architecture decisions
|
||||
|
||||
> Preview example for voting-services. This proposed review note is test content, not an upstream architectural decision.
|
||||
|
||||
## 001: Distinguish declared and observed connections
|
||||
|
||||
**Status:** Proposed · **Scope:** Service map review
|
||||
|
||||
### Context
|
||||
|
||||
A small connected application is useful for checking whether a service visualization makes each relationship understandable.
|
||||
|
||||
### Decision
|
||||
|
||||
Review deployment dependencies, request relationships, and datastore connections separately. Inspect the source or configuration evidence for each edge. Do not interpret an edge count as traffic volume or a static dependency as an observed runtime call.
|
||||
|
||||
### Consequences
|
||||
|
||||
The service map can explain what is connected and why without implying measurements that were never collected. Missing connections become concrete questions for further inspection.
|
||||
|
||||
### Next review
|
||||
|
||||
- [ ] Select a service and inspect its neighboring connections.
|
||||
- [ ] Verify one connection against source or configuration.
|
||||
- [ ] Edit this note with the outcome and save it.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Agent-event fixture provenance
|
||||
|
||||
These stable replay inputs exercise activity visualization without requiring a live
|
||||
coding session. They contain no source contents or tool output. They are test
|
||||
fixtures, not evidence of current agent activity.
|
||||
|
||||
## `w11a-replay.jsonl`
|
||||
|
||||
Derived from a real tool-hook recording made while working on this repository on
|
||||
29-30 August 2026. The private original is not committed because it contains local
|
||||
paths and commands. This fixture preserves its event shape, relative tool mix,
|
||||
initial timestamp (`1788038561000`) and 44 consecutive time intervals in milliseconds:
|
||||
|
||||
```text
|
||||
10065, 13599, 115703, 5123, 18182, 6789, 1140, 13387, 5243, 5570, 6676,
|
||||
21168, 51146, 6330, 6339, 28257, 4029, 318, 2709, 709, 83, 6176, 1116,
|
||||
3373, 3642, 3149, 2275, 4175, 2884, 4066, 2310, 2028, 2838, 33596, 5878,
|
||||
17621, 826, 5788, 11062, 6988, 5724, 10001, 17767, 13461
|
||||
```
|
||||
|
||||
The intervals came from the first 45-event window without a pause over two minutes,
|
||||
beginning at line 57 of the original. The following content was deliberately changed:
|
||||
|
||||
- Absolute paths became relative `fixtures/atlas-sample` paths. `package.json` is
|
||||
intentionally outside the indexed graph to exercise unmapped activity.
|
||||
- One original agent became the synthetic roles `implementer`, `checker` and
|
||||
`explorer`; their eight-character run IDs are invented.
|
||||
- Explicit `lines` ranges were added from fixture symbols. The original hook did
|
||||
not record line ranges.
|
||||
- Run `b7e4d19c` jumps from sequence 6 to 9 to expose two missing events.
|
||||
- Two synthetic `intent` fields exercise explicitly labeled self-reports.
|
||||
- `detail` values describe fixture operations instead of retaining private commands.
|
||||
|
||||
The result contains 45 events, three runs, four work types, one two-event sequence
|
||||
gap, two self-reports and one path outside the index.
|
||||
|
||||
## `w11b-replay.jsonl`
|
||||
|
||||
Entirely constructed test data, not a session recording. Its paths and line spans
|
||||
come from `fixtures/atlas-sample`; timestamps and activity are deliberately chosen.
|
||||
|
||||
- Eight agents plus the browser reader exercise the eight-body display limit.
|
||||
- `explorer` and `planner` end 75 and 85 seconds before the last event. Both are
|
||||
idle but still within retention; the oldest actor exercises the display cap.
|
||||
- `implementer` writes `createUser` lines 23-36 five times in 1.8 seconds to test
|
||||
aggregation into one visible wave, and visits fifteen symbols to exceed trail limits.
|
||||
- `reviewer`, `indexer` and `tracer` end at `hotspotScan` in `src/repo/db.ts`, lines
|
||||
39-54, to test deterministic orbit separation at one symbol.
|
||||
- Activity rates differ: fifteen recent events for `implementer`, two for `packer`,
|
||||
none for idle `planner`. One `package.json` path remains unmapped and one event
|
||||
has a synthetic self-reported `intent`.
|
||||
|
||||
There are 73 events across eight runs and four work types over 145 seconds. Last
|
||||
activity offsets are 0, 3.8, 4.4, 5, 7, 10, 75 and 85 seconds for `implementer`,
|
||||
`tracer`, `indexer`, `reviewer`, `packer`, `checker`, `explorer` and `planner`.
|
||||
The fixture does not encode progress, quality assessments or source contents.
|
||||
@@ -0,0 +1,45 @@
|
||||
{"ts": 1788038561000, "agent": "explorer", "run": "c3a80f52", "seq": 1, "phase": "end", "tool": "Grep", "path": "src", "detail": "validate"}
|
||||
{"ts": 1788038571065, "agent": "implementer", "run": "a1f2c3d4", "seq": 1, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "detail": "read the whole module before touching it", "lines": [1, 43]}
|
||||
{"ts": 1788038584664, "agent": "checker", "run": "b7e4d19c", "seq": 1, "phase": "end", "tool": "Read", "path": "package.json", "detail": "read the scripts of the fixture repository"}
|
||||
{"ts": 1788038700367, "agent": "explorer", "run": "c3a80f52", "seq": 2, "phase": "end", "tool": "Grep", "path": "src", "detail": "createUser"}
|
||||
{"ts": 1788038705490, "agent": "implementer", "run": "a1f2c3d4", "seq": 2, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "detail": "read validateUser", "lines": [19, 31]}
|
||||
{"ts": 1788038723672, "agent": "implementer", "run": "a1f2c3d4", "seq": 3, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "replaced the validation branch of createUser", "lines": [24, 30]}
|
||||
{"ts": 1788038730461, "agent": "checker", "run": "b7e4d19c", "seq": 2, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "detail": "read the whole file"}
|
||||
{"ts": 1788038731601, "agent": "explorer", "run": "c3a80f52", "seq": 3, "phase": "end", "tool": "Grep", "path": "src", "detail": "query"}
|
||||
{"ts": 1788038744988, "agent": "implementer", "run": "a1f2c3d4", "seq": 4, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "reflowed createUser after the edit above", "lines": [23, 36]}
|
||||
{"ts": 1788038750231, "agent": "checker", "run": "b7e4d19c", "seq": 3, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788038755801, "agent": "explorer", "run": "c3a80f52", "seq": 4, "phase": "end", "tool": "Read", "path": "src/routes/users.ts", "detail": "read registerUserRoutes", "lines": [7, 18]}
|
||||
{"ts": 1788038762477, "agent": "implementer", "run": "a1f2c3d4", "seq": 5, "phase": "end", "tool": "Edit", "path": "src/util/validate.ts", "detail": "validateUser now names the field it rejected", "lines": [19, 31]}
|
||||
{"ts": 1788038783645, "agent": "checker", "run": "b7e4d19c", "seq": 4, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788038834791, "agent": "explorer", "run": "c3a80f52", "seq": 5, "phase": "end", "tool": "Grep", "path": "src", "detail": "Order"}
|
||||
{"ts": 1788038841121, "agent": "implementer", "run": "a1f2c3d4", "seq": 6, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "detail": "read getOrder", "lines": [8, 26]}
|
||||
{"ts": 1788038847460, "agent": "checker", "run": "b7e4d19c", "seq": 5, "phase": "end", "tool": "Read", "path": "test/userService.test.ts", "detail": "read the test file itself"}
|
||||
{"ts": 1788038875717, "agent": "implementer", "run": "a1f2c3d4", "seq": 7, "phase": "end", "tool": "Edit", "path": "src/services/orderService.ts", "detail": "create now carries the customer id it was given", "lines": [30, 35]}
|
||||
{"ts": 1788038879746, "agent": "explorer", "run": "c3a80f52", "seq": 6, "phase": "end", "tool": "Grep", "path": "src", "detail": "insert"}
|
||||
{"ts": 1788038880064, "agent": "checker", "run": "b7e4d19c", "seq": 6, "phase": "end", "tool": "Bash", "path": "", "detail": "node --test tests/scaffold/"}
|
||||
{"ts": 1788038882773, "agent": "implementer", "run": "a1f2c3d4", "seq": 8, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "detail": "read insert", "lines": [31, 35]}
|
||||
{"ts": 1788038883482, "agent": "explorer", "run": "c3a80f52", "seq": 7, "phase": "end", "tool": "Read", "path": "src/config.ts", "detail": "read loadConfig", "lines": [11, 18]}
|
||||
{"ts": 1788038883565, "agent": "implementer", "run": "a1f2c3d4", "seq": 9, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "detail": "insert writes the payload it was handed", "lines": [31, 35], "intent": "making insert keep the payload instead of dropping it"}
|
||||
{"ts": 1788038889741, "agent": "checker", "run": "b7e4d19c", "seq": 9, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788038890857, "agent": "explorer", "run": "c3a80f52", "seq": 8, "phase": "end", "tool": "Grep", "path": "src", "detail": "walk"}
|
||||
{"ts": 1788038894230, "agent": "implementer", "run": "a1f2c3d4", "seq": 10, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "detail": "read hotspotScan", "lines": [39, 54]}
|
||||
{"ts": 1788038897872, "agent": "checker", "run": "b7e4d19c", "seq": 10, "phase": "end", "tool": "Read", "path": "src/server.ts", "detail": "read createApp", "lines": [32, 37]}
|
||||
{"ts": 1788038901021, "agent": "implementer", "run": "a1f2c3d4", "seq": 11, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "detail": "hotspotScan stops at the first needle it finds", "lines": [39, 54]}
|
||||
{"ts": 1788038903296, "agent": "explorer", "run": "c3a80f52", "seq": 9, "phase": "end", "tool": "Read", "path": "src/types.ts", "detail": "read UserEntity", "lines": [37, 53]}
|
||||
{"ts": 1788038907471, "agent": "checker", "run": "b7e4d19c", "seq": 11, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788038910355, "agent": "implementer", "run": "a1f2c3d4", "seq": 12, "phase": "end", "tool": "Write", "path": "src/util/validate.ts", "detail": "wrote validateId over the old body", "lines": [33, 38]}
|
||||
{"ts": 1788038914421, "agent": "explorer", "run": "c3a80f52", "seq": 10, "phase": "end", "tool": "Grep", "path": "src", "detail": "Router"}
|
||||
{"ts": 1788038916731, "agent": "checker", "run": "b7e4d19c", "seq": 12, "phase": "end", "tool": "Read", "path": "src/routes/orders.ts", "detail": "read the whole route module"}
|
||||
{"ts": 1788038918759, "agent": "implementer", "run": "a1f2c3d4", "seq": 13, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "detail": "read listUsers", "lines": [18, 21]}
|
||||
{"ts": 1788038921597, "agent": "explorer", "run": "c3a80f52", "seq": 11, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "detail": "read ValidationError", "lines": [4, 12]}
|
||||
{"ts": 1788038955193, "agent": "checker", "run": "b7e4d19c", "seq": 13, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788038961071, "agent": "implementer", "run": "a1f2c3d4", "seq": 14, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "listUsers takes the limit it is given", "lines": [18, 21]}
|
||||
{"ts": 1788038978692, "agent": "explorer", "run": "c3a80f52", "seq": 12, "phase": "end", "tool": "Grep", "path": "src", "detail": "loadConfig"}
|
||||
{"ts": 1788038979518, "agent": "checker", "run": "b7e4d19c", "seq": 14, "phase": "end", "tool": "Read", "path": "src/config.ts", "detail": "read isProduction", "lines": [20, 22]}
|
||||
{"ts": 1788038985306, "agent": "implementer", "run": "a1f2c3d4", "seq": 15, "phase": "end", "tool": "Read", "path": "src/routes/users.ts", "detail": "read registerUserRoutes before wiring it", "lines": [7, 18]}
|
||||
{"ts": 1788038996368, "agent": "explorer", "run": "c3a80f52", "seq": 13, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "detail": "read the whole order module"}
|
||||
{"ts": 1788039003356, "agent": "checker", "run": "b7e4d19c", "seq": 15, "phase": "end", "tool": "Read", "path": "src/server.ts", "detail": "read main", "lines": [39, 46]}
|
||||
{"ts": 1788039009080, "agent": "implementer", "run": "a1f2c3d4", "seq": 16, "phase": "end", "tool": "Edit", "path": "src/routes/users.ts", "detail": "the user routes hand the limit through", "lines": [7, 18]}
|
||||
{"ts": 1788039019081, "agent": "implementer", "run": "a1f2c3d4", "seq": 17, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "tightened the input check of createUser again", "lines": [24, 30], "intent": "tightening the input check in createUser"}
|
||||
{"ts": 1788039036848, "agent": "checker", "run": "b7e4d19c", "seq": 16, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788039050309, "agent": "explorer", "run": "c3a80f52", "seq": 14, "phase": "end", "tool": "Grep", "path": "src", "detail": "validate"}
|
||||
@@ -0,0 +1,73 @@
|
||||
{"ts": 1788139200000, "agent": "explorer", "run": "c3a80f52", "seq": 1, "phase": "end", "tool": "Grep", "path": "src", "detail": "validate"}
|
||||
{"ts": 1788139201200, "agent": "implementer", "run": "a1f2c3d4", "seq": 1, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "read createUser before touching it"}
|
||||
{"ts": 1788139202400, "agent": "planner", "run": "1b8d5f27", "seq": 1, "phase": "end", "tool": "Read", "path": "package.json", "detail": "read the scripts of the fixture repository"}
|
||||
{"ts": 1788139205000, "agent": "reviewer", "run": "d5b1e807", "seq": 1, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [17, 29], "detail": "read query"}
|
||||
{"ts": 1788139207000, "agent": "tracer", "run": "e9c72f16", "seq": 1, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [37, 53], "detail": "read UserEntity"}
|
||||
{"ts": 1788139209000, "agent": "indexer", "run": "f2d0a34b", "seq": 1, "phase": "end", "tool": "Glob", "path": "src", "detail": "loadConfig"}
|
||||
{"ts": 1788139211000, "agent": "packer", "run": "0a4e6c81", "seq": 1, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [11, 18], "detail": "read loadConfig"}
|
||||
{"ts": 1788139213000, "agent": "packer", "run": "0a4e6c81", "seq": 2, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [32, 37], "detail": "read createApp"}
|
||||
{"ts": 1788139216000, "agent": "explorer", "run": "c3a80f52", "seq": 2, "phase": "end", "tool": "Grep", "path": "src", "detail": "createUser"}
|
||||
{"ts": 1788139218000, "agent": "implementer", "run": "a1f2c3d4", "seq": 2, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [19, 31], "detail": "read validateUser"}
|
||||
{"ts": 1788139221000, "agent": "checker", "run": "b7e4d19c", "seq": 1, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788139224000, "agent": "reviewer", "run": "d5b1e807", "seq": 2, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [31, 35], "detail": "insert writes the payload it was handed"}
|
||||
{"ts": 1788139227000, "agent": "tracer", "run": "e9c72f16", "seq": 2, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "lines": [8, 26], "detail": "read getOrder"}
|
||||
{"ts": 1788139230000, "agent": "indexer", "run": "f2d0a34b", "seq": 2, "phase": "end", "tool": "Glob", "path": "src", "detail": "Router"}
|
||||
{"ts": 1788139233000, "agent": "packer", "run": "0a4e6c81", "seq": 3, "phase": "end", "tool": "Read", "path": "src/routes/users.ts", "lines": [7, 18], "detail": "read registerUserRoutes"}
|
||||
{"ts": 1788139236000, "agent": "reviewer", "run": "d5b1e807", "seq": 3, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [20, 22], "detail": "read isProduction"}
|
||||
{"ts": 1788139239000, "agent": "explorer", "run": "c3a80f52", "seq": 3, "phase": "end", "tool": "Read", "path": "src/routes/orders.ts", "lines": [6, 15], "detail": "read registerOrderRoutes"}
|
||||
{"ts": 1788139242000, "agent": "implementer", "run": "a1f2c3d4", "seq": 3, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [18, 21], "detail": "listUsers takes the limit it is given"}
|
||||
{"ts": 1788139245000, "agent": "tracer", "run": "e9c72f16", "seq": 3, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [39, 46], "detail": "read main"}
|
||||
{"ts": 1788139248000, "agent": "checker", "run": "b7e4d19c", "seq": 2, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788139251000, "agent": "reviewer", "run": "d5b1e807", "seq": 4, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [33, 38], "detail": "read validateId"}
|
||||
{"ts": 1788139254000, "agent": "tracer", "run": "e9c72f16", "seq": 4, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [58, 66], "detail": "read walk"}
|
||||
{"ts": 1788139257000, "agent": "indexer", "run": "f2d0a34b", "seq": 3, "phase": "end", "tool": "Glob", "path": "src", "detail": "insert"}
|
||||
{"ts": 1788139260000, "agent": "packer", "run": "0a4e6c81", "seq": 4, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [27, 30], "detail": "read User"}
|
||||
{"ts": 1788139260000, "agent": "planner", "run": "1b8d5f27", "seq": 2, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [32, 37], "detail": "read createApp"}
|
||||
{"ts": 1788139266000, "agent": "implementer", "run": "a1f2c3d4", "seq": 4, "phase": "end", "tool": "Edit", "path": "src/util/validate.ts", "lines": [19, 31], "detail": "validateUser now names the field it rejected"}
|
||||
{"ts": 1788139269000, "agent": "checker", "run": "b7e4d19c", "seq": 3, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [4, 8], "detail": "read Row"}
|
||||
{"ts": 1788139270000, "agent": "explorer", "run": "c3a80f52", "seq": 4, "phase": "end", "tool": "Grep", "path": "src", "detail": "Order"}
|
||||
{"ts": 1788139272000, "agent": "reviewer", "run": "d5b1e807", "seq": 5, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "lines": [30, 35], "detail": "read create"}
|
||||
{"ts": 1788139275000, "agent": "tracer", "run": "e9c72f16", "seq": 5, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [16, 30], "detail": "read SimpleRouter"}
|
||||
{"ts": 1788139278000, "agent": "indexer", "run": "f2d0a34b", "seq": 4, "phase": "end", "tool": "Glob", "path": "src", "detail": "query"}
|
||||
{"ts": 1788139281000, "agent": "checker", "run": "b7e4d19c", "seq": 4, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [4, 9], "detail": "read AppConfig"}
|
||||
{"ts": 1788139284000, "agent": "implementer", "run": "a1f2c3d4", "seq": 5, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [17, 29], "detail": "read query"}
|
||||
{"ts": 1788139286000, "agent": "implementer", "run": "a1f2c3d4", "seq": 6, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "lines": [8, 26], "detail": "read getOrder"}
|
||||
{"ts": 1788139287000, "agent": "checker", "run": "b7e4d19c", "seq": 5, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788139289000, "agent": "implementer", "run": "a1f2c3d4", "seq": 7, "phase": "end", "tool": "Edit", "path": "src/services/orderService.ts", "lines": [30, 35], "detail": "create now carries the customer id it was given"}
|
||||
{"ts": 1788139290000, "agent": "checker", "run": "b7e4d19c", "seq": 6, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [22, 25], "detail": "read Entity"}
|
||||
{"ts": 1788139292000, "agent": "implementer", "run": "a1f2c3d4", "seq": 8, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [37, 53], "detail": "read UserEntity"}
|
||||
{"ts": 1788139293000, "agent": "packer", "run": "0a4e6c81", "seq": 5, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "lines": [40, 42], "detail": "read create"}
|
||||
{"ts": 1788139295000, "agent": "implementer", "run": "a1f2c3d4", "seq": 9, "phase": "end", "tool": "Edit", "path": "src/util/validate.ts", "lines": [33, 38], "detail": "validateId rejects the empty string by name"}
|
||||
{"ts": 1788139296000, "agent": "reviewer", "run": "d5b1e807", "seq": 6, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "read hotspotScan"}
|
||||
{"ts": 1788139298000, "agent": "implementer", "run": "a1f2c3d4", "seq": 10, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [39, 46], "detail": "read main"}
|
||||
{"ts": 1788139299000, "agent": "tracer", "run": "e9c72f16", "seq": 6, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [17, 29], "detail": "read query"}
|
||||
{"ts": 1788139301000, "agent": "implementer", "run": "a1f2c3d4", "seq": 11, "phase": "end", "tool": "Edit", "path": "src/routes/users.ts", "lines": [7, 18], "detail": "the user routes hand the limit through"}
|
||||
{"ts": 1788139302000, "agent": "indexer", "run": "f2d0a34b", "seq": 5, "phase": "end", "tool": "Glob", "path": "src", "detail": "hotspot"}
|
||||
{"ts": 1788139304000, "agent": "implementer", "run": "a1f2c3d4", "seq": 12, "phase": "end", "tool": "Read", "path": "src/routes/orders.ts", "lines": [6, 15], "detail": "read registerOrderRoutes"}
|
||||
{"ts": 1788139305000, "agent": "tracer", "run": "e9c72f16", "seq": 7, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [11, 18], "detail": "read loadConfig"}
|
||||
{"ts": 1788139307000, "agent": "implementer", "run": "a1f2c3d4", "seq": 13, "phase": "end", "tool": "Edit", "path": "src/config.ts", "lines": [11, 18], "detail": "loadConfig reads the port it was given"}
|
||||
{"ts": 1788139308000, "agent": "checker", "run": "b7e4d19c", "seq": 7, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788139310000, "agent": "implementer", "run": "a1f2c3d4", "seq": 14, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [31, 35], "detail": "read insert"}
|
||||
{"ts": 1788139311000, "agent": "tracer", "run": "e9c72f16", "seq": 8, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [31, 35], "detail": "read insert"}
|
||||
{"ts": 1788139313000, "agent": "implementer", "run": "a1f2c3d4", "seq": 15, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "hotspotScan stops at the first needle it finds"}
|
||||
{"ts": 1788139314000, "agent": "indexer", "run": "f2d0a34b", "seq": 6, "phase": "end", "tool": "Glob", "path": "src", "detail": "walk"}
|
||||
{"ts": 1788139316000, "agent": "implementer", "run": "a1f2c3d4", "seq": 16, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "lines": [9, 16], "detail": "read toUser"}
|
||||
{"ts": 1788139317000, "agent": "indexer", "run": "f2d0a34b", "seq": 7, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [4, 12], "detail": "read ValidationError"}
|
||||
{"ts": 1788139319000, "agent": "implementer", "run": "a1f2c3d4", "seq": 17, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [18, 21], "detail": "listUsers takes the limit it is given"}
|
||||
{"ts": 1788139320000, "agent": "checker", "run": "b7e4d19c", "seq": 8, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [32, 35], "detail": "read Order"}
|
||||
{"ts": 1788139322000, "agent": "implementer", "run": "a1f2c3d4", "seq": 18, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [19, 31], "detail": "read validateUser"}
|
||||
{"ts": 1788139323000, "agent": "reviewer", "run": "d5b1e807", "seq": 7, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [58, 66], "detail": "walk returns the names it visited"}
|
||||
{"ts": 1788139325000, "agent": "implementer", "run": "a1f2c3d4", "seq": 19, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [58, 66], "detail": "read walk"}
|
||||
{"ts": 1788139326000, "agent": "tracer", "run": "e9c72f16", "seq": 9, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [4, 8], "detail": "read Row"}
|
||||
{"ts": 1788139329000, "agent": "indexer", "run": "f2d0a34b", "seq": 8, "phase": "end", "tool": "Glob", "path": "src", "detail": "Row"}
|
||||
{"ts": 1788139332000, "agent": "checker", "run": "b7e4d19c", "seq": 9, "phase": "end", "tool": "Grep", "path": "src", "detail": "insert"}
|
||||
{"ts": 1788139335000, "agent": "checker", "run": "b7e4d19c", "seq": 10, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"}
|
||||
{"ts": 1788139338000, "agent": "packer", "run": "0a4e6c81", "seq": 6, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [20, 22], "detail": "read isProduction"}
|
||||
{"ts": 1788139340000, "agent": "reviewer", "run": "d5b1e807", "seq": 8, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "hotspotScan keeps the first hit"}
|
||||
{"ts": 1788139340600, "agent": "indexer", "run": "f2d0a34b", "seq": 9, "phase": "end", "tool": "Glob", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "hotspotScan"}
|
||||
{"ts": 1788139341200, "agent": "tracer", "run": "e9c72f16", "seq": 10, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "read hotspotScan"}
|
||||
{"ts": 1788139343200, "agent": "implementer", "run": "a1f2c3d4", "seq": 20, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser", "intent": "tightening the input check in createUser"}
|
||||
{"ts": 1788139343650, "agent": "implementer", "run": "a1f2c3d4", "seq": 21, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"}
|
||||
{"ts": 1788139344100, "agent": "implementer", "run": "a1f2c3d4", "seq": 22, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"}
|
||||
{"ts": 1788139344550, "agent": "implementer", "run": "a1f2c3d4", "seq": 23, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"}
|
||||
{"ts": 1788139345000, "agent": "implementer", "run": "a1f2c3d4", "seq": 24, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Large-source fixture provenance
|
||||
|
||||
Generated for this frontend on 2026-08-28; no external code was copied.
|
||||
`src/big.ts` deterministically contains 100 small exported functions and `runAll`.
|
||||
`src/support.ts` and `src/index.ts` were written for the fixture.
|
||||
|
||||
The 717-line source exceeds the 500-line `get_code_snippet` limit and exercises
|
||||
`source_clipped`. The reader must disclose missing source rather than presenting
|
||||
an incomplete file as complete. This corpus is separate from `atlas-sample` to
|
||||
preserve that fixture's graph counts. It is indexing input, not a runnable package.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "atlas-sample-large",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Cap-Mess-Fixture fuer CodeAtlas. Never built, installed or executed by the monorepo.",
|
||||
"main": "src/index.ts",
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
// Generierte Datei fuer die Cap-Messung des Readers (2026-08-28).
|
||||
// Deterministisch erzeugt: 100 kleine exportierte Funktionen, damit der
|
||||
// Indexer sie sauber frisst und die Datei sicher ueber 550 Zeilen liegt.
|
||||
// Der 500-Zeilen-Cap von get_code_snippet schneidet sie also mitten durch,
|
||||
// und genau das soll der Reader ehrlich benennen statt zu verschweigen.
|
||||
//
|
||||
// Von Hand aendern hat keinen Sinn: die Datei ist Messmaterial, kein Code,
|
||||
// der gelesen werden will.
|
||||
|
||||
import { normalize, weigh } from './support';
|
||||
|
||||
/** Schritt 1 des generierten Korpus. */
|
||||
export function step001(input: number): number {
|
||||
const scaled = input * 1 + 7;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 2 des generierten Korpus. */
|
||||
export function step002(input: number): number {
|
||||
const scaled = input * 2 + 1;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 3 des generierten Korpus. */
|
||||
export function step003(input: number): number {
|
||||
const scaled = input * 3 + 8;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 4 des generierten Korpus. */
|
||||
export function step004(input: number): number {
|
||||
const scaled = input * 4 + 2;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 5 des generierten Korpus. */
|
||||
export function step005(input: number): number {
|
||||
const scaled = input * 5 + 9;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 6 des generierten Korpus. */
|
||||
export function step006(input: number): number {
|
||||
const scaled = input * 6 + 3;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 7 des generierten Korpus. */
|
||||
export function step007(input: number): number {
|
||||
const scaled = input * 7 + 10;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 8 des generierten Korpus. */
|
||||
export function step008(input: number): number {
|
||||
const scaled = input * 8 + 4;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 9 des generierten Korpus. */
|
||||
export function step009(input: number): number {
|
||||
const scaled = input * 9 + 11;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 10 des generierten Korpus. */
|
||||
export function step010(input: number): number {
|
||||
const scaled = input * 10 + 5;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 11 des generierten Korpus. */
|
||||
export function step011(input: number): number {
|
||||
const scaled = input * 11 + 12;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 12 des generierten Korpus. */
|
||||
export function step012(input: number): number {
|
||||
const scaled = input * 12 + 6;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 13 des generierten Korpus. */
|
||||
export function step013(input: number): number {
|
||||
const scaled = input * 13 + 0;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 14 des generierten Korpus. */
|
||||
export function step014(input: number): number {
|
||||
const scaled = input * 14 + 7;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 15 des generierten Korpus. */
|
||||
export function step015(input: number): number {
|
||||
const scaled = input * 15 + 1;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 16 des generierten Korpus. */
|
||||
export function step016(input: number): number {
|
||||
const scaled = input * 16 + 8;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 17 des generierten Korpus. */
|
||||
export function step017(input: number): number {
|
||||
const scaled = input * 17 + 2;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 18 des generierten Korpus. */
|
||||
export function step018(input: number): number {
|
||||
const scaled = input * 18 + 9;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 19 des generierten Korpus. */
|
||||
export function step019(input: number): number {
|
||||
const scaled = input * 19 + 3;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 20 des generierten Korpus. */
|
||||
export function step020(input: number): number {
|
||||
const scaled = input * 20 + 10;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 21 des generierten Korpus. */
|
||||
export function step021(input: number): number {
|
||||
const scaled = input * 21 + 4;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 22 des generierten Korpus. */
|
||||
export function step022(input: number): number {
|
||||
const scaled = input * 22 + 11;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 23 des generierten Korpus. */
|
||||
export function step023(input: number): number {
|
||||
const scaled = input * 23 + 5;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 24 des generierten Korpus. */
|
||||
export function step024(input: number): number {
|
||||
const scaled = input * 24 + 12;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 25 des generierten Korpus. */
|
||||
export function step025(input: number): number {
|
||||
const scaled = input * 25 + 6;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 26 des generierten Korpus. */
|
||||
export function step026(input: number): number {
|
||||
const scaled = input * 26 + 0;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 27 des generierten Korpus. */
|
||||
export function step027(input: number): number {
|
||||
const scaled = input * 27 + 7;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 28 des generierten Korpus. */
|
||||
export function step028(input: number): number {
|
||||
const scaled = input * 28 + 1;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 29 des generierten Korpus. */
|
||||
export function step029(input: number): number {
|
||||
const scaled = input * 29 + 8;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 30 des generierten Korpus. */
|
||||
export function step030(input: number): number {
|
||||
const scaled = input * 30 + 2;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 31 des generierten Korpus. */
|
||||
export function step031(input: number): number {
|
||||
const scaled = input * 31 + 9;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 32 des generierten Korpus. */
|
||||
export function step032(input: number): number {
|
||||
const scaled = input * 32 + 3;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 33 des generierten Korpus. */
|
||||
export function step033(input: number): number {
|
||||
const scaled = input * 33 + 10;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 34 des generierten Korpus. */
|
||||
export function step034(input: number): number {
|
||||
const scaled = input * 34 + 4;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 35 des generierten Korpus. */
|
||||
export function step035(input: number): number {
|
||||
const scaled = input * 35 + 11;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 36 des generierten Korpus. */
|
||||
export function step036(input: number): number {
|
||||
const scaled = input * 36 + 5;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 37 des generierten Korpus. */
|
||||
export function step037(input: number): number {
|
||||
const scaled = input * 37 + 12;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 38 des generierten Korpus. */
|
||||
export function step038(input: number): number {
|
||||
const scaled = input * 38 + 6;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 39 des generierten Korpus. */
|
||||
export function step039(input: number): number {
|
||||
const scaled = input * 39 + 0;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 40 des generierten Korpus. */
|
||||
export function step040(input: number): number {
|
||||
const scaled = input * 40 + 7;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 41 des generierten Korpus. */
|
||||
export function step041(input: number): number {
|
||||
const scaled = input * 41 + 1;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 42 des generierten Korpus. */
|
||||
export function step042(input: number): number {
|
||||
const scaled = input * 42 + 8;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 43 des generierten Korpus. */
|
||||
export function step043(input: number): number {
|
||||
const scaled = input * 43 + 2;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 44 des generierten Korpus. */
|
||||
export function step044(input: number): number {
|
||||
const scaled = input * 44 + 9;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 45 des generierten Korpus. */
|
||||
export function step045(input: number): number {
|
||||
const scaled = input * 45 + 3;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 46 des generierten Korpus. */
|
||||
export function step046(input: number): number {
|
||||
const scaled = input * 46 + 10;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 47 des generierten Korpus. */
|
||||
export function step047(input: number): number {
|
||||
const scaled = input * 47 + 4;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 48 des generierten Korpus. */
|
||||
export function step048(input: number): number {
|
||||
const scaled = input * 48 + 11;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 49 des generierten Korpus. */
|
||||
export function step049(input: number): number {
|
||||
const scaled = input * 49 + 5;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 50 des generierten Korpus. */
|
||||
export function step050(input: number): number {
|
||||
const scaled = input * 50 + 12;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 51 des generierten Korpus. */
|
||||
export function step051(input: number): number {
|
||||
const scaled = input * 51 + 6;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 52 des generierten Korpus. */
|
||||
export function step052(input: number): number {
|
||||
const scaled = input * 52 + 0;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 53 des generierten Korpus. */
|
||||
export function step053(input: number): number {
|
||||
const scaled = input * 53 + 7;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 54 des generierten Korpus. */
|
||||
export function step054(input: number): number {
|
||||
const scaled = input * 54 + 1;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 55 des generierten Korpus. */
|
||||
export function step055(input: number): number {
|
||||
const scaled = input * 55 + 8;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 56 des generierten Korpus. */
|
||||
export function step056(input: number): number {
|
||||
const scaled = input * 56 + 2;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 57 des generierten Korpus. */
|
||||
export function step057(input: number): number {
|
||||
const scaled = input * 57 + 9;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 58 des generierten Korpus. */
|
||||
export function step058(input: number): number {
|
||||
const scaled = input * 58 + 3;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 59 des generierten Korpus. */
|
||||
export function step059(input: number): number {
|
||||
const scaled = input * 59 + 10;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 60 des generierten Korpus. */
|
||||
export function step060(input: number): number {
|
||||
const scaled = input * 60 + 4;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 61 des generierten Korpus. */
|
||||
export function step061(input: number): number {
|
||||
const scaled = input * 61 + 11;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 62 des generierten Korpus. */
|
||||
export function step062(input: number): number {
|
||||
const scaled = input * 62 + 5;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 63 des generierten Korpus. */
|
||||
export function step063(input: number): number {
|
||||
const scaled = input * 63 + 12;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 64 des generierten Korpus. */
|
||||
export function step064(input: number): number {
|
||||
const scaled = input * 64 + 6;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 65 des generierten Korpus. */
|
||||
export function step065(input: number): number {
|
||||
const scaled = input * 65 + 0;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 66 des generierten Korpus. */
|
||||
export function step066(input: number): number {
|
||||
const scaled = input * 66 + 7;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 67 des generierten Korpus. */
|
||||
export function step067(input: number): number {
|
||||
const scaled = input * 67 + 1;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 68 des generierten Korpus. */
|
||||
export function step068(input: number): number {
|
||||
const scaled = input * 68 + 8;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 69 des generierten Korpus. */
|
||||
export function step069(input: number): number {
|
||||
const scaled = input * 69 + 2;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 70 des generierten Korpus. */
|
||||
export function step070(input: number): number {
|
||||
const scaled = input * 70 + 9;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 71 des generierten Korpus. */
|
||||
export function step071(input: number): number {
|
||||
const scaled = input * 71 + 3;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 72 des generierten Korpus. */
|
||||
export function step072(input: number): number {
|
||||
const scaled = input * 72 + 10;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 73 des generierten Korpus. */
|
||||
export function step073(input: number): number {
|
||||
const scaled = input * 73 + 4;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 74 des generierten Korpus. */
|
||||
export function step074(input: number): number {
|
||||
const scaled = input * 74 + 11;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 75 des generierten Korpus. */
|
||||
export function step075(input: number): number {
|
||||
const scaled = input * 75 + 5;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 76 des generierten Korpus. */
|
||||
export function step076(input: number): number {
|
||||
const scaled = input * 76 + 12;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 77 des generierten Korpus. */
|
||||
export function step077(input: number): number {
|
||||
const scaled = input * 77 + 6;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 78 des generierten Korpus. */
|
||||
export function step078(input: number): number {
|
||||
const scaled = input * 78 + 0;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 79 des generierten Korpus. */
|
||||
export function step079(input: number): number {
|
||||
const scaled = input * 79 + 7;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 80 des generierten Korpus. */
|
||||
export function step080(input: number): number {
|
||||
const scaled = input * 80 + 1;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 81 des generierten Korpus. */
|
||||
export function step081(input: number): number {
|
||||
const scaled = input * 81 + 8;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 82 des generierten Korpus. */
|
||||
export function step082(input: number): number {
|
||||
const scaled = input * 82 + 2;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 83 des generierten Korpus. */
|
||||
export function step083(input: number): number {
|
||||
const scaled = input * 83 + 9;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 84 des generierten Korpus. */
|
||||
export function step084(input: number): number {
|
||||
const scaled = input * 84 + 3;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 85 des generierten Korpus. */
|
||||
export function step085(input: number): number {
|
||||
const scaled = input * 85 + 10;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 86 des generierten Korpus. */
|
||||
export function step086(input: number): number {
|
||||
const scaled = input * 86 + 4;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 87 des generierten Korpus. */
|
||||
export function step087(input: number): number {
|
||||
const scaled = input * 87 + 11;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 88 des generierten Korpus. */
|
||||
export function step088(input: number): number {
|
||||
const scaled = input * 88 + 5;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 89 des generierten Korpus. */
|
||||
export function step089(input: number): number {
|
||||
const scaled = input * 89 + 12;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 90 des generierten Korpus. */
|
||||
export function step090(input: number): number {
|
||||
const scaled = input * 90 + 6;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 91 des generierten Korpus. */
|
||||
export function step091(input: number): number {
|
||||
const scaled = input * 91 + 0;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 92 des generierten Korpus. */
|
||||
export function step092(input: number): number {
|
||||
const scaled = input * 92 + 7;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 93 des generierten Korpus. */
|
||||
export function step093(input: number): number {
|
||||
const scaled = input * 93 + 1;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 94 des generierten Korpus. */
|
||||
export function step094(input: number): number {
|
||||
const scaled = input * 94 + 8;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 95 des generierten Korpus. */
|
||||
export function step095(input: number): number {
|
||||
const scaled = input * 95 + 2;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 96 des generierten Korpus. */
|
||||
export function step096(input: number): number {
|
||||
const scaled = input * 96 + 9;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 97 des generierten Korpus. */
|
||||
export function step097(input: number): number {
|
||||
const scaled = input * 97 + 3;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 98 des generierten Korpus. */
|
||||
export function step098(input: number): number {
|
||||
const scaled = input * 98 + 10;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 99 des generierten Korpus. */
|
||||
export function step099(input: number): number {
|
||||
const scaled = input * 99 + 4;
|
||||
return normalize(scaled);
|
||||
}
|
||||
|
||||
/** Schritt 100 des generierten Korpus. */
|
||||
export function step100(input: number): number {
|
||||
const scaled = input * 100 + 11;
|
||||
return weigh(scaled);
|
||||
}
|
||||
|
||||
/** Alle Schritte der Reihe nach, damit die Datei einen Einstiegspunkt hat. */
|
||||
export function runAll(seed: number): number {
|
||||
const steps = [
|
||||
step001,
|
||||
step002,
|
||||
step003,
|
||||
step004,
|
||||
step005,
|
||||
step006,
|
||||
step007,
|
||||
step008,
|
||||
step009,
|
||||
step010,
|
||||
step011,
|
||||
step012,
|
||||
step013,
|
||||
step014,
|
||||
step015,
|
||||
step016,
|
||||
step017,
|
||||
step018,
|
||||
step019,
|
||||
step020,
|
||||
step021,
|
||||
step022,
|
||||
step023,
|
||||
step024,
|
||||
step025,
|
||||
step026,
|
||||
step027,
|
||||
step028,
|
||||
step029,
|
||||
step030,
|
||||
step031,
|
||||
step032,
|
||||
step033,
|
||||
step034,
|
||||
step035,
|
||||
step036,
|
||||
step037,
|
||||
step038,
|
||||
step039,
|
||||
step040,
|
||||
step041,
|
||||
step042,
|
||||
step043,
|
||||
step044,
|
||||
step045,
|
||||
step046,
|
||||
step047,
|
||||
step048,
|
||||
step049,
|
||||
step050,
|
||||
step051,
|
||||
step052,
|
||||
step053,
|
||||
step054,
|
||||
step055,
|
||||
step056,
|
||||
step057,
|
||||
step058,
|
||||
step059,
|
||||
step060,
|
||||
step061,
|
||||
step062,
|
||||
step063,
|
||||
step064,
|
||||
step065,
|
||||
step066,
|
||||
step067,
|
||||
step068,
|
||||
step069,
|
||||
step070,
|
||||
step071,
|
||||
step072,
|
||||
step073,
|
||||
step074,
|
||||
step075,
|
||||
step076,
|
||||
step077,
|
||||
step078,
|
||||
step079,
|
||||
step080,
|
||||
step081,
|
||||
step082,
|
||||
step083,
|
||||
step084,
|
||||
step085,
|
||||
step086,
|
||||
step087,
|
||||
step088,
|
||||
step089,
|
||||
step090,
|
||||
step091,
|
||||
step092,
|
||||
step093,
|
||||
step094,
|
||||
step095,
|
||||
step096,
|
||||
step097,
|
||||
step098,
|
||||
step099,
|
||||
step100,
|
||||
];
|
||||
return steps.reduce((acc, step) => step(acc), seed);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Einstiegspunkt des Mess-Korpus. Er tut absichtlich fast nichts: gemessen
|
||||
// wird der Reader an big.ts, nicht dieses Projekt.
|
||||
|
||||
import { runAll, step001 } from './big';
|
||||
import { normalize } from './support';
|
||||
|
||||
/** Faehrt den generierten Korpus einmal durch und gibt das Ergebnis zurueck. */
|
||||
export function main(seed = 1): number {
|
||||
const first = step001(seed);
|
||||
return normalize(runAll(first));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Zwei Hilfsfunktionen, die big.ts aufruft. Sie sind hier, damit die grosse
|
||||
// Datei nicht beziehungslos im Graphen haengt: der Indexer sieht Importe und
|
||||
// Aufrufkanten und der Reader hat neben big.ts noch etwas Kleines zu zeigen.
|
||||
|
||||
/** Haelt einen Wert in einem festen Fenster, damit runAll nicht davonlaeuft. */
|
||||
export function normalize(value: number): number {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return ((value % 997) + 997) % 997;
|
||||
}
|
||||
|
||||
/** Gewichtet einen Wert und normalisiert ihn danach. */
|
||||
export function weigh(value: number): number {
|
||||
return normalize(Math.trunc(value / 3) + 11);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"rootDir": ".",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Sample fixture provenance
|
||||
|
||||
Copied from Bernhard Jackiewicz's CodeAtlasIDE project on 2026-08-28,
|
||||
`fixtures/atlas-sample`, by the same author.
|
||||
|
||||
The twelve TypeScript and configuration files form a deliberately small, stable
|
||||
demo and test corpus. Graph assertions depend on that stable source. Changes here
|
||||
apply only to this fixture; they do not update its source project.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "atlas-sample",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Indexing fixture for CodeAtlas. Never built, installed or executed by the monorepo.",
|
||||
"main": "src/server.ts",
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Configuration loader. Every value comes from the environment, which gives
|
||||
// the fixture a stable set of environment read sites for the graph.
|
||||
|
||||
export interface AppConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
databaseUrl: string;
|
||||
logLevel: string;
|
||||
}
|
||||
|
||||
export function loadConfig(): AppConfig {
|
||||
return {
|
||||
host: process.env.HOST ?? '127.0.0.1',
|
||||
port: Number(process.env.PORT ?? '8080'),
|
||||
databaseUrl: process.env.DB_URL ?? 'memory://atlas-sample',
|
||||
logLevel: process.env.LOG_LEVEL ?? 'info',
|
||||
};
|
||||
}
|
||||
|
||||
export function isProduction(): boolean {
|
||||
return process.env.NODE_ENV === 'production';
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// In-memory storage stand-in. No driver and no network: the fixture only needs
|
||||
// call sites and control flow that an indexer can see.
|
||||
|
||||
export interface Row {
|
||||
id: string;
|
||||
table: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
name: string;
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
const rows: Row[] = [];
|
||||
|
||||
export function query(table: string, limit: number): Row[] {
|
||||
const matches: Row[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
if (row.table === table) {
|
||||
matches.push(row);
|
||||
}
|
||||
if (matches.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function insert(table: string, id: string, payload: Record<string, unknown>): Row {
|
||||
const row: Row = { id, table, payload };
|
||||
rows.push(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
// Deliberate hotspot: three nested loops with a linear lookup in the innermost
|
||||
// body. The complexity view is expected to flag this function.
|
||||
export function hotspotScan(tables: string[], keys: string[], needles: string[]): Row[] {
|
||||
const found: Row[] = [];
|
||||
for (const table of tables) {
|
||||
for (const key of keys) {
|
||||
for (const needle of needles) {
|
||||
const candidates = query(table, 1000);
|
||||
const position = candidates.map((row) => row.id).indexOf(needle);
|
||||
const hit = candidates.find((row) => String(row.payload[key]) === needle);
|
||||
if (position >= 0 && hit) {
|
||||
found.push(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// Deliberately unguarded recursion: walk has no base case of its own and
|
||||
// relies on the child list running out.
|
||||
export function walk(node: TreeNode): string[] {
|
||||
const names: string[] = [node.name];
|
||||
for (const child of node.children) {
|
||||
for (const name of walk(child)) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Order routes. A single parameterised lookup that delegates to the service.
|
||||
|
||||
import { getOrder } from '../services/orderService';
|
||||
// Deliberately unused harness fixture: proves the file-level import finding.
|
||||
import { insert } from '../repo/db';
|
||||
import type { HttpRequest, HttpResponse, Router } from '../types';
|
||||
|
||||
export function registerOrderRoutes(router: Router): void {
|
||||
router.get('/orders/:id', (req: HttpRequest, res: HttpResponse) => {
|
||||
const order = getOrder(req.params.id);
|
||||
if (!order) {
|
||||
res.status(404).json({ error: 'order not found' });
|
||||
return;
|
||||
}
|
||||
res.json({ order });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// User routes. Handlers stay thin so the call edge from the route to the
|
||||
// service is the only thing the graph has to resolve.
|
||||
|
||||
import { createUser, listUsers } from '../services/userService';
|
||||
import type { HttpRequest, HttpResponse, Router } from '../types';
|
||||
|
||||
export function registerUserRoutes(router: Router): void {
|
||||
router.get('/users', (req: HttpRequest, res: HttpResponse) => {
|
||||
const limit = Number(req.query.limit ?? '50');
|
||||
const users = listUsers(limit);
|
||||
res.json({ users, count: users.length });
|
||||
});
|
||||
|
||||
router.post('/users', (req: HttpRequest, res: HttpResponse) => {
|
||||
const user = createUser(req.body);
|
||||
res.status(201).json({ user });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Entry point of the atlas-sample fixture. Builds the router, registers the
|
||||
// route modules and prints the routing table. Nothing here opens a socket:
|
||||
// the fixture is indexed, never executed.
|
||||
|
||||
import { loadConfig } from './config';
|
||||
import { registerOrderRoutes } from './routes/orders';
|
||||
import { registerUserRoutes } from './routes/users';
|
||||
import type { RouteHandler, Router } from './types';
|
||||
|
||||
interface Route {
|
||||
method: string;
|
||||
path: string;
|
||||
handler: RouteHandler;
|
||||
}
|
||||
|
||||
export class SimpleRouter implements Router {
|
||||
private readonly routes: Route[] = [];
|
||||
|
||||
get(path: string, handler: RouteHandler): void {
|
||||
this.routes.push({ method: 'GET', path, handler });
|
||||
}
|
||||
|
||||
post(path: string, handler: RouteHandler): void {
|
||||
this.routes.push({ method: 'POST', path, handler });
|
||||
}
|
||||
|
||||
table(): string[] {
|
||||
return this.routes.map((route) => `${route.method} ${route.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp(): SimpleRouter {
|
||||
const router = new SimpleRouter();
|
||||
registerUserRoutes(router);
|
||||
registerOrderRoutes(router);
|
||||
return router;
|
||||
}
|
||||
|
||||
export function main(): void {
|
||||
const config = loadConfig();
|
||||
const app = createApp();
|
||||
for (const entry of app.table()) {
|
||||
console.log(`[atlas-sample] route ${entry}`);
|
||||
}
|
||||
console.log(`[atlas-sample] would listen on ${config.host}:${config.port}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,35 @@
|
||||
// Order use cases. getOrder wraps the repository call in try/catch so the
|
||||
// fixture has a recoverable failure path to render.
|
||||
|
||||
import { insert, query } from '../repo/db';
|
||||
import type { Order } from '../types';
|
||||
import { validateId } from '../util/validate';
|
||||
|
||||
export function getOrder(id: string): Order | undefined {
|
||||
try {
|
||||
const key = validateId(id);
|
||||
const rows = query('orders', 100);
|
||||
const row = rows.find((candidate) => candidate.id === key);
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
createdAt: String(row.payload.createdAt ?? ''),
|
||||
customerId: String(row.payload.customerId ?? ''),
|
||||
total: Number(row.payload.total ?? 0),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[orders] lookup failed for ${id}: ${String(error)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Same bare name as userService.create on purpose: the pair is the ambiguity
|
||||
// fixture for path tracing.
|
||||
export function create(customerId: string, total: number): Order {
|
||||
const createdAt = new Date(0).toISOString();
|
||||
const id = `order-${customerId}-${total}`;
|
||||
insert('orders', id, { customerId, total, createdAt });
|
||||
return { id, createdAt, customerId, total };
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// User use cases. Kept small on purpose so the facts the graph reports for
|
||||
// createUser stay stable: it calls, throws and reads exactly one thing each.
|
||||
|
||||
import { insert, query, Row } from '../repo/db';
|
||||
import { UserEntity } from '../types';
|
||||
import type { User } from '../types';
|
||||
import { ValidationError, validateUser } from '../util/validate';
|
||||
|
||||
function toUser(row: Row): User {
|
||||
return {
|
||||
id: row.id,
|
||||
createdAt: String(row.payload.createdAt ?? ''),
|
||||
email: String(row.payload.email ?? ''),
|
||||
name: String(row.payload.name ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
export function listUsers(limit = 50): User[] {
|
||||
const rows = query('users', limit);
|
||||
return rows.map(toUser);
|
||||
}
|
||||
|
||||
export function createUser(input: unknown): User {
|
||||
const parsed = validateUser(input);
|
||||
const dsn = process.env.DB_URL ?? '';
|
||||
if (dsn.length === 0) {
|
||||
throw new ValidationError('DB_URL', 'DB_URL must be set before writing users');
|
||||
}
|
||||
const entity = new UserEntity(`user-${listUsers().length + 1}`, parsed.email, parsed.name);
|
||||
const row = insert('users', entity.id, {
|
||||
email: entity.email,
|
||||
name: entity.name,
|
||||
createdAt: entity.createdAt,
|
||||
});
|
||||
return toUser(row);
|
||||
}
|
||||
|
||||
// Thin alias kept on purpose: orderService exports a function with the same
|
||||
// bare name, which makes "create" an ambiguous trace target.
|
||||
export function create(input: unknown): User {
|
||||
return createUser(input);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Shared types for the atlas-sample fixture: a small express-like HTTP surface
|
||||
// plus the domain entities the services work with. No runtime dependencies.
|
||||
|
||||
export interface HttpRequest {
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status(code: number): HttpResponse;
|
||||
json(payload: unknown): void;
|
||||
}
|
||||
|
||||
export type RouteHandler = (req: HttpRequest, res: HttpResponse) => void;
|
||||
|
||||
export interface Router {
|
||||
get(path: string, handler: RouteHandler): void;
|
||||
post(path: string, handler: RouteHandler): void;
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface User extends Entity {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Order extends Entity {
|
||||
customerId: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export class UserEntity implements Entity {
|
||||
readonly id: string;
|
||||
readonly createdAt: string;
|
||||
readonly email: string;
|
||||
readonly name: string;
|
||||
|
||||
constructor(id: string, email: string, name: string) {
|
||||
this.id = id;
|
||||
this.email = email;
|
||||
this.name = name;
|
||||
this.createdAt = new Date(0).toISOString();
|
||||
}
|
||||
|
||||
label(): string {
|
||||
return `${this.name} <${this.email}>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Input validation. ValidationError lives here so both services throw the
|
||||
// same class and the graph has a single error type to point at.
|
||||
|
||||
export class ValidationError extends Error {
|
||||
readonly field: string;
|
||||
|
||||
constructor(field: string, message: string) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserInput {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function validateUser(input: unknown): UserInput {
|
||||
const candidate = input as Partial<UserInput> | null;
|
||||
if (!candidate || typeof candidate.email !== 'string') {
|
||||
throw new ValidationError('email', 'email must be a string');
|
||||
}
|
||||
if (!candidate.email.includes('@')) {
|
||||
throw new ValidationError('email', 'email must contain an at sign');
|
||||
}
|
||||
if (typeof candidate.name !== 'string' || candidate.name.length === 0) {
|
||||
throw new ValidationError('name', 'name must be a non-empty string');
|
||||
}
|
||||
return { email: candidate.email, name: candidate.name };
|
||||
}
|
||||
|
||||
export function validateId(value: string): string {
|
||||
if (value.trim().length === 0) {
|
||||
throw new ValidationError('id', 'id must not be empty');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Minimal fixture test. It exercises listUsers and nothing else, so the graph
|
||||
// is expected to show a test edge for that function only.
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { listUsers } from '../src/services/userService';
|
||||
|
||||
test('listUsers returns an array', () => {
|
||||
const users = listUsers();
|
||||
assert.ok(Array.isArray(users));
|
||||
});
|
||||
|
||||
test('listUsers honours the limit', () => {
|
||||
const users = listUsers(1);
|
||||
assert.ok(users.length <= 1);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"rootDir": ".",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Codebase Memory — Graph</title>
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Crect width=%2716%27 height=%2716%27 fill=%27%230a0e0d%27/%3E%3Ccircle cx=%278%27 cy=%278%27 r=%274%27 fill=%27%2333ff99%27/%3E%3C/svg%3E" />
|
||||
<title>CBM Lens Beta</title>
|
||||
</head>
|
||||
<body class="bg-[#0a0a10] text-[#e4e4ed] min-h-screen">
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/bin/sh
|
||||
# Ein Modell von Hugging Face in das Cache-Verzeichnis dieses Projekts holen.
|
||||
#
|
||||
# Aufruf:
|
||||
# llm/fetch-model.sh <user>/<repo>[:<quant>] holt das Modell
|
||||
# llm/fetch-model.sh --print <user>/<repo> zeigt nur den Befehl
|
||||
# llm/fetch-model.sh --list zeigt, was im Cache liegt
|
||||
#
|
||||
# ## Warum dieses Skript und nicht ein Knopf in der Oberflaeche
|
||||
#
|
||||
# Die Oberflaeche ist eine SPA ohne eigenes Backend. Sie startet keinen Prozess
|
||||
# und schreibt nichts auf die Platte, also kann sie auch nichts herunterladen.
|
||||
# Was sie kann, tut sie: sie zeigt die Vorschlaege mit ihren gemessenen Zahlen
|
||||
# und den fertigen Aufruf zum Kopieren. Der Aufruf ist dieser hier.
|
||||
#
|
||||
# ## Was hier wirklich passiert
|
||||
#
|
||||
# llama-server kann selbst laden: `-hf user/repo[:quant]` holt die Datei von
|
||||
# huggingface.co in das Verzeichnis, das llama.cpp als Cache liest
|
||||
# (Umgebungsvariable LLAMA_CACHE, ohne sie auf macOS $HOME/Library/Caches/
|
||||
# llama.cpp). Dieses Skript setzt LLAMA_CACHE auf das Cache-Verzeichnis dieses
|
||||
# Projekts, damit ein geholtes Modell in derselben Liste auftaucht wie die schon
|
||||
# vorhandenen und llm/start.sh es ohne weitere Angabe findet.
|
||||
#
|
||||
# Der Quant ist optional; ohne ihn nimmt llama-server Q4_K_M, und wenn es den
|
||||
# nicht gibt, die erste Datei des Repositories. Er wird ohne Ansehen der
|
||||
# Gross- und Kleinschreibung gesucht.
|
||||
#
|
||||
# ## Der eine Satz, der vor jedem Lauf dasteht
|
||||
#
|
||||
# Dieser Befehl GEHT INS NETZ. Er ist damit die eine Stelle, an der dieses
|
||||
# ansonsten abgeschottete Produkt eine fremde Adresse anspricht, und er sagt das
|
||||
# vorher, samt Ziel und Ablageort. Ein Beweislauf fuehrt ihn nicht aus.
|
||||
|
||||
set -eu
|
||||
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/.." && pwd)
|
||||
SERVER="$ROOT/vendor/llama/llama-server"
|
||||
|
||||
# Dasselbe Verzeichnis wie in llm/start.sh, mit derselben Reihenfolge. Zwei
|
||||
# Vorgaben fuer denselben Ort waeren ein geholtes Modell, das in der Liste des
|
||||
# Starters nicht auftaucht.
|
||||
MODELS_DIR=${ATLAS_MODELS_DIR:-${LLAMA_CACHE:-$ROOT/models}}
|
||||
|
||||
usage() {
|
||||
echo "Aufruf: llm/fetch-model.sh <user>/<repo>[:<quant>]" >&2
|
||||
echo " llm/fetch-model.sh --print <user>/<repo>[:<quant>]" >&2
|
||||
echo " llm/fetch-model.sh --list" >&2
|
||||
echo "" >&2
|
||||
echo "Beispiele (die sechs gemessenen Kandidaten stehen in docs/adr/0001-modellwahl.md):" >&2
|
||||
echo " llm/fetch-model.sh unsloth/Qwen3.5-2B-GGUF" >&2
|
||||
echo " llm/fetch-model.sh LiquidAI/LFM2.5-1.2B-Instruct-GGUF:Q4_K_M" >&2
|
||||
echo "" >&2
|
||||
echo "Cache-Verzeichnis: $MODELS_DIR (ueber ATLAS_MODELS_DIR aenderbar)" >&2
|
||||
}
|
||||
|
||||
PRINT_ONLY=0
|
||||
case "${1:-}" in
|
||||
--list)
|
||||
echo "Cache-Verzeichnis: $MODELS_DIR"
|
||||
FOUND=0
|
||||
for CANDIDATE in "$MODELS_DIR"/*.gguf; do
|
||||
if [ -f "$CANDIDATE" ]; then
|
||||
FOUND=1
|
||||
echo " $(basename "$CANDIDATE")"
|
||||
fi
|
||||
done
|
||||
if [ "$FOUND" -eq 0 ]; then
|
||||
echo " (leer)"
|
||||
echo "Ein Modell holen: llm/fetch-model.sh unsloth/Qwen3.5-2B-GGUF"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
--print)
|
||||
PRINT_ONLY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help|'')
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
REPO=${1:-}
|
||||
if [ -z "$REPO" ]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Eine reine Formpruefung, keine Netzabfrage: user/repo, wahlweise mit :quant.
|
||||
# Ob es das Repository gibt, weiss nur huggingface.co, und dieses Skript
|
||||
# behauptet darueber nichts.
|
||||
if ! printf '%s' "$REPO" | grep -Eq '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+(:[A-Za-z0-9._-]+)?$'; then
|
||||
echo "Das ist keine Repo-Kennung: $REPO" >&2
|
||||
echo "Erwartet wird user/repo oder user/repo:quant, zum Beispiel" >&2
|
||||
echo "unsloth/Qwen3.5-2B-GGUF oder unsloth/Qwen3.5-2B-GGUF:Q4_K_M." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
COMMAND="LLAMA_CACHE=\"$MODELS_DIR\" $SERVER -hf $REPO --no-warmup -c 512"
|
||||
|
||||
echo "Dieser Aufruf geht ins Netz: er laedt $REPO von huggingface.co"
|
||||
echo "und legt die Datei in $MODELS_DIR ab."
|
||||
echo "Die Anwendung selbst laedt nichts herunter; sie zeigt nur diesen Aufruf."
|
||||
echo ""
|
||||
echo "$COMMAND"
|
||||
|
||||
if [ "$PRINT_ONLY" -eq 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x "$SERVER" ]; then
|
||||
echo "" >&2
|
||||
echo "llama-server fehlt: $SERVER" >&2
|
||||
echo "vendor/llama/ ist gitignoriert; Herkunft und Baubefehl stehen in vendor/llama/HERKUNFT.md." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
mkdir -p "$MODELS_DIR"
|
||||
|
||||
# Im Vordergrund und ohne Kunstgriffe.
|
||||
#
|
||||
# `-hf` laedt und startet danach den Dienst; einen Modus "nur laden" gibt es
|
||||
# nicht. Dieses Skript raet deshalb nicht, wann der Download fertig ist, sondern
|
||||
# sagt, woran man es sieht, und ueberlaesst das Beenden dem Leser. Ein Skript,
|
||||
# das nach einer geschaetzten Frist zuschlaegt, waere ein Skript, das einen
|
||||
# halben Download fuer fertig erklaeren kann.
|
||||
echo ""
|
||||
echo "Der Server laedt jetzt und startet danach von selbst. Sobald in seiner"
|
||||
echo "Ausgabe \"server is listening\" steht, ist die Datei im Cache: dann mit"
|
||||
echo "Ctrl-C beenden. Die Datei bleibt liegen, und llm/start.sh findet sie."
|
||||
echo ""
|
||||
exec env LLAMA_CACHE="$MODELS_DIR" "$SERVER" -hf "$REPO" --no-warmup -c 512
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/bin/sh
|
||||
# Den lokalen Modell-Sidecar starten. Ein Prozess, ein Port, eine PID-Datei.
|
||||
#
|
||||
# Warum es dieses Skript gibt und nicht einen Knopf in der Oberflaeche: die SPA
|
||||
# hat kein eigenes Backend und kann keinen Prozess starten (ADR 0001,
|
||||
# Abschnitt "Engine-Entscheidung"). Ein Knopf, der einen Prozess verspricht,
|
||||
# waere eine Luege in Knopfform. Der Manager im Browser erkennt, zeigt und
|
||||
# leitet an; gestartet wird hier, von Hand oder aus einem Beweislauf.
|
||||
#
|
||||
# ## Die Vorgabe ist seit W10 der Router, nicht ein Dateiname
|
||||
#
|
||||
# Ohne Argument faehrt dieses Skript llama-server ueber ein CACHE-VERZEICHNIS:
|
||||
#
|
||||
# --models-dir "$ATLAS_MODELS_DIR" --models-max N --models-autoload
|
||||
#
|
||||
# Damit listet /v1/models, was in dem Verzeichnis liegt, und das Feld "model"
|
||||
# einer Chat-Anfrage waehlt daraus aus. Das Einstellungen-Panel kann so zwischen
|
||||
# Modellen umschalten, ohne dass jemand einen Prozess neu startet, und ohne dass
|
||||
# die Oberflaeche einen Prozess anfassen muesste, was sie nicht darf.
|
||||
#
|
||||
# Der Grund fuer die Umstellung steht im Nachtrag W10 von docs/adr/0001: das
|
||||
# Programm liefert kein Modell mehr aus und nennt auch keins als gesetzt. Welche
|
||||
# Datei laeuft, entscheidet der Leser.
|
||||
#
|
||||
# ## Woher das Cache-Verzeichnis kommt
|
||||
#
|
||||
# ATLAS_MODELS_DIR, sonst $LLAMA_CACHE (die Umgebungsvariable, die llama.cpp
|
||||
# selbst fuer seinen Cache liest), sonst <repo>/models. Das letzte ist kein
|
||||
# Notbehelf: models/ IST das lokale Cache-Verzeichnis dieses Projekts. Es ist
|
||||
# gitignoriert, sein Inhalt gehoert dem Leser, und llm/fetch-model.sh laedt in
|
||||
# genau dieses Verzeichnis, damit ein geholtes Modell in derselben Liste
|
||||
# auftaucht wie die schon vorhandenen.
|
||||
#
|
||||
# ## Aufruf
|
||||
#
|
||||
# llm/start.sh Router ueber das Cache-Verzeichnis
|
||||
# llm/start.sh <pfad>.gguf eine bestimmte Datei
|
||||
# llm/start.sh <name> eine Datei aus dem Cache-Verzeichnis
|
||||
# llm/start.sh hf:user/repo[:quant] direkt von Hugging Face (geht ins Netz)
|
||||
# llm/start.sh class-a die Reproduktionsgriffe der ADR-Eval
|
||||
# llm/start.sh --print-command [..] zeigt die Befehlszeile und startet nichts
|
||||
#
|
||||
# Aus der Umgebung: ATLAS_MODEL (wie das Argument), ATLAS_CTX (Kontextfenster),
|
||||
# ATLAS_MODELS_DIR, ATLAS_MODELS_MAX (wie viele Instanzen gleichzeitig).
|
||||
#
|
||||
# ## --print-command, und warum es das gibt
|
||||
#
|
||||
# Ein Beweislauf soll pruefen koennen, WOMIT dieses Skript den Server aufruft,
|
||||
# ohne dafuer ein Modell zu laden. Ohne diesen Schalter muesste er Gigabyte in
|
||||
# den Speicher heben, um eine Zeichenkette zu sehen. Er schreibt die Zeile auf
|
||||
# stdout, startet nichts und fasst weder Port noch PID-Datei an.
|
||||
#
|
||||
# Idempotent: laeuft der eigene Sidecar schon, passiert nichts und das Skript
|
||||
# endet mit 0. Lauscht etwas ANDERES auf dem Port, wird nichts gestartet und
|
||||
# nichts abgeschossen; das Skript sagt, was es vorgefunden hat, und endet mit 3.
|
||||
|
||||
set -eu
|
||||
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/.." && pwd)
|
||||
PORT=4141
|
||||
HOST=127.0.0.1
|
||||
PID_FILE="$HERE/.pid"
|
||||
LOG_FILE="$HERE/llama-server.log"
|
||||
SERVER="$ROOT/vendor/llama/llama-server"
|
||||
|
||||
PRINT_ONLY=0
|
||||
if [ "${1:-}" = "--print-command" ]; then
|
||||
PRINT_ONLY=1
|
||||
shift
|
||||
fi
|
||||
|
||||
# Das Cache-Verzeichnis. Siehe Kopf: die Reihenfolge ist Argument-Umgebung,
|
||||
# llama.cpp-Umgebung, Projektverzeichnis.
|
||||
MODELS_DIR=${ATLAS_MODELS_DIR:-${LLAMA_CACHE:-$ROOT/models}}
|
||||
# Wie viele Instanzen der Router gleichzeitig geladen halten darf. Zwei, weil
|
||||
# genau zwei gebraucht werden, um ohne Neustart umzuschalten: die laufende und
|
||||
# die gewaehlte. Jede weitere kostet den Speicher eines ganzen Modells fuer eine
|
||||
# Auswahl, die niemand getroffen hat. Die Vorgabe von llama-server waere 4.
|
||||
MODELS_MAX=${ATLAS_MODELS_MAX:-2}
|
||||
|
||||
CHOICE=${1:-${ATLAS_MODEL:-}}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Die Reproduktionsgriffe der ADR-Eval.
|
||||
#
|
||||
# Das ist KEINE Vorgabe mehr, und die Reihenfolge in dieser Datei sagt das: die
|
||||
# Vorgabe ist der Router weiter unten. Diese sechs Namen sind die Griffe, mit
|
||||
# denen die Laeufe von W5 wiederholbar bleiben (tools/smoke-w5a.mjs faehrt
|
||||
# class-a und erwartet Qwen3.5-2B mit Kontext 3072, tools/smoke-w5b.mjs und
|
||||
# tools/smoke-w6-full.mjs fahren die anderen, tools/eval-check.mjs erwartet die
|
||||
# Dateien unter models/). Sie sind Belege und keine Empfehlung; welches Modell
|
||||
# gut ist, sagt die Eval-Tabelle im ADR, und welches laeuft, sagt der Leser.
|
||||
#
|
||||
# Sie loesen darum ausdruecklich gegen <repo>/models auf und nicht gegen
|
||||
# $ATLAS_MODELS_DIR: die Dateien der Eval liegen dort, und ein gesetztes
|
||||
# LLAMA_CACHE wuerde sonst einen Reproduktionslauf ins Leere schicken.
|
||||
#
|
||||
# Die Wahlen hiessen bis zum 2026-08-29 "1b" und "4b". Befund 17 des
|
||||
# unabhaengigen Audits: der Sieger der Klasse A ist ein 2B-Modell, und eine
|
||||
# Beschriftung, die es "1b" nennt, sagt etwas Falsches ueber die Datei, die sie
|
||||
# laedt. Die Klasse haengt ohnehin am Kontextfenster und nicht an der
|
||||
# Parameterzahl (siehe modelClassOf in src/llm/sidecar.ts). Die alten Namen
|
||||
# bleiben als Alias gueltig.
|
||||
# ---------------------------------------------------------------------------
|
||||
ALIAS_FILE=''
|
||||
ALIAS_CTX=''
|
||||
case "$CHOICE" in
|
||||
class-a|1b) ALIAS_FILE=Qwen3.5-2B-Q4_K_M.gguf ; ALIAS_CTX=3072 ;;
|
||||
class-a-lfm|1b-lfm) ALIAS_FILE=LFM2.5-1.2B-Instruct-Q4_K_M.gguf ; ALIAS_CTX=3072 ;;
|
||||
class-a-minicpm|1b-minicpm) ALIAS_FILE=MiniCPM5-1B-Q4_K_M.gguf ; ALIAS_CTX=3072 ;;
|
||||
class-a-coder|1b-coder) ALIAS_FILE=qwen2.5-coder-1.5b-instruct-q4_k_m.gguf ; ALIAS_CTX=3072 ;;
|
||||
class-b|4b) ALIAS_FILE=Qwen3.5-4B-Q4_K_M.gguf ; ALIAS_CTX=8192 ;;
|
||||
class-b-gemma|4b-gemma) ALIAS_FILE=gemma-4-E4B-it-Q4_K_M.gguf ; ALIAS_CTX=8192 ;;
|
||||
esac
|
||||
|
||||
case "$CHOICE" in
|
||||
1b|1b-lfm|1b-minicpm|1b-coder|4b|4b-gemma)
|
||||
echo "Hinweis: \"$CHOICE\" ist der alte Name dieser Wahl. Sie heisst jetzt nach ihrer" >&2
|
||||
echo "Klasse; die Klassen sind class-a (Kontext 3072) und class-b (Kontext 8192)." >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
# Der Kontext: die Umgebung schlaegt den Alias, der Alias die Vorgabe. 3072 ist
|
||||
# das Budget der Klasse A aus PLAN Paragraph 5 und damit die konservative Wahl
|
||||
# fuer ein Modell, ueber das dieses Skript nichts weiss.
|
||||
CTX=${ATLAS_CTX:-${ALIAS_CTX:-3072}}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Die Argumente des Servers, in genau einer Variablen.
|
||||
#
|
||||
# Sie entstehen hier und werden weiter unten entweder gedruckt oder gefahren.
|
||||
# Zwei Stellen, an denen die Zeile entsteht, waeren zwei Zeilen: der Beweislauf
|
||||
# saehe die eine und der Leser bekaeme die andere.
|
||||
# ---------------------------------------------------------------------------
|
||||
MODE=''
|
||||
DESCRIPTION=''
|
||||
set -- --host "$HOST" --port "$PORT"
|
||||
|
||||
if [ -n "$ALIAS_FILE" ]; then
|
||||
MODE=eval-alias
|
||||
MODEL_PATH="$ROOT/models/$ALIAS_FILE"
|
||||
if [ "$PRINT_ONLY" -eq 0 ] && [ ! -f "$MODEL_PATH" ]; then
|
||||
echo "Modell fehlt: $MODEL_PATH" >&2
|
||||
echo "\"$CHOICE\" ist ein Reproduktionsgriff der ADR-Eval und erwartet die Datei aus" >&2
|
||||
echo "models/SHA256SUMS. Holen: llm/fetch-model.sh --list" >&2
|
||||
exit 4
|
||||
fi
|
||||
# Der relative Pfad, byte-identisch zu dem, was dieses Skript vor W10
|
||||
# uebergeben hat: /props meldet ihn woertlich zurueck, und die Beweislaeufe
|
||||
# von W5 und W6 lesen ihn dort.
|
||||
set -- "$@" -m "models/$ALIAS_FILE" -c "$CTX"
|
||||
DESCRIPTION="$ALIAS_FILE (Reproduktionsgriff $CHOICE), Kontext $CTX"
|
||||
elif [ -z "$CHOICE" ]; then
|
||||
MODE=router
|
||||
set -- "$@" --models-dir "$MODELS_DIR" --models-max "$MODELS_MAX" --models-autoload -c "$CTX"
|
||||
DESCRIPTION="Router ueber $MODELS_DIR (hoechstens $MODELS_MAX geladen), Kontext $CTX"
|
||||
else
|
||||
case "$CHOICE" in
|
||||
hf:*)
|
||||
MODE=hf
|
||||
REPO=${CHOICE#hf:}
|
||||
set -- "$@" -hf "$REPO" -c "$CTX"
|
||||
DESCRIPTION="$REPO von huggingface.co, Kontext $CTX"
|
||||
;;
|
||||
*)
|
||||
MODE=file
|
||||
# Ein Pfad, oder ein Name im Cache-Verzeichnis. Erst wird gefragt,
|
||||
# ob die Datei so existiert; erst dann wird sie im Cache gesucht.
|
||||
if [ -f "$CHOICE" ]; then
|
||||
MODEL_PATH="$CHOICE"
|
||||
elif [ -f "$MODELS_DIR/$CHOICE" ]; then
|
||||
MODEL_PATH="$MODELS_DIR/$CHOICE"
|
||||
elif [ -f "$MODELS_DIR/$CHOICE.gguf" ]; then
|
||||
MODEL_PATH="$MODELS_DIR/$CHOICE.gguf"
|
||||
else
|
||||
echo "unbekannte Modellwahl: $CHOICE" >&2
|
||||
echo "Erwartet wird eines von:" >&2
|
||||
echo " ein Pfad auf eine .gguf-Datei" >&2
|
||||
echo " ein Dateiname aus $MODELS_DIR" >&2
|
||||
echo " hf:user/repo[:quant] (laedt von huggingface.co)" >&2
|
||||
echo " ein Reproduktionsgriff der ADR-Eval, davon" >&2
|
||||
# Diese Zeile ist eine Naht: src/llm/strings.test.ts liest sie aus
|
||||
# diesem Skript und vergleicht sie mit LLM_MODEL_CHOICES. Die
|
||||
# Liste steht damit einmal in der Wahrheit und einmal in der
|
||||
# Anzeige, und ein Test haelt beide zusammen (Audit-Befund 17).
|
||||
echo "erlaubt: class-a | class-a-lfm | class-a-minicpm | class-a-coder | class-b | class-b-gemma" >&2
|
||||
echo " (die alten Namen 1b, 1b-lfm, 1b-minicpm, 1b-coder, 4b, 4b-gemma gelten weiter)" >&2
|
||||
echo "Ohne Argument faehrt dieses Skript den Router ueber $MODELS_DIR." >&2
|
||||
exit 2
|
||||
fi
|
||||
set -- "$@" -m "$MODEL_PATH" -c "$CTX"
|
||||
DESCRIPTION="$MODEL_PATH, Kontext $CTX"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$PRINT_ONLY" -eq 1 ]; then
|
||||
printf '%s' "$SERVER"
|
||||
for ARG in "$@"; do
|
||||
printf ' %s' "$ARG"
|
||||
done
|
||||
printf '\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x "$SERVER" ]; then
|
||||
echo "llama-server fehlt: $SERVER" >&2
|
||||
echo "vendor/llama/ ist gitignoriert; Herkunft und Baubefehl stehen in vendor/llama/HERKUNFT.md." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Ein leeres Cache-Verzeichnis ist kein Start, sondern eine Auskunft.
|
||||
#
|
||||
# Ohne diese Pruefung liefe der Router an und antwortete auf jede Anfrage mit
|
||||
# einer leeren Modell-Liste, und aus der Ferne saehe das aus wie ein Defekt.
|
||||
# Gesagt wird darum, was fehlt und wie man es holt.
|
||||
if [ "$MODE" = router ]; then
|
||||
# Ein Glob und kein find: `find -quit` gibt es nicht ueberall, und die Frage
|
||||
# ist ohnehin nur, ob EINE Datei da ist.
|
||||
HAVE_GGUF=0
|
||||
for CANDIDATE in "$MODELS_DIR"/*.gguf; do
|
||||
if [ -f "$CANDIDATE" ]; then
|
||||
HAVE_GGUF=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$HAVE_GGUF" -eq 0 ]; then
|
||||
echo "Kein Modell im Cache-Verzeichnis: $MODELS_DIR" >&2
|
||||
echo "Dieses Programm liefert keines mit. So holen Sie eines (geht ins Netz):" >&2
|
||||
echo " llm/fetch-model.sh unsloth/Qwen3.5-2B-GGUF" >&2
|
||||
echo "oder direkt:" >&2
|
||||
echo " LLAMA_CACHE=\"$MODELS_DIR\" $SERVER -hf unsloth/Qwen3.5-2B-GGUF:Q4_K_M" >&2
|
||||
echo "Ein anderes Verzeichnis: ATLAS_MODELS_DIR=/pfad/zum/cache llm/start.sh" >&2
|
||||
echo "Die Vorschlaege mit ihren gemessenen Zahlen stehen im Einstellungen-Panel." >&2
|
||||
exit 5
|
||||
fi
|
||||
fi
|
||||
|
||||
# Laeuft der eigene Prozess schon? Die PID-Datei allein reicht als Auskunft
|
||||
# nicht: sie ueberlebt einen Absturz. Gefragt wird deshalb das Betriebssystem.
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
OLD=$(cat "$PID_FILE" 2>/dev/null || echo '')
|
||||
if [ -n "$OLD" ] && kill -0 "$OLD" 2>/dev/null; then
|
||||
echo "Sidecar laeuft bereits (pid $OLD) auf $HOST:$PORT. Nichts zu tun."
|
||||
exit 0
|
||||
fi
|
||||
echo "verwaiste PID-Datei ($OLD) wird entfernt."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
# Der Port gehoert dem Sidecar. Wer sonst dort lauscht, wird nicht abgeschossen:
|
||||
# ein Startskript, das fremde Prozesse beendet, ist ein Startskript, dem man
|
||||
# nicht mehr zusehen kann.
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
BUSY=$(lsof -ti "tcp:$PORT" 2>/dev/null || true)
|
||||
if [ -n "$BUSY" ]; then
|
||||
echo "Port $PORT ist belegt (pid $BUSY), aber nicht von diesem Skript." >&2
|
||||
echo "Nichts gestartet und nichts beendet." >&2
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MODE" = hf ]; then
|
||||
echo "Dieser Aufruf geht ins Netz (huggingface.co) und laedt nach ${LLAMA_CACHE:-$MODELS_DIR}."
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
nohup "$SERVER" "$@" > "$LOG_FILE" 2>&1 &
|
||||
PID=$!
|
||||
echo "$PID" > "$PID_FILE"
|
||||
|
||||
echo "Sidecar gestartet: pid $PID, $DESCRIPTION, http://$HOST:$PORT"
|
||||
if [ "$MODE" = router ]; then
|
||||
echo "Die Modelle im Cache: curl -s http://$HOST:$PORT/v1/models"
|
||||
echo "Umschalten ohne Neustart: im Einstellungen-Panel der Oberflaeche."
|
||||
fi
|
||||
echo "Bereitschaft: curl -s http://$HOST:$PORT/health -> {\"status\":\"ok\"}"
|
||||
echo "Log: $LOG_FILE"
|
||||
echo "Beenden: $HERE/stop.sh"
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
# Den eigenen Sidecar beenden, und nur den eigenen.
|
||||
#
|
||||
# Beendet wird ausschliesslich die PID aus llm/.pid. Kein pkill auf einen
|
||||
# Namen, kein "was auch immer auf 4141 lauscht": beides wuerde fremde Prozesse
|
||||
# treffen, die dieses Skript nie gestartet hat. Steht in der Datei eine PID, die
|
||||
# es nicht mehr gibt, wird die Datei aufgeraeumt und das gesagt.
|
||||
#
|
||||
# Aufruf: llm/stop.sh
|
||||
|
||||
set -eu
|
||||
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
PID_FILE="$HERE/.pid"
|
||||
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "keine PID-Datei ($PID_FILE): dieses Skript hat gerade nichts laufen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE" 2>/dev/null || echo '')
|
||||
if [ -z "$PID" ]; then
|
||||
echo "PID-Datei war leer, wird entfernt."
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! kill -0 "$PID" 2>/dev/null; then
|
||||
echo "pid $PID laeuft nicht mehr; die PID-Datei wird entfernt."
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
kill "$PID" 2>/dev/null || true
|
||||
|
||||
# Erst hoeflich, dann nicht mehr. Fuenf Sekunden reichen dem Sidecar, um seine
|
||||
# Slots abzuraeumen; laenger zu warten hiesse, jedem Beweislauf eine Pause
|
||||
# aufzuzwingen, die nur der Hoeflichkeit dient.
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
if ! kill -0 "$PID" 2>/dev/null; then
|
||||
rm -f "$PID_FILE"
|
||||
echo "Sidecar beendet (pid $PID)."
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
echo "pid $PID reagierte nicht auf SIGTERM, sende SIGKILL." >&2
|
||||
kill -9 "$PID" 2>/dev/null || true
|
||||
sleep 0.3
|
||||
rm -f "$PID_FILE"
|
||||
echo "Sidecar hart beendet (pid $PID)."
|
||||