mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
Merge main into fix/issue-2120 for the release batch
Conflict in internal/cbm/cbm.h resolved by keeping both added CBMDefinition fields: decl_namespace (this branch) and the test-definition role/spans (main). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"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.*"],
|
||||
"action": "run_all"
|
||||
},
|
||||
{
|
||||
"id": "build",
|
||||
"paths": ["scripts/**", "test-infrastructure/**", "pkg/**"],
|
||||
"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}"],
|
||||
"action": "ignore"
|
||||
},
|
||||
{
|
||||
"id": "notices",
|
||||
"paths": ["NOTICE*", "CHANGELOG*", "THIRD_PARTY*"],
|
||||
"action": "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
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
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:
|
||||
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
|
||||
@@ -64,6 +70,7 @@ jobs:
|
||||
# provability (never suppressed — the NOLINT ban applies here too).
|
||||
# Vendored-tree diagnostics are path-filtered, mirroring .cppcheck.
|
||||
lint-mem:
|
||||
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
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
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
|
||||
@@ -23,6 +28,7 @@ jobs:
|
||||
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:
|
||||
@@ -39,6 +45,7 @@ jobs:
|
||||
run: scripts/audit-license-provenance.py
|
||||
|
||||
codeql-gate:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'codeql-gate') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 240
|
||||
# Without security-events:read this job inherits the workflow's
|
||||
|
||||
+101
-16
@@ -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 test-unix and test-windows legs run only its tests. Empty = every test (dry run, release, and any PR the selection does not narrow)'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -30,18 +42,26 @@ 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.
|
||||
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-14","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.
|
||||
@@ -59,7 +79,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 +88,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-14","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 +122,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) }}
|
||||
@@ -160,12 +221,20 @@ jobs:
|
||||
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-
|
||||
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-
|
||||
|
||||
- name: Fetch the test selection
|
||||
if: ${{ inputs.test_selection != '' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
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 != '' && format('{0}/test-selection', runner.temp) || '' }}
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1500M
|
||||
|
||||
@@ -205,6 +274,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:
|
||||
@@ -245,6 +315,7 @@ jobs:
|
||||
# parallel suite — catches what an older compiler-rt can miss. Same
|
||||
# canonical scripts/test.sh wave as every other leg.
|
||||
test-diag:
|
||||
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
|
||||
@@ -310,6 +381,7 @@ jobs:
|
||||
# list both venues now share. Zero use-of-uninitialized-value findings were
|
||||
# reported across the lane on either architecture.
|
||||
test-msan:
|
||||
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'msan') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
@@ -334,6 +406,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:
|
||||
@@ -372,13 +445,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:
|
||||
@@ -431,6 +502,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) }}
|
||||
@@ -481,6 +553,13 @@ jobs:
|
||||
ccache-test-${{ matrix.os }}-${{ matrix.msystem }}-${{ github.ref }}-
|
||||
ccache-test-${{ matrix.os }}-${{ matrix.msystem }}-
|
||||
|
||||
- name: Fetch the test selection
|
||||
if: ${{ inputs.test_selection != '' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ inputs.test_selection }}
|
||||
path: ${{ runner.temp }}/test-selection
|
||||
|
||||
- name: Test
|
||||
shell: msys2 {0}
|
||||
# AddressSanitizer is unavailable on native ARM64 Windows (LLVM ships no
|
||||
@@ -508,6 +587,7 @@ jobs:
|
||||
CBM_TEST_SHARD: ${{ matrix.shard }}
|
||||
CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.msystem }}
|
||||
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
|
||||
CBM_TEST_SELECTION_DIR: ${{ inputs.test_selection != '' && format('{0}/test-selection', runner.temp) || '' }}
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 1500M
|
||||
|
||||
@@ -564,8 +644,12 @@ jobs:
|
||||
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
|
||||
@@ -576,6 +660,7 @@ 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:
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
with:
|
||||
languages: c-cpp
|
||||
build-mode: manual
|
||||
@@ -38,6 +38,6 @@ jobs:
|
||||
run: scripts/build.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
with:
|
||||
category: "/language:c-cpp"
|
||||
|
||||
+199
-33
@@ -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,6 +24,7 @@ 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,
|
||||
@@ -30,33 +34,91 @@ jobs:
|
||||
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:
|
||||
# The PR merge commit and its base parent: the change to select for.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
- name: Build this checkout's engine
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-selection
|
||||
path: ${{ runner.temp }}/test-selection
|
||||
if-no-files-found: error
|
||||
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 }}
|
||||
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 +126,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,31 +145,45 @@ 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-14","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"
|
||||
|
||||
# ── 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:
|
||||
@@ -177,27 +255,115 @@ jobs:
|
||||
# ── 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:
|
||||
# The PR merge commit and its base parent: the diff to predict for.
|
||||
fetch-depth: 2
|
||||
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' }}
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get 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' }}
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
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
|
||||
|
||||
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]
|
||||
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
|
||||
|
||||
@@ -27,6 +27,6 @@ jobs:
|
||||
publish_results: true
|
||||
|
||||
- name: Upload SARIF results
|
||||
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
|
||||
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install build deps and LLVM 21 (coverage toolchain)
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get 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 update
|
||||
sudo apt-get 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
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
|
||||
@@ -51,6 +51,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
|
||||
|
||||
+164
-13
@@ -71,13 +71,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
|
||||
@@ -102,8 +122,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
|
||||
@@ -309,6 +331,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 \
|
||||
@@ -352,12 +375,14 @@ EXTRACTION_SRCS = \
|
||||
$(CBM_DIR)/extract_dbt.c \
|
||||
$(CBM_DIR)/sql_values.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
|
||||
@@ -400,13 +425,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/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/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 = \
|
||||
@@ -425,7 +452,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)
|
||||
@@ -445,6 +483,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 \
|
||||
@@ -617,6 +656,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 \
|
||||
@@ -636,6 +677,7 @@ 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 \
|
||||
@@ -647,6 +689,9 @@ 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_store_search.c \
|
||||
tests/test_store_arch.c \
|
||||
tests/test_store_bulk.c \
|
||||
@@ -659,6 +704,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 = \
|
||||
@@ -676,12 +738,23 @@ 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_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
|
||||
|
||||
@@ -720,7 +793,7 @@ 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_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
|
||||
|
||||
@@ -860,7 +933,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)
|
||||
@@ -899,7 +972,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 $@ $<
|
||||
@@ -1150,7 +1223,7 @@ $(BUILD_DIR)/prod_ts_runtime.o: $(TS_RUNTIME_DEPS) $(BUILD_DIR)/.build-config |
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD_DIR)/prod_lsp_all.o: $(LSP_UNITY_DEPS) $(BUILD_DIR)/.build-config | $(BUILD_DIR)
|
||||
$(CC) $(GRAMMAR_CFLAGS) -c -o $@ $<
|
||||
$(CC) $(GRAMMAR_CFLAGS) $(TEST_COVERAGE_FLAGS) -c -o $@ $<
|
||||
|
||||
$(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 $@ $<
|
||||
@@ -1188,7 +1261,7 @@ MAIN_SRC = src/main.c
|
||||
# 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)
|
||||
@@ -1384,6 +1457,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
|
||||
|
||||
@@ -229,6 +229,7 @@ The install script placed beside the binary is **reported, not deleted** — uni
|
||||
- `USAGE` — an identifier is used, but a unique callable target is not proven (including ambiguous or complex expressions)
|
||||
- `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
|
||||
- `HTTP_CALLS`, `ASYNC_CALLS` (cross-service)
|
||||
- `SPAWNS` — the caller starts another program (`subprocess.run`, `exec.Command`, `posix_spawn`, `Process.Start`, ...); the target is a `Process` node named after the program, or `<dynamic>` when the program is not a literal
|
||||
- `EMITS`, `LISTENS_ON` (channels)
|
||||
- `DATA_FLOWS` with arg-to-param mapping + field access chains
|
||||
- `SIMILAR_TO` (MinHash + LSH near-clone detection, Jaccard scored)
|
||||
@@ -740,7 +741,7 @@ mode. `index_status` keeps describing the published graph and its freshness.
|
||||
|
||||
### Edge Types
|
||||
|
||||
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `REFERENCES_FILE`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
|
||||
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `SPAWNS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `REFERENCES_FILE`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
|
||||
|
||||
### Qualified Names
|
||||
|
||||
|
||||
+140
-6
@@ -22,6 +22,7 @@
|
||||
#include "lsp/rust_lsp.h"
|
||||
#include "preprocessor.h"
|
||||
#include "sql_values.h" // #1735: literal INSERT rows kept out of the SQL parse
|
||||
#include "cpp_branch_views.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write
|
||||
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
|
||||
@@ -2146,13 +2147,110 @@ static bool cbm_sql_values_exclusion_on(const char *rel_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Drop the entries a branch view re-extracted from code outside the
|
||||
* conditionals: a definition with a QN the file already has at the same line,
|
||||
* an import with the same name and path. */
|
||||
static void cpp_view_drop_seen(CBMFileResult *r, int defs_before, int imports_before) {
|
||||
int w = defs_before;
|
||||
for (int i = defs_before; i < r->defs.count; i++) {
|
||||
const CBMDefinition *d = &r->defs.items[i];
|
||||
bool seen = false;
|
||||
for (int j = 0; j < w && !seen; j++) {
|
||||
const CBMDefinition *e = &r->defs.items[j];
|
||||
seen = e->start_line == d->start_line && e->qualified_name && d->qualified_name &&
|
||||
strcmp(e->qualified_name, d->qualified_name) == 0;
|
||||
}
|
||||
if (!seen) {
|
||||
r->defs.items[w++] = *d;
|
||||
}
|
||||
}
|
||||
r->defs.count = w;
|
||||
w = imports_before;
|
||||
for (int i = imports_before; i < r->imports.count; i++) {
|
||||
const CBMImport *m = &r->imports.items[i];
|
||||
bool seen = false;
|
||||
for (int j = 0; j < w && !seen; j++) {
|
||||
const CBMImport *e = &r->imports.items[j];
|
||||
seen = e->local_name && m->local_name && e->module_path && m->module_path &&
|
||||
strcmp(e->local_name, m->local_name) == 0 &&
|
||||
strcmp(e->module_path, m->module_path) == 0;
|
||||
}
|
||||
if (!seen) {
|
||||
r->imports.items[w++] = *m;
|
||||
}
|
||||
}
|
||||
r->imports.count = w;
|
||||
}
|
||||
|
||||
/* Haskell: the scanner parses the first branch of an #if group and swallows
|
||||
* the rest up to #endif as one token, so what is written once per
|
||||
* configuration (a definition, an import, a call in the other branch) was
|
||||
* seen in its first variant only. Every further branch is parsed as its own
|
||||
* view (cpp_branch_views.h: same bytes and lines, the other branches and the
|
||||
* conditional lines blanked) and extracted into the same result, under the
|
||||
* raw parse's budget. A definition at another line under a QN the file
|
||||
* already has is that definition's variant; the graph keeps one identity with
|
||||
* both spans. Calls and usages outside the conditionals come back as
|
||||
* duplicates, which the pipeline dedups by caller and callee. The raw parse
|
||||
* alone decides the file's parse status. */
|
||||
static void extract_cpp_branch_views(const CBMExtractCtx *raw, const TSLanguage *ts_lang,
|
||||
int64_t timeout_micros) {
|
||||
int views = cbm_cpp_branch_view_count(raw->arena, raw->source, raw->source_len);
|
||||
for (int v = 1; v <= views; v++) {
|
||||
char *view = cbm_cpp_branch_view(raw->arena, raw->source, raw->source_len, v);
|
||||
TSParser *parser = view ? get_thread_parser(ts_lang, raw->language) : NULL;
|
||||
if (!parser) {
|
||||
return;
|
||||
}
|
||||
ts_parser_reset(parser);
|
||||
CBMStringInput input = {view, (uint32_t)raw->source_len};
|
||||
TSInput ts_input = {&input, cbm_string_read, TSInputEncodingUTF8, NULL};
|
||||
TSParseOptions opts = {0};
|
||||
CBMParseBudget budget = {0}; // cppcheck-suppress unreadVariable
|
||||
if (timeout_micros > 0) {
|
||||
uint64_t budget_ns = (uint64_t)timeout_micros * USEC_TO_NSEC;
|
||||
budget.cpu_deadline_ns = cbm_thread_cpu_time_ns() + budget_ns;
|
||||
budget.wall_ceiling_ns = now_ns() + budget_ns * CBM_PARSE_WALL_CEILING_FACTOR;
|
||||
opts.payload = &budget;
|
||||
opts.progress_callback = cbm_timeout_cb;
|
||||
}
|
||||
TSTree *tree = ts_parser_parse_with_options(parser, NULL, ts_input, opts);
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
CBMExtractCtx vctx = {
|
||||
.arena = raw->arena,
|
||||
.scratch = raw->scratch,
|
||||
.result = raw->result,
|
||||
.source = view,
|
||||
.source_len = raw->source_len,
|
||||
.language = raw->language,
|
||||
.project = raw->project,
|
||||
.rel_path = raw->rel_path,
|
||||
.module_qn = raw->module_qn,
|
||||
.root = ts_tree_root_node(tree),
|
||||
.macro_table = raw->macro_table,
|
||||
.return_type_table = raw->return_type_table,
|
||||
.walk_budget_nodes = cbm_walk_max_nodes(),
|
||||
};
|
||||
int defs_before = raw->result->defs.count;
|
||||
int imports_before = raw->result->imports.count;
|
||||
cbm_extract_definitions(&vctx);
|
||||
cbm_extract_imports(&vctx);
|
||||
cbm_extract_unified(&vctx);
|
||||
cpp_view_drop_seen(raw->result, defs_before, imports_before);
|
||||
ts_tree_delete(tree);
|
||||
}
|
||||
}
|
||||
|
||||
static CBMFileResult *extract_file_ex_body(const char *source, int source_len, CBMLanguage language,
|
||||
const char *project, const char *rel_path,
|
||||
int64_t timeout_micros, const char **extra_defines,
|
||||
const char **include_paths,
|
||||
const CBMMacroTable *macro_table,
|
||||
const CBMReturnTypeTable *return_type_table,
|
||||
CBMArena *scratch) {
|
||||
CBMArena *scratch,
|
||||
const cbm_test_declarations_t *test_declarations) {
|
||||
// Allocate result on heap (arena inside for all string data)
|
||||
CBMFileResult *result = cbm_result_alloc();
|
||||
if (!result) {
|
||||
@@ -2161,6 +2259,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
|
||||
cbm_work_arena_take(&result->arena);
|
||||
CBMArena *a = &result->arena;
|
||||
if (!cbm_test_declarations_validate(result, test_declarations)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Crash-quarantine hard guard (Stage 3c): a file the supervisor pinned as a
|
||||
* crasher must NEVER be parsed again. Return a clean empty result BEFORE the
|
||||
@@ -2339,13 +2440,19 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
.macro_table = macro_table,
|
||||
.return_type_table = return_type_table,
|
||||
.walk_budget_nodes = cbm_walk_max_nodes(),
|
||||
.test_declarations = test_declarations,
|
||||
.test_declarations_raw_source = true,
|
||||
};
|
||||
|
||||
// Run extractors: defs + imports use separate walks (unique recursion patterns),
|
||||
// then a single unified cursor walk handles the remaining 7 extractors.
|
||||
cbm_extract_definitions(&ctx);
|
||||
cbm_test_declarations_finish(&ctx);
|
||||
cbm_extract_imports(&ctx);
|
||||
cbm_extract_unified(&ctx);
|
||||
if (cbm_lang_needs_cpp_branch_views(language)) {
|
||||
extract_cpp_branch_views(&ctx, ts_lang, timeout_micros);
|
||||
}
|
||||
result->tree_nodes = ts_node_descendant_count(root);
|
||||
result->walk_nodes_visited = ctx.walk_nodes_visited;
|
||||
if (ctx.walk_budget_exhausted) {
|
||||
@@ -2378,8 +2485,20 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
cbm_run_go_lsp(a, result, source, source_len, root);
|
||||
}
|
||||
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
|
||||
cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C,
|
||||
CBM_SOURCE_ORIGIN_RAW);
|
||||
if (!cbm_run_c_lsp_with_test_owners(a, result, source, source_len, root,
|
||||
language != CBM_LANG_C, CBM_SOURCE_ORIGIN_RAW,
|
||||
result)) {
|
||||
result->has_error = true;
|
||||
if (result->test_declarations_status == CBM_TEST_EXTRACT_OK) {
|
||||
result->test_declarations_status = CBM_TEST_EXTRACT_UNSUPPORTED_FORM;
|
||||
result->test_declaration_index = -1;
|
||||
result->test_declaration_line = 0;
|
||||
result->error_msg =
|
||||
cbm_arena_strdup(a, "configured owner source identity mismatch");
|
||||
if (!result->error_msg)
|
||||
result->test_declarations_status = CBM_TEST_EXTRACT_OOM;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (language == CBM_LANG_PHP) {
|
||||
cbm_run_php_lsp(a, result, source, source_len, root);
|
||||
@@ -2490,6 +2609,10 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
.rel_path = rel_path,
|
||||
.module_qn = result->module_qn,
|
||||
.root = pp_root,
|
||||
/* Preset switches still apply, but expanded bytes
|
||||
* cannot acquire a raw configured definition identity. */
|
||||
.test_declarations = test_declarations,
|
||||
.test_declarations_raw_source = false,
|
||||
};
|
||||
// Re-run unified extraction on expanded source.
|
||||
// This adds macro-expanded calls; duplicates with original calls are
|
||||
@@ -3051,6 +3174,16 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
|
||||
int64_t timeout_micros, const char **extra_defines,
|
||||
const char **include_paths, const CBMMacroTable *macro_table,
|
||||
const CBMReturnTypeTable *return_type_table) {
|
||||
return cbm_extract_file_ex_with_tests(source, source_len, language, project, rel_path,
|
||||
timeout_micros, extra_defines, include_paths, macro_table,
|
||||
return_type_table, NULL);
|
||||
}
|
||||
|
||||
CBMFileResult *cbm_extract_file_ex_with_tests(
|
||||
const char *source, int source_len, CBMLanguage language, const char *project,
|
||||
const char *rel_path, int64_t timeout_micros, const char **extra_defines,
|
||||
const char **include_paths, const CBMMacroTable *macro_table,
|
||||
const CBMReturnTypeTable *return_type_table, const cbm_test_declarations_t *test_declarations) {
|
||||
CBMArena scratch;
|
||||
if (tl_scratch_live && tl_scratch_slot) {
|
||||
scratch = *tl_scratch_slot;
|
||||
@@ -3059,9 +3192,9 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
|
||||
} else {
|
||||
cbm_arena_init_lazy(&scratch, CBM_EXTRACT_SCRATCH_BLOCK);
|
||||
}
|
||||
CBMFileResult *result = extract_file_ex_body(source, source_len, language, project, rel_path,
|
||||
timeout_micros, extra_defines, include_paths,
|
||||
macro_table, return_type_table, &scratch);
|
||||
CBMFileResult *result = extract_file_ex_body(
|
||||
source, source_len, language, project, rel_path, timeout_micros, extra_defines,
|
||||
include_paths, macro_table, return_type_table, &scratch, test_declarations);
|
||||
/* !tl_scratch_live: a nested extraction (an embedded language inside this
|
||||
* file) may already have parked its own; never overwrite it. */
|
||||
/* Kept up to CBM_EXTRACT_SCRATCH_KEEP_BYTES, grown blocks included: the
|
||||
@@ -3082,6 +3215,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
|
||||
} else {
|
||||
cbm_arena_destroy(&scratch);
|
||||
}
|
||||
cbm_test_declarations_degrade(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,27 @@ TSNode cbm_ts_child_by_field_name(TSNode node, const char *name, uint32_t name_l
|
||||
* (TS_FIELD("body") expands to "body", 4), which must expand before the call. */
|
||||
#define ts_node_child_by_field_name(...) cbm_ts_child_by_field_name(__VA_ARGS__)
|
||||
|
||||
/* Immutable, explicitly supplied test-declaration snapshot. */
|
||||
typedef struct cbm_test_declarations cbm_test_declarations_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_TEST_ROLE_NONE = 0,
|
||||
CBM_TEST_ROLE_CASE,
|
||||
CBM_TEST_ROLE_SUITE
|
||||
} CBMTestDefinitionRole;
|
||||
|
||||
typedef enum {
|
||||
CBM_TEST_EXTRACT_OK = 0,
|
||||
CBM_TEST_EXTRACT_UNSUPPORTED_LANGUAGE,
|
||||
CBM_TEST_EXTRACT_UNSUPPORTED_PRESET,
|
||||
CBM_TEST_EXTRACT_UNSUPPORTED_NAME,
|
||||
CBM_TEST_EXTRACT_MISSING_ARGUMENT,
|
||||
CBM_TEST_EXTRACT_UNSUPPORTED_ARGUMENT,
|
||||
CBM_TEST_EXTRACT_AMBIGUOUS,
|
||||
CBM_TEST_EXTRACT_UNSUPPORTED_FORM,
|
||||
CBM_TEST_EXTRACT_OOM
|
||||
} CBMTestExtractStatus;
|
||||
|
||||
// Language enum mirrors lang.Language in Go.
|
||||
// Order must match lang_specs.c tables.
|
||||
typedef enum {
|
||||
@@ -254,6 +275,11 @@ typedef struct {
|
||||
* the global namespace and every other language). Per type, because the
|
||||
* file-level namespace_name records only a file's first namespace. */
|
||||
const char *decl_namespace;
|
||||
CBMTestDefinitionRole test_role; /* NONE preserves legacy helper/test-file semantics */
|
||||
/* Configured raw definitions only; zero for all legacy rows. Exact spans
|
||||
* are bound to the owning result's source identity before cross-file use. */
|
||||
uint32_t test_name_start_byte, test_name_end_byte;
|
||||
uint32_t test_body_start_byte, test_body_end_byte;
|
||||
} CBMDefinition;
|
||||
|
||||
/* Argument captured from a call expression */
|
||||
@@ -447,6 +473,10 @@ typedef struct {
|
||||
typedef enum {
|
||||
CBM_RESOLVED_INVOCATION = 0,
|
||||
CBM_RESOLVED_CALL_REFERENCE,
|
||||
/* C-family member access `a.b` / `a->b`: callee_qn is the Field `b` of the
|
||||
* type of `a`. One row per (caller, field), not per occurrence, and no site
|
||||
* span: the join is by enclosing function and member name. */
|
||||
CBM_RESOLVED_FIELD_REFERENCE,
|
||||
} CBMResolvedKind;
|
||||
|
||||
// LSP-resolved invocation/reference: high-confidence type-aware resolution.
|
||||
@@ -651,6 +681,24 @@ typedef struct CBMFileResult {
|
||||
* the Rust inner docs (//!). NULL for other languages and undocumented
|
||||
* files. */
|
||||
const char *module_doc;
|
||||
/* A consumed configured definition could not be mapped safely. Only an
|
||||
* allocation failure (OOM) still aborts publication: any other status is
|
||||
* degraded per file by cbm_test_declarations_degrade. error_msg is
|
||||
* arena-owned as usual. */
|
||||
CBMTestExtractStatus test_declarations_status;
|
||||
int test_declaration_index; /* meaningful on failure; -1 = preset */
|
||||
uint32_t test_declaration_line; /* 0 = snapshot preflight */
|
||||
bool has_test_definition_owners;
|
||||
/* has_error was raised by a configured-definition issue alone. */
|
||||
bool test_error_only;
|
||||
/* The file's configured test forms could not be mapped: it carries no
|
||||
* configured test roles; the status names why (no pointer: compaction
|
||||
* and spill relocate only the strings they know). */
|
||||
bool test_declarations_degraded;
|
||||
CBMTestExtractStatus test_declarations_degraded_status;
|
||||
int test_owner_source_len;
|
||||
CBMLanguage test_owner_language;
|
||||
char test_owner_source_sha256[65]; /* exact raw source; identity, not authentication */
|
||||
} CBMFileResult;
|
||||
|
||||
// --- Enclosing function cache ---
|
||||
@@ -742,8 +790,35 @@ typedef struct {
|
||||
* POD section index. */
|
||||
void *doc_memo;
|
||||
void *doc_pod_index;
|
||||
const cbm_test_declarations_t *test_declarations; /* borrowed for this call only */
|
||||
bool test_declarations_raw_source;
|
||||
struct CBMTestDefinitionMatch *test_definition_matches; /* traversal scratch */
|
||||
int test_definition_match_count;
|
||||
int test_definition_match_cap;
|
||||
} CBMExtractCtx;
|
||||
|
||||
/* Internal configured-definition seams. No declarations pointer enters a result. */
|
||||
/* A configured test form the extractor cannot map degrades the FILE, not
|
||||
* the index (user decision 2026-10-04): its configured test roles, owner
|
||||
* spans and owner flag are dropped, the status reads OK again, an error the
|
||||
* issue alone raised is cleared, and test_declarations_degraded keeps the
|
||||
* reason for a per-file diagnostic. OK and OOM results are left unchanged:
|
||||
* an allocation failure still fails. Idempotent. */
|
||||
void cbm_test_declarations_degrade(CBMFileResult *result);
|
||||
/* The fixed description of a configured-definition status (static). */
|
||||
const char *cbm_test_extract_status_message(CBMTestExtractStatus status);
|
||||
bool cbm_test_declarations_validate(CBMFileResult *result,
|
||||
const cbm_test_declarations_t *declarations);
|
||||
void cbm_test_declarations_finish(CBMExtractCtx *ctx);
|
||||
/* Raw AST candidate only: no declaration/configuration decisions. A C split
|
||||
* invocation uses its adjacent compound body as scope; ordinary definitions
|
||||
* use their function node. Callers still validate raw bytes before ownership. */
|
||||
bool cbm_test_definition_candidate(TSNode scope, CBMLanguage language, TSNode *name, TSNode *body);
|
||||
const char *cbm_test_definition_qn(CBMExtractCtx *ctx, TSNode function);
|
||||
bool cbm_test_definition_owners_match(const CBMFileResult *owners, const char *source,
|
||||
int source_len, bool cpp_mode, const char *module_qn);
|
||||
const char *cbm_test_definition_owner_qn(const CBMFileResult *owners, TSNode function);
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
// Bind third-party allocators (tree-sitter, sqlite3) to mimalloc as
|
||||
@@ -846,6 +921,16 @@ CBMFileResult *cbm_extract_file_ex(
|
||||
const CBMReturnTypeTable *return_type_table // OS return types, or NULL
|
||||
);
|
||||
|
||||
/* Additive immutable-snapshot entry point. NULL preserves legacy behavior.
|
||||
* Snapshot must outlive this call; returned data is independently result-owned.
|
||||
* A non-OK result status is an explicit configured-mapping failure, not a
|
||||
* successful empty inventory. error_msg may be NULL on diagnostic OOM. */
|
||||
CBMFileResult *cbm_extract_file_ex_with_tests(
|
||||
const char *source, int source_len, CBMLanguage language, const char *project,
|
||||
const char *rel_path, int64_t timeout_micros, const char **extra_defines,
|
||||
const char **include_paths, const CBMMacroTable *macro_table,
|
||||
const CBMReturnTypeTable *return_type_table, const cbm_test_declarations_t *test_declarations);
|
||||
|
||||
// Free all memory associated with a result.
|
||||
void cbm_free_result(CBMFileResult *result);
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* cpp_branch_views.c — one parse view per #if/#else branch (see the header).
|
||||
*/
|
||||
#include "cpp_branch_views.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
enum { CPP_LINE_CODE, CPP_LINE_START, CPP_LINE_ELSE, CPP_LINE_END };
|
||||
|
||||
enum { CPP_NEST_MAX = 64 };
|
||||
|
||||
bool cbm_lang_needs_cpp_branch_views(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_HASKELL;
|
||||
}
|
||||
|
||||
static bool cpp_word_char(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
}
|
||||
|
||||
// Classify the line at src[pos..end): a conditional directive is a '#' in
|
||||
// column 0, optional blanks, then the directive word — the same lines the
|
||||
// Haskell scanner treats as conditionals.
|
||||
static int cpp_line_kind(const char *src, int pos, int end) {
|
||||
if (pos >= end || src[pos] != '#') {
|
||||
return CPP_LINE_CODE;
|
||||
}
|
||||
int p = pos + 1;
|
||||
while (p < end && (src[p] == ' ' || src[p] == '\t')) {
|
||||
p++;
|
||||
}
|
||||
int w = p;
|
||||
while (w < end && cpp_word_char(src[w])) {
|
||||
w++;
|
||||
}
|
||||
static const struct {
|
||||
const char *word;
|
||||
int kind;
|
||||
} words[] = {
|
||||
{"if", CPP_LINE_START}, {"ifdef", CPP_LINE_START}, {"ifndef", CPP_LINE_START},
|
||||
{"else", CPP_LINE_ELSE}, {"elif", CPP_LINE_ELSE}, {"elifdef", CPP_LINE_ELSE},
|
||||
{"elifndef", CPP_LINE_ELSE}, {"endif", CPP_LINE_END},
|
||||
};
|
||||
size_t n = (size_t)(w - p);
|
||||
for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) {
|
||||
if (strlen(words[i].word) == n && memcmp(src + p, words[i].word, n) == 0) {
|
||||
return words[i].kind;
|
||||
}
|
||||
}
|
||||
return CPP_LINE_CODE;
|
||||
}
|
||||
|
||||
static int cpp_line_end(const char *src, int pos, int len) {
|
||||
const char *nl = memchr(src + pos, '\n', (size_t)(len - pos));
|
||||
return nl ? (int)(nl - src) : len;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int group; // groups are numbered in order of their #if line
|
||||
int branch; // branch the current line is in
|
||||
bool outer; // the enclosing context is kept
|
||||
} cpp_frame_t;
|
||||
|
||||
typedef struct {
|
||||
cpp_frame_t frames[CPP_NEST_MAX];
|
||||
int depth;
|
||||
int overflow; // groups nested past CPP_NEST_MAX are not tracked
|
||||
int next;
|
||||
} cpp_nest_t;
|
||||
|
||||
// Track one line. Returns the innermost tracked group when the line is one of
|
||||
// its #else/#elif lines, else NULL.
|
||||
static cpp_frame_t *cpp_nest_step(cpp_nest_t *n, int kind, bool active) {
|
||||
switch (kind) {
|
||||
case CPP_LINE_START:
|
||||
if (n->depth < CPP_NEST_MAX && n->overflow == 0) {
|
||||
n->frames[n->depth++] = (cpp_frame_t){n->next, 0, active};
|
||||
} else {
|
||||
n->overflow++;
|
||||
}
|
||||
n->next++;
|
||||
return NULL;
|
||||
case CPP_LINE_ELSE:
|
||||
if (n->overflow > 0 || n->depth == 0) {
|
||||
return NULL;
|
||||
}
|
||||
n->frames[n->depth - 1].branch++;
|
||||
return &n->frames[n->depth - 1];
|
||||
case CPP_LINE_END:
|
||||
if (n->overflow > 0) {
|
||||
n->overflow--;
|
||||
} else if (n->depth > 0) {
|
||||
n->depth--;
|
||||
}
|
||||
return NULL;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Branch count per group. Returns the group count, or -1 on allocation failure.
|
||||
static int cpp_group_branches(CBMArena *a, const char *src, int len, int **out) {
|
||||
int groups = 0;
|
||||
for (int pos = 0; pos < len;) {
|
||||
int end = cpp_line_end(src, pos, len);
|
||||
groups += cpp_line_kind(src, pos, end) == CPP_LINE_START;
|
||||
pos = end + 1;
|
||||
}
|
||||
*out = NULL;
|
||||
if (groups == 0) {
|
||||
return 0;
|
||||
}
|
||||
int *branches = cbm_arena_alloc(a, sizeof(int) * (size_t)groups);
|
||||
if (!branches) {
|
||||
return -1;
|
||||
}
|
||||
for (int g = 0; g < groups; g++) {
|
||||
branches[g] = 1;
|
||||
}
|
||||
cpp_nest_t nest = {.depth = 0};
|
||||
for (int pos = 0; pos < len;) {
|
||||
int end = cpp_line_end(src, pos, len);
|
||||
cpp_frame_t *frame = cpp_nest_step(&nest, cpp_line_kind(src, pos, end), true);
|
||||
if (frame) {
|
||||
branches[frame->group] = frame->branch + 1;
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
*out = branches;
|
||||
return groups;
|
||||
}
|
||||
|
||||
int cbm_cpp_branch_view_count(CBMArena *a, const char *src, int len) {
|
||||
int *branches = NULL;
|
||||
int groups = cpp_group_branches(a, src, len, &branches);
|
||||
int most = 1;
|
||||
for (int g = 0; g < groups; g++) {
|
||||
if (branches[g] > most) {
|
||||
most = branches[g];
|
||||
}
|
||||
}
|
||||
int views = most - 1;
|
||||
return views > CBM_CPP_BRANCH_VIEWS_MAX ? CBM_CPP_BRANCH_VIEWS_MAX : views;
|
||||
}
|
||||
|
||||
// The current line belongs to the view: every enclosing group sits in the
|
||||
// branch the view keeps.
|
||||
static bool cpp_view_keeps(const cpp_nest_t *n, const int *branches, int view) {
|
||||
if (n->depth == 0) {
|
||||
return true;
|
||||
}
|
||||
const cpp_frame_t *top = &n->frames[n->depth - 1];
|
||||
int nb = branches[top->group];
|
||||
int kept = 0;
|
||||
if (nb >= 2) {
|
||||
kept = view < nb - 1 ? view : nb - 1;
|
||||
}
|
||||
return top->outer && top->branch == kept;
|
||||
}
|
||||
|
||||
char *cbm_cpp_branch_view(CBMArena *a, const char *src, int len, int view) {
|
||||
int *branches = NULL;
|
||||
int groups = cpp_group_branches(a, src, len, &branches);
|
||||
char *buf = groups >= 0 ? cbm_arena_alloc(a, (size_t)len + 1) : NULL;
|
||||
if (!buf) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(buf, src, (size_t)len);
|
||||
buf[len] = '\0';
|
||||
cpp_nest_t nest = {.depth = 0};
|
||||
for (int pos = 0; pos < len;) {
|
||||
int end = cpp_line_end(src, pos, len);
|
||||
bool kept = cpp_view_keeps(&nest, branches, view);
|
||||
int kind = cpp_line_kind(src, pos, end);
|
||||
cpp_nest_step(&nest, kind, kept);
|
||||
if (kind != CPP_LINE_CODE || !kept) {
|
||||
for (int i = pos; i < end; i++) {
|
||||
buf[i] = buf[i] == '\r' ? '\r' : ' ';
|
||||
}
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef CBM_CPP_BRANCH_VIEWS_H
|
||||
#define CBM_CPP_BRANCH_VIEWS_H
|
||||
|
||||
#include "cbm.h"
|
||||
|
||||
/*
|
||||
* Branch views of a source whose grammar parses only the first branch of an
|
||||
* #if/#else group (the Haskell scanner swallows everything from #else to
|
||||
* #endif as one token, so a definition written once per configuration is seen
|
||||
* in its first variant only).
|
||||
*
|
||||
* A view keeps the source byte for byte and line for line: the branch it
|
||||
* selects in every group stays, the other branches and all conditional lines
|
||||
* (#if/#ifdef/#ifndef/#else/#elif.../#endif) become spaces, newlines stay. So
|
||||
* every span extracted from a view is a raw-source span.
|
||||
*
|
||||
* View v (1-based) selects branch min(v, branches - 1) of a group with at
|
||||
* least two branches, and the only branch of a group without one.
|
||||
*/
|
||||
|
||||
#define CBM_CPP_BRANCH_VIEWS_MAX 3
|
||||
|
||||
// True when `lang`'s grammar parses only the first branch of a conditional.
|
||||
bool cbm_lang_needs_cpp_branch_views(CBMLanguage lang);
|
||||
|
||||
// Number of views the source needs (0 when no group has a second branch),
|
||||
// at most CBM_CPP_BRANCH_VIEWS_MAX.
|
||||
int cbm_cpp_branch_view_count(CBMArena *a, const char *src, int len);
|
||||
|
||||
// View `view` (1..count) as an arena buffer of exactly `len` bytes plus a NUL,
|
||||
// or NULL on allocation failure.
|
||||
char *cbm_cpp_branch_view(CBMArena *a, const char *src, int len, int view);
|
||||
|
||||
#endif
|
||||
+1295
-69
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,8 @@
|
||||
enum {
|
||||
USE_PREFIX_LEN = 4, /* strlen("use ") */
|
||||
MIN_WOLFRAM_CHILDREN = 2,
|
||||
/* Nested `#if` branches followed while collecting C includes. */
|
||||
C_INCLUDE_NEST_LIMIT = 64,
|
||||
SECOND_IDX = 1,
|
||||
};
|
||||
|
||||
@@ -45,7 +47,7 @@ static void parse_lisp_imports(CBMExtractCtx *ctx);
|
||||
static void parse_starlark_imports(CBMExtractCtx *ctx);
|
||||
static void parse_tcl_imports(CBMExtractCtx *ctx);
|
||||
static void parse_teal_imports(CBMExtractCtx *ctx);
|
||||
static void parse_zsh_imports(CBMExtractCtx *ctx);
|
||||
static void parse_shell_source_imports(CBMExtractCtx *ctx);
|
||||
static void parse_css_imports(CBMExtractCtx *ctx);
|
||||
static void parse_html_imports(CBMExtractCtx *ctx);
|
||||
static void parse_cmake_imports(CBMExtractCtx *ctx);
|
||||
@@ -663,10 +665,22 @@ static char *strip_angle_brackets(CBMArena *a, char *path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
static void parse_c_imports(CBMExtractCtx *ctx) {
|
||||
/* Node kinds that only group file-level items: the branches of `#if` /
|
||||
* `#ifdef` (an include guard is one) and the body of `extern "C" { ... }`. */
|
||||
static bool is_c_include_container(const char *kind) {
|
||||
return strcmp(kind, "preproc_if") == 0 || strcmp(kind, "preproc_ifdef") == 0 ||
|
||||
strcmp(kind, "preproc_else") == 0 || strcmp(kind, "preproc_elif") == 0 ||
|
||||
strcmp(kind, "preproc_elifdef") == 0 || strcmp(kind, "linkage_specification") == 0 ||
|
||||
strcmp(kind, "declaration_list") == 0;
|
||||
}
|
||||
|
||||
/* Collect the includes among `parent`'s children, descending into
|
||||
* preprocessor branches: every header keeps its includes inside its include
|
||||
* guard, so the top level alone gave a guarded header no imports at all. */
|
||||
static void parse_c_imports_in(CBMExtractCtx *ctx, TSNode parent, int depth) {
|
||||
CBMArena *a = ctx->arena;
|
||||
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(ctx->root);
|
||||
TSTreeCursor cursor = ts_tree_cursor_new(parent);
|
||||
if (!ts_tree_cursor_goto_first_child(&cursor)) {
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
return;
|
||||
@@ -675,6 +689,9 @@ static void parse_c_imports(CBMExtractCtx *ctx) {
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
const char *kind = ts_node_type(node);
|
||||
if (strcmp(kind, "preproc_include") != 0 && strcmp(kind, "preproc_import") != 0) {
|
||||
if (depth < C_INCLUDE_NEST_LIMIT && is_c_include_container(kind)) {
|
||||
parse_c_imports_in(ctx, node, depth + SKIP_ONE);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -695,6 +712,10 @@ static void parse_c_imports(CBMExtractCtx *ctx) {
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
}
|
||||
|
||||
static void parse_c_imports(CBMExtractCtx *ctx) {
|
||||
parse_c_imports_in(ctx, ctx->root, 0);
|
||||
}
|
||||
|
||||
// --- Ruby imports ---
|
||||
// call nodes: require("X"), require_relative("X")
|
||||
|
||||
@@ -2166,10 +2187,13 @@ static void parse_teal_imports(CBMExtractCtx *ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Zsh imports ---
|
||||
// --- Bash / Zsh imports ---
|
||||
// source file / . file — `command` nodes whose command_name is "source" or ".".
|
||||
// The argument field carries the sourced path.
|
||||
static void parse_zsh_imports(CBMExtractCtx *ctx) {
|
||||
// The argument field carries the sourced path. Every other command is a call,
|
||||
// not an import: read as an import, `set -e` imported "set", which the import
|
||||
// resolver then bound to whatever project symbol is named set (a TSX method),
|
||||
// and every `set` call in the script followed it.
|
||||
static void parse_shell_source_imports(CBMExtractCtx *ctx) {
|
||||
CBMArena *a = ctx->arena;
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx, CBM_SZ_512);
|
||||
@@ -3028,8 +3052,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
|
||||
parse_generic_imports(ctx, "call");
|
||||
break;
|
||||
case CBM_LANG_BASH:
|
||||
// source/. commands
|
||||
parse_generic_imports(ctx, "command");
|
||||
parse_shell_source_imports(ctx);
|
||||
break;
|
||||
case CBM_LANG_ZIG:
|
||||
parse_zig_imports(ctx);
|
||||
@@ -3099,7 +3122,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
|
||||
parse_teal_imports(ctx);
|
||||
break;
|
||||
case CBM_LANG_ZSH:
|
||||
parse_zsh_imports(ctx);
|
||||
parse_shell_source_imports(ctx);
|
||||
break;
|
||||
case CBM_LANG_CMAKE:
|
||||
parse_cmake_imports(ctx);
|
||||
|
||||
@@ -918,6 +918,13 @@ static const char *objectscript_get_method_qn(CBMExtractCtx *ctx, TSNode node,
|
||||
// Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic).
|
||||
static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state) {
|
||||
/* Raw configured definitions already own the canonical QN, including
|
||||
* namespace scope. Never rederive it from the bare macro identifier. */
|
||||
const char *configured_qn = cbm_test_definition_qn(ctx, node);
|
||||
if (configured_qn) {
|
||||
return configured_qn;
|
||||
}
|
||||
|
||||
(void)spec;
|
||||
if (ctx->language == CBM_LANG_WOLFRAM) {
|
||||
return compute_wolfram_func_qn(ctx, node);
|
||||
@@ -1103,9 +1110,13 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
|
||||
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, qn_name);
|
||||
}
|
||||
/* Java/Go: directory-based module so this enclosing-func QN matches the def
|
||||
* QN and the LSP caller_qn (the lsp_resolve join keys on exact equality). */
|
||||
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, qn_name,
|
||||
ctx->language);
|
||||
* QN and the LSP caller_qn (the lsp_resolve join keys on exact equality).
|
||||
* C-family platform variants share their platform-neutral QN — the same
|
||||
* rule extract_func_def applies, so in-body calls source to the def. */
|
||||
const char *qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, qn_name,
|
||||
ctx->language);
|
||||
return cbm_platform_variant_qn(ctx->arena, ctx->language, ctx->project, ctx->rel_path, qn_name,
|
||||
qn, node);
|
||||
}
|
||||
|
||||
// Compute class QN for scope tracking.
|
||||
@@ -2370,7 +2381,12 @@ static void push_lexical_boundary(TSNode node, WalkState *state, uint32_t depth)
|
||||
static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
||||
WalkState *state, uint32_t depth,
|
||||
const CBMInvocationDescriptor *invocation) {
|
||||
if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) {
|
||||
const char *configured_body_qn = NULL;
|
||||
if (ctx->language == CBM_LANG_C && strcmp(ts_node_type(node), "compound_statement") == 0)
|
||||
configured_body_qn = cbm_test_definition_qn(ctx, node);
|
||||
if (configured_body_qn) {
|
||||
(void)push_function_scope(state, depth, configured_body_qn, node);
|
||||
} else if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) {
|
||||
/* OCaml: a nested local `let x = e in ...` is itself a value_definition,
|
||||
* but the def walk does not descend into function bodies, so it emits no
|
||||
* node for it. Pushing a func scope here would attribute in-body calls to
|
||||
|
||||
@@ -1300,6 +1300,18 @@ const char *cbm_enclosing_func_qn(CBMArena *a, TSNode node, CBMLanguage lang, co
|
||||
// --- Cached enclosing function QN ---
|
||||
|
||||
const char *cbm_enclosing_func_qn_cached(CBMExtractCtx *ctx, TSNode node) {
|
||||
if (ctx->test_definition_match_count > 0) {
|
||||
const char **function_kinds = func_kinds_for_lang(ctx->language);
|
||||
for (TSNode current = node; !ts_node_is_null(current); current = ts_node_parent(current)) {
|
||||
const char *configured_qn = cbm_test_definition_qn(ctx, current);
|
||||
if (configured_qn)
|
||||
return configured_qn;
|
||||
/* A nested native function owns its own body; do not inherit a
|
||||
* surrounding configured macro's QN through that boundary. */
|
||||
if (cbm_kind_in_set(current, function_kinds))
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint32_t pos = ts_node_start_byte(node);
|
||||
|
||||
// Check cache: find a function range that contains this position.
|
||||
@@ -1666,6 +1678,146 @@ char *cbm_fqn_compute_source_lang(CBMArena *a, const char *project, const char *
|
||||
return cbm_arena_sprintf(a, "%s.%s", module, name);
|
||||
}
|
||||
|
||||
/* Operating systems and CPU architectures only: a token names a platform, so a
|
||||
* path segment or stem suffix made of one marks a platform alternative. Vague
|
||||
* words (generic, default, common, stub) are deliberately absent. */
|
||||
static const char *const platform_tokens[] = {
|
||||
"win", "win32", "win64", "windows", "wince", "nt", "posix", "unix",
|
||||
"linux", "darwin", "mac", "macos", "macosx", "osx", "apple", "ios",
|
||||
"android", "bsd", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "sunos",
|
||||
"aix", "hpux", "haiku", "fuchsia", "cygwin", "mingw", "msvc", "wasm",
|
||||
"wasi", "emscripten", "x86", "x86_64", "x64", "amd64", "i386", "i686",
|
||||
"ia32", "arm", "arm64", "aarch64", "armv7", "riscv", "riscv64", "mips",
|
||||
"mips64", "ppc", "ppc64", "powerpc", "s390", "s390x", "sparc", "sparc64",
|
||||
"loongarch", "loongarch64", NULL};
|
||||
|
||||
static bool platform_token_eq(const char *s, size_t len) {
|
||||
for (int i = 0; platform_tokens[i]; i++) {
|
||||
const char *t = platform_tokens[i];
|
||||
size_t tl = strlen(t);
|
||||
if (tl != len) {
|
||||
continue;
|
||||
}
|
||||
size_t k = 0;
|
||||
while (k < len && tolower((unsigned char)s[k]) == t[k]) {
|
||||
k++;
|
||||
}
|
||||
if (k == len) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Length of `stem` without a trailing `_<token>` / `-<token>` / `.<token>`
|
||||
* platform suffix (the longest one), or `len` when it has none. */
|
||||
static size_t platform_stem_len(const char *stem, size_t len) {
|
||||
size_t best = len;
|
||||
for (size_t sep = 1; sep < len; sep++) {
|
||||
char c = stem[sep - 1];
|
||||
if ((c == '_' || c == '-' || c == '.') && platform_token_eq(stem + sep, len - sep)) {
|
||||
if (sep - 1 < best) {
|
||||
best = sep - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static bool c_family_variant_lang(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
|
||||
lang == CBM_LANG_OBJC;
|
||||
}
|
||||
|
||||
static bool func_is_static(TSNode func_node) {
|
||||
uint32_t n = ts_node_child_count(func_node);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
TSNode child = ts_node_child(func_node, i);
|
||||
if (strcmp(ts_node_type(child), "storage_class_specifier") == 0 &&
|
||||
ts_node_child_count(child) > 0 &&
|
||||
strcmp(ts_node_type(ts_node_child(child, 0)), "static") == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *cbm_platform_variant_qn(CBMArena *a, CBMLanguage lang, const char *project,
|
||||
const char *rel_path, const char *name, const char *qn,
|
||||
TSNode func_node) {
|
||||
if (!c_family_variant_lang(lang) || !rel_path || !name || !name[0] || !qn ||
|
||||
ts_node_is_null(func_node) || func_is_static(func_node)) {
|
||||
return qn;
|
||||
}
|
||||
char *plain = cbm_fqn_compute(a, project, rel_path, name);
|
||||
if (!plain || strcmp(plain, qn) != 0) {
|
||||
return qn; /* namespaced or otherwise scoped: not a file-scope function */
|
||||
}
|
||||
/* Rebuild the path without platform tokens: a directory segment that is
|
||||
* one is dropped; a filename stem loses its platform suffix, and a stem
|
||||
* that IS a token leaves the function at its directory. */
|
||||
size_t len = strlen(rel_path);
|
||||
char *neutral = (char *)cbm_arena_alloc(a, len + SKIP_ONE);
|
||||
if (!neutral) {
|
||||
return qn;
|
||||
}
|
||||
size_t out = 0;
|
||||
bool changed = false;
|
||||
bool dir_level = false;
|
||||
const char *seg = rel_path;
|
||||
while (*seg) {
|
||||
const char *slash = strchr(seg, '/');
|
||||
size_t seg_len = slash ? (size_t)(slash - seg) : strlen(seg);
|
||||
if (slash) {
|
||||
if (platform_token_eq(seg, seg_len)) {
|
||||
changed = true;
|
||||
} else {
|
||||
memcpy(neutral + out, seg, seg_len);
|
||||
out += seg_len;
|
||||
neutral[out++] = '/';
|
||||
}
|
||||
seg = slash + SKIP_ONE;
|
||||
continue;
|
||||
}
|
||||
const char *dot = NULL;
|
||||
for (const char *p = seg + seg_len; p > seg; p--) {
|
||||
if (p[-1] == '.') {
|
||||
dot = p - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t stem_len = dot && dot > seg ? (size_t)(dot - seg) : seg_len;
|
||||
size_t keep = platform_token_eq(seg, stem_len) ? 0 : platform_stem_len(seg, stem_len);
|
||||
if (keep != stem_len) {
|
||||
changed = true;
|
||||
}
|
||||
if (keep == 0) {
|
||||
dir_level = true; /* the function lives at the directory */
|
||||
if (out > 0 && neutral[out - SKIP_ONE] == '/') {
|
||||
out--;
|
||||
}
|
||||
} else {
|
||||
memcpy(neutral + out, seg, keep);
|
||||
out += keep;
|
||||
memcpy(neutral + out, seg + stem_len, seg_len - stem_len);
|
||||
out += seg_len - stem_len;
|
||||
}
|
||||
break;
|
||||
}
|
||||
neutral[out] = '\0';
|
||||
if (!changed) {
|
||||
return qn;
|
||||
}
|
||||
if (dir_level) {
|
||||
/* No file stem left: the folder form keeps every directory segment
|
||||
* (cbm_fqn_compute would strip a dotted segment as an extension). */
|
||||
char *folder = cbm_fqn_folder(a, project ? project : "", neutral);
|
||||
return folder ? cbm_arena_sprintf(a, "%s.%s", folder, name) : qn;
|
||||
}
|
||||
char *variant = cbm_fqn_compute(a, project, neutral, name);
|
||||
return variant ? variant : qn;
|
||||
}
|
||||
|
||||
char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir) {
|
||||
// project.dir1.dir2
|
||||
size_t proj_len = strlen(project);
|
||||
|
||||
@@ -250,6 +250,17 @@ char *cbm_fqn_module_source_lang(CBMArena *a, const char *project, const char *r
|
||||
char *cbm_fqn_compute_source_lang(CBMArena *a, const char *project, const char *rel_path,
|
||||
const char *name, CBMLanguage lang);
|
||||
|
||||
// C-family platform variants: the same external-linkage function defined in
|
||||
// files that differ only by a platform token (foo_win.c / foo_posix.c,
|
||||
// src/unix/fs.c / src/win/fs.c) is ONE function with platform variants — two
|
||||
// external definitions of one name cannot link into one program, so they are
|
||||
// alternatives by construction. Returns the platform-neutral QN (the path with
|
||||
// its platform tokens removed) for a non-static file-scope function of
|
||||
// C/C++/CUDA/ObjC whose `qn` is the plain file-stem form; otherwise `qn`.
|
||||
const char *cbm_platform_variant_qn(CBMArena *a, CBMLanguage lang, const char *project,
|
||||
const char *rel_path, const char *name, const char *qn,
|
||||
TSNode func_node);
|
||||
|
||||
// Folder QN: project.dir_parts
|
||||
char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir);
|
||||
|
||||
|
||||
@@ -2737,6 +2737,28 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
|
||||
_Static_assert(sizeof(lang_specs) / sizeof(lang_specs[0]) == CBM_LANG_COUNT,
|
||||
"lang_specs array size must match CBM_LANG_COUNT");
|
||||
|
||||
/* Conditional-compilation blocks per grammar (the `*_in_field_declaration_list`
|
||||
* / `*_in_top_level` forms are aliased to these names by the grammars). */
|
||||
static const char *c_family_conditional_types[] = {
|
||||
"preproc_if", "preproc_ifdef", "preproc_else", "preproc_elif", "preproc_elifdef", NULL};
|
||||
|
||||
const char **cbm_conditional_block_types(CBMLanguage lang) {
|
||||
switch (lang) {
|
||||
case CBM_LANG_C:
|
||||
case CBM_LANG_CPP:
|
||||
case CBM_LANG_CUDA:
|
||||
case CBM_LANG_OBJC:
|
||||
case CBM_LANG_CSHARP:
|
||||
case CBM_LANG_GLSL:
|
||||
case CBM_LANG_HLSL:
|
||||
case CBM_LANG_ISPC:
|
||||
case CBM_LANG_SLANG:
|
||||
return c_family_conditional_types;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const CBMLangSpec *cbm_lang_spec(CBMLanguage lang) {
|
||||
if (lang < 0 || lang >= CBM_LANG_COUNT) {
|
||||
return NULL;
|
||||
|
||||
@@ -48,6 +48,13 @@ typedef struct {
|
||||
// language rows; the table lives in extract_calls.c next to the dispatch code.
|
||||
const char **cbm_string_dispatch_suffixes(CBMLanguage lang);
|
||||
|
||||
// Returns a NULL-terminated list of node types that wrap conditionally
|
||||
// compiled alternatives (#if/#ifdef/#else/#elif blocks) for a language, or
|
||||
// NULL when the grammar has none. A definition inside one is still a member
|
||||
// of its enclosing scope; its #else twin is a variant of the same definition.
|
||||
// Side table for the same -Wmissing-field-initializers reason as above.
|
||||
const char **cbm_conditional_block_types(CBMLanguage lang);
|
||||
|
||||
// Get the language spec for a given language. Returns NULL for unsupported.
|
||||
const CBMLangSpec *cbm_lang_spec(CBMLanguage lang);
|
||||
|
||||
|
||||
+800
-125
File diff suppressed because it is too large
Load Diff
@@ -63,9 +63,28 @@ typedef struct {
|
||||
int neg_memo_cap; // power-of-two; 0 until first insert
|
||||
int neg_memo_count; // live entries (grow by rehash at 70% load)
|
||||
|
||||
// Field references already emitted for this file, keyed by (enclosing
|
||||
// function, owner type, field). The function -> Field edge needs one row
|
||||
// per pair; a row per occurrence would cost a record for every member
|
||||
// access in the corpus. Same uint64 hash-set shape and ownership as
|
||||
// neg_memo; freed at end of c_lsp_process_file.
|
||||
uint64_t *field_ref_seen;
|
||||
int field_ref_seen_cap;
|
||||
int field_ref_seen_count;
|
||||
|
||||
// The type an initializer_list is about to initialize, handed from the
|
||||
// declarator / compound literal / enclosing list that knows it to the walk
|
||||
// of that list. Bound to the list's node id: a hint nothing consumed must
|
||||
// never type a later, unrelated list.
|
||||
const CBMType *init_list_type;
|
||||
const void *init_list_id;
|
||||
|
||||
// Output
|
||||
CBMResolvedCallArray *resolved_calls;
|
||||
CBMSourceOrigin source_origin; // source buffer represented by emitted occurrence spans
|
||||
const CBMFileResult *test_definition_owners; /* borrowed, prevalidated raw source identity */
|
||||
int test_owner_seen_count;
|
||||
bool test_owner_error;
|
||||
|
||||
// Function pointer targets: lexical binding -> exact target function QN.
|
||||
// A NULL target is an explicitly unknown/ambiguous binding and must shadow
|
||||
@@ -154,6 +173,31 @@ void cbm_run_c_lsp_cross_with_registry(CBMArena *arena, const char *source, int
|
||||
TSTree *cached_tree, // NULL = parse internally
|
||||
CBMResolvedCallArray *out);
|
||||
|
||||
/* Additive configured-owner entrypoints. Owners must outlive the call. With
|
||||
* owners, RAW origin, exact source identity and module identity are required.
|
||||
* Configured owners fresh-parse verified source: supplied roots/cached trees
|
||||
* have no source provenance and are not trusted. NULL owners retain caching.
|
||||
* False leaves output untouched when identity validation fails. A later
|
||||
* mapping/OOM failure may leave partial output that callers must discard. QNs
|
||||
* in successful output belong to the supplied output arena. Callers must
|
||||
* abort publication on false. Legacy wrappers pass NULL and retain previous behavior. */
|
||||
bool cbm_run_c_lsp_with_test_owners(
|
||||
CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
|
||||
TSNode root, bool cpp_mode, CBMSourceOrigin source_origin,
|
||||
const CBMFileResult *test_owners);
|
||||
bool cbm_run_c_lsp_cross_with_test_owners(
|
||||
CBMArena *arena, const char *source, int source_len, const char *module_qn,
|
||||
bool cpp_mode, CBMLSPDef *defs, int def_count, const char **include_paths,
|
||||
const char **include_ns_qns, int include_count, TSTree *cached_tree,
|
||||
CBMResolvedCallArray *out, CBMSourceOrigin source_origin,
|
||||
const CBMFileResult *test_owners);
|
||||
bool cbm_run_c_lsp_cross_with_registry_with_test_owners(
|
||||
CBMArena *arena, const char *source, int source_len, const char *module_qn,
|
||||
bool cpp_mode, CBMTypeRegistry *reg, const char **include_paths,
|
||||
const char **include_ns_qns, int include_count, TSTree *cached_tree,
|
||||
CBMResolvedCallArray *out, CBMSourceOrigin source_origin,
|
||||
const CBMFileResult *test_owners);
|
||||
|
||||
// Register C stdlib types and functions into a registry.
|
||||
void cbm_c_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
|
||||
|
||||
|
||||
@@ -757,9 +757,9 @@ static bool has_filesystem_extension(const char *path) {
|
||||
ext[ext_len] = '\0';
|
||||
|
||||
static const char *const hard_file_exts[] = {
|
||||
".cfg", ".conf", ".credentials", ".crt", ".db", ".env",
|
||||
".ini", ".key", ".pem", ".pid", ".properties", ".service",
|
||||
".sock", ".socket", ".sqlite", ".toml", NULL};
|
||||
".cfg", ".conf", ".credentials", ".crt", ".db", ".env", ".ini", ".key",
|
||||
".log", ".md", ".pdf", ".pem", ".pid", ".properties", ".rst", ".service",
|
||||
".sock", ".socket", ".sqlite", ".toml", ".txt", NULL};
|
||||
for (int i = 0; hard_file_exts[i]; i++) {
|
||||
if (path_ext_matches(ext, hard_file_exts[i])) {
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
* spawn_patterns.c — process-spawning call sites, per language (see the header).
|
||||
*
|
||||
* Spellings come from what the extractor records as the callee (checked per
|
||||
* language against real calls): Rust `Command::new`, Go `exec.Command`, Java
|
||||
* `Runtime.getRuntime().exec`, a constructor as its class name.
|
||||
*/
|
||||
#include "spawn_patterns.h"
|
||||
#include "foundation/constants.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define Q CBM_SPAWN_QUALIFIED
|
||||
#define U CBM_SPAWN_BARE
|
||||
#define I CBM_SPAWN_BARE_IMPORTED
|
||||
#define S CBM_SPAWN_STRING_RECEIVER
|
||||
|
||||
/* libc / POSIX / Win32. posix_spawn and _spawn* take the path second;
|
||||
* CreateProcess names the program first or only in its command line. */
|
||||
static const cbm_spawn_api_t c_family[] = {
|
||||
{"system", NULL, U, 0, -1}, {"std::system", NULL, U, 0, -1},
|
||||
{"_wsystem", NULL, U, 0, -1}, {"popen", NULL, U, 0, -1},
|
||||
{"_popen", NULL, U, 0, -1}, {"_wpopen", NULL, U, 0, -1},
|
||||
{"execl", NULL, U, 0, -1}, {"execle", NULL, U, 0, -1},
|
||||
{"execlp", NULL, U, 0, -1}, {"execv", NULL, U, 0, -1},
|
||||
{"execve", NULL, U, 0, -1}, {"execvp", NULL, U, 0, -1},
|
||||
{"execvpe", NULL, U, 0, -1}, {"fexecve", NULL, U, 0, -1},
|
||||
{"posix_spawn", NULL, U, 1, -1}, {"posix_spawnp", NULL, U, 1, -1},
|
||||
{"_execl", NULL, U, 0, -1}, {"_execv", NULL, U, 0, -1},
|
||||
{"_execlp", NULL, U, 0, -1}, {"_execvp", NULL, U, 0, -1},
|
||||
{"_spawnl", NULL, U, 1, -1}, {"_spawnle", NULL, U, 1, -1},
|
||||
{"_spawnlp", NULL, U, 1, -1}, {"_spawnv", NULL, U, 1, -1},
|
||||
{"_spawnve", NULL, U, 1, -1}, {"_spawnvp", NULL, U, 1, -1},
|
||||
{"_wspawnv", NULL, U, 1, -1}, {"_wspawnvp", NULL, U, 1, -1},
|
||||
{"CreateProcess", NULL, U, 0, 1}, {"CreateProcessA", NULL, U, 0, 1},
|
||||
{"CreateProcessW", NULL, U, 0, 1}, {"ShellExecute", NULL, U, 2, -1},
|
||||
{"ShellExecuteA", NULL, U, 2, -1}, {"ShellExecuteW", NULL, U, 2, -1},
|
||||
{"ShellExecuteExA", NULL, U, 0, -1}, {"ShellExecuteExW", NULL, U, 0, -1},
|
||||
{"WinExec", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t python[] = {
|
||||
{"subprocess.run", NULL, Q, 0, -1},
|
||||
{"subprocess.call", NULL, Q, 0, -1},
|
||||
{"subprocess.check_call", NULL, Q, 0, -1},
|
||||
{"subprocess.check_output", NULL, Q, 0, -1},
|
||||
{"subprocess.Popen", NULL, Q, 0, -1},
|
||||
{"subprocess.getoutput", NULL, Q, 0, -1},
|
||||
{"subprocess.getstatusoutput", NULL, Q, 0, -1},
|
||||
{"os.system", NULL, Q, 0, -1},
|
||||
{"os.popen", NULL, Q, 0, -1},
|
||||
{"os.startfile", NULL, Q, 0, -1},
|
||||
{"os.execl", NULL, Q, 0, -1},
|
||||
{"os.execle", NULL, Q, 0, -1},
|
||||
{"os.execlp", NULL, Q, 0, -1},
|
||||
{"os.execlpe", NULL, Q, 0, -1},
|
||||
{"os.execv", NULL, Q, 0, -1},
|
||||
{"os.execve", NULL, Q, 0, -1},
|
||||
{"os.execvp", NULL, Q, 0, -1},
|
||||
{"os.execvpe", NULL, Q, 0, -1},
|
||||
{"os.posix_spawn", NULL, Q, 0, -1},
|
||||
{"os.posix_spawnp", NULL, Q, 0, -1},
|
||||
{"os.spawnl", NULL, Q, 1, -1},
|
||||
{"os.spawnle", NULL, Q, 1, -1},
|
||||
{"os.spawnlp", NULL, Q, 1, -1},
|
||||
{"os.spawnlpe", NULL, Q, 1, -1},
|
||||
{"os.spawnv", NULL, Q, 1, -1},
|
||||
{"os.spawnve", NULL, Q, 1, -1},
|
||||
{"os.spawnvp", NULL, Q, 1, -1},
|
||||
{"os.spawnvpe", NULL, Q, 1, -1},
|
||||
{"asyncio.create_subprocess_exec", NULL, Q, 0, -1},
|
||||
{"asyncio.create_subprocess_shell", NULL, Q, 0, -1},
|
||||
{"pexpect.spawn", NULL, Q, 0, -1},
|
||||
{"pexpect.run", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
/* Node's child_process (also behind `node:`), the destructured-require form,
|
||||
* execa / cross-spawn, and the Bun and Deno globals. */
|
||||
static const cbm_spawn_api_t js_family[] = {
|
||||
{"child_process.spawn", NULL, Q, 0, -1},
|
||||
{"child_process.spawnSync", NULL, Q, 0, -1},
|
||||
{"child_process.exec", NULL, Q, 0, -1},
|
||||
{"child_process.execSync", NULL, Q, 0, -1},
|
||||
{"child_process.execFile", NULL, Q, 0, -1},
|
||||
{"child_process.execFileSync", NULL, Q, 0, -1},
|
||||
{"child_process.fork", NULL, Q, 0, -1},
|
||||
{"spawn", "child_process", I, 0, -1},
|
||||
{"spawnSync", "child_process", I, 0, -1},
|
||||
{"exec", "child_process", I, 0, -1},
|
||||
{"execSync", "child_process", I, 0, -1},
|
||||
{"execFile", "child_process", I, 0, -1},
|
||||
{"execFileSync", "child_process", I, 0, -1},
|
||||
{"fork", "child_process", I, 0, -1},
|
||||
{"execa.execa", NULL, Q, 0, -1},
|
||||
{"execa.execaSync", NULL, Q, 0, -1},
|
||||
{"execa.execaCommand", NULL, Q, 0, -1},
|
||||
{"execa.execaCommandSync", NULL, Q, 0, -1},
|
||||
{"cross-spawn.spawn", NULL, Q, 0, -1},
|
||||
{"cross-spawn.sync", NULL, Q, 0, -1},
|
||||
{"Bun.spawn", NULL, Q, 0, -1},
|
||||
{"Bun.spawnSync", NULL, Q, 0, -1},
|
||||
{"Deno.Command", NULL, Q, 0, -1},
|
||||
{"Deno.run", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t go[] = {
|
||||
{"os/exec.Command", NULL, Q, 0, -1},
|
||||
{"os/exec.CommandContext", NULL, Q, 1, -1},
|
||||
{"golang.org/x/sys/execabs.Command", NULL, Q, 0, -1},
|
||||
{"golang.org/x/sys/execabs.CommandContext", NULL, Q, 1, -1},
|
||||
{"os.StartProcess", NULL, Q, 0, -1},
|
||||
{"syscall.Exec", NULL, Q, 0, -1},
|
||||
{"syscall.ForkExec", NULL, Q, 0, -1},
|
||||
{"syscall.StartProcess", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t rust[] = {
|
||||
{"std::process::Command::new", NULL, Q, 0, -1},
|
||||
{"tokio::process::Command::new", NULL, Q, 0, -1},
|
||||
{"async_process::Command::new", NULL, Q, 0, -1},
|
||||
{"async_std::process::Command::new", NULL, Q, 0, -1},
|
||||
{"smol::process::Command::new", NULL, Q, 0, -1},
|
||||
{"duct::cmd", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
/* java.lang is implicit, so the class name alone is the spelling. */
|
||||
static const cbm_spawn_api_t jvm[] = {
|
||||
{"ProcessBuilder", NULL, U, 0, -1},
|
||||
{"java.lang.ProcessBuilder", NULL, Q, 0, -1},
|
||||
{"Runtime.getRuntime().exec", NULL, U, 0, -1},
|
||||
{".execute", NULL, S, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t dotnet[] = {
|
||||
{"Process.Start", NULL, U, 0, -1}, {"System.Diagnostics.Process.Start", NULL, Q, 0, -1},
|
||||
{"ProcessStartInfo", NULL, U, 0, -1}, {"System.Diagnostics.ProcessStartInfo", NULL, Q, 0, -1},
|
||||
{"Cli.Wrap", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t ruby[] = {
|
||||
{"system", NULL, U, 0, -1}, {"exec", NULL, U, 0, -1},
|
||||
{"spawn", NULL, U, 0, -1}, {"Kernel.system", NULL, U, 0, -1},
|
||||
{"Kernel.exec", NULL, U, 0, -1}, {"Kernel.spawn", NULL, U, 0, -1},
|
||||
{"Process.spawn", NULL, U, 0, -1}, {"Process.exec", NULL, U, 0, -1},
|
||||
{"IO.popen", NULL, U, 0, -1}, {"PTY.spawn", NULL, U, 0, -1},
|
||||
{"Open3.popen2", NULL, U, 0, -1}, {"Open3.popen2e", NULL, U, 0, -1},
|
||||
{"Open3.popen3", NULL, U, 0, -1}, {"Open3.capture2", NULL, U, 0, -1},
|
||||
{"Open3.capture2e", NULL, U, 0, -1}, {"Open3.capture3", NULL, U, 0, -1},
|
||||
{"Open3.pipeline", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t php[] = {
|
||||
{"exec", NULL, U, 0, -1},
|
||||
{"shell_exec", NULL, U, 0, -1},
|
||||
{"system", NULL, U, 0, -1},
|
||||
{"passthru", NULL, U, 0, -1},
|
||||
{"proc_open", NULL, U, 0, -1},
|
||||
{"popen", NULL, U, 0, -1},
|
||||
{"pcntl_exec", NULL, U, 0, -1},
|
||||
{"Symfony\\Component\\Process\\Process", NULL, Q, 0, -1},
|
||||
{"Symfony\\Component\\Process\\Process::fromShellCommandline", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t perl[] = {
|
||||
{"system", NULL, U, 0, -1},
|
||||
{"exec", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t lua[] = {
|
||||
{"os.execute", NULL, U, 0, -1},
|
||||
{"io.popen", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t r_lang[] = {
|
||||
{"system", NULL, U, 0, -1},
|
||||
{"system2", NULL, U, 0, -1},
|
||||
{"shell", NULL, U, 0, -1},
|
||||
{"processx::run", NULL, Q, 0, -1},
|
||||
{"sys::exec_wait", NULL, Q, 0, -1},
|
||||
{"sys::exec_internal", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t elixir[] = {
|
||||
{"System.cmd", NULL, U, 0, -1},
|
||||
{"System.shell", NULL, U, 0, -1},
|
||||
{"Port.open", NULL, U, 0, CBM_SPAWN_ANY_LITERAL},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t erlang[] = {
|
||||
{"os:cmd", NULL, Q, 0, -1},
|
||||
{"open_port", NULL, U, 0, -1},
|
||||
{"erlang:open_port", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t haskell[] = {
|
||||
{"callProcess", "System.Process", I, 0, -1},
|
||||
{"callCommand", "System.Process", I, 0, -1},
|
||||
{"readProcess", "System.Process", I, 0, -1},
|
||||
{"readProcessWithExitCode", "System.Process", I, 0, -1},
|
||||
{"readCreateProcess", "System.Process", I, 0, -1},
|
||||
{"readCreateProcessWithExitCode", "System.Process", I, 0, -1},
|
||||
{"createProcess", "System.Process", I, 0, -1},
|
||||
{"spawnProcess", "System.Process", I, 0, -1},
|
||||
{"spawnCommand", "System.Process", I, 0, -1},
|
||||
{"system", "System.Process", I, 0, -1},
|
||||
{"rawSystem", "System.Process", I, 0, -1},
|
||||
{"runProcess", "System.Process", I, 0, -1},
|
||||
{"runCommand", "System.Process", I, 0, -1},
|
||||
{"runInteractiveProcess", "System.Process", I, 0, -1},
|
||||
{"runInteractiveCommand", "System.Process", I, 0, -1},
|
||||
{"runProcess", "System.Process.Typed", I, 0, -1},
|
||||
{"readProcess", "System.Process.Typed", I, 0, -1},
|
||||
{"startProcess", "System.Process.Typed", I, 0, -1},
|
||||
{"system", "System.Cmd", I, 0, -1},
|
||||
{"rawSystem", "System.Cmd", I, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t dlang[] = {
|
||||
{"execute", "std.process", I, 0, -1},
|
||||
{"executeShell", "std.process", I, 0, -1},
|
||||
{"spawnProcess", "std.process", I, 0, -1},
|
||||
{"spawnShell", "std.process", I, 0, -1},
|
||||
{"pipeProcess", "std.process", I, 0, -1},
|
||||
{"pipeShell", "std.process", I, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
/* Foundation's Process: the program is a property set after construction,
|
||||
* so a bare construction is a spawn site with a dynamic program. */
|
||||
static const cbm_spawn_api_t swift[] = {
|
||||
{"Process.launchedProcess", NULL, U, 0, -1},
|
||||
{"Process.run", NULL, U, 0, -1},
|
||||
{"Process", "Foundation", I, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t crystal[] = {
|
||||
{"Process.run", NULL, U, 0, -1}, {"Process.new", NULL, U, 0, -1},
|
||||
{"Process.exec", NULL, U, 0, -1}, {"system", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t zig[] = {
|
||||
{"std.process.Child.init", NULL, Q, 0, -1},
|
||||
{"std.process.Child.run", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
|
||||
{"std.ChildProcess.init", NULL, Q, 0, -1},
|
||||
{"std.ChildProcess.run", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
|
||||
{"std.ChildProcess.exec", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
|
||||
{"std.process.execv", NULL, Q, 1, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t ocaml[] = {
|
||||
{"Sys.command", NULL, U, 0, -1},
|
||||
{"Unix.system", NULL, U, 0, -1},
|
||||
{"Unix.create_process", NULL, U, 0, -1},
|
||||
{"Unix.create_process_env", NULL, U, 0, -1},
|
||||
{"Unix.open_process", NULL, U, 0, -1},
|
||||
{"Unix.open_process_in", NULL, U, 0, -1},
|
||||
{"Unix.open_process_out", NULL, U, 0, -1},
|
||||
{"Unix.open_process_full", NULL, U, 0, -1},
|
||||
{"Unix.open_process_args", NULL, U, 0, -1},
|
||||
{"Unix.execv", NULL, U, 0, -1},
|
||||
{"Unix.execvp", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t clojure[] = {
|
||||
{"clojure.java.shell/sh", NULL, Q, 0, -1},
|
||||
{"sh", "clojure.java.shell", I, 0, -1},
|
||||
{"babashka.process/process", NULL, Q, 0, -1},
|
||||
{"babashka.process/shell", NULL, Q, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t tcl[] = {
|
||||
{"exec", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t fortran[] = {
|
||||
{"execute_command_line", NULL, U, 0, -1},
|
||||
{"system", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t julia[] = {
|
||||
{"run", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
static const cbm_spawn_api_t powershell[] = {
|
||||
{"Start-Process", NULL, U, 0, -1},
|
||||
{NULL, NULL, 0, 0, -1},
|
||||
};
|
||||
|
||||
#undef Q
|
||||
#undef U
|
||||
#undef I
|
||||
#undef S
|
||||
|
||||
static const cbm_spawn_api_t *spawn_table(CBMLanguage lang) {
|
||||
switch (lang) {
|
||||
case CBM_LANG_C:
|
||||
case CBM_LANG_CPP:
|
||||
case CBM_LANG_CUDA:
|
||||
case CBM_LANG_OBJC:
|
||||
return c_family;
|
||||
case CBM_LANG_PYTHON:
|
||||
return python;
|
||||
case CBM_LANG_JAVASCRIPT:
|
||||
case CBM_LANG_TYPESCRIPT:
|
||||
case CBM_LANG_TSX:
|
||||
case CBM_LANG_ARKTS:
|
||||
return js_family;
|
||||
case CBM_LANG_GO:
|
||||
return go;
|
||||
case CBM_LANG_RUST:
|
||||
return rust;
|
||||
case CBM_LANG_JAVA:
|
||||
case CBM_LANG_KOTLIN:
|
||||
case CBM_LANG_SCALA:
|
||||
case CBM_LANG_GROOVY:
|
||||
return jvm;
|
||||
case CBM_LANG_CSHARP:
|
||||
case CBM_LANG_FSHARP:
|
||||
return dotnet;
|
||||
case CBM_LANG_RUBY:
|
||||
return ruby;
|
||||
case CBM_LANG_PHP:
|
||||
return php;
|
||||
case CBM_LANG_PERL:
|
||||
return perl;
|
||||
case CBM_LANG_LUA:
|
||||
case CBM_LANG_LUAU:
|
||||
case CBM_LANG_TEAL:
|
||||
return lua;
|
||||
case CBM_LANG_R:
|
||||
return r_lang;
|
||||
case CBM_LANG_ELIXIR:
|
||||
return elixir;
|
||||
case CBM_LANG_ERLANG:
|
||||
return erlang;
|
||||
case CBM_LANG_HASKELL:
|
||||
return haskell;
|
||||
case CBM_LANG_DLANG:
|
||||
return dlang;
|
||||
case CBM_LANG_SWIFT:
|
||||
return swift;
|
||||
case CBM_LANG_CRYSTAL:
|
||||
return crystal;
|
||||
case CBM_LANG_ZIG:
|
||||
return zig;
|
||||
case CBM_LANG_OCAML:
|
||||
return ocaml;
|
||||
case CBM_LANG_CLOJURE:
|
||||
return clojure;
|
||||
case CBM_LANG_TCL:
|
||||
return tcl;
|
||||
case CBM_LANG_FORTRAN:
|
||||
return fortran;
|
||||
case CBM_LANG_JULIA:
|
||||
return julia;
|
||||
case CBM_LANG_POWERSHELL:
|
||||
return powershell;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* An import value without Node's `node:` scheme. */
|
||||
static const char spawn_node_scheme[] = "node:";
|
||||
static const char *spawn_module(const char *val) {
|
||||
size_t n = sizeof(spawn_node_scheme) - 1;
|
||||
return strncmp(val, spawn_node_scheme, n) == 0 ? val + n : val;
|
||||
}
|
||||
|
||||
static const char *const spawn_separators[] = {"::", ".", ":", "/", "\\", NULL};
|
||||
|
||||
/* Length of the callee's head (before its first separator) and the
|
||||
* separator's offset, or 0 when the callee has none. */
|
||||
static size_t spawn_head_len(const char *callee) {
|
||||
size_t best = 0;
|
||||
for (int i = 0; spawn_separators[i]; i++) {
|
||||
const char *at = strstr(callee, spawn_separators[i]);
|
||||
if (at && at > callee && (best == 0 || (size_t)(at - callee) < best)) {
|
||||
best = (size_t)(at - callee);
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* True when `val` names `name` as its last segment ("subprocess.run" for
|
||||
* run, "java.lang.ProcessBuilder" for ProcessBuilder). */
|
||||
static bool spawn_val_names(const char *val, const char *name) {
|
||||
size_t vl = strlen(val);
|
||||
size_t nl = strlen(name);
|
||||
if (vl <= nl || strcmp(val + vl - nl, name) != 0) {
|
||||
return false;
|
||||
}
|
||||
char sep = val[vl - nl - 1];
|
||||
return sep == '.' || sep == ':' || sep == '/' || sep == '\\';
|
||||
}
|
||||
|
||||
/* The callee as its import makes it: `sp.run` -> subprocess.run (an alias),
|
||||
* `run` -> subprocess.run (a member import), `spawn` -> child_process.spawn
|
||||
* (a named import mapped to its module). Empty when no import applies. */
|
||||
static void spawn_expand(const char *callee, const CBMImport *imports, int count, char *out,
|
||||
size_t n) {
|
||||
out[0] = '\0';
|
||||
size_t head = spawn_head_len(callee);
|
||||
size_t key_len = head ? head : strlen(callee);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *key = imports[i].local_name;
|
||||
if (!key || !imports[i].module_path || strlen(key) != key_len ||
|
||||
strncmp(key, callee, key_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
const char *val = spawn_module(imports[i].module_path);
|
||||
if (head) {
|
||||
snprintf(out, n, "%s%s", val, callee + head);
|
||||
} else if (spawn_val_names(val, callee)) {
|
||||
snprintf(out, n, "%s", val);
|
||||
} else {
|
||||
snprintf(out, n, "%s.%s", val, callee);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static bool spawn_imports_module(const CBMImport *imports, int count, const char *module) {
|
||||
size_t ml = strlen(module);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!imports[i].module_path) {
|
||||
continue;
|
||||
}
|
||||
const char *val = spawn_module(imports[i].module_path);
|
||||
if (strncmp(val, module, ml) == 0 &&
|
||||
(val[ml] == '\0' || val[ml] == '.' || val[ml] == ':' || val[ml] == '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Groovy `"git status".execute`: a quoted receiver, then the api suffix. */
|
||||
static bool spawn_string_receiver(const char *callee, const char *suffix) {
|
||||
size_t cl = strlen(callee);
|
||||
size_t sl = strlen(suffix);
|
||||
return (callee[0] == '"' || callee[0] == '\'') && cl > sl + 1 &&
|
||||
strcmp(callee + cl - sl, suffix) == 0 && callee[cl - sl - 1] == callee[0];
|
||||
}
|
||||
|
||||
static bool spawn_entry_matches(const cbm_spawn_api_t *e, const char *callee, const char *expanded,
|
||||
const CBMImport *imports, int count) {
|
||||
switch (e->rule) {
|
||||
case CBM_SPAWN_QUALIFIED:
|
||||
return strcmp(callee, e->api) == 0 || (expanded[0] && strcmp(expanded, e->api) == 0);
|
||||
case CBM_SPAWN_BARE:
|
||||
return strcmp(callee, e->api) == 0;
|
||||
case CBM_SPAWN_BARE_IMPORTED:
|
||||
return strcmp(callee, e->api) == 0 && spawn_imports_module(imports, count, e->module);
|
||||
case CBM_SPAWN_STRING_RECEIVER:
|
||||
return spawn_string_receiver(callee, e->api);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const cbm_spawn_api_t *cbm_spawn_match(CBMLanguage lang, const char *callee,
|
||||
const CBMImport *imports, int count) {
|
||||
const cbm_spawn_api_t *table = spawn_table(lang);
|
||||
if (!table || !callee || !callee[0]) {
|
||||
return NULL;
|
||||
}
|
||||
if (!imports) {
|
||||
count = 0;
|
||||
}
|
||||
char expanded[CBM_SZ_512];
|
||||
spawn_expand(callee, imports, count, expanded, sizeof(expanded));
|
||||
for (const cbm_spawn_api_t *e = table; e->api; e++) {
|
||||
if (spawn_entry_matches(e, callee, expanded, imports, count)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The first quoted literal in `text` ("..", '..' or `..`), copied without
|
||||
* its quotes. False when there is none. */
|
||||
static bool spawn_first_literal(const char *text, char *out, size_t n) {
|
||||
for (const char *p = text; *p; p++) {
|
||||
if (*p != '"' && *p != '\'' && *p != '`') {
|
||||
continue;
|
||||
}
|
||||
const char *end = p + 1;
|
||||
while (*end && *end != *p) {
|
||||
end += (*end == '\\' && end[1]) ? 2 : 1;
|
||||
}
|
||||
if (!*end) {
|
||||
return false;
|
||||
}
|
||||
size_t len = (size_t)(end - p - 1);
|
||||
if (len >= n) {
|
||||
len = n - 1;
|
||||
}
|
||||
memcpy(out, p + 1, len);
|
||||
out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *spawn_arg_expr(const CBMCall *call, int index) {
|
||||
for (int i = 0; i < call->arg_count; i++) {
|
||||
if (call->args[i].index == index && call->args[i].expr) {
|
||||
return call->args[i].expr;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool spawn_word_char(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
}
|
||||
|
||||
static const char *spawn_skip_space(const char *p) {
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
|
||||
p++;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Past a keyword label in front of an argument (`launchPath: `, `args=`). */
|
||||
static const char *spawn_skip_label(const char *p) {
|
||||
const char *q = p;
|
||||
while (spawn_word_char(*q)) {
|
||||
q++;
|
||||
}
|
||||
if (q == p) {
|
||||
return p;
|
||||
}
|
||||
q = spawn_skip_space(q);
|
||||
if ((*q == ':' && q[1] != ':') || (*q == '=' && q[1] != '=')) {
|
||||
return spawn_skip_space(q + 1);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* A string literal at the start of `p`, after a prefix such as r, b, f, L
|
||||
* or @ (r"..", L"..", @".."). */
|
||||
static bool spawn_leading_literal(const char *p, char *out, size_t n) {
|
||||
for (int i = 0; i < 2 && *p && strchr("rRbBuUfFL@$", *p); i++) {
|
||||
p++;
|
||||
}
|
||||
return (*p == '"' || *p == '\'' || *p == '`') && spawn_first_literal(p, out, n);
|
||||
}
|
||||
|
||||
/* What may stand before a collection's bracket: nothing, Zig's `&.` / `.`,
|
||||
* Java's `new String[]`, or a collection constructor. A name before `[` is a
|
||||
* subscript (`record["argv"]`) and any other call's arguments are not the
|
||||
* command (`os.path.join("bin", tool)`). */
|
||||
static bool spawn_collection_prefix(const char *p, size_t len) {
|
||||
static const char *const ctors[] = {
|
||||
"listOf", "arrayOf", "mutableListOf", "List.of", "Arrays.asList", "vec!", "c", "Cmd", NULL};
|
||||
if (len == 0 || (len <= 2 && strspn(p, "&.") == len) ||
|
||||
(len > 2 && strncmp(p + len - 2, "[]", 2) == 0 && strncmp(p, "new ", 4) == 0)) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; ctors[i]; i++) {
|
||||
if (strlen(ctors[i]) == len && strncmp(p, ctors[i], len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The first element of a collection written at `p` ([..], (..), {..},
|
||||
* listOf(..), &.{..}), copied into `out`. False when `p` is no collection. */
|
||||
static bool spawn_first_element(const char *p, char *out, size_t n) {
|
||||
const char *open = p;
|
||||
while (*open && (spawn_word_char(*open) || strchr("!.&:<> ", *open) ||
|
||||
(open[0] == '[' && open[1] == ']'))) {
|
||||
open += (open[0] == '[') ? 2 : 1;
|
||||
}
|
||||
if ((*open != '[' && *open != '(' && *open != '{') ||
|
||||
!spawn_collection_prefix(p, (size_t)(open - p))) {
|
||||
return false;
|
||||
}
|
||||
p = open;
|
||||
const char *start = spawn_skip_space(p + 1);
|
||||
int depth = 0;
|
||||
const char *end = start;
|
||||
for (; *end; end++) {
|
||||
if (*end == '"' || *end == '\'' || *end == '`') {
|
||||
const char *close = strchr(end + 1, *end);
|
||||
if (!close) {
|
||||
break;
|
||||
}
|
||||
end = close;
|
||||
} else if (strchr("[({", *end)) {
|
||||
depth++;
|
||||
} else if (strchr("])}", *end)) {
|
||||
if (depth-- == 0) {
|
||||
break;
|
||||
}
|
||||
} else if (*end == ',' && depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t len = (size_t)(end - start);
|
||||
if (len == 0 || len >= n) {
|
||||
return false;
|
||||
}
|
||||
memcpy(out, start, len);
|
||||
out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The program literal an argument spells: a leading literal, or the first
|
||||
* element of a collection when that element is one. A program held in a
|
||||
* variable (`[binary, "--version"]`) is no literal: the arguments after it
|
||||
* are not the program. */
|
||||
static bool spawn_expr_literal(const char *expr, char *out, size_t n) {
|
||||
char element[CBM_SZ_256];
|
||||
for (int depth = 0; depth < 3; depth++) {
|
||||
expr = spawn_skip_label(spawn_skip_space(expr));
|
||||
if (spawn_leading_literal(expr, out, n)) {
|
||||
return true;
|
||||
}
|
||||
if (!spawn_first_element(expr, element, sizeof(element))) {
|
||||
return false;
|
||||
}
|
||||
expr = element;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A literal from the argument at `index`. Tcl words are literals unquoted;
|
||||
* elsewhere an unquoted argument counts only when the extractor itself read
|
||||
* it as the call's string (Perl's `system("make")`). */
|
||||
static bool spawn_arg_literal(const CBMCall *call, CBMLanguage lang, int index, bool anywhere,
|
||||
char *out, size_t n) {
|
||||
const char *expr = spawn_arg_expr(call, index);
|
||||
if (!expr) {
|
||||
if (call->arg_count == 0 && index == 0 && call->first_string_arg) {
|
||||
snprintf(out, n, "%s", call->first_string_arg);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (anywhere ? spawn_first_literal(expr, out, n) : spawn_expr_literal(expr, out, n)) {
|
||||
return true;
|
||||
}
|
||||
bool bareword = lang == CBM_LANG_TCL && strpbrk(expr, "$[") == NULL;
|
||||
if (bareword || (call->first_string_arg && strcmp(expr, call->first_string_arg) == 0)) {
|
||||
snprintf(out, n, "%s", expr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A shell word that is not the program: an environment assignment
|
||||
* (`LC_ALL=C make`) or the `env` that runs the program after them
|
||||
* (`/usr/bin/env make`). */
|
||||
static bool spawn_prefix_word(const char *word, size_t len) {
|
||||
static const char env[] = "env";
|
||||
size_t env_len = sizeof(env) - 1;
|
||||
if (len >= env_len && strncmp(word + len - env_len, env, env_len) == 0 &&
|
||||
(len == env_len || word[len - env_len - 1] == '/')) {
|
||||
return true;
|
||||
}
|
||||
size_t i = 0;
|
||||
while (i < len && spawn_word_char(word[i])) {
|
||||
i++;
|
||||
}
|
||||
return i > 0 && i < len && word[i] == '=';
|
||||
}
|
||||
|
||||
/* The program word of a command line: its first word that is not an
|
||||
* environment prefix, without directories. Anything built at run time is not
|
||||
* a program name. */
|
||||
static void spawn_program_word(const char *literal, char *buf, size_t n) {
|
||||
size_t len = 0;
|
||||
for (;;) {
|
||||
while (*literal == ' ' || *literal == '\t') {
|
||||
literal++;
|
||||
}
|
||||
len = strcspn(literal, " \t\r\n");
|
||||
if (!spawn_prefix_word(literal, len) || !literal[len]) {
|
||||
break;
|
||||
}
|
||||
literal += len;
|
||||
}
|
||||
const char *start = literal;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (literal[i] == '/' || literal[i] == '\\') {
|
||||
start = literal + i + 1;
|
||||
}
|
||||
}
|
||||
len -= (size_t)(start - literal);
|
||||
bool dynamic = len == 0 || len >= CBM_SPAWN_PROGRAM_MAX || len >= n;
|
||||
for (size_t i = 0; i < len && !dynamic; i++) {
|
||||
dynamic = strchr("$%{}`()<>|;&*?[]\"'", start[i]) != NULL;
|
||||
}
|
||||
if (dynamic) {
|
||||
snprintf(buf, n, "%s", CBM_SPAWN_DYNAMIC);
|
||||
return;
|
||||
}
|
||||
memcpy(buf, start, len);
|
||||
buf[len] = '\0';
|
||||
}
|
||||
|
||||
void cbm_spawn_program(const cbm_spawn_api_t *api, CBMLanguage lang, const CBMCall *call, char *buf,
|
||||
size_t n) {
|
||||
char literal[CBM_SZ_256];
|
||||
bool found = false;
|
||||
if (api->rule == CBM_SPAWN_STRING_RECEIVER) {
|
||||
found = spawn_first_literal(call->callee_name, literal, sizeof(literal));
|
||||
} else {
|
||||
bool anywhere = api->alt == CBM_SPAWN_ANY_LITERAL;
|
||||
found = spawn_arg_literal(call, lang, api->arg, anywhere, literal, sizeof(literal)) ||
|
||||
(api->alt >= 0 &&
|
||||
spawn_arg_literal(call, lang, api->alt, false, literal, sizeof(literal)));
|
||||
}
|
||||
if (!found) {
|
||||
snprintf(buf, n, "%s", CBM_SPAWN_DYNAMIC);
|
||||
return;
|
||||
}
|
||||
spawn_program_word(literal, buf, n);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* spawn_patterns.h — process-spawning call sites, per language.
|
||||
*
|
||||
* Like the HTTP client tables in service_patterns.h: a call that starts
|
||||
* another program (subprocess.run, exec.Command, posix_spawn, Process.Start,
|
||||
* Command::new, ...) becomes a SPAWNS edge to a Process node named after the
|
||||
* program it starts. Nothing resolves the program to code: the edge records
|
||||
* that the caller leaves the graph through a process boundary, and which
|
||||
* program it starts when the call spells it as a literal.
|
||||
*/
|
||||
#ifndef CBM_SPAWN_PATTERNS_H
|
||||
#define CBM_SPAWN_PATTERNS_H
|
||||
|
||||
#include "cbm.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* How a table spelling matches a call. */
|
||||
typedef enum {
|
||||
/* The full spelling, written out or reached through the file's imports
|
||||
* (`sp.run` with `import subprocess as sp`, `run` from
|
||||
* `from subprocess import run`, Go `exec.Command` from "os/exec"). */
|
||||
CBM_SPAWN_QUALIFIED = 0,
|
||||
/* A bare name (`system`, `popen`, `exec`): the spelling alone. A
|
||||
* definition of that name in the calling module (or one the call reaches
|
||||
* through an import) still makes it an ordinary call. */
|
||||
CBM_SPAWN_BARE,
|
||||
/* A bare name that counts when the file imports `module`
|
||||
* (Haskell `callProcess` with System.Process, a destructured
|
||||
* `const { spawn } = require("child_process")`). */
|
||||
CBM_SPAWN_BARE_IMPORTED,
|
||||
/* Groovy `"git status".execute()`: the receiver is the command line. */
|
||||
CBM_SPAWN_STRING_RECEIVER,
|
||||
} cbm_spawn_rule_t;
|
||||
|
||||
typedef struct {
|
||||
const char *api; /* canonical spelling: "subprocess.run", "os/exec.Command", "system" */
|
||||
const char *module; /* the import a CBM_SPAWN_BARE_IMPORTED name needs, else NULL */
|
||||
unsigned char rule; /* cbm_spawn_rule_t */
|
||||
signed char arg; /* argument holding the program or command line */
|
||||
signed char alt; /* second place to look, -1 for none (CreateProcess: name or line),
|
||||
* or CBM_SPAWN_ANY_LITERAL */
|
||||
} cbm_spawn_api_t;
|
||||
|
||||
/* `alt` value: the program is the first literal anywhere in the argument,
|
||||
* which is an options tuple or struct (Elixir {:spawn, "cat"}, Zig
|
||||
* .{ .argv = &.{"ls"} }). Elsewhere only a leading literal or a collection's
|
||||
* first element is the program. */
|
||||
#define CBM_SPAWN_ANY_LITERAL (-2)
|
||||
|
||||
/* The spawn API a call names, or NULL. `imports` are the calling file's own
|
||||
* import statements (local name -> module path) as extracted, external
|
||||
* modules included: subprocess or os/exec never resolve to project nodes, so
|
||||
* the resolver's import map does not hold them. Whether the call is a spawn
|
||||
* after all is the resolver's to decide: an evidence-backed resolution onto
|
||||
* project code (an import, the same module, the LSP; never a name guess)
|
||||
* makes it an ordinary call (src/pipeline/pass_spawns.c). */
|
||||
const cbm_spawn_api_t *cbm_spawn_match(CBMLanguage lang, const char *callee,
|
||||
const CBMImport *imports, int count);
|
||||
|
||||
/* The program a matched spawn starts: the first literal in its program
|
||||
* argument, that literal's first word, without directories
|
||||
* ("/usr/bin/git status" -> "git"). CBM_SPAWN_DYNAMIC when the program is
|
||||
* not a literal. Writes into buf (at least CBM_SPAWN_PROGRAM_MAX bytes). */
|
||||
#define CBM_SPAWN_DYNAMIC "<dynamic>"
|
||||
#define CBM_SPAWN_PROGRAM_MAX 64
|
||||
void cbm_spawn_program(const cbm_spawn_api_t *api, CBMLanguage lang, const CBMCall *call, char *buf,
|
||||
size_t n);
|
||||
|
||||
#endif
|
||||
+1
-1
@@ -17,7 +17,7 @@ codes) and rejects unknown flags with exit 2 + `Please consult --help.`
|
||||
|
||||
| leg | entry | what a run gives you |
|
||||
|---|---|---|
|
||||
| **test** | `test.sh` | DEFAULT = the venue leg: static contracts (Step 0a–0r) + CLEAN ASan+UBSan build + all suites via the parallel harness + prod-binary guards. `--suites a,b` = iteration mode (incremental rebuild, subset, seconds). `--tsan` = the ThreadSanitizer leg. CLANGARM64 gets CI's trap-UBSan flags automatically. |
|
||||
| **test** | `test.sh` | DEFAULT = the venue leg: static contracts (Step 0a–0r) + CLEAN ASan+UBSan build + all suites via the parallel harness + prod-binary guards. `--suites a,b` = iteration mode (incremental rebuild, subset, seconds). `--tsan` = the ThreadSanitizer leg. `--contracts-only` = the static contract steps alone (PR CI's contracts job for docs-only and non-product changes). CLANGARM64 gets CI's trap-UBSan flags automatically. |
|
||||
| **package wrappers** | `ci/test-package-wrappers.sh` | Go, npm, and PyPI runtime-set publication/lock suites on the current host. CI runs the same entry on Linux and Windows so both platform lock implementations gate. |
|
||||
| **build** | `build.sh` | CLEAN production runtime set (native executable + authenticated integration asset; `--with-ui` adds one content-addressed UI pack). ccache via `env.sh` makes repeats fast; `CCACHE_COMPILERCHECK=content` guarantees a hit is byte-identical to a cold compile — never stale. `--version`, `STATIC=1`, `BUILD_DIR=`. |
|
||||
| **lint** | `lint.sh` | clang-tidy + cppcheck + clang-format (+ no-skips policy). `--ci` = the CI gate set (no clang-tidy). Drives the same make targets as `make lint`/`lint-ci`. |
|
||||
|
||||
@@ -13,8 +13,10 @@ CI and the local infrastructure — both of which the venue-parity contract
|
||||
| `preflight-docker.sh` | Same idea for Colima/docker: prune runner-unlike residue, assert free space on the filesystem backing the docker data root (not the VM's `/`). Build cache + named volumes KEPT (the local analogue of actions/cache); `--deep` drops them. | `test-infrastructure/run.sh` |
|
||||
| `check-glibc-compat.sh` | Run a linux binary in debian:bullseye (glibc 2.31) — the portable binary must start on old glibc. | `_smoke.yml` portable legs |
|
||||
| `generate-sbom.py` | The release SPDX SBOM (vendored versions reviewable here, diffable by vendoring PRs — was inline YAML). | `release.yml` |
|
||||
| `require-all-green.sh` | The aggregate gate: fail unless every needed job succeeded or legitimately skipped (was inline YAML). | `pr.yml ci-ok` |
|
||||
| `verify-shard-union.sh` | Prove sharded test legs lost nothing: shard count agreement, indices 1..n, identical suite lists, union of slices == full list (was inline YAML). | `_test.yml` shard-completeness |
|
||||
| `require-all-green.sh` | The aggregate gate: fail unless every needed job succeeded or legitimately skipped (was inline YAML) — and, under lane selection, every SELECTED lane ran and succeeded (skipped/cancelled/missing = red; unselected lanes may skip). | `pr.yml ci-ok` |
|
||||
| `select-lanes.sh` | Decide which PR lanes a change set runs (tier, lanes, full, reasons) from its file list — the R1 rule set, fail-safe to every lane. Pinned by `tests/test_select_lanes.sh` and the September 2026 history replay (`scripts/test-impact/replay-selector.sh`). | `pr.yml changes` |
|
||||
| `test-impact-shadow.sh` | The report-only test-impact shadow: fetch the latest released binary (checksum-verified) or take a given one, index this checkout into a throwaway cache, run `detect_changes` against the base and map it to suites (`scripts/test-impact/predict.py`). Never gates. | `pr.yml test-impact-shadow` |
|
||||
| `verify-shard-union.sh` | Prove sharded test legs lost nothing: shard count agreement, indices 1..n, identical suite lists, union of slices == full list (was inline YAML). No manifests is green only when the lane selection holds no test leg. | `_test.yml` shard-completeness |
|
||||
| `prepare-release-candidates.sh` | Copy one linker output into stripped/unstripped candidates, finalize signatures, composition-check them without execution, and record their hashes. | `_build.yml`, local artifact smoke |
|
||||
| `stage-release-candidates.py` | Admit exactly eight candidate artifacts / sixteen byte-distinct binaries into the content-addressed VirusTotal scan set. | `_build.yml` |
|
||||
| `select-release-candidates.py` | Apply the reviewed tuple-local VT truth table, or the explicit dry-run stripped default, and atomically copy one content-bound binary per target. | `_build.yml` |
|
||||
|
||||
@@ -26,14 +26,23 @@ CI venue would never see.
|
||||
|
||||
.PARAMETER SkipGate
|
||||
Sweep and report, but do not fail when free space is short. For inspection only.
|
||||
|
||||
.PARAMETER CheckOnly
|
||||
Assert the free-space floor without enumerating or removing residue. Isolated
|
||||
runs use fresh checkout/build/temp roots and must not sweep another run's files.
|
||||
Cannot be combined with SkipGate.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[double]$MinFreeGB = 14,
|
||||
[switch]$SkipGate
|
||||
[switch]$SkipGate,
|
||||
[switch]$CheckOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($CheckOnly -and $SkipGate) {
|
||||
throw 'CheckOnly cannot be combined with SkipGate: the disk floor is required.'
|
||||
}
|
||||
|
||||
function Get-FreeGB {
|
||||
[math]::Round((Get-PSDrive C).Free / 1GB, 2)
|
||||
@@ -56,21 +65,23 @@ function Remove-ResidueDir([string]$Path) {
|
||||
$before = Get-FreeGB
|
||||
$removed = 0
|
||||
$failed = @()
|
||||
$roots = @($env:USERPROFILE, $env:TEMP) | Where-Object { $_ } | Select-Object -Unique
|
||||
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root)) { continue }
|
||||
Get-ChildItem -LiteralPath $root -Directory -Filter 'cbm-*' -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {
|
||||
if (Remove-ResidueDir $_.FullName) {
|
||||
$removed++
|
||||
} else {
|
||||
$failed += $_.FullName
|
||||
if (-not $CheckOnly) {
|
||||
$roots = @($env:USERPROFILE, $env:TEMP) | Where-Object { $_ } | Select-Object -Unique
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root)) { continue }
|
||||
Get-ChildItem -LiteralPath $root -Directory -Filter 'cbm-*' -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {
|
||||
if (Remove-ResidueDir $_.FullName) {
|
||||
$removed++
|
||||
} else {
|
||||
$failed += $_.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$after = Get-FreeGB
|
||||
if ($CheckOnly) { Write-Output 'preflight-mode=check-only' }
|
||||
Write-Output "residue-dirs-removed=$removed residue-dirs-failed=$($failed.Count) free-gb-before=$before free-gb-after=$after"
|
||||
foreach ($f in $failed) { Write-Warning "could not sweep: $f" }
|
||||
|
||||
|
||||
+100
-10
@@ -1,31 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# require-all-green.sh — the aggregate gate step: fail unless every needed job
|
||||
# succeeded (or was legitimately skipped).
|
||||
# succeeded (or was legitimately skipped) and every SELECTED lane ran green.
|
||||
#
|
||||
# Canonical CI step (called by pr.yml ci-ok with RESULTS = toJSON(needs)).
|
||||
# Lived inline in workflow YAML until 2026-07-26; the venue-parity contract
|
||||
# forbids logic in workflow run-blocks. `skipped` counts as OK because gated
|
||||
# jobs (e.g. pr-smoke on docs-only PRs) skip by design — a matrix rename can
|
||||
# still never silently deadlock a merge, which is this gate's purpose.
|
||||
# forbids logic in workflow run-blocks. `skipped` counts as OK at the needs
|
||||
# level because gated jobs (e.g. pr-smoke on docs-only PRs) skip by design — a
|
||||
# matrix rename can still never silently deadlock a merge, which is this
|
||||
# gate's purpose.
|
||||
#
|
||||
# Usage: RESULTS='<json of the needs context>' scripts/ci/require-all-green.sh
|
||||
# Lane selection (LANES + JOBS, both set by pr.yml): a PR runs only the lanes
|
||||
# scripts/ci/select-lanes.sh selected, and there `skipped` is no longer a free
|
||||
# pass — a selected lane whose jobs were skipped, cancelled, never created or
|
||||
# are anything but success is a silent gate loss, so it fails. Lanes nobody
|
||||
# selected may be skipped or absent (but may not fail); memwaste is
|
||||
# report-only and never gates. The selection itself must be trustworthy: a
|
||||
# list of known lanes that holds lint or contracts (every tier runs one).
|
||||
#
|
||||
# Usage: RESULTS='<json of the needs context>' \
|
||||
# [LANES='<selector lanes, JSON list>' JOBS=<file>] \
|
||||
# scripts/ci/require-all-green.sh
|
||||
# JOBS: this run's jobs as JSON lines {"name","status","conclusion"}
|
||||
# (gh api .../actions/runs/<id>/jobs?filter=latest --jq '.jobs[] | ...').
|
||||
set -euo pipefail
|
||||
|
||||
case "${1:-}" in
|
||||
-h | --help)
|
||||
sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'
|
||||
sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
: "${RESULTS:?set RESULTS to the toJSON(needs) context}"
|
||||
|
||||
UNIVERSE=""
|
||||
if [ -n "${LANES+set}" ]; then
|
||||
UNIVERSE=$(bash "$(dirname "$0")/select-lanes.sh" --list-lanes)
|
||||
fi
|
||||
|
||||
printf '%s' "$RESULTS" | python3 -c "
|
||||
import json, sys
|
||||
import json, os, re, sys
|
||||
|
||||
needs = json.load(sys.stdin)
|
||||
bad = {k: v['result'] for k, v in needs.items() if v['result'] not in ('success', 'skipped')}
|
||||
if bad:
|
||||
print('CI NOT OK:', bad)
|
||||
problems = [f'needed job {k}: {v}' for k, v in sorted(bad.items())]
|
||||
|
||||
if 'LANES' in os.environ:
|
||||
universe = set(sys.argv[1].split())
|
||||
lanes = None
|
||||
try:
|
||||
lanes = json.loads(os.environ['LANES'])
|
||||
except ValueError:
|
||||
pass
|
||||
if not isinstance(lanes, list) or not all(isinstance(l, str) for l in lanes):
|
||||
problems.append('LANES is not a JSON list of lane names: ' + repr(os.environ['LANES']))
|
||||
lanes = []
|
||||
selected = set(lanes)
|
||||
if selected - universe:
|
||||
problems.append('unknown lane(s) selected: ' + ', '.join(sorted(selected - universe)))
|
||||
if not selected & {'lint', 'contracts'}:
|
||||
problems.append('selection has neither lint nor contracts -- refusing to trust it')
|
||||
|
||||
# job name -> lane. Matrix jobs carry their lane among the values in
|
||||
# parentheses; everything else is named by its (caller /) job id.
|
||||
single = {
|
||||
'lint / lint': 'lint', 'lint / lint-mem': 'lint-mem',
|
||||
'security / security-static': 'security-static',
|
||||
'security / license-gate': 'license-gate', 'security / codeql-gate': 'codeql-gate',
|
||||
'test / test-diag': 'diag', 'test / test-msan': 'msan',
|
||||
'test / test-lsan-macos': 'lsan-macos', 'test / test-windows-guards': 'windows-guards',
|
||||
'test / shard-completeness': 'shard-completeness', 'contracts': 'contracts',
|
||||
}
|
||||
matrix = ('test / test-unix (', 'test / test-windows (', 'test / test-tsan (', 'pr-smoke (')
|
||||
def lane_of(name):
|
||||
if name in single:
|
||||
return single[name]
|
||||
if name.startswith('test / test-package-wrappers'):
|
||||
return 'pkg-wrappers'
|
||||
if name == 'memwaste' or name.startswith('memwaste / '):
|
||||
return 'memwaste'
|
||||
if name.startswith(matrix) and name.endswith(')'):
|
||||
values = name[name.index('(') + 1:-1].split(', ')
|
||||
hits = [v for v in values if v in universe]
|
||||
return hits[0] if len(hits) == 1 else None
|
||||
return None
|
||||
|
||||
jobs_by_lane = {}
|
||||
path = os.environ.get('JOBS', '')
|
||||
if not path or not os.path.isfile(path):
|
||||
problems.append('JOBS (the run\'s job list) is missing: cannot verify the selected lanes')
|
||||
else:
|
||||
for line in open(path, encoding='utf-8'):
|
||||
if line.strip():
|
||||
job = json.loads(line)
|
||||
lane = lane_of(job.get('name', ''))
|
||||
if lane:
|
||||
jobs_by_lane.setdefault(lane, []).append(job)
|
||||
for lane in sorted(universe - {'memwaste'}):
|
||||
jobs = jobs_by_lane.get(lane, [])
|
||||
if lane in selected:
|
||||
if not jobs:
|
||||
problems.append(f'selected lane {lane}: no job ran')
|
||||
for job in jobs:
|
||||
state = job.get('conclusion') if job.get('status') == 'completed' else job.get('status')
|
||||
if state != 'success':
|
||||
problems.append(f'selected lane {lane}: {job[\"name\"]} is {state}')
|
||||
else:
|
||||
for job in jobs:
|
||||
if job.get('status') == 'completed' and job.get('conclusion') not in ('success', 'skipped'):
|
||||
problems.append(f'unselected lane {lane}: {job[\"name\"]} is {job[\"conclusion\"]}')
|
||||
if not problems:
|
||||
print('selected lanes green:', ', '.join(sorted(selected)))
|
||||
|
||||
if problems:
|
||||
print('CI NOT OK:')
|
||||
for p in problems:
|
||||
print(' ' + p)
|
||||
sys.exit(1)
|
||||
print('CI OK:', ', '.join(needs))
|
||||
"
|
||||
" "$UNIVERSE"
|
||||
|
||||
Executable
+377
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env bash
|
||||
# select-lanes.sh — decide which PR CI lanes a change set must run.
|
||||
#
|
||||
# Canonical CI step (called by pr.yml's `changes` job on the PR's file list).
|
||||
# This is the R1 rule set, measured against every PR push of September 2026
|
||||
# (767 pushes; scripts/test-impact/replay-selector.sh replays them, plus the 22
|
||||
# real failures, as a contract): each changed path gets a category, the
|
||||
# categories give a tier, the tier gives a lane set, and a few categories add
|
||||
# lanes of their own. `tier` is the classification the analysis measured;
|
||||
# `full` overrides it with every lane.
|
||||
#
|
||||
# It FAILS SAFE: full=true for tier T5, a path no rule knows, an empty file
|
||||
# list, a truncated one (file_list_truncated: the pulls/files API stops at
|
||||
# 3000 files), this script, any workflow, the test harness, Makefile.cbm
|
||||
# (source registration and flag changes look identical in a file list) and
|
||||
# vendored code. It is a pure function of the file SET (and the expected
|
||||
# count): order, duplicates, blank lines and CRLF never change a byte of the
|
||||
# output.
|
||||
#
|
||||
# Usage: scripts/ci/select-lanes.sh [--format json|github-output]
|
||||
# [--expect-files N] [FILE]
|
||||
# FILE (default stdin): changed paths, one per line.
|
||||
# --expect-files N: the PR's changed_files; fewer distinct paths, or
|
||||
# N or the list at the 3000-file API cap, means truncated -> full.
|
||||
# json (default): {"full","lanes","reasons","tier"} on one line.
|
||||
# github-output: tier=, full=, lanes=, reasons= lines ($GITHUB_OUTPUT).
|
||||
# scripts/ci/select-lanes.sh --batch FILE
|
||||
# FILE: JSON lines {"id":..., "files":[...], "expect_files"?: N};
|
||||
# prints one json result per line with its "id" (the history
|
||||
# replay's single process).
|
||||
# scripts/ci/select-lanes.sh --list-lanes
|
||||
# Every lane name, one per line (the universe ci-ok checks against).
|
||||
# Exit: 0 = decided · 2 = usage error.
|
||||
set -euo pipefail
|
||||
|
||||
usage() { sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
FORMAT=json
|
||||
MODE="select"
|
||||
INPUT=""
|
||||
EXPECT=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--format)
|
||||
FORMAT="${2:-}"
|
||||
[ $# -gt 1 ] && shift
|
||||
;;
|
||||
--format=*) FORMAT="${1#--format=}" ;;
|
||||
--batch)
|
||||
MODE="batch"
|
||||
INPUT="${2:-}"
|
||||
[ $# -gt 1 ] && shift
|
||||
;;
|
||||
--list-lanes) MODE="list" ;;
|
||||
--expect-files)
|
||||
# An empty value (a workflow whose count went missing) must fail
|
||||
# loudly, never quietly skip the truncation check.
|
||||
EXPECT="${2:-}"
|
||||
[ -n "$EXPECT" ] || EXPECT="missing"
|
||||
[ $# -gt 1 ] && shift
|
||||
;;
|
||||
-*)
|
||||
echo "select-lanes.sh: unknown option '$1'. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
if [ -n "$INPUT" ]; then
|
||||
echo "select-lanes.sh: more than one input file. Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
INPUT="$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
case "$FORMAT" in
|
||||
json | github-output) ;;
|
||||
*)
|
||||
echo "select-lanes.sh: --format must be json or github-output. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
case "${EXPECT:-0}" in
|
||||
*[!0-9]*)
|
||||
echo "select-lanes.sh: --expect-files takes a file count. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
if [ "$MODE" = batch ] && [ -z "$INPUT" ]; then
|
||||
echo "select-lanes.sh: --batch needs a file. Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -n "$INPUT" ] && [ ! -f "$INPUT" ]; then
|
||||
echo "select-lanes.sh: no such file '$INPUT'. Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# The heredoc below is the program, so stdin input is staged in a file first.
|
||||
STAGED=""
|
||||
if [ "$MODE" = select ] && [ -z "$INPUT" ]; then
|
||||
STAGED=$(mktemp "${TMPDIR:-/tmp}/cbm-select-lanes.XXXXXX")
|
||||
trap 'rm -f "$STAGED"' EXIT
|
||||
cat >"$STAGED"
|
||||
INPUT="$STAGED"
|
||||
fi
|
||||
|
||||
python3 - "$MODE" "$FORMAT" "$INPUT" "$EXPECT" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
mode, fmt, source = sys.argv[1:4]
|
||||
expect = int(sys.argv[4]) if sys.argv[4] else None
|
||||
|
||||
# ── Path -> category (first matching rule wins; order is part of the rule) ──
|
||||
GATE_WORKFLOWS = {"pr.yml", "_test.yml", "_lint.yml", "_security.yml",
|
||||
"_smoke.yml", "_memwaste.yml", "codeql.yml", "dco.yml"}
|
||||
BUILD = {"Makefile.cbm", "scripts/test.sh", "scripts/run-tests-parallel.sh",
|
||||
"scripts/env.sh", "scripts/build.sh", "scripts/clean.sh",
|
||||
"scripts/path-safety.sh", "scripts/test-runtime.sh", "scripts/msan.sh",
|
||||
"scripts/lint.sh", ".clang-format", ".clang-tidy",
|
||||
"scripts/vendored-checksums.txt"}
|
||||
SMOKE = {"scripts/smoke-test.sh", "scripts/smoke-local.sh",
|
||||
"scripts/smoke-fixture-server.py", "scripts/smoke-invariants.sh",
|
||||
"test-infrastructure/vm/vm-smoke.sh",
|
||||
"test-infrastructure/vm/windows-user-path-guard.ps1",
|
||||
"scripts/test-windows.ps1"}
|
||||
LINT_CONFIG = {"scripts/memory-core-baseline.txt", "scripts/lint-memory-core.py",
|
||||
"scripts/hooks/pre-commit"}
|
||||
NON_GATING = {"scripts/package-release.sh", "scripts/benchmark-index.sh",
|
||||
"scripts/benchmark-search-graph.sh", "scripts/benchmark-streaming.py",
|
||||
"scripts/soak-test.sh", ".cbmignore", ".node-version", "flake.nix"}
|
||||
PKG_FILES = {"install.sh", "install.ps1", "server.json", "package.json", "manifest.json"}
|
||||
DOC_SUFFIXES = (".md", ".png", ".svg", ".jpg", ".gif", ".txt")
|
||||
DOC_FILES = {"LICENSE", "CODEOWNERS", ".gitignore", ".gitattributes"}
|
||||
|
||||
|
||||
def category(path):
|
||||
base = path.rsplit("/", 1)[-1]
|
||||
if path == "Makefile.cbm":
|
||||
return "build"
|
||||
if path in LINT_CONFIG:
|
||||
return "lint-cfg"
|
||||
if path.startswith(("scripts/memwaste", "scripts/memlab")):
|
||||
return "memwaste-tools"
|
||||
if path in NON_GATING or path.startswith("scripts/test-impact/"):
|
||||
return "ci-other"
|
||||
if path == "scripts/test_mcp_in_process.py":
|
||||
return "test-contract"
|
||||
if path == "scripts/fuzz.sh":
|
||||
return "fuzz"
|
||||
if path == "scripts/audit-license-provenance.py":
|
||||
return "ci-gate"
|
||||
if path.startswith(("internal/cbm/vendored/grammars/", "tools/tree-sitter-")):
|
||||
return "grammar"
|
||||
if path.startswith(".github/workflows/"):
|
||||
return "ci-gate" if base in GATE_WORKFLOWS else "ci-other"
|
||||
if path.startswith(".github/"):
|
||||
if path.endswith(".md") or ("ISSUE_TEMPLATE" in path and path.endswith((".yml", ".yaml"))):
|
||||
return "docs"
|
||||
return "ci-other"
|
||||
if path.startswith("scripts/ci/"):
|
||||
return "ci-gate"
|
||||
if path in BUILD:
|
||||
return "build"
|
||||
if path in SMOKE:
|
||||
return "smoke"
|
||||
if path.startswith("tests/fixtures/"):
|
||||
return "test"
|
||||
if path.startswith("tests/windows/"):
|
||||
return "test-win"
|
||||
if path.startswith("tests/fuzz/"):
|
||||
return "fuzz"
|
||||
if path.startswith("tests/"):
|
||||
return "test-contract" if path.endswith((".sh", ".py", ".ps1")) else "test"
|
||||
if path.startswith(("docs/", "eval-results/")) or base in DOC_FILES or (
|
||||
path.endswith(DOC_SUFFIXES) and not path.startswith(("src/", "internal/", "scripts/"))):
|
||||
return "docs"
|
||||
if path.startswith("graph-ui/"):
|
||||
return "ui-frontend"
|
||||
if path.startswith(("pkg/", "scripts/setup")) or base in PKG_FILES:
|
||||
return "pkg"
|
||||
if path.startswith(("internal/cbm/vendored/", "vendored/")):
|
||||
return "vendored"
|
||||
m = re.match(r"internal/cbm/lsp/([a-z]+)_", path)
|
||||
if m and m.group(1) not in ("lsp", "type", "scope"):
|
||||
return "lsp:" + m.group(1)
|
||||
if path.startswith("internal/cbm/lsp/"):
|
||||
return "lsp-core"
|
||||
if path.startswith("internal/cbm/"):
|
||||
return "extract"
|
||||
if path == "src/main.c":
|
||||
return "cli"
|
||||
if path.startswith("src/foundation/"):
|
||||
return "mem-core" if re.search(r"/(mem|slab|arena|alloc|mimalloc)", path) else "foundation"
|
||||
if path.startswith("src/"):
|
||||
return "src:" + path.split("/")[1]
|
||||
if path.startswith("test-infrastructure/"):
|
||||
return "local-infra"
|
||||
if path.startswith("scripts/"):
|
||||
return "scripts-other"
|
||||
return "other"
|
||||
|
||||
|
||||
# ── Categories -> tier (highest wins) ──
|
||||
EXTRACT = {"grammar", "lsp-core", "extract"}
|
||||
MID = {"pipeline", "mcp", "store", "cypher", "discover", "git", "graph_buffer",
|
||||
"semantic", "simhash", "traces", "personal_memory"}
|
||||
PLATFORM = {"daemon", "cli", "watcher", "ui", "foundation", "smoke", "pkg", "test-win"}
|
||||
CORE = {"mem-core", "build", "vendored", "ci-gate", "other", "scripts-other"}
|
||||
LOWER = {"test", "test-contract", "fuzz", "ui-frontend", "ci-other",
|
||||
"local-infra", "docs", "lint-cfg", "memwaste-tools"}
|
||||
|
||||
|
||||
def normalized(cat):
|
||||
# src/<dir>/... categories are the directory name; a directory no tier
|
||||
# names stays unknown (and therefore T5 + full).
|
||||
if cat.startswith("src:"):
|
||||
name = cat[4:]
|
||||
return name if name in MID | PLATFORM else cat
|
||||
return cat
|
||||
|
||||
|
||||
def known(cat):
|
||||
return cat in EXTRACT | MID | PLATFORM | CORE | LOWER or cat.startswith("lsp:")
|
||||
|
||||
|
||||
def tier_of(cats):
|
||||
if cats & CORE or not all(known(c) for c in cats):
|
||||
return "T5-core"
|
||||
if cats & PLATFORM:
|
||||
return "T4-platform"
|
||||
if cats & MID:
|
||||
return "T3-mid"
|
||||
if cats & EXTRACT or any(c.startswith("lsp:") for c in cats):
|
||||
return "T2-extract"
|
||||
if cats & {"test", "test-contract", "fuzz"}:
|
||||
return "T1-tests"
|
||||
if cats & {"ui-frontend", "ci-other", "local-infra", "lint-cfg", "memwaste-tools"}:
|
||||
return "T0b-nonproduct"
|
||||
return "T0a-docs"
|
||||
|
||||
|
||||
# ── Tier -> lanes (R1) ──
|
||||
FULL = {"codeql-gate", "diag", "license-gate", "lint", "lint-mem", "lsan-macos",
|
||||
"memwaste", "msan", "pkg-wrappers", "security-static", "shard-completeness",
|
||||
"smoke-mac", "smoke-ubuntu", "smoke-win", "tsan-arm", "tsan-mac", "tsan-x86",
|
||||
"unix-arm64", "unix-macos-intel", "unix-macos14", "unix-x86", "windows",
|
||||
"windows-guards"}
|
||||
UNIVERSE = FULL | {"contracts"} # contracts: the T0a/T0b job; every test leg runs them too
|
||||
LINT = {"lint", "lint-mem"}
|
||||
CORE_LEGS = {"unix-x86", "unix-macos14", "windows", "shard-completeness"}
|
||||
TEST_LEGS = {"unix-x86", "unix-arm64", "unix-macos14", "unix-macos-intel", "windows"}
|
||||
ALWAYS = {"security-static"}
|
||||
R1 = {
|
||||
"T0a-docs": {"contracts"},
|
||||
# The contract steps police test-infrastructure/ and lint config too.
|
||||
"T0b-nonproduct": LINT | {"contracts"},
|
||||
"T1-tests": LINT | CORE_LEGS,
|
||||
"T2-extract": LINT | CORE_LEGS | {"codeql-gate", "tsan-x86", "smoke-ubuntu"},
|
||||
"T3-mid": LINT | CORE_LEGS | {"codeql-gate", "tsan-x86", "smoke-ubuntu"},
|
||||
"T4-platform": FULL - {"msan", "diag", "memwaste"},
|
||||
"T5-core": FULL,
|
||||
}
|
||||
# Category add-ons: (categories, lanes). pkg adds nothing to a docs-only set
|
||||
# because a pkg path is never docs-only (it lifts the tier to T4).
|
||||
ADD_ONS = [
|
||||
({"pkg"}, {"pkg-wrappers", "smoke-ubuntu", "smoke-mac", "smoke-win"}),
|
||||
({"vendored", "pkg", "ci-gate", "build"}, {"license-gate"}),
|
||||
({"ui-frontend"}, {"windows-guards", "smoke-ubuntu"}),
|
||||
({"memwaste-tools", "mem-core"}, {"memwaste"}),
|
||||
({"test-win"}, {"windows", "windows-guards"}),
|
||||
# the contract steps run inside test.sh's default leg
|
||||
({"ci-other", "test-contract", "fuzz"}, {"unix-x86"}),
|
||||
]
|
||||
HARNESS = {"scripts/test.sh", "scripts/run-tests-parallel.sh", "scripts/run-test-wave.py",
|
||||
"tests/test_main.c", "tests/test_framework.h"}
|
||||
|
||||
|
||||
def triggers(path, cat):
|
||||
found = []
|
||||
if path == "scripts/ci/select-lanes.sh":
|
||||
found.append("the lane selector itself")
|
||||
if path.startswith(".github/workflows/"):
|
||||
found.append("workflow")
|
||||
if path in HARNESS:
|
||||
found.append("test harness")
|
||||
if path == "Makefile.cbm":
|
||||
found.append("Makefile.cbm (registration and flag changes are indistinguishable by name)")
|
||||
if cat == "vendored":
|
||||
found.append("vendored code")
|
||||
if cat in ("other", "scripts-other") or not known(cat):
|
||||
found.append("path matched by no rule")
|
||||
return found
|
||||
|
||||
|
||||
LIST_CAP = 3000 # the pulls/files API lists at most this many files
|
||||
|
||||
|
||||
def truncated(paths, expect):
|
||||
"""A list that may be missing files: at or over the API cap, or shorter
|
||||
than the PR's changed_files (a complete list is never shorter: every
|
||||
changed file is listed, renames add their old path on top)."""
|
||||
if len(paths) >= LIST_CAP:
|
||||
return f"{len(paths)} paths listed, at the {LIST_CAP}-file API cap"
|
||||
if expect is not None and expect >= LIST_CAP:
|
||||
return f"the PR changes {expect} files, over the {LIST_CAP}-file API cap"
|
||||
if expect is not None and len(paths) < expect:
|
||||
return f"{len(paths)} paths listed for {expect} changed files"
|
||||
return None
|
||||
|
||||
|
||||
def select(paths, expect=None):
|
||||
paths = sorted({p for p in paths if p})
|
||||
by_cat, by_trigger = {}, {}
|
||||
for path in paths:
|
||||
cat = normalized(category(path))
|
||||
by_cat.setdefault(cat, []).append(path)
|
||||
for trigger in triggers(path, cat):
|
||||
by_trigger.setdefault(trigger, []).append(path)
|
||||
cats = set(by_cat)
|
||||
tier = tier_of(cats)
|
||||
lanes = R1[tier] | ALWAYS
|
||||
reasons = [f"category {c}: {len(p)} file(s), e.g. {p[0]}" for c, p in by_cat.items()]
|
||||
for when, extra in ADD_ONS:
|
||||
hit = sorted(cats & when)
|
||||
if hit and tier != "T0a-docs" and not extra <= lanes:
|
||||
reasons.append(f"add-on {'/'.join(hit)}: +{' +'.join(sorted(extra - lanes))}")
|
||||
lanes = lanes | extra
|
||||
if lanes & TEST_LEGS:
|
||||
lanes = lanes | {"shard-completeness"}
|
||||
for trigger, hits in by_trigger.items():
|
||||
more = f" (+{len(hits) - 1} more)" if len(hits) > 1 else ""
|
||||
reasons.append(f"full: {trigger}: {hits[0]}{more}")
|
||||
if not paths:
|
||||
reasons.append("full: empty file list (fail safe)")
|
||||
if tier == "T5-core":
|
||||
reasons.append("full: tier T5-core")
|
||||
short = truncated(paths, expect)
|
||||
if short:
|
||||
reasons.append(f"full: file_list_truncated: {short} (fail safe)")
|
||||
full = tier == "T5-core" or bool(by_trigger) or not paths or bool(short)
|
||||
if full:
|
||||
lanes = set(FULL)
|
||||
return {"full": full, "lanes": sorted(lanes), "reasons": sorted(reasons), "tier": tier}
|
||||
|
||||
|
||||
def dump(obj):
|
||||
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def read_paths(path):
|
||||
with open(path, encoding="utf-8") as fh: # universal newlines: CRLF == LF
|
||||
return [line.strip() for line in fh]
|
||||
|
||||
|
||||
if mode == "list":
|
||||
print("\n".join(sorted(UNIVERSE)))
|
||||
elif mode == "batch":
|
||||
with open(source, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
if line.strip():
|
||||
row = json.loads(line)
|
||||
print(dump({"id": row["id"], **select(row["files"], row.get("expect_files"))}))
|
||||
elif fmt == "github-output":
|
||||
decision = select(read_paths(source), expect)
|
||||
print(f"tier={decision['tier']}")
|
||||
print(f"full={'true' if decision['full'] else 'false'}")
|
||||
print(f"lanes={dump(decision['lanes'])}")
|
||||
print(f"reasons={dump(decision['reasons'])}")
|
||||
else:
|
||||
print(dump(select(read_paths(source), expect)))
|
||||
PY
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-impact-bundle.sh — fetch the team artifact bundle of one main commit.
|
||||
#
|
||||
# The bundle (codebase-memory-mcp test-impact publish; src/mcp/test_impact_artifact.h)
|
||||
# is published by .github/workflows/test-impact-artifact.yml as the Actions
|
||||
# artifact `test-impact-<sha>`. A selection may pass --artifact-verified ONLY
|
||||
# for a bundle this script fetched: it accepts nothing but a SUCCESSFUL run of
|
||||
# that workflow, in THIS repository, on the main branch, whose head is exactly
|
||||
# the requested commit — the provenance the receipt itself can never assert.
|
||||
#
|
||||
# Usage: scripts/ci/test-impact-bundle.sh --fetch SHA DIR
|
||||
# Download the bundle of SHA into DIR (created). Needs GH_TOKEN with
|
||||
# actions:read; $GH overrides the gh command, $GITHUB_REPOSITORY the
|
||||
# repository (OWNER/NAME).
|
||||
# Exit: 0 = fetched and verified · 3 = no admissible bundle for SHA ·
|
||||
# 1 = the tooling broke · 2 = usage error.
|
||||
set -euo pipefail
|
||||
|
||||
GH="${GH:-gh}"
|
||||
REPO="${GITHUB_REPOSITORY:-DeusData/codebase-memory-mcp}"
|
||||
WORKFLOW="test-impact-artifact.yml"
|
||||
|
||||
usage() { sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
[ $# -eq 3 ] && [ "$1" = "--fetch" ] || { usage >&2; exit 2; }
|
||||
SHA="$2"
|
||||
DIR="$3"
|
||||
case "$SHA" in
|
||||
*[!0-9a-f]* | "") echo "test-impact-bundle: not a lowercase commit id: $SHA" >&2; exit 2 ;;
|
||||
esac
|
||||
[ ${#SHA} -eq 40 ] || [ ${#SHA} -eq 64 ] || { echo "test-impact-bundle: bad id length" >&2; exit 2; }
|
||||
|
||||
# The newest successful main-branch run of the producer for exactly this head.
|
||||
run=$("$GH" api -X GET "repos/$REPO/actions/workflows/$WORKFLOW/runs" \
|
||||
-f branch=main -f status=success -f head_sha="$SHA" -f per_page=20 \
|
||||
--jq '[.workflow_runs[]
|
||||
| select(.head_sha == "'"$SHA"'" and .head_branch == "main"
|
||||
and .repository.full_name == "'"$REPO"'"
|
||||
and .head_repository.full_name == "'"$REPO"'"
|
||||
and (.event == "push" or .event == "schedule"
|
||||
or .event == "workflow_dispatch"))]
|
||||
| sort_by(.run_started_at) | last | .id // empty') || exit 1
|
||||
if [ -z "$run" ]; then
|
||||
echo "test-impact-bundle: no successful producer run for $SHA" >&2
|
||||
exit 3
|
||||
fi
|
||||
mkdir -p "$DIR"
|
||||
if ! "$GH" run download "$run" --repo "$REPO" --name "test-impact-$SHA" --dir "$DIR"; then
|
||||
echo "test-impact-bundle: run $run has no artifact test-impact-$SHA" >&2
|
||||
exit 3
|
||||
fi
|
||||
# The receipt must name the commit the run was for; the engine checks the rest.
|
||||
if ! python3 - "$DIR/receipt.json" "$SHA" <<'PY'
|
||||
import json, sys
|
||||
receipt = json.load(open(sys.argv[1]))
|
||||
sys.exit(0 if receipt.get("commit") == sys.argv[2] else 1)
|
||||
PY
|
||||
then
|
||||
echo "test-impact-bundle: the receipt does not name $SHA" >&2
|
||||
exit 3
|
||||
fi
|
||||
echo "test-impact-bundle: run $run, bundle of $SHA in $DIR"
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-impact-publish.sh — publish the team artifact bundle of the checked-out
|
||||
# main commit (.github/workflows/test-impact-artifact.yml).
|
||||
#
|
||||
# Incremental (a push): the previous commit's bundle is the graph base and its
|
||||
# coverage map the starting point; only the suites the push's own selection
|
||||
# picks are re-run with coverage and merged in (scripts/test-impact/
|
||||
# coverage-merge.py). The oldest observation is carried forward, so an
|
||||
# incremental map ages out unless a full refresh runs.
|
||||
# Full (weekly, manual, or no usable previous bundle): every suite is re-run
|
||||
# and the graph is built from scratch. A weekly run ALSO builds the graph
|
||||
# incrementally from the previous bundle and fails when the two graphs'
|
||||
# content digests differ: an incremental index must equal a full one.
|
||||
#
|
||||
# Usage: scripts/ci/test-impact-publish.sh --binary PATH --runner PATH --out DIR
|
||||
# --mode incremental|full [--before SHA] [--llvm-bin DIR] [--platform LABEL]
|
||||
# Exit: 0 = published · 1 = failed · 2 = usage error.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BINARY="" RUNNER="" OUT="" MODE="" BEFORE="" LLVM_BIN="" PLATFORM="linux-x86_64-clang21"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--binary) BINARY="$2"; shift 2 ;;
|
||||
--runner) RUNNER="$2"; shift 2 ;;
|
||||
--out) OUT="$2"; shift 2 ;;
|
||||
--mode) MODE="$2"; shift 2 ;;
|
||||
--before) BEFORE="$2"; shift 2 ;;
|
||||
--llvm-bin) LLVM_BIN="$2"; shift 2 ;;
|
||||
--platform) PLATFORM="$2"; shift 2 ;;
|
||||
*) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BINARY" ] && [ -n "$RUNNER" ] && [ -n "$OUT" ] || exit 2
|
||||
case "$MODE" in incremental | full) ;; *) exit 2 ;; esac
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
# A private daemon rendezvous: the socket path must fit sun_path.
|
||||
export CBM_RUNTIME_DIR="$(mktemp -d /tmp/cbm-rt.XXXXXX)"
|
||||
export CBM_CACHE_DIR="$WORK/cache"
|
||||
mkdir -p "$WORK/engine" && chmod 700 "$WORK/engine"
|
||||
HEAD_SHA="$(git -C "$ROOT" rev-parse HEAD)"
|
||||
PREVIOUS=""
|
||||
|
||||
if [ -n "$BEFORE" ]; then
|
||||
if scripts/ci/test-impact-bundle.sh --fetch "$BEFORE" "$WORK/previous"; then
|
||||
PREVIOUS="$WORK/previous"
|
||||
fi
|
||||
fi
|
||||
if [ "$MODE" = incremental ] && [ -z "$PREVIOUS" ]; then
|
||||
echo "test-impact-publish: no previous bundle for ${BEFORE:-?}: full refresh"
|
||||
MODE=full
|
||||
fi
|
||||
|
||||
map() { # out [suite...]
|
||||
local out="$1"; shift
|
||||
local args=()
|
||||
for suite in "$@"; do args+=(--suite "$suite"); done
|
||||
python3 "$ROOT/scripts/test-impact/coverage-map.py" --runner "$RUNNER" --out "$out" \
|
||||
${LLVM_BIN:+--llvm-bin "$LLVM_BIN"} ${args[@]+"${args[@]}"}
|
||||
}
|
||||
|
||||
OBSERVED=""
|
||||
if [ "$MODE" = full ]; then
|
||||
OBSERVED="$(date +%s)"
|
||||
map "$WORK/coverage"
|
||||
else
|
||||
# The push's own selection decides what to re-run (whole suites).
|
||||
"$BINARY" test-impact select --repo "$ROOT" --base "$BEFORE" --work "$WORK/engine" \
|
||||
--artifact "$PREVIOUS" --artifact-commit "$BEFORE" --artifact-verified \
|
||||
--platform "$PLATFORM" > "$WORK/selection.json"
|
||||
python3 "$ROOT/scripts/test-impact/selection.py" suites "$WORK/selection.json" > "$WORK/suites.txt"
|
||||
"$RUNNER" --list-suites > "$WORK/listed.txt"
|
||||
if grep -qx '\*' "$WORK/suites.txt"; then
|
||||
OBSERVED="$(date +%s)"
|
||||
map "$WORK/coverage"
|
||||
else
|
||||
mapfile -t rerun < <(sort -u "$WORK/suites.txt")
|
||||
map "$WORK/fresh" ${rerun[@]+"${rerun[@]}"}
|
||||
python3 "$ROOT/scripts/test-impact/coverage-merge.py" --previous "$PREVIOUS/coverage" \
|
||||
--fresh "$WORK/fresh" --suites "$WORK/listed.txt" --commit "$HEAD_SHA" \
|
||||
--out "$WORK/coverage"
|
||||
fi
|
||||
fi
|
||||
|
||||
publish() { # out [previous]
|
||||
"$BINARY" test-impact publish --repo "$ROOT" --out "$1" --work "$WORK/engine" \
|
||||
--coverage "$WORK/coverage" ${OBSERVED:+--observed-at "$OBSERVED"} \
|
||||
--platform "$PLATFORM" ${2:+--previous "$2"}
|
||||
}
|
||||
if [ "$MODE" = full ]; then
|
||||
publish "$OUT"
|
||||
if [ -n "$PREVIOUS" ]; then
|
||||
publish "$WORK/incremental" "$PREVIOUS"
|
||||
field() { python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))[sys.argv[2]])' "$1" "$2"; }
|
||||
# Topology (what selection reads) must be equal; edge properties and the
|
||||
# corpus-statistical edges are reported, not gated (recorded defects).
|
||||
full_topology="$(field "$OUT/receipt.json" graph_topology_sha256)"
|
||||
incr_topology="$(field "$WORK/incremental/receipt.json" graph_topology_sha256)"
|
||||
if [ "$full_topology" != "$incr_topology" ]; then
|
||||
echo "test-impact-publish: incremental topology differs from the full one ($incr_topology != $full_topology)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(field "$OUT/receipt.json" graph_content_sha256)" != \
|
||||
"$(field "$WORK/incremental/receipt.json" graph_content_sha256)" ]; then
|
||||
echo "::warning::incremental graph content (edge properties, similarity edges) differs from the full one; topology is equal"
|
||||
fi
|
||||
echo "test-impact-publish: incremental topology equals the full one ($full_topology)"
|
||||
fi
|
||||
else
|
||||
publish "$OUT" "$PREVIOUS"
|
||||
fi
|
||||
echo "test-impact-publish: bundle of $HEAD_SHA in $OUT ($MODE)"
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-impact-select.sh — the PR selection with the team artifact: what
|
||||
# `codebase-memory-mcp test-impact select` runs for this change.
|
||||
#
|
||||
# Shadow (default): REPORT-ONLY; pr.yml's test-impact-shadow job runs it
|
||||
# continue-on-error and ci-ok does not need it.
|
||||
# --gate DIR (pr.yml's select-tests job, only when the repository variable
|
||||
# TEST_IMPACT_MODE is `gate`): also turns the answer into what the test legs
|
||||
# run (scripts/test-impact/selection.py) — DIR/test-only.txt and
|
||||
# DIR/optional-suites.txt for a narrowed selection — and writes
|
||||
# `selection=narrowed` or `selection=all` to $GITHUB_OUTPUT. Anything but a
|
||||
# well-formed narrowed answer, a lane that selects nothing included, is `all`.
|
||||
#
|
||||
# The base is the base side of the PR merge commit (HEAD^1 by default). Its
|
||||
# bundle is fetched with scripts/ci/test-impact-bundle.sh, which only accepts
|
||||
# the main-branch producer's own run for exactly that commit; only then does
|
||||
# the selection pass --artifact-verified. Without a bundle the engine builds
|
||||
# the graph itself and admits no coverage: every reached suite runs whole.
|
||||
#
|
||||
# Usage: scripts/ci/test-impact-select.sh --binary PATH --out DIR [--base REV]
|
||||
# [--platform LABEL] [--gate DIR]
|
||||
# Writes DIR/selection-engine.json and DIR/summary-engine.md (appended
|
||||
# to $GITHUB_STEP_SUMMARY when set).
|
||||
# Exit: 0 = selection written · 1 = the tooling broke · 2 = usage error.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BINARY="" OUT="" BASE="HEAD^1" PLATFORM="linux-x86_64-clang21" GATE=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--binary) BINARY="$2"; shift 2 ;;
|
||||
--out) OUT="$2"; shift 2 ;;
|
||||
--base) BASE="$2"; shift 2 ;;
|
||||
--platform) PLATFORM="$2"; shift 2 ;;
|
||||
--gate) GATE="$2"; shift 2 ;;
|
||||
*) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BINARY" ] && [ -n "$OUT" ] || exit 2
|
||||
|
||||
mkdir -p "$OUT"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
export CBM_RUNTIME_DIR="$(mktemp -d /tmp/cbm-rt.XXXXXX)" # sun_path must fit
|
||||
export CBM_CACHE_DIR="$WORK/cache"
|
||||
mkdir -p "$WORK/engine" && chmod 700 "$WORK/engine"
|
||||
base="$(git -C "$ROOT" rev-parse "$BASE")"
|
||||
|
||||
artifact=()
|
||||
bundle_rc=0
|
||||
"$ROOT/scripts/ci/test-impact-bundle.sh" --fetch "$base" "$WORK/bundle" || bundle_rc=$?
|
||||
if [ "$bundle_rc" -eq 0 ]; then
|
||||
artifact=(--artifact "$WORK/bundle" --artifact-commit "$base" --artifact-verified)
|
||||
elif [ "$bundle_rc" -ne 3 ]; then
|
||||
# No bundle means no coverage: more tests run, never fewer. Say so.
|
||||
echo "::warning::test-impact bundle fetch failed (exit $bundle_rc): selecting without coverage"
|
||||
fi
|
||||
started=$(date +%s)
|
||||
"$BINARY" test-impact select --repo "$ROOT" --base "$base" --work "$WORK/engine" \
|
||||
--platform "$PLATFORM" ${artifact[@]+"${artifact[@]}"} > "$OUT/selection-engine.json"
|
||||
elapsed=$(( $(date +%s) - started ))
|
||||
|
||||
python3 - "$OUT/selection-engine.json" "$elapsed" "${#artifact[@]}" > "$OUT/summary-engine.md" <<'PY'
|
||||
import json, sys
|
||||
answer = json.load(open(sys.argv[1]))
|
||||
coverage = answer.get("receipt", {}).get("coverage", {})
|
||||
lines = ["### test-impact engine", "",
|
||||
f"- decision: `{answer.get('decision')}` in {sys.argv[2]} s",
|
||||
f"- team artifact: {'fetched' if sys.argv[3] != '0' else 'none'}; coverage "
|
||||
f"`{coverage.get('state')}` {coverage.get('rejection_reasons') or ''}"]
|
||||
for lane in answer.get("lanes") or []:
|
||||
suites = lane.get("suites") or []
|
||||
whole = [s["suite"] for s in suites if s.get("mode") == "whole"]
|
||||
narrowed = {s["suite"]: len(s.get("tests") or []) for s in suites if s.get("mode") == "tests"}
|
||||
lines.append(f"- lane `{lane.get('lane')}` ({lane.get('decision')}): {len(whole)} whole, "
|
||||
f"{len(narrowed)} narrowed ({sum(narrowed.values())} tests)")
|
||||
print("\n".join(lines))
|
||||
PY
|
||||
|
||||
if [ -n "$GATE" ]; then
|
||||
mkdir -p "$GATE"
|
||||
selection=all
|
||||
filter_rc=0
|
||||
python3 "$ROOT/scripts/test-impact/selection.py" filter "$OUT/selection-engine.json" \
|
||||
--lane unit --out "$GATE/test-only.txt" --optional-out "$GATE/optional-suites.txt" ||
|
||||
filter_rc=$?
|
||||
if [ "$filter_rc" -eq 0 ]; then
|
||||
selection=narrowed
|
||||
echo "- PR CI runs $(wc -l < "$GATE/test-only.txt" | tr -d ' ') selection tokens" \
|
||||
>> "$OUT/summary-engine.md"
|
||||
else
|
||||
rm -f "$GATE/test-only.txt" "$GATE/optional-suites.txt"
|
||||
echo "- PR CI runs every test (selection exit $filter_rc)" >> "$OUT/summary-engine.md"
|
||||
fi
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
echo "selection=$selection" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
cat "$OUT/summary-engine.md"
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
cat "$OUT/summary-engine.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-impact-shadow.sh — the PR test-impact SHADOW: predict from the code graph
|
||||
# which C test suites a change can affect, and publish the prediction. It is
|
||||
# REPORT-ONLY: pr.yml runs it continue-on-error and ci-ok does not need it, so
|
||||
# it can never block or pass a merge. Its predictions accumulate next to the
|
||||
# lanes that actually ran and failed, which is how graph-based selection gets
|
||||
# evaluated before anyone gates on it.
|
||||
#
|
||||
# The binary is the latest PUBLISHED release, not this checkout's build: a few
|
||||
# seconds of download instead of minutes of compile, and a PR that breaks the
|
||||
# indexer cannot corrupt its own prediction. pr.yml builds the checkout only
|
||||
# if the download fails.
|
||||
#
|
||||
# Usage: scripts/ci/test-impact-shadow.sh --fetch-release DIR [--repo OWNER/REPO]
|
||||
# Download the latest release's archive for this host + checksums.txt
|
||||
# into DIR, verify the SHA-256, extract DIR/codebase-memory-mcp.
|
||||
# Needs GH_TOKEN (read-only); $GH overrides the gh command.
|
||||
# scripts/ci/test-impact-shadow.sh --binary PATH --out DIR [--base REV] [--depth N]
|
||||
# Index this checkout into a throwaway cache (HOME, XDG dirs and
|
||||
# CBM_CACHE_DIR under DIR/work, a private CBM_RUNTIME_DIR under /tmp:
|
||||
# the daemon's socket path must fit sun_path), run
|
||||
# detect_changes against REV (default HEAD^1, the base side of a PR
|
||||
# merge commit) at depth N (default 15), and let
|
||||
# scripts/test-impact/predict.py map it to suites with the lane
|
||||
# selector's run-all triggers. Writes DIR/prediction.json and
|
||||
# DIR/summary.md (appended to $GITHUB_STEP_SUMMARY when set). The
|
||||
# graph is only consulted when C sources changed.
|
||||
# Exit: 0 = prediction written · 1 = the tooling broke · 2 = usage error.
|
||||
set -euo pipefail
|
||||
|
||||
usage() { sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
FETCH_DIR=""
|
||||
REPO="${GITHUB_REPOSITORY:-DeusData/codebase-memory-mcp}"
|
||||
BINARY=""
|
||||
OUT=""
|
||||
BASE="HEAD^1"
|
||||
DEPTH=15
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--fetch-release | --repo | --binary | --out | --base | --depth)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "test-impact-shadow.sh: $1 needs a value. Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$1" in
|
||||
--fetch-release) FETCH_DIR="$2" ;;
|
||||
--repo) REPO="$2" ;;
|
||||
--binary) BINARY="$2" ;;
|
||||
--out) OUT="$2" ;;
|
||||
--base) BASE="$2" ;;
|
||||
--depth) DEPTH="$2" ;;
|
||||
esac
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "test-impact-shadow.sh: unknown argument '$1'. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
sha256_check() { # file with "<sha> <name>" lines, run in the archive's dir
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum -c "$1"
|
||||
else
|
||||
shasum -a 256 -c "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "$FETCH_DIR" ]; then
|
||||
case "$(uname -s)/$(uname -m)" in
|
||||
Linux/x86_64) asset=codebase-memory-mcp-linux-amd64.tar.gz ;;
|
||||
Linux/aarch64 | Linux/arm64) asset=codebase-memory-mcp-linux-arm64.tar.gz ;;
|
||||
Darwin/arm64) asset=codebase-memory-mcp-darwin-arm64.tar.gz ;;
|
||||
Darwin/x86_64) asset=codebase-memory-mcp-darwin-amd64.tar.gz ;;
|
||||
*)
|
||||
echo "test-impact-shadow.sh: no release archive for $(uname -s)/$(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
read -r -a gh_cmd <<<"${GH:-gh}"
|
||||
mkdir -p "$FETCH_DIR"
|
||||
# No tag: the latest release.
|
||||
"${gh_cmd[@]}" release download --repo "$REPO" --pattern "$asset" \
|
||||
--pattern checksums.txt --dir "$FETCH_DIR" --clobber
|
||||
grep " $asset\$" "$FETCH_DIR/checksums.txt" >"$FETCH_DIR/asset.sha256"
|
||||
(cd "$FETCH_DIR" && sha256_check asset.sha256)
|
||||
tar -xzf "$FETCH_DIR/$asset" -C "$FETCH_DIR" codebase-memory-mcp
|
||||
echo "release binary: $FETCH_DIR/codebase-memory-mcp ($asset, SHA-256 verified)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "$BINARY" ] || [ -z "$OUT" ]; then
|
||||
echo "test-impact-shadow.sh: --binary and --out are required. Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$DEPTH" in
|
||||
'' | *[!0-9]*)
|
||||
echo "test-impact-shadow.sh: --depth must be a number. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
BINARY="$(cd "$(dirname "$BINARY")" && pwd)/$(basename "$BINARY")"
|
||||
PREDICT="$ROOT/scripts/test-impact/predict.py"
|
||||
mkdir -p "$OUT"
|
||||
OUT="$(cd "$OUT" && pwd)"
|
||||
WORK="$OUT/work"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK/home/.config" "$WORK/home/.cache" "$WORK/cache"
|
||||
# <runtime>/cbm-daemon-<uid>/cbm-<key>.sock adds ~42 bytes to this path, and
|
||||
# sun_path holds 104 (macOS) / 108: a deep $OUT or $TMPDIR does not fit.
|
||||
RUNTIME=$(mktemp -d /tmp/cbm-rt.XXXXXX)
|
||||
trap 'rm -rf "$RUNTIME"' EXIT
|
||||
|
||||
BASE_SHA=$(git -C "$ROOT" rev-parse --verify "$BASE^{commit}")
|
||||
git -C "$ROOT" diff --name-only --no-renames "$BASE_SHA" HEAD >"$OUT/changed-files.txt"
|
||||
bash "$ROOT/scripts/ci/select-lanes.sh" "$OUT/changed-files.txt" >"$OUT/selection.json"
|
||||
|
||||
# From here on nothing may touch the host's real config, cache or daemon.
|
||||
export HOME="$WORK/home"
|
||||
export XDG_CONFIG_HOME="$WORK/home/.config"
|
||||
export XDG_CACHE_HOME="$WORK/home/.cache"
|
||||
export CBM_CACHE_DIR="$WORK/cache"
|
||||
export CBM_RUNTIME_DIR="$RUNTIME"
|
||||
|
||||
VERSION=$("$BINARY" --version 2>/dev/null | head -1 || true)
|
||||
DETECT_ARGS=()
|
||||
T_INDEX="-"
|
||||
T_DETECT="-"
|
||||
if [ "$(python3 "$PREDICT" --needs-graph --changed "$OUT/changed-files.txt")" = yes ]; then
|
||||
python3 -c 'import json, sys; json.dump({"repo_path": sys.argv[1]}, open(sys.argv[2], "w"))' \
|
||||
"$ROOT" "$WORK/index-args.json"
|
||||
t0=$(date +%s)
|
||||
"$BINARY" cli --quiet index_repository --args-file "$WORK/index-args.json" >"$WORK/index.json"
|
||||
t1=$(date +%s)
|
||||
python3 -c '
|
||||
import json, sys
|
||||
project = json.load(open(sys.argv[1]))["project"]
|
||||
json.dump({"project": project, "base_branch": sys.argv[2], "depth": int(sys.argv[3]),
|
||||
"limit": 5000, "changed_limit": 5000, "module_limit": 0,
|
||||
"max_output_tokens": 1000000, "format": "json"}, open(sys.argv[4], "w"))
|
||||
' "$WORK/index.json" "$BASE_SHA" "$DEPTH" "$WORK/detect-args.json"
|
||||
"$BINARY" cli --quiet detect_changes --args-file "$WORK/detect-args.json" >"$WORK/detect.json"
|
||||
t2=$(date +%s)
|
||||
# The CLI started a private daemon under $RUNTIME; stop it (best effort).
|
||||
"$BINARY" daemon stop >/dev/null 2>&1 || true
|
||||
T_INDEX="$((t1 - t0))s"
|
||||
T_DETECT="$((t2 - t1))s"
|
||||
DETECT_ARGS=(--detect "$WORK/detect.json")
|
||||
fi
|
||||
|
||||
META=$(python3 -c 'import json, sys; print(json.dumps(dict(zip(sys.argv[1::2], sys.argv[2::2]))))' \
|
||||
binary "${VERSION:-unknown}" base "$BASE_SHA" "index time" "$T_INDEX" "detect time" "$T_DETECT")
|
||||
python3 "$PREDICT" --root "$ROOT" --changed "$OUT/changed-files.txt" \
|
||||
--selection "$OUT/selection.json" ${DETECT_ARGS[@]+"${DETECT_ARGS[@]}"} --depth "$DEPTH" \
|
||||
--meta "$META" --out "$OUT/prediction.json" --summary "$OUT/summary.md"
|
||||
cat "$OUT/summary.md"
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
cat "$OUT/summary.md" >>"$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
@@ -8,24 +8,56 @@
|
||||
# For every leg it asserts: all shards agree on the shard count, indices form
|
||||
# exactly 1..n, every shard saw the same full suite list, and the UNION of the
|
||||
# shard slices equals that list — a rename/re-shard can never silently drop a
|
||||
# suite (gate-quality loss) without failing here.
|
||||
# suite (gate-quality loss) without failing here. A narrowed leg (PR CI test
|
||||
# selection) records the selected list and its selection_sha256; all shards
|
||||
# must agree on that too.
|
||||
#
|
||||
# Usage: scripts/ci/verify-shard-union.sh <manifests-dir>
|
||||
# No manifests at all is a failure — unless the PR's lane selection (--lanes,
|
||||
# the JSON list from scripts/ci/select-lanes.sh) holds no test leg, in which
|
||||
# case no leg ran by design. Without --lanes (or with "all": dry run,
|
||||
# release) every leg is expected, exactly as before lane selection.
|
||||
#
|
||||
# Usage: scripts/ci/verify-shard-union.sh <manifests-dir> [--lanes <json-list>|all]
|
||||
set -eu
|
||||
|
||||
case "${1:-}" in
|
||||
-h | --help)
|
||||
sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'
|
||||
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
MANIFEST_DIR="${1:?usage: verify-shard-union.sh <manifests-dir> (see --help)}"
|
||||
LANES=all
|
||||
if [ "${2:-}" = "--lanes" ]; then
|
||||
LANES="${3:-}"
|
||||
elif [ -n "${2:-}" ]; then
|
||||
echo "verify-shard-union.sh: unknown argument '$2'. Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$LANES" in
|
||||
all | \[*\]) ;;
|
||||
*)
|
||||
echo "verify-shard-union.sh: --lanes must be 'all' or a JSON list. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
files=$(find "$MANIFEST_DIR" -name shard-manifest.txt | sort)
|
||||
files=""
|
||||
if [ -d "$MANIFEST_DIR" ]; then
|
||||
files=$(find "$MANIFEST_DIR" -name shard-manifest.txt | sort)
|
||||
fi
|
||||
if [ -z "$files" ]; then
|
||||
echo "FAIL: no shard manifests were uploaded" >&2
|
||||
exit 1
|
||||
# The test legs are the sharded ones: unix-* and windows (matched with its
|
||||
# quotes, so the windows-guards lane never reads as the windows leg).
|
||||
case "$LANES" in
|
||||
all | *\"unix-* | *\"windows\"*)
|
||||
echo "FAIL: no shard manifests were uploaded" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "OK: no test leg was selected ($LANES) — no manifests expected"
|
||||
exit 0
|
||||
fi
|
||||
rc=0
|
||||
for leg in $(grep -h '^leg=' $files | sort -u | sed 's/^leg=//'); do
|
||||
@@ -48,9 +80,19 @@ for leg in $(grep -h '^leg=' $files | sort -u | sed 's/^leg=//'); do
|
||||
rc=1
|
||||
continue
|
||||
fi
|
||||
# A narrowed leg: every shard must have applied the same test selection
|
||||
# (two selections can name the same suites and different tests).
|
||||
sel_sha=$(for f in $leg_files; do
|
||||
grep -h '^selection_sha256=' "$f" || echo "selection_sha256=none"
|
||||
done | sort -u)
|
||||
if [ "$(printf '%s\n' "$sel_sha" | wc -l)" -ne 1 ]; then
|
||||
echo "FAIL: $leg shards applied different test selections" >&2
|
||||
rc=1
|
||||
continue
|
||||
fi
|
||||
union_sha=$(for f in $leg_files; do
|
||||
sed -n '/^--- slice ---$/,$p' "$f" | tail -n +2
|
||||
done | sort | sha256sum | awk '{print $1}')
|
||||
done | sort | { sha256sum 2>/dev/null || shasum -a 256; } | awk '{print $1}')
|
||||
if [ "$union_sha" != "$list_sha" ]; then
|
||||
echo "FAIL: $leg union of shard slices != full suite list (GATE-QUALITY LOSS)" >&2
|
||||
rc=1
|
||||
|
||||
@@ -22,7 +22,7 @@ src/cli/client_adapter.c 3
|
||||
src/cli/config_edit_path.c 2
|
||||
src/cli/config_json_like.c 142
|
||||
src/cli/config_text_edit.c 72
|
||||
src/cli/config_toml_edit.c 85
|
||||
src/cli/config_toml_edit.c 73
|
||||
src/cli/config_yaml_edit.c 117
|
||||
src/cli/hook_augment.c 31
|
||||
src/cypher/cypher.c 132
|
||||
@@ -44,25 +44,24 @@ src/foundation/platform.c 11
|
||||
src/foundation/platform.h 5
|
||||
src/foundation/private_file_lock.c 56
|
||||
src/foundation/str_intern.c 7
|
||||
src/foundation/subprocess.c 24
|
||||
src/foundation/win_utf8.h 17
|
||||
src/foundation/subprocess.c 17
|
||||
src/foundation/win_utf8.h 7
|
||||
src/foundation/workspace.c 3
|
||||
src/foundation/yaml.c 10
|
||||
src/git/git_context.c 18
|
||||
src/main.c 32
|
||||
src/mcp/compact_out.c 39
|
||||
src/mcp/index_supervisor.c 8
|
||||
src/mcp/mcp.c 778
|
||||
src/mcp/mcp.c 773
|
||||
src/pipeline/artifact.c 38
|
||||
src/pipeline/fqn.c 23
|
||||
src/pipeline/lsp_resolve.h 4
|
||||
src/pipeline/lsp_surface.c 5
|
||||
src/pipeline/pass_calls.c 22
|
||||
src/pipeline/pass_compile_commands.c 17
|
||||
src/pipeline/pass_complexity.c 15
|
||||
src/pipeline/pass_configlink.c 3
|
||||
src/pipeline/pass_cross_repo.c 8
|
||||
src/pipeline/pass_definitions.c 14
|
||||
src/pipeline/pass_definitions.c 11
|
||||
src/pipeline/pass_enrichment.c 15
|
||||
src/pipeline/pass_ensemble_routing.c 10
|
||||
src/pipeline/pass_envscan.c 2
|
||||
@@ -70,7 +69,7 @@ src/pipeline/pass_githistory.c 27
|
||||
src/pipeline/pass_importance.c 4
|
||||
src/pipeline/pass_infrascan.c 2
|
||||
src/pipeline/pass_k8s.c 6
|
||||
src/pipeline/pass_lsp_cross.c 50
|
||||
src/pipeline/pass_lsp_cross.c 49
|
||||
src/pipeline/pass_parallel.c 31
|
||||
src/pipeline/pass_pkgmap.c 82
|
||||
src/pipeline/pass_semantic.c 3
|
||||
@@ -79,7 +78,7 @@ src/pipeline/pass_usages.c 4
|
||||
src/pipeline/path_alias.c 42
|
||||
src/pipeline/pipeline.c 134
|
||||
src/pipeline/pipeline_delta.c 19
|
||||
src/pipeline/pipeline_incremental.c 144
|
||||
src/pipeline/pipeline_incremental.c 143
|
||||
src/pipeline/registry.c 17
|
||||
src/pipeline/worker_pool.c 2
|
||||
src/store/store.c 358
|
||||
|
||||
@@ -35,7 +35,10 @@ if [ -z "$JOBS" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
LOGDIR="$(dirname "$RUNNER")/test-logs"
|
||||
# CBM_TEST_LOG_DIR: a private log directory (tests/test_harness_selection.sh
|
||||
# drives this harness before the real run and must not touch its logs). Keep
|
||||
# it relative: the native Windows runner reads the selection file below it.
|
||||
LOGDIR="${CBM_TEST_LOG_DIR:-$(dirname "$RUNNER")/test-logs}"
|
||||
rm -rf "$LOGDIR"
|
||||
mkdir -p "$LOGDIR"
|
||||
|
||||
@@ -129,6 +132,54 @@ if [ "$NSUITES" -lt 1 ] || grep -qvE '^[a-z0-9_]+$' "$SUITES_FILE"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test selection (smart CI; PR CI only, scripts/test-impact/selection.py).
|
||||
# CBM_TEST_SELECTION_FILE lists `suite` / `suite:test` tokens. Only the suites
|
||||
# it names are launched, and every launched runner judges its own tokens
|
||||
# through CBM_TEST_ONLY_FILE, so a selected test the runner cannot find fails
|
||||
# that suite. A selected suite this build does not register fails the run,
|
||||
# unless CBM_TEST_SELECTION_OPTIONAL names it (registered only in some builds,
|
||||
# e.g. the coverage runner): then it is dropped with a notice, exactly as a
|
||||
# full run of this build never runs it. Everything below — sharding, the
|
||||
# manifest, the union guard — then works on the selected list. Unset: the
|
||||
# full list, unchanged.
|
||||
SELECTION="none"
|
||||
if [ -n "${CBM_TEST_SELECTION_FILE:-}" ]; then
|
||||
SEL_TOKENS="$LOGDIR/selection-tokens.txt"
|
||||
if ! tr -d '\r' < "$CBM_TEST_SELECTION_FILE" > "$SEL_TOKENS"; then
|
||||
echo "FAIL: cannot read the test selection $CBM_TEST_SELECTION_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -s "$SEL_TOKENS" ] || grep -qvE '^[A-Za-z0-9_]+(:[A-Za-z0-9_]+)?$' "$SEL_TOKENS"; then
|
||||
echo "FAIL: test selection empty or malformed: $CBM_TEST_SELECTION_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
cut -d: -f1 "$SEL_TOKENS" | sort -u > "$LOGDIR/selection-suites.txt"
|
||||
if [ -n "${CBM_TEST_SELECTION_OPTIONAL:-}" ]; then
|
||||
tr -d '\r' < "$CBM_TEST_SELECTION_OPTIONAL" | sort -u > "$LOGDIR/selection-optional.txt"
|
||||
else
|
||||
: > "$LOGDIR/selection-optional.txt"
|
||||
fi
|
||||
ABSENT=$(comm -23 "$LOGDIR/selection-suites.txt" <(sort -u "$SUITES_FILE"))
|
||||
REQUIRED_ABSENT=$(comm -23 <(printf '%s\n' $ABSENT | sed '/^$/d' | sort -u) "$LOGDIR/selection-optional.txt")
|
||||
if [ -n "$REQUIRED_ABSENT" ]; then
|
||||
echo "FAIL: the test selection names suites this build does not register:" $REQUIRED_ABSENT >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -z "$ABSENT" ] || echo "selection: conditional suites not in this build, not run:" $ABSENT
|
||||
# The runner's filter keeps only this build's suites (an unknown suite's
|
||||
# token would fail every process); the suite list keeps list order.
|
||||
awk 'NR == FNR { listed[$0] = 1; next } { split($0, p, ":") } p[1] in listed' \
|
||||
"$SUITES_FILE" "$SEL_TOKENS" > "$LOGDIR/selection.txt"
|
||||
awk 'NR == FNR { selected[$0] = 1; next } $0 in selected' \
|
||||
"$LOGDIR/selection-suites.txt" "$SUITES_FILE" > "$SUITES_FILE.selected"
|
||||
mv "$SUITES_FILE.selected" "$SUITES_FILE"
|
||||
SELECTION="$(sort "$LOGDIR/selection.txt" | { sha256sum 2>/dev/null || shasum -a 256; } | awk '{print $1}')"
|
||||
echo "selection: $(wc -l < "$SUITES_FILE" | tr -d ' ') of $NSUITES suites, $(wc -l < "$LOGDIR/selection.txt" | tr -d ' ') tokens ($SELECTION)"
|
||||
NSUITES=$(wc -l < "$SUITES_FILE" | tr -d ' ')
|
||||
export CBM_TEST_ONLY_FILE="$LOGDIR/selection.txt"
|
||||
unset CBM_TEST_ONLY
|
||||
fi
|
||||
|
||||
# CBM_TEST_SHARD="i/N" runs this invocation's deterministic slice of the
|
||||
# suite list so CI can spread one platform's suites across N runner jobs.
|
||||
# Unset (or "1/1") selects everything — the sharding path is inert unless a
|
||||
@@ -246,6 +297,7 @@ NSHARD=$(wc -l < "$SHARD_EXPECT" | tr -d ' ')
|
||||
echo "leg=${CBM_TEST_LEG:-local}"
|
||||
echo "shard=${SHARD_INDEX}/${SHARD_TOTAL}"
|
||||
echo "list_sha256=$(sort "$SUITES_FILE" | { sha256sum 2>/dev/null || shasum -a 256; } | awk '{print $1}')"
|
||||
echo "selection_sha256=$SELECTION"
|
||||
echo "--- slice ---"
|
||||
cat "$SHARD_EXPECT"
|
||||
} > "$LOGDIR/shard-manifest.txt"
|
||||
@@ -344,7 +396,11 @@ for f in $(grep -v ' rc=0 ' "$RESULTS_FILE" | awk '{print $1}'); do
|
||||
done
|
||||
|
||||
echo "────────────────────────────────────────────"
|
||||
echo " $TOTAL_PASS passed, $TOTAL_FAIL failed, $TOTAL_SKIP skipped ($NSUITES suites, $JOBS jobs)"
|
||||
if [ "$SELECTION" = none ]; then
|
||||
echo " $TOTAL_PASS passed, $TOTAL_FAIL failed, $TOTAL_SKIP skipped ($NSUITES suites, $JOBS jobs)"
|
||||
else
|
||||
echo " $TOTAL_PASS passed, $TOTAL_FAIL failed, $TOTAL_SKIP skipped ($NSUITES selected suites, $JOBS jobs; selection $SELECTION)"
|
||||
fi
|
||||
echo "────────────────────────────────────────────"
|
||||
|
||||
if [ "$TOTAL_FAIL" -gt 0 ] || [ "$BAD_RC" -gt 0 ]; then
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
#!/usr/bin/env python3
|
||||
"""coverage-map.py — build the per-test coverage map of the C test runner: for
|
||||
every test, the set of functions it executed, children included. The test-impact
|
||||
engine uses it to narrow the suites whose tests cross a process boundary, where
|
||||
the call graph alone cannot say which test reaches which function.
|
||||
|
||||
Input: the instrumented runner (make -f Makefile.cbm test-runner-cov). Run with
|
||||
CBM_TEST_COVERAGE_DIR it leaves, per suite, raw profiles named after the test
|
||||
(tests/test_main.c, "Per-test coverage profiles"):
|
||||
<test>.parent.profraw the runner inside the test
|
||||
<test>.<pid>.profraw one per child process of the test
|
||||
<test>.<pid>.forked an unaccounted forked child
|
||||
_setup.<n>.profraw the runner between tests
|
||||
_setup.child.<pid>.profraw children started between tests
|
||||
_setup.child.<pid>.forked unaccounted children started between tests
|
||||
|
||||
Output directory:
|
||||
functions.tsv <id> TAB <source file> TAB <name>
|
||||
The file is where the function body stands, relative to the
|
||||
repository root, read from the coverage mapping of the
|
||||
runner itself (llvm-cov export). The profile only names the
|
||||
translation unit: a function of a unity build or a static
|
||||
inline of a header would otherwise carry the wrong file,
|
||||
and the engine joins the map to the graph by file and name.
|
||||
tests.tsv <suite>:<test> TAB <complete|incomplete> TAB <reason> TAB <ids>
|
||||
ids ascending, space separated. <suite>:* is what ran BETWEEN
|
||||
the tests of a suite (its setup); a change there concerns
|
||||
every test of the suite.
|
||||
meta.json commit, toolchain, and per suite: tests, incomplete tests,
|
||||
exit code, wall seconds
|
||||
|
||||
A test is `incomplete` when its coverage cannot be trusted to be whole: a
|
||||
forked child left its marker, a profile is unreadable, the parent profile is
|
||||
missing, or the suite did not exit 0. An incomplete test must always be
|
||||
selected; the map never claims less than it knows.
|
||||
|
||||
Raw profiles are about 7 MiB each, so every suite is reduced and deleted before
|
||||
the next one runs.
|
||||
|
||||
Usage:
|
||||
coverage-map.py --runner build/cov/test-runner --out DIR
|
||||
[--suite NAME ...] [--llvm-bin DIR] [--root DIR]
|
||||
[--keep-raw] [--timeout SECONDS]
|
||||
Exit status: 0 map written; 1 a suite failed or timed out (map still written,
|
||||
its tests incomplete); 2 usage or toolchain error.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
PROFILE = re.compile(r"^(?P<stem>.+?)\.(?P<who>parent|\d+)\.profraw$")
|
||||
MARKER = re.compile(r"^(?P<stem>.+?)\.(?P<pid>\d+)\.forked$")
|
||||
SETUP = re.compile(r"^_setup\.(?:\d+|child\.\d+)\.profraw$")
|
||||
# `llvm-profdata show --all-functions` prints one block per function, opened by
|
||||
# the name at two spaces of indent and a trailing colon.
|
||||
FUNCTION_LINE = re.compile(r"^ (?P<name>\S.*):$")
|
||||
SETUP_TEST = "*"
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"coverage-map: {message}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def executed_functions(profdata, profile):
|
||||
"""Names of the functions `profile` counted at least once, or None when the
|
||||
profile cannot be read."""
|
||||
proc = subprocess.run([profdata, "show", "--all-functions", "--value-cutoff=1", profile],
|
||||
capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
names = set()
|
||||
for line in proc.stdout.splitlines():
|
||||
match = FUNCTION_LINE.match(line)
|
||||
if match:
|
||||
names.add(match.group("name"))
|
||||
return names
|
||||
|
||||
|
||||
def source_table(llvm_cov, profdata_tool, runner, profile, root, work):
|
||||
"""{profile name: (source file, function name)} for every function compiled
|
||||
into the runner, from its coverage mapping. `profile` is any raw profile of
|
||||
that runner: llvm-cov wants one, its counts are not used."""
|
||||
indexed = os.path.join(work, "table.profdata")
|
||||
merged = subprocess.run([profdata_tool, "merge", "-sparse", "-o", indexed, profile],
|
||||
capture_output=True, text=True)
|
||||
if merged.returncode != 0:
|
||||
fail(f"llvm-profdata merge failed: {merged.stderr.strip()[-300:]}")
|
||||
exported = subprocess.run([llvm_cov, "export", "-format=text", "-skip-expansions",
|
||||
"-skip-branches", f"-instr-profile={indexed}", runner],
|
||||
capture_output=True, text=True)
|
||||
os.remove(indexed)
|
||||
if exported.returncode != 0:
|
||||
fail(f"llvm-cov export failed: {exported.stderr.strip()[-300:]}")
|
||||
base = os.path.realpath(root) + os.sep
|
||||
table = {}
|
||||
for record in json.loads(exported.stdout)["data"][0]["functions"]:
|
||||
files = record.get("filenames") or []
|
||||
if not files:
|
||||
continue
|
||||
path = os.path.realpath(files[0])
|
||||
if path.startswith(base):
|
||||
path = path[len(base):]
|
||||
table[record["name"]] = (path.replace(os.sep, "/"), record["name"].rpartition(":")[2])
|
||||
return table
|
||||
|
||||
|
||||
class Functions:
|
||||
"""Stable ids for (source file, function name), in order of first
|
||||
appearance. Several profile names can be one function: a static inline of a
|
||||
header is recorded once per file that includes it."""
|
||||
|
||||
def __init__(self, table):
|
||||
self.table = table
|
||||
self.ids = {}
|
||||
|
||||
def key(self, name):
|
||||
if name in self.table:
|
||||
return self.table[name]
|
||||
# Not in the coverage mapping: keep what the profile says, the
|
||||
# translation unit for a function with internal linkage.
|
||||
source, _, short = name.rpartition(":")
|
||||
return (source, short)
|
||||
|
||||
def ids_of(self, names):
|
||||
out = set()
|
||||
for name in names:
|
||||
key = self.key(name)
|
||||
if key not in self.ids:
|
||||
self.ids[key] = len(self.ids)
|
||||
out.add(self.ids[key])
|
||||
return sorted(out)
|
||||
|
||||
def rows(self):
|
||||
for (source, short), ident in sorted(self.ids.items(), key=lambda item: item[1]):
|
||||
yield ident, source, short
|
||||
|
||||
|
||||
def reduce_suite(suite, suite_dir, profdata, suite_ok):
|
||||
"""Turn one suite's raw profiles into {test: (status, reason, names)}."""
|
||||
tests = {}
|
||||
markers = {}
|
||||
setup = set()
|
||||
setup_unreadable = False
|
||||
setup_markers = []
|
||||
entries = sorted(os.listdir(suite_dir)) if os.path.isdir(suite_dir) else []
|
||||
for entry in entries:
|
||||
path = os.path.join(suite_dir, entry)
|
||||
marker = MARKER.match(entry)
|
||||
if marker:
|
||||
if marker.group("stem") == "_setup.child":
|
||||
setup_markers.append(marker.group("pid"))
|
||||
else:
|
||||
markers.setdefault(marker.group("stem"), []).append(marker.group("pid"))
|
||||
continue
|
||||
if SETUP.match(entry):
|
||||
names = executed_functions(profdata, path)
|
||||
if names is None:
|
||||
setup_unreadable = True
|
||||
else:
|
||||
setup |= names
|
||||
continue
|
||||
profile = PROFILE.match(entry)
|
||||
if not profile:
|
||||
continue
|
||||
test = tests.setdefault(profile.group("stem"),
|
||||
{"names": set(), "parent": False, "unreadable": []})
|
||||
names = executed_functions(profdata, path)
|
||||
if names is None:
|
||||
test["unreadable"].append(entry)
|
||||
continue
|
||||
test["names"] |= names
|
||||
test["parent"] = test["parent"] or profile.group("who") == "parent"
|
||||
|
||||
result = {}
|
||||
for stem, pids in markers.items():
|
||||
tests.setdefault(stem, {"names": set(), "parent": False, "unreadable": []})
|
||||
for stem, test in sorted(tests.items()):
|
||||
reasons = []
|
||||
if stem in markers:
|
||||
reasons.append("unaccounted forked child: " + ",".join(sorted(markers[stem])))
|
||||
if test["unreadable"]:
|
||||
reasons.append("unreadable profile: " + ",".join(sorted(test["unreadable"])))
|
||||
if not test["parent"]:
|
||||
reasons.append("no parent profile")
|
||||
if not suite_ok:
|
||||
reasons.append("suite did not exit 0")
|
||||
result[stem] = ("incomplete" if reasons else "complete", "; ".join(reasons), test["names"])
|
||||
setup_reasons = []
|
||||
if setup_markers:
|
||||
setup_reasons.append("unaccounted forked child: " + ",".join(sorted(setup_markers)))
|
||||
if setup_unreadable:
|
||||
setup_reasons.append("unreadable setup profile")
|
||||
if not suite_ok:
|
||||
setup_reasons.append("suite did not exit 0")
|
||||
result[SETUP_TEST] = ("incomplete" if setup_reasons else "complete",
|
||||
"; ".join(setup_reasons), setup)
|
||||
return result
|
||||
|
||||
|
||||
def tool_version(tool):
|
||||
proc = subprocess.run([tool, "--version"], capture_output=True, text=True)
|
||||
lines = [line.strip() for line in proc.stdout.splitlines() if line.strip()]
|
||||
return lines[0] if lines else "unknown"
|
||||
|
||||
|
||||
def git_commit(root):
|
||||
proc = subprocess.run(["git", "-C", root, "rev-parse", "HEAD"], capture_output=True, text=True)
|
||||
return proc.stdout.strip() if proc.returncode == 0 else "unknown"
|
||||
|
||||
|
||||
|
||||
def add_v2_arguments(parser):
|
||||
parser.add_argument("--format", type=int, choices=(1, 2), default=1,
|
||||
help="wire format (2 is experimental and unadmitted)")
|
||||
parser.add_argument("--registry", help="format2 image-bound expected registry JSON")
|
||||
for flag, default, help_text in (
|
||||
("max-output-bytes", 32 * 1024 * 1024, "combined stdout/stderr bytes per command"),
|
||||
("max-total-output-bytes", 512 * 1024 * 1024, "aggregate command stdout/stderr bytes"),
|
||||
("max-profile-bytes", 64 * 1024 * 1024, "bytes per raw input profile"),
|
||||
("max-total-profile-bytes", 512 * 1024 * 1024, "aggregate raw and separately converted bytes"),
|
||||
("max-profile-files", 4096, "raw entries including directories, markers and listing profile"),
|
||||
("conversion-timeout", 60, "seconds per tool command"),
|
||||
("reduction-timeout", 600, "seconds per strict parse or final pure reduction"),
|
||||
("max-map-bytes", 64 * 1024 * 1024, "combined producer wire bytes"),
|
||||
):
|
||||
parser.add_argument("--" + flag, type=int, default=default,
|
||||
help="format2: %s (default: %s)" % (help_text, default))
|
||||
parser.epilog = ((parser.epilog or "") +
|
||||
" Format2 always retains bounded raw and diagnostic evidence; "
|
||||
"--keep-raw only changes format1. Format2 supports the observed "
|
||||
"LLVM 22.1.8 frontend text dialect and POSIX process groups.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Build the per-test coverage map.")
|
||||
parser.add_argument("--runner", required=True, help="instrumented test runner")
|
||||
parser.add_argument("--out", required=True, help="output directory")
|
||||
parser.add_argument("--suite", action="append", default=[], help="suite to run (repeatable)")
|
||||
parser.add_argument("--llvm-bin", default=os.environ.get("COV_LLVM_BIN", ""),
|
||||
help="directory holding llvm-profdata and llvm-cov of the compiler that "
|
||||
"built the runner")
|
||||
parser.add_argument("--root", default="",
|
||||
help="repository root the source files are named relative to "
|
||||
"(default: the repository this script is in)")
|
||||
parser.add_argument("--keep-raw", action="store_true", help="keep the raw profiles")
|
||||
parser.add_argument("--timeout", type=int, default=3600, help="seconds per suite")
|
||||
add_v2_arguments(parser)
|
||||
args = parser.parse_args()
|
||||
if args.format == 2:
|
||||
from coverage_live_v2 import collect
|
||||
return collect(args)
|
||||
|
||||
runner = os.path.abspath(args.runner)
|
||||
if not os.access(runner, os.X_OK):
|
||||
fail(f"runner is not executable: {runner}")
|
||||
profdata = os.path.join(args.llvm_bin, "llvm-profdata") if args.llvm_bin else "llvm-profdata"
|
||||
llvm_cov = os.path.join(args.llvm_bin, "llvm-cov") if args.llvm_bin else "llvm-cov"
|
||||
for tool in (profdata, llvm_cov):
|
||||
if not shutil.which(tool):
|
||||
fail(f"not found: {tool}")
|
||||
root = args.root or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
out = os.path.abspath(args.out)
|
||||
raw = os.path.join(out, "raw")
|
||||
os.makedirs(raw, exist_ok=True)
|
||||
|
||||
# Listing the suites is also the cheapest run of the runner: the profile it
|
||||
# leaves is the one llvm-cov needs to print the coverage mapping.
|
||||
listing_profile = os.path.join(out, "listing.profraw")
|
||||
listed = subprocess.run([runner, "--list-suites"], capture_output=True, text=True,
|
||||
env=dict(os.environ, LLVM_PROFILE_FILE=listing_profile))
|
||||
if listed.returncode != 0:
|
||||
fail("the runner could not list its suites")
|
||||
if not os.path.exists(listing_profile):
|
||||
fail("the runner wrote no profile: is it the instrumented build (make test-runner-cov)?")
|
||||
table = source_table(llvm_cov, profdata, runner, listing_profile, root, out)
|
||||
os.remove(listing_profile)
|
||||
suites = args.suite or [line.strip() for line in listed.stdout.splitlines() if line.strip()]
|
||||
if not suites:
|
||||
fail("no suites to run")
|
||||
|
||||
functions = Functions(table)
|
||||
test_rows = []
|
||||
suite_meta = []
|
||||
failed = False
|
||||
env = dict(os.environ)
|
||||
# A plain name: a runner built for continuous profiles would otherwise start
|
||||
# in that mode and ignore every per-test name (tests/test_main.c).
|
||||
env["LLVM_PROFILE_FILE"] = os.devnull
|
||||
env["CBM_TEST_COVERAGE_DIR"] = raw
|
||||
for suite in suites:
|
||||
suite_dir = os.path.join(raw, suite)
|
||||
shutil.rmtree(suite_dir, ignore_errors=True)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
run = subprocess.run([runner, suite], env=env, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=args.timeout)
|
||||
code = run.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code = -1
|
||||
wall = time.monotonic() - started
|
||||
suite_ok = code == 0
|
||||
failed = failed or not suite_ok
|
||||
reduced = reduce_suite(suite, suite_dir, profdata, suite_ok)
|
||||
incomplete = 0
|
||||
for test, (status, reason, names) in sorted(reduced.items()):
|
||||
if test != SETUP_TEST and status != "complete":
|
||||
incomplete += 1
|
||||
ids = " ".join(str(i) for i in functions.ids_of(names))
|
||||
test_rows.append(f"{suite}:{test}\t{status}\t{reason}\t{ids}")
|
||||
suite_meta.append({"suite": suite, "tests": len(reduced) - 1, "incomplete": incomplete,
|
||||
"exit": code, "wall_s": round(wall, 2)})
|
||||
print(f"{suite}: {len(reduced) - 1} tests, {incomplete} incomplete, exit {code}, "
|
||||
f"{wall:.1f} s", flush=True)
|
||||
if not args.keep_raw:
|
||||
shutil.rmtree(suite_dir, ignore_errors=True)
|
||||
if not args.keep_raw:
|
||||
shutil.rmtree(raw, ignore_errors=True)
|
||||
|
||||
with open(os.path.join(out, "functions.tsv"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
for ident, source, short in functions.rows():
|
||||
handle.write(f"{ident}\t{source}\t{short}\n")
|
||||
with open(os.path.join(out, "tests.tsv"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write("\n".join(test_rows) + "\n")
|
||||
meta = {
|
||||
"format": 1,
|
||||
"commit": git_commit(root),
|
||||
"llvm_profdata": tool_version(profdata),
|
||||
"platform": sys.platform,
|
||||
"functions": len(functions.ids),
|
||||
"functions_compiled": len(set(table.values())),
|
||||
"tests": sum(entry["tests"] for entry in suite_meta),
|
||||
"incomplete": sum(entry["incomplete"] for entry in suite_meta),
|
||||
"suites": suite_meta,
|
||||
}
|
||||
with open(os.path.join(out, "meta.json"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump(meta, handle, indent=1)
|
||||
handle.write("\n")
|
||||
print(f"map: {meta['tests']} tests, {meta['incomplete']} incomplete, "
|
||||
f"{meta['functions']} functions -> {out}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""coverage-merge.py — the incremental coverage map of a main-branch push.
|
||||
|
||||
Merges FRESH (coverage-map.py output for the suites the push re-ran, made with
|
||||
the new commit's instrumented runner, so its functions.tsv is the new image's
|
||||
complete function table) into PREVIOUS (the previous published map):
|
||||
|
||||
* a suite FRESH ran keeps FRESH's rows exactly;
|
||||
* every other suite the new runner still lists is carried from PREVIOUS,
|
||||
each row's function ids remapped by (source file, name) onto FRESH's
|
||||
table; a function the new image no longer has drops out of the row, and a
|
||||
row's status (an incomplete row stays incomplete) is never improved;
|
||||
* a PREVIOUS suite the new runner no longer lists is dropped.
|
||||
|
||||
The merged meta.json names --commit and recounts every suite, so the C reader
|
||||
(cbm_coverage_map_metadata_matches) checks it like a full map. When the map was
|
||||
observed is not decided here: the producer carries the oldest observation of
|
||||
the previous bundle forward, so an incremental map ages out unless a full
|
||||
refresh runs (the receipt's 7-day bound).
|
||||
|
||||
Usage: coverage-merge.py --previous DIR --fresh DIR --suites FILE --commit SHA --out DIR
|
||||
Exit: 0 merged · 1 inconsistent inputs · 2 usage.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
SETUP = "*"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
print(f"coverage-merge: {message}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def read_functions(path: str) -> tuple[dict[int, tuple[str, str]], list[str]]:
|
||||
by_id: dict[int, tuple[str, str]] = {}
|
||||
lines = []
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
line = line.rstrip("\n")
|
||||
if not line:
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 3 or not fields[0].isdigit():
|
||||
fail(f"malformed function row in {path}: {line!r}")
|
||||
by_id[int(fields[0])] = (fields[1], fields[2])
|
||||
lines.append(line)
|
||||
return by_id, lines
|
||||
|
||||
|
||||
def read_tests(path: str) -> list[tuple[str, str, str, str, list[int]]]:
|
||||
rows = []
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
line = line.rstrip("\n")
|
||||
if not line:
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 4 or ":" not in fields[0]:
|
||||
fail(f"malformed test row in {path}: {line!r}")
|
||||
suite, test = fields[0].split(":", 1)
|
||||
ids = [int(token) for token in fields[3].split()] if fields[3] else []
|
||||
rows.append((suite, test, fields[1], fields[2], ids))
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
for flag in ("previous", "fresh", "suites", "commit", "out"):
|
||||
parser.add_argument("--" + flag, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
_, fresh_lines = read_functions(os.path.join(args.fresh, "functions.tsv"))
|
||||
fresh_by_id, _ = read_functions(os.path.join(args.fresh, "functions.tsv"))
|
||||
previous_by_id, _ = read_functions(os.path.join(args.previous, "functions.tsv"))
|
||||
fresh_ids = {key: ident for ident, key in fresh_by_id.items()}
|
||||
with open(os.path.join(args.fresh, "meta.json"), encoding="utf-8") as handle:
|
||||
fresh_meta = json.load(handle)
|
||||
with open(os.path.join(args.previous, "meta.json"), encoding="utf-8") as handle:
|
||||
previous_meta = json.load(handle)
|
||||
with open(args.suites, encoding="utf-8") as handle:
|
||||
listed = [line.strip() for line in handle if line.strip()]
|
||||
|
||||
fresh_rows = read_tests(os.path.join(args.fresh, "tests.tsv"))
|
||||
previous_rows = read_tests(os.path.join(args.previous, "tests.tsv"))
|
||||
rerun = {row[0] for row in fresh_rows}
|
||||
if not rerun <= set(listed):
|
||||
fail("a re-run suite is not listed by the new runner")
|
||||
|
||||
merged = [row for row in fresh_rows]
|
||||
for suite, test, status, reason, ids in previous_rows:
|
||||
if suite in rerun or suite not in listed:
|
||||
continue
|
||||
mapped = sorted({fresh_ids[previous_by_id[i]] for i in ids
|
||||
if i in previous_by_id and previous_by_id[i] in fresh_ids})
|
||||
merged.append((suite, test, status, reason, mapped))
|
||||
merged.sort(key=lambda row: (row[0], row[1]))
|
||||
|
||||
suite_meta = {entry["suite"]: entry for entry in previous_meta.get("suites", [])}
|
||||
for entry in fresh_meta.get("suites", []):
|
||||
suite_meta[entry["suite"]] = entry
|
||||
counts: dict[str, list[int]] = {}
|
||||
for suite, test, status, _, _ in merged:
|
||||
tally = counts.setdefault(suite, [0, 0])
|
||||
if test != SETUP:
|
||||
tally[0] += 1
|
||||
tally[1] += status != "complete"
|
||||
suites = []
|
||||
for suite in sorted(counts):
|
||||
entry = dict(suite_meta.get(suite, {"suite": suite, "exit": 0}))
|
||||
entry["tests"], entry["incomplete"] = counts[suite]
|
||||
suites.append(entry)
|
||||
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
with open(os.path.join(args.out, "functions.tsv"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write("".join(line + "\n" for line in fresh_lines))
|
||||
with open(os.path.join(args.out, "tests.tsv"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
for suite, test, status, reason, ids in merged:
|
||||
handle.write(f"{suite}:{test}\t{status}\t{reason}\t{' '.join(map(str, ids))}\n")
|
||||
meta = dict(fresh_meta)
|
||||
meta.update({"commit": args.commit, "tests": sum(c[0] for c in counts.values()),
|
||||
"incomplete": sum(c[1] for c in counts.values()), "suites": suites})
|
||||
with open(os.path.join(args.out, "meta.json"), "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump(meta, handle, indent=1)
|
||||
handle.write("\n")
|
||||
print(f"merged: {len(rerun)} suite(s) re-run, "
|
||||
f"{len(counts) - len(rerun & set(counts))} carried, {len(merged)} rows")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,649 @@
|
||||
"""Experimental, bounded POSIX native transport for unadmitted profile-map v2.
|
||||
|
||||
Only actual observations are transported. Registry/image hashes are custody
|
||||
facts, not evidence that a registry, interval, runtime or image set is complete.
|
||||
No RowEvidence is created here, so this bundle cannot authorize negative
|
||||
coverage evidence or test narrowing. Raw evidence is retained, including on
|
||||
failure. Process groups cannot prove closure of children that leave the group.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import selectors
|
||||
import signal
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from profile_map_v2 import (
|
||||
ExpectedSuite, ObservationRole, ProfileMapError, ProfileMapLimits,
|
||||
ProfileMapLimitExceededError, ProfileMapCancelledError,
|
||||
ProfileObservation, SuiteOutcome, build_profile_map_v2,
|
||||
)
|
||||
from profile_text_parser import (
|
||||
ParseLimits, ProfileTextError, LimitExceededError, ParseCancelledError, parse_profile_text,
|
||||
)
|
||||
|
||||
|
||||
REGISTRY_CAP = 2 * 1024 * 1024
|
||||
ROW_CAP = 65536
|
||||
RECORD_CAP = 4000000
|
||||
COUNTER_CAP = 32000000
|
||||
NAME_CAP = 128 * 1024 * 1024
|
||||
GRACE = 2.0
|
||||
PROFILE = re.compile(r"^(.+?)\.(parent|[0-9]+)\.profraw$")
|
||||
SETUP = re.compile(r"^_setup\.(?:[0-9]+|child\.[0-9]+)\.profraw$")
|
||||
MARKER = re.compile(r"^(.+?)\.([0-9]+)\.forked$")
|
||||
|
||||
|
||||
class CollectionError(ValueError):
|
||||
def __init__(self, kind, message):
|
||||
super().__init__(message)
|
||||
self.kind = kind
|
||||
|
||||
|
||||
def _error(kind, message):
|
||||
raise CollectionError(kind, message)
|
||||
|
||||
|
||||
def _directory(path):
|
||||
"""Open each absolute component without following symlinks; caller closes."""
|
||||
path = os.path.abspath(path)
|
||||
fd = os.open(os.sep, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
for part in path.split(os.sep):
|
||||
if not part:
|
||||
continue
|
||||
next_fd = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
dir_fd=fd)
|
||||
os.close(fd)
|
||||
fd = next_fd
|
||||
return fd
|
||||
except BaseException:
|
||||
os.close(fd)
|
||||
raise
|
||||
|
||||
|
||||
def _regular(path):
|
||||
parent, name = os.path.split(os.path.abspath(path))
|
||||
directory = _directory(parent)
|
||||
try:
|
||||
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK,
|
||||
dir_fd=directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
||||
os.close(fd)
|
||||
_error("input", "input is not a regular file")
|
||||
return fd
|
||||
|
||||
|
||||
def _signature(info):
|
||||
return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
|
||||
|
||||
|
||||
def _read(path, limit):
|
||||
fd = _regular(path)
|
||||
try:
|
||||
before = os.fstat(fd)
|
||||
if before.st_size > limit:
|
||||
_error("resource_limit", "file byte limit exceeded")
|
||||
data = bytearray()
|
||||
while True:
|
||||
part = os.read(fd, min(65536, limit + 1 - len(data)))
|
||||
if not part:
|
||||
break
|
||||
data.extend(part)
|
||||
if len(data) > limit:
|
||||
_error("resource_limit", "file byte limit exceeded")
|
||||
if _signature(before) != _signature(os.fstat(fd)):
|
||||
_error("identity", "input changed during capture")
|
||||
return bytes(data)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def _identity(path):
|
||||
fd = _regular(path)
|
||||
try:
|
||||
before = os.fstat(fd)
|
||||
if before.st_size > 1024 * 1024 * 1024:
|
||||
_error("resource_limit", "executable exceeds 1GiB identity limit")
|
||||
digest = hashlib.sha256()
|
||||
size = 0
|
||||
while True:
|
||||
part = os.read(fd, 65536)
|
||||
if not part:
|
||||
break
|
||||
size += len(part)
|
||||
if size > 1024 * 1024 * 1024:
|
||||
_error("resource_limit", "executable grew beyond identity limit")
|
||||
digest.update(part)
|
||||
if _signature(before) != _signature(os.fstat(fd)) or size != before.st_size:
|
||||
_error("identity", "executable changed while hashing")
|
||||
if not os.access(path, os.X_OK):
|
||||
_error("input", "executable is not executable")
|
||||
return {"path": path, "sha256": digest.hexdigest(), "bytes": size}
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def _pairs(values):
|
||||
result = {}
|
||||
for key, value in values:
|
||||
if key in result:
|
||||
_error("input", "duplicate registry key")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _identifier(value, suite=False):
|
||||
if type(value) is not str or not value:
|
||||
_error("input", "registry identifier must be a nonempty string")
|
||||
try:
|
||||
data = value.encode("utf-8", "strict")
|
||||
except UnicodeError:
|
||||
_error("input", "registry identifier is not valid UTF-8")
|
||||
if any(char in data for char in (0, 9, 10, 13)) or (suite and b":" in data):
|
||||
_error("input", "registry identifier violates producer grammar")
|
||||
if not suite and value == "*":
|
||||
_error("input", "setup test name is reserved")
|
||||
# Native routing uses each identifier as one filesystem component. The
|
||||
# pure producer accepts a wider byte grammar; those routes are unsupported.
|
||||
if value in (".", "..") or "/" in value or "\\" in value:
|
||||
_error("unsupported", "registry identifier cannot be a native path component")
|
||||
if not suite and value in ("_setup", "_setup.child"):
|
||||
_error("unsupported", "registry test collides with native setup routing")
|
||||
return data
|
||||
|
||||
|
||||
def _registry(path, image):
|
||||
raw = _read(path, REGISTRY_CAP)
|
||||
try:
|
||||
value = json.loads(raw.decode("utf-8", "strict"), object_pairs_hook=_pairs,
|
||||
parse_constant=lambda _: _error("input", "nonfinite JSON number"))
|
||||
except (UnicodeError, json.JSONDecodeError, RecursionError):
|
||||
_error("input", "invalid registry JSON")
|
||||
if type(value) is not dict or set(value) != {"format", "image_sha256", "suites"}:
|
||||
_error("input", "registry keys do not match v0 schema")
|
||||
if value["format"] != "cbm.coverage.registry.v0":
|
||||
_error("input", "unsupported registry format")
|
||||
declared = value["image_sha256"]
|
||||
if type(declared) is not str or not re.fullmatch(r"[0-9a-f]{64}", declared):
|
||||
_error("input", "registry image digest must be lowercase SHA256")
|
||||
if declared != image["sha256"]:
|
||||
_error("identity", "registry does not match runner image")
|
||||
suites = value["suites"]
|
||||
if type(suites) is not list or not suites:
|
||||
_error("input", "registry suites must be nonempty")
|
||||
result = {}
|
||||
rows = 0
|
||||
for item in suites:
|
||||
if type(item) is not dict or set(item) != {"name", "tests"}:
|
||||
_error("input", "registry suite keys do not match schema")
|
||||
name = item["name"]
|
||||
_identifier(name, suite=True)
|
||||
if name in result:
|
||||
_error("input", "duplicate registry suite")
|
||||
tests = item["tests"]
|
||||
if type(tests) is not list or not tests:
|
||||
_error("input", "registry tests must be nonempty")
|
||||
seen = set()
|
||||
for test in tests:
|
||||
_identifier(test)
|
||||
if test in seen:
|
||||
_error("input", "duplicate registry test")
|
||||
seen.add(test)
|
||||
rows += 1 + len(tests)
|
||||
if rows > ROW_CAP:
|
||||
_error("resource_limit", "registry row limit exceeded")
|
||||
result[name] = tuple(tests)
|
||||
return result, hashlib.sha256(raw).hexdigest(), raw
|
||||
|
||||
|
||||
class Evidence:
|
||||
def __init__(self, path, args):
|
||||
self.path = os.path.abspath(path)
|
||||
self.args = args
|
||||
self.records = []
|
||||
self.uncertainty = []
|
||||
self.converted_bytes = 0
|
||||
self.captured_bytes = 0
|
||||
self.allowed_suites = set()
|
||||
self.directory = None
|
||||
parent, name = os.path.split(self.path)
|
||||
if not name:
|
||||
_error("input", "output must name a fresh directory")
|
||||
fd = _directory(parent)
|
||||
try:
|
||||
os.mkdir(name, 0o700, dir_fd=fd)
|
||||
self.directory = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
dir_fd=fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
try:
|
||||
self.directory_identity = os.fstat(self.directory)
|
||||
for name in ("evidence", "raw"):
|
||||
os.mkdir(name, 0o700, dir_fd=self.directory)
|
||||
except BaseException:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
if self.directory is not None:
|
||||
os.close(self.directory)
|
||||
self.directory = None
|
||||
|
||||
def check(self):
|
||||
fd = _directory(self.path)
|
||||
try:
|
||||
actual = os.fstat(fd)
|
||||
if (actual.st_dev, actual.st_ino) != (self.directory_identity.st_dev,
|
||||
self.directory_identity.st_ino):
|
||||
_error("identity", "owned output directory was replaced")
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
def write(self, relative, data):
|
||||
self.check()
|
||||
parent, name = os.path.split(relative)
|
||||
fd = _directory(os.path.join(self.path, parent))
|
||||
try:
|
||||
target = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
||||
0o600, dir_fd=fd)
|
||||
try:
|
||||
with os.fdopen(target, "wb") as stream:
|
||||
stream.write(data)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
except BaseException:
|
||||
# Partial evidence remains; no marker is created on this path.
|
||||
raise
|
||||
finally:
|
||||
os.close(fd)
|
||||
return os.path.join(self.path, relative)
|
||||
|
||||
def json(self, relative, value):
|
||||
data = (json.dumps(value, sort_keys=True, indent=2,
|
||||
ensure_ascii=True) + "\n").encode("ascii")
|
||||
if len(data) > 64 * 1024 * 1024:
|
||||
_error("resource_limit", "metadata exceeds 64MiB bound")
|
||||
return self.write(relative, data)
|
||||
|
||||
def publish(self, meta):
|
||||
# A partial JSON file is never a commit marker. The pending complete
|
||||
# file remains evidence, and hard-link creation cannot overwrite a
|
||||
# preexisting or symlink destination.
|
||||
self.json("meta.pending.json", meta)
|
||||
for name in ("profiles", "tests"):
|
||||
data = _read(os.path.join(self.path, name + ".tsv"), self.args.max_map_bytes)
|
||||
if hashlib.sha256(data).hexdigest() != meta[name + "_sha256"]:
|
||||
_error("identity", "wire changed before publication")
|
||||
self.check()
|
||||
os.fsync(self.directory)
|
||||
os.link("meta.pending.json", "meta.json", src_dir_fd=self.directory,
|
||||
dst_dir_fd=self.directory, follow_symlinks=False)
|
||||
|
||||
def inventory(self):
|
||||
"""Bound scans while children run; never follow raw tree symlinks."""
|
||||
self.check()
|
||||
root = os.path.join(self.path, "raw")
|
||||
pending = [root]
|
||||
files, total, count = [], 0, 1 # listing counts as one profile slot
|
||||
try:
|
||||
info = os.stat("listing.profraw", dir_fd=self.directory, follow_symlinks=False)
|
||||
except FileNotFoundError:
|
||||
info = None
|
||||
if info is not None:
|
||||
if not stat.S_ISREG(info.st_mode):
|
||||
_error("input", "listing profile is not a regular file")
|
||||
if info.st_size > self.args.max_profile_bytes:
|
||||
_error("resource_limit", "listing profile byte limit exceeded")
|
||||
total = info.st_size
|
||||
if total > self.args.max_total_profile_bytes:
|
||||
_error("resource_limit", "aggregate raw byte limit exceeded")
|
||||
while pending:
|
||||
directory = pending.pop()
|
||||
fd = _directory(directory)
|
||||
try:
|
||||
with os.scandir(fd) as entries:
|
||||
for entry in entries:
|
||||
count += 1
|
||||
if count > self.args.max_profile_files:
|
||||
_error("resource_limit", "raw entry count limit exceeded")
|
||||
info = entry.stat(follow_symlinks=False)
|
||||
path = os.path.join(directory, entry.name)
|
||||
if stat.S_ISDIR(info.st_mode) and directory == root:
|
||||
if entry.name not in self.allowed_suites:
|
||||
self.uncertainty.append({"path": path, "reason": "unknown_suite_destination"})
|
||||
_error("input", "raw suite directory is outside requested scope")
|
||||
pending.append(path)
|
||||
elif stat.S_ISREG(info.st_mode):
|
||||
if info.st_size > self.args.max_profile_bytes:
|
||||
_error("resource_limit", "raw file byte limit exceeded")
|
||||
total += info.st_size
|
||||
if total > self.args.max_total_profile_bytes:
|
||||
_error("resource_limit", "aggregate raw byte limit exceeded")
|
||||
files.append(path)
|
||||
else:
|
||||
self.uncertainty.append({"path": path, "reason": "unsupported_raw_entry"})
|
||||
_error("input", "unsupported raw tree entry")
|
||||
finally:
|
||||
os.close(fd)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def _group_exists(pid):
|
||||
try:
|
||||
os.killpg(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
|
||||
|
||||
def _quiesce(proc):
|
||||
sent = []
|
||||
for sig in (signal.SIGTERM, signal.SIGKILL):
|
||||
if proc.poll() is not None and not _group_exists(proc.pid):
|
||||
return True, sent
|
||||
try:
|
||||
os.killpg(proc.pid, sig)
|
||||
sent.append(signal.Signals(sig).name)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
until = time.monotonic() + GRACE
|
||||
while time.monotonic() < until:
|
||||
if proc.poll() is not None and not _group_exists(proc.pid):
|
||||
return True, sent
|
||||
time.sleep(0.02)
|
||||
return proc.poll() is not None and not _group_exists(proc.pid), sent
|
||||
|
||||
|
||||
def _run(evidence, label, argv, environment, timeout, cwd, watch_raw=False):
|
||||
selector = selectors.DefaultSelector()
|
||||
streams = {"stdout": bytearray(), "stderr": bytearray()}
|
||||
started = time.monotonic()
|
||||
proc = None
|
||||
failure = None
|
||||
quiescent, cleanup = False, []
|
||||
try:
|
||||
proc = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, env=environment, cwd=cwd,
|
||||
start_new_session=True, bufsize=0)
|
||||
for name, pipe in (("stdout", proc.stdout), ("stderr", proc.stderr)):
|
||||
os.set_blocking(pipe.fileno(), False)
|
||||
selector.register(pipe, selectors.EVENT_READ, name)
|
||||
while True:
|
||||
if watch_raw:
|
||||
evidence.inventory()
|
||||
left = timeout - (time.monotonic() - started)
|
||||
if left <= 0:
|
||||
failure = "timeout"
|
||||
break
|
||||
for key, _ in selector.select(min(left, 0.05)):
|
||||
part = os.read(key.fd, 65536)
|
||||
if not part:
|
||||
selector.unregister(key.fileobj)
|
||||
continue
|
||||
used = sum(len(data) for data in streams.values())
|
||||
allowed = min(evidence.args.max_output_bytes - used,
|
||||
evidence.args.max_total_output_bytes - evidence.captured_bytes - used)
|
||||
streams[key.data].extend(part[:max(0, allowed)])
|
||||
if len(part) > allowed:
|
||||
failure = "resource_limit"
|
||||
break
|
||||
if failure:
|
||||
break
|
||||
if proc.poll() is not None:
|
||||
if _group_exists(proc.pid):
|
||||
failure = "quiescence"
|
||||
break
|
||||
if not selector.get_map():
|
||||
break
|
||||
except CollectionError as error:
|
||||
failure = error.kind
|
||||
except (OSError, ValueError):
|
||||
failure = "process"
|
||||
finally:
|
||||
if proc is not None:
|
||||
quiescent, cleanup = _quiesce(proc)
|
||||
if not quiescent:
|
||||
failure = "quiescence"
|
||||
if cleanup and failure is None:
|
||||
failure = "quiescence"
|
||||
proc.stdout.close()
|
||||
proc.stderr.close()
|
||||
selector.close()
|
||||
record = {"label": label, "argv": argv, "returncode": proc.returncode if proc else None,
|
||||
"failure": failure, "quiescent": quiescent, "cleanup": cleanup,
|
||||
"wall_s": round(time.monotonic() - started, 6), "timeout_s": timeout}
|
||||
evidence.captured_bytes += sum(len(data) for data in streams.values())
|
||||
for name, data in streams.items():
|
||||
path = evidence.write("evidence/" + label + "." + name, bytes(data))
|
||||
record[name] = {"path": path, "bytes": len(data),
|
||||
"sha256": hashlib.sha256(data).hexdigest(),
|
||||
"capture": "complete" if failure is None else "bounded_prefix"}
|
||||
evidence.json("evidence/" + label + ".process.json", record)
|
||||
evidence.records.append(record)
|
||||
return record, bytes(streams["stdout"])
|
||||
|
||||
|
||||
def _required(record):
|
||||
if record["failure"] or record["returncode"] != 0 or not record["quiescent"]:
|
||||
_error(record["failure"] or "tool", "required native command failed: " + record["label"])
|
||||
|
||||
|
||||
def _environment():
|
||||
result = dict(os.environ)
|
||||
for name in ("CBM_TEST_COVERAGE_DIR", "CBM_TEST_ONLY", "CBM_TEST_ONLY_FILE"):
|
||||
result.pop(name, None)
|
||||
return result
|
||||
|
||||
|
||||
def _convert(evidence, tool, path, label, environment, root, unavailable=False):
|
||||
try:
|
||||
raw = _read(path, evidence.args.max_profile_bytes)
|
||||
except OSError as error:
|
||||
if unavailable and error.errno in (errno.EACCES, errno.EPERM, errno.ENOENT):
|
||||
evidence.uncertainty.append({"path": path, "reason": "profile_unreadable"})
|
||||
return None
|
||||
raise
|
||||
snapshot = evidence.write("evidence/" + label + ".profraw", raw)
|
||||
raw_digest = hashlib.sha256(raw).hexdigest()
|
||||
record, text = _run(evidence, label, [tool, "merge", "--instr", "--text", "--failure-mode=any",
|
||||
"--num-threads=1", "--output=-", snapshot],
|
||||
environment, evidence.args.conversion_timeout, root)
|
||||
record["input"] = {"path": path, "snapshot": snapshot, "sha256": raw_digest,
|
||||
"bytes": len(raw)}
|
||||
_required(record)
|
||||
if hashlib.sha256(_read(snapshot, evidence.args.max_profile_bytes)).hexdigest() != raw_digest:
|
||||
_error("identity", "conversion input changed")
|
||||
evidence.converted_bytes += len(text)
|
||||
if evidence.converted_bytes > evidence.args.max_total_profile_bytes:
|
||||
_error("resource_limit", "aggregate converted byte limit exceeded")
|
||||
# Reject malformed transport, even though the pure producer can represent
|
||||
# unavailable/malformed observations conservatively. Unknown identities and
|
||||
# counter-shape incompatibilities are rejected by the producer's full join.
|
||||
deadline = time.monotonic() + evidence.args.reduction_timeout
|
||||
parse_profile_text(text, ParseLimits(evidence.args.max_output_bytes, RECORD_CAP,
|
||||
COUNTER_CAP, NAME_CAP),
|
||||
cancel=lambda _: time.monotonic() >= deadline)
|
||||
return text
|
||||
|
||||
|
||||
def _scope(registry, requested, available):
|
||||
selected = requested or list(registry)
|
||||
if len(set(selected)) != len(selected):
|
||||
_error("input", "duplicate requested suite")
|
||||
for name in selected:
|
||||
if name not in registry or name not in available:
|
||||
_error("input", "requested suite is absent from registry or native list")
|
||||
return selected
|
||||
|
||||
|
||||
def _observations(evidence, tool, registry, selected, environment, root):
|
||||
observations = []
|
||||
for index, path in enumerate(evidence.inventory()):
|
||||
relative = os.path.relpath(path, os.path.join(evidence.path, "raw"))
|
||||
parts = relative.split(os.sep)
|
||||
if len(parts) != 2 or parts[0] not in selected:
|
||||
evidence.uncertainty.append({"path": path, "reason": "unknown_profile_destination"})
|
||||
_error("input", "raw profile destination is outside requested scope")
|
||||
suite, name = parts
|
||||
marker = MARKER.fullmatch(name)
|
||||
setup = SETUP.fullmatch(name)
|
||||
profile = PROFILE.fullmatch(name)
|
||||
if setup:
|
||||
test = "*"
|
||||
role = ObservationRole.CHILD if name.startswith("_setup.child.") else ObservationRole.SETUP
|
||||
elif marker:
|
||||
test = "*" if marker.group(1) == "_setup.child" else marker.group(1)
|
||||
role = ObservationRole.CHILD
|
||||
elif profile:
|
||||
test = profile.group(1)
|
||||
role = ObservationRole.PARENT if profile.group(2) == "parent" else ObservationRole.CHILD
|
||||
else:
|
||||
evidence.uncertainty.append({"path": path, "reason": "unaccounted_raw_entry"})
|
||||
_error("input", "unaccounted raw profile entry")
|
||||
if test != "*" and test not in registry[suite]:
|
||||
evidence.uncertainty.append({"path": path, "reason": "unknown_test_identity"})
|
||||
_error("input", "raw test identifier is outside expected registry")
|
||||
if marker:
|
||||
evidence.uncertainty.append({"path": path, "reason": "unaccounted_fork"})
|
||||
text = None
|
||||
else:
|
||||
text = _convert(evidence, tool, path, "profile-%06d" % index,
|
||||
environment, root, unavailable=True)
|
||||
observations.append(ProfileObservation(suite.encode("utf-8"), test.encode("utf-8"),
|
||||
name.encode("utf-8"), role, text))
|
||||
return tuple(observations)
|
||||
|
||||
|
||||
def collect(args):
|
||||
evidence = None
|
||||
try:
|
||||
if os.name != "posix" or not all(hasattr(os, name) for name in
|
||||
("O_NOFOLLOW", "O_DIRECTORY", "killpg", "set_blocking")):
|
||||
_error("unsupported", "format2 requires POSIX owned process-group custody")
|
||||
for name in ("timeout", "max_output_bytes", "max_total_output_bytes", "max_profile_bytes", "max_total_profile_bytes",
|
||||
"max_profile_files", "conversion_timeout", "reduction_timeout", "max_map_bytes"):
|
||||
value = getattr(args, name)
|
||||
if type(value) is not int or value <= 0 or value > sys.maxsize // 2:
|
||||
_error("input", "resource limits must be positive bounded integers")
|
||||
if not args.registry or not args.llvm_bin:
|
||||
_error("input", "format2 requires --registry and explicit --llvm-bin")
|
||||
runner = os.path.realpath(os.path.abspath(args.runner))
|
||||
tool = os.path.realpath(os.path.join(os.path.abspath(args.llvm_bin), "llvm-profdata"))
|
||||
root = os.path.abspath(args.root or os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
root_fd = _directory(root)
|
||||
os.close(root_fd)
|
||||
image = _identity(runner)
|
||||
tool_identity = _identity(tool)
|
||||
registry_path = os.path.abspath(args.registry)
|
||||
registry, registry_digest, registry_bytes = _registry(registry_path, image)
|
||||
# Reject explicit scope errors before any runner/tool action.
|
||||
_scope(registry, args.suite, set(registry))
|
||||
evidence = Evidence(args.out, args)
|
||||
registry_snapshot = evidence.write("evidence/registry.json", registry_bytes)
|
||||
environment = _environment()
|
||||
environment["LLVM_PROFILE_FILE"] = os.devnull
|
||||
version_record, version = _run(evidence, "tool-version", [tool, "--version"], environment,
|
||||
args.conversion_timeout, root)
|
||||
_required(version_record)
|
||||
if not re.search(rb"LLVM version 22\.1\.8(?:\s|$)", version):
|
||||
_error("unsupported", "only observed LLVM 22.1.8 proftext transport is supported")
|
||||
listing_path = os.path.join(evidence.path, "listing.profraw")
|
||||
listing_env = dict(environment, LLVM_PROFILE_FILE=listing_path)
|
||||
listed, listing = _run(evidence, "list-suites", [runner, "--list-suites"], listing_env,
|
||||
args.timeout, root, watch_raw=True)
|
||||
_required(listed)
|
||||
try:
|
||||
available = listing.decode("utf-8", "strict").splitlines()
|
||||
except UnicodeError:
|
||||
_error("input", "native suite list is not UTF-8")
|
||||
if not available or any(not name for name in available) or len(set(available)) != len(available):
|
||||
_error("input", "native suite list is empty or ambiguous")
|
||||
for name in available:
|
||||
_identifier(name, suite=True)
|
||||
selected = _scope(registry, args.suite, set(available))
|
||||
evidence.allowed_suites = set(selected)
|
||||
enumeration = _convert(evidence, tool, listing_path, "enumeration", environment, root)
|
||||
evidence.inventory()
|
||||
suite_env = dict(environment, CBM_TEST_COVERAGE_DIR=os.path.join(evidence.path, "raw"))
|
||||
outcomes = []
|
||||
expected = []
|
||||
failed = False
|
||||
for index, suite in enumerate(selected):
|
||||
record, _ = _run(evidence, "suite-%06d" % index, [runner, suite], suite_env,
|
||||
args.timeout, root, watch_raw=True)
|
||||
if record["failure"] not in (None, "timeout") or not record["quiescent"]:
|
||||
_error(record["failure"] or "quiescence", "native suite capture failed")
|
||||
outcome = (SuiteOutcome.TIMED_OUT if record["failure"] == "timeout" else
|
||||
SuiteOutcome.PASSED if record["returncode"] == 0 else SuiteOutcome.FAILED)
|
||||
failed = failed or outcome is not SuiteOutcome.PASSED
|
||||
expected.append(ExpectedSuite(suite.encode("utf-8"),
|
||||
tuple(test.encode("utf-8") for test in registry[suite]), outcome))
|
||||
outcomes.append({"suite": suite, "tests": list(registry[suite]),
|
||||
"outcome": outcome.value, "exit": record["returncode"], "process": record})
|
||||
observations = _observations(evidence, tool, registry, selected, environment, root)
|
||||
if _identity(runner) != image or _identity(tool) != tool_identity:
|
||||
_error("identity", "runner or conversion tool changed during collection")
|
||||
# No affirmative runtime/interval/image/child assertions are supplied.
|
||||
limits = ProfileMapLimits(args.max_total_profile_bytes + REGISTRY_CAP * 2, ROW_CAP,
|
||||
args.max_profile_files, RECORD_CAP, RECORD_CAP, COUNTER_CAP,
|
||||
NAME_CAP, COUNTER_CAP, args.max_map_bytes)
|
||||
deadline = time.monotonic() + args.reduction_timeout
|
||||
produced = build_profile_map_v2(bytes.fromhex(image["sha256"]), enumeration,
|
||||
tuple(expected), observations, (), limits,
|
||||
cancel=lambda _: time.monotonic() >= deadline)
|
||||
profiles_path = evidence.write("profiles.tsv", produced.profiles_tsv)
|
||||
tests_path = evidence.write("tests.tsv", produced.tests_tsv)
|
||||
meta = {"format": 2, "admission": "unverified", "image_sha256": image["sha256"],
|
||||
"registry_sha256": registry_digest, "profiles_sha256": produced.profiles_sha256.hex(),
|
||||
"tests_sha256": produced.tests_sha256.hex(), "registry": {
|
||||
"path": registry_path, "snapshot": registry_snapshot, "sha256": registry_digest,
|
||||
"format": "cbm.coverage.registry.v0", "completeness": "unverified"},
|
||||
"scope": selected,
|
||||
"image": image, "llvm_profdata": dict(tool_identity, version=version.decode("utf-8", "replace")),
|
||||
"profiles": {"path": profiles_path, "sha256": produced.profiles_sha256.hex()},
|
||||
"tests": {"path": tests_path, "sha256": produced.tests_sha256.hex()},
|
||||
"suites": outcomes, "processes": evidence.records, "uncertainty": evidence.uncertainty,
|
||||
"evidence_root": os.path.join(evidence.path, "evidence"),
|
||||
"raw_root": os.path.join(evidence.path, "raw"),
|
||||
"row_evidence": "absent", "platform": sys.platform,
|
||||
"raw_retained": True, "captured_bytes": evidence.captured_bytes,
|
||||
"converted_bytes": evidence.converted_bytes,
|
||||
"custody": {"method": "POSIX session and process group",
|
||||
"descendant_closure": "unproved",
|
||||
"compiler_runtime_compatibility": "unverified"},
|
||||
"limits": {name: getattr(args, name) for name in (
|
||||
"timeout", "max_output_bytes", "max_total_output_bytes", "max_profile_bytes",
|
||||
"max_total_profile_bytes", "max_profile_files", "conversion_timeout",
|
||||
"reduction_timeout", "max_map_bytes")},
|
||||
"selection_environment": {"CBM_SKIP_PERF": environment.get("CBM_SKIP_PERF")}}
|
||||
# Consumer acceptance requires this final marker AND both wire digests.
|
||||
evidence.publish(meta)
|
||||
return 1 if failed else 0
|
||||
except (CollectionError, ProfileMapError, ProfileTextError, OSError, UnicodeError,
|
||||
ValueError, MemoryError) as error:
|
||||
kind = getattr(error, "kind", "input_or_conversion")
|
||||
if isinstance(error, (ProfileMapLimitExceededError, LimitExceededError, MemoryError)):
|
||||
kind = "resource_limit"
|
||||
if isinstance(error, (ProfileMapCancelledError, ParseCancelledError)):
|
||||
kind = "reduction_timeout"
|
||||
print("coverage-map: format2 %s: %s" % (kind, str(error)), file=sys.stderr)
|
||||
if evidence is not None:
|
||||
try:
|
||||
evidence.json("failure.json", {"format": 2, "error": kind,
|
||||
"message": str(error), "processes": evidence.records,
|
||||
"uncertainty": evidence.uncertainty})
|
||||
except (OSError, ValueError, MemoryError):
|
||||
pass
|
||||
return 2
|
||||
finally:
|
||||
if evidence is not None:
|
||||
evidence.close()
|
||||
@@ -0,0 +1,326 @@
|
||||
# Distinct changed-file sets of the September 2026 PR pushes (git diff
|
||||
# --name-only merge-base..head per push), referenced by 2026-09-pushes.tsv.
|
||||
# Columns (TAB): fileset, paths (space-separated; no path contains a space).
|
||||
f001 README.md
|
||||
f002 internal/cbm/vendored/grammars/swift/scanner.c scripts/vendored-checksums.txt tests/test_extraction.c
|
||||
f003 internal/cbm/lsp/go_lsp.c src/pipeline/pass_lsp_cross.c tests/test_go_lsp.c tests/test_parallel.c
|
||||
f004 src/foundation/mem.c src/foundation/mem.h tests/test_mem.c
|
||||
f005 Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h src/mcp/mcp.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline_internal.h src/store/store.c tests/test_index_resilience.c tests/test_mcp.c tests/test_parse_coverage.c tests/test_pipeline.c
|
||||
f006 .github/workflows/pr.yml Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h scripts/ci/README.md scripts/ci/coverage-gate-allowlist.txt scripts/ci/parse-partial-baseline.txt scripts/ci/self-index-coverage-gate.sh src/mcp/mcp.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline_internal.h src/store/store.c tests/test_index_resilience.c tests/test_mcp.c tests/test_parse_coverage.c tests/test_pipeline.c tests/test_venue_parity_contract.sh
|
||||
f007 README.md scripts/smoke-test.sh src/cli/cli.c tests/test_cli.c
|
||||
f008 src/ui/http_server.c tests/test_httpd.c
|
||||
f009 src/daemon/application.c src/daemon/application.h src/daemon/frontend.c src/daemon/frontend.h src/daemon/runtime.c src/daemon/runtime.h src/main.c tests/test_daemon_frontend.c tests/test_daemon_runtime.c
|
||||
f010 internal/cbm/lsp/c_lsp.c internal/cbm/lsp/type_registry.c internal/cbm/lsp/type_registry.h tests/test_c_lsp.c
|
||||
f011 src/pipeline/fqn.c src/pipeline/pass_pkgmap.c tests/test_edge_imports.c tests/test_fqn.c
|
||||
f012 internal/cbm/extract_calls.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f013 src/mcp/mcp.c tests/test_mcp.c
|
||||
f014 src/cypher/cypher.c tests/test_cypher.c
|
||||
f015 src/mcp/mcp.c src/store/store.c src/store/store.h src/ui/layout3d.c src/ui/layout3d.h tests/test_store_arch.c tests/test_ui.c
|
||||
f016 Makefile.cbm README.md docs/index.html docs/llms.txt scripts/smoke-invariants.sh src/cli/cli.c src/cli/cli.h src/foundation/platform.c src/foundation/platform.h src/mcp/mcp.c src/personal_memory/memory.c src/personal_memory/memory.h tests/test_cli.c tests/test_mcp.c
|
||||
f017 internal/cbm/extract_defs.c tests/test_extraction.c
|
||||
f018 internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/swift/scanner.c scripts/vendored-checksums.txt tests/test_extraction.c
|
||||
f019 internal/cbm/extract_calls.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/swift/scanner.c scripts/vendored-checksums.txt tests/test_extraction.c tests/test_pipeline.c
|
||||
f020 src/daemon/application.c tests/test_daemon_application.c tests/windows/test_non_ascii_path.py
|
||||
f021 internal/cbm/lsp/ts_lsp.c tests/repro/repro_lsp_ts.c
|
||||
f022 src/graph_buffer/graph_buffer.c src/mcp/mcp.c tests/test_graph_buffer.c tests/test_mcp.c
|
||||
f023 internal/cbm/cbm.h internal/cbm/extract_calls.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f024 .github/workflows/pr.yml Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h scripts/ci/README.md scripts/ci/coverage-gate-allowlist.txt scripts/ci/parse-partial-baseline.txt scripts/ci/self-index-coverage-gate.sh scripts/test.sh src/mcp/mcp.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline_internal.h src/store/store.c tests/test_coverage_gate_contract.sh tests/test_index_resilience.c tests/test_mcp.c tests/test_parse_coverage.c tests/test_pipeline.c tests/test_venue_parity_contract.sh
|
||||
f025 Makefile.cbm README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/cli/cli.h src/daemon/application.c src/discover/discover.c src/discover/discover.h src/foundation/index_policy.c src/foundation/index_policy.h src/mcp/mcp.c src/mcp/mcp_internal.h src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_daemon_application.c tests/test_discover.c tests/test_index_policy.c tests/test_main.c tests/test_pipeline.c tests/test_worker_error_response.sh tests/test_worker_watchdog.sh
|
||||
f026 Makefile.cbm src/cli/cli.c src/pipeline/pass_doclinks.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_doclinks.c tests/test_edge_structural.c tests/test_lang_contract.c tests/test_main.c
|
||||
f027 src/mcp/mcp.c src/mcp/mcp_internal.h tests/test_mcp.c
|
||||
f028 internal/cbm/extract_defs.c src/discover/language.c src/pipeline/pass_route_nodes.c tests/test_extraction.c tests/test_language.c
|
||||
f029 README.md docs/CONFIGURATION.md scripts/smoke-test.sh src/cli/cli.c src/cli/cli.h src/main.c tests/test_cli.c
|
||||
f030 src/main.c tests/test_parent_watchdog.sh
|
||||
f031 internal/cbm/extract_imports.c tests/test_extraction.c
|
||||
f032 internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_unified.c internal/cbm/helpers.h src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f033 internal/cbm/extract_calls.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f034 internal/cbm/cbm.c internal/cbm/preprocessor.cpp internal/cbm/preprocessor.h tests/test_extraction.c
|
||||
f035 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_unified.c internal/cbm/helpers.c internal/cbm/lsp/type_registry.c src/pipeline/lsp_resolve.h src/pipeline/registry.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f036 internal/cbm/cbm.h internal/cbm/extract_semantic.c internal/cbm/extract_usages.c src/pipeline/pass_parallel.c src/pipeline/pass_usages.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_pipeline.c tests/test_registry.c
|
||||
f037 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_defs.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f038 internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_unified.c internal/cbm/extract_unified.h src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f039 src/cli/cli.c src/ui/http_server.c tests/test_cli.c
|
||||
f040 internal/cbm/extract_channels.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f041 internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_unified.c internal/cbm/helpers.h internal/cbm/lsp/go_lsp.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_parallel.c tests/test_pipeline.c tests/test_registry.c
|
||||
f042 internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_unified.c internal/cbm/helpers.c internal/cbm/helpers.h internal/cbm/lsp/go_lsp.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_extraction.c tests/test_parallel.c tests/test_pipeline.c tests/test_registry.c
|
||||
f043 internal/cbm/extract_defs.c tests/test_edge_types_probe.c
|
||||
f044 internal/cbm/extract_calls.c tests/test_extraction.c
|
||||
f045 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_channels.c internal/cbm/extract_defs.c internal/cbm/extract_env_accesses.c internal/cbm/extract_imports.c internal/cbm/extract_node_stack.h internal/cbm/extract_semantic.c internal/cbm/extract_type_assigns.c internal/cbm/extract_type_refs.c tests/test_extraction.c tests/test_mem.c
|
||||
f046 src/cli/client_adapter.c tests/test_agent_clients.c
|
||||
f047 graph-ui/src/hooks/useProjects.test.tsx graph-ui/src/hooks/useProjects.ts
|
||||
f048 internal/cbm/cbm.c tests/test_parse_coverage.c
|
||||
f049 graph-ui/src/App.test.tsx graph-ui/src/App.tsx src/ui/http_server.c tests/test_httpd.c
|
||||
f050 src/daemon/application.c src/foundation/workspace.c src/foundation/workspace.h src/mcp/mcp.c src/ui/http_server.c tests/test_workspace.c
|
||||
f051 README.md docs/CONFIGURATION.md src/cli/cli.c src/cli/cli.h src/daemon/ipc.c src/foundation/compat_fs.c src/foundation/compat_fs.h src/main.c tests/test_daemon_ipc.c
|
||||
f052 src/traces/traces.c src/traces/traces.h tests/test_traces.c
|
||||
f053 src/pipeline/registry.c tests/test_pipeline.c
|
||||
f054 src/cli/cli.h src/cli/hook_augment.c src/foundation/platform.c src/foundation/platform.h src/mcp/index_supervisor.c src/mcp/mcp.c src/mcp/mcp.h tests/test_cli.c tests/test_platform.c
|
||||
f055 tests/test_cli.c
|
||||
f056 src/daemon/application.c src/mcp/mcp.c src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c tests/test_daemon_application.c tests/test_mcp.c tests/test_pipeline.c
|
||||
f057 internal/cbm/extract_defs.c src/discover/language.c tests/test_extraction.c tests/test_language.c
|
||||
f058 src/cli/hook_augment.c src/daemon/runtime.c src/daemon/runtime.h src/foundation/sha256.c src/foundation/sha256.h src/main.c tests/test_cli.c tests/test_daemon_runtime.c
|
||||
f059 tests/windows/test_cli_non_ascii_arg.py tests/windows/test_daemon_stability.py tests/windows/test_hook_augment.py
|
||||
f060 src/cli/cli.c
|
||||
f061 .github/workflows/_test.yml
|
||||
f062 scripts/smoke-test.sh
|
||||
f063 src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c tests/test_main.c tests/test_subprocess.c
|
||||
f064 graph-ui/src/components/EdgeLines.test.ts graph-ui/src/components/EdgeLines.tsx graph-ui/src/components/GraphTab.edgeSampling.test.tsx graph-ui/src/components/GraphTab.test.ts graph-ui/src/components/GraphTab.tsx graph-ui/src/components/NodeCloud.test.ts graph-ui/src/components/NodeCloud.tsx graph-ui/src/components/NodeDetailPanel.test.tsx graph-ui/src/components/NodeDetailPanel.tsx graph-ui/src/components/NodeLabels.tsx graph-ui/src/hooks/useGraphData.test.ts graph-ui/src/hooks/useGraphData.ts graph-ui/src/lib/edgeBudget.test.ts graph-ui/src/lib/edgeBudget.ts graph-ui/src/lib/graphIndex.test.ts graph-ui/src/lib/graphIndex.ts graph-ui/src/lib/topK.test.ts graph-ui/src/lib/topK.ts src/store/store.c src/store/store.h src/ui/http_server.c src/ui/layout3d.c src/ui/layout3d.h src/ui/layout3d_internal.h tests/test_httpd.c tests/test_store_edges.c tests/test_store_search.c tests/test_ui.c
|
||||
f065 graph-ui/package-lock.json
|
||||
f066 src/foundation/lock_registry.c src/foundation/lock_registry_internal.h tests/test_lock_registry.c
|
||||
f067 src/daemon/version_cohort.c src/daemon/version_cohort.h tests/test_version_cohort.c
|
||||
f068 internal/cbm/extract_defs.c tests/test_extraction_inheritance.c
|
||||
f069 internal/cbm/extract_channels.c tests/test_extraction.c
|
||||
f070 test-infrastructure/Dockerfile.msan
|
||||
f071 src/git/git_context.c src/ui/http_server.c tests/test_daemon_application.c tests/test_git_context.c tests/test_httpd.c
|
||||
f072 scripts/hooks/pre-commit
|
||||
f073 tests/test_daemon_runtime.c
|
||||
f074 src/cli/agent_profiles.c src/cli/client_adapter.c tests/test_agent_clients.c tests/test_agent_profiles.c
|
||||
f075 internal/cbm/sqlite_writer.c src/graph_buffer/graph_buffer.c tests/test_graph_buffer.c
|
||||
f076 scripts/run-test-wave.py tests/test_parallel_harness_contract.sh
|
||||
f077 internal/cbm/extract_imports.c internal/cbm/lang_specs.c tests/test_edge_structural.c tests/test_extraction.c tests/test_grammar_labels.c
|
||||
f078 internal/cbm/sqlite_writer.c src/graph_buffer/graph_buffer.c tests/test_graph_buffer.c tests/test_sqlite_writer.c
|
||||
f079 src/foundation/dump_verify.c src/foundation/dump_verify.h tests/test_dump_verify.c
|
||||
f080 Makefile.cbm scripts/ci/check-binary-composition.sh tests/test_mcpb_bundle_contract.sh tests/test_release_candidate_derivation_contract.sh
|
||||
f081 scripts/smoke-test.sh src/cli/cli.c tests/test_cli.c tests/windows/test_hook_augment.py
|
||||
f082 src/main.c tests/windows/test_daemon_lifecycle.py
|
||||
f083 README.md docs/CONFIGURATION.md graph-ui/src/components/NodeDetailPanel.test.tsx graph-ui/src/components/NodeDetailPanel.tsx graph-ui/src/components/StatsTab.test.tsx graph-ui/src/hooks/useProjects.test.tsx graph-ui/src/hooks/useProjects.ts scripts/smoke-test.sh src/cli/cli.c src/cli/cli.h src/cli/client_adapter.c src/cli/hook_augment.c src/cli/progress_sink.c src/cli/progress_sink.h src/cypher/cypher.c src/cypher/cypher.h src/foundation/compat_fs.c src/foundation/compat_fs.h src/foundation/log.c src/foundation/log.h src/main.c src/mcp/compact_out.c src/mcp/compact_out.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c src/store/store.c src/store/store.h src/ui/http_server.c tests/scale_contract.sh tests/test_cli.c tests/test_cypher.c tests/test_daemon_runtime.c tests/test_diagnostics.c tests/test_httpd.c tests/test_incremental.c tests/test_index_resilience.c tests/test_index_supervisor.c tests/test_log.c tests/test_main.c tests/test_mcp.c tests/test_pipeline.c tests/test_store_pragmas.c tests/windows/test_daemon_lifecycle.py tests/windows/test_daemon_stability.py
|
||||
f084 README.md docs/CONFIGURATION.md graph-ui/src/components/NodeDetailPanel.test.tsx graph-ui/src/components/NodeDetailPanel.tsx graph-ui/src/components/StatsTab.test.tsx graph-ui/src/hooks/useProjects.test.tsx graph-ui/src/hooks/useProjects.ts scripts/smoke-test.sh src/cli/cli.c src/cli/cli.h src/cli/client_adapter.c src/cli/hook_augment.c src/cli/progress_sink.c src/cli/progress_sink.h src/cypher/cypher.c src/cypher/cypher.h src/foundation/compat_fs.c src/foundation/compat_fs.h src/foundation/log.c src/foundation/log.h src/main.c src/mcp/compact_out.c src/mcp/compact_out.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c src/store/store.c src/store/store.h src/ui/http_server.c tests/scale_contract.sh tests/test_cli.c tests/test_cypher.c tests/test_daemon_runtime.c tests/test_diagnostics.c tests/test_httpd.c tests/test_incremental.c tests/test_index_resilience.c tests/test_index_supervisor.c tests/test_log.c tests/test_main.c tests/test_mcp.c tests/test_pipeline.c tests/test_store_pragmas.c tests/windows/test_daemon_lifecycle.py tests/windows/test_daemon_stability.py tests/windows/test_hook_augment.py
|
||||
f085 src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_pipeline.c tests/test_registry.c
|
||||
f086 src/cli/config_yaml_edit.c tests/test_config_yaml_edit.c
|
||||
f087 src/foundation/compat_fs.c tests/test_cli.c tests/test_platform.c
|
||||
f088 .github/workflows/release.yml
|
||||
f089 .github/workflows/pages.yml
|
||||
f090 src/store/store.c tests/test_store_nodes.c
|
||||
f091 src/discover/discover.c tests/test_discover.c
|
||||
f092 Makefile.cbm internal/cbm/extract_defs.c internal/cbm/helpers.c internal/cbm/helpers.h tests/repro/repro_issue1692.c tests/repro/repro_main.c
|
||||
f093 src/mcp/mcp.c src/store/store.c tests/test_mcp.c tests/test_store_nodes.c
|
||||
f094 tests/test_mcp.c
|
||||
f095 src/mcp/mcp.c src/store/store.c src/store/store.h tests/test_mcp.c
|
||||
f096 scripts/msan.sh scripts/test.sh tests/test_main.c
|
||||
f097 src/daemon/ipc.c src/daemon/ipc.h tests/test_cli.c tests/test_daemon_ipc.c
|
||||
f098 src/daemon/application.c src/mcp/mcp.c tests/test_daemon.c
|
||||
f099 Makefile.cbm README.md graph-ui/index.html graph-ui/src/App.tsx graph-ui/src/components/ChangesTab.tsx graph-ui/src/components/CommandK.tsx graph-ui/src/components/ControlTab.tsx graph-ui/src/components/DashboardTab.tsx graph-ui/src/components/DisplaySettingsMenu.tsx graph-ui/src/components/ErrorBoundary.tsx graph-ui/src/components/FilterPanel.tsx graph-ui/src/components/FlowsTab.observed.test.tsx graph-ui/src/components/FlowsTab.tsx graph-ui/src/components/FlowsTab.zh.test.tsx graph-ui/src/components/GraphLoader.tsx graph-ui/src/components/GraphScene.tsx graph-ui/src/components/GraphTab.deadcode.test.tsx graph-ui/src/components/GraphTab.filters.test.tsx graph-ui/src/components/GraphTab.regions.test.tsx graph-ui/src/components/GraphTab.selection.test.tsx graph-ui/src/components/GraphTab.tsx graph-ui/src/components/HaloLayer.tsx graph-ui/src/components/MetricChip.tsx graph-ui/src/components/Minimap.tsx graph-ui/src/components/MissedCallout.tsx graph-ui/src/components/ModulesTab.tsx graph-ui/src/components/NodeDetailPanel.showmore.test.tsx graph-ui/src/components/NodeDetailPanel.tsx graph-ui/src/components/NodeTooltip.tsx graph-ui/src/components/OverviewTab.tsx graph-ui/src/components/ProjectCard.tsx graph-ui/src/components/ProjectSwitcher.tsx graph-ui/src/components/PromptBasket.tsx graph-ui/src/components/RegionPanel.tsx graph-ui/src/components/RegionTooltip.tsx graph-ui/src/components/Sidebar.tsx graph-ui/src/components/StatsTab.tsx graph-ui/src/components/SymbolTab.tsx graph-ui/src/components/TriggerTree.tsx graph-ui/src/components/WikiHoverCard.test.tsx graph-ui/src/components/WikiHoverCard.tsx graph-ui/src/components/WikiPanel.test.tsx graph-ui/src/components/WikiPanel.tsx graph-ui/src/components/WikiPanel.zh.test.tsx graph-ui/src/components/wikiTier.ts graph-ui/src/hooks/useGraphData.ts graph-ui/src/hooks/useProjects.test.tsx graph-ui/src/hooks/useProjects.ts graph-ui/src/lib/atlas.test.ts graph-ui/src/lib/atlas.ts graph-ui/src/lib/complexity.test.ts graph-ui/src/lib/complexity.ts graph-ui/src/lib/composer.ts graph-ui/src/lib/density.test.ts graph-ui/src/lib/density.ts graph-ui/src/lib/findings.test.ts graph-ui/src/lib/findings.ts graph-ui/src/lib/firstread.test.ts graph-ui/src/lib/firstread.ts graph-ui/src/lib/flowgroup.test.ts graph-ui/src/lib/flowgroup.ts graph-ui/src/lib/i18n.ts graph-ui/src/lib/impact.test.ts graph-ui/src/lib/impact.ts graph-ui/src/lib/observed.test.ts graph-ui/src/lib/observed.ts graph-ui/src/lib/questions.test.ts graph-ui/src/lib/questions.ts graph-ui/src/lib/rationale.test.ts graph-ui/src/lib/rationale.ts graph-ui/src/lib/regions.test.ts graph-ui/src/lib/regions.ts graph-ui/src/lib/treemap.ts graph-ui/src/lib/types.ts graph-ui/src/lib/who.test.ts graph-ui/src/lib/who.ts graph-ui/src/lib/why.test.ts graph-ui/src/lib/why.ts graph-ui/src/lib/whyfetch.ts graph-ui/src/styles/globals.css graph-ui/src/test/setup.ts graph-ui/src/wiki/entries.test.ts graph-ui/src/wiki/entries.ts graph-ui/tsconfig.tsbuildinfo graph-ui/vite.config.ts scripts/run-test-wave.py scripts/security-ui.sh src/main.c src/mcp/mcp.c src/pipeline/pipeline.h src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h src/ui/atlas.h src/ui/atlas_api.c src/ui/atlas_flows.c src/ui/atlas_handout.c src/ui/atlas_metrics.c src/ui/atlas_why.c src/ui/http_server.c src/ui/layout3d.c src/ui/layout3d.h src/ui/layout_internal.h src/ui/layout_regions.c src/watcher/watcher.c src/watcher/watcher.h test-infrastructure/docker-compose.yml test-infrastructure/ladder.sh test-infrastructure/run.sh tests/test_cypher.c tests/test_daemon_runtime.c tests/test_httpd.c tests/test_incremental.c tests/test_mcp.c tests/test_py_lsp_scale.c tests/test_ui.c tests/test_watcher.c
|
||||
f100 README.md docs/CONFIGURATION.md scripts/test_mcp_in_process.py src/main.c
|
||||
f101 tests/test_pipeline.c
|
||||
f102 src/daemon/application.c src/daemon/application_internal.h tests/test_daemon.c tests/test_daemon_application.c
|
||||
f103 src/discover/discover.c src/discover/discover.h src/discover/language.c tests/test_language.c
|
||||
f104 src/watcher/watcher.c src/watcher/watcher.h tests/test_watcher.c
|
||||
f105 scripts/test.sh src/main.c tests/test_worker_session_scope.sh
|
||||
f106 src/pipeline/pass_complexity.c tests/fixtures/complexity_pass_cycle_order/p00_a.py tests/fixtures/complexity_pass_cycle_order/p00_b.py tests/fixtures/complexity_pass_cycle_order/p01_a.py tests/fixtures/complexity_pass_cycle_order/p01_b.py tests/fixtures/complexity_pass_cycle_order/p02_a.py tests/fixtures/complexity_pass_cycle_order/p02_b.py tests/fixtures/complexity_pass_cycle_order/p03_a.py tests/fixtures/complexity_pass_cycle_order/p03_b.py tests/fixtures/complexity_pass_cycle_order/p04_a.py tests/fixtures/complexity_pass_cycle_order/p04_b.py tests/fixtures/complexity_pass_cycle_order/p05_a.py tests/fixtures/complexity_pass_cycle_order/p05_b.py tests/fixtures/complexity_pass_cycle_order/p06_a.py tests/fixtures/complexity_pass_cycle_order/p06_b.py tests/fixtures/complexity_pass_cycle_order/p07_a.py tests/fixtures/complexity_pass_cycle_order/p07_b.py tests/fixtures/complexity_pass_cycle_order/p08_a.py tests/fixtures/complexity_pass_cycle_order/p08_b.py tests/fixtures/complexity_pass_cycle_order/p09_a.py tests/fixtures/complexity_pass_cycle_order/p09_b.py tests/fixtures/complexity_pass_cycle_order/p10_a.py tests/fixtures/complexity_pass_cycle_order/p10_b.py tests/fixtures/complexity_pass_cycle_order/p11_a.py tests/fixtures/complexity_pass_cycle_order/p11_b.py tests/fixtures/complexity_pass_cycle_order/p12_a.py tests/fixtures/complexity_pass_cycle_order/p12_b.py tests/fixtures/complexity_pass_cycle_order/p13_a.py tests/fixtures/complexity_pass_cycle_order/p13_b.py tests/fixtures/complexity_pass_cycle_order/p14_a.py tests/fixtures/complexity_pass_cycle_order/p14_b.py tests/fixtures/complexity_pass_cycle_order/p15_a.py tests/fixtures/complexity_pass_cycle_order/p15_b.py tests/fixtures/complexity_pass_cycle_order/p16_a.py tests/fixtures/complexity_pass_cycle_order/p16_b.py tests/fixtures/complexity_pass_cycle_order/p17_a.py tests/fixtures/complexity_pass_cycle_order/p17_b.py tests/fixtures/complexity_pass_cycle_order/p18_a.py tests/fixtures/complexity_pass_cycle_order/p18_b.py tests/fixtures/complexity_pass_cycle_order/p19_a.py tests/fixtures/complexity_pass_cycle_order/p19_b.py tests/fixtures/complexity_pass_cycle_order/p20_a.py tests/fixtures/complexity_pass_cycle_order/p20_b.py tests/fixtures/complexity_pass_cycle_order/p21_a.py tests/fixtures/complexity_pass_cycle_order/p21_b.py tests/fixtures/complexity_pass_cycle_order/p22_a.py tests/fixtures/complexity_pass_cycle_order/p22_b.py tests/fixtures/complexity_pass_cycle_order/p23_a.py tests/fixtures/complexity_pass_cycle_order/p23_b.py tests/fixtures/complexity_pass_cycle_order/t00_x.py tests/fixtures/complexity_pass_cycle_order/t00_y.py tests/fixtures/complexity_pass_cycle_order/t00_z.py tests/fixtures/complexity_pass_cycle_order/t01_x.py tests/fixtures/complexity_pass_cycle_order/t01_y.py tests/fixtures/complexity_pass_cycle_order/t01_z.py tests/fixtures/complexity_pass_cycle_order/t02_x.py tests/fixtures/complexity_pass_cycle_order/t02_y.py tests/fixtures/complexity_pass_cycle_order/t02_z.py tests/fixtures/complexity_pass_cycle_order/t03_x.py tests/fixtures/complexity_pass_cycle_order/t03_y.py tests/fixtures/complexity_pass_cycle_order/t03_z.py tests/fixtures/complexity_pass_cycle_order/t04_x.py tests/fixtures/complexity_pass_cycle_order/t04_y.py tests/fixtures/complexity_pass_cycle_order/t04_z.py tests/fixtures/complexity_pass_cycle_order/t05_x.py tests/fixtures/complexity_pass_cycle_order/t05_y.py tests/fixtures/complexity_pass_cycle_order/t05_z.py tests/test_pipeline.c
|
||||
f107 docs/benchmarks/bounded-two-pass-results.json docs/benchmarks/bounded-two-pass.md scripts/benchmark-streaming.py src/pipeline/lsp_surface.c src/pipeline/lsp_surface.h src/pipeline/pass_parallel.c src/pipeline/pipeline.c src/pipeline/pipeline_internal.h tests/test_pipeline.c
|
||||
f108 src/pipeline/pass_semantic_edges.c tests/test_pipeline.c
|
||||
f109 .github/workflows/pr.yml internal/cbm/cbm.c scripts/ci/README.md scripts/ci/coverage-gate-allowlist.txt scripts/ci/parse-partial-baseline.txt scripts/ci/self-index-coverage-gate.sh scripts/test.sh tests/test_coverage_gate_contract.sh tests/test_parse_coverage.c tests/test_venue_parity_contract.sh
|
||||
f110 Makefile.cbm README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/cli/cli.h src/daemon/application.c src/discover/discover.c src/discover/discover.h src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_daemon_application.c tests/test_discover.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_main.c tests/test_mcp.c tests/test_pipeline.c tests/test_subprocess.c tests/test_worker_error_response.sh tests/test_worker_watchdog.sh
|
||||
f111 Makefile.cbm README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/cli/cli.h src/daemon/application.c src/daemon/bootstrap.c src/discover/discover.c src/discover/discover.h src/foundation/compat_fs.c src/foundation/compat_fs.h src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/main.c src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_daemon_application.c tests/test_discover.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_main.c tests/test_mcp.c tests/test_pipeline.c tests/test_platform.c tests/test_subprocess.c tests/test_worker_error_response.sh tests/test_worker_watchdog.sh
|
||||
f112 Makefile.cbm README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/cli/cli.h src/daemon/application.c src/daemon/bootstrap.c src/discover/discover.c src/discover/discover.h src/foundation/compat_fs.c src/foundation/compat_fs.h src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/git/git_context.c src/git/git_context.h src/main.c src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_daemon_application.c tests/test_discover.c tests/test_git_context.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_main.c tests/test_mcp.c tests/test_pipeline.c tests/test_platform.c tests/test_subprocess.c tests/test_worker_error_response.sh tests/test_worker_watchdog.sh
|
||||
f113 src/pipeline/pipeline_incremental.c tests/test_pipeline.c
|
||||
f114 README.md THIRD_PARTY.md docs/index.html docs/llms.txt internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/grammar_vba.c internal/cbm/helpers.c internal/cbm/lang_specs.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/vba/LICENSE internal/cbm/vendored/grammars/vba/parser.c internal/cbm/vendored/grammars/vba/tree_sitter/alloc.h internal/cbm/vendored/grammars/vba/tree_sitter/array.h internal/cbm/vendored/grammars/vba/tree_sitter/parser.h pkg/chocolatey/codebase-memory-mcp.nuspec pkg/npm/README.md scripts/new-languages.json scripts/package-release.sh scripts/vendored-checksums.txt server.json src/discover/discover.h src/discover/language.c src/discover/userconfig.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_manifest.c tests/repro/repro_invariant_breadth.c tests/repro/repro_language_registry.c tests/test_extraction.c tests/test_grammar_labels.c tests/test_grammar_regression.c tests/test_lang_contract.c tests/test_language.c
|
||||
f115 README.md THIRD_PARTY.md docs/index.html docs/llms.txt internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/extract_unified.h internal/cbm/grammar_vba.c internal/cbm/helpers.c internal/cbm/helpers.h internal/cbm/lang_specs.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/vba/LICENSE internal/cbm/vendored/grammars/vba/parser.c internal/cbm/vendored/grammars/vba/tree_sitter/alloc.h internal/cbm/vendored/grammars/vba/tree_sitter/array.h internal/cbm/vendored/grammars/vba/tree_sitter/parser.h pkg/chocolatey/codebase-memory-mcp.nuspec pkg/npm/README.md scripts/new-languages.json scripts/package-release.sh scripts/vendored-checksums.txt server.json src/discover/discover.h src/discover/language.c src/discover/userconfig.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_manifest.c tests/repro/repro_invariant_breadth.c tests/repro/repro_language_registry.c tests/test_extraction.c tests/test_grammar_labels.c tests/test_grammar_regression.c tests/test_lang_contract.c tests/test_language.c
|
||||
f116 internal/cbm/lang_specs.c
|
||||
f117 internal/cbm/extract_node_stack.h internal/cbm/lsp/ts_lsp.c tests/test_ts_lsp.c
|
||||
f118 .cbmignore src/cli/cli.c src/cli/cli.h src/main.c src/mcp/mcp.c
|
||||
f119 .cbmignore src/cli/cli.c src/cli/cli.h src/main.c src/mcp/mcp.c tests/test_cli.c
|
||||
f120 src/git/git_context.c src/git/git_context.h src/mcp/mcp.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h tests/test_git_context.c tests/test_mcp.c tests/test_store_nodes.c
|
||||
f121 src/watcher/watcher.c tests/test_watcher.c
|
||||
f122 scripts/run-tests-parallel.sh src/git/git_context.c src/git/git_context.h src/mcp/mcp.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h tests/test_git_context.c tests/test_mcp.c tests/test_store_nodes.c
|
||||
f123 scripts/test-windows.ps1 tests/windows/test_mcp_stdio.py
|
||||
f124 internal/cbm/extract_defs.c tests/test_grammar_labels.c tests/test_grammar_regression.c
|
||||
f125 src/cli/cli.c tests/test_cli.c
|
||||
f126 src/cli/cli.c src/cli/config_json_like.c src/cli/hook_augment.c src/daemon/application.c src/foundation/compat_fs.c src/foundation/compat_fs.h src/main.c src/mcp/index_supervisor.c src/mcp/mcp.c src/pipeline/artifact.c src/pipeline/pipeline.c src/ui/config.c tests/test_artifact.c tests/test_cli.c tests/test_platform.c
|
||||
f127 tests/test_daemon_frontend.c
|
||||
f128 src/daemon/ipc.c src/daemon/ipc_internal.h tests/test_daemon_ipc.c
|
||||
f129 .github/dependabot.yml .github/workflows/_build.yml .github/workflows/_lint.yml .github/workflows/_test.yml .github/workflows/release.yml .node-version CONTRIBUTING.md scripts/ci/lint-workflows.sh scripts/ci/test-frontend.sh tests/test_venue_parity_contract.sh
|
||||
f130 src/foundation/compat_fs.c src/foundation/compat_fs.h src/pipeline/pipeline.c src/pipeline/pipeline_internal.h tests/test_pipeline.c
|
||||
f131 src/daemon/application.c src/mcp/mcp.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/pipeline_internal.h tests/test_daemon_application.c tests/test_incremental.c tests/test_mcp.c tests/test_pipeline.c
|
||||
f132 src/daemon/bootstrap.c src/daemon/bootstrap.h src/daemon/host.c src/daemon/ipc.c src/daemon/ipc.h src/daemon/ipc_internal.h src/foundation/platform.c src/foundation/platform.h tests/test_daemon_bootstrap.c tests/test_daemon_ipc.c
|
||||
f133 Makefile.cbm src/cli/cli.c src/cli/config_edit_path.c src/cli/config_edit_path.h src/cli/config_json_like.c src/cli/config_text_edit.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c tests/test_cli.c tests/test_config_json_like.c tests/test_config_text_edit.c tests/test_config_toml_edit.c tests/test_config_yaml_edit.c
|
||||
f134 src/cli/cli.c src/mcp/index_supervisor.c src/mcp/index_supervisor.h tests/test_cli.c
|
||||
f135 src/cli/cli.c src/cli/config_json_like.c src/cli/config_json_like.h src/mcp/mcp.c tests/test_cli.c tests/test_config_json_like.c tests/test_mcp.c
|
||||
f136 src/cli/cli.c tests/test_cli.c tests/test_smoke_fixture_contract.sh
|
||||
f137 .github/workflows/_lint.yml .github/workflows/_test.yml
|
||||
f138 src/daemon/ipc.c src/daemon/ipc.h tests/test_daemon_ipc.c
|
||||
f139 src/cli/activation_transaction.c src/daemon/ipc.c src/daemon/ipc.h tests/test_daemon_ipc.c
|
||||
f140 src/cli/cli.h src/main.c tests/test_cli.c
|
||||
f141 internal/cbm/extract_defs.c tests/test_edge_types_probe.c tests/test_extraction.c
|
||||
f142 internal/cbm/lsp/php_lsp.c
|
||||
f143 internal/cbm/lsp/php_lsp.c src/pipeline/registry.c
|
||||
f144 src/cli/cli.c src/cli/cli.h src/main.c src/mcp/mcp.c tests/test_cli.c
|
||||
f145 README.md docs/UPSTREAM-SYNC.md docs/lsp-uplift/PERL-CROSS-FILE-INHERITANCE.md docs/lsp-uplift/PERL-DUCK-TYPING-AND-CROSS-LANG-AUDIT.md docs/lsp-uplift/PLAN.md docs/lsp-uplift/RESOLVER-PLAYBOOK.md docs/lsp-uplift/SOP.md internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c internal/cbm/lang_specs.c internal/cbm/lsp/generated/perl_stdlib_data.c internal/cbm/lsp/generated/rust_crates_seed.c internal/cbm/lsp/go_lsp.c internal/cbm/lsp/go_lsp.h internal/cbm/lsp/go_stdlib_modern.c internal/cbm/lsp/java_lsp.c internal/cbm/lsp/perl_lsp.c internal/cbm/lsp/perl_lsp.h internal/cbm/lsp/py_builtins.c internal/cbm/lsp/py_lsp.c internal/cbm/lsp/py_stdlib_compat.c internal/cbm/lsp/rust_cargo.c internal/cbm/lsp/rust_cargo.h internal/cbm/lsp/rust_lsp.c internal/cbm/lsp/rust_lsp.h internal/cbm/lsp/type_registry.h internal/cbm/lsp_all.c internal/cbm/service_patterns.c internal/cbm/service_patterns.h src/discover/language.c src/pipeline/lsp_surface.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_lsp_cross.h src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pass_route_nodes.c src/pipeline/pass_semantic.c src/pipeline/pass_tests.c src/pipeline/pipeline.c src/pipeline/registry.c tests/test_extraction.c tests/test_go_lsp.c tests/test_java_lsp.c tests/test_language.c tests/test_perl_lsp.c tests/test_pipeline.c tests/test_py_lsp.c tests/test_registry.c tests/test_route_canon.c tests/test_rust_lsp.c
|
||||
f146 src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h tests/test_index_supervisor.c tests/test_main.c tests/test_subprocess.c
|
||||
f147 src/daemon/daemon.c src/daemon/daemon.h src/daemon/host.c src/daemon/runtime.c src/daemon/runtime.h tests/test_daemon_runtime.c
|
||||
f148 internal/cbm/cbm.c src/foundation/compat.c src/foundation/compat.h tests/test_edge_types_probe.c
|
||||
f149 scripts/test-windows.ps1 tests/windows/mcp_stdio.py tests/windows/test_mcp_stdio.py
|
||||
f150 .github/workflows/codeql.yml
|
||||
f151 .github/workflows/scorecard.yml
|
||||
f152 src/cli/cli.c src/foundation/sha256.c src/foundation/sha256.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h tests/test_cli.c
|
||||
f153 install.ps1 scripts/test.sh src/cli/activation_transaction.c src/cli/cli.c tests/test_activation_diagnostic_contract.sh tests/test_windows_bundle_contract.sh
|
||||
f154 scripts/run-test-wave.py src/cli/cli.c src/foundation/sha256.c src/foundation/sha256.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h tests/test_cli.c
|
||||
f155 .github/workflows/_security.yml graph-ui/package-lock.json graph-ui/package.json scripts/test.sh tests/test_security_gate_fail_closed.sh
|
||||
f156 graph-ui/package-lock.json graph-ui/package.json
|
||||
f157 .github/workflows/_security.yml .github/workflows/pr.yml .github/workflows/release.yml graph-ui/package-lock.json graph-ui/package.json scripts/test.sh tests/test_security_gate_fail_closed.sh
|
||||
f158 tests/test_fqn.c
|
||||
f159 .github/workflows/dry-run.yml tests/test_security_gate_fail_closed.sh
|
||||
f160 tests/test_daemon_ipc.c tests/test_main.c
|
||||
f161 src/daemon/ipc.c tests/test_daemon_ipc.c tests/test_main.c
|
||||
f162 .github/workflows/_test.yml src/daemon/ipc.c src/daemon/ipc.h tests/test_daemon_ipc.c tests/test_main.c
|
||||
f163 scripts/soak-test.sh scripts/test.sh tests/test_soak_runtime_isolation_contract.sh
|
||||
f164 scripts/memlab.sh scripts/test.sh tests/test_memlab_runtime_isolation_contract.sh
|
||||
f165 scripts/benchmark-index.sh scripts/benchmark-search-graph.sh scripts/test.sh tests/test_benchmark_runtime_isolation_contract.sh
|
||||
f166 scripts/smoke-test.sh scripts/test.sh tests/test_smoke_runtime_isolation_contract.sh
|
||||
f167 Makefile.cbm scripts/lint-memory-core.py scripts/memory-core-baseline.txt src/foundation/mem.c src/foundation/mem.h src/foundation/mem_core.c src/foundation/mem_core.h src/foundation/platform.h src/foundation/system_info.c src/pipeline/pipeline.c tests/test_mem.c
|
||||
f168 src/cli/activation_transaction.c
|
||||
f169 Makefile.cbm internal/cbm/arena.h internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_unified.c internal/cbm/lsp/c_lsp.c internal/cbm/lsp/c_lsp.h internal/cbm/lsp/cs_lsp.c internal/cbm/lsp/go_lsp.c internal/cbm/lsp/py_lsp.c internal/cbm/lsp/py_lsp.h internal/cbm/lsp/type_registry.c internal/cbm/lsp/type_registry.h internal/cbm/result_compact.c internal/cbm/result_spill.c internal/cbm/result_spill.h scripts/lint-memory-core.py scripts/memory-core-baseline.txt src/foundation/arena.c src/foundation/arena.h src/foundation/dyn_array.h src/foundation/hash_table.c src/foundation/hash_table.h src/foundation/mem.c src/foundation/mem.h src/foundation/mem_core.c src/foundation/mem_core.h src/foundation/platform.h src/foundation/slab_alloc.c src/foundation/system_info.c src/graph_buffer/graph_buffer.c src/graph_buffer/graph_buffer.h src/mcp/mcp.c src/pipeline/lsp_surface.c src/pipeline/lsp_surface.h src/pipeline/pass_complexity.c src/pipeline/pass_importance.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_lsp_cross.h src/pipeline/pass_parallel.c src/pipeline/pass_semantic.c src/pipeline/pass_semantic_edges.c src/pipeline/pass_similarity.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/worker_pool.c src/semantic/semantic.c src/semantic/semantic.h src/simhash/minhash.c tests/test_arena.c tests/test_c_lsp.c tests/test_extraction.c tests/test_mcp.c tests/test_mem.c tests/test_parallel.c tests/test_pipeline.c
|
||||
f170 docs/CONFIGURATION.md src/main.c tests/test_daemon_open_readiness.py
|
||||
f171 src/daemon/ipc.c tests/test_daemon_ipc.c
|
||||
f172 src/cli/hook_augment.c src/daemon/runtime.c src/daemon/runtime.h src/main.c tests/test_cli.c tests/test_daemon_runtime.c
|
||||
f173 Makefile.cbm internal/cbm/arena.h internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_unified.c internal/cbm/lsp/c_lsp.c internal/cbm/lsp/c_lsp.h internal/cbm/lsp/cs_lsp.c internal/cbm/lsp/go_lsp.c internal/cbm/lsp/py_lsp.c internal/cbm/lsp/py_lsp.h internal/cbm/lsp/type_registry.c internal/cbm/lsp/type_registry.h internal/cbm/result_compact.c internal/cbm/result_spill.c internal/cbm/result_spill.h scripts/lint-memory-core.py scripts/memory-core-baseline.txt src/foundation/arena.c src/foundation/arena.h src/foundation/diagnostics.c src/foundation/dyn_array.h src/foundation/hash_table.c src/foundation/hash_table.h src/foundation/mem.c src/foundation/mem.h src/foundation/mem_core.c src/foundation/mem_core.h src/foundation/platform.h src/foundation/slab_alloc.c src/foundation/system_info.c src/graph_buffer/graph_buffer.c src/graph_buffer/graph_buffer.h src/main.c src/mcp/mcp.c src/pipeline/lsp_surface.c src/pipeline/lsp_surface.h src/pipeline/pass_complexity.c src/pipeline/pass_importance.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_lsp_cross.h src/pipeline/pass_parallel.c src/pipeline/pass_semantic.c src/pipeline/pass_semantic_edges.c src/pipeline/pass_similarity.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/worker_pool.c src/semantic/semantic.c src/semantic/semantic.h src/simhash/minhash.c tests/test_arena.c tests/test_c_lsp.c tests/test_extraction.c tests/test_graph_buffer.c tests/test_mcp.c tests/test_mem.c tests/test_parallel.c tests/test_pipeline.c
|
||||
f174 README.md scripts/smoke-test.sh src/cli/cli.c src/cli/cli.h tests/test_cli.c
|
||||
f175 src/foundation/mem.c src/foundation/mem.h
|
||||
f176 flake.nix pkg/chocolatey/codebase-memory-mcp.nuspec pkg/chocolatey/tools/chocolateyInstall.ps1 pkg/go/cmd/codebase-memory-mcp/main.go pkg/npm/package.json pkg/pypi/pyproject.toml pkg/pypi/src/codebase_memory_mcp/_cli.py server.json
|
||||
f177 graph-ui/src/components/FilterPanel.tsx graph-ui/src/components/GraphTab.deadcode.test.tsx graph-ui/src/lib/colors.ts
|
||||
f178 CONTRIBUTING.md README.md docs/index.html pkg/npm/README.md
|
||||
f179 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/service_patterns.c internal/cbm/service_patterns.h src/cli/cli.c src/cypher/cypher.c src/foundation/str_util.c src/foundation/str_util.h src/mcp/mcp.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_cli.c tests/test_cypher.c tests/test_extraction.c tests/test_infrascan.c tests/test_mcp.c tests/test_parse_coverage.c tests/test_pipeline.c tests/test_registry.c tests/test_str_util.c
|
||||
f180 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/service_patterns.c internal/cbm/service_patterns.h scripts/memory-core-baseline.txt src/cli/cli.c src/cypher/cypher.c src/foundation/str_util.c src/foundation/str_util.h src/mcp/mcp.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_cli.c tests/test_cypher.c tests/test_extraction.c tests/test_infrascan.c tests/test_mcp.c tests/test_parse_coverage.c tests/test_pipeline.c tests/test_registry.c tests/test_str_util.c
|
||||
f181 src/discover/language.c tests/test_language.c
|
||||
f182 .github/workflows/_memwaste.yml .github/workflows/dry-run.yml .github/workflows/nightly-soak.yml .github/workflows/pr.yml .github/workflows/release.yml Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_node_stack.h internal/cbm/lsp/go_lsp.c internal/cbm/result_compact.c internal/cbm/sqlite_writer.c scripts/fuzz.sh scripts/memlab-drive.py scripts/memlab-report.py scripts/memlab.sh scripts/memwaste-padding.py scripts/memwaste-ratchet.py scripts/memwaste-report.py scripts/memwaste-scaling.py scripts/memwaste.sh scripts/sanitizer-coverage-map.py scripts/sanitizer-map.json src/foundation/arena.c src/foundation/arena.h src/foundation/compat_thread.c src/foundation/dyn_array.h src/foundation/hash_table.c src/foundation/mem.c src/foundation/mem.h src/foundation/mem_core.c src/foundation/mem_events.c src/foundation/mem_events.h src/foundation/mem_override_darwin.c src/foundation/mem_override_libc.c src/foundation/mem_override_posix.c src/foundation/mem_override_win.c src/foundation/mem_profile.c src/foundation/mem_profile.h src/main.c src/pipeline/lsp_surface.c src/pipeline/pass_configlink.c src/pipeline/pass_k8s.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_lsp_cross.h src/pipeline/pass_parallel.c src/pipeline/pass_semantic.c src/pipeline/pass_semantic_edges.c src/pipeline/pass_similarity.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/registry.c src/pipeline/worker_pool.c src/semantic/semantic.c src/semantic/semantic.h src/simhash/minhash.c src/simhash/minhash.h tests/fuzz/cbm_fuzz.c tests/fuzz/corpus/config/json-empty.seed tests/fuzz/corpus/config/json.seed tests/fuzz/corpus/config/toml.seed tests/fuzz/corpus/config/yaml.seed tests/fuzz/corpus/cypher/agg.seed tests/fuzz/corpus/cypher/hops.seed tests/fuzz/corpus/cypher/match.seed tests/fuzz/corpus/cypher/optional.seed tests/fuzz/corpus/cypher/union.seed tests/fuzz/corpus/cypher/where.seed tests/fuzz/corpus/extract/c.seed tests/fuzz/corpus/extract/cpp.seed tests/fuzz/corpus/extract/csharp.seed tests/fuzz/corpus/extract/go.seed tests/fuzz/corpus/extract/java.seed tests/fuzz/corpus/extract/javascript.seed tests/fuzz/corpus/extract/makefile.seed tests/fuzz/corpus/extract/php.seed tests/fuzz/corpus/extract/python.seed tests/fuzz/corpus/extract/ruby.seed tests/fuzz/corpus/extract/rust.seed tests/fuzz/corpus/extract/sql.seed tests/fuzz/corpus/extract/typescript.seed tests/fuzz/corpus/extract/yaml.seed tests/test_arena.c tests/test_extraction.c tests/test_hash_table.c tests/test_main.c tests/test_mem.c tests/test_mem_events.c tests/test_registry.c tests/test_semantic.c tests/test_shell_line_endings.sh tests/test_simhash.c tests/test_venue_parity_contract.sh
|
||||
f183 src/cli/cli.h src/main.c tests/test_cli.c tests/test_daemon_bootstrap.c
|
||||
f184 src/cli/cli.h src/main.c tests/test_cli.c tests/test_daemon_bootstrap.c tests/test_mcp.c
|
||||
f185 src/ui/layout3d.c tests/test_ui.c
|
||||
f186 Makefile.cbm README.md src/daemon/application.c src/daemon/bootstrap.c src/foundation/mem_core.c src/foundation/mem_core.h src/main.c src/mcp/cache.c src/mcp/cache.h src/mcp/mcp.c tests/test_cache.c tests/test_cache_cli.py tests/test_daemon_application.c tests/test_daemon_bootstrap.c tests/test_main.c tests/test_mcp.c
|
||||
f187 .github/workflows/_memwaste.yml .github/workflows/dry-run.yml .github/workflows/nightly-soak.yml .github/workflows/pr.yml .github/workflows/release.yml Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_node_stack.h internal/cbm/lsp/go_lsp.c internal/cbm/result_compact.c internal/cbm/result_spill.c internal/cbm/result_spill.h internal/cbm/sqlite_writer.c scripts/fuzz.sh scripts/memlab-drive.py scripts/memlab-report.py scripts/memlab.sh scripts/memory-core-baseline.txt scripts/memwaste-padding.py scripts/memwaste-ratchet.py scripts/memwaste-report.py scripts/memwaste-scaling.py scripts/memwaste.sh scripts/sanitizer-coverage-map.py scripts/sanitizer-map.json scripts/smoke-test.sh scripts/test.sh src/foundation/arena.c src/foundation/arena.h src/foundation/compat_thread.c src/foundation/dyn_array.h src/foundation/hash_table.c src/foundation/mem.c src/foundation/mem.h src/foundation/mem_core.c src/foundation/mem_events.c src/foundation/mem_events.h src/foundation/mem_override_darwin.c src/foundation/mem_override_libc.c src/foundation/mem_override_posix.c src/foundation/mem_override_win.c src/foundation/mem_profile.c src/foundation/mem_profile.h src/foundation/platform.h src/foundation/system_info.c src/graph_buffer/graph_buffer.c src/main.c src/pipeline/lsp_surface.c src/pipeline/pass_configlink.c src/pipeline/pass_k8s.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_lsp_cross.h src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pass_route_nodes.c src/pipeline/pass_semantic.c src/pipeline/pass_semantic_edges.c src/pipeline/pass_similarity.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/registry.c src/pipeline/worker_pool.c src/semantic/semantic.c src/semantic/semantic.h src/simhash/minhash.c src/simhash/minhash.h tests/fuzz/cbm_fuzz.c tests/fuzz/corpus/config/json-empty.seed tests/fuzz/corpus/config/json.seed tests/fuzz/corpus/config/toml.seed tests/fuzz/corpus/config/yaml.seed tests/fuzz/corpus/cypher/agg.seed tests/fuzz/corpus/cypher/hops.seed tests/fuzz/corpus/cypher/match.seed tests/fuzz/corpus/cypher/optional.seed tests/fuzz/corpus/cypher/union.seed tests/fuzz/corpus/cypher/where.seed tests/fuzz/corpus/extract/c.seed tests/fuzz/corpus/extract/cpp.seed tests/fuzz/corpus/extract/csharp.seed tests/fuzz/corpus/extract/go.seed tests/fuzz/corpus/extract/java.seed tests/fuzz/corpus/extract/javascript.seed tests/fuzz/corpus/extract/makefile.seed tests/fuzz/corpus/extract/php.seed tests/fuzz/corpus/extract/python.seed tests/fuzz/corpus/extract/ruby.seed tests/fuzz/corpus/extract/rust.seed tests/fuzz/corpus/extract/sql.seed tests/fuzz/corpus/extract/typescript.seed tests/fuzz/corpus/extract/yaml.seed tests/test_arena.c tests/test_extraction.c tests/test_hash_table.c tests/test_main.c tests/test_mem.c tests/test_mem_events.c tests/test_parallel.c tests/test_pipeline.c tests/test_registry.c tests/test_semantic.c tests/test_shell_line_endings.sh tests/test_simhash.c tests/test_thread_stack_tls_contract.sh tests/test_venue_parity_contract.sh
|
||||
f188 .github/workflows/_memwaste.yml .github/workflows/dry-run.yml .github/workflows/nightly-soak.yml .github/workflows/pr.yml .github/workflows/release.yml Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_node_stack.h internal/cbm/extract_unified.c internal/cbm/extract_usages.c internal/cbm/lsp/go_lsp.c internal/cbm/result_compact.c internal/cbm/result_spill.c internal/cbm/result_spill.h internal/cbm/sqlite_writer.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/properties/scanner.c scripts/fuzz.sh scripts/memlab-drive.py scripts/memlab-report.py scripts/memlab.sh scripts/memory-core-baseline.txt scripts/memwaste-padding.py scripts/memwaste-ratchet.py scripts/memwaste-report.py scripts/memwaste-scaling.py scripts/memwaste.sh scripts/sanitizer-coverage-map.py scripts/sanitizer-map.json scripts/smoke-test.sh scripts/test.sh scripts/vendored-checksums.txt src/foundation/arena.c src/foundation/arena.h src/foundation/compat_thread.c src/foundation/dyn_array.h src/foundation/hash_table.c src/foundation/mem.c src/foundation/mem.h src/foundation/mem_core.c src/foundation/mem_events.c src/foundation/mem_events.h src/foundation/mem_override_darwin.c src/foundation/mem_override_libc.c src/foundation/mem_override_posix.c src/foundation/mem_override_win.c src/foundation/mem_profile.c src/foundation/mem_profile.h src/foundation/platform.h src/foundation/system_info.c src/graph_buffer/graph_buffer.c src/main.c src/pipeline/lsp_surface.c src/pipeline/pass_configlink.c src/pipeline/pass_k8s.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_lsp_cross.h src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pass_route_nodes.c src/pipeline/pass_semantic.c src/pipeline/pass_semantic_edges.c src/pipeline/pass_similarity.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/registry.c src/pipeline/worker_pool.c src/semantic/semantic.c src/semantic/semantic.h src/simhash/minhash.c src/simhash/minhash.h test-infrastructure/vm/vm-run-tests.sh tests/fuzz/cbm_fuzz.c tests/fuzz/corpus/config/json-empty.seed tests/fuzz/corpus/config/json.seed tests/fuzz/corpus/config/toml.seed tests/fuzz/corpus/config/yaml.seed tests/fuzz/corpus/cypher/agg.seed tests/fuzz/corpus/cypher/hops.seed tests/fuzz/corpus/cypher/match.seed tests/fuzz/corpus/cypher/optional.seed tests/fuzz/corpus/cypher/union.seed tests/fuzz/corpus/cypher/where.seed tests/fuzz/corpus/extract/c.seed tests/fuzz/corpus/extract/cpp.seed tests/fuzz/corpus/extract/csharp.seed tests/fuzz/corpus/extract/go.seed tests/fuzz/corpus/extract/java.seed tests/fuzz/corpus/extract/javascript.seed tests/fuzz/corpus/extract/makefile.seed tests/fuzz/corpus/extract/php.seed tests/fuzz/corpus/extract/python.seed tests/fuzz/corpus/extract/ruby.seed tests/fuzz/corpus/extract/rust.seed tests/fuzz/corpus/extract/sql.seed tests/fuzz/corpus/extract/typescript.seed tests/fuzz/corpus/extract/yaml.seed tests/test_arena.c tests/test_extraction.c tests/test_hash_table.c tests/test_main.c tests/test_mem.c tests/test_mem_events.c tests/test_parallel.c tests/test_pipeline.c tests/test_registry.c tests/test_semantic.c tests/test_shell_line_endings.sh tests/test_simhash.c tests/test_thread_stack_tls_contract.sh tests/test_venue_parity_contract.sh tests/test_vm_verdict_contract.sh
|
||||
f189 internal/cbm/extract_calls.c internal/cbm/lsp/ts_lsp.c tests/test_extraction.c tests/test_ts_lsp.c
|
||||
f190 .github/workflows/pr.yml Makefile.cbm README.md src/cli/cache.c src/cli/cache.h src/daemon/bootstrap.c src/daemon/version_cohort.c src/daemon/version_cohort.h src/foundation/mem_core.c src/foundation/mem_core.h src/main.c tests/test_cache.c tests/test_cache_cli.py tests/test_daemon_bootstrap.c tests/test_main.c tests/test_mcp.c tests/test_version_cohort.c
|
||||
f191 .github/workflows/pr.yml Makefile.cbm README.md pkg/pypi/tests/test_cli.py src/cli/cache.c src/cli/cache.h src/daemon/bootstrap.c src/daemon/version_cohort.c src/daemon/version_cohort.h src/foundation/mem_core.c src/foundation/mem_core.h src/main.c tests/test_cache.c tests/test_cache_cli.py tests/test_daemon_bootstrap.c tests/test_main.c tests/test_mcp.c tests/test_version_cohort.c
|
||||
f192 install.sh tests/test-install-android-guard.sh
|
||||
f193 src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_pipeline.c tests/test_registry.c
|
||||
f194 internal/cbm/vendored/grammars/sql/parser.c internal/cbm/vendored/grammars/tsx/parser.c scripts/vendored-checksums.txt tests/test_parse_coverage.c
|
||||
f195 internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/sql/parser.c internal/cbm/vendored/grammars/tsx/parser.c scripts/vendored-checksums.txt tests/test_parse_coverage.c
|
||||
f196 src/mcp/mcp.c src/pipeline/pipeline.c src/pipeline/pipeline.h tests/test_pipeline.c
|
||||
f197 .github/workflows/_lint.yml .github/workflows/_test.yml .github/workflows/release.yml
|
||||
f198 internal/cbm/lang_specs.c tests/test_extraction.c
|
||||
f199 install.ps1
|
||||
f200 internal/cbm/lsp/py_lsp.c internal/cbm/lsp/scope.c internal/cbm/lsp/scope.h tests/test_scope.c
|
||||
f201 src/cypher/cypher.c src/foundation/recursion_whitelist.h tests/test_cypher.c
|
||||
f202 internal/cbm/service_patterns.c tests/test_infrascan.c tests/test_pipeline.c
|
||||
f203 scripts/memory-core-baseline.txt src/mcp/mcp.c
|
||||
f204 internal/cbm/service_patterns.c tests/test_pipeline.c
|
||||
f205 src/foundation/compat_thread.c
|
||||
f206 CONTRIBUTING.md README.md
|
||||
f207 README.md docs/WINDOWS_TROUBLESHOOTING.md
|
||||
f208 scripts/memory-core-baseline.txt src/mcp/mcp.c tests/test_worker_session_scope.sh
|
||||
f209 .github/workflows/smoke.yml scripts/smoke-invariants.sh
|
||||
f210 internal/cbm/cbm.c internal/cbm/preprocessor.cpp internal/cbm/preprocessor.h src/mcp/mcp.c tests/test_extraction.c
|
||||
f211 README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/daemon/application.c src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c tests/test_daemon_application.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_mcp.c tests/test_subprocess.c
|
||||
f212 docs/EVALUATION_PLAN.md scripts/benchmark-index.sh scripts/benchmark-search-graph.sh scripts/test.sh tests/test_benchmark_runtime_isolation_contract.sh
|
||||
f213 Makefile.cbm src/foundation/compat.c src/foundation/compat.h src/foundation/limits.h src/foundation/mem_override_win.c src/foundation/source_encoding.c src/foundation/source_encoding.h src/mcp/index_supervisor.c src/mcp/mcp.c src/pipeline/pass_calls.c src/pipeline/pass_compile_commands.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_semantic.c src/pipeline/pass_usages.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_pipeline.c tests/test_platform.c
|
||||
f214 README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/daemon/application.c src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c tests/test_daemon_application.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_mcp.c tests/test_subprocess.c tests/test_worker_session_scope.sh
|
||||
f215 .github/workflows/pr.yml
|
||||
f216 CONTRIBUTING.md
|
||||
f217 README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/daemon/application.c src/daemon/application.h src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c tests/test_daemon_application.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_mcp.c tests/test_subprocess.c tests/test_worker_session_scope.sh
|
||||
f218 tests/test_daemon_bootstrap.c
|
||||
f219 internal/cbm/service_patterns.c tests/test_infrascan.c
|
||||
f220 Makefile.cbm src/cli/cli.c src/pipeline/pass_doclinks.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_doclinks.c tests/test_edge_structural.c tests/test_lang_contract.c tests/test_main.c tests/test_pipeline.c
|
||||
f221 Makefile.cbm README.md src/cli/cli.c src/pipeline/pass_doclinks.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_doclinks.c tests/test_edge_structural.c tests/test_lang_contract.c tests/test_main.c tests/test_pipeline.c
|
||||
f222 src/daemon/application.c src/mcp/mcp.c src/pipeline/lsp_surface.c src/pipeline/pipeline.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_daemon_application.c tests/test_mcp.c tests/test_pipeline.c
|
||||
f223 README.md docs/CONFIGURATION.md docs/INDEX_RESOURCE_LIMITS.md scripts/test-runtime.sh src/cli/cli.c src/daemon/application.c src/daemon/application.h src/foundation/index_policy.c src/foundation/index_policy.h src/foundation/subprocess.c src/foundation/subprocess.h src/mcp/index_supervisor.c src/mcp/index_supervisor.h src/mcp/mcp.c src/mcp/mcp.h src/mcp/mcp_internal.h src/pipeline/pipeline.c tests/test_daemon_application.c tests/test_daemon_bootstrap.c tests/test_index_policy.c tests/test_index_supervisor.c tests/test_mcp.c tests/test_subprocess.c tests/test_worker_session_scope.sh
|
||||
f224 internal/cbm/lsp/ts_lsp.c internal/cbm/lsp/ts_lsp.h tests/test_stack_overflow.c
|
||||
f225 src/foundation/mem_override_win.c
|
||||
f226 src/foundation/compat.c src/mcp/index_supervisor.c src/mcp/mcp.c tests/test_platform.c
|
||||
f227 src/pipeline/lsp_surface.c tests/test_pipeline.c
|
||||
f228 src/daemon/host.c src/daemon/ipc.c src/daemon/ipc.h src/daemon/runtime.c src/daemon/runtime.h src/daemon/version_cohort.c src/daemon/version_cohort.h src/foundation/private_file_lock.c src/foundation/private_file_lock_internal.h tests/test_daemon_ipc.c tests/test_private_file_lock.c tests/test_version_cohort.c
|
||||
f229 src/foundation/compat.h tests/test_platform.c
|
||||
f230 README.md docs/CONFIGURATION.md src/cli/cli.c src/cli/cli.h src/cli/hook_augment.c src/daemon/application.c src/git/git_context.c src/git/git_context.h src/mcp/mcp.c src/mcp/mcp_internal.h tests/test_cli.c tests/test_git_context.c tests/test_mcp.c
|
||||
f231 internal/cbm/extract_defs.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_call_reference_contract.c tests/test_registry.c
|
||||
f232 internal/cbm/extract_calls.c internal/cbm/extract_unified.c internal/cbm/extract_unified.h tests/repro/repro_call_scope_usages.c tests/test_convergence_probe.c
|
||||
f233 pkg/scoop/codebase-memory-mcp.json
|
||||
f234 src/mcp/mcp.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h tests/test_mcp.c tests/test_pipeline.c
|
||||
f235 internal/cbm/extract_defs.c internal/cbm/helpers.c internal/cbm/helpers.h tests/test_extraction.c
|
||||
f236 src/pipeline/registry.c tests/test_pipeline.c tests/test_registry.c
|
||||
f237 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_unified.c internal/cbm/extract_usages.c internal/cbm/helpers.c internal/cbm/helpers.h internal/cbm/result_compact.c src/foundation/constants.h src/foundation/recursion_whitelist.h src/mcp/mcp.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_usages.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/registry.c src/store/store.c src/store/store.h tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_node_behaviors.c tests/test_extraction.c tests/test_mcp.c tests/test_pipeline.c tests/test_registry.c tests/test_store_nodes.c
|
||||
f238 src/foundation/compat_fs.c src/foundation/compat_fs.h src/mcp/mcp.c src/pipeline/pipeline.c tests/test_pipeline.c
|
||||
f239 src/mcp/mcp.c src/pipeline/pass_cross_repo.c src/pipeline/pass_cross_repo.h tests/test_cli.c tests/test_cross_repo.c
|
||||
f240 src/pipeline/pass_cross_repo.c tests/test_cross_repo.c
|
||||
f241 internal/cbm/extract_defs.c internal/cbm/lang_specs.c tests/test_extraction.c
|
||||
f242 docs/CONFIGURATION.md src/daemon/ipc.c src/daemon/ipc.h tests/test_daemon_ipc.c
|
||||
f243 docs/CONFIGURATION.md src/daemon/ipc.c src/daemon/ipc.h src/main.c tests/test_daemon_ipc.c
|
||||
f244 src/cli/config_toml_edit.c src/cli/config_toml_edit.h tests/test_cli.c tests/test_config_toml_edit.c
|
||||
f245 docs/cbmignore.md src/discover/discover.c tests/test_discover.c
|
||||
f246 Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/sql_values.c internal/cbm/sql_values.h tests/test_extraction.c tests/test_parse_coverage.c tests/test_pipeline.c
|
||||
f247 THIRD_PARTY.md internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/javascript/parser.c internal/cbm/vendored/grammars/tsx/parser.c internal/cbm/vendored/grammars/tsx/tree_sitter/array.h internal/cbm/vendored/grammars/tsx/tree_sitter/parser.h scripts/audit-license-provenance.py scripts/vendored-checksums.txt tests/test_parse_coverage.c tools/tree-sitter-javascript/grammar.js tools/tree-sitter-javascript/package.json tools/tree-sitter-javascript/patches/jsx-lone-ampersand.patch tools/tree-sitter-javascript/src/grammar.json tools/tree-sitter-javascript/src/node-types.json tools/tree-sitter-javascript/src/parser.c tools/tree-sitter-javascript/src/scanner.c tools/tree-sitter-javascript/src/tree_sitter/alloc.h tools/tree-sitter-javascript/src/tree_sitter/array.h tools/tree-sitter-javascript/src/tree_sitter/parser.h tools/tree-sitter-javascript/test/corpus/jsx_ampersand.txt tools/tree-sitter-javascript/tree-sitter.json tools/tree-sitter-tsx/define-grammar.js tools/tree-sitter-tsx/grammar.js tools/tree-sitter-tsx/javascript-grammar.js tools/tree-sitter-tsx/package.json tools/tree-sitter-tsx/src/_common_scanner.h tools/tree-sitter-tsx/src/grammar.json tools/tree-sitter-tsx/src/node-types.json tools/tree-sitter-tsx/src/parser.c tools/tree-sitter-tsx/src/scanner.c tools/tree-sitter-tsx/src/tree_sitter/alloc.h tools/tree-sitter-tsx/src/tree_sitter/array.h tools/tree-sitter-tsx/src/tree_sitter/parser.h tools/tree-sitter-tsx/test/corpus/jsx_ampersand.txt tools/tree-sitter-tsx/tree-sitter.json
|
||||
f248 internal/cbm/lsp/c_lsp.c internal/cbm/lsp/ts_lsp.c internal/cbm/lsp/ts_lsp.h tests/test_c_lsp.c tests/test_ts_lsp.c
|
||||
f249 internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/c_sharp/parser.c internal/cbm/vendored/grammars/c_sharp/scanner.c internal/cbm/vendored/grammars/c_sharp/tree_sitter/alloc.h internal/cbm/vendored/grammars/c_sharp/tree_sitter/array.h internal/cbm/vendored/grammars/c_sharp/tree_sitter/parser.h scripts/vendored-checksums.txt tests/test_parse_coverage.c
|
||||
f250 internal/cbm/sqlite_writer.c tests/test_sqlite_writer.c
|
||||
f251 src/mcp/mcp.c src/pipeline/fqn.c src/pipeline/pipeline.h tests/test_mcp.c
|
||||
f252 internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/result_compact.c src/pipeline/lsp_surface.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline.c src/pipeline/pipeline_delta.c src/pipeline/pipeline_internal.h tests/test_extraction.c tests/test_pipeline.c
|
||||
f253 README.md docs/CONFIGURATION.md src/cli/cli.c src/cli/cli.h src/daemon/host.c src/discover/discover.c src/discover/discover.h src/watcher/watcher.c src/watcher/watcher.h tests/test_cli.c tests/test_watcher.c
|
||||
f254 src/daemon/runtime.c src/daemon/runtime.h tests/test_daemon_runtime.c
|
||||
f255 internal/cbm/cbm.c internal/cbm/cbm.h tests/test_parse_coverage.c
|
||||
f256 scripts/test.sh tests/repro/repro_main.c tests/test_git_env_isolation_contract.sh tests/test_helpers.h tests/test_main.c
|
||||
f257 src/cli/config_toml_edit.c tests/test_config_toml_edit.c
|
||||
f258 internal/cbm/lsp/generated/rust_stdlib_data.c src/pipeline/lsp_resolve.h src/pipeline/pass_calls.c src/pipeline/pass_parallel.c tests/test_parallel.c tests/test_pipeline.c
|
||||
f259 internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/lsp/c_lsp.c internal/cbm/lsp/cs_lsp.c internal/cbm/lsp/go_lsp.c internal/cbm/lsp/java_lsp.c internal/cbm/lsp/kotlin_lsp.c internal/cbm/lsp/php_lsp.c internal/cbm/lsp/py_lsp.c internal/cbm/lsp/rust_lsp.c internal/cbm/lsp/ts_lsp.c tests/test_parse_coverage.c
|
||||
f260 Makefile.cbm src/foundation/compat_fs.c src/foundation/compat_fs.h src/foundation/git_env.c src/foundation/git_env.h src/foundation/subprocess.c src/foundation/subprocess.h src/git/git_context.c src/mcp/mcp.c src/pipeline/artifact.c src/pipeline/pass_githistory.c src/watcher/watcher.c tests/test_git_context.c tests/test_subprocess.c
|
||||
f261 Makefile.cbm internal/cbm/callable_sig.c internal/cbm/callable_sig.h internal/cbm/cbm.h internal/cbm/lsp/go_lsp.h internal/cbm/lsp/type_registry.h internal/cbm/result_compact.c scripts/memory-core-baseline.txt src/mcp/mcp.c src/pipeline/lsp_resolve.h src/pipeline/pass_ensemble_routing.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_semantic.c src/pipeline/registry.c src/store/store.c src/store/store.h tests/test_callable_sig.c tests/test_main.c tests/test_store_nodes.c
|
||||
f262 src/pipeline/pass_cross_repo.c src/store/store.c src/store/store.h tests/test_cross_repo.c
|
||||
f263 src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_registry.c
|
||||
f264 internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/lsp/cs_lsp.c internal/cbm/lsp/type_registry.h internal/cbm/result_compact.c src/pipeline/pass_lsp_cross.c src/pipeline/pipeline.c tests/test_parallel.c
|
||||
f265 README.md docs/CONFIGURATION.md docs/index.html src/daemon/application.c src/mcp/mcp.c src/mcp/mcp.h src/watcher/watcher.c src/watcher/watcher.h tests/test_daemon_application.c tests/test_watcher.c
|
||||
f266 src/daemon/host.c src/daemon/host_internal.h src/daemon/ipc.c src/daemon/ipc.h src/daemon/runtime.c src/daemon/runtime.h src/daemon/version_cohort.c src/daemon/version_cohort.h src/foundation/private_file_lock.c src/foundation/private_file_lock_internal.h tests/test_daemon_ipc.c tests/test_daemon_runtime.c tests/test_private_file_lock.c tests/test_version_cohort.c
|
||||
f267 src/mcp/mcp.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c tests/test_pipeline.c
|
||||
f268 README.md docs/CONFIGURATION.md
|
||||
f269 internal/cbm/extract_usages.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/rescript/scanner.c src/discover/discover.c src/discover/discover.h src/discover/language.c tests/test_complexity.c tests/test_extraction.c tests/test_language.c
|
||||
f270 internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/result_compact.c scripts/memory-core-baseline.txt src/pipeline/lsp_surface.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pipeline.c src/pipeline/pipeline_delta.c src/pipeline/pipeline_internal.h tests/test_extraction.c tests/test_pipeline.c
|
||||
f271 THIRD_PARTY.md internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/pascal/parser.c internal/cbm/vendored/grammars/pascal/tree_sitter/parser.h scripts/audit-license-provenance.py scripts/vendored-checksums.txt tests/test_parse_coverage.c tools/tree-sitter-pascal/grammar.js tools/tree-sitter-pascal/package.json tools/tree-sitter-pascal/patches/unicode-identifiers.patch tools/tree-sitter-pascal/src/grammar.json tools/tree-sitter-pascal/src/node-types.json tools/tree-sitter-pascal/src/parser.c tools/tree-sitter-pascal/src/tree_sitter/alloc.h tools/tree-sitter-pascal/src/tree_sitter/array.h tools/tree-sitter-pascal/src/tree_sitter/parser.h tools/tree-sitter-pascal/test/corpus/unicode_identifiers.txt tools/tree-sitter-pascal/tree-sitter.json
|
||||
f272 README.md src/daemon/application.c src/daemon/frontend.c src/daemon/frontend.h src/mcp/mcp.c src/mcp/mcp.h src/ui/http_server.c tests/test_daemon_application.c tests/test_daemon_frontend.c tests/test_mcp.c
|
||||
f273 internal/cbm/cbm.h src/pipeline/pipeline_internal.h
|
||||
f274 pkg/scoop/codebase-memory-mcp.json tests/test_version_metadata_contract.sh
|
||||
f275 README.md scripts/memory-core-baseline.txt src/cli/cli.c src/mcp/mcp.c src/pipeline/artifact.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h tests/test_artifact.c tests/test_mcp.c tests/test_pipeline.c
|
||||
f276 internal/cbm/extract_imports.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline_internal.h tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f277 internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f278 src/foundation/compat.c
|
||||
f279 internal/cbm/cbm.c src/cli/cli.c src/foundation/compat.h tests/test_extraction.c tests/test_platform.c
|
||||
f280 src/mcp/mcp.c src/pipeline/pipeline.h src/pipeline/pipeline_internal.h tests/test_mcp.c
|
||||
f281 internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f282 THIRD_PARTY.md internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_usages.c internal/cbm/helpers.c internal/cbm/helpers.h internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/objectscript_routine/objectscript_common.h internal/cbm/vendored/grammars/objectscript_routine/parser.c internal/cbm/vendored/grammars/objectscript_routine/scanner.c internal/cbm/vendored/grammars/objectscript_routine/tree_sitter/array.h internal/cbm/vendored/grammars/objectscript_udl/objectscript_common.h internal/cbm/vendored/grammars/objectscript_udl/parser.c internal/cbm/vendored/grammars/objectscript_udl/scanner.c internal/cbm/vendored/grammars/objectscript_udl/tree_sitter/array.h scripts/vendored-checksums.txt tests/test_extraction.c
|
||||
f283 internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_unified.c internal/cbm/extract_usages.c internal/cbm/helpers.c internal/cbm/helpers.h tests/test_extraction.c
|
||||
f284 src/daemon/application.c src/foundation/log.c src/foundation/log.h src/mcp/mcp.c src/ui/http_server.c tests/test_log.c tests/test_mcp.c
|
||||
f285 internal/cbm/extract_defs.c scripts/memory-core-baseline.txt tests/test_extraction.c
|
||||
f286 README.md docs/CONFIGURATION.md scripts/test.sh src/main.c tests/test_in_process_mcp.py tests/test_in_process_mcp.sh tests/test_runtime_isolation_contract.sh
|
||||
f287 internal/cbm/extract_imports.c scripts/memory-core-baseline.txt src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline_internal.h tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f288 internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c scripts/memory-core-baseline.txt src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f289 internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c scripts/memory-core-baseline.txt src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f290 src/cli/client_adapter.c src/cli/client_adapter.h tests/test_agent_clients.c
|
||||
f291 internal/cbm/extract_usages.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/rescript/scanner.c scripts/vendored-checksums.txt src/discover/discover.c src/discover/discover.h src/discover/language.c tests/test_complexity.c tests/test_extraction.c tests/test_language.c
|
||||
f292 Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/lsp/php_lsp.c internal/cbm/php_inline_html.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f293 Makefile.cbm internal/cbm/cbm.c internal/cbm/cbm.h internal/cbm/lsp/php_lsp.c internal/cbm/php_inline_html.c tests/test_extraction.c tests/test_php_lsp.c tests/test_pipeline.c
|
||||
f294 src/daemon/bootstrap.h src/daemon/runtime.c src/daemon/runtime.h src/main.c tests/test_daemon_version.c
|
||||
f295 scripts/memory-core-baseline.txt src/cli/agent_profiles.c src/cli/agent_profiles.h src/cli/cli.c tests/test_cli.c
|
||||
f296 src/foundation/mem.c src/foundation/mem.h src/pipeline/pass_parallel.c tests/test_parallel.c
|
||||
f297 internal/cbm/extract_defs.c internal/cbm/helpers.c internal/cbm/lang_specs.c tests/test_extraction.c
|
||||
f298 src/mcp/mcp.c
|
||||
f299 README.md THIRD_PARTY.md docs/index.html docs/llms.txt internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_usages.c internal/cbm/grammar_st.c internal/cbm/lang_specs.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/st/LICENSE internal/cbm/vendored/grammars/st/parser.c internal/cbm/vendored/grammars/st/scanner.c internal/cbm/vendored/grammars/st/tree_sitter/alloc.h internal/cbm/vendored/grammars/st/tree_sitter/array.h internal/cbm/vendored/grammars/st/tree_sitter/parser.h pkg/chocolatey/codebase-memory-mcp.nuspec pkg/npm/README.md scripts/new-languages.json scripts/package-release.sh scripts/vendored-checksums.txt server.json src/discover/language.c src/discover/userconfig.c src/mcp/mcp.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_manifest.c tests/repro/repro_language_registry.c tests/test_extraction.c tests/test_grammar_labels.c tests/test_grammar_regression.c tests/test_language.c
|
||||
f300 README.md scripts/memory-core-baseline.txt src/cli/cli.c src/mcp/mcp.c src/pipeline/artifact.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h tests/test_artifact.c tests/test_mcp.c tests/test_pipeline.c tests/test_store_arch.c
|
||||
f301 internal/cbm/cbm.h src/mcp/mcp.c src/pipeline/pass_calls.c src/pipeline/pass_parallel.c src/pipeline/pipeline.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/store/store.c src/store/store.h tests/test_mcp.c tests/test_pipeline.c
|
||||
f302 THIRD_PARTY.md internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_usages.c internal/cbm/helpers.c internal/cbm/helpers.h internal/cbm/lang_specs.c internal/cbm/vendored/grammars/MANIFEST.md internal/cbm/vendored/grammars/objectscript_routine/objectscript_common.h internal/cbm/vendored/grammars/objectscript_routine/parser.c internal/cbm/vendored/grammars/objectscript_routine/scanner.c internal/cbm/vendored/grammars/objectscript_routine/tree_sitter/array.h internal/cbm/vendored/grammars/objectscript_udl/objectscript_common.h internal/cbm/vendored/grammars/objectscript_udl/parser.c internal/cbm/vendored/grammars/objectscript_udl/scanner.c internal/cbm/vendored/grammars/objectscript_udl/tree_sitter/array.h scripts/vendored-checksums.txt tests/repro/repro_call_node_behaviors.c tests/repro/repro_call_node_manifest.c tests/test_extraction.c
|
||||
f303 internal/cbm/lsp/ts_lsp.c tests/repro/repro_issue514.c tests/test_ts_lsp.c
|
||||
f304 src/cli/cli.c src/cli/cli.h src/foundation/str_util.c src/foundation/str_util.h src/pipeline/pass_cross_repo.c tests/test_cli.c
|
||||
f305 internal/cbm/result_spill.c internal/cbm/result_spill.h tests/test_extraction.c tests/test_main.c
|
||||
f306 internal/cbm/lsp/ts_lsp.c tests/test_pipeline.c tests/test_ts_lsp.c
|
||||
f307 tests/test_watcher.c
|
||||
f308 tests/test_daemon_ipc.c
|
||||
f309 src/daemon/bootstrap.c src/daemon/bootstrap.h tests/repro/repro_main.c tests/test_daemon_bootstrap.c tests/test_helpers.h tests/test_main.c
|
||||
f310 src/cli/cli.c src/cli/cli.h tests/test_cli.c
|
||||
f311 internal/cbm/extract_imports.c scripts/memory-core-baseline.txt src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c
|
||||
f312 src/pipeline/pass_definitions.c tests/test_parallel.c
|
||||
f313 internal/cbm/extract_usages.c tests/repro/repro_lexical_binding_precision.c tests/test_extraction.c
|
||||
f314 internal/cbm/lsp/py_lsp.c tests/test_py_lsp.c tests/test_py_lsp_scale.c
|
||||
f315 src/discover/discover.c src/discover/discover.h src/discover/language.c tests/test_discover.c tests/test_language.c
|
||||
f316 internal/cbm/lsp/py_lsp.c internal/cbm/lsp/py_lsp.h tests/test_py_lsp.c
|
||||
f317 internal/cbm/extract_defs.c src/store/store.h tests/test_extraction.c
|
||||
f318 scripts/memory-core-baseline.txt src/cli/activation_transaction.c src/cli/activation_transaction.h tests/test_activation_transaction.c
|
||||
f319 internal/cbm/lsp/py_lsp.c src/pipeline/lsp_resolve.h tests/repro/repro_lsp_go_py.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_py_lsp.c
|
||||
f320 internal/cbm/cbm.h internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c scripts/memory-core-baseline.txt src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f321 internal/cbm/cbm.h internal/cbm/extract_calls.c internal/cbm/extract_defs.c internal/cbm/extract_imports.c internal/cbm/extract_unified.c internal/cbm/helpers.c scripts/memory-core-baseline.txt src/pipeline/pass_calls.c src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_pkgmap.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_edge_imports.c tests/test_extraction.c tests/test_pipeline.c tests/test_registry.c
|
||||
f322 src/pipeline/pass_definitions.c src/pipeline/pass_parallel.c src/pipeline/pass_tests.c src/pipeline/pipeline.h src/pipeline/pipeline_incremental.c src/pipeline/pipeline_internal.h src/pipeline/registry.c tests/test_pipeline.c tests/test_registry.c
|
||||
f323 src/pipeline/pass_calls.c src/pipeline/pass_lsp_cross.c src/pipeline/pass_parallel.c src/pipeline/pass_semantic.c src/pipeline/pass_usages.c src/pipeline/pipeline.h src/pipeline/registry.c tests/test_pipeline.c
|
||||
|
@@ -0,0 +1,776 @@
|
||||
# Every PR push of September 2026 (one pr.yml run each) with the tier the
|
||||
# lane analysis assigned it (sel_classify.py) and the tier the selector must
|
||||
# produce. They differ only where a deviation is documented in `note`:
|
||||
# makefile-by-name the analysis classified Makefile.cbm by its DIFF
|
||||
# (source registration / test flags -> no tier of its
|
||||
# own); a file list cannot show that, so the selector
|
||||
# treats every Makefile.cbm change as a build-flag
|
||||
# change: T5-core, full. Deliberately conservative.
|
||||
# Columns (TAB): run_id, pr, head_sha, fileset, analysis_tier, expected_tier, note.
|
||||
33464275799 1697 22f90fe883f641c2c460a210a1de3a8df74c4285 f001 T0a-docs T0a-docs
|
||||
33464725171 1986 9d91891cb5dc6b9d67a0d4919d44d6346799af23 f002 T5-core T5-core
|
||||
33484752078 1939 e11d5900cd8735fd2ff5ca5308bc028fbe5ed2a5 f003 T3-mid T3-mid
|
||||
33492041913 1657 7f158e950f874bea6d9530eba16f44b0032f5cac f004 T5-core T5-core
|
||||
33512870122 1941 35efca813cb2e280fd98168e037cc2a098e365c3 f005 T3-mid T5-core makefile-by-name
|
||||
33512876012 1971 a5af586d9275249ff9ab8fd7504c9a1d12f7ef85 f005 T3-mid T5-core makefile-by-name
|
||||
33512878163 1968 9e954104ff5c23bbbb05eccce024b74e6e0a1a6a f006 T5-core T5-core
|
||||
33525282952 1739 2a2e817bbc11c64755e9d9e3c2e3bb951cf29083 f007 T4-platform T4-platform
|
||||
33525334366 1817 e7bf16ac616d3d04733caa075a59902fa204d664 f008 T4-platform T4-platform
|
||||
33529411666 1784 fad564e1e1ad683657ae604bec2c5f0852f19151 f009 T4-platform T4-platform
|
||||
33530652927 1992 abd82a108b459d70a923d7b4a43651fbeb835912 f001 T0a-docs T0a-docs
|
||||
33530742647 1697 22f90fe883f641c2c460a210a1de3a8df74c4285 f001 T0a-docs T0a-docs
|
||||
33533600441 1993 da61c81af243e3277e3089fb474ee5c08fe5c148 f010 T2-extract T2-extract
|
||||
33533605337 1697 4b844b7d9e782ff26284248c072578215cda3d2f f001 T0a-docs T0a-docs
|
||||
33541770974 1697 957b27fca50d2c3b1ac86d771733f39612bae841 f001 T0a-docs T0a-docs
|
||||
33542424474 1811 fb2e38f5bbd45a2504d9daee09fc4c57a8d7db7a f011 T3-mid T3-mid
|
||||
33542805417 1697 4f3412b78a5fa64ad01cb9ffedea603d101262ec f001 T0a-docs T0a-docs
|
||||
33542807266 1896 50eaeebeaa2edb2ce14cabbf0d128e47f47b9c5b f012 T2-extract T2-extract
|
||||
33542815580 1703 359f7360ddd39dc155637076e76a4e855c27611f f013 T3-mid T3-mid
|
||||
33543233252 1811 b149247a0fd78d912ec906f44c2cf30bae7e5209 f011 T3-mid T3-mid
|
||||
33549536985 1918 738174f74a2fbc2ad53ee9f4b2aed1eea2214d8e f014 T3-mid T3-mid
|
||||
33550775043 1996 5e328814fc99ffe588daae4f0f1e8cf5066e084b f015 T4-platform T4-platform
|
||||
33556880174 808 c19373cd4eba3dc836ccd9b12569cceb9aeab970 f016 T4-platform T5-core makefile-by-name
|
||||
33557912833 808 909d01dd80de2c2d2b3518b60e33901dd4f284d0 f016 T4-platform T5-core makefile-by-name
|
||||
33558106767 808 4b33971df0796fe841df867ca202f28a59a5824f f016 T4-platform T5-core makefile-by-name
|
||||
33558842421 1697 40e5e373ab27bba3129405f346aa5937f9c93850 f001 T0a-docs T0a-docs
|
||||
33558848235 1895 230f42454d76e103209e3baa54f6b7bf563cd2bd f017 T2-extract T2-extract
|
||||
33560684540 1998 45b97287c75a0d2f49a28ac900c2dc00d518e594 f014 T3-mid T3-mid
|
||||
33564199875 1986 b2ac3c58a99c534d4882674b5651deb61296be42 f018 T5-core T5-core
|
||||
33564348882 1697 40e5e373ab27bba3129405f346aa5937f9c93850 f001 T0a-docs T0a-docs
|
||||
33564426767 1976 82e1de8cef4121db80c53f4e144530dcf5dcb747 f019 T5-core T5-core
|
||||
33565344188 1876 9d4ac2b98b9c19633ce2cbf8d9c6bf9edc57bba6 f020 T4-platform T4-platform
|
||||
33569142503 808 89f1edcb50ecdec1e0db64002f1d8eed07164d46 f016 T4-platform T5-core makefile-by-name
|
||||
33569301401 1999 2641ec858dff11307d42e60e8e3dccc627d02f0e f021 T2-extract T2-extract
|
||||
33571644044 1875 25a7aacc87250bff752ff61cd33e9236be5d1310 f014 T3-mid T3-mid
|
||||
33571903536 1877 62b44519c68b403ea44d9a85ba8ee74cddee66e6 f022 T3-mid T3-mid
|
||||
33572811960 1912 63f79365e3217d2ef7a3615567bb96627f3fd6b3 f023 T3-mid T3-mid
|
||||
33574864607 1968 a3e8fa3d08691f050f16d75879fd8a84b73ad76a f024 T5-core T5-core
|
||||
33576560737 1986 e8204092333ee1c38cd681a81abb36f125288894 f018 T5-core T5-core
|
||||
33577534823 1999 a739a2d81b43eb6938d014e005b306ab27e02396 f021 T2-extract T2-extract
|
||||
33578326689 2002 976f312ea24acb8b1840ee5f766f3e2588af333e f014 T3-mid T3-mid
|
||||
33578871318 1723 b769ad6324f866a580d731f5579a9d076cc45cf0 f025 T5-core T5-core
|
||||
33580573384 1832 844a36b78aa10f16338d24a1a819ca9a31525129 f026 T4-platform T5-core makefile-by-name
|
||||
33586571817 1836 2f37f61e8848f31f308bce8623f66dc2fdb50619 f027 T3-mid T3-mid
|
||||
33588509339 1824 fd73c347fbcfd7f66c210577b4d713fc6ce7780b f028 T3-mid T3-mid
|
||||
33590570368 1862 0e8fc16abce48346d07af78acf3293a805085e23 f029 T4-platform T4-platform
|
||||
33593858493 1874 c0173a3362878e3cb9e7bf9b65c7ab79b46e73ef f030 T4-platform T4-platform
|
||||
33593864739 1871 4772f4715172173559ba3f9130ddd90df4416ed7 f013 T3-mid T3-mid
|
||||
33596260489 1779 98e386e58b753bcf63f67aeeacbfcb94961d035c f013 T3-mid T3-mid
|
||||
33599715238 1739 4c1b23477b8e0de27feee74eac11c21a1fd64151 f007 T4-platform T4-platform
|
||||
33600443796 808 53a97f0f74e34a55076ea3d1f1dc0be562b1c7b9 f016 T4-platform T5-core makefile-by-name
|
||||
33600512620 808 003f8f4c2efbc22fe72e2dcab7fec5cfd1b42cd0 f016 T4-platform T5-core makefile-by-name
|
||||
33605426923 1931 5cb90f64adaf9c5a38b591971aa7c09e3d9e958c f031 T2-extract T2-extract
|
||||
33605453120 1936 27d4d34f7973823530922ec5485ce19a64589e22 f032 T3-mid T3-mid
|
||||
33605453184 1907 651ae3c2af1a22ec61a498b39350785aee480973 f033 T3-mid T3-mid
|
||||
33605453306 1913 a6b1a69a7c9a9ba67ac16c88e9c11de3524f87cc f032 T3-mid T3-mid
|
||||
33605454090 1915 fcbd12ee1ba8580c1c83ce7ba797613ff5af00e7 f032 T3-mid T3-mid
|
||||
33608282620 2005 256b3d3722247f451a2213023dc36a1ebd5827af f034 T2-extract T2-extract
|
||||
33610039868 1811 cd8a5ed0d7c5674072f5d422b12302aa6424fda3 f011 T3-mid T3-mid
|
||||
33610440955 1946 4bfb074b2892ec181e6b16dee3ee984af6dda201 f035 T3-mid T3-mid
|
||||
33612193345 2006 349aecba9528427fa635975e246f5b8959b14f30 f036 T3-mid T3-mid
|
||||
33612686565 2002 3d9a6cf5cb7fd39c6b5fb4dfa43011aa70f736ea f014 T3-mid T3-mid
|
||||
33614840408 1947 ec0ca565a6571cad6e1c90914400b8dd4c2098e9 f037 T3-mid T3-mid
|
||||
33615574338 1912 97517a4652f376cb6bf096255e235edf456f86b1 f038 T3-mid T3-mid
|
||||
33615892072 1777 7fedaba331d8fbd241582f3ceeccd70af0ade149 f039 T4-platform T4-platform
|
||||
33616176807 1949 91a3c6a5173de50fb47c4ebc5a8ba448598f7d5a f040 T2-extract T2-extract
|
||||
33617264651 1913 dbba7573016e17f20fe5afaafe5cc2cddd98744e f041 T3-mid T3-mid
|
||||
33617265496 1936 ac46c17fc37f78ae518145e73d51b46ca3013fac f042 T3-mid T3-mid
|
||||
33617265877 1915 adff62e7f20dd439129d17c5dc028c7069af92f2 f042 T3-mid T3-mid
|
||||
33620018950 2008 c36b4fbc446f9085704a6073b81ec743fd4180fc f043 T2-extract T2-extract
|
||||
33620040078 2009 592894a4387a50e1d128ff6a2695ff48f8cc32a5 f044 T2-extract T2-extract
|
||||
33641828637 2013 40f2722d6166c1f97fa1667c1e63e8c97e6f05f0 f045 T2-extract T2-extract
|
||||
33653455785 1809 88269ac2ee70ab88f3ee8d84bd509399e3a8f3d6 f046 T4-platform T4-platform
|
||||
33654490848 1809 b026a6907807f7a411e7da4de45d0cedc2e93dc9 f046 T4-platform T4-platform
|
||||
33656319321 1819 64ea496a7b3623f5f8b28343aba99535b282d86a f047 T0b-nonproduct T0b-nonproduct
|
||||
33658603433 1773 572725e61a00b9deda7407971dc9290477170515 f048 T2-extract T2-extract
|
||||
33658798739 1811 3f08fac5139a4acd3f393e9ca78538c1ba8b94d7 f011 T3-mid T3-mid
|
||||
33663175876 2016 0e3c9a3d52fbdd234db180894e94f434efce3505 f049 T4-platform T4-platform
|
||||
33663200103 2017 1e92269d7562d89a816271c4d99904ee53a94606 f050 T4-platform T4-platform
|
||||
33669707175 2016 e0785b353af1af3812d5d14cad55c839efc5749d f049 T4-platform T4-platform
|
||||
33683309620 1649 ff8714662728f55a5ff368462fd8daedc80b89a6 f051 T4-platform T4-platform
|
||||
33687771558 1832 69a9c9005a6d81889cc7fe5e0355e610d2a08e6a f026 T4-platform T5-core makefile-by-name
|
||||
33688645901 1723 83ca40ced11f51e479d4d868ed167b47cfe993d9 f025 T5-core T5-core
|
||||
33700059940 1896 c4e902849f8062e37a0f0ddaed1c6cc042780cb3 f012 T2-extract T2-extract
|
||||
33700061299 1976 7b528b3247257fdc8d503a4af617007bbe3f8454 f012 T2-extract T2-extract
|
||||
33702992083 1874 4529db7eb56c5793227bdce34723827030796a8b f030 T4-platform T4-platform
|
||||
33702997094 1871 2283bcb9eb85b17863786608a0c41b0937521b29 f013 T3-mid T3-mid
|
||||
33703360466 1998 41e649281bb9b27ead5d6ec223a0f0c78924c8f8 f014 T3-mid T3-mid
|
||||
33705255783 1918 426e415f5427f2fb2e9c6f918cc7cc9abd9c109b f014 T3-mid T3-mid
|
||||
33705617151 1881 57ef0a6d8662e9854dc5711922f801c3da381071 f052 T3-mid T3-mid
|
||||
33706085211 1897 2c76563a137876b110cec15b9b6fa08d35069f73 f053 T3-mid T3-mid
|
||||
33706763915 1880 6b801eec68d6465aad2020de438e629cead99f5c f054 T4-platform T4-platform
|
||||
33708048488 2019 ad04339030bbfea4e77139bcfa9a87356c1e0e5b f055 T1-tests T1-tests
|
||||
33727721488 2022 c846ed7b37331696543e71a70a8e5c8dd301467d f056 T4-platform T4-platform
|
||||
33731062466 1874 c38d19139ad9657ab563951ccbf83dcbd1bb2435 f030 T4-platform T4-platform
|
||||
33731071275 1871 adb49b3a28826e6eef871b1777b63668199c0463 f013 T3-mid T3-mid
|
||||
33732861010 1811 da09b73bc0c03f3909a0153d75b8479220c31e2c f011 T3-mid T3-mid
|
||||
33739760521 1811 e79a7d73b45644d86ab3ca3c7af84aa04aebe75f f011 T3-mid T3-mid
|
||||
33740406928 2024 1770d68a2c934501b889e625dc94ef933e3a1815 f057 T3-mid T3-mid
|
||||
33745916176 1811 bbecc9b9e92af203a03433e8cb6968e1e8d21cb2 f011 T3-mid T3-mid
|
||||
33751987784 1996 7c4b5fc8a7c9ed2f1cf3b675bb821dbbccf2c1bb f015 T4-platform T4-platform
|
||||
33752032210 1811 b64e2170009159a5111b1339312512f4e66b1b57 f011 T3-mid T3-mid
|
||||
33754008677 1874 956489dcf88d0fd571efc493e6a93bb1d96760c7 f030 T4-platform T4-platform
|
||||
33757191193 1767 06576511ce84b54b2177a75ae82c3af7dc897160 f058 T4-platform T4-platform
|
||||
33758229230 2026 73f4a432e4ac1d575635a490d96e0f5ea588344d f059 T4-platform T4-platform
|
||||
33763283492 2027 0ac290e2e243148b9e84bb682cf1101fe38bc6e3 f060 T4-platform T4-platform
|
||||
33765101593 2028 1d38be3331576f50ce555d99717974060a6a16a8 f061 T5-core T5-core
|
||||
33765957247 2029 be66a2d2c6f1538a4a831eb2c91622997be5b5ec f062 T4-platform T4-platform
|
||||
33771464451 1809 e65378e6a5faf49abd7a0987994d7c98a9f834ad f046 T4-platform T4-platform
|
||||
33775413774 1809 d402623c20e1dbff62e560a865fd4955798dfbfe f046 T4-platform T4-platform
|
||||
33779258079 2030 3507d9555d73aa746cde23139588081c6c02d2e2 f063 T4-platform T4-platform
|
||||
33799475629 1819 b25b1f65b402ba5395da846f05a306746055594f f047 T0b-nonproduct T0b-nonproduct
|
||||
33799483900 1918 74700067c48b967edc4ef723b658a0fbb04f291c f014 T3-mid T3-mid
|
||||
33799487158 1880 e63292a8d4036fc2618ecbee973bd6cccaaaffc4 f054 T4-platform T4-platform
|
||||
33799492841 1881 4d4af28672729a5c1326974684728c9ee150926a f052 T3-mid T3-mid
|
||||
33799495512 1897 429141261daefc04fc94f35300f7dbd899ed28f2 f053 T3-mid T3-mid
|
||||
33799501343 1767 8a74d814e5149be17f2fc70f9a59d0c166604273 f058 T4-platform T4-platform
|
||||
33799505794 2024 380b1c5af1d5f3e94248752266031b3035080793 f057 T3-mid T3-mid
|
||||
33799508762 1811 f47cd47afe9e7e144f3c6d49df37c5b53d3d1cdb f011 T3-mid T3-mid
|
||||
33809990745 2040 12f5e2c3636b694cbcc76b197815a983349fff5a f064 T4-platform T4-platform
|
||||
33816056229 2041 588cd5543a617c73646be61de6926b9ac7004064 f055 T1-tests T1-tests
|
||||
33816348076 2042 9933548586760fd81f2cf76673ec42c338ff62eb f065 T0b-nonproduct T0b-nonproduct
|
||||
33818108315 2043 7109e5e7b013f43230f735f93043ae608be5d98f f066 T4-platform T4-platform
|
||||
33831172851 2005 ec28e66434bc03ec33dcd05563b3bfd7f58efe86 f034 T2-extract T2-extract
|
||||
33849949280 2047 9104feb67f78959e64ddff78d236e0dea475a434 f067 T4-platform T4-platform
|
||||
33850387435 1976 aa3627d3bfb38627d700043a599622bb0e80ef2f f012 T2-extract T2-extract
|
||||
33850532415 1709 2efb7a6323f992d5c407cc85e37b6d167c184ca1 f068 T2-extract T2-extract
|
||||
33850538524 1721 dc703dad4a706a0f2c8130a869922677dba09ae3 f044 T2-extract T2-extract
|
||||
33850542851 1730 90ba8990764674b2eeaca6fa372c654a63a04f51 f069 T2-extract T2-extract
|
||||
33864581134 2048 f404dfbc7ae47f9912b18a0554a5e5d91f65be1a f070 T0b-nonproduct T0b-nonproduct
|
||||
33868849245 1816 2ea73059f894df564e21be409908c2b5d3d53b37 f071 T4-platform T4-platform
|
||||
33879400189 1996 8a1f2503c33bdd9a4d87782493af5640a505594f f015 T4-platform T4-platform
|
||||
33879404796 2041 e74f041b3387e89fc3d3bc727cae772516099e54 f055 T1-tests T1-tests
|
||||
33879411345 1809 b435bd87b6d0e6a8da43c2964829927fff331f5c f046 T4-platform T4-platform
|
||||
33879415252 2002 c6ff57df0c216dc0fc86442f111607d8b3bd7ef8 f014 T3-mid T3-mid
|
||||
33879421262 1741 fbda6bacf78b47e4c498dfb44ae653f9d10a4add f072 T0b-nonproduct T0b-nonproduct
|
||||
33879427336 1742 e63bc6f06a03e93fad9e962c9b9aaf4b0309306e f073 T1-tests T1-tests
|
||||
33879430001 1976 6f7a4af4a3a1716a5aeb5cae60a112e2231195a5 f012 T2-extract T2-extract
|
||||
33879554682 2043 7e47043acce9bdfd87e7becc7fe7a348b02753ec f066 T4-platform T4-platform
|
||||
33883643915 2042 6b0c44afcfeae65eed82e2dbbd58f00b043f21f6 f065 T0b-nonproduct T0b-nonproduct
|
||||
33883715988 1649 f195c6aa81f425882626cca1186116ec64a0143e f051 T4-platform T4-platform
|
||||
33884431041 1649 89186a26bcc37ae51f943b785270aa66716c832c f051 T4-platform T4-platform
|
||||
33885636468 1933 8d46d2586255fc85e35f00ff8b814424b140e623 f074 T4-platform T4-platform
|
||||
33887517545 2051 e2195dcb8f6fba33b72da60c1f3fffc83cdcd714 f075 T3-mid T3-mid
|
||||
33891234168 1874 8069c7cd4c73ff7ea4513d38d7dfd08c350472a4 f030 T4-platform T4-platform
|
||||
33892259683 2055 36bee9039848829c7d2271c44a46515cdaa8d870 f076 T5-core T5-core
|
||||
33892321505 2056 24f54c0eb64c516941abee2ad0d7de758b34ac86 f077 T2-extract T2-extract
|
||||
33892540012 2055 132e8fc32309e843ccd97138c8f032e317333673 f076 T5-core T5-core
|
||||
33910645341 2051 1067e519c04f6273dd6e2b95b16bd0a9db4c3056 f078 T3-mid T3-mid
|
||||
33913569180 1851 20c7dd3dbbba55770b82a99f6fcec6120f362873 f079 T4-platform T4-platform
|
||||
33914255147 1914 7b92d2a6218936767b112081b9aa4e79d5b81dd9 f080 T5-core T5-core
|
||||
33917205956 1849 39c74bdd5c8b5dc3bcd43f962195add0ba794150 f081 T4-platform T4-platform
|
||||
33918309198 1796 ad479b61e9f23c01606f42c7d44ca174f104a2c8 f082 T4-platform T4-platform
|
||||
33921848127 1597 335279ee403922c0b84c3972c3fc4782ffe878bc f083 T4-platform T4-platform
|
||||
33923008470 1597 0094133cca7d92e587c4d876ccdb37140515fa7c f084 T4-platform T4-platform
|
||||
33923354326 1766 797a11a15eff6154428aeb268c0b55edb5647a7a f085 T3-mid T3-mid
|
||||
33924300711 2059 e0f37d7cb305cd2ff1d709c3ba1198cf950e83df f086 T4-platform T4-platform
|
||||
33924381537 1597 3cd0043d0a3dc282c6a36ae0a262ebe02a4cec0f f084 T4-platform T4-platform
|
||||
33932138466 2060 3a8b8e82120872a56ca4d0157260569d90abe85c f087 T4-platform T4-platform
|
||||
33934626037 1784 4cf2acf2b7483ffa0fe3918f1a38e6d72d08b743 f009 T4-platform T4-platform
|
||||
33935475331 1597 b900338a71c7395ade83c4e371ca277f2cb5498b f084 T4-platform T4-platform
|
||||
33940288019 2062 9b4a3bc91e2f4d50e00deccf236c6b0db177f4eb f065 T0b-nonproduct T0b-nonproduct
|
||||
33941972045 1597 10d05b38a63ff1efa62bfe2d419b01c72bcb2bc9 f084 T4-platform T4-platform
|
||||
33944160492 1597 a15d99e39fd9ea04722f064ed02dc726b13d2943 f084 T4-platform T4-platform
|
||||
33947308282 1874 9ae4ba7648101603eada6e7cd54c6cdc284617f6 f030 T4-platform T4-platform
|
||||
33947974349 2063 f8132b42ac9ee1fde464866b5f3b08939528f267 f088 T0b-nonproduct T0b-nonproduct
|
||||
33948072527 2064 3979f12a749232e1f934949f58f68c83a8b66014 f089 T0b-nonproduct T0b-nonproduct
|
||||
33949446878 1597 9427f435158662074d6576053a6675a403696543 f084 T4-platform T4-platform
|
||||
33951152502 2065 7dfd2c064911a7f2b246b7edd610fab4f993240c f090 T3-mid T3-mid
|
||||
33951945765 2066 4b73a518d720bbc8773970402ca96f5661726b78 f013 T3-mid T3-mid
|
||||
33963617997 1874 41f52a0f3b948468c468ddcca36489eb5f42fff3 f030 T4-platform T4-platform
|
||||
33970672289 2063 3b173288feafebac3f0845f53e481f69adcd70b1 f088 T0b-nonproduct T0b-nonproduct
|
||||
33971609329 1784 b9da6dca03126ef0c8cacccb3321dd5d69ccf34d f009 T4-platform T4-platform
|
||||
33979110285 1822 7fbaf02904363d38c88f9b749b4523a614feecd5 f091 T3-mid T3-mid
|
||||
33979848957 1759 683d4bc312625a3d26819635673529ab02a0d0e1 f092 T2-extract T5-core makefile-by-name
|
||||
33980369725 2056 4f82a1e56f0eef435fed0eac5c8dd06b5b30c2fc f077 T2-extract T2-extract
|
||||
33980647419 2065 3e1a22897ccea3404b95126112009e8b1a719a39 f093 T3-mid T3-mid
|
||||
33980897876 2065 73c509f4246b0f97ca772b6d9a3159181a7fa6b6 f093 T3-mid T3-mid
|
||||
33980899537 2066 c4bec4a6b38ed66ccd97df06490e051d3d9d81cd f094 T1-tests T1-tests
|
||||
33982832791 2067 b7e18c8940d2bb58d85339d27da0754e5aba92e5 f095 T3-mid T3-mid
|
||||
33984123524 1716 439659b333895a49174dca1fe4343f356616210c f096 T5-core T5-core
|
||||
33985769258 1894 b49c7a736c0789069e5c2d07a301143e382af53c f097 T4-platform T4-platform
|
||||
33986188532 1739 b51411753e9423f1c67f810ecf54f9fce903cefc f007 T4-platform T4-platform
|
||||
33986691207 2069 3b9052c955a9aafbb3baff874a93bba19afeaace f076 T5-core T5-core
|
||||
33990039141 1941 b04f54506ab42eddcbd89232a0d47d8355edd596 f005 T3-mid T5-core makefile-by-name
|
||||
33990592318 1971 f89f01e9456c8241a28fb909d930ec8d88b23239 f005 T3-mid T5-core makefile-by-name
|
||||
33993950950 2059 3007dbf8426764a92ace4da111836b86bec6b508 f086 T4-platform T4-platform
|
||||
34001348423 2056 e384f88f90d1f13ac95220f15de72e888a654c98 f077 T2-extract T2-extract
|
||||
34001350759 1822 eb3bd818b597b9c1f317906b350cbf323f2a4995 f091 T3-mid T3-mid
|
||||
34001352917 1716 26682d58c7595c4166d7b7d274885115f2dac324 f096 T5-core T5-core
|
||||
34001355464 1759 b35f73123302d8d101323c3621e6fe755d4970c6 f092 T2-extract T5-core makefile-by-name
|
||||
34001357884 2069 f8951524252b71be8d8d3ea1f86d03eb97d6e9d5 f076 T5-core T5-core
|
||||
34001359042 2066 7da8bf15838c42f8e3f366fc2188ca63f2b61942 f094 T1-tests T1-tests
|
||||
34001473433 1739 9a5e3f36a9580f76211300faa8b7ed5942169b47 f007 T4-platform T4-platform
|
||||
34002279390 2070 c1b9c451c7f124936e635835ca4dd0cf8bf7d5b3 f098 T4-platform T4-platform
|
||||
34003689551 1860 a44732b6f8ee6409cd12b135f14867f1b5ecab33 f099 T5-core T5-core
|
||||
34010341126 1998 04ba2fa368db12a80091315bfa649626a36145e1 f014 T3-mid T3-mid
|
||||
34011344933 1968 17b6dfe68b65ae7fae797487c69efdf110b0cbae f024 T5-core T5-core
|
||||
34012479012 2065 c72fe3222a5f3caac2e87636252931f3cfce2619 f093 T3-mid T3-mid
|
||||
34012788868 2041 874a972fb7c4eb06e739dce0a390fffb85d31f83 f055 T1-tests T1-tests
|
||||
34012788874 1976 eb80a4ecff04d8bfacef20e1aa2ee29445a782ff f012 T2-extract T2-extract
|
||||
34014013202 1874 a3500040e69af8284e3cac1398e00ae3d00a0524 f030 T4-platform T4-platform
|
||||
34014527172 2065 60449f895dbaedb4e74c4f155a8e214d78486e12 f093 T3-mid T3-mid
|
||||
34018899805 2072 b0f9398d59c0c8a5bb03b80eba84d30e630160ec f100 T4-platform T4-platform
|
||||
34019712929 2072 06644428b6d0a6295414d23d2c0613f29a73e1d7 f100 T4-platform T4-platform
|
||||
34019861168 2072 bae6977e91daf703b812136c70556cefe4e3c283 f100 T4-platform T4-platform
|
||||
34035459918 1968 f3e1253859c299d194c608d91d5c89065ca0c9ff f024 T5-core T5-core
|
||||
34037162475 2073 5abd77f5d9580d75f8600914b46495be21e9a4a5 f101 T1-tests T1-tests
|
||||
34037415003 1971 9540d64c52aecf89ce8b0524b8fea69c2aafb0e9 f005 T3-mid T5-core makefile-by-name
|
||||
34037489952 2056 9cb2e9dd1c9fd4e6b3cb9f939ec46264b05c75ab f077 T2-extract T2-extract
|
||||
34037491375 1716 8ab6bb6edd69a4ec64df07c3910a5c035b42c937 f096 T5-core T5-core
|
||||
34037493874 1739 53f55ebe72870695c15ace98101df5af6eeef38e f007 T4-platform T4-platform
|
||||
34037496632 1759 bb21edbbbba442364b88598af97478a97e62243a f092 T2-extract T5-core makefile-by-name
|
||||
34037543448 2041 99fa463023fe63520d5a23c29f777b9d91d553b7 f055 T1-tests T1-tests
|
||||
34038315061 1941 ddb6533cd9c16e8e2be12aa791521420fb427881 f005 T3-mid T5-core makefile-by-name
|
||||
34038319912 2059 306c02d44e9a464faf68640f183fa6e82fea0907 f086 T4-platform T4-platform
|
||||
34038901925 1971 ede0e3b111054c165b0237124084894b45e0e052 f005 T3-mid T5-core makefile-by-name
|
||||
34039040074 2070 9846c3f1711d557c78426913efaff340e37feb4e f102 T4-platform T4-platform
|
||||
34044235267 2074 6db22aadb6e5a92b9b193608e2ac3f420e461ec3 f103 T3-mid T3-mid
|
||||
34044659567 1998 c8ea1cad0dd34e6f6f8c542014a1a22885f67ee3 f014 T3-mid T3-mid
|
||||
34044858364 2075 086422020f5fd9396b46384b10413cdca6994c96 f104 T4-platform T4-platform
|
||||
34044911261 2065 d550dcbe73558bb773211a0ff9463daff91721fd f093 T3-mid T3-mid
|
||||
34045088264 2076 a08b9ecbbda54793275cbd4e93fecf0ad4ff1c0a f105 T5-core T5-core
|
||||
34063281067 1739 1d35def402f586b4c18bba41de33c881eda8cfa2 f007 T4-platform T4-platform
|
||||
34064340654 2079 e410c86f270a013d79639577d82009698b207e8b f106 T3-mid T3-mid
|
||||
34065487385 1998 98899ba5d62c9a66d038b24f3857b137d03d44b0 f014 T3-mid T3-mid
|
||||
34066407580 1716 e3c0010267e783b4270e0ac7c8ee6332b61aeabb f096 T5-core T5-core
|
||||
34066408930 2076 1b89228d90b32ddd46f494df8755f496f6aba030 f105 T5-core T5-core
|
||||
34066410797 1759 fbe9eae11c237459da50a06ebd83f709d655d491 f092 T2-extract T5-core makefile-by-name
|
||||
34066413612 2065 8eb63ecfe79ea1f33fd911b8913a8b704ecde9d8 f093 T3-mid T3-mid
|
||||
34066415650 1739 76d0382f8f958238119c8e36bbdd204838d15db5 f007 T4-platform T4-platform
|
||||
34066415871 2079 dab58c0701d8806ddac2783949f1a478225f80bb f106 T3-mid T3-mid
|
||||
34066418484 1998 a143b0882d0457f2f56b2d734128f914325d555f f014 T3-mid T3-mid
|
||||
34068996256 2082 16a739d32314faeee5539386a92afd2363dc1879 f107 T3-mid T3-mid
|
||||
34069613782 2083 77a0c7d94156a43f19047330c7356be4ee12cba5 f108 T3-mid T3-mid
|
||||
34071276309 1874 4ef5672c53777f9dd316510dacfa3b88e2166fe2 f030 T4-platform T4-platform
|
||||
34074515665 1894 088fedb2d25b869621fd93db05b2f3aa69e30e65 f097 T4-platform T4-platform
|
||||
34074587051 1894 4d3d06ac2aba832ba774cad396a3f445b290f0b4 f097 T4-platform T4-platform
|
||||
34081576339 1971 e5c17de6217ec5938d18d8c51e2c0d7ead116a19 f048 T2-extract T2-extract
|
||||
34081831353 1968 2b1dafd83d6df8b0ad8a064e1b23bc27ecf4de7d f109 T5-core T5-core
|
||||
34085524473 2084 4e0b7e1d168744438f0602735c4fee3970f3caf1 f008 T4-platform T4-platform
|
||||
34086907163 2082 d8dbf01c3f90257d98c7e23a21c5c6f7f64d3a4d f107 T3-mid T3-mid
|
||||
34090735558 1723 c8e7f7e3cc26f1faf8eaff5889d6ff0abd75ae9e f025 T5-core T5-core
|
||||
34090831729 1724 70b411d5cf392e1ad6528e9f78d391304368ee30 f110 T5-core T5-core
|
||||
34090999743 1725 8152277aebdd974badfbe0ffce13dfd231a23732 f111 T5-core T5-core
|
||||
34091031555 1725 980934b2a2cf4c1dd32163894b500e0c96587bc6 f111 T5-core T5-core
|
||||
34091168476 1726 fa11ee4193bb7537e18865fb649d1e73e8be1db9 f111 T5-core T5-core
|
||||
34091245271 1727 f9a7e1fece6264d1677ed5dbbc2894553f7ecebf f112 T5-core T5-core
|
||||
34091261353 1728 4602cb100fb0dd404d49531158204fa3f00d9ac2 f112 T5-core T5-core
|
||||
34091378540 2072 c72cb8bb950019b5451325e52940da956250f0d0 f100 T4-platform T4-platform
|
||||
34091504731 2072 bc945a4d6c6716ee22d314b6b53a7d50ed007081 f100 T4-platform T4-platform
|
||||
34091593211 1725 ca6a5d5bc408d59af6ae9ca1edab0d169664092c f111 T5-core T5-core
|
||||
34091642820 1726 f870ce4cd80c0882568ba19438a3cb6ebb2e43a4 f111 T5-core T5-core
|
||||
34091649819 1727 8d4c416579db0148869f3f5c7ffed3e0e90c4417 f112 T5-core T5-core
|
||||
34091656935 1728 37e213860e8f8390f54880bfff717345d2da5689 f112 T5-core T5-core
|
||||
34091819685 2022 84fffefa48cbb60ca1533d2433921787c8ea1031 f056 T4-platform T4-platform
|
||||
34092213233 2072 838fb7090f3a08920363e9ee793824b4d6202bef f100 T4-platform T4-platform
|
||||
34098390879 2085 9f57edfa576072398728eda2c6fc2585d2ecb063 f113 T3-mid T3-mid
|
||||
34109049475 2086 b9d550b6f3e1c08ce6cf59140601205d609dcd52 f114 T5-core T5-core
|
||||
34109055503 2087 45b28f0b11d3e74e28e492c6c234a0f7ed0b30e4 f115 T5-core T5-core
|
||||
34113927999 2086 2082b2d2890ceb007e04b9ba77129d14a8f4df52 f114 T5-core T5-core
|
||||
34113933101 2087 9a7445e93de55eaca53855057ce6ce1a6f234564 f115 T5-core T5-core
|
||||
34115081932 2073 8e29e1568f442ed9422e0fcd24d2594ae0ba7893 f101 T1-tests T1-tests
|
||||
34118689934 2089 dc34e63a0a0aa29cb70456b692733124aba65350 f046 T4-platform T4-platform
|
||||
34126118251 2090 6b56d78e1bbb5715c9d0c715453244187bd299bc f116 T2-extract T2-extract
|
||||
34132427862 2073 aec9bfe3751d8c1f2eb2cb48d8fe584b9d3fd973 f101 T1-tests T1-tests
|
||||
34134175243 1725 8d57fa45ae548e23b11083072447a62ff053da86 f111 T5-core T5-core
|
||||
34134183283 1726 c263f48d0307540b0837726ba5a3c2cd9d2ea012 f111 T5-core T5-core
|
||||
34134191503 1727 94119b3803e276a4cc45f4b92ddaf3d1377e3889 f112 T5-core T5-core
|
||||
34134201351 1728 f588bc374ab91697ccb4467f5df9686ada5cb62c f112 T5-core T5-core
|
||||
34138662959 1784 85482e3c599d454d76ccef5e862a5dc6c38a898e f009 T4-platform T4-platform
|
||||
34139187715 2091 b974adf6ed9b82d8669d1a65b1757816ce0d12df f117 T2-extract T2-extract
|
||||
34139262637 1784 a21562666317f752f9921109a0c70427f3febe04 f009 T4-platform T4-platform
|
||||
34141564461 2092 d1abca20c091b9623187c5cfc61f251e7bd7796e f118 T4-platform T4-platform
|
||||
34143404498 2091 ec56ea8f60f0058bd1cd0fffb38cc0153b99f895 f117 T2-extract T2-extract
|
||||
34143899077 2092 1f08f82adcd979ba2f0efb38a1ccfed13901f76c f118 T4-platform T4-platform
|
||||
34146676636 2092 4f8290aae42ad3f2ca4767089057e74756311743 f119 T4-platform T4-platform
|
||||
34159409001 1561 b939a2d6ee76e482288a0dbbf712cd94d0d57164 f120 T3-mid T3-mid
|
||||
34168455959 2094 7219a24cc692500cdeb5cc192adbd47ac8bb6ee1 f086 T4-platform T4-platform
|
||||
34168458726 2095 d3eb7e9823b43a8e2e5917d2d234367c1f88436d f091 T3-mid T3-mid
|
||||
34168461878 2096 9e1faa7327f8192101f16d8d6d81020937475166 f121 T4-platform T4-platform
|
||||
34180076668 1561 6875483ffe40017f79884872a325e2ff6b37befb f122 T5-core T5-core
|
||||
34180195791 1561 a7941a4d2094aa6bbb0683e3a17371a0463de209 f122 T5-core T5-core
|
||||
34186497464 2097 d653052508998144e39357a37f72a12448c769f1 f123 T4-platform T4-platform
|
||||
34187802611 2098 5f9ac15818e3bbda9e031db54867dfab2b6c531e f124 T2-extract T2-extract
|
||||
34187888044 2099 95f252447de4ae08707b199f02f296f28f8dcb21 f125 T4-platform T4-platform
|
||||
34188664789 2100 2a47a6556cc14dd5ac036b3c2f3225c9925cdeec f013 T3-mid T3-mid
|
||||
34190172584 2060 8622d6a0ed4935aedaa336d216d8b6b269732de5 f126 T4-platform T4-platform
|
||||
34192737413 2098 f6c310c08e6dad38b92e953c85c7c5876f6df839 f124 T2-extract T2-extract
|
||||
34194384003 2097 d9dec5dd3b334a514113670b42732ca9ea4a15de f123 T4-platform T4-platform
|
||||
34194639858 2100 669f6a3741ef7b875aa5eaeda317acf1900bcd33 f013 T3-mid T3-mid
|
||||
34195390569 2101 0c1d6f68dbf3f2c8215ae9c52003d785ac874a22 f127 T1-tests T1-tests
|
||||
34195424010 2060 3c854ae217c4c5d2f48d8aaeb136541bb0d8b52a f126 T4-platform T4-platform
|
||||
34196235240 2103 9f876f70ed13636a8f9a25ba043e8f682c6688f6 f128 T4-platform T4-platform
|
||||
34236364592 2105 22e0335d94552dc1a9d9ef0ee8221e7665608209 f013 T3-mid T3-mid
|
||||
34240237866 1784 18b2f8670c64fd83221617c5e7970fb95af17454 f009 T4-platform T4-platform
|
||||
34244310689 2105 dcb10fe902905926eaf9325bb03254dde9cea6ab f013 T3-mid T3-mid
|
||||
34249699686 1823 b09ef3df3289e74bae129df4df29886fefc75484 f076 T5-core T5-core
|
||||
34249700997 1823 b09ef3df3289e74bae129df4df29886fefc75484 f076 T5-core T5-core
|
||||
34253442941 2109 e65fffc5b1ac269c16d4371fc8c1ccbd102e267a f129 T5-core T5-core
|
||||
34255410324 2109 840de91e384d2ea36dc2360ee12ace26d25b680f f129 T5-core T5-core
|
||||
34257609138 2111 7b4eb4a00ac600060be4836ee4e1bbfa7bbedf24 f130 T4-platform T4-platform
|
||||
34257629348 2112 21591d51168d26f931f64d62edc36013cd1f6249 f131 T4-platform T4-platform
|
||||
34257632907 2113 00060cec854dbd38d7864aeb95a2d8ba1baa8ab2 f132 T4-platform T4-platform
|
||||
34260896139 2110 d40ed1a6bbc968bbddf90abd3b38e74a131a1b5a f133 T4-platform T5-core makefile-by-name
|
||||
34261770203 2113 0567dc70a097f36a953368978d6728b0a412b229 f132 T4-platform T4-platform
|
||||
34261855843 1768 e8dcb28ba719e4272f605f3b961422bb1f3c3fad f055 T1-tests T1-tests
|
||||
34263433901 2110 7bf366f63b3fd5211cec19e483ee2ed27145ca14 f133 T4-platform T5-core makefile-by-name
|
||||
34263688699 2115 013e1f7ec7d4b981a1cd28bf35d5767c195f8447 f134 T4-platform T4-platform
|
||||
34264299704 2116 3a4160b4610f83ea26daf69f1ffe7cb898bf48ad f135 T4-platform T4-platform
|
||||
34273160491 2119 8135be28a764fad4ee55abe466762bbda7ebab74 f136 T4-platform T4-platform
|
||||
34274955966 2116 7e73b48ef155a0e14a6e058be56287232e3976ea f135 T4-platform T4-platform
|
||||
34275249072 2111 91e31211df9dd61459617d857d46b71699e34df8 f130 T4-platform T4-platform
|
||||
34278140735 2110 a3c24a71b4c28e805b912f9f928b6b0b151f9495 f133 T4-platform T5-core makefile-by-name
|
||||
34282381014 2113 ecfb9642e153ffc656d0bc2344b17e9d454ff916 f132 T4-platform T4-platform
|
||||
34288397854 2115 92cb3b9a4bebbc79bd283e447104669ca068ea64 f134 T4-platform T4-platform
|
||||
34315414268 2116 e45d705105a63f2fd9b35ab9e37ffbc640be8231 f135 T4-platform T4-platform
|
||||
34315782814 2124 8134712dc43b159fd863244ce53ba23641f20f8c f070 T0b-nonproduct T0b-nonproduct
|
||||
34327374288 1723 21ae02154a198265d7dbed13af4280aa2b1e5690 f025 T5-core T5-core
|
||||
34327382260 1724 87da4422c34feb4df8ff369dab8d770c33d142eb f110 T5-core T5-core
|
||||
34327387577 1725 d82f760040412d51d424f3c287d277e89d4fb199 f111 T5-core T5-core
|
||||
34327494146 1726 9380b22a24b3ec85482fd209475f5b1641b5f699 f111 T5-core T5-core
|
||||
34327502315 1727 579336cbdaa6493f47ed4de10f64387b6f7f16b8 f112 T5-core T5-core
|
||||
34327526954 1728 9d5cfa53098003cb68bf1812a94ca51f5e046056 f112 T5-core T5-core
|
||||
34379806132 1823 5e5e433d94880c283400215ba79094225a8b33df f076 T5-core T5-core
|
||||
34392601650 2116 9ef7da8cbfba8de5526ccd05e7196f5fa73af8f1 f135 T4-platform T4-platform
|
||||
34392735468 2110 91c63be5d4e41d44e0746ef533af4a0b1967d6b3 f133 T4-platform T5-core makefile-by-name
|
||||
34392998828 2136 557cb0102863e66cbd5cc622509a29a4964d1831 f137 T5-core T5-core
|
||||
34393093741 2137 4fa11d1ab0b5807a7eaed645a6e3675f0663def8 f125 T4-platform T4-platform
|
||||
34408696449 2138 8116672a7e3a02aecb5cb0e6fd9e685cd5066fe1 f138 T4-platform T4-platform
|
||||
34413434571 2139 df579830f3941e5ebaef73dc87d183aee358fbcb f139 T4-platform T4-platform
|
||||
34414749652 2139 618849512d01a3d14fcb704532911f9d02c23436 f139 T4-platform T4-platform
|
||||
34415791973 2139 62bafb76b380d3af1cd1d6228b4afea0082c2a7b f139 T4-platform T4-platform
|
||||
34433093443 2140 a1d11e79087bb1284d6126b02b22c3ce7e72ab07 f140 T4-platform T4-platform
|
||||
34446903088 2089 8f3c15044b8200a15ee1895c5127f4bf117da493 f046 T4-platform T4-platform
|
||||
34456518090 2160 f7b56b900d4726221de2882d5a5d076b29f6758f f125 T4-platform T4-platform
|
||||
34456521294 2161 c8e244d8ff1d51e57c9d757a8e6bad80dc67f107 f091 T3-mid T3-mid
|
||||
34460157213 2159 8d353a573dcc4be7ae69a74685eeb819a5626b5f f013 T3-mid T3-mid
|
||||
34460157936 2158 b5f119c3f27bc3601bc048b16bbfaf57af3908f2 f141 T2-extract T2-extract
|
||||
34464851279 2161 6790e1ac0fc134360cda6544befc1af2e29b9605 f091 T3-mid T3-mid
|
||||
34464853698 2160 b3af44832790fbc6dd6a7e09d18362f55149a4da f125 T4-platform T4-platform
|
||||
34465154636 2164 70926a2956ce986bfacbf6e7ebfd398fd91ff266 f142 T2-extract T2-extract
|
||||
34465653349 2164 9115f5154383bd091e125c956aa4f241ebac1c7b f143 T3-mid T3-mid
|
||||
34470429949 2159 7e111f877b85b79efdd371475d7f71e61174f95e f013 T3-mid T3-mid
|
||||
34484066063 2092 b2bb18cfc79e129f50b5da940413e5735bce09b1 f144 T4-platform T4-platform
|
||||
34491583983 2092 b317506b5a62a3383e763011e5b148a5afd92a89 f144 T4-platform T4-platform
|
||||
34491670375 2092 b037b488be4620a5e92424e7e390ee625cd7e5af f144 T4-platform T4-platform
|
||||
34524166740 2170 5fc107a0a61419faf0a2495ab923aba25a37ddb5 f053 T3-mid T3-mid
|
||||
34528400072 1823 ac75e587425c60154dbcaa7392c0522764c93647 f076 T5-core T5-core
|
||||
34551561701 2171 4842e11b2d9f5e6c2ced39d12dc487bcc7d915be f145 T3-mid T3-mid
|
||||
34595459266 2030 e65bc6b71e32d20aeafb42c00b6097a0e2ded2c0 f146 T4-platform T4-platform
|
||||
34595482014 2030 eeff3f19ff97d60af4f34298aa38a652fe0705b2 f146 T4-platform T4-platform
|
||||
34636198205 2179 6091db7104bcaa0b61566272290e04ce448851a7 f147 T4-platform T4-platform
|
||||
34636557089 2116 f6a29270b72a5373e4d949ed3f4078a40830c57d f135 T4-platform T4-platform
|
||||
34640188970 2180 527016258e0da33ef883966f618609f4cf58abf8 f148 T4-platform T4-platform
|
||||
34649361704 2139 53700ee1c72f1a5c2551a83c74d20278e4ac664b f139 T4-platform T4-platform
|
||||
34649395431 2137 ca36316c6b34a6081de24d69699665a20b16de0e f125 T4-platform T4-platform
|
||||
34649399412 2113 ca86fdb057b3d7f6d31e179ccbe64629fb1b0921 f132 T4-platform T4-platform
|
||||
34649404928 2110 33534653f10c052f2a5c7f1953756aeb10499be9 f133 T4-platform T5-core makefile-by-name
|
||||
34649406520 2115 78a6d19f1eaaa73054639ab0f14c372c53cd1bb3 f134 T4-platform T4-platform
|
||||
34649409769 2111 951b9ab022df391410528fe98769d091c203fc60 f130 T4-platform T4-platform
|
||||
34651276769 2181 c8184ed94887a6b5e130726885b44a07c63a47b7 f091 T3-mid T3-mid
|
||||
34656706072 2182 14bacf8919b64bea19b537b74c894f0f82783bdf f149 T4-platform T4-platform
|
||||
34676336284 2185 eb5e34cfc141454e849daf413f648e73f74c455a f150 T5-core T5-core
|
||||
34676377540 2186 f6281874bba9a78ac7543564ff8f76b5262daeef f151 T0b-nonproduct T0b-nonproduct
|
||||
34676429093 2187 cea35fcc0520a0ba95139bd0210052f09b9cfa20 f150 T5-core T5-core
|
||||
34696991657 2115 9a460b389ce3002ac164605c3627bb86aa3b9be3 f152 T4-platform T4-platform
|
||||
34700514102 2188 734906379f235c07cce960ce6a57746ea3be48bb f153 T5-core T5-core
|
||||
34703996478 2115 fa99ff64eb6579fb15f3aae1aff7da349cb2999a f154 T5-core T5-core
|
||||
34708249832 2189 22cdf7d03db87bba19a76a072dd517dd88a54d40 f155 T5-core T5-core
|
||||
34708361984 2190 8c2692c1ee32284c813c05746e833dea5dfedfe0 f156 T0b-nonproduct T0b-nonproduct
|
||||
34710253103 2189 1ad52f5fb1b3d96255756de01ce30912b1899e09 f157 T5-core T5-core
|
||||
34712092850 2191 27cacee1c2d3022fa0fe915b03ca4310042b7514 f124 T2-extract T2-extract
|
||||
34713347879 2190 044fef7957d48a097b48f4ac3c505aa67de8164f f156 T0b-nonproduct T0b-nonproduct
|
||||
34713405483 2192 9073fe4469bd222ca01bd48249b52bbf7ed5710d f158 T1-tests T1-tests
|
||||
34720983404 2076 2b80b31306b3dbb9ac8bc0ad50372f724f1dd243 f105 T5-core T5-core
|
||||
34725488475 2194 09c0e88a6484c5fb696d7fc7b5a5e3f32b8cba68 f159 T1-tests T1-tests
|
||||
34729075944 1561 e0b9642010cbf003bef9b8b29ba4ebb02b38171c f120 T3-mid T3-mid
|
||||
34729170802 1561 789c60c86bc75b342c52ddedcaf174d12ccdfa73 f120 T3-mid T3-mid
|
||||
34745069386 2195 469c3dd996f23cdb629f1bbd6a751bc0f29a99f2 f160 T1-tests T1-tests
|
||||
34746469011 2195 f9038b84eb3bf82c2dff4cc4ea7f1ec1d8d13f01 f161 T4-platform T4-platform
|
||||
34747474867 2195 d21e6cc762a1816d8c274174c9bba01601b888ee f162 T5-core T5-core
|
||||
34747919526 2195 085017ab2172c2f3d4cd3e27251033ecbc5b814b f162 T5-core T5-core
|
||||
34748079069 2195 ac8f5b8a8137f15e95478be71ee6742e928ba5bc f162 T5-core T5-core
|
||||
34751940360 2073 232764a12295a59f05619a2bd032c8f92af6f17d f101 T1-tests T1-tests
|
||||
34752354822 1716 8d4fed61db63559a0961f11f821db21c26e7d24f f096 T5-core T5-core
|
||||
34752961647 2196 81a8a03c820cb8bccafd0ebac355eede21b5e8bf f163 T5-core T5-core
|
||||
34752965817 2197 823e720982e1bce047a9243bd9b24d9688dadb72 f164 T5-core T5-core
|
||||
34752971903 2198 b5b69af7f8d0d1083974e993f0e9b92a435fa253 f165 T5-core T5-core
|
||||
34753122921 2199 d14bc7051c8a32b2678bf10d5ee711637d606edf f166 T5-core T5-core
|
||||
34758237901 2201 1167972031d427be6773b86ec6dc9903317360d7 f013 T3-mid T3-mid
|
||||
34764451388 2202 7fedfe85c63446d3695b75d463a9b8767655730f f167 T5-core T5-core
|
||||
34769627346 2203 848ce778766d73ab4eb09ba8c98001da8f587fe0 f168 T4-platform T4-platform
|
||||
34769883104 2203 00411ce7bc41995783bfb16aada673515585446d f168 T4-platform T4-platform
|
||||
34769958569 2203 d64c9f2afe6b2add648ee1e3eead48acda025579 f168 T4-platform T4-platform
|
||||
34797944694 2203 581f0c74006beb3edd28babbebeda7e48b3fa1d6 f168 T4-platform T4-platform
|
||||
34805101784 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 f169 T5-core T5-core
|
||||
34822774278 2205 e0a6133293556d87de6c2ca43de674e3a643e339 f170 T4-platform T4-platform
|
||||
34823711563 2158 334e0b0849241430d26c3fdb6ffc8ec7a676965c f141 T2-extract T2-extract
|
||||
34823714967 2159 63cdde702d9eb366b58006986e1498a18ae7f7d8 f013 T3-mid T3-mid
|
||||
34823719320 2160 c1dd44151fee8cfd815c693392a7c1c72def98fd f125 T4-platform T4-platform
|
||||
34823723515 2161 80f8bda5670c8ac1956e2a8fac7221c40c12da47 f091 T3-mid T3-mid
|
||||
34833703378 2203 3b7f2559213ee80f5cfe0e2329c418211e09749e f168 T4-platform T4-platform
|
||||
34853838753 2207 45c94be627a3088c275ff4f6acdc1f118515849a f171 T4-platform T4-platform
|
||||
34858382857 1767 036562df35486924c62cdb11a8c9518e479959d1 f172 T4-platform T4-platform
|
||||
34876200704 2202 9724d903046626df0640ced218259cb58070e4e8 f173 T5-core T5-core
|
||||
34884655464 2202 9e69cb7d9b7ad4b40fa5f8cbbb653dc70e108130 f173 T5-core T5-core
|
||||
34886874258 2202 d89ccabef064661794269c0de726021736d72b51 f173 T5-core T5-core
|
||||
34891515627 2202 d70b79417978a9f3ab0d1aa849879dbf049793cf f173 T5-core T5-core
|
||||
34898586377 1823 d4ae05a03dae831972a9dae7d2a2dd2ffce39e35 f076 T5-core T5-core
|
||||
34943783091 1781 7c88e64eb954e9400273d3ebb9f6f88f1aebc165 f174 T4-platform T4-platform
|
||||
34986091885 1823 955e4c9c58504b3c8e7d3e42866d16fadbb6ca7d f076 T5-core T5-core
|
||||
34993109423 2212 172787de9c18352bb29545da2426d9d61b260fc3 f175 T5-core T5-core
|
||||
35010526411 2213 d401186ca09e3992bc0e804af3945fe1cf81d6d2 f123 T4-platform T4-platform
|
||||
35010533052 2214 1f1f8334ca25502c3ed1d4e91e8c6144d07bb7e9 f013 T3-mid T3-mid
|
||||
35010542675 2216 729c3acc0fedae0ff77629281caf6dba1ac487fb f013 T3-mid T3-mid
|
||||
35010543243 2215 783547e4fd79bb6b38f0741b6118a719e4109059 f127 T1-tests T1-tests
|
||||
35010546755 2217 193c60d66db7eb36c58a7d0803d3936f5dae1d70 f149 T4-platform T4-platform
|
||||
35010550773 2218 0a1d229ba89c5a7ad6c63c1587f5a938cc890270 f124 T2-extract T2-extract
|
||||
35010559332 2219 a335422f63be3b5711ad40742bbaba40b2600f25 f158 T1-tests T1-tests
|
||||
35016014963 1907 50ee111714baa9d3bfe9bdba3b81e27cd1bdfe38 f033 T3-mid T3-mid
|
||||
35018285825 1913 da7619474d5ad27ff5073cf505b0b6cd5d0db36e f041 T3-mid T3-mid
|
||||
35037109751 2220 62fd80b842f00bd72e655d23c6afbfe12e199b6b f176 T4-platform T4-platform
|
||||
35037798343 2221 22ce867efd88dff9c219f789967ed2612329ee6d f001 T0a-docs T0a-docs
|
||||
35047093950 2072 e5076d215e3a31ee7f7528d9e5ceda83fd8e8a8a f100 T4-platform T4-platform
|
||||
35048046352 2072 8199efa4710893eb77d7c98d2730683a6c51327f f100 T4-platform T4-platform
|
||||
35072023020 2223 17a326a58b591e786790e79ceb3be6dc14ee3988 f177 T0b-nonproduct T0b-nonproduct
|
||||
35113639467 2223 43620d39d27554548fc2d613ed707efc8676748c f177 T0b-nonproduct T0b-nonproduct
|
||||
35129477140 2225 58c1c0caa7061ae868305dbe59a6c5e67004849d f178 T0a-docs T0a-docs
|
||||
35142303658 2227 dce2c5be9710c364eaa3a654d417ffcc392b62a4 f179 T4-platform T4-platform
|
||||
35145042859 2227 3d848acaa7d22885d5040c957b3aecdcc4376107 f180 T4-platform T4-platform
|
||||
35186676917 2140 b74c03558045bf29b7ef95c8aca803c15d3c7594 f140 T4-platform T4-platform
|
||||
35272232918 2230 e0becdeb094dd5e898997a2add826a9f84befbd7 f181 T3-mid T3-mid
|
||||
35273896570 2231 7054e4ff8af76f4fc1fe89407874e9910d0e412d f017 T2-extract T2-extract
|
||||
35295711667 2233 f0a86ef2c2bc352a479162cca79d1aa64598f09e f182 T5-core T5-core
|
||||
35303378012 2140 b52df4b0ce5c38667277716cd97ede964684802c f183 T4-platform T4-platform
|
||||
35303488221 2140 ca3461170fefc959fa7688fae4e212d6e9f2f99a f183 T4-platform T4-platform
|
||||
35312734228 2140 d85fe1d513ac97c8044b1065bbd0b7b9ebc87ec9 f184 T4-platform T4-platform
|
||||
35314622268 2234 9b4d697a57a03f242164db956843a5313168005d f185 T4-platform T4-platform
|
||||
35330927778 2236 949492b50c7dd81271f0180d8425d32316becbcf f186 T5-core T5-core
|
||||
35397596933 2233 1d58385b513a414ead34544346e86549dda3c4b1 f187 T5-core T5-core
|
||||
35443109227 2203 e176ae552b99f0fd08a2dde8bdf52025a3e20059 f168 T4-platform T4-platform
|
||||
35446976110 2233 8c1a9d61c27292bf7a52f10fb54186d4e24de28f f188 T5-core T5-core
|
||||
35448941856 2233 9db5217bdbb779212c5d066ed1a84f7ca6fc0282 f188 T5-core T5-core
|
||||
35467285032 2233 9db483402ea2fd4e0a5ff3bdd2b1fc50a7518ec9 f188 T5-core T5-core
|
||||
35470450948 808 bf2c54e0ef45d0d9767d7945a160f630f890f27d f016 T4-platform T5-core makefile-by-name
|
||||
35470489882 808 80d277834a233f2e7a2df68b8c08a1f233cf3dfa f016 T4-platform T5-core makefile-by-name
|
||||
35472952969 808 5515ca4ec6b20b62ed0530489785e4f131fd5159 f016 T4-platform T5-core makefile-by-name
|
||||
35473919864 2225 c067c771b9f93d08a3e614e68aee7d710d2f256c f178 T0a-docs T0a-docs
|
||||
35475706415 2241 6166934defd9bc5b687ab9c8de3779e71a844395 f014 T3-mid T3-mid
|
||||
35475754479 2242 e8a46c16e233bff111092dff18985cca94890bcc f189 T2-extract T2-extract
|
||||
35481840120 2236 aac550b732040acfe6ab6c0bfe5d0cbef5d9f9e9 f186 T5-core T5-core
|
||||
35483670414 2243 243b405afeb3998707ea4d556f3178c8e8419cab f150 T5-core T5-core
|
||||
35483768695 2236 d2dd3b347bbdcc695f911e80b1b7f7a32d790e5a f190 T5-core T5-core
|
||||
35486887056 1999 aa4da14d23dc28df58b9881dc66d89c0d1c0d788 f021 T2-extract T2-extract
|
||||
35493947102 2236 2caafc0c41bd083092afeb692824e718a8e1e007 f191 T5-core T5-core
|
||||
35495099219 808 83667268d9c435c2634994efef45b09daa98904a f016 T4-platform T5-core makefile-by-name
|
||||
35498544795 2227 656fe99f9934654bbab927d29add13f7822e1bc8 f180 T4-platform T4-platform
|
||||
35499978643 2244 56de845327dce03cdd1269b8aa73c6a1ecdb5cfa f125 T4-platform T4-platform
|
||||
35500380927 2245 ed78ff056e89da814ff5ae05da6fb32b7e10c0db f192 T4-platform T4-platform
|
||||
35501823298 2089 e171bd48b5e5845ec6f00db65ff300caac4f7304 f046 T4-platform T4-platform
|
||||
35502589515 2140 618fbca0c39da7157c7f01a26071ef3a4d4a5b00 f140 T4-platform T4-platform
|
||||
35502728596 1766 81820262e61f6e7f11920a5c4596b4297e793cf8 f193 T3-mid T3-mid
|
||||
35502852336 2246 fed0c1b4e3563f1567b3ff85652ec51229014a83 f194 T5-core T5-core
|
||||
35502954849 2246 2eeb15f434d9520e0050fe1f5b6c4c8316a4a69e f195 T5-core T5-core
|
||||
35503248954 1766 4eb3af36c7b055987d5c0764482135658b28c550 f193 T3-mid T3-mid
|
||||
35503753154 2246 5b4ea78927772d00f11abc41f217f89e714ace54 f195 T5-core T5-core
|
||||
35508860520 2247 9b1dfda875736a91073c57badce3dea6d0f03080 f196 T3-mid T3-mid
|
||||
35509728718 2248 96a2fc35dfcf14662cee491430366f4e8d8b8149 f094 T1-tests T1-tests
|
||||
35514366896 2249 051702c6f9163f0a19dda19ea14ac1b72dc7eb3d f197 T5-core T5-core
|
||||
35515754123 2090 7036c4d5996253c05d720ec4d0b3008509b4cad4 f198 T2-extract T2-extract
|
||||
35517816031 2090 f9350caae0ccccf823d8cde85683a900b257f239 f198 T2-extract T2-extract
|
||||
35517948650 2251 151d6077cf418a42628bc7e38bda4f873877f147 f199 T4-platform T4-platform
|
||||
35518289769 2252 4ecd8ab2c3995cc92c1a8df4f5e2a1e2d3a8c342 f200 T2-extract T2-extract
|
||||
35519906910 2253 458b0db1ba3274ceee1e975cfd7300f13aef82e9 f201 T4-platform T4-platform
|
||||
35519951214 2254 8aa77f89ce1ef56c98d989b03bc35fa4c6289b8a f017 T2-extract T2-extract
|
||||
35520101152 2249 238f9e6c6cad0e086eb06c19f1a6a303fbb30fdc f197 T5-core T5-core
|
||||
35520168086 2247 9af73b327e3899c45fc1faf13b590c09efa2078e f196 T3-mid T3-mid
|
||||
35520178069 2005 fcf451c6a6dda768dfec5a8f61a01ac1e1cfa6bf f034 T2-extract T2-extract
|
||||
35520796432 2255 0541b033fa874c38167e4f4d204ad452f1d96d08 f202 T2-extract T2-extract
|
||||
35520869334 2256 b565e9d8b06e8703f1c15a4263a987837dd0c5f3 f013 T3-mid T3-mid
|
||||
35522168154 2257 af3e003ad8e67cd1fe3b20dafaf4d33a4b351852 f203 T3-mid T3-mid
|
||||
35522190984 2258 de4a88f22242f53f724fc6cc8dcaed79642dc5cd f204 T2-extract T2-extract
|
||||
35523164377 2259 460ebefc87df4dd5142bdc339702edd4feec4696 f078 T3-mid T3-mid
|
||||
35523331086 2260 25d08258e567ec417bf7b18b14a63e30162fc7da f205 T4-platform T4-platform
|
||||
35532624016 2261 28d62001fd1e5d0a18085a8a662a1b806747815f f206 T0a-docs T0a-docs
|
||||
35532636527 2262 16a7291b56aafdcde27928f892d477e8b94269eb f207 T0a-docs T0a-docs
|
||||
35533089935 2257 c1128db8187db2dac27a7ad14120fc09802c8018 f208 T3-mid T3-mid
|
||||
35533328007 2263 cda5e3c9e5c4d2251467ced00ff305205c368d17 f209 T4-platform T4-platform
|
||||
35537157817 2005 6eb8992462e506bf0c58aec88274a9a62a07676b f210 T3-mid T3-mid
|
||||
35537525864 2005 fcf451c6a6dda768dfec5a8f61a01ac1e1cfa6bf f034 T2-extract T2-extract
|
||||
35551212771 2236 39a8bc3f2a7fbbc4d86ec8379b669a31ed1f8318 f191 T5-core T5-core
|
||||
35556558511 2140 af4604189bf23c14dceee9bccd89a16f5bfcad8c f140 T4-platform T4-platform
|
||||
35561381902 1724 42a3d7105548c65ee12f6dffabea718573501601 f211 T5-core T5-core
|
||||
35565389237 1809 19d9e4ac4d0a8c6839b627c0cd5890188121fc2b f046 T4-platform T4-platform
|
||||
35565392645 2075 e321b1fceaaf4c9f03fbe82547f02c1079e3b760 f104 T4-platform T4-platform
|
||||
35565401625 1784 381f64c15ebd0a676b85347acfcfd3f45c806a11 f009 T4-platform T4-platform
|
||||
35565472800 2260 2dba3cf81750f245d8c1d912997f04be5615df99 f205 T4-platform T4-platform
|
||||
35565493742 2005 7d01d3ec031dc26759283fff96e979c3e0ce270e f034 T2-extract T2-extract
|
||||
35565496531 2259 6f2f83618134bee208c98c47931579f27a712809 f078 T3-mid T3-mid
|
||||
35565502343 1768 6115e5785dc01f5b403cbfe7f184ffbfba058e45 f055 T1-tests T1-tests
|
||||
35572275939 2158 d4931241adaef732066afb5ebf1cb225e2474403 f141 T2-extract T2-extract
|
||||
35573014946 1716 95fa63441bf80c9ed871fa090ba925b8b6106d83 f096 T5-core T5-core
|
||||
35574966373 1724 81ff2cabcfeb4619e24307c0267a1244f38afb40 f211 T5-core T5-core
|
||||
35575687750 2160 2e4b45931b69671a92402f8fdf51f85542eff268 f125 T4-platform T4-platform
|
||||
35578606399 2199 fa138acf44525ef94cfd6e16772d601da5623aef f166 T5-core T5-core
|
||||
35585540293 2198 9574c316e43527d6ca90eb007a9c8fc24fcfd88b f212 T5-core T5-core
|
||||
35597253045 2268 f0fac62b7140f2cc3ccc011c74e798be7681ec61 f213 T5-core T5-core
|
||||
35606545382 1724 93f86be139b4bff1d079205fc776babc67f67349 f211 T5-core T5-core
|
||||
35607169136 1716 bc5b3dd216568c864b41303a674020a2d456ced9 f096 T5-core T5-core
|
||||
35623513958 1823 c017685d66ed9bca5476313ede52837ad07f17db f076 T5-core T5-core
|
||||
35632304230 1724 bc81c4a84f19f2f5d90d985ec69dcd5fc357ea25 f214 T5-core T5-core
|
||||
35637728335 2140 fca33717d1b6a9171decaf4d1755c88f48dcb572 f140 T4-platform T4-platform
|
||||
35638029971 1724 d48afe8d3070e360ca908d841d85adad344b2ed6 f214 T5-core T5-core
|
||||
35638134068 1724 3743fe95a0fc40f584b88bf4cbaa0f9bdfd8e52c f214 T5-core T5-core
|
||||
35639932135 2227 df83d25ace9885e252363cb04be25a17da85c9e8 f180 T4-platform T4-platform
|
||||
35640451450 2269 65d60e72f5b86f5982b15f0d88826ba79a085aa2 f215 T5-core T5-core
|
||||
35640454709 2270 f428d583008ce87281bac138c02bd4375c56d735 f216 T0a-docs T0a-docs
|
||||
35641634636 1999 e032a8ee9409c7a53a5d732ddff67c15721bf1ae f021 T2-extract T2-extract
|
||||
35641641970 2158 61cfb09b52ba40e8fb3ee9703c618b28739e3298 f141 T2-extract T2-extract
|
||||
35643957306 1724 5033f97d5f5154cddecee760e5b2a5ad290b181a f217 T5-core T5-core
|
||||
35664547053 2272 da4bd25a01b612517512ee628bf0ec652000e80a f218 T1-tests T1-tests
|
||||
35665885098 2273 c314dea4c1409a3a68fb33706b49e272809b5079 f219 T2-extract T2-extract
|
||||
35665890640 2274 343d7dd5e47b817999098605370f0eab706675f0 f017 T2-extract T2-extract
|
||||
35666550961 2275 a041794f8e51ddf42342899be7b2ac26f951d777 f128 T4-platform T4-platform
|
||||
35667708865 1741 c0bfd073ff25484c2d7b3b59dea937be6c92ed08 f072 T0b-nonproduct T0b-nonproduct
|
||||
35667710693 1742 dfae618b0e9508cd4998b583ebbeb36cf458278e f073 T1-tests T1-tests
|
||||
35667714864 2170 36730c7d895ae7541f780d650151b3590fc766cd f053 T3-mid T3-mid
|
||||
35667829346 1724 879aa5f9623bab14e8fb903da23bf84a8d0bc32e f217 T5-core T5-core
|
||||
35669146114 1724 89030aacb21df9b72f23b42286e3a4eee63fd43e f217 T5-core T5-core
|
||||
35669196507 2140 2f557f53986976b2df1eb2a1a540c9d48cf91a8e f140 T4-platform T4-platform
|
||||
35670303027 1724 b47ba846bbd1a6fe7359c86314c81fb33f5adaa4 f217 T5-core T5-core
|
||||
35696787516 1832 6ae6cd4c3f280cb8e3ffec2858269055e390ffec f220 T4-platform T5-core makefile-by-name
|
||||
35697012777 1832 9a9534cbc45d40afa4e8bf070030a806acea1f5f f221 T4-platform T5-core makefile-by-name
|
||||
35697183623 2022 7498e2b75b535171b5a560c6493fe86b5ad881cc f222 T4-platform T4-platform
|
||||
35697752061 1724 3889b47527084eccc7c54c8a3c7e2592c5d5d30a f217 T5-core T5-core
|
||||
35698464152 1724 08d5e2062f1ee9aaf8dce8eaf77662cc7c96a1c0 f223 T5-core T5-core
|
||||
35714885737 2280 f930ed6026c6f1e5f5b146a07664efed9d4da9c7 f224 T2-extract T2-extract
|
||||
35730933863 2198 f913f0eaf5ee308619ef1fdd4f60534d4509fab5 f212 T5-core T5-core
|
||||
35734984657 2281 34d172c9038a9b7cee78e42bb7768b93fbb8ed70 f225 T5-core T5-core
|
||||
35734995796 2282 1d2bbc425988eb78fcd966dc8c9cfbdb814f0685 f226 T4-platform T4-platform
|
||||
35766202700 2288 427bb73c638481842bd13d73ed171953469eca02 f227 T3-mid T3-mid
|
||||
35767446435 2288 d7eba5a71d2a99d7b7bc588c666d22516ac0b563 f227 T3-mid T3-mid
|
||||
35772088984 2289 38fc6ea995c9db78cedb10b9bba2b6aa79f616fc f228 T4-platform T4-platform
|
||||
35797101033 2290 255463126650ab6deab3e62aab441b71479704f1 f229 T4-platform T4-platform
|
||||
35797928618 1702 aecfb315d8ddbfab2cacbbd6e4a26b408b973975 f085 T3-mid T3-mid
|
||||
35800424142 1702 26bd387753a9a08fa6de647481bf36cf338e5c28 f085 T3-mid T3-mid
|
||||
35812291988 1702 6233394d8e893546af92876fb8d8da81e592cc2c f085 T3-mid T3-mid
|
||||
35813226724 1702 5770ed83741009e71c12876c40b655758f4f4440 f085 T3-mid T3-mid
|
||||
35822291393 1702 8165c34015699379d31fd31060c0f16fbe5447de f085 T3-mid T3-mid
|
||||
35824077381 1702 24795fc665df85200ddc112aaddfe34572171ef8 f085 T3-mid T3-mid
|
||||
35824588131 1430 6bb8ad6a7a1c99849d101322ca8fe81b6adfbc50 f230 T4-platform T4-platform
|
||||
35826272552 1430 0d2f840c8ed2695db206acbcad1ad1c36a1f3958 f230 T4-platform T4-platform
|
||||
35836874903 1702 f5ec03f0e1a7e90ace14c21490dca39c4c8bb092 f085 T3-mid T3-mid
|
||||
35841856084 1702 492e59a181a27af324c82a8db3c4e2e37382d567 f085 T3-mid T3-mid
|
||||
35841866366 1702 9e92e67649d62a2de63e57893290851d7247476a f085 T3-mid T3-mid
|
||||
35842147817 2158 246e4190c3086d1074adbe8afe39bf4261db59b8 f141 T2-extract T2-extract
|
||||
35846258328 1702 e9339ff1f5b28cb85d2810f434471700e2fc3b5e f085 T3-mid T3-mid
|
||||
35862574659 2294 54c143ac932b6e291a17be0327fb1b5e86b99984 f013 T3-mid T3-mid
|
||||
35899891384 1823 4a8c2eb422d9083ac3bf1e01e18e558e470ad099 f076 T5-core T5-core
|
||||
35918117863 2295 f4c897110cb1f02edee37db7c8849e63894ed8a4 f231 T3-mid T3-mid
|
||||
35936681521 2296 59345314187021f50c5e5c12da793b27b8485d7b f232 T2-extract T2-extract
|
||||
35948281533 1724 48f9de8ccd7cc75d7e466b7d5167e2930dfe33f3 f217 T5-core T5-core
|
||||
35948963693 1724 a8da9a366353bb0a67806b40f840500444ac963f f217 T5-core T5-core
|
||||
35948982011 2272 9b5bf70dc400fdfe989ed2bf1b623b2103bbf2f0 f218 T1-tests T1-tests
|
||||
35952295014 2181 282fbfcdabfec79c19c36f5bec5f75c22eb36df5 f091 T3-mid T3-mid
|
||||
35957866374 2158 cf7eecd7d0b1288c7f2e290a1d1bfc1b1b6f8698 f141 T2-extract T2-extract
|
||||
35960900747 1716 a6d3d5d820b07ba8544ddae1668e8107d7961532 f096 T5-core T5-core
|
||||
35960904447 2199 f07c18702ac82957d0eb73f96f37d30184b7e6b5 f166 T5-core T5-core
|
||||
35960910227 2263 fcdc6bedb7d05489462639da38c4ac755137734f f209 T4-platform T4-platform
|
||||
35960928169 1768 92cafd1ec888362ef3223ad669a5aa4f378a54d5 f055 T1-tests T1-tests
|
||||
36006341620 2301 64297ff3e8e142887fc36c933d57fd4d32924cc2 f233 T4-platform T4-platform
|
||||
36057012376 2304 6f91a16fc2a753db248f122dea7e4b50763972d1 f055 T1-tests T1-tests
|
||||
36057063533 2305 69b84cacff1b08f449e563c2d24861e6f10694e4 f234 T3-mid T3-mid
|
||||
36081432074 2307 2a6dd92f658b15d571130b0eee14a67b95a36465 f235 T2-extract T2-extract
|
||||
36081473175 2308 707f6f0db58a16b706b29d249bced477094ef92c f236 T3-mid T3-mid
|
||||
36081733263 2309 a9a25357ec2dc57f3c6f2e353e7ca61a7575df06 f168 T4-platform T4-platform
|
||||
36081918547 2310 2f922141972a18f0d95de4505b33f5362f51164b f237 T4-platform T4-platform
|
||||
36082054353 2310 8955d98983e0ca1e1df4a2173b4b4273904566c7 f237 T4-platform T4-platform
|
||||
36087900420 2310 7c24b020457924fee9a7acdd6cb8c5f7e262ca4e f237 T4-platform T4-platform
|
||||
36093255940 2310 b2e9199773977f28d73a052d41c37b8c111750a5 f237 T4-platform T4-platform
|
||||
36102327028 2310 d636aa7f21fb001325b61cd41fc431d5f255e21e f237 T4-platform T4-platform
|
||||
36137231318 2315 9527694e90ee9dfd29d64940425f9f23290ed702 f238 T4-platform T4-platform
|
||||
36146313096 2310 9b10ba281cfdba2676e9d24c445470c4b6f7d4e3 f237 T4-platform T4-platform
|
||||
36150749742 2022 7a307abbf8c1c879edee3193dd482ef427c83287 f222 T4-platform T4-platform
|
||||
36160883414 1766 9d53f32dc1753fcece6c0d2af27a5958f8ce1799 f193 T3-mid T3-mid
|
||||
36164363201 2318 6d508350c7713e10e0e935625ad74d8401fe89e2 f239 T3-mid T3-mid
|
||||
36164368040 2319 3faecc78144175a81acf76721c4f695cf80b5676 f240 T3-mid T3-mid
|
||||
36164372390 2320 4f5c951dde792c9cee4e33e45119cef8fc62765e f017 T2-extract T2-extract
|
||||
36164376373 2321 c6ccdd9395dbbafc6f66a49040b478b445387946 f241 T2-extract T2-extract
|
||||
36164382846 2322 19f50de3d2e2bdea5f9054caf07af70f46d74244 f242 T4-platform T4-platform
|
||||
36164406666 2323 e9f6596e82cac5b3377cf212e7199a286815418f f243 T4-platform T4-platform
|
||||
36164410674 2324 a808295986d7599ddbfe8324beefacfdd5161c04 f244 T4-platform T4-platform
|
||||
36164415830 2325 d56e046ddd08ac6bf8b14f5049a88382a10a1636 f245 T3-mid T3-mid
|
||||
36164420947 2326 5ef8ccff7023db106df18cef0615776e7ed94686 f246 T2-extract T5-core makefile-by-name
|
||||
36164427320 2327 098fcc883e2c720ee838df90f3475938715d4056 f247 T5-core T5-core
|
||||
36164450931 2328 a8c4040e8a92dcf831c64f57458694ff8fa6e9f9 f248 T2-extract T2-extract
|
||||
36164460893 2329 12f51855cab461cda3bd4c4d63d79f09fcb69732 f249 T5-core T5-core
|
||||
36164465591 2330 e7324bb6904c6d0a295b956e83c8585219198358 f250 T2-extract T2-extract
|
||||
36164472858 2331 a0c71225f7c0c5e4bee36ef235f966866abffb24 f251 T3-mid T3-mid
|
||||
36164477427 2332 b9ad9ed1449540e06e9f814ded548d9f2bf52f49 f252 T3-mid T3-mid
|
||||
36164497120 2333 e65a4e70be9c87366f66e93e252c0f307aa50ac2 f253 T4-platform T4-platform
|
||||
36164502402 2334 357d71e198ae3cd4614b9e06e54957423291e5c4 f013 T3-mid T3-mid
|
||||
36164505003 2335 1c73f8676c31e227c6ed478ccbb55662b34d6db2 f254 T4-platform T4-platform
|
||||
36164512328 2336 b8b88bcf088b8cf3f6b44949e917d172aaa82d2e f255 T2-extract T2-extract
|
||||
36164516453 2337 8e12fc8a69dc6f83296e660191448ef8ba7819bc f256 T5-core T5-core
|
||||
36164537929 2338 dad40ccdb633c10557baba4049aec35049fa3b15 f257 T4-platform T4-platform
|
||||
36164542709 2339 793e0b6127db3c8333b1dc29faf0b35167e3ceda f258 T3-mid T3-mid
|
||||
36164547188 2340 b3bc2a80aad766d37477a163ae5b158913a20f45 f259 T2-extract T2-extract
|
||||
36164550014 2341 40dad610eec783fe243e6b6141c1bebc75c23f0d f260 T4-platform T5-core makefile-by-name
|
||||
36164556221 2342 c650292433534127b5f45edd066e56a06b3376d1 f261 T3-mid T5-core makefile-by-name
|
||||
36165117354 2181 65fc0bb4f77fdbb6449199c147e1ed112654b151 f091 T3-mid T3-mid
|
||||
36165123674 2158 d772b1e5af0ce80d08aa6603ea47d1f51f324193 f141 T2-extract T2-extract
|
||||
36165128132 1716 ff127ea44c56ce21bd5a4f9cf9f51bb27523d0aa f096 T5-core T5-core
|
||||
36165431134 2343 45f0c430d28cf188753f44536d2cac26564a04ba f262 T3-mid T3-mid
|
||||
36165866718 2344 4b2b3b90adc44694d70624c9d0372752ffd0e2bf f013 T3-mid T3-mid
|
||||
36171533714 2326 c0a06b3d0a90513e0d80f79514b869f91cb958f1 f246 T2-extract T5-core makefile-by-name
|
||||
36171575767 2345 772ea06a6115128c3160f9631322b089aeb85a53 f263 T3-mid T3-mid
|
||||
36176174515 2347 b220ecec48010db71ac0a77b2888fefe802c9182 f264 T3-mid T3-mid
|
||||
36176260818 2348 886e6da479891b6dfe386ef25dfe5abe7ed2f080 f265 T4-platform T4-platform
|
||||
36181164078 2289 8cd2ee5f421d0b7c06022091d59dbe71002de30d f266 T4-platform T4-platform
|
||||
36183268529 2315 c27a53512da9469dc50fb243c1ecd3a55975ebfc f267 T3-mid T3-mid
|
||||
36183549298 2350 99ccc7d64a275bba1c015b30df036597637363f5 f268 T0a-docs T0a-docs
|
||||
36184450486 2351 951b2fddf68a6e6605b1664bd3252a2a6578ef35 f014 T3-mid T3-mid
|
||||
36185562216 2352 8093d10b812d9a7840ae99d8f9d24a04cb0f9559 f269 T3-mid T3-mid
|
||||
36185568093 2353 c64ea20feae160d1eeddadf09e7e0df36b08e818 f055 T1-tests T1-tests
|
||||
36185818550 2354 3ef7d8cdc201115dd76cc373ea2c64a567a2defe f012 T2-extract T2-extract
|
||||
36186982935 2318 b7bcb15c219761f27ff146063a50481303527563 f239 T3-mid T3-mid
|
||||
36186985986 2331 6b7628bb6bbf041a8bdf069dd9d6bc8861d84e73 f251 T3-mid T3-mid
|
||||
36186989409 2332 1d5ff68e508e78b149589396d547166ad41362e0 f270 T3-mid T3-mid
|
||||
36186989799 2334 87471e1f1d6a01ca8be6ca45b968145d0de95362 f013 T3-mid T3-mid
|
||||
36186993787 2342 7abaa91dea653af3e890759a63e40a09cfcb81d8 f261 T3-mid T5-core makefile-by-name
|
||||
36187019606 2327 f5c8631f2032c5598b1bfe12d329b875532f814d f247 T5-core T5-core
|
||||
36188745373 2355 ae578bc1cc32bb8f159a69c486f938ad745c643f f086 T4-platform T4-platform
|
||||
36189334816 2356 d5a3110d9e1e82de5416807d1f81e3ef85049c1a f271 T5-core T5-core
|
||||
36189454232 2357 28b41c35b71cb039988f39f72689dd657e30522d f272 T4-platform T4-platform
|
||||
36189574830 2358 f3754d6197b626d5432ac84b0363b0d3a5544c5c f273 T3-mid T3-mid
|
||||
36197747139 2301 ff8dd47011c43c622d3dc82b33be32b4493e4ea9 f274 T4-platform T4-platform
|
||||
36199747285 2344 bac3fc80b532eb294fd6eda6a2cc93d656df9369 f013 T3-mid T3-mid
|
||||
36199749518 2345 1810a8964caae06e33b7f3c6f944de570c21c292 f263 T3-mid T3-mid
|
||||
36201323199 1702 5557f0ec4a54bcaf99bd7442b866579ab68578f4 f085 T3-mid T3-mid
|
||||
36210099904 2359 efb231285d188ffda16a58f7a5ce8e0e6448638f f101 T1-tests T1-tests
|
||||
36222963285 2315 2d2c0fd0c008973c63565c3e7f4591ed0b1bb379 f275 T4-platform T4-platform
|
||||
36227652952 2160 bc960d87d663b7b4fc4c3ba558013c4609073762 f055 T1-tests T1-tests
|
||||
36232110014 2360 1ccbb432ae1281ac96f743850be317fb541cf1db f276 T3-mid T3-mid
|
||||
36238137680 2361 4dfeb2cefc402c7d1f1d3d01603a2e8409ed24cc f277 T3-mid T3-mid
|
||||
36239758139 2281 9dca8d2fede860605943ee8dbd57ba6b927ae41b f225 T5-core T5-core
|
||||
36239789418 2362 ed991154f212928df63ce5a5cd1159cfc8196f00 f278 T4-platform T4-platform
|
||||
36239835633 2290 26e2d0cba6df7b1cb53e655d0b902bdb7debdbd4 f279 T4-platform T4-platform
|
||||
36241153105 1816 3e67838b05e67915d472ec7698c80649ab8ad03a f071 T4-platform T4-platform
|
||||
36241168954 1766 8f441c19fb8a32b8ccf43ffcf65df7f4dc2bc16a f193 T3-mid T3-mid
|
||||
36242675954 2363 363575af920b05f911fc76ca964dcfa7ef1c62bb f150 T5-core T5-core
|
||||
36242676040 2294 6e3d8c296f2f99375d9693e706b4818c72caad13 f280 T3-mid T3-mid
|
||||
36242724677 2364 e94f5a70d01e4cf62b0c9f496175dcd930bcbd0e f151 T0b-nonproduct T0b-nonproduct
|
||||
36242808005 2365 7549e6d09c021f6d3a385a37898f591626ae62b8 f150 T5-core T5-core
|
||||
36245108554 2366 06f97613a28a18bef0145f37c1036fa60e73533e f281 T3-mid T3-mid
|
||||
36248365534 2367 8f803f7262d1d1e58068f677f3e365048050b728 f012 T2-extract T2-extract
|
||||
36248371079 2368 a59d53a14b779401621c44aa757e3bc9f0588ef0 f282 T5-core T5-core
|
||||
36259321198 2370 79a0c92e33a53758c36d87d4124b12cc83dac5be f236 T3-mid T3-mid
|
||||
36259389383 2371 0dba49208e8537eda2c3355387c0307f6ee95075 f283 T2-extract T2-extract
|
||||
36259523618 2372 6f17caa7a763d9987ebcd7ed33063d47d34bf635 f284 T4-platform T4-platform
|
||||
36262536017 2370 12de590a4935488d83380a33865cbb59a7a16c32 f236 T3-mid T3-mid
|
||||
36266366437 2373 77ab4bea87008547acee60435a8e3df29d90a5cb f285 T2-extract T2-extract
|
||||
36266372862 2374 7c7811f81aabb4c1fdc0914e78d46b71493a9dce f013 T3-mid T3-mid
|
||||
36266444913 2375 3ad76fd7728f3b19ca1100b10830e986b7f631b0 f235 T2-extract T2-extract
|
||||
36266447071 2376 5a9c72d8f8c65b38c66a29a34184b49edf3378ff f104 T4-platform T4-platform
|
||||
36268746769 1702 8ffd03ecd9ccae5a32508801645fb1cac7e4a4d1 f085 T3-mid T3-mid
|
||||
36270666016 1784 ca58149ae4757325158af07e4e3b8968d315e5da f009 T4-platform T4-platform
|
||||
36271539681 1784 71ac6ecc15d3ab3ebcd31f0e467071a506006ca2 f009 T4-platform T4-platform
|
||||
36271900288 2367 075f40e9d4ccbfead4df0e199c7730f94c2154ff f012 T2-extract T2-extract
|
||||
36274019750 2304 262d7ca7659b3f1f6ee619cda2b5f6aad2815314 f055 T1-tests T1-tests
|
||||
36274021183 2305 3d9e5ed5cf1ad1e4bf89b385d0be28c8f011f52f f234 T3-mid T3-mid
|
||||
36284225343 2370 208be7d9374bb27d2875e3568265b591ae7c4b5b f236 T3-mid T3-mid
|
||||
36284230521 2371 eafaab15c802c46ee22233b50f0e9474b0a3085d f283 T2-extract T2-extract
|
||||
36284315887 2280 756d7f42d4aab0045d75e91651cead399e4a7b35 f224 T2-extract T2-extract
|
||||
36285289867 2315 a1ac80740ed800d84a9d5e44d725bdc29d99b88e f275 T4-platform T4-platform
|
||||
36286798412 2072 1a37f0dd52ab62cfbb439c5a2f72afddca4cb009 f286 T5-core T5-core
|
||||
36299419426 2207 631317536426136f580e34d3c25dd5a951d443a8 f171 T4-platform T4-platform
|
||||
36313881874 2381 9d8f51d7f3340381afa450aa33cfd320879d6ae9 f060 T4-platform T4-platform
|
||||
36323957389 1702 b46c6d43f2efe6f0a1de8c77b73085183aa5da1a f085 T3-mid T3-mid
|
||||
36324735265 2376 07eb426c95386e299a6aa6adbf2b4eb9f272ff3b f104 T4-platform T4-platform
|
||||
36339435944 2370 e9058e11db9ed94e401ad711b2016249970a9040 f236 T3-mid T3-mid
|
||||
36339438890 2371 4d21c3bbd34b420c8987e8588b88b5fc5446116f f283 T2-extract T2-extract
|
||||
36346241955 2360 09f704d23f0d990dfc72b916d9daea80acce73ce f287 T3-mid T3-mid
|
||||
36347358048 2361 0d720d52b253cfb64e7bfb1eee034bc3035f1a04 f288 T3-mid T3-mid
|
||||
36348236142 2366 06ef2f64caea016fc792e9069d5a3befc30bca4a f289 T3-mid T3-mid
|
||||
36352906531 2375 7707c9d7918263cd446a4b54d4eb48c3fea91048 f235 T2-extract T2-extract
|
||||
36353006100 2247 fa2292111802b5f3b66db3e4dd168280b53ac17e f196 T3-mid T3-mid
|
||||
36353528883 2249 a45a99c9c73dd54a4625d680da700f7cb0387eff f197 T5-core T5-core
|
||||
36353741468 2385 1b2e8f1fac105b0eaf15e15b95c3ef336bf0a44e f290 T4-platform T4-platform
|
||||
36354332711 2181 82a6ddff81c6c8583c73f3d628037e41a9b412f0 f091 T3-mid T3-mid
|
||||
36354336229 1781 fd2456316e7b5dc27125497f640dc7e41760c104 f174 T4-platform T4-platform
|
||||
36354339289 2065 87ba464596337fc6574a825ef88392e70a733dc9 f093 T3-mid T3-mid
|
||||
36354790786 2252 df8dd1011bd1987d0ca5a45f324775aad2411f54 f200 T2-extract T2-extract
|
||||
36355512051 2089 11957ae6554b7d768095dcda95168331bd654363 f046 T4-platform T4-platform
|
||||
36355610699 2371 ee9d1365e2c17d4e2bbdd84ac10ec6b89cffca5f f283 T2-extract T2-extract
|
||||
36357921046 2344 7f649b29dc778b0f54c28dfcc2f25b76f3d1947a f013 T3-mid T3-mid
|
||||
36357921739 2326 259dbb67bee1d3547ae61a98c9d0c1cf4e491d44 f246 T2-extract T5-core makefile-by-name
|
||||
36357923794 2352 ea6fa49fc2c209de69c1b0f393d9d2cc25b61419 f291 T5-core T5-core
|
||||
36358160313 2389 27c87104a57eefe33543a04e3e43eb0721df8b4a f292 T2-extract T5-core makefile-by-name
|
||||
36358451648 2207 8bd9d37b156641ecf1c82b16b0ec0a8246e8c201 f171 T4-platform T4-platform
|
||||
36358713716 2295 9f5f5ab56fce2baf095b75f1e7a8dc98368dbcad f231 T3-mid T3-mid
|
||||
36359201669 2073 9130fab63d23b5386f12fa4cfa57961b247ff07b f101 T1-tests T1-tests
|
||||
36359210035 2385 c03575ef1ab1abb51793d03303ae3e5d4cd3b708 f290 T4-platform T4-platform
|
||||
36359646097 2391 d450275e20b663e3c5c8a06627030702e61e4184 f017 T2-extract T2-extract
|
||||
36359899206 2385 cb307a496de36c9803dffea0815f3ea96bf849db f290 T4-platform T4-platform
|
||||
36360333971 2389 5ff3e8d6fd3fd87d46807084058e2944f6486d76 f293 T2-extract T5-core makefile-by-name
|
||||
36360464106 2305 8a49fc684ad08ac2d99591e0b364249570c9e676 f234 T3-mid T3-mid
|
||||
36364475838 2392 19d144477b9558dad148d3f555ec6ede16caa3f5 f294 T4-platform T4-platform
|
||||
36364478399 2393 dc735c01fbbb4eddbbb73ff4df6007ab68413a4d f295 T4-platform T4-platform
|
||||
36364479687 2394 26cbccd998819c428589156a82e167db061522ae f296 T5-core T5-core
|
||||
36370343201 2395 f200d7477f192caedaa5b4cda7d9eccc2022a069 f297 T2-extract T2-extract
|
||||
36371911963 2396 629e9c2c1f87e2ac8b5d644d38ff4e935928752a f298 T3-mid T3-mid
|
||||
36371921838 2397 4c7bc08ff02d49afa07808309d5586bc61724ba4 f299 T5-core T5-core
|
||||
36416318776 2315 3327ee5935a7b03ef5f850c12c163c11ef9d91e2 f300 T4-platform T4-platform
|
||||
36440070485 2305 853192aa7e76eedceb1fce6e2a6874948cf1c1a4 f301 T3-mid T3-mid
|
||||
36440993073 2207 343cc949270a3e18e317fd6cd781167ed2fb3dad f171 T4-platform T4-platform
|
||||
36441585217 2368 f277c5e0a9aad35c730e4189b969b74964614d87 f302 T5-core T5-core
|
||||
36451035334 2207 1c840df7495e3b71924b8efd7e3bbf53c9076abc f171 T4-platform T4-platform
|
||||
36453341395 2401 9d5b6e998514b76dcc95b55f362325b3d49945da f264 T3-mid T3-mid
|
||||
36455603700 2401 874286abb9817db94e7789c7938921f6a03f3448 f264 T3-mid T3-mid
|
||||
36456464356 2402 555727b50da6086f00c00f57876e2632caa69c45 f303 T2-extract T2-extract
|
||||
36459403945 2394 78a2ec399fec3dc28e61439a4bcd900b7c64c23f f296 T5-core T5-core
|
||||
36459418705 2344 b205f0b51dacd89da57f6fcbeb46f55617606091 f013 T3-mid T3-mid
|
||||
36459497707 2403 95eaf01755d18c501fa8d1c5dae2255cf460b9a2 f001 T0a-docs T0a-docs
|
||||
36459501942 2404 fe3c285fdc63bd9abe027026edd7adffe5f62677 f013 T3-mid T3-mid
|
||||
36459506386 2405 460153204ba9833913e5245ca894f783c1d7229d f304 T4-platform T4-platform
|
||||
36459537782 2406 a1b7c05685e18597f3461783d4bc98661f05427d f305 T2-extract T2-extract
|
||||
36459545006 2407 c02dff113a90aa6bbc3d89fedda962a53f8b4c64 f306 T2-extract T2-extract
|
||||
36460081689 2280 6ba9a21eed2165932b90f7ede772d77b046eb08a f224 T2-extract T2-extract
|
||||
36460156038 2408 23d48336d3367fb27cbde54049b1414a2aea1b48 f307 T1-tests T1-tests
|
||||
36460162959 2409 4c6ecffdcdb8d78c0704f0d7e825f676ad77c240 f308 T1-tests T1-tests
|
||||
36460990240 1907 b74cecc53cea20abdc64dbdc484f5ba0b44f5789 f033 T3-mid T3-mid
|
||||
36461038395 1913 71ebbb9a9b54f2dcef84321de939429dd7707600 f041 T3-mid T3-mid
|
||||
36466048726 2410 533eee07ece1d01ff2c88da55972c7720cd1604b f309 T4-platform T4-platform
|
||||
36466057374 2411 0d783bcca4edde896d5716e8a4060c3100595973 f076 T5-core T5-core
|
||||
36466065600 2412 ed76cf8e88cfb1f26c82110e46a518c13577f788 f310 T4-platform T4-platform
|
||||
36469132661 2360 5a77358277a83d7651965f762a673be76fb157b3 f311 T3-mid T3-mid
|
||||
36474434156 2402 0a658840b8f029e7632910284281e32286c1f0aa f303 T2-extract T2-extract
|
||||
36474499483 2402 f88aaeb8f9db801545c21e085812559df9f80083 f303 T2-extract T2-extract
|
||||
36474752681 2418 3db66f22ad7d5c435559869a34b6e14b1e54793b f312 T3-mid T3-mid
|
||||
36476594662 2402 33a9bbe85eb922bf01c5297f09cc50044f39e351 f303 T2-extract T2-extract
|
||||
36476662334 2419 3f53a308f4034f4d280130fbcc275f545d7bf05e f313 T2-extract T2-extract
|
||||
36476726058 2420 e36930742a63efd2bbca491996ba8e37bfa5822c f314 T2-extract T2-extract
|
||||
36476760149 2421 fd1b88374c0c21892c991c29909d22ed499c20fa f315 T3-mid T3-mid
|
||||
36476819919 2422 ffa0b320047f4db99dc53f09e228c94703436ae1 f316 T2-extract T2-extract
|
||||
36477490833 2423 ba5e7dd639a3d29c54023d1203ef18fc4e330c7f f013 T3-mid T3-mid
|
||||
36484216122 2391 f297737db0510ecc573edf57a2a30a25e5fbfcfc f317 T3-mid T3-mid
|
||||
36484725527 2391 538ac25edda676ca7d8cb8a4c7c5fb3a3a36fda2 f317 T3-mid T3-mid
|
||||
36488109160 2418 702c3afb6bbab15e18e13f22880d5a61fa3317e1 f312 T3-mid T3-mid
|
||||
36488232420 2419 82361ffdd66ffe39efc1ff9e00e0cdcc61223e4f f313 T2-extract T2-extract
|
||||
36488247857 2420 a46dc5a9fbb39310027812234d0459d4bbcbe99c f314 T2-extract T2-extract
|
||||
36488262530 2421 a85bef05f169e4f2c36cf4cf121a7b6d1309e155 f315 T3-mid T3-mid
|
||||
36488285902 2422 c695cb728e4850b4152cd7cc54f06b6520372371 f316 T2-extract T2-extract
|
||||
36491809136 2309 48265f5367d93c22cdaf2203d19dc65fd6eefea4 f318 T4-platform T4-platform
|
||||
36492368152 2424 904eccabe39b03d8104de989a56f224ccf6d81d3 f319 T3-mid T3-mid
|
||||
36495192814 2425 e548d971b3e471ca5a149201c39ba62ab1a72261 f013 T3-mid T3-mid
|
||||
36499773006 2385 534274c1601034b5eca97ec286c0346389076a8f f290 T4-platform T4-platform
|
||||
36500785757 2360 bb3f1cdd6f4f45ab416ba97ad09a404aebf21e36 f311 T3-mid T3-mid
|
||||
36500790466 2361 8116eaaa426ff39a37b4078e0abd018b09398e58 f320 T3-mid T3-mid
|
||||
36500796009 2366 4a0f9660623b53d4f9eccd7149d54482707fdc22 f321 T3-mid T3-mid
|
||||
36504138642 2371 96f91005f0777e3fee5f5b5f9da69962ddf93695 f283 T2-extract T2-extract
|
||||
36506100535 2427 f4efd0b86844bf2ac965ba9ec30c8805cc774e40 f322 T3-mid T3-mid
|
||||
36508613643 2315 232730cb091d852bd7820bf1d5aeedc43aa860f8 f300 T4-platform T4-platform
|
||||
36509039978 2170 5a3e82520408db86161b8fff08175aa30a0adc6c f323 T3-mid T3-mid
|
||||
36535949326 2368 cc71a2087d32a7c06b0dcc2f925a9a623335c2ef f302 T5-core T5-core
|
||||
36538846045 2170 1ebd1e40fe715ac9efb328b2c7cebd2c0a4e140f f323 T3-mid T3-mid
|
||||
|
@@ -0,0 +1,26 @@
|
||||
# The 22 real (non-flake, non-infra) job failures of the September 2026 PR
|
||||
# pushes, from the lane analysis ground truth: which lane caught each. The
|
||||
# replay asserts the selector selects that lane for that push.
|
||||
# Columns (TAB): run_id, attempt, pr, head_sha, lane, classification.
|
||||
34263688699 1 2115 013e1f7ec7d4b981a1cd28bf35d5767c195f8447 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
|
||||
34288397854 1 2115 92cb3b9a4bebbc79bd283e447104669ca068ea64 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
|
||||
34649406520 1 2115 78a6d19f1eaaa73054639ab0f14c372c53cd1bb3 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
|
||||
34649406520 2 2115 78a6d19f1eaaa73054639ab0f14c372c53cd1bb3 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
|
||||
34676336284 1 2185 eb5e34cfc141454e849daf413f648e73f74c455a codeql-gate real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
|
||||
34676429093 1 2187 cea35fcc0520a0ba95139bd0210052f09b9cfa20 codeql-gate real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
|
||||
34696991657 1 2115 9a460b389ce3002ac164605c3627bb86aa3b9be3 unix-macos-intel real: cli suite hang on slow runners (arm64, then intel)
|
||||
34805101784 1 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 lsan-macos real: macOS-only LSan leaks (memory-core)
|
||||
34805101784 1 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 msan real?: MSan stack overflow (memory-core, branch-specific)
|
||||
34805101784 1 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 tsan-arm real?: backpressure nap-cycle bound (memory-core)
|
||||
34805101784 1 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 tsan-mac real?: backpressure nap-cycle bound (memory-core)
|
||||
34876200704 1 2202 9724d903046626df0640ced218259cb58070e4e8 lsan-macos real: macOS-only LSan leaks (memory-core)
|
||||
34876200704 1 2202 9724d903046626df0640ced218259cb58070e4e8 msan real?: MSan stack overflow (memory-core, branch-specific)
|
||||
34876200704 1 2202 9724d903046626df0640ced218259cb58070e4e8 tsan-arm real?: backpressure nap-cycle bound (memory-core)
|
||||
35448941856 1 2233 9db5217bdbb779212c5d066ed1a84f7ca6fc0282 tsan-arm real: TSan data race system_info.c:274
|
||||
35448941856 1 2233 9db5217bdbb779212c5d066ed1a84f7ca6fc0282 tsan-mac real: TSan data race system_info.c:274
|
||||
35448941856 1 2233 9db5217bdbb779212c5d066ed1a84f7ca6fc0282 tsan-x86 real: TSan data race system_info.c:274
|
||||
35483768695 1 2236 d2dd3b347bbdcc695f911e80b1b7f7a32d790e5a pkg-wrappers real?: wrapper publication crash-gate (cache-management)
|
||||
35483768695 1 2236 d2dd3b347bbdcc695f911e80b1b7f7a32d790e5a smoke-mac real: cache CLI broken on macOS/Windows
|
||||
35483768695 1 2236 d2dd3b347bbdcc695f911e80b1b7f7a32d790e5a smoke-win real: cache CLI broken on macOS/Windows
|
||||
35493947102 1 2236 2caafc0c41bd083092afeb692824e718a8e1e007 smoke-mac real: cache CLI broken on macOS/Windows
|
||||
36242675954 1 2363 363575af920b05f911fc76ca964dcfa7ef1c62bb codeql-gate real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
|
||||
|
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""predict.py — map a detect_changes impact set to the C test suites it can
|
||||
affect: the PR test-impact SHADOW (scripts/ci/test-impact-shadow.sh).
|
||||
Report-only; nothing here decides a merge.
|
||||
|
||||
The mapping is the one the September 2026 test-impact replay measured
|
||||
(detect_changes at depth 15 plus rule fallbacks, "dcF@15"):
|
||||
- impacted test FUNCTIONS (graph rows in tests/*.c) map to the suites that
|
||||
RUN_TEST them, resolved in their own file first; a nested suite (RUN_SUITE
|
||||
inside another) resolves to the registered suite that runs it; only suites
|
||||
registered in tests/test_main.c count, perf suites excluded;
|
||||
- a changed tests/*.c file always selects its whole suite;
|
||||
- a changed fixture (tests/fixtures/<key>/...) selects the suites whose test
|
||||
files name <key>; a changed grammar selects the grammar suites;
|
||||
- run-all: the lane selector's full, a changed test header or
|
||||
test-infrastructure file, a saturated or truncated impact set, or a C
|
||||
change the graph was not asked about -> every suite.
|
||||
|
||||
Usage: predict.py --root DIR --changed FILE --selection FILE [--detect FILE]
|
||||
[--depth N] [--meta JSON] --out FILE [--summary FILE]
|
||||
predict.py --needs-graph --changed FILE (prints yes|no)
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
GRAMMAR_SUITES = re.compile(r"^(grammar_|lang_contract|parse_coverage|extraction|matrix_|"
|
||||
r"edge_types_probe|convergence_probe|ac$|language$)")
|
||||
C_SOURCE = re.compile(r"\.(c|h|cc|cpp|inc)$")
|
||||
SUITE_DEF = re.compile(r"^\s*(?:SUITE\((\w+)\)|void\s+suite_(\w+)\s*\(\s*void\s*\))\s*\{", re.M)
|
||||
RUN_TEST = re.compile(r"\bRUN_TEST\((\w+)\)")
|
||||
RUN_SUITE = re.compile(r"\bRUN_SUITE\((\w+)\)")
|
||||
|
||||
|
||||
def needs_graph(changed):
|
||||
"""A C source change the graph can place (prod or test code)."""
|
||||
return any(C_SOURCE.search(p) and (p.startswith(("src/", "tests/")) or (
|
||||
p.startswith("internal/cbm/") and not p.startswith("internal/cbm/vendored/")))
|
||||
for p in changed)
|
||||
|
||||
|
||||
def suitemap(root):
|
||||
"""file -> suites defined there, test -> suites running it, suite universe,
|
||||
and the resolver from any suite to the registered suites that run it."""
|
||||
file_suites, test_suites, sources, parents = {}, {}, {}, {}
|
||||
for directory, dirs, names in os.walk(os.path.join(root, "tests")):
|
||||
dirs[:] = sorted(d for d in dirs if not (directory == os.path.join(root, "tests")
|
||||
and d == "fixtures"))
|
||||
for name in sorted(names):
|
||||
if not name.endswith(".c"):
|
||||
continue
|
||||
path = os.path.join(directory, name)
|
||||
rel = os.path.relpath(path, root).replace(os.sep, "/")
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
src = fh.read()
|
||||
sources[rel] = src
|
||||
for m in SUITE_DEF.finditer(src):
|
||||
suite = m.group(1) or m.group(2)
|
||||
start = depth = m.end() - 1
|
||||
end = start
|
||||
depth = 0
|
||||
while end < len(src): # the body is the balanced { } block
|
||||
if src[end] == "{":
|
||||
depth += 1
|
||||
elif src[end] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
end += 1
|
||||
file_suites.setdefault(rel, []).append(suite)
|
||||
for test in RUN_TEST.findall(src[start:end]):
|
||||
test_suites.setdefault(test, [])
|
||||
if suite not in test_suites[test]:
|
||||
test_suites[test].append(suite)
|
||||
for child in RUN_SUITE.findall(src[start:end]):
|
||||
parents.setdefault(child, set()).add(suite)
|
||||
main = sources.get("tests/test_main.c", "")
|
||||
registered = set(re.findall(r"RUN_SELECTED_SUITE(?:_PERF)?\((\w+)\)", main))
|
||||
universe = registered - set(re.findall(r"RUN_SELECTED_SUITE_PERF\((\w+)\)", main))
|
||||
|
||||
def runnable(suite, seen=frozenset()):
|
||||
"""A registered suite runs itself; a nested one runs inside its parents."""
|
||||
if suite in registered:
|
||||
return {suite} & universe
|
||||
out = set()
|
||||
for parent in parents.get(suite, ()):
|
||||
if parent not in seen:
|
||||
out |= runnable(parent, seen | {suite})
|
||||
return out
|
||||
return file_suites, test_suites, universe, sources, runnable
|
||||
|
||||
|
||||
def predict(root, changed, selection, detect, depth):
|
||||
file_suites, test_suites, universe, sources, runnable = suitemap(root)
|
||||
reasons, run_all = [], False
|
||||
if selection.get("full"):
|
||||
run_all = True
|
||||
reasons.append(f"run-all: the lane selector ran everything ({selection.get('tier')})")
|
||||
for p in changed:
|
||||
if p.startswith("tests/") and not p.startswith("tests/fixtures/") and p.endswith(".h"):
|
||||
run_all = True
|
||||
reasons.append(f"run-all: test header {p}")
|
||||
elif p.startswith("test-infrastructure/"):
|
||||
run_all = True
|
||||
reasons.append(f"run-all: test infrastructure {p}")
|
||||
|
||||
graph = {"consulted": detect is not None}
|
||||
picked = {} # suite -> "ALL" | set(tests)
|
||||
if detect is not None:
|
||||
rows = detect.get("impacted", [])
|
||||
total = detect.get("impacted_total", len(rows))
|
||||
shown = detect.get("impacted_shown", len(rows))
|
||||
graph.update(impacted_total=total, impacted_shown=shown,
|
||||
saturated=bool(detect.get("engine_saturated")))
|
||||
if graph["saturated"]:
|
||||
run_all = True
|
||||
reasons.append("run-all: detect_changes saturated")
|
||||
if total > len(rows) or shown > len(rows):
|
||||
run_all = True
|
||||
reasons.append(f"run-all: impact set truncated ({len(rows)} of {total} rows)")
|
||||
tests = 0
|
||||
for r in rows:
|
||||
fp, name = r.get("file", ""), r.get("qn", "").rsplit(".", 1)[-1]
|
||||
if not fp.startswith("tests/") or r.get("label") != "Function" or r.get("hop", 0) > depth:
|
||||
continue
|
||||
local = file_suites.get(fp, [])
|
||||
if name in local:
|
||||
continue # the SUITE function itself
|
||||
candidates = test_suites.get(name, [])
|
||||
for suite in [s for s in candidates if s in local] or candidates:
|
||||
for runner in runnable(suite):
|
||||
picked.setdefault(runner, set()).add(name)
|
||||
tests += 1
|
||||
graph["test_rows"] = tests
|
||||
elif needs_graph(changed):
|
||||
run_all = True
|
||||
reasons.append("run-all: C change but no impact set (graph not consulted)")
|
||||
|
||||
for p in changed:
|
||||
if p.startswith("tests/fixtures/"): # fixture sources are data, not tests
|
||||
key = p.split("/")[2] if p.count("/") >= 2 else p
|
||||
for f, src in sources.items():
|
||||
if key in src:
|
||||
for suite in file_suites.get(f, []):
|
||||
for runner in runnable(suite):
|
||||
picked[runner] = "ALL"
|
||||
reasons.append(f"changed fixture {p}: suites whose tests name '{key}'")
|
||||
elif p.startswith("tests/") and p.endswith(".c"):
|
||||
for suite in file_suites.get(p, []):
|
||||
for runner in runnable(suite):
|
||||
picked[runner] = "ALL"
|
||||
reasons.append(f"changed test file {p}: suite {runner}")
|
||||
elif p.startswith("internal/cbm/vendored/grammars/"):
|
||||
for suite in universe:
|
||||
if GRAMMAR_SUITES.match(suite):
|
||||
picked[suite] = "ALL"
|
||||
reasons.append(f"changed grammar {p}: grammar suites")
|
||||
|
||||
if run_all:
|
||||
mode, suites = "all", {s: "ALL" for s in sorted(universe)}
|
||||
else:
|
||||
suites = {s: v if v == "ALL" else sorted(v) for s, v in sorted(picked.items())}
|
||||
mode = "graph" if (graph["consulted"] or suites) else "none"
|
||||
return {"version": 1, "mode": mode, "depth": depth,
|
||||
"selector": {"tier": selection.get("tier"), "full": bool(selection.get("full"))},
|
||||
"universe": len(universe), "selected_suites": len(suites), "suites": suites,
|
||||
"graph": graph, "reasons": sorted(set(reasons))}
|
||||
|
||||
|
||||
def summary(result, meta):
|
||||
g = result["graph"]
|
||||
whole = sum(1 for v in result["suites"].values() if v == "ALL")
|
||||
tests = sum(len(v) for v in result["suites"].values() if v != "ALL")
|
||||
lines = ["### Test-impact shadow (report-only, never gates)", "",
|
||||
"| | |", "|---|---|",
|
||||
f"| selector | {result['selector']['tier']} (full: {'yes' if result['selector']['full'] else 'no'}) |",
|
||||
f"| prediction | **{result['mode']}**: {result['selected_suites']} of {result['universe']} "
|
||||
f"suites ({whole} whole, {tests} tests in the rest) |"]
|
||||
if g.get("consulted"):
|
||||
lines.append(f"| graph | {g.get('impacted_shown')} of {g.get('impacted_total')} impacted rows at "
|
||||
f"depth {result['depth']}, {g.get('test_rows')} test rows"
|
||||
f"{', saturated' if g.get('saturated') else ''} |")
|
||||
for key in sorted(meta):
|
||||
lines.append(f"| {key} | {meta[key]} |")
|
||||
if result["reasons"]:
|
||||
lines += ["", "Reasons: " + "; ".join(result["reasons"][:12])]
|
||||
if result["mode"] == "graph" and result["suites"]:
|
||||
lines += ["", "Suites: " + ", ".join(result["suites"])[:1500]]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
ap.add_argument("--needs-graph", action="store_true")
|
||||
ap.add_argument("--root")
|
||||
ap.add_argument("--changed", required=True)
|
||||
ap.add_argument("--selection")
|
||||
ap.add_argument("--detect")
|
||||
ap.add_argument("--depth", type=int, default=15)
|
||||
ap.add_argument("--meta", default="{}")
|
||||
ap.add_argument("--out")
|
||||
ap.add_argument("--summary")
|
||||
a = ap.parse_args()
|
||||
with open(a.changed, encoding="utf-8") as fh:
|
||||
changed = sorted({line.strip() for line in fh if line.strip()})
|
||||
if a.needs_graph:
|
||||
print("yes" if needs_graph(changed) else "no")
|
||||
return 0
|
||||
if not (a.root and a.selection and a.out):
|
||||
ap.error("--root, --selection and --out are required")
|
||||
with open(a.selection, encoding="utf-8") as fh:
|
||||
selection = json.load(fh)
|
||||
detect = None
|
||||
if a.detect:
|
||||
with open(a.detect, encoding="utf-8") as fh:
|
||||
detect = json.load(fh)
|
||||
result = predict(a.root, changed, selection, detect, a.depth)
|
||||
meta = json.loads(a.meta)
|
||||
result["meta"] = meta
|
||||
with open(a.out, "w", encoding="utf-8") as fh:
|
||||
json.dump(result, fh, indent=1, sort_keys=True)
|
||||
fh.write("\n")
|
||||
if a.summary:
|
||||
with open(a.summary, "w", encoding="utf-8") as fh:
|
||||
fh.write(summary(result, meta))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,730 @@
|
||||
"""Pure bounded single-image profile-map v2 encoding and row reduction.
|
||||
|
||||
Evidence fields are caller claims, not authentication or closure certificates.
|
||||
Names/hashes/shapes retain exact profile identity; source names are not inferred.
|
||||
Limits count logical data, not interpreter RSS. Runtime allocation/deallocation
|
||||
is not preemptible. This module performs no I/O and leaves format 1 unchanged.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
import sys
|
||||
from typing import Callable, Optional, Tuple
|
||||
|
||||
from profile_text_parser import (
|
||||
LimitExceededError, MalformedProfileError, ParseCancelledError,
|
||||
ParseLimits, parse_profile_text,
|
||||
)
|
||||
|
||||
|
||||
class SuiteOutcome(Enum):
|
||||
PASSED = "passed"
|
||||
FAILED = "failed"
|
||||
TIMED_OUT = "timed_out"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class ObservationRole(Enum):
|
||||
PARENT = "parent"
|
||||
SETUP = "setup"
|
||||
CHILD = "child"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpectedSuite:
|
||||
__slots__ = ("suite", "tests", "outcome")
|
||||
|
||||
suite: bytes
|
||||
tests: Tuple[bytes, ...]
|
||||
outcome: SuiteOutcome
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileObservation:
|
||||
__slots__ = ("suite", "test", "key", "role", "text")
|
||||
|
||||
suite: bytes
|
||||
test: bytes
|
||||
key: bytes
|
||||
role: ObservationRole
|
||||
text: Optional[bytes] # None means an explicitly unavailable observation.
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RowEvidence:
|
||||
__slots__ = (
|
||||
"suite", "test", "interval_complete", "runtime_compatible",
|
||||
"same_image", "children_complete",
|
||||
)
|
||||
|
||||
suite: bytes
|
||||
test: bytes
|
||||
interval_complete: bool
|
||||
runtime_compatible: bool
|
||||
same_image: bool
|
||||
children_complete: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileMapLimits:
|
||||
__slots__ = (
|
||||
"max_input_bytes", "max_rows", "max_observations", "max_ids",
|
||||
"max_records", "max_counter_values", "max_name_bytes",
|
||||
"max_id_references", "max_output_bytes",
|
||||
)
|
||||
|
||||
max_input_bytes: int
|
||||
max_rows: int
|
||||
max_observations: int
|
||||
max_ids: int
|
||||
max_records: int
|
||||
max_counter_values: int
|
||||
max_name_bytes: int
|
||||
max_id_references: int
|
||||
max_output_bytes: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProducedMap:
|
||||
__slots__ = (
|
||||
"profiles_tsv", "tests_tsv", "profiles_sha256", "tests_sha256",
|
||||
)
|
||||
|
||||
profiles_tsv: bytes
|
||||
tests_tsv: bytes
|
||||
profiles_sha256: bytes
|
||||
tests_sha256: bytes
|
||||
|
||||
|
||||
class ProfileMapError(ValueError):
|
||||
"""Base for classified build failures; no ProducedMap is returned."""
|
||||
|
||||
|
||||
class ProfileMapInvalidArgumentError(ProfileMapError):
|
||||
"""Invalid exact type, field, routing, membership, duplicate or callback result."""
|
||||
|
||||
|
||||
class ProfileMapEnumerationError(ProfileMapError):
|
||||
"""Enumeration text is malformed or unsupported, including empty text."""
|
||||
|
||||
|
||||
class ProfileMapCompatibilityError(ProfileMapError):
|
||||
"""An observation contains an unknown full identity or mismatching shape."""
|
||||
|
||||
|
||||
class ProfileMapLimitExceededError(ProfileMapError):
|
||||
"""A declared logical or fixed representation bound would be exceeded."""
|
||||
|
||||
|
||||
class ProfileMapCancelledError(ProfileMapError):
|
||||
"""The caller's pure prompt predicate requested cancellation."""
|
||||
|
||||
|
||||
_INT_MAX = 2147483647
|
||||
_SHA_BYTES_MAX = ((1 << 64) - 1) // 8
|
||||
_POLL_INTERVAL = 4096
|
||||
_HEX = b"0123456789abcdef"
|
||||
_REASONS = (
|
||||
b"suite_failed", b"suite_timeout", b"suite_unknown",
|
||||
b"row_evidence_missing", b"interval_unproved", b"runtime_unproved",
|
||||
b"image_unproved", b"children_unproved", b"missing_parent",
|
||||
b"missing_setup", b"observation_unavailable", b"observation_malformed",
|
||||
b"partial_stream",
|
||||
)
|
||||
_SUITE_FAILED, _SUITE_TIMEOUT, _SUITE_UNKNOWN = 0, 1, 2
|
||||
_NO_EVIDENCE, _INTERVAL, _RUNTIME, _IMAGE, _CHILDREN = 3, 4, 5, 6, 7
|
||||
_NO_PARENT, _NO_SETUP, _UNAVAILABLE, _MALFORMED, _PARTIAL = 8, 9, 10, 11, 12
|
||||
|
||||
|
||||
class _Gate:
|
||||
__slots__ = ("cancel", "context", "work", "callback_error")
|
||||
|
||||
def __init__(self, cancel, context):
|
||||
self.cancel = cancel
|
||||
self.context = context
|
||||
self.work = 0
|
||||
self.callback_error = None
|
||||
|
||||
def poll(self):
|
||||
self.work = 0
|
||||
if self.cancel is None:
|
||||
return
|
||||
try:
|
||||
answer = self.cancel(self.context)
|
||||
except BaseException as error:
|
||||
# Identity distinguishes a callback's parser-shaped exception from
|
||||
# a parser failure; neither its class nor message is sufficient.
|
||||
self.callback_error = error
|
||||
raise
|
||||
if type(answer) is not bool:
|
||||
raise ProfileMapInvalidArgumentError("cancel must return bool")
|
||||
if answer:
|
||||
raise ProfileMapCancelledError("profile map build cancelled")
|
||||
|
||||
def step(self):
|
||||
self.work += 1
|
||||
if self.work == _POLL_INTERVAL:
|
||||
self.poll()
|
||||
|
||||
def parser_cancel(self, _context):
|
||||
self.poll()
|
||||
return False
|
||||
|
||||
|
||||
def _bounded_add(current, amount, cap, label):
|
||||
if amount > cap - current:
|
||||
raise ProfileMapLimitExceededError(label)
|
||||
return current + amount
|
||||
|
||||
|
||||
class _Budget:
|
||||
__slots__ = (
|
||||
"limits", "input_bytes", "rows", "records", "counter_values",
|
||||
"name_bytes", "id_references", "output_bytes",
|
||||
)
|
||||
|
||||
def __init__(self, limits, enumeration_text):
|
||||
self.limits = limits
|
||||
self.input_bytes = _bounded_add(
|
||||
32, len(enumeration_text), limits.max_input_bytes, "input byte limit"
|
||||
)
|
||||
self.rows = 0
|
||||
self.records = 0
|
||||
self.counter_values = 0
|
||||
self.name_bytes = 0
|
||||
self.id_references = 0
|
||||
self.output_bytes = 0
|
||||
|
||||
def name(self, value):
|
||||
self.input_bytes = _bounded_add(
|
||||
self.input_bytes, len(value), self.limits.max_input_bytes,
|
||||
"input byte limit",
|
||||
)
|
||||
self.name_bytes = _bounded_add(
|
||||
self.name_bytes, len(value), self.limits.max_name_bytes,
|
||||
"name byte limit",
|
||||
)
|
||||
|
||||
def text(self, value):
|
||||
self.input_bytes = _bounded_add(
|
||||
self.input_bytes, len(value), self.limits.max_input_bytes,
|
||||
"input byte limit",
|
||||
)
|
||||
|
||||
def add_rows(self, count):
|
||||
self.rows = _bounded_add(
|
||||
self.rows, count, min(self.limits.max_rows, _INT_MAX), "row limit"
|
||||
)
|
||||
|
||||
def parsed(self, records, gate):
|
||||
self.records = _bounded_add(
|
||||
self.records, len(records), self.limits.max_records, "record limit"
|
||||
)
|
||||
for record in records:
|
||||
gate.step()
|
||||
self.counter_values = _bounded_add(
|
||||
self.counter_values, len(record.counters),
|
||||
self.limits.max_counter_values, "counter value limit",
|
||||
)
|
||||
self.name_bytes = _bounded_add(
|
||||
self.name_bytes, len(record.raw_name),
|
||||
self.limits.max_name_bytes, "name byte limit",
|
||||
)
|
||||
|
||||
def reference(self):
|
||||
self.id_references = _bounded_add(
|
||||
self.id_references, 1, self.limits.max_id_references,
|
||||
"ID reference limit",
|
||||
)
|
||||
|
||||
def output(self, count):
|
||||
self.output_bytes = _bounded_add(
|
||||
self.output_bytes, count,
|
||||
min(self.limits.max_output_bytes, _SHA_BYTES_MAX), "output byte limit",
|
||||
)
|
||||
|
||||
|
||||
class _Row:
|
||||
__slots__ = (
|
||||
"suite", "test", "outcome", "evidence", "observations", "ids",
|
||||
"reasons", "parent_seen", "primary_parsed",
|
||||
)
|
||||
|
||||
def __init__(self, suite, test, outcome):
|
||||
self.suite = suite
|
||||
self.test = test
|
||||
self.outcome = outcome
|
||||
self.evidence = None
|
||||
self.observations = []
|
||||
self.ids = set()
|
||||
self.reasons = 0
|
||||
self.parent_seen = False
|
||||
self.primary_parsed = False
|
||||
|
||||
def mark(self, reason):
|
||||
self.reasons |= 1 << reason
|
||||
|
||||
|
||||
def _exact(value, cls, label):
|
||||
if type(value) is not cls:
|
||||
raise ProfileMapInvalidArgumentError(label)
|
||||
|
||||
|
||||
def _validate_top(image, enumeration, suites, observations, evidence, limits, cancel):
|
||||
_exact(image, bytes, "image digest must be bytes")
|
||||
if len(image) != 32:
|
||||
raise ProfileMapInvalidArgumentError("image digest must contain 32 bytes")
|
||||
_exact(enumeration, bytes, "enumeration must be bytes")
|
||||
_exact(suites, tuple, "expected suites must be a tuple")
|
||||
_exact(observations, tuple, "observations must be a tuple")
|
||||
_exact(evidence, tuple, "row evidence must be a tuple")
|
||||
_exact(limits, ProfileMapLimits, "invalid limits type")
|
||||
for field in ProfileMapLimits.__slots__:
|
||||
value = getattr(limits, field)
|
||||
if type(value) is not int or value <= 0 or value > sys.maxsize:
|
||||
raise ProfileMapInvalidArgumentError("limits must be positive bounded ints")
|
||||
if limits.max_ids > _INT_MAX:
|
||||
raise ProfileMapInvalidArgumentError("max_ids exceeds INT_MAX")
|
||||
if cancel is not None and not callable(cancel):
|
||||
raise ProfileMapInvalidArgumentError("cancel must be callable or None")
|
||||
|
||||
|
||||
def _name(value, budget, gate, *, suite=False, declared_test=False):
|
||||
_exact(value, bytes, "names and keys must be bytes")
|
||||
if not value:
|
||||
raise ProfileMapInvalidArgumentError("empty name or key")
|
||||
budget.name(value)
|
||||
for char in value:
|
||||
gate.step()
|
||||
if char in (0, 9, 10, 13) or (suite and char == 58):
|
||||
raise ProfileMapInvalidArgumentError("unrepresentable name or key")
|
||||
if declared_test and _is_setup(value):
|
||||
raise ProfileMapInvalidArgumentError("setup name is reserved")
|
||||
|
||||
|
||||
def _is_setup(test):
|
||||
return len(test) == 1 and test[0] == 42
|
||||
|
||||
|
||||
def _compare_bytes(left, right, gate):
|
||||
for index in range(min(len(left), len(right))):
|
||||
gate.step()
|
||||
a, b = left[index], right[index]
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
gate.step()
|
||||
return (len(left) > len(right)) - (len(left) < len(right))
|
||||
|
||||
|
||||
def _compare_fields(left, right, fields, gate):
|
||||
for field in fields:
|
||||
gate.step()
|
||||
order = _compare_bytes(getattr(left, field), getattr(right, field), gate)
|
||||
if order:
|
||||
return order
|
||||
return 0
|
||||
|
||||
|
||||
def _merge(source, target, start, middle, end, compare, gate):
|
||||
left, right = start, middle
|
||||
for destination in range(start, end):
|
||||
gate.step()
|
||||
take_left = right == end or (
|
||||
left < middle and compare(source[left], source[right]) <= 0
|
||||
)
|
||||
if take_left:
|
||||
target[destination] = source[left]
|
||||
left += 1
|
||||
else:
|
||||
target[destination] = source[right]
|
||||
right += 1
|
||||
|
||||
|
||||
def _sort(values, compare, gate):
|
||||
source, target = [], []
|
||||
for value in values:
|
||||
gate.step()
|
||||
source.append(value)
|
||||
target.append(None)
|
||||
width = 1
|
||||
count = len(source)
|
||||
while width < count:
|
||||
gate.step()
|
||||
for start in range(0, count, width * 2):
|
||||
gate.step()
|
||||
middle, end = min(start + width, count), min(start + width * 2, count)
|
||||
_merge(source, target, start, middle, end, compare, gate)
|
||||
source, target = target, source
|
||||
width *= 2
|
||||
return source
|
||||
|
||||
|
||||
def _sort_unique(values, fields, gate, label):
|
||||
compare = lambda left, right: _compare_fields(left, right, fields, gate)
|
||||
result = _sort(values, compare, gate)
|
||||
for index in range(1, len(result)):
|
||||
gate.step()
|
||||
if compare(result[index - 1], result[index]) == 0:
|
||||
raise ProfileMapInvalidArgumentError(label)
|
||||
return result
|
||||
|
||||
|
||||
def _suites(suites, budget, gate):
|
||||
rows = []
|
||||
if len(suites) > min(budget.limits.max_rows, _INT_MAX):
|
||||
raise ProfileMapLimitExceededError("row limit")
|
||||
for suite in suites:
|
||||
gate.step()
|
||||
_exact(suite, ExpectedSuite, "invalid expected suite type")
|
||||
_name(suite.suite, budget, gate, suite=True)
|
||||
_exact(suite.tests, tuple, "expected tests must be a tuple")
|
||||
_exact(suite.outcome, SuiteOutcome, "invalid suite outcome")
|
||||
budget.add_rows(1 + len(suite.tests))
|
||||
rows.append(_Row(suite.suite, b"*", suite.outcome))
|
||||
for test in suite.tests:
|
||||
gate.step()
|
||||
_name(test, budget, gate, declared_test=True)
|
||||
rows.append(_Row(suite.suite, test, suite.outcome))
|
||||
_sort_unique(suites, ("suite",), gate, "duplicate expected suite")
|
||||
return _sort_unique(rows, ("suite", "test"), gate, "duplicate expected test")
|
||||
|
||||
|
||||
def _compare_target(row, suite, test, gate):
|
||||
order = _compare_bytes(row.suite, suite, gate)
|
||||
return order if order else _compare_bytes(row.test, test, gate)
|
||||
|
||||
|
||||
def _find_row(rows, suite, test, gate):
|
||||
low, high = 0, len(rows)
|
||||
while low < high:
|
||||
gate.step()
|
||||
middle = (low + high) // 2
|
||||
order = _compare_target(rows[middle], suite, test, gate)
|
||||
if order == 0:
|
||||
return rows[middle]
|
||||
if order < 0:
|
||||
low = middle + 1
|
||||
else:
|
||||
high = middle
|
||||
raise ProfileMapInvalidArgumentError("observation/evidence outside expected rows")
|
||||
|
||||
|
||||
def _observation_schema(observation, budget, gate):
|
||||
_exact(observation, ProfileObservation, "invalid observation type")
|
||||
_name(observation.suite, budget, gate, suite=True)
|
||||
_name(observation.test, budget, gate)
|
||||
_name(observation.key, budget, gate)
|
||||
_exact(observation.role, ObservationRole, "invalid observation role")
|
||||
if observation.text is not None:
|
||||
_exact(observation.text, bytes, "observation text must be bytes or None")
|
||||
budget.text(observation.text)
|
||||
|
||||
|
||||
def _route_observation(observation, row):
|
||||
setup = _is_setup(row.test)
|
||||
if ((setup and observation.role is ObservationRole.PARENT)
|
||||
or (not setup and observation.role is ObservationRole.SETUP)):
|
||||
raise ProfileMapInvalidArgumentError("invalid role for row")
|
||||
if observation.role is ObservationRole.PARENT:
|
||||
if row.parent_seen:
|
||||
raise ProfileMapInvalidArgumentError("multiple parent observations")
|
||||
row.parent_seen = True
|
||||
row.observations.append(observation)
|
||||
|
||||
|
||||
def _observations(observations, rows, budget, gate):
|
||||
if len(observations) > budget.limits.max_observations:
|
||||
raise ProfileMapLimitExceededError("observation limit")
|
||||
for observation in observations:
|
||||
gate.step()
|
||||
_observation_schema(observation, budget, gate)
|
||||
ordered = _sort_unique(
|
||||
observations, ("suite", "test", "key"), gate, "duplicate observation key"
|
||||
)
|
||||
for observation in ordered:
|
||||
gate.step()
|
||||
row = _find_row(rows, observation.suite, observation.test, gate)
|
||||
_route_observation(observation, row)
|
||||
|
||||
|
||||
def _evidence(evidence, rows, budget, gate):
|
||||
if len(evidence) > len(rows):
|
||||
raise ProfileMapInvalidArgumentError("too many row evidence records")
|
||||
for item in evidence:
|
||||
gate.step()
|
||||
_exact(item, RowEvidence, "invalid row evidence type")
|
||||
_name(item.suite, budget, gate, suite=True)
|
||||
_name(item.test, budget, gate)
|
||||
for field in ("interval_complete", "runtime_compatible", "same_image", "children_complete"):
|
||||
gate.step()
|
||||
_exact(getattr(item, field), bool, "evidence claims must be bool")
|
||||
ordered = _sort_unique(evidence, ("suite", "test"), gate, "duplicate row evidence")
|
||||
for item in ordered:
|
||||
gate.step()
|
||||
_find_row(rows, item.suite, item.test, gate).evidence = item
|
||||
|
||||
|
||||
def _preflight(suites, observations, evidence, budget, gate):
|
||||
rows = _suites(suites, budget, gate)
|
||||
_observations(observations, rows, budget, gate)
|
||||
_evidence(evidence, rows, budget, gate)
|
||||
gate.poll()
|
||||
return rows
|
||||
|
||||
|
||||
def _parser_limits(budget, enumeration):
|
||||
limits = budget.limits
|
||||
records = limits.max_records - budget.records
|
||||
counters = limits.max_counter_values - budget.counter_values
|
||||
names = limits.max_name_bytes - budget.name_bytes
|
||||
if enumeration:
|
||||
records = min(records, limits.max_ids)
|
||||
if records <= 0 or counters <= 0 or names <= 0:
|
||||
raise ProfileMapLimitExceededError("no remaining parser item allowance")
|
||||
return ParseLimits(limits.max_input_bytes, records, counters, names)
|
||||
|
||||
|
||||
def _parse(text, budget, gate, *, enumeration=False):
|
||||
limits = _parser_limits(budget, enumeration)
|
||||
try:
|
||||
records = parse_profile_text(text, limits, cancel=gate.parser_cancel)
|
||||
except MalformedProfileError as error:
|
||||
if error is gate.callback_error:
|
||||
raise
|
||||
if enumeration:
|
||||
raise ProfileMapEnumerationError("invalid profile enumeration") from error
|
||||
return None
|
||||
except LimitExceededError as error:
|
||||
if error is gate.callback_error:
|
||||
raise
|
||||
raise ProfileMapLimitExceededError("profile parser resource limit") from error
|
||||
except ParseCancelledError as error:
|
||||
if error is gate.callback_error:
|
||||
raise
|
||||
raise ProfileMapCancelledError("profile parsing cancelled") from error
|
||||
budget.parsed(records, gate)
|
||||
return records
|
||||
|
||||
|
||||
def _record_order(left, right, gate):
|
||||
order = _compare_bytes(left.raw_name, right.raw_name, gate)
|
||||
if order:
|
||||
return order
|
||||
gate.step()
|
||||
return (left.function_hash > right.function_hash) - (left.function_hash < right.function_hash)
|
||||
|
||||
|
||||
def _has_hit(record, gate):
|
||||
for value in record.counters:
|
||||
gate.step()
|
||||
if value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _join(records, universe, row, budget, gate):
|
||||
position = 0
|
||||
for record in records:
|
||||
gate.step()
|
||||
while position < len(universe):
|
||||
gate.step()
|
||||
order = _record_order(universe[position], record, gate)
|
||||
if order >= 0:
|
||||
break
|
||||
position += 1
|
||||
if position == len(universe) or order != 0:
|
||||
raise ProfileMapCompatibilityError("observation identity outside universe")
|
||||
if len(record.counters) != len(universe[position].counters):
|
||||
raise ProfileMapCompatibilityError("observation counter shape mismatch")
|
||||
if _has_hit(record, gate) and position not in row.ids:
|
||||
budget.reference()
|
||||
row.ids.add(position)
|
||||
position += 1
|
||||
if len(records) != len(universe):
|
||||
row.mark(_PARTIAL)
|
||||
|
||||
|
||||
def _base_reasons(row, gate):
|
||||
outcomes = (
|
||||
(SuiteOutcome.FAILED, _SUITE_FAILED),
|
||||
(SuiteOutcome.TIMED_OUT, _SUITE_TIMEOUT),
|
||||
(SuiteOutcome.UNKNOWN, _SUITE_UNKNOWN),
|
||||
)
|
||||
for outcome, reason in outcomes:
|
||||
gate.step()
|
||||
if row.outcome is outcome:
|
||||
row.mark(reason)
|
||||
if row.evidence is None:
|
||||
row.mark(_NO_EVIDENCE)
|
||||
return
|
||||
for field, reason in (
|
||||
("interval_complete", _INTERVAL), ("runtime_compatible", _RUNTIME),
|
||||
("same_image", _IMAGE), ("children_complete", _CHILDREN),
|
||||
):
|
||||
gate.step()
|
||||
if not getattr(row.evidence, field):
|
||||
row.mark(reason)
|
||||
|
||||
|
||||
def _reduce_row(row, universe, budget, gate):
|
||||
_base_reasons(row, gate)
|
||||
for observation in row.observations:
|
||||
gate.step()
|
||||
if observation.text is None:
|
||||
row.mark(_UNAVAILABLE)
|
||||
continue
|
||||
records = _parse(observation.text, budget, gate)
|
||||
if records is None:
|
||||
row.mark(_MALFORMED)
|
||||
continue
|
||||
_join(records, universe, row, budget, gate)
|
||||
if observation.role in (ObservationRole.PARENT, ObservationRole.SETUP):
|
||||
row.primary_parsed = True
|
||||
if not row.primary_parsed:
|
||||
row.mark(_NO_SETUP if _is_setup(row.test) else _NO_PARENT)
|
||||
|
||||
|
||||
class _Wire:
|
||||
__slots__ = ("budget", "gate", "data")
|
||||
|
||||
def __init__(self, budget, gate):
|
||||
self.budget = budget
|
||||
self.gate = gate
|
||||
self.data = bytearray()
|
||||
|
||||
def emit(self, value):
|
||||
self.budget.output(len(value))
|
||||
for char in value:
|
||||
self.gate.step()
|
||||
self.data.append(char)
|
||||
|
||||
def hex(self, value):
|
||||
self.budget.output(len(value) * 2)
|
||||
for char in value:
|
||||
self.gate.step()
|
||||
self.gate.step()
|
||||
self.data.append(_HEX[char >> 4])
|
||||
self.gate.step()
|
||||
self.data.append(_HEX[char & 15])
|
||||
|
||||
def decimal(self, value):
|
||||
# All emitted integers are <= uint64, so conversion is <=20 digits.
|
||||
self.gate.poll()
|
||||
self.emit(str(value).encode("ascii"))
|
||||
|
||||
def _values(self):
|
||||
for char in self.data:
|
||||
self.gate.step()
|
||||
yield char
|
||||
|
||||
def finish(self):
|
||||
self.gate.poll()
|
||||
result = bytes(self._values())
|
||||
self.gate.poll()
|
||||
return result
|
||||
|
||||
|
||||
def _profile_wire(image, universe, budget, gate):
|
||||
out = _Wire(budget, gate)
|
||||
out.emit(b"CBM_PROFILE_MAP\t2\t")
|
||||
out.hex(image)
|
||||
out.emit(b"\t")
|
||||
out.decimal(len(universe))
|
||||
out.emit(b"\n")
|
||||
for ident, record in enumerate(universe):
|
||||
gate.step()
|
||||
out.decimal(ident)
|
||||
out.emit(b"\t")
|
||||
out.hex(record.raw_name)
|
||||
out.emit(b"\t")
|
||||
gate.poll()
|
||||
out.emit(format(record.function_hash, "016x").encode("ascii"))
|
||||
out.emit(b"\t")
|
||||
out.decimal(len(record.counters))
|
||||
out.emit(b"\n")
|
||||
return out.finish()
|
||||
|
||||
|
||||
def _reason_wire(row, out, gate):
|
||||
first = True
|
||||
for index, reason in enumerate(_REASONS):
|
||||
gate.step()
|
||||
if row.reasons & (1 << index):
|
||||
if not first:
|
||||
out.emit(b";")
|
||||
out.emit(reason)
|
||||
first = False
|
||||
|
||||
|
||||
def _id_wire(row, out, gate):
|
||||
def compare(left, right):
|
||||
gate.step()
|
||||
return (left > right) - (left < right)
|
||||
|
||||
ordered = _sort(row.ids, compare, gate)
|
||||
for index, ident in enumerate(ordered):
|
||||
gate.step()
|
||||
if index:
|
||||
out.emit(b" ")
|
||||
out.decimal(ident)
|
||||
|
||||
|
||||
def _test_wire(rows, budget, gate):
|
||||
out = _Wire(budget, gate)
|
||||
for row in rows:
|
||||
gate.step()
|
||||
out.emit(row.suite)
|
||||
out.emit(b":")
|
||||
out.emit(row.test)
|
||||
out.emit(b"\tincomplete\t" if row.reasons else b"\tcomplete\t")
|
||||
_reason_wire(row, out, gate)
|
||||
out.emit(b"\t")
|
||||
_id_wire(row, out, gate)
|
||||
out.emit(b"\n")
|
||||
return out.finish()
|
||||
|
||||
|
||||
def _digest(data, gate):
|
||||
digest = hashlib.sha256()
|
||||
for start in range(0, len(data), _POLL_INTERVAL):
|
||||
gate.poll()
|
||||
chunk = data[start:start + _POLL_INTERVAL]
|
||||
gate.poll()
|
||||
digest.update(chunk)
|
||||
gate.poll()
|
||||
return digest.digest()
|
||||
|
||||
|
||||
def build_profile_map_v2(
|
||||
image_sha256: bytes,
|
||||
enumeration_text: bytes,
|
||||
expected_suites: Tuple[ExpectedSuite, ...],
|
||||
observations: Tuple[ProfileObservation, ...],
|
||||
row_evidence: Tuple[RowEvidence, ...],
|
||||
limits: ProfileMapLimits,
|
||||
*,
|
||||
cancel: Optional[Callable[[object], bool]] = None,
|
||||
cancel_context: object = None,
|
||||
) -> ProducedMap:
|
||||
"""Produce exact v2 wires or raise, without I/O or a partial result.
|
||||
|
||||
Inputs are immutable exact types. Evidence is supplied by the caller;
|
||||
parsing cannot establish image/process closure, provenance or admission.
|
||||
"""
|
||||
_validate_top(image_sha256, enumeration_text, expected_suites, observations,
|
||||
row_evidence, limits, cancel)
|
||||
gate = _Gate(cancel, cancel_context)
|
||||
gate.poll()
|
||||
budget = _Budget(limits, enumeration_text)
|
||||
rows = _preflight(expected_suites, observations, row_evidence, budget, gate)
|
||||
universe = _parse(enumeration_text, budget, gate, enumeration=True)
|
||||
for row in rows:
|
||||
gate.step()
|
||||
_reduce_row(row, universe, budget, gate)
|
||||
profiles = _profile_wire(image_sha256, universe, budget, gate)
|
||||
tests = _test_wire(rows, budget, gate)
|
||||
profiles_digest = _digest(profiles, gate)
|
||||
tests_digest = _digest(tests, gate)
|
||||
gate.poll()
|
||||
return ProducedMap(profiles, tests, profiles_digest, tests_digest)
|
||||
@@ -0,0 +1,295 @@
|
||||
"""Strict data-only parser for the observed LLVM 22.1.8 frontend proftext.
|
||||
|
||||
parse_profile_text(bytes, ParseLimits, *, cancel=None, cancel_context=None)
|
||||
returns an immutable tuple of ProfileRecord(raw_name, function_hash, counters),
|
||||
ordered by unsigned raw-name bytes and then uint64 hash. Zero values survive.
|
||||
|
||||
Each record is a nonempty name line, '# Func Hash:', uint64, '# Num Counters:',
|
||||
positive uint64, '# Counter Values:', exactly that many uint64 lines, and one
|
||||
mandatory blank line. All delimiters are LF. Decimals are canonical. Empty
|
||||
documents, CR/NUL, extra sections and duplicate name/hash tuples fail closed.
|
||||
Names remain opaque bytes: no normalization, decoding or comment processing.
|
||||
|
||||
Four exact positive integer limits bound input bytes, records, total counter
|
||||
values and total name bytes. Errors derive from ProfileTextError; MemoryError
|
||||
and callback exceptions propagate without a partial result. A pure, prompt
|
||||
cancel(context) predicate must return bool. It is called at entry/final publish
|
||||
and within explicit loops at intervals of at most 4096 byte/element operations.
|
||||
Python allocation/resizing/deallocation and callbacks are not preemptible.
|
||||
|
||||
Limits bound logical input and result items, not exact interpreter heap use.
|
||||
This module performs no I/O and makes no image, authentication or closure claim.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
from typing import Callable
|
||||
|
||||
|
||||
_UINT64_MAX = (1 << 64) - 1
|
||||
_POLL_INTERVAL = 4096
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileRecord:
|
||||
__slots__ = ("raw_name", "function_hash", "counters")
|
||||
|
||||
raw_name: bytes
|
||||
function_hash: int
|
||||
counters: tuple[int, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParseLimits:
|
||||
__slots__ = ("max_input_bytes", "max_records", "max_counter_values", "max_name_bytes")
|
||||
|
||||
max_input_bytes: int
|
||||
max_records: int
|
||||
max_counter_values: int
|
||||
max_name_bytes: int
|
||||
|
||||
|
||||
class ProfileTextError(ValueError):
|
||||
"""Base for parser failures; no partial result is published."""
|
||||
|
||||
|
||||
class InvalidArgumentError(ProfileTextError):
|
||||
"""Invalid public argument, limit, or callback return type."""
|
||||
|
||||
|
||||
class MalformedProfileError(ProfileTextError):
|
||||
"""Unsupported or malformed text, including duplicate identities."""
|
||||
|
||||
|
||||
class LimitExceededError(ProfileTextError):
|
||||
"""A configured logical resource bound would be exceeded."""
|
||||
|
||||
|
||||
class ParseCancelledError(ProfileTextError):
|
||||
"""The caller's predicate requested cancellation."""
|
||||
|
||||
|
||||
class _Gate:
|
||||
__slots__ = ("cancel", "context", "work")
|
||||
|
||||
def __init__(self, cancel, context):
|
||||
self.cancel = cancel
|
||||
self.context = context
|
||||
self.work = 0
|
||||
|
||||
def poll(self):
|
||||
self.work = 0
|
||||
if self.cancel is None:
|
||||
return
|
||||
answer = self.cancel(self.context)
|
||||
if type(answer) is not bool:
|
||||
raise InvalidArgumentError("cancel must return bool")
|
||||
if answer:
|
||||
raise ParseCancelledError("profile parsing cancelled")
|
||||
|
||||
def step(self):
|
||||
self.work += 1
|
||||
if self.work == _POLL_INTERVAL:
|
||||
self.poll()
|
||||
|
||||
|
||||
class _Parser:
|
||||
__slots__ = ("data", "limits", "gate", "position", "names", "values")
|
||||
|
||||
def __init__(self, data, limits, gate):
|
||||
self.data = data
|
||||
self.limits = limits
|
||||
self.gate = gate
|
||||
self.position = 0
|
||||
self.names = 0
|
||||
self.values = 0
|
||||
|
||||
def line(self):
|
||||
start = self.position
|
||||
while self.position < len(self.data):
|
||||
self.gate.step()
|
||||
value = self.data[self.position]
|
||||
self.position += 1
|
||||
if value == 0 or value == 13:
|
||||
raise MalformedProfileError("NUL and CR are unsupported")
|
||||
if value == 10:
|
||||
return start, self.position - 1
|
||||
raise MalformedProfileError("unterminated or missing line")
|
||||
|
||||
def marker(self, expected):
|
||||
start, end = self.line()
|
||||
if end - start != len(expected):
|
||||
raise MalformedProfileError("unexpected record marker")
|
||||
for index, value in enumerate(expected):
|
||||
self.gate.step()
|
||||
if self.data[start + index] != value:
|
||||
raise MalformedProfileError("unexpected record marker")
|
||||
|
||||
def uint64(self):
|
||||
start, end = self.line()
|
||||
length = end - start
|
||||
if length == 0 or length > 20:
|
||||
raise MalformedProfileError("invalid unsigned decimal")
|
||||
if length > 1 and self.data[start] == 48:
|
||||
raise MalformedProfileError("noncanonical unsigned decimal")
|
||||
value = 0
|
||||
for index in range(start, end):
|
||||
self.gate.step()
|
||||
digit = self.data[index] - 48
|
||||
if digit < 0 or digit > 9:
|
||||
raise MalformedProfileError("invalid unsigned decimal")
|
||||
if value > (_UINT64_MAX - digit) // 10:
|
||||
raise MalformedProfileError("unsigned decimal overflow")
|
||||
value = value * 10 + digit
|
||||
return value
|
||||
|
||||
def name_values(self, start, end):
|
||||
for index in range(start, end):
|
||||
self.gate.step()
|
||||
yield self.data[index]
|
||||
|
||||
def counter_values(self, count):
|
||||
for _ in range(count):
|
||||
self.gate.step()
|
||||
yield self.uint64()
|
||||
|
||||
def record(self):
|
||||
start, end = self.line()
|
||||
length = end - start
|
||||
if length == 0:
|
||||
raise MalformedProfileError("empty record name")
|
||||
if length > self.limits.max_name_bytes - self.names:
|
||||
raise LimitExceededError("total name bytes limit exceeded")
|
||||
self.names += length
|
||||
self.marker(b"# Func Hash:")
|
||||
function_hash = self.uint64()
|
||||
self.marker(b"# Num Counters:")
|
||||
count = self.uint64()
|
||||
if count == 0:
|
||||
raise MalformedProfileError("counter count must be positive")
|
||||
if count > self.limits.max_counter_values - self.values:
|
||||
raise LimitExceededError("total counter values limit exceeded")
|
||||
self.values += count
|
||||
self.marker(b"# Counter Values:")
|
||||
# Each remaining value needs at least '0\n', then the final blank line.
|
||||
remaining = len(self.data) - self.position
|
||||
if remaining < 1 or count > (remaining - 1) // 2:
|
||||
raise MalformedProfileError("truncated counter values")
|
||||
self.gate.poll()
|
||||
counters = tuple(self.counter_values(count))
|
||||
self.gate.poll()
|
||||
blank_start, blank_end = self.line()
|
||||
if blank_start != blank_end:
|
||||
raise MalformedProfileError("missing record blank line")
|
||||
self.gate.poll()
|
||||
name = bytes(self.name_values(start, end))
|
||||
self.gate.poll()
|
||||
return ProfileRecord(name, function_hash, counters)
|
||||
|
||||
def records(self):
|
||||
self.gate.poll()
|
||||
records = []
|
||||
while self.position < len(self.data):
|
||||
self.gate.step()
|
||||
if len(records) == self.limits.max_records:
|
||||
raise LimitExceededError("record count limit exceeded")
|
||||
record = self.record()
|
||||
self.gate.poll()
|
||||
records.append(record)
|
||||
if not records:
|
||||
raise MalformedProfileError("empty document")
|
||||
return records
|
||||
|
||||
|
||||
def _compare(left, right, gate):
|
||||
common = min(len(left.raw_name), len(right.raw_name))
|
||||
for index in range(common):
|
||||
gate.step()
|
||||
a = left.raw_name[index]
|
||||
b = right.raw_name[index]
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
gate.step()
|
||||
if len(left.raw_name) != len(right.raw_name):
|
||||
return -1 if len(left.raw_name) < len(right.raw_name) else 1
|
||||
if left.function_hash == right.function_hash:
|
||||
return 0
|
||||
return -1 if left.function_hash < right.function_hash else 1
|
||||
|
||||
|
||||
def _merge(source, target, start, middle, end, gate):
|
||||
left, right = start, middle
|
||||
for destination in range(start, end):
|
||||
gate.step()
|
||||
take_left = right == end or (
|
||||
left < middle and _compare(source[left], source[right], gate) <= 0
|
||||
)
|
||||
if take_left:
|
||||
target[destination] = source[left]
|
||||
left += 1
|
||||
else:
|
||||
target[destination] = source[right]
|
||||
right += 1
|
||||
|
||||
|
||||
def _sorted_unique(records, gate):
|
||||
gate.poll()
|
||||
scratch = []
|
||||
for _ in records:
|
||||
gate.step()
|
||||
scratch.append(None)
|
||||
gate.poll()
|
||||
width = 1
|
||||
length = len(records)
|
||||
while width < length:
|
||||
for start in range(0, length, width * 2):
|
||||
middle = min(start + width, length)
|
||||
end = min(start + width * 2, length)
|
||||
_merge(records, scratch, start, middle, end, gate)
|
||||
records, scratch = scratch, records
|
||||
width *= 2
|
||||
for index in range(1, length):
|
||||
gate.step()
|
||||
if _compare(records[index - 1], records[index], gate) == 0:
|
||||
raise MalformedProfileError("duplicate name/hash identity")
|
||||
return records
|
||||
|
||||
|
||||
def _record_values(records, gate):
|
||||
for record in records:
|
||||
gate.step()
|
||||
yield record
|
||||
|
||||
|
||||
def _validate_arguments(data, limits, cancel):
|
||||
if type(data) is not bytes or type(limits) is not ParseLimits:
|
||||
raise InvalidArgumentError("expected bytes and ParseLimits")
|
||||
for value in (limits.max_input_bytes, limits.max_records,
|
||||
limits.max_counter_values, limits.max_name_bytes):
|
||||
if type(value) is not int or value <= 0 or value > sys.maxsize:
|
||||
raise InvalidArgumentError("limits must be positive bounded integers")
|
||||
if cancel is not None and not callable(cancel):
|
||||
raise InvalidArgumentError("cancel must be callable or None")
|
||||
|
||||
|
||||
def parse_profile_text(
|
||||
data: bytes,
|
||||
limits: ParseLimits,
|
||||
*,
|
||||
cancel: Callable[[object], bool] | None = None,
|
||||
cancel_context: object = None,
|
||||
) -> tuple[ProfileRecord, ...]:
|
||||
"""Parse all records or raise a specific error; never return a partial result."""
|
||||
_validate_arguments(data, limits, cancel)
|
||||
gate = _Gate(cancel, cancel_context)
|
||||
gate.poll()
|
||||
if len(data) > limits.max_input_bytes:
|
||||
raise LimitExceededError("input bytes limit exceeded")
|
||||
parser = _Parser(data, limits, gate)
|
||||
records = _sorted_unique(parser.records(), gate)
|
||||
gate.poll()
|
||||
result = tuple(_record_values(records, gate))
|
||||
gate.poll()
|
||||
return result
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# replay-selector.sh — replay every September 2026 PR push through the lane
|
||||
# selector (scripts/ci/select-lanes.sh) and hold it to the measured history.
|
||||
#
|
||||
# The rule set was chosen by replaying this history offline; this keeps the
|
||||
# committed selector honest against the same data, as a contract step:
|
||||
# 1. every push (767) gets exactly the tier recorded in fixtures/2026-09-
|
||||
# pushes.tsv. Where that differs from the analysis tier the row must name
|
||||
# its documented deviation, and a deviation may only ever raise the tier
|
||||
# (the fail-safe direction) -- never quietly select less than was measured;
|
||||
# 2. every real failure of the month (22, fixtures/2026-09-real-failures.tsv)
|
||||
# had its catching lane selected for its push;
|
||||
# 3. every push runs the static contract steps (the contracts lane or any
|
||||
# test leg) -- they police docs, test-infrastructure/ and lint config.
|
||||
# Pure data, one selector process (--batch over the distinct file sets).
|
||||
#
|
||||
# Usage: scripts/test-impact/replay-selector.sh [repo-root]
|
||||
set -euo pipefail
|
||||
|
||||
case "${1:-}" in
|
||||
-h | --help)
|
||||
sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "replay-selector.sh: unknown option '$1'. Please consult --help." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
ROOT="${1:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
||||
FIXTURES="$ROOT/scripts/test-impact/fixtures"
|
||||
WORK=$(mktemp -d "${TMPDIR:-/tmp}/cbm-replay-selector.XXXXXX")
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
python3 - "$FIXTURES" "$WORK/batch.jsonl" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
fixtures, batch = sys.argv[1:3]
|
||||
|
||||
|
||||
def rows(name):
|
||||
with open(f"{fixtures}/{name}", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip("\r\n")
|
||||
if line and not line.startswith("#"):
|
||||
yield line.split("\t")
|
||||
|
||||
|
||||
with open(batch, "w", encoding="utf-8") as out:
|
||||
for fileset, paths in rows("2026-09-filesets.tsv"):
|
||||
out.write(json.dumps({"id": fileset, "files": paths.split(" ")}) + "\n")
|
||||
PY
|
||||
|
||||
bash "$ROOT/scripts/ci/select-lanes.sh" --batch "$WORK/batch.jsonl" >"$WORK/decisions.jsonl"
|
||||
|
||||
python3 - "$FIXTURES" "$WORK/decisions.jsonl" <<'PY'
|
||||
import collections
|
||||
import json
|
||||
import sys
|
||||
|
||||
fixtures, decisions_path = sys.argv[1:3]
|
||||
TIERS = ["T0a-docs", "T0b-nonproduct", "T1-tests", "T2-extract", "T3-mid",
|
||||
"T4-platform", "T5-core"]
|
||||
DEVIATIONS = {"makefile-by-name"}
|
||||
|
||||
|
||||
def rows(name, width):
|
||||
with open(f"{fixtures}/{name}", encoding="utf-8") as fh:
|
||||
for number, line in enumerate(fh, 1):
|
||||
line = line.rstrip("\r\n")
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
cols = line.split("\t")
|
||||
if len(cols) != width:
|
||||
sys.exit(f"FAIL: {name}:{number}: expected {width} columns, got {len(cols)}")
|
||||
yield cols
|
||||
|
||||
|
||||
decisions = {}
|
||||
with open(decisions_path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
row = json.loads(line)
|
||||
decisions[row["id"]] = row
|
||||
|
||||
failures = []
|
||||
pushes = {}
|
||||
matched = deviated = 0
|
||||
tiers = collections.Counter()
|
||||
for run_id, pr, sha, fileset, analysis, expected, note in rows("2026-09-pushes.tsv", 7):
|
||||
decision = decisions.get(fileset)
|
||||
if decision is None:
|
||||
failures.append(f"run {run_id}: fileset {fileset} missing")
|
||||
continue
|
||||
pushes[run_id] = decision
|
||||
tiers[decision["tier"]] += 1
|
||||
if decision["tier"] != expected:
|
||||
failures.append(f"run {run_id} (PR #{pr}, {sha[:10]}): selector tier "
|
||||
f"{decision['tier']}, expected {expected} -- reasons: "
|
||||
f"{'; '.join(decision['reasons'])}")
|
||||
continue
|
||||
if analysis == expected:
|
||||
if note:
|
||||
failures.append(f"run {run_id}: note '{note}' on a row with no deviation")
|
||||
matched += 1
|
||||
elif note not in DEVIATIONS:
|
||||
failures.append(f"run {run_id}: tier differs from the analysis ({analysis}) "
|
||||
f"without a documented deviation")
|
||||
elif TIERS.index(expected) < TIERS.index(analysis):
|
||||
failures.append(f"run {run_id}: deviation '{note}' LOWERS the tier "
|
||||
f"{analysis} -> {expected}")
|
||||
else:
|
||||
deviated += 1
|
||||
|
||||
# The static contract steps police docs, test-infrastructure/ and lint config,
|
||||
# so every push runs them: in the contracts job or inside any test leg.
|
||||
CONTRACT_CARRIERS = {"contracts", "unix-x86", "unix-arm64", "unix-macos14",
|
||||
"unix-macos-intel", "windows"}
|
||||
uncovered = collections.Counter(d["tier"] for d in pushes.values()
|
||||
if not set(d["lanes"]) & CONTRACT_CARRIERS)
|
||||
for tier, count in sorted(uncovered.items()):
|
||||
failures.append(f"{count} {tier} push(es) run no contract step (neither the "
|
||||
f"contracts lane nor a test leg)")
|
||||
|
||||
caught = 0
|
||||
for run_id, attempt, pr, sha, lane, why in rows("2026-09-real-failures.tsv", 6):
|
||||
decision = pushes.get(run_id)
|
||||
if decision is None:
|
||||
failures.append(f"real failure run {run_id}: no such push in the replay")
|
||||
elif lane not in decision["lanes"]:
|
||||
failures.append(f"real failure run {run_id} (PR #{pr}): lane {lane} NOT selected "
|
||||
f"(tier {decision['tier']}) -- {why}")
|
||||
else:
|
||||
caught += 1
|
||||
|
||||
if failures:
|
||||
print("SELECTOR HISTORY REPLAY FAILED:")
|
||||
for failure in failures:
|
||||
print(f" {failure}")
|
||||
sys.exit(1)
|
||||
print(f"selector history replay OK: {len(pushes)} pushes, tier as expected for all "
|
||||
f"({matched} equal to the analysis, {deviated} documented fail-safe deviations); "
|
||||
f"{caught} real failures, every catching lane selected; every push runs the "
|
||||
f"contract steps")
|
||||
print(" pushes per tier: " + ", ".join(f"{t} {tiers[t]}" for t in TIERS))
|
||||
print(f" full runs: {sum(1 for d in pushes.values() if d['full'])}")
|
||||
PY
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Turn a test-impact answer (`codebase-memory-mcp test-impact select`) into
|
||||
what a CI consumer runs.
|
||||
|
||||
selection.py suites ANSWER
|
||||
One line per suite whose tests the answer selects, or a single `*` when a
|
||||
lane runs everything. The team-artifact producer re-runs exactly these
|
||||
suites with coverage (scripts/ci/test-impact-publish.sh).
|
||||
|
||||
selection.py filter ANSWER --lane LANE --out FILE [--optional-out FILE2]
|
||||
exit 0 narrowed: FILE holds the lane's CBM_TEST_ONLY_FILE lines, and
|
||||
FILE2 the selected suites the engine saw registered only
|
||||
conditionally (CONDITIONAL): a build may legitimately lack them
|
||||
(scripts/run-tests-parallel.sh, CBM_TEST_SELECTION_OPTIONAL)
|
||||
exit 11 the lane selects nothing: FILE is empty
|
||||
anything else: run everything (FILE and FILE2 are removed)
|
||||
|
||||
It fails safe by construction: a run-all answer, a lane it cannot find, an
|
||||
unknown schema or filter encoding, a token that is not `suite` or
|
||||
`suite:test`, and a filter that disagrees with the lane's own suite list all
|
||||
mean "run everything", and so does any crash (exit 1).
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
SCHEMAS = {"cbm.test_impact.v0", "cbm.test_impact.v1"}
|
||||
TOKEN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(:[A-Za-z_][A-Za-z0-9_]*)?$")
|
||||
NARROWED, NOTHING, RUN_ALL = 0, 11, 10
|
||||
|
||||
|
||||
def suites(answer):
|
||||
if answer.get("decision") == "run_all":
|
||||
return ["*"]
|
||||
out = []
|
||||
for lane in answer.get("lanes") or []:
|
||||
if lane.get("decision") == "run_all" or lane.get("suites") is None:
|
||||
return ["*"]
|
||||
out += [suite["suite"] for suite in lane["suites"]]
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def run_all(why):
|
||||
print(f"selection: run everything: {why}", file=sys.stderr)
|
||||
return RUN_ALL
|
||||
|
||||
|
||||
def lane_filter(answer, name):
|
||||
"""-> (exit code, lines)"""
|
||||
if answer.get("schema") not in SCHEMAS:
|
||||
return run_all(f"unknown schema {answer.get('schema')!r}"), []
|
||||
if answer.get("decision") == "run_all":
|
||||
return run_all("the answer runs everything"), []
|
||||
lanes = [lane for lane in answer.get("lanes") or [] if lane.get("lane") == name]
|
||||
if len(lanes) != 1:
|
||||
return run_all(f"lane {name!r} appears {len(lanes)} times"), []
|
||||
lane = lanes[0]
|
||||
decision = lane.get("decision")
|
||||
if decision == "skip":
|
||||
if lane.get("suites") or lane.get("runner_filter") is not None:
|
||||
return run_all("a skipped lane that still lists suites or a filter"), []
|
||||
return NOTHING, []
|
||||
if decision != "selected" or not isinstance(lane.get("suites"), list):
|
||||
return run_all(f"lane decision {decision!r}"), []
|
||||
flt = lane.get("runner_filter")
|
||||
if not isinstance(flt, dict) or flt.get("encoding") != "cbm-test-only-lines" or \
|
||||
not isinstance(flt.get("value"), str):
|
||||
return run_all("no cbm-test-only-lines filter"), []
|
||||
lines = [line for line in flt["value"].split("\n") if line]
|
||||
bad = [line for line in lines if not TOKEN.match(line)]
|
||||
if not lines or bad:
|
||||
return run_all(f"filter tokens unusable ({bad[:3] or 'empty'})"), []
|
||||
expected = set()
|
||||
for suite in lane["suites"]:
|
||||
if suite.get("mode") == "whole":
|
||||
expected.add(suite["suite"])
|
||||
elif suite.get("mode") == "tests" and suite.get("tests"):
|
||||
expected |= {f"{suite['suite']}:{test['test']}" for test in suite["tests"]}
|
||||
else:
|
||||
return run_all(f"suite {suite.get('suite')!r} has mode {suite.get('mode')!r}"), []
|
||||
if set(lines) != expected:
|
||||
return run_all("the filter disagrees with the lane's suite list"), []
|
||||
return NARROWED, lines
|
||||
|
||||
|
||||
def conditional_suites(answer, name):
|
||||
"""The lane's selected suites whose registration the engine saw as conditional."""
|
||||
for lane in answer.get("lanes") or []:
|
||||
if lane.get("lane") == name:
|
||||
return sorted({suite["suite"] for suite in lane.get("suites") or []
|
||||
if "CONDITIONAL" in (suite.get("whole_reason") or [])})
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
s = sub.add_parser("suites")
|
||||
s.add_argument("answer")
|
||||
f = sub.add_parser("filter")
|
||||
f.add_argument("answer")
|
||||
f.add_argument("--lane", required=True)
|
||||
f.add_argument("--out", required=True)
|
||||
f.add_argument("--optional-out")
|
||||
args = parser.parse_args()
|
||||
if args.command == "suites":
|
||||
# LF on every host, like the files below: a Windows python would
|
||||
# otherwise write CRLF.
|
||||
sys.stdout.reconfigure(newline="\n")
|
||||
with open(args.answer, encoding="utf-8") as handle:
|
||||
print("\n".join(suites(json.load(handle))))
|
||||
return 0
|
||||
for stale in (args.out, args.optional_out):
|
||||
if stale and os.path.exists(stale):
|
||||
os.remove(stale)
|
||||
try:
|
||||
with open(args.answer, encoding="utf-8") as handle:
|
||||
answer = json.load(handle)
|
||||
except (OSError, ValueError) as error:
|
||||
return run_all(f"unreadable answer ({error})")
|
||||
if not isinstance(answer, dict):
|
||||
return run_all("the answer is not an object")
|
||||
code, lines = lane_filter(answer, args.lane)
|
||||
if code in (NARROWED, NOTHING):
|
||||
with open(args.out, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write("".join(line + "\n" for line in lines))
|
||||
if args.optional_out:
|
||||
optional = conditional_suites(answer, args.lane) if code == NARROWED else []
|
||||
with open(args.optional_out, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write("".join(suite + "\n" for suite in optional))
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+92
-4
@@ -10,7 +10,7 @@ cd "$ROOT"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/test.sh [--suites LIST] [--arch ARCH] [VAR=VAL ...]
|
||||
Usage: scripts/test.sh [--suites LIST | --tsan | --contracts-only] [--arch ARCH] [VAR=VAL ...]
|
||||
|
||||
The canonical test entry: identical in local CI, PR CI, dry run and release.
|
||||
DEFAULT (no --suites) is exactly what CI runs: static contract checks
|
||||
@@ -29,6 +29,10 @@ Modes:
|
||||
--tsan ThreadSanitizer leg (data-race gate): builds and runs the
|
||||
widened TSan runner via make test-tsan — the same leg CI's
|
||||
tsan jobs and the compose test-tsan service run.
|
||||
--contracts-only
|
||||
Only the static contract steps (Step 0*) of the default leg:
|
||||
no compiler, no build, no suites. PR CI runs this for
|
||||
docs-only and non-product changes (tiers T0a, T0b).
|
||||
|
||||
Options:
|
||||
--arch ARCH Force target arch (arm64 | x86_64), e.g. under Rosetta.
|
||||
@@ -50,6 +54,11 @@ Environment:
|
||||
CBM_RUN_HANG_TEST=1 Opt-in C++ index-hang regression (#410, needs prod).
|
||||
CBM_NO_CCACHE=1 Disable the content-verified compiler cache.
|
||||
CBM_TEST_SHARD/_LEG Set by CI's sharded legs; leave unset locally.
|
||||
CBM_TEST_SELECTION_DIR PR CI's test selection (smart CI): a directory with
|
||||
test-only.txt (suite / suite:test lines) and
|
||||
optional-suites.txt. Narrows the parallel suite step
|
||||
only; contract steps and CBM_TEST_SEQUENTIAL=1 runs
|
||||
keep running everything.
|
||||
|
||||
Examples:
|
||||
scripts/test.sh # the full venue leg (what CI runs)
|
||||
@@ -64,11 +73,13 @@ EOF
|
||||
# silently swallowed — agents must know exactly what a run will do.
|
||||
SUITES=""
|
||||
TSAN=0
|
||||
CONTRACTS_ONLY=0
|
||||
prev_arg=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--tsan) :;;
|
||||
--contracts-only) :;;
|
||||
--suites) :;; # next arg is the value, handled below
|
||||
--suites=*) SUITES="${arg#--suites=}" ;;
|
||||
--arch) :;; # next arg is the value, handled below
|
||||
@@ -96,6 +107,7 @@ done
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--tsan) TSAN=1 ;;
|
||||
--contracts-only) CONTRACTS_ONLY=1 ;;
|
||||
arm64|x86_64)
|
||||
if [[ "${prev_arg2:-}" == "--arch" ]]; then
|
||||
export CBM_ARCH="$arg"
|
||||
@@ -121,6 +133,10 @@ if [ "$TSAN" -eq 1 ] && [ -n "$SUITES" ]; then
|
||||
echo "test.sh: --tsan and --suites are separate modes (the TSan leg has its own suite set). Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "$CONTRACTS_ONLY" -eq 1 ] && { [ "$TSAN" -eq 1 ] || [ -n "$SUITES" ]; }; then
|
||||
echo "test.sh: --contracts-only is its own mode (no build, no suites). Please consult --help." >&2
|
||||
exit 2
|
||||
fi
|
||||
prev_arg=""
|
||||
|
||||
# Also support --arch=value
|
||||
@@ -152,7 +168,7 @@ for arg in "$@"; do
|
||||
CC=*|CXX=*) export "${arg}" ;;
|
||||
--arch|--arch=*) ;; # already handled
|
||||
arm64|x86_64) ;; # already handled
|
||||
--tsan) ;; # already handled
|
||||
--tsan|--contracts-only) ;; # already handled
|
||||
--suites|--suites=*) ;; # already handled (value skipped via prev_arg below)
|
||||
BUILD_DIR=*) BUILD_DIR="${arg#BUILD_DIR=}"; MAKE_ARGS+=("$arg") ;;
|
||||
SANITIZE=*)
|
||||
@@ -338,9 +354,45 @@ bash "$ROOT/tests/test_version_metadata_contract.sh"
|
||||
echo "=== Step 0y: VM leg verdict contract ==="
|
||||
bash "$ROOT/tests/test_vm_verdict_contract.sh"
|
||||
|
||||
echo "=== Step 0z: setup scripts install through the installers' verified path ==="
|
||||
echo "=== Step 0y2: isolated Windows venue contract ==="
|
||||
bash "$ROOT/tests/test_vm_isolation_contract.sh"
|
||||
|
||||
echo "=== Step 0y3: setup scripts install through the installers' verified path ==="
|
||||
bash "$ROOT/tests/test_setup_scripts_contract.sh"
|
||||
|
||||
# Step 0z: PR CI runs only the lanes scripts/ci/select-lanes.sh selects, so a
|
||||
# wrong selection is a silent gate loss. The decision table, and the replay of
|
||||
# every September 2026 PR push and real failure against it.
|
||||
echo "=== Step 0z: PR lane selector contract ==="
|
||||
bash "$ROOT/tests/test_select_lanes.sh"
|
||||
echo "=== Step 0z2: lane selector history replay ==="
|
||||
bash "$ROOT/scripts/test-impact/replay-selector.sh"
|
||||
echo "=== Step 0z3: lane-aware aggregate gates (ci-ok, shard union) ==="
|
||||
bash "$ROOT/tests/test_lane_gate_contract.sh"
|
||||
echo "=== Step 0z4: lane wiring (PRs select, dry run and release run all) ==="
|
||||
bash "$ROOT/tests/test_lane_wiring_contract.sh"
|
||||
echo "=== Step 0z5: test-impact shadow prediction contract ==="
|
||||
bash "$ROOT/tests/test_test_impact_predict.sh"
|
||||
# Step 0z6: the per-test coverage map must never report a test as executing
|
||||
# less than it did: every case where the builder cannot know (a killed forked
|
||||
# child, an unreadable profile, a failed suite) has to come out `incomplete`.
|
||||
echo "=== Step 0z6: per-test coverage map builder contract ==="
|
||||
bash "$ROOT/tests/test_coverage_map.sh"
|
||||
# Step 0z7: a push's incremental coverage map carries the suites it did not
|
||||
# re-run onto the new image's function table; a wrong remap would hand the PR
|
||||
# selection a map that claims tests ran functions they never touched.
|
||||
echo "=== Step 0z7: incremental coverage map merge contract ==="
|
||||
bash "$ROOT/tests/test_coverage_merge.sh"
|
||||
# Step 0z8: the one place a test-impact answer becomes what PR CI runs. Only a
|
||||
# well-formed, self-consistent selection may narrow; everything else runs all.
|
||||
echo "=== Step 0z8: test selection (answer -> runner filter) contract ==="
|
||||
bash "$ROOT/tests/test_test_impact_selection.sh"
|
||||
|
||||
if [ "$CONTRACTS_ONLY" -eq 1 ]; then
|
||||
echo "=== test.sh: contracts-only — every contract step passed ==="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Verify compiler supports target arch
|
||||
verify_compiler "$CC"
|
||||
|
||||
@@ -354,10 +406,46 @@ BUILD_DIR="$BUILD_DIR" scripts/clean.sh
|
||||
# CBM_TEST_SEQUENTIAL=1 restores the single-process runner.
|
||||
make -j"$NPROC" -f Makefile.cbm "$BUILD_DIR/test-runner" ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
||||
assert_test_runner_build_config "$BUILD_DIR/test-runner" "$EXPECTED_SANITIZED"
|
||||
|
||||
# Step 2b: per-test selection process regression. CBM_TEST_ONLY must run exactly
|
||||
# the tests it names and fail on a token that matches nothing; a suite cannot
|
||||
# assert that about the runner executing it. Runs against the runner just built
|
||||
# and BEFORE the suites: it takes seconds, and a selection that silently drops
|
||||
# tests is a property of the harness every later result depends on.
|
||||
echo "=== Step 2b: per-test selection regression (CBM_TEST_ONLY) ==="
|
||||
CBM_TEST_RUNNER="$ROOT/$BUILD_DIR/test-runner" bash "$ROOT/tests/test_harness_test_only.sh"
|
||||
# Step 2c: PR CI's test selection (smart CI) through the parallel harness — the
|
||||
# selected suites only, unknown suites and tests fail, conditional ones drop.
|
||||
echo "=== Step 2c: parallel-harness test selection regression ==="
|
||||
CBM_TEST_RUNNER="$ROOT/$BUILD_DIR/test-runner" bash "$ROOT/tests/test_harness_selection.sh"
|
||||
|
||||
# PR CI's test selection (smart CI, the select-tests job) narrows the parallel
|
||||
# suite step and nothing else (scripts/run-tests-parallel.sh). A selection that
|
||||
# was promised but is missing is a plumbing fault: fail, never run silently.
|
||||
SELECTION_ENV=()
|
||||
if [ -n "${CBM_TEST_SELECTION_DIR:-}" ]; then
|
||||
selection_dir="$CBM_TEST_SELECTION_DIR"
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
selection_dir="$(cygpath -u "$selection_dir")"
|
||||
fi
|
||||
if [ ! -s "$selection_dir/test-only.txt" ]; then
|
||||
echo "FAIL: CBM_TEST_SELECTION_DIR=$CBM_TEST_SELECTION_DIR holds no test-only.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
SELECTION_ENV=(CBM_TEST_SELECTION_FILE="$selection_dir/test-only.txt")
|
||||
if [ -f "$selection_dir/optional-suites.txt" ]; then
|
||||
SELECTION_ENV+=(CBM_TEST_SELECTION_OPTIONAL="$selection_dir/optional-suites.txt")
|
||||
fi
|
||||
if [ "${CBM_TEST_SEQUENTIAL:-0}" = "1" ]; then
|
||||
echo "test.sh: CBM_TEST_SEQUENTIAL=1 runs every test; the selection narrows the parallel harness only"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${CBM_TEST_SEQUENTIAL:-0}" = "1" ]; then
|
||||
make -f Makefile.cbm test ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
||||
else
|
||||
make -f Makefile.cbm test-par ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
||||
env ${SELECTION_ENV[@]+"${SELECTION_ENV[@]}"} \
|
||||
make -f Makefile.cbm test-par ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
||||
fi
|
||||
|
||||
# Step 3a: the runner must ignore an inherited git repository environment
|
||||
|
||||
+1
-1
@@ -1564,7 +1564,7 @@ static const char skill_content[] =
|
||||
"`check_index_coverage`, `manage_adr`, `ingest_traces`\n"
|
||||
"\n"
|
||||
"## Edge Types\n"
|
||||
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
|
||||
"CALLS, HTTP_CALLS, ASYNC_CALLS, SPAWNS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
|
||||
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, REFERENCES_FILE,\n"
|
||||
"FILE_CHANGES_WITH, SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n"
|
||||
"CONTAINS_PACKAGE\n"
|
||||
|
||||
+91
-1
@@ -2778,6 +2778,96 @@ static bool eval_where(const cbm_where_clause_t *w, binding_t *b) {
|
||||
return is_and;
|
||||
}
|
||||
|
||||
/* Three-valued WHERE evaluation for the early (seed-row) pass in
|
||||
* execute_single: only the first MATCH alias is bound there, so a leaf on
|
||||
* any other alias is UNKNOWN rather than true. The two-valued evaluator
|
||||
* returns true for an unbound alias, which NOT / XOR then invert -- pruning
|
||||
* every seed before relationship expansion (distilled from PR #1245). The
|
||||
* full evaluation reruns after expansion (Step 3), so the early pass only has
|
||||
* to prune on a definite false. Allocation-free, O(expression nodes). */
|
||||
typedef enum { CYP_PARTIAL_FALSE = 0, CYP_PARTIAL_TRUE, CYP_PARTIAL_UNKNOWN } cyp_partial_t;
|
||||
|
||||
static cyp_partial_t partial_and(cyp_partial_t l, cyp_partial_t r) {
|
||||
if (l == CYP_PARTIAL_FALSE || r == CYP_PARTIAL_FALSE) {
|
||||
return CYP_PARTIAL_FALSE;
|
||||
}
|
||||
return (l == CYP_PARTIAL_TRUE && r == CYP_PARTIAL_TRUE) ? CYP_PARTIAL_TRUE
|
||||
: CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
|
||||
static cyp_partial_t partial_or(cyp_partial_t l, cyp_partial_t r) {
|
||||
if (l == CYP_PARTIAL_TRUE || r == CYP_PARTIAL_TRUE) {
|
||||
return CYP_PARTIAL_TRUE;
|
||||
}
|
||||
return (l == CYP_PARTIAL_FALSE && r == CYP_PARTIAL_FALSE) ? CYP_PARTIAL_FALSE
|
||||
: CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
|
||||
/* A condition whose alias is not bound yet is UNKNOWN; a literal-only LHS
|
||||
* (func condition with no variable arg) has nothing to wait for. */
|
||||
static cyp_partial_t eval_condition_partial(const cbm_condition_t *c, binding_t *b) {
|
||||
if (c->variable && !binding_get(b, c->variable) && !binding_get_edge(b, c->variable)) {
|
||||
return CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
return eval_condition(c, b) ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||
}
|
||||
|
||||
static cyp_partial_t eval_expr_partial(const cbm_expr_t *e, // NOLINT(misc-no-recursion)
|
||||
binding_t *b) {
|
||||
if (!e) {
|
||||
return CYP_PARTIAL_TRUE;
|
||||
}
|
||||
if (e->type == EXPR_CONDITION) {
|
||||
return eval_condition_partial(&e->cond, b);
|
||||
}
|
||||
cyp_partial_t left = eval_expr_partial(e->left, b);
|
||||
if (e->type == EXPR_NOT) {
|
||||
if (left == CYP_PARTIAL_UNKNOWN) {
|
||||
return CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
return left == CYP_PARTIAL_TRUE ? CYP_PARTIAL_FALSE : CYP_PARTIAL_TRUE;
|
||||
}
|
||||
/* Same short-circuits as eval_expr: a definite left decides AND / OR. */
|
||||
if (e->type == EXPR_AND && left == CYP_PARTIAL_FALSE) {
|
||||
return CYP_PARTIAL_FALSE;
|
||||
}
|
||||
if (e->type == EXPR_OR && left == CYP_PARTIAL_TRUE) {
|
||||
return CYP_PARTIAL_TRUE;
|
||||
}
|
||||
cyp_partial_t right = eval_expr_partial(e->right, b);
|
||||
if (e->type == EXPR_AND) {
|
||||
return partial_and(left, right);
|
||||
}
|
||||
if (e->type == EXPR_OR) {
|
||||
return partial_or(left, right);
|
||||
}
|
||||
/* EXPR_XOR */
|
||||
if (left == CYP_PARTIAL_UNKNOWN || right == CYP_PARTIAL_UNKNOWN) {
|
||||
return CYP_PARTIAL_UNKNOWN;
|
||||
}
|
||||
return left != right ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||
}
|
||||
|
||||
static cyp_partial_t eval_where_partial(const cbm_where_clause_t *w, binding_t *b) {
|
||||
if (!w) {
|
||||
return CYP_PARTIAL_TRUE;
|
||||
}
|
||||
if (w->root) {
|
||||
return eval_expr_partial(w->root, b);
|
||||
}
|
||||
/* Legacy flat evaluation */
|
||||
if (w->count == 0) {
|
||||
return CYP_PARTIAL_TRUE;
|
||||
}
|
||||
bool is_and = (w->op && strcmp(w->op, "AND") == 0) != 0;
|
||||
cyp_partial_t result = is_and ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||
for (int i = 0; i < w->count; i++) {
|
||||
cyp_partial_t r = eval_condition_partial(&w->conditions[i], b);
|
||||
result = is_and ? partial_and(result, r) : partial_or(result, r);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Check if a string value looks like a regex pattern. */
|
||||
static bool looks_like_regex(const char *s) {
|
||||
if (!s) {
|
||||
@@ -5281,7 +5371,7 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec
|
||||
binding_t b = {0};
|
||||
b.store = store;
|
||||
binding_set(&b, var_name, &scanned[i]);
|
||||
bool pass = !q->where || eval_where(q->where, &b);
|
||||
bool pass = eval_where_partial(q->where, &b) != CYP_PARTIAL_FALSE;
|
||||
if (pass) {
|
||||
bindings[bind_count++] = b;
|
||||
} else {
|
||||
|
||||
@@ -178,6 +178,10 @@ cbm_daemon_process_role_t cbm_daemon_process_role(int argc, char *const argv[])
|
||||
* project, so it needs no daemon. Listed here rather than routed through
|
||||
* the daemon so enrolling a root cannot depend on daemon state. */
|
||||
"allow-root",
|
||||
/* test-impact runs its own frozen index of a pinned tree in process
|
||||
* (CI: publish the team artifact, select tests); it never reads or
|
||||
* writes the daemon's projects, so it needs no daemon. */
|
||||
"test-impact",
|
||||
};
|
||||
/* Stop at the first top-level mode token. Tool names, flag values, and JSON
|
||||
* following `cli` are opaque user input: a search query named "install"
|
||||
|
||||
@@ -7078,3 +7078,21 @@ int cbm_daemon_ipc_receive_frame(cbm_daemon_ipc_connection_t *connection, uint32
|
||||
return cbm_daemon_ipc_receive_frame_bounded(connection, timeout_ms, CBM_DAEMON_MAX_FRAME_SIZE,
|
||||
frame_out, payload_out);
|
||||
}
|
||||
|
||||
int cbm_daemon_ipc_receive_frame_after_failed_send(cbm_daemon_ipc_connection_t *connection,
|
||||
uint32_t timeout_ms, uint32_t max_payload_length,
|
||||
cbm_daemon_frame_t *frame_out,
|
||||
uint8_t **payload_out) {
|
||||
if (!connection) {
|
||||
return cbm_daemon_ipc_receive_frame_bounded(NULL, timeout_ms, max_payload_length, frame_out,
|
||||
payload_out);
|
||||
}
|
||||
/* The poison came from the failed send alone (nothing was read yet), and it
|
||||
* guards the OUTBOUND frame boundary; the inbound one is intact. Lift it for
|
||||
* this one receive, then restore it so no later send can reuse the stream. */
|
||||
atomic_store_explicit(&connection->poisoned, false, memory_order_release);
|
||||
int received = cbm_daemon_ipc_receive_frame_bounded(connection, timeout_ms, max_payload_length,
|
||||
frame_out, payload_out);
|
||||
atomic_store_explicit(&connection->poisoned, true, memory_order_release);
|
||||
return received;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,22 @@ int cbm_daemon_ipc_receive_frame_bounded(cbm_daemon_ipc_connection_t *connection
|
||||
uint32_t timeout_ms, uint32_t max_payload_length,
|
||||
cbm_daemon_frame_t *frame_out, uint8_t **payload_out);
|
||||
|
||||
/* Bounded receive of an answer the peer queued before it closed, on a
|
||||
* connection whose send just failed. A daemon at capacity or stopping answers
|
||||
* from its accept loop without reading the request and closes at once; when
|
||||
* that close lands before the client's first send, the send fails (EPIPE) and
|
||||
* poisons the connection although the complete answer is still queued for
|
||||
* reading -- POSIX keeps it readable after the peer's close (a Windows server
|
||||
* close discards it, so there this fails like any other receive). A failed
|
||||
* send consumes no inbound bytes, so the inbound frame boundary is intact;
|
||||
* call this ONLY before anything was read from the connection. The connection
|
||||
* stays poisoned for sends afterwards. Returns like
|
||||
* cbm_daemon_ipc_receive_frame_bounded. */
|
||||
int cbm_daemon_ipc_receive_frame_after_failed_send(cbm_daemon_ipc_connection_t *connection,
|
||||
uint32_t timeout_ms, uint32_t max_payload_length,
|
||||
cbm_daemon_frame_t *frame_out,
|
||||
uint8_t **payload_out);
|
||||
|
||||
/* Narrow crash/fault seams for publication-state and retry-state tests. They
|
||||
* are inert unless a test installs a hook/failure count in its own process. */
|
||||
typedef enum {
|
||||
|
||||
+72
-8
@@ -76,8 +76,26 @@ static _Atomic uint32_t runtime_ephemeral_linger_timeout_seam = UINT32_MAX;
|
||||
void cbm_daemon_runtime_service_set_ephemeral_linger_timeout_for_testing(uint32_t timeout_ms) {
|
||||
atomic_store(&runtime_ephemeral_linger_timeout_seam, timeout_ms);
|
||||
}
|
||||
|
||||
/* Connection-cap seams (flaky-ledger item 12): see runtime.h. */
|
||||
static atomic_bool runtime_hold_peer_waits_seam;
|
||||
void cbm_daemon_runtime_hold_peer_waits_for_testing(bool hold) {
|
||||
atomic_store(&runtime_hold_peer_waits_seam, hold);
|
||||
}
|
||||
static _Atomic(cbm_daemon_runtime_hello_hook_t) runtime_hello_hook_seam;
|
||||
void cbm_daemon_runtime_set_hello_hook_for_testing(cbm_daemon_runtime_hello_hook_t hook) {
|
||||
atomic_store(&runtime_hello_hook_seam, hook);
|
||||
}
|
||||
static void runtime_hello_point(cbm_daemon_runtime_hello_point_t point) {
|
||||
cbm_daemon_runtime_hello_hook_t hook = atomic_load(&runtime_hello_hook_seam);
|
||||
if (hook) {
|
||||
hook(point);
|
||||
}
|
||||
}
|
||||
#define RUNTIME_HELLO_POINT(point) runtime_hello_point(CBM_DAEMON_RUNTIME_HELLO_POINT_##point)
|
||||
#else
|
||||
static void runtime_note_peer_image_hash(void) {}
|
||||
#define RUNTIME_HELLO_POINT(point) ((void)0)
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -1989,6 +2007,29 @@ static void runtime_worker_handle_activation_shutdown(cbm_daemon_runtime_worker_
|
||||
runtime_worker_finish(worker);
|
||||
}
|
||||
|
||||
/* The first frame is due within the request deadline. Under the item-12 seam a
|
||||
* connection that never sends one holds its slot until it closes; stop still
|
||||
* interrupts the wait like any other worker receive. */
|
||||
static uint32_t runtime_first_frame_timeout_ms(const cbm_daemon_runtime_service_t *service) {
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
if (atomic_load(&runtime_hold_peer_waits_seam)) {
|
||||
return CBM_DAEMON_IPC_WAIT_FOREVER;
|
||||
}
|
||||
#endif
|
||||
return service->request_timeout_ms;
|
||||
}
|
||||
|
||||
/* Under the same seam an inline rejection's drain ends only on the peer's
|
||||
* close, never on the 250 ms bound (the drain only waits on Windows). */
|
||||
static uint32_t runtime_reject_drain_timeout_ms(void) {
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
if (atomic_load(&runtime_hold_peer_waits_seam)) {
|
||||
return CBM_DAEMON_IPC_WAIT_FOREVER;
|
||||
}
|
||||
#endif
|
||||
return RUNTIME_REJECT_DRAIN_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
static void *runtime_connection_worker(void *opaque) {
|
||||
cbm_daemon_runtime_worker_t *worker = opaque;
|
||||
cbm_daemon_runtime_service_t *service = worker->service;
|
||||
@@ -1998,7 +2039,7 @@ static void *runtime_connection_worker(void *opaque) {
|
||||
char requested_build[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
|
||||
|
||||
int received = cbm_daemon_ipc_receive_frame_bounded(
|
||||
worker->connection, service->request_timeout_ms,
|
||||
worker->connection, runtime_first_frame_timeout_ms(service),
|
||||
CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE, &frame, &payload);
|
||||
if (received != 1 || frame.type != CBM_DAEMON_FRAME_REQUEST) {
|
||||
free(payload);
|
||||
@@ -2274,14 +2315,19 @@ static cbm_daemon_runtime_worker_t *runtime_find_free_worker_locked(
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Answers WITHOUT reading the peer's HELLO, so on POSIX (where the drain is a
|
||||
* no-op) the close can land before the peer has even sent it; the client reads
|
||||
* this answer after its failed send (cbm_daemon_runtime_client_connect). */
|
||||
static void runtime_reject_inline(cbm_daemon_ipc_connection_t *connection, const char *message) {
|
||||
cbm_daemon_runtime_connect_result_t result;
|
||||
runtime_result_rejected(&result, message);
|
||||
RUNTIME_HELLO_POINT(DAEMON_REJECTING);
|
||||
(void)runtime_send_hello_response(connection, &result);
|
||||
/* Same Windows named-pipe discard hazard as runtime_worker_finish: the
|
||||
* peer must get to read the rejection before the handle closes. */
|
||||
cbm_daemon_ipc_connection_drain(connection, RUNTIME_REJECT_DRAIN_TIMEOUT_MS);
|
||||
cbm_daemon_ipc_connection_drain(connection, runtime_reject_drain_timeout_ms());
|
||||
cbm_daemon_ipc_connection_close(connection);
|
||||
RUNTIME_HELLO_POINT(DAEMON_REJECTED);
|
||||
}
|
||||
|
||||
static void runtime_accept_connection(cbm_daemon_runtime_service_t *service,
|
||||
@@ -3063,21 +3109,39 @@ cbm_daemon_runtime_client_t *cbm_daemon_runtime_client_connect(
|
||||
return NULL;
|
||||
}
|
||||
cbm_daemon_ipc_connection_t *connection = cbm_daemon_ipc_connect(endpoint, timeout_ms);
|
||||
if (!connection || !cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST,
|
||||
CBM_DAEMON_RUNTIME_OP_HELLO, request,
|
||||
(uint32_t)sizeof(request))) {
|
||||
cbm_daemon_ipc_connection_close(connection);
|
||||
if (!connection) {
|
||||
return NULL;
|
||||
}
|
||||
RUNTIME_HELLO_POINT(CLIENT_CONNECTED);
|
||||
bool hello_sent =
|
||||
cbm_daemon_ipc_send_frame(connection, CBM_DAEMON_FRAME_REQUEST, CBM_DAEMON_RUNTIME_OP_HELLO,
|
||||
request, (uint32_t)sizeof(request));
|
||||
RUNTIME_HELLO_POINT(CLIENT_SENT);
|
||||
cbm_daemon_frame_t frame = {0};
|
||||
uint8_t *payload = NULL;
|
||||
int received = cbm_daemon_ipc_receive_frame_bounded(
|
||||
connection, timeout_ms, CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE, &frame, &payload);
|
||||
/* A daemon at capacity or stopping answers from its accept loop without
|
||||
* reading this HELLO and closes at once. When that close lands first, the
|
||||
* send fails (EPIPE) with the whole answer already queued here: read it,
|
||||
* or the caller gets a bare transport error and bootstrap classifies by
|
||||
* lock state instead of by the daemon's own "capacity" / "stopping". */
|
||||
int received =
|
||||
hello_sent
|
||||
? cbm_daemon_ipc_receive_frame_bounded(
|
||||
connection, timeout_ms, CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE, &frame, &payload)
|
||||
: cbm_daemon_ipc_receive_frame_after_failed_send(
|
||||
connection, timeout_ms, CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE, &frame, &payload);
|
||||
bool valid = received == 1 && frame.type == CBM_DAEMON_FRAME_RESPONSE &&
|
||||
frame.flags == CBM_DAEMON_RUNTIME_OP_HELLO &&
|
||||
frame.length == CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE &&
|
||||
runtime_hello_response_decode(payload, result_out);
|
||||
free(payload);
|
||||
if (!hello_sent && (!valid || result_out->status != CBM_DAEMON_RUNTIME_CONNECT_REJECTED)) {
|
||||
/* Only a rejection can answer a HELLO the daemon never received;
|
||||
* anything else stays the plain send failure it always was. */
|
||||
memset(result_out, 0, sizeof(*result_out));
|
||||
cbm_daemon_ipc_connection_close(connection);
|
||||
return NULL;
|
||||
}
|
||||
if (received != 1) {
|
||||
/* The kernel completed the pipe/socket connect, so a server process
|
||||
* exists, yet the HELLO went unanswered. Name that holder: a dead
|
||||
|
||||
@@ -471,6 +471,27 @@ void cbm_daemon_runtime_set_containment_hook_for_testing(
|
||||
* in test time. UINT32_MAX restores the production constant; any other value
|
||||
* (0 = expire immediately) overrides. Process-global; reset it after use. */
|
||||
void cbm_daemon_runtime_service_set_ephemeral_linger_timeout_for_testing(uint32_t timeout_ms);
|
||||
/* Connection-cap seams (flaky-ledger item 12). While held, the two bounded
|
||||
* waits on a peer in the connection-cap path end only on that peer: a
|
||||
* connection that has not sent its first frame keeps its worker slot until it
|
||||
* closes (or the service stops) instead of until the request deadline, and an
|
||||
* inline rejection's drain (Windows; a no-op on POSIX) lasts until the peer
|
||||
* closes instead of 250 ms. A "slow hello" becomes a state the test releases,
|
||||
* not a window it races. Process-global; reset it after use. */
|
||||
void cbm_daemon_runtime_hold_peer_waits_for_testing(bool hold);
|
||||
/* The four points that order a HELLO against an inline accept-loop rejection.
|
||||
* The hook runs on the client thread for the CLIENT_* points and on the accept
|
||||
* thread for the DAEMON_* points (never under the service mutex), so a test can
|
||||
* pin either order: daemon answers after the HELLO is on the wire, or the
|
||||
* daemon has answered and closed before the HELLO is sent. NULL removes it. */
|
||||
typedef enum {
|
||||
CBM_DAEMON_RUNTIME_HELLO_POINT_CLIENT_CONNECTED = 0, /* transport up, HELLO not sent */
|
||||
CBM_DAEMON_RUNTIME_HELLO_POINT_CLIENT_SENT, /* HELLO send attempted */
|
||||
CBM_DAEMON_RUNTIME_HELLO_POINT_DAEMON_REJECTING, /* inline rejection not yet written */
|
||||
CBM_DAEMON_RUNTIME_HELLO_POINT_DAEMON_REJECTED, /* inline rejection written + closed */
|
||||
} cbm_daemon_runtime_hello_point_t;
|
||||
typedef void (*cbm_daemon_runtime_hello_hook_t)(cbm_daemon_runtime_hello_point_t point);
|
||||
void cbm_daemon_runtime_set_hello_hook_for_testing(cbm_daemon_runtime_hello_hook_t hook);
|
||||
#endif
|
||||
|
||||
#endif /* CBM_DAEMON_RUNTIME_H */
|
||||
|
||||
+13
-75
@@ -10,6 +10,7 @@
|
||||
* 5. Language detection for accepted files
|
||||
*/
|
||||
#include "discover/discover.h"
|
||||
#include "discover/discover_policy_internal.h"
|
||||
#include "cbm.h" // CBMLanguage, CBM_LANG_COUNT, CBM_LANG_JSON
|
||||
|
||||
#include "foundation/constants.h"
|
||||
@@ -107,20 +108,6 @@ static const char *FAST_PATTERNS[] = {".d.ts", ".bundle.", ".chunk.", ".gen
|
||||
"_string.go", "mock_", "_mock.", "_test_helpers.",
|
||||
".stories.", ".spec.", ".test.", NULL};
|
||||
|
||||
/* ── Ignored JSON filenames ──────────────────────── */
|
||||
|
||||
static const char *IGNORED_JSON_FILES[] = {
|
||||
"package.json", "package-lock.json", "tsconfig.json",
|
||||
"jsconfig.json", "composer.json", "composer.lock",
|
||||
"yarn.lock", "openapi.json", "swagger.json",
|
||||
"jest.config.json", ".eslintrc.json", ".prettierrc.json",
|
||||
".babelrc.json", "tslint.json", "angular.json",
|
||||
"firebase.json", "renovate.json", "lerna.json",
|
||||
"turbo.json", ".stylelintrc.json", "pnpm-lock.json",
|
||||
"deno.json", "biome.json", "devcontainer.json",
|
||||
".devcontainer.json", "launch.json", "settings.json",
|
||||
"extensions.json", "tasks.json", NULL};
|
||||
|
||||
/* ── Helper: check if string is in NULL-terminated array ─────────── */
|
||||
|
||||
static bool str_in_list(const char *s, const char *const *list) {
|
||||
@@ -134,8 +121,7 @@ static bool str_in_list(const char *s, const char *const *list) {
|
||||
|
||||
/* ── Helper: check if string ends with suffix ────────────── */
|
||||
|
||||
static bool ends_with(const char *s, const char *suffix) {
|
||||
size_t slen = strlen(s);
|
||||
static bool ends_with(const char *s, size_t slen, const char *suffix) {
|
||||
size_t sufflen = strlen(suffix);
|
||||
if (sufflen > slen) {
|
||||
return false;
|
||||
@@ -349,7 +335,7 @@ static bool resolve_global_excludes_path(char *out, size_t out_sz) {
|
||||
}
|
||||
|
||||
/* True when rel_path equals a suffix rule or ends with "/" + the rule. */
|
||||
static bool rel_path_has_skip_suffix(const char *rel_path) {
|
||||
bool cbm_discover_path_has_skip_suffix(const char *rel_path) {
|
||||
if (!rel_path) {
|
||||
return false;
|
||||
}
|
||||
@@ -393,15 +379,16 @@ bool cbm_has_ignored_suffix(const char *filename, cbm_index_mode_t mode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t filename_length = strlen(filename);
|
||||
for (int i = 0; ALWAYS_IGNORED_SUFFIXES[i]; i++) {
|
||||
if (ends_with(filename, ALWAYS_IGNORED_SUFFIXES[i])) {
|
||||
if (ends_with(filename, filename_length, ALWAYS_IGNORED_SUFFIXES[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode != CBM_MODE_FULL) {
|
||||
for (int i = 0; FAST_IGNORED_SUFFIXES[i]; i++) {
|
||||
if (ends_with(filename, FAST_IGNORED_SUFFIXES[i])) {
|
||||
if (ends_with(filename, filename_length, FAST_IGNORED_SUFFIXES[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -725,7 +712,7 @@ static int gitignore_chain_result(const gitignore_link_t *link, const char *rel_
|
||||
* (.worktrees / .claude-worktrees, the worktree entries in ALWAYS_SKIP_DIRS)
|
||||
* contain parallel checkouts of the same repo whose indexing would duplicate
|
||||
* the whole codebase (#802). */
|
||||
static bool is_safety_core_dir(const char *name) {
|
||||
bool cbm_discover_is_safety_core_directory(const char *name) {
|
||||
static const char *const SAFETY_CORE_DIRS[] = {".git", "node_modules", ".worktrees",
|
||||
".claude-worktrees", NULL};
|
||||
return str_in_list(name, SAFETY_CORE_DIRS);
|
||||
@@ -775,12 +762,12 @@ static bool should_skip_directory(const char *entry_name, const char *rel_path,
|
||||
const cbm_gitignore_t *global_gi,
|
||||
const cbm_gitignore_t *cbmignore) {
|
||||
if (cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) ||
|
||||
rel_path_has_skip_suffix(rel_path)) {
|
||||
cbm_discover_path_has_skip_suffix(rel_path)) {
|
||||
/* #500: a .cbmignore negation (e.g. "!obj/") whose rule is the last
|
||||
* match for this dir un-skips a built-in skip-list dir — except the
|
||||
* non-negatable safety core. Fall through so .gitignore/global/local
|
||||
* rules still apply to the un-skipped dir. */
|
||||
bool unskipped = cbmignore && !is_safety_core_dir(entry_name) &&
|
||||
bool unskipped = cbmignore && !cbm_discover_is_safety_core_directory(entry_name) &&
|
||||
cbm_gitignore_match_result(cbmignore, rel_path, true) < 0;
|
||||
if (!unskipped) {
|
||||
return true;
|
||||
@@ -841,60 +828,11 @@ static const char *file_skip_reason(const char *entry_name, const char *rel_path
|
||||
return global_ignored ? "gitignore" : NULL;
|
||||
}
|
||||
|
||||
/* Detect language for a file, handling .m disambiguation and JSON filtering. */
|
||||
/* Detect language for a file: its name, and its first bytes where the name is
|
||||
* not enough (cbm_language_classify; the pinned test-impact inventory uses the
|
||||
* same rule on git blobs). */
|
||||
static CBMLanguage detect_file_language(const char *entry_name, const char *abs_path) {
|
||||
CBMLanguage lang = cbm_language_for_filename(entry_name);
|
||||
if (lang == CBM_LANG_COUNT) {
|
||||
/* Filename/extension detection failed: fall back to a conservative
|
||||
* shebang probe so extensionless scripts get indexed (#1199). Filename
|
||||
* detection stays authoritative — this runs only when it returns
|
||||
* unknown. */
|
||||
return cbm_language_from_shebang(abs_path);
|
||||
}
|
||||
/* Special: .m files need content-based disambiguation */
|
||||
const char *dot = strrchr(entry_name, '.');
|
||||
if (dot && strcmp(dot, ".m") == 0) {
|
||||
lang = cbm_disambiguate_m(abs_path);
|
||||
}
|
||||
/* Special: .cls is shared by ObjectScript UDL, Apex and VB6 class modules */
|
||||
if (dot && strcmp(dot, ".cls") == 0) {
|
||||
lang = cbm_disambiguate_cls(abs_path);
|
||||
}
|
||||
/* Special: .inc is shared by BitBake and ObjectScript include files */
|
||||
if (dot && strcmp(dot, ".inc") == 0) {
|
||||
lang = cbm_disambiguate_inc(abs_path);
|
||||
}
|
||||
/* Special: .cfc components may be script-dialect or tag-dialect (<cfcomponent>) */
|
||||
if (dot && strcmp(dot, ".cfc") == 0) {
|
||||
lang = cbm_disambiguate_cfc(abs_path);
|
||||
}
|
||||
/* Special: .frm is shared by FORM and VB6 forms (#721) */
|
||||
if (dot && strcmp(dot, ".frm") == 0) {
|
||||
lang = cbm_disambiguate_frm(abs_path);
|
||||
}
|
||||
/* Special: .res is also a binary Godot / Windows resource (#2176) */
|
||||
if (dot && strcmp(dot, ".res") == 0) {
|
||||
lang = cbm_disambiguate_res(abs_path);
|
||||
}
|
||||
/* Special: ObjectScript Studio Export XML (<Export generator="...">) is
|
||||
* detected by content; otherwise .xml stays XML. */
|
||||
if (lang == CBM_LANG_XML) {
|
||||
FILE *xf = cbm_fopen(abs_path, "r");
|
||||
if (xf) {
|
||||
char xbuf[CBM_SZ_256];
|
||||
size_t xn = fread(xbuf, SKIP_ONE, sizeof(xbuf) - SKIP_ONE, xf);
|
||||
(void)fclose(xf);
|
||||
xbuf[xn] = '\0';
|
||||
if (strstr(xbuf, "<Export generator=")) {
|
||||
return CBM_LANG_OBJECTSCRIPT_EXPORT;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Check ignored JSON files */
|
||||
if (lang == CBM_LANG_JSON && str_in_list(entry_name, IGNORED_JSON_FILES)) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
return lang;
|
||||
return cbm_language_for_file(entry_name, abs_path);
|
||||
}
|
||||
|
||||
/* UTF-8-safe stat: wide API on Windows, regular stat on POSIX. */
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
/* Use the existing CBMLanguage enum from extraction layer */
|
||||
#include "cbm.h"
|
||||
#include "foundation/index_policy.h"
|
||||
#include "discover/userconfig.h"
|
||||
|
||||
/* ── Language detection ──────────────────────────────────────────── */
|
||||
|
||||
@@ -83,6 +84,32 @@ CBMLanguage cbm_language_from_shebang(const char *path);
|
||||
* On read failure, defaults to CBM_LANG_CFSCRIPT. */
|
||||
CBMLanguage cbm_disambiguate_cfc(const char *path);
|
||||
|
||||
/* The language of a file from its base name and, where the name is not enough,
|
||||
* its first bytes: the one rule discovery and the pinned test-impact inventory
|
||||
* share. cbm_language_probe_bytes says how many leading bytes the name needs
|
||||
* (0: the name decides). cbm_language_classify takes those bytes raw, `more`
|
||||
* when the file has bytes beyond them, and `readable` false when they could
|
||||
* not be read (each probe then keeps the name's default, as a failed open
|
||||
* always did). The name lookup consults the installed user language config. */
|
||||
size_t cbm_language_probe_bytes(const char *filename);
|
||||
CBMLanguage cbm_language_classify(const char *filename, const unsigned char *head, size_t head_len,
|
||||
bool more, bool readable);
|
||||
|
||||
/* The same with the language config given explicitly (NULL: none) instead of
|
||||
* the installed process-wide one, for a caller that classifies a snapshot
|
||||
* under that snapshot's config while another index may have its own
|
||||
* installed. */
|
||||
size_t cbm_language_probe_bytes_with(const cbm_userconfig_t *config, const char *filename);
|
||||
CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const char *filename,
|
||||
const unsigned char *head, size_t head_len, bool more,
|
||||
bool readable);
|
||||
|
||||
/* The most leading bytes any name asks cbm_language_probe_bytes for. */
|
||||
#define CBM_LANGUAGE_PROBE_MAX 16384
|
||||
|
||||
/* cbm_language_classify on the file at `path`, read raw. */
|
||||
CBMLanguage cbm_language_for_file(const char *filename, const char *path);
|
||||
|
||||
/* ── Gitignore pattern matching ──────────────────────────────────── */
|
||||
|
||||
typedef struct cbm_gitignore cbm_gitignore_t;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef CBM_DISCOVER_POLICY_INTERNAL_H
|
||||
#define CBM_DISCOVER_POLICY_INTERNAL_H
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Pure legacy policy predicates; callers provide live terminated strings. */
|
||||
bool cbm_discover_path_has_skip_suffix(const char *relative_path);
|
||||
bool cbm_discover_is_safety_core_directory(const char *name);
|
||||
|
||||
#endif
|
||||
+8
-266
@@ -13,8 +13,9 @@
|
||||
*/
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "discover/gitignore_internal.h"
|
||||
|
||||
enum { GI_INIT_CAP = 16, GI_CHAR_IDX1 = 1, GI_CHAR_IDX2 = 2, GI_SKIP3 = 3 };
|
||||
enum { GI_INIT_CAP = 16 };
|
||||
#include "discover/discover.h"
|
||||
|
||||
#include <ctype.h>
|
||||
@@ -24,236 +25,22 @@ enum { GI_INIT_CAP = 16, GI_CHAR_IDX1 = 1, GI_CHAR_IDX2 = 2, GI_SKIP3 = 3 };
|
||||
|
||||
/* ── Pattern representation ──────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
char *pattern; /* the glob pattern (normalized) */
|
||||
bool negated; /* starts with ! */
|
||||
bool dir_only; /* ends with / */
|
||||
bool rooted; /* contains / (anchored to root) */
|
||||
} gi_pattern_t;
|
||||
|
||||
struct cbm_gitignore {
|
||||
gi_pattern_t *patterns;
|
||||
int count;
|
||||
int capacity;
|
||||
};
|
||||
|
||||
/* ── Pattern matching engine ─────────────────────────────────────── */
|
||||
|
||||
/* Backtracking budget. `**` retries the remainder at every position, and
|
||||
* consecutive literal + `**` groups multiply, so cost is exponential in the
|
||||
* number of groups. Patterns come from a committed .gitignore / .cbmignore and
|
||||
* every discovered path is matched against every pattern, so a small ignore file
|
||||
* can make discovery take unbounded time. Cap the match steps per
|
||||
* (pattern, path) pair.
|
||||
*
|
||||
* Exhausting the budget reports "no match", so a pathological pattern fails to
|
||||
* ignore rather than ignoring the wrong files: the file gets indexed, which is
|
||||
* the recoverable direction. The cap is far above any real pattern — ordinary
|
||||
* globs finish in tens of steps. */
|
||||
enum { GI_MATCH_MAX_STEPS = 20000 };
|
||||
|
||||
/* Forward declaration for recursive calls. */
|
||||
static bool glob_match(const char *pat, const char *str,
|
||||
int *budget); // NOLINT(misc-no-recursion)
|
||||
|
||||
/* Match a ** (doublestar-slash) pattern: try rest at every / boundary. */
|
||||
static bool glob_match_doublestar_slash(const char *pat, // NOLINT(misc-no-recursion)
|
||||
const char *str, int *budget) {
|
||||
if (glob_match(pat, str, budget)) {
|
||||
return true;
|
||||
}
|
||||
for (const char *s = str; *s; s++) {
|
||||
if (*s == '/' && glob_match(pat, s + SKIP_ONE, budget)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Match a ** (doublestar) followed by non-slash: try at every position. */
|
||||
static bool glob_match_doublestar_any(const char *pat, // NOLINT(misc-no-recursion)
|
||||
const char *str, int *budget) {
|
||||
for (const char *s = str;; s++) {
|
||||
if (glob_match(pat, s, budget)) {
|
||||
return true;
|
||||
}
|
||||
if (!*s) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Match a * (single star): match any sequence not containing /. */
|
||||
static bool glob_match_star(const char *pat, const char *str,
|
||||
int *budget) { // NOLINT(misc-no-recursion)
|
||||
for (const char *s = str;; s++) {
|
||||
if (glob_match(pat, s, budget)) {
|
||||
return true;
|
||||
}
|
||||
if (!*s || *s == '/') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Match a [...] character class at current position.
|
||||
* Returns true if matched. Advances *pat_out past the closing ']'. */
|
||||
static bool glob_match_charclass(const char *pat, char ch, const char **pat_out) {
|
||||
bool negate_class = false;
|
||||
if (*pat == '!' || *pat == '^') {
|
||||
negate_class = true;
|
||||
pat++;
|
||||
}
|
||||
bool matched = false;
|
||||
char prev = 0;
|
||||
while (*pat && *pat != ']') {
|
||||
if (*pat == '-' && prev && pat[GI_CHAR_IDX1] && pat[GI_CHAR_IDX1] != ']') {
|
||||
pat++;
|
||||
if (ch >= prev && ch <= *pat) {
|
||||
matched = true;
|
||||
}
|
||||
prev = *pat;
|
||||
pat++;
|
||||
} else {
|
||||
if (ch == *pat) {
|
||||
matched = true;
|
||||
}
|
||||
prev = *pat;
|
||||
pat++;
|
||||
}
|
||||
}
|
||||
if (*pat == ']') {
|
||||
pat++;
|
||||
}
|
||||
*pat_out = pat;
|
||||
return negate_class ? !matched : matched;
|
||||
}
|
||||
|
||||
/*
|
||||
* Match a glob pattern against a string.
|
||||
* Handles: * (non-slash), ** (any path), ? (single non-slash), [class]
|
||||
*/
|
||||
/* Handle ** at current position. Returns match result. */
|
||||
static bool glob_match_doublestar(const char *pat, const char *str,
|
||||
int *budget) { // NOLINT(misc-no-recursion)
|
||||
if (pat[GI_CHAR_IDX2] == '/') {
|
||||
return glob_match_doublestar_slash(pat + GI_SKIP3, str, budget);
|
||||
}
|
||||
if (pat[GI_CHAR_IDX2] == '\0') {
|
||||
return true;
|
||||
}
|
||||
return glob_match_doublestar_any(pat + GI_CHAR_IDX2, str, budget);
|
||||
}
|
||||
|
||||
static bool glob_match(const char *pat, const char *str,
|
||||
int *budget) { // NOLINT(misc-no-recursion)
|
||||
if (--*budget <= 0) {
|
||||
return false; /* budget exhausted — see GI_MATCH_MAX_STEPS */
|
||||
}
|
||||
while (*pat && *str) {
|
||||
if (pat[0] == '*' && pat[GI_CHAR_IDX1] == '*') {
|
||||
return glob_match_doublestar(pat, str, budget);
|
||||
}
|
||||
|
||||
if (*pat == '*') {
|
||||
return glob_match_star(pat + SKIP_ONE, str, budget);
|
||||
}
|
||||
|
||||
if (*pat == '?') {
|
||||
if (*str == '/') {
|
||||
return false;
|
||||
}
|
||||
pat++;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (*pat == '[') {
|
||||
const char *new_pat = NULL;
|
||||
if (!glob_match_charclass(pat + SKIP_ONE, *str, &new_pat)) {
|
||||
return false;
|
||||
}
|
||||
pat = new_pat;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (*pat != *str) {
|
||||
return false;
|
||||
}
|
||||
pat++;
|
||||
str++;
|
||||
}
|
||||
|
||||
while (*pat == '*') {
|
||||
pat++;
|
||||
}
|
||||
return *pat == '\0' && *str == '\0';
|
||||
}
|
||||
|
||||
/* Match one pattern against one path with a fresh backtracking budget. Not
|
||||
* recursive itself — it seeds the budget and hands off to the engine. */
|
||||
static bool glob_match_bounded(const char *pat, const char *str) {
|
||||
int budget = GI_MATCH_MAX_STEPS;
|
||||
return glob_match(pat, str, &budget);
|
||||
}
|
||||
|
||||
/* ── Pattern parsing ─────────────────────────────────────────────── */
|
||||
|
||||
static void gi_add_pattern(cbm_gitignore_t *gi, const char *line, int len) {
|
||||
/* Trim trailing whitespace */
|
||||
while (len > 0 && (line[len - SKIP_ONE] == ' ' || line[len - SKIP_ONE] == '\t' ||
|
||||
line[len - SKIP_ONE] == '\r')) {
|
||||
len--;
|
||||
}
|
||||
if (len == 0) {
|
||||
gi_view_t view;
|
||||
if (!gi_normalize(line, (size_t)len, &view, NULL) || !view.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
gi_pattern_t p = {0};
|
||||
|
||||
/* Check for negation */
|
||||
const char *start = line;
|
||||
if (*start == '!') {
|
||||
p.negated = true;
|
||||
start++;
|
||||
len--;
|
||||
}
|
||||
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check for trailing / (directory-only) */
|
||||
if (start[len - SKIP_ONE] == '/') {
|
||||
p.dir_only = true;
|
||||
len--;
|
||||
}
|
||||
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check for leading / (rooted) */
|
||||
if (*start == '/') {
|
||||
p.rooted = true;
|
||||
start++;
|
||||
len--;
|
||||
}
|
||||
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if pattern contains / anywhere (makes it rooted) */
|
||||
if (!p.rooted) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (start[i] == '/') {
|
||||
p.rooted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
gi_pattern_t p = {.negated = view.negated, .dir_only = view.dir_only, .rooted = view.rooted};
|
||||
const char *start = view.text;
|
||||
len = (int)view.length;
|
||||
|
||||
/* Copy pattern */
|
||||
p.pattern = malloc(len + SKIP_ONE);
|
||||
@@ -345,56 +132,11 @@ cbm_gitignore_t *cbm_gitignore_load(const char *path) {
|
||||
return gi;
|
||||
}
|
||||
|
||||
/* Match a non-rooted pattern against basename and path suffixes. */
|
||||
static bool match_unrooted(const char *pattern, const char *rel_path, const char *basename) {
|
||||
if (glob_match_bounded(pattern, basename)) {
|
||||
return true;
|
||||
}
|
||||
if (!strchr(rel_path, '/')) {
|
||||
return false;
|
||||
}
|
||||
/* Try matching at every / boundary */
|
||||
const char *s = rel_path;
|
||||
while (*s) {
|
||||
if (glob_match_bounded(pattern, s)) {
|
||||
return true;
|
||||
}
|
||||
const char *next = strchr(s, '/');
|
||||
if (!next) {
|
||||
break;
|
||||
}
|
||||
s = next + SKIP_ONE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int cbm_gitignore_match_result(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir) {
|
||||
if (!gi || !rel_path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Extract the basename for non-rooted pattern matching */
|
||||
const char *basename = strrchr(rel_path, '/');
|
||||
basename = basename ? basename + SKIP_ONE : rel_path;
|
||||
|
||||
int matched = 0;
|
||||
|
||||
for (int i = 0; i < gi->count; i++) {
|
||||
const gi_pattern_t *p = &gi->patterns[i];
|
||||
|
||||
if (p->dir_only && !is_dir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool this_match = p->rooted ? glob_match_bounded(p->pattern, rel_path)
|
||||
: match_unrooted(p->pattern, rel_path, basename);
|
||||
|
||||
if (this_match) {
|
||||
matched = p->negated ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return matched;
|
||||
return gi_core_match(gi->patterns, (size_t)gi->count, rel_path, is_dir, NULL);
|
||||
}
|
||||
|
||||
bool cbm_gitignore_matches(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "discover/gitignore_internal.h"
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
static bool gi_limits_valid(const cbm_ignore_checked_limits_t *limits) {
|
||||
return limits && limits->max_bytes && limits->max_work && limits->max_patterns &&
|
||||
limits->max_patterns <= INT_MAX && limits->max_arena_bytes && limits->max_depth &&
|
||||
limits->max_depth <= CBM_IGNORE_CHECKED_DEPTH_MAX;
|
||||
}
|
||||
|
||||
static cbm_ignore_checked_status_t gi_open(const cbm_ignore_checked_limits_t *limits,
|
||||
cbm_ignore_checked_t **out,
|
||||
cbm_ignore_checked_error_t *error, bool fault) {
|
||||
if (out) {
|
||||
*out = NULL;
|
||||
}
|
||||
gi_error(error, CBM_IGNORE_CHECKED_OK, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
if (!out || !gi_limits_valid(limits)) {
|
||||
gi_error(error, CBM_IGNORE_CHECKED_INVALID, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
return CBM_IGNORE_CHECKED_INVALID;
|
||||
}
|
||||
if (limits->max_arena_bytes < sizeof(cbm_ignore_checked_t)) {
|
||||
gi_error(error, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_ARENA, SIZE_MAX);
|
||||
return CBM_IGNORE_CHECKED_LIMIT;
|
||||
}
|
||||
CBMArena arena;
|
||||
cbm_arena_init_lazy(&arena, CBM_ARENA_DEFAULT_BLOCK_SIZE);
|
||||
cbm_ignore_checked_t *owner = fault ? NULL : cbm_arena_alloc(&arena, sizeof(*owner));
|
||||
if (!owner) {
|
||||
cbm_arena_destroy(&arena);
|
||||
gi_error(error, CBM_IGNORE_CHECKED_OOM, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
return CBM_IGNORE_CHECKED_OOM;
|
||||
}
|
||||
memset(owner, 0, sizeof(*owner));
|
||||
owner->arena = arena;
|
||||
owner->limits = *limits;
|
||||
owner->usage.arena_requested_bytes = sizeof(*owner);
|
||||
gi_error(&owner->error, CBM_IGNORE_CHECKED_OK, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
*out = owner;
|
||||
return CBM_IGNORE_CHECKED_OK;
|
||||
}
|
||||
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_open(const cbm_ignore_checked_limits_t *limits,
|
||||
cbm_ignore_checked_t **out,
|
||||
cbm_ignore_checked_error_t *error) {
|
||||
return gi_open(limits, out, error, false);
|
||||
}
|
||||
|
||||
static bool gi_path_valid(gi_guard_t *guard, const char *path, size_t length) {
|
||||
for (size_t i = 0; i <= length; i++) {
|
||||
guard->offset = i;
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
if ((path[i] == '\0') != (i == length)) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_UNSUPPORTED, CBM_IGNORE_CAP_NONE);
|
||||
}
|
||||
}
|
||||
guard->offset = SIZE_MAX;
|
||||
return true;
|
||||
}
|
||||
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_match(
|
||||
cbm_ignore_checked_t *owner, const cbm_ignore_program_t *program, const char *path,
|
||||
size_t length, bool is_directory, const cbm_ignore_checked_control_t *control,
|
||||
cbm_ignore_decision_t *decision, cbm_ignore_checked_error_t *error) {
|
||||
if (decision) {
|
||||
*decision = CBM_IGNORE_UNAVAILABLE;
|
||||
}
|
||||
gi_error(error, CBM_IGNORE_CHECKED_OK, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
if (!owner || !program || !path || length == SIZE_MAX || !control || !control->deadline_ms ||
|
||||
!decision || program->owner != owner) {
|
||||
gi_error(error, CBM_IGNORE_CHECKED_INVALID, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
return CBM_IGNORE_CHECKED_INVALID;
|
||||
}
|
||||
gi_guard_t guard;
|
||||
if (!gi_begin(owner, control, &guard) || !gi_bytes(&guard, length + 1) ||
|
||||
!gi_path_valid(&guard, path, length)) {
|
||||
return gi_finish(owner, error);
|
||||
}
|
||||
int result = gi_core_match(program->rows, program->count, path, is_directory, &guard);
|
||||
if (gi_poll(&guard)) {
|
||||
*decision = (cbm_ignore_decision_t)result;
|
||||
}
|
||||
return gi_finish(owner, error);
|
||||
}
|
||||
|
||||
bool cbm_ignore_checked_usage(const cbm_ignore_checked_t *owner, cbm_ignore_checked_usage_t *out) {
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
*out = owner->usage;
|
||||
return true;
|
||||
}
|
||||
|
||||
void cbm_ignore_checked_free(cbm_ignore_checked_t *owner) {
|
||||
if (owner) {
|
||||
/* The owner itself lives in the arena being destroyed. */
|
||||
CBMArena arena = owner->arena;
|
||||
cbm_arena_destroy(&arena);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_open_oom_for_tests(
|
||||
const cbm_ignore_checked_limits_t *limits, cbm_ignore_checked_t **out,
|
||||
cbm_ignore_checked_error_t *error) {
|
||||
return gi_open(limits, out, error, true);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,137 @@
|
||||
/* Checked in-memory ignore parsing and matching; no filesystem/MCP dependency. */
|
||||
#ifndef CBM_GITIGNORE_CHECKED_H
|
||||
#define CBM_GITIGNORE_CHECKED_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct cbm_ignore_checked cbm_ignore_checked_t;
|
||||
typedef struct cbm_ignore_program cbm_ignore_program_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_IGNORE_CHECKED_OK = 0,
|
||||
CBM_IGNORE_CHECKED_INVALID,
|
||||
CBM_IGNORE_CHECKED_UNSUPPORTED,
|
||||
CBM_IGNORE_CHECKED_LIMIT,
|
||||
CBM_IGNORE_CHECKED_OOM,
|
||||
CBM_IGNORE_CHECKED_CANCELLED,
|
||||
CBM_IGNORE_CHECKED_DEADLINE
|
||||
} cbm_ignore_checked_status_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_IGNORE_CAP_NONE = 0,
|
||||
CBM_IGNORE_CAP_BYTES,
|
||||
CBM_IGNORE_CAP_PATTERNS,
|
||||
CBM_IGNORE_CAP_WORK,
|
||||
CBM_IGNORE_CAP_ARENA,
|
||||
CBM_IGNORE_CAP_DEPTH,
|
||||
CBM_IGNORE_CAP_ATTEMPT,
|
||||
CBM_IGNORE_CAP_REPRESENTATION
|
||||
} cbm_ignore_cap_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_IGNORE_REINCLUDED = -1,
|
||||
CBM_IGNORE_NO_OPINION = 0,
|
||||
CBM_IGNORE_IGNORED = 1,
|
||||
CBM_IGNORE_UNAVAILABLE = 2 /* every failure clears the decision to this */
|
||||
} cbm_ignore_decision_t;
|
||||
|
||||
enum { CBM_IGNORE_CHECKED_DEPTH_MAX = 32 };
|
||||
|
||||
typedef struct {
|
||||
uint64_t max_bytes; /* aggregate parse lengths and match path lengths + NUL */
|
||||
uint64_t max_work; /* aggregate charged work, including parsing */
|
||||
size_t max_patterns; /* cumulative reservations; positive, <= INT_MAX */
|
||||
size_t max_arena_bytes; /* cumulative logical requests, not resident memory */
|
||||
unsigned max_depth; /* 1..CBM_IGNORE_CHECKED_DEPTH_MAX active glob entries */
|
||||
} cbm_ignore_checked_limits_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t deadline_ms; /* positive absolute cbm_now_ms deadline */
|
||||
bool (*cancelled)(void *context); /* optional prompt, pure, non-reentrant */
|
||||
void *context; /* borrowed for this synchronous call only */
|
||||
} cbm_ignore_checked_control_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_ignore_checked_status_t status;
|
||||
cbm_ignore_cap_t cap; /* LIMIT only; otherwise NONE */
|
||||
size_t byte_offset; /* input offset when known, otherwise SIZE_MAX */
|
||||
char diagnostic[128]; /* bounded owned text, never input contents */
|
||||
} cbm_ignore_checked_error_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t bytes_reserved;
|
||||
uint64_t work_used;
|
||||
size_t patterns_reserved;
|
||||
size_t arena_requested_bytes;
|
||||
cbm_ignore_checked_status_t terminal_status;
|
||||
} cbm_ignore_checked_usage_t;
|
||||
|
||||
typedef struct {
|
||||
const cbm_ignore_program_t *program; /* borrowed until owner free */
|
||||
size_t pattern_count; /* zero is successful empty/comment-only normalization */
|
||||
} cbm_ignore_checked_parse_result_t;
|
||||
|
||||
/* All caps positive; copies limits. Initializes optional error and clears *out.
|
||||
* No callbacks/IO. Failure publishes no owner. Uses a CBMArena, including owner.
|
||||
*/
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_open(const cbm_ignore_checked_limits_t *limits,
|
||||
cbm_ignore_checked_t **out,
|
||||
cbm_ignore_checked_error_t *error);
|
||||
|
||||
/* Exact span: NULL permitted only for length==0; terminator not required.
|
||||
* Embedded NUL => UNSUPPORTED, never truncation. Result is required/cleared.
|
||||
* Copies normalized patterns; caller may discard bytes after return.
|
||||
* Every started failure is terminal for owner; earlier programs remain allocated
|
||||
* but no further parse/match can succeed. No partial program is published.
|
||||
*/
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_parse(cbm_ignore_checked_t *owner, const void *bytes,
|
||||
size_t length,
|
||||
const cbm_ignore_checked_control_t *control,
|
||||
cbm_ignore_checked_parse_result_t *out,
|
||||
cbm_ignore_checked_error_t *error);
|
||||
|
||||
/* program must belong to this live owner. path is readable for length+1 bytes,
|
||||
* with path[length]==0 and no earlier NUL. Empty path is permitted. No decoding
|
||||
* or path normalization. Inputs borrowed for this call; no match allocation.
|
||||
* Required decision is initialized to UNAVAILABLE and set only on full success.
|
||||
*/
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_match(
|
||||
cbm_ignore_checked_t *owner, const cbm_ignore_program_t *program, const char *path,
|
||||
size_t length, bool is_directory, const cbm_ignore_checked_control_t *control,
|
||||
cbm_ignore_decision_t *decision, cbm_ignore_checked_error_t *error);
|
||||
|
||||
/* Read-only; clears output, false for NULL owner/output. Does not consume quotas,
|
||||
* poll or clear a terminal failure. No operation may overlap any other call.
|
||||
*/
|
||||
bool cbm_ignore_checked_usage(const cbm_ignore_checked_t *owner, cbm_ignore_checked_usage_t *out);
|
||||
/* NULL-safe; releases all programs/arena at once. No callback/IO or cancellation. */
|
||||
void cbm_ignore_checked_free(cbm_ignore_checked_t *owner);
|
||||
|
||||
/* Test-only per-call allocation failure seams.
|
||||
* No persistent/global fault, raw allocator replacement or production option.
|
||||
*/
|
||||
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
||||
typedef enum {
|
||||
CBM_IGNORE_PARSE_FAULT_NONE = 0,
|
||||
CBM_IGNORE_PARSE_FAULT_PROGRAM,
|
||||
CBM_IGNORE_PARSE_FAULT_PATTERN
|
||||
} cbm_ignore_parse_fault_t;
|
||||
|
||||
/* Simulate NULL at the owner's first arena allocation, after normal quota checks. */
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_open_oom_for_tests(
|
||||
const cbm_ignore_checked_limits_t *limits, cbm_ignore_checked_t **out,
|
||||
cbm_ignore_checked_error_t *error);
|
||||
|
||||
/* PROGRAM: program/header+row-storage allocation. PATTERN: first effective
|
||||
* pattern-string allocation. Unreached boundary has no effect. Same normal
|
||||
* implementation/error path; no extra allocations or changed matching logic.
|
||||
*/
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_parse_fault_for_tests(
|
||||
cbm_ignore_checked_t *owner, const void *bytes, size_t length,
|
||||
const cbm_ignore_checked_control_t *control, cbm_ignore_parse_fault_t fault,
|
||||
cbm_ignore_checked_parse_result_t *out, cbm_ignore_checked_error_t *error);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "discover/gitignore_internal.h"
|
||||
#include <limits.h>
|
||||
|
||||
typedef struct {
|
||||
gi_guard_t *guard;
|
||||
const char *bytes;
|
||||
size_t length;
|
||||
cbm_ignore_program_t *program;
|
||||
size_t count;
|
||||
unsigned fault;
|
||||
} gi_parse_t;
|
||||
|
||||
static bool gi_line(gi_parse_t *parse, size_t *cursor, gi_view_t *view) {
|
||||
size_t start = *cursor;
|
||||
while (*cursor < parse->length) {
|
||||
parse->guard->offset = *cursor;
|
||||
/* Both exact-byte validation and line splitting visit this byte. */
|
||||
if (!gi_work(parse->guard, 2)) {
|
||||
return false;
|
||||
}
|
||||
char byte = parse->bytes[*cursor];
|
||||
if (!byte) {
|
||||
return gi_fail(parse->guard, CBM_IGNORE_CHECKED_UNSUPPORTED, CBM_IGNORE_CAP_NONE);
|
||||
}
|
||||
if (byte == '\n') {
|
||||
break;
|
||||
}
|
||||
(*cursor)++;
|
||||
if (*cursor - start > INT_MAX) {
|
||||
return gi_fail(parse->guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_REPRESENTATION);
|
||||
}
|
||||
}
|
||||
size_t length = *cursor - start;
|
||||
if (*cursor < parse->length) {
|
||||
(*cursor)++;
|
||||
}
|
||||
parse->guard->offset = start;
|
||||
return gi_normalize(parse->bytes + start, length, view, parse->guard);
|
||||
}
|
||||
|
||||
static bool gi_store_pattern(gi_parse_t *parse, const gi_view_t *view) {
|
||||
bool fault = parse->fault == GI_FAULT_PATTERN && parse->count == 0;
|
||||
char *text = gi_allocate(parse->guard, view->length + 1, fault);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i <= view->length; i++) {
|
||||
if (!gi_work(parse->guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
text[i] = i == view->length ? '\0' : view->text[i];
|
||||
}
|
||||
gi_pattern_t row = {.pattern = text,
|
||||
.negated = view->negated,
|
||||
.dir_only = view->dir_only,
|
||||
.rooted = view->rooted};
|
||||
parse->program->rows[parse->count++] = row;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool gi_parse_pass(gi_parse_t *parse) {
|
||||
size_t cursor = 0;
|
||||
while (cursor < parse->length) {
|
||||
gi_view_t view;
|
||||
if (!gi_line(parse, &cursor, &view)) {
|
||||
return false;
|
||||
}
|
||||
if (!view.length) {
|
||||
continue;
|
||||
}
|
||||
if (parse->program) {
|
||||
if (!gi_store_pattern(parse, &view)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!gi_pattern_reserve(parse->guard)) {
|
||||
return false;
|
||||
}
|
||||
parse->count++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool gi_parse_storage(gi_parse_t *parse) {
|
||||
size_t count = parse->count;
|
||||
if (count > (SIZE_MAX - sizeof(cbm_ignore_program_t)) / sizeof(gi_pattern_t)) {
|
||||
return gi_fail(parse->guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_ARENA);
|
||||
}
|
||||
size_t bytes = sizeof(cbm_ignore_program_t) + count * sizeof(gi_pattern_t);
|
||||
parse->program = gi_allocate(parse->guard, bytes, parse->fault == GI_FAULT_PROGRAM);
|
||||
if (!parse->program || !gi_zero(parse->guard, parse->program, bytes)) {
|
||||
return false;
|
||||
}
|
||||
parse->program->owner = parse->guard->owner;
|
||||
parse->program->count = count;
|
||||
parse->count = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static cbm_ignore_checked_status_t gi_parse(cbm_ignore_checked_t *owner, const void *bytes,
|
||||
size_t length,
|
||||
const cbm_ignore_checked_control_t *control,
|
||||
unsigned fault, cbm_ignore_checked_parse_result_t *out,
|
||||
cbm_ignore_checked_error_t *error) {
|
||||
if (out) {
|
||||
*out = (cbm_ignore_checked_parse_result_t){0};
|
||||
}
|
||||
gi_error(error, CBM_IGNORE_CHECKED_OK, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
if (!owner || (!bytes && length) || !control || !control->deadline_ms || !out ||
|
||||
fault > GI_FAULT_PATTERN) {
|
||||
gi_error(error, CBM_IGNORE_CHECKED_INVALID, CBM_IGNORE_CAP_NONE, SIZE_MAX);
|
||||
return CBM_IGNORE_CHECKED_INVALID;
|
||||
}
|
||||
gi_guard_t guard;
|
||||
if (!gi_begin(owner, control, &guard) || !gi_bytes(&guard, length)) {
|
||||
return gi_finish(owner, error);
|
||||
}
|
||||
gi_parse_t parse = {.guard = &guard, .bytes = bytes, .length = length, .fault = fault};
|
||||
if (gi_parse_pass(&parse) && gi_parse_storage(&parse) && gi_parse_pass(&parse) &&
|
||||
gi_poll(&guard)) {
|
||||
out->program = parse.program;
|
||||
out->pattern_count = parse.count;
|
||||
}
|
||||
return gi_finish(owner, error);
|
||||
}
|
||||
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_parse(cbm_ignore_checked_t *owner, const void *bytes,
|
||||
size_t length,
|
||||
const cbm_ignore_checked_control_t *control,
|
||||
cbm_ignore_checked_parse_result_t *out,
|
||||
cbm_ignore_checked_error_t *error) {
|
||||
return gi_parse(owner, bytes, length, control, GI_FAULT_NONE, out, error);
|
||||
}
|
||||
|
||||
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
||||
cbm_ignore_checked_status_t cbm_ignore_checked_parse_fault_for_tests(
|
||||
cbm_ignore_checked_t *owner, const void *bytes, size_t length,
|
||||
const cbm_ignore_checked_control_t *control, cbm_ignore_parse_fault_t fault,
|
||||
cbm_ignore_checked_parse_result_t *out, cbm_ignore_checked_error_t *error) {
|
||||
return gi_parse(owner, bytes, length, control, (unsigned)fault, out, error);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,243 @@
|
||||
#include "discover/gitignore_internal.h"
|
||||
|
||||
/* These recursive functions and annotations move unchanged in identity from
|
||||
* gitignore.c. A NULL guard preserves the legacy budget/fallback behavior. */
|
||||
static bool glob_match(const char *pat, const char *str, int *budget,
|
||||
gi_guard_t *guard); // NOLINT(misc-no-recursion)
|
||||
|
||||
static bool glob_match_doublestar_slash(const char *pat, const char *str, int *budget,
|
||||
gi_guard_t *guard) { // NOLINT(misc-no-recursion)
|
||||
if (glob_match(pat, str, budget, guard)) {
|
||||
return true;
|
||||
}
|
||||
for (const char *s = str;; s++) {
|
||||
if (!gi_work(guard, 1) || !*s) {
|
||||
return false;
|
||||
}
|
||||
if (*s == '/' && glob_match(pat, s + 1, budget, guard)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool glob_match_doublestar_any(const char *pat, const char *str, int *budget,
|
||||
gi_guard_t *guard) { // NOLINT(misc-no-recursion)
|
||||
for (const char *s = str;; s++) {
|
||||
if (glob_match(pat, s, budget, guard)) {
|
||||
return true;
|
||||
}
|
||||
if (!gi_work(guard, 1) || !*s) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool glob_match_star(const char *pat, const char *str, int *budget,
|
||||
gi_guard_t *guard) { // NOLINT(misc-no-recursion)
|
||||
for (const char *s = str;; s++) {
|
||||
if (glob_match(pat, s, budget, guard)) {
|
||||
return true;
|
||||
}
|
||||
if (!gi_work(guard, 1) || !*s || *s == '/') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool glob_match_charclass(const char *pat, char ch, const char **pat_out,
|
||||
gi_guard_t *guard) {
|
||||
bool negate_class = false;
|
||||
if (*pat == '!' || *pat == '^') {
|
||||
negate_class = true;
|
||||
pat++;
|
||||
}
|
||||
bool matched = false;
|
||||
char prev = 0;
|
||||
while (*pat && *pat != ']') {
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (*pat == '-' && prev && pat[1] && pat[1] != ']') {
|
||||
pat++;
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (ch >= prev && ch <= *pat) {
|
||||
matched = true;
|
||||
}
|
||||
prev = *pat;
|
||||
pat++;
|
||||
} else {
|
||||
if (ch == *pat) {
|
||||
matched = true;
|
||||
}
|
||||
prev = *pat;
|
||||
pat++;
|
||||
}
|
||||
}
|
||||
if (*pat == ']') {
|
||||
pat++;
|
||||
}
|
||||
*pat_out = pat;
|
||||
return negate_class ? !matched : matched;
|
||||
}
|
||||
|
||||
static bool glob_match_doublestar(const char *pat, const char *str, int *budget,
|
||||
gi_guard_t *guard) { // NOLINT(misc-no-recursion)
|
||||
if (pat[2] == '/') {
|
||||
return glob_match_doublestar_slash(pat + 3, str, budget, guard);
|
||||
}
|
||||
if (pat[2] == '\0') {
|
||||
return true;
|
||||
}
|
||||
return glob_match_doublestar_any(pat + 2, str, budget, guard);
|
||||
}
|
||||
|
||||
static bool glob_match(const char *pat, const char *str, int *budget,
|
||||
gi_guard_t *guard) { // NOLINT(misc-no-recursion)
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (--*budget <= 0) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_ATTEMPT);
|
||||
}
|
||||
if (guard) {
|
||||
if (guard->depth == guard->owner->limits.max_depth) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_DEPTH);
|
||||
}
|
||||
guard->depth++;
|
||||
}
|
||||
bool matched = false;
|
||||
while (*pat && *str) {
|
||||
if (!gi_work(guard, 1)) {
|
||||
goto done;
|
||||
}
|
||||
if (pat[0] == '*' && pat[1] == '*') {
|
||||
matched = glob_match_doublestar(pat, str, budget, guard);
|
||||
goto done;
|
||||
}
|
||||
if (*pat == '*') {
|
||||
matched = glob_match_star(pat + 1, str, budget, guard);
|
||||
goto done;
|
||||
}
|
||||
if (*pat == '?') {
|
||||
if (*str == '/') {
|
||||
goto done;
|
||||
}
|
||||
pat++;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
if (*pat == '[') {
|
||||
const char *next = NULL;
|
||||
if (*str == '/' || !glob_match_charclass(pat + 1, *str, &next, guard)) {
|
||||
goto done;
|
||||
}
|
||||
pat = next;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
if (*pat != *str) {
|
||||
goto done;
|
||||
}
|
||||
pat++;
|
||||
str++;
|
||||
}
|
||||
while (*pat == '*') {
|
||||
if (!gi_work(guard, 1)) {
|
||||
goto done;
|
||||
}
|
||||
pat++;
|
||||
}
|
||||
matched = *pat == '\0' && *str == '\0';
|
||||
done:
|
||||
if (guard) {
|
||||
guard->depth--;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
static bool glob_match_bounded(const char *pat, const char *str, gi_guard_t *guard) {
|
||||
int budget = GI_MATCH_MAX_STEPS;
|
||||
return gi_work(guard, 1) && glob_match(pat, str, &budget, guard);
|
||||
}
|
||||
|
||||
static const char *gi_slash(const char *path, gi_guard_t *guard) {
|
||||
for (;;) {
|
||||
if (!gi_work(guard, 1)) {
|
||||
return NULL;
|
||||
}
|
||||
if (*path == '/') {
|
||||
return path;
|
||||
}
|
||||
if (!*path) {
|
||||
return NULL;
|
||||
}
|
||||
path++;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *gi_basename(const char *path, gi_guard_t *guard) {
|
||||
const char *basename = path;
|
||||
for (;;) {
|
||||
if (!gi_work(guard, 1)) {
|
||||
return NULL;
|
||||
}
|
||||
if (!*path) {
|
||||
return basename;
|
||||
}
|
||||
if (*path == '/') {
|
||||
basename = path + 1;
|
||||
}
|
||||
path++;
|
||||
}
|
||||
}
|
||||
|
||||
static bool match_unrooted(const char *pattern, const char *path, const char *basename,
|
||||
gi_guard_t *guard) {
|
||||
if (glob_match_bounded(pattern, basename, guard)) {
|
||||
return true;
|
||||
}
|
||||
if (!gi_slash(path, guard)) {
|
||||
return false;
|
||||
}
|
||||
const char *suffix = path;
|
||||
while (*suffix) {
|
||||
if (glob_match_bounded(pattern, suffix, guard)) {
|
||||
return true;
|
||||
}
|
||||
const char *next = gi_slash(suffix, guard);
|
||||
if (!next) {
|
||||
break;
|
||||
}
|
||||
suffix = next + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int gi_core_match(const gi_pattern_t *rows, size_t count, const char *path, bool is_directory,
|
||||
gi_guard_t *guard) {
|
||||
const char *basename = gi_basename(path, guard);
|
||||
if (!basename) {
|
||||
return 0;
|
||||
}
|
||||
int matched = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (!gi_work(guard, 1)) {
|
||||
return 0;
|
||||
}
|
||||
const gi_pattern_t *pattern = &rows[i];
|
||||
if (pattern->dir_only && !is_directory) {
|
||||
continue;
|
||||
}
|
||||
bool this_match = pattern->rooted ? glob_match_bounded(pattern->pattern, path, guard)
|
||||
: match_unrooted(pattern->pattern, path, basename, guard);
|
||||
if (!gi_active(guard)) {
|
||||
return 0;
|
||||
}
|
||||
if (this_match) {
|
||||
matched = pattern->negated ? -1 : 1;
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "discover/gitignore_internal.h"
|
||||
#include "foundation/platform.h"
|
||||
#include <string.h>
|
||||
|
||||
void gi_error(cbm_ignore_checked_error_t *error, cbm_ignore_checked_status_t status,
|
||||
cbm_ignore_cap_t cap, size_t offset) {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
memset(error, 0, sizeof(*error));
|
||||
error->status = status;
|
||||
error->cap = cap;
|
||||
error->byte_offset = offset;
|
||||
if (status != CBM_IGNORE_CHECKED_OK) {
|
||||
static const char message[] = "checked ignore operation failed";
|
||||
memcpy(error->diagnostic, message, sizeof(message));
|
||||
}
|
||||
}
|
||||
|
||||
bool gi_active(const gi_guard_t *guard) {
|
||||
return !guard || guard->owner->usage.terminal_status == CBM_IGNORE_CHECKED_OK;
|
||||
}
|
||||
|
||||
bool gi_fail(gi_guard_t *guard, cbm_ignore_checked_status_t status, cbm_ignore_cap_t cap) {
|
||||
if (guard && gi_active(guard)) {
|
||||
guard->owner->usage.terminal_status = status;
|
||||
gi_error(&guard->owner->error, status, cap, guard->offset);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gi_poll(gi_guard_t *guard) {
|
||||
if (!gi_active(guard)) {
|
||||
return false;
|
||||
}
|
||||
if (!guard) {
|
||||
return true;
|
||||
}
|
||||
const cbm_ignore_checked_control_t *control = guard->control;
|
||||
if (control->cancelled && control->cancelled(control->context)) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_CANCELLED, CBM_IGNORE_CAP_NONE);
|
||||
}
|
||||
if (cbm_now_ms() >= control->deadline_ms) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_DEADLINE, CBM_IGNORE_CAP_NONE);
|
||||
}
|
||||
guard->ticks = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gi_work(gi_guard_t *guard, size_t units) {
|
||||
if (!gi_active(guard)) {
|
||||
return false;
|
||||
}
|
||||
if (!guard) {
|
||||
return true;
|
||||
}
|
||||
cbm_ignore_checked_t *owner = guard->owner;
|
||||
if (units > owner->limits.max_work - owner->usage.work_used) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_WORK);
|
||||
}
|
||||
/* All callers submit a constant-size event or a bounded memory chunk. */
|
||||
while (units) {
|
||||
size_t part = GI_POLL_UNITS - guard->ticks;
|
||||
if (part > units) {
|
||||
part = units;
|
||||
}
|
||||
owner->usage.work_used += part;
|
||||
guard->ticks += (unsigned)part;
|
||||
units -= part;
|
||||
if (guard->ticks == GI_POLL_UNITS && !gi_poll(guard)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gi_bytes(gi_guard_t *guard, size_t bytes) {
|
||||
cbm_ignore_checked_t *owner = guard->owner;
|
||||
if (bytes > owner->limits.max_bytes - owner->usage.bytes_reserved) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_BYTES);
|
||||
}
|
||||
owner->usage.bytes_reserved += bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gi_pattern_reserve(gi_guard_t *guard) {
|
||||
cbm_ignore_checked_t *owner = guard->owner;
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (owner->usage.patterns_reserved == owner->limits.max_patterns) {
|
||||
return gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_PATTERNS);
|
||||
}
|
||||
owner->usage.patterns_reserved++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gi_begin(cbm_ignore_checked_t *owner, const cbm_ignore_checked_control_t *control,
|
||||
gi_guard_t *guard) {
|
||||
*guard = (gi_guard_t){.owner = owner, .control = control, .offset = SIZE_MAX};
|
||||
return gi_poll(guard) && gi_work(guard, 1);
|
||||
}
|
||||
|
||||
cbm_ignore_checked_status_t gi_finish(const cbm_ignore_checked_t *owner,
|
||||
cbm_ignore_checked_error_t *error) {
|
||||
if (error) {
|
||||
*error = owner->error;
|
||||
}
|
||||
return owner->usage.terminal_status;
|
||||
}
|
||||
|
||||
void *gi_allocate(gi_guard_t *guard, size_t bytes, bool simulate_oom) {
|
||||
cbm_ignore_checked_t *owner = guard->owner;
|
||||
CBMArena *arena = &owner->arena;
|
||||
if (!gi_poll(guard)) {
|
||||
return NULL;
|
||||
}
|
||||
if (bytes > owner->limits.max_arena_bytes - owner->usage.arena_requested_bytes ||
|
||||
bytes > (SIZE_MAX / 2) - GI_ARENA_ALIGN) {
|
||||
gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_ARENA);
|
||||
return NULL;
|
||||
}
|
||||
size_t aligned = (bytes + GI_ARENA_ALIGN) & ~(size_t)GI_ARENA_ALIGN;
|
||||
if (aligned > SIZE_MAX - arena->used || aligned > SIZE_MAX - arena->total_alloc) {
|
||||
gi_fail(guard, CBM_IGNORE_CHECKED_LIMIT, CBM_IGNORE_CAP_ARENA);
|
||||
return NULL;
|
||||
}
|
||||
owner->usage.arena_requested_bytes += bytes;
|
||||
/* Keep the arena's doubling arithmetic representable; no allocator changes. */
|
||||
if (arena->grow_size > SIZE_MAX / 2) {
|
||||
arena->grow_size = CBM_ARENA_DEFAULT_BLOCK_SIZE;
|
||||
}
|
||||
void *memory = simulate_oom ? NULL : cbm_arena_alloc(arena, bytes);
|
||||
if (!memory) {
|
||||
gi_fail(guard, CBM_IGNORE_CHECKED_OOM, CBM_IGNORE_CAP_NONE);
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
|
||||
bool gi_zero(gi_guard_t *guard, void *storage, size_t bytes) {
|
||||
unsigned char *output = storage;
|
||||
enum { ZERO_CHUNK = 256 };
|
||||
while (bytes) {
|
||||
size_t chunk = bytes < ZERO_CHUNK ? bytes : ZERO_CHUNK;
|
||||
if (!gi_work(guard, chunk)) {
|
||||
return false;
|
||||
}
|
||||
memset(output, 0, chunk);
|
||||
output += chunk;
|
||||
bytes -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool gi_trim(const char *line, size_t *length, gi_guard_t *guard) {
|
||||
while (*length) {
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
char last = line[*length - 1];
|
||||
if (last != ' ' && last != '\t' && last != '\r') {
|
||||
break;
|
||||
}
|
||||
(*length)--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gi_normalize(const char *line, size_t length, gi_view_t *view, gi_guard_t *guard) {
|
||||
*view = (gi_view_t){0};
|
||||
if (!length || !gi_work(guard, 1) || line[0] == '#') {
|
||||
return gi_active(guard);
|
||||
}
|
||||
if (!gi_trim(line, &length, guard) || !length) {
|
||||
return gi_active(guard);
|
||||
}
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (*line == '!') {
|
||||
view->negated = true;
|
||||
line++;
|
||||
length--;
|
||||
}
|
||||
if (!length || !gi_work(guard, 1)) {
|
||||
return gi_active(guard);
|
||||
}
|
||||
if (line[length - 1] == '/') {
|
||||
view->dir_only = true;
|
||||
length--;
|
||||
}
|
||||
if (!length || !gi_work(guard, 1)) {
|
||||
return gi_active(guard);
|
||||
}
|
||||
if (*line == '/') {
|
||||
view->rooted = true;
|
||||
line++;
|
||||
length--;
|
||||
}
|
||||
for (size_t i = 0; !view->rooted && i < length; i++) {
|
||||
if (!gi_work(guard, 1)) {
|
||||
return false;
|
||||
}
|
||||
view->rooted = line[i] == '/';
|
||||
}
|
||||
view->text = line;
|
||||
view->length = length;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef CBM_GITIGNORE_INTERNAL_H
|
||||
#define CBM_GITIGNORE_INTERNAL_H
|
||||
#include "discover/gitignore_checked.h"
|
||||
#include "foundation/arena.h"
|
||||
|
||||
typedef struct {
|
||||
char *pattern;
|
||||
bool negated;
|
||||
bool dir_only;
|
||||
bool rooted;
|
||||
} gi_pattern_t;
|
||||
|
||||
typedef struct {
|
||||
const char *text;
|
||||
size_t length;
|
||||
bool negated;
|
||||
bool dir_only;
|
||||
bool rooted;
|
||||
} gi_view_t;
|
||||
|
||||
struct cbm_ignore_checked {
|
||||
CBMArena arena;
|
||||
cbm_ignore_checked_limits_t limits;
|
||||
cbm_ignore_checked_usage_t usage;
|
||||
cbm_ignore_checked_error_t error;
|
||||
};
|
||||
|
||||
struct cbm_ignore_program {
|
||||
const cbm_ignore_checked_t *owner;
|
||||
size_t count;
|
||||
gi_pattern_t rows[];
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
cbm_ignore_checked_t *owner;
|
||||
const cbm_ignore_checked_control_t *control;
|
||||
size_t offset;
|
||||
unsigned ticks;
|
||||
unsigned depth;
|
||||
} gi_guard_t;
|
||||
|
||||
enum { GI_POLL_UNITS = 4096, GI_MATCH_MAX_STEPS = 20000, GI_ARENA_ALIGN = 7 };
|
||||
enum { GI_FAULT_NONE, GI_FAULT_PROGRAM, GI_FAULT_PATTERN };
|
||||
|
||||
void gi_error(cbm_ignore_checked_error_t *error, cbm_ignore_checked_status_t status,
|
||||
cbm_ignore_cap_t cap, size_t offset);
|
||||
bool gi_fail(gi_guard_t *guard, cbm_ignore_checked_status_t status, cbm_ignore_cap_t cap);
|
||||
bool gi_active(const gi_guard_t *guard);
|
||||
bool gi_poll(gi_guard_t *guard);
|
||||
bool gi_work(gi_guard_t *guard, size_t units);
|
||||
bool gi_bytes(gi_guard_t *guard, size_t bytes);
|
||||
bool gi_pattern_reserve(gi_guard_t *guard);
|
||||
bool gi_begin(cbm_ignore_checked_t *owner, const cbm_ignore_checked_control_t *control,
|
||||
gi_guard_t *guard);
|
||||
cbm_ignore_checked_status_t gi_finish(const cbm_ignore_checked_t *owner,
|
||||
cbm_ignore_checked_error_t *error);
|
||||
void *gi_allocate(gi_guard_t *guard, size_t bytes, bool simulate_oom);
|
||||
bool gi_zero(gi_guard_t *guard, void *storage, size_t bytes);
|
||||
bool gi_normalize(const char *line, size_t length, gi_view_t *view, gi_guard_t *guard);
|
||||
int gi_core_match(const gi_pattern_t *rows, size_t count, const char *path, bool is_directory,
|
||||
gi_guard_t *guard);
|
||||
#endif
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "discover/inventory_internal.h"
|
||||
#include "discover/inventory_limits_internal.h"
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
bool cif_limits_valid(const cbm_inventory_limits_t *l) {
|
||||
return l && l->max_files && l->max_files <= INT_MAX && l->max_directories &&
|
||||
l->max_directories <= INT_MAX && l->max_arena_bytes && l->max_ignore_arena_bytes &&
|
||||
l->max_ignore_arena_bytes <= l->max_arena_bytes && l->max_control_file_bytes &&
|
||||
l->max_control_total_bytes && l->max_ignore_patterns &&
|
||||
l->max_ignore_patterns <= INT_MAX && l->max_probe_prefix_bytes && l->max_ignore_work &&
|
||||
l->max_verified_file_reads && l->max_verified_content_bytes;
|
||||
}
|
||||
|
||||
static bool cif_open_ignore(cif_context *c) {
|
||||
cbm_inventory_limits_t *l = &c->owner->limits;
|
||||
cbm_ignore_checked_limits_t limits = {.max_bytes =
|
||||
l->max_control_total_bytes + l->max_ignore_work,
|
||||
.max_work = l->max_ignore_work,
|
||||
.max_patterns = l->max_ignore_patterns,
|
||||
.max_arena_bytes = l->max_ignore_arena_bytes,
|
||||
.max_depth = CBM_IGNORE_CHECKED_DEPTH_MAX};
|
||||
c->file_index = SIZE_MAX;
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
cbm_ignore_checked_status_t status = cbm_ignore_checked_open(&limits, &c->owner->ignore, NULL);
|
||||
return cif_checked(c, status) && cif_poll(c);
|
||||
}
|
||||
|
||||
static bool cif_publish(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
cbm_ignore_checked_usage_t checked;
|
||||
c->file_index = SIZE_MAX;
|
||||
if (!cbm_ignore_checked_usage(o->ignore, &checked) ||
|
||||
checked.terminal_status != CBM_IGNORE_CHECKED_OK) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
o->usage.ignore_bytes_reserved = checked.bytes_reserved;
|
||||
o->usage.ignore_work_used = checked.work_used;
|
||||
o->usage.ignore_patterns_reserved = checked.patterns_reserved;
|
||||
o->usage.ignore_arena_requested_bytes = checked.arena_requested_bytes;
|
||||
size_t d = o->usage.non_ignore_arena_requested_bytes;
|
||||
size_t i = o->limits.max_ignore_arena_bytes;
|
||||
if (checked.arena_requested_bytes > i || d > o->limits.max_arena_bytes - i) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
o->usage.arena_requested_bytes = d + checked.arena_requested_bytes;
|
||||
o->usage.arena_budget_used_bytes = d + i;
|
||||
o->view.files = o->files;
|
||||
o->view.rows = o->rows;
|
||||
o->view.controls = o->controls;
|
||||
o->view.excluded_directories = o->exclusions;
|
||||
return cif_poll(c);
|
||||
}
|
||||
|
||||
cbm_inventory_status_t cbm_inventory_filter_prepare(const cbm_inventory_source_t *source,
|
||||
const cbm_inventory_limits_t *limits,
|
||||
const cbm_inventory_control_t *control,
|
||||
cbm_inventory_filter_t **out,
|
||||
cbm_inventory_error_t *error) {
|
||||
cbm_inventory_error_t initial = {.status = CBM_INVENTORY_OK, .file_index = SIZE_MAX};
|
||||
if (out) {
|
||||
*out = NULL;
|
||||
}
|
||||
if (error) {
|
||||
*error = initial;
|
||||
}
|
||||
cbm_inventory_filter_t bootstrap = {0};
|
||||
cif_context c = {.owner = &bootstrap,
|
||||
.source = source,
|
||||
.control = control,
|
||||
.error = initial,
|
||||
.file_index = SIZE_MAX};
|
||||
if (!out || !source || !cif_limits_valid(limits) || !control || !control->deadline_ms ||
|
||||
!source->native_root || !source->read || (source->file_count && !source->files)) {
|
||||
cif_fail(&c, CBM_INVENTORY_INVALID);
|
||||
} else if (source->file_count > limits->max_files || source->file_count > INT_MAX ||
|
||||
limits->max_control_total_bytes > UINT64_MAX - limits->max_ignore_work) {
|
||||
cif_fail(&c, CBM_INVENTORY_LIMIT);
|
||||
} else if (cif_poll(&c)) {
|
||||
bootstrap.limits = *limits;
|
||||
bootstrap.deadline_ms = control->deadline_ms;
|
||||
cbm_arena_init_lazy(&bootstrap.arena, CBM_ARENA_DEFAULT_BLOCK_SIZE);
|
||||
cbm_inventory_filter_t *owner = cif_alloc(&c, 1, sizeof(*owner));
|
||||
if (owner && cif_copy(&c, owner, &bootstrap, sizeof(*owner))) {
|
||||
c.owner = owner;
|
||||
if (cif_metadata(&c) && cif_directories(&c) && cif_open_ignore(&c) && cif_filter(&c) &&
|
||||
cif_publish(&c)) {
|
||||
*out = owner;
|
||||
return CBM_INVENTORY_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
cbm_inventory_filter_free(c.owner);
|
||||
if (error) {
|
||||
*error = c.error;
|
||||
}
|
||||
return c.error.status;
|
||||
}
|
||||
|
||||
const cbm_inventory_filter_view_t *cbm_inventory_filter_view(const cbm_inventory_filter_t *owner) {
|
||||
return owner ? &owner->view : NULL;
|
||||
}
|
||||
|
||||
bool cbm_inventory_filter_usage(const cbm_inventory_filter_t *owner,
|
||||
cbm_inventory_filter_usage_t *out) {
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
*out = owner->usage;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_inventory_filter_limits(const cbm_inventory_filter_t *owner, cbm_inventory_limits_t *out) {
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
*out = owner->limits;
|
||||
return true;
|
||||
}
|
||||
|
||||
void cbm_inventory_filter_free(cbm_inventory_filter_t *owner) {
|
||||
if (owner) {
|
||||
cbm_ignore_checked_free(owner->ignore);
|
||||
CBMArena arena = owner->arena;
|
||||
cbm_arena_destroy(&arena);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "discover/inventory_internal.h"
|
||||
#include "foundation/sha256.h"
|
||||
#include <string.h>
|
||||
|
||||
static bool cif_control_index(cif_context *c, size_t directory, const char *name,
|
||||
size_t name_length, size_t *index) {
|
||||
cbm_inventory_path_t path = c->owner->directories[directory].path;
|
||||
size_t separator = path.length ? 1 : 0;
|
||||
*index = SIZE_MAX;
|
||||
/* A longer candidate cannot occur in this fully validated inventory. */
|
||||
if (path.length > CIF_PATH_MAX - name_length - separator) {
|
||||
return true;
|
||||
}
|
||||
unsigned char buffer[CIF_PATH_MAX + 1];
|
||||
if (!cif_copy(c, buffer, path.data, path.length) || !cif_bytes(c, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (separator) {
|
||||
buffer[path.length] = '/';
|
||||
}
|
||||
if (!cif_copy(c, buffer + path.length + separator, name, name_length + 1)) {
|
||||
return false;
|
||||
}
|
||||
cbm_inventory_path_t candidate = {buffer, path.length + separator + name_length};
|
||||
if (!cif_find_file(c, candidate, index)) {
|
||||
return false;
|
||||
}
|
||||
if (*index == SIZE_MAX) {
|
||||
size_t collision = SIZE_MAX;
|
||||
if (!cif_find_directory(c, candidate, &collision)) {
|
||||
return false;
|
||||
}
|
||||
if (collision != SIZE_MAX) {
|
||||
return cif_fail(c, CBM_INVENTORY_UNSUPPORTED);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_reserve_control(cif_context *c, uint64_t length) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
cbm_inventory_limits_t *l = &o->limits;
|
||||
cbm_inventory_filter_usage_t *u = &o->usage;
|
||||
if ((uint64_t)(size_t)length != length || length > l->max_control_file_bytes ||
|
||||
u->verified_file_reads_reserved >= l->max_verified_file_reads ||
|
||||
length > l->max_verified_content_bytes - u->verified_content_bytes_reserved ||
|
||||
length > l->max_control_total_bytes - u->control_bytes_reserved) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
u->verified_file_reads_reserved++;
|
||||
u->verified_content_bytes_reserved += length;
|
||||
u->control_bytes_reserved += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_read_control(cif_context *c, size_t index, size_t length) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
if (length > o->scratch_capacity) {
|
||||
unsigned char *buffer = cif_alloc(c, length, 1);
|
||||
if (!buffer) {
|
||||
return false;
|
||||
}
|
||||
o->scratch = buffer;
|
||||
o->scratch_capacity = length;
|
||||
}
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
size_t copied = 0;
|
||||
cbm_inventory_error_t error = {.status = CBM_INVENTORY_OK, .file_index = SIZE_MAX};
|
||||
cbm_inventory_status_t status =
|
||||
c->source->read(c->source->read_context, index, length ? o->scratch : NULL, length, &copied,
|
||||
c->control, &error);
|
||||
c->error.cleanup_required |= error.cleanup_required;
|
||||
if ((unsigned)status > (unsigned)CBM_INVENTORY_DEADLINE || error.status != status) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
if (status != CBM_INVENTORY_OK) {
|
||||
return cif_fail(c, copied ? CBM_INVENTORY_INVALID : status);
|
||||
}
|
||||
if (copied != length || error.cleanup_required) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
return cif_poll(c);
|
||||
}
|
||||
|
||||
static bool cif_hash_control(cif_context *c, size_t index, size_t length) {
|
||||
cbm_sha256_ctx hash;
|
||||
unsigned char digest[CBM_SHA256_DIGEST_LEN];
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
cbm_sha256_init(&hash);
|
||||
for (size_t at = 0; at < length;) {
|
||||
size_t n = length - at < 512 ? length - at : 512;
|
||||
if (!cif_poll(c) || !cif_bytes(c, n)) {
|
||||
return false;
|
||||
}
|
||||
cbm_sha256_update(&hash, c->owner->scratch + at, n);
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
at += n;
|
||||
}
|
||||
cbm_sha256_final(&hash, digest);
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
if (memcmp(digest, c->owner->files[index].content_sha256, sizeof(digest))) {
|
||||
return cif_fail(c, CBM_INVENTORY_CHANGED);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_load_control(cif_context *c, size_t directory, cbm_inventory_control_kind_t kind,
|
||||
cbm_inventory_control_row_t *row,
|
||||
const cbm_ignore_program_t **program) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
c->file_index = SIZE_MAX;
|
||||
if (!cif_event(c) || !cif_bytes(c, 256)) {
|
||||
return false;
|
||||
}
|
||||
*row = (cbm_inventory_control_row_t){
|
||||
.kind = kind, .directory = o->directories[directory].path, .file_index = SIZE_MAX};
|
||||
const char *name = kind == CBM_INVENTORY_CONTROL_CBMIGNORE ? ".cbmignore" : ".gitignore";
|
||||
size_t index = SIZE_MAX;
|
||||
if (!cif_control_index(c, directory, name, 10, &index)) {
|
||||
return false;
|
||||
}
|
||||
if (index == SIZE_MAX) {
|
||||
return true;
|
||||
}
|
||||
c->file_index = index;
|
||||
uint64_t size = o->files[index].content_length;
|
||||
if (!cif_reserve_control(c, size)) {
|
||||
return false;
|
||||
}
|
||||
size_t length = (size_t)size;
|
||||
if (!cif_read_control(c, index, length) || !cif_hash_control(c, index, length) ||
|
||||
!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
cbm_ignore_checked_control_t control = cif_ignore_control(c);
|
||||
cbm_ignore_checked_parse_result_t parsed = {0};
|
||||
cbm_ignore_checked_status_t status = cbm_ignore_checked_parse(
|
||||
o->ignore, length ? o->scratch : NULL, length, &control, &parsed, NULL);
|
||||
if (!cif_checked(c, status)) {
|
||||
return false;
|
||||
}
|
||||
if (!parsed.program) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
*program = parsed.program;
|
||||
row->file_index = index;
|
||||
row->effective_patterns = parsed.pattern_count;
|
||||
row->outcome =
|
||||
parsed.pattern_count ? CBM_INVENTORY_CONTROL_APPLIED : CBM_INVENTORY_CONTROL_APPLIED_EMPTY;
|
||||
o->rows[index].roles |= kind == CBM_INVENTORY_CONTROL_CBMIGNORE
|
||||
? CBM_INVENTORY_FILTER_ROLE_CBMIGNORE
|
||||
: CBM_INVENTORY_FILTER_ROLE_GITIGNORE;
|
||||
return cif_poll(c);
|
||||
}
|
||||
|
||||
bool cif_load_controls(cif_context *c, size_t directory) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
if (!directory && !cif_load_control(c, 0, CBM_INVENTORY_CONTROL_CBMIGNORE, &o->cbm_control,
|
||||
&o->cbm_program)) {
|
||||
return false;
|
||||
}
|
||||
cbm_inventory_control_row_t *row = &o->controls[o->view.control_count];
|
||||
if (!cif_load_control(c, directory, CBM_INVENTORY_CONTROL_GITIGNORE, row,
|
||||
&o->directories[directory].program)) {
|
||||
return false;
|
||||
}
|
||||
o->view.control_count++;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "discover/inventory_internal.h"
|
||||
|
||||
static bool cif_common(cif_context *c, cbm_inventory_path_t a, cbm_inventory_path_t b,
|
||||
size_t *length) {
|
||||
*length = 0;
|
||||
size_t n = a.length < b.length ? a.length : b.length;
|
||||
while (*length < n) {
|
||||
if (!cif_bytes(c, 2)) {
|
||||
return false;
|
||||
}
|
||||
if (a.data[*length] != b.data[*length]) {
|
||||
break;
|
||||
}
|
||||
(*length)++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_directory_init(cif_context *c, cif_directory *d, cbm_inventory_path_t path) {
|
||||
cif_directory value = {.parent = SIZE_MAX,
|
||||
.next = SIZE_MAX,
|
||||
.first_directory = SIZE_MAX,
|
||||
.last_directory = SIZE_MAX,
|
||||
.first_file = SIZE_MAX,
|
||||
.last_file = SIZE_MAX,
|
||||
.excluded = SIZE_MAX};
|
||||
value.path.data = cif_string(c, path.data, path.length);
|
||||
value.path.length = path.length;
|
||||
return value.path.data && cif_copy(c, d, &value, sizeof(value));
|
||||
}
|
||||
|
||||
/* Equal slash-terminated prefixes occupy one contiguous run of sorted files.
|
||||
* Their first occurrence is unique; the generated directory order still needs
|
||||
* sorting (a-/x precedes a/x although directory a precedes directory a-). */
|
||||
static bool cif_prefixes(cif_context *c, bool fill, size_t *total) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t count = 1;
|
||||
for (size_t i = 0; i < o->view.file_count; i++) {
|
||||
c->file_index = i;
|
||||
cbm_inventory_path_t path = o->files[i].path;
|
||||
size_t common = 0;
|
||||
if (!cif_event(c) || !cif_bytes(c, 128) ||
|
||||
(i && !cif_common(c, o->files[i - 1].path, path, &common))) {
|
||||
return false;
|
||||
}
|
||||
for (size_t j = 0; j < path.length; j++) {
|
||||
if (!cif_bytes(c, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (path.data[j] != '/' || j < common) {
|
||||
continue;
|
||||
}
|
||||
if (count >= o->limits.max_directories) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
if (fill && !cif_directory_init(c, &o->directories[count],
|
||||
(cbm_inventory_path_t){path.data, j})) {
|
||||
return false;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
*total = count;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_merge(cif_context *c, const cif_directory *from, cif_directory *to, size_t start,
|
||||
size_t middle, size_t end) {
|
||||
size_t left = start, right = middle;
|
||||
for (size_t at = start; at < end; at++) {
|
||||
int order = 0;
|
||||
if (!cif_event(c)) {
|
||||
return false;
|
||||
}
|
||||
if (left < middle && right < end &&
|
||||
!cif_compare(c, from[left].path, from[right].path, &order)) {
|
||||
return false;
|
||||
}
|
||||
size_t selected = right == end || (left < middle && order <= 0) ? left++ : right++;
|
||||
if (!cif_copy(c, &to[at], &from[selected], sizeof(*to))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_sort_directories(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t n = o->view.directory_count;
|
||||
if (n < 2) {
|
||||
return true;
|
||||
}
|
||||
cif_directory *temporary = cif_alloc(c, n, sizeof(*temporary));
|
||||
if (!temporary) {
|
||||
return false;
|
||||
}
|
||||
cif_directory *from = o->directories, *to = temporary;
|
||||
for (size_t width = 1; width < n;) {
|
||||
for (size_t start = 0; start < n;) {
|
||||
size_t middle = start + (width < n - start ? width : n - start);
|
||||
size_t end = middle + (width < n - middle ? width : n - middle);
|
||||
if (!cif_merge(c, from, to, start, middle, end)) {
|
||||
return false;
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
cif_directory *swap = from;
|
||||
from = to;
|
||||
to = swap;
|
||||
if (width >= n - width) {
|
||||
break;
|
||||
}
|
||||
width *= 2;
|
||||
}
|
||||
return from == o->directories || cif_copy(c, o->directories, from, n * sizeof(*from));
|
||||
}
|
||||
|
||||
static bool cif_find(cif_context *c, cbm_inventory_path_t path, bool directory, size_t *index) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t low = 0, high = directory ? o->view.directory_count : o->view.file_count;
|
||||
*index = SIZE_MAX;
|
||||
while (low < high) {
|
||||
size_t mid = low + (high - low) / 2;
|
||||
cbm_inventory_path_t candidate = directory ? o->directories[mid].path : o->files[mid].path;
|
||||
int order = 0;
|
||||
if (!cif_compare(c, candidate, path, &order)) {
|
||||
return false;
|
||||
}
|
||||
if (!order) {
|
||||
*index = mid;
|
||||
return true;
|
||||
}
|
||||
if (order < 0) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_find_file(cif_context *c, cbm_inventory_path_t path, size_t *index) {
|
||||
return cif_find(c, path, false, index);
|
||||
}
|
||||
|
||||
bool cif_find_directory(cif_context *c, cbm_inventory_path_t path, size_t *index) {
|
||||
return cif_find(c, path, true, index);
|
||||
}
|
||||
|
||||
static bool cif_parent(cif_context *c, cbm_inventory_path_t path, size_t *parent) {
|
||||
size_t length = path.length;
|
||||
while (length) {
|
||||
if (!cif_bytes(c, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (path.data[--length] == '/') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!cif_find_directory(c, (cbm_inventory_path_t){path.data, length}, parent)) {
|
||||
return false;
|
||||
}
|
||||
return *parent != SIZE_MAX || cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
|
||||
static bool cif_link_directories(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
for (size_t i = 1; i < o->view.directory_count; i++) {
|
||||
c->file_index = SIZE_MAX;
|
||||
size_t collision = SIZE_MAX;
|
||||
cif_directory *d = &o->directories[i];
|
||||
if (!cif_event(c) || !cif_bytes(c, 256) || !cif_find_file(c, d->path, &collision)) {
|
||||
return false;
|
||||
}
|
||||
if (collision != SIZE_MAX) {
|
||||
c->file_index = collision;
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
if (!cif_parent(c, d->path, &d->parent)) {
|
||||
return false;
|
||||
}
|
||||
if (d->parent >= i) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
if (!cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
cif_directory *parent = &o->directories[d->parent];
|
||||
if (parent->last_directory != SIZE_MAX) {
|
||||
o->directories[parent->last_directory].next = i;
|
||||
} else {
|
||||
parent->first_directory = i;
|
||||
}
|
||||
parent->last_directory = i;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_link_files(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
for (size_t i = 0; i < o->view.file_count; i++) {
|
||||
c->file_index = i;
|
||||
size_t at = SIZE_MAX;
|
||||
if (!cif_event(c) || !cif_bytes(c, 256) || !cif_parent(c, o->files[i].path, &at)) {
|
||||
return false;
|
||||
}
|
||||
if (!cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
cif_directory *parent = &o->directories[at];
|
||||
if (parent->last_file != SIZE_MAX) {
|
||||
o->file_next[parent->last_file] = i;
|
||||
} else {
|
||||
parent->first_file = i;
|
||||
}
|
||||
parent->last_file = i;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_directories(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t count = 0;
|
||||
if (!cif_prefixes(c, false, &count)) {
|
||||
return false;
|
||||
}
|
||||
c->file_index = SIZE_MAX;
|
||||
o->view.directory_count = count;
|
||||
o->directories = cif_alloc(c, count, sizeof(*o->directories));
|
||||
o->controls = cif_alloc(c, count + 1, sizeof(*o->controls));
|
||||
o->exclusions = cif_alloc(c, count, sizeof(*o->exclusions));
|
||||
if (!o->directories || !o->controls || !o->exclusions ||
|
||||
!cif_directory_init(c, &o->directories[0], (cbm_inventory_path_t){NULL, 0}) ||
|
||||
!cif_prefixes(c, true, &count)) {
|
||||
return false;
|
||||
}
|
||||
c->file_index = SIZE_MAX;
|
||||
return cif_sort_directories(c) && cif_link_directories(c) && cif_link_files(c);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "discover/inventory_internal.h"
|
||||
#include "discover/discover.h"
|
||||
#include "discover/discover_policy_internal.h"
|
||||
|
||||
static bool cif_match(cif_context *c, const cbm_ignore_program_t *program,
|
||||
cbm_inventory_path_t path, bool directory, cbm_ignore_decision_t *decision) {
|
||||
*decision = CBM_IGNORE_NO_OPINION;
|
||||
if (!program) {
|
||||
return true;
|
||||
}
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
cbm_ignore_checked_control_t control = cif_ignore_control(c);
|
||||
cbm_ignore_checked_status_t status =
|
||||
cbm_ignore_checked_match(c->owner->ignore, program, (const char *)path.data, path.length,
|
||||
directory, &control, decision, NULL);
|
||||
if (!cif_checked(c, status)) {
|
||||
return false;
|
||||
}
|
||||
if (*decision != CBM_IGNORE_REINCLUDED && *decision != CBM_IGNORE_NO_OPINION &&
|
||||
*decision != CBM_IGNORE_IGNORED) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
return cif_poll(c);
|
||||
}
|
||||
|
||||
static bool cif_git_opinion(cif_context *c, size_t parent, cbm_inventory_path_t path,
|
||||
bool directory, cbm_ignore_decision_t *decision) {
|
||||
*decision = CBM_IGNORE_NO_OPINION;
|
||||
while (parent != SIZE_MAX) {
|
||||
if (!cif_event(c) || !cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
const cif_directory *d = &c->owner->directories[parent];
|
||||
size_t offset = d->path.length ? d->path.length + 1 : 0;
|
||||
if (offset > path.length) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
cbm_inventory_path_t local = {path.data + offset, path.length - offset};
|
||||
if (!cif_match(c, d->program, local, directory, decision)) {
|
||||
return false;
|
||||
}
|
||||
if (*decision != CBM_IGNORE_NO_OPINION) {
|
||||
return true;
|
||||
}
|
||||
parent = d->parent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_ignore_reason(cif_context *c, size_t parent, cbm_inventory_path_t path,
|
||||
bool directory, cbm_inventory_filter_reason_t *reason) {
|
||||
cbm_ignore_decision_t decision;
|
||||
if (!cif_git_opinion(c, parent, path, directory, &decision)) {
|
||||
return false;
|
||||
}
|
||||
if (decision == CBM_IGNORE_IGNORED) {
|
||||
*reason = CBM_INVENTORY_FILTER_REASON_GITIGNORE;
|
||||
return true;
|
||||
}
|
||||
if (!cif_match(c, c->owner->cbm_program, path, directory, &decision)) {
|
||||
return false;
|
||||
}
|
||||
if (decision == CBM_IGNORE_IGNORED) {
|
||||
*reason = CBM_INVENTORY_FILTER_REASON_CBMIGNORE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *cif_basename(const cbm_inventory_filter_t *o, size_t parent,
|
||||
cbm_inventory_path_t path) {
|
||||
size_t length = o->directories[parent].path.length;
|
||||
return (const char *)path.data + (length ? length + 1 : 0);
|
||||
}
|
||||
|
||||
static bool cif_file_reason(cif_context *c, size_t parent, cbm_inventory_path_t path,
|
||||
cbm_inventory_filter_reason_t *reason) {
|
||||
const char *name = cif_basename(c->owner, parent, path);
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
/* Shared FULL helpers have bounded literal tables; suffix length is cached
|
||||
* in discover.c so a 4095-byte name cannot cause repeated full scans. */
|
||||
if (cbm_has_ignored_suffix(name, CBM_MODE_FULL)) {
|
||||
*reason = CBM_INVENTORY_FILTER_REASON_IGNORED_SUFFIX;
|
||||
} else if (cbm_should_skip_filename(name, CBM_MODE_FULL)) {
|
||||
*reason = CBM_INVENTORY_FILTER_REASON_SKIP_LIST;
|
||||
} else if (cbm_matches_fast_pattern(name, CBM_MODE_FULL)) {
|
||||
*reason = CBM_INVENTORY_FILTER_REASON_FAST_PATTERN;
|
||||
}
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
return *reason != CBM_INVENTORY_FILTER_REASON_NONE ||
|
||||
cif_ignore_reason(c, parent, path, false, reason);
|
||||
}
|
||||
|
||||
static bool cif_directory_reason(cif_context *c, size_t parent, cbm_inventory_path_t path,
|
||||
cbm_inventory_filter_reason_t *reason) {
|
||||
const char *name = cif_basename(c->owner, parent, path);
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL);
|
||||
bool suffix = !builtin && cbm_discover_path_has_skip_suffix((const char *)path.data);
|
||||
bool safety =
|
||||
(builtin || suffix) && c->owner->cbm_program && cbm_discover_is_safety_core_directory(name);
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
if (builtin || suffix) {
|
||||
cbm_ignore_decision_t decision = CBM_IGNORE_NO_OPINION;
|
||||
if (!safety && !cif_match(c, c->owner->cbm_program, path, true, &decision)) {
|
||||
return false;
|
||||
}
|
||||
if (decision != CBM_IGNORE_REINCLUDED) {
|
||||
*reason = builtin ? CBM_INVENTORY_FILTER_REASON_DIRECTORY_BUILTIN
|
||||
: CBM_INVENTORY_FILTER_REASON_DIRECTORY_SUFFIX;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return cif_ignore_reason(c, parent, path, true, reason);
|
||||
}
|
||||
|
||||
static bool cif_file_entry(cif_context *c, size_t parent, size_t file) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
c->file_index = file;
|
||||
cbm_inventory_filter_reason_t reason = CBM_INVENTORY_FILTER_REASON_NONE;
|
||||
if (!cif_file_reason(c, parent, o->files[file].path, &reason) || !cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
o->rows[file].reason = reason;
|
||||
if (reason != CBM_INVENTORY_FILTER_REASON_NONE) {
|
||||
o->rows[file].disposition = CBM_INVENTORY_FILTER_IGNORED_FILE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_directory_entry(cif_context *c, size_t parent, size_t directory) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
c->file_index = SIZE_MAX;
|
||||
cbm_inventory_filter_reason_t reason = CBM_INVENTORY_FILTER_REASON_NONE;
|
||||
if (!cif_directory_reason(c, parent, o->directories[directory].path, &reason) ||
|
||||
!cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
o->directories[directory].reason = reason;
|
||||
if (reason != CBM_INVENTORY_FILTER_REASON_NONE) {
|
||||
o->directories[directory].excluded = directory;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_entries(cif_context *c, size_t parent) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t file = o->directories[parent].first_file;
|
||||
size_t directory = o->directories[parent].first_directory;
|
||||
while (file != SIZE_MAX || directory != SIZE_MAX) {
|
||||
if (!cif_event(c) || !cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
int order = directory == SIZE_MAX ? -1 : 1;
|
||||
if (file != SIZE_MAX && directory != SIZE_MAX &&
|
||||
!cif_compare(c, o->files[file].path, o->directories[directory].path, &order)) {
|
||||
return false;
|
||||
}
|
||||
if (order < 0) {
|
||||
if (!cif_file_entry(c, parent, file)) {
|
||||
return false;
|
||||
}
|
||||
file = o->file_next[file];
|
||||
} else {
|
||||
if (!cif_directory_entry(c, parent, directory)) {
|
||||
return false;
|
||||
}
|
||||
directory = o->directories[directory].next;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_excluded_files(cif_context *c, size_t directory) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
const cif_directory *d = &o->directories[directory];
|
||||
const cif_directory *excluded = &o->directories[d->excluded];
|
||||
for (size_t file = d->first_file; file != SIZE_MAX; file = o->file_next[file]) {
|
||||
c->file_index = file;
|
||||
if (!cif_event(c) || !cif_bytes(c, 256)) {
|
||||
return false;
|
||||
}
|
||||
o->rows[file].disposition = CBM_INVENTORY_FILTER_EXCLUDED_SUBTREE;
|
||||
o->rows[file].reason = excluded->reason;
|
||||
o->rows[file].excluded_ancestor = excluded->path;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_final_ledgers(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
c->file_index = SIZE_MAX;
|
||||
/* Git rows were appended in canonical directory order; CBM kind follows. */
|
||||
if (!cif_copy(c, &o->controls[o->view.control_count], &o->cbm_control,
|
||||
sizeof(o->cbm_control))) {
|
||||
return false;
|
||||
}
|
||||
o->view.control_count++;
|
||||
for (size_t i = 1; i < o->view.directory_count; i++) {
|
||||
if (!cif_event(c) || !cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
if (o->directories[i].excluded == i) {
|
||||
o->exclusions[o->view.excluded_count++] = o->directories[i].path;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_filter(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
for (size_t i = 0; i < o->view.directory_count; i++) {
|
||||
c->file_index = SIZE_MAX;
|
||||
if (!cif_event(c) || !cif_bytes(c, 256)) {
|
||||
return false;
|
||||
}
|
||||
cif_directory *d = &o->directories[i];
|
||||
if (d->parent != SIZE_MAX && o->directories[d->parent].excluded != SIZE_MAX) {
|
||||
d->excluded = o->directories[d->parent].excluded;
|
||||
}
|
||||
if (d->excluded != SIZE_MAX) {
|
||||
if (!cif_excluded_files(c, i)) {
|
||||
return false;
|
||||
}
|
||||
} else if (!cif_load_controls(c, i) || !cif_entries(c, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return cif_final_ledgers(c);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/* DRAFT internal pre-language inventory filter; parent review required.
|
||||
* Proposed destination: src/discover/inventory_filter.h.
|
||||
* No language classification, full-discovery success, pipeline or admission API.
|
||||
* Shared inventory data types can be included by the later inventory.h rather
|
||||
* than redeclared. Normative behavior is in contract.txt.
|
||||
*/
|
||||
#ifndef CBM_DISCOVER_INVENTORY_FILTER_H
|
||||
#define CBM_DISCOVER_INVENTORY_FILTER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
CBM_INVENTORY_OK = 0,
|
||||
CBM_INVENTORY_INVALID,
|
||||
CBM_INVENTORY_STATE,
|
||||
CBM_INVENTORY_CHANGED,
|
||||
CBM_INVENTORY_UNSUPPORTED,
|
||||
CBM_INVENTORY_LIMIT,
|
||||
CBM_INVENTORY_OOM,
|
||||
CBM_INVENTORY_IO,
|
||||
CBM_INVENTORY_CANCELLED,
|
||||
CBM_INVENTORY_DEADLINE
|
||||
} cbm_inventory_status_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_inventory_status_t status;
|
||||
size_t file_index; /* SIZE_MAX unless a particular input row is known. */
|
||||
bool cleanup_required; /* Independent provider cleanup obligation. */
|
||||
char diagnostic[256]; /* Owned bounded generic text, no input contents. */
|
||||
} cbm_inventory_error_t;
|
||||
|
||||
typedef struct {
|
||||
const unsigned char *data;
|
||||
size_t length;
|
||||
} cbm_inventory_path_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_inventory_path_t path; /* data[length]==0; no earlier NUL. */
|
||||
uint32_t git_mode; /* 0100644 or 0100755. */
|
||||
char oid[65]; /* First NUL at 40 or 64; preceding lowercase hex only. */
|
||||
uint64_t content_length;
|
||||
unsigned char content_sha256[32];
|
||||
} cbm_inventory_file_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t deadline_ms; /* Positive absolute cbm_now_ms deadline. */
|
||||
bool (*cancelled)(void *context); /* Optional prompt, pure, non-reentrant. */
|
||||
void *context; /* Borrowed only until prepare returns. */
|
||||
} cbm_inventory_control_t;
|
||||
|
||||
typedef struct {
|
||||
size_t max_files; /* Positive, <=INT_MAX; complete inventory, not survivors. */
|
||||
size_t max_directories; /* Positive, <=INT_MAX; root plus proper prefixes. */
|
||||
size_t max_arena_bytes; /* Total cumulative logical requests/reservations. */
|
||||
size_t max_ignore_arena_bytes; /* Positive reserved slice <= total. */
|
||||
size_t max_control_file_bytes;
|
||||
uint64_t max_control_total_bytes;
|
||||
size_t max_ignore_patterns; /* Positive, <=INT_MAX. */
|
||||
size_t max_probe_prefix_bytes; /* Copied/validated; unused before language. */
|
||||
uint64_t max_ignore_work; /* Exclusive checked parse+match allowance. */
|
||||
uint64_t max_verified_file_reads;
|
||||
uint64_t max_verified_content_bytes;
|
||||
} cbm_inventory_limits_t;
|
||||
|
||||
/* Contractual full-file verification followed by min(capacity,length) exact
|
||||
* prefix bytes. OK publishes copied; error publishes copied==0. No terminator.
|
||||
* Serial; all inputs borrowed. An error may require caller-owned native cleanup.
|
||||
* This callback type cannot authenticate a synthetic provider.
|
||||
*/
|
||||
typedef cbm_inventory_status_t (*cbm_inventory_read_fn)(void *context, size_t file_index,
|
||||
unsigned char *prefix, size_t capacity,
|
||||
size_t *copied,
|
||||
const cbm_inventory_control_t *control,
|
||||
cbm_inventory_error_t *error);
|
||||
|
||||
typedef struct {
|
||||
const char *native_root;
|
||||
const cbm_inventory_file_t *files;
|
||||
size_t file_count;
|
||||
unsigned char manifest_sha256[32]; /* Copied opaque binding, not authenticated. */
|
||||
cbm_inventory_read_fn read;
|
||||
void *read_context;
|
||||
} cbm_inventory_source_t;
|
||||
|
||||
/* Separate internal type: never cast to the future final disposition enum. */
|
||||
typedef enum {
|
||||
CBM_INVENTORY_FILTER_NEEDS_LANGUAGE = 0,
|
||||
CBM_INVENTORY_FILTER_IGNORED_FILE,
|
||||
CBM_INVENTORY_FILTER_EXCLUDED_SUBTREE
|
||||
} cbm_inventory_filter_disposition_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_INVENTORY_FILTER_REASON_NONE = 0,
|
||||
CBM_INVENTORY_FILTER_REASON_DIRECTORY_BUILTIN,
|
||||
CBM_INVENTORY_FILTER_REASON_DIRECTORY_SUFFIX,
|
||||
CBM_INVENTORY_FILTER_REASON_GITIGNORE,
|
||||
CBM_INVENTORY_FILTER_REASON_CBMIGNORE,
|
||||
CBM_INVENTORY_FILTER_REASON_IGNORED_SUFFIX,
|
||||
CBM_INVENTORY_FILTER_REASON_SKIP_LIST,
|
||||
CBM_INVENTORY_FILTER_REASON_FAST_PATTERN
|
||||
} cbm_inventory_filter_reason_t;
|
||||
|
||||
enum {
|
||||
CBM_INVENTORY_FILTER_ROLE_GITIGNORE = 1u << 0,
|
||||
CBM_INVENTORY_FILTER_ROLE_CBMIGNORE = 1u << 1,
|
||||
CBM_INVENTORY_FILTER_ROLE_PHYSICAL_PROJECT_CONFIG = 1u << 2
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
size_t file_index;
|
||||
cbm_inventory_filter_disposition_t disposition;
|
||||
cbm_inventory_filter_reason_t reason;
|
||||
unsigned roles;
|
||||
cbm_inventory_path_t excluded_ancestor; /* Otherwise {NULL,0}. */
|
||||
} cbm_inventory_filter_row_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_INVENTORY_CONTROL_GITIGNORE = 0,
|
||||
CBM_INVENTORY_CONTROL_CBMIGNORE
|
||||
} cbm_inventory_control_kind_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_INVENTORY_CONTROL_ABSENT = 0,
|
||||
CBM_INVENTORY_CONTROL_APPLIED_EMPTY,
|
||||
CBM_INVENTORY_CONTROL_APPLIED
|
||||
} cbm_inventory_control_outcome_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_inventory_control_kind_t kind;
|
||||
cbm_inventory_path_t directory; /* Empty root or reached directory. */
|
||||
size_t file_index; /* SIZE_MAX iff ABSENT. */
|
||||
cbm_inventory_control_outcome_t outcome;
|
||||
size_t effective_patterns;
|
||||
} cbm_inventory_control_row_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t verified_file_reads_reserved;
|
||||
uint64_t verified_content_bytes_reserved;
|
||||
uint64_t control_bytes_reserved;
|
||||
uint64_t ignore_bytes_reserved;
|
||||
uint64_t ignore_work_used;
|
||||
size_t ignore_patterns_reserved;
|
||||
size_t ignore_arena_requested_bytes;
|
||||
size_t non_ignore_arena_requested_bytes;
|
||||
size_t arena_requested_bytes; /* D+C actual logical requests. */
|
||||
size_t arena_budget_used_bytes; /* D+I unavailable quota. */
|
||||
} cbm_inventory_filter_usage_t;
|
||||
|
||||
typedef struct cbm_inventory_filter cbm_inventory_filter_t;
|
||||
|
||||
typedef struct {
|
||||
const char *native_root;
|
||||
const cbm_inventory_file_t *files;
|
||||
const cbm_inventory_filter_row_t *rows;
|
||||
size_t file_count; /* Exactly the complete input count. */
|
||||
size_t directory_count; /* Includes excluded directories and root. */
|
||||
const cbm_inventory_control_row_t *controls;
|
||||
size_t control_count;
|
||||
const cbm_inventory_path_t *excluded_directories; /* Minimal roots only. */
|
||||
size_t excluded_count;
|
||||
unsigned char manifest_sha256[32];
|
||||
} cbm_inventory_filter_view_t;
|
||||
|
||||
/* Synchronous FULL/no-size-skip pre-language pass. Clears out/error first.
|
||||
* Required source/limits/control/out; read callback required even for empty input.
|
||||
* No config/global/Git/directory enumeration/stat/non-control content read.
|
||||
* Success owns all returned metadata/rows/strings and one checked-ignore owner;
|
||||
* retains neither callbacks nor caller contexts. Failure: out==NULL, no partial
|
||||
* view. Native cleanup remains the provider caller's responsibility.
|
||||
*/
|
||||
cbm_inventory_status_t cbm_inventory_filter_prepare(const cbm_inventory_source_t *source,
|
||||
const cbm_inventory_limits_t *limits,
|
||||
const cbm_inventory_control_t *control,
|
||||
cbm_inventory_filter_t **out,
|
||||
cbm_inventory_error_t *error);
|
||||
|
||||
/* Borrowed read-only view until free; NULL for NULL. No IO/poll/allocation. */
|
||||
const cbm_inventory_filter_view_t *cbm_inventory_filter_view(const cbm_inventory_filter_t *owner);
|
||||
|
||||
/* Clears out; false for NULL owner/output. Copies usage or original limits.
|
||||
* These are diagnostics, never a capability to replenish/reset a live owner.
|
||||
*/
|
||||
bool cbm_inventory_filter_usage(const cbm_inventory_filter_t *owner,
|
||||
cbm_inventory_filter_usage_t *out);
|
||||
bool cbm_inventory_filter_limits(const cbm_inventory_filter_t *owner, cbm_inventory_limits_t *out);
|
||||
|
||||
/* NULL-safe; frees checked owner before the containing arena; no callback/IO. */
|
||||
void cbm_inventory_filter_free(cbm_inventory_filter_t *owner);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,172 @@
|
||||
#include "discover/inventory_internal.h"
|
||||
#include "foundation/platform.h"
|
||||
#include <string.h>
|
||||
|
||||
bool cif_fail(cif_context *c, cbm_inventory_status_t status) {
|
||||
if (c->error.status == CBM_INVENTORY_OK) {
|
||||
c->error.status = status;
|
||||
c->error.file_index = c->file_index;
|
||||
strcpy(c->error.diagnostic, "inventory filter could not complete");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool cif_poll(cif_context *c) {
|
||||
if (c->error.status != CBM_INVENTORY_OK) {
|
||||
return false;
|
||||
}
|
||||
if (c->control->cancelled && c->control->cancelled(c->control->context)) {
|
||||
return cif_fail(c, CBM_INVENTORY_CANCELLED);
|
||||
}
|
||||
if (cbm_now_ms() >= c->control->deadline_ms) {
|
||||
return cif_fail(c, CBM_INVENTORY_DEADLINE);
|
||||
}
|
||||
c->byte_gap = CIF_BYTE_GAP;
|
||||
c->event_gap = CIF_EVENT_GAP;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_bytes(cif_context *c, size_t count) {
|
||||
if (c->error.status != CBM_INVENTORY_OK) {
|
||||
return false;
|
||||
}
|
||||
if (count > CIF_BYTE_GAP) {
|
||||
return cif_fail(c, CBM_INVENTORY_STATE);
|
||||
}
|
||||
if (count > c->byte_gap && !cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
c->byte_gap -= count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_event(cif_context *c) {
|
||||
if (c->error.status != CBM_INVENTORY_OK || (!c->event_gap && !cif_poll(c))) {
|
||||
return false;
|
||||
}
|
||||
c->event_gap--;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_copy(cif_context *c, void *destination, const void *source, size_t length) {
|
||||
unsigned char *out = destination;
|
||||
const unsigned char *in = source;
|
||||
while (length) {
|
||||
size_t n = length < CIF_COPY_CHUNK ? length : CIF_COPY_CHUNK;
|
||||
if (!cif_bytes(c, n * 2)) {
|
||||
return false;
|
||||
}
|
||||
if (in) {
|
||||
memcpy(out, in, n);
|
||||
in += n;
|
||||
} else {
|
||||
memset(out, 0, n);
|
||||
}
|
||||
out += n;
|
||||
length -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Guard arithmetic also used internally by the existing arena allocator. */
|
||||
static bool cif_arena_room(cif_context *c, size_t n) {
|
||||
CBMArena *a = &c->owner->arena;
|
||||
if (n > SIZE_MAX - 7) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
size_t aligned = (n + 7) & ~(size_t)7;
|
||||
if (a->used > SIZE_MAX - aligned || a->total_alloc > SIZE_MAX - aligned) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
if (!a->nblocks || a->used + aligned > a->block_size) {
|
||||
size_t next = a->grow_size > aligned ? a->grow_size : aligned;
|
||||
size_t tail = a->block_size > a->used ? a->block_size - a->used : 0;
|
||||
if (next > SIZE_MAX / 2 || a->waste_tail > SIZE_MAX - tail) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void *cif_alloc(cif_context *c, size_t count, size_t width) {
|
||||
if (!count || !width) {
|
||||
return NULL;
|
||||
}
|
||||
if (count > SIZE_MAX / width) {
|
||||
cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
return NULL;
|
||||
}
|
||||
size_t n = count * width;
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t available = o->limits.max_arena_bytes - o->limits.max_ignore_arena_bytes;
|
||||
if (n > available - o->usage.non_ignore_arena_requested_bytes) {
|
||||
cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
return NULL;
|
||||
}
|
||||
if (!cif_poll(c) || !cif_arena_room(c, n)) {
|
||||
return NULL;
|
||||
}
|
||||
o->usage.non_ignore_arena_requested_bytes += n;
|
||||
void *result = cbm_arena_alloc(&o->arena, n);
|
||||
if (!result) {
|
||||
cif_fail(c, CBM_INVENTORY_OOM);
|
||||
return NULL;
|
||||
}
|
||||
if (!cif_poll(c) || !cif_copy(c, result, NULL, n)) {
|
||||
return NULL;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
unsigned char *cif_string(cif_context *c, const unsigned char *source, size_t length) {
|
||||
if (length == SIZE_MAX) {
|
||||
cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
return NULL;
|
||||
}
|
||||
unsigned char *result = cif_alloc(c, length + 1, 1);
|
||||
return result && cif_copy(c, result, source, length) ? result : NULL;
|
||||
}
|
||||
|
||||
bool cif_compare(cif_context *c, cbm_inventory_path_t a, cbm_inventory_path_t b, int *result) {
|
||||
if (!cif_event(c)) {
|
||||
return false;
|
||||
}
|
||||
size_t n = a.length < b.length ? a.length : b.length;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!cif_bytes(c, 2)) {
|
||||
return false;
|
||||
}
|
||||
unsigned char left = a.data[i], right = b.data[i];
|
||||
if (left != right) {
|
||||
*result = left < right ? -1 : 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
*result = a.length < b.length ? -1 : a.length > b.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_checked(cif_context *c, cbm_ignore_checked_status_t status) {
|
||||
switch (status) {
|
||||
case CBM_IGNORE_CHECKED_OK:
|
||||
return true;
|
||||
case CBM_IGNORE_CHECKED_INVALID:
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
case CBM_IGNORE_CHECKED_UNSUPPORTED:
|
||||
return cif_fail(c, CBM_INVENTORY_UNSUPPORTED);
|
||||
case CBM_IGNORE_CHECKED_LIMIT:
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
case CBM_IGNORE_CHECKED_OOM:
|
||||
return cif_fail(c, CBM_INVENTORY_OOM);
|
||||
case CBM_IGNORE_CHECKED_CANCELLED:
|
||||
return cif_fail(c, CBM_INVENTORY_CANCELLED);
|
||||
case CBM_IGNORE_CHECKED_DEADLINE:
|
||||
return cif_fail(c, CBM_INVENTORY_DEADLINE);
|
||||
}
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
|
||||
cbm_ignore_checked_control_t cif_ignore_control(const cif_context *c) {
|
||||
return (cbm_ignore_checked_control_t){c->control->deadline_ms, c->control->cancelled,
|
||||
c->control->context};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef CBM_INVENTORY_INTERNAL_H
|
||||
#define CBM_INVENTORY_INTERNAL_H
|
||||
|
||||
#include "discover/inventory_filter.h"
|
||||
#include "discover/gitignore_checked.h"
|
||||
#include "foundation/arena.h"
|
||||
|
||||
enum { CIF_PATH_MAX = 4095, CIF_BYTE_GAP = 65536, CIF_EVENT_GAP = 4096, CIF_COPY_CHUNK = 4096 };
|
||||
|
||||
typedef struct {
|
||||
cbm_inventory_path_t path;
|
||||
size_t parent, next, first_directory, last_directory, first_file, last_file;
|
||||
size_t excluded;
|
||||
cbm_inventory_filter_reason_t reason;
|
||||
const cbm_ignore_program_t *program;
|
||||
} cif_directory;
|
||||
|
||||
struct cbm_inventory_filter {
|
||||
CBMArena arena;
|
||||
cbm_inventory_limits_t limits;
|
||||
uint64_t deadline_ms;
|
||||
cbm_inventory_filter_usage_t usage;
|
||||
cbm_inventory_filter_view_t view;
|
||||
cbm_inventory_file_t *files;
|
||||
cbm_inventory_filter_row_t *rows;
|
||||
cif_directory *directories;
|
||||
size_t *file_next;
|
||||
cbm_inventory_control_row_t *controls;
|
||||
cbm_inventory_control_row_t cbm_control;
|
||||
cbm_inventory_path_t *exclusions;
|
||||
cbm_ignore_checked_t *ignore;
|
||||
const cbm_ignore_program_t *cbm_program;
|
||||
unsigned char *scratch;
|
||||
size_t scratch_capacity;
|
||||
};
|
||||
|
||||
/* Only this synchronous stack context borrows caller functions and contexts. */
|
||||
typedef struct {
|
||||
cbm_inventory_filter_t *owner;
|
||||
const cbm_inventory_source_t *source;
|
||||
const cbm_inventory_control_t *control;
|
||||
cbm_inventory_error_t error;
|
||||
size_t file_index, byte_gap, event_gap;
|
||||
} cif_context;
|
||||
|
||||
bool cif_fail(cif_context *c, cbm_inventory_status_t status);
|
||||
bool cif_poll(cif_context *c);
|
||||
bool cif_bytes(cif_context *c, size_t count);
|
||||
bool cif_event(cif_context *c);
|
||||
bool cif_copy(cif_context *c, void *destination, const void *source, size_t length);
|
||||
void *cif_alloc(cif_context *c, size_t count, size_t width);
|
||||
unsigned char *cif_string(cif_context *c, const unsigned char *source, size_t length);
|
||||
bool cif_compare(cif_context *c, cbm_inventory_path_t a, cbm_inventory_path_t b, int *result);
|
||||
bool cif_checked(cif_context *c, cbm_ignore_checked_status_t status);
|
||||
cbm_ignore_checked_control_t cif_ignore_control(const cif_context *c);
|
||||
bool cif_metadata(cif_context *c);
|
||||
bool cif_directories(cif_context *c);
|
||||
bool cif_find_file(cif_context *c, cbm_inventory_path_t path, size_t *index);
|
||||
bool cif_find_directory(cif_context *c, cbm_inventory_path_t path, size_t *index);
|
||||
bool cif_load_controls(cif_context *c, size_t directory);
|
||||
bool cif_filter(cif_context *c);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef CBM_INVENTORY_LIMITS_INTERNAL_H
|
||||
#define CBM_INVENTORY_LIMITS_INTERNAL_H
|
||||
|
||||
#include "discover/inventory_filter.h"
|
||||
|
||||
/* Shared Boolean configuration check; arithmetic exhaustion is checked separately. */
|
||||
bool cif_limits_valid(const cbm_inventory_limits_t *limits);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "discover/inventory_internal.h"
|
||||
#include "foundation/path_syntax_internal.h"
|
||||
#include <limits.h>
|
||||
|
||||
static bool cif_separator(unsigned char ch) {
|
||||
#ifdef _WIN32
|
||||
return ch == '/' || ch == '\\';
|
||||
#else
|
||||
return ch == '/';
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static bool cif_windows_encoding(cif_context *c, const char *path, size_t length) {
|
||||
wchar_t wide[CIF_PATH_MAX + 1];
|
||||
char roundtrip[CIF_PATH_MAX + 1];
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
cbm_path_syntax_result_t s = cbm_path_windows_roundtrip(path, length, wide, CIF_PATH_MAX + 1,
|
||||
roundtrip, sizeof(roundtrip));
|
||||
if (s != CBM_PATH_SYNTAX_OK) {
|
||||
return cif_fail(c, CBM_INVENTORY_UNSUPPORTED);
|
||||
}
|
||||
return cif_poll(c);
|
||||
}
|
||||
|
||||
static bool cif_device_root(const unsigned char *root, size_t n) {
|
||||
if (n < 4) {
|
||||
return false;
|
||||
}
|
||||
return (cif_separator(root[0]) && cif_separator(root[1]) &&
|
||||
(root[2] == '?' || root[2] == '.') && cif_separator(root[3])) ||
|
||||
(root[0] == '\\' && root[1] == '?' && root[2] == '?' && root[3] == '\\');
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool cif_root(cif_context *c, size_t *length) {
|
||||
const unsigned char *root = (const unsigned char *)c->source->native_root;
|
||||
size_t n = 0;
|
||||
for (; n <= CIF_PATH_MAX; n++) {
|
||||
if (!cif_bytes(c, 1)) {
|
||||
return false;
|
||||
}
|
||||
if (!root[n]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!n) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
if (n > CIF_PATH_MAX) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (cif_device_root(root, n)) {
|
||||
return cif_fail(c, CBM_INVENTORY_UNSUPPORTED);
|
||||
}
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
if (cbm_path_windows_absolute((const char *)root, n) != CBM_PATH_SYNTAX_OK) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
if (!cif_poll(c) || !cif_windows_encoding(c, (const char *)root, n)) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (root[0] != '/') {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
#endif
|
||||
*length = n;
|
||||
c->owner->view.native_root = (const char *)cif_string(c, root, n);
|
||||
return c->owner->view.native_root != NULL;
|
||||
}
|
||||
|
||||
static bool cif_component(cif_context *c, const unsigned char *p, size_t n) {
|
||||
if (!n || (n == 1 && p[0] == '.') || (n == 2 && p[0] == '.' && p[1] == '.')) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
if (cbm_path_windows_component(p, n) != CBM_PATH_SYNTAX_OK) {
|
||||
return cif_fail(c, CBM_INVENTORY_UNSUPPORTED);
|
||||
}
|
||||
return cif_poll(c);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool cif_file_path(cif_context *c, cbm_inventory_path_t path, size_t root_length) {
|
||||
if (path.length > CIF_PATH_MAX) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
if (!path.data || !path.length) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
size_t separator =
|
||||
cif_separator((unsigned char)c->owner->view.native_root[root_length - 1]) ? 0 : 1;
|
||||
if (path.length > CIF_PATH_MAX - root_length ||
|
||||
separator > CIF_PATH_MAX - root_length - path.length) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= path.length; i++) {
|
||||
if (!cif_bytes(c, 1)) {
|
||||
return false;
|
||||
}
|
||||
unsigned char ch = path.data[i];
|
||||
if ((ch == 0) != (i == path.length)) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
if (!ch || ch == '/') {
|
||||
if (!cif_component(c, path.data + start, i - start)) {
|
||||
return false;
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
return cif_windows_encoding(c, (const char *)path.data, path.length);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool cif_oid(cif_context *c, const char oid[65], size_t *width) {
|
||||
size_t n = 0;
|
||||
while (n < 65) {
|
||||
if (!cif_bytes(c, 1)) {
|
||||
return false;
|
||||
}
|
||||
unsigned char ch = (unsigned char)oid[n];
|
||||
if (!ch) {
|
||||
break;
|
||||
}
|
||||
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'))) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
if ((n != 40 && n != 64) || (*width && *width != n)) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
*width = n;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cif_file(cif_context *c, size_t i, size_t root_length, size_t *width) {
|
||||
const cbm_inventory_file_t *in = &c->source->files[i];
|
||||
cbm_inventory_file_t *out = &c->owner->files[i];
|
||||
c->file_index = i;
|
||||
if (!cif_event(c) || !cif_bytes(c, 256) || !cif_file_path(c, in->path, root_length)) {
|
||||
return false;
|
||||
}
|
||||
if (i) {
|
||||
int order = 0;
|
||||
if (!cif_compare(c, c->owner->files[i - 1].path, in->path, &order)) {
|
||||
return false;
|
||||
}
|
||||
if (order >= 0) {
|
||||
return cif_fail(c, CBM_INVENTORY_INVALID);
|
||||
}
|
||||
}
|
||||
if (in->git_mode != 0100644 && in->git_mode != 0100755) {
|
||||
return cif_fail(c, CBM_INVENTORY_UNSUPPORTED);
|
||||
}
|
||||
if (in->content_length > INT64_MAX) {
|
||||
return cif_fail(c, CBM_INVENTORY_LIMIT);
|
||||
}
|
||||
if (!cif_oid(c, in->oid, width)) {
|
||||
return false;
|
||||
}
|
||||
out->path.data = cif_string(c, in->path.data, in->path.length);
|
||||
if (!out->path.data || !cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
out->path.length = in->path.length;
|
||||
out->git_mode = in->git_mode;
|
||||
out->content_length = in->content_length;
|
||||
if (!out->path.data || !cif_copy(c, out->oid, in->oid, *width + 1) ||
|
||||
!cif_copy(c, out->content_sha256, in->content_sha256, 32)) {
|
||||
return false;
|
||||
}
|
||||
cbm_inventory_filter_row_t *row = &c->owner->rows[i];
|
||||
row->file_index = i;
|
||||
row->disposition = CBM_INVENTORY_FILTER_NEEDS_LANGUAGE;
|
||||
static const unsigned char config[] = ".codebase-memory.json";
|
||||
int order = 0;
|
||||
if (!cif_compare(c, out->path, (cbm_inventory_path_t){config, sizeof(config) - 1}, &order)) {
|
||||
return false;
|
||||
}
|
||||
if (!cif_bytes(c, 128)) {
|
||||
return false;
|
||||
}
|
||||
if (!order) {
|
||||
row->roles = CBM_INVENTORY_FILTER_ROLE_PHYSICAL_PROJECT_CONFIG;
|
||||
}
|
||||
c->owner->file_next[i] = SIZE_MAX;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cif_metadata(cif_context *c) {
|
||||
cbm_inventory_filter_t *o = c->owner;
|
||||
size_t root_length = 0, width = 0;
|
||||
if (!cif_root(c, &root_length)) {
|
||||
return false;
|
||||
}
|
||||
size_t n = c->source->file_count;
|
||||
o->view.file_count = n;
|
||||
o->files = cif_alloc(c, n, sizeof(*o->files));
|
||||
o->rows = cif_alloc(c, n, sizeof(*o->rows));
|
||||
o->file_next = cif_alloc(c, n, sizeof(*o->file_next));
|
||||
if ((n && (!o->files || !o->rows || !o->file_next)) ||
|
||||
!cif_copy(c, o->view.manifest_sha256, c->source->manifest_sha256, 32)) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!cif_file(c, i, root_length, &width)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
c->file_index = SIZE_MAX;
|
||||
return true;
|
||||
}
|
||||
+291
-126
@@ -122,11 +122,16 @@ static const ext_entry_t EXT_TABLE[] = {
|
||||
{".frm", CBM_LANG_FORM},
|
||||
{".prc", CBM_LANG_FORM},
|
||||
|
||||
/* Fortran */
|
||||
/* Fortran. Upper-case suffixes are the C-preprocessed spellings of the
|
||||
* same free-form sources (#ifdef blocks); lookup is case-sensitive. */
|
||||
{".f03", CBM_LANG_FORTRAN},
|
||||
{".f08", CBM_LANG_FORTRAN},
|
||||
{".f90", CBM_LANG_FORTRAN},
|
||||
{".f95", CBM_LANG_FORTRAN},
|
||||
{".F03", CBM_LANG_FORTRAN},
|
||||
{".F08", CBM_LANG_FORTRAN},
|
||||
{".F90", CBM_LANG_FORTRAN},
|
||||
{".F95", CBM_LANG_FORTRAN},
|
||||
|
||||
/* GLSL */
|
||||
{".frag", CBM_LANG_GLSL},
|
||||
@@ -918,13 +923,12 @@ static const char *LANG_NAMES[CBM_LANG_COUNT] = {
|
||||
|
||||
/* ── Public API ──────────────────────────────────────────────────── */
|
||||
|
||||
CBMLanguage cbm_language_for_extension(const char *ext) {
|
||||
static CBMLanguage lang_for_extension(const cbm_userconfig_t *ucfg, const char *ext) {
|
||||
if (!ext || !ext[0]) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
|
||||
/* Check user-defined overrides first */
|
||||
const cbm_userconfig_t *ucfg = cbm_get_user_lang_config();
|
||||
if (ucfg) {
|
||||
CBMLanguage ulang = cbm_userconfig_lookup(ucfg, ext);
|
||||
if (ulang != CBM_LANG_COUNT) {
|
||||
@@ -940,7 +944,11 @@ CBMLanguage cbm_language_for_extension(const char *ext) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_for_filename(const char *filename) {
|
||||
CBMLanguage cbm_language_for_extension(const char *ext) {
|
||||
return lang_for_extension(cbm_get_user_lang_config(), ext);
|
||||
}
|
||||
|
||||
static CBMLanguage lang_for_filename(const cbm_userconfig_t *ucfg, const char *filename) {
|
||||
if (!filename || !filename[0]) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
@@ -980,7 +988,6 @@ CBMLanguage cbm_language_for_filename(const char *filename) {
|
||||
} COMPOUND_EXT_TABLE[] = {
|
||||
{".blade.php", CBM_LANG_BLADE},
|
||||
};
|
||||
const cbm_userconfig_t *ucfg = cbm_get_user_lang_config();
|
||||
const char *p = strchr(filename, '.');
|
||||
while (p && p < last_dot) {
|
||||
for (size_t i = 0; i < sizeof(COMPOUND_EXT_TABLE) / sizeof(COMPOUND_EXT_TABLE[0]); i++) {
|
||||
@@ -998,7 +1005,11 @@ CBMLanguage cbm_language_for_filename(const char *filename) {
|
||||
}
|
||||
|
||||
/* Standard single-extension lookup (built-ins + user overrides). */
|
||||
return cbm_language_for_extension(last_dot);
|
||||
return lang_for_extension(ucfg, last_dot);
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_for_filename(const char *filename) {
|
||||
return lang_for_filename(cbm_get_user_lang_config(), filename);
|
||||
}
|
||||
|
||||
const char *cbm_language_name(CBMLanguage lang) {
|
||||
@@ -1089,45 +1100,61 @@ static char *shebang_next_token(char **cursor) {
|
||||
return start;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_from_shebang(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
/* ── Content probes ────────────────────────────────────────────────
|
||||
* A few names do not decide a language alone; the file's first bytes do. Every
|
||||
* probe is a function of those bytes only, so discovery (which reads files)
|
||||
* and the pinned test-impact inventory (which reads git blobs) give a file the
|
||||
* same language. The bytes are raw on every platform: a Windows text-mode read
|
||||
* dropped carriage returns and stopped at a Ctrl-Z, which made the answer
|
||||
* depend on the host. */
|
||||
enum {
|
||||
LANG_PROBE_LINE = 255, /* a shebang first line; the ObjectScript export marker */
|
||||
LANG_PROBE_HEAD = CBM_SZ_4K, /* .m .cls .inc .frm .res */
|
||||
LANG_PROBE_CFC = CBM_SZ_16K, /* .cfc: a long license block may precede the first tag */
|
||||
};
|
||||
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
/* The first min(n, cap) bytes as a C string in dst[cap + 1]. The probes are
|
||||
* string scans, so an embedded NUL ends what they see, as it always did. */
|
||||
static void lang_head_text(char *dst, size_t cap, const unsigned char *head, size_t n) {
|
||||
size_t len = n < cap ? n : cap;
|
||||
if (len > 0) {
|
||||
memcpy(dst, head, len);
|
||||
}
|
||||
dst[len] = '\0';
|
||||
}
|
||||
|
||||
/* The first `cap` bytes of a file, raw. false when the file cannot be opened.
|
||||
* A read error keeps the bytes read before it, as the probes always did. */
|
||||
static bool lang_read_head(const char *path, unsigned char *buf, size_t cap, size_t *n) {
|
||||
*n = 0;
|
||||
FILE *f = path ? cbm_fopen(path, "rb") : NULL;
|
||||
if (!f) {
|
||||
return CBM_LANG_COUNT; /* fail closed on read error */
|
||||
return false;
|
||||
}
|
||||
*n = fread(buf, SKIP_ONE, cap, f);
|
||||
(void)fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Read only a bounded first line. */
|
||||
char buf[CBM_SZ_256];
|
||||
size_t n = fread(buf, SKIP_ONE, sizeof(buf) - SKIP_ONE, f);
|
||||
|
||||
/* Fail closed on any read error rather than parsing a partial buffer. */
|
||||
if (ferror(f)) {
|
||||
(void)fclose(f);
|
||||
/* `more`: the file has bytes beyond head[0..n). A first line that fills the
|
||||
* probe and goes on is cut, and a cut line names no interpreter. */
|
||||
static CBMLanguage lang_shebang(const unsigned char *head, size_t n, bool more) {
|
||||
if (n > LANG_PROBE_LINE) {
|
||||
n = LANG_PROBE_LINE;
|
||||
more = true;
|
||||
}
|
||||
if (n < PAIR_LEN) {
|
||||
return CBM_LANG_COUNT; /* too short to begin with "#!" */
|
||||
}
|
||||
char buf[LANG_PROBE_LINE + SKIP_ONE];
|
||||
memcpy(buf, head, n);
|
||||
bool have_newline = (memchr(buf, '\n', n) != NULL);
|
||||
if (!have_newline && n == LANG_PROBE_LINE && more) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
|
||||
/* If the bounded buffer filled without containing a newline, the first
|
||||
* line may extend past our bound. Probe a single extra byte to tell an
|
||||
* exact EOF (the whole file is <= 255 bytes) from a truncated longer
|
||||
* line: any surviving byte -- including a newline just beyond the bound --
|
||||
* means the first line was cut off, so fail closed. A probe read error
|
||||
* fails closed too. This keeps the read bounded (no unbounded line read
|
||||
* or allocation). */
|
||||
bool have_newline = (memchr(buf, '\n', n) != NULL);
|
||||
if (!have_newline && n == sizeof(buf) - SKIP_ONE) {
|
||||
int probe = fgetc(f);
|
||||
if (probe != EOF || ferror(f)) {
|
||||
(void)fclose(f);
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
}
|
||||
(void)fclose(f);
|
||||
|
||||
/* Must begin with "#!". */
|
||||
if (n < PAIR_LEN || buf[0] != '#' || buf[1] != '!') {
|
||||
if (buf[0] != '#' || buf[1] != '!') {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
|
||||
@@ -1174,6 +1201,29 @@ CBMLanguage cbm_language_from_shebang(const char *path) {
|
||||
return lang_for_interpreter(base);
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_from_shebang(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
if (!f) {
|
||||
return CBM_LANG_COUNT; /* fail closed on read error */
|
||||
}
|
||||
|
||||
/* Read only a bounded first line, plus one byte to tell an exact end of
|
||||
* file from a longer first line. A read error fails closed rather than
|
||||
* parsing a partial buffer. */
|
||||
unsigned char buf[LANG_PROBE_LINE + SKIP_ONE];
|
||||
size_t n = fread(buf, SKIP_ONE, sizeof(buf), f);
|
||||
bool failed = ferror(f) != 0;
|
||||
(void)fclose(f);
|
||||
if (failed) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
return lang_shebang(buf, n > LANG_PROBE_LINE ? LANG_PROBE_LINE : n, n > LANG_PROBE_LINE);
|
||||
}
|
||||
|
||||
/* ── .m file disambiguation ──────────────────────────────────────── */
|
||||
|
||||
/* Simple substring search helper */
|
||||
@@ -1238,22 +1288,7 @@ static bool has_matlab_line_markers(const char *buf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_m(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_MATLAB;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "r");
|
||||
if (!f) {
|
||||
return CBM_LANG_MATLAB;
|
||||
}
|
||||
|
||||
/* Read first 4KB */
|
||||
char buf[CBM_SZ_4K + SKIP_ONE];
|
||||
size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f);
|
||||
buf[n] = '\0';
|
||||
(void)fclose(f);
|
||||
|
||||
static CBMLanguage lang_m_text(const char *buf) {
|
||||
if (has_objc_markers(buf)) {
|
||||
return CBM_LANG_OBJC;
|
||||
}
|
||||
@@ -1271,6 +1306,17 @@ CBMLanguage cbm_disambiguate_m(const char *path) {
|
||||
return CBM_LANG_MATLAB;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_m(const char *path) {
|
||||
unsigned char head[LANG_PROBE_HEAD];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_MATLAB;
|
||||
}
|
||||
char buf[LANG_PROBE_HEAD + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_HEAD, head, n);
|
||||
return lang_m_text(buf);
|
||||
}
|
||||
|
||||
/* Visual Basic 6 / VBA source exports are recognisable from their header (#721):
|
||||
* every module carries `Attribute VB_Name = "..."`, class modules open with
|
||||
* `VERSION 1.0 CLASS`, forms/controls with `VERSION 5.00` + `Begin VB.Form` /
|
||||
@@ -1286,21 +1332,7 @@ static bool has_vb6_markers(const char *buf) {
|
||||
* VB6 form is reported as unsupported (CBM_LANG_COUNT) rather than handed to
|
||||
* the FORM grammar, which yields no defs and stray junk nodes. Defaults to
|
||||
* FORM on any doubt (preserves existing behaviour). */
|
||||
CBMLanguage cbm_disambiguate_frm(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_FORM;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "r");
|
||||
if (!f) {
|
||||
return CBM_LANG_FORM;
|
||||
}
|
||||
|
||||
char buf[CBM_SZ_4K + SKIP_ONE];
|
||||
size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f);
|
||||
buf[n] = '\0';
|
||||
(void)fclose(f);
|
||||
|
||||
static CBMLanguage lang_frm_text(const char *buf) {
|
||||
/* VB6 form files open with "VERSION x.yy" on line 1. */
|
||||
if (strncmp(buf, "VERSION ", SLEN("VERSION ")) == 0 &&
|
||||
isdigit((unsigned char)buf[SLEN("VERSION ")])) {
|
||||
@@ -1309,25 +1341,34 @@ CBMLanguage cbm_disambiguate_frm(const char *path) {
|
||||
return has_vb6_markers(buf) ? CBM_LANG_COUNT : CBM_LANG_FORM;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_frm(const char *path) {
|
||||
unsigned char head[LANG_PROBE_HEAD];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_FORM;
|
||||
}
|
||||
char buf[LANG_PROBE_HEAD + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_HEAD, head, n);
|
||||
return lang_frm_text(buf);
|
||||
}
|
||||
|
||||
/* Disambiguate .res files (#2176): ReScript source shares the extension with
|
||||
* two binary formats -- Godot resources (RSRC/RSCC magic, the default save
|
||||
* format for imported meshes) and Windows compiled resource files. Binary
|
||||
* content has no ReScript meaning, and a NUL byte never occurs in ReScript
|
||||
* text while both binary formats carry NULs in their first bytes. */
|
||||
static CBMLanguage lang_res_bytes(const unsigned char *head, size_t n) {
|
||||
size_t len = n < LANG_PROBE_HEAD ? n : LANG_PROBE_HEAD;
|
||||
return (len > 0 && memchr(head, '\0', len)) ? CBM_LANG_COUNT : CBM_LANG_RESCRIPT;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_res(const char *path) {
|
||||
if (!path) {
|
||||
unsigned char head[LANG_PROBE_HEAD];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_RESCRIPT;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
if (!f) {
|
||||
return CBM_LANG_RESCRIPT;
|
||||
}
|
||||
|
||||
char buf[CBM_SZ_4K];
|
||||
size_t n = fread(buf, SKIP_ONE, sizeof(buf), f);
|
||||
(void)fclose(f);
|
||||
return memchr(buf, '\0', n) ? CBM_LANG_COUNT : CBM_LANG_RESCRIPT;
|
||||
return lang_res_bytes(head, n);
|
||||
}
|
||||
|
||||
/* Disambiguate .cls files: shared by InterSystems ObjectScript UDL, Salesforce
|
||||
@@ -1335,21 +1376,7 @@ CBMLanguage cbm_disambiguate_res(const char *path) {
|
||||
* with a line of the form "Class <UppercasePackage>..."; VB6 class modules
|
||||
* carry the VB6 header markers and are reported as unsupported (CBM_LANG_COUNT)
|
||||
* until a Visual Basic grammar exists. Defaults to Apex on any doubt. */
|
||||
CBMLanguage cbm_disambiguate_cls(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_APEX;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "r");
|
||||
if (!f) {
|
||||
return CBM_LANG_APEX;
|
||||
}
|
||||
|
||||
char buf[CBM_SZ_4K + SKIP_ONE];
|
||||
size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f);
|
||||
buf[n] = '\0';
|
||||
(void)fclose(f);
|
||||
|
||||
static CBMLanguage lang_cls_text(const char *buf) {
|
||||
if (has_vb6_markers(buf)) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
@@ -1369,6 +1396,17 @@ CBMLanguage cbm_disambiguate_cls(const char *path) {
|
||||
return CBM_LANG_APEX;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_cls(const char *path) {
|
||||
unsigned char head[LANG_PROBE_HEAD];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_APEX;
|
||||
}
|
||||
char buf[LANG_PROBE_HEAD + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_HEAD, head, n);
|
||||
return lang_cls_text(buf);
|
||||
}
|
||||
|
||||
/* Disambiguate .inc files: shared by BitBake include fragments and
|
||||
* InterSystems ObjectScript include (macro) files. ObjectScript .inc files are
|
||||
* predominantly macro definitions ("#define NAME ..." / "#def1arg NAME ...");
|
||||
@@ -1378,21 +1416,7 @@ CBMLanguage cbm_disambiguate_cls(const char *path) {
|
||||
* We therefore match ObjectScript preprocessor directives ('#' immediately
|
||||
* followed by 'def'/';'), which BitBake never produces. Defaults to BitBake on
|
||||
* any doubt (preserves existing behaviour). */
|
||||
CBMLanguage cbm_disambiguate_inc(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_BITBAKE;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "r");
|
||||
if (!f) {
|
||||
return CBM_LANG_BITBAKE;
|
||||
}
|
||||
|
||||
char buf[CBM_SZ_4K + SKIP_ONE];
|
||||
size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f);
|
||||
buf[n] = '\0';
|
||||
(void)fclose(f);
|
||||
|
||||
static CBMLanguage lang_inc_text(const char *buf) {
|
||||
const char *line = buf;
|
||||
while (*line) {
|
||||
/* ObjectScript include header: a line beginning "ROUTINE <Uppercase>". */
|
||||
@@ -1417,6 +1441,17 @@ CBMLanguage cbm_disambiguate_inc(const char *path) {
|
||||
return CBM_LANG_BITBAKE;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_inc(const char *path) {
|
||||
unsigned char head[LANG_PROBE_HEAD];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_BITBAKE;
|
||||
}
|
||||
char buf[LANG_PROBE_HEAD + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_HEAD, head, n);
|
||||
return lang_inc_text(buf);
|
||||
}
|
||||
|
||||
/* Case-insensitive prefix match (portable — no strncasecmp dependency). */
|
||||
static bool starts_with_ci(const char *s, const char *prefix) {
|
||||
for (; *prefix; s++, prefix++) {
|
||||
@@ -1442,23 +1477,7 @@ static bool starts_with_ci(const char *s, const char *prefix) {
|
||||
* - a different leading tag (e.g. <cfquery> in a bare-tag file) ⇒ cfml;
|
||||
* - anything else ("component { ... }") ⇒ cfscript.
|
||||
* Defaults to CBM_LANG_CFSCRIPT on any doubt (preserves table behaviour). */
|
||||
CBMLanguage cbm_disambiguate_cfc(const char *path) {
|
||||
if (!path) {
|
||||
return CBM_LANG_CFSCRIPT;
|
||||
}
|
||||
|
||||
FILE *f = cbm_fopen(path, "r");
|
||||
if (!f) {
|
||||
return CBM_LANG_CFSCRIPT;
|
||||
}
|
||||
|
||||
/* Read a generous head: tag components can carry a large license/revision
|
||||
* comment block before the <cfcomponent> opener. */
|
||||
char buf[CBM_SZ_16K + SKIP_ONE];
|
||||
size_t n = fread(buf, SKIP_ONE, CBM_SZ_16K, f);
|
||||
buf[n] = '\0';
|
||||
(void)fclose(f);
|
||||
|
||||
static CBMLanguage lang_cfc_text(const char *buf) {
|
||||
/* Rule 1: explicit tag-component markers ⇒ tag dialect. */
|
||||
if (cbm_strcasestr(buf, "<cfcomponent") != NULL || cbm_strcasestr(buf, "<cffunction") != NULL) {
|
||||
return CBM_LANG_CFML;
|
||||
@@ -1487,3 +1506,149 @@ CBMLanguage cbm_disambiguate_cfc(const char *path) {
|
||||
}
|
||||
return CBM_LANG_CFSCRIPT;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_cfc(const char *path) {
|
||||
unsigned char head[LANG_PROBE_CFC];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_CFSCRIPT;
|
||||
}
|
||||
char buf[LANG_PROBE_CFC + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_CFC, head, n);
|
||||
return lang_cfc_text(buf);
|
||||
}
|
||||
|
||||
/* ── One classifier for a name plus its first bytes ──────────────── */
|
||||
|
||||
/* JSON files that are configuration, not source worth indexing. */
|
||||
static const char *const IGNORED_JSON_FILES[] = {
|
||||
"package.json", "package-lock.json", "tsconfig.json",
|
||||
"jsconfig.json", "composer.json", "composer.lock",
|
||||
"yarn.lock", "openapi.json", "swagger.json",
|
||||
"jest.config.json", ".eslintrc.json", ".prettierrc.json",
|
||||
".babelrc.json", "tslint.json", "angular.json",
|
||||
"firebase.json", "renovate.json", "lerna.json",
|
||||
"turbo.json", ".stylelintrc.json", "pnpm-lock.json",
|
||||
"deno.json", "biome.json", "devcontainer.json",
|
||||
".devcontainer.json", "launch.json", "settings.json",
|
||||
"extensions.json", "tasks.json", NULL};
|
||||
|
||||
static bool lang_name_in(const char *name, const char *const *list) {
|
||||
for (int i = 0; list[i]; i++) {
|
||||
if (strcmp(name, list[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The content rule a name needs, or NULL. */
|
||||
static const char *lang_probe_extension(const char *filename) {
|
||||
static const char *const PROBED[] = {".m", ".cls", ".inc", ".cfc", ".frm", ".res", NULL};
|
||||
const char *dot = strrchr(filename, '.');
|
||||
return (dot && lang_name_in(dot, PROBED)) ? dot : NULL;
|
||||
}
|
||||
|
||||
/* "<Export generator=" in the first line-probe bytes: an ObjectScript Studio
|
||||
* export, which is XML by name. */
|
||||
static bool lang_objectscript_export(const unsigned char *head, size_t n) {
|
||||
char buf[LANG_PROBE_LINE + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_LINE, head, n);
|
||||
return strstr(buf, "<Export generator=") != NULL;
|
||||
}
|
||||
|
||||
_Static_assert(LANG_PROBE_CFC == CBM_LANGUAGE_PROBE_MAX, "the longest probe is the public bound");
|
||||
|
||||
size_t cbm_language_probe_bytes_with(const cbm_userconfig_t *config, const char *filename) {
|
||||
if (!filename) {
|
||||
return 0;
|
||||
}
|
||||
CBMLanguage lang = lang_for_filename(config, filename);
|
||||
if (lang == CBM_LANG_COUNT) {
|
||||
return LANG_PROBE_LINE; /* the shebang fallback */
|
||||
}
|
||||
const char *ext = lang_probe_extension(filename);
|
||||
if (ext) {
|
||||
return strcmp(ext, ".cfc") == 0 ? LANG_PROBE_CFC : LANG_PROBE_HEAD;
|
||||
}
|
||||
return lang == CBM_LANG_XML ? LANG_PROBE_LINE : 0;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const char *filename,
|
||||
const unsigned char *head, size_t head_len, bool more,
|
||||
bool readable) {
|
||||
if (!filename) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
if (!readable || !head) {
|
||||
head = NULL;
|
||||
head_len = 0;
|
||||
}
|
||||
CBMLanguage lang = lang_for_filename(config, filename);
|
||||
if (lang == CBM_LANG_COUNT) {
|
||||
/* The name says nothing: a conservative shebang probe, so extensionless
|
||||
* scripts get indexed (#1199). The name stays authoritative otherwise. */
|
||||
return readable ? lang_shebang(head, head_len, more) : CBM_LANG_COUNT;
|
||||
}
|
||||
const char *ext = lang_probe_extension(filename);
|
||||
if (ext) {
|
||||
char buf[LANG_PROBE_CFC + SKIP_ONE];
|
||||
bool cfc = strcmp(ext, ".cfc") == 0;
|
||||
lang_head_text(buf, cfc ? LANG_PROBE_CFC : LANG_PROBE_HEAD, head, head_len);
|
||||
if (strcmp(ext, ".m") == 0) {
|
||||
/* Objective-C, Magma or MATLAB */
|
||||
lang = readable ? lang_m_text(buf) : CBM_LANG_MATLAB;
|
||||
} else if (strcmp(ext, ".cls") == 0) {
|
||||
/* ObjectScript UDL, Apex or a Visual Basic 6 class module */
|
||||
lang = readable ? lang_cls_text(buf) : CBM_LANG_APEX;
|
||||
} else if (strcmp(ext, ".inc") == 0) {
|
||||
/* BitBake or an ObjectScript include */
|
||||
lang = readable ? lang_inc_text(buf) : CBM_LANG_BITBAKE;
|
||||
} else if (cfc) {
|
||||
/* script or tag dialect */
|
||||
lang = readable ? lang_cfc_text(buf) : CBM_LANG_CFSCRIPT;
|
||||
} else if (strcmp(ext, ".frm") == 0) {
|
||||
/* FORM or a Visual Basic 6 form (#721) */
|
||||
lang = readable ? lang_frm_text(buf) : CBM_LANG_FORM;
|
||||
} else {
|
||||
/* .res: ReScript, or a binary Godot / Windows resource (#2176) */
|
||||
lang = readable ? lang_res_bytes(head, head_len) : CBM_LANG_RESCRIPT;
|
||||
}
|
||||
}
|
||||
if (lang == CBM_LANG_XML && readable && lang_objectscript_export(head, head_len)) {
|
||||
return CBM_LANG_OBJECTSCRIPT_EXPORT;
|
||||
}
|
||||
if (lang == CBM_LANG_JSON && lang_name_in(filename, IGNORED_JSON_FILES)) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
return lang;
|
||||
}
|
||||
|
||||
size_t cbm_language_probe_bytes(const char *filename) {
|
||||
return cbm_language_probe_bytes_with(cbm_get_user_lang_config(), filename);
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_classify(const char *filename, const unsigned char *head, size_t head_len,
|
||||
bool more, bool readable) {
|
||||
return cbm_language_classify_with(cbm_get_user_lang_config(), filename, head, head_len, more,
|
||||
readable);
|
||||
}
|
||||
|
||||
CBMLanguage cbm_language_for_file(const char *filename, const char *path) {
|
||||
size_t probe = cbm_language_probe_bytes(filename);
|
||||
if (probe == 0) {
|
||||
return cbm_language_classify(filename, NULL, 0, false, true);
|
||||
}
|
||||
/* One byte past the probe tells a file that ends there from a longer one. */
|
||||
unsigned char head[LANG_PROBE_CFC + SKIP_ONE];
|
||||
size_t n = 0;
|
||||
FILE *f = path ? cbm_fopen(path, "rb") : NULL;
|
||||
if (!f) {
|
||||
return cbm_language_classify(filename, NULL, 0, false, false);
|
||||
}
|
||||
n = fread(head, SKIP_ONE, probe + SKIP_ONE, f);
|
||||
bool failed = ferror(f) != 0;
|
||||
(void)fclose(f);
|
||||
bool more = n > probe;
|
||||
return cbm_language_classify(filename, head, more ? probe : n, more, !failed);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/* Immutable test-convention declarations from one project JSON snapshot. */
|
||||
#include "discover/test_conventions.h"
|
||||
#include "foundation/arena.h"
|
||||
#include "foundation/sha256.h"
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <yyjson/yyjson.h>
|
||||
|
||||
enum { TD_MAX_BYTES = 65536, TD_MAX_NAME_ARGS = 64 };
|
||||
|
||||
struct cbm_test_declarations {
|
||||
CBMArena arena;
|
||||
cbm_test_declaration_t *items;
|
||||
int count;
|
||||
bool configured;
|
||||
bool presets[CBM_TEST_PRESET_COUNT];
|
||||
char digest[CBM_SHA256_HEX_LEN + 1];
|
||||
};
|
||||
|
||||
static yyjson_val *td_field(yyjson_val *object, const char *name, bool *valid) {
|
||||
if (!yyjson_is_obj(object)) {
|
||||
*valid = false;
|
||||
return NULL;
|
||||
}
|
||||
yyjson_val *found = NULL, *key, *value;
|
||||
size_t index, maximum, len = strlen(name);
|
||||
yyjson_obj_foreach(object, index, maximum, key, value) {
|
||||
if (yyjson_get_len(key) == len && memcmp(yyjson_get_str(key), name, len) == 0) {
|
||||
if (found) {
|
||||
*valid = false;
|
||||
return NULL;
|
||||
}
|
||||
found = value;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static const char *td_string(yyjson_val *value, size_t *len) {
|
||||
const char *text = yyjson_get_str(value);
|
||||
size_t size = yyjson_get_len(value);
|
||||
if (!text || !size || memchr(text, '\0', size) || memchr(text, '\n', size) ||
|
||||
memchr(text, '\r', size))
|
||||
return NULL;
|
||||
if (len)
|
||||
*len = size;
|
||||
return text;
|
||||
}
|
||||
|
||||
static bool td_identifier_first(unsigned char ch) {
|
||||
return ch == '_' || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
|
||||
}
|
||||
|
||||
static bool td_identifier(const char *text, size_t len) {
|
||||
if (!len || !td_identifier_first((unsigned char)text[0]))
|
||||
return false;
|
||||
for (size_t i = 1; i < len; i++) {
|
||||
unsigned char ch = (unsigned char)text[i];
|
||||
if (!td_identifier_first(ch) && !(ch >= '0' && ch <= '9'))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool td_copy_string(cbm_test_declarations_t *declarations, yyjson_val *value,
|
||||
bool identifier, const char **result) {
|
||||
size_t len = 0;
|
||||
const char *text = td_string(value, &len);
|
||||
if (!text || (identifier && !td_identifier(text, len)))
|
||||
return false;
|
||||
*result = cbm_arena_strndup(&declarations->arena, text, len);
|
||||
return *result != NULL;
|
||||
}
|
||||
|
||||
static bool td_argument(yyjson_val *value, int *result) {
|
||||
if (!yyjson_is_uint(value) || yyjson_get_uint(value) > INT_MAX)
|
||||
return false;
|
||||
*result = (int)yyjson_get_uint(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool td_name_args(cbm_test_declarations_t *declarations, yyjson_val *value,
|
||||
cbm_test_declaration_t *item) {
|
||||
if (!yyjson_is_arr(value))
|
||||
return false;
|
||||
size_t count = yyjson_arr_size(value);
|
||||
if (!count || count > TD_MAX_NAME_ARGS)
|
||||
return false;
|
||||
int *args = cbm_arena_alloc(&declarations->arena, count * sizeof(*args));
|
||||
if (!args)
|
||||
return false;
|
||||
size_t index, maximum;
|
||||
yyjson_val *argument;
|
||||
yyjson_arr_foreach(value, index, maximum, argument) {
|
||||
if (!td_argument(argument, &args[index]))
|
||||
return false;
|
||||
}
|
||||
/* Ordered duplicates carry meaning for an adapter, so retain them. */
|
||||
item->name_args = args;
|
||||
item->name_arg_count = (int)count;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool td_record(cbm_test_declarations_t *declarations, yyjson_val *record,
|
||||
cbm_test_declaration_t *item) {
|
||||
bool valid = true;
|
||||
yyjson_val *language = td_field(record, "language", &valid);
|
||||
yyjson_val *role = td_field(record, "role", &valid);
|
||||
yyjson_val *define_macro = td_field(record, "define_macro", &valid);
|
||||
yyjson_val *name_args = td_field(record, "name_args", &valid);
|
||||
yyjson_val *runner_id = td_field(record, "runner_id", &valid);
|
||||
yyjson_val *macro = td_field(record, "macro", &valid);
|
||||
yyjson_val *test_arg = td_field(record, "test_arg", &valid);
|
||||
yyjson_val *suite_arg = td_field(record, "suite_arg", &valid);
|
||||
yyjson_val *perf_macro = td_field(record, "perf_macro", &valid);
|
||||
const char *role_text = td_string(role, NULL);
|
||||
if (!valid || !role_text || !td_copy_string(declarations, language, false, &item->language))
|
||||
return false;
|
||||
item->test_arg = -1;
|
||||
item->suite_arg = -1;
|
||||
if (strcmp(role_text, "case") == 0 || strcmp(role_text, "suite") == 0) {
|
||||
bool is_case = strcmp(role_text, "case") == 0;
|
||||
item->role = is_case ? CBM_TEST_DECL_CASE : CBM_TEST_DECL_SUITE;
|
||||
if (macro || test_arg || suite_arg || perf_macro || (!is_case && runner_id))
|
||||
return false;
|
||||
return td_copy_string(declarations, define_macro, true, &item->define_macro) &&
|
||||
td_name_args(declarations, name_args, item) &&
|
||||
(!runner_id || td_copy_string(declarations, runner_id, false, &item->runner_id));
|
||||
}
|
||||
if (strcmp(role_text, "registration") == 0) {
|
||||
item->role = CBM_TEST_DECL_REGISTRATION;
|
||||
if (define_macro || name_args || runner_id || suite_arg || perf_macro)
|
||||
return false;
|
||||
return td_copy_string(declarations, macro, true, &item->macro) &&
|
||||
td_argument(test_arg, &item->test_arg);
|
||||
}
|
||||
if (strcmp(role_text, "suite_registration") == 0) {
|
||||
item->role = CBM_TEST_DECL_SUITE_REGISTRATION;
|
||||
if (define_macro || name_args || runner_id || test_arg)
|
||||
return false;
|
||||
return td_copy_string(declarations, macro, true, &item->macro) &&
|
||||
td_argument(suite_arg, &item->suite_arg) &&
|
||||
(!perf_macro || td_copy_string(declarations, perf_macro, true, &item->perf_macro));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool td_conventions(cbm_test_declarations_t *declarations, yyjson_val *value) {
|
||||
if (!yyjson_is_arr(value))
|
||||
return false;
|
||||
size_t count = yyjson_arr_size(value);
|
||||
if (count > INT_MAX || count > SIZE_MAX / sizeof(*declarations->items))
|
||||
return false;
|
||||
if (!count)
|
||||
return true;
|
||||
declarations->items =
|
||||
cbm_arena_calloc(&declarations->arena, count * sizeof(*declarations->items));
|
||||
if (!declarations->items)
|
||||
return false;
|
||||
size_t index, maximum;
|
||||
yyjson_val *record;
|
||||
yyjson_arr_foreach(value, index, maximum, record) {
|
||||
if (!td_record(declarations, record, &declarations->items[index]))
|
||||
return false;
|
||||
}
|
||||
declarations->count = (int)count;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool td_presets(cbm_test_declarations_t *declarations, yyjson_val *value) {
|
||||
static const char *const names[CBM_TEST_PRESET_COUNT] = {
|
||||
[CBM_TEST_PRESET_C_CBM] = "c-cbm", [CBM_TEST_PRESET_PYTEST] = "pytest",
|
||||
[CBM_TEST_PRESET_GO] = "go", [CBM_TEST_PRESET_JUNIT] = "junit",
|
||||
[CBM_TEST_PRESET_GTEST] = "gtest", [CBM_TEST_PRESET_RUST] = "rust",
|
||||
[CBM_TEST_PRESET_JEST] = "jest"};
|
||||
if (!yyjson_is_obj(value))
|
||||
return false;
|
||||
bool seen[CBM_TEST_PRESET_COUNT] = {false};
|
||||
size_t index, maximum;
|
||||
yyjson_val *key, *enabled;
|
||||
yyjson_obj_foreach(value, index, maximum, key, enabled) {
|
||||
int preset = -1;
|
||||
size_t len = yyjson_get_len(key);
|
||||
const char *text = yyjson_get_str(key);
|
||||
for (int candidate = 0; candidate < CBM_TEST_PRESET_COUNT; candidate++) {
|
||||
if (len == strlen(names[candidate]) && memcmp(text, names[candidate], len) == 0) {
|
||||
preset = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* An unknown switch is an unsupported semantic request, not an
|
||||
* unrelated future field whose intent can safely remain unconsumed. */
|
||||
if (preset < 0 || seen[preset] || !yyjson_is_bool(enabled))
|
||||
return false;
|
||||
seen[preset] = true;
|
||||
declarations->presets[preset] = yyjson_get_bool(enabled);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool td_project(cbm_test_declarations_t *declarations, yyjson_val *root) {
|
||||
bool valid = true;
|
||||
yyjson_val *impact = td_field(root, "test_impact", &valid);
|
||||
if (!valid || !impact)
|
||||
return valid;
|
||||
yyjson_val *version = td_field(impact, "version", &valid);
|
||||
yyjson_val *tests = td_field(impact, "tests", &valid);
|
||||
if (!valid || !yyjson_is_uint(version) || yyjson_get_uint(version) != 1)
|
||||
return false;
|
||||
declarations->configured = true;
|
||||
if (!tests)
|
||||
return true;
|
||||
yyjson_val *conventions = td_field(tests, "conventions", &valid);
|
||||
yyjson_val *presets = td_field(tests, "presets", &valid);
|
||||
if (!valid)
|
||||
return false;
|
||||
return (!conventions || td_conventions(declarations, conventions)) &&
|
||||
(!presets || td_presets(declarations, presets));
|
||||
}
|
||||
|
||||
static void td_digest(cbm_test_declarations_t *declarations, const char *bytes, size_t len,
|
||||
bool absent) {
|
||||
cbm_sha256_ctx hash;
|
||||
cbm_sha256_init(&hash);
|
||||
const char *domain = absent ? "cbm-test-config-absent-v1" : "cbm-test-config-bytes-v1";
|
||||
cbm_sha256_update(&hash, domain, strlen(domain) + 1);
|
||||
if (len)
|
||||
cbm_sha256_update(&hash, bytes, len);
|
||||
uint8_t digest[CBM_SHA256_DIGEST_LEN];
|
||||
cbm_sha256_final(&hash, digest);
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (int i = 0; i < CBM_SHA256_DIGEST_LEN; i++) {
|
||||
declarations->digest[i * 2] = hex[digest[i] >> 4];
|
||||
declarations->digest[i * 2 + 1] = hex[digest[i] & 15];
|
||||
}
|
||||
declarations->digest[CBM_SHA256_HEX_LEN] = '\0';
|
||||
}
|
||||
|
||||
cbm_test_declarations_t *cbm_test_declarations_parse(const char *bytes, size_t len, bool absent) {
|
||||
if (len > TD_MAX_BYTES || (absent && len) || (!absent && (!bytes || !len)) ||
|
||||
(len && memchr(bytes, '\0', len)))
|
||||
return NULL;
|
||||
CBMArena arena;
|
||||
cbm_arena_init_lazy(&arena, 4096);
|
||||
cbm_test_declarations_t *declarations = cbm_arena_calloc(&arena, sizeof(*declarations));
|
||||
if (!declarations) {
|
||||
cbm_arena_destroy(&arena);
|
||||
return NULL;
|
||||
}
|
||||
declarations->arena = arena;
|
||||
for (int preset = 0; preset < CBM_TEST_PRESET_COUNT; preset++)
|
||||
declarations->presets[preset] = preset != CBM_TEST_PRESET_C_CBM;
|
||||
bool valid = true;
|
||||
if (!absent) {
|
||||
yyjson_doc *doc = yyjson_read(bytes, len, 0);
|
||||
if (!doc) {
|
||||
valid = false;
|
||||
} else {
|
||||
valid = td_project(declarations, yyjson_doc_get_root(doc));
|
||||
yyjson_doc_free(doc);
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
cbm_test_declarations_free(declarations);
|
||||
return NULL;
|
||||
}
|
||||
td_digest(declarations, bytes, len, absent);
|
||||
return declarations;
|
||||
}
|
||||
|
||||
void cbm_test_declarations_free(cbm_test_declarations_t *declarations) {
|
||||
if (!declarations)
|
||||
return;
|
||||
CBMArena arena = declarations->arena;
|
||||
cbm_arena_destroy(&arena);
|
||||
}
|
||||
|
||||
bool cbm_test_declarations_configured(const cbm_test_declarations_t *declarations) {
|
||||
return declarations && declarations->configured;
|
||||
}
|
||||
|
||||
const cbm_test_declaration_t *cbm_test_declarations_items(
|
||||
const cbm_test_declarations_t *declarations, int *count) {
|
||||
if (count)
|
||||
*count = declarations ? declarations->count : 0;
|
||||
return declarations ? declarations->items : NULL;
|
||||
}
|
||||
|
||||
bool cbm_test_declarations_preset(const cbm_test_declarations_t *declarations,
|
||||
cbm_test_preset_t preset, bool *enabled) {
|
||||
if (enabled)
|
||||
*enabled = false;
|
||||
if (!declarations || !enabled || (unsigned)preset >= CBM_TEST_PRESET_COUNT)
|
||||
return false;
|
||||
*enabled = declarations->presets[preset];
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *cbm_test_declarations_digest(const cbm_test_declarations_t *declarations) {
|
||||
return declarations ? declarations->digest : "";
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef CBM_TEST_CONVENTIONS_H
|
||||
#define CBM_TEST_CONVENTIONS_H
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Internal immutable descriptor snapshot for the approved tests.conventions
|
||||
* and tests.presets schema. This parser classifies no source and creates no
|
||||
* TESTS/macro edges. Consumers must separately verify that they can implement
|
||||
* every applicable declaration; an unsupported record is never ignored to
|
||||
* justify narrowing. */
|
||||
typedef enum {
|
||||
CBM_TEST_DECL_CASE,
|
||||
CBM_TEST_DECL_SUITE,
|
||||
CBM_TEST_DECL_REGISTRATION,
|
||||
CBM_TEST_DECL_SUITE_REGISTRATION
|
||||
} cbm_test_decl_role_t;
|
||||
|
||||
typedef struct {
|
||||
const char *language;
|
||||
cbm_test_decl_role_t role;
|
||||
const char *define_macro; /* definition roles only */
|
||||
const int *name_args; /* definition roles; preserve order */
|
||||
int name_arg_count;
|
||||
const char *runner_id; /* optional case template; NULL != guessed default */
|
||||
const char *macro; /* registration roles only */
|
||||
int test_arg; /* registration; -1 otherwise */
|
||||
int suite_arg; /* suite_registration; -1 otherwise */
|
||||
const char *perf_macro; /* optional suite_registration alternate */
|
||||
} cbm_test_declaration_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_TEST_PRESET_C_CBM,
|
||||
CBM_TEST_PRESET_PYTEST,
|
||||
CBM_TEST_PRESET_GO,
|
||||
CBM_TEST_PRESET_JUNIT,
|
||||
CBM_TEST_PRESET_GTEST,
|
||||
CBM_TEST_PRESET_RUST,
|
||||
CBM_TEST_PRESET_JEST,
|
||||
CBM_TEST_PRESET_COUNT
|
||||
} cbm_test_preset_t;
|
||||
|
||||
typedef struct cbm_test_declarations cbm_test_declarations_t;
|
||||
|
||||
/* Full project JSON, explicit bytes, max64KiB, independently owned. absent
|
||||
* requires len=0 (NULL bytes allowed); malformed or unsupported consumed
|
||||
* syntax => NULL. No path read, global state, env or shell evaluation.
|
||||
* Missing tests/conventions => empty custom list. Missing preset switches
|
||||
* preserve existing defaults (c-cbm false; six language presets true).
|
||||
* A missing impact section is distinct from a configured one with defaults.
|
||||
* runner_id is retained as opaque data; adapters must validate its template
|
||||
* language and produce an error for missing/unsupported required mappings. */
|
||||
cbm_test_declarations_t *cbm_test_declarations_parse(const char *bytes, size_t len, bool absent);
|
||||
void cbm_test_declarations_free(cbm_test_declarations_t *declarations);
|
||||
bool cbm_test_declarations_configured(const cbm_test_declarations_t *declarations);
|
||||
const cbm_test_declaration_t *cbm_test_declarations_items(
|
||||
const cbm_test_declarations_t *declarations, int *count);
|
||||
/* false for invalid object/enum; clears enabled first. */
|
||||
bool cbm_test_declarations_preset(const cbm_test_declarations_t *declarations,
|
||||
cbm_test_preset_t preset, bool *enabled);
|
||||
const char *cbm_test_declarations_digest(const cbm_test_declarations_t *declarations);
|
||||
|
||||
#endif
|
||||
+280
-4
@@ -1,3 +1,4 @@
|
||||
#include "foundation/arena.h" /* public lazy-arena declarations before cbm.h */
|
||||
/*
|
||||
* userconfig.c — User-defined extension→language mappings.
|
||||
*
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "foundation/platform.h" /* cbm_safe_getenv */
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/sha256.h"
|
||||
#include "foundation/arena.h"
|
||||
|
||||
enum { MAX_CONFIG_SIZE = 65536 };
|
||||
#include "foundation/log.h"
|
||||
@@ -30,6 +32,21 @@ enum { MAX_CONFIG_SIZE = 65536 };
|
||||
|
||||
static const cbm_userconfig_t *g_userconfig = NULL;
|
||||
|
||||
struct cbm_userconfig_source {
|
||||
CBMArena arena;
|
||||
cbm_userconfig_source_state_t state;
|
||||
const char *bytes;
|
||||
size_t len;
|
||||
bool owns_config_arena; /* false for both legacy loaders */
|
||||
};
|
||||
|
||||
static void userconfig_source_free(cbm_userconfig_source_t *source) {
|
||||
if (source) {
|
||||
CBMArena arena = source->arena;
|
||||
cbm_arena_destroy(&arena);
|
||||
}
|
||||
}
|
||||
|
||||
static void userconfig_source_digest(const char *state, const void *bytes, size_t len,
|
||||
char out[CBM_SHA256_HEX_LEN + 1]) {
|
||||
static const char domain[] = "cbm-userconfig-source-v1";
|
||||
@@ -315,13 +332,76 @@ static int load_config_file(const char *path, cbm_userext_t **entries, int *coun
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Retain the project input before parsing it; never reopen for declarations. */
|
||||
static int load_project_source(const char *path, cbm_userconfig_t *cfg, cbm_userext_t **entries,
|
||||
int *count) {
|
||||
cbm_userconfig_source_t *source = cfg->project_source;
|
||||
source->state = CBM_USERCONFIG_SOURCE_ERROR;
|
||||
userconfig_source_digest("read-error", NULL, 0, cfg->project_source_sha256);
|
||||
cbm_path_info_t info = {0};
|
||||
int status = cbm_path_info_utf8(path, &info);
|
||||
if (status == CBM_PATH_INFO_ABSENT) {
|
||||
source->state = CBM_USERCONFIG_SOURCE_ABSENT;
|
||||
/* Preserve the existing digest for actual absence. */
|
||||
userconfig_source_digest("missing-or-unreadable", NULL, 0, cfg->project_source_sha256);
|
||||
return 0;
|
||||
}
|
||||
if (status != CBM_PATH_INFO_OK || !info.is_regular || info.size > MAX_CONFIG_SIZE)
|
||||
return 0;
|
||||
char *bytes = cbm_arena_alloc(&source->arena, MAX_CONFIG_SIZE + 1U);
|
||||
if (!bytes)
|
||||
return CBM_NOT_FOUND;
|
||||
FILE *file = cbm_fopen(path, "rb");
|
||||
if (!file)
|
||||
return 0;
|
||||
size_t len = fread(bytes, 1, MAX_CONFIG_SIZE + 1U, file);
|
||||
bool valid = !ferror(file) && feof(file) && len <= MAX_CONFIG_SIZE;
|
||||
if (fclose(file) != 0)
|
||||
valid = false;
|
||||
if (!valid)
|
||||
return 0;
|
||||
bytes[len] = '\0';
|
||||
source->state = CBM_USERCONFIG_SOURCE_PRESENT;
|
||||
source->bytes = bytes;
|
||||
source->len = len;
|
||||
userconfig_source_digest(len ? "present" : "empty", bytes, len, cfg->project_source_sha256);
|
||||
if (!len)
|
||||
return 0;
|
||||
yyjson_read_err error = {0};
|
||||
yyjson_doc *doc = yyjson_read_opts(bytes, len, 0, NULL, &error);
|
||||
if (!doc) {
|
||||
if (error.code == YYJSON_READ_ERROR_MEMORY_ALLOCATION)
|
||||
return CBM_NOT_FOUND;
|
||||
cbm_log_warn("userconfig.corrupt_json", "path", path);
|
||||
return 0;
|
||||
}
|
||||
int rc = parse_extra_extensions(yyjson_doc_get_root(doc), entries, count, path);
|
||||
yyjson_doc_free(doc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ── Public API ──────────────────────────────────────────────────── */
|
||||
|
||||
cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
static cbm_userconfig_t *userconfig_load(const char *repo_path, bool retain_source) {
|
||||
/* The retained source first: a failure here has nothing else to undo. */
|
||||
cbm_userconfig_source_t *source = NULL;
|
||||
if (retain_source) {
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
source = cbm_arena_calloc(&arena, sizeof(*source));
|
||||
if (!source) {
|
||||
cbm_arena_destroy(&arena);
|
||||
return NULL;
|
||||
}
|
||||
source->arena = arena;
|
||||
source->state = CBM_USERCONFIG_SOURCE_ABSENT;
|
||||
}
|
||||
cbm_userconfig_t *cfg = calloc(CBM_ALLOC_ONE, sizeof(cbm_userconfig_t));
|
||||
if (!cfg) {
|
||||
userconfig_source_free(source);
|
||||
return NULL;
|
||||
}
|
||||
cfg->project_source = source;
|
||||
|
||||
cbm_userext_t *entries = NULL;
|
||||
int count = 0;
|
||||
@@ -338,6 +418,7 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
free(entries[i].ext);
|
||||
}
|
||||
free(entries);
|
||||
userconfig_source_free(cfg->project_source);
|
||||
free(cfg);
|
||||
return NULL;
|
||||
}
|
||||
@@ -348,14 +429,27 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
userconfig_source_digest("not-applicable", NULL, 0, cfg->project_source_sha256);
|
||||
if (repo_path && repo_path[0]) {
|
||||
char project_path[PATH_BUF_SZ];
|
||||
snprintf(project_path, sizeof(project_path), "%s/.codebase-memory.json", repo_path);
|
||||
|
||||
if (load_config_file(project_path, &entries, &count, cfg->project_source_sha256) != 0) {
|
||||
int path_len =
|
||||
snprintf(project_path, sizeof(project_path), "%s/.codebase-memory.json", repo_path);
|
||||
int rc;
|
||||
if (retain_source) {
|
||||
if (path_len < 0 || (size_t)path_len >= sizeof(project_path)) {
|
||||
cfg->project_source->state = CBM_USERCONFIG_SOURCE_ERROR;
|
||||
userconfig_source_digest("path-error", NULL, 0, cfg->project_source_sha256);
|
||||
rc = 0;
|
||||
} else {
|
||||
rc = load_project_source(project_path, cfg, &entries, &count);
|
||||
}
|
||||
} else {
|
||||
rc = load_config_file(project_path, &entries, &count, cfg->project_source_sha256);
|
||||
}
|
||||
if (rc != 0) {
|
||||
/* Free already-allocated entries */
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(entries[i].ext);
|
||||
}
|
||||
free(entries);
|
||||
userconfig_source_free(cfg->project_source);
|
||||
free(cfg);
|
||||
return NULL;
|
||||
}
|
||||
@@ -398,6 +492,32 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) {
|
||||
return userconfig_load(repo_path, false);
|
||||
}
|
||||
|
||||
cbm_userconfig_t *cbm_userconfig_load_with_source(const char *repo_path) {
|
||||
return userconfig_load(repo_path, true);
|
||||
}
|
||||
|
||||
cbm_userconfig_source_state_t cbm_userconfig_project_source(const cbm_userconfig_t *cfg,
|
||||
const char **bytes, size_t *len) {
|
||||
if (bytes)
|
||||
*bytes = NULL;
|
||||
if (len)
|
||||
*len = 0;
|
||||
if (!cfg || !cfg->project_source)
|
||||
return CBM_USERCONFIG_SOURCE_ERROR;
|
||||
const cbm_userconfig_source_t *source = cfg->project_source;
|
||||
if (source->state == CBM_USERCONFIG_SOURCE_PRESENT) {
|
||||
if (bytes)
|
||||
*bytes = source->bytes;
|
||||
if (len)
|
||||
*len = source->len;
|
||||
}
|
||||
return source->state;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_userconfig_lookup(const cbm_userconfig_t *cfg, const char *ext) {
|
||||
if (!cfg || !ext || !ext[0]) {
|
||||
return CBM_LANG_COUNT;
|
||||
@@ -414,9 +534,165 @@ void cbm_userconfig_free(cbm_userconfig_t *cfg) {
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
if (cfg->project_source && cfg->project_source->owns_config_arena) {
|
||||
CBMArena arena = cfg->project_source->arena;
|
||||
cbm_arena_destroy(&arena);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < cfg->count; i++) {
|
||||
free(cfg->entries[i].ext);
|
||||
}
|
||||
free(cfg->entries);
|
||||
userconfig_source_free(cfg->project_source);
|
||||
free(cfg);
|
||||
}
|
||||
|
||||
/* Frozen project-only input. The legacy pathname loaders remain fail-open. */
|
||||
static bool snapshot_key_is(yyjson_val *key, const char *name) {
|
||||
size_t len = strlen(name);
|
||||
return yyjson_get_len(key) == len && memcmp(yyjson_get_str(key), name, len) == 0;
|
||||
}
|
||||
|
||||
static cbm_userconfig_snapshot_status_t snapshot_extensions(yyjson_val *root, yyjson_val **out) {
|
||||
*out = NULL;
|
||||
if (!yyjson_is_obj(root))
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
yyjson_obj_iter iter;
|
||||
yyjson_obj_iter_init(root, &iter);
|
||||
yyjson_val *key;
|
||||
while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
|
||||
if (!snapshot_key_is(key, "extra_extensions"))
|
||||
continue;
|
||||
if (*out)
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
*out = yyjson_obj_iter_get_val(key);
|
||||
if (!yyjson_is_obj(*out))
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
}
|
||||
return CBM_USERCONFIG_SNAPSHOT_OK;
|
||||
}
|
||||
|
||||
static bool snapshot_string(yyjson_val *value) {
|
||||
if (!yyjson_is_str(value) || yyjson_get_len(value) == 0)
|
||||
return false;
|
||||
return memchr(yyjson_get_str(value), 0, yyjson_get_len(value)) == NULL;
|
||||
}
|
||||
|
||||
/* Frozen config matching must not depend on the process locale. */
|
||||
static CBMLanguage snapshot_language(yyjson_val *value) {
|
||||
const unsigned char *name = (const unsigned char *)yyjson_get_str(value);
|
||||
size_t len = yyjson_get_len(value);
|
||||
for (size_t i = 0; i < LANG_NAME_TABLE_SIZE; i++) {
|
||||
const char *alias = LANG_NAME_TABLE[i].name;
|
||||
if (strlen(alias) != len)
|
||||
continue;
|
||||
size_t j = 0;
|
||||
for (; j < len; j++) {
|
||||
unsigned char byte = name[j];
|
||||
if (byte >= 'A' && byte <= 'Z')
|
||||
byte = (unsigned char)(byte + ('a' - 'A'));
|
||||
if (byte != (unsigned char)alias[j])
|
||||
break;
|
||||
}
|
||||
if (j == len)
|
||||
return LANG_NAME_TABLE[i].lang;
|
||||
}
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
|
||||
static cbm_userconfig_snapshot_status_t snapshot_entry(CBMArena *arena, cbm_userconfig_t *cfg,
|
||||
yyjson_val *key, yyjson_val *value) {
|
||||
if (!snapshot_string(key) || !snapshot_string(value) || yyjson_get_str(key)[0] != '.')
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
const char *ext = yyjson_get_str(key);
|
||||
for (int i = 0; i < cfg->count; i++) {
|
||||
if (strcmp(cfg->entries[i].ext, ext) == 0)
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
}
|
||||
CBMLanguage language = snapshot_language(value);
|
||||
if (language == CBM_LANG_COUNT)
|
||||
return CBM_USERCONFIG_SNAPSHOT_UNSUPPORTED;
|
||||
char *copy = cbm_arena_strndup(arena, ext, yyjson_get_len(key));
|
||||
if (!copy)
|
||||
return CBM_USERCONFIG_SNAPSHOT_OOM;
|
||||
cfg->entries[cfg->count++] = (cbm_userext_t){.ext = copy, .lang = language};
|
||||
return CBM_USERCONFIG_SNAPSHOT_OK;
|
||||
}
|
||||
|
||||
static cbm_userconfig_snapshot_status_t snapshot_parse(CBMArena *arena, cbm_userconfig_t *cfg) {
|
||||
cbm_userconfig_source_t *source = cfg->project_source;
|
||||
yyjson_read_err error = {0};
|
||||
yyjson_doc *doc = yyjson_read_opts((char *)source->bytes, source->len, 0, NULL, &error);
|
||||
if (!doc)
|
||||
return error.code == YYJSON_READ_ERROR_MEMORY_ALLOCATION ? CBM_USERCONFIG_SNAPSHOT_OOM
|
||||
: CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
yyjson_val *extra = NULL;
|
||||
cbm_userconfig_snapshot_status_t status = snapshot_extensions(yyjson_doc_get_root(doc), &extra);
|
||||
size_t count = extra ? yyjson_obj_size(extra) : 0;
|
||||
/* The 64 KiB source cap already bounds count; keep allocation math explicit. */
|
||||
if (count > MAX_CONFIG_SIZE || count > SIZE_MAX / sizeof(*cfg->entries))
|
||||
status = CBM_USERCONFIG_SNAPSHOT_LIMIT;
|
||||
if (status == CBM_USERCONFIG_SNAPSHOT_OK && count > 0) {
|
||||
cfg->entries = cbm_arena_calloc(arena, count * sizeof(*cfg->entries));
|
||||
if (!cfg->entries)
|
||||
status = CBM_USERCONFIG_SNAPSHOT_OOM;
|
||||
}
|
||||
if (status == CBM_USERCONFIG_SNAPSHOT_OK && extra) {
|
||||
yyjson_obj_iter iter;
|
||||
yyjson_obj_iter_init(extra, &iter);
|
||||
yyjson_val *key;
|
||||
while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
|
||||
status = snapshot_entry(arena, cfg, key, yyjson_obj_iter_get_val(key));
|
||||
if (status != CBM_USERCONFIG_SNAPSHOT_OK)
|
||||
break;
|
||||
}
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
return status;
|
||||
}
|
||||
|
||||
static cbm_userconfig_snapshot_status_t snapshot_input(cbm_userconfig_source_state_t state,
|
||||
const void *bytes, size_t len) {
|
||||
if (state == CBM_USERCONFIG_SOURCE_ABSENT)
|
||||
return (!bytes && len == 0) ? CBM_USERCONFIG_SNAPSHOT_OK : CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
if (state != CBM_USERCONFIG_SOURCE_PRESENT || !bytes || len == 0)
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
return len > MAX_CONFIG_SIZE ? CBM_USERCONFIG_SNAPSHOT_LIMIT : CBM_USERCONFIG_SNAPSHOT_OK;
|
||||
}
|
||||
|
||||
cbm_userconfig_snapshot_status_t cbm_userconfig_from_project_bytes(
|
||||
cbm_userconfig_source_state_t state, const void *bytes, size_t len, cbm_userconfig_t **out) {
|
||||
if (!out)
|
||||
return CBM_USERCONFIG_SNAPSHOT_INVALID;
|
||||
*out = NULL;
|
||||
cbm_userconfig_snapshot_status_t status = snapshot_input(state, bytes, len);
|
||||
if (status != CBM_USERCONFIG_SNAPSHOT_OK)
|
||||
return status;
|
||||
CBMArena arena;
|
||||
cbm_arena_init_lazy(&arena, CBM_ARENA_DEFAULT_BLOCK_SIZE);
|
||||
cbm_userconfig_t *cfg = cbm_arena_calloc(&arena, sizeof(*cfg));
|
||||
cbm_userconfig_source_t *source = cbm_arena_calloc(&arena, sizeof(*source));
|
||||
if (!cfg || !source) {
|
||||
cbm_arena_destroy(&arena);
|
||||
return CBM_USERCONFIG_SNAPSHOT_OOM;
|
||||
}
|
||||
cfg->project_source = source;
|
||||
source->state = state;
|
||||
source->len = len;
|
||||
if (state == CBM_USERCONFIG_SOURCE_PRESENT) {
|
||||
source->bytes = cbm_arena_strndup(&arena, bytes, len);
|
||||
status = source->bytes ? snapshot_parse(&arena, cfg) : CBM_USERCONFIG_SNAPSHOT_OOM;
|
||||
}
|
||||
if (status != CBM_USERCONFIG_SNAPSHOT_OK) {
|
||||
cbm_arena_destroy(&arena);
|
||||
return status;
|
||||
}
|
||||
userconfig_source_digest("disabled-frozen-v1", NULL, 0, cfg->global_source_sha256);
|
||||
userconfig_source_digest(state == CBM_USERCONFIG_SOURCE_ABSENT ? "missing-or-unreadable"
|
||||
: "present",
|
||||
source->bytes, len, cfg->project_source_sha256);
|
||||
source->owns_config_arena = true;
|
||||
source->arena = arena;
|
||||
*out = cfg;
|
||||
return CBM_USERCONFIG_SNAPSHOT_OK;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,17 @@ typedef struct {
|
||||
CBMLanguage lang; /* resolved language enum */
|
||||
} cbm_userext_t;
|
||||
|
||||
typedef struct cbm_userconfig_source cbm_userconfig_source_t;
|
||||
typedef enum {
|
||||
CBM_USERCONFIG_SOURCE_ERROR = 0,
|
||||
CBM_USERCONFIG_SOURCE_ABSENT,
|
||||
CBM_USERCONFIG_SOURCE_PRESENT
|
||||
} cbm_userconfig_source_state_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_userext_t *entries; /* heap-allocated array */
|
||||
int count; /* number of entries */
|
||||
cbm_userext_t *entries; /* heap-allocated array */
|
||||
int count; /* number of entries */
|
||||
cbm_userconfig_source_t *project_source; /* optional owned project snapshot */
|
||||
/* Digests of the exact bytes/state consumed by cbm_userconfig_load(). */
|
||||
char global_source_sha256[CBM_SHA256_HEX_LEN + 1];
|
||||
char project_source_sha256[CBM_SHA256_HEX_LEN + 1];
|
||||
@@ -46,6 +54,18 @@ typedef struct {
|
||||
*/
|
||||
cbm_userconfig_t *cbm_userconfig_load(const char *repo_path);
|
||||
|
||||
/* Additive indexing loader: retain exactly the project bytes used for extension
|
||||
* parsing and project_source_sha256. Global configuration keeps legacy behavior.
|
||||
* At most 64 KiB; actual absence is distinct from nonregular/read/close/size errors.
|
||||
* NULL means allocation failure; inspect the snapshot state for other failures.
|
||||
* Empty or malformed JSON remains PRESENT for downstream strict validation. */
|
||||
cbm_userconfig_t *cbm_userconfig_load_with_source(const char *repo_path);
|
||||
|
||||
/* The PRESENT byte view belongs to cfg and remains valid until free. Outputs
|
||||
* are cleared for ABSENT/ERROR. A legacy loader result has no snapshot: ERROR. */
|
||||
cbm_userconfig_source_state_t cbm_userconfig_project_source(const cbm_userconfig_t *cfg,
|
||||
const char **bytes, size_t *len);
|
||||
|
||||
/*
|
||||
* Look up a file extension in the user config.
|
||||
* ext: extension including dot, e.g. ".blade.php"
|
||||
@@ -56,6 +76,22 @@ CBMLanguage cbm_userconfig_lookup(const cbm_userconfig_t *cfg, const char *ext);
|
||||
/* Free a cbm_userconfig_t returned by cbm_userconfig_load. NULL-safe. */
|
||||
void cbm_userconfig_free(cbm_userconfig_t *cfg);
|
||||
|
||||
/* Pure project-only snapshot; never reads global/home/project paths. ABSENT
|
||||
* requires NULL/0; PRESENT requires a strict object of 1..65536 bytes. Consumed
|
||||
* extra_extensions are strict (duplicates/types/NUL rejected, known languages).
|
||||
* Copies bytes/entries into an owned arena; release through cbm_userconfig_free.
|
||||
* Clears *out before validation and never exposes a partial config. */
|
||||
typedef enum {
|
||||
CBM_USERCONFIG_SNAPSHOT_OK = 0,
|
||||
CBM_USERCONFIG_SNAPSHOT_INVALID,
|
||||
CBM_USERCONFIG_SNAPSHOT_UNSUPPORTED,
|
||||
CBM_USERCONFIG_SNAPSHOT_LIMIT,
|
||||
CBM_USERCONFIG_SNAPSHOT_OOM
|
||||
} cbm_userconfig_snapshot_status_t;
|
||||
|
||||
cbm_userconfig_snapshot_status_t cbm_userconfig_from_project_bytes(
|
||||
cbm_userconfig_source_state_t state, const void *bytes, size_t len, cbm_userconfig_t **out);
|
||||
|
||||
/* ── Integration hook ───────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
|
||||
@@ -74,13 +74,21 @@ bool cbm_git_env_entry_is_repo_local(const char *entry) {
|
||||
return git_env_name_is_repo_local(entry, name_len);
|
||||
}
|
||||
|
||||
/* Explicit diff flags must not be overridden by the caller's environment.
|
||||
* Keep the public repository-local classification and its Git-derived list
|
||||
* unchanged; this extra removal belongs only to child construction. */
|
||||
static bool git_env_name_should_strip(const char *name, size_t name_len) {
|
||||
return git_env_name_is_repo_local(name, name_len) ||
|
||||
git_env_name_equals(name, name_len, "GIT_DIFF_OPTS");
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
/* Wide entry name → is it repository-local? Names we match are ASCII, so any
|
||||
/* Wide entry name → should the child omit it? Names we match are ASCII, so any
|
||||
* non-ASCII code unit is an immediate mismatch. Windows keeps per-drive cwd
|
||||
* entries ("=C:=C:\\dir") whose name starts with '='; the search for the
|
||||
* separator starts at index 1 so they are kept untouched. */
|
||||
static bool git_env_wide_entry_is_repo_local(const wchar_t *entry) {
|
||||
static bool git_env_wide_entry_should_strip(const wchar_t *entry) {
|
||||
char name[64] = {0};
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; entry[i] && (i == 0 || entry[i] != L'='); i++) {
|
||||
@@ -89,7 +97,7 @@ static bool git_env_wide_entry_is_repo_local(const wchar_t *entry) {
|
||||
}
|
||||
name[n++] = (char)entry[i];
|
||||
}
|
||||
return git_env_name_is_repo_local(name, n);
|
||||
return git_env_name_should_strip(name, n);
|
||||
}
|
||||
|
||||
wchar_t *cbm_git_child_env_block(void) {
|
||||
@@ -99,7 +107,7 @@ wchar_t *cbm_git_child_env_block(void) {
|
||||
}
|
||||
size_t kept = 0;
|
||||
for (const wchar_t *e = current; *e; e += wcslen(e) + 1) {
|
||||
if (!git_env_wide_entry_is_repo_local(e)) {
|
||||
if (!git_env_wide_entry_should_strip(e)) {
|
||||
kept += wcslen(e) + 1;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +119,7 @@ wchar_t *cbm_git_child_env_block(void) {
|
||||
}
|
||||
size_t pos = 0;
|
||||
for (const wchar_t *e = current; *e; e += wcslen(e) + 1) {
|
||||
if (!git_env_wide_entry_is_repo_local(e)) {
|
||||
if (!git_env_wide_entry_should_strip(e)) {
|
||||
size_t len = wcslen(e) + 1;
|
||||
memcpy(block + pos, e, len * sizeof(wchar_t));
|
||||
pos += len;
|
||||
@@ -127,11 +135,17 @@ wchar_t *cbm_git_child_env_block(void) {
|
||||
|
||||
#else
|
||||
|
||||
static bool git_env_entry_should_strip(const char *entry) {
|
||||
const char *eq = strchr(entry, '=');
|
||||
size_t name_len = eq ? (size_t)(eq - entry) : strlen(entry);
|
||||
return git_env_name_should_strip(entry, name_len);
|
||||
}
|
||||
|
||||
char **cbm_git_child_envp(void) {
|
||||
size_t count = 0;
|
||||
size_t bytes = 0;
|
||||
for (char **e = environ; e && *e; e++) {
|
||||
if (!cbm_git_env_entry_is_repo_local(*e)) {
|
||||
if (!git_env_entry_should_strip(*e)) {
|
||||
count++;
|
||||
bytes += strlen(*e) + 1;
|
||||
}
|
||||
@@ -144,7 +158,7 @@ char **cbm_git_child_envp(void) {
|
||||
char *strings = (char *)envp + table;
|
||||
size_t slot = 0;
|
||||
for (char **e = environ; e && *e && slot < count; e++) {
|
||||
if (!cbm_git_env_entry_is_repo_local(*e)) {
|
||||
if (!git_env_entry_should_strip(*e)) {
|
||||
size_t len = strlen(*e) + 1;
|
||||
memcpy(strings, *e, len);
|
||||
envp[slot++] = strings;
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
* would otherwise resolve every indexed project against the CALLER's repository
|
||||
* (a non-git directory reported as is_git=true, history and branch data from
|
||||
* the wrong repo). Every git spawn therefore gets a child environment with
|
||||
* exactly the variables `git rev-parse --local-env-vars` prints removed. The
|
||||
* scrub is per spawn: cbm's own environment is never modified (unsetenv in the
|
||||
* the variables `git rev-parse --local-env-vars` prints removed. GIT_DIFF_OPTS
|
||||
* is also removed so it cannot override explicit unified-context flags.
|
||||
* The scrub is per spawn: cbm's own environment is never modified (unsetenv in the
|
||||
* parent would race every other thread reading the environment).
|
||||
*/
|
||||
#ifndef CBM_GIT_ENV_H
|
||||
@@ -31,12 +32,14 @@ bool cbm_git_env_entry_is_repo_local(const char *entry);
|
||||
|
||||
#ifdef _WIN32
|
||||
/* A CREATE_UNICODE_ENVIRONMENT block: the current environment minus the
|
||||
* repository-local git variables. One allocation; release with cbm_git_child_env_free().
|
||||
* repository-local git variables and GIT_DIFF_OPTS. One allocation; release with
|
||||
* cbm_git_child_env_free().
|
||||
* NULL on allocation/snapshot failure. */
|
||||
wchar_t *cbm_git_child_env_block(void);
|
||||
#else
|
||||
/* A NULL-terminated envp: the current environment minus the repository-local
|
||||
* git variables. Pointers and strings live in ONE allocation; release with
|
||||
* git variables and GIT_DIFF_OPTS. Pointers and strings live in ONE allocation;
|
||||
* release with
|
||||
* cbm_git_child_env_free(). NULL on allocation failure. */
|
||||
char **cbm_git_child_envp(void);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "foundation/path_syntax_internal.h"
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <limits.h>
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
static unsigned char path_upper(unsigned char c) {
|
||||
return c >= 'a' && c <= 'z' ? (unsigned char)(c - 'a' + 'A') : c;
|
||||
}
|
||||
|
||||
static bool path_device(const unsigned char *p, size_t n) {
|
||||
size_t base = 0;
|
||||
while (base < n && p[base] != '.')
|
||||
base++;
|
||||
if (base < 3)
|
||||
return false;
|
||||
unsigned char a = path_upper(p[0]), b = path_upper(p[1]), d = path_upper(p[2]);
|
||||
if (base == 3)
|
||||
return (a == 'C' && b == 'O' && d == 'N') || (a == 'P' && b == 'R' && d == 'N') ||
|
||||
(a == 'A' && b == 'U' && d == 'X') || (a == 'N' && b == 'U' && d == 'L');
|
||||
if (!((a == 'C' && b == 'O' && d == 'M') || (a == 'L' && b == 'P' && d == 'T')))
|
||||
return false;
|
||||
if (base == 4)
|
||||
return p[3] >= '1' && p[3] <= '9';
|
||||
return base == 5 && p[3] == 0xc2 && (p[4] == 0xb9 || p[4] == 0xb2 || p[4] == 0xb3);
|
||||
}
|
||||
|
||||
cbm_path_syntax_result_t cbm_path_windows_component(const unsigned char *component, size_t length) {
|
||||
if (!component || !length || length > CBM_PATH_SYNTAX_MAX_BYTES)
|
||||
return CBM_PATH_SYNTAX_INVALID;
|
||||
if (component[length - 1] == '.' || component[length - 1] == ' ' ||
|
||||
path_device(component, length))
|
||||
return CBM_PATH_SYNTAX_DEVICE_OR_TRIMMED;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
unsigned char ch = component[i];
|
||||
if (ch < 32 || ch == '\\' || ch == ':' || ch == '<' || ch == '>' || ch == '"' ||
|
||||
ch == '|' || ch == '?' || ch == '*')
|
||||
return CBM_PATH_SYNTAX_COMPONENT_BYTE;
|
||||
}
|
||||
return CBM_PATH_SYNTAX_OK;
|
||||
}
|
||||
|
||||
cbm_path_syntax_result_t cbm_path_windows_absolute(const char *path, size_t length) {
|
||||
if (!path || !length || length > CBM_PATH_SYNTAX_MAX_BYTES)
|
||||
return CBM_PATH_SYNTAX_INVALID;
|
||||
bool drive = length >= 3 &&
|
||||
((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) &&
|
||||
path[1] == ':' && (path[2] == '/' || path[2] == '\\');
|
||||
bool unc = length >= 5 && path[0] == '\\' && path[1] == '\\' && path[2] != '?' &&
|
||||
path[2] != '.' && path[2] != '\\';
|
||||
if (!drive && !unc)
|
||||
return CBM_PATH_SYNTAX_NOT_ABSOLUTE;
|
||||
if (unc) {
|
||||
size_t i = 2;
|
||||
while (i < length && path[i] != '\\')
|
||||
i++;
|
||||
if (i + 1 >= length || path[i + 1] == '\\')
|
||||
return CBM_PATH_SYNTAX_INCOMPLETE_UNC;
|
||||
}
|
||||
return CBM_PATH_SYNTAX_OK;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static bool path_roundtrip_args(const char *source, size_t length, const wchar_t *wide,
|
||||
size_t wide_capacity, const char *roundtrip,
|
||||
size_t roundtrip_capacity) {
|
||||
if (!source || !wide || !roundtrip || !length || length > CBM_PATH_SYNTAX_MAX_BYTES ||
|
||||
!wide_capacity || wide_capacity > INT_MAX || !roundtrip_capacity ||
|
||||
roundtrip_capacity > INT_MAX)
|
||||
return false;
|
||||
return source[length] == '\0' && !memchr(source, '\0', length);
|
||||
}
|
||||
|
||||
cbm_path_syntax_result_t cbm_path_windows_roundtrip(const char *source, size_t length,
|
||||
wchar_t *wide, size_t wide_capacity,
|
||||
char *roundtrip, size_t roundtrip_capacity) {
|
||||
if (!path_roundtrip_args(source, length, wide, wide_capacity, roundtrip, roundtrip_capacity))
|
||||
return CBM_PATH_SYNTAX_INVALID;
|
||||
/* length <=4095 establishes the checked int conversion including terminal NUL. */
|
||||
int source_count = (int)(length + 1);
|
||||
int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, source, source_count, wide,
|
||||
(int)wide_capacity);
|
||||
if (!count)
|
||||
return CBM_PATH_SYNTAX_UTF8;
|
||||
int bytes = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide, count, roundtrip,
|
||||
(int)roundtrip_capacity, NULL, NULL);
|
||||
if (bytes != source_count || memcmp(source, roundtrip, length + 1))
|
||||
return CBM_PATH_SYNTAX_ROUNDTRIP;
|
||||
return CBM_PATH_SYNTAX_OK;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Internal lexical checks shared by discovery and the pinned-tree adapter.
|
||||
* No allocation, IO, normalization, owner, callback or retained pointers. */
|
||||
#ifndef CBM_PATH_SYNTAX_INTERNAL_H
|
||||
#define CBM_PATH_SYNTAX_INTERNAL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#ifdef _WIN32
|
||||
#include <wchar.h>
|
||||
#endif
|
||||
|
||||
enum { CBM_PATH_SYNTAX_MAX_BYTES = 4095 };
|
||||
typedef enum {
|
||||
CBM_PATH_SYNTAX_OK = 0,
|
||||
CBM_PATH_SYNTAX_INVALID,
|
||||
CBM_PATH_SYNTAX_DEVICE_OR_TRIMMED,
|
||||
CBM_PATH_SYNTAX_COMPONENT_BYTE,
|
||||
CBM_PATH_SYNTAX_UTF8,
|
||||
CBM_PATH_SYNTAX_ROUNDTRIP,
|
||||
CBM_PATH_SYNTAX_NOT_ABSOLUTE,
|
||||
CBM_PATH_SYNTAX_INCOMPLETE_UNC
|
||||
} cbm_path_syntax_result_t;
|
||||
|
||||
/* Byte spans have length 1..MAX_BYTES. No terminator is accessed by these two
|
||||
* functions. The component caller supplies one already-separated component;
|
||||
* relative-path structure (including slash/dot components) belongs to caller.
|
||||
* Device/trailing-dot-or-space rejection precedes forbidden-byte rejection.
|
||||
* Absolute checks preserve the ordinary drive/backslash-UNC lexical domain;
|
||||
* these functions do not establish native existence, aliases or permission. */
|
||||
cbm_path_syntax_result_t cbm_path_windows_component(const unsigned char *component, size_t length);
|
||||
cbm_path_syntax_result_t cbm_path_windows_absolute(const char *path, size_t length);
|
||||
|
||||
#ifdef _WIN32
|
||||
/* source has exactly length non-NUL bytes followed by an accessible NUL;
|
||||
* length is 1..MAX_BYTES. Buffers are distinct, caller-owned writable spans.
|
||||
* Capacities are element counts, positive and <=INT_MAX; wide counts wchar_t.
|
||||
* Strict UTF8 -> wide -> UTF8 conversion preserves exact bytes including NUL.
|
||||
* INVALID is a bad pointer/length/capacity/terminated-span argument; UTF8 is a
|
||||
* forward conversion failure; ROUNDTRIP is reverse failure/length/byte mismatch.
|
||||
* Outputs may contain partial work on non-OK; only OK certifies conversion.
|
||||
* Calls are bounded, synchronous and retain neither input nor output storage. */
|
||||
cbm_path_syntax_result_t cbm_path_windows_roundtrip(const char *source, size_t length,
|
||||
wchar_t *wide, size_t wide_capacity,
|
||||
char *roundtrip, size_t roundtrip_capacity);
|
||||
#endif
|
||||
#endif
|
||||
@@ -8,8 +8,8 @@
|
||||
* - parse_or_expr, parse_xor_expr, parse_and_expr, parse_not_expr
|
||||
* - parse_atom_expr, parse_post_where, cbm_parse
|
||||
*
|
||||
* Cypher expression evaluator (bounded by WHERE clause depth ~5):
|
||||
* - eval_expr
|
||||
* Cypher expression evaluators (bounded by WHERE clause depth ~5):
|
||||
* - eval_expr, eval_expr_partial
|
||||
*
|
||||
* Glob pattern matcher (bounded by pattern nesting ~3):
|
||||
* - glob_match, glob_match_star, glob_match_doublestar
|
||||
@@ -25,9 +25,9 @@
|
||||
* To add a function: add it below AND add NOLINT(misc-no-recursion) on
|
||||
* the function definition line. The lint gate verifies both match.
|
||||
*/
|
||||
#define CBM_RECURSION_WHITELIST \
|
||||
"parse_or_expr", "parse_xor_expr", "parse_and_expr", "parse_not_expr", "parse_atom_expr", \
|
||||
"parse_post_where", "cbm_parse", "eval_expr", "glob_match", "glob_match_star", \
|
||||
"glob_match_doublestar", "glob_match_doublestar_slash", "glob_match_doublestar_any", \
|
||||
"parse_bool_expr", "parse_bool_atom", "r_collect_imports", \
|
||||
#define CBM_RECURSION_WHITELIST \
|
||||
"parse_or_expr", "parse_xor_expr", "parse_and_expr", "parse_not_expr", "parse_atom_expr", \
|
||||
"parse_post_where", "cbm_parse", "eval_expr", "eval_expr_partial", "glob_match", \
|
||||
"glob_match_star", "glob_match_doublestar", "glob_match_doublestar_slash", \
|
||||
"glob_match_doublestar_any", "parse_bool_expr", "parse_bool_atom", "r_collect_imports", \
|
||||
"find_first_descendant_by_kind", "find_first_descendant_of"
|
||||
|
||||
+284
-55
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
#include "subprocess.h"
|
||||
|
||||
#include "arena.h"
|
||||
#include "compat.h" /* cbm_nanosleep */
|
||||
#include "compat_fs.h"
|
||||
#include "git_env.h" /* strip_git_repo_env: scrubbed child environment for git */
|
||||
@@ -411,6 +412,9 @@ struct cbm_subprocess {
|
||||
size_t argc;
|
||||
char *windows_cmd_payload;
|
||||
char *log_file;
|
||||
CBMArena stdout_arena;
|
||||
char *stdout_file;
|
||||
char *stdin_file;
|
||||
cbm_proc_log_cb on_log_line;
|
||||
void *log_ud;
|
||||
int quiet_timeout_ms;
|
||||
@@ -471,6 +475,7 @@ static void cbm_subprocess_free_config(cbm_subprocess_t *process) {
|
||||
free(process->bin);
|
||||
free(process->windows_cmd_payload);
|
||||
free(process->log_file);
|
||||
cbm_arena_destroy(&process->stdout_arena);
|
||||
#ifndef _WIN32
|
||||
cbm_git_child_env_free(process->envp);
|
||||
#endif
|
||||
@@ -481,6 +486,16 @@ static cbm_subprocess_t *cbm_subprocess_copy_opts(const cbm_proc_opts_t *opts) {
|
||||
if (!opts || !opts->bin || !opts->bin[0]) {
|
||||
return NULL;
|
||||
}
|
||||
if (opts->stdout_file && (!opts->stdout_file[0] ||
|
||||
(opts->log_file && strcmp(opts->stdout_file, opts->log_file) == 0))) {
|
||||
return NULL;
|
||||
}
|
||||
if (opts->stdin_file &&
|
||||
(!opts->stdin_file[0] ||
|
||||
(opts->log_file && strcmp(opts->stdin_file, opts->log_file) == 0) ||
|
||||
(opts->stdout_file && strcmp(opts->stdin_file, opts->stdout_file) == 0))) {
|
||||
return NULL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (opts->windows_cmd_payload &&
|
||||
(!opts->windows_cmd_payload[0] || opts->argv || !cbm_win_cmd_path_is_absolute(opts->bin))) {
|
||||
@@ -507,6 +522,7 @@ static cbm_subprocess_t *cbm_subprocess_copy_opts(const cbm_proc_opts_t *opts) {
|
||||
if (!process) {
|
||||
return NULL;
|
||||
}
|
||||
cbm_arena_init_lazy(&process->stdout_arena, 256);
|
||||
process->bin = cbm_strdup(opts->bin);
|
||||
process->windows_cmd_payload =
|
||||
opts->windows_cmd_payload ? cbm_strdup(opts->windows_cmd_payload) : NULL;
|
||||
@@ -532,6 +548,20 @@ static cbm_subprocess_t *cbm_subprocess_copy_opts(const cbm_proc_opts_t *opts) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (opts->stdout_file) {
|
||||
process->stdout_file = cbm_arena_strdup(&process->stdout_arena, opts->stdout_file);
|
||||
if (!process->stdout_file) {
|
||||
cbm_subprocess_free_config(process);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (opts->stdin_file) {
|
||||
process->stdin_file = cbm_arena_strdup(&process->stdout_arena, opts->stdin_file);
|
||||
if (!process->stdin_file) {
|
||||
cbm_subprocess_free_config(process);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
process->on_log_line = opts->on_log_line;
|
||||
process->log_ud = opts->log_ud;
|
||||
process->quiet_timeout_ms = opts->quiet_timeout_ms;
|
||||
@@ -649,17 +679,80 @@ static cbm_proc_poll_t cbm_subprocess_finish_failed(cbm_subprocess_t *process,
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static void cbm_win_close_spawn_handles(HANDLE nul, HANDLE log, LPPROC_THREAD_ATTRIBUTE_LIST attrs,
|
||||
bool attrs_init) {
|
||||
/* Match cbm_path_to_wide's file-path spelling using the process arena. Existing
|
||||
* option conversions retain their legacy heap ownership. */
|
||||
static wchar_t *cbm_win_stdin_path(cbm_subprocess_t *process) {
|
||||
int length =
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, process->stdin_file, -1, NULL, 0);
|
||||
if (length <= 0 || length > 32767) {
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *wide = cbm_arena_alloc(&process->stdout_arena, (size_t)length * sizeof(*wide));
|
||||
if (!wide || MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, process->stdin_file, -1, wide,
|
||||
length) != length) {
|
||||
return NULL;
|
||||
}
|
||||
size_t input_length = (size_t)length - 1U;
|
||||
if (cbm_win_path_has_namespace_prefix(wide, input_length)) {
|
||||
cbm_win_path_normalize_wide_separators(wide);
|
||||
return wide;
|
||||
}
|
||||
if (input_length < 240U) {
|
||||
return wide;
|
||||
}
|
||||
DWORD needed = GetFullPathNameW(wide, 0, NULL, NULL);
|
||||
if (needed == 0U || needed > 32767U) {
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *full = cbm_arena_alloc(&process->stdout_arena, (size_t)needed * sizeof(*full));
|
||||
DWORD copied = full ? GetFullPathNameW(wide, needed, full, NULL) : 0U;
|
||||
if (copied == 0U || copied >= needed) {
|
||||
return NULL;
|
||||
}
|
||||
cbm_win_path_normalize_wide_separators(full);
|
||||
bool drive_absolute =
|
||||
copied >= 3U &&
|
||||
((full[0] >= L'A' && full[0] <= L'Z') || (full[0] >= L'a' && full[0] <= L'z')) &&
|
||||
full[1] == L':' && full[2] == L'\\';
|
||||
bool unc_absolute = copied >= 5U && full[0] == L'\\' && full[1] == L'\\';
|
||||
if (!drive_absolute && !unc_absolute) {
|
||||
return NULL;
|
||||
}
|
||||
const wchar_t *prefix = drive_absolute ? L"\\\\?\\" : L"\\\\?\\UNC\\";
|
||||
size_t prefix_length = drive_absolute ? 4U : 8U;
|
||||
size_t tail_offset = drive_absolute ? 0U : 2U;
|
||||
size_t tail_length = (size_t)copied - tail_offset;
|
||||
size_t extended_length = prefix_length + tail_length;
|
||||
if (extended_length >= 32767U) {
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *prefixed =
|
||||
cbm_arena_alloc(&process->stdout_arena, (extended_length + 1U) * sizeof(*prefixed));
|
||||
if (!prefixed) {
|
||||
return NULL;
|
||||
}
|
||||
wmemcpy(prefixed, prefix, prefix_length);
|
||||
wmemcpy(prefixed + prefix_length, full + tail_offset, tail_length + 1U);
|
||||
return prefixed;
|
||||
}
|
||||
|
||||
static void cbm_win_close_spawn_handles(HANDLE nul, HANDLE input, HANDLE log, HANDLE stdout_capture,
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attrs, bool attrs_init) {
|
||||
if (attrs) {
|
||||
if (attrs_init) {
|
||||
DeleteProcThreadAttributeList(attrs);
|
||||
}
|
||||
free(attrs);
|
||||
}
|
||||
if (stdout_capture != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(stdout_capture);
|
||||
}
|
||||
if (log != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(log);
|
||||
}
|
||||
if (input != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(input);
|
||||
}
|
||||
if (nul != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(nul);
|
||||
}
|
||||
@@ -675,12 +768,19 @@ static int cbm_subprocess_spawn_win(cbm_subprocess_t *process) {
|
||||
if (!built) {
|
||||
return -1;
|
||||
}
|
||||
/* Everything a failure must release, so every failure leaves through
|
||||
* the one exit at the end. */
|
||||
HANDLE job = NULL;
|
||||
HANDLE nul = INVALID_HANDLE_VALUE;
|
||||
HANDLE input = INVALID_HANDLE_VALUE;
|
||||
HANDLE log = INVALID_HANDLE_VALUE;
|
||||
HANDLE stdout_capture = INVALID_HANDLE_VALUE;
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attrs = NULL;
|
||||
bool attrs_init = false;
|
||||
wchar_t *wbin = cbm_utf8_to_wide(process->bin);
|
||||
wchar_t *wcmdline = cbm_utf8_to_wide(cmdline);
|
||||
if (!wbin || !wcmdline) {
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
return -1;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits;
|
||||
@@ -690,29 +790,39 @@ static int cbm_subprocess_spawn_win(cbm_subprocess_t *process) {
|
||||
limits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_JOB_MEMORY;
|
||||
limits.JobMemoryLimit = (SIZE_T)process->memory_limit_bytes;
|
||||
}
|
||||
HANDLE job = CreateJobObjectW(NULL, NULL);
|
||||
job = CreateJobObjectW(NULL, NULL);
|
||||
if (!job ||
|
||||
!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) {
|
||||
if (job) {
|
||||
CloseHandle(job);
|
||||
}
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
return -1;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
SECURITY_ATTRIBUTES security;
|
||||
ZeroMemory(&security, sizeof(security));
|
||||
security.nLength = sizeof(security);
|
||||
security.bInheritHandle = TRUE;
|
||||
HANDLE nul = CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &security, OPEN_EXISTING, 0, NULL);
|
||||
HANDLE log = INVALID_HANDLE_VALUE;
|
||||
nul = CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, OPEN_EXISTING, 0, NULL);
|
||||
if (nul == INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(job);
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
return -1;
|
||||
goto fail;
|
||||
}
|
||||
if (process->stdin_file) {
|
||||
wchar_t *winput = cbm_win_stdin_path(process);
|
||||
/* Validate before any output can be truncated; recheck the opened
|
||||
* handle rather than relying only on path attributes. */
|
||||
DWORD attributes = winput ? GetFileAttributesW(winput) : INVALID_FILE_ATTRIBUTES;
|
||||
if (attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
!(attributes &
|
||||
(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DEVICE))) {
|
||||
input = CreateFileW(winput, GENERIC_READ, FILE_SHARE_READ, &security, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
}
|
||||
BY_HANDLE_FILE_INFORMATION input_info;
|
||||
if (input == INVALID_HANDLE_VALUE || GetFileType(input) != FILE_TYPE_DISK ||
|
||||
!GetFileInformationByHandle(input, &input_info) ||
|
||||
(input_info.dwFileAttributes &
|
||||
(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DEVICE))) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
if (process->log_file) {
|
||||
wchar_t *wlog = cbm_path_to_wide(process->log_file);
|
||||
@@ -727,41 +837,53 @@ static int cbm_subprocess_spawn_win(cbm_subprocess_t *process) {
|
||||
free(wlog);
|
||||
}
|
||||
if (log == INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(nul);
|
||||
CloseHandle(job);
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
return -1;
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLE inherit[2];
|
||||
if (process->stdout_file) {
|
||||
wchar_t *wstdout = cbm_path_to_wide(process->stdout_file);
|
||||
if (wstdout) {
|
||||
stdout_capture = CreateFileW(wstdout, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
free(wstdout);
|
||||
}
|
||||
if (stdout_capture == INVALID_HANDLE_VALUE) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLE inherit[4];
|
||||
SIZE_T inherit_count = 0;
|
||||
inherit[inherit_count++] = nul;
|
||||
if (input != INVALID_HANDLE_VALUE) {
|
||||
inherit[inherit_count++] = input;
|
||||
}
|
||||
if (log != INVALID_HANDLE_VALUE) {
|
||||
inherit[inherit_count++] = log;
|
||||
}
|
||||
if (stdout_capture != INVALID_HANDLE_VALUE) {
|
||||
inherit[inherit_count++] = stdout_capture;
|
||||
}
|
||||
SIZE_T attrs_size = 0;
|
||||
(void)InitializeProcThreadAttributeList(NULL, 1, 0, &attrs_size);
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attrs = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attrs_size);
|
||||
bool attrs_init = attrs && InitializeProcThreadAttributeList(attrs, 1, 0, &attrs_size);
|
||||
attrs = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attrs_size);
|
||||
attrs_init = attrs && InitializeProcThreadAttributeList(attrs, 1, 0, &attrs_size);
|
||||
bool attrs_ready = attrs_init && UpdateProcThreadAttribute(
|
||||
attrs, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherit,
|
||||
inherit_count * sizeof(inherit[0]), NULL, NULL);
|
||||
if (!attrs_ready) {
|
||||
cbm_win_close_spawn_handles(nul, log, attrs, attrs_init);
|
||||
CloseHandle(job);
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
return -1;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
STARTUPINFOEXW startup;
|
||||
ZeroMemory(&startup, sizeof(startup));
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput = nul;
|
||||
startup.StartupInfo.hStdOutput = log != INVALID_HANDLE_VALUE ? log : nul;
|
||||
startup.StartupInfo.hStdInput = input != INVALID_HANDLE_VALUE ? input : nul;
|
||||
startup.StartupInfo.hStdOutput = stdout_capture != INVALID_HANDLE_VALUE
|
||||
? stdout_capture
|
||||
: (log != INVALID_HANDLE_VALUE ? log : nul);
|
||||
startup.StartupInfo.hStdError = log != INVALID_HANDLE_VALUE ? log : nul;
|
||||
startup.lpAttributeList = attrs;
|
||||
|
||||
@@ -778,7 +900,7 @@ static int cbm_subprocess_spawn_win(cbm_subprocess_t *process) {
|
||||
CreateProcessW(wbin, wcmdline, NULL, NULL, TRUE, flags, (LPVOID)env, NULL,
|
||||
&startup.StartupInfo, &child);
|
||||
cbm_git_child_env_free(env); /* CreateProcessW copied the block into the child */
|
||||
cbm_win_close_spawn_handles(nul, log, attrs, attrs_init);
|
||||
cbm_win_close_spawn_handles(nul, input, log, stdout_capture, attrs, attrs_init);
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
if (!created) {
|
||||
@@ -810,6 +932,15 @@ static int cbm_subprocess_spawn_win(cbm_subprocess_t *process) {
|
||||
process->process_id = child.dwProcessId;
|
||||
process->job = job;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
cbm_win_close_spawn_handles(nul, input, log, stdout_capture, attrs, attrs_init);
|
||||
if (job) {
|
||||
CloseHandle(job);
|
||||
}
|
||||
free(wbin);
|
||||
free(wcmdline);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool cbm_win_job_active(cbm_subprocess_t *process, bool *known) {
|
||||
@@ -989,6 +1120,14 @@ static int g_force_spawn_eagain = 0;
|
||||
void cbm_subprocess_force_spawn_eagain_for_testing(int attempts) {
|
||||
g_force_spawn_eagain = attempts > 0 ? attempts : 0;
|
||||
}
|
||||
static int g_hold_until_exit = 0;
|
||||
void cbm_subprocess_hold_parent_until_child_exits_for_testing(int spawns) {
|
||||
g_hold_until_exit = spawns > 0 ? spawns : 0;
|
||||
}
|
||||
static int g_observe_exiting = 0;
|
||||
void cbm_subprocess_observe_exiting_child_for_testing(int spawns) {
|
||||
g_observe_exiting = spawns > 0 ? spawns : 0;
|
||||
}
|
||||
int cbm_subprocess_pending_spawn_eagain_for_testing(void) {
|
||||
return g_force_spawn_eagain;
|
||||
}
|
||||
@@ -1133,7 +1272,8 @@ cbm_fd_close_strategy_t cbm_subprocess_close_fds_from_for_testing(int lowfd, lon
|
||||
/* fork+exec child setup. On Apple this runs ONLY for the exec-failure
|
||||
* fallback (see cbm_posix_spawn_apple), which preserves the documented
|
||||
* "bogus binary => child exits 127" contract across platforms. */
|
||||
static void cbm_posix_child_exec(cbm_subprocess_t *process, int input, int output, long max_fd) {
|
||||
static void cbm_posix_child_exec(cbm_subprocess_t *process, int input, int output, int error,
|
||||
long max_fd) {
|
||||
if (setpgid(0, 0) < 0) {
|
||||
_exit(127);
|
||||
}
|
||||
@@ -1141,8 +1281,8 @@ static void cbm_posix_child_exec(cbm_subprocess_t *process, int input, int outpu
|
||||
|
||||
/* Never let a worker consume the MCP transport inherited as stdin. Only
|
||||
* async-signal-safe calls are used between fork and exec. */
|
||||
if (input < 0 || output < 0 || dup2(input, STDIN_FILENO) < 0 ||
|
||||
dup2(output, STDOUT_FILENO) < 0 || dup2(output, STDERR_FILENO) < 0) {
|
||||
if (input < 0 || output < 0 || error < 0 || dup2(input, STDIN_FILENO) < 0 ||
|
||||
dup2(output, STDOUT_FILENO) < 0 || dup2(error, STDERR_FILENO) < 0) {
|
||||
_exit(127);
|
||||
}
|
||||
if (input > STDERR_FILENO) {
|
||||
@@ -1200,7 +1340,8 @@ static int cbm_posix_fd_at_least_three(int fd) {
|
||||
* the child's close-everything loop, and the three dup2'd fds stay open
|
||||
* because dup2 clears close-on-exec.
|
||||
* posix_spawnp keeps execvp's PATH semantics for a bare tool name. */
|
||||
static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int output, pid_t *pid_out) {
|
||||
static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int output, int error,
|
||||
pid_t *pid_out) {
|
||||
/* posix_spawn is the PRIMARY path on macOS; fork+exec is only the
|
||||
* exec-class fallback. Injection therefore has to live here too, or a test
|
||||
* on macOS exercises nothing. */
|
||||
@@ -1230,7 +1371,7 @@ static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int outpu
|
||||
posix_spawnattr_setsigdefault(&attr, &all_signals) == 0 &&
|
||||
posix_spawn_file_actions_adddup2(&actions, input, STDIN_FILENO) == 0 &&
|
||||
posix_spawn_file_actions_adddup2(&actions, output, STDOUT_FILENO) == 0 &&
|
||||
posix_spawn_file_actions_adddup2(&actions, output, STDERR_FILENO) == 0;
|
||||
posix_spawn_file_actions_adddup2(&actions, error, STDERR_FILENO) == 0;
|
||||
pid_t pid = -1;
|
||||
int rc = configured ? posix_spawnp(&pid, process->bin, &actions, &attr, process->argv,
|
||||
process->envp ? process->envp : environ)
|
||||
@@ -1263,12 +1404,49 @@ static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int outpu
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Our own unreaped child that setpgid/getpgid cannot find (ESRCH) is not
|
||||
* running: it has exited, or is exiting -- macOS stops finding a child as it
|
||||
* starts to exit, before waitid can report it. Wait until it is reapable;
|
||||
* the wait ends with its exit, and WNOWAIT leaves it for the supervision to
|
||||
* collect with its exit status. */
|
||||
static bool cbm_posix_child_reapable(pid_t pid) {
|
||||
for (;;) {
|
||||
siginfo_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT) == 0) {
|
||||
return info.si_pid == pid;
|
||||
}
|
||||
if (errno != EINTR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
int input_flags = O_RDONLY;
|
||||
#ifdef O_CLOEXEC
|
||||
input_flags |= O_CLOEXEC;
|
||||
#endif
|
||||
int input = cbm_posix_fd_at_least_three(open("/dev/null", input_flags));
|
||||
if (process->stdin_file) {
|
||||
/* O_NONBLOCK prevents waiting for a FIFO writer before fstat rejects
|
||||
* non-regular input. No output has been opened at this point. */
|
||||
input_flags |= O_NONBLOCK;
|
||||
#ifdef O_NOFOLLOW
|
||||
input_flags |= O_NOFOLLOW;
|
||||
#endif
|
||||
}
|
||||
int input = cbm_posix_fd_at_least_three(
|
||||
open(process->stdin_file ? process->stdin_file : "/dev/null", input_flags));
|
||||
if (input < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (process->stdin_file) {
|
||||
struct stat input_status;
|
||||
if (fstat(input, &input_status) != 0 || !S_ISREG(input_status.st_mode)) {
|
||||
(void)close(input);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
const char *target = process->log_file ? process->log_file : "/dev/null";
|
||||
int output_flags = O_WRONLY | O_CREAT | O_TRUNC;
|
||||
#ifdef O_CLOEXEC
|
||||
@@ -1278,13 +1456,8 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
output_flags |= O_NOFOLLOW;
|
||||
#endif
|
||||
int output = cbm_posix_fd_at_least_three(open(target, output_flags, 0600));
|
||||
if (input < 0 || output < 0) {
|
||||
if (input >= 0) {
|
||||
(void)close(input);
|
||||
}
|
||||
if (output >= 0) {
|
||||
(void)close(output);
|
||||
}
|
||||
if (output < 0) {
|
||||
(void)close(input);
|
||||
return -1;
|
||||
}
|
||||
struct stat output_status;
|
||||
@@ -1300,6 +1473,23 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int stdout_capture = -1;
|
||||
if (process->stdout_file) {
|
||||
stdout_capture =
|
||||
cbm_posix_fd_at_least_three(open(process->stdout_file, output_flags, 0600));
|
||||
struct stat stdout_status;
|
||||
if (stdout_capture < 0 || fstat(stdout_capture, &stdout_status) != 0 ||
|
||||
!S_ISREG(stdout_status.st_mode) || fchmod(stdout_capture, 0600) != 0) {
|
||||
if (stdout_capture >= 0) {
|
||||
(void)close(stdout_capture);
|
||||
}
|
||||
(void)close(input);
|
||||
(void)close(output);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int child_output = stdout_capture >= 0 ? stdout_capture : output;
|
||||
|
||||
long max_fd = sysconf(_SC_OPEN_MAX);
|
||||
if (max_fd < 0 || max_fd > 1048576L) {
|
||||
max_fd = 65536L;
|
||||
@@ -1307,11 +1497,11 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
|
||||
pid_t pid = -1;
|
||||
#ifdef __APPLE__
|
||||
int spawn_rc = cbm_posix_spawn_apple(process, input, output, &pid);
|
||||
int spawn_rc = cbm_posix_spawn_apple(process, input, child_output, output, &pid);
|
||||
for (int attempt = 0; spawn_rc == CBM_SPAWN_RETRY && attempt < CBM_SPAWN_RETRY_ATTEMPTS;
|
||||
attempt++) {
|
||||
cbm_spawn_backoff(attempt);
|
||||
spawn_rc = cbm_posix_spawn_apple(process, input, output, &pid);
|
||||
spawn_rc = cbm_posix_spawn_apple(process, input, child_output, output, &pid);
|
||||
}
|
||||
if (spawn_rc == CBM_SPAWN_RETRY) {
|
||||
spawn_rc = -1; /* still exhausted after backoff: a real failure */
|
||||
@@ -1319,6 +1509,9 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
if (spawn_rc < 0) {
|
||||
(void)close(input);
|
||||
(void)close(output);
|
||||
if (stdout_capture >= 0) {
|
||||
(void)close(stdout_capture);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (spawn_rc > 0) { /* exec-class failure: reproduce the fork+exec 127 */
|
||||
@@ -1326,10 +1519,13 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
if (pid < 0) {
|
||||
(void)close(input);
|
||||
(void)close(output);
|
||||
if (stdout_capture >= 0) {
|
||||
(void)close(stdout_capture);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
cbm_posix_child_exec(process, input, output, max_fd);
|
||||
cbm_posix_child_exec(process, input, child_output, output, max_fd);
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -1337,21 +1533,54 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
|
||||
if (pid < 0) {
|
||||
(void)close(input);
|
||||
(void)close(output);
|
||||
if (stdout_capture >= 0) {
|
||||
(void)close(stdout_capture);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
cbm_posix_child_exec(process, input, output, max_fd);
|
||||
cbm_posix_child_exec(process, input, child_output, output, max_fd);
|
||||
}
|
||||
#endif
|
||||
(void)close(input);
|
||||
(void)close(output);
|
||||
if (stdout_capture >= 0) {
|
||||
(void)close(stdout_capture);
|
||||
}
|
||||
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
if (g_hold_until_exit > 0) {
|
||||
g_hold_until_exit--;
|
||||
siginfo_t held;
|
||||
(void)waitid(P_PID, (id_t)pid, &held, WEXITED | WNOWAIT);
|
||||
}
|
||||
#endif
|
||||
/* Parent and child both establish the group, removing scheduler-order races.
|
||||
* If the child won and already execed, EACCES is accepted only after proving
|
||||
* that its process group is the expected isolated one. */
|
||||
bool contained = setpgid(pid, pid) == 0;
|
||||
* that its process group is the expected isolated one. A child that has
|
||||
* EXITED, or is exiting, is a finished run, not a failed spawn: macOS no
|
||||
* longer shows such an unreaped child to setpgid/getpgid (ESRCH), and on a
|
||||
* loaded machine a short command (`git config`) gets there before the
|
||||
* parent does. Its group was set before exec, by the spawn attributes or by
|
||||
* the child itself; the parent waits until it is reapable, and the
|
||||
* supervision below reaps it with its real exit status. */
|
||||
bool observe_exiting = false;
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
if (g_observe_exiting > 0) {
|
||||
g_observe_exiting--;
|
||||
observe_exiting = true;
|
||||
}
|
||||
#endif
|
||||
bool contained = !observe_exiting && setpgid(pid, pid) == 0;
|
||||
if (observe_exiting) {
|
||||
errno = ESRCH;
|
||||
}
|
||||
if (!contained && (errno == EACCES || errno == EPERM || errno == ESRCH)) {
|
||||
contained = getpgid(pid) == pid;
|
||||
bool missing = errno == ESRCH;
|
||||
contained = !observe_exiting && getpgid(pid) == pid;
|
||||
if (!contained && (missing || errno == ESRCH)) {
|
||||
contained = cbm_posix_child_reapable(pid);
|
||||
}
|
||||
}
|
||||
if (!contained) {
|
||||
(void)kill(pid, SIGKILL);
|
||||
|
||||
@@ -63,8 +63,9 @@ typedef struct {
|
||||
* be an absolute path ending in cmd.exe and argv must be
|
||||
* NULL. The fixed /D /S /V:OFF /C prefix is added while
|
||||
* this payload is copied verbatim for cmd.exe to parse. */
|
||||
const char *log_file; /* child stdout+stderr are redirected here and tailed;
|
||||
* NULL => discard child output, no tailing */
|
||||
const char *log_file; /* stderr and, when stdout_file is NULL, stdout are
|
||||
* redirected here and tailed; NULL => discard those
|
||||
* streams, no tailing */
|
||||
cbm_proc_log_cb on_log_line; /* optional per-line callback */
|
||||
void *log_ud; /* user data for on_log_line */
|
||||
int quiet_timeout_ms; /* <= 0 => no timeout; else kill+HANG after this many
|
||||
@@ -77,6 +78,20 @@ typedef struct {
|
||||
bool strip_git_repo_env; /* child env omits git's repository-local variables
|
||||
* (foundation/git_env.h) — set for every git spawn so
|
||||
* an inherited GIT_DIR never overrides `git -C` */
|
||||
const char *stdout_file; /* optional separate stdout capture; NULL preserves
|
||||
* merged log_file behavior. Must be nonempty and
|
||||
* distinct from log_file; caller supplies private,
|
||||
* non-aliasing paths and owns capture-file cleanup.
|
||||
* Not tailed; quiet_timeout_ms observes log_file only.
|
||||
* delete_log_on_exit never deletes this file. */
|
||||
const char *stdin_file; /* optional private, immutable regular binary input;
|
||||
* NULL => null device, never inherited stdin.
|
||||
* Must be nonempty and non-aliasing with log_file and
|
||||
* stdout_file; exact equal strings are rejected.
|
||||
* Caller closes its writer before spawn and retains
|
||||
* the file until the child tree is quiescent.
|
||||
* The child chooses its CRT binary mode on Windows.
|
||||
* Never modified or deleted by this API. */
|
||||
} cbm_proc_opts_t;
|
||||
|
||||
#define CBM_SUBPROCESS_DEFAULT_CANCEL_GRACE_MS 1000
|
||||
@@ -191,6 +206,15 @@ bool cbm_build_win_cmd_payload(char *buf, size_t cap, const char *cmd_executable
|
||||
* of hoping a loaded machine reproduces it. Test builds only. */
|
||||
void cbm_subprocess_force_spawn_eagain_for_testing(int attempts);
|
||||
int cbm_subprocess_pending_spawn_eagain_for_testing(void);
|
||||
/* Hold the parent of the next N POSIX spawns until the child has exited (left
|
||||
* unreaped) before it checks the child's process group: the order a loaded
|
||||
* machine produces when a short command finishes before the parent runs
|
||||
* again. Test builds only. */
|
||||
void cbm_subprocess_hold_parent_until_child_exits_for_testing(int spawns);
|
||||
/* For the next N POSIX spawns, the parent's group check observes what macOS
|
||||
* shows for a child that is EXITING: setpgid and getpgid answer ESRCH while
|
||||
* waitid does not report it yet. Test builds only. */
|
||||
void cbm_subprocess_observe_exiting_child_for_testing(int spawns);
|
||||
#endif
|
||||
|
||||
/* How the POSIX fork+exec child closed its inherited descriptors (#1484). */
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef CBM_TEST_SELECTION_H
|
||||
#define CBM_TEST_SELECTION_H
|
||||
|
||||
/* Bytes in one executable suite or suite:test token, excluding whitespace,
|
||||
* the line terminator and storage NUL. An individual token includes its colon. */
|
||||
enum { CBM_TEST_SELECTION_TOKEN_MAX = 511 };
|
||||
|
||||
#endif /* CBM_TEST_SELECTION_H */
|
||||
+74
-17
@@ -10,7 +10,41 @@
|
||||
#include <stdlib.h>
|
||||
#include <wchar.h>
|
||||
|
||||
static inline wchar_t *cbm_utf8_to_wide(const char *utf8) {
|
||||
#include "mem_core.h"
|
||||
|
||||
/* Converter heaps. The `*_in(cls, ...)` forms allocate through the memory
|
||||
* core and their result is released with cbm_free(cls, p). The legacy names
|
||||
* allocate with the C library for the callers that still release with free();
|
||||
* that route is the residual the memory-core ratchet drives down, so new
|
||||
* callers use the `_in` forms. */
|
||||
typedef struct {
|
||||
int core;
|
||||
cbm_mem_class_t cls;
|
||||
} cbm_wide_heap_t;
|
||||
|
||||
static inline cbm_wide_heap_t cbm_wide_heap_core(cbm_mem_class_t cls) {
|
||||
cbm_wide_heap_t heap = {1, cls};
|
||||
return heap;
|
||||
}
|
||||
|
||||
static inline cbm_wide_heap_t cbm_wide_heap_libc(void) {
|
||||
cbm_wide_heap_t heap = {0, CBM_MEM_CLASS_OTHER};
|
||||
return heap;
|
||||
}
|
||||
|
||||
static inline void *cbm_wide_heap_alloc(cbm_wide_heap_t heap, size_t bytes) {
|
||||
return heap.core ? cbm_alloc(heap.cls, bytes) : malloc(bytes);
|
||||
}
|
||||
|
||||
static inline void cbm_wide_heap_free(cbm_wide_heap_t heap, void *block) {
|
||||
if (heap.core) {
|
||||
cbm_free(heap.cls, block);
|
||||
} else {
|
||||
free(block);
|
||||
}
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_utf8_to_wide_heap(cbm_wide_heap_t heap, const char *utf8) {
|
||||
if (!utf8) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -18,14 +52,22 @@ static inline wchar_t *cbm_utf8_to_wide(const char *utf8) {
|
||||
if (len <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *w = (wchar_t *)malloc((size_t)len * sizeof(wchar_t));
|
||||
wchar_t *w = (wchar_t *)cbm_wide_heap_alloc(heap, (size_t)len * sizeof(wchar_t));
|
||||
if (!w || MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, -1, w, len) != len) {
|
||||
free(w);
|
||||
cbm_wide_heap_free(heap, w);
|
||||
return NULL;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_utf8_to_wide(const char *utf8) {
|
||||
return cbm_utf8_to_wide_heap(cbm_wide_heap_libc(), utf8);
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_utf8_to_wide_in(cbm_mem_class_t cls, const char *utf8) {
|
||||
return cbm_utf8_to_wide_heap(cbm_wide_heap_core(cls), utf8);
|
||||
}
|
||||
|
||||
static inline int cbm_win_path_separator(wchar_t value) {
|
||||
return value == L'\\' || value == L'/';
|
||||
}
|
||||
@@ -54,7 +96,8 @@ static inline void cbm_win_path_normalize_wide_separators(wchar_t *path) {
|
||||
* This makes behavior independent of the process manifest and LongPathsEnabled
|
||||
* policy. Never use this for non-file strings (command lines, pipe names,
|
||||
* registry paths). */
|
||||
static inline wchar_t *cbm_wide_path_to_extended(const wchar_t *wide_path) {
|
||||
static inline wchar_t *cbm_wide_path_to_extended_heap(cbm_wide_heap_t heap,
|
||||
const wchar_t *wide_path) {
|
||||
if (!wide_path) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -62,7 +105,7 @@ static inline wchar_t *cbm_wide_path_to_extended(const wchar_t *wide_path) {
|
||||
if (input_length >= 32767U) {
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *wide = (wchar_t *)malloc((input_length + 1U) * sizeof(*wide));
|
||||
wchar_t *wide = (wchar_t *)cbm_wide_heap_alloc(heap, (input_length + 1U) * sizeof(*wide));
|
||||
if (!wide) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -77,11 +120,12 @@ static inline wchar_t *cbm_wide_path_to_extended(const wchar_t *wide_path) {
|
||||
}
|
||||
|
||||
DWORD needed = GetFullPathNameW(wide, 0, NULL, NULL);
|
||||
wchar_t *full = needed > 0U ? (wchar_t *)malloc((size_t)needed * sizeof(wchar_t)) : NULL;
|
||||
wchar_t *full =
|
||||
needed > 0U ? (wchar_t *)cbm_wide_heap_alloc(heap, (size_t)needed * sizeof(wchar_t)) : NULL;
|
||||
DWORD copied = full ? GetFullPathNameW(wide, needed, full, NULL) : 0U;
|
||||
free(wide);
|
||||
cbm_wide_heap_free(heap, wide);
|
||||
if (!full || copied == 0U || copied >= needed) {
|
||||
free(full);
|
||||
cbm_wide_heap_free(heap, full);
|
||||
return NULL;
|
||||
}
|
||||
cbm_win_path_normalize_wide_separators(full);
|
||||
@@ -92,7 +136,7 @@ static inline wchar_t *cbm_wide_path_to_extended(const wchar_t *wide_path) {
|
||||
full[1] == L':' && full[2] == L'\\';
|
||||
int unc_absolute = copied >= 5U && full[0] == L'\\' && full[1] == L'\\';
|
||||
if (!drive_absolute && !unc_absolute) {
|
||||
free(full);
|
||||
cbm_wide_heap_free(heap, full);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -103,27 +147,40 @@ static inline wchar_t *cbm_wide_path_to_extended(const wchar_t *wide_path) {
|
||||
size_t extended_length = prefix_length + tail_length;
|
||||
/* The documented extended-length limit includes the terminating NUL. */
|
||||
if (extended_length >= 32767U) {
|
||||
free(full);
|
||||
cbm_wide_heap_free(heap, full);
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *prefixed = (wchar_t *)malloc((extended_length + 1U) * sizeof(wchar_t));
|
||||
wchar_t *prefixed =
|
||||
(wchar_t *)cbm_wide_heap_alloc(heap, (extended_length + 1U) * sizeof(wchar_t));
|
||||
if (!prefixed) {
|
||||
free(full);
|
||||
cbm_wide_heap_free(heap, full);
|
||||
return NULL;
|
||||
}
|
||||
wmemcpy(prefixed, prefix, prefix_length);
|
||||
wmemcpy(prefixed + prefix_length, full + tail_offset, tail_length + 1U);
|
||||
free(full);
|
||||
cbm_wide_heap_free(heap, full);
|
||||
return prefixed;
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_path_to_wide(const char *utf8_path) {
|
||||
wchar_t *wide = cbm_utf8_to_wide(utf8_path);
|
||||
wchar_t *extended = cbm_wide_path_to_extended(wide);
|
||||
free(wide);
|
||||
static inline wchar_t *cbm_wide_path_to_extended(const wchar_t *wide_path) {
|
||||
return cbm_wide_path_to_extended_heap(cbm_wide_heap_libc(), wide_path);
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_path_to_wide_heap(cbm_wide_heap_t heap, const char *utf8_path) {
|
||||
wchar_t *wide = cbm_utf8_to_wide_heap(heap, utf8_path);
|
||||
wchar_t *extended = cbm_wide_path_to_extended_heap(heap, wide);
|
||||
cbm_wide_heap_free(heap, wide);
|
||||
return extended;
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_path_to_wide(const char *utf8_path) {
|
||||
return cbm_path_to_wide_heap(cbm_wide_heap_libc(), utf8_path);
|
||||
}
|
||||
|
||||
static inline wchar_t *cbm_path_to_wide_in(cbm_mem_class_t cls, const char *utf8_path) {
|
||||
return cbm_path_to_wide_heap(cbm_wide_heap_core(cls), utf8_path);
|
||||
}
|
||||
|
||||
/* GetModuleFileNameW can preserve the \\?\ spelling used to launch a process.
|
||||
* Internal UTF-8 paths use conventional DOS/UNC spelling and add the prefix
|
||||
* only at the Win32 file-API boundary, so remove it before widening callers
|
||||
|
||||
@@ -835,6 +835,162 @@ void cbm_gbuf_set_next_id(cbm_gbuf_t *gb, int64_t next_id) {
|
||||
gb->next_id = next_id;
|
||||
}
|
||||
|
||||
/* ── Definition variants ─────────────────────────────────────────────
|
||||
* One name defined more than once under mutually exclusive conditions —
|
||||
* C-family #if/#else, Rust #[cfg], build-tagged files, a definition under an
|
||||
* `if` — is ONE identity: one QN, one node, whose edges are the union of
|
||||
* every variant's body. The collision rule below keeps one canonical winner
|
||||
* for name, file, lines and properties; every variant's span survives in the
|
||||
* node's `variants` property ([{"file_path","start_line","end_line"}],
|
||||
* sorted, unique), so a change in any variant maps to the node. Recorded only
|
||||
* between definitions of the same label with real spans: a different kind
|
||||
* sharing the QN (C: struct vs function) is no variant, a span on line 0 is
|
||||
* no location (delta-route proxies), and a full tie is the same entity. */
|
||||
static void *gb_yy_malloc(void *ctx, size_t size) {
|
||||
(void)ctx;
|
||||
return cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, size);
|
||||
}
|
||||
|
||||
static void *gb_yy_realloc(void *ctx, void *block, size_t old_size, size_t size) {
|
||||
(void)ctx;
|
||||
(void)old_size;
|
||||
return cbm_realloc(CBM_MEM_CLASS_GBUF_STRING, block, size);
|
||||
}
|
||||
|
||||
static void gb_yy_free(void *ctx, void *block) {
|
||||
(void)ctx;
|
||||
cbm_free(CBM_MEM_CLASS_GBUF_STRING, block);
|
||||
}
|
||||
|
||||
static const yyjson_alc gb_yy_alc = {gb_yy_malloc, gb_yy_realloc, gb_yy_free, NULL};
|
||||
|
||||
enum { GB_VARIANTS_MAX = 64 };
|
||||
|
||||
typedef struct {
|
||||
const char *file;
|
||||
int start;
|
||||
int end;
|
||||
} gb_span_t;
|
||||
|
||||
static bool gb_variant_label(const char *label) {
|
||||
static const char *const labels[] = {"Function", "Method", "Class", "Interface", "Enum",
|
||||
"Struct", "Trait", "Type", "Macro"};
|
||||
for (size_t i = 0; label && i < sizeof(labels) / sizeof(labels[0]); i++) {
|
||||
if (strcmp(label, labels[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int gb_span_cmp(const void *a, const void *b) {
|
||||
const gb_span_t *x = a;
|
||||
const gb_span_t *y = b;
|
||||
int c = strcmp(x->file, y->file);
|
||||
if (c == 0) {
|
||||
c = (x->start > y->start) - (x->start < y->start);
|
||||
}
|
||||
if (c == 0) {
|
||||
c = (x->end > y->end) - (x->end < y->end);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/* A definition's spans: its `variants` property, else its own span. */
|
||||
static int gb_spans_of(yyjson_doc *doc, const char *file, int start, int end, gb_span_t *out,
|
||||
int n) {
|
||||
yyjson_val *list = doc ? yyjson_obj_get(yyjson_doc_get_root(doc), "variants") : NULL;
|
||||
if (yyjson_is_arr(list) && yyjson_arr_size(list) > 0) {
|
||||
size_t i;
|
||||
size_t max;
|
||||
yyjson_val *v;
|
||||
yyjson_arr_foreach(list, i, max, v) {
|
||||
const char *f = yyjson_get_str(yyjson_obj_get(v, "file_path"));
|
||||
int s = (int)yyjson_get_int(yyjson_obj_get(v, "start_line"));
|
||||
int e = (int)yyjson_get_int(yyjson_obj_get(v, "end_line"));
|
||||
if (f && s > 0 && n < GB_VARIANTS_MAX) {
|
||||
out[n++] = (gb_span_t){f, s, e};
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
if (start > 0 && n < GB_VARIANTS_MAX) {
|
||||
out[n++] = (gb_span_t){file ? file : "", start, end};
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The survivor's properties with the union of both definitions' variant
|
||||
* spans. Returns a GBUF_STRING-owned JSON string, or NULL when there is
|
||||
* nothing to record (fewer than two distinct spans) or on any failure — the
|
||||
* survivor then keeps its own properties: a lost variants list costs
|
||||
* precision, never data. */
|
||||
static char *gb_variants_props(const char *survivor_props, const char *a_props, const char *a_file,
|
||||
int a_start, int a_end, const char *b_props, const char *b_file,
|
||||
int b_start, int b_end) {
|
||||
if (a_start <= 0 || b_start <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_doc *a_doc =
|
||||
a_props ? yyjson_read_opts((char *)a_props, strlen(a_props), 0, &gb_yy_alc, NULL) : NULL;
|
||||
yyjson_doc *b_doc =
|
||||
b_props ? yyjson_read_opts((char *)b_props, strlen(b_props), 0, &gb_yy_alc, NULL) : NULL;
|
||||
gb_span_t spans[GB_VARIANTS_MAX * 2];
|
||||
int n = gb_spans_of(a_doc, a_file, a_start, a_end, spans, 0);
|
||||
n = gb_spans_of(b_doc, b_file, b_start, b_end, spans, n);
|
||||
qsort(spans, (size_t)n, sizeof(spans[0]), gb_span_cmp);
|
||||
int unique = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (unique == 0 || gb_span_cmp(&spans[unique - 1], &spans[i]) != 0) {
|
||||
spans[unique++] = spans[i];
|
||||
}
|
||||
}
|
||||
char *out = NULL;
|
||||
if (unique >= 2) {
|
||||
yyjson_mut_doc *mut = yyjson_mut_doc_new(&gb_yy_alc);
|
||||
yyjson_doc *s_doc = survivor_props
|
||||
? yyjson_read_opts((char *)survivor_props, strlen(survivor_props),
|
||||
0, &gb_yy_alc, NULL)
|
||||
: NULL;
|
||||
yyjson_mut_val *root = s_doc && yyjson_is_obj(yyjson_doc_get_root(s_doc))
|
||||
? yyjson_val_mut_copy(mut, yyjson_doc_get_root(s_doc))
|
||||
: yyjson_mut_obj(mut);
|
||||
yyjson_mut_val *list = yyjson_mut_arr(mut);
|
||||
for (int i = 0; root && list && i < unique; i++) {
|
||||
yyjson_mut_val *v = yyjson_mut_arr_add_obj(mut, list);
|
||||
yyjson_mut_obj_add_strcpy(mut, v, "file_path", spans[i].file);
|
||||
yyjson_mut_obj_add_int(mut, v, "start_line", spans[i].start);
|
||||
yyjson_mut_obj_add_int(mut, v, "end_line", spans[i].end);
|
||||
}
|
||||
if (mut && root && list) {
|
||||
yyjson_mut_obj_remove_key(root, "variants");
|
||||
yyjson_mut_obj_add_val(mut, root, "variants", list);
|
||||
yyjson_mut_doc_set_root(mut, root);
|
||||
out = yyjson_mut_write_opts(mut, 0, &gb_yy_alc, NULL, NULL);
|
||||
}
|
||||
yyjson_doc_free(s_doc);
|
||||
yyjson_mut_doc_free(mut);
|
||||
}
|
||||
yyjson_doc_free(a_doc);
|
||||
yyjson_doc_free(b_doc);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Record the variant spans of a same-QN collision on the surviving node. */
|
||||
static void gb_record_variants(cbm_gbuf_node_t *survivor, const char *a_label, const char *a_props,
|
||||
const char *a_file, int a_start, int a_end, const char *b_label,
|
||||
const char *b_props, const char *b_file, int b_start, int b_end) {
|
||||
if (!gb_variant_label(a_label) || !b_label || strcmp(a_label, b_label) != 0) {
|
||||
return;
|
||||
}
|
||||
char *merged = gb_variants_props(survivor->properties_json, a_props, a_file, a_start, a_end,
|
||||
b_props, b_file, b_start, b_end);
|
||||
if (merged) {
|
||||
cbm_free(CBM_MEM_CLASS_GBUF_STRING, survivor->properties_json);
|
||||
survivor->properties_json = merged;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Node operations ─────────────────────────────────────────────── */
|
||||
|
||||
int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name,
|
||||
@@ -892,8 +1048,19 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
|
||||
c = strcmp(existing->label ? existing->label : "", label ? label : "");
|
||||
}
|
||||
if (c > 0) {
|
||||
gb_record_variants(existing, existing->label, existing->properties_json,
|
||||
existing->file_path, existing->start_line, existing->end_line, label,
|
||||
properties_json, file_path, start_line, end_line);
|
||||
return existing->id; /* existing entity is the canonical winner */
|
||||
}
|
||||
/* The arrival wins: its properties, plus every variant span both carry. */
|
||||
char *variant_props = NULL;
|
||||
if (gb_variant_label(label) && existing->label && strcmp(existing->label, label) == 0) {
|
||||
variant_props =
|
||||
gb_variants_props(properties_json, existing->properties_json, existing->file_path,
|
||||
existing->start_line, existing->end_line, properties_json,
|
||||
file_path, start_line, end_line);
|
||||
}
|
||||
/* Update in-place. name/properties are strdup'd BEFORE freeing old ones
|
||||
* (callers may pass existing->name as an argument). label/file_path are
|
||||
* interned: gb_intern returns a stable pool pointer (idempotent even when
|
||||
@@ -903,7 +1070,9 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
|
||||
* label/name — cbm_gbuf_find_by_label/name then missed or mis-listed it,
|
||||
* which is how the flickering Function set reached the semantic pass). */
|
||||
char *new_name = heap_strdup(name);
|
||||
char *new_props = properties_json ? heap_strdup(properties_json) : NULL;
|
||||
char *new_props = variant_props ? variant_props
|
||||
: properties_json ? heap_strdup(properties_json)
|
||||
: NULL;
|
||||
const char *new_label_interned = gb_intern(gb, label);
|
||||
bool label_changed = !existing->label || !new_label_interned ||
|
||||
strcmp(existing->label, new_label_interned) != 0;
|
||||
@@ -1493,7 +1662,22 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
|
||||
c = strcmp(existing->label ? existing->label : "", sn->label ? sn->label : "");
|
||||
}
|
||||
bool sn_wins = c <= 0;
|
||||
if (!sn_wins) {
|
||||
gb_record_variants(existing, existing->label, existing->properties_json,
|
||||
existing->file_path, existing->start_line, existing->end_line,
|
||||
sn->label, sn->properties_json, sn->file_path, sn->start_line,
|
||||
sn->end_line);
|
||||
}
|
||||
if (sn_wins) {
|
||||
/* src's properties, plus every variant span both carry. */
|
||||
char *variant_props = NULL;
|
||||
if (gb_variant_label(sn->label) && existing->label &&
|
||||
strcmp(existing->label, sn->label) == 0) {
|
||||
variant_props = gb_variants_props(sn->properties_json, existing->properties_json,
|
||||
existing->file_path, existing->start_line,
|
||||
existing->end_line, sn->properties_json,
|
||||
sn->file_path, sn->start_line, sn->end_line);
|
||||
}
|
||||
/* Keep the secondary indexes consistent when the surviving
|
||||
* label/name changes (the old code left the node listed under its
|
||||
* original label/name, so find_by_label/name mis-listed it). */
|
||||
@@ -1518,9 +1702,10 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
|
||||
existing->file_path = (char *)gb_intern(dst, sn->file_path);
|
||||
existing->start_line = sn->start_line;
|
||||
existing->end_line = sn->end_line;
|
||||
if (sn->properties_json) {
|
||||
if (variant_props || sn->properties_json) {
|
||||
cbm_free(CBM_MEM_CLASS_GBUF_STRING, existing->properties_json);
|
||||
existing->properties_json = heap_strdup(sn->properties_json);
|
||||
existing->properties_json =
|
||||
variant_props ? variant_props : heap_strdup(sn->properties_json);
|
||||
}
|
||||
if (label_changed) {
|
||||
node_ptr_array_t *by_label = get_or_create_node_array(
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "daemon/version_cohort.h"
|
||||
#include "mcp/mcp.h"
|
||||
#include "mcp/index_supervisor.h"
|
||||
#include "mcp/test_impact_engine.h"
|
||||
#include "cli/cli.h"
|
||||
#include "cli/progress_sink.h"
|
||||
#include "foundation/constants.h"
|
||||
@@ -1280,6 +1281,10 @@ static int handle_subcommand(int argc, char **argv, cbm_project_lock_manager_t *
|
||||
cbm_mem_init(cbm_mem_ram_fraction_for_total(cbm_system_info().total_ram));
|
||||
return cbm_cmd_hook_augment(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
||||
}
|
||||
if (strcmp(argv[i], "test-impact") == 0) {
|
||||
cbm_mem_init(cbm_mem_ram_fraction_for_total(cbm_system_info().total_ram));
|
||||
return cbm_test_impact_cli(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
||||
}
|
||||
if (strcmp(argv[i], "install") == 0) {
|
||||
return cbm_cmd_install(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
||||
}
|
||||
|
||||
+683
-33
@@ -56,7 +56,10 @@ enum {
|
||||
#define SLEN(s) (sizeof(s) - 1)
|
||||
#include "mcp/mcp.h"
|
||||
#include "mcp/mcp_internal.h"
|
||||
#include "mcp/test_impact.h"
|
||||
#include "mcp/test_impact_engine.h"
|
||||
#include "store/store.h"
|
||||
#include "store/store_impact.h"
|
||||
#include <sqlite3.h>
|
||||
#include "cypher/cypher.h"
|
||||
#include "discover/discover.h"
|
||||
@@ -68,6 +71,7 @@ enum {
|
||||
#include "watcher/watcher.h"
|
||||
#include "foundation/mem.h"
|
||||
#include "foundation/mem_core.h"
|
||||
#include "foundation/arena.h"
|
||||
#include "foundation/diagnostics.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/compat.h"
|
||||
@@ -775,10 +779,24 @@ static const tool_def_t TOOLS[] = {
|
||||
|
||||
{"detect_changes", "Map a Git diff to files and impact. Page with snapshot cursors.",
|
||||
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"scope\":{\"type\":"
|
||||
"\"string\",\"enum\":[\"files\",\"impact\"],\"default\":\"impact\"},"
|
||||
"\"string\",\"enum\":[\"files\",\"impact\",\"tests\"],\"default\":\"impact\"},"
|
||||
"\"direction\":{\"type\":\"string\",\"enum\":[\"inbound\",\"outbound\",\"both\"],\"default\":"
|
||||
"\"inbound\",\"description\":\"inbound=callers; outbound=dependencies; both=union.\"},"
|
||||
"\"depth\":{\"type\":\"integer\",\"default\":2},"
|
||||
"\"edge_types\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},"
|
||||
"\"description\":\"Inbound walk; default CALLS.\"},"
|
||||
"\"fixpoint\":{\"type\":\"boolean\",\"description\":\"Walk to closure; no depth.\"},"
|
||||
"\"via\":{\"type\":\"boolean\",\"description\":\"Add via_qn, via_edge.\"},"
|
||||
"\"seeds\":{\"type\":\"string\",\"enum\":[\"none\",\"list\"],\"default\":\"none\","
|
||||
"\"description\":\"List changed-symbol seeds with independent pagination.\"},"
|
||||
"\"seed_filter\":{\"type\":\"string\",\"enum\":[\"all\",\"product\"],\"default\":\"all\","
|
||||
"\"description\":\"Product excludes indexed test metadata and configured test globs.\"},"
|
||||
"\"config_path\":{\"type\":\"string\",\"description\":\"Project config override; absolute or "
|
||||
"project-relative.\"},"
|
||||
"\"seed_limit\":{\"type\":\"integer\",\"default\":50,\"minimum\":0,\"maximum\":1000},"
|
||||
"\"seed_offset\":{\"type\":\"integer\",\"default\":0,\"minimum\":0},"
|
||||
"\"seed_cursor\":{\"type\":\"string\",\"description\":\"Continue seeds:list; excludes nonzero "
|
||||
"seed_offset.\"},"
|
||||
"\"limit\":{\"type\":\"integer\",\"default\":200,\"maximum\":5000},"
|
||||
"\"impact_offset\":{\"type\":\"integer\",\"default\":0,\"minimum\":0},"
|
||||
"\"impact_cursor\":{\"type\":\"string\"},"
|
||||
@@ -1688,8 +1706,19 @@ int cbm_mcp_get_int_arg(const char *args_json, const char *key, int default_val)
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
yyjson_val *val = yyjson_obj_get(root, key);
|
||||
int result = default_val;
|
||||
if (val && yyjson_is_int(val)) {
|
||||
result = yyjson_get_int(val);
|
||||
/* yyjson_get_int truncates 64-bit integers to int (2^32 + 1 became 1);
|
||||
* read the full width and treat anything outside int range like a
|
||||
* non-integer, i.e. fall back to the caller's default. */
|
||||
if (val && yyjson_is_sint(val)) {
|
||||
int64_t parsed = yyjson_get_sint(val);
|
||||
if (parsed >= INT_MIN && parsed <= INT_MAX) {
|
||||
result = (int)parsed;
|
||||
}
|
||||
} else if (val && yyjson_is_uint(val)) {
|
||||
uint64_t parsed = yyjson_get_uint(val);
|
||||
if (parsed <= (uint64_t)INT_MAX) {
|
||||
result = (int)parsed;
|
||||
}
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
return result;
|
||||
@@ -15905,12 +15934,169 @@ static bool detect_is_seedable_label(const char *lb) {
|
||||
strcmp(lb, "Section") != 0;
|
||||
}
|
||||
|
||||
/* Seed presentation is optional. The default request retains its traversal,
|
||||
* ordering, output bytes and cursor identity. */
|
||||
typedef struct {
|
||||
bool list;
|
||||
bool product;
|
||||
int limit;
|
||||
int offset;
|
||||
char cursor[80];
|
||||
char config_path[CBM_SZ_4K];
|
||||
} detect_seed_request_t;
|
||||
|
||||
typedef struct {
|
||||
const char *qn;
|
||||
const char *label;
|
||||
const char *file;
|
||||
const char *reason;
|
||||
int start;
|
||||
int end;
|
||||
} detect_seed_row_t;
|
||||
|
||||
typedef struct {
|
||||
CBMArena arena;
|
||||
detect_seed_row_t *rows;
|
||||
int count;
|
||||
int capacity;
|
||||
bool failed;
|
||||
bool config_failed;
|
||||
cbm_test_config_t *config;
|
||||
} detect_seed_rows_t;
|
||||
|
||||
static bool detect_seed_string(yyjson_val *value, const char **out) {
|
||||
*out = yyjson_get_str(value);
|
||||
return *out && strlen(*out) == yyjson_get_len(value);
|
||||
}
|
||||
|
||||
static void detect_seed_parse(const char *args, detect_seed_request_t *out, char *err, size_t len) {
|
||||
*out = (detect_seed_request_t){.limit = 50};
|
||||
yyjson_doc *doc = yyjson_read(args, strlen(args), 0);
|
||||
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
|
||||
const char *keys[] = {"seeds", "seed_filter", "seed_limit",
|
||||
"seed_offset", "seed_cursor", "config_path"};
|
||||
for (int i = 0; i < 6 && !err[0]; i++) {
|
||||
yyjson_val *value = yyjson_obj_get(root, keys[i]);
|
||||
if (!value || yyjson_is_null(value))
|
||||
continue;
|
||||
const char *text = NULL;
|
||||
if (i < 2) {
|
||||
const char *off = i == 0 ? "none" : "all";
|
||||
const char *on = i == 0 ? "list" : "product";
|
||||
if (!detect_seed_string(value, &text) || (strcmp(text, off) && strcmp(text, on))) {
|
||||
snprintf(err, len, "%s must be \"%s\" or \"%s\"", keys[i], off, on);
|
||||
} else if (i == 0)
|
||||
out->list = strcmp(text, on) == 0;
|
||||
else
|
||||
out->product = strcmp(text, on) == 0;
|
||||
} else if (i < 4) {
|
||||
uint64_t maximum = i == 2 ? 1000 : INT_MAX;
|
||||
if (!yyjson_is_uint(value) || yyjson_get_uint(value) > maximum) {
|
||||
snprintf(err, len, "%s must be an integer from 0 to %llu", keys[i],
|
||||
(unsigned long long)maximum);
|
||||
} else if (i == 2)
|
||||
out->limit = (int)yyjson_get_uint(value);
|
||||
else
|
||||
out->offset = (int)yyjson_get_uint(value);
|
||||
} else {
|
||||
char *destination = i == 4 ? out->cursor : out->config_path;
|
||||
size_t capacity = i == 4 ? sizeof(out->cursor) : sizeof(out->config_path);
|
||||
if (!detect_seed_string(value, &text) || strlen(text) >= capacity ||
|
||||
(i == 5 && !*text)) {
|
||||
snprintf(err, len, "%s must be a nonempty string within the path/cursor size limit",
|
||||
keys[i]);
|
||||
} else {
|
||||
memcpy(destination, text, strlen(text) + 1);
|
||||
#ifdef _WIN32
|
||||
if (i == 5)
|
||||
for (char *p = destination; *p; p++)
|
||||
if (*p == '\\')
|
||||
*p = '/';
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!err[0] && out->cursor[0] && !out->list) {
|
||||
snprintf(err, len, "seed_cursor requires seeds:\"list\"");
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
}
|
||||
|
||||
static bool detect_seed_is_test(const cbm_node_t *node) {
|
||||
if (!node->properties_json)
|
||||
return false;
|
||||
yyjson_doc *doc = yyjson_read(node->properties_json, strlen(node->properties_json), 0);
|
||||
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
|
||||
yyjson_val *test = yyjson_obj_get(root, "is_test");
|
||||
yyjson_val *role = yyjson_obj_get(root, "test_role");
|
||||
const char *role_name = yyjson_get_str(role);
|
||||
bool is_test = yyjson_get_bool(test) || (yyjson_is_int(test) && yyjson_get_sint(test) == 1) ||
|
||||
(role_name && strlen(role_name) == yyjson_get_len(role) &&
|
||||
(strcmp(role_name, "case") == 0 || strcmp(role_name, "suite") == 0 ||
|
||||
strcmp(role_name, "helper") == 0));
|
||||
yyjson_doc_free(doc);
|
||||
return is_test;
|
||||
}
|
||||
|
||||
static void detect_seed_add_row(detect_seed_rows_t *rows, const cbm_node_t *node, bool hunk) {
|
||||
if (rows->failed)
|
||||
return;
|
||||
if (rows->count == rows->capacity) {
|
||||
if (rows->capacity > INT_MAX / 2) {
|
||||
rows->failed = true;
|
||||
return;
|
||||
}
|
||||
int capacity = rows->capacity ? rows->capacity * 2 : 16;
|
||||
if ((size_t)capacity > SIZE_MAX / sizeof(*rows->rows)) {
|
||||
rows->failed = true;
|
||||
return;
|
||||
}
|
||||
detect_seed_row_t *next = cbm_arena_alloc(&rows->arena, (size_t)capacity * sizeof(*next));
|
||||
if (!next) {
|
||||
rows->failed = true;
|
||||
return;
|
||||
}
|
||||
if (rows->count)
|
||||
memcpy(next, rows->rows, (size_t)rows->count * sizeof(*next));
|
||||
rows->rows = next;
|
||||
rows->capacity = capacity;
|
||||
}
|
||||
detect_seed_row_t *row = &rows->rows[rows->count++];
|
||||
*row = (detect_seed_row_t){
|
||||
.qn = cbm_arena_strdup(&rows->arena, node->qualified_name ? node->qualified_name : ""),
|
||||
.label = cbm_arena_strdup(&rows->arena, node->label ? node->label : ""),
|
||||
.file = cbm_arena_strdup(&rows->arena, node->file_path ? node->file_path : ""),
|
||||
.reason = hunk ? "hunk" : "whole_file",
|
||||
.start = node->start_line,
|
||||
.end = node->end_line};
|
||||
rows->failed = !row->qn || !row->label || !row->file;
|
||||
}
|
||||
|
||||
static int detect_seed_row_compare(const void *left, const void *right) {
|
||||
const detect_seed_row_t *a = left, *b = right;
|
||||
int cmp = strcmp(a->qn, b->qn);
|
||||
if (!cmp)
|
||||
cmp = strcmp(a->file, b->file);
|
||||
if (!cmp)
|
||||
cmp = (a->start > b->start) - (a->start < b->start);
|
||||
if (!cmp)
|
||||
cmp = (a->end > b->end) - (a->end < b->end);
|
||||
if (!cmp)
|
||||
cmp = strcmp(a->label, b->label);
|
||||
return cmp;
|
||||
}
|
||||
|
||||
static void detect_collect_seeds(cbm_store_t *store, const char *project, const char *file,
|
||||
const cbm_changed_hunk_t *hunks, int hunk_count, int64_t **seeds,
|
||||
int *n, int *cap) {
|
||||
int *n, int *cap, const detect_seed_request_t *request,
|
||||
detect_seed_rows_t *rows) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int ncount = 0;
|
||||
cbm_store_find_nodes_by_file(store, project, file, &nodes, &ncount);
|
||||
if (cbm_store_find_nodes_by_file(store, project, file, &nodes, &ncount) != CBM_STORE_OK) {
|
||||
rows->failed = true;
|
||||
return;
|
||||
}
|
||||
bool file_is_test = request->product && cbm_test_config_is_test_file(rows->config, file);
|
||||
bool scope_to_hunks = false;
|
||||
for (int h = 0; h < hunk_count; h++) {
|
||||
if (strcmp(hunks[h].path, file) == 0) {
|
||||
@@ -15942,6 +16128,14 @@ static void detect_collect_seeds(cbm_store_t *store, const char *project, const
|
||||
if (scope_to_hunks && !cbm_detect_node_in_hunks(&nodes[i], hunks, hunk_count, file)) {
|
||||
continue;
|
||||
}
|
||||
/* Probe overlap before filtering: a test-only hunk must not
|
||||
* fall back onto untouched product definitions in this file. */
|
||||
if (request->product && (file_is_test || detect_seed_is_test(&nodes[i]))) {
|
||||
continue;
|
||||
}
|
||||
if (request->list) {
|
||||
detect_seed_add_row(rows, &nodes[i], scope_to_hunks);
|
||||
}
|
||||
if (*n >= *cap) {
|
||||
*cap = *cap ? *cap * 2 : 16;
|
||||
*seeds = safe_realloc(*seeds, (size_t)*cap * sizeof(int64_t));
|
||||
@@ -16023,14 +16217,28 @@ static void detect_module_rollup_free(detect_module_row_t *modules, int count) {
|
||||
* is hop-ordered, so page zero preserves the closest, highest-signal rows.
|
||||
* Engine saturation is reported as a lower-bound total, never as an exact
|
||||
* count. */
|
||||
/* How the walk reached a row: the parent one hop closer and the edge type that
|
||||
* led there. Empty strings for a row the walk does not know. */
|
||||
static void detect_walk_via(const cbm_impact_walk_t *walk, int64_t id, const char **via_qn,
|
||||
const char **via_edge) {
|
||||
const cbm_impact_hit_t *hit = cbm_impact_walk_hit(walk, id);
|
||||
const cbm_impact_hit_t *parent = hit ? cbm_impact_walk_hit(walk, hit->via_id) : NULL;
|
||||
*via_qn = (parent && parent->qualified_name) ? parent->qualified_name : "";
|
||||
*via_edge = (hit && hit->via_edge) ? hit->via_edge : "";
|
||||
}
|
||||
|
||||
/* `via_walk` is the open impact walk when the request asked for `via`, and adds
|
||||
* the via_qn and via_edge columns; NULL keeps the four default columns. */
|
||||
static void detect_emit_impacted_tree(cbm_sb_t *sb, const cbm_traverse_result_t *tr, int start,
|
||||
int count, bool engine_saturated) {
|
||||
int count, bool engine_saturated,
|
||||
const cbm_impact_walk_t *via_walk) {
|
||||
cbm_tree_scalar_int(sb, "impacted_total", tr->visited_count);
|
||||
cbm_tree_scalar_str(sb, "impacted_total_relation", engine_saturated ? "gte" : "eq");
|
||||
int shown = count;
|
||||
/* qn order for stable grouping, but keep hop-closeness: sort by (hop) is
|
||||
* lost under qn sort, so group AFTER selecting the nearest `shown` rows —
|
||||
* the visited array is already (hop,id)-ordered from the BFS. */
|
||||
* the visited array is already (hop,id)-ordered from the BFS, and
|
||||
* (hop,qn)-ordered from the impact walk. */
|
||||
cbm_tree_scalar_int(sb, "impacted_shown", shown);
|
||||
cbm_node_hop_t *selected = NULL;
|
||||
const cbm_node_hop_t *rows = tr->visited;
|
||||
@@ -16046,24 +16254,28 @@ static void detect_emit_impacted_tree(cbm_sb_t *sb, const cbm_traverse_result_t
|
||||
if (shown > 1 && selected) {
|
||||
qsort(selected, (size_t)shown, sizeof(*selected), tree_hop_cmp_qn);
|
||||
}
|
||||
static const char *const columns[] = {"qn", "label", "file", "hop"};
|
||||
const char **cells = shown > 0 ? calloc((size_t)shown * 4U, sizeof(*cells)) : NULL;
|
||||
static const char *const columns[] = {"qn", "label", "file", "hop", "via_qn", "via_edge"};
|
||||
size_t ncols = via_walk ? 6U : 4U;
|
||||
const char **cells = shown > 0 ? calloc((size_t)shown * ncols, sizeof(*cells)) : NULL;
|
||||
char (*hop_text)[32] = shown > 0 ? calloc((size_t)shown, sizeof(*hop_text)) : NULL;
|
||||
if (shown == 0 || (cells && hop_text)) {
|
||||
for (int i = 0; i < shown; i++) {
|
||||
size_t base = (size_t)i * 4U;
|
||||
size_t base = (size_t)i * ncols;
|
||||
cells[base] = rows[i].node.qualified_name ? rows[i].node.qualified_name : "";
|
||||
cells[base + 1U] = rows[i].node.label ? rows[i].node.label : "";
|
||||
cells[base + 2U] = rows[i].node.file_path ? rows[i].node.file_path : "";
|
||||
snprintf(hop_text[i], sizeof(hop_text[i]), "%d", rows[i].hop);
|
||||
cells[base + 3U] = hop_text[i];
|
||||
if (via_walk) {
|
||||
detect_walk_via(via_walk, rows[i].node.id, &cells[base + 4U], &cells[base + 5U]);
|
||||
}
|
||||
}
|
||||
static const bool string_cols[] = {true, true, true, false};
|
||||
static const bool prefix_cols[] = {true, false, true, false};
|
||||
cbm_tree_table_rows_profiled(sb, "impacted", shown, columns, 4, cells, string_cols,
|
||||
static const bool string_cols[] = {true, true, true, false, true, true};
|
||||
static const bool prefix_cols[] = {true, false, true, false, true, false};
|
||||
cbm_tree_table_rows_profiled(sb, "impacted", shown, columns, (int)ncols, cells, string_cols,
|
||||
prefix_cols);
|
||||
} else {
|
||||
cbm_tree_table_header(sb, "impacted", 0, columns, 4);
|
||||
cbm_tree_table_header(sb, "impacted", 0, columns, (int)ncols);
|
||||
cbm_tree_scalar_str(sb, "impacted_render_error", "out_of_memory");
|
||||
}
|
||||
free(hop_text);
|
||||
@@ -16316,6 +16528,199 @@ static uint64_t detect_params_hash(const char *project, const char *base_branch,
|
||||
return cursor_fnv1a64(depth_text, hash);
|
||||
}
|
||||
|
||||
/* ── detect_changes: opt-in impact walk ───────────────────────────
|
||||
* `edge_types`, `fixpoint` and `via` switch the impact step from the
|
||||
* depth-bounded CALLS traversal to cbm_impact_walk (store/store_impact.h). A
|
||||
* request that names none of them keeps the old traversal and its bytes. */
|
||||
enum { DETECT_WALK_TYPE_MAX = 16, DETECT_WALK_TYPE_LEN = 64 };
|
||||
|
||||
typedef struct {
|
||||
bool active; /* edge_types, fixpoint or via was requested */
|
||||
bool fixpoint; /* walk until nothing new is reached; `depth` is ignored */
|
||||
bool via; /* rows also say which parent and edge type reached them */
|
||||
int type_count;
|
||||
char types[DETECT_WALK_TYPE_MAX][DETECT_WALK_TYPE_LEN];
|
||||
} detect_walk_request_t;
|
||||
|
||||
/* An argument that is absent or JSON null was not given. */
|
||||
static yyjson_val *detect_walk_arg(yyjson_val *root, const char *key) {
|
||||
yyjson_val *val = root ? yyjson_obj_get(root, key) : NULL;
|
||||
return (val && !yyjson_is_null(val)) ? val : NULL;
|
||||
}
|
||||
|
||||
/* "unknown edge type" plus the types this index does hold: a walk over a
|
||||
* mistyped edge type would come back empty and read as "nothing depends on
|
||||
* this". */
|
||||
static void detect_walk_unknown_type(const cbm_schema_info_t *schema, const char *name, char *err,
|
||||
size_t err_sz) {
|
||||
int used = snprintf(err, err_sz, "unknown edge type \"%.60s\" — this index has: ", name);
|
||||
for (int i = 0; i < schema->edge_type_count && used > 0 && (size_t)used < err_sz; i++) {
|
||||
used += snprintf(err + used, err_sz - (size_t)used, "%s%s", i > 0 ? ", " : "",
|
||||
schema->edge_types[i].type ? schema->edge_types[i].type : "");
|
||||
}
|
||||
}
|
||||
|
||||
static void detect_walk_parse_types(cbm_store_t *store, const char *project, yyjson_val *types,
|
||||
detect_walk_request_t *out, char *err, size_t err_sz) {
|
||||
static const char *const shape = "edge_types must be a non-empty array of at most 16 edge "
|
||||
"type names, e.g. [\"CALLS\",\"USAGE\"]";
|
||||
if (!yyjson_is_arr(types) || yyjson_arr_size(types) == 0 ||
|
||||
yyjson_arr_size(types) > DETECT_WALK_TYPE_MAX) {
|
||||
snprintf(err, err_sz, "%s", shape);
|
||||
return;
|
||||
}
|
||||
cbm_schema_info_t schema = {0};
|
||||
if (cbm_store_get_schema_counts(store, project, &schema) != CBM_STORE_OK) {
|
||||
snprintf(err, err_sz,
|
||||
"index_metadata_error: the edge types of this index are unreadable; reindex "
|
||||
"before detecting changes");
|
||||
return;
|
||||
}
|
||||
size_t idx;
|
||||
size_t max;
|
||||
yyjson_val *val;
|
||||
yyjson_arr_foreach(types, idx, max, val) {
|
||||
const char *name = yyjson_is_str(val) ? yyjson_get_str(val) : NULL;
|
||||
if (!name) {
|
||||
snprintf(err, err_sz, "%s", shape);
|
||||
break;
|
||||
}
|
||||
bool known = false;
|
||||
for (int i = 0; i < schema.edge_type_count && !known; i++) {
|
||||
known = schema.edge_types[i].type && strcmp(schema.edge_types[i].type, name) == 0;
|
||||
}
|
||||
if (!known || strlen(name) >= DETECT_WALK_TYPE_LEN) {
|
||||
detect_walk_unknown_type(&schema, name, err, err_sz);
|
||||
break;
|
||||
}
|
||||
bool twice = false;
|
||||
for (int i = 0; i < out->type_count && !twice; i++) {
|
||||
twice = strcmp(out->types[i], name) == 0;
|
||||
}
|
||||
if (twice) {
|
||||
/* The order is the rank that picks a row's `via` parent, so a
|
||||
* repeated name has no single meaning. */
|
||||
snprintf(err, err_sz, "edge type \"%s\" is listed twice in edge_types", name);
|
||||
break;
|
||||
}
|
||||
snprintf(out->types[out->type_count++], DETECT_WALK_TYPE_LEN, "%s", name);
|
||||
}
|
||||
cbm_store_schema_free(&schema);
|
||||
}
|
||||
|
||||
/* Read and validate the walk parameters. `err` stays empty when the request is
|
||||
* usable; otherwise it holds the teaching error and nothing was corrected. */
|
||||
static void detect_walk_parse(cbm_store_t *store, const char *project, const char *args,
|
||||
const char *direction, detect_walk_request_t *out, char *err,
|
||||
size_t err_sz) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
yyjson_doc *doc = yyjson_read(args, strlen(args), 0);
|
||||
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
|
||||
yyjson_val *types = detect_walk_arg(root, "edge_types");
|
||||
yyjson_val *fixpoint = detect_walk_arg(root, "fixpoint");
|
||||
yyjson_val *via = detect_walk_arg(root, "via");
|
||||
if (fixpoint && !yyjson_is_bool(fixpoint)) {
|
||||
snprintf(err, err_sz, "fixpoint must be true or false");
|
||||
} else if (via && !yyjson_is_bool(via)) {
|
||||
snprintf(err, err_sz, "via must be true or false");
|
||||
} else if (types) {
|
||||
detect_walk_parse_types(store, project, types, out, err, err_sz);
|
||||
}
|
||||
if (!err[0]) {
|
||||
out->fixpoint = fixpoint && yyjson_get_bool(fixpoint);
|
||||
out->via = via && yyjson_get_bool(via);
|
||||
out->active = types || out->fixpoint || out->via;
|
||||
}
|
||||
if (!err[0] && out->active && strcmp(direction, "inbound") != 0) {
|
||||
snprintf(err, err_sz,
|
||||
"edge_types, fixpoint and via walk the blast radius only — use direction "
|
||||
"\"inbound\" (the default)");
|
||||
}
|
||||
if (!err[0] && out->active && out->type_count == 0) {
|
||||
snprintf(out->types[out->type_count++], DETECT_WALK_TYPE_LEN, "CALLS");
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
}
|
||||
|
||||
/* The walk parameters are part of the question a cursor continues. A request
|
||||
* without them hashes exactly as before, so its cursors are unchanged. */
|
||||
static uint64_t detect_walk_hash(uint64_t hash, const detect_walk_request_t *walk) {
|
||||
if (!walk->active) {
|
||||
return hash;
|
||||
}
|
||||
hash = cursor_fnv1a64(walk->fixpoint ? "|walk:fixpoint" : "|walk:bounded", hash);
|
||||
hash = cursor_fnv1a64(walk->via ? "|via" : "|novia", hash);
|
||||
for (int i = 0; i < walk->type_count; i++) {
|
||||
hash = cursor_fnv1a64("|", hash);
|
||||
hash = cursor_fnv1a64(walk->types[i], hash);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* "CALLS,USAGE": the edge types of the walk, in rank order, for the answer. */
|
||||
static void detect_walk_types_text(const detect_walk_request_t *walk, char *out, size_t out_sz) {
|
||||
size_t used = 0;
|
||||
out[0] = '\0';
|
||||
for (int i = 0; i < walk->type_count && used < out_sz; i++) {
|
||||
int n = snprintf(out + used, out_sz - used, "%s%s", i > 0 ? "," : "", walk->types[i]);
|
||||
used += n > 0 ? (size_t)n : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Run the walk and hand its answer over in the traversal's shape, so paging,
|
||||
* the module rollup and the snapshot fingerprint are shared with the default
|
||||
* path. Seeds are left out, as in cbm_store_bfs_multi: a changed symbol is not
|
||||
* its own impact. Rows arrive in the walk's (hop, qualified name) order. The
|
||||
* walk stays open for the `via` columns; the caller closes it.
|
||||
*
|
||||
* false = the walk or a row could not be completed. Nothing shorter is handed
|
||||
* over: an incomplete impact set would read as "nothing else depends on this". */
|
||||
static bool detect_walk_run(cbm_store_t *store, const char *project,
|
||||
const detect_walk_request_t *req, int depth, const int64_t *seeds,
|
||||
int seed_count, cbm_traverse_result_t *impact,
|
||||
cbm_impact_walk_t **walk_out) {
|
||||
const char *types[DETECT_WALK_TYPE_MAX];
|
||||
for (int i = 0; i < req->type_count; i++) {
|
||||
types[i] = req->types[i];
|
||||
}
|
||||
cbm_impact_policy_t policy = {.project = project,
|
||||
.edge_types = types,
|
||||
.edge_type_count = req->type_count,
|
||||
.max_hops = req->fixpoint ? 0 : depth};
|
||||
cbm_impact_walk_t *walk = NULL;
|
||||
if (cbm_impact_walk_open(store, &policy, &walk) != CBM_STORE_OK) {
|
||||
return false;
|
||||
}
|
||||
bool ok = cbm_impact_walk_run(walk, seeds, seed_count) == CBM_STORE_OK;
|
||||
int total = ok ? cbm_impact_walk_count(walk) : 0;
|
||||
const cbm_impact_hit_t *hits = cbm_impact_walk_hits(walk);
|
||||
/* cbm_store_traverse_free owns and releases these rows. */
|
||||
cbm_node_hop_t *rows = total > 0 ? safe_realloc(NULL, (size_t)total * sizeof(*rows)) : NULL;
|
||||
ok = ok && (total == 0 || rows);
|
||||
if (rows) {
|
||||
memset(rows, 0, (size_t)total * sizeof(*rows));
|
||||
}
|
||||
int n = 0;
|
||||
for (int i = 0; ok && i < total; i++) {
|
||||
if (hits[i].hop == 0) {
|
||||
continue;
|
||||
}
|
||||
ok = cbm_store_find_node_by_id(store, hits[i].id, &rows[n].node) == CBM_STORE_OK;
|
||||
if (ok) {
|
||||
rows[n++].hop = hits[i].hop;
|
||||
}
|
||||
}
|
||||
impact->visited = rows;
|
||||
impact->visited_count = n;
|
||||
if (!ok) {
|
||||
cbm_store_traverse_free(impact);
|
||||
cbm_impact_walk_close(walk);
|
||||
return false;
|
||||
}
|
||||
*walk_out = walk;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void detect_cursor_encode(char stream, const char snapshot[33], uint64_t qhash, int offset,
|
||||
char out[80]) {
|
||||
snprintf(out, 80, "d1.%c.%s.%016llx.%d", stream, snapshot, (unsigned long long)qhash, offset);
|
||||
@@ -16516,6 +16921,68 @@ static bool detect_snapshot_fingerprint(cbm_mcp_server_t *srv, const char *root_
|
||||
return complete;
|
||||
}
|
||||
|
||||
/* scope:"tests": which tests the change needs (mcp/test_impact_engine.h). The
|
||||
* answer is computed from a graph of exactly the pinned HEAD commit, built
|
||||
* for this request; the live index is not consulted. Missing evidence is a
|
||||
* run-all answer that names why; only a failed request is an error. */
|
||||
static char *detect_test_impact(const char *args, const char *root_path, const char *base_branch) {
|
||||
detect_seed_request_t seed_req = {0};
|
||||
char err[CBM_SZ_1K] = "";
|
||||
detect_seed_parse(args, &seed_req, err, sizeof(err));
|
||||
if (err[0]) {
|
||||
return cbm_mcp_text_result(err, true);
|
||||
}
|
||||
char config_file[CBM_SZ_4K] = "";
|
||||
if (seed_req.config_path[0]) {
|
||||
if (repo_path_is_absolute(seed_req.config_path)) {
|
||||
snprintf(config_file, sizeof(config_file), "%s", seed_req.config_path);
|
||||
} else {
|
||||
snprintf(config_file, sizeof(config_file), "%s/%s", root_path, seed_req.config_path);
|
||||
}
|
||||
}
|
||||
char work[CBM_SZ_4K];
|
||||
snprintf(work, sizeof(work), "%s/test-impact", cbm_resolve_cache_dir());
|
||||
if (!cbm_mkdir_p(work, 0700)) {
|
||||
return cbm_mcp_text_result("test_impact_failed: no private work directory", true);
|
||||
}
|
||||
cbm_test_impact_request_t request = {.repo_root = root_path,
|
||||
.base_ref = base_branch,
|
||||
.work_parent = work,
|
||||
.config_path = config_file[0] ? config_file : NULL,
|
||||
.deadline_ms = cbm_now_ms() + 30ULL * 60ULL * 1000ULL};
|
||||
cbm_test_result_t *result = NULL;
|
||||
char diagnostic[CBM_SZ_512] = "";
|
||||
cbm_test_impact_status_t status =
|
||||
cbm_test_impact_run(&request, &result, diagnostic, sizeof(diagnostic));
|
||||
if (status != CBM_TEST_IMPACT_OK) {
|
||||
char text[CBM_SZ_1K];
|
||||
snprintf(text, sizeof(text), "test_impact_failed: status %d%s%s", (int)status,
|
||||
diagnostic[0] ? ": " : "", diagnostic);
|
||||
return cbm_mcp_text_result(text, true);
|
||||
}
|
||||
size_t length = 0;
|
||||
const char *json = cbm_test_result_json(result, &length);
|
||||
char *res = NULL;
|
||||
size_t total = length + sizeof("{\"test_impact\":}");
|
||||
char *text = json ? cbm_alloc(CBM_MEM_CLASS_OTHER, total) : NULL;
|
||||
if (text) {
|
||||
snprintf(text, total, "{\"test_impact\":%s}", json);
|
||||
res = cbm_mcp_text_result(text, false);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, text);
|
||||
}
|
||||
cbm_test_result_free(result);
|
||||
return res ? res : cbm_mcp_text_result("test_impact_failed: out of memory", true);
|
||||
}
|
||||
|
||||
/* The request strings handle_detect_changes owns, released on every early
|
||||
* return (NULL is fine). */
|
||||
static void detect_release(char *root_path, char *project, char *base_branch, char *scope) {
|
||||
free(root_path);
|
||||
free(project);
|
||||
free(base_branch);
|
||||
free(scope);
|
||||
}
|
||||
|
||||
static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *project = get_project_arg(args);
|
||||
char *base_branch = cbm_mcp_get_string_arg(args, "base_branch");
|
||||
@@ -16580,24 +17047,24 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
base_branch = heap_strdup(default_base);
|
||||
}
|
||||
if (!root_ok || !base_branch) {
|
||||
free(root_path);
|
||||
free(project);
|
||||
free(base_branch);
|
||||
free(scope);
|
||||
detect_release(root_path, project, base_branch, scope);
|
||||
return cbm_mcp_text_result(
|
||||
root_ok ? "out of memory" : "project path contains invalid characters", true);
|
||||
}
|
||||
|
||||
if (scope && strcmp(scope, "tests") == 0) {
|
||||
char *res = detect_test_impact(args, root_path, base_branch);
|
||||
detect_release(root_path, project, base_branch, scope);
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Every detect snapshot and cursor is generation-bound. Validate the
|
||||
* metadata immediately after store resolution so fresh requests and
|
||||
* cursor replays fail identically before Git work or cursor minting. */
|
||||
cbm_store_t *store = srv->store;
|
||||
char generation[96] = "";
|
||||
if (cbm_store_generation(store, generation, sizeof(generation)) != CBM_STORE_OK) {
|
||||
free(root_path);
|
||||
free(project);
|
||||
free(base_branch);
|
||||
free(scope);
|
||||
detect_release(root_path, project, base_branch, scope);
|
||||
return cbm_mcp_text_result(
|
||||
"index_metadata_error: generation metadata is unreadable; reindex before detecting "
|
||||
"changes",
|
||||
@@ -16613,13 +17080,22 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
}
|
||||
/* Teaching error, same contract as trace_path: never silently correct an
|
||||
* unknown direction — the caller would misread the result's semantics. */
|
||||
char errbuf[CBM_SZ_1K] = "";
|
||||
detect_walk_request_t walk_req = {0};
|
||||
detect_seed_request_t seed_req = {0};
|
||||
if (strcmp(direction, "inbound") != 0 && strcmp(direction, "outbound") != 0 &&
|
||||
strcmp(direction, "both") != 0) {
|
||||
char errbuf[CBM_SZ_256];
|
||||
snprintf(errbuf, sizeof(errbuf),
|
||||
"invalid direction \"%s\" — use \"inbound\" (blast radius: transitive callers), "
|
||||
"\"outbound\" (dependencies), or \"both\"",
|
||||
direction);
|
||||
} else {
|
||||
/* Same contract for the opt-in walk parameters. */
|
||||
detect_walk_parse(store, project, args, direction, &walk_req, errbuf, sizeof(errbuf));
|
||||
if (!errbuf[0])
|
||||
detect_seed_parse(args, &seed_req, errbuf, sizeof(errbuf));
|
||||
}
|
||||
if (errbuf[0]) {
|
||||
free(direction);
|
||||
free(root_path);
|
||||
free(project);
|
||||
@@ -16985,8 +17461,37 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *impact_cursor_arg = cbm_mcp_get_string_arg(args, "impact_cursor");
|
||||
char *changed_cursor_arg = cbm_mcp_get_string_arg(args, "changed_cursor");
|
||||
char *module_cursor_arg = cbm_mcp_get_string_arg(args, "module_cursor");
|
||||
uint64_t detect_qhash = detect_params_hash(project, base_branch,
|
||||
want_symbols ? "impact" : "files", direction, depth);
|
||||
uint64_t detect_qhash =
|
||||
detect_walk_hash(detect_params_hash(project, base_branch, want_symbols ? "impact" : "files",
|
||||
direction, depth),
|
||||
&walk_req);
|
||||
|
||||
if (seed_req.list)
|
||||
detect_qhash = cursor_fnv1a64("|seeds:list", detect_qhash);
|
||||
if (seed_req.product)
|
||||
detect_qhash = cursor_fnv1a64("|seed_filter:product", detect_qhash);
|
||||
detect_seed_rows_t seed_rows = {0};
|
||||
cbm_arena_init_lazy(&seed_rows.arena, 4096);
|
||||
if (seed_req.product) {
|
||||
const char *name = seed_req.config_path[0] ? seed_req.config_path : ".codebase-memory.json";
|
||||
char *config_file = NULL;
|
||||
if (repo_path_is_absolute(name))
|
||||
config_file = cbm_arena_strdup(&seed_rows.arena, name);
|
||||
else {
|
||||
size_t a = strlen(root_path), b = strlen(name);
|
||||
if (a <= SIZE_MAX - b - 2) {
|
||||
config_file = cbm_arena_alloc(&seed_rows.arena, a + b + 2);
|
||||
if (config_file)
|
||||
snprintf(config_file, a + b + 2, "%s/%s", root_path, name);
|
||||
}
|
||||
}
|
||||
seed_rows.config =
|
||||
config_file ? cbm_test_config_load(config_file, !seed_req.config_path[0]) : NULL;
|
||||
seed_rows.config_failed = !seed_rows.config;
|
||||
seed_rows.failed = seed_rows.config_failed;
|
||||
detect_qhash = cursor_fnv1a64("|test-config:", detect_qhash);
|
||||
detect_qhash = cursor_fnv1a64(cbm_test_config_digest(seed_rows.config), detect_qhash);
|
||||
}
|
||||
|
||||
/* Changed paths drive traversal seeds and both output encodings. */
|
||||
int64_t *seeds = NULL;
|
||||
@@ -17012,7 +17517,7 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
* `files` when the project root is a repository subdirectory (#1951). */
|
||||
cbm_changed_hunk_t *hunks = NULL;
|
||||
int hunk_count = 0;
|
||||
if (want_symbols) {
|
||||
if (!seed_rows.failed && (want_symbols || seed_req.list)) {
|
||||
char hunk_cmd[CBM_SZ_2K];
|
||||
#ifdef _WIN32
|
||||
snprintf(hunk_cmd, sizeof(hunk_cmd),
|
||||
@@ -17070,17 +17575,39 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
* decision. */
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
detect_collect_seeds(store, project, files[i], hunks, hunk_count, &seeds, &seed_count,
|
||||
&seed_cap);
|
||||
&seed_cap, &seed_req, &seed_rows);
|
||||
}
|
||||
}
|
||||
|
||||
if (!seed_rows.failed && seed_rows.count > 1)
|
||||
qsort(seed_rows.rows, (size_t)seed_rows.count, sizeof(*seed_rows.rows),
|
||||
detect_seed_row_compare);
|
||||
|
||||
/* The impact traversal: ONE multi-source BFS over all seeds. */
|
||||
cbm_traverse_result_t impact = {0};
|
||||
bool engine_saturated = false;
|
||||
if (want_symbols && seed_count > 0) {
|
||||
/* The opt-in walk has no result ceiling, so it never saturates: it answers
|
||||
* completely or the request fails. It stays open while `via` is rendered. */
|
||||
cbm_impact_walk_t *impact_walk = NULL;
|
||||
const char *walk_error =
|
||||
seed_rows.config_failed
|
||||
? "config_invalid: could not read a valid project test configuration"
|
||||
: seed_rows.failed ? "seed_collection_failed: no partial answer is returned"
|
||||
: NULL;
|
||||
if (!walk_error && want_symbols && seed_count > 0 && walk_req.active) {
|
||||
if (!detect_walk_run(store, project, &walk_req, depth, seeds, seed_count, &impact,
|
||||
&impact_walk)) {
|
||||
walk_error = "impact_walk_failed: the impact walk could not be completed — no "
|
||||
"partial answer is returned; rerun, or drop edge_types, fixpoint and "
|
||||
"via for the bounded default traversal";
|
||||
}
|
||||
} else if (!walk_error && want_symbols && seed_count > 0) {
|
||||
(void)cbm_store_bfs_multi(store, seeds, seed_count, direction, NULL, 0, depth,
|
||||
MCP_BFS_LIMIT_MAX, &impact, &engine_saturated);
|
||||
}
|
||||
const cbm_impact_walk_t *via_walk = walk_req.via ? impact_walk : NULL;
|
||||
char walk_types_text[DETECT_WALK_TYPE_MAX * DETECT_WALK_TYPE_LEN] = "";
|
||||
detect_walk_types_text(&walk_req, walk_types_text, sizeof(walk_types_text));
|
||||
|
||||
detect_module_row_t *modules = NULL;
|
||||
int nmods = 0;
|
||||
@@ -17095,17 +17622,39 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
* number of pageable rollup rows for the materialized impact set; its
|
||||
* relation becomes `gte` if traversal hit the engine ceiling. */
|
||||
int module_total = nmods + (module_overflow > 0 ? 1 : 0);
|
||||
const char *cursor_error = NULL;
|
||||
const char *cursor_error = walk_error;
|
||||
char detect_snapshot[33] = "";
|
||||
bool detect_snapshot_complete = detect_snapshot_fingerprint(
|
||||
srv, root_path, head_oid, base_oid, merge_base, generation, files, file_count, &impact,
|
||||
modules, nmods, module_overflow, detect_snapshot);
|
||||
|
||||
if (seed_req.list && !seed_rows.failed) {
|
||||
cbm_sha256_ctx hash;
|
||||
cbm_sha256_init(&hash);
|
||||
detect_snapshot_add_field(&hash, detect_snapshot);
|
||||
for (int i = 0; i < seed_rows.count; i++) {
|
||||
detect_seed_row_t *row = &seed_rows.rows[i];
|
||||
detect_snapshot_add_field(&hash, row->qn);
|
||||
detect_snapshot_add_field(&hash, row->label);
|
||||
detect_snapshot_add_field(&hash, row->file);
|
||||
detect_snapshot_add_field(&hash, row->reason);
|
||||
char location[64];
|
||||
snprintf(location, sizeof(location), "%d:%d", row->start, row->end);
|
||||
detect_snapshot_add_field(&hash, location);
|
||||
}
|
||||
uint8_t digest[CBM_SHA256_DIGEST_LEN];
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
cbm_sha256_final(&hash, digest);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
detect_snapshot[i * 2] = hex[digest[i] >> 4];
|
||||
detect_snapshot[i * 2 + 1] = hex[digest[i] & 15];
|
||||
}
|
||||
}
|
||||
detect_cursor_t decoded_cursor = {0};
|
||||
bool cursor_supplied = (changed_cursor_arg && changed_cursor_arg[0]) ||
|
||||
bool cursor_supplied = seed_req.cursor[0] || (changed_cursor_arg && changed_cursor_arg[0]) ||
|
||||
(impact_cursor_arg && impact_cursor_arg[0]) ||
|
||||
(module_cursor_arg && module_cursor_arg[0]);
|
||||
if (cursor_supplied && !detect_snapshot_complete) {
|
||||
if (!cursor_error && cursor_supplied && !detect_snapshot_complete) {
|
||||
cursor_error = "snapshot_unavailable: changed file bytes could not be fingerprinted — "
|
||||
"rerun without the cursor after the files are readable";
|
||||
}
|
||||
@@ -17145,7 +17694,21 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cursor_error && seed_req.cursor[0]) {
|
||||
if (seed_req.offset != 0)
|
||||
cursor_error =
|
||||
"cursor_params_mismatch: seed_cursor cannot be combined with a nonzero seed_offset";
|
||||
else {
|
||||
cursor_error = detect_cursor_decode(seed_req.cursor, 's', detect_snapshot, detect_qhash,
|
||||
&decoded_cursor);
|
||||
if (!cursor_error)
|
||||
seed_req.offset = decoded_cursor.offset;
|
||||
}
|
||||
}
|
||||
if (cursor_error) {
|
||||
cbm_test_config_free(seed_rows.config);
|
||||
cbm_arena_destroy(&seed_rows.arena);
|
||||
cbm_impact_walk_close(impact_walk);
|
||||
cbm_store_traverse_free(&impact);
|
||||
detect_module_rollup_free(modules, nmods);
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
@@ -17165,6 +17728,10 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
return cbm_mcp_text_result(cursor_error, true);
|
||||
}
|
||||
|
||||
int seed_start = seed_req.offset < seed_rows.count ? seed_req.offset : seed_rows.count;
|
||||
int seed_returned = seed_rows.count - seed_start;
|
||||
if (seed_returned > seed_req.limit)
|
||||
seed_returned = seed_req.limit;
|
||||
int changed_start = changed_offset < file_count ? changed_offset : file_count;
|
||||
int changed_returned = file_count - changed_start;
|
||||
if (changed_returned > changed_limit) {
|
||||
@@ -17187,11 +17754,19 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *out_str = NULL;
|
||||
|
||||
render_detect_output:;
|
||||
bool seed_has_more = seed_req.list && seed_start + seed_returned < seed_rows.count;
|
||||
char seed_next_cursor[80] = "";
|
||||
if (seed_has_more && seed_returned > 0 && detect_snapshot_complete)
|
||||
detect_cursor_encode('s', detect_snapshot, detect_qhash, seed_start + seed_returned,
|
||||
seed_next_cursor);
|
||||
const char *seed_continuation = seed_req.limit == 0
|
||||
? "seed_continuation_requires_positive_limit"
|
||||
: "seed_continuation_requires_higher_budget";
|
||||
bool changed_has_more = changed_start + changed_returned < file_count;
|
||||
bool impacted_has_more = want_symbols && imp_start + imp_returned < impact.visited_count;
|
||||
bool module_has_more = want_symbols && module_start + module_returned < module_total;
|
||||
bool response_truncated = engine_saturated || output_budget_hit || changed_has_more ||
|
||||
impacted_has_more || module_has_more;
|
||||
impacted_has_more || module_has_more || seed_has_more;
|
||||
if (!legacy_json) {
|
||||
cbm_sb_t sb;
|
||||
cbm_sb_init(&sb);
|
||||
@@ -17200,6 +17775,10 @@ render_detect_output:;
|
||||
cbm_tree_scalar_str(&sb, "merge_base", merge_base);
|
||||
}
|
||||
cbm_tree_scalar_str(&sb, "direction", direction);
|
||||
if (walk_req.active) {
|
||||
cbm_tree_scalar_str(&sb, "edge_types", walk_types_text);
|
||||
cbm_tree_scalar_bool(&sb, "fixpoint", walk_req.fixpoint);
|
||||
}
|
||||
if (output_budget_hit) {
|
||||
cbm_tree_scalar_str(&sb, "truncation_reason", "output_budget");
|
||||
cbm_tree_scalar_int(&sb, "max_output_bytes", (long long)output_budget_bytes);
|
||||
@@ -17250,8 +17829,34 @@ render_detect_output:;
|
||||
}
|
||||
free(changed_cells);
|
||||
cbm_tree_scalar_int(&sb, "seed_symbols", seed_count);
|
||||
if (seed_req.list) {
|
||||
cbm_tree_scalar_int(&sb, "seed_total", seed_rows.count);
|
||||
cbm_tree_scalar_int(&sb, "seed_returned", seed_returned);
|
||||
cbm_tree_scalar_bool(&sb, "seed_has_more", seed_has_more);
|
||||
if (seed_has_more && seed_returned > 0) {
|
||||
cbm_tree_scalar_int(&sb, "seed_next_offset", seed_start + seed_returned);
|
||||
if (seed_next_cursor[0])
|
||||
cbm_tree_scalar_str(&sb, "seed_next_cursor", seed_next_cursor);
|
||||
} else if (seed_has_more)
|
||||
cbm_tree_scalar_bool(&sb, seed_continuation, true);
|
||||
static const char *const columns[] = {"qn", "label", "file",
|
||||
"start", "end", "seed_reason"};
|
||||
cbm_tree_table_header(&sb, "seeds", seed_returned, columns, 6);
|
||||
for (int i = seed_start; i < seed_start + seed_returned; i++) {
|
||||
const detect_seed_row_t *row = &seed_rows.rows[i];
|
||||
cbm_tree_row_begin(&sb);
|
||||
cbm_tree_cell_str(&sb, row->qn, true);
|
||||
cbm_tree_cell_str(&sb, row->label, false);
|
||||
cbm_tree_cell_str(&sb, row->file, false);
|
||||
cbm_tree_cell_int(&sb, row->start, false);
|
||||
cbm_tree_cell_int(&sb, row->end, false);
|
||||
cbm_tree_cell_str(&sb, row->reason, false);
|
||||
cbm_tree_row_end(&sb);
|
||||
}
|
||||
}
|
||||
if (want_symbols) {
|
||||
detect_emit_impacted_tree(&sb, &impact, imp_start, imp_returned, engine_saturated);
|
||||
detect_emit_impacted_tree(&sb, &impact, imp_start, imp_returned, engine_saturated,
|
||||
via_walk);
|
||||
if (detect_snapshot_complete && imp_start + imp_returned < impact.visited_count &&
|
||||
imp_returned > 0) {
|
||||
char cursor[80];
|
||||
@@ -17323,6 +17928,14 @@ render_detect_output:;
|
||||
yyjson_mut_obj_add_strcpy(doc, root_obj, "merge_base", merge_base);
|
||||
}
|
||||
yyjson_mut_obj_add_strcpy(doc, root_obj, "direction", direction);
|
||||
if (walk_req.active) {
|
||||
yyjson_mut_val *walk_types = yyjson_mut_arr(doc);
|
||||
for (int i = 0; i < walk_req.type_count; i++) {
|
||||
yyjson_mut_arr_add_strcpy(doc, walk_types, walk_req.types[i]);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, root_obj, "edge_types", walk_types);
|
||||
yyjson_mut_obj_add_bool(doc, root_obj, "fixpoint", walk_req.fixpoint);
|
||||
}
|
||||
if (output_budget_hit) {
|
||||
yyjson_mut_obj_add_str(doc, root_obj, "truncation_reason", "output_budget");
|
||||
yyjson_mut_obj_add_uint(doc, root_obj, "max_output_bytes", output_budget_bytes);
|
||||
@@ -17361,6 +17974,31 @@ render_detect_output:;
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, root_obj, "changed_files", cf);
|
||||
yyjson_mut_obj_add_int(doc, root_obj, "seed_symbols", seed_count);
|
||||
if (seed_req.list) {
|
||||
yyjson_mut_obj_add_int(doc, root_obj, "seed_total", seed_rows.count);
|
||||
yyjson_mut_obj_add_int(doc, root_obj, "seed_returned", seed_returned);
|
||||
yyjson_mut_obj_add_bool(doc, root_obj, "seed_has_more", seed_has_more);
|
||||
if (seed_has_more && seed_returned > 0) {
|
||||
yyjson_mut_obj_add_int(doc, root_obj, "seed_next_offset",
|
||||
seed_start + seed_returned);
|
||||
if (seed_next_cursor[0])
|
||||
yyjson_mut_obj_add_strcpy(doc, root_obj, "seed_next_cursor", seed_next_cursor);
|
||||
} else if (seed_has_more)
|
||||
yyjson_mut_obj_add_bool(doc, root_obj, seed_continuation, true);
|
||||
yyjson_mut_val *seed_array = yyjson_mut_arr(doc);
|
||||
for (int i = seed_start; i < seed_start + seed_returned; i++) {
|
||||
const detect_seed_row_t *row = &seed_rows.rows[i];
|
||||
yyjson_mut_val *item = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_strcpy(doc, item, "qn", row->qn);
|
||||
yyjson_mut_obj_add_strcpy(doc, item, "label", row->label);
|
||||
yyjson_mut_obj_add_strcpy(doc, item, "file", row->file);
|
||||
yyjson_mut_obj_add_int(doc, item, "start", row->start);
|
||||
yyjson_mut_obj_add_int(doc, item, "end", row->end);
|
||||
yyjson_mut_obj_add_strcpy(doc, item, "seed_reason", row->reason);
|
||||
yyjson_mut_arr_add_val(seed_array, item);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, root_obj, "seeds", seed_array);
|
||||
}
|
||||
yyjson_mut_obj_add_int(doc, root_obj, "impacted_total", impact.visited_count);
|
||||
yyjson_mut_obj_add_str(doc, root_obj, "impacted_total_relation",
|
||||
engine_saturated ? "gte" : "eq");
|
||||
@@ -17377,6 +18015,13 @@ render_detect_output:;
|
||||
doc, o, "file",
|
||||
impact.visited[i].node.file_path ? impact.visited[i].node.file_path : "");
|
||||
yyjson_mut_obj_add_int(doc, o, "hop", impact.visited[i].hop);
|
||||
if (via_walk) {
|
||||
const char *via_qn = "";
|
||||
const char *via_edge = "";
|
||||
detect_walk_via(via_walk, impact.visited[i].node.id, &via_qn, &via_edge);
|
||||
yyjson_mut_obj_add_strcpy(doc, o, "via_qn", via_qn);
|
||||
yyjson_mut_obj_add_strcpy(doc, o, "via_edge", via_edge);
|
||||
}
|
||||
yyjson_mut_arr_add_val(imp, o);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, root_obj, "impacted", imp);
|
||||
@@ -17445,6 +18090,8 @@ render_detect_output:;
|
||||
module_returned = 0;
|
||||
} else if (changed_returned > 0) {
|
||||
changed_returned = 0;
|
||||
} else if (seed_returned > 0) {
|
||||
seed_returned--;
|
||||
} else if (imp_returned > 0) {
|
||||
/* Prefix-directory activation can make N compact rows smaller
|
||||
* than N-1 direct rows. Probe every smaller whole-row prefix so
|
||||
@@ -17496,6 +18143,9 @@ render_detect_output:;
|
||||
|
||||
detect_output_done:
|
||||
|
||||
cbm_test_config_free(seed_rows.config);
|
||||
cbm_arena_destroy(&seed_rows.arena);
|
||||
cbm_impact_walk_close(impact_walk);
|
||||
cbm_store_traverse_free(&impact);
|
||||
detect_module_rollup_free(modules, nmods);
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,568 @@
|
||||
/*
|
||||
* test_impact.h — which tests does a change reach? (detect_changes scope:"tests")
|
||||
*
|
||||
* Part 1, the test model: what a repository's test sources define and what
|
||||
* its runner registers, read from the source text by convention.
|
||||
*
|
||||
* Suite membership comes from the registration itself — the name inside
|
||||
* RUN_TEST(...) and the TEST of that name in the same file — and never from a
|
||||
* resolver edge. An edge can be missing or point at a same-named symbol
|
||||
* elsewhere; the registration is what the compiler sees, and a selection that
|
||||
* names a test its suite never runs fails the run (tests/test_main.c).
|
||||
*/
|
||||
#ifndef CBM_MCP_TEST_IMPACT_H
|
||||
#define CBM_MCP_TEST_IMPACT_H
|
||||
|
||||
#include "discover/test_conventions.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* The spellings a test framework uses. A NULL entry is a convention the
|
||||
* framework does not have. */
|
||||
typedef struct {
|
||||
const char *case_macro; /* TEST(name) { ... } */
|
||||
const char *suite_macro; /* SUITE(name) { ... } */
|
||||
const char *suite_fn_prefix; /* void suite_name(void) { ... }: what SUITE expands to */
|
||||
const char *run_macro; /* RUN_TEST(name) inside a suite body */
|
||||
const char *suite_run_macro; /* RUN_SELECTED_SUITE(name) in the runner */
|
||||
const char *suite_run_perf_macro; /* RUN_SELECTED_SUITE_PERF(name): a perf suite */
|
||||
} cbm_test_conventions_t;
|
||||
|
||||
/* This repository's own C test framework (tests/test_framework.h). */
|
||||
const cbm_test_conventions_t *cbm_test_conventions_cbm(void);
|
||||
|
||||
typedef struct {
|
||||
const char *file; /* as given to cbm_test_model_add_source */
|
||||
const char *name;
|
||||
int start_line; /* the line of TEST(name) */
|
||||
int end_line; /* the line of the closing brace of its body */
|
||||
/* Defined under a preprocessor condition (an include guard is not one):
|
||||
* some builds do not have this test. */
|
||||
bool conditional;
|
||||
} cbm_test_case_t;
|
||||
|
||||
typedef struct {
|
||||
const char *file;
|
||||
const char *name;
|
||||
int start_line;
|
||||
int end_line;
|
||||
/* The file defines a macro that registers tests. The suite may then run
|
||||
* tests the model cannot name, so it can only be selected whole. */
|
||||
bool macro_registrations;
|
||||
/* The model could not read this suite's file with certainty (for
|
||||
* example tests defined by a macro). The suite can only run whole; other
|
||||
* suites stay narrowable (cbm_test_model_narrowable). */
|
||||
bool uncertain;
|
||||
} cbm_test_suite_t;
|
||||
|
||||
typedef struct {
|
||||
const char *file;
|
||||
const char *suite;
|
||||
const char *test;
|
||||
int line;
|
||||
bool conditional; /* registered under a preprocessor condition */
|
||||
bool resolved; /* a case of that name is defined in the same file */
|
||||
} cbm_test_registration_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
bool perf;
|
||||
const char *file; /* the runner file that registers it */
|
||||
int line;
|
||||
} cbm_test_runner_suite_t;
|
||||
|
||||
typedef struct cbm_test_model cbm_test_model_t;
|
||||
|
||||
/* NULL when out of memory. The conventions are copied. */
|
||||
cbm_test_model_t *cbm_test_model_new(const cbm_test_conventions_t *conventions);
|
||||
|
||||
/* Read one source file. `text` need not be NUL-terminated; (NULL, 0) is empty.
|
||||
* false = invalid input/state or out of memory. A successful read can still
|
||||
* leave parse uncertainty; it does not establish completeness. */
|
||||
bool cbm_test_model_add_source(cbm_test_model_t *m, const char *file, const char *text, size_t len);
|
||||
|
||||
/* Order and link what was read. Call once, after the last file and before any
|
||||
* row accessors. Mapping and declaration accessors also work before finish.
|
||||
* false = out of memory. */
|
||||
bool cbm_test_model_finish(cbm_test_model_t *m);
|
||||
|
||||
void cbm_test_model_free(cbm_test_model_t *m);
|
||||
|
||||
/* True only after finish and no sticky parse/lexical/unsupported-form
|
||||
* uncertainty in any added source. Rows remain available for diagnostics
|
||||
* when false, but cannot justify narrowing. This concerns the supplied
|
||||
* source text only: callers must separately establish that the external
|
||||
* file/runner inventory is complete and conventions are applicable. */
|
||||
bool cbm_test_model_complete(const cbm_test_model_t *m);
|
||||
|
||||
/* True after finish when every uncertainty the model found is scoped to the
|
||||
* suites of the file it was found in (those suites are marked `uncertain` and
|
||||
* can only run whole). Uncertainty in a file that registers runner suites, in
|
||||
* a file with no suite of its own, in the configuration, or a failure keeps
|
||||
* this false, and nothing may be narrowed. complete() implies narrowable(). */
|
||||
bool cbm_test_model_narrowable(const cbm_test_model_t *m);
|
||||
|
||||
/* Descriptor mode is independent of the legacy six-spelling constructor.
|
||||
* Copies every original record, ordered name_args, optional template and
|
||||
* preset switch. NULL means invalid input or allocation failure. Unsupported
|
||||
* mapping produces an owned diagnostic and prevents complete() from being true.
|
||||
* Exact supported explicit case template: "{suite}:{name}". Missing templates
|
||||
* do not acquire a default. Multi-component names are retained but unsupported.
|
||||
* c-cbm supplies C declarations plus the existing suite_ function form. */
|
||||
typedef enum {
|
||||
CBM_TEST_MODEL_MAPPING_OK = 0,
|
||||
CBM_TEST_MODEL_MAPPING_INVALID_INPUT,
|
||||
CBM_TEST_MODEL_MAPPING_UNSUPPORTED_LANGUAGE,
|
||||
CBM_TEST_MODEL_MAPPING_UNSUPPORTED_PRESET,
|
||||
CBM_TEST_MODEL_MAPPING_UNSUPPORTED_NAME,
|
||||
CBM_TEST_MODEL_MAPPING_UNSUPPORTED_TEMPLATE,
|
||||
CBM_TEST_MODEL_MAPPING_AMBIGUOUS,
|
||||
CBM_TEST_MODEL_MAPPING_MISSING_ARGUMENT,
|
||||
CBM_TEST_MODEL_MAPPING_UNSUPPORTED_ARGUMENT,
|
||||
CBM_TEST_MODEL_MAPPING_MALFORMED_INVOCATION,
|
||||
CBM_TEST_MODEL_MAPPING_UNSUPPORTED_FORM
|
||||
} cbm_test_model_mapping_status_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_test_model_mapping_status_t reason;
|
||||
int declaration_index; /* original zero-based record index; -1 for builtin/input */
|
||||
const char *file; /* borrowed model-owned copy, or NULL for config issues */
|
||||
int line; /* 1-based source line, or 0 for config/input issues */
|
||||
} cbm_test_model_mapping_issue_t;
|
||||
|
||||
cbm_test_model_t *cbm_test_model_new_declarations(const cbm_test_declarations_t *declarations);
|
||||
/* Descriptor mode requires explicit language. Accepted canonical config names:
|
||||
* c/cpp/cuda; also accepts the discover API's C/C++/CUDA display names, and c++.
|
||||
* This adapter does not handle native presets. Applicable gtest in C++/CUDA
|
||||
* prevents narrowing; unrelated default native presets do not poison C.
|
||||
* false means invalid state/input or allocation failure. Mapping uncertainty
|
||||
* may return true: callers MUST require model_complete() before narrowing. */
|
||||
bool cbm_test_model_add_source_language(cbm_test_model_t *model, const char *file,
|
||||
const char *language, const char *text, size_t len);
|
||||
/* Canonically smallest issue by (file, line, declaration_index, reason), not
|
||||
* discovery order. Strings and original declaration rows live until model free.
|
||||
* NULL model yields INVALID_INPUT and initializes issue when supplied. */
|
||||
cbm_test_model_mapping_status_t cbm_test_model_mapping_status(
|
||||
const cbm_test_model_t *model, cbm_test_model_mapping_issue_t *issue);
|
||||
const cbm_test_declaration_t *cbm_test_model_declarations(const cbm_test_model_t *model,
|
||||
int *count);
|
||||
|
||||
/* Cases, suites and registrations are ordered by (file, line); runner suites by
|
||||
* name, each once. The order never depends on the order the files were added
|
||||
* in. */
|
||||
const cbm_test_case_t *cbm_test_model_cases(const cbm_test_model_t *m, int *count);
|
||||
const cbm_test_suite_t *cbm_test_model_suites(const cbm_test_model_t *m, int *count);
|
||||
const cbm_test_registration_t *cbm_test_model_registrations(const cbm_test_model_t *m, int *count);
|
||||
const cbm_test_runner_suite_t *cbm_test_model_runner_suites(const cbm_test_model_t *m, int *count);
|
||||
|
||||
/* ── Part 2: the change ──────────────────────────────────────────────
|
||||
* A unified diff with zero context lines, as git writes it under the pinned
|
||||
* flags below, read into files and hunks. The reader takes text, not a
|
||||
* repository, so the selection is a function of the diff bytes. */
|
||||
|
||||
/* The flags that make `git diff` output independent of the user's git
|
||||
* settings: no external diff or textconv driver, no renames, no color, one
|
||||
* algorithm, fixed prefixes, zero context. Spliced in as
|
||||
* `git <CBM_TEST_IMPACT_GIT_PINS> diff <CBM_TEST_IMPACT_DIFF_FLAGS> A B`. */
|
||||
#define CBM_TEST_IMPACT_GIT_PINS \
|
||||
"-c core.quotePath=false -c color.ui=never -c diff.noprefix=false " \
|
||||
"-c diff.mnemonicPrefix=false"
|
||||
#define CBM_TEST_IMPACT_DIFF_FLAGS \
|
||||
"--no-ext-diff --no-textconv --no-color --no-renames --diff-algorithm=myers " \
|
||||
"--unified=0 --inter-hunk-context=0 --src-prefix=a/ --dst-prefix=b/"
|
||||
|
||||
typedef struct {
|
||||
/* Lines on the new side: [start, start + count). A hunk that only removes
|
||||
* lines has count 0, and start is then the line the removal follows. */
|
||||
int start;
|
||||
int count;
|
||||
const char *const *added; /* the new lines, without the leading '+' */
|
||||
int added_count;
|
||||
const char *const *removed; /* the old lines, without the leading '-' */
|
||||
int removed_count;
|
||||
} cbm_diff_hunk_t;
|
||||
|
||||
typedef struct {
|
||||
const char *path;
|
||||
bool created; /* the file did not exist before */
|
||||
bool deleted; /* the file does not exist any more */
|
||||
bool binary; /* git printed no text for it: there are no hunks to read */
|
||||
const cbm_diff_hunk_t *hunks;
|
||||
int hunk_count;
|
||||
} cbm_diff_file_t;
|
||||
|
||||
typedef struct cbm_diff cbm_diff_t;
|
||||
|
||||
/* NULL when out of memory. A diff the reader does not fully understand still
|
||||
* parses; cbm_diff_complete then says so. */
|
||||
cbm_diff_t *cbm_diff_parse(const char *text, size_t len);
|
||||
void cbm_diff_free(cbm_diff_t *d);
|
||||
|
||||
/* Files ordered by path. */
|
||||
const cbm_diff_file_t *cbm_diff_files(const cbm_diff_t *d, int *count);
|
||||
|
||||
/* false = something in the text was not understood (a quoted path, a hunk
|
||||
* shorter than its header says, a line outside any file). What was read is
|
||||
* then not the whole change, and nothing may be narrowed by it. */
|
||||
bool cbm_diff_complete(const cbm_diff_t *d);
|
||||
|
||||
/* Coverage-map format 1 from scripts/test-impact/coverage-map.py. These are
|
||||
* internal data accessors, not an admission decision: callers must separately
|
||||
* verify metadata, provenance, ancestry, age and harness/build compatibility.
|
||||
* Missing tests and incomplete test/setup rows cannot justify narrowing. */
|
||||
typedef struct {
|
||||
int id;
|
||||
const char *file; /* empty when only an external profile name was available */
|
||||
const char *name;
|
||||
} cbm_coverage_function_t;
|
||||
|
||||
typedef struct {
|
||||
const char *suite;
|
||||
const char *name; /* "*" is between-test setup for the entire suite */
|
||||
bool complete;
|
||||
const char *reason;
|
||||
const int *function_ids; /* ascending, unique; IDs index the function table */
|
||||
int function_count;
|
||||
} cbm_coverage_test_t;
|
||||
|
||||
typedef struct cbm_coverage_map cbm_coverage_map_t;
|
||||
|
||||
/* Explicit single-image profile identity format; no admission is implied. */
|
||||
typedef enum {
|
||||
CBM_COVERAGE_FORMAT_NONE = 0,
|
||||
CBM_COVERAGE_FORMAT_FUNCTIONS = 1,
|
||||
CBM_COVERAGE_FORMAT_PROFILES = 2
|
||||
} cbm_coverage_format_t;
|
||||
|
||||
typedef struct {
|
||||
char image_sha256[65]; /* lowercase SHA-256 of the one immutable image */
|
||||
} cbm_coverage_profile_binding_t;
|
||||
|
||||
typedef struct {
|
||||
int id; /* dense 0-based; row index */
|
||||
const unsigned char *name; /* owned bytes; NOT a C-string contract */
|
||||
size_t name_length; /* positive; embedded NUL is representable */
|
||||
uint64_t function_hash; /* all 64 bits, without masking */
|
||||
uint64_t counter_count; /* positive shape; no counter values stored */
|
||||
} cbm_coverage_profile_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t max_input_bytes; /* profiles_len + tests_len; <= UINT64_MAX/8 */
|
||||
uint64_t max_items; /* exactly 1 header + profile rows + test rows + ID references */
|
||||
size_t max_alloc_bytes; /* cumulative logical allocation requests, including scratch */
|
||||
int max_ids; /* positive, <= INT_MAX; dense universe cardinality */
|
||||
} cbm_coverage_parse_limits_t;
|
||||
|
||||
typedef bool (*cbm_coverage_parse_cancel_fn)(void *context);
|
||||
|
||||
typedef enum {
|
||||
CBM_COVERAGE_PARSE_OK = 0,
|
||||
CBM_COVERAGE_PARSE_INVALID, /* bad pointers/limits, invalid C call */
|
||||
CBM_COVERAGE_PARSE_FORMAT, /* malformed/noncanonical identities or invalid test rows */
|
||||
CBM_COVERAGE_PARSE_UNSUPPORTED, /* well-formed recognized header, wire version != 2 */
|
||||
CBM_COVERAGE_PARSE_LIMIT, /* byte/item/ID/logical allocation/representation limit */
|
||||
CBM_COVERAGE_PARSE_CANCELLED,
|
||||
CBM_COVERAGE_PARSE_OOM
|
||||
} cbm_coverage_parse_status_t;
|
||||
|
||||
/* profiles.tsv strict header/rows are specified in the companion proposal.
|
||||
* Clear *out before all other validation. profiles must contain its header;
|
||||
* tests=NULL is allowed only with tests_len=0. No NUL terminators required.
|
||||
* limits and out are required; every limit is positive. Callback may be NULL.
|
||||
* Inputs/limits/context remain immutable for this synchronous call; callback
|
||||
* is prompt, non-reentrant and cannot mutate/free inputs or call this owner.
|
||||
* Poll before parsing, within <=65536 bytes or <=4096 visited elements,
|
||||
* including hash/copy/comparison/sort work, and before publishing success.
|
||||
* Every non-OK result leaves *out=NULL and frees all call-owned allocations.
|
||||
* OK owns all data in CBMArena storage and permits unobserved IDs explicitly.
|
||||
* OK never establishes image/universe/row completeness or artifact admission.
|
||||
* Use existing cbm_coverage_map_free; no shared mutable or cancellation state. */
|
||||
cbm_coverage_parse_status_t cbm_coverage_map_parse_v2(const void *profiles, size_t profiles_len,
|
||||
const char *tests, size_t tests_len,
|
||||
const cbm_coverage_parse_limits_t *limits,
|
||||
cbm_coverage_parse_cancel_fn cancelled,
|
||||
void *cancel_context,
|
||||
cbm_coverage_map_t **out);
|
||||
|
||||
/* O(1) borrowed views until map_free. NULL map -> NONE / 0 / NULL.
|
||||
* Generic count and exact digest work for formats 1 and 2. No reserialization:
|
||||
* v1 digest = original functions.tsv; v2 digest = entire original profiles.tsv,
|
||||
* including binding header. SHA-256 is a lowercase 64-hex C string. */
|
||||
cbm_coverage_format_t cbm_coverage_map_format(const cbm_coverage_map_t *map);
|
||||
int cbm_coverage_map_id_count(const cbm_coverage_map_t *map);
|
||||
const char *cbm_coverage_map_identity_sha256(const cbm_coverage_map_t *map);
|
||||
|
||||
/* Format 2 only; other formats/NULL -> NULL and, for profiles, count=0.
|
||||
* count may be NULL. For a zero-ID v2 map profiles returns NULL/count=0,
|
||||
* but binding remains available. Records remain in dense ID order. */
|
||||
const cbm_coverage_profile_binding_t *cbm_coverage_map_profile_binding(
|
||||
const cbm_coverage_map_t *map);
|
||||
const cbm_coverage_profile_t *cbm_coverage_map_profiles(const cbm_coverage_map_t *map, int *count);
|
||||
|
||||
/* Existing test/find_test/intersection/free APIs retain their row semantics.
|
||||
* On v2, old functions() returns NULL/count=0, find_function() returns NULL,
|
||||
* functions_sha256() returns NULL, metadata_matches() returns false, and
|
||||
* check_receipt() returns INVALID|METADATA before interpreting v1 evidence.
|
||||
* All format-1 behavior, uniqueness and observed-ID guards stay unchanged. */
|
||||
|
||||
/* Explicit byte lengths; inputs need not be NUL-terminated. LF and CRLF are
|
||||
* accepted. NULL means invalid/incomplete syntax, inconsistent references, or
|
||||
* allocation failure; no partial map is returned. Does not read meta.json. */
|
||||
cbm_coverage_map_t *cbm_coverage_map_parse(const char *functions, size_t functions_len,
|
||||
const char *tests, size_t tests_len);
|
||||
void cbm_coverage_map_free(cbm_coverage_map_t *map);
|
||||
/* Exact original functions.tsv byte digest; borrowed until map_free. */
|
||||
const char *cbm_coverage_map_functions_sha256(const cbm_coverage_map_t *map);
|
||||
const cbm_coverage_function_t *cbm_coverage_map_functions(const cbm_coverage_map_t *map,
|
||||
int *count);
|
||||
/* Rows are ordered by (suite, name), independent of input order. */
|
||||
const cbm_coverage_test_t *cbm_coverage_map_tests(const cbm_coverage_map_t *map, int *count);
|
||||
const cbm_coverage_function_t *cbm_coverage_map_find_function(const cbm_coverage_map_t *map,
|
||||
const char *file, const char *name);
|
||||
const cbm_coverage_test_t *cbm_coverage_map_find_test(const cbm_coverage_map_t *map,
|
||||
const char *suite, const char *name);
|
||||
/* Coverage intersection only. false does not establish that a test is safe
|
||||
* to omit: static reachability, incomplete/setup rows and other rules still apply. */
|
||||
bool cbm_coverage_test_intersects(const cbm_coverage_test_t *test, const int *changed_ids,
|
||||
int changed_count);
|
||||
|
||||
/* Cross-check format-1 metadata against every parsed row and the trusted
|
||||
* artifact manifest's commit. Does not establish age, ancestry, harness/build
|
||||
* compatibility, provenance or expected runner membership; all remain required.
|
||||
* Valid incomplete rows stay incomplete and cannot justify narrowing. */
|
||||
bool cbm_coverage_map_metadata_matches(const cbm_coverage_map_t *map, const char *metadata,
|
||||
size_t len, const char *expected_commit);
|
||||
|
||||
/* Query-local project configuration. No process environment or global config
|
||||
* participates. Only the fields consumed by this API are validated here;
|
||||
* the selection engine must validate its additional rules before narrowing. */
|
||||
typedef struct cbm_test_config cbm_test_config_t;
|
||||
|
||||
/* Read one regular JSON file, at most 64 KiB. optional permits an absent
|
||||
* default file only; malformed, unreadable and oversized files always fail.
|
||||
* NULL means invalid configuration or allocation failure. */
|
||||
cbm_test_config_t *cbm_test_config_load(const char *path, bool optional);
|
||||
void cbm_test_config_free(cbm_test_config_t *config);
|
||||
bool cbm_test_config_is_test_file(const cbm_test_config_t *config, const char *file);
|
||||
/* Exact source-byte digest (or a domain-separated absent-file digest). */
|
||||
const char *cbm_test_config_digest(const cbm_test_config_t *config);
|
||||
|
||||
/* Pure hybrid-selection kernel. It performs no I/O and does not admit an
|
||||
* artifact. The adapter must establish the evidence flags before calling it.
|
||||
* A false completeness flag is uncertainty, never proof of no changes. */
|
||||
typedef struct {
|
||||
const char *file;
|
||||
const char *test;
|
||||
/* An unambiguous case mapping in the complete static traversal. Merely
|
||||
* finding a graph node is insufficient. Missing rows mean unknown. */
|
||||
bool mapped;
|
||||
bool reached;
|
||||
bool changed;
|
||||
} cbm_test_reach_t;
|
||||
|
||||
enum {
|
||||
CBM_TEST_SELECT_CHANGED = 1U << 0,
|
||||
CBM_TEST_SELECT_STATIC = 1U << 1,
|
||||
CBM_TEST_SELECT_COVERAGE = 1U << 2,
|
||||
CBM_TEST_SELECT_UNMAPPED = 1U << 3,
|
||||
CBM_TEST_SELECT_COVERAGE_UNKNOWN = 1U << 4,
|
||||
CBM_TEST_SELECT_SETUP_HIT = 1U << 5,
|
||||
CBM_TEST_SELECT_SETUP_UNKNOWN = 1U << 6,
|
||||
CBM_TEST_SELECT_INVENTORY_UNKNOWN = 1U << 7,
|
||||
CBM_TEST_SELECT_CONDITIONAL = 1U << 8,
|
||||
CBM_TEST_SELECT_ARTIFACT_REJECTED = 1U << 9,
|
||||
CBM_TEST_SELECT_DIFF_INCOMPLETE = 1U << 10,
|
||||
CBM_TEST_SELECT_STATIC_INCOMPLETE = 1U << 11,
|
||||
CBM_TEST_SELECT_CHANGE_IDENTITY_UNKNOWN = 1U << 12,
|
||||
CBM_TEST_SELECT_INVALID_INPUT = 1U << 13,
|
||||
CBM_TEST_SELECT_CHANGED_UNREGISTERED = 1U << 14,
|
||||
CBM_TEST_SELECT_RULE = 1U << 15
|
||||
};
|
||||
|
||||
/* Reached/changed suite bodies and explicit suite rules select the whole
|
||||
* suite even when no individual case is reached. Names come from the runner. */
|
||||
typedef struct {
|
||||
const char *suite;
|
||||
bool reached;
|
||||
bool changed;
|
||||
bool rule;
|
||||
} cbm_test_suite_trigger_t;
|
||||
|
||||
typedef struct {
|
||||
const cbm_test_model_t *model; /* finished, borrowed for this call only */
|
||||
const cbm_coverage_map_t *coverage;
|
||||
const cbm_test_reach_t *reach;
|
||||
int reach_count;
|
||||
const cbm_test_suite_trigger_t *suite_triggers;
|
||||
int suite_trigger_count;
|
||||
const int *changed_function_ids;
|
||||
int changed_function_count;
|
||||
bool has_changes;
|
||||
bool diff_complete;
|
||||
bool inventory_complete; /* external file/runner inventory, separate from model parse */
|
||||
bool static_complete; /* complete seeds and uncapped no-lift traversal */
|
||||
bool coverage_admitted; /* independently verified artifact admission */
|
||||
bool coverage_changes_complete; /* includes old-side/deleted identities */
|
||||
} cbm_test_selection_input_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
bool whole;
|
||||
unsigned reasons; /* union of whole-suite causes and retained-test reasons */
|
||||
} cbm_test_selected_suite_t;
|
||||
|
||||
typedef struct {
|
||||
const char *suite;
|
||||
const char *test;
|
||||
const char *file;
|
||||
unsigned reasons;
|
||||
} cbm_test_selected_case_t;
|
||||
|
||||
typedef struct cbm_test_selection cbm_test_selection_t;
|
||||
|
||||
/* NULL means allocation failure: callers MUST use their full-run fallback.
|
||||
* No successful partial result is returned. Results own all their strings.
|
||||
* A run-all result has no suite/case rows; its reason mask is nonzero.
|
||||
* Whole suites dominate individual rows. Selected conditional registrations
|
||||
* use whole suites until the runner has an explicit optional-test contract. */
|
||||
cbm_test_selection_t *cbm_test_select(const cbm_test_selection_input_t *input);
|
||||
void cbm_test_selection_free(cbm_test_selection_t *selection);
|
||||
unsigned cbm_test_selection_run_all(const cbm_test_selection_t *selection);
|
||||
/* Only selected suites/cases appear, ordered by name and (suite,test). */
|
||||
const cbm_test_selected_suite_t *cbm_test_selection_suites(const cbm_test_selection_t *selection,
|
||||
int *count);
|
||||
const cbm_test_selected_case_t *cbm_test_selection_cases(const cbm_test_selection_t *selection,
|
||||
int *count);
|
||||
|
||||
/* Coverage receipt-policy checks. This is NOT artifact retrieval or proof of
|
||||
* provenance: the adapter must independently establish the context below.
|
||||
* SHA-256 strings use 64 lowercase hex digits and bind exact file bytes. */
|
||||
typedef struct {
|
||||
const char *commit;
|
||||
const char *functions_sha256;
|
||||
const char *tests_sha256;
|
||||
const char *metadata_sha256;
|
||||
const char *graph_sha256;
|
||||
const char *compatibility_sha256;
|
||||
/* Oldest retained test/setup observation, including complete empty rows.
|
||||
* A full-refresh baseline is valid only if all retained rows were refreshed.
|
||||
* Incremental publication MUST NOT advance this timestamp by itself. */
|
||||
int64_t oldest_observation_at;
|
||||
} cbm_coverage_receipt_t;
|
||||
|
||||
typedef struct {
|
||||
/* Authenticates the entire receipt tuple and expected producer/repository,
|
||||
* not merely a download location. Never populated from artifact JSON. */
|
||||
bool source_verified;
|
||||
const char *trusted_commit;
|
||||
/* Evidence is for this exact pair, in this one frozen comparison. Git
|
||||
* errors/unknown ancestry are false; replacement objects must be disabled. */
|
||||
bool ancestor_verified;
|
||||
const char *ancestor_commit;
|
||||
const char *ancestor_merge_base;
|
||||
const char *merge_base;
|
||||
/* Independently measured from the exact team graph imported for this map,
|
||||
* before the adapter incrementally updates it to the PR head. */
|
||||
const char *graph_commit;
|
||||
const char *graph_sha256;
|
||||
/* Independently computed with the same versioned input encoding as the
|
||||
* producer, covering harness, runner, spawn helpers, config, build flags,
|
||||
* platform and toolchain. Equality cannot establish those inputs itself. */
|
||||
const char *compatibility_sha256;
|
||||
int64_t now;
|
||||
} cbm_coverage_receipt_context_t;
|
||||
|
||||
enum {
|
||||
CBM_COVERAGE_RECEIPT_INVALID = 1U << 0,
|
||||
CBM_COVERAGE_RECEIPT_SOURCE = 1U << 1,
|
||||
CBM_COVERAGE_RECEIPT_ANCESTRY = 1U << 2,
|
||||
CBM_COVERAGE_RECEIPT_COMMIT = 1U << 3,
|
||||
CBM_COVERAGE_RECEIPT_CONTENT = 1U << 4,
|
||||
CBM_COVERAGE_RECEIPT_AGE = 1U << 5,
|
||||
CBM_COVERAGE_RECEIPT_COMPATIBILITY = 1U << 6,
|
||||
CBM_COVERAGE_RECEIPT_METADATA = 1U << 7
|
||||
};
|
||||
|
||||
/* Zero means every receipt-policy check passed against the supplied evidence.
|
||||
* Newest eligible ancestor discovery, authenticated receipt delivery, actual
|
||||
* ancestry checks, graph freshness at head and runner inventory stay with the
|
||||
* adapter. A nonzero result prohibits coverage-based omission. */
|
||||
unsigned cbm_coverage_map_check_receipt(const cbm_coverage_map_t *map, const char *metadata,
|
||||
size_t metadata_len, const cbm_coverage_receipt_t *receipt,
|
||||
const cbm_coverage_receipt_context_t *context);
|
||||
|
||||
/* Exact bytes retained by the query-local loader; borrowed until config free.
|
||||
* An optional absent file is distinct from an invalid config (NULL). */
|
||||
const char *cbm_test_config_source(const cbm_test_config_t *config, size_t *len, bool *absent);
|
||||
|
||||
typedef enum {
|
||||
CBM_TEST_RULE_RUN_ALL,
|
||||
CBM_TEST_RULE_LANES,
|
||||
CBM_TEST_RULE_SUITES,
|
||||
CBM_TEST_RULE_REFERENCING_TESTS,
|
||||
CBM_TEST_RULE_IGNORE
|
||||
} cbm_test_rule_action_t;
|
||||
|
||||
typedef struct {
|
||||
const char *id;
|
||||
cbm_test_rule_action_t action;
|
||||
const char *const *paths;
|
||||
int path_count;
|
||||
const char *const *suites;
|
||||
int suite_count;
|
||||
const char *const *lanes;
|
||||
int lane_count;
|
||||
int key_segment; /* zero-based path_segment:N; -1 for other actions */
|
||||
bool builtin;
|
||||
} cbm_test_rule_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *const *suites;
|
||||
int suite_count;
|
||||
const char *const *exclude_suites;
|
||||
int exclude_count;
|
||||
const char *const *signals;
|
||||
int signal_count;
|
||||
bool default_run;
|
||||
bool narrow; /* omitted => false; never changes lane activation itself */
|
||||
} cbm_test_lane_t;
|
||||
|
||||
typedef struct cbm_test_policy cbm_test_policy_t;
|
||||
|
||||
/* Parse rules/lanes from the exact loaded source into an independent owner.
|
||||
* NULL means invalid consumed policy or allocation/expansion failure; no
|
||||
* partial policy escapes. Presets/conventions/signals definitions are left to
|
||||
* their adapters. Existing seed-only config loading does not call this API.
|
||||
* Missing policy uses conservative built-ins and one unit lane (narrow=true,
|
||||
* all suites except @perf). Project rules precede built-ins, FIRST MATCH wins.
|
||||
* Pattern grammar: positive gitignore-style globs plus comma brace alternatives
|
||||
* (including nested/adjacent groups); no shell evaluation, ranges or regex.
|
||||
* Expansion is bounded; overflow rejects the whole policy. */
|
||||
cbm_test_policy_t *cbm_test_policy_new(const cbm_test_config_t *config);
|
||||
void cbm_test_policy_free(cbm_test_policy_t *policy);
|
||||
const cbm_test_rule_t *cbm_test_policy_rules(const cbm_test_policy_t *policy, int *count);
|
||||
const cbm_test_lane_t *cbm_test_policy_lanes(const cbm_test_policy_t *policy, int *count);
|
||||
const char *cbm_test_policy_digest(const cbm_test_policy_t *policy);
|
||||
/* test_impact.coverage.compatibility_paths in config order: the files besides
|
||||
* the measured code whose bytes a coverage map depends on (harness, runner,
|
||||
* spawn helpers, build flags). NULL with *count 0 when none are configured. */
|
||||
const char *const *cbm_test_policy_compatibility_paths(const cbm_test_policy_t *policy, int *count);
|
||||
|
||||
/* Successful classification returns true and either the first matched rule
|
||||
* or NULL for no match. false is an evaluation error, NEVER permission to
|
||||
* fall through or omit. Target applicability is resolved separately: an
|
||||
* unavailable target does not permit consulting a later rule. */
|
||||
bool cbm_test_policy_match_path(const cbm_test_policy_t *policy, const char *path,
|
||||
const cbm_test_rule_t **rule);
|
||||
/* Match one known suite against lane membership or a suites rule. @perf is a
|
||||
* semantic selector. These functions do not establish inventory completeness,
|
||||
* platform applicability, or whether a lane is activated. false means error. */
|
||||
bool cbm_test_policy_lane_selects_suite(const cbm_test_policy_t *policy, int lane,
|
||||
const char *suite, bool perf, bool *selected);
|
||||
bool cbm_test_policy_rule_selects_suite(const cbm_test_policy_t *policy, int rule,
|
||||
const char *suite, bool perf, bool *selected);
|
||||
|
||||
#endif /* CBM_MCP_TEST_IMPACT_H */
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* test_impact_artifact.c — receipt.json and the bundle digests.
|
||||
*/
|
||||
#include "mcp/test_impact_artifact.h"
|
||||
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/mem_core.h"
|
||||
#include "foundation/sha256.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <yyjson/yyjson.h>
|
||||
|
||||
enum { TA_READ_CHUNK = 65536, TA_RECEIPT_MAX = 65536, TA_PATH_CAP = 4096 };
|
||||
|
||||
/* yyjson allocates through the memory core, so documents and written text
|
||||
* are released with cbm_free like everything else here. */
|
||||
static void *ta_yy_malloc(void *ctx, size_t size) {
|
||||
(void)ctx;
|
||||
return cbm_alloc(CBM_MEM_CLASS_OTHER, size);
|
||||
}
|
||||
|
||||
static void *ta_yy_realloc(void *ctx, void *block, size_t old_size, size_t size) {
|
||||
(void)ctx;
|
||||
(void)old_size;
|
||||
return cbm_realloc(CBM_MEM_CLASS_OTHER, block, size);
|
||||
}
|
||||
|
||||
static void ta_yy_free(void *ctx, void *block) {
|
||||
(void)ctx;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, block);
|
||||
}
|
||||
|
||||
static const yyjson_alc ta_yy_alc = {ta_yy_malloc, ta_yy_realloc, ta_yy_free, NULL};
|
||||
|
||||
static bool ta_hex(const char *text, size_t want_a, size_t want_b) {
|
||||
size_t n = text ? strlen(text) : 0;
|
||||
if (n != want_a && n != want_b) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!((text[i] >= '0' && text[i] <= '9') || (text[i] >= 'a' && text[i] <= 'f'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ta_hex_out(const uint8_t digest[CBM_SHA256_DIGEST_LEN], char hex[65]) {
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
for (int i = 0; i < CBM_SHA256_DIGEST_LEN; i++) {
|
||||
hex[2 * i] = digits[digest[i] >> 4];
|
||||
hex[2 * i + 1] = digits[digest[i] & 15];
|
||||
}
|
||||
hex[64] = '\0';
|
||||
}
|
||||
|
||||
static bool ta_path(char *out, const char *dir, const char *name) {
|
||||
int n = snprintf(out, TA_PATH_CAP, "%s/%s", dir, name);
|
||||
return n > 0 && n < TA_PATH_CAP;
|
||||
}
|
||||
|
||||
/* A length-prefixed frame, so no field can run into the next. */
|
||||
static void ta_frame(cbm_sha256_ctx *ctx, const void *bytes, uint64_t length) {
|
||||
uint8_t prefix[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
prefix[i] = (uint8_t)(length >> (56 - 8 * i));
|
||||
}
|
||||
cbm_sha256_update(ctx, prefix, sizeof(prefix));
|
||||
if (length) {
|
||||
cbm_sha256_update(ctx, bytes, (size_t)length);
|
||||
}
|
||||
}
|
||||
|
||||
/* Feeds a file's exact bytes as one frame; false on a read error.
|
||||
* *absent: the file does not exist (a frame-free outcome the caller encodes). */
|
||||
static bool ta_feed_file(cbm_sha256_ctx *ctx, const char *path, bool framed, bool *absent) {
|
||||
*absent = false;
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
if (!f) {
|
||||
cbm_path_info_t info;
|
||||
*absent = cbm_path_info_utf8(path, &info) != 0;
|
||||
return *absent;
|
||||
}
|
||||
unsigned char *buf = cbm_alloc(CBM_MEM_CLASS_OTHER, TA_READ_CHUNK);
|
||||
if (!buf) {
|
||||
(void)fclose(f);
|
||||
return false;
|
||||
}
|
||||
/* Size first, so a framed file's prefix is its real length. */
|
||||
uint64_t total = 0;
|
||||
size_t got;
|
||||
while ((got = fread(buf, 1, TA_READ_CHUNK, f)) > 0) {
|
||||
total += got;
|
||||
}
|
||||
bool ok = !ferror(f) && fseek(f, 0, SEEK_SET) == 0;
|
||||
if (ok && framed) {
|
||||
ta_frame(ctx, NULL, 0); /* domain: a present file */
|
||||
uint8_t prefix[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
prefix[i] = (uint8_t)(total >> (56 - 8 * i));
|
||||
}
|
||||
cbm_sha256_update(ctx, prefix, sizeof(prefix));
|
||||
}
|
||||
uint64_t again = 0;
|
||||
while (ok && (got = fread(buf, 1, TA_READ_CHUNK, f)) > 0) {
|
||||
cbm_sha256_update(ctx, buf, got);
|
||||
again += got;
|
||||
}
|
||||
ok = ok && !ferror(f) && again == total;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, buf);
|
||||
ok = fclose(f) == 0 && ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool cbm_ti_sha256_file(const char *path, char hex[65]) {
|
||||
hex[0] = '\0';
|
||||
cbm_sha256_ctx ctx;
|
||||
cbm_sha256_init(&ctx);
|
||||
bool absent = false;
|
||||
if (!path || !ta_feed_file(&ctx, path, false, &absent) || absent) {
|
||||
return false;
|
||||
}
|
||||
uint8_t digest[CBM_SHA256_DIGEST_LEN];
|
||||
cbm_sha256_final(&ctx, digest);
|
||||
ta_hex_out(digest, hex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cbm_ti_compatibility_digest(const char *root, const char *const *paths, int path_count,
|
||||
const char *platform, char hex[65]) {
|
||||
hex[0] = '\0';
|
||||
if (!root || !platform || path_count < 0 || (path_count && !paths)) {
|
||||
return false;
|
||||
}
|
||||
static const char domain[] = "cbm.test_impact.compatibility.v1";
|
||||
cbm_sha256_ctx ctx;
|
||||
cbm_sha256_init(&ctx);
|
||||
ta_frame(&ctx, domain, sizeof(domain) - 1);
|
||||
ta_frame(&ctx, platform, strlen(platform));
|
||||
for (int i = 0; i < path_count; i++) {
|
||||
char path[TA_PATH_CAP];
|
||||
if (!paths[i] || !*paths[i] || !ta_path(path, root, paths[i])) {
|
||||
return false;
|
||||
}
|
||||
ta_frame(&ctx, paths[i], strlen(paths[i]));
|
||||
bool absent = false;
|
||||
if (!ta_feed_file(&ctx, path, true, &absent)) {
|
||||
return false;
|
||||
}
|
||||
if (absent) {
|
||||
static const char marker[] = "absent";
|
||||
ta_frame(&ctx, marker, sizeof(marker) - 1);
|
||||
}
|
||||
}
|
||||
uint8_t digest[CBM_SHA256_DIGEST_LEN];
|
||||
cbm_sha256_final(&ctx, digest);
|
||||
ta_hex_out(digest, hex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ta_copy_string(yyjson_val *root, const char *key, char *out, size_t cap) {
|
||||
yyjson_val *v = yyjson_obj_get(root, key);
|
||||
const char *s = yyjson_is_str(v) ? yyjson_get_str(v) : NULL;
|
||||
if (!s || strlen(s) >= cap) {
|
||||
return false;
|
||||
}
|
||||
memcpy(out, s, strlen(s) + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
cbm_ti_receipt_status_t cbm_ti_receipt_read(const char *bundle_dir, cbm_ti_receipt_t *out) {
|
||||
if (!out) {
|
||||
return CBM_TI_RECEIPT_INVALID;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char path[TA_PATH_CAP];
|
||||
if (!bundle_dir || !ta_path(path, bundle_dir, CBM_TI_RECEIPT_FILE)) {
|
||||
return CBM_TI_RECEIPT_INVALID;
|
||||
}
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
if (!f) {
|
||||
cbm_path_info_t info;
|
||||
return cbm_path_info_utf8(path, &info) != 0 ? CBM_TI_RECEIPT_ABSENT : CBM_TI_RECEIPT_IO;
|
||||
}
|
||||
char *text = cbm_alloc(CBM_MEM_CLASS_OTHER, TA_RECEIPT_MAX + 1);
|
||||
size_t len = text ? fread(text, 1, TA_RECEIPT_MAX + 1, f) : 0;
|
||||
bool read_ok = text && !ferror(f) && len <= TA_RECEIPT_MAX;
|
||||
(void)fclose(f);
|
||||
if (!read_ok) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, text);
|
||||
return text ? CBM_TI_RECEIPT_INVALID : CBM_TI_RECEIPT_IO;
|
||||
}
|
||||
yyjson_doc *doc = yyjson_read_opts(text, len, 0, &ta_yy_alc, NULL);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, text);
|
||||
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
|
||||
cbm_ti_receipt_t r = {0};
|
||||
char schema[64] = "";
|
||||
bool ok = yyjson_is_obj(root) && ta_copy_string(root, "schema", schema, sizeof(schema)) &&
|
||||
strcmp(schema, CBM_TI_RECEIPT_SCHEMA) == 0 &&
|
||||
ta_copy_string(root, "commit", r.commit, sizeof(r.commit)) &&
|
||||
ta_hex(r.commit, 40, 64) &&
|
||||
ta_copy_string(root, "graph_sha256", r.graph_sha256, sizeof(r.graph_sha256)) &&
|
||||
ta_hex(r.graph_sha256, 64, 64) &&
|
||||
ta_copy_string(root, "graph_content_sha256", r.graph_content_sha256,
|
||||
sizeof(r.graph_content_sha256)) &&
|
||||
ta_hex(r.graph_content_sha256, 64, 64) &&
|
||||
ta_copy_string(root, "graph_topology_sha256", r.graph_topology_sha256,
|
||||
sizeof(r.graph_topology_sha256)) &&
|
||||
ta_hex(r.graph_topology_sha256, 64, 64) &&
|
||||
ta_copy_string(root, "platform", r.platform, sizeof(r.platform));
|
||||
yyjson_val *coverage = ok ? yyjson_obj_get(root, "coverage") : NULL;
|
||||
if (ok && coverage && !yyjson_is_null(coverage)) {
|
||||
yyjson_val *oldest = yyjson_obj_get(coverage, "oldest_observation_at");
|
||||
r.has_coverage = true;
|
||||
ok = yyjson_is_obj(coverage) &&
|
||||
ta_copy_string(coverage, "functions_sha256", r.functions_sha256, 65) &&
|
||||
ta_hex(r.functions_sha256, 64, 64) &&
|
||||
ta_copy_string(coverage, "tests_sha256", r.tests_sha256, 65) &&
|
||||
ta_hex(r.tests_sha256, 64, 64) &&
|
||||
ta_copy_string(coverage, "metadata_sha256", r.metadata_sha256, 65) &&
|
||||
ta_hex(r.metadata_sha256, 64, 64) &&
|
||||
ta_copy_string(coverage, "compatibility_sha256", r.compatibility_sha256, 65) &&
|
||||
ta_hex(r.compatibility_sha256, 64, 64) && yyjson_is_int(oldest) &&
|
||||
yyjson_get_sint(oldest) > 0;
|
||||
r.oldest_observation_at = ok ? yyjson_get_sint(oldest) : 0;
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
if (!ok) {
|
||||
return CBM_TI_RECEIPT_INVALID;
|
||||
}
|
||||
*out = r;
|
||||
return CBM_TI_RECEIPT_OK;
|
||||
}
|
||||
|
||||
bool cbm_ti_receipt_write(const char *bundle_dir, const cbm_ti_receipt_t *r) {
|
||||
char path[TA_PATH_CAP];
|
||||
char tmp[TA_PATH_CAP];
|
||||
if (!bundle_dir || !r || !ta_path(path, bundle_dir, CBM_TI_RECEIPT_FILE) ||
|
||||
!ta_path(tmp, bundle_dir, CBM_TI_RECEIPT_FILE ".tmp")) {
|
||||
return false;
|
||||
}
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(&ta_yy_alc);
|
||||
yyjson_mut_val *root = doc ? yyjson_mut_obj(doc) : NULL;
|
||||
if (!root) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
return false;
|
||||
}
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
yyjson_mut_obj_add_str(doc, root, "schema", CBM_TI_RECEIPT_SCHEMA);
|
||||
yyjson_mut_obj_add_str(doc, root, "commit", r->commit);
|
||||
yyjson_mut_obj_add_str(doc, root, "graph_sha256", r->graph_sha256);
|
||||
yyjson_mut_obj_add_str(doc, root, "graph_content_sha256", r->graph_content_sha256);
|
||||
yyjson_mut_obj_add_str(doc, root, "graph_topology_sha256", r->graph_topology_sha256);
|
||||
yyjson_mut_obj_add_str(doc, root, "platform", r->platform);
|
||||
if (r->has_coverage) {
|
||||
yyjson_mut_val *coverage = yyjson_mut_obj_add_obj(doc, root, "coverage");
|
||||
yyjson_mut_obj_add_str(doc, coverage, "functions_sha256", r->functions_sha256);
|
||||
yyjson_mut_obj_add_str(doc, coverage, "tests_sha256", r->tests_sha256);
|
||||
yyjson_mut_obj_add_str(doc, coverage, "metadata_sha256", r->metadata_sha256);
|
||||
yyjson_mut_obj_add_str(doc, coverage, "compatibility_sha256", r->compatibility_sha256);
|
||||
yyjson_mut_obj_add_int(doc, coverage, "oldest_observation_at", r->oldest_observation_at);
|
||||
} else {
|
||||
yyjson_mut_obj_add_null(doc, root, "coverage");
|
||||
}
|
||||
size_t len = 0;
|
||||
char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_PRETTY, &ta_yy_alc, &len, NULL);
|
||||
yyjson_mut_doc_free(doc);
|
||||
if (!json) {
|
||||
return false;
|
||||
}
|
||||
FILE *f = cbm_fopen(tmp, "wb");
|
||||
bool ok = f && fwrite(json, 1, len, f) == len && fputc('\n', f) != EOF;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
||||
if (f) {
|
||||
ok = fclose(f) == 0 && ok;
|
||||
}
|
||||
ok = ok && cbm_rename_replace(tmp, path) == 0;
|
||||
if (!ok) {
|
||||
(void)cbm_unlink(tmp);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* test_impact_artifact.h — The team artifact bundle of one commit.
|
||||
*
|
||||
* A bundle travels from the main-branch producer to PR selection (never
|
||||
* committed). Layout under its directory:
|
||||
*
|
||||
* receipt.json this module; the bundle's authority
|
||||
* .codebase-memory/graph.db.zst the commit's frozen graph (artifact.h)
|
||||
* .codebase-memory/artifact.json
|
||||
* coverage/functions.tsv optional coverage map, format 1
|
||||
* coverage/tests.tsv
|
||||
* coverage/meta.json
|
||||
*
|
||||
* The receipt binds every part by SHA-256: the graph by the digest of the
|
||||
* graph as an importer sees it (cbm_store_graph_digest of the imported
|
||||
* database), the map by the exact bytes of its three files. A digest is
|
||||
* content binding only; where a bundle came from is established by whoever
|
||||
* fetched it, never by anything inside it.
|
||||
*/
|
||||
#ifndef CBM_TEST_IMPACT_ARTIFACT_H
|
||||
#define CBM_TEST_IMPACT_ARTIFACT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define CBM_TI_RECEIPT_FILE "receipt.json"
|
||||
#define CBM_TI_RECEIPT_SCHEMA "cbm.test_impact.artifact.v1"
|
||||
#define CBM_TI_COVERAGE_DIR "coverage"
|
||||
|
||||
typedef struct {
|
||||
char commit[65]; /* 40 or 64 lowercase hex */
|
||||
char graph_sha256[65]; /* 64 lowercase hex */
|
||||
/* cbm_store_graph_content_digest of the same graph: equal for equal graphs
|
||||
* however they were built (the weekly incremental-equals-full check). */
|
||||
char graph_content_sha256[65];
|
||||
/* cbm_store_graph_topology_digest: what selection reads. The weekly check
|
||||
* fails when an incremental graph's topology differs from the full one. */
|
||||
char graph_topology_sha256[65];
|
||||
bool has_coverage;
|
||||
char functions_sha256[65];
|
||||
char tests_sha256[65];
|
||||
char metadata_sha256[65];
|
||||
char compatibility_sha256[65];
|
||||
/* Oldest retained coverage observation (seconds since the epoch). A full
|
||||
* refresh sets it; an incremental publish carries it forward unchanged. */
|
||||
int64_t oldest_observation_at;
|
||||
char platform[128]; /* producer's platform/toolchain label, informational */
|
||||
} cbm_ti_receipt_t;
|
||||
|
||||
typedef enum {
|
||||
CBM_TI_RECEIPT_OK = 0,
|
||||
CBM_TI_RECEIPT_ABSENT, /* no receipt.json */
|
||||
CBM_TI_RECEIPT_INVALID, /* unreadable shape, unknown schema, a malformed field */
|
||||
CBM_TI_RECEIPT_IO,
|
||||
} cbm_ti_receipt_status_t;
|
||||
|
||||
/* Strict: every field present and well formed, no unknown schema. Clears *out
|
||||
* first; on any non-OK status *out stays zero. */
|
||||
cbm_ti_receipt_status_t cbm_ti_receipt_read(const char *bundle_dir, cbm_ti_receipt_t *out);
|
||||
|
||||
/* Writes receipt.json atomically (temporary file + rename). */
|
||||
bool cbm_ti_receipt_write(const char *bundle_dir, const cbm_ti_receipt_t *receipt);
|
||||
|
||||
/* Lowercase SHA-256 hex of a file's exact bytes. false when unreadable. */
|
||||
bool cbm_ti_sha256_file(const char *path, char hex[65]);
|
||||
|
||||
/* Versioned digest of what coverage depends on beyond the code it measures:
|
||||
* the platform/toolchain label and the exact bytes of each listed path under
|
||||
* root (an absent path is encoded as absent, so adding one changes it).
|
||||
* Producer and consumer call this same function; equality is the check. */
|
||||
bool cbm_ti_compatibility_digest(const char *root, const char *const *paths, int path_count,
|
||||
const char *platform, char hex[65]);
|
||||
|
||||
#endif /* CBM_TEST_IMPACT_ARTIFACT_H */
|
||||
@@ -0,0 +1,524 @@
|
||||
#include "mcp/test_impact_changes.h"
|
||||
|
||||
#include "foundation/arena.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
struct cbm_changes {
|
||||
CBMArena arena;
|
||||
unsigned char *name_status;
|
||||
size_t name_status_length;
|
||||
unsigned char *patch;
|
||||
size_t patch_length;
|
||||
cbm_diff_t *diff;
|
||||
cbm_change_file_t *files;
|
||||
size_t count;
|
||||
cbm_changes_state_t state;
|
||||
unsigned issues;
|
||||
bool reconciled;
|
||||
bool can_narrow;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const cbm_diff_file_t *file;
|
||||
size_t count;
|
||||
} changes_match_t;
|
||||
|
||||
typedef struct {
|
||||
const unsigned char *bytes;
|
||||
size_t length;
|
||||
} changes_slice_t;
|
||||
|
||||
/* Git tree paths use '/' on every host; backslash/colon are literal bytes. */
|
||||
static bool changes_path_valid(const unsigned char *path, size_t length) {
|
||||
if (!length || path[0] == '/' || path[length - 1] == '/') {
|
||||
return false;
|
||||
}
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= length; i++) {
|
||||
if (i < length && path[i] != '/') {
|
||||
if (path[i] == 0) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
size_t component = i - start;
|
||||
if (!component || (component == 1 && path[start] == '.') ||
|
||||
(component == 2 && path[start] == '.' && path[start + 1] == '.')) {
|
||||
return false;
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool changes_record(const unsigned char *bytes, size_t length, size_t *offset, char *status,
|
||||
changes_slice_t *path) {
|
||||
size_t at = *offset;
|
||||
if (length - at < 4 || bytes[at + 1] != 0 ||
|
||||
(bytes[at] != 'A' && bytes[at] != 'M' && bytes[at] != 'D' && bytes[at] != 'T')) {
|
||||
return false;
|
||||
}
|
||||
const unsigned char *first = bytes + at + 2;
|
||||
const unsigned char *end = memchr(first, 0, length - at - 2);
|
||||
if (!end) {
|
||||
return false;
|
||||
}
|
||||
path->bytes = first;
|
||||
path->length = (size_t)(end - first);
|
||||
if (!changes_path_valid(path->bytes, path->length)) {
|
||||
return false;
|
||||
}
|
||||
*status = (char)bytes[at];
|
||||
*offset = (size_t)(end - bytes) + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int changes_path_compare(const unsigned char *a, size_t a_length, const unsigned char *b,
|
||||
size_t b_length) {
|
||||
size_t common = a_length < b_length ? a_length : b_length;
|
||||
int order = memcmp(a, b, common); /* specified unsigned-byte comparison */
|
||||
return order ? order : (a_length > b_length) - (a_length < b_length);
|
||||
}
|
||||
|
||||
/* Stable merge order, including equal keys before duplicate rejection. */
|
||||
static void changes_sort(cbm_change_file_t *files, cbm_change_file_t *scratch, size_t count) {
|
||||
cbm_change_file_t *source = files;
|
||||
cbm_change_file_t *target = scratch;
|
||||
for (size_t width = 1; width < count;) {
|
||||
for (size_t left = 0; left < count;) {
|
||||
size_t middle = left + (width < count - left ? width : count - left);
|
||||
size_t right = middle + (width < count - middle ? width : count - middle);
|
||||
size_t a = left;
|
||||
size_t b = middle;
|
||||
for (size_t i = left; i < right; i++) {
|
||||
if (b == right || (a < middle && changes_path_compare(
|
||||
source[a].path, source[a].path_length,
|
||||
source[b].path, source[b].path_length) <= 0)) {
|
||||
target[i] = source[a++];
|
||||
} else {
|
||||
target[i] = source[b++];
|
||||
}
|
||||
}
|
||||
left = right;
|
||||
}
|
||||
cbm_change_file_t *swap = source;
|
||||
source = target;
|
||||
target = swap;
|
||||
if (width > count / 2) {
|
||||
break;
|
||||
}
|
||||
width *= 2;
|
||||
}
|
||||
if (source != files) {
|
||||
memcpy(files, source, count * sizeof(*files));
|
||||
}
|
||||
}
|
||||
|
||||
static size_t changes_find(const cbm_changes_t *changes, const unsigned char *path, size_t length) {
|
||||
size_t low = 0;
|
||||
size_t high = changes->count;
|
||||
while (low < high) {
|
||||
size_t middle = low + (high - low) / 2;
|
||||
const cbm_change_file_t *file = &changes->files[middle];
|
||||
int order = changes_path_compare(file->path, file->path_length, path, length);
|
||||
if (!order) {
|
||||
return middle;
|
||||
}
|
||||
if (order < 0) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
static bool changes_starts(changes_slice_t line, const char *prefix) {
|
||||
size_t length = strlen(prefix);
|
||||
return line.length >= length && memcmp(line.bytes, prefix, length) == 0;
|
||||
}
|
||||
|
||||
static bool changes_line(const unsigned char *bytes, size_t length, size_t *offset,
|
||||
changes_slice_t *line) {
|
||||
if (*offset == length) {
|
||||
return false;
|
||||
}
|
||||
const unsigned char *first = bytes + *offset;
|
||||
const unsigned char *end = memchr(first, '\n', length - *offset);
|
||||
line->bytes = first;
|
||||
line->length = end ? (size_t)(end - first) : length - *offset;
|
||||
*offset += line->length + (end ? 1 : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool changes_header_path(changes_slice_t line, changes_slice_t *path) {
|
||||
static const char prefix[] = "diff --git a/";
|
||||
const size_t prefix_length = sizeof(prefix) - 1;
|
||||
if (!changes_starts(line, prefix)) {
|
||||
return false;
|
||||
}
|
||||
size_t rest = line.length - prefix_length;
|
||||
if (rest < 5 || (rest - 3) % 2) {
|
||||
return false;
|
||||
}
|
||||
size_t length = (rest - 3) / 2;
|
||||
const unsigned char *first = line.bytes + prefix_length;
|
||||
if (memcmp(first + length, " b/", 3) || memcmp(first, first + length + 3, length) ||
|
||||
!changes_path_valid(first, length)) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
/* Git quotes these bytes even with core.quotePath=false. The existing
|
||||
* reader does not decode that representation. Non-UTF8 remains raw. */
|
||||
if (first[i] < 32 || first[i] == 127 || first[i] == '"' || first[i] == '\\') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*path = (changes_slice_t){first, length};
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool changes_mode(changes_slice_t line, const char *prefix) {
|
||||
size_t length = strlen(prefix);
|
||||
if (line.length != length + 6) {
|
||||
return false;
|
||||
}
|
||||
const unsigned char *mode = line.bytes + length;
|
||||
return memcmp(mode, "100644", 6) == 0 || memcmp(mode, "100755", 6) == 0 ||
|
||||
memcmp(mode, "120000", 6) == 0 || memcmp(mode, "160000", 6) == 0;
|
||||
}
|
||||
|
||||
static bool changes_side_path(changes_slice_t line, const cbm_change_file_t *file, bool old) {
|
||||
const unsigned char *bytes = line.bytes + 4; /* checked ---/+++ prefix */
|
||||
size_t length = line.length - 4;
|
||||
bool absent = old ? file->status == 'A' : file->status == 'D';
|
||||
if (absent) {
|
||||
return length == 9 && memcmp(bytes, "/dev/null", 9) == 0;
|
||||
}
|
||||
if (length < 2 || bytes[0] != (old ? 'a' : 'b') || bytes[1] != '/') {
|
||||
return false;
|
||||
}
|
||||
bytes += 2;
|
||||
length -= 2;
|
||||
/* Accept an exact trailing TAB delimiter after the known path, never
|
||||
* timestamps, path truncation or another spelling. */
|
||||
if (length > file->path_length && length - file->path_length == 1 &&
|
||||
bytes[length - 1] == '\t') {
|
||||
length--;
|
||||
}
|
||||
return length == file->path_length && memcmp(bytes, file->path, length) == 0;
|
||||
}
|
||||
|
||||
/* The legacy reader discards the old start. Keep its old-side range from
|
||||
* certifying an impossible nonempty range beginning at zero. */
|
||||
static bool changes_old_range_usable(changes_slice_t line) {
|
||||
if (!changes_starts(line, "@@ -")) {
|
||||
return false;
|
||||
}
|
||||
size_t offset = 4;
|
||||
int values[2] = {0, 1};
|
||||
for (int part = 0; part < 2; part++) {
|
||||
size_t first = offset;
|
||||
int value = 0;
|
||||
while (offset < line.length && line.bytes[offset] >= '0' && line.bytes[offset] <= '9') {
|
||||
int digit = line.bytes[offset++] - '0';
|
||||
if (value > (INT_MAX - digit) / 10) {
|
||||
return false;
|
||||
}
|
||||
value = value * 10 + digit;
|
||||
}
|
||||
if (offset == first) {
|
||||
return false;
|
||||
}
|
||||
values[part] = value;
|
||||
if (part == 0 && offset < line.length && line.bytes[offset] == ',') {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return offset < line.length && line.bytes[offset] == ' ' && (values[1] == 0 || values[0] > 0);
|
||||
}
|
||||
|
||||
/* The legacy file result does not retain old/new mode metadata or the old
|
||||
* hunk start. Audit headers, never payload (removed text can look like ---). */
|
||||
static void changes_audit_headers(cbm_changes_t *changes) {
|
||||
size_t offset = 0;
|
||||
size_t current = SIZE_MAX;
|
||||
bool in_hunks = false;
|
||||
bool old_seen = false;
|
||||
bool new_seen = false;
|
||||
changes_slice_t line;
|
||||
while (changes_line(changes->patch, changes->patch_length, &offset, &line)) {
|
||||
if (changes_starts(line, "diff ")) {
|
||||
changes_slice_t path;
|
||||
current = SIZE_MAX;
|
||||
in_hunks = false;
|
||||
old_seen = false;
|
||||
new_seen = false;
|
||||
if (!changes_header_path(line, &path)) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
} else {
|
||||
current = changes_find(changes, path.bytes, path.length);
|
||||
if (current == SIZE_MAX) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_EXTRA_PATH;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (changes_starts(line, "@@ ")) {
|
||||
if (!changes_old_range_usable(line)) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
if (current != SIZE_MAX && (!old_seen || !new_seen)) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
in_hunks = true;
|
||||
}
|
||||
if (current == SIZE_MAX || in_hunks) {
|
||||
continue;
|
||||
}
|
||||
cbm_change_file_t *file = &changes->files[current];
|
||||
if (changes_starts(line, "old mode ") || changes_starts(line, "new mode ")) {
|
||||
file->reasons |= CBM_CHANGE_MODE;
|
||||
if (!changes_mode(line,
|
||||
changes_starts(line, "old mode ") ? "old mode " : "new mode ")) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
} else if (changes_starts(line, "new file mode ") ||
|
||||
changes_starts(line, "deleted file mode ")) {
|
||||
if (!changes_mode(line, changes_starts(line, "new file mode ")
|
||||
? "new file mode "
|
||||
: "deleted file mode ")) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
} else if (changes_starts(line, "--- ")) {
|
||||
if (old_seen || !changes_side_path(line, file, true)) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
old_seen = true;
|
||||
} else if (changes_starts(line, "+++ ")) {
|
||||
if (new_seen || !changes_side_path(line, file, false)) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
new_seen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool changes_hunks_usable(const cbm_diff_file_t *file) {
|
||||
if (file->hunk_count <= 0 || !file->hunks) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < file->hunk_count; i++) {
|
||||
const cbm_diff_hunk_t *hunk = &file->hunks[i];
|
||||
if (hunk->start < 0 || hunk->count < 0 || hunk->count > INT_MAX - hunk->start ||
|
||||
hunk->added_count != hunk->count || hunk->removed_count < 0 ||
|
||||
(hunk->count > 0 && (!hunk->added || hunk->start == 0)) ||
|
||||
(hunk->removed_count > 0 && !hunk->removed) ||
|
||||
(hunk->added_count == 0 && hunk->removed_count == 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void changes_reconcile(cbm_changes_t *changes, changes_match_t *matches) {
|
||||
int patch_count = 0;
|
||||
const cbm_diff_file_t *files = cbm_diff_files(changes->diff, &patch_count);
|
||||
if (!cbm_diff_complete(changes->diff)) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
for (int i = 0; i < patch_count; i++) {
|
||||
const cbm_diff_file_t *file = &files[i];
|
||||
size_t length = strlen(file->path);
|
||||
size_t index = changes_path_valid((const unsigned char *)file->path, length)
|
||||
? changes_find(changes, (const unsigned char *)file->path, length)
|
||||
: SIZE_MAX;
|
||||
if (index == SIZE_MAX) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_EXTRA_PATH;
|
||||
continue;
|
||||
}
|
||||
changes_match_t *match = &matches[index];
|
||||
match->file = file;
|
||||
if (match->count++ != 0) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_DUPLICATE_PATH;
|
||||
}
|
||||
char status = changes->files[index].status;
|
||||
if (file->created != (status == 'A') || file->deleted != (status == 'D')) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_FLAG_MISMATCH;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < changes->count; i++) {
|
||||
if (!matches[i].count) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_MISSING_PATH;
|
||||
}
|
||||
}
|
||||
if (changes->count == 0 && changes->patch_length != 0) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_WITHOUT_NAMES;
|
||||
}
|
||||
changes_audit_headers(changes);
|
||||
changes->reconciled = changes->issues == 0;
|
||||
changes->can_narrow = changes->reconciled;
|
||||
for (size_t i = 0; i < changes->count; i++) {
|
||||
cbm_change_file_t *file = &changes->files[i];
|
||||
if (file->status == 'T') {
|
||||
file->reasons |= CBM_CHANGE_TYPE;
|
||||
}
|
||||
if (matches[i].count == 1) {
|
||||
if (matches[i].file->binary) {
|
||||
file->reasons |= CBM_CHANGE_BINARY;
|
||||
}
|
||||
if (!changes_hunks_usable(matches[i].file)) {
|
||||
file->reasons |= CBM_CHANGE_NO_HUNKS;
|
||||
}
|
||||
}
|
||||
if (!changes->reconciled) {
|
||||
file->reasons |= CBM_CHANGE_UNRECONCILED;
|
||||
}
|
||||
if (file->reasons) {
|
||||
file->evidence = CBM_CHANGE_WHOLE_FILE;
|
||||
changes->can_narrow = false;
|
||||
} else {
|
||||
file->evidence = CBM_CHANGE_HUNKS;
|
||||
file->patch_file = matches[i].file;
|
||||
}
|
||||
}
|
||||
changes->state =
|
||||
changes->count ? CBM_CHANGES_NONEMPTY
|
||||
: (changes->patch_length == 0 && changes->reconciled ? CBM_CHANGES_EMPTY
|
||||
: CBM_CHANGES_UNKNOWN);
|
||||
}
|
||||
|
||||
cbm_changes_status_t cbm_changes_parse(const unsigned char *name_status, size_t name_status_length,
|
||||
const unsigned char *patch, size_t patch_length,
|
||||
cbm_changes_t **out) {
|
||||
if (out) {
|
||||
*out = NULL;
|
||||
}
|
||||
if (!out || (!name_status && name_status_length) || (!patch && patch_length) ||
|
||||
name_status_length > PTRDIFF_MAX || patch_length > PTRDIFF_MAX) {
|
||||
return CBM_CHANGES_INVALID;
|
||||
}
|
||||
size_t count = 0;
|
||||
size_t offset = 0;
|
||||
while (offset < name_status_length) {
|
||||
char status;
|
||||
changes_slice_t path;
|
||||
if (!changes_record(name_status, name_status_length, &offset, &status, &path)) {
|
||||
return CBM_CHANGES_INVALID;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
if (count > SIZE_MAX / sizeof(cbm_change_file_t) ||
|
||||
count > SIZE_MAX / sizeof(changes_match_t)) {
|
||||
return CBM_CHANGES_OOM;
|
||||
}
|
||||
CBMArena arena;
|
||||
cbm_arena_init(&arena);
|
||||
cbm_changes_t *changes = cbm_arena_calloc(&arena, sizeof(*changes));
|
||||
if (!changes) {
|
||||
cbm_arena_destroy(&arena);
|
||||
return CBM_CHANGES_OOM;
|
||||
}
|
||||
changes->arena = arena;
|
||||
changes->name_status = cbm_arena_alloc(&changes->arena, name_status_length + 1);
|
||||
changes->patch = cbm_arena_alloc(&changes->arena, patch_length + 1);
|
||||
if (!changes->name_status || !changes->patch) {
|
||||
goto oom;
|
||||
}
|
||||
if (name_status_length) {
|
||||
memcpy(changes->name_status, name_status, name_status_length);
|
||||
}
|
||||
if (patch_length) {
|
||||
memcpy(changes->patch, patch, patch_length);
|
||||
}
|
||||
changes->name_status[name_status_length] = 0;
|
||||
changes->patch[patch_length] = 0;
|
||||
changes->name_status_length = name_status_length;
|
||||
changes->patch_length = patch_length;
|
||||
changes->count = count;
|
||||
changes_match_t *matches = NULL;
|
||||
if (count) {
|
||||
changes->files = cbm_arena_calloc(&changes->arena, count * sizeof(*changes->files));
|
||||
cbm_change_file_t *scratch = cbm_arena_alloc(&changes->arena, count * sizeof(*scratch));
|
||||
matches = cbm_arena_calloc(&changes->arena, count * sizeof(*matches));
|
||||
if (!changes->files || !scratch || !matches) {
|
||||
goto oom;
|
||||
}
|
||||
offset = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
changes_slice_t path;
|
||||
char status;
|
||||
if (!changes_record(changes->name_status, name_status_length, &offset, &status,
|
||||
&path)) {
|
||||
cbm_changes_free(changes);
|
||||
return CBM_CHANGES_INVALID;
|
||||
}
|
||||
changes->files[i].path = path.bytes;
|
||||
changes->files[i].path_length = path.length;
|
||||
changes->files[i].status = status;
|
||||
}
|
||||
changes_sort(changes->files, scratch, count);
|
||||
for (size_t i = 1; i < count; i++) {
|
||||
if (changes_path_compare(changes->files[i - 1].path, changes->files[i - 1].path_length,
|
||||
changes->files[i].path, changes->files[i].path_length) == 0) {
|
||||
cbm_changes_free(changes);
|
||||
return CBM_CHANGES_INVALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Raw NUL is not a representable text patch. Avoid passing it to a legacy
|
||||
* C-string path consumer; retain the exact bytes and conservative inventory. */
|
||||
bool binary_text = patch_length && memchr(changes->patch, 0, patch_length);
|
||||
changes->diff = cbm_diff_parse(binary_text ? "" : (const char *)changes->patch,
|
||||
binary_text ? 0 : patch_length);
|
||||
if (!changes->diff) {
|
||||
goto oom;
|
||||
}
|
||||
if (binary_text) {
|
||||
changes->issues |= CBM_CHANGES_PATCH_INCOMPLETE;
|
||||
}
|
||||
changes_reconcile(changes, matches);
|
||||
*out = changes;
|
||||
return CBM_CHANGES_OK;
|
||||
|
||||
oom:
|
||||
cbm_changes_free(changes);
|
||||
return CBM_CHANGES_OOM;
|
||||
}
|
||||
|
||||
void cbm_changes_free(cbm_changes_t *changes) {
|
||||
if (!changes) {
|
||||
return;
|
||||
}
|
||||
cbm_diff_free(changes->diff);
|
||||
CBMArena arena = changes->arena;
|
||||
cbm_arena_destroy(&arena);
|
||||
}
|
||||
|
||||
const cbm_change_file_t *cbm_changes_files(const cbm_changes_t *changes, size_t *count) {
|
||||
if (count) {
|
||||
*count = changes ? changes->count : 0;
|
||||
}
|
||||
return changes ? changes->files : NULL;
|
||||
}
|
||||
|
||||
cbm_changes_state_t cbm_changes_state(const cbm_changes_t *changes) {
|
||||
return changes ? changes->state : CBM_CHANGES_UNKNOWN;
|
||||
}
|
||||
|
||||
bool cbm_changes_patch_reconciled(const cbm_changes_t *changes) {
|
||||
return changes && changes->reconciled;
|
||||
}
|
||||
|
||||
bool cbm_changes_can_narrow(const cbm_changes_t *changes) {
|
||||
return changes && changes->can_narrow;
|
||||
}
|
||||
|
||||
unsigned cbm_changes_issues(const cbm_changes_t *changes) {
|
||||
return changes ? changes->issues : 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user