diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc0cd4e4..5c8f732e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,13 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable - uses: actions/setup-node@v6 with: node-version: 22 cache: npm - run: npm ci --ignore-scripts --no-audit --no-fund + - run: cargo build --locked --release --manifest-path rust/Cargo.toml --package oaf - run: npm run bootstrap - run: npm run doctor - run: npm run ci diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index c91a75e4..83813cfa 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -18,14 +18,20 @@ on: options: - trusted-publishing - npm-token + native_run_id: + description: Successful Rust workflow run containing the exact attested native release set + required: true + type: string permissions: + actions: read + artifact-metadata: write + attestations: write contents: read id-token: write jobs: - publish: + validate: runs-on: ubuntu-latest timeout-minutes: 25 - environment: npm-release steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 @@ -33,11 +39,13 @@ jobs: node-version: 22.14.0 registry-url: https://registry.npmjs.org package-manager-cache: false - - run: npm install -g npm@latest + - uses: dtolnay/rust-toolchain@stable + - run: npm install -g npm@11.18.0 - name: Validate manual confirmation env: INPUT_VERSION: ${{ inputs.version }} INPUT_CONFIRM: ${{ inputs.confirm }} + INPUT_NATIVE_RUN_ID: ${{ inputs.native_run_id }} run: | ACTUAL_VERSION="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")" EXPECTED_CONFIRM="publish memory-recall@${ACTUAL_VERSION}" @@ -49,17 +57,244 @@ jobs: echo "Confirmation must exactly match: $EXPECTED_CONFIRM" >&2 exit 1 fi + if ! printf '%s' "$INPUT_NATIVE_RUN_ID" | grep -Eq '^[1-9][0-9]*$'; then + echo "native_run_id must be a positive GitHub Actions run ID" >&2 + exit 1 + fi - run: npm ci --ignore-scripts --no-audit --no-fund + - run: cargo build --locked --release --manifest-path rust/Cargo.toml --package oaf + - run: npm run bootstrap - run: npm run ci - run: npm run native:smoke - run: npm run consumer:smoke - run: npm run release:readiness:check - - run: npm publish --access public --dry-run - - name: Publish with npm Trusted Publishing - if: ${{ inputs.auth_mode == 'trusted-publishing' }} - run: npm publish --access public - - name: Publish with NPM_TOKEN fallback - if: ${{ inputs.auth_mode == 'npm-token' }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish --access public --provenance + - name: Verify source identity before packing + env: + RELEASE_COMMIT: ${{ github.sha }} + shell: bash + run: | + test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" + test -z "$(git status --porcelain --untracked-files=all)" + - name: Pack exact root release artifact once + env: + RELEASE_COMMIT: ${{ github.sha }} + shell: bash + run: | + SOURCE_CREATED="$(git show -s --format=%cI "$RELEASE_COMMIT")" + CREATED="$(node -e "process.stdout.write(new Date(process.argv[1]).toISOString().replace('.000Z', 'Z'))" "$SOURCE_CREATED")" + node scripts/native-release-set.mjs package-root --out output/root-release --commit "$RELEASE_COMMIT" --created "$CREATED" + - name: Validate exact root release artifact + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + run: node scripts/native-release-set.mjs validate-root --artifacts output/root-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" > output/root-release.validation.json + - name: Dry-run exact root tarball + shell: bash + run: | + ROOT_TARBALL="output/root-release/$(node -p "JSON.parse(require('fs').readFileSync('output/root-release/memory-recall-root-release.json', 'utf8')).package.tarball")" + npm publish "$ROOT_TARBALL" --access public --dry-run + - name: Attest root package provenance + uses: actions/attest@v4 + with: + subject-path: output/root-release/*.tgz + - name: Attest root package SBOM + uses: actions/attest@v4 + with: + subject-path: output/root-release/*.tgz + sbom-path: output/root-release/memory-recall-root.spdx.json + - name: Upload exact root release artifact + uses: actions/upload-artifact@v6 + with: + name: memory-recall-root-release-${{ github.sha }} + path: output/root-release/* + if-no-files-found: error + retention-days: 14 + + publish: + needs: validate + runs-on: ubuntu-latest + timeout-minutes: 25 + environment: npm-release + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 22.14.0 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - run: npm install -g npm@11.18.0 + - name: Download exact root release artifact + uses: actions/download-artifact@v8 + with: + name: memory-recall-root-release-${{ github.sha }} + path: output/root-release + - name: Validate exact root release artifact + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + run: node scripts/native-release-set.mjs validate-root --artifacts output/root-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" > output/root-release.validation.json + - name: Verify root provenance signature + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh attestation verify output/root-release/*.tgz \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/npm-publish.yml" \ + --signer-digest "$GITHUB_SHA" \ + --source-digest "$GITHUB_SHA" \ + --deny-self-hosted-runners + - name: Verify root SBOM signature + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh attestation verify output/root-release/*.tgz \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/npm-publish.yml" \ + --signer-digest "$GITHUB_SHA" \ + --source-digest "$GITHUB_SHA" \ + --predicate-type https://spdx.dev/Document/v2.3 \ + --deny-self-hosted-runners + - name: Verify exact successful Rust workflow run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUT_NATIVE_RUN_ID: ${{ inputs.native_run_id }} + shell: bash + run: | + RUN_JSON="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$INPUT_NATIVE_RUN_ID")" + RUN_JSON="$RUN_JSON" node --input-type=module <<'NODE' + const run = JSON.parse(process.env.RUN_JSON); + const expected = { + repository: process.env.GITHUB_REPOSITORY, + commit: process.env.GITHUB_SHA, + workflow: '.github/workflows/rust.yml' + }; + if (run.head_repository?.full_name !== expected.repository) throw new Error('native run repository mismatch'); + if (run.path !== expected.workflow) throw new Error('native run workflow mismatch'); + if (run.status !== 'completed' || run.conclusion !== 'success') throw new Error('native run must be completed successfully'); + if (run.head_sha !== expected.commit) throw new Error('native run commit mismatch'); + if (!['push', 'workflow_dispatch'].includes(run.event)) throw new Error('native run event is not release eligible'); + NODE + - name: Download exact native release artifacts + uses: actions/download-artifact@v8 + with: + run-id: ${{ inputs.native_run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + name: memory-recall-native-release-set-${{ github.sha }} + path: output/native-release + - name: Validate complete signed native release set + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + run: node scripts/native-release-set.mjs validate --artifacts output/native-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" > output/native-release.validation.json + - name: Verify native provenance signatures + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + while IFS= read -r -d '' artifact; do + gh attestation verify "$artifact" \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/rust.yml" \ + --signer-digest "$GITHUB_SHA" \ + --source-digest "$GITHUB_SHA" \ + --deny-self-hosted-runners + done < <(find output/native-release -type f \( -name '*.tgz' -o -name 'native-package-*-receipt.json' \) -print0) + - name: Verify native SBOM signatures + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + while IFS= read -r -d '' artifact; do + gh attestation verify "$artifact" \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/rust.yml" \ + --signer-digest "$GITHUB_SHA" \ + --source-digest "$GITHUB_SHA" \ + --predicate-type https://spdx.dev/Document/v2.3 \ + --deny-self-hosted-runners + done < <(find output/native-release -type f -name '*.tgz' -print0) + - name: Prove exact root install, uninstall, and clean reinstall + shell: bash + run: | + export MEMORY_RECALL_ROOT_PACKAGE_TARBALL="$(node -p "require('path').resolve('output/root-release', JSON.parse(require('fs').readFileSync('output/root-release.validation.json', 'utf8')).artifact.tarball)")" + export MEMORY_RECALL_NATIVE_PACKAGE_TARBALL="$(node -p "require('path').resolve('output/native-release', JSON.parse(require('fs').readFileSync('output/native-release.validation.json', 'utf8')).artifacts.find(({ target }) => target === 'linux-x64-gnu').tarball)")" + test -f "$MEMORY_RECALL_ROOT_PACKAGE_TARBALL" + test -f "$MEMORY_RECALL_NATIVE_PACKAGE_TARBALL" + node scripts/native-code-intelligence-consumer-smoke.mjs + - name: Publish native packages + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + AUTH_MODE: ${{ inputs.auth_mode }} + NODE_AUTH_TOKEN: ${{ inputs.auth_mode == 'npm-token' && secrets.NPM_TOKEN || '' }} + run: node scripts/native-release-set.mjs publish-native --artifacts output/native-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" --auth-mode "$AUTH_MODE" + - name: Verify native packages on npm + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + run: node scripts/native-release-set.mjs verify-registry --artifacts output/native-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" + - name: Dry-run root package after native registry verification + shell: bash + run: | + ROOT_TARBALL="output/root-release/$(node -p "JSON.parse(require('fs').readFileSync('output/root-release/memory-recall-root-release.json', 'utf8')).package.tarball")" + npm publish "$ROOT_TARBALL" --access public --dry-run + - name: Publish exact root package + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + AUTH_MODE: ${{ inputs.auth_mode }} + NODE_AUTH_TOKEN: ${{ inputs.auth_mode == 'npm-token' && secrets.NPM_TOKEN || '' }} + run: node scripts/native-release-set.mjs publish-root --artifacts output/root-release --native-artifacts output/native-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" --auth-mode "$AUTH_MODE" + - name: Verify root package on npm + env: + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_COMMIT: ${{ github.sha }} + run: node scripts/native-release-set.mjs verify-root-registry --artifacts output/root-release --version "$RELEASE_VERSION" --commit "$RELEASE_COMMIT" + + registry-consumer: + name: Published package consumer (${{ matrix.target }}) + needs: publish + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-15 + - target: darwin-x64 + runner: macos-15-intel + - target: linux-arm64-gnu + runner: ubuntu-22.04-arm + - target: linux-x64-gnu + runner: ubuntu-22.04 + - target: win32-x64 + runner: windows-2025 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 22.14.0 + registry-url: https://registry.npmjs.org + package-manager-cache: false + - run: npm install -g npm@11.18.0 + - run: npm ci --ignore-scripts --no-audit --no-fund + - name: Fetch the published root and matching native package + env: + RELEASE_VERSION: ${{ inputs.version }} + NATIVE_TARGET: ${{ matrix.target }} + shell: bash + run: | + mkdir -p output/registry-consumer + npm pack "memory-recall@${RELEASE_VERSION}" --pack-destination output/registry-consumer --json > output/registry-consumer/root.json + npm pack "@memory-recall/native-${NATIVE_TARGET}@${RELEASE_VERSION}" --pack-destination output/registry-consumer --json > output/registry-consumer/native.json + ROOT_TARBALL="$(node -e "const [pack]=JSON.parse(require('fs').readFileSync('output/registry-consumer/root.json', 'utf8')); process.stdout.write(require('path').resolve('output/registry-consumer', pack.filename));")" + NATIVE_TARBALL="$(node -e "const [pack]=JSON.parse(require('fs').readFileSync('output/registry-consumer/native.json', 'utf8')); process.stdout.write(require('path').resolve('output/registry-consumer', pack.filename));")" + test -f "$ROOT_TARBALL" + test -f "$NATIVE_TARBALL" + printf 'MEMORY_RECALL_ROOT_PACKAGE_TARBALL=%s\n' "$ROOT_TARBALL" >> "$GITHUB_ENV" + printf 'MEMORY_RECALL_NATIVE_PACKAGE_TARBALL=%s\n' "$NATIVE_TARBALL" >> "$GITHUB_ENV" + - name: Prove clean global install, MCP, uninstall, and reinstall + run: node scripts/native-code-intelligence-consumer-smoke.mjs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e9dd098d..7dda7482 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,8 +3,13 @@ on: pull_request: push: branches: [main] + workflow_dispatch: permissions: + actions: read + artifact-metadata: write + attestations: write contents: read + id-token: write jobs: rust: runs-on: ubuntu-latest @@ -19,8 +24,310 @@ jobs: - run: cargo test --manifest-path rust/Cargo.toml - run: cargo build --release --manifest-path rust/Cargo.toml - run: node scripts/rust-recall-conformance.mjs + + rust-ingest-graph-quality: + needs: rust + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v6 + with: + node-version: 22 + - run: cargo build --release --manifest-path rust/Cargo.toml - run: node scripts/rust-ingest-quality.mjs - run: node scripts/rust-graph-query-quality.mjs + + rust-intelligence-eval: + needs: rust + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v6 + with: + node-version: 22 + - run: cargo build --release --manifest-path rust/Cargo.toml - run: node scripts/rust-intelligence-quality.mjs - run: node scripts/rust-distribution-quality.mjs - run: node scripts/rust-eval.mjs + + native-artifacts: + name: Native artifact (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-15 + binary: rust/target/release/oaf + platform: darwin + arch: arm64 + libc: '' + - target: darwin-x64 + runner: macos-15-intel + binary: rust/target/release/oaf + platform: darwin + arch: x64 + libc: '' + - target: linux-arm64-gnu + runner: ubuntu-22.04-arm + binary: rust/target/release/oaf + platform: linux + arch: arm64 + libc: glibc + - target: linux-x64-gnu + runner: ubuntu-22.04 + binary: rust/target/release/oaf + platform: linux + arch: x64 + libc: glibc + - target: win32-x64 + runner: windows-2025 + binary: rust/target/release/oaf.exe + platform: win32 + arch: x64 + libc: '' + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Verify native runner identity + env: + EXPECTED_PLATFORM: ${{ matrix.platform }} + EXPECTED_ARCH: ${{ matrix.arch }} + run: node -e "const assert=require('node:assert/strict'); assert.equal(process.platform, process.env.EXPECTED_PLATFORM); assert.equal(process.arch, process.env.EXPECTED_ARCH);" + - name: Build locked release binary + shell: bash + run: cargo build --locked --release --manifest-path rust/Cargo.toml --package oaf + - name: Stage unsigned platform package + shell: bash + run: | + node -e "require('fs').mkdirSync('output', { recursive: true })" + node scripts/package-native-platform.mjs \ + --target '${{ matrix.target }}' \ + --binary '${{ matrix.binary }}' \ + --out output/native-packages \ + > output/native-package-report.json + - name: Verify packed platform package + shell: bash + env: + EXPECTED_TARGET: ${{ matrix.target }} + EXPECTED_PLATFORM: ${{ matrix.platform }} + EXPECTED_ARCH: ${{ matrix.arch }} + EXPECTED_LIBC: ${{ matrix.libc }} + run: | + TARBALL="$(node -p "JSON.parse(require('fs').readFileSync('output/native-package-report.json', 'utf8')).tarball")" + npm install --prefix output/verify "$TARBALL" --ignore-scripts --no-audit --no-fund + node --input-type=module <<'NODE' + import assert from 'node:assert/strict'; + import { createHash } from 'node:crypto'; + import { spawnSync } from 'node:child_process'; + import { readdirSync, readFileSync, statSync } from 'node:fs'; + import path from 'node:path'; + + const report = JSON.parse(readFileSync('output/native-package-report.json', 'utf8')); + assert.equal(report.target, process.env.EXPECTED_TARGET); + assert.equal(report.entryCount, 5); + assert.equal(statSync(report.tarball).isFile(), true); + + const packageRoot = path.join('output', 'verify', 'node_modules', ...report.packageName.split('/')); + const packageJson = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + const manifest = JSON.parse(readFileSync(path.join(packageRoot, 'native-manifest.json'), 'utf8')); + assert.equal(packageJson.name, report.packageName); + assert.equal(packageJson.version, report.version); + assert.deepEqual(packageJson.os, [process.env.EXPECTED_PLATFORM]); + assert.deepEqual(packageJson.cpu, [process.env.EXPECTED_ARCH]); + assert.deepEqual(packageJson.libc ?? [], process.env.EXPECTED_LIBC ? [process.env.EXPECTED_LIBC] : []); + assert.deepEqual(Object.keys(manifest).sort(), [ + 'binary', 'packageName', 'packageVersion', 'schemaVersion', 'sha256', 'target' + ]); + assert.equal(manifest.schemaVersion, '1.0.0'); + assert.equal(manifest.packageName, report.packageName); + assert.equal(manifest.packageVersion, report.version); + assert.equal(manifest.target, process.env.EXPECTED_TARGET); + + const binary = path.join(packageRoot, manifest.binary); + const checksum = `sha256:${createHash('sha256').update(readFileSync(binary)).digest('hex')}`; + assert.equal(manifest.sha256, checksum); + assert.equal(report.binarySha256, checksum); + const version = spawnSync(binary, ['--version'], { encoding: 'utf8', windowsHide: true }); + assert.equal(version.status, 0, version.stderr || version.error?.message); + assert.equal(version.stdout.trim(), `oaf ${report.version}`); + + const files = []; + const walk = (directory, relative = '') => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const next = relative ? `${relative}/${entry.name}` : entry.name; + if (entry.isDirectory()) walk(path.join(directory, entry.name), next); + else files.push(next); + } + }; + walk(packageRoot); + assert.deepEqual(files.sort(), [ + 'LICENSE', 'NOTICE', manifest.binary, 'native-manifest.json', 'package.json' + ].sort()); + const expectedFiles = new Map(report.files.map((file) => [file.path, file])); + assert.equal(expectedFiles.size, files.length); + for (const file of files) { + const bytes = readFileSync(path.join(packageRoot, file)); + const expected = expectedFiles.get(file); + assert(expected, `missing package report checksum for ${file}`); + assert.equal(createHash('sha1').update(bytes).digest('hex'), expected.sha1); + assert.equal(createHash('sha256').update(bytes).digest('hex'), expected.sha256); + } + NODE + - name: Record Linux ABI baseline + if: ${{ matrix.platform == 'linux' }} + shell: bash + run: | + ldd --version + file '${{ matrix.binary }}' + - name: Verify exact artifact in installed consumer + shell: bash + run: | + export MEMORY_RECALL_NATIVE_PACKAGE_TARBALL="$(node -p "JSON.parse(require('fs').readFileSync('output/native-package-report.json', 'utf8')).tarball")" + node scripts/native-code-intelligence-consumer-smoke.mjs + - name: Generate deterministic native SPDX SBOM + shell: bash + env: + GIT_COMMIT: ${{ github.sha }} + run: | + export SOURCE_CREATED="$(git show -s --format=%cI "$GIT_COMMIT")" + node --input-type=module <<'NODE' + import { createHash } from 'node:crypto'; + import { readFileSync, writeFileSync } from 'node:fs'; + import { buildNativeSpdxSbom } from './scripts/package-native-platform.mjs'; + + const packageReport = JSON.parse(readFileSync('output/native-package-report.json', 'utf8')); + const tarballSha256 = `sha256:${createHash('sha256').update(readFileSync(packageReport.tarball)).digest('hex')}`; + const sbom = buildNativeSpdxSbom({ + packageReport, + commit: process.env.GIT_COMMIT, + created: new Date(process.env.SOURCE_CREATED).toISOString().replace('.000Z', 'Z'), + tarballSha256 + }); + writeFileSync(`output/native-package-${packageReport.target}.spdx.json`, `${JSON.stringify(sbom, null, 2)}\n`); + NODE + - name: Attest native package provenance + if: ${{ github.event_name != 'pull_request' }} + id: attest-package + uses: actions/attest@v4 + with: + subject-path: output/native-packages/*.tgz + - name: Attest native package SBOM + if: ${{ github.event_name != 'pull_request' }} + uses: actions/attest@v4 + with: + subject-path: output/native-packages/*.tgz + sbom-path: output/native-package-${{ matrix.target }}.spdx.json + - name: Record sanitized native package receipt + shell: bash + env: + EXPECTED_TARGET: ${{ matrix.target }} + EXPECTED_RUNNER: ${{ matrix.runner }} + GIT_COMMIT: ${{ github.sha }} + PACKAGE_ATTESTATION_ID: ${{ steps.attest-package.outputs.attestation-id }} + PACKAGE_ATTESTATION_URL: ${{ steps.attest-package.outputs.attestation-url }} + run: | + node --input-type=module <<'NODE' + import { createHash } from 'node:crypto'; + import { readFileSync, writeFileSync } from 'node:fs'; + import { buildNativeDistributionReceipt } from './scripts/package-native-platform.mjs'; + + const packageReport = JSON.parse(readFileSync('output/native-package-report.json', 'utf8')); + if (packageReport.target !== process.env.EXPECTED_TARGET) throw new Error('native package report target mismatch'); + const tarballSha256 = `sha256:${createHash('sha256').update(readFileSync(packageReport.tarball)).digest('hex')}`; + const sbom = `output/native-package-${packageReport.target}.spdx.json`; + const receipt = buildNativeDistributionReceipt({ + packageReport, + commit: process.env.GIT_COMMIT, + runner: process.env.EXPECTED_RUNNER, + tarballSha256, + sbomReport: { + name: `native-package-${packageReport.target}.spdx.json`, + sha256: `sha256:${createHash('sha256').update(readFileSync(sbom)).digest('hex')}` + }, + consumerGateResult: 'pass', + packageAttestation: process.env.PACKAGE_ATTESTATION_ID ? { + id: process.env.PACKAGE_ATTESTATION_ID, + url: process.env.PACKAGE_ATTESTATION_URL + } : null + }); + writeFileSync(`output/native-package-${receipt.target}-receipt.json`, `${JSON.stringify(receipt, null, 2)}\n`); + NODE + - name: Attest native receipt provenance + if: ${{ github.event_name != 'pull_request' }} + uses: actions/attest@v4 + with: + subject-path: output/native-package-*-receipt.json + - name: Upload native package and receipt + uses: actions/upload-artifact@v6 + with: + name: memory-recall-native-${{ matrix.target }}-${{ github.event_name == 'pull_request' && 'unsigned' || 'attested' }}-${{ github.sha }} + path: | + output/native-packages/*.tgz + output/native-package-*-receipt.json + output/native-package-*.spdx.json + if-no-files-found: error + retention-days: 14 + + native-release-set: + name: Native release set + if: ${{ github.event_name != 'pull_request' }} + needs: [rust, rust-ingest-graph-quality, rust-intelligence-eval, native-artifacts] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 22.14.0 + - name: Download all attested native packages + uses: actions/download-artifact@v8 + with: + pattern: memory-recall-native-*-attested-${{ github.sha }} + path: output/native-release + merge-multiple: true + - name: Validate exact native release set + run: node scripts/native-release-set.mjs validate --artifacts output/native-release --version "$(node -p "require('./package.json').version")" --commit "$GITHUB_SHA" + - name: Verify native provenance signatures + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + while IFS= read -r -d '' artifact; do + gh attestation verify "$artifact" \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/rust.yml" \ + --signer-digest "$GITHUB_SHA" \ + --source-digest "$GITHUB_SHA" \ + --deny-self-hosted-runners + done < <(find output/native-release -type f \( -name '*.tgz' -o -name 'native-package-*-receipt.json' \) -print0) + - name: Verify native SBOM signatures + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + while IFS= read -r -d '' artifact; do + gh attestation verify "$artifact" \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/rust.yml" \ + --signer-digest "$GITHUB_SHA" \ + --source-digest "$GITHUB_SHA" \ + --predicate-type https://spdx.dev/Document/v2.3 \ + --deny-self-hosted-runners + done < <(find output/native-release -type f -name '*.tgz' -print0) + - name: Upload exact native release set + uses: actions/upload-artifact@v6 + with: + name: memory-recall-native-release-set-${{ github.sha }} + path: output/native-release + if-no-files-found: error + retention-days: 14 diff --git a/.npmignore b/.npmignore index 2de09647..bc46b9fa 100644 --- a/.npmignore +++ b/.npmignore @@ -19,3 +19,4 @@ adapters/**/UPSTREAM.lock *.zip .DS_Store REPOSITORY_MANIFEST.json.tmp +REPOSITORY_MANIFEST.json diff --git a/ASSIGN_TO_AGENT.md b/ASSIGN_TO_AGENT.md index 7bca3463..c22cf30f 100644 --- a/ASSIGN_TO_AGENT.md +++ b/ASSIGN_TO_AGENT.md @@ -1,6 +1,6 @@ # Assignment Brief for the Development Agent -You are working on **Memory Recall 1.1.0**, currently an unreleased release candidate. Evolve it through small verified changes. Do not rebuild the architecture from scratch and never claim that specified capabilities already exist. +You are working on **Memory Recall 2.0.0**, currently a major-release candidate. Evolve it through small verified changes. Do not rebuild the architecture from scratch and never claim that specified capabilities already exist. ## Mission diff --git a/CHANGELOG.md b/CHANGELOG.md index a01a0c83..2e3ec7f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,48 @@ # Changelog -## [Unreleased] — 1.1.0 release candidate +## [Unreleased] — 2.0.0 major candidate + +### Added + +- Connected the loopback Control API and web Recall Map to healthy current Rust indexes. Browser reads stay bounded and read-only, preserve native coverage diagnostics, and return an exact build, refresh, repair, or package action instead of silently scanning with JS/TS. +- Added source-backed community and entry-to-sink process evidence to the native Map, with bounded paths, confidence, and explicit truncation state. +- Approved the Rust/Node production-engine boundary, added provider-neutral graph and language-evidence contracts, pinned a 43-repository Tier 1 benchmark corpus, and recorded a clean Phase 0 baseline. The baseline makes no competitor-parity claim. +- Added a versioned, bounded Rust provider and made its packaged SQLite index the production default for graph, MCP, Control API, and web reads. `native-preview` and `auto` remain strict native aliases; the temporary JS/TS path requires explicit `compatibility` selection. +- Added isolated packed-package proof and a reproducible Phase 1 compatibility receipt across JS/TS fixtures and two exact-commit repositories. The receipt records current import and call gaps and makes no accuracy, parity, or leadership claim. +- Added PHP and Ruby native-preview structure, namespaces, traits and mixins, typed calls, imports, and framework-route evidence, completing the five planned Tier 1 language batches. +- Added a deterministic Phase 2 aggregate over 14 fixtures and all 43 pinned Tier 1 repositories, with per-capability worst-case status, response bytes, resource measurements, and explicit node and edge budget diagnostics. +- Added an isolated SQLite native source index with generation commits, incremental refresh, bounded queries, doctor/confirm-gated repair, and a no-write MCP preview. A reviewed integration fixture proves build, query, and exact no-op refresh behavior for all 14 Tier 1 languages. +- Added deterministic bounded communities and evidence-backed entry-to-sink processes to the existing native `repo.architecture` MCP result, plus a reproducible Phase 4 correctness and query-deadline receipt. The MCP surface remains twelve read-only tools and no parity claim is made. +- Added deterministic native hybrid search that ranks one exact hit, lexical matches, and bounded one-hop structural neighbors while returning source-backed relationship evidence through the existing `code.search` MCP tool. +- Added safe constrained traversal to the existing `code.context` MCP tool. A current native index can filter direction, depth, and canonical edge kinds inside each SQLite expansion while keeping the public surface at twelve read-only tools. +- Added five optional native-platform package templates, exact npm/Cargo/binary version alignment, a verified platform-binary resolver, and an offline macOS arm64 installed-package gate covering all fourteen Tier 1 parsers and the SQLite lifecycle without a compiler. A five-runner CI matrix now defines the same exact-tarball gate for the remaining targets, but those hosted runs, signing, and publication remain unproven. +- Made MCP native by default. Each structural read requires a healthy current committed SQLite generation and otherwise returns an actionable build, refresh, repair, schema, target, or package error. It never silently falls back; explicit `compatibility` is the temporary JS/TS path. +- Extended the isolated native-package consumer gate through package removal and same-version reinstall. It proves the CLI and both npm packages are removed while workspace source, governed memory, home configuration, and the SQLite index bundle remain unchanged, then reopens the same generation from freshly installed exact tarballs without Cargo, rustc, build, or refresh. +- Added confirmed `mcp uninstall` for Codex, Claude Code, and Cursor. Install and removal now reject drifted entries, bind confirmation to exact config bytes, create private backups, write atomically, and preserve neighboring configuration and workspace data. +- Added a Rust-index cross-service fixture for bounded import, call, trace, and process evidence. Same-name and unresolved negatives pass; multi-repository and parity claims remain false. +- Added a Rust-owned SQLite repository registry and explicit repository-scoped search for up to eight registered indexes. Results carry deterministic repository-qualified identities; list and search remain bounded, sequential, read-only, and local. Cross-repository relationships, trace, impact, and parity remain unproven. + +### Fixed + +- Hardened the existing `connect` and `disconnect` config writer with exact preflight rechecks, private atomic replacement, and `0600` config and backup files. +- Default packaged benchmarks now use bundled fixtures unless the caller explicitly supplies `--dataset`, preventing same-named repository files from changing the package benchmark. +- `recall serve` now forwards interrupt and termination signals to the Control API child process. +- Raised the native provider output ceiling within its existing 10 MB hard maximum so valid 5,000-node engine responses do not fail on medium repositories. +- Native `index.status` now verifies the active SQLite generation against a bounded source snapshot and reports changed, added, deleted, partial, or unverified state without writing the database. Normal queries remain SQLite-only, and stale source state requires refresh rather than repair. +- Native `index.refresh` now refuses to plan or write from incomplete discovery. If `maxFiles`, the byte budget, or the deadline prevents a complete source snapshot, it returns partial with zero writes and keeps the active generation unchanged; raising `maxFiles` permits a complete recovery refresh. +- Python route extraction now inspects only executable decorator syntax, so `@app.route(...)` examples inside docstrings no longer become route nodes or handler edges. +- Python framework routes now require imported and constructed FastAPI, APIRouter, Flask, or Blueprint receivers. Conventional Django URL patterns use structural positional arguments, ignore naming metadata, and leave unresolved `include(...)` composition explicit. +- Python project metadata and root package markers now produce package-keyed configuration resources, exact configuration-to-package evidence, and freshness-sensitive absolute package resolution. + +### Changed + +- Stable npm publication is now native-first and fail-closed: one exact successful Rust run must supply all five checksum-verified packages, file-complete SPDX 2.3 SBOMs, and signed GitHub provenance. The root package is packed once, checksum-bound, given a complete SPDX document, attested, installed through the exact-artifact lifecycle, and published as those same bytes only after every native registry integrity and npm publish/SLSA attestation verifies. The workflow, command-line publisher, and exported root publisher all require the exact native artifact directory and repeat the five-package registry integrity, provenance, and npm signature audit immediately before any root lookup or publication. Root integrity and both npm predicates must also verify. The lane remains local and unproven until the remote five-platform and final release gates pass. +- Token Saver and handoff measurement reuse the already-built context pack for their real MCP stdio readback, avoiding a second repository scan while preserving fingerprint verification. +- Native changed-file refresh now parses only the bounded invalidation closure plus dependency context. The clean Phase 3 receipt covers 781 files across one dependency fixture and three pinned repository scopes; it keeps scale, competitor, parity, and leadership claims false. +- Tier 1 documentation now names the 75 capability rows that meet the sampled Phase 2 floor, the 78 applicable rows that remain unmeasured, the zero recorded floor failures, and the single not-applicable row. Go, Rust, and Dart exports retain their sampled fixture-plus-three-repository floor. Dart calls and heritage, Kotlin calls and types, and C++ types also retain their reviewed sampled floors. C++ heritage now meets the sampled floor for direct base specifiers: template arguments and body references are explicit negative decoys, while template substitution, alias expansion, dependent names, and compiler-equivalent inheritance analysis remain unmeasured. Every Tier 1 language remains unmeasured overall; no competitor, parity, leadership, general multi-repository, or scale claim is made. +- The npm package excludes checkout-only Tier 1 audit and real-world benchmark scripts, the nested Rust build guide, the architecture bitmap, and the 114 KiB capability matrix, keeping the verified unpacked package below its fixed size ceiling. + +## [1.1.0] — 2026-07-15 No breaking CLI migration is required. Local-only defaults, proposal-gated memory, and read-only MCP remain unchanged. @@ -30,7 +72,7 @@ No breaking CLI migration is required. Local-only defaults, proposal-gated memor ### Internal -- Release gates cover 659 Node tests, 182 protocol fixtures, 144 evaluation assertions, installed-package smoke, browser smoke, package-content verification, and release-evidence drift checks. +- Release gates cover the full Node suite, 182 protocol fixtures, 144 evaluation assertions, installed-package smoke, browser smoke, package-content verification, and release-evidence drift checks. ## [1.0.5] — 2026-07-09 diff --git a/DESIGN.md b/DESIGN.md index 5ef06d9d..33f89299 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -11,7 +11,10 @@ This is the canonical UI contract. Code, prototypes, screenshots, and design-age - Typography: native system sans for interface text; native monospace only for code, paths, hashes, commands, and identifiers. - Shape: 6 px controls and 8 px bounded panels. Lists and dividers take priority over nested cards. - Motion: state transitions only. No ambient or decorative animation. -- Copy: object, state, and action labels only. No product slogan inside the application shell. +- Density: minimal means fewer elements, not oversized gaps. Use the spacing scale to group related work and do not add empty height for visual drama. +- Decoration: no gradients, glass effects, glowing accents, decorative illustrations, heavy shadows, or walls of equal cards. Color and containers must communicate structure or state. +- Copy: object, state, and action labels only. No product slogan, manifesto, inspirational subtitle, or capability hype inside the application shell. +- AI language: use `AI`, `intelligent`, or similar terms only when naming a real model, provider, setting, or technical boundary. Never use them as decoration or a product claim. ## Principles @@ -49,6 +52,7 @@ Executable tokens live in `packages/ui/tokens.json` and `apps/web/tokens.css`. - Spacing: `4, 8, 12, 16, 24, 32, 48, 64`. - Radius: controls 6 px and bounded panels 8 px. - Prefer borders and surface contrast over shadows. +- Avoid nested panels when a divider, row, or disclosure communicates the same hierarchy. ## AI-native components diff --git a/HANDOFF_VERIFICATION.json b/HANDOFF_VERIFICATION.json index 48840450..7fd17837 100644 --- a/HANDOFF_VERIFICATION.json +++ b/HANDOFF_VERIFICATION.json @@ -1,8 +1,8 @@ { "schemaVersion": "1.0.0", - "project": "open-agent-fabric", - "version": "0.2.0-dev", - "verifiedAt": "2026-07-08T15:33:32.001Z", + "project": "memory-recall", + "version": "2.0.0", + "verifiedAt": "2026-07-19T13:48:38.922Z", "environment": { "platform": "darwin", "release": "25.5.0", @@ -18,32 +18,32 @@ { "command": "npm run check", "exitCode": 0, - "durationMs": 284 + "durationMs": 340 }, { "command": "npm run protocol:validate", "exitCode": 0, - "durationMs": 176 + "durationMs": 233 }, { "command": "npm test", "exitCode": 0, - "durationMs": 47096 + "durationMs": 91889 }, { "command": "npm run eval", "exitCode": 0, - "durationMs": 282 + "durationMs": 294 }, { "command": "npm run demo", "exitCode": 0, - "durationMs": 163 + "durationMs": 183 }, { "command": "npm run native:smoke", "exitCode": 0, - "durationMs": 223 + "durationMs": 236 } ], "result": "passed" diff --git a/PROJECT_STATUS.json b/PROJECT_STATUS.json index 3bb09671..4ac23509 100644 --- a/PROJECT_STATUS.json +++ b/PROJECT_STATUS.json @@ -1,8 +1,8 @@ { "schemaVersion": "1.0.0", "project": "memory-recall", - "release": "1.1.0", - "phase": "1.1-release-candidate-readiness", + "release": "2.0.0", + "phase": "2.0.0-major-readiness", "nextTask": null, "defaults": { "network": "deny", @@ -69,9 +69,19 @@ "evidence": [ "apps/web/index.html", "apps/web/app.js", + "apps/web/orientation-model.js", + "apps/web/orientation-view.js", + "apps/web/source-map-view.js", + "apps/web/graph-layout-worker.js", + "apps/web/graph-viewport.js", + "apps/web/memory-graph-view.js", "apps/web/styles.css", "apps/web/tokens.css", + "tests/web-orientation.test.mjs", + "tests/web-source-map.test.mjs", + "tests/web-memory-graph.test.mjs", "tests/web-shell.test.mjs", + "scripts/consumer-browser-smoke.mjs", "docs/implementation/OAF-022-production-web-shell-note.md", "docs/implementation/OAF-023-run-context-inspector-note.md", "docs/implementation/OAF-024-memory-evidence-approval-views-note.md", @@ -114,10 +124,8 @@ "tests/context-compiler.test.mjs", "tests/context-candidate-sources.test.mjs", "tests/native-memory-profile-context.test.mjs", - "tests/ast-code-candidate-source.test.mjs", - "tests/source-graph-preview.test.mjs", - "tests/recall-map-ranking.test.mjs", - "evals/recall-map/architecture-ranking.v1.json", + "tests/cli-graph-index.test.mjs", + "tests/mcp-code-intelligence.test.mjs", "tests/cli.test.mjs", "tests/control-api-boundary.test.mjs", "packages/protocol/schemas/candidate-source-request.schema.json", @@ -127,6 +135,7 @@ "packages/protocol/schemas/source-graph-preview.schema.json", "packages/protocol/src/source-graph-locator.mjs", "packages/source-graph/src/index.mjs", + "packages/source-graph/src/native-index-projection.mjs", "packages/protocol/schemas/context-candidate.schema.json", "packages/protocol/schemas/context-selection-policy.schema.json", "packages/protocol/schemas/context-selection-result.schema.json", @@ -137,15 +146,38 @@ "docs/implementation/OAF-012-context-manifests-note.md" ], "limitations": [ - "Native exact, lexical, AST-code, and graph locator candidate sources only; AST-code also exposes dependency-free JS/TS symbol-index and source-graph queries plus a bounded read-only CLI/API source-graph preview with 512 KiB default static JS/TS file coverage and a 1 MiB hard ceiling", + "Native exact and lexical context candidate sources are retained; repository structure and context-pack graph hints come only from the packaged Rust SQLite index", "Context views are opt-in deterministic local representations for noisy selected records and tool output; they preserve original/view hashes and token accounting but do not replace canonical source or memory state", "No model reranking, embeddings, vector database, graph database, external search, browser automation, outbound network, or adapter activation", "Compressed memory profile context and oaf measure savings use bounded local before/after delivery-token estimates over a realistic workspace candidate-file and git-history baseline, surfaced in the memory cockpit and MCP stats; MCP stats separates context.profile compression-path savings from memory.recall compaction savings and does not claim provider billing-token savings", - "Native source graph is read-only derived metadata; graph candidate records are locator-only and it is not canonical source state, not a graph database, and not persisted as memory", + "Native source-graph projections are read-only, locator-only derived metadata from the explicit local SQLite index; they are not canonical source or memory state", "Token estimate is approximate", "Manifest persistence is local native or repository-layer only; no production orchestration is implied" ] }, + { + "id": "source.graph-index", + "name": "Persistent incremental polyglot Rust source index", + "status": "implemented", + "evidence": [ + "apps/cli/oaf.mjs", + "rust/oaf-index/src/lib.rs", + "rust/oaf-index/src/store.rs", + "providers/native/code-intelligence-rust/src/index.mjs", + "packages/source-graph/src/native-index-projection.mjs", + "tests/native-code-intelligence-index.test.mjs", + "tests/cli-graph-index.test.mjs", + "tests/mcp-code-intelligence.test.mjs", + "docs/usage/mcp-server-reference.md", + "docs/usage/support-matrix.md" + ], + "limitations": [ + "Index writes require an explicit graph index writer command; all CLI, MCP, Control API, and web reads require a healthy current committed generation and fail closed otherwise", + "Incremental refresh reuses unchanged file records and reparses changed, added, renamed, or deleted files with bounded dependency invalidation", + "The index contains hashes, locators, structural parse metadata, and evidence; it contains no raw source bodies or absolute workspace paths", + "The fourteen shipped parsers remain honestly capability-bounded; semantic embeddings, remote indexes, and a graph database are not claimed" + ] + }, { "id": "context.candidate-sources", "name": "Provider-neutral context candidate-source generation", @@ -154,13 +186,8 @@ "packages/context-compiler/src/index.mjs", "providers/native/context-candidate-exact/provider.json", "providers/native/context-candidate-lexical/provider.json", - "providers/native/context-candidate-ast-code/provider.json", - "providers/native/context-candidate-graph/provider.json", "providers/native/catalog.json", "tests/context-candidate-sources.test.mjs", - "tests/ast-code-candidate-source.test.mjs", - "tests/source-graph-preview.test.mjs", - "tests/recall-map-ranking.test.mjs", "packages/adapter-contracts/src/index.mjs", "packages/protocol/schemas/candidate-source-descriptor.schema.json", "packages/protocol/schemas/candidate-source-hit.schema.json", @@ -173,8 +200,8 @@ "docs/architecture/context-compiler.md" ], "limitations": [ - "Only native exact, lexical, AST-code, and graph locator sources are implemented and enabled", - "AST-code source is a dependency-free static JS/TS chunker with symbol index and read-only source graph preview helpers; graph source emits locator-only derived JS/TS graph hits; preview facade input and ranking output share the strict source-graph locator and label grammars; over-limit files remain hash/read-plan-only and are not treated as symbol-impact coverage; coverage distinguishes declared out-of-scope directories from source-relevant excluded trees and records unreadable source directories as partial static scope without hiding readable siblings; it is not a full semantic parser, language server, graph database, or executor", + "Only native exact and lexical candidate sources are enabled; code-intelligence candidates and source-graph previews are projections of the packaged Rust SQLite index", + "Context Compiler consumes locator-only native index evidence and never receives raw source bodies; missing, stale, corrupt, or unavailable indexes remain explicit rather than selecting another intelligence engine", "Vector, temporal, preference, and episode sources are declared but unavailable", "Sources generate candidates only; final selection remains in the Context Compiler selector", "No external adapters, outbound network, embeddings, vector database, graph database, browser automation, or public search endpoint" @@ -196,7 +223,7 @@ "docs/usage/recall-map.md" ], "limitations": [ - "CLI only: Recall Map composes the existing bounded JS/TS static source graph and governed local SQLite memory status; it does not create a missing SQLite store or persist a graph.", + "Recall Map composes the healthy current native SQLite index and governed local SQLite memory status; it does not create a missing SQLite store or persist a graph.", "The command is read-only and local-only: no workspace writes, canonical-memory mutation, model or network calls, external adapters, raw source bodies, or absolute workspace paths are emitted.", "Recall Map API, browser, and MCP map surfaces are not implied by this CLI capability." ] @@ -320,7 +347,7 @@ "docs/implementation/OAF-031-context-intake-preview-note.md" ], "limitations": [ - "Generates schema-validated local handoff reports from explicit source-family selections over documented harness project files, explicit user-selected relative files, compact native JS/TS source-graph hints, a utility read plan, and a copyable launch prompt", + "Generates schema-validated local handoff reports from explicit source-family selections over documented harness project files, explicit user-selected relative files, compact native source-graph hints, a utility read plan, and a copyable launch prompt", "Omission refs expose skipped context through safe locators, hashes, token costs, reason codes, and recovery hints without embedding raw bodies", "Utility read plans record required local reads, changed-locator coverage, graph-hint coverage, redacted content hashes for safely readable changed files, aggregate changed-source byte/token counts, unavailable-hash reasons for missing or unsafe changed files, explicit user-selected files omitted by budget, and source-selection reduction without embedding raw source bodies", "Context packs preserve requested source families, user-selected locators, and changed locators in schema-backed requestedInputs, and reject secret-like or absolute-path objective and step text before rendering handoff markdown", @@ -785,8 +812,8 @@ "docs/release/1.0-RELEASE-CHECKLIST.md" ], "limitations": [ - "memory-recall 1.1.0 is the package release candidate; verify `npm view memory-recall version` returns 1.1.0 before using the global Recall Map quickstart", - "Publishing, tagging, or signing the 1.1.0 release candidate remains a maintainer action", + "memory-recall 2.0.0 is the source major-release candidate; use `npm view memory-recall version` to verify the current registry release before global installation", + "Publishing, tagging, or signing the 2.0.0 major release remains a maintainer action", "No signing keys, release credentials, hosted publishing automation, external writes, or external adapter activation are added" ] }, @@ -874,6 +901,7 @@ "packages/harness-context/src/index.mjs", "tests/protocol-bridges.test.mjs", "tests/cli.test.mjs", + "tests/mcp-code-intelligence.test.mjs", "docs/architecture/protocol-bridges.md", "docs/implementation/OAF-028-protocol-bridges-note.md" ], @@ -882,6 +910,7 @@ "In-process dependency-free MCP-shaped bridge plus local read-only CLI stdio compositions and the preview-confirm mcp install planner using npm --silent; no HTTP, SSE, hosted bridge, or network listener is started", "Read-only resources expose sanitized summaries for status, latest context manifest, latest run, memory proposals, handoff/artifact review, opt-in current context packs, auto-discovered pinned context-pack use plans, pinned context-pack registry verification, skill catalog, per-skill load plans, and the reviewed tool catalog; they do not expose write tools, source bodies, skill bodies, tool manifest bodies, prompts, model results, credentials, provider URLs, absolute local paths, external egress, or canonical state mutation", "MCP memory.recall and context.profile serve active temporal facts first and pending proposal facts separately, include active/proposal count summaries, support since cursors for deltas, and persist last-delivered cursors locally per client/scope", + "The read-only MCP server exposes twelve tools: governed memory and compact context plus repository map, architecture, index status, code search, symbol context, call trace, dependencies, routes, and changed-file impact; symbol context supports optional native direction, depth, and canonical edge-kind constraints, and all structural results are bounded and locator-only", "The read-only bridge never approves proposals or activates memory", "The MCP token-saver install command writes only the selected local client MCP config entry after --apply plus a matching dry-run fingerprint; default dry-run prints exact config only, rejects --write, and does not print raw home config bodies or absolute paths", "The stdio bridge enforces bounded stdin, JSON-RPC line size, message count, child runtime, child stdout/stderr, JSON-RPC id, method, resource URI, and tool name limits; JSON-RPC batch arrays are rejected and request-metadata failures are redacted", @@ -1023,10 +1052,74 @@ "Most fixtures are expectations, not evidence that adapters are installed or passing", "adapter:tool:ecc is the single experimental exception with executable conformance evidence" ] + }, + { + "id": "code-intelligence.polyglot-production-engine", + "name": "Polyglot production code-intelligence engine contract", + "status": "experimental", + "evidence": [ + "docs/superpowers/specs/2026-07-16-memory-recall-polyglot-leadership-design.md", + "docs/adr/0023-production-rust-code-intelligence-engine.md", + "packages/protocol/schemas/code-intelligence-graph.schema.json", + "packages/protocol/schemas/code-intelligence-capability-matrix.schema.json", + "packages/protocol/schemas/code-intelligence-benchmark-manifest.schema.json", + "packages/protocol/schemas/code-intelligence-engine-request.schema.json", + "packages/protocol/schemas/code-intelligence-engine-response.schema.json", + "packages/protocol/src/code-intelligence-contract.mjs", + "evals/code-intelligence/capability-matrix.v1.json", + "evals/code-intelligence/corpus.v1.json", + "evals/code-intelligence/benchmark-gates.v1.json", + "evals/code-intelligence/results/phase2-batch-a.json", + "evals/code-intelligence/results/phase2-batch-b.json", + "evals/code-intelligence/results/phase2-batch-c.json", + "evals/code-intelligence/results/phase2-batch-d.json", + "evals/code-intelligence/results/phase2-batch-e.json", + "evals/code-intelligence/results/phase2-tier1-summary.json", + "evals/code-intelligence/results/phase3-source-index.json", + "evals/code-intelligence/results/phase4-intelligence.json", + "evals/code-intelligence/results/phase5-cross-service.json", + ".github/workflows/rust.yml", + "providers/native/code-intelligence-rust/src/index.mjs", + "providers/native/code-intelligence-rust/src/binary-resolver.mjs", + "scripts/package-native-platform.mjs", + "scripts/native-code-intelligence-consumer-smoke.mjs", + "scripts/code-intelligence-phase2-tier1.mjs", + "scripts/code-intelligence-phase3-index.mjs", + "scripts/code-intelligence-phase4-intelligence.mjs", + "scripts/code-intelligence-phase5-cross-service.mjs", + "tests/code-intelligence-contract.test.mjs", + "tests/code-intelligence-index-contract.test.mjs", + "tests/code-intelligence-tier1.test.mjs", + "tests/mcp-code-intelligence.test.mjs", + "tests/native-code-intelligence-14-languages.test.mjs", + "tests/native-code-intelligence-provider.test.mjs", + "tests/code-intelligence-phase5-cross-service.test.mjs", + "tests/native-binary-resolver.test.mjs", + "tests/native-platform-packaging.test.mjs", + "tests/control-api-boundary.test.mjs", + "tests/web-orientation.test.mjs", + "rust/oaf-ingest/src/lib.rs", + "rust/oaf/src/index_protocol.rs", + "rust/oaf-index/tests/community_process_projection.rs", + "tests/native-code-intelligence-consumer-smoke.test.mjs" + ], + "limitations": [ + "Graph, MCP, Control API, and web reads use verified packaged Rust and a healthy current committed SQLite index. Missing, stale, corrupt, mismatched, or unavailable native state fails closed with an exact build, refresh, repair, schema, target, or package action; no JavaScript intelligence fallback remains.", + "The current registry release does not ship the new Rust binary. This source checkout has exact-version optional package templates, a verified resolver, and a five-native-runner packaging gate; the workflow, command-line publisher, and exported root publisher fail closed unless an exact signed five-package artifact directory passes version, commit, registry integrity, provenance, and npm signature verification immediately before the root lookup or publication. The macOS arm64 root-plus-platform packed install passes without Cargo or rustc, including explicit first-run indexing, all twelve MCP tools, and installed-package absence checks for the retired JavaScript source-graph paths; the four hosted target runs, real signed attestations, publication, and published-package promotion remain unproven.", + "Phase 2 covers 14 fixtures and all 43 pinned Tier 1 repositories; 75 capability rows meet the sampled floor, 78 applicable rows remain unmeasured, no row has a recorded floor failure, and C heritage is the single not-applicable row. Reviewed imports preserve full coordinates across all fourteen Tier 1 language fixtures plus three pinned repositories per language. Go, Rust, and Dart exports meet the sampled floor across one fixture and three pinned repositories per language. Dart calls and heritage, Kotlin calls and types, and C++ types retain their reviewed sampled floors. C++ heritage now meets the sampled floor for direct base specifiers: template arguments and body references are explicit negative decoys, while template substitution, alias expansion, dependent names, and compiler-equivalent inheritance analysis remain unmeasured. Generic substitution, compiler-equivalent analysis, grouped PHP imports, broader framework or monorepo resolution, and the remaining applicable capability rows remain unmeasured. Every Tier 1 language remains overall unmeasured; missing rows remain explicit, and no competitor, parity, or leadership claim is made.", + "Five bounded repository scopes report explicit node or edge omissions; their available evidence is partial and no scale claim is made.", + "GitNexus and Codebase Memory MCP remain unmeasured on the pinned corpus.", + "Phase 3 adds the isolated SQLite lifecycle, bounded invalidation refresh, watcher coordination, doctor/repair, CLI, provider, explicit read-only MCP preview, and a clean four-case performance receipt over 781 files; it does not promote the runtime default, bundle a native binary, move web, prove multi-repository or million-node scale, or make a parity or leadership claim.", + "Phase 4 adds deterministic bounded exact, lexical, and one-hop structural search plus communities, evidence-backed entry-to-sink processes, and constrained direction/depth/edge-kind traversal through the existing native MCP tools. Its local fixture proves deterministic output, relationship evidence, read-only behavior, and the two-second query deadline; it does not prove per-language process support, competitor parity, leadership, multi-repository behavior, or million-node scale.", + "The Phase 5 fixtures prove bounded cross-service evidence inside one repository and an experimental exact Go path across two explicitly registered repositories. The Rust-owned SQLite registry supports bounded search across up to eight indexes; exact Go module evidence drives dependencies, one entry-to-service trace, and reverse impact through the existing twelve MCP tools, with same-name wrong-module decoy rejection. General cross-repository and cross-language resolution, competitor parity, and scale remain unproven.", + "Native index.status now performs a bounded, read-only source-hash comparison against the active SQLite generation and reports source changes or unverified bounds as stale. Persisted node, edge, or file omissions remain partial. Normal queries remain SQLite-only, indexes created before scan-scope persistence require a rebuild for verified freshness, and historical custom max-file-byte settings can conservatively produce false-stale results because that setting is not yet persisted.", + "Default MCP and the strict auto/native-preview aliases recheck native index freshness before each structural tool call. They use only a healthy current committed generation; every other native state fails with an actionable bounded error. They never build, refresh, repair, or select a JavaScript intelligence path.", + "The local Phase 6 distribution gate proves current-platform package discovery, compiler-free 14-language parsing without the legacy JS intelligence module, installed SQLite lifecycle, confirmed MCP config install/removal with private backups and neighbor preservation, package removal with workspace-state preservation, and same-version exact-tarball reinstall on macOS arm64. CI defines the same package gate for all five initial targets, but the four non-local lanes have not run here; signed or published packages, cross-version downgrade, and cross-platform release readiness remain unproven." + ] } ], "qualitySnapshot": { - "asOf": "2026-07-15", + "asOf": "2026-07-18", "commands": [ "npm ci --ignore-scripts --no-audit --no-fund", "npm run local:run", @@ -1046,10 +1139,25 @@ "npm run native:smoke", "npm run verify:handoff", "npm run consumer:smoke", - "npm run consumer:browser-smoke" + "npm run consumer:browser-smoke", + "node --test tests/code-intelligence-contract.test.mjs", + "node scripts/pin-code-intelligence-corpus.mjs --check", + "node scripts/rust-code-intelligence-protocol-quality.mjs", + "node scripts/native-code-intelligence-consumer-smoke.mjs", + "node scripts/code-intelligence-batch-a.mjs --check", + "node scripts/code-intelligence-batch-b.mjs --check", + "node scripts/code-intelligence-batch-c.mjs --check", + "node scripts/code-intelligence-batch-d.mjs --check", + "node scripts/code-intelligence-batch-e.mjs --check", + "node scripts/code-intelligence-phase2-tier1.mjs --check", + "node scripts/code-intelligence-phase3-index.mjs --check", + "node scripts/code-intelligence-phase4-intelligence.mjs --check", + "node scripts/code-intelligence-phase5-cross-service.mjs --check", + "cargo test --locked --manifest-path rust/Cargo.toml -p oaf-index --test repository_registry", + "node --test tests/native-code-intelligence-provider.test.mjs tests/native-code-intelligence-repository-provider.test.mjs" ], - "testsExpected": 660, - "protocolFixturesExpected": 182, + "testsExpected": 715, + "protocolFixturesExpected": 208, "evaluationsExpected": 144, "note": "Counts are a dated snapshot; command results and HANDOFF_VERIFICATION.json are authoritative." } diff --git a/README.md b/README.md index 77d9a1ee..1b075be5 100644 --- a/README.md +++ b/README.md @@ -15,25 +15,25 @@

Local repo memory and context for Codex, Claude Code, Cursor, and other coding agents.
- Governed SQLite memory. Read-only MCP. Experimental Rust acceleration requires a local build. The default local path needs no hosted account or model API key. + Governed SQLite memory. Read-only MCP. Verified packaged Rust reads when a current local index exists. No hosted account or model API key required.

Memory Recall turns a repository into a governed context source. New agent sessions get reviewed repo facts, changed-file impact, required local reads, and proof of what was sent instead of a giant pasted transcript. -The source and package release candidate are 1.1.0. Check the registry before -using the global install path: +The source checkout is the 2.0.0 major-release candidate. The registry remains the +installation authority: ```bash npm view memory-recall version -npm install -g memory-recall@1.1.0 +npm install -g memory-recall@latest recall setup recall map --root . --sqlite .local/memory.sqlite --format summary recall handoff ``` -If the registry still reports an older version, use the source checkout: +Use the source checkout when testing changes that are not yet on the registry: ```bash git clone https://github.com/rebel0789/Memory-Recall.git @@ -59,8 +59,10 @@ for preserved legacy names and URIs. | --- | --- | | New agent session | A compact handoff with required local reads, changed-file coverage, hashes, and MCP proof. | | Repo memory | SQLite/FTS5 facts that start as proposals and become ACTIVE only after review. | -| Fast local code intelligence | Implemented JS/TS static graph; experimental Rust ingest and graph/search require a local build. | -| First look at a repository | Recall Map shows bounded source coverage, entry points, changed impact, and separate memory status without writing. | +| Fast local code intelligence | Packaged Rust is the only engine. Reads use the local SQLite index and return an explicit build, refresh, repair, or package action when native state is not ready. | +| Larger repositories | Explicit local index with incremental refresh and watch mode; MCP and the web workbench read it without writing. | +| Two-repository Go calls | Experimental Rust path resolves an exact Go module import, traces one entry-to-service call, and reports reverse impact with source evidence. | +| First look at a repository | The web Recall Map reads a healthy current Rust index, including source-backed communities and bounded entry-to-sink processes. Until first-run indexing is complete, it returns a bounded recovery state instead of silently switching engines. | | Long context pressure | Repeat MCP pulls use cursors and deltas instead of resending the same profile. | | Trust | Dry-run first, confirm-gated writes, local-only storage, and no automatic transcript import. | | Codebase context | Source graph hints, locator-only context packs, and manifest-backed selection. | @@ -68,9 +70,9 @@ for preserved legacy names and URIs. ## Five-Minute Path ```bash -# Confirm and install the 1.1.0 package. +# Confirm and install the current registry release. npm view memory-recall version -npm install -g memory-recall@1.1.0 +npm install -g memory-recall@latest # Create local state only. This does not scan the repository. recall setup @@ -113,8 +115,14 @@ approval. Manual MCP install flow: run the `mcp install --client claude-code --dry-run --format json` preview, review it, then run the printed `--apply --confirm ` -command only when the fingerprint matches. That path installs the five-tool -read-only MCP server. `recall connect` is separate: it installs a resource +command only when the fingerprint matches. That path installs the twelve-tool +read-only MCP server with `--engine native`; install never builds or refreshes an +index, and its `indexBuildCommand` is the separate explicit native-index write. +Until a healthy, current native index exists, structural tools return the exact +native recovery command. Reverse the install with `recall mcp uninstall --client +claude-code --dry-run --format json`, then the printed confirmed command. It +removes only the exact package-owned entry and preserves neighboring servers. +`recall connect` is separate: it installs a resource bridge plus hooks for Codex or Claude Code, and its MCP `tools/list` is empty. The support matrix names the difference and reversal path. @@ -147,6 +155,7 @@ billing claims. | Temporal current-truth fixture | 10/10 correct and clean; not a token-saving claim | `recall bench temporal --read-only --root . --format json` | | In-repo structured-ingest sufficiency | 12/12 checkout-derived answers present after structured ingest | Source checkout: `npm run recall -- bench realqa --read-only --root . --format json` | | Truth-floor regression gate | Fixture-backed merge gate, not a user-task benchmark | `recall benchmark truth-floor --suite benchmark-truth-floor --dataset evals/benchmark-truth-floor/cases.v1.json --format json` | +| Native source-index Phase 3 | Clean pinned receipt over 781 files, 6,808 nodes, and 15,115 edges; machine-specific, not a scale or parity claim | Source checkout: `node scripts/code-intelligence-phase3-index.mjs --check` | The package bundles the session, temporal, and truth-floor fixtures. The structured-ingest check measures this repository's own status and provider @@ -177,19 +186,36 @@ cross-product benchmark leaderboard. harness; strict result import; pending-only proposals; and source-rechecked named approval. Optional direct API execution is experimental and explicitly consented. -- Read-only `recall mcp server` exposing `memory.recall`, `context.profile`, - `context.pack`, `repo.map`, and `code.impact` over local stdio with active - facts separated from proposals. +- Read-only `recall mcp server` exposing twelve local tools for governed memory, + compact context, repository maps, architecture, code search, symbol context, + call traces, dependencies, routes, changed-file impact, and index status. +- Production-default packaged Rust reads and a local SQLite + generation store, incremental refresh, doctor/confirm-gated repair, bounded + queries, and read-only MCP access to a prebuilt index. Native + `repo.architecture` returns deterministic communities, bounded entry-to-sink + processes, and their source relationships. Native use requires a verified + matching binary from a local build, an explicit environment path, or an + optional platform package. Graph commands, direct MCP startup, and + installer-generated MCP config select native by default and have no alternate + intelligence engine. The current-host packed-install gate is green; other targets, + signing, registry publication, and full language parity remain unproven. - Persisted MCP cursors and `since` deltas so repeated reads send only changed current truth, including after restart. - Preview-then-confirm install paths for Claude Code, Cursor, and Codex with explicit project root and project SQLite memory path. - Local `/memory` cockpit over the loopback Control API with temporal facts, proposal counts, MCP delivery stats, and confirm-gated approvals. -- Experimental Rust acceleration paths for local ingest, governed graph/search, - wiki, MCP, and static analysis when explicitly invoked. They require a local - `cargo build --release` before use; Rust source ships in the npm package, but - build output stays out of the tarball. +- Rust paths for local ingest, governed graph/search, wiki, MCP, and static + analysis. Read commands select native by default; source checkouts + can use a local release build, and installed packages can use a verified + matching optional platform package. +- Release-candidate native packaging now has five target-specific optional + package manifests and a checksum-, version-, target-, and path-verified + resolver. A local macOS arm64 packed-install gate passes without Cargo or + Rust and covers all fourteen Tier 1 parsers plus the SQLite lifecycle. These + platform packages are not published or signed yet, so stable publication + remains blocked until the five verified packages are available before the + root package. - Deterministic local benches for temporal correctness, session delta delivery, and checkout-derived structured-ingest sufficiency. @@ -200,7 +226,9 @@ Memory Recall is built around explicit authority: - memory ingest creates proposals, not trusted facts; - active memory requires review and approval; - rejections and supersessions are recorded, not silently deleted; -- MCP resources are read-only by default; +- MCP structural reads do not write canonical memory, source indexes, config, + network, or external systems; `memory.recall` and `context.profile` may persist + local cursors and append delivery statistics; - setup and connect commands support dry-run previews; - local storage remains local unless you intentionally move it; - external adapters stay disabled until reviewed, pinned, licensed, and tested. @@ -214,7 +242,9 @@ for the deeper boundary rules. Production PostgreSQL repositories, production authentication, hosted embeddings, vector databases, hosted memory sync, write-capable MCP tools, automatic harness history import, real social connectors, hardened sandboxes, signed Agent Pack -distribution, and a production frontend framework are not claimed. +distribution, fully measured Tier 1 language capability, general +cross-repository analysis beyond the bounded exact Go two-repository path, and +cross-platform million-node performance are not claimed. `PROJECT_STATUS.json` is the machine-readable source for current capability status and limitations. @@ -234,6 +264,15 @@ recall token-saver recall graph stats --root . --format summary recall graph search --root . --query "auth workflow" --format summary recall graph trace --root . --symbol runAuthWorkflow --format summary +recall graph index --status --root . --format summary +recall graph index --write --root . --format json +recall graph index --refresh --watch --root . --format summary +recall graph index --write --engine native --root . --format summary +recall graph index --query main --kind exact --engine native --root . --format json +recall graph index --doctor --engine native --root . --format summary +recall graph repositories register --write --root . --repository repositories/client --name Client --format json +recall graph repositories list --read-only --root . --limit 10 --format summary +recall mcp server --read-only --engine native --root . --stdio recall context handoff --read-only --from codex --root . --objective "Prepare handoff" --step "select next agent context" --target codex --format summary recall handoff ``` @@ -264,12 +303,12 @@ boundaries. ## Status -Release candidate: **1.1.0**. Registry version: verify with `npm view memory-recall version`. +Source major-release candidate: **2.0.0**. Registry version: verify with `npm view memory-recall version`. | Surface | Status | | --- | --- | -| Source checkout | 1.1.0 local-ready release candidate | -| npm package | 1.1.0 publish-ready; use only after the registry reports 1.1.0 | +| Source checkout | 2.0.0 local-ready major-release candidate | +| npm package | Install the current registry release with `memory-recall@latest` | | CLI | `recall` | | Marketplace / plugin registry | Manifest prepared; not submitted | | Client hooks | Opt-in Codex/Claude connect writer; dry-run/manual fallback | diff --git a/REPOSITORY_MANIFEST.json b/REPOSITORY_MANIFEST.json index 011a3af1..7ac2b44d 100644 --- a/REPOSITORY_MANIFEST.json +++ b/REPOSITORY_MANIFEST.json @@ -1,9 +1,9 @@ { "schemaVersion": "1.0.0", "project": "memory-recall", - "generatedAt": "2026-07-15T14:04:23.410Z", - "fileCount": 923, - "totalBytes": 6437920, + "generatedAt": "2026-07-19T13:48:39.185Z", + "fileCount": 1170, + "totalBytes": 9523628, "exclusions": [ ".git/**", ".local/**", @@ -40,6 +40,11 @@ "bytes": 591, "sha256": "033047b9cfbe8de9c932558ee3c9c3ec622b1413ff52fc23628a86bd28de57b4" }, + { + "path": ".git", + "bytes": 91, + "sha256": "a57aeef226106fbcf7f1463d808725de7c032443bbecfad8c5997a9d17da77dc" + }, { "path": ".gitignore", "bytes": 175, @@ -57,8 +62,8 @@ }, { "path": ".npmignore", - "bytes": 266, - "sha256": "bd684e155960e61975c6bf9e83d4dabd8e0125f77c19e699cc62a50d2c0672ff" + "bytes": 291, + "sha256": "727d4a7ea446f5696cdb926cf9bf900646f319dc648dba3826d40f1671671a65" }, { "path": ".npmrc", @@ -452,48 +457,88 @@ }, { "path": "apps/cli/help.mjs", - "bytes": 25248, - "sha256": "0c0117d86106ec7e5f9cd65a3481ef92b06d5daf63fab94cb26517a9d7ac766d" + "bytes": 27627, + "sha256": "08e6a709fa5a750837c4eae50979c694639fad7669180bb60a55e6b87aa91a65" }, { "path": "apps/cli/oaf.mjs", - "bytes": 460424, - "sha256": "62512b730c7146f2ed243d22a9071290bd00301d28cce64d69b34461b1774787" + "bytes": 529377, + "sha256": "338ef3256ed2863a49d8fc5add7d3591c07156b2d1b92391c5fa1cb3faddba59" }, { "path": "apps/web/AGENTS.md", "bytes": 202, "sha256": "abb5d47e0b5056b0bdbc4fa67f85b9cf658b53cce0c7f2f21e561f6a349283d1" }, + { + "path": "apps/web/api.js", + "bytes": 1839, + "sha256": "368921a08446f8b24e6ba514f5178af084a070d52f96a0eaa92d6218828eeb4f" + }, { "path": "apps/web/app.js", - "bytes": 260078, - "sha256": "fff751153ac96b67bc6d2df262fe262764283a533c29c65ce1f90131893c7819" + "bytes": 226781, + "sha256": "e55264fc4e5189f20219658a6d772992c48275815e94ad781e8d81bfd94da4db" }, { "path": "apps/web/favicon.svg", "bytes": 495, "sha256": "b1774942e350f0c007b10e9e82077e2442f1484e88eb79d1e90efc1e38afa1c5" }, + { + "path": "apps/web/graph-layout-worker.js", + "bytes": 2825, + "sha256": "0c9330628c65c52c557d812829339dde9c4e3d9c85fbb4557134d345055b5145" + }, + { + "path": "apps/web/graph-viewport.js", + "bytes": 12417, + "sha256": "42e2a55943277d68ac07db31291911a53dc63271ce0d1cb51c2b3fbe0e11ebf8" + }, { "path": "apps/web/index.html", - "bytes": 2807, - "sha256": "f0ce53a83c84bb79e23acc12855c6dc322dade75d98c8c3f6a01799d695255f5" + "bytes": 3345, + "sha256": "265fa59c60cab744ea57cfa8856db30d73b9ada7791455db5d3e8a6c077ebb03" + }, + { + "path": "apps/web/memory-graph-view.js", + "bytes": 13299, + "sha256": "86db53e576aabaac5e44575ea25159f2bd95258988807a16d0f391c63679cb05" + }, + { + "path": "apps/web/orientation-model.js", + "bytes": 27172, + "sha256": "f43b29b5ff70a097689578654fbcc5e2d9f60ba9f92dc5c143c2b893ee9c35ec" + }, + { + "path": "apps/web/orientation-view.js", + "bytes": 9023, + "sha256": "577e52e5f8c5917237ae2f57e476cebf2e4c5c813bafc3bb8977d84fa85e098c" }, { "path": "apps/web/shell-model.js", - "bytes": 2049, - "sha256": "723e987aa0296a8df2707ca6779fc1d734e6958a73f4b62b6051dc96651aee5d" + "bytes": 2187, + "sha256": "a18db7bf8e798993bc03394336bd75b904e2ecc3497d0a13a46eeab250178123" + }, + { + "path": "apps/web/source-map-view.js", + "bytes": 21439, + "sha256": "a779669514dcf97fbd1c184d00e33052924b5019648c6754f287891fab33a252" }, { "path": "apps/web/styles.css", - "bytes": 44685, - "sha256": "dd95e6ebaca5e659f47776cc6fd6cc7d6d9fa565ac3e9281753a9329740598a3" + "bytes": 60052, + "sha256": "2f1c3c62d37eb9da48faed7c2d7db13de1848294bf7f2c51073c7f4ee49bcdf8" }, { "path": "apps/web/tokens.css", - "bytes": 2351, - "sha256": "18b19c1168909f956be55ccca78adbb12ce5db74041f17eefc445a747a11a54e" + "bytes": 2350, + "sha256": "3ed7617e934302dac3f43142167270984f126d226027a9039f6563257fba00cb" + }, + { + "path": "apps/web/ui-primitives.js", + "bytes": 5261, + "sha256": "ff826063df128ce473a7aac672901f0f35e3fb7fca9cc8eafd97e9e98159bd79" }, { "path": "assets/brand/logo.svg", @@ -517,8 +562,8 @@ }, { "path": "ASSIGN_TO_AGENT.md", - "bytes": 3313, - "sha256": "d200fc428f0f6b53ee9fd5a22e248564da09217e43e3662349504c7bd55ca578" + "bytes": 3307, + "sha256": "df1e4428531479c13a84cb244feffb238042390480390be8edcecee85f16e297" }, { "path": "BOOTSTRAP_REPORT.md", @@ -532,8 +577,8 @@ }, { "path": "CHANGELOG.md", - "bytes": 24010, - "sha256": "0f0377bf2901ab2cc29d38fb80f858f4928c207f94b1d80941690be294fae1d6" + "bytes": 32751, + "sha256": "a2274f5e817972b32a40f09e9053cb5a2cd84a73e57b95f8891ae89cbbb16a3d" }, { "path": "CITATION.cff", @@ -607,8 +652,8 @@ }, { "path": "DESIGN.md", - "bytes": 4800, - "sha256": "be5c21fb3ac78de26d0bffa7cf0e955d421de0d42d5c1aa8e5aa6aa24243ad57" + "bytes": 5459, + "sha256": "1d9b98a5634f517efd8a2a85276f25dfee6c39a6daa8145ab89cd1498ae6e0f1" }, { "path": "Dockerfile", @@ -725,6 +770,16 @@ "bytes": 1763, "sha256": "1306750b623880637b64b1729b79fbcd1a24f2d7b42f8b2b3e758cba95981e25" }, + { + "path": "docs/adr/0023-production-rust-code-intelligence-engine.md", + "bytes": 2088, + "sha256": "e7d669f42e786d979892831d415bb1195670c815665cea34a29d9abf699bf46d" + }, + { + "path": "docs/adr/0024-rust-sqlite-source-index.md", + "bytes": 2579, + "sha256": "3d16efd5c47392f9eaf88e3e429a88b332693e19bd3495dae775861f6b09769f" + }, { "path": "docs/AGENTS.md", "bytes": 380, @@ -747,8 +802,8 @@ }, { "path": "docs/api/openapi.yaml", - "bytes": 60235, - "sha256": "c7f95a629fc72f146614c92db1d2a58147a675b441a893ae531f771b23a1b4d3" + "bytes": 63453, + "sha256": "591a803329cc9a4550be5048a4f26823ce3187ea9ccc7877f89f74c60fa28e27" }, { "path": "docs/api/README.md", @@ -797,8 +852,8 @@ }, { "path": "docs/architecture/native-providers.md", - "bytes": 14390, - "sha256": "e8ff700fc10c909222e85d6f9bfc51726d6ba3197c0a9dc9f77711ddce3fdc6a" + "bytes": 13816, + "sha256": "2cf77d39c461765a4e52e317df6dbce3f0db8ae2cd2e00c7e00749c6a71a3fd3" }, { "path": "docs/architecture/overview.md", @@ -807,8 +862,8 @@ }, { "path": "docs/architecture/protocol-bridges.md", - "bytes": 9116, - "sha256": "7c192869b1598612eb962a262ae7ea715ba296b9c9b54dee301a43a68dce8825" + "bytes": 9566, + "sha256": "abd4d5ebcabf4a711c0205a386a5f7770f1ac786a3477668a17b785cf4422841" }, { "path": "docs/architecture/sequence-flows.md", @@ -837,8 +892,8 @@ }, { "path": "docs/benchmarks.md", - "bytes": 8750, - "sha256": "d486819287f1f97c1f51efded6dd7bf6c8491dc8e7a135c9cfaba9ed8f14b212" + "bytes": 20219, + "sha256": "975701efde58e54b64213b6904758f562118f29111eae4d77d90ae46dd5ad068" }, { "path": "docs/implementation/AGENT_EXECUTION_PLAYBOOK.md", @@ -855,6 +910,21 @@ "bytes": 1471, "sha256": "943f6ae5489c7f738aac16a08950b3a8aa67a068e34e0487c44ed1f4836626ca" }, + { + "path": "docs/implementation/MEMORY_RECALL_COMPLETION_MATRIX.md", + "bytes": 38712, + "sha256": "b4cc25a83405170ca59c2b10387b87687f03057874fcccca77add7354a8b1a1f" + }, + { + "path": "docs/implementation/MEMORY_RECALL_SEMVER_COMPATIBILITY_AUDIT.md", + "bytes": 8160, + "sha256": "813c7daea56e54a4a381b0150dee3860131132f37cce26d3de5445f7145fcdff" + }, + { + "path": "docs/implementation/MEMORY_RECALL_UI_ANTI_SLOP_REVIEW.md", + "bytes": 4551, + "sha256": "3e2052e55ca16ffb0effa87565d339d064a51829f89b34448c5b29e5e5545aea" + }, { "path": "docs/implementation/OAF-011-context-selection-note.md", "bytes": 3038, @@ -952,8 +1022,8 @@ }, { "path": "docs/implementation/OAF-031-context-intake-preview-note.md", - "bytes": 21232, - "sha256": "1544d985babf5ffc6101cec46886a10cda8e3f5b23df0198d5cb0cf7963e9ca1" + "bytes": 20740, + "sha256": "42bae7b6885bcae32274bec4bc42c827eb2df5eff8054cb9f74339f2aaf43e5b" }, { "path": "docs/implementation/PARALLELIZATION.md", @@ -992,8 +1062,8 @@ }, { "path": "docs/open-source/release-engineering.md", - "bytes": 2073, - "sha256": "e87aeca2fe36c341ec8649be417c5faa06bd91d29262b7dbedaaf82236220d70" + "bytes": 3779, + "sha256": "7605bf4b6697008bbcecf36726f525dc54ea24c55e9787e34f2ccaa7d40613bc" }, { "path": "docs/operations/backup-restore.md", @@ -1047,8 +1117,8 @@ }, { "path": "docs/product/memory-recall-developer-first.md", - "bytes": 2055, - "sha256": "0ba4cd0631230ee575cd09cba47470619e4eea1ccbb9b61ea489369b1f75e81e" + "bytes": 2639, + "sha256": "7d8c95940966ca0cd9a5ac464d1bd42f216aaaa04f97232ea637621ecef765db" }, { "path": "docs/product/oaf-rust-supertool-spec.md", @@ -1077,13 +1147,13 @@ }, { "path": "docs/release/1.0-COMPATIBILITY-MATRIX.md", - "bytes": 5427, - "sha256": "5b5b502956a17a26abe69785faf0c11cc36f1d1d13f218fc1eaf9d67b79b69df" + "bytes": 5577, + "sha256": "1c169823075abd172a333e43421a6fc6732712ff609d82752eb4ea80cfd4e218" }, { "path": "docs/release/1.0-MARKETPLACE-MANIFEST.json", "bytes": 911, - "sha256": "b24c59297530d79be9ae7b1c5b81f48c654fd72156c688a56bbd02191ef788a1" + "sha256": "f66b0b0725fcf660ae6c9b2d088d38834b7a4e04f9a67aaa5875a882492d29b9" }, { "path": "docs/release/1.0-NORTH-STAR-GAP-AUDIT.md", @@ -1093,12 +1163,12 @@ { "path": "docs/release/1.0-PROVENANCE.json", "bytes": 2414, - "sha256": "559f17ed84af28b5896f6bd346c3fd37fc8d4e1cd3b7c30886a907c3585c858b" + "sha256": "e60c76e1f7f226aaa8112986493b765ddf0c244496efc34924c2737490ec7268" }, { "path": "docs/release/1.0-READINESS-REPORT.md", - "bytes": 3391, - "sha256": "c143026eca72465202cc3f5a713e2c1efe98c03dac8e0423a34301a6760667fc" + "bytes": 3381, + "sha256": "bc68cecaf118c4e1473053b503b9782851db2018812e5dbb1ad1c543ea0ad567" }, { "path": "docs/release/1.0-RELEASE-CHECKLIST.md", @@ -1108,12 +1178,12 @@ { "path": "docs/release/1.0-REPRODUCIBILITY.md", "bytes": 1723, - "sha256": "ffaa143ba56be41c18e99ddee9826b518d2e609eaead617743a9ad7e02db0648" + "sha256": "b287eb55d830378e47e2cf718630fd63a20a7ca0683d9cce7a16e78e429d1941" }, { "path": "docs/release/1.0-SBOM.json", - "bytes": 17159, - "sha256": "59e9ba25e87acc81c2b6cc02b6d803fee49bc5a7f7644a8590c8dd6562926e76" + "bytes": 17965, + "sha256": "7b1f2dd6e94ca85dcda8947f11890bdddbe8edf8d9d4c56069ee82a4297105e7" }, { "path": "docs/release/1.0-SECURITY-REVIEW.md", @@ -1195,6 +1265,11 @@ "bytes": 1588, "sha256": "8252eac4790ca9704f36e15e20b5bf6d0bf1dc35d104da35e3791a18c94ec3f6" }, + { + "path": "docs/usage/code-intelligence-support.md", + "bytes": 20639, + "sha256": "1741195cc8415cbefc9b557a90aba4917a79923bd240d63f1d422e00f1dac202" + }, { "path": "docs/usage/codex-setup.md", "bytes": 1084, @@ -1208,12 +1283,12 @@ { "path": "docs/usage/local-agent-handoff.md", "bytes": 17390, - "sha256": "61d97e5cb58115dd5e596da2d7a9c7a08a16ae205744dffbe5c25529bce0ea0d" + "sha256": "7d86ba77c2cf5083423743509d3ac1ce1e2f579de28aa07747a79ce6c187808a" }, { "path": "docs/usage/mcp-server-reference.md", - "bytes": 2616, - "sha256": "be31b255e4d79565fb713ef9a75ffc56ef42e438f874b36957b99c92bc6265f1" + "bytes": 8765, + "sha256": "662eef7072dcaf9e99becf2c5e7be9c4cc6b7de4eb38f9830177713c943551a1" }, { "path": "docs/usage/memory-lifecycle.md", @@ -1232,13 +1307,13 @@ }, { "path": "docs/usage/recall-map.md", - "bytes": 2866, - "sha256": "abeaf7d2a788d9301fea56ea207103cffb418ca32ab3d5a2f91b06bd1b6e5f1f" + "bytes": 5087, + "sha256": "502b2f5a6e2f6848e69a24e14a6c1a2755e485b9f58954b244d274b9bf290125" }, { "path": "docs/usage/rust-acceleration.md", - "bytes": 1762, - "sha256": "76cfd88a85d1d353b5f24a719570b423358a6441bdcdc1d76a081b9186cce8d5" + "bytes": 4351, + "sha256": "e35cdb45f74fd448f6b966d7911bbe95ff48caaf00b860a58221b1229f139e50" }, { "path": "docs/usage/security-model.md", @@ -1252,8 +1327,8 @@ }, { "path": "docs/usage/support-matrix.md", - "bytes": 4490, - "sha256": "dd4108828b742e8d06112e6abdfa4119f90a8b034b417b9e7e41f6924dcf26a6" + "bytes": 8551, + "sha256": "73844778b418c382828bc0877c554ac1592024e078fc2b65ef6a02670d7ac80e" }, { "path": "docs/usage/token-savings.md", @@ -1262,13 +1337,13 @@ }, { "path": "docs/usage/troubleshooting.md", - "bytes": 2446, - "sha256": "317f19dfec8cc3656981460e2bd914a77c193915935d3f36a51683f7719970ec" + "bytes": 2448, + "sha256": "21e8f9acf0a78d8a2218ef4818ae9258af708e99767e4fe65d2df1dff3699eb2" }, { "path": "docs/usage/uninstall.md", - "bytes": 3349, - "sha256": "b2e6b05ca2429fe0b2cb74f5e61cbf75649eb708293ca6ff9431b9a6217cf79a" + "bytes": 3990, + "sha256": "1a8ff992647486f49625d1112bddf586347c6a0e621ff29a2071c69fa714c8bd" }, { "path": "docs/ux/accessibility.md", @@ -1315,10 +1390,630 @@ "bytes": 6152, "sha256": "817f8fc65a07dbffbc9ca84d6671afab300d19a4e4e937b28aec363c99d1dbc0" }, + { + "path": "evals/code-intelligence/benchmark-gates.v1.json", + "bytes": 649, + "sha256": "76150d22d01c59559658ad7d1c76491185910bf1e45d5b205cda862540e042d2" + }, + { + "path": "evals/code-intelligence/capability-matrix.v1.json", + "bytes": 223828, + "sha256": "2f4cd8adadd4fd833a45a37938a1d1f2ec8b17f2ac44152b265c6ff38701a334" + }, + { + "path": "evals/code-intelligence/corpus-candidates.v1.json", + "bytes": 13646, + "sha256": "e8baef596e81059c9d8b7db83015cea2d1fbb9ecd90f56cecabdf2ba68243b30" + }, + { + "path": "evals/code-intelligence/corpus.v1.json", + "bytes": 16470, + "sha256": "479c89ce3dd93b123691ade145a1d24c266bd9d166cf23a30cceff284e02e5a4" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/go/api/routes.go", + "bytes": 264, + "sha256": "d8baa74b2cdb9a5cdb24097598ce64f70073f41ab61f36eed8dbf99a3b36d702" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/go/go.mod", + "bytes": 33, + "sha256": "f1f4b932d4c5239bc3890b499c05844a84bb6eccdb1e4208d3eed77d0c00ba5d" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/go/service/service.go", + "bytes": 195, + "sha256": "6ffaff386b38653a7498e49c1053c802116ebcaf08658f6483cb62081fe10fd3" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/python/pyproject.toml", + "bytes": 46, + "sha256": "abc237752d5486897316f297cbe8a2dcc8583f0f550aba4c93077e6c2062f616" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/python/src/demo_app/api.py", + "bytes": 333, + "sha256": "89b256d5fcac6d65dc8cb020f43f6dab550ae313c4d98a2f14d0c7847346dd56" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/python/src/demo_app/base.py", + "bytes": 60, + "sha256": "bc6b11b9723ee774da7cc8d0856728c15fa1ab561dfe1eb910c70de2dfe66f9e" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/python/src/demo_app/service.py", + "bytes": 112, + "sha256": "fc9c32c28555149f41a4fed9e851e29783c03a29864766d61e0692bf7bc3e77c" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/rust/Cargo.toml", + "bytes": 65, + "sha256": "9740eff6b92845309c301682c15bedf99312f9a0deb91f8de9dde382f4f51eb3" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/rust/src/lib.rs", + "bytes": 342, + "sha256": "a805d9c71e59359a377c3b1f6e0a338cdc961c2554fd9048ba9259101e227bb7" + }, + { + "path": "evals/code-intelligence/fixtures/batch-b/rust/src/service.rs", + "bytes": 307, + "sha256": "1ace400c771238f9d636a50e38c37df2d9899598cf48c664c16f29f88832c7f7" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/csharp/src/Demo/Api.cs", + "bytes": 686, + "sha256": "46d28a16e7215c462f8bedb0e4887498db15da5cd63607f6e757acb18ad1082a" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/csharp/src/Demo/Models.cs", + "bytes": 62, + "sha256": "b04ca5882630f45623c191f84b9fdd8b38b60dad4f9f34ee93a2de48a33a4b46" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/csharp/src/Demo/Services.cs", + "bytes": 402, + "sha256": "ad522a040adc2968e245ddc1b0b73675e422f9d54de4b480a15b6876e9faddfd" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/java/src/main/java/com/acme/api/ItemController.java", + "bytes": 658, + "sha256": "900b87174336fa4334af7d29b708e85023159d320e206e3501e1a62b595c1ba3" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/java/src/main/java/com/acme/model/Item.java", + "bytes": 58, + "sha256": "80ea51977ff10507d20bf85fa9beee66502ddfd11f734d0dcf6c0b0b1aa38ffa" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/java/src/main/java/com/acme/service/ItemService.java", + "bytes": 379, + "sha256": "358b7b2b52e2378f156d3108e6e41b053a6355f01d7b74f808a4ef4ec7752ba8" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/kotlin/src/main/kotlin/com/acme/api/Routes.kt", + "bytes": 361, + "sha256": "2bdd4a1314523debf11e37774de49394df6d6e8ef12c73a46fbc2b2ea464df54" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/kotlin/src/main/kotlin/com/acme/model/Item.kt", + "bytes": 56, + "sha256": "5bef93c39c4d6379f588a0466fc82c0c11d62e4114f07309cd90d8f0b1013570" + }, + { + "path": "evals/code-intelligence/fixtures/batch-c/kotlin/src/main/kotlin/com/acme/service/ItemService.kt", + "bytes": 376, + "sha256": "5fb8298c5271e2b236f27c2a39ebdbe47957988c508cc2bc8087691ed0a6b658" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/c/CMakeLists.txt", + "bytes": 98, + "sha256": "d1f0ded956621518d5700f86ecc401631844cfe8bad973adcf12bd6d7291820b" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/c/include/item.h", + "bytes": 120, + "sha256": "7ca5e02feb71ac36c28f07ca9692e00573cf7ca0360c44f5ee2e56bcf4ed0868" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/c/src/item.c", + "bytes": 100, + "sha256": "df27596301efb98fb1ac7021f533c4d243960a08e513960eac055b03541574f0" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/c/src/main.c", + "bytes": 104, + "sha256": "ec82b793161accc286fda1fcee55a71687306733e8dcef74e269118fa4a8b416" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/cpp/CMakeLists.txt", + "bytes": 120, + "sha256": "40ae3c72b5c82f0b816cfec20e7cf41a05f29dd84f9fbe2cd56a567c40d61a4e" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/cpp/include/item_service.hpp", + "bytes": 421, + "sha256": "344f0d490f8b44cbe551ebed6e068af8daca51c466ee22b6628f41034c24de1e" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/cpp/src/item_service.cpp", + "bytes": 442, + "sha256": "daeb8f7634250075b92338eb2c1fa9a3c7b5c8f44ecbd1e02f8c46f28282e041" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/cpp/src/main.cpp", + "bytes": 125, + "sha256": "1c722a7c4d63b0e4d1211111aa41763a6eb400b7a52c1b3ff9173021ffe3ab18" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/dart/lib/item.dart", + "bytes": 421, + "sha256": "64fe207f87ecbfea3b9575bb6c6cad38337da6ed1c4b65bcd1787c77915a237e" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/dart/lib/main.dart", + "bytes": 139, + "sha256": "6579f06c19c878e84e22a96be27ec2f659a8cbe8bb63ef2ceca3546e3ff595a9" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/dart/lib/routes_part.dart", + "bytes": 55, + "sha256": "0420555af70718feb67d436b10e3276a503d5fe6f29e99d0f66c2bb47b46e6a1" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/dart/lib/routes.dart", + "bytes": 309, + "sha256": "6ad05b92c07ef564c0cd7251fbc044f63745e9e448b44d26838e40a14047ef70" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/dart/pubspec.yaml", + "bytes": 107, + "sha256": "73fe8a5feb6dc153265031f8817ac791999dacb71ae67ea126330b2a9d56aeaa" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/swift/Package.swift", + "bytes": 270, + "sha256": "0a8adfaa723e2dd99230e33b46148c0d5c3d7b85fa24e4edf368217acf5a7412" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/swift/Sources/App/Item.swift", + "bytes": 95, + "sha256": "385489a8960f7936db6e00d4f7667305f382f1de92963f0efba50dda75342c69" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/swift/Sources/App/ItemService.swift", + "bytes": 289, + "sha256": "bb541ef0a439320162765b5c66f891203789873fde8bfbb3053b219ab7209c4e" + }, + { + "path": "evals/code-intelligence/fixtures/batch-d/swift/Sources/App/routes.swift", + "bytes": 180, + "sha256": "f66ccd6a663f63581d0d1be0aa9dbdf960bb0eb7ff4e1f125df93af898234594" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/composer.json", + "bytes": 51, + "sha256": "cc2c40955526b0a0a975fd54639b62605343c6d65dc3d1536f3b07ba6584dd3e" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/routes/web.php", + "bytes": 137, + "sha256": "70b13f4888cbf07789078127d88b23ae9206521cc50d8f35b8acfc482e7f7e64" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/src/BaseService.php", + "bytes": 52, + "sha256": "642b76d6d6a4d0802ac90d600eb6885864bf9791b1b1c9a225a1f05a8cd4605f" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/src/Contracts/ItemLoader.php", + "bytes": 103, + "sha256": "fb8715d7347a355cc693ea34ead05403b7b01e7f88842683bc910cf09fb6c5dd" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/src/Controller/ItemController.php", + "bytes": 336, + "sha256": "14936bdfd17fbe69b8ba8998856db4678a597471591843063bad2fa8a8ad1ee2" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/src/Item.php", + "bytes": 97, + "sha256": "bef7499d9162430814547d5252cabab8c5ce153c7dc41455494ffff78f143512" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/src/ItemService.php", + "bytes": 466, + "sha256": "0fc1bdeb91fc4b253ab251c3fd4b65f681cde202a63470949c70d2bda0b31abd" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/php/src/Support/LogsItems.php", + "bytes": 99, + "sha256": "6fc0b6b2ab2d13af96bc6d00582d570b507a7c757c161914a4e35c46779081bb" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/ruby/app.rb", + "bytes": 126, + "sha256": "bc77525e032f7210b9d93b3f94bb4391c0847d0580b0ff9ad938de669f6f1d69" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/ruby/app/controllers/items_controller.rb", + "bytes": 83, + "sha256": "89327faf625b10b396eadf983c14c0699dcea8b4a69d9892cb16f9d238376a87" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/ruby/config/routes.rb", + "bytes": 74, + "sha256": "9078cd1a3a63714f14313ad2cce74fc95baf7b7455fc8d1b195c4e2aced172e6" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/ruby/lib/demo/item_service.rb", + "bytes": 369, + "sha256": "e02fee4e0dc17ad8a24e83bbc0cf4ee872461cc5ccbfce4a5e81eefc71351db9" + }, + { + "path": "evals/code-intelligence/fixtures/batch-e/ruby/lib/demo/item.rb", + "bytes": 102, + "sha256": "2093b475a630087e7af2f55a6ccae43d6d48926fa54535be60068fd35b44a015" + }, + { + "path": "evals/code-intelligence/results/million-node-rust-index.json", + "bytes": 9121, + "sha256": "246c301d78a0111a29ef6b9ca2e8b17316e8f016962095e4d8c93d6251bcb48f" + }, + { + "path": "evals/code-intelligence/results/phase2-batch-a.json", + "bytes": 46612, + "sha256": "20fe204a0ce349550d3c493ba01fbab25ef4604688959bb5aee8e97a2dfbc6e5" + }, + { + "path": "evals/code-intelligence/results/phase2-batch-b.json", + "bytes": 84840, + "sha256": "439d51b0b622ec920c443ba44767d1d0df7563fb302610eacd2f32012b221c78" + }, + { + "path": "evals/code-intelligence/results/phase2-batch-c.json", + "bytes": 69865, + "sha256": "3e21fb2c3911eb43fa383ef5f47a5a0226a3c2caf3e584157fdc1f1a9cd9875b" + }, + { + "path": "evals/code-intelligence/results/phase2-batch-d.json", + "bytes": 91356, + "sha256": "da0daf03063da4b7ceeeec212876ce3efe40b846db129742988a58b58d86494d" + }, + { + "path": "evals/code-intelligence/results/phase2-batch-e.json", + "bytes": 46052, + "sha256": "439f7bfa22d977acdb70148704057fa6dc3eecd675c6b06cd21f5fbb87a8f8b2" + }, + { + "path": "evals/code-intelligence/results/phase2-tier1-summary.json", + "bytes": 397171, + "sha256": "bb96ec1b9666a35d898c5118c31ecb0e33b4afcb80659b66e5a29a454e0e00e8" + }, + { + "path": "evals/code-intelligence/results/phase3-source-index.json", + "bytes": 27403, + "sha256": "f26d2cd804bc3122a4841127448eeba3253c94616e3d33c09a951df78266c524" + }, + { + "path": "evals/code-intelligence/results/phase4-intelligence.json", + "bytes": 54908, + "sha256": "1d0bb5da8155bcba6e549979edf7030ac3f0070d5fd55a30d400321a5099581a" + }, + { + "path": "evals/code-intelligence/results/phase5-cross-service.json", + "bytes": 2835, + "sha256": "71555b704fbb2da0bdab46f79f58efe8758a9a2c43b980ad11c593dc811a06fb" + }, + { + "path": "evals/code-intelligence/results/phase5-go-cross-repository.json", + "bytes": 3736, + "sha256": "883cb4b5befa8b825d379e03863a11a7bbe9698430a5a46dcff21b01f88aed4f" + }, + { + "path": "evals/code-intelligence/truth/fixtures/c.json", + "bytes": 4255, + "sha256": "632c06a5c27568361cbb667cb7c8380ca97d26ba42e899706bd476bdeff543f1" + }, + { + "path": "evals/code-intelligence/truth/fixtures/cpp.json", + "bytes": 6392, + "sha256": "34a6f3a66d8502e75802ce1ee2cfb159debf43d6d2028c596e6988f740a5241c" + }, + { + "path": "evals/code-intelligence/truth/fixtures/csharp.json", + "bytes": 9466, + "sha256": "03562f8c6be5824791d1626344b398718bf7e3d8aad09e1804db18a073e42a23" + }, + { + "path": "evals/code-intelligence/truth/fixtures/dart.json", + "bytes": 8029, + "sha256": "8a0273fb7b56f2e57fc26af660bf0d16e7e8c1c7b15ee73d691d318754b9596c" + }, + { + "path": "evals/code-intelligence/truth/fixtures/go.json", + "bytes": 9130, + "sha256": "854b6f11d94d4e7f1af5561deec4ef15da34b672666be33e9b60a17dbdd94d9a" + }, + { + "path": "evals/code-intelligence/truth/fixtures/java.json", + "bytes": 9088, + "sha256": "7b8defab09d5bfe426af7a9bce3ace87551352c49c62729e49e2cbebd9c21904" + }, + { + "path": "evals/code-intelligence/truth/fixtures/javascript.json", + "bytes": 4490, + "sha256": "699f7321d4da058e8a110944eb1dbdeb402e7530d9ae25624b84f0a57f5b2545" + }, + { + "path": "evals/code-intelligence/truth/fixtures/kotlin.json", + "bytes": 9350, + "sha256": "c2c84e5f173eb843c6897a7d9f86e0b7b1989738d8e14d833fa4877d545df3c8" + }, + { + "path": "evals/code-intelligence/truth/fixtures/php.json", + "bytes": 7817, + "sha256": "b45342018297b20703ef7b3a5b101f5ba085ce0b2292acfa88579025b8635549" + }, + { + "path": "evals/code-intelligence/truth/fixtures/python.json", + "bytes": 9823, + "sha256": "b200c52d55e672693e540dc49ae62d9bce44e407364613ff0996e06eff88efc7" + }, + { + "path": "evals/code-intelligence/truth/fixtures/ruby.json", + "bytes": 5902, + "sha256": "03595655c4e335de0c7c4ec2bb270594190837216f5c7f758e4f6c4dbdea6854" + }, + { + "path": "evals/code-intelligence/truth/fixtures/rust.json", + "bytes": 10969, + "sha256": "a2464a9a7fae8568d18b260a9123e3ce31e9daab4c21b35a2cd1f7bfed650271" + }, + { + "path": "evals/code-intelligence/truth/fixtures/swift.json", + "bytes": 6086, + "sha256": "bf212e62ad5d421f76e18d1d78fd744755703306d1d868beda5c710c6834dc28" + }, + { + "path": "evals/code-intelligence/truth/fixtures/typescript.json", + "bytes": 5467, + "sha256": "4411c8e765db910d62b6bb161beaa0a914218de8b0aaa9f31f3a1cff7e7dcfe0" + }, + { + "path": "evals/code-intelligence/truth/README.md", + "bytes": 3969, + "sha256": "0447f7cbfc2d8a7bb926ee5cd3bfd96c154a4cbd8bd66b1886a509132afd5e85" + }, + { + "path": "evals/code-intelligence/truth/repositories/c/cirepo_c_antirez_kilo.json", + "bytes": 3493, + "sha256": "a88251b541db7ff8923344eec557a2d3421d53e8e5b6d89e3a83fce9b026b160" + }, + { + "path": "evals/code-intelligence/truth/repositories/c/cirepo_c_curl_curl.json", + "bytes": 3573, + "sha256": "5636f41a9e236b5701e9ae73e4764593a23706149855cfe53451d967f293f3ca" + }, + { + "path": "evals/code-intelligence/truth/repositories/c/cirepo_c_libuv_libuv.json", + "bytes": 3934, + "sha256": "82b1d107be458bb40fc47cd27bb49b2f5650adaf3aeef2ce0fb60adbdb28f1f4" + }, + { + "path": "evals/code-intelligence/truth/repositories/cpp/cirepo_cpp_catchorg_catch2.json", + "bytes": 5321, + "sha256": "af6bafac8ac539d2ee86578eb6c5456f8fbc54a5524a089cf617cf35055382da" + }, + { + "path": "evals/code-intelligence/truth/repositories/cpp/cirepo_cpp_fmtlib_fmt.json", + "bytes": 5634, + "sha256": "02ae45a8b9db02e3b4a34764b4109fc134477a4be18de361a05931297e3262d5" + }, + { + "path": "evals/code-intelligence/truth/repositories/cpp/cirepo_cpp_nlohmann_json.json", + "bytes": 6081, + "sha256": "bc73fc6cd1560b927080986933efb419a923f7e3b5ff5066f3ff0bda95852998" + }, + { + "path": "evals/code-intelligence/truth/repositories/csharp/cirepo_csharp_dotnet_aspnetcore.json", + "bytes": 4593, + "sha256": "35dd7b14e1b954eb38ec141a494b35390e787bc9cef5662aad13e74442f8e404" + }, + { + "path": "evals/code-intelligence/truth/repositories/csharp/cirepo_csharp_dotnet_runtime.json", + "bytes": 4459, + "sha256": "73a93f5e08ffa01f67e60f2d92b3b19cf16c2e65dade351a3f50a0b1f8362eff" + }, + { + "path": "evals/code-intelligence/truth/repositories/csharp/cirepo_csharp_jamesnk_newtonsoft_json.json", + "bytes": 4159, + "sha256": "bd2f4cef23922b1e93888a7baab633b8ae772ba29f92c3f14f4ab014cc6146c1" + }, + { + "path": "evals/code-intelligence/truth/repositories/dart/cirepo_dart_dart_lang_http.json", + "bytes": 4431, + "sha256": "a628474d8f9cb1f6d095947dbe97f6f934044ef80295898cff7f1f82b61c70d9" + }, + { + "path": "evals/code-intelligence/truth/repositories/dart/cirepo_dart_dart_lang_shelf.json", + "bytes": 4826, + "sha256": "e8da959c4810fba2d6b588f915cdd2c0feb0b7b50421f4da5e8fe93b62d58cba" + }, + { + "path": "evals/code-intelligence/truth/repositories/dart/cirepo_dart_flutter_samples.json", + "bytes": 6898, + "sha256": "ed60da6e4ef7fd4748f1547a7ce40c75fb05cfdc8158dec457aff3ac81e31b1d" + }, + { + "path": "evals/code-intelligence/truth/repositories/go/cirepo_go_gin_gonic_gin.json", + "bytes": 6367, + "sha256": "0ea5f274291f9dbbc1ea87392c37e423187707640b84ab6f8a6ff68936365f13" + }, + { + "path": "evals/code-intelligence/truth/repositories/go/cirepo_go_go_chi_chi.json", + "bytes": 7277, + "sha256": "eaff398b50b309a047076e74b40e9052853ece42b5addc6a1281993984ee365c" + }, + { + "path": "evals/code-intelligence/truth/repositories/go/cirepo_go_hashicorp_go_multierror.json", + "bytes": 6058, + "sha256": "cf721a816fdb39c0b7cf697b15c895110f7f3bb9c775b4b0fbc355cb2266e9c2" + }, + { + "path": "evals/code-intelligence/truth/repositories/java/cirepo_java_google_gson.json", + "bytes": 3448, + "sha256": "0fa86a922a3bc67e40e6785af580cb67a5f9ed7a9e5211b304d4aee7ab3baaf4" + }, + { + "path": "evals/code-intelligence/truth/repositories/java/cirepo_java_google_guava.json", + "bytes": 3386, + "sha256": "7c548ec3566ca5ce471b6bedef8dcfddb4a3559adf484be845baf918725170e6" + }, + { + "path": "evals/code-intelligence/truth/repositories/java/cirepo_java_spring_projects_spring_petclinic.json", + "bytes": 4184, + "sha256": "5a7519b0bace969998d5b650abdac6e12a0cf996cd9d6a5aeeeb5828f7590e79" + }, + { + "path": "evals/code-intelligence/truth/repositories/javascript/cirepo_javascript_axios_axios.json", + "bytes": 4741, + "sha256": "426f8be9be9c04e85d70ff5e6b7d156287cb7e47497430f763d655973dfb2c54" + }, + { + "path": "evals/code-intelligence/truth/repositories/javascript/cirepo_javascript_expressjs_express.json", + "bytes": 4606, + "sha256": "c4294d8ef73c8cce2b34ea62f32c04ea47f7611e28994cb24a2fd12134f298ec" + }, + { + "path": "evals/code-intelligence/truth/repositories/javascript/cirepo_javascript_lodash_lodash.json", + "bytes": 4831, + "sha256": "47352be0d04f2c525cc65a9cddc5107b26e3ff11dde2e2aaf7c5e97739bafb3c" + }, + { + "path": "evals/code-intelligence/truth/repositories/kotlin/cirepo_kotlin_android_nowinandroid.json", + "bytes": 7406, + "sha256": "dc4b5089f5671e80bcef72355fc538b35ac5174be90b4fa336bf104bf4694a2a" + }, + { + "path": "evals/code-intelligence/truth/repositories/kotlin/cirepo_kotlin_kotlin_kotlinx_coroutines.json", + "bytes": 5688, + "sha256": "4fef8f3333dec15810924936cbb663fe8a6709587cb299fd5d0870c7a885830c" + }, + { + "path": "evals/code-intelligence/truth/repositories/kotlin/cirepo_kotlin_ktorio_ktor.json", + "bytes": 6083, + "sha256": "9dc5eb134365ef95e776665649ca9c1307466d4b8f45a18c7ca158ce1cdd2011" + }, + { + "path": "evals/code-intelligence/truth/repositories/php/cirepo_php_laravel_framework.json", + "bytes": 5775, + "sha256": "3b257311890480e88c3d0e689bef8ec74bcb35cdd1dc41957cae0eec144f2197" + }, + { + "path": "evals/code-intelligence/truth/repositories/php/cirepo_php_slimphp_slim.json", + "bytes": 5716, + "sha256": "dbbf50f153ef75ba4c2f7c4b9542dc37d461c0e10ca7207d341bab66e6ee59de" + }, + { + "path": "evals/code-intelligence/truth/repositories/php/cirepo_php_symfony_symfony.json", + "bytes": 6015, + "sha256": "1a7de3a4cb04349b24b96b79035f31d2fa451c8694c2a757bc858edee52f1875" + }, + { + "path": "evals/code-intelligence/truth/repositories/python/cicase_python_djangoproject_accounts.json", + "bytes": 1761, + "sha256": "b10430b245bbe981c29e0b47290fb3eb7a60f29c51a325bf8543c99b6355a442" + }, + { + "path": "evals/code-intelligence/truth/repositories/python/cicase_python_fastapi_app_testing.json", + "bytes": 1781, + "sha256": "6c765e1c2c50e7ce8f9f5479dc3d27a6256b8e63042c55a42b8529e0ed06ac70" + }, + { + "path": "evals/code-intelligence/truth/repositories/python/cicase_python_flask_tutorial.json", + "bytes": 1725, + "sha256": "c117b378f4238a567d913efa9a36dc90f35855274b67bf0475d87fffa8895418" + }, + { + "path": "evals/code-intelligence/truth/repositories/python/cirepo_python_fastapi_fastapi.json", + "bytes": 6046, + "sha256": "51107dd3aa0b803564a9010cc4c02494fd6e0fbefb3aecb594e1a19a4721f86c" + }, + { + "path": "evals/code-intelligence/truth/repositories/python/cirepo_python_pallets_flask.json", + "bytes": 5833, + "sha256": "177f11c3549b821f90aaa1dbca598c89dc8bc155b606bbc32c78daaff4ec63e9" + }, + { + "path": "evals/code-intelligence/truth/repositories/python/cirepo_python_psf_requests.json", + "bytes": 6385, + "sha256": "c1683102ce53ae02b1da967a040e528eb3c03cb0957ae830715ff5e0da5ae0fe" + }, + { + "path": "evals/code-intelligence/truth/repositories/ruby/cirepo_ruby_rails_rails.json", + "bytes": 5279, + "sha256": "51bf420cdbf4214656c7b161059ef461c812ad2852860ba0364bcd37208eb2aa" + }, + { + "path": "evals/code-intelligence/truth/repositories/ruby/cirepo_ruby_ruby_rake.json", + "bytes": 4725, + "sha256": "fa37cdf5d0264007cae3ec914737dff4085ed36a42a42f0d29c4bd070b508e75" + }, + { + "path": "evals/code-intelligence/truth/repositories/ruby/cirepo_ruby_sinatra_sinatra.json", + "bytes": 4707, + "sha256": "1f02b84be11dfd362a28cf4b1c1f532197bdc4efa036680eccc4dd37f32cbde3" + }, + { + "path": "evals/code-intelligence/truth/repositories/rust/cirepo_rust_dtolnay_itoa.json", + "bytes": 6398, + "sha256": "c6a88c358e29eb9ac1a6ccd82cb3e7b6ddb440bee763290bcc59cb3352dac83d" + }, + { + "path": "evals/code-intelligence/truth/repositories/rust/cirepo_rust_serde_rs_json.json", + "bytes": 5913, + "sha256": "eef637f38d62d58cc62f5c746b635ace5947f30bfec06cdd57cff52228feda12" + }, + { + "path": "evals/code-intelligence/truth/repositories/rust/cirepo_rust_tokio_rs_axum.json", + "bytes": 7268, + "sha256": "4457c6248a9f5be16178f5858155290b2475ccc443ab36f5004c79987dd0e1ca" + }, + { + "path": "evals/code-intelligence/truth/repositories/swift/cirepo_swift_alamofire_alamofire.json", + "bytes": 3667, + "sha256": "e25d44af2bbd6a3c35ea082a9faf5111f97da84cc47e8e22dff28af37e506baf" + }, + { + "path": "evals/code-intelligence/truth/repositories/swift/cirepo_swift_apple_swift_nio.json", + "bytes": 3874, + "sha256": "943c1aca61544aaa230c7144b6c815bc6dfd4e7d21ac517945295f43c0ddf330" + }, + { + "path": "evals/code-intelligence/truth/repositories/swift/cirepo_swift_vapor_vapor.json", + "bytes": 3564, + "sha256": "03ee8d711649ace440c24a5da68213a43b76632dc1c86aae425fbf0f29b29942" + }, + { + "path": "evals/code-intelligence/truth/repositories/typescript/cirepo_typescript_microsoft_typescript.json", + "bytes": 5633, + "sha256": "d5790697006add791a3850c5a98f34e091e6cca79ae3459322e2d50f39f36561" + }, + { + "path": "evals/code-intelligence/truth/repositories/typescript/cirepo_typescript_microsoft_vscode.json", + "bytes": 4863, + "sha256": "027b8602f753b28ff04d5b6109d37d5d8813490e77d7bfb2ab8e80aaa4125ebf" + }, + { + "path": "evals/code-intelligence/truth/repositories/typescript/cirepo_typescript_vercel_next_js.json", + "bytes": 5609, + "sha256": "8660f49b785874edc516c5e7f975d1cd9cb310d2c194c46310114d7931ef66fd" + }, { "path": "evals/context-recall/oaf-repo-gold.v1.json", - "bytes": 3401, - "sha256": "a98371403340b7938e5fc56e35d8929d5a5420b416bc8c19a724e7a4be4c2b71" + "bytes": 3402, + "sha256": "c6c6dfc3eed2eeeaeccf14623ec08e4f8f6bfdfe388412ccbe5c5b264bef3800" }, { "path": "evals/context-selection/cases.json", @@ -1373,12 +2068,7 @@ { "path": "evals/README.md", "bytes": 4329, - "sha256": "f98c9b361ed781666049a35f128c77adbb4934f1c93c5baca0765c88535ba4a6" - }, - { - "path": "evals/recall-map/architecture-ranking.v1.json", - "bytes": 953, - "sha256": "b5dc1628ab7c6e849052010c97f9263907ebf8c7168b3d7f4f1987beccc5cdff" + "sha256": "f98c9b361ed781666049a35f128c77adbb4934f1c93c5baca0765c88535ba4a6" }, { "path": "evals/sufficiency/gold.v1.json", @@ -1590,6 +2280,71 @@ "bytes": 891, "sha256": "dc8eb1892b400c12d3d688583d58e0bee08b5d2e69370379aef418e988ad7ba6" }, + { + "path": "examples/protocol/code-intelligence-engine-request.json", + "bytes": 452, + "sha256": "896466e02dd67490a0ec0e387b277877b39fc209b4d8016aea083635c63c9aa7" + }, + { + "path": "examples/protocol/code-intelligence-engine-response.json", + "bytes": 2231, + "sha256": "2d20de72a8579e1dd7dc5e1be56039f4c434cae5b1338bd320f8813b9d74db8a" + }, + { + "path": "examples/protocol/code-intelligence-graph.json", + "bytes": 2767, + "sha256": "8547d7e59583d2a3c0c4294076c14a7ba406b4c6edc75ccc2af0f5a1c7af2883" + }, + { + "path": "examples/protocol/code-intelligence-index-build-request.json", + "bytes": 495, + "sha256": "763eea1eb9a6c964f0a53db67dc91d551af2d10be6a285141fc38e89639734b0" + }, + { + "path": "examples/protocol/code-intelligence-index-doctor-request.json", + "bytes": 315, + "sha256": "02ffb6cceeda50dab7041d8ad1b8d53c6807f40c60f14b1d52382cdcf633805d" + }, + { + "path": "examples/protocol/code-intelligence-index-query-request.json", + "bytes": 479, + "sha256": "65a73cf2f7dbce9bfc7ff66e05d050525396f50ec47dfc305b9d0eae8e990d8b" + }, + { + "path": "examples/protocol/code-intelligence-index-reader-response.json", + "bytes": 2418, + "sha256": "8c1b08b3fe9ba74368e1f18fe5b83fa3a4bd4e19e7eac5a45f0f3e6b271eb1ce" + }, + { + "path": "examples/protocol/code-intelligence-index-refresh-request.json", + "bytes": 440, + "sha256": "6f79857eb593931f7cf8b3054f0d2a1d91c186b1c82b33ea1f5c5352a20d2c4e" + }, + { + "path": "examples/protocol/code-intelligence-index-repair-request.json", + "bytes": 653, + "sha256": "88fae5994ff1e4967a984a481cc06d70b22f1f4d0d763cce0ec181307c779ea4" + }, + { + "path": "examples/protocol/code-intelligence-index-status-request.json", + "bytes": 315, + "sha256": "57a660e05ff21540123d7399e41c2d563242b64a3dab3f3262980944541c2796" + }, + { + "path": "examples/protocol/code-intelligence-index-writer-response.json", + "bytes": 1422, + "sha256": "e00aeef77571beec6ee29f5a2848599f144eac6f0bc6e9df1f3753799e117b02" + }, + { + "path": "examples/protocol/code-intelligence-language-report.json", + "bytes": 2810, + "sha256": "d555d3ca26c20830ecae6ff972d554765f39acae2de8adab13c0e2e5bf115a66" + }, + { + "path": "examples/protocol/code-intelligence-language-truth.json", + "bytes": 2096, + "sha256": "c309c4fb9a39c5fdde35b0e4e79b2ae0db37fece250e84ba13caffcdfc9f177b" + }, { "path": "examples/protocol/compatibility/bounded-tool/error-code-valid.json", "bytes": 21, @@ -1677,8 +2432,8 @@ }, { "path": "examples/protocol/compatibility/fixtures.json", - "bytes": 45596, - "sha256": "9a80ea8a22eff4c6d3b3d37218b3c0f68c9903a7a3438fe07f190f2d07fb04f5" + "bytes": 53048, + "sha256": "65d8262577768c906b1caae76dc160d6ef38473b5ceee0cf82d7c74bf7bfb875" }, { "path": "examples/protocol/compatibility/invalid-harness-context-source.json", @@ -1720,6 +2475,76 @@ "bytes": 658, "sha256": "ec11b869dc74eb3757a3f510c1a3bd8294c5af1c0c7b1e02d6ea6859ca919f5e" }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-capability-matrix-false-full.json", + "bytes": 871, + "sha256": "5e0b430dc5f075d4d8e34221b7012a5b4e9c02aa17bfa527ccc12b998f9cbb3c" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-engine-request-absolute-root.json", + "bytes": 361, + "sha256": "a4b9f4fa6ab20107cb2a21fb21c4bb8f53c230c30d7a8b6b3fefd27d7fc30f6c" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-engine-response-raw-error.json", + "bytes": 291, + "sha256": "7c1eb72e7f91a0b137732af3095d07704bb7336fd6b0004f18fb099f4ca631df" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-graph-absolute-path.json", + "bytes": 1158, + "sha256": "a5deebac8709051fb84916c729cb1b30deed2ecff9828616758944c15e90b9f1" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-graph-raw-body.json", + "bytes": 1214, + "sha256": "4d40dbd6764b0f4c32f0553d85e2716dcc33021fb6d8b59e5533330638044576" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-index-absolute-path.json", + "bytes": 329, + "sha256": "6ced62eb9842a095a9ee67ec4f8fab9b5ccdb9cb745a693dbac05016f500643e" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-index-doctor-repair.json", + "bytes": 337, + "sha256": "7dc7b0a7851dfb23ab70d67757554054ef3ddb64472a9398879beb58a9542c58" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-index-raw-sql.json", + "bytes": 394, + "sha256": "0e6833a2ec7892d2a8ed37a54b0637f75086df718ed1893f3ed37abe76e4e868" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-index-status-writer.json", + "bytes": 336, + "sha256": "282c5f8f1362da6397e98465d8071e8a8bcd154772c2f55cdeec05573180ebcf" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-index-unbounded-query.json", + "bytes": 361, + "sha256": "bfdba1c4dc0f58db07931dda787f89b5dce362eef60aaf2b3ac77611623c7366" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-language-truth-absolute-path.json", + "bytes": 1376, + "sha256": "12411bcf7602ffb7cf84d31c63d7f02056e39d37b5715dee59702f57d265dca6" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-language-truth-duplicate-id.json", + "bytes": 1664, + "sha256": "663fa3699183673a5c5a6636fd01b78a43e3ef8608953e79308925920c14d427" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-language-truth-raw-body.json", + "bytes": 1420, + "sha256": "c733b99452b147b77822e711c9ef09eb3477150174fc5edb2d5964b2cd0c80b8" + }, + { + "path": "examples/protocol/compatibility/invalid/code-intelligence-language-truth-unsupported-full.json", + "bytes": 1368, + "sha256": "851cd534400c8c7072a3cf7c5c28fe0ad6d40e87f38789c2be346c4cfe69ec80" + }, { "path": "examples/protocol/compatibility/invalid/context-pack-handoff-report-memory-preflight-leak.json", "bytes": 6631, @@ -1887,8 +2712,8 @@ }, { "path": "examples/protocol/compatibility/invalid/source-graph-local-path.json", - "bytes": 914, - "sha256": "d477221f96e7c4253afa9938e07f0ca75e867bb1c0cee979bb820c18bb4a5a9d" + "bytes": 919, + "sha256": "973960cacdb84e196fd6c50130cfda67c1f6d5b5cf059dfb145afb3ae2960f9b" }, { "path": "examples/protocol/compatibility/invalid/source-graph-preview-double-encoded-locator.json", @@ -1897,8 +2722,8 @@ }, { "path": "examples/protocol/compatibility/invalid/source-graph-preview-local-path.json", - "bytes": 2036, - "sha256": "0650e9b217ce60bacd037961d811b92bafe1d5809ae7cddb4a3451069e220bbb" + "bytes": 2041, + "sha256": "80e030fcb5af063dd907d8d92aacb33069e38324b1387fef8eac322137aeb8ef" }, { "path": "examples/protocol/compatibility/invalid/source-graph-preview-raw-body.json", @@ -2292,8 +3117,8 @@ }, { "path": "examples/protocol/recall-map.json", - "bytes": 2852, - "sha256": "cdaafe7b410e15c349fa149b67fc6b624cca3cc1e3851a41d366762a5904b7f4" + "bytes": 3587, + "sha256": "dfd39493742143504358274b23c0234c1549a34d4f48a6ee9936f8892b05677e" }, { "path": "examples/protocol/research-ingestion-result.json", @@ -2327,13 +3152,13 @@ }, { "path": "examples/protocol/source-graph-preview.json", - "bytes": 4991, - "sha256": "e7a0d65f5fdc5082b1f90044b901b60451d11058986929a03a03402843d4a0d5" + "bytes": 7998, + "sha256": "13c5d3a2050f74e07003e5a521c8ae5744b79e3816f58ffd0f87bee112bdeafa" }, { "path": "examples/protocol/source-graph.json", - "bytes": 6272, - "sha256": "08fd3be564e1869bc5e92f24d1afd206b1ba3e68adf47c4dc550e5f2e843d257" + "bytes": 7276, + "sha256": "7937c7754fe41def7ef7a2c2ea7c96a3dfb935231ba76f9efc4e5860a19d8222" }, { "path": "examples/protocol/source-snapshot.json", @@ -2372,8 +3197,8 @@ }, { "path": "HANDOFF_VERIFICATION.json", - "bytes": 975, - "sha256": "fc3e3c8dcf101d72e70e1a1e062802fce1b7b70cab9c48aacfb3efabc12be7d2" + "bytes": 967, + "sha256": "57de5f619e0331f4b375db356974affe6885546ab7802b07ea51890a1d206113" }, { "path": "LICENSE", @@ -2395,6 +3220,31 @@ "bytes": 498, "sha256": "9284cecc7e960c19025c219450a1fbd4033b32ce0cd285a92385b4468d4ae2ee" }, + { + "path": "native-packages/darwin-arm64/package.json", + "bytes": 559, + "sha256": "3afb629c54c0aae6f23268d3008b0d27bc54ecb67807857267d3ec2143191100" + }, + { + "path": "native-packages/darwin-x64/package.json", + "bytes": 551, + "sha256": "a0518737a0221bab07c8f0d745079f9b01cdb5822f38ea3d4a8104d2c16dbe4b" + }, + { + "path": "native-packages/linux-arm64-gnu/package.json", + "bytes": 589, + "sha256": "e694928c842dea8dde1062b135d12a9140e936087913993cd42535743ad7b930" + }, + { + "path": "native-packages/linux-x64-gnu/package.json", + "bytes": 581, + "sha256": "67ca046d9c13e6563859125e42d3b71d5dbf44e1b1507442d68b059328b55043" + }, + { + "path": "native-packages/win32-x64/package.json", + "bytes": 558, + "sha256": "76db91330ae678df3f44dc2e2f714184b2c495d510ff61b4aa69430b16bc7cbe" + }, { "path": "NOTICE", "bytes": 160, @@ -2402,13 +3252,13 @@ }, { "path": "package-lock.json", - "bytes": 11169, - "sha256": "66bac759b6c28f95c29eca52c0b42e9bb24237a4a926fb67a42093f9ee425ed7" + "bytes": 12582, + "sha256": "5a80cd7fc840b4df82d873ca398a68fef89d05c4f2194c2156b41c44aa6d579e" }, { "path": "package.json", - "bytes": 4476, - "sha256": "0b4ad199b2c9bcb0d4832f3da1747633cee59deb65ec94d3a33f83292bef5dff" + "bytes": 8967, + "sha256": "e5ff8b5bee26f0403a31c2c15b119f4e9b9952db895ccfec9c1da2afd089f4c3" }, { "path": "packages/adapter-contracts/package.json", @@ -2417,8 +3267,8 @@ }, { "path": "packages/adapter-contracts/src/index.mjs", - "bytes": 18745, - "sha256": "98564fc1f45a9784230e4198292e893060cb4feb4cb16e87575e35ffcd67e9cb" + "bytes": 19886, + "sha256": "1c81a602a063e270319e16ebefba6002b801e06f18077706d143a02699ed1a45" }, { "path": "packages/agentpack/AGENTS.md", @@ -2492,8 +3342,8 @@ }, { "path": "packages/harness-context/src/index.mjs", - "bytes": 219824, - "sha256": "b046445715ddada47138c119cf30ee67d015ae5616c8f7ebcacf7f320fde357a" + "bytes": 219812, + "sha256": "df51b9467f528f501304163f4db9ab5a4ad0303d73e60572a766ce1271cae104" }, { "path": "packages/memory-core/package.json", @@ -2577,8 +3427,8 @@ }, { "path": "packages/protocol/README.md", - "bytes": 12048, - "sha256": "f0ec98afb85ee4fbeb501932354def0557b8507b9a7ad951b36549d8686fe45c" + "bytes": 16486, + "sha256": "145f9de2d4268a82afdd89d616da8e07fe7a148d1c87efb4eeb3b79fa2e3b06b" }, { "path": "packages/protocol/schemas/adapter-conformance-fixture.schema.json", @@ -2795,6 +3645,61 @@ "bytes": 3097, "sha256": "8a584b9e6ca896b5783a8d3d52d8875a4d0192dbd943f539a60bfc12f5a41a47" }, + { + "path": "packages/protocol/schemas/code-intelligence-benchmark-manifest.schema.json", + "bytes": 2311, + "sha256": "905defa33739b66e639e03fdf265eb971b978a27836a027401e1bd1297bc31f8" + }, + { + "path": "packages/protocol/schemas/code-intelligence-capability-matrix.schema.json", + "bytes": 4009, + "sha256": "2ef5c04c65d7e20ff277588b37a9d825b42a9b6649bec82244f588cbffaf657a" + }, + { + "path": "packages/protocol/schemas/code-intelligence-engine-request.schema.json", + "bytes": 2294, + "sha256": "6a057470276574ff7120aaf94fe2e6cb4c5d577bc419b92f968a7a2bc10969c5" + }, + { + "path": "packages/protocol/schemas/code-intelligence-engine-response.schema.json", + "bytes": 3472, + "sha256": "a26564fbc471886b2e8c023e036a0c779cfd0c8402db708b8c296b8a7dccfdbc" + }, + { + "path": "packages/protocol/schemas/code-intelligence-graph.schema.json", + "bytes": 9541, + "sha256": "236c661df380f9377e907bf43183966db4170ca401bab3dd9be1586640b7e21a" + }, + { + "path": "packages/protocol/schemas/code-intelligence-index-request.schema.json", + "bytes": 9215, + "sha256": "07f627dcf2afa7e37d608341718ccc4990d1b3fd27d652b3e2d6f958b0ae0b31" + }, + { + "path": "packages/protocol/schemas/code-intelligence-index-response.schema.json", + "bytes": 14603, + "sha256": "cf28ace9a8e9b22ae44bd14785e06c01e568986818d600dab33bf6801aebed2f" + }, + { + "path": "packages/protocol/schemas/code-intelligence-language-report.schema.json", + "bytes": 5669, + "sha256": "92fc9dc8652dbb24442e4de0202d7222b95732c78164d9fad4c36f1d6541bc52" + }, + { + "path": "packages/protocol/schemas/code-intelligence-language-truth.schema.json", + "bytes": 7547, + "sha256": "0b502bc60af696962876467d7bc4ff28c5ab9b64476acf22006ba2cfb2f411c8" + }, + { + "path": "packages/protocol/schemas/code-intelligence-repository-request.schema.json", + "bytes": 8463, + "sha256": "8538f89fe299331d0f1fc3dc83b1fb6b478ef5021a3eb82142de7f9373ac520c" + }, + { + "path": "packages/protocol/schemas/code-intelligence-repository-response.schema.json", + "bytes": 11571, + "sha256": "bee3576a3cfed3a923b3190b061af92ce3c8503e1b6ae494b5944819da3d4e14" + }, { "path": "packages/protocol/schemas/content-local-draft-outcome.schema.json", "bytes": 10020, @@ -3147,13 +4052,13 @@ }, { "path": "packages/protocol/schemas/provider-manifest.schema.json", - "bytes": 1453, - "sha256": "003404f77fd47261c469e1951438727e0e953ec85d9d17e2f213d1ca5fc80453" + "bytes": 1474, + "sha256": "79f3f2ed0a2d7dfbed0507bfa4cc3e9b6da2c35fe8856b168c8bdb57494a495e" }, { "path": "packages/protocol/schemas/recall-map.schema.json", - "bytes": 13170, - "sha256": "6626b9c93a3b446c1db8b24a5344967f9876d60a8087054b9d532e2dff78928f" + "bytes": 18640, + "sha256": "5b1476a7475af8dc74afbdcc394d977124239a8d52f780c3304a2e66d129a2fb" }, { "path": "packages/protocol/schemas/replay-request.schema.json", @@ -3197,13 +4102,13 @@ }, { "path": "packages/protocol/schemas/source-graph-preview.schema.json", - "bytes": 19619, - "sha256": "469a3247a99319f6e13f10ae797cc42700cd2c6f2de5abc94540f8c2bb27ce19" + "bytes": 27438, + "sha256": "62de5358ef640fe4918e56d3a3a841cb4e59cd867a71ab7fe09755413cd71f8a" }, { "path": "packages/protocol/schemas/source-graph.schema.json", - "bytes": 9998, - "sha256": "4624b7fe439c8fb6c41a9c1b6f074f8a319db242df1f158f2a26c5795a71161b" + "bytes": 12413, + "sha256": "3ba52ff8bdd56be530c337d38bf6b9f6cd35922e771d7b2bd5a9a9492a5af776" }, { "path": "packages/protocol/schemas/source-snapshot.schema.json", @@ -3280,6 +4185,21 @@ "bytes": 2140, "sha256": "7ba9ba39a56db0dccfb4e2d2ec7d4bcd1b2f43546bfed2e75ce32e10fe8a18fb" }, + { + "path": "packages/protocol/src/code-intelligence-contract.mjs", + "bytes": 5185, + "sha256": "e0860fa74232b8028f7fb100e26dbadce33a5750ff92a5e8c3771471894a81fb" + }, + { + "path": "packages/protocol/src/code-intelligence-evaluation.mjs", + "bytes": 12133, + "sha256": "6b1aaf63771f1d1f86c8feb42949b485c36a5382e348265d6bfbfbf143973ace" + }, + { + "path": "packages/protocol/src/code-intelligence-index-contract.mjs", + "bytes": 485, + "sha256": "ca7af90986c9ccab3c80c96dfd69f0b2f54b58172f15878c7d14ddb6a74b3cf8" + }, { "path": "packages/protocol/src/fingerprint.mjs", "bytes": 545, @@ -3287,8 +4207,8 @@ }, { "path": "packages/protocol/src/index.mjs", - "bytes": 2710, - "sha256": "5e92e31561443d946a5d0750ed06bf5c2042aa1a853d7d257136170df53d459f" + "bytes": 3261, + "sha256": "0c37d75a77ee352676e3e2433b7e5b3eef7ff57b6a394f23f05e4d4e4d57100a" }, { "path": "packages/protocol/src/schema-validator.mjs", @@ -3297,8 +4217,8 @@ }, { "path": "packages/protocol/src/source-graph-locator.mjs", - "bytes": 3909, - "sha256": "0be7b97d58aa5d871418eb2ecb49ae6235edb35efc9272844fac71815430452b" + "bytes": 3778, + "sha256": "25b32c7497d99485a40039355072dd1d049d6b548c1a7204ffb6cbc0f50c3929" }, { "path": "packages/README.md", @@ -3312,8 +4232,8 @@ }, { "path": "packages/recall-map/src/index.mjs", - "bytes": 19093, - "sha256": "6005d3e3262ae27868ba6ae6684ad3536e4c609d004779b99fb27bbe3ad9cf1a" + "bytes": 24839, + "sha256": "ad619fd72a5e438e6ae0948e0480bf9b1d6cd3687458e28266f752c10fac4b61" }, { "path": "packages/release-readiness/package.json", @@ -3362,8 +4282,13 @@ }, { "path": "packages/source-graph/src/index.mjs", - "bytes": 13169, - "sha256": "69c20de2bab3f0fcc4bf32141457c0a89284bb60a36c57d67cfe7902a270c3b8" + "bytes": 384, + "sha256": "671c6176d272093abb1f5e80bec940f62b0651738e20bec8720f7f7a879200f5" + }, + { + "path": "packages/source-graph/src/native-index-projection.mjs", + "bytes": 44443, + "sha256": "30bd81bcdc79b5596b826516ea02a091ec68a6ae338378845885a4849abac858" }, { "path": "packages/storage/package.json", @@ -3412,8 +4337,8 @@ }, { "path": "packages/ui/tokens.json", - "bytes": 1847, - "sha256": "a62bc9b393cd468d071cdb1a032492195d018281e2e115c0ebef17f5e0bcac86" + "bytes": 1846, + "sha256": "0efd0b96083a059a68907ebc860775e8229f93171f4fbc9d3d09f84438cc8d4d" }, { "path": "packages/workflow-runtime/AGENTS.md", @@ -3447,8 +4372,8 @@ }, { "path": "PROJECT_STATUS.json", - "bytes": 60894, - "sha256": "66a4cea825b1893e0f234f0f8ae71906dae322756cdde7cccfb967a4d44e2a83" + "bytes": 71591, + "sha256": "bf386cad39c718e8d22f08a98ab6fd637b090eda39fc79ec5636ff30c0a46465" }, { "path": "providers/AGENTS.md", @@ -3472,23 +4397,23 @@ }, { "path": "providers/native/catalog.json", - "bytes": 3359, - "sha256": "d91b874543ac6cdfec0968aec317d12df3c6504f24dd3b7ce8a97467c7951fd3" + "bytes": 3135, + "sha256": "f57d1bf3ebe65723b010eca67ee79c9273f0187410c227b86407e8f11c5d3147" }, { - "path": "providers/native/context-candidate-ast-code/package.json", - "bytes": 193, - "sha256": "32e405dd437f2450ee7a54b639a35759e97826ad190df186ee5c45b89d057aff" + "path": "providers/native/code-intelligence-rust/provider.json", + "bytes": 1831, + "sha256": "696c661451f8953e64fb4298db61b1a6b81bde45fff599363f070e0fa5824965" }, { - "path": "providers/native/context-candidate-ast-code/provider.json", - "bytes": 1185, - "sha256": "436a2c8cda4f1e333b79aa9b6838dec4ada2836250da5b9f3bd6388aa4a70af6" + "path": "providers/native/code-intelligence-rust/src/binary-resolver.mjs", + "bytes": 8396, + "sha256": "54c1c4335113ba63c050dadb4d0fe8cd07da517cce117af3590ac96d8ff70ac9" }, { - "path": "providers/native/context-candidate-ast-code/src/index.mjs", - "bytes": 145608, - "sha256": "0124c48325443dd0ef8bb03159531a0793ea80acd2da6f2cfdb84c5daf889753" + "path": "providers/native/code-intelligence-rust/src/index.mjs", + "bytes": 17569, + "sha256": "c6656b59b8a1dddc5187ba24cfd4d17ca8b3cd0289f87d5caaf06cebd139e025" }, { "path": "providers/native/context-candidate-exact/package.json", @@ -3505,11 +4430,6 @@ "bytes": 104, "sha256": "2105f7b65f7220ccfc97be7214205ab3b6579949b0f566f99d4176b7ca968fe3" }, - { - "path": "providers/native/context-candidate-graph/provider.json", - "bytes": 1015, - "sha256": "0aae3a87e35c40323cf5bad7d322289bd91aa06ea03453f34ec3cd8d3d25e52d" - }, { "path": "providers/native/context-candidate-lexical/package.json", "bytes": 192, @@ -3662,8 +4582,8 @@ }, { "path": "README.md", - "bytes": 14946, - "sha256": "032ed4f09e75d866df80450c60f17f83dd772ea4a4d09ead0481156e407cc4e4" + "bytes": 18342, + "sha256": "23c600d0afd3a62747409b29048c485669f67243f3cac30df4870b20707f0d8e" }, { "path": "REPOSITORY_MAP.md", @@ -3672,8 +4592,8 @@ }, { "path": "rfcs/0001-protocol-contracts.md", - "bytes": 4999, - "sha256": "f7967d8858a7e492f3b6a116005cc87c6817bd78ae66218cc9f21071ef041821" + "bytes": 7255, + "sha256": "2ef9b7958ed30313512c94d0e1efd2728b649d9cb9dde70d6dc7897ae6b38495" }, { "path": "ROADMAP.md", @@ -3687,13 +4607,78 @@ }, { "path": "rust/Cargo.lock", - "bytes": 37156, - "sha256": "3493480c19ebb60150243014131d2710c29ef518db9d53292797df6cac9ecd56" + "bytes": 37356, + "sha256": "82f0f29eb376744e9322e7bd37392cf5fbe87db769594a8f664600d28ef36daa" }, { "path": "rust/Cargo.toml", - "bytes": 151, - "sha256": "89e8515f57e367d84898b699f16cb859d254cda3a709f703af60830d2bfd688c" + "bytes": 164, + "sha256": "bf641846d1039b665509e3ded5e28eef8dfb3e694f0e131983111916b7658ff6" + }, + { + "path": "rust/oaf-index/Cargo.toml", + "bytes": 384, + "sha256": "5b4cd10700aa1c8d128a7acdb5c5f30b0a005b14aa9ad5a7777748eb5cb3fc1d" + }, + { + "path": "rust/oaf-index/src/doctor.rs", + "bytes": 11036, + "sha256": "6ec6fdf0093d3043f258185b9ceaf3354968ea21633e3d55589b15ea769e5036" + }, + { + "path": "rust/oaf-index/src/lib.rs", + "bytes": 101853, + "sha256": "a263201cdc0e7e5aee6d68d1e6d6a15d8b51e3239254630900b168cf337f976b" + }, + { + "path": "rust/oaf-index/src/model.rs", + "bytes": 5707, + "sha256": "4d3664f0110bef5d292d778706a4f911c3251d59f206dc3088b62a4732f9bb95" + }, + { + "path": "rust/oaf-index/src/registry.rs", + "bytes": 52587, + "sha256": "7c9c97a85d94463efeeb0e43ab9c2cf3936355babf384f07b4eddf40c7e25a04" + }, + { + "path": "rust/oaf-index/src/watcher.rs", + "bytes": 10528, + "sha256": "66f714dce370e70cdd087237d37ed5b58fa1f9f124bd52d69e312351f6aa4eba" + }, + { + "path": "rust/oaf-index/tests/community_process_projection.rs", + "bytes": 11260, + "sha256": "a9a1594d7c8d8dd4cc69f558faff99dddbea4008f852cedaf9554034cbda557a" + }, + { + "path": "rust/oaf-index/tests/doctor_repair.rs", + "bytes": 10808, + "sha256": "2feea3c218443feba9c6f9483350582e905d5af5762f5bac24f158496f7949b6" + }, + { + "path": "rust/oaf-index/tests/generation_roundtrip.rs", + "bytes": 16976, + "sha256": "8971c979b073288d594b67d7a3831efb5330080510dc4df44d74a791b880c5a2" + }, + { + "path": "rust/oaf-index/tests/incremental_refresh.rs", + "bytes": 15202, + "sha256": "0855401aa66d1b3d7ee09a634b3566cd6ff83d7fdbdae2c9201c52d2016a8300" + }, + { + "path": "rust/oaf-index/tests/repository_registry.rs", + "bytes": 22152, + "sha256": "9944ba4068f7e7c6625d72181703ee11a372d2c042ddcef8257b1c1a6983d696" + }, + { + "path": "rust/oaf-index/tests/store_contract.rs", + "bytes": 6223, + "sha256": "bbd7c8595462e689bc33f3214049377c00d843a8d8def88766ac91e52fbb5226" + }, + { + "path": "rust/oaf-index/tests/watcher.rs", + "bytes": 9610, + "sha256": "c9a31649c661175c7f20fa54f7fae7a762b641f820d9617bf19eb5c024272df7" }, { "path": "rust/oaf-ingest/Cargo.toml", @@ -3702,8 +4687,13 @@ }, { "path": "rust/oaf-ingest/src/lib.rs", - "bytes": 100061, - "sha256": "59b7e5b74ca2d2a201c61945735f094289883fef606b6da43e30c56fc803bbaa" + "bytes": 251579, + "sha256": "49557cd873133111832de7e2172244d1dd989680eb7a25ac7ca4bfa92cf61fb4" + }, + { + "path": "rust/oaf-ingest/test-support/lib_unit.rs", + "bytes": 90971, + "sha256": "0498f1fa68650a2fc0b2287dfd4aa7e01747ee3cbbb20b8d4a781e7bf510de19" }, { "path": "rust/oaf-store/Cargo.toml", @@ -3722,13 +4712,38 @@ }, { "path": "rust/oaf/Cargo.toml", - "bytes": 423, - "sha256": "d701d011db4142967de88c33728bcd285a1e9438f8ef83dd3f68de02c21bf4fe" + "bytes": 556, + "sha256": "e912038c4b9396b4b6043c101c40fe234456c10d24039c5715df30f1e3e9ecdb" + }, + { + "path": "rust/oaf/src/code_intelligence.rs", + "bytes": 57358, + "sha256": "5d5657f0934207b061c581167542149e35974cb676fef9f97f165c1d4d036c7e" + }, + { + "path": "rust/oaf/src/index_protocol.rs", + "bytes": 105534, + "sha256": "9f6c09d3f5fc4ceda4840916279b09e7a232e720147b143c79a4823e86a4faec" }, { "path": "rust/oaf/src/main.rs", - "bytes": 179738, - "sha256": "0857b482770209875102cfb78510eb23af0540db020cab384ad591f6e6fb7379" + "bytes": 180552, + "sha256": "01069883c485ff2208fbeb2961652c1e77547463154cfcf72c92f79e87d405e2" + }, + { + "path": "rust/oaf/src/repository_protocol.rs", + "bytes": 25736, + "sha256": "142625cd31c56c9d84f9324a5c19c362b748637ce55a4f25c3b8dc8bdc01b954" + }, + { + "path": "rust/oaf/test-support/code_intelligence_unit.rs", + "bytes": 71971, + "sha256": "941334ef28b86da8a505f303c76a45743162fece71a4eec6e6a14595d0d233c4" + }, + { + "path": "rust/oaf/test-support/index_protocol_refresh.rs", + "bytes": 8191, + "sha256": "eba76ee61479734a99b6e3474e6c3bbae9b22ac1accc71e407fc9ccd3d115ebe" }, { "path": "rust/README.md", @@ -3755,20 +4770,85 @@ "bytes": 12305, "sha256": "a5cec9490a619d954189f4da3e46bf3d9816a05b96c59b73a0a4cf559bc6972f" }, + { + "path": "scripts/code-intelligence-batch-a.mjs", + "bytes": 13878, + "sha256": "90c1f6c17a7bb36a49be11061a389aeaa3f464866a52c9512b4d5a896994a09d" + }, + { + "path": "scripts/code-intelligence-batch-b.mjs", + "bytes": 1178, + "sha256": "9646e5733bdf1517df2baa3edc653d882897b290dd2a199965aa485ad12919db" + }, + { + "path": "scripts/code-intelligence-batch-c.mjs", + "bytes": 1089, + "sha256": "15529aa3a10f51af81f71f32b028dc64ef6485d5c60a56cf0ef780d3014ef3ad" + }, + { + "path": "scripts/code-intelligence-batch-d.mjs", + "bytes": 892, + "sha256": "f27c017e9d41ebcf1e3c93c54f4fc49364553e6549a8709d538d9f08736a962a" + }, + { + "path": "scripts/code-intelligence-batch-e.mjs", + "bytes": 623, + "sha256": "f14d1f33f7fe297768368c7d32afa92d2dad357cc2c93a845dbc30d99bf40ba3" + }, + { + "path": "scripts/code-intelligence-case-counts.mjs", + "bytes": 184, + "sha256": "4efd6da5f6ec7104360242caad0cb29fbd06341b16fbb0928cbf081bd229d1c1" + }, + { + "path": "scripts/code-intelligence-language-batch.mjs", + "bytes": 13081, + "sha256": "9c68fdcd994d9907ddbb07a6a474effd75a35c3f60b3cc93be539805e5e85ea4" + }, + { + "path": "scripts/code-intelligence-million-node-index.mjs", + "bytes": 31953, + "sha256": "22e59f361f24ebeb5ef9308f9c2887606a1d866f561d0bf36dd17dae0dd83ab7" + }, + { + "path": "scripts/code-intelligence-phase2-tier1.mjs", + "bytes": 19453, + "sha256": "b6177af2cbd4bb18a58e09e22f2c25573ed72dfd45971295668768117ea5f992" + }, + { + "path": "scripts/code-intelligence-phase3-index.mjs", + "bytes": 26708, + "sha256": "03af85ca40b3425369648e2b5d2e18d72d5128e38028528e6203b7b3c6c13b50" + }, + { + "path": "scripts/code-intelligence-phase4-intelligence.mjs", + "bytes": 34886, + "sha256": "60d3ea1be2f3cd88bc4d778aa1d5c63202f2e0f19f2091217986a71b1553e778" + }, + { + "path": "scripts/code-intelligence-phase5-cross-service.mjs", + "bytes": 18823, + "sha256": "ae120817402df2ca7ce9404d5c6044b36cdb629672d56ce103491d9659ab512f" + }, + { + "path": "scripts/code-intelligence-phase8-gitnexus.mjs", + "bytes": 22825, + "sha256": "c07e39cecc0c49fb6bebf981afddd0ce9ef6f117c783a70aa7b7ac27aaf6d1be" + }, { "path": "scripts/consumer-browser-smoke.mjs", - "bytes": 24218, - "sha256": "dc17608ad6af8d0c1afd4670410b6611d68dfb2c47a85917954a23fc5955f6e9" + "bytes": 47555, + "sha256": "5ed2fc88903cb91a5f7f3b6c544465b4ecf81c86ec723c7a73a3d991df78d704" }, { "path": "scripts/consumer-smoke.mjs", - "bytes": 15345, - "sha256": "c1a2e0bdd7b67707249d2dc8b62680c5c879e502b3716eb598cd782e12b1c134" + "bytes": 16190, + "sha256": "3d72170d9712297c8324c610121b7e56f25e5baf261ccfce9a89007a03fce772" }, { "path": "scripts/context-recall-eval.mjs", - "bytes": 24537, - "sha256": "dfc03c9e5821e5ce2fd9a9a1da179577bd8a340e36adaa5789ca8502e0c5d2ad" + "bytes": 21364, + "sha256": "f0c8887d3567dfb6e3429020c39e3c8cb58c776d3deaa1a1e416472fd5d58b42" }, { "path": "scripts/db.mjs", @@ -3790,16 +4870,46 @@ "bytes": 2540, "sha256": "d9404c7ad8de320a796781027355bc30765b883f047623c2b6447b77de547a7d" }, + { + "path": "scripts/large-repository-browser-smoke.mjs", + "bytes": 10692, + "sha256": "62d2085d2d9f04b0ef298140932f20802825091c756153fd7d8131cc33869fa8" + }, + { + "path": "scripts/native-code-intelligence-consumer-smoke.mjs", + "bytes": 57995, + "sha256": "ffb4edea2cd090911f6b2b9a54b9be590c9421179c2b0662282e3e26c0085dd2" + }, + { + "path": "scripts/native-release-set.mjs", + "bytes": 33342, + "sha256": "f2d99279af5fc66b66a5d819c7dc85b6fe16a96534a0f4e0abd4fad8843e84be" + }, { "path": "scripts/native-smoke.mjs", - "bytes": 19730, - "sha256": "8c3606465778a109052eb9ceac13401c5a05efa4e4f582111464341c865e7b6f" + "bytes": 15507, + "sha256": "0bec2d8a1cd5704a90a283691e2f7469eb1f03d8d2006917bdb4ae6c7752fe72" }, { "path": "scripts/operations-smoke.mjs", "bytes": 4473, "sha256": "501743d134f1d4783130f19b7a2aa824b8e7c0140cf767941dba4d565e571f3e" }, + { + "path": "scripts/package-native-platform.mjs", + "bytes": 17284, + "sha256": "5fce96170c62bc336eef4ce2f4785baf784936ef3012c202f22c7ab51d5f11cc" + }, + { + "path": "scripts/pin-code-intelligence-corpus.mjs", + "bytes": 8272, + "sha256": "b626f81a7e8d41a9c5ee3d012185d8f86bbcc4559d6ba310e5f1203bcd08e804" + }, + { + "path": "scripts/pinned-repository-acquisition.mjs", + "bytes": 13495, + "sha256": "4a92d85d79f0c48339f498f1b11ca60d83014c301f16cad2e857b73234656dc2" + }, { "path": "scripts/postgres-recovery-test.mjs", "bytes": 6211, @@ -3822,14 +4932,19 @@ }, { "path": "scripts/run-evals.mjs", - "bytes": 49056, - "sha256": "8d16a746e6abe7c56fd41a72cc7725d6cb4b9012d5255ae6e197746f8cf5f4fc" + "bytes": 44058, + "sha256": "1eb08e1f8e11b8d7fd2ad352cdfca07280bcd3b409e46581ab4c6df596cb703c" }, { "path": "scripts/rust-clone-deadcode-quality.mjs", "bytes": 6671, "sha256": "8acb78f2bdb85e318e1e7f92a226b40051f690b522467b0e712bd3abbb7b4484" }, + { + "path": "scripts/rust-code-intelligence-protocol-quality.mjs", + "bytes": 5995, + "sha256": "cff3826f839b79c2c09d0f7e919e0f74273685fec39b13e40d48754e054e55e2" + }, { "path": "scripts/rust-connectors-quality.mjs", "bytes": 6399, @@ -3882,8 +4997,8 @@ }, { "path": "scripts/rust-ingest-quality.mjs", - "bytes": 35987, - "sha256": "328ae65564a165e5723b3e6e9c08cfbfbbd5a396016c299ce6fc8d5a43725f5b" + "bytes": 33589, + "sha256": "e762338a6d8eaf97d3e048b79d037f57929da7b4ac7ad7d61a84bc2990e17ebe" }, { "path": "scripts/rust-intelligence-quality.mjs", @@ -3922,8 +5037,8 @@ }, { "path": "scripts/rust-typed-calls-quality.mjs", - "bytes": 10539, - "sha256": "bf40113d64e88512f90d6960b347397f0d47452acc60111e3f3636cc7ba0004d" + "bytes": 10013, + "sha256": "b4b7d927322a77d8be1f8a981ac3cd2efb364a7a8fec8e1b68120ce8ac9c6c1e" }, { "path": "scripts/rust-wiki-quality.mjs", @@ -3992,13 +5107,13 @@ }, { "path": "services/control-api/src/route-contracts.mjs", - "bytes": 85191, - "sha256": "d56878cde8db320aa8b8d1000a2ae3be9024964f4d59e70c12ccc4a8a2c57341" + "bytes": 85266, + "sha256": "09cf097d01d089f11285dea13934dce27883c08e96f4b6342cb8300af4179854" }, { "path": "services/control-api/src/server.mjs", - "bytes": 77454, - "sha256": "1a4a8efae64d68668e26295234c6db413f32bc1841da24f4237a5b37ef0c4806" + "bytes": 80081, + "sha256": "08df11a9e58dcbd1478869879368fcf244a9a26726a8d63f409ab193491d151f" }, { "path": "services/README.md", @@ -4167,33 +5282,78 @@ }, { "path": "tests/adapter-contracts.test.mjs", - "bytes": 2095, - "sha256": "cf2f09ebda11b29e165dcfc222db82dc45ca3e877ffec300f18707057c164713" + "bytes": 2753, + "sha256": "9c231aa1845d3607e6a5c58907d50866a8a1cac21d3f3f1b554e7d49deb78662" }, { "path": "tests/agentpack.test.mjs", "bytes": 1888, "sha256": "a8ebe62d1e84da01c2d36caec8385159288912e9ce80b25bc258235dfe495607" }, - { - "path": "tests/ast-code-candidate-source.test.mjs", - "bytes": 34196, - "sha256": "bf6744a69b4f22d20efd59c1de73771c8d16191f7ab1cc08bef80db1f99dbcce" - }, { "path": "tests/benchmark-truth-floor.test.mjs", "bytes": 13663, "sha256": "eda7b42e5a2b1f52e648e886cf0e403eec748b319f134d31a5bb8e9113b234b6" }, + { + "path": "tests/cli-graph-index.test.mjs", + "bytes": 13155, + "sha256": "8c37c757203c9dca2dcd516844bde0edeb2a11f31abfaec4e8d55bb3c8b5b349" + }, { "path": "tests/cli-measure-context-pack.test.mjs", - "bytes": 13781, - "sha256": "fb679543a73e9e212a31791a7a14e18642f3aec581a0c03dd05fbac53f42e34d" + "bytes": 14229, + "sha256": "5fdb79033abea5a0ab0141f34788b9fdd0543dd48bac269ed23709b6b105f36a" }, { "path": "tests/cli.test.mjs", - "bytes": 307634, - "sha256": "308e9a00e6e07b882b14318fc5de57060ce1f1cde71c6115bcee6f32f930b55d" + "bytes": 319614, + "sha256": "3de81c5b79ee239a66b7645b3f7341d1d60111274a72c012111eb880ef423bb0" + }, + { + "path": "tests/code-intelligence-case-counts.test.mjs", + "bytes": 554, + "sha256": "69d2164ddd79699e4c2edf8a2d1af58492527143488c6d6c3d2c589ef15e87a2" + }, + { + "path": "tests/code-intelligence-contract.test.mjs", + "bytes": 6851, + "sha256": "a07964f67f21d70fffc5450ac75a3d91830baf8116e649ea96f5aff0390313df" + }, + { + "path": "tests/code-intelligence-index-contract.test.mjs", + "bytes": 13494, + "sha256": "9afb50237098b28852dad413a5444f015a3c0d882d84e489652324fa2363390d" + }, + { + "path": "tests/code-intelligence-language-evaluation.test.mjs", + "bytes": 10047, + "sha256": "ed315bc21941a9eb6e5b1a5ade6bddfa299d0641c662b69815d09b6bbd04658b" + }, + { + "path": "tests/code-intelligence-million-node-benchmark.test.mjs", + "bytes": 4930, + "sha256": "357c81a747aa3801dfece921a6b987dd18bdaaa80bdaa380f5b499d301254c65" + }, + { + "path": "tests/code-intelligence-phase5-cross-service.test.mjs", + "bytes": 11798, + "sha256": "11e0f70b93fc2e7f0dbf72846f34fce95973e3a745e5eb7d93356da7780a0d50" + }, + { + "path": "tests/code-intelligence-phase5-go-cross-repository.test.mjs", + "bytes": 19646, + "sha256": "4c2c9da24bd0606d64721b4ca75f38e6820c890fc7becc436d4483999fd5669e" + }, + { + "path": "tests/code-intelligence-phase8-gitnexus.test.mjs", + "bytes": 7193, + "sha256": "93881fb2adba73bbfa7e68c883455f6ff34123f29991c081d6ee97f1788f6a0d" + }, + { + "path": "tests/code-intelligence-tier1.test.mjs", + "bytes": 11496, + "sha256": "56a3830303cc03b39da81ac31a5940e895f044d8480300706d3d2d6b634add3b" }, { "path": "tests/content-intelligence.test.mjs", @@ -4227,8 +5387,8 @@ }, { "path": "tests/context-recall-eval.test.mjs", - "bytes": 9204, - "sha256": "de20ffa9e867a890f987c4737498087ab03ab07efed19cfae6c0e608401cac31" + "bytes": 9180, + "sha256": "dc8bb5042f2b7351d3343b0d7ecaf435a057b300d4e25857ea21d911f40b5546" }, { "path": "tests/context-selection.test.mjs", @@ -4247,13 +5407,13 @@ }, { "path": "tests/control-api-boundary.test.mjs", - "bytes": 74195, - "sha256": "77376826e22d0565f5feff2ae4586ae85501d46cc9444e23482c9d90be7b879b" + "bytes": 91527, + "sha256": "5abff660c2343e765637d1bbb166853e6e1e0623ebcd44522bc5e36b4faaf1ec" }, { "path": "tests/control-api.test.mjs", - "bytes": 23693, - "sha256": "e12a3d726c79be95d543f0945e4873ef664f3de59052be33be35403b08b5e086" + "bytes": 24877, + "sha256": "ed08fd832f43fd99d6ba1f96ab3ea17281a1a9ff44b6e08b88def0dc95533134" }, { "path": "tests/docs.test.mjs", @@ -4275,10 +5435,30 @@ "bytes": 7113, "sha256": "f3cf5842666723bacc09669b363fd5753b5393648e1f4d7bbae30901c39a4695" }, + { + "path": "tests/fixtures/code-intelligence-multi-repository/go/go.mod", + "bytes": 59, + "sha256": "c36a0ab75182b5726c937293fd9e06adeec48a64a7ac53e24089a338e8adde0f" + }, + { + "path": "tests/fixtures/code-intelligence-multi-repository/go/shared.go", + "bytes": 130, + "sha256": "998f24e2ed2852d50acdda596e4ab914e9274bd87e5884726eb18d7f3a2c97c8" + }, + { + "path": "tests/fixtures/code-intelligence-multi-repository/python/src/shared.py", + "bytes": 94, + "sha256": "5b03ed02ed4203323eef47e3fe33636d88b8599d240cfb64465ad31c2823c50e" + }, + { + "path": "tests/fixtures/code-intelligence-multi-repository/typescript/src/shared.ts", + "bytes": 141, + "sha256": "330bbc834577685df624b7b08a8f38efe12699b3977deadfa020a61d14d87024" + }, { "path": "tests/fixtures/context-recall-external-baseline.json", - "bytes": 2840, - "sha256": "b4ed3462808dc2f5e6b90f4d77888626a823c6ca8a2be6ee03c0e2f853825e4e" + "bytes": 2828, + "sha256": "3778850d76783b5ffd71c2924af4bb9fbf821b8b116f57816dbf142540472c03" }, { "path": "tests/fixtures/durable-worker-child.mjs", @@ -4312,8 +5492,8 @@ }, { "path": "tests/harness-context-pack.test.mjs", - "bytes": 56008, - "sha256": "e65fd71af0fa05912219bd9493dc3e0220421866c4243c23c290e517fcb606e0" + "bytes": 57966, + "sha256": "cab45998c02affa0f06d08ce7cbcf7d8da822cd8890b474d63a51912bc5a5301" }, { "path": "tests/harness-context-preview.test.mjs", @@ -4330,6 +5510,11 @@ "bytes": 3729, "sha256": "ee81df87cc9f0df4c4df41cc5b7d421791e2c56fe53cb60853202a339f970f3e" }, + { + "path": "tests/mcp-code-intelligence.test.mjs", + "bytes": 28429, + "sha256": "3925475d5cd89aaefb19ddc81dfcfffa474bd9fb31d70da421617ac9d47164d8" + }, { "path": "tests/memory-core.test.mjs", "bytes": 6706, @@ -4342,8 +5527,8 @@ }, { "path": "tests/memory-recall-integrity.test.mjs", - "bytes": 16547, - "sha256": "6252f6eebac43c5cef90178dd6735d7096bac9c10a55d2c3e464ac211eedb1a4" + "bytes": 16978, + "sha256": "9cb66d3c4247ba3608a209e294f11cf92902067d9d984ee93f3ac82359ac38bb" }, { "path": "tests/model-gateway-local-integration.test.mjs", @@ -4360,6 +5545,31 @@ "bytes": 12297, "sha256": "dc5b83cfb42fa58001520c7172efccb295b2722d734f2de308b600cb5ff8669d" }, + { + "path": "tests/native-binary-resolver.test.mjs", + "bytes": 6588, + "sha256": "85c8cb268291c65b36c036d3161976082ce2aaa1d0b6242cf453ecf724ce9fae" + }, + { + "path": "tests/native-code-intelligence-14-languages.test.mjs", + "bytes": 12392, + "sha256": "26da338ffc4847c2d15824d7cb90b2490e1249aecd650c85ee5d1889f65120b9" + }, + { + "path": "tests/native-code-intelligence-multi-repository-search.test.mjs", + "bytes": 9672, + "sha256": "c515b3a0255ee7f856615bd1576a9729d9fea492b90f05c173e2cc5b207ed13d" + }, + { + "path": "tests/native-code-intelligence-provider.test.mjs", + "bytes": 12079, + "sha256": "65af289c4a40c38d2838ed2cc01ff88a3f89e202dff1c955b22399bcd8a0dded" + }, + { + "path": "tests/native-code-intelligence-repository-provider.test.mjs", + "bytes": 15077, + "sha256": "2453b1addf2a51617d749852d7cf01824a16a50c6cbcdfa716e28ea0b7d2af98" + }, { "path": "tests/native-core-local-profile-e2e.test.mjs", "bytes": 6469, @@ -4395,6 +5605,16 @@ "bytes": 3214, "sha256": "2d2d3e4ae4ebc68766eb1d50fb34444334398751512d7b9adf23ed4eb425374b" }, + { + "path": "tests/native-platform-packaging.test.mjs", + "bytes": 7634, + "sha256": "001247911de73382c36d9d3944a27e2b40db18d5bd4baff8c8542a79895c45ae" + }, + { + "path": "tests/native-release-set.test.mjs", + "bytes": 22078, + "sha256": "feb8e1245bec4d676da02af23b791be6c71fd012b4ef4a69a2935a9d6172b955" + }, { "path": "tests/native-workflow-embedded.test.mjs", "bytes": 2428, @@ -4410,6 +5630,11 @@ "bytes": 9783, "sha256": "12abca709267e72605b37cc3543380190c68bfdacb41575337039478770e0f49" }, + { + "path": "tests/pinned-repository-acquisition.test.mjs", + "bytes": 3793, + "sha256": "d5f136a6f44e8f509dd757603d5b90ea6dcbb3d802b98b9101b171d38fd33f69" + }, { "path": "tests/planning.test.mjs", "bytes": 4904, @@ -4450,20 +5675,20 @@ "bytes": 2514, "sha256": "b8691b69e21208e3d9fb49d7a2e70d4091beaa46373c459a16921ca2672b00e5" }, - { - "path": "tests/recall-map-ranking.test.mjs", - "bytes": 27311, - "sha256": "0412de5c334debd2e859220f14ae10bba8afd48197d093e1d3dc5ecc0f9afa92" - }, { "path": "tests/recall-map.test.mjs", - "bytes": 17197, - "sha256": "fe1e3a731bdea70805ebfb77193816d6d16475813362b30d64c0b30eafd69999" + "bytes": 23148, + "sha256": "ab2397c294231d794c8336c8a6cd8612f3c0e29c2227d453088c8934c060c319" }, { "path": "tests/release-readiness.test.mjs", - "bytes": 21729, - "sha256": "56124e792cccb107c8528e50342dd81106235944828e17f37aecc4b270559af7" + "bytes": 23180, + "sha256": "e83ce1bf2c7084aaf2bca6311a73be2f41a99d4d122844c4acd18924cb3147ed" + }, + { + "path": "tests/release-regressions.test.mjs", + "bytes": 7715, + "sha256": "abfb3aaab0021d7a5d46b5afe0e87166ec5ed3eee597f205978521d29f5d64a5" }, { "path": "tests/replay.test.mjs", @@ -4487,13 +5712,8 @@ }, { "path": "tests/skill-catalog.test.mjs", - "bytes": 30945, - "sha256": "6ea779227d1dce5c84c55812cc0834df8c5ce8a969aaf736d6313387ae9eae03" - }, - { - "path": "tests/source-graph-preview.test.mjs", - "bytes": 82027, - "sha256": "b7469c2e3ae26345950968c678e5e4484ee4fe9443765e288028dd85bc89b92d" + "bytes": 30946, + "sha256": "64ec4c506e92b8f228c4954cf051f4dedc6b23dac66843fba768adf4c4f9246b" }, { "path": "tests/tool-durable-integration.test.mjs", @@ -4527,13 +5747,28 @@ }, { "path": "tests/usage-docs.test.mjs", - "bytes": 17606, - "sha256": "885efd11e5c90f3c41e2c45d452e802176f4b7523eb71756f8e05a674a7c6f1f" + "bytes": 18846, + "sha256": "aa9a19fd64a8c2d24f301c1d43fef9f91ca0515bd89f5740541b9718f4b2ac37" + }, + { + "path": "tests/web-memory-graph.test.mjs", + "bytes": 5701, + "sha256": "486feed038e139cb98c38bf02be6287094a687212fde8b2a60f3628bc2b2ce37" + }, + { + "path": "tests/web-orientation.test.mjs", + "bytes": 10109, + "sha256": "26d5ec037466cd1a49a7582b578ed7706b7404d4edb2e8b9202761f478c5064b" }, { "path": "tests/web-shell.test.mjs", - "bytes": 99856, - "sha256": "3749602b34492de746d712ae0fe56fe88da56c4ff8a088cc8ebe45d111e46fd5" + "bytes": 103555, + "sha256": "647ff1ef1611bcff8b685ee3a42a296304844c0ba236e48ca1540dbec74aadd1" + }, + { + "path": "tests/web-source-map.test.mjs", + "bytes": 11630, + "sha256": "d49e4c040f5351ee4acb16164adc8d37a983e969ec5667cb0657c83ad75463d9" }, { "path": "tests/workflow-durable-crash.test.mjs", diff --git a/apps/cli/help.mjs b/apps/cli/help.mjs index 7f74275c..a4057227 100644 --- a/apps/cli/help.mjs +++ b/apps/cli/help.mjs @@ -11,7 +11,7 @@ function renderHelpText(text) { if (command === 'oaf') return text; return text .replace( - /\boaf (?=(status|setup|verify|doctor|connect|disconnect|task|demo|serve|check|eval|manifest|map|semantic|handoff|token-saver|context|loop|skill|measure|benchmark|bench|memory|mcp|harness|hook|version)\b)/g, + /\boaf (?=(status|setup|verify|doctor|connect|disconnect|task|demo|serve|check|eval|manifest|map|semantic|handoff|token-saver|context|graph|loop|skill|measure|benchmark|bench|memory|mcp|harness|hook|version)\b)/g, `${command} ` ); } @@ -65,9 +65,17 @@ Usage: oaf context registry status --read-only --format json oaf context graph preview --root . --query "approve token reset" --trace runAuthWorkflow --changed src/auth.ts --changed-from-git --dry-run --format summary oaf graph stats --root . --format summary + oaf graph stats --root . --engine native --format summary + oaf graph search --root . --query "main" --engine native --format json oaf graph search --root . --query "route registration hooks" --format summary oaf graph trace --root . --symbol runAuthWorkflow --direction outbound --format summary oaf graph impact --root . --changed src/auth.ts --format summary + oaf graph index --status --root . --format summary + oaf graph index --write --root . --format json + oaf graph index --refresh --watch --root . --format summary + oaf graph index --write --engine native --root . --format summary + oaf graph index --query main --engine native --root . --format json + oaf graph index --doctor --engine native --root . --format summary oaf loop plan --read-only --root . --objective "Ship safely" --stop-condition "focused tests pass" --validation "node --test tests/web-shell.test.mjs" --format json oaf loop observe --root . --plan loop-plan.json --execute-commands --format json oaf loop verify --root . --plan loop-plan.json --worktree ../isolated-worktree --sqlite .local/memory.sqlite --execute-commands --format json @@ -118,8 +126,10 @@ Usage: oaf mcp smoke context-pack --read-only --objective "Ship safely" --step "handoff" --target codex --changed src/auth.ts --changed-from-git --format json oaf mcp resources --read-only --stdio oaf mcp server --read-only --root . --stdio + oaf mcp server --read-only --engine native --root . --stdio oaf mcp stats --read-only --root . --format json oaf mcp install --client claude-code --dry-run --format json + oaf mcp uninstall --client claude-code --dry-run --format json oaf harness setup status --client codex --dry-run --format json oaf harness setup plan --client cursor --server oaf --dry-run --format json oaf harness setup uninstall --client cursor --server oaf --dry-run --format json @@ -162,10 +172,10 @@ Options: --sqlite Local SQLite memory store; defaults to .local/memory.sqlite. --changed Add a reviewed changed workspace path; repeatable. --changed-from-git Detect changed paths with local git only. - --query Search the bounded JS/TS source graph. + --query Search the current bounded native source index. --format Emit the full safe report or a compact rendering. -Builds a bounded local repository map from the implemented JS/TS static graph +Builds a bounded local repository map from the current native SQLite source index and the governed local SQLite memory store. It does not write files, call models, use network access, enable external adapters, or expose raw source bodies.`], @@ -240,11 +250,28 @@ Usage: oaf graph trace --root . --symbol runAuthWorkflow --direction outbound --format summary oaf graph impact --root . --changed src/auth.ts --format summary oaf graph impact --root . --changed-from-git --format json - -Graph commands build a bounded local JS/TS source graph and return locator-only -stats, search, trace, or changed-file impact reports. They are read-only by default: -no files are written, no model calls are made, no network calls are made, and -raw source bodies are not included.`], + oaf graph index --status --root . --format summary + oaf graph index --write --root . --format json + oaf graph index --refresh --root . --format json + oaf graph index --refresh --watch --root . --format summary + oaf graph index --write --engine native --root . --format summary + oaf graph index --refresh --engine native --root . --format summary + oaf graph index --query main --kind exact --engine native --root . --format json + oaf graph index --doctor --engine native --root . --format summary + oaf graph index --repair --confirm --engine native --root . --format summary + +Options for read commands: + --engine Use the packaged native engine. The other spellings are strict native aliases. + +Graph read commands return bounded locator-only stats, search, trace, or +changed-file impact reports. Native is the default and requires the verified +platform package plus a current local SQLite index. Missing or stale native +state returns the exact build, refresh, repair, or package action. The former +auto and native-preview spellings remain strict native aliases. Index writes +are explicit. Native stores its versioned SQLite index under .local/source-index. +It stores metadata only; raw source bodies are not included. +MCP reads the index but never builds or refreshes it. +No graph command makes model or network calls.`], ['context handoff', `Memory Recall CLI: context handoff Usage: @@ -369,15 +396,24 @@ Usage: oaf mcp resources --read-only --memory-refine --uri oaf://workspace/ws_local/memory/refine --format summary oaf mcp resources --read-only --context-pack --objective "Ship safely" --step "handoff" --target codex --changed src/auth.ts --format json oaf mcp server --read-only --root . --stdio + oaf mcp server --read-only --engine native --root . --stdio oaf mcp stats --read-only --root . --format json oaf mcp smoke context-pack --read-only --objective "Ship safely" --step "handoff" --target codex --changed src/auth.ts --format json oaf mcp install --client claude-code --dry-run --format json + oaf mcp uninstall --client claude-code --dry-run --format json MCP commands inspect or expose local read-only resources, run the stdio bridge, preview install plans, or report delivery stats. Resource summaries require --uri and do not dump full resource bodies. Resource/server paths require ---read-only; install remains dry-run unless explicitly confirmed by the install -flow.`], +--read-only; install and uninstall remain dry-run unless explicitly confirmed. +Uninstall removes only an exact Memory Recall-owned entry and preserves .local. +Direct MCP server commands and mcp install default to native mode and print +indexBuildCommand; install never builds or +refreshes an index. Run the explicit writer when wanted: + oaf graph index --write --engine native --root . --format summary +Native mode reads only a current, healthy prebuilt .local/source-index database +and returns an actionable error otherwise. It never builds, refreshes, or falls +back to a source scan.`], ['memory refine', `Memory Recall CLI: memory refine Usage: diff --git a/apps/cli/oaf.mjs b/apps/cli/oaf.mjs index d39eb0e5..ae154b05 100755 --- a/apps/cli/oaf.mjs +++ b/apps/cli/oaf.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { createHash, randomUUID } from 'node:crypto'; import { execFileSync, spawn } from 'node:child_process'; -import { constants as fsConstants, createReadStream, existsSync, realpathSync } from 'node:fs'; +import { constants as fsConstants, createReadStream, existsSync, realpathSync, watch as watchFs } from 'node:fs'; import { appendFile, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename as renameFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -42,7 +42,7 @@ import { evaluateMemoryWrite, normalizeMemoryPathsConfig } from '../../packages/memory-core/src/index.mjs'; -import { buildRecallMap } from '../../packages/recall-map/src/index.mjs'; +import { buildRecallMapMemorySummary } from '../../packages/recall-map/src/index.mjs'; import { assertSemanticProposalSourcesCurrent, assertSemanticSourceBindingsCurrent, @@ -57,8 +57,10 @@ import { } from '../../packages/semantic-setup/src/index.mjs'; import { assertSafeContextPackUsePlanForResource, buildOafReadOnlyResourceCatalog, createMcpBridge } from '../../packages/protocol-bridges/src/index.mjs'; import contextPackUsePlanSchema from '../../packages/protocol/schemas/context-pack-use-plan.schema.json' with { type: 'json' }; +import contextPackSchema from '../../packages/protocol/schemas/context-pack.schema.json' with { type: 'json' }; import contextPackHandoffReportSchema from '../../packages/protocol/schemas/context-pack-handoff-report.schema.json' with { type: 'json' }; import contextPackMeasurementReportSchema from '../../packages/protocol/schemas/context-pack-measurement-report.schema.json' with { type: 'json' }; +import codeIntelligenceGraphSchema from '../../packages/protocol/schemas/code-intelligence-graph.schema.json' with { type: 'json' }; import mcpContextPackSmokeSchema from '../../packages/protocol/schemas/mcp-context-pack-smoke.schema.json' with { type: 'json' }; import memoryRefineReportSchema from '../../packages/protocol/schemas/memory-refine-report.schema.json' with { type: 'json' }; import semanticSetupReportSchema from '../../packages/protocol/schemas/semantic-setup-report.schema.json' with { type: 'json' }; @@ -72,7 +74,13 @@ import { loadReviewedToolCatalog } from '../../packages/tool-registry/src/index. import { DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILES, DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILE_BYTES, - buildSourceGraphPreview + buildNativeIndexArchitecture, + buildNativeIndexSourceGraphPreview, + buildUnavailableSourceGraphPreview, + nativeIndexReadyForAutomaticRead, + nativeIndexSource, + nativeStructuralNode, + nativeStructuralRelationship } from '../../packages/source-graph/src/index.mjs'; const CLI_PATH = fileURLToPath(import.meta.url); @@ -86,6 +94,8 @@ const MCP_STDIO_MAX_MESSAGES = boundedEnvInteger('OAF_MCP_STDIO_MAX_MESSAGES', 1 const MCP_STDIO_CHILD_TIMEOUT_MS = boundedEnvInteger('OAF_MCP_STDIO_CHILD_TIMEOUT_MS', 30_000, { min: 1, max: 60_000 }); const MCP_STDIO_CHILD_MAX_STDOUT_BYTES = boundedEnvInteger('OAF_MCP_STDIO_CHILD_MAX_STDOUT_BYTES', 512 * 1024, { min: 1, max: 2_000_000 }); const MCP_STDIO_CHILD_MAX_STDERR_BYTES = boundedEnvInteger('OAF_MCP_STDIO_CHILD_MAX_STDERR_BYTES', 64 * 1024, { min: 1, max: 512 * 1024 }); +const MCP_CONTEXT_PACK_AUX_MAX_BYTES = 2_000_000; +const CODE_INTELLIGENCE_EDGE_KINDS = Object.freeze([...codeIntelligenceGraphSchema.$defs.edge.properties.kind.enum]); const MEMORY_PATH_MAX_BYTES = 8 * 1024 * 1024; const SEMANTIC_HARNESSES = new Set(['codex', 'claude-code', 'cursor', 'generic']); const SEMANTIC_PROVIDERS = new Set(['gemini', 'openai-compatible']); @@ -93,8 +103,10 @@ const SEMANTIC_RESULT_MAX_BYTES = 256 * 1024; const SECRET_LIKE = /\b(?:authorization\s*[:=]\s*(?:"[^"\r\n]*"|'[^'\r\n]*'|(?:Bearer|Basic|Digest|Token)\s+[^\s"'`,;)]+|[^\s"'`,;)]+)|(?:api[_-]?key|token|secret|password)\s*[:=]\s*(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s"'`,;)]+))/iu; const PRIVATE_LOCAL_PATH = /(?:^|[\s"'`(])(?:\/Users(?:\/|$)|\/home\/[A-Za-z0-9._-]+(?:\/|$)|\/private(?:\/|$)|\/var\/folders(?:\/|$)|[A-Za-z]:\\)/u; const AUTO_DETECTED_SECRET_PATH = /(^|\/)(?:\.env(?:[./_-]|$)|secrets?(?:[./_-]|$)|credentials?(?:[./_-]|$)|id_rsa(?:[./_-]|$)|id_ed25519(?:[./_-]|$)|[^/]+\.(?:pem|key|p12|pfx|crt|cert)$)/iu; -const MCP_PRIVATE_MATERIAL = /(?:\/Users(?:\/|$)[^\s"',;]*|\/home\/[A-Za-z0-9._-]+(?:\/|$)[^\s"',;]*|[A-Za-z]:\\[^\s"',;]*|sk-[A-Za-z0-9_-]{12,}|OPENAI_API_KEY|AKIA[0-9A-Z]{16}|gh[opsu]_[A-Za-z0-9_]{12,}|(?:token|secret|password|api[_-]?key)\s*[=:]\s*[^\s"',;]+)/iu; -const MCP_PRIVATE_MATERIAL_GLOBAL = /(?:\/Users(?:\/|$)[^\s"',;]*|\/home\/[A-Za-z0-9._-]+(?:\/|$)[^\s"',;]*|[A-Za-z]:\\[^\s"',;]*|sk-[A-Za-z0-9_-]{12,}|OPENAI_API_KEY|AKIA[0-9A-Z]{16}|gh[opsu]_[A-Za-z0-9_]{12,}|(?:token|secret|password|api[_-]?key)\s*[=:]\s*[^\s"',;]+)/giu; +const MCP_PRIVATE_PATH = /(?:^|[\s"'`(])(?:\/Users(?:\/|$)[^\s"',;]*|\/home\/[A-Za-z0-9._-]+(?:\/|$)[^\s"',;]*|[A-Za-z]:\\[^\s"',;]*)/u; +const MCP_PRIVATE_PATH_GLOBAL = /(?:^|[\s"'`(])(?:\/Users(?:\/|$)[^\s"',;]*|\/home\/[A-Za-z0-9._-]+(?:\/|$)[^\s"',;]*|[A-Za-z]:\\[^\s"',;]*)/gu; +const MCP_SECRET_MATERIAL = /(?:sk-[A-Za-z0-9_-]{12,}|OPENAI_API_KEY|AKIA[0-9A-Z]{16}|gh[opsu]_[A-Za-z0-9_]{12,}|(?:token|secret|password|api[_-]?key)\s*[=:]\s*[^\s"',;]+)/iu; +const MCP_SECRET_MATERIAL_GLOBAL = /(?:sk-[A-Za-z0-9_-]{12,}|OPENAI_API_KEY|AKIA[0-9A-Z]{16}|gh[opsu]_[A-Za-z0-9_]{12,}|(?:token|secret|password|api[_-]?key)\s*[=:]\s*[^\s"',;]+)/giu; const MEMORY_BATCH_UNSAFE_TEXT = /(?:^|[\s('"`])\/(?:[A-Za-z0-9._-]+\/)+[^\s)'"<>]+|file:\/\/|[A-Za-z]:\\|\n|\r/iu; const MEMORY_BATCH_CONFIDENCES = new Set(['extracted', 'inferred', 'ambiguous']); const REALQA_QUERY_STOPWORDS = new Set(['what', 'which', 'who', 'where', 'when', 'why', 'how', 'is', 'the', 'a', 'an', 'by', 'does', 'do', 'for', 'to', 'of', 'provider', 'default', 'implements']); @@ -1871,21 +1883,21 @@ async function normalizeMemoryBatchFact(root, input) { function safeMemoryBatchToken(value, name) { const text = String(value ?? '').trim(); if (!/^[A-Za-z0-9:_-]{1,128}$/u.test(text)) throw new Error(`${name} must be safe`); - if (MCP_PRIVATE_MATERIAL.test(text) || MEMORY_BATCH_UNSAFE_TEXT.test(text)) throw new Error(`${name} must be safe`); + if (mcpContainsPrivateMaterial(text) || MEMORY_BATCH_UNSAFE_TEXT.test(text)) throw new Error(`${name} must be safe`); return text; } function safeMemoryBatchObject(value) { const text = String(value ?? '').trim().replace(/[.;:,]+$/u, '').trim(); if (!/^[A-Za-z0-9][A-Za-z0-9:_./ =,;()'-]{0,239}$/u.test(text)) throw new Error('object must be safe'); - if (MCP_PRIVATE_MATERIAL.test(text) || MEMORY_BATCH_UNSAFE_TEXT.test(text)) throw new Error('object must be safe'); + if (mcpContainsPrivateMaterial(text) || MEMORY_BATCH_UNSAFE_TEXT.test(text)) throw new Error('object must be safe'); return text; } function safeMemoryBatchNotes(value) { const text = String(value ?? '').trim(); if (!text || text.length > 500) throw new Error('notes must be safe'); - if (/[\n\r]/u.test(text) || MCP_PRIVATE_MATERIAL.test(text) || MEMORY_BATCH_UNSAFE_TEXT.test(text)) throw new Error('notes must be safe'); + if (/[\n\r]/u.test(text) || mcpContainsPrivateMaterial(text) || MEMORY_BATCH_UNSAFE_TEXT.test(text)) throw new Error('notes must be safe'); return text; } @@ -2874,10 +2886,375 @@ async function graphCommand(values) { if (subcommand === 'search') return graphSearchCommand(rest); if (subcommand === 'trace') return graphTraceCommand(rest); if (subcommand === 'impact') return graphImpactCommand(rest); - console.error('graph requires stats, search, trace, or impact'); + if (subcommand === 'index') return graphIndexCommand(rest); + if (subcommand === 'repositories') return graphRepositoriesCommand(rest); + console.error('graph requires stats, search, trace, impact, index, or repositories'); process.exitCode = 2; } +async function graphRepositoriesCommand(values) { + const action = values[0]; + const args = values.slice(1); + const actionOptions = { + register: { + allowed: new Set(['--write', '--root', '--repository', '--name', '--workspace', '--format']), + valued: new Set(['--root', '--repository', '--name', '--workspace', '--format']) + }, + list: { + allowed: new Set(['--read-only', '--root', '--workspace', '--format', '--limit']), + valued: new Set(['--root', '--workspace', '--format', '--limit']) + }, + search: { + allowed: new Set(['--read-only', '--root', '--workspace', '--format', '--query', '--repository-ids', '--per-repository-limit', '--limit']), + valued: new Set(['--root', '--workspace', '--format', '--query', '--repository-ids', '--per-repository-limit', '--limit']) + } + }[action]; + if (!actionOptions) { + console.error('graph repositories requires register, list, or search'); + process.exitCode = 2; + return; + } + if ( + unsupportedFlags(args, actionOptions.allowed, actionOptions.valued).length + || firstPositional(args, actionOptions.valued) + || [...actionOptions.valued].some((name) => args.includes(name) && missingOptionValue(args, name)) + ) { + console.error(`graph repositories ${action} options are invalid`); + process.exitCode = 2; + return; + } + const root = option(args, '--root'); + if (!root || root.startsWith('--')) { + console.error(`graph repositories ${action} requires --root `); + process.exitCode = 2; + return; + } + const format = option(args, '--format') ?? 'summary'; + if (!['json', 'summary'].includes(format)) { + console.error('graph repositories only supports --format json or summary'); + process.exitCode = 2; + return; + } + const workspaceId = option(args, '--workspace') ?? 'ws_local'; + if (!/^[a-z][a-z0-9_-]{0,127}$/u.test(workspaceId)) { + console.error('graph repositories workspace is invalid'); + process.exitCode = 2; + return; + } + if (action === 'register' ? !args.includes('--write') : !args.includes('--read-only')) { + console.error(`graph repositories ${action} requires ${action === 'register' ? '--write' : '--read-only'}`); + process.exitCode = 2; + return; + } + try { + const { RustCodeIntelligenceProvider } = await import('../../providers/native/code-intelligence-rust/src/index.mjs'); + const provider = new RustCodeIntelligenceProvider(); + let result; + if (action === 'register') { + const repository = option(args, '--repository'); + const displayName = option(args, '--name'); + const segments = repository?.split('/') ?? []; + if ( + !repository + || path.isAbsolute(repository) + || repository.includes('\\') + || segments.some((segment) => !segment || segment === '.' || segment === '..' || !/^[A-Za-z0-9._@+-]+$/u.test(segment)) + ) { + throw new Error('graph repositories register repository is invalid'); + } + if (!displayName || !/^[A-Za-z0-9][A-Za-z0-9._ -]{0,79}$/u.test(displayName)) { + throw new Error('graph repositories register name is invalid'); + } + result = await provider.registerRepository({ + root, + workspaceId, + displayName, + rootLocator: `workspace://${repository}` + }); + } else if (action === 'list') { + if (option(args, '--limit') === null) throw new Error('graph repositories list requires --limit'); + const limit = strictIntegerOption(args, '--limit', 64); + if (limit < 1 || limit > 64) throw new Error('graph repositories list limit must be between 1 and 64'); + result = await provider.listRepositories({ root, workspaceId, limit }); + } else { + const query = option(args, '--query'); + const repositoryIds = (option(args, '--repository-ids') ?? '').split(',').map((value) => value.trim()).filter(Boolean); + if (option(args, '--per-repository-limit') === null || option(args, '--limit') === null) { + throw new Error('graph repositories search requires --per-repository-limit and --limit'); + } + const perRepositoryLimit = strictIntegerOption(args, '--per-repository-limit', 25); + const limit = strictIntegerOption(args, '--limit', 50); + if (!query || !/^[A-Za-z0-9_.$:/#@ -]{1,160}$/u.test(query)) throw new Error('graph repositories search query is invalid'); + if ( + repositoryIds.length < 1 + || repositoryIds.length > 8 + || new Set(repositoryIds).size !== repositoryIds.length + || repositoryIds.some((repositoryId) => !/^repo_[a-f0-9]{32}$/u.test(repositoryId)) + ) { + throw new Error('graph repositories search repository ids are invalid'); + } + if (perRepositoryLimit < 1 || perRepositoryLimit > 25) { + throw new Error('graph repositories search per-repository limit must be between 1 and 25'); + } + if (limit < 1 || limit > 50) throw new Error('graph repositories search limit must be between 1 and 50'); + result = await provider.searchRepositories({ root, workspaceId, query, repositoryIds, perRepositoryLimit, limit }); + } + const report = compactNativeGraphRepositoriesReport(result); + console.log(format === 'json' ? JSON.stringify(report, null, 2) : renderGraphRepositoriesSummary(report)); + } catch (error) { + console.error(strictNativeReadMessage(error)); + process.exitCode = 2; + } +} + +async function graphIndexCommand(values) { + const modes = ['--status', '--write', '--refresh', '--doctor', '--repair', '--query'].filter((flag) => values.includes(flag)); + if (modes.length !== 1) { + console.error('graph index requires --status, --write, --refresh, --doctor, --repair, or --query'); + process.exitCode = 2; + return; + } + const watch = values.includes('--watch'); + if (watch && modes[0] !== '--refresh') { + console.error('graph index --watch requires --refresh'); + process.exitCode = 2; + return; + } + const allowedFlags = new Set(['--status', '--write', '--refresh', '--doctor', '--repair', '--query', '--watch', '--root', '--workspace', '--out', '--format', '--engine', '--confirm', '--kind', '--locator', '--direction', '--depth', '--limit', '--cursor', '--max-files', '--max-file-bytes', '--max-nodes', '--max-edges', '--languages']); + const valueFlags = new Set(['--query', '--root', '--workspace', '--out', '--format', '--engine', '--confirm', '--kind', '--locator', '--direction', '--depth', '--limit', '--cursor', '--max-files', '--max-file-bytes', '--max-nodes', '--max-edges', '--languages']); + const unsupported = unsupportedFlags(values, allowedFlags, valueFlags); + if (unsupported.length) { + console.error('graph index options are invalid'); + process.exitCode = 2; + return; + } + const format = option(values, '--format') ?? 'summary'; + if (!['json', 'summary'].includes(format)) { + console.error('graph index only supports --format json or summary'); + process.exitCode = 2; + return; + } + const root = option(values, '--root') ?? process.cwd(); + const workspaceId = option(values, '--workspace') ?? 'ws_local'; + const requestedEngine = option(values, '--engine') ?? 'native'; + if (!['auto', 'native', 'native-preview'].includes(requestedEngine)) { + console.error('graph index --engine must be native, native-preview, or auto'); + process.exitCode = 2; + return; + } + const invalidForMode = invalidGraphIndexModeOption(values, modes[0]); + if (invalidForMode) { + console.error(`graph index ${modes[0]} does not accept ${invalidForMode}`); + process.exitCode = 2; + return; + } + if (option(values, '--out')) { + console.error('native source index uses the fixed workspace-local index path'); + process.exitCode = 2; + return; + } + return nativeGraphIndexCommand(values, { mode: modes[0], root, workspaceId, format, watch }); +} + +async function nativeGraphIndexCommand(values, { mode, root, workspaceId, format, watch }) { + if (mode === '--repair' && missingOptionValue(values, '--confirm')) { + console.error('graph index --repair requires --confirm '); + process.exitCode = 2; + return; + } + if (mode === '--query' && missingOptionValue(values, '--query')) { + console.error('graph index --query requires a query value'); + process.exitCode = 2; + return; + } + try { + const languagesValue = option(values, '--languages'); + const common = { + root, + workspaceId, + maxFiles: strictIntegerOption(values, '--max-files', DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILES), + maxFileBytes: strictIntegerOption(values, '--max-file-bytes', DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILE_BYTES), + maxNodes: strictIntegerOption(values, '--max-nodes', 5000), + maxEdges: strictIntegerOption(values, '--max-edges', 10000), + ...(languagesValue === null ? {} : { languages: languagesValue.split(',').map((value) => value.trim()).filter(Boolean) }) + }; + const { RustCodeIntelligenceProvider } = await import('../../providers/native/code-intelligence-rust/src/index.mjs'); + const provider = new RustCodeIntelligenceProvider(); + const execute = async () => { + let result; + if (mode === '--status') result = await provider.indexStatus(common); + else if (mode === '--write') result = await provider.buildIndex(common); + else if (mode === '--refresh') result = await provider.refreshIndex(common); + else if (mode === '--doctor') result = await provider.doctorIndex(common); + else if (mode === '--repair') result = await provider.repairIndex({ ...common, confirmRepairPlan: option(values, '--confirm') }); + else { + result = await provider.queryIndex({ + ...common, + kind: option(values, '--kind') ?? 'exact', + query: option(values, '--query'), + ...(option(values, '--locator') === null ? {} : { locator: option(values, '--locator') }), + ...(option(values, '--direction') === null ? {} : { direction: option(values, '--direction') }), + ...(option(values, '--depth') === null ? {} : { depth: strictIntegerOption(values, '--depth', 1) }), + ...(option(values, '--cursor') === null ? {} : { cursor: option(values, '--cursor') }), + limit: strictIntegerOption(values, '--limit', 25) + }); + } + const report = compactNativeGraphIndexReport(result); + console.log(format === 'json' ? JSON.stringify(report, null, 2) : renderGraphIndexSummary(report)); + return report; + }; + await execute(); + if (watch) await watchGraphIndex({ root: path.resolve(root), execute, engine: 'native' }); + } catch (error) { + console.error(error?.code ?? error.message); + process.exitCode = 2; + } +} + +function invalidGraphIndexModeOption(values, mode) { + const base = new Set([mode, '--root', '--workspace', '--format', '--engine']); + const allowed = mode === '--status' || mode === '--doctor' + ? base + : mode === '--write' || mode === '--refresh' + ? new Set([...base, '--max-files', '--max-file-bytes', '--max-nodes', '--max-edges', '--languages', ...(mode === '--refresh' ? ['--watch'] : [])]) + : mode === '--repair' + ? new Set([...base, '--confirm', '--max-files', '--max-file-bytes', '--max-nodes', '--max-edges', '--languages']) + : new Set([...base, '--kind', '--locator', '--direction', '--depth', '--limit', '--cursor']); + return values.find((value) => value.startsWith('--') && !allowed.has(value)) ?? null; +} + +function missingOptionValue(values, name) { + const index = values.indexOf(name); + return index < 0 || index + 1 >= values.length || values[index + 1].startsWith('--'); +} + +function compactNativeGraphIndexReport(result) { + return { + schemaVersion: result.responseSchemaVersion, + command: `graph index ${result.operation.slice('index.'.length)}`, + engine: { selection: 'native', implementation: 'memory-recall-native', previewOnly: false, publicDefaultChanged: true }, + status: result.state, + indexLocator: result.indexLocator, + activeGeneration: result.activeGeneration, + freshness: result.freshness, + health: result.health, + fileCount: result.summary.fileCount, + nodeCount: result.summary.nodeCount, + edgeCount: result.summary.edgeCount, + unresolvedCount: result.summary.unresolvedCount, + omittedCount: result.summary.omittedCount, + databaseBytes: result.summary.databaseBytes, + measurements: result.measurements, + results: result.results, + truncated: result.truncated, + nextCursor: result.nextCursor, + diagnostics: result.diagnostics, + safeguards: result.safeguards + }; +} + +function compactNativeGraphRepositoriesReport(result) { + return { + schemaVersion: result.responseSchemaVersion, + command: `graph repositories ${result.operation.slice('repository.'.length)}`, + engine: { selection: 'native', implementation: 'memory-recall-native', previewOnly: false, publicDefaultChanged: true }, + status: result.state, + registryLocator: result.registryLocator, + repositories: result.repositories, + results: result.results, + perRepository: result.perRepository, + partial: result.partial, + truncated: result.truncated, + measurements: result.measurements, + safeguards: result.safeguards + }; +} + +function renderGraphRepositoriesSummary(report) { + return [ + '# Graph Repositories', + `Command: ${report.command}`, + `Status: ${report.status}`, + `Repositories: ${report.repositories.length}`, + `Results: ${report.results.length}`, + ...report.repositories.map((repository) => `- ${repository.displayName}: ${repository.repositoryId} (${repository.rootLocator})`), + ...report.results.map((result) => `- ${result.repositoryId}: ${result.label} (${result.locator})`), + '', + 'Safeguards', + `Read-only: ${report.safeguards.readOnly ? 'yes' : 'no'}`, + `Local files written: ${report.safeguards.localFilesWritten}`, + `Raw source bodies included: ${report.safeguards.rawSourceBodiesIncluded ? 'yes' : 'no'}` + ].join('\n'); +} + +function renderGraphIndexSummary(report) { + const measurements = report.measurements ?? {}; + const queryLines = report.command === 'graph index query' + ? [ + `Results returned: ${report.results?.length ?? 0}`, + `Query truncated: ${report.truncated ? 'yes' : 'no'}`, + `Next cursor: ${report.nextCursor ?? 'none'}` + ] + : []; + return [ + '# Source Graph Index', + `Status: ${report.status}`, + `Index: ${report.indexLocator}`, + `Files: ${report.fileCount ?? (measurements.parsedFileCount ?? 0) + (measurements.reusedFileCount ?? 0)}`, + `Nodes: ${report.nodeCount ?? report.graph?.nodeCount ?? 0}`, + `Edges: ${report.edgeCount ?? report.graph?.edgeCount ?? 0}`, + `Parsed: ${measurements.parsedFileCount ?? 0}`, + `Reused: ${measurements.reusedFileCount ?? 0}`, + `Changed: ${measurements.changedFileCount ?? 0}`, + `Added: ${measurements.addedFileCount ?? 0}`, + `Deleted: ${measurements.deletedFileCount ?? 0}`, + ...queryLines, + `Raw source bodies stored: no` + ].join('\n'); +} + +async function watchGraphIndex({ root, execute, engine = 'native' }) { + let timer = null; + let running = false; + let pending = false; + const refresh = async () => { + if (running) { + pending = true; + return; + } + running = true; + try { + await execute(); + } finally { + running = false; + if (pending) { + pending = false; + await refresh(); + } + } + }; + const watcher = watchFs(root, { recursive: true }, (_event, filename) => { + const relative = String(filename ?? '').replaceAll('\\', '/'); + const sourcePattern = engine === 'native' + ? /(?:\.(?:[cm]?[jt]sx?|py|java|kts?|cs|go|rs|php|rb|swift|c|h|cc|cpp|cxx|hpp|dart|lua|sh|bash|sql|m|mm|scala|r|jl|zig)|(?:^|\/)\.gitignore|(?:^|\/)\.recallignore)$/iu + : /(?:\.(?:[cm]?[jt]sx?)|(?:^|\/)\.gitignore|(?:^|\/)\.recallignore)$/u; + if (!relative || relative.startsWith('.local/source-graph/') || relative.startsWith('.local/source-index/') || !sourcePattern.test(relative)) return; + if (timer) clearTimeout(timer); + timer = setTimeout(() => void refresh(), 250); + }); + await new Promise((resolve) => { + const stop = () => { + if (timer) clearTimeout(timer); + watcher.close(); + process.off('SIGINT', stop); + process.off('SIGTERM', stop); + resolve(); + }; + process.on('SIGINT', stop); + process.on('SIGTERM', stop); + }); +} + async function recallMapCommand(values) { const options = parseRecallMapOptions(values); if (!options) return; @@ -2892,6 +3269,16 @@ async function recallMapCommand(values) { changedLocators, query: options.query, sqliteLocator: options.sqliteLocator, + sourceGraphPreviewBuilder: async (previewOptions) => { + try { + return await buildCurrentNativeSourceGraphPreview(previewOptions); + } catch (error) { + return buildUnavailableSourceGraphPreview({ + ...previewOptions, + errorCode: String(error?.code ?? error?.message ?? 'native_index_unavailable') + }); + } + }, clock: fixedNow }); if (options.format === 'json') { @@ -2902,7 +3289,7 @@ async function recallMapCommand(values) { console.log(renderRecallMapSummary({ command: 'recall map', ...map })); } } catch (error) { - console.error(error.message); + console.error(strictNativeReadMessage(error)); process.exitCode = 2; } } @@ -3112,8 +3499,14 @@ async function graphPreviewBackedCommand(values, { } const root = option(values, '--root') ?? process.cwd(); const workspaceId = option(values, '--workspace') ?? 'ws_local'; + const requestedEngine = option(values, '--engine') ?? 'native'; + if (!['auto', 'native', 'native-preview'].includes(requestedEngine)) { + console.error('graph --engine must be native, native-preview, or auto'); + process.exitCode = 2; + return; + } try { - const preview = await buildSourceGraphPreview({ + const preview = await buildCurrentNativeSourceGraphPreview({ root, workspaceId, query, @@ -3121,15 +3514,12 @@ async function graphPreviewBackedCommand(values, { changedLocators, nodeKinds: option(values, '--node-kinds'), edgeKinds: option(values, '--edge-kinds'), - labelPattern: option(values, '--label-pattern'), locatorPrefix: option(values, '--locator-prefix'), direction, limit: strictIntegerOption(values, '--limit', 20), offset: strictIntegerOption(values, '--offset', 0), depth: strictIntegerOption(values, '--depth', 2), sampleLimit: strictIntegerOption(values, '--sample-limit', 3), - maxFiles: strictIntegerOption(values, '--max-files', DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILES), - maxFileBytes: strictIntegerOption(values, '--max-file-bytes', DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILE_BYTES), clock: fixedNow }); const baseReport = { @@ -3137,6 +3527,14 @@ async function graphPreviewBackedCommand(values, { command: commandName, generatedAt: preview.generatedAt, workspaceId: preview.workspaceId, + engine: { + selection: 'native', + implementation: 'memory-recall-native', + previewOnly: false, + publicDefaultChanged: true, + requested: requestedEngine, + reason: requestedEngine === 'native' ? null : 'native_alias' + }, graph: compactGraphCommandGraph(preview.graph), ...pick(preview), safeguards: preview.safeguards @@ -3144,11 +3542,83 @@ async function graphPreviewBackedCommand(values, { const report = { ...baseReport, measurements: graphCommandMeasurements(preview.measurements, baseReport) }; console.log(format === 'summary' ? renderSummary(report) : JSON.stringify(report, null, 2)); } catch (error) { - console.error(error.message); + console.error(strictNativeReadMessage(error)); process.exitCode = 2; } } +async function buildCurrentNativeSourceGraphPreview(options) { + const { RustCodeIntelligenceProvider } = await import('../../providers/native/code-intelligence-rust/src/index.mjs'); + const provider = new RustCodeIntelligenceProvider(); + const status = await provider.indexStatus({ root: options.root, workspaceId: options.workspaceId }); + if (!nativeIndexReadyForAutomaticRead(status)) { + const code = nativeIndexRecoveryCode(status); + const error = new Error(code); + error.code = code; + throw error; + } + return buildNativeIndexSourceGraphPreview({ ...options, provider, status }); +} + +async function currentNativeSourceGraphPreviewIfReady(options) { + try { + return await buildCurrentNativeSourceGraphPreview(options); + } catch (error) { + if (isNativeIndexRecoveryRequired(error)) return null; + throw error; + } +} + +function nativeIndexRecoveryCode(status) { + switch (status?.health?.status) { + case 'absent': return 'source_index_build_required'; + case 'stale': return 'source_index_refresh_required'; + case 'interrupted': + case 'corrupt': return 'source_index_repair_required'; + case 'migration-required': return 'source_index_migration_required'; + case 'wrong-repository': return 'source_index_wrong_repository'; + case 'unsupported-schema': return 'source_index_schema_newer'; + default: return 'source_index_query_unavailable'; + } +} + +function strictNativeReadMessage(error) { + const code = String(error?.code ?? error?.message ?? 'native_engine_unavailable'); + if (code === 'source_index_build_required' || code === 'source_index_query_unavailable') { + return `${code}: run recall graph index --write --engine native --root . --format summary`; + } + if (code === 'source_index_refresh_required') { + return `${code}: run recall graph index --refresh --engine native --root . --format summary`; + } + if ([ + 'source_index_corrupt', + 'source_index_integrity_failed', + 'source_index_migration_checksum_invalid', + 'source_index_migration_required', + 'source_index_repair_required', + 'source_index_wrong_repository' + ].includes(code)) { + return `${code}: run recall graph index --doctor --engine native --root . --format summary, then use the exact repair command it reports`; + } + if (code === 'source_index_schema_newer') { + return `${code}: use a Memory Recall version compatible with the newer index schema; do not overwrite it with this version`; + } + if (code === 'native_platform_unsupported') { + return `${code}: this platform has no supported packaged native engine`; + } + if ([ + 'native_engine_checksum_mismatch', + 'native_engine_manifest_invalid', + 'native_engine_path_invalid', + 'native_engine_unavailable', + 'native_engine_version_mismatch', + 'native_platform_package_missing' + ].includes(code)) { + return `${code}: install the matching @memory-recall/native-* package`; + } + return code; +} + function graphCommandMeasurements(previewMeasurements = {}, report = {}) { const fullGraphTokenEstimate = Number(previewMeasurements.fullGraphTokenEstimate ?? 0); const deliveredTokenEstimate = estimateTokens(JSON.stringify(report)); @@ -3187,10 +3657,11 @@ function renderGraphStatsSummary(report) { const entryPoints = summary.entryPoints ?? []; return [ '# Graph Stats', + `Engine: ${report.engine?.selection ?? 'js'}`, `Files: ${summary.fileCount ?? 0}`, `Symbols: ${summary.symbolCount ?? 0}`, - `Qualified symbols: ${summary.qualifiedSymbolCount ?? 0}`, - `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 0}`, + `Qualified symbols: ${summary.qualifiedSymbolCount ?? 'unmeasured'}`, + `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 'unmeasured'}`, `Nodes: ${summary.nodeCount ?? 0}`, `Edges: ${summary.edgeCount ?? 0}`, `Call edges: ${edgeKinds.calls ?? 0}`, @@ -3216,10 +3687,11 @@ function renderGraphSearchSummary(report) { const results = report.search?.results ?? []; return [ '# Graph Search', + `Engine: ${report.engine?.selection ?? 'js'}`, `Files: ${summary.fileCount ?? 0}`, `Symbols: ${summary.symbolCount ?? 0}`, - `Qualified symbols: ${summary.qualifiedSymbolCount ?? 0}`, - `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 0}`, + `Qualified symbols: ${summary.qualifiedSymbolCount ?? 'unmeasured'}`, + `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 'unmeasured'}`, `Results: ${results.length}/${report.search?.total ?? 0}`, `Fingerprint: ${report.graph?.graphFingerprint ?? 'unavailable'}`, `Delivered graph tokens: ${report.measurements?.deliveredTokenEstimate ?? 0}`, @@ -3263,10 +3735,11 @@ function renderGraphTraceSummary(report) { const paths = report.trace?.paths ?? []; return [ '# Graph Trace', + `Engine: ${report.engine?.selection ?? 'js'}`, `Files: ${summary.fileCount ?? 0}`, `Symbols: ${summary.symbolCount ?? 0}`, - `Qualified symbols: ${summary.qualifiedSymbolCount ?? 0}`, - `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 0}`, + `Qualified symbols: ${summary.qualifiedSymbolCount ?? 'unmeasured'}`, + `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 'unmeasured'}`, `Start nodes: ${report.trace?.startNodeIds?.length ?? 0}`, `Paths: ${paths.length}`, `Direction: ${report.trace?.direction ?? 'unknown'}`, @@ -3290,10 +3763,11 @@ function renderGraphImpactSummary(report) { const edgeKindSummary = Object.entries(impact.impactedEdgeKindCounts ?? {}).sort((a, b) => a[0].localeCompare(b[0])).map(([kind, count]) => `${kind} ${count}`).join(', ') || 'none'; return [ '# Graph Impact', + `Engine: ${report.engine?.selection ?? 'js'}`, `Files: ${summary.fileCount ?? 0}`, `Symbols: ${summary.symbolCount ?? 0}`, - `Qualified symbols: ${summary.qualifiedSymbolCount ?? 0}`, - `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 0}`, + `Qualified symbols: ${summary.qualifiedSymbolCount ?? 'unmeasured'}`, + `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 'unmeasured'}`, `Changed coverage: ${impact.representedChangedLocators?.length ?? 0}/${impact.changedLocators?.length ?? 0}`, `Affected symbols: ${symbols.length}`, `Impacted edges: ${impact.impactedEdgeIds?.length ?? 0}`, @@ -4626,7 +5100,12 @@ async function loadTemporalDataset(datasetPath) { } async function resolveBenchmarkDataset(requestedPath, bundledPath) { - const selectedPath = requestedPath ?? bundledPath; + if (requestedPath === null || requestedPath === undefined) { + const packagePath = path.join(PACKAGE_ROOT, ...bundledPath.split('/')); + return { path: packagePath, ref: `package://${bundledPath}` }; + } + + const selectedPath = requestedPath; const workspacePath = path.resolve(selectedPath); const workspaceFile = await stat(workspacePath).catch(() => null); if (workspaceFile?.isFile()) { @@ -5180,7 +5659,16 @@ async function contextPackCommand(values) { const userSelectedFiles = options(values, '--include-file'); try { const { changedLocators, detection: changedLocatorDetection } = await resolveChangedLocators(values, { root, workspaceId }); - const pack = await buildContextPack({ root, harnesses, userSelectedFiles, changedLocators, workspaceId, objective, step, targetHarness, tokenBudget }); + const sourceGraphPreview = await currentNativeSourceGraphPreviewIfReady({ + root, + workspaceId, + query: `${objective} ${step}`, + changedLocators, + limit: 12, + sampleLimit: 1, + clock: fixedNow + }); + const pack = await buildContextPack({ root, harnesses, userSelectedFiles, changedLocators, workspaceId, objective, step, targetHarness, tokenBudget, sourceGraphPreview }); const markdown = renderContextPackMarkdown(pack); const usePlan = buildContextPackUsePlan(pack); if (write) { @@ -5479,31 +5967,32 @@ async function contextGraphPreviewCommand(values) { const root = option(values, '--root') ?? process.cwd(); const workspaceId = option(values, '--workspace') ?? 'ws_local'; const query = option(values, '--query') ?? firstPositional(values, new Set(['--format', '--root', '--workspace', '--query', '--trace', '--start-name', '--start-node', '--changed', '--changed-locator', '--changed-locators', '--node-kinds', '--edge-kinds', '--label-pattern', '--locator-prefix', '--direction', '--limit', '--offset', '--depth', '--sample-limit', '--max-files', '--max-file-bytes'])) ?? ''; + const unsupportedNativeOptions = ['--start-node', '--node-kinds', '--edge-kinds', '--label-pattern', '--max-files', '--max-file-bytes'] + .filter((flag) => values.includes(flag)); + if (unsupportedNativeOptions.length) { + console.error(`context graph preview does not support ${unsupportedNativeOptions[0]} with the native index`); + process.exitCode = 2; + return; + } try { const { changedLocators } = await resolveChangedLocators(values, { root, workspaceId }); - const preview = await buildSourceGraphPreview({ + const preview = await buildCurrentNativeSourceGraphPreview({ root, workspaceId, query, startName: option(values, '--trace') ?? option(values, '--start-name'), - startNodeId: option(values, '--start-node'), changedLocators: [...changedLocators, ...(option(values, '--changed-locators') ? [option(values, '--changed-locators')] : [])], - nodeKinds: option(values, '--node-kinds'), - edgeKinds: option(values, '--edge-kinds'), - labelPattern: option(values, '--label-pattern'), locatorPrefix: option(values, '--locator-prefix'), direction: option(values, '--direction') ?? 'outbound', limit: strictIntegerOption(values, '--limit', 20), offset: strictIntegerOption(values, '--offset', 0), depth: strictIntegerOption(values, '--depth', 2), sampleLimit: strictIntegerOption(values, '--sample-limit', 12), - maxFiles: strictIntegerOption(values, '--max-files', DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILES), - maxFileBytes: strictIntegerOption(values, '--max-file-bytes', DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILE_BYTES), clock: fixedNow }); console.log(format === 'summary' ? renderSourceGraphPreviewSummary(preview) : JSON.stringify(preview, null, 2)); } catch (error) { - console.error(error.message); + console.error(strictNativeReadMessage(error)); process.exitCode = 2; } } @@ -5521,8 +6010,8 @@ function renderSourceGraphPreviewSummary(preview) { `Status: ${preview.status ?? 'ready'}`, `Files: ${summary.fileCount ?? 0}`, `Symbols: ${summary.symbolCount ?? 0}`, - `Qualified symbols: ${summary.qualifiedSymbolCount ?? 0}`, - `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 0}`, + `Qualified symbols: ${summary.qualifiedSymbolCount ?? 'unmeasured'}`, + `Ambiguous labels: ${summary.ambiguousSymbolLabelCount ?? 'unmeasured'}`, `Nodes: ${summary.nodeCount ?? 0}`, `Edges: ${summary.edgeCount ?? 0}`, `Hotspots: ${hotspots}`, @@ -5547,9 +6036,10 @@ async function mcpCommand(values) { if (subcommand === 'resources') return await mcpResourcesCommand(rest); if (subcommand === 'server') return await mcpServerCommand(rest); if (subcommand === 'install') return await mcpInstallCommand(rest); + if (subcommand === 'uninstall') return await mcpUninstallCommand(rest); if (subcommand === 'stats') return await mcpStatsCommand(rest); if (subcommand === 'smoke') return await mcpSmokeCommand(rest); - console.error('mcp requires inspect, resources, server, install, stats, or smoke'); + console.error('mcp requires inspect, resources, server, install, uninstall, stats, or smoke'); process.exitCode = 2; } catch (error) { console.error(error.message); @@ -6233,6 +6723,12 @@ async function mcpServerCommand(values) { process.exitCode = 2; return; } + const sourceIndexEngine = option(values, '--engine') ?? 'native'; + if (!['auto', 'native', 'native-preview'].includes(sourceIndexEngine)) { + console.error('mcp server --engine must be native, native-preview, or auto'); + process.exitCode = 2; + return; + } const root = path.resolve(option(values, '--root') ?? process.cwd()); const workspaceId = option(values, '--workspace') ?? 'ws_local'; const state = await loadWorkspaceJson(root, option(values, '--state') ?? '.local/state.json', { @@ -6349,7 +6845,467 @@ async function buildMcpRealisticSavingsBenchmark({ values, root, workspaceId, ge } function buildMcpTokenSaverTools({ values, root, workspaceId, generatedAt, statsRecorder = null, cursorStore = null }) { + let nativeProviderPromise = null; + const loadNativeProvider = () => { + nativeProviderPromise ??= import('../../providers/native/code-intelligence-rust/src/index.mjs') + .then(({ RustCodeIntelligenceProvider }) => new RustCodeIntelligenceProvider()); + return nativeProviderPromise; + }; + const actionableNativeReadError = (error) => { + throw new Error(strictNativeReadMessage(error)); + }; + const nativeStatus = async () => { + try { + return await (await loadNativeProvider()).indexStatus({ root, workspaceId }); + } catch (error) { + return actionableNativeReadError(error); + } + }; + let nativeRepositoryProviderPromise = null; + const requireNativeRepositoryProvider = () => { + nativeRepositoryProviderPromise ??= loadNativeProvider() + .then(async (provider) => { + const health = await provider.health(); + if (health.status !== 'healthy') throw new Error('unhealthy'); + return provider; + }) + .catch(() => { + throw new Error('cross-repository native engine is unavailable; install the matching @memory-recall/native-* package'); + }); + return nativeRepositoryProviderPromise; + }; + const nativeQuery = async (kind, argumentsValue = {}) => { + try { + return await (await loadNativeProvider()).queryIndex({ + root, + workspaceId, + kind, + limit: argumentsValue.limit ?? 20, + ...(argumentsValue.query === undefined ? {} : { query: argumentsValue.query }), + ...(argumentsValue.locator === undefined ? {} : { locator: argumentsValue.locator }), + ...(argumentsValue.direction === undefined ? {} : { direction: argumentsValue.direction }), + ...(argumentsValue.depth === undefined ? {} : { depth: argumentsValue.depth }), + ...(argumentsValue.edgeKinds === undefined ? {} : { edgeKinds: argumentsValue.edgeKinds }), + ...(argumentsValue.cursor === undefined ? {} : { cursor: argumentsValue.cursor }), + ...(argumentsValue.signal === undefined ? {} : { signal: argumentsValue.signal }) + }); + } catch (error) { + return actionableNativeReadError(error); + } + }; + const nativeCompleteness = (result, locallyTruncated = false) => ({ + truncated: Boolean(result?.truncated || result?.nextCursor || locallyTruncated), + nextCursor: result?.nextCursor ?? null, + locallyTruncated + }); + const nativeQueryAtOffset = async (kind, argumentsValue, offset, limit) => { + if (offset === 0) return nativeQuery(kind, { ...argumentsValue, limit }); + const maxPageCalls = 8; + const controller = new AbortController(); + const deadlineTimer = setTimeout(() => controller.abort(), 1_500); + let pageCalls = 0; + let reachedOffset = 0; + let cursor = argumentsValue.cursor ?? null; + let lastResult = null; + const seenCursors = new Set(cursor ? [cursor] : []); + const incompleteResult = () => ({ + ...(lastResult ?? {}), + results: [], + relationships: [], + truncated: true, + offsetIncomplete: true, + reachedOffset, + nextCursor: lastResult?.nextCursor ?? cursor + }); + try { + while (reachedOffset < offset && pageCalls < maxPageCalls - 1 && !controller.signal.aborted) { + const skipLimit = Math.min(100, offset - reachedOffset); + try { + lastResult = await nativeQuery(kind, { ...argumentsValue, limit: skipLimit, ...(cursor ? { cursor } : {}), signal: controller.signal }); + } catch (error) { + if (controller.signal.aborted) return incompleteResult(); + throw error; + } + pageCalls += 1; + reachedOffset += lastResult.results?.length ?? 0; + if (reachedOffset >= offset) { + cursor = lastResult.nextCursor ?? null; + break; + } + if (!lastResult.nextCursor || seenCursors.has(lastResult.nextCursor)) break; + cursor = lastResult.nextCursor; + seenCursors.add(cursor); + } + if (controller.signal.aborted) return incompleteResult(); + if (reachedOffset < offset) { + if (lastResult?.nextCursor || lastResult?.truncated) return incompleteResult(); + return { + ...(lastResult ?? {}), + results: [], + relationships: [], + truncated: false, + offsetIncomplete: false, + reachedOffset, + nextCursor: null + }; + } + if (!cursor) { + return { + ...(lastResult ?? {}), + results: [], + relationships: [], + truncated: false, + offsetIncomplete: false, + reachedOffset, + nextCursor: null + }; + } + try { + const result = await nativeQuery(kind, { ...argumentsValue, limit, cursor, signal: controller.signal }); + return { ...result, offsetIncomplete: false, reachedOffset }; + } catch (error) { + if (controller.signal.aborted) return incompleteResult(); + throw error; + } + } finally { + clearTimeout(deadlineTimer); + } + }; return [ + { + name: 'repo.architecture', + description: 'Return bounded architecture groups, communities, entry points, hotspots, and evidence-backed entry-to-sink processes from local source metadata.', + operation: 'repo.architecture', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['limit'], 'repo.architecture'); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); + const [communities, processes] = await Promise.all([ + nativeQuery('communities', { limit }), + nativeQuery('processes', { depth: 4, limit }) + ]); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'repo.architecture', workspaceId, generatedAt: fixedNow(), + data: buildNativeIndexArchitecture(communities, processes, limit) + })); + } + }, + { + name: 'repo.index_status', + description: 'Report local persistent-index status or list registered repositories; this tool never builds or refreshes indexes.', + operation: 'repo.index_status', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + scope: { type: 'string', enum: ['local', 'repositories'] }, + limit: { type: 'integer', minimum: 1, maximum: 64 } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['scope', 'limit'], 'repo.index_status'); + const scope = input.scope ?? 'local'; + if (!['local', 'repositories'].includes(scope)) throw new Error('repo.index_status scope is invalid'); + if (scope === 'repositories') { + const limit = mcpStrictBoundedInteger(input.limit, 64, { min: 1, max: 64, name: 'limit' }); + const provider = await requireNativeRepositoryProvider(); + const result = await provider.listRepositories({ root, workspaceId, limit }); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'repo.index_status', workspaceId, generatedAt: fixedNow(), + data: nativeRepositoryListData(result) + })); + } + if (input.limit !== undefined) throw new Error('repo.index_status limit requires repository scope'); + const result = await nativeStatus(); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'repo.index_status', workspaceId, generatedAt: fixedNow(), + data: nativeIndexStatusData(result) + })); + } + }, + { + name: 'code.search', + description: 'Search bounded symbols, files, modules, and relationships in the local or selected registered repositories.', + operation: 'code.search', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['query'], + properties: { + query: { type: 'string', minLength: 1, maxLength: 512 }, + repositoryIds: { + type: 'array', + minItems: 1, + maxItems: 8, + uniqueItems: true, + items: { type: 'string', pattern: '^repo_[a-f0-9]{32}$' } + }, + nodeKinds: { type: 'array', maxItems: 4, items: { type: 'string', enum: ['file', 'chunk', 'symbol', 'module'] } }, + edgeKinds: { type: 'array', maxItems: 6, items: { type: 'string', enum: ['contains', 'defined_in', 'imports', 'exports', 'references', 'calls'] } }, + locatorPrefix: { type: 'string', minLength: 1, maxLength: 512 }, + limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 }, + offset: { type: 'integer', minimum: 0, maximum: 10000, default: 0 }, + cursor: { type: 'string', pattern: '^idxcur_[a-f0-9]{32}$' } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['query', 'repositoryIds', 'nodeKinds', 'edgeKinds', 'locatorPrefix', 'limit', 'offset', 'cursor'], 'code.search'); + const query = mcpStructuralString(input.query, 'code.search query', { required: true, max: 512 }); + const repositoryIds = mcpRepositoryIds(input.repositoryIds, { min: 1, max: 8 }); + const nodeKinds = mcpStructuralKinds(input.nodeKinds, ['file', 'chunk', 'symbol', 'module']); + const edgeKinds = mcpStructuralKinds(input.edgeKinds, ['contains', 'defined_in', 'imports', 'exports', 'references', 'calls']); + const locatorPrefix = mcpStructuralLocatorPrefix(input.locatorPrefix); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: repositoryIds ? 25 : 50, name: 'limit' }); + const offset = mcpStrictBoundedInteger(input.offset, 0, { min: 0, max: 10000, name: 'offset' }); + const cursor = input.cursor === undefined ? null : mcpStructuralString(input.cursor, 'code.search cursor', { required: true, max: 39 }); + if (cursor && !/^idxcur_[a-f0-9]{32}$/u.test(cursor)) throw new Error('code.search cursor is invalid'); + if (cursor && offset !== 0) throw new Error('code.search cursor cannot be combined with a non-zero offset'); + if (repositoryIds) { + if (query.length > 160) throw new Error('native repository query exceeds 160 characters'); + if (nodeKinds?.length || edgeKinds?.length || locatorPrefix || offset !== 0 || cursor) { + throw new Error('code.search repository mode does not support local filters, offset, or cursor'); + } + const provider = await requireNativeRepositoryProvider(); + const result = await provider.searchRepositories({ + root, + workspaceId, + query, + repositoryIds, + perRepositoryLimit: limit, + limit + }); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.search', workspaceId, generatedAt: fixedNow(), + data: nativeRepositorySearchData(result, query, repositoryIds) + })); + } + if (query.length > 160) throw new Error('native index query exceeds 160 characters'); + if (edgeKinds?.length) throw new Error('native index edge-kind filtering is not available'); + if (offset !== 0 && (nodeKinds?.length || locatorPrefix)) { + throw new Error('native code.search offset cannot be combined with local result filters; continue with nextCursor'); + } + const result = await nativeQueryAtOffset('search', { query, ...(cursor ? { cursor } : {}) }, offset, limit); + const results = result.results + .filter((item) => nativeNodeMatchesKinds(item, nodeKinds)) + .filter((item) => !locatorPrefix || item.locator.startsWith(locatorPrefix)) + .slice(0, limit) + .map(nativeStructuralNode); + const resultIds = new Set(results.map((item) => item.id)); + const relationships = (result.relationships ?? []) + .filter((item) => resultIds.has(item.fromNodeId) && resultIds.has(item.toNodeId)) + .slice(0, limit) + .map(nativeStructuralRelationship); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.search', workspaceId, generatedAt: fixedNow(), + data: { + query, + results, + relationships, + resultCount: results.length, + limit, + offset, + reachedOffset: result.reachedOffset ?? offset, + offsetIncomplete: result.offsetIncomplete === true, + truncated: Boolean(result.truncated || result.nextCursor), + hasMore: Boolean(result.nextCursor), + nextCursor: result.nextCursor, + source: nativeIndexSource(result) + } + })); + } + }, + { + name: 'code.context', + description: 'Return one selected symbol with bounded, optionally filtered incoming and outgoing structural relationships.', + operation: 'code.context', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['query'], + properties: { + query: { type: 'string', minLength: 1, maxLength: 512 }, + direction: { type: 'string', enum: ['outbound', 'inbound', 'both'] }, + depth: { type: 'integer', enum: [1, 2, 3] }, + edgeKinds: { + type: 'array', + minItems: 1, + maxItems: 16, + uniqueItems: true, + items: { type: 'string', enum: CODE_INTELLIGENCE_EDGE_KINDS } + }, + limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['query', 'direction', 'depth', 'edgeKinds', 'limit'], 'code.context'); + const query = mcpStructuralString(input.query, 'code.context query', { required: true, max: 512 }); + const constrained = ['direction', 'depth', 'edgeKinds'].some((key) => Object.hasOwn(input, key)); + const direction = mcpStructuralDirection(input.direction ?? 'both'); + const depth = mcpStrictBoundedInteger(input.depth, 1, { min: 1, max: 3, name: 'depth' }); + const edgeKinds = mcpStructuralKinds(input.edgeKinds, CODE_INTELLIGENCE_EDGE_KINDS, { minItems: 1, maxItems: 16, unique: true }); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); + if (constrained) { + const status = await nativeStatus(); + if (!nativeIndexReadyForAutomaticRead(status)) { + throw new Error('code.context constraints require a current native index; refresh it with recall graph index --refresh --engine native --root . --format summary'); + } + } + if (query.length > 160) throw new Error('native index query exceeds 160 characters'); + const [selected, neighborhood] = await Promise.all([ + nativeQuery('exact', { query, limit: 1 }), + constrained + ? nativeQuery('dependencies', { query, direction, depth, edgeKinds, limit }) + : nativeQuery('neighborhood', { query, limit, depth: 1 }) + ]); + const related = neighborhood.results.map(nativeStructuralNode); + const relatedIds = new Set(related.map((item) => item.id)); + const relationships = (neighborhood.relationships ?? []) + .filter((item) => relatedIds.has(item.fromNodeId) && relatedIds.has(item.toNodeId)) + .filter((item) => !edgeKinds || edgeKinds.includes(item.kind)) + .map(nativeStructuralRelationship); + const completeness = { + selection: nativeCompleteness(selected), + neighborhood: nativeCompleteness(neighborhood) + }; + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.context', workspaceId, generatedAt: fixedNow(), + data: { + query, + selected: selected.results[0] ? nativeStructuralNode(selected.results[0]) : null, + related, + relationships, + completeness, + truncated: completeness.selection.truncated || completeness.neighborhood.truncated, + ...(constrained ? { direction, depth, edgeKinds: edgeKinds ?? [] } : {}), + source: nativeIndexSource(neighborhood) + } + })); + } + }, + { + name: 'code.trace', + description: 'Trace bounded local call paths or one evidence-backed Go path across two registered repositories.', + operation: 'code.trace', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + oneOf: [{ required: ['symbol'] }, { required: ['crossRepository'] }], + properties: { + symbol: { type: 'string', minLength: 1, maxLength: 240 }, + crossRepository: mcpCrossRepositoryInputSchema(), + direction: { type: 'string', enum: ['outbound', 'inbound', 'both'] }, + depth: { type: 'integer', enum: [1, 2, 3] }, + limit: { type: 'integer', minimum: 1, maximum: 50 }, + locatorPrefix: { type: 'string', minLength: 1, maxLength: 512 } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['symbol', 'crossRepository', 'direction', 'depth', 'limit', 'locatorPrefix'], 'code.trace'); + const crossRepository = mcpCrossRepository(input.crossRepository); + mcpRejectMixedCrossRepositoryArguments(input, crossRepository, ['symbol', 'direction', 'depth', 'locatorPrefix'], 'code.trace'); + const symbol = mcpStructuralString(input.symbol, 'code.trace symbol', { required: !crossRepository, max: 240 }); + const direction = mcpStructuralDirection(input.direction); + const depth = mcpStrictBoundedInteger(input.depth, 2, { min: 1, max: 3, name: 'depth' }); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: crossRepository ? 25 : 50, name: 'limit' }); + const locatorPrefix = mcpStructuralLocatorPrefix(input.locatorPrefix); + if (crossRepository) { + const provider = await requireNativeRepositoryProvider(); + const result = await provider.traceGoRepositories({ root, workspaceId, ...crossRepository, limit }); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.trace', workspaceId, generatedAt: fixedNow(), + data: nativeCrossRepositoryData(result, crossRepository) + })); + } + if (symbol.length > 160) throw new Error('native index query exceeds 160 characters'); + const result = await nativeQuery('dependencies', { query: symbol, direction, depth, limit }); + const completeness = nativeCompleteness(result); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.trace', workspaceId, generatedAt: fixedNow(), + data: { symbol, direction, depth, nodes: result.results.filter((item) => !locatorPrefix || item.locator.startsWith(locatorPrefix)).map(nativeStructuralNode), relationships: (result.relationships ?? []).filter((item) => !locatorPrefix || item.locator.startsWith(locatorPrefix)).map(nativeStructuralRelationship), completeness, truncated: completeness.truncated, nextCursor: completeness.nextCursor, source: nativeIndexSource(result) } + })); + } + }, + { + name: 'code.dependencies', + description: 'Walk a bounded local dependency neighborhood or resolve one exact Go module boundary across repositories.', + operation: 'code.dependencies', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + oneOf: [{ required: ['query'] }, { required: ['crossRepository'] }], + properties: { + query: { type: 'string', minLength: 1, maxLength: 512 }, + crossRepository: mcpCrossRepositoryInputSchema(), + direction: { type: 'string', enum: ['outbound', 'inbound', 'both'] }, + depth: { type: 'integer', enum: [1, 2, 3] }, + limit: { type: 'integer', minimum: 1, maximum: 50 } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['query', 'crossRepository', 'direction', 'depth', 'limit'], 'code.dependencies'); + const crossRepository = mcpCrossRepository(input.crossRepository); + mcpRejectMixedCrossRepositoryArguments(input, crossRepository, ['query', 'direction', 'depth', 'limit'], 'code.dependencies'); + const query = mcpStructuralString(input.query, 'code.dependencies query', { required: !crossRepository, max: 512 }); + const direction = mcpStructuralDirection(input.direction); + const depth = mcpStrictBoundedInteger(input.depth, 2, { min: 1, max: 3, name: 'depth' }); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: crossRepository ? 25 : 50, name: 'limit' }); + if (crossRepository) { + const provider = await requireNativeRepositoryProvider(); + const result = await provider.resolveGoRepositories({ root, workspaceId, ...crossRepository }); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.dependencies', workspaceId, generatedAt: fixedNow(), + data: nativeCrossRepositoryData(result, crossRepository) + })); + } + if (query.length > 160) throw new Error('native index query exceeds 160 characters'); + const result = await nativeQuery('dependencies', { query, direction, depth, limit }); + const completeness = nativeCompleteness(result); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.dependencies', workspaceId, generatedAt: fixedNow(), + data: { query, direction, depth, nodes: result.results.map(nativeStructuralNode), relationships: (result.relationships ?? []).map(nativeStructuralRelationship), completeness, truncated: completeness.truncated, nextCursor: completeness.nextCursor, source: nativeIndexSource(result) } + })); + } + }, + { + name: 'code.routes', + description: 'Discover bounded HTTP route exports with locator-safe static evidence.', + operation: 'code.routes', + sideEffectClass: 'read-only', + inputSchema: { + type: 'object', + additionalProperties: false, + properties: { + query: { type: 'string', maxLength: 240 }, + limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 } + } + }, + handler: async ({ arguments: args }) => { + const input = mcpMapArguments(args, ['query', 'limit'], 'code.routes'); + const query = mcpStructuralString(input.query, 'code.routes query', { required: false, max: 240 }); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); + const result = await nativeQuery('routes', { limit }); + const routes = result.results + .filter((item) => !query || item.label.toLocaleLowerCase().includes(query.toLocaleLowerCase())) + .map(nativeStructuralNode); + const completeness = nativeCompleteness(result); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.routes', workspaceId, generatedAt: fixedNow(), + data: { query, routes, relationships: (result.relationships ?? []).map(nativeStructuralRelationship), completeness, truncated: completeness.truncated, nextCursor: completeness.nextCursor, source: nativeIndexSource(result) } + })); + } + }, { name: 'repo.map', description: 'Return a bounded Recall Map of local source coverage, governed memory, and handoff readiness.', @@ -6366,40 +7322,108 @@ function buildMcpTokenSaverTools({ values, root, workspaceId, generatedAt, stats } }, handler: async ({ arguments: args }) => { - const payload = await buildMcpRepoMapPayload({ - values, - root, - workspaceId, - generatedAt: fixedNow(), - args - }); - return mcpToolJsonResult(payload); + const input = mcpMapArguments(args, ['client', 'changed', 'query', 'limit'], 'repo.map'); + mcpMapClient(input.client); + const changedLocators = mcpMapChangedLocators(input.changed, { required: false }); + const query = mcpMapQuery(input.query); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); + const status = await nativeStatus(); + const search = query ? await nativeQuery('exact', { query, limit }) : null; + const impact = []; + for (const locator of changedLocators) { + const result = await nativeQuery('impact', { query: locator, limit, depth: 2 }); + impact.push({ locator, nodes: result.results.map(nativeStructuralNode), relationships: (result.relationships ?? []).map(nativeStructuralRelationship), ...nativeCompleteness(result) }); + } + const completeness = { + search: search ? nativeCompleteness(search) : null, + impact: impact.map(({ locator, truncated, nextCursor }) => ({ locator, truncated, nextCursor })) + }; + const sqlitePath = await resolveWorkspaceSqlitePath( + root, + option(values, '--sqlite') ?? '.local/memory.sqlite', + 'mcp server', + { mustExist: false } + ); + const memory = await buildRecallMapMemorySummary({ + root, + workspaceId, + clock: () => fixedNow(), + sqliteLocator: sqlitePath.relative + }); + const affectedSymbols = [...new Map( + impact.flatMap(({ nodes }) => nodes).map((node) => [node.id, node]) + ).values()]; + const payload = mcpStructuralPayload({ + command: 'repo.map', workspaceId, generatedAt: fixedNow(), + data: { + sourceIndex: nativeIndexStatusData(status), + search: search ? search.results.map(nativeStructuralNode) : [], + impact, + architecture: { + search: { + query, + results: search ? search.results.map(nativeStructuralNode) : [], + truncated: completeness.search?.truncated ?? false, + nextCursor: completeness.search?.nextCursor ?? null + }, + impact: { + changedLocators, + representedChangedLocators: impact.filter(({ nodes }) => nodes.length > 0).map(({ locator }) => locator), + affectedSymbols + } + }, + completeness, + truncated: Boolean(completeness.search?.truncated || completeness.impact.some((item) => item.truncated)), + memory, + source: nativeIndexSource(status) + } + }); + payload.data.safeguards = payload.safeguards; + return mcpToolJsonResult(payload); } }, { name: 'code.impact', - description: 'Return bounded locator-safe impact for changed local source files.', + description: 'Return bounded impact for changed local files or one evidence-backed Go boundary across repositories.', operation: 'code.impact', sideEffectClass: 'read-only', inputSchema: { type: 'object', additionalProperties: false, - required: ['changed'], + oneOf: [{ required: ['changed'] }, { required: ['crossRepository'] }], properties: { changed: { type: 'array', minItems: 1, maxItems: 16, items: { type: 'string', minLength: 1, maxLength: 512 } }, - depth: { type: 'integer', enum: [1, 2, 3], default: 2 }, - limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 } + crossRepository: mcpCrossRepositoryInputSchema(), + depth: { type: 'integer', enum: [1, 2, 3] }, + limit: { type: 'integer', minimum: 1, maximum: 50 } } }, handler: async ({ arguments: args }) => { - const payload = await buildMcpCodeImpactPayload({ - values, - root, - workspaceId, - generatedAt: fixedNow(), - args - }); - return mcpToolJsonResult(payload); + const input = mcpMapArguments(args, ['changed', 'crossRepository', 'depth', 'limit'], 'code.impact'); + const crossRepository = mcpCrossRepository(input.crossRepository); + mcpRejectMixedCrossRepositoryArguments(input, crossRepository, ['changed', 'depth'], 'code.impact'); + if (crossRepository) { + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 25, name: 'limit' }); + const provider = await requireNativeRepositoryProvider(); + const result = await provider.impactGoRepositories({ root, workspaceId, ...crossRepository, limit }); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.impact', workspaceId, generatedAt: fixedNow(), + data: nativeCrossRepositoryData(result, crossRepository) + })); + } + const changedLocators = mcpMapChangedLocators(input.changed, { required: true }); + const depth = mcpStrictBoundedInteger(input.depth, 2, { min: 1, max: 3, name: 'depth' }); + const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); + const results = []; + for (const locator of changedLocators) { + const result = await nativeQuery('impact', { query: locator, depth, limit }); + results.push({ locator, nodes: result.results.map(nativeStructuralNode), relationships: (result.relationships ?? []).map(nativeStructuralRelationship), ...nativeCompleteness(result) }); + } + const status = await nativeStatus(); + return mcpToolJsonResult(mcpStructuralPayload({ + command: 'code.impact', workspaceId, generatedAt: fixedNow(), + data: { changedLocators, depth, results, completeness: results.map(({ locator, truncated, nextCursor }) => ({ locator, truncated, nextCursor })), truncated: results.some((item) => item.truncated), source: nativeIndexSource(status) } + })); } }, { @@ -6492,85 +7516,30 @@ function buildMcpTokenSaverTools({ values, root, workspaceId, generatedAt, stats budget: { type: 'integer', minimum: 1, maximum: 100000, default: 4096 } } }, - handler: async ({ arguments: args }) => mcpToolTextResult(await buildMcpContextPackToolText({ - root, - workspaceId, - generatedAt, - args - })) - } - ]; -} - -async function buildMcpRepoMapPayload({ values, root, workspaceId, generatedAt, args }) { - const input = mcpMapArguments(args, ['client', 'changed', 'query', 'limit'], 'repo.map'); - mcpMapClient(input.client); - const changedLocators = mcpMapChangedLocators(input.changed, { required: false }); - const query = mcpMapQuery(input.query); - const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); - const report = await buildMcpRecallMapReport({ - values, - root, - workspaceId, - generatedAt, - changedLocators, - query, - depth: 2, - limit - }); - return mcpNoWritePayload({ - command: 'repo.map', - workspaceId, - generatedAt, - data: report - }); -} - -async function buildMcpCodeImpactPayload({ values, root, workspaceId, generatedAt, args }) { - const input = mcpMapArguments(args, ['changed', 'depth', 'limit'], 'code.impact'); - const changedLocators = mcpMapChangedLocators(input.changed, { required: true }); - const depth = mcpStrictBoundedInteger(input.depth, 2, { min: 1, max: 3, name: 'depth' }); - const limit = mcpStrictBoundedInteger(input.limit, 20, { min: 1, max: 50, name: 'limit' }); - const report = await buildMcpRecallMapReport({ - values, - root, - workspaceId, - generatedAt, - changedLocators, - query: '', - depth, - limit - }); - return mcpNoWritePayload({ - command: 'code.impact', - workspaceId, - generatedAt, - data: { - schemaVersion: report.schemaVersion, - reportVersion: report.reportVersion, - ...report.architecture.impact, - safeguards: report.safeguards - } - }); -} - -async function buildMcpRecallMapReport({ values, root, workspaceId, generatedAt, changedLocators, query, depth, limit }) { - const sqlitePath = await resolveWorkspaceSqlitePath( - root, - option(values, '--sqlite') ?? '.local/memory.sqlite', - 'mcp server', - { mustExist: false } - ); - return buildRecallMap({ - root, - workspaceId, - changedLocators, - query, - depth, - limit, - clock: () => generatedAt, - sqliteLocator: sqlitePath.relative - }); + handler: async ({ arguments: args }) => { + const objective = mcpRequiredString(args.objective, 'objective', 500); + const step = mcpRequiredString(args.step, 'step', 500); + const [provider, status] = await Promise.all([loadNativeProvider(), nativeStatus()]); + const sourceGraphPreview = await buildNativeIndexSourceGraphPreview({ + provider, + status, + root, + workspaceId, + query: `${objective} ${step}`, + limit: 12, + sampleLimit: 1, + clock: () => generatedAt + }); + return mcpToolTextResult(await buildMcpContextPackToolText({ + root, + workspaceId, + generatedAt, + args, + sourceGraphPreview + })); + } + } + ]; } function mcpMapArguments(args, allowedKeys, toolName) { @@ -6581,7 +7550,7 @@ function mcpMapArguments(args, allowedKeys, toolName) { function mcpMapClient(value) { if (value === undefined || value === null) return; - if (typeof value !== 'string' || !/^[A-Za-z0-9._:-]{1,80}$/u.test(value) || MCP_PRIVATE_MATERIAL.test(value)) { + if (typeof value !== 'string' || !/^[A-Za-z0-9._:-]{1,80}$/u.test(value) || mcpContainsPrivateMaterial(value)) { throw new Error('repo.map client is invalid'); } } @@ -6594,7 +7563,7 @@ function mcpMapChangedLocators(value, { required }) { if (!Array.isArray(value) || value.length > 16 || (required && !value.length)) throw new Error('mcp map changed locators are invalid'); if (!value.length) return []; const locators = value.map((item) => { - if (typeof item !== 'string' || item.length > 512 || MCP_PRIVATE_MATERIAL.test(item)) { + if (typeof item !== 'string' || item.length > 512 || mcpContainsPrivateMaterial(item)) { throw new Error('mcp map changed locators are invalid'); } try { @@ -6608,7 +7577,7 @@ function mcpMapChangedLocators(value, { required }) { function mcpMapQuery(value) { if (value === undefined || value === null) return ''; - if (typeof value !== 'string' || value.length > 512 || /[\0\r\n]/u.test(value) || MCP_PRIVATE_MATERIAL.test(value)) { + if (typeof value !== 'string' || value.length > 512 || /[\0\r\n]/u.test(value) || mcpContainsPrivateMaterial(value)) { throw new Error('repo.map query is invalid'); } return value.trim(); @@ -6622,6 +7591,133 @@ function mcpStrictBoundedInteger(value, fallback, { min, max, name }) { return value; } +function mcpStructuralString(value, name, { required, max }) { + if (value === undefined || value === null) { + if (required) throw new Error(`${name} is invalid`); + return ''; + } + if (typeof value !== 'string' || value.length > max || /[\0\r\n]/u.test(value) || mcpContainsPrivateMaterial(value)) { + throw new Error(`${name} is invalid`); + } + const normalized = value.trim(); + if (required && !normalized) throw new Error(`${name} is invalid`); + return normalized; +} + +function mcpStructuralKinds(value, allowed, { minItems = 0, maxItems = allowed.length, unique = false } = {}) { + if (value === undefined || value === null) return null; + if ( + !Array.isArray(value) + || value.length < minItems + || value.length > maxItems + || (unique && new Set(value).size !== value.length) + || value.some((item) => !allowed.includes(item)) + ) { + throw new Error('mcp structural kinds are invalid'); + } + return [...new Set(value)]; +} + +function mcpStructuralDirection(value) { + const direction = value ?? 'outbound'; + if (!['outbound', 'inbound', 'both'].includes(direction)) throw new Error('mcp direction is invalid'); + return direction; +} + +function mcpStructuralLocatorPrefix(value) { + if (value === undefined || value === null || value === '') return null; + if (typeof value !== 'string' || value.length > 512 || /[\0\r\n]/u.test(value) || mcpContainsPrivateMaterial(value)) { + throw new Error('mcp locator prefix is invalid'); + } + try { + return normalizeSourceGraphWorkspaceLocator(value, { stripFragment: true }); + } catch { + throw new Error('mcp locator prefix is invalid'); + } +} + +function mcpCrossRepositoryInputSchema() { + return { + type: 'object', + additionalProperties: false, + required: [ + 'repositoryIds', + 'clientRepositoryId', + 'serviceRepositoryId', + 'clientEntryNativeId', + 'serviceTargetNativeId' + ], + properties: { + repositoryIds: { + type: 'array', + minItems: 2, + maxItems: 2, + uniqueItems: true, + items: { type: 'string', pattern: '^repo_[a-f0-9]{32}$' } + }, + clientRepositoryId: { type: 'string', pattern: '^repo_[a-f0-9]{32}$' }, + serviceRepositoryId: { type: 'string', pattern: '^repo_[a-f0-9]{32}$' }, + clientEntryNativeId: { type: 'string', pattern: '^cinode_[a-f0-9]{32}$' }, + serviceTargetNativeId: { type: 'string', pattern: '^cinode_[a-f0-9]{32}$' } + } + }; +} + +function mcpRepositoryIds(value, { min, max }) { + if (value === undefined || value === null) return null; + if ( + !Array.isArray(value) + || value.length < min + || value.length > max + || new Set(value).size !== value.length + || value.some((item) => typeof item !== 'string' || !/^repo_[a-f0-9]{32}$/u.test(item)) + ) { + throw new Error('mcp repository ids are invalid'); + } + return [...value]; +} + +function mcpCrossRepository(value) { + if (value === undefined || value === null) return null; + const expectedKeys = [ + 'repositoryIds', + 'clientRepositoryId', + 'serviceRepositoryId', + 'clientEntryNativeId', + 'serviceTargetNativeId' + ]; + if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some((key) => !expectedKeys.includes(key))) { + throw new Error('mcp cross-repository selector is invalid'); + } + if (expectedKeys.some((key) => value[key] === undefined)) throw new Error('mcp cross-repository selector is invalid'); + const repositoryIds = mcpRepositoryIds(value.repositoryIds, { min: 2, max: 2 }); + const repositoryIdPattern = /^repo_[a-f0-9]{32}$/u; + const nativeIdPattern = /^cinode_[a-f0-9]{32}$/u; + if ( + !repositoryIdPattern.test(value.clientRepositoryId) + || !repositoryIdPattern.test(value.serviceRepositoryId) + || !nativeIdPattern.test(value.clientEntryNativeId) + || !nativeIdPattern.test(value.serviceTargetNativeId) + || repositoryIds[0] !== value.clientRepositoryId + || repositoryIds[1] !== value.serviceRepositoryId + ) { + throw new Error('mcp cross-repository selector is invalid'); + } + return { + repositoryIds, + clientRepositoryId: value.clientRepositoryId, + serviceRepositoryId: value.serviceRepositoryId, + clientEntryNativeId: value.clientEntryNativeId, + serviceTargetNativeId: value.serviceTargetNativeId + }; +} + +function mcpRejectMixedCrossRepositoryArguments(input, crossRepository, localKeys, toolName) { + if (crossRepository && localKeys.some((key) => input[key] !== undefined)) { + throw new Error(`${toolName} cannot mix local and cross-repository selectors`); + } +} + async function buildMcpMemoryRecallPayload({ values, root, workspaceId, generatedAt, args }) { const query = mcpRequiredString(args.query, 'query', 240); const scope = mcpSafeScope(args.scope ?? 'workspace'); @@ -6841,7 +7937,7 @@ async function buildMcpContextProfilePayload({ values, root, workspaceId, genera return payload; } -async function buildMcpContextPackToolText({ root, workspaceId, generatedAt, args }) { +async function buildMcpContextPackToolText({ root, workspaceId, generatedAt, args, sourceGraphPreview = null }) { const objective = mcpRequiredString(args.objective, 'objective', 500); const step = mcpRequiredString(args.step, 'step', 500); const toolValues = [ @@ -6852,7 +7948,7 @@ async function buildMcpContextPackToolText({ root, workspaceId, generatedAt, arg '--target', mcpSanitizeString(args.target ?? 'generic', 80), '--token-budget', String(mcpBoundedInteger(args.budget, 4096, { min: 1, max: 100000 })) ]; - const currentContextPack = await buildMcpContextPackResource(toolValues, { root, workspaceId }); + const currentContextPack = await buildMcpContextPackResource(toolValues, { root, workspaceId, sourceGraphPreview }); const resources = buildOafReadOnlyResourceCatalog({ state: {}, projectStatus: {}, @@ -7159,15 +8255,24 @@ function mcpBoundedInteger(value, fallback, { min, max }) { return Math.max(min, Math.min(max, parsed)); } +function mcpContainsPrivateMaterial(value) { + return MCP_PRIVATE_PATH.test(value) || MCP_SECRET_MATERIAL.test(value); +} + function mcpSanitizeString(value, maxLength = 240) { - const text = String(value ?? '').replace(MCP_PRIVATE_MATERIAL_GLOBAL, '[redacted]').normalize('NFKC').replace(/\s+/gu, ' ').trim(); + const text = String(value ?? '') + .replace(MCP_PRIVATE_PATH_GLOBAL, '[redacted]') + .replace(MCP_SECRET_MATERIAL_GLOBAL, '[redacted]') + .normalize('NFKC') + .replace(/\s+/gu, ' ') + .trim(); return text.slice(0, maxLength); } function mcpSafeLocator(value) { const raw = String(value ?? ''); if (!raw) return null; - if (MCP_PRIVATE_MATERIAL.test(raw) || raw.startsWith('file:')) return fingerprintJson(raw); + if (mcpContainsPrivateMaterial(raw) || raw.startsWith('file:')) return fingerprintJson(raw); return mcpSanitizeString(raw, 240); } @@ -7204,6 +8309,99 @@ function mcpNoWritePayload({ command, workspaceId, generatedAt, data }) { }; } +function mcpStructuralPayload({ command, workspaceId, generatedAt, data }) { + const payload = mcpNoWritePayload({ command, workspaceId, generatedAt, data }); + return { + ...payload, + safeguards: { + ...payload.safeguards, + rawSourceBodiesIncluded: false + } + }; +} + +function nativeRepositorySource(result) { + return { + kind: 'native-persistent-repository-index', + engine: 'memory-recall-native', + registryLocator: result.registryLocator, + operation: result.operation + }; +} + +function nativeRepositorySearchData(result, query, repositoryIds) { + return { + query, + repositoryIds, + repositories: result.repositories, + results: result.results, + resultCount: result.results.length, + perRepository: result.perRepository, + partial: result.partial, + truncated: result.truncated, + measurements: result.measurements, + source: nativeRepositorySource(result) + }; +} + +function nativeRepositoryListData(result) { + return { + scope: 'repositories', + repositories: result.repositories, + repositoryCount: result.repositories.length, + partial: result.partial, + truncated: result.truncated, + measurements: result.measurements, + source: nativeRepositorySource(result) + }; +} + +function nativeCrossRepositoryData(result, crossRepository) { + return { + crossRepository, + repositories: result.repositories, + modules: result.goModules, + relationships: result.goRelationships, + paths: result.paths, + impactedNodes: result.impactedNodes, + partial: result.partial, + truncated: result.truncated, + measurements: result.measurements, + source: nativeRepositorySource(result) + }; +} + +function nativeNodeMatchesKinds(item, requestedKinds) { + if (!requestedKinds?.length) return true; + const normalized = item.kind === 'file' + ? 'file' + : item.kind === 'module' || item.kind === 'package' || item.kind === 'namespace' + ? 'module' + : 'symbol'; + return requestedKinds.includes(normalized); +} + +function nativeIndexStatusData(result) { + return { + status: result.state, + freshness: result.freshness, + activeGeneration: result.activeGeneration, + indexLocator: result.indexLocator, + fileCount: result.summary.fileCount, + nodeCount: result.summary.nodeCount, + edgeCount: result.summary.edgeCount, + unresolvedCount: result.summary.unresolvedCount, + omittedCount: result.summary.omittedCount, + databaseBytes: result.summary.databaseBytes, + diagnostics: (result.diagnostics ?? []).slice(0, 32).map((diagnostic) => ({ + code: mcpSanitizeString(diagnostic.code, 80), + ...(Number.isSafeInteger(diagnostic.count) && diagnostic.count >= 0 ? { count: diagnostic.count } : {}) + })), + health: result.health, + source: nativeIndexSource(result) + }; +} + async function createMcpStatsRecorder({ values, root, workspaceId, generatedAt }) { const statsPath = await resolveWorkspaceStatsPath(root, option(values, '--stats') ?? '.local/mcp-stats.jsonl', 'mcp server stats', { mustExist: false }); const sessionId = `mcpsess_${randomUUID().replaceAll('-', '').slice(0, 24)}`; @@ -7464,43 +8662,17 @@ function mcpToolJsonResult(payload) { } function mcpToolTextResult(text) { - if (MCP_PRIVATE_MATERIAL.test(text)) throw new Error('mcp tool output contains private material'); + if (mcpContainsPrivateMaterial(text)) throw new Error('mcp tool output contains private material'); return { content: [{ type: 'text', text }] }; } async function mcpInstallCommand(values) { - if (values.includes('--write')) { - console.error('mcp install uses --apply with --confirm; --write is not supported'); - process.exitCode = 2; - return; - } - const format = option(values, '--format') ?? 'json'; - if (format !== 'json') { - console.error('mcp install only supports --format json'); - process.exitCode = 2; - return; - } - const client = normalizeMcpInstallClient(option(values, '--client')); - const apply = values.includes('--apply'); - if (apply && values.includes('--dry-run')) { - console.error('mcp install accepts either dry-run/default or --apply, not both'); - process.exitCode = 2; - return; - } - const allowedFlags = new Set(['--client', '--server', '--home', '--config', '--root', '--sqlite', '--stats', '--format', '--dry-run', '--apply', '--confirm']); - const valueFlags = new Set(['--client', '--server', '--home', '--config', '--root', '--sqlite', '--stats', '--format', '--confirm']); - const unsupported = unsupportedFlags(values, allowedFlags, valueFlags); - if (unsupported.length) { - console.error(`mcp install unsupported option: ${unsupported[0]}`); - process.exitCode = 2; - return; - } + const { client, apply, home, configPath } = parseMcpConfigMutationOptions(values, 'install'); const root = path.resolve(option(values, '--root') ?? process.cwd()); const rootStat = await stat(root).catch(() => null); if (!rootStat?.isDirectory()) throw new Error('mcp install --root must point at a local workspace directory'); const sqlitePath = await resolveWorkspaceSqlitePath(root, option(values, '--sqlite') ?? '.local/memory.sqlite', 'mcp install', { mustExist: false }); const statsPath = await resolveWorkspaceStatsPath(root, option(values, '--stats') ?? '.local/mcp-stats.jsonl', 'mcp install', { mustExist: false }); - const home = option(values, '--home') ?? process.env.HOME ?? process.cwd(); const setup = await buildHarnessSetupReport({ action: 'plan', client: client.id, @@ -7510,22 +8682,116 @@ async function mcpInstallCommand(values) { bridgeMode: 'token-saver', generatedAt: fixedNow() }); - const installPlan = await buildPortableMcpInstallPlan({ setup, client, root, sqlitePath, statsPath, home, configPath: option(values, '--config') ?? client.configPath }); + const installPlan = await buildPortableMcpInstallPlan({ setup, client, root, sqlitePath, statsPath, home, configPath }); const preview = buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, statsPath, apply, applied: false, localFilesWritten: 0 }); const confirm = option(values, '--confirm'); + if (apply && installPlan.status.server === 'drifted') { + console.error('mcp install refuses to replace a drifted server entry; remove or rename that entry manually after reviewing it'); + process.exitCode = 2; + return; + } if (apply && confirm !== preview.planFingerprint) { console.error('mcp install --apply requires --confirm from a dry-run preview'); process.exitCode = 2; return; } if (apply) { - await applyMcpInstallConfig({ home, client, configPath: option(values, '--config') ?? client.configPath, server: setup.server, desiredServer: installPlan.desiredServer }); - console.log(JSON.stringify(buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, statsPath, apply, applied: true, localFilesWritten: 1 }), null, 2)); + const result = await applyMcpInstallConfig({ + home, + client, + configPath, + server: setup.server, + desiredServer: installPlan.desiredServer, + expectedPreimageFingerprint: installPlan.configPreimageFingerprint, + generatedAt: setup.generatedAt + }); + console.log(JSON.stringify(buildMcpInstallReport({ + setup, + installPlan, + client, + root, + sqlitePath, + statsPath, + apply, + applied: result.changed, + localFilesWritten: result.localFilesWritten, + backupRef: result.backupRef + }), null, 2)); + return; + } + console.log(JSON.stringify(preview, null, 2)); +} + +async function mcpUninstallCommand(values) { + const { client, apply, home, configPath } = parseMcpConfigMutationOptions(values, 'uninstall'); + const setup = await buildHarnessSetupReport({ + action: 'plan', + client: client.id, + server: option(values, '--server') ?? 'oaf', + home, + configPath: option(values, '--config'), + bridgeMode: 'token-saver', + generatedAt: fixedNow() + }); + const uninstallPlan = await buildPortableMcpUninstallPlan({ setup, client, home, configPath }); + const preview = buildMcpUninstallReport({ setup, uninstallPlan, client, apply, applied: false, localFilesWritten: 0 }); + if (apply && uninstallPlan.status.server === 'drifted') { + console.error('mcp uninstall refuses to remove a drifted or unowned server entry'); + process.exitCode = 2; + return; + } + if (apply && uninstallPlan.status.server === 'absent') { + console.error('mcp uninstall found no exact owned server entry to remove'); + process.exitCode = 2; + return; + } + if (apply && option(values, '--confirm') !== preview.planFingerprint) { + console.error('mcp uninstall --apply requires --confirm from a dry-run preview'); + process.exitCode = 2; + return; + } + if (apply) { + const result = await removeMcpInstallConfig({ + home, + client, + configPath, + server: setup.server, + expectedPreimageFingerprint: uninstallPlan.configPreimageFingerprint, + generatedAt: setup.generatedAt + }); + console.log(JSON.stringify(buildMcpUninstallReport({ + setup, + uninstallPlan, + client, + apply, + applied: result.changed, + localFilesWritten: result.localFilesWritten, + backupRef: result.backupRef + }), null, 2)); return; } console.log(JSON.stringify(preview, null, 2)); } +function parseMcpConfigMutationOptions(values, action) { + if (values.includes('--write')) throw new Error(`mcp ${action} uses --apply with --confirm; --write is not supported`); + if ((option(values, '--format') ?? 'json') !== 'json') throw new Error(`mcp ${action} only supports --format json`); + const apply = values.includes('--apply'); + if (apply && values.includes('--dry-run')) throw new Error(`mcp ${action} accepts either dry-run/default or --apply, not both`); + const installOnly = action === 'install' ? ['--root', '--sqlite', '--stats'] : []; + const allowedFlags = new Set(['--client', '--server', '--home', '--config', '--format', '--dry-run', '--apply', '--confirm', ...installOnly]); + const valueFlags = new Set(['--client', '--server', '--home', '--config', '--format', '--confirm', ...installOnly]); + const unsupported = unsupportedFlags(values, allowedFlags, valueFlags); + if (unsupported.length) throw new Error(`mcp ${action} unsupported option: ${unsupported[0]}`); + const client = normalizeMcpInstallClient(option(values, '--client')); + return { + client, + apply, + home: option(values, '--home') ?? process.env.HOME ?? process.cwd(), + configPath: option(values, '--config') ?? client.configPath + }; +} + function normalizeMcpInstallClient(value) { const aliases = new Map([['claude', 'claude-code']]); const id = aliases.get(String(value ?? '').trim()) ?? String(value ?? '').trim(); @@ -7545,6 +8811,8 @@ async function buildPortableMcpInstallPlan({ setup, client, root, sqlitePath, st 'mcp', 'server', '--read-only', + '--engine', + 'native', '--root', realRoot, '--sqlite', @@ -7558,20 +8826,26 @@ async function buildPortableMcpInstallPlan({ setup, client, root, sqlitePath, st externalWrites: false }; const serverConfig = { command: desiredServer.command, args: desiredServer.args }; - const status = await classifyMcpInstallServer({ home, client, configPath, server: setup.server, desiredServer }).catch(() => setup.status.server); + const configState = await readMcpInstallConfigState({ home, client, configPath, server: setup.server }); + const status = classifyMcpInstallServer({ configState, desiredServer }); const diffOperations = status === 'installed' ? [] : [{ - op: status === 'absent' ? 'add' : 'replace', + op: status === 'absent' ? 'add' : status === 'upgradeable' ? 'replace' : 'conflict', target: client.format === 'toml' ? `mcp_servers.${setup.server}` : `mcpServers.${setup.server}`, before: status, after: 'read-only-oaf-mcp-stdio', - summary: `${status === 'absent' ? 'add' : 'replace'} ${setup.server} with read-only OAF MCP stdio token-saver server` + summary: status === 'absent' + ? `add ${setup.server} as read-only OAF MCP stdio token-saver server` + : status === 'upgradeable' + ? `upgrade ${setup.server} from the owned native alias to the canonical native engine` + : `refuse to replace drifted ${setup.server} server entry` }]; return { desiredServer, workspaceRoot: realRoot, sqlitePath, + configPreimageFingerprint: fingerprintMcpConfigPreimage(configState), status: { ...setup.status, server: status @@ -7590,6 +8864,44 @@ async function buildPortableMcpInstallPlan({ setup, client, root, sqlitePath, st }; } +async function buildPortableMcpUninstallPlan({ setup, client, home, configPath }) { + const configState = await readMcpInstallConfigState({ home, client, configPath, server: setup.server }); + const status = !configState.serverConfig + ? 'absent' + : isOwnedMcpInstallServer(configState.serverConfig) ? 'installed' : 'drifted'; + const target = client.format === 'toml' ? `mcp_servers.${setup.server}` : `mcpServers.${setup.server}`; + const operations = status === 'installed' + ? [{ op: 'remove', target, before: 'installed', after: 'absent', summary: `remove exact Memory Recall-owned ${setup.server} server entry` }] + : status === 'drifted' + ? [{ op: 'conflict', target, before: 'drifted', after: 'unchanged', summary: `refuse to remove drifted or unowned ${setup.server} server entry` }] + : []; + return { + workspaceRoot: null, + desiredServer: null, + configPreimageFingerprint: fingerprintMcpConfigPreimage(configState), + status: { ...setup.status, server: status }, + diff: { + ...setup.diff, + operations, + preview: operations.map((operation) => operation.summary) + } + }; +} + +function isOwnedMcpInstallServer(serverConfig) { + if (serverConfig?.command !== process.execPath || !Array.isArray(serverConfig.args)) return false; + const args = serverConfig.args; + return args.length === 13 && + args[0] === CLI_PATH && + arraysEqual(args.slice(1, 5), ['mcp', 'server', '--read-only', '--engine']) && + ['native', 'auto', 'native-preview'].includes(args[5]) && + args[6] === '--root' && + path.isAbsolute(args[7]) && + args[8] === '--sqlite' && path.isAbsolute(args[9]) && + args[10] === '--stats' && path.isAbsolute(args[11]) && + args[12] === '--stdio'; +} + function buildMcpInstallManualConfigSnippet({ client, server, configRef, serverConfig }) { let content; if (client.format === 'toml') { @@ -7607,31 +8919,29 @@ function buildMcpInstallManualConfigSnippet({ client, server, configRef, serverC }; } -async function classifyMcpInstallServer({ home, client, configPath, server, desiredServer }) { - const existing = await readMcpInstallServerConfig({ home, client, configPath, server }); +function classifyMcpInstallServer({ configState, desiredServer }) { + const existing = configState.serverConfig; if (!existing) return 'absent'; if (existing.command === desiredServer.command && arraysEqual(existing.args, desiredServer.args)) return 'installed'; + if (isOwnedMcpInstallServer(existing)) return 'upgradeable'; return 'drifted'; } -async function readMcpInstallServerConfig({ home, client, configPath, server }) { - const realHome = await realpath(home); - if (path.isAbsolute(configPath) || configPath.includes('..')) throw new Error('mcp install config path must stay inside --home'); - const target = path.resolve(realHome, configPath); - if (!isInside(realHome, target)) throw new Error('mcp install config path escapes --home'); - const text = await readFile(target, 'utf8').catch((error) => { - if (error.code === 'ENOENT') return null; - throw error; - }); - if (text === null) return null; +async function readMcpInstallConfigState({ home, client, configPath, server }) { + const current = await readHomeFile(home, configPath); + const text = current.text; + let serverConfig = null; if (client.format === 'json') { const parsed = JSON.parse(text || '{}'); const existing = parsed?.mcpServers?.[server]; - if (existing && typeof existing === 'object' && !Array.isArray(existing)) return existing; - return null; + if (existing && typeof existing === 'object' && !Array.isArray(existing)) serverConfig = existing; } - if (client.format === 'toml') return readMcpInstallTomlServerConfig(text, server); - return null; + if (client.format === 'toml') serverConfig = readMcpInstallTomlServerConfig(text, server); + return { exists: current.exists, text, serverConfig }; +} + +function fingerprintMcpConfigPreimage(configState) { + return fingerprintJson({ exists: configState.exists, text: configState.text }); } function readMcpInstallTomlServerConfig(text, server) { @@ -7658,7 +8968,7 @@ function arraysEqual(left, right) { return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => item === right[index]); } -function buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, statsPath, apply, applied, localFilesWritten }) { +function buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, statsPath, apply, applied, localFilesWritten, backupRef = null }) { const reportBase = { schemaVersion: '1.0.0', command: 'mcp install', @@ -7667,7 +8977,8 @@ function buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, s apply: { requested: apply, confirmed: apply, - applied + applied, + backupRef }, client: setup.client, clientLabel: setup.clientLabel, @@ -7688,6 +8999,8 @@ function buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, s config: setup.config, status: installPlan.status, desiredServer: installPlan.desiredServer, + indexBuildCommand: `recall graph index --write --engine native --root ${JSON.stringify(installPlan.workspaceRoot)} --format summary`, + configPreimageFingerprint: installPlan.configPreimageFingerprint, manualConfigSnippet: installPlan.manualConfigSnippet, reversal: { mode: 'manual', @@ -7706,12 +9019,55 @@ function buildMcpInstallReport({ setup, installPlan, client, root, sqlitePath, s return { ...reportBase, planFingerprint, - nextCommand: apply || applied + nextCommand: apply || applied || installPlan.status.server === 'drifted' || installPlan.status.server === 'installed' + ? null + : `recall mcp install --client ${client.id} --root ${JSON.stringify(root)} --apply --confirm ${planFingerprint} --format json`, + warnings: [ + 'Dry-run is the default; Memory Recall writes home config only with --apply and matching --confirm.', + 'mcp install configures the packaged native engine but never builds or refreshes an index. Run indexBuildCommand explicitly before using structural tools.', + 'Structural tools fail with an actionable build, refresh, repair, or package error. The MCP server remains local stdio and read-only.' + ] + }; +} + +function buildMcpUninstallReport({ setup, uninstallPlan, client, apply, applied, localFilesWritten, backupRef = null }) { + const reportBase = { + schemaVersion: '1.0.0', + command: 'mcp uninstall', + generatedAt: setup.generatedAt, + dryRun: !apply, + apply: { + requested: apply, + confirmed: apply, + applied, + backupRef + }, + client: setup.client, + clientLabel: setup.clientLabel, + server: setup.server, + bridgeMode: setup.bridgeMode, + config: setup.config, + configPreimageFingerprint: uninstallPlan.configPreimageFingerprint, + status: uninstallPlan.status, + desiredServer: uninstallPlan.desiredServer, + diff: uninstallPlan.diff, + safeguards: { + ...setup.safeguards, + localFilesWritten, + homeConfigMutated: applied, + workspaceStateMutated: false + } + }; + const planFingerprint = fingerprintMcpInstallPlan(reportBase); + return { + ...reportBase, + planFingerprint, + nextCommand: apply || applied || uninstallPlan.status.server !== 'installed' ? null - : `npm run oaf -- mcp install --client ${client.id} --root ${JSON.stringify(root)} --apply --confirm ${planFingerprint} --format json`, + : `recall mcp uninstall --client ${client.id} --apply --confirm ${planFingerprint} --format json`, warnings: [ - 'Dry-run is the default; OAF writes home config only with --apply and matching --confirm.', - 'Review the config before applying. The MCP server is local stdio and read-only.' + 'Dry-run is the default; Memory Recall removes only an exact owned server entry after matching confirmation.', + 'Workspace .local data is never removed by this command.' ] }; } @@ -7724,27 +9080,40 @@ function fingerprintMcpInstallPlan(report) { bridgeMode: report.bridgeMode, workspaceRootRef: report.workspaceRootRef, config: report.config, + configPreimageFingerprint: report.configPreimageFingerprint, status: report.status, desiredServer: report.desiredServer, diff: report.diff }); } -async function applyMcpInstallConfig({ home, client, configPath, server, desiredServer }) { - const realHome = await realpath(home); - if (path.isAbsolute(configPath) || configPath.includes('..')) throw new Error('mcp install config path must stay inside --home'); - const target = path.resolve(realHome, configPath); - if (!isInside(realHome, target)) throw new Error('mcp install config path escapes --home'); - await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }); - const current = await readFile(target, 'utf8').catch((error) => { - if (error.code === 'ENOENT') return null; - throw error; - }); +async function applyMcpInstallConfig({ home, client, configPath, server, desiredServer, expectedPreimageFingerprint, generatedAt }) { + const current = await readMcpInstallConfigState({ home, client, configPath, server }); + if (fingerprintMcpConfigPreimage(current) !== expectedPreimageFingerprint) throw new Error('mcp install config changed after preview; run a new dry-run'); const serverConfig = { command: desiredServer.command, args: desiredServer.args }; const next = client.format === 'toml' - ? mergeMcpInstallToml(current ?? '', server, serverConfig) - : mergeMcpInstallJson(current ?? '{}', server, serverConfig); - await writeFile(target, next, { mode: 0o600 }); + ? mergeMcpInstallToml(current.text, server, serverConfig) + : mergeMcpInstallJson(current.text || '{}', server, serverConfig); + return writeMcpInstallConfig({ home, configPath, current, next, generatedAt }); +} + +async function removeMcpInstallConfig({ home, client, configPath, server, expectedPreimageFingerprint, generatedAt }) { + const current = await readMcpInstallConfigState({ home, client, configPath, server }); + if (fingerprintMcpConfigPreimage(current) !== expectedPreimageFingerprint) throw new Error('mcp uninstall config changed after preview; run a new dry-run'); + const next = client.format === 'toml' + ? removeMcpInstallToml(current.text, server) + : removeMcpInstallJson(current.text, server); + return writeMcpInstallConfig({ home, configPath, current, next, generatedAt }); +} + +async function writeMcpInstallConfig({ home, configPath, current, next, generatedAt }) { + if (next === current.text) return { changed: false, localFilesWritten: 0, backupRef: null }; + const { root, absolute } = await resolveHomePath(home, configPath); + await assertNoSymlinkAncestors(root, configPath); + await mkdir(path.dirname(absolute), { recursive: true, mode: 0o700 }); + const backupRef = current.exists ? await writeHomeBackup({ home, relativePath: configPath, text: current.text, generatedAt }) : null; + await writePrivateFileAtomic(absolute, next); + return { changed: true, localFilesWritten: backupRef ? 2 : 1, backupRef }; } function mergeMcpInstallJson(text, server, serverConfig) { @@ -7766,6 +9135,22 @@ function mergeMcpInstallToml(text, server, serverConfig) { return `${prefix ? `${prefix}\n\n` : ''}${section}`; } +function removeMcpInstallJson(text, server) { + const parsed = JSON.parse(text || '{}'); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('mcp uninstall JSON config must be an object'); + const mcpServers = parsed.mcpServers && typeof parsed.mcpServers === 'object' && !Array.isArray(parsed.mcpServers) + ? { ...parsed.mcpServers } + : {}; + delete mcpServers[server]; + return `${JSON.stringify({ ...parsed, mcpServers }, null, 2)}\n`; +} + +function removeMcpInstallToml(text, server) { + const escaped = server.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const sectionPattern = new RegExp(`(?:^|\\n)\\[mcp_servers\\.${escaped}\\]\\n(?:[^\\[]|\\[(?!mcp_servers\\.))*`, 'u'); + return text.replace(sectionPattern, (match) => match.startsWith('\n') ? '\n' : '').replace(/^\n+|\n+$/gu, '').concat('\n'); +} + async function mcpSmokeCommand(values) { const [target, ...rest] = values; if (target !== 'context-pack') { @@ -8412,8 +9797,23 @@ async function loadMcpContextPackRegistryStatus(values, { root, workspaceId }) { return report.registry.exists || report.currentPointer.exists ? report : null; } -async function buildMcpContextPackResource(values, { root, workspaceId }) { +async function buildMcpContextPackResource(values, { root, workspaceId, sourceGraphPreview = null }) { if (!values.includes('--context-pack')) return null; + const contextPackFd = option(values, '--context-pack-fd'); + if (contextPackFd !== null) { + if (!values.includes('--stdio') || contextPackFd !== '3') { + throw new Error('prebuilt context pack input is available only to internal MCP stdio verification'); + } + const serialized = await readBoundedFileDescriptor(3, MCP_CONTEXT_PACK_AUX_MAX_BYTES, 'prebuilt context pack'); + let pack; + try { + pack = JSON.parse(serialized); + } catch { + throw new Error('prebuilt context pack is not valid JSON'); + } + assertJsonSchema(contextPackSchema, pack, 'prebuilt context pack'); + return { pack, markdown: renderContextPackMarkdown(pack) }; + } const objective = option(values, '--objective'); const step = option(values, '--step'); if (!objective || !step) { @@ -8435,6 +9835,7 @@ async function buildMcpContextPackResource(values, { root, workspaceId }) { step, targetHarness, tokenBudget, + sourceGraphPreview, clock: fixedNow }); return { @@ -8443,7 +9844,7 @@ async function buildMcpContextPackResource(values, { root, workspaceId }) { }; } -async function buildMcpContextPackSmokeReport(values, { objective, step, fixedTimestamp = null }) { +async function buildMcpContextPackSmokeReport(values, { objective, step, fixedTimestamp = null, contextPack = null }) { const root = option(values, '--root') ?? process.cwd(); const workspaceId = option(values, '--workspace') ?? 'ws_local'; const targetHarness = option(values, '--target') ?? option(values, '--target-harness') ?? 'generic'; @@ -8480,6 +9881,7 @@ async function buildMcpContextPackSmokeReport(values, { objective, step, fixedTi for (const value of options(values, '--changed-locator')) childArgs.push('--changed-locator', value); if (gitChangedLocatorsRequested(values)) childArgs.push('--changed-from-git'); if (option(values, '--changed-shard')) childArgs.push('--changed-shard', String(changedShard(values))); + if (contextPack) childArgs.push('--context-pack-fd', '3'); const messages = [ { jsonrpc: '2.0', id: 1, method: 'initialize' }, @@ -8490,7 +9892,8 @@ async function buildMcpContextPackSmokeReport(values, { objective, step, fixedTi ]; const started = process.hrtime.bigint(); const child = await runCliStdio(childArgs, messages.map((message) => JSON.stringify(message)).join('\n'), { - env: fixedTimestamp ? { ...process.env, OAF_FIXED_NOW: fixedTimestamp } : process.env + env: fixedTimestamp ? { ...process.env, OAF_FIXED_NOW: fixedTimestamp } : process.env, + auxiliaryInput: contextPack ? JSON.stringify(contextPack) : null }); const durationMs = Math.max(0, Math.round(Number(process.hrtime.bigint() - started) / 1_000_000)); if (child.code !== 0) { @@ -8606,6 +10009,15 @@ async function buildContextHandoffReport(values, { objective, step }) { const tokenBudget = parseIntegerOption(values, '--token-budget', parseIntegerOption(values, '--budget', 4096)); const generatedAt = fixedNow(); const { changedLocators, detection } = await resolveChangedLocators(values, { root, workspaceId }); + const sourceGraphPreview = await currentNativeSourceGraphPreviewIfReady({ + root, + workspaceId, + query: `${objective} ${step}`, + changedLocators, + limit: 12, + sampleLimit: 1, + clock: () => generatedAt + }); const pack = await buildContextPack({ root, harnesses: sourceHarnesses, @@ -8616,11 +10028,12 @@ async function buildContextHandoffReport(values, { objective, step }) { step, targetHarness, tokenBudget, + sourceGraphPreview, clock: () => generatedAt }); const usePlan = buildContextPackUsePlan(pack); assertJsonSchema(contextPackUsePlanSchema, usePlan, 'context-pack handoff use plan'); - const smoke = await buildMcpContextPackSmokeReport(values, { objective, step, fixedTimestamp: generatedAt }); + const smoke = await buildMcpContextPackSmokeReport(values, { objective, step, fixedTimestamp: generatedAt, contextPack: pack }); const memoryProposalPreflight = await buildMemoryProposalPreflight(values, { root, workspaceId, generatedAt }); const skillCatalog = await buildSkillCatalogPreflight({ root, workspaceId, generatedAt }); const setupClient = contextHandoffSetupClient(targetHarness); @@ -9162,6 +10575,15 @@ async function buildContextPackMeasurementReport(values, { objective, step }) { const { changedLocators, detection } = await resolveChangedLocators(values, { root, workspaceId }); const largeContext = buildLargeContextMeasurement({ values, changedLocators, detection }); const started = process.hrtime.bigint(); + const sourceGraphPreview = await currentNativeSourceGraphPreviewIfReady({ + root, + workspaceId, + query: `${objective} ${step}`, + changedLocators, + limit: 12, + sampleLimit: 1, + clock: () => generatedAt + }); const pack = await buildContextPack({ root, harnesses: sourceHarnesses, @@ -9172,10 +10594,11 @@ async function buildContextPackMeasurementReport(values, { objective, step }) { step, targetHarness, tokenBudget, + sourceGraphPreview, clock: () => generatedAt }); const buildDurationMs = Math.max(0, Math.round(Number(process.hrtime.bigint() - started) / 1_000_000)); - const smoke = await buildMcpContextPackSmokeReport(values, { objective, step, fixedTimestamp: generatedAt }); + const smoke = await buildMcpContextPackSmokeReport(values, { objective, step, fixedTimestamp: generatedAt, contextPack: pack }); const usePlan = buildContextPackUsePlan(pack, { generatedAt }); const impactBrief = buildContextPackImpactBrief(pack, { generatedAt, @@ -9425,6 +10848,7 @@ function runCliStdio( input, { env = process.env, + auxiliaryInput = null, timeoutMs = MCP_STDIO_CHILD_TIMEOUT_MS, maxStdoutBytes = MCP_STDIO_CHILD_MAX_STDOUT_BYTES, maxStderrBytes = MCP_STDIO_CHILD_MAX_STDERR_BYTES @@ -9434,11 +10858,15 @@ function runCliStdio( if (inputBytes > MCP_STDIO_MAX_STDIN_BYTES) { return Promise.reject(new Error(`mcp stdio child input exceeded ${MCP_STDIO_MAX_STDIN_BYTES} bytes`)); } + const auxiliaryBytes = auxiliaryInput === null ? 0 : Buffer.byteLength(auxiliaryInput, 'utf8'); + if (auxiliaryBytes > MCP_CONTEXT_PACK_AUX_MAX_BYTES) { + return Promise.reject(new Error(`mcp stdio auxiliary input exceeded ${MCP_CONTEXT_PACK_AUX_MAX_BYTES} bytes`)); + } return new Promise((resolve, reject) => { const child = spawn(process.execPath, nodeArgs, { cwd: process.cwd(), env, - stdio: ['pipe', 'pipe', 'pipe'] + stdio: auxiliaryInput === null ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe', 'pipe'] }); const stdout = []; const stderr = []; @@ -9482,6 +10910,12 @@ function runCliStdio( child.stdin.on('error', (error) => { if (!settled) fail(error); }); + if (auxiliaryInput !== null) { + child.stdio[3].on('error', (error) => { + if (!settled) fail(error); + }); + child.stdio[3].end(auxiliaryInput); + } child.on('error', fail); child.on('close', (code) => { if (settled) return; @@ -10092,7 +11526,7 @@ function cleanDecisionObject(value) { .replace(/[.;:,]+$/u, '') .trim() .slice(0, 240); - if (!object || MCP_PRIVATE_MATERIAL.test(object)) return null; + if (!object || mcpContainsPrivateMaterial(object)) return null; return object; } @@ -10145,14 +11579,21 @@ function dedupeMemoryIngestEpisodes(episodes) { } async function collectSourceGraphMemoryFacts(root, workspaceId, generatedAt, projectSubject) { - const preview = await buildSourceGraphPreview({ - root, - workspaceId, - query: 'memory context mcp', - sampleLimit: 12, - maxFiles: DEFAULT_SOURCE_GRAPH_PREVIEW_MAX_FILES, - clock: () => generatedAt - }); + let preview; + try { + preview = await buildCurrentNativeSourceGraphPreview({ + root, + workspaceId, + query: 'memory context mcp', + sampleLimit: 12, + clock: () => generatedAt + }); + } catch (error) { + // Memory ingestion is useful before a repository has been indexed. It must + // not manufacture graph facts or fall back to the retired JS engine. + if (isNativeIndexRecoveryRequired(error)) return []; + throw error; + } const summary = preview.graph?.summary ?? {}; const facts = [ factTriple(projectSubject, 'source_graph_files', `files_${Math.max(0, Number(summary.fileCount ?? 0))}`), @@ -10166,6 +11607,28 @@ async function collectSourceGraphMemoryFacts(root, workspaceId, generatedAt, pro return facts; } +function isNativeIndexRecoveryRequired(error) { + return [ + // Read-only memory and handoff operations remain useful before a matching + // platform package has been installed. They must report no graph facts, + // rather than resurrecting the retired JS engine or failing unrelated + // memory work. + 'native_platform_package_missing', + 'native_platform_unsupported', + 'native_engine_unavailable', + 'native_engine_checksum_mismatch', + 'native_engine_manifest_invalid', + 'native_engine_path_invalid', + 'native_engine_version_mismatch', + 'source_index_build_required', + 'source_index_refresh_required', + 'source_index_repair_required', + 'source_index_migration_required', + 'source_index_wrong_repository', + 'source_index_schema_newer' + ].includes(String(error?.code ?? error?.message ?? '')); +} + function factTriple(subject, predicate, object) { return `${safeFactToken(subject, 'subject')} ${safeFactToken(predicate, 'predicate')} ${safeFactObjectToken(object, 'object')}.`; } @@ -10477,6 +11940,8 @@ async function readHomeFile(home, relativePath) { } async function writeHomeFileIfChanged({ home, relativePath, current, nextText, generatedAt, role }) { + const latest = await readHomeFile(home, relativePath); + if (fingerprintMcpConfigPreimage(latest) !== fingerprintMcpConfigPreimage(current)) throw new Error('home config changed after preflight; retry the command'); if ((current.text ?? '') === nextText) return { role, target: `home://${toPosix(relativePath)}`, @@ -10487,14 +11952,9 @@ async function writeHomeFileIfChanged({ home, relativePath, current, nextText, g }; const { root, absolute } = await resolveHomePath(home, relativePath); await assertNoSymlinkAncestors(root, relativePath); - await mkdir(path.dirname(absolute), { recursive: true }); + await mkdir(path.dirname(absolute), { recursive: true, mode: 0o700 }); const backupRef = current.exists ? await writeHomeBackup({ home, relativePath, text: current.text, generatedAt }) : null; - const existing = await lstat(absolute).catch((error) => { - if (error.code === 'ENOENT') return null; - throw error; - }); - if (existing?.isSymbolicLink()) throw new Error(`home config target is a symlink: ${relativePath}`); - await writeFile(absolute, nextText, 'utf8'); + await writePrivateFileAtomic(absolute, nextText); return { role, target: `home://${toPosix(relativePath)}`, @@ -10506,15 +11966,31 @@ async function writeHomeFileIfChanged({ home, relativePath, current, nextText, g } async function writeHomeBackup({ home, relativePath, text, generatedAt }) { - const suffix = `${generatedAt.replace(/[^0-9A-Za-z_-]/gu, '-')}-${createHash('sha256').update(text).digest('hex').slice(0, 8)}`; + const suffix = `${generatedAt.replace(/[^0-9A-Za-z_-]/gu, '-')}-${createHash('sha256').update(text).digest('hex').slice(0, 8)}-${randomUUID().slice(0, 8)}`; const backupRelative = `${relativePath}.oaf-backup-${suffix}`; const { root, absolute } = await resolveHomePath(home, backupRelative); await assertNoSymlinkAncestors(root, backupRelative); - await mkdir(path.dirname(absolute), { recursive: true }); - await writeFile(absolute, text, 'utf8'); + await mkdir(path.dirname(absolute), { recursive: true, mode: 0o700 }); + await writePrivateFileAtomic(absolute, text); return `home://${toPosix(backupRelative)}`; } +async function writePrivateFileAtomic(target, text) { + const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${randomUUID()}.tmp`); + let handle; + try { + handle = await open(temporary, 'wx', 0o600); + await handle.writeFile(text, 'utf8'); + await handle.sync(); + await handle.close(); + handle = null; + await renameFile(temporary, target); + } finally { + await handle?.close().catch(() => {}); + await rm(temporary, { force: true }).catch(() => {}); + } +} + async function resolveHomePath(home, relativePath) { if (!relativePath || path.isAbsolute(relativePath) || relativePath.includes('..')) throw new Error(`home config path is unsupported: ${relativePath}`); const root = await realpath(home); @@ -10738,11 +12214,43 @@ function runNode(nodeArgs, { cwd = PACKAGE_ROOT, env = process.env } = {}) { const [script, ...rest] = nodeArgs; const resolvedScript = path.isAbsolute(script) ? script : path.join(PACKAGE_ROOT, script); const child = spawn(process.execPath, [resolvedScript, ...rest], { stdio: 'inherit', env, cwd }); - child.on('error', reject); - child.on('exit', (code) => resolve(code ?? 1)); + const forwardSignal = (signal) => { + if (!child.killed) child.kill(signal); + }; + const onSigint = () => forwardSignal('SIGINT'); + const onSigterm = () => forwardSignal('SIGTERM'); + const cleanup = () => { + process.removeListener('SIGINT', onSigint); + process.removeListener('SIGTERM', onSigterm); + }; + process.once('SIGINT', onSigint); + process.once('SIGTERM', onSigterm); + child.once('error', (error) => { + cleanup(); + reject(error); + }); + child.once('exit', (code, signal) => { + cleanup(); + resolve(code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1)); + }); }); } +async function readBoundedFileDescriptor(fd, maxBytes, label) { + const stream = createReadStream(null, { fd, autoClose: false }); + const chunks = []; + let total = 0; + for await (const chunk of stream) { + total += chunk.length; + if (total > maxBytes) { + stream.destroy(); + throw new Error(`${label} exceeded ${maxBytes} bytes`); + } + chunks.push(chunk); + } + return Buffer.concat(chunks).toString('utf8'); +} + function isHelpCommand(value) { return ['help', '--help', '-h'].includes(value); } diff --git a/apps/web/api.js b/apps/web/api.js new file mode 100644 index 00000000..fbfc9445 --- /dev/null +++ b/apps/web/api.js @@ -0,0 +1,54 @@ +export class ApiRequestError extends Error { + constructor(message, { status = null, code = '', correlationId = '', issues = [] } = {}) { + super(message); + this.name = 'ApiRequestError'; + this.status = status; + this.code = code; + this.correlationId = correlationId; + this.issues = issues; + } +} + +export async function requestJson(path, { method = 'GET', body, signal, headers: inputHeaders } = {}) { + const headers = new Headers(inputHeaders ?? {}); + if (body !== undefined && !headers.has('content-type')) headers.set('content-type', 'application/json'); + if (!['GET', 'HEAD'].includes(method)) { + const token = csrfToken(); + if (token) headers.set('x-csrf-token', token); + } + const response = await fetch(path, { + method, + body, + signal, + headers, + credentials: 'same-origin' + }); + const payload = await readPayload(response); + if (!response.ok) throw apiError(response, payload); + return payload; +} + +export function csrfToken() { + return /(?:^|;\s*)oaf_csrf=([^;]+)/u.exec(globalThis.document?.cookie ?? '')?.[1] ?? ''; +} + +async function readPayload(response) { + return response.clone().json().catch(() => null); +} + +function apiError(response, payload) { + const code = payload?.error?.code ?? ''; + const message = code === 'bootstrap_required' + ? 'Local owner setup is required.' + : code === 'invalid_credentials' + ? 'Username or password is incorrect.' + : response.status === 401 + ? 'Local authentication required.' + : payload?.error?.message ?? `Request failed with ${response.status}`; + return new ApiRequestError(message, { + status: response.status, + code, + correlationId: payload?.error?.correlationId ?? response.headers.get('x-correlation-id') ?? '', + issues: Array.isArray(payload?.error?.issues) ? payload.error.issues : [] + }); +} diff --git a/apps/web/app.js b/apps/web/app.js index d70b77a9..efd54938 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -1,8 +1,36 @@ -import { navigationItemsFor, navigationOwner, selectOverviewPrimaryAction } from './shell-model.js'; +import { navigationItemsFor, navigationOwner } from './shell-model.js'; +import { csrfToken, requestJson as api } from './api.js'; +import { buildOrientationModel, selectOrientationGroup } from './orientation-model.js'; +import { bindOrientation, renderOrientation } from './orientation-view.js'; +import { + bindSourceMap, + buildMapRequest, + parseMapUrl, + renderSourceMap, + serializeMapUrl +} from './source-map-view.js'; +import { + bindMemoryGraph, + buildMemoryGraphViewModel, + renderMemoryGraphView +} from './memory-graph-view.js'; +import { + buildApiErrorUiModel, + escapeHtml as esc, + formatDate as date, + renderApiErrorPanel, + renderApiErrorRecovery, + safeErrorToken, + shortFingerprint, + statePanel, + titleize +} from './ui-primitives.js'; + +export { buildApiErrorUiModel } from './ui-primitives.js'; export const SHELL_STATES = new Set(['loading','setup','empty','error','denied','stale','partial','success']); -const OAF_CHECKOUT_COMMAND_PREFIX = 'npm --silent run oaf --'; +const OAF_CHECKOUT_COMMAND_PREFIX = 'recall'; const OAF_COMPATIBILITY_URL = 'https://github.com/rebel0789/Memory-Recall/blob/main/docs/usage/oaf-compatibility.md'; const OAF_URI_COMPATIBILITY_NOTE = 'Legacy oaf:// URIs remain supported compatibility identifiers; normal commands use recall.'; @@ -14,7 +42,7 @@ export const ROUTES = [ { id:'fabric-map', path:'/fabric-map', label:'Fabric Map', title:'Fabric Map', description:'Visualize local process flow, context assembly, node handoffs, and disabled external boundaries.' }, { id:'context', path:'/context', label:'Context', title:'Context', description:'Selected and excluded records, budgets, conflicts, assembly, and compiler versions.' }, { id:'context-pack', path:'/context-pack', label:'Context Pack', title:'Context Pack', description:'Build a safe, token-aware handoff for Codex, Claude Code, Cursor, or a generic agent.' }, - { id:'source-graph', path:'/source-graph', label:'Source Graph', title:'Source Graph', description:'Search symbols, trace calls, and inspect likely diff impact from local JS/TS metadata.' }, + { id:'source-graph', path:'/source-graph', label:'Source Graph', title:'Source Graph', description:'Search symbols, trace calls, and inspect likely diff impact from the local source index.' }, { id:'memory', path:'/memory', label:'Memory', title:'Memory', description:'Proposals, active records, supersession, retraction, expiry, and provenance.' }, { id:'memory-graph', path:'/memory-graph', label:'Graph', title:'Memory Graph', description:'Explore current and historical governed memory relationships from the local SQLite store.' }, { id:'evidence', path:'/evidence', label:'Evidence', title:'Evidence', description:'Snapshots, observations, citations, staleness, and inferred pattern boundaries.' }, @@ -61,13 +89,19 @@ let memoryIntakeDraft={sourceLocator:'memory/inbox.md',text:''}; let memoryGraph=null; let memoryGraphError=null; let memoryGraphOptions={history:false,query:'',entity:'',communities:false}; +let memoryGraphCleanup=()=>{}; let contextSourcePreviewResult=null; let contextSourcePreviewError=null; let sourceGraphResult=null; let sourceGraphError=null; +let sourceGraphLoading=false; +let sourceGraphLoadSequence=0; +let sourceMapCleanup=()=>{}; let harnessSetupResult=null; let harnessSetupError=null; let activeFabricNode='context'; +let orientationModel=null; +let orientationCleanup=()=>{}; export function legacyViewPath(view) { return legacyViews.get(String(view??'')) ?? '/'; @@ -160,55 +194,6 @@ function gitDetectionUnavailableMessage(reason) { })[reason] ?? 'Git change detection is unavailable. Retry the local scan.'; } -const API_ISSUE_HINTS = new Map([ - ['$.body.changedLocators',['Changed files','Use workspace-relative paths under this repository, one per line. Keep the list bounded and review it before building.']], - ['$.body.userSelectedFiles',['Explicit files','Use workspace-relative paths under this repository. Do not paste file bodies, absolute paths, credentials, or provider URLs.']], - ['$.body.memoryConfig.memoryPaths',['Memory preflight sources','Use reviewed workspace-relative files only. Do not use absolute paths, URLs, credentials, or generated/local state directories.']], - ['$.body.client',['Client','Choose a supported local harness client from the menu.']], - ['$.body.objective',['Objective','Use a plain task summary. Do not include secrets, provider URLs, session tokens, absolute paths, or hidden reasoning.']], - ['$.body.step',['Step','Use a short current-step label. Do not include secrets, provider URLs, session tokens, absolute paths, or hidden reasoning.']], - ['$.body.tokenBudget',['Token budget','Use a positive number within the field limit.']], - ['$.body.sourceLocator',['Source locator','Use a workspace-relative source file such as notes/memory.md.']], - ['$.body.text',['Memory text','Use simple Fact or Decision lines with safe subject, predicate, and object text.']], - ['$.body.targetHarness',['Target','Choose Codex, Claude Code, Cursor, or Generic agent.']], - ['$.body.from',['Source families','Use supported source families only, such as codex, cursor, or claude-code.']], - ['$.body.workspaceId',['Workspace','Use the current local workspace.']] -]); - -function safeErrorToken(value,fallback,maxLength=120) { - const text=String(value ?? '').trim(); - if(!text)return fallback; - if(/(?:\/Users|\/private|\/var\/folders|https?:|file:|token|secret|api[_-]?key|authorization|cookie)/iu.test(text))return fallback; - const normalized=text.replace(/[^\w$.[\]:-]/gu,'_').slice(0,maxLength); - if(/(?:\/Users|\/private|\/var\/folders|https?:|file:|token|secret|api[_-]?key|authorization|cookie)/iu.test(normalized))return fallback; - return normalized; -} - -function apiIssueHint(path,code) { - const direct=API_ISSUE_HINTS.get(path); - if(direct)return { label:direct[0], detail:direct[1] }; - if(path.startsWith('$.body.'))return { label:titleize(path.slice('$.body.'.length)), detail:'Review this field and use only supported local values.' }; - return { label:'Request field', detail:'Review the highlighted request field and retry with supported local values.' }; -} - -export function buildApiErrorUiModel(errorLike) { - const error=typeof errorLike==='object' && errorLike ? errorLike : { message:String(errorLike ?? 'Request failed.') }; - const message=String(error.message ?? 'Request failed.'); - const issues=Array.isArray(error.issues) ? error.issues.slice(0,5).map((issue)=>{ - const path=safeErrorToken(issue?.path,'$.body'); - const code=safeErrorToken(issue?.code,'validation_failed',64); - return { path, code, ...apiIssueHint(path,code) }; - }) : []; - const correlationId=safeErrorToken(error.correlationId,'',96); - return { - message, - status:Number.isFinite(Number(error.status)) ? Number(error.status) : null, - code:safeErrorToken(error.code,'',64), - correlationId, - issues - }; -} - export const WORKFLOW_STEPS = [ { id:'collect', label:'Collect', detail:'Read bounded local or caller-supplied sources.' }, { id:'normalize', label:'Normalize', detail:'Create observation records and retrieval eligibility.' }, @@ -574,7 +559,8 @@ export function buildMemoryCockpitModel(cockpit = null) { }; } -export function deliveryChangeLabel(percent) { +export function deliveryChangeLabel(percent, baselineTokens = null) { + if (baselineTokens !== null && !(Number(baselineTokens) > 0)) return 'Not measured'; const value=Math.round(Number(percent) || 0); if(value>0)return `${value}% reduction`; if(value<0)return `${Math.abs(value)}% overhead`; @@ -810,8 +796,8 @@ export function buildHarnessSetupUiModel(report = null) { serverStatus:safeText(report?.status?.server ?? 'not checked'), operation:operation ? safeText(operation.summary) : 'No MCP config change needed', operationKind:safeText(operation?.op ?? 'none'), - command:report ? `npm run oaf -- harness setup plan --client ${safeText(report.client)} --server oaf --dry-run --format json` : 'npm run oaf -- harness setup plan --client codex --server oaf --dry-run --format json', - bridgeCommand:report?.desiredServer ? [report.desiredServer.command,...report.desiredServer.args].join(' ') : oafCommand('mcp resources --read-only --stdio'), + command:report ? `recall harness setup plan --client ${safeText(report.client)} --server oaf --dry-run --format json` : 'recall harness setup plan --client codex --server oaf --dry-run --format json', + bridgeCommand:report?.desiredServer ? publicRecallCommand([report.desiredServer.command,...report.desiredServer.args].join(' ')) : oafCommand('mcp resources --read-only --stdio'), manualConfigSnippet:report?.manualConfigSnippet ? { format:safeText(report.manualConfigSnippet.format), configRef:safeText(report.manualConfigSnippet.configRef), @@ -856,40 +842,10 @@ function currentRoute() { return resolveRoute(globalThis.location?.href ?? '/'); } -function csrfToken() { - return /(?:^|;\s*)oaf_csrf=([^;]+)/.exec(globalThis.document?.cookie ?? '')?.[1] ?? ''; -} - export function shouldLoadProtectedShellData({ bootstrapRequired = false, csrfTokenValue = '' } = {}) { return bootstrapRequired === false && String(csrfTokenValue ?? '').trim().length > 0; } -async function api(path, options = {}) { - const headers = new Headers(options.headers ?? {}); - if (options.body && !headers.has('content-type')) headers.set('content-type','application/json'); - const token = csrfToken(); - if (token && options.method && !['GET','HEAD'].includes(options.method)) headers.set('x-csrf-token', token); - const response = await fetch(path, { ...options, headers }); - const payload = await response.clone().json().catch(()=>null); - if (!response.ok) { - const code = payload?.error?.code ?? null; - const message = code === 'bootstrap_required' - ? 'Local owner setup is required.' - : code === 'invalid_credentials' - ? 'Username or password is incorrect.' - : response.status === 401 - ? 'Local authentication required.' - : payload?.error?.message ?? `Request failed with ${response.status}`; - const error = new Error(message); - error.status = response.status; - error.code = code; - error.correlationId = payload?.error?.correlationId ?? response.headers.get('x-correlation-id') ?? null; - error.issues = Array.isArray(payload?.error?.issues) ? payload.error.issues : []; - throw error; - } - return payload ?? response.json(); -} - async function load() { const root = document.querySelector('#view-root'); root.setAttribute('aria-busy','true'); @@ -908,7 +864,14 @@ async function load() { return; } dashboard = await api(`/api/dashboard?workspaceId=${encodeURIComponent(workspaceId())}`); - await Promise.all([loadRecallMap(), loadPinnedHandoffStatus(), loadLoopWorkbench(), loadMemoryCockpit(), loadMemoryGraph()]); + await Promise.all([ + loadRecallMap(), + loadPinnedHandoffStatus(), + loadLoopWorkbench(), + loadMemoryCockpit(), + loadMemoryGraph(), + currentRoute().id==='source-graph' ? loadSourceMap(parseMapUrl(globalThis.location?.href)) : Promise.resolve() + ]); shellState = classifyDashboardState(dashboard); } catch (error) { dashboard = { error:{ status:error.status, code:error.code, message:error.message }, metrics:{ runs:0, completed:0, events:0, pendingApprovals:0 }, runs:[], approvals:[], latestRun:null, latestManifest:null }; @@ -944,6 +907,25 @@ async function loadRecallMap() { } } +async function loadSourceMap(state,{refresh=false}={}) { + const sequence=++sourceGraphLoadSequence; + sourceGraphLoading=true; + try{ + sourceGraphResult=await api('/api/context/graph/preview',{ + method:'POST', + body:JSON.stringify({workspaceId:workspaceId(),...buildMapRequest(state),...(refresh?{refresh:true}:{})}) + }); + if(sequence!==sourceGraphLoadSequence)return; + sourceGraphError=null; + }catch(error){ + if(sequence!==sourceGraphLoadSequence)return; + sourceGraphResult=null; + sourceGraphError=error; + }finally{ + if(sequence===sourceGraphLoadSequence)sourceGraphLoading=false; + } +} + function recallMapSearchQuery(){return String(new URL(globalThis.location?.href??'http://127.0.0.1/').searchParams.get('query')??'').trim().slice(0,256)} async function refreshRecallMap(event) { @@ -1069,6 +1051,12 @@ async function submitMemoryIntake(event) { } function render() { + orientationCleanup(); + orientationCleanup=()=>{}; + sourceMapCleanup(); + sourceMapCleanup=()=>{}; + memoryGraphCleanup(); + memoryGraphCleanup=()=>{}; const route=currentRoute(); const setupScreen = shellState.kind === 'setup' || shellState.kind === 'denied'; const appShell = document.querySelector('.app-shell'); @@ -1084,7 +1072,7 @@ function render() { : renderRoute(route); root.querySelectorAll('[data-action=run]').forEach(button=>button.addEventListener('click',runDemo)); root.querySelectorAll('[data-action=reset]').forEach(button=>button.addEventListener('click',resetDemo)); - root.querySelectorAll('[data-action=refresh-recall-map]').forEach(button=>button.addEventListener('click',refreshRecallMap)); + if(route.id!=='home')root.querySelectorAll('[data-action=refresh-recall-map]').forEach(button=>button.addEventListener('click',refreshRecallMap)); root.querySelectorAll('[data-run-id]').forEach(link=>link.addEventListener('click',showRun)); root.querySelectorAll('[data-step-id],[data-record-id]').forEach(link=>link.addEventListener('click',navigateLocal)); root.querySelector('#auth-form')?.addEventListener('submit',submitAuthForm); @@ -1093,11 +1081,7 @@ function render() { root.querySelectorAll('[data-action=detect-git-changes]').forEach(button=>button.addEventListener('click',detectContextPackGitChanges)); root.querySelectorAll('[data-action=refresh-pinned-handoff]').forEach(button=>button.addEventListener('click',refreshPinnedHandoff)); root.querySelectorAll('[data-action=receive-pinned-handoff]').forEach(button=>button.addEventListener('click',receivePinnedHandoff)); - root.querySelector('#source-graph-form')?.addEventListener('submit',submitSourceGraph); - root.querySelector('#memory-graph-form')?.addEventListener('submit',submitMemoryGraph); root.querySelector('#memory-intake-form')?.addEventListener('submit',submitMemoryIntake); - root.querySelector('#memory-graph-history')?.addEventListener('change',toggleMemoryGraphHistory); - root.querySelector('#memory-graph-communities')?.addEventListener('change',toggleMemoryGraphCommunities); root.querySelector('#harness-setup-form')?.addEventListener('submit',submitHarnessSetupPlan); root.querySelectorAll('[data-action=copy-pack]').forEach(button=>button.addEventListener('click',copyContextPack)); root.querySelectorAll('[data-action=copy-receiver-packet]').forEach(button=>button.addEventListener('click',copyPinnedReceiverPacket)); @@ -1112,8 +1096,42 @@ function render() { root.querySelectorAll('[data-action=preview-pack-setup]').forEach(button=>button.addEventListener('click',previewContextPackSetup)); root.querySelectorAll('[data-action=copy-launch-prompt]').forEach(button=>button.addEventListener('click',copyContextPackLaunchPrompt)); root.querySelectorAll('[data-fabric-node]').forEach(button=>button.addEventListener('click',selectFabricNode)); - if(route.id==='memory-graph')drawMemoryGraphCanvas(root.querySelector('#memory-graph-canvas'),memoryGraph,memoryGraphOptions); document.querySelectorAll('[data-route]').forEach(link=>link.onclick=navigate); + if(route.id==='home')bindCurrentOrientation(root); + if(route.id==='source-graph')bindCurrentSourceMap(root); + if(route.id==='memory-graph')bindCurrentMemoryGraph(root); +} + +function bindCurrentOrientation(root) { + orientationCleanup=bindOrientation(root,{ + onSelectGroup:(groupId)=>{ + orientationModel=selectOrientationGroup(orientationModel,groupId); + root.innerHTML=renderOrientation(orientationModel); + document.querySelectorAll('[data-route]').forEach(link=>link.onclick=navigate); + orientationCleanup(); + bindCurrentOrientation(root); + }, + onRefresh:refreshRecallMap + }); +} + +function bindCurrentSourceMap(root) { + sourceMapCleanup=bindSourceMap(root,{ + report:sourceGraphResult, + onSubmit:submitSourceGraph, + onRefresh:refreshSourceGraph, + onPage:pageSourceGraph + }); +} + +function bindCurrentMemoryGraph(root) { + const model=buildMemoryGraphViewModel(memoryGraph,memoryGraphOptions,memoryGraphError); + memoryGraphCleanup=bindMemoryGraph(root,{ + model, + onSubmit:submitMemoryGraph, + onHistoryChange:toggleMemoryGraphHistory, + onGroupChange:toggleMemoryGraphCommunities + }); } function renderNav(container, mode) { @@ -1132,6 +1150,10 @@ function renderRepositoryBar(route) { document.querySelector('#repository-name').textContent = repository?.name ?? 'Local workspace'; document.querySelector('#repository-branch').textContent = repository?.branch ?? 'Branch unavailable'; document.querySelector('#repository-scan').textContent = recallMap?.generatedAt ? `Scanned ${date(recallMap.generatedAt)}` : 'Not scanned'; + const boundary=document.querySelector('#repository-boundary'); + const externalWrites=recallMap?.safeguards?.externalWritesEnabled===true; + boundary.textContent=`Local only / External writes ${externalWrites?'on':'off'}`; + boundary.dataset.state=externalWrites?'warning':'safe'; const conditionNode = document.querySelector('#repository-condition'); conditionNode.textContent = condition.kind; conditionNode.dataset.state = condition.kind; @@ -1220,7 +1242,7 @@ export function buildPinnedHandoffStatusModel(report=null,error=null) { usePlanFingerprint:currentEntry?.usePlan?.fingerprint ? shortFingerprint(currentEntry.usePlan.fingerprint) : 'unavailable', sourceChecks, targetLabel:harnessClientLabel(targetHarness), - primaryCommand:verified ? {label:'Receive pinned pack',command:`npm run oaf -- context receive --read-only --root . --target ${targetHarness} --format json`} : null, + primaryCommand:verified ? {label:'Receive pinned pack',command:`recall context receive --read-only --root . --target ${targetHarness} --format json`} : null, commands:pinnedHandoffCommands(targetHarness,verified), facts:[ ['Registry', registryExists ? report.registry.fingerprintStatus : 'missing'], @@ -1239,13 +1261,13 @@ export function canReceivePinnedHandoff(state) { function pinnedHandoffCommands(targetHarness='codex',includeUsePlan=false) { const commands=[ - {label:'Receive pinned pack',command:`npm run oaf -- context receive --read-only --root . --target ${targetHarness} --format json`}, - {label:'Receive summary',command:`npm run oaf -- context receive --read-only --root . --target ${targetHarness} --format summary`}, - {label:'Check registry',command:'npm run oaf -- context registry status --read-only --format json'}, - {label:'Read registry',command:'npm run oaf -- mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/registry/current --format json'} + {label:'Receive pinned pack',command:`recall context receive --read-only --root . --target ${targetHarness} --format json`}, + {label:'Receive summary',command:`recall context receive --read-only --root . --target ${targetHarness} --format summary`}, + {label:'Check registry',command:'recall context registry status --read-only --format json'}, + {label:'Read registry',command:'recall mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/registry/current --format json'} ]; if(includeUsePlan){ - commands.splice(2,0,{label:'Read pinned use plan',command:'npm run oaf -- mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/use-plan/current --format json'}); + commands.splice(2,0,{label:'Read pinned use plan',command:'recall mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/use-plan/current --format json'}); } return commands; } @@ -1255,238 +1277,54 @@ function renderRoute(route) { if (shellState.kind === 'denied') return renderSetupScreen('login', 'Use the local owner account for this workspace.'); if (shellState.kind === 'error') return statePanel('error','Could not load local state', shellState.message, true); if (route.id === 'home') return renderHome(); - if (route.id === 'runs') return activeRunDetail ? renderRunDetail(activeRunDetail) : renderRuns(); - if (route.id === 'workflows') return renderWorkflows(); - if (route.id === 'loop-workbench') return renderLoopWorkbench(); - if (route.id === 'fabric-map') return renderFabricMap(); - if (route.id === 'context') return renderContext(); + if (route.id === 'runs') return renderSecondaryRoute(route, activeRunDetail ? renderRunDetail(activeRunDetail) : renderRuns()); + if (route.id === 'workflows') return renderSecondaryRoute(route, renderWorkflows()); + if (route.id === 'loop-workbench') return renderSecondaryRoute(route, renderLoopWorkbench()); + if (route.id === 'fabric-map') return renderSecondaryRoute(route, renderFabricMap()); + if (route.id === 'context') return renderSecondaryRoute(route, renderContext()); if (route.id === 'context-pack') return renderContextPack(); if (route.id === 'source-graph') return renderSourceGraph(); if (route.id === 'memory') return renderMemory(); - if (route.id === 'memory-graph') return renderMemoryGraph(memoryGraph,memoryGraphOptions,memoryGraphError); - if (route.id === 'evidence') return renderEvidence(); - if (route.id === 'approvals') return renderApprovals(); - if (route.id === 'content') return renderContentLab(); - if (route.id === 'agents') return renderAgentsTools(); + if (route.id === 'memory-graph') return renderMemoryGraphView(buildMemoryGraphViewModel(memoryGraph,memoryGraphOptions,memoryGraphError)); + if (route.id === 'evidence') return renderSecondaryRoute(route, renderEvidence()); + if (route.id === 'approvals') return renderSecondaryRoute(route, renderApprovals()); + if (route.id === 'content') return renderSecondaryRoute(route, renderContentLab()); + if (route.id === 'agents') return renderSecondaryRoute(route, renderAgentsTools()); if (route.id === 'settings') return renderSettings(); return renderHome(); } +export function renderSecondaryRoute(route, content) { + return `

${esc(route?.title ?? 'Advanced view')}

${esc(route?.description ?? '')}

Advanced view
${content}
`; +} + function renderHome() { - return renderOverview(buildRecallMapHomeModel({ + orientationModel=buildOrientationModel({ report: recallMap, error: recallMapError, + loading:!recallMap&&!recallMapError, gitChanges: recallMapGitChanges, - pinnedHandoffStatus, - pinnedHandoffError - })); + handoff:pinnedHandoffStatus, + handoffError:pinnedHandoffError, + selectedGroupId:orientationModel?.selectedGroupId + }); + return renderOrientation(orientationModel); } export function buildRecallMapHomeModel({ report=null, error=null, gitChanges=null, pinnedHandoffStatus=null, pinnedHandoffError=null }={}) { - if (error) { - return { - state:'error', - error:buildApiErrorUiModel(error), - title:'Recall Map unavailable', - copy:'The local API did not return a map. Retry the read-only request or inspect the correlation details below.', - commands:[] - }; - } - if (!report) { - return { - state:'loading', - title:'Loading Recall Map', - copy:'Reading bounded local source metadata and governed memory summaries.', - commands:[] - }; - } - const sourceGraph=report.support?.sourceGraph ?? {}; - const coverage=sourceGraph.coverage ?? {}; - const architecture=report.architecture ?? {}; - const impact=architecture.impact ?? {}; - const memory=report.memory ?? {}; - const repository=report.repository ?? { - name:'Local workspace', - branch:null, - commitSha:null, - dirtyCount:0, - gitStatusAvailable:false, - reason:'repository_identity_unavailable' - }; - const entryPoints=Array.isArray(architecture.entryPoints)?architecture.entryPoints:[]; - const hotspots=Array.isArray(architecture.hotspots)?architecture.hotspots:[]; - const changedLocators=Array.isArray(impact.changedLocators)?impact.changedLocators:[]; - const representedChangedLocators=Array.isArray(impact.representedChangedLocators)?impact.representedChangedLocators:[]; - const affectedSymbols=Array.isArray(impact.affectedSymbols)?impact.affectedSymbols:[]; - const detectedChanges=normalizeRecallMapGitChanges(gitChanges); - const activeFacts=Array.isArray(memory.activeFacts)?memory.activeFacts:[]; - const pendingProposals=Array.isArray(memory.pendingProposals)?memory.pendingProposals:[]; - const handoffStatus=String(pinnedHandoffStatus?.current?.status ?? ''); - const handoffEntryId=pinnedHandoffStatus?.current?.entryId ?? null; - const handoffEntry=(pinnedHandoffStatus?.entries ?? []).find((entry)=>entry.id===handoffEntryId) ?? null; - const handoffState=pinnedHandoffError - ? 'blocked' - : handoffStatus==='verified' - ? 'ready' - : handoffStatus==='stale'||handoffStatus==='review' - ? 'review' - : handoffStatus==='tampered' - ? 'blocked' - : report.readiness?.handoff?.status==='available' - ? 'pending' - : 'blocked'; - const sourceUnavailable=sourceGraph.status==='unavailable'||coverage.status==='unavailable'; - const noArchitecture=entryPoints.length===0&&hotspots.length===0&&changedLocators.length===0&&affectedSymbols.length===0; - const baseState=sourceUnavailable?'partial':noArchitecture?'empty':'success'; - const state=handoffState==='review'?'stale':baseState; - const nextCommands=[ - 'recall map --root . --sqlite .local/memory.sqlite --format summary', - ...(Array.isArray(report.readiness?.nextCommands)?report.readiness.nextCommands:[]), - report.readiness?.mcp?.command - ].filter((command,index,all)=>typeof command==='string'&&command.length>0&&all.indexOf(command)===index).slice(0,5); - return { - state, - generatedAt:report.generatedAt ?? null, - repository, - index:{ - status:sourceGraph.status ?? 'unavailable', - kind:sourceGraph.status==='implemented'?'success':'error', - label:sourceGraph.status==='implemented'?'JS/TS map indexed':'Source graph unavailable', - copy:sourceGraph.status==='implemented'?'Bounded static metadata only; raw source bodies stay local.':'The local source graph could not be read.' - }, - coverage:{ - status:coverage.status ?? 'unavailable', - kind:coverage.status==='unavailable'?'error':'partial', - label:`${Number(coverage.analyzedFileCount??0)} / ${Number(coverage.maxFiles??0)} files`, - diagnosticCount:Number(coverage.diagnosticCount??0), - reasonCodes:Array.isArray(coverage.reasonCodes)?coverage.reasonCodes:[] - }, - entryPoints, - hotspots, - impact:{ - changedLocators, - representedChangedLocators, - affectedSymbols, - changedCount:changedLocators.length, - representedCount:representedChangedLocators.length, - affectedCount:affectedSymbols.length, - totalChangedCount:detectedChanges.status==='available'?detectedChanges.totalCount:changedLocators.length, - omittedChangedCount:detectedChanges.status==='available'?detectedChanges.omittedCount:0, - truncated:detectedChanges.truncated, - detectionStatus:detectedChanges.status, - detectionReason:detectedChanges.reason, - detectionMessage:detectedChanges.message, - repositoryDirtyCount:Number(repository.dirtyCount??0), - depth:Number(impact.depth??0) - }, - memory:{ - status:memory.status ?? 'unavailable', - kind:memory.status==='available'?(Number(memory.staleFactCount??0)>0?'stale':'success'):'error', - activeCount:activeFacts.length, - pendingCount:pendingProposals.length, - staleCount:Number(memory.staleFactCount??0), - unavailableReason:memory.unavailableReason ?? null - }, - handoff:{ - state:handoffState, - kind:handoffState==='ready'?'success':handoffState==='review'||handoffState==='pending'?'partial':'error', - command:report.readiness?.handoff?.command ?? 'recall handoff', - createdAt:handoffEntry?.createdAt ?? null, - ageLabel:relativeAge(handoffEntry?.createdAt,pinnedHandoffStatus?.generatedAt??report.generatedAt), - copy:handoffState==='ready' - ? 'Pinned handoff is verified for the next coding agent.' - : handoffState==='review' - ? 'Pinned sources changed and need review before handoff.' - : handoffState==='pending' - ? 'The handoff command is available; no verified pinned packet is active.' - : 'Handoff verification is unavailable. Review the local registry before sharing context.' - }, - safeguards:report.safeguards ?? {}, - commands:nextCommands.map((command)=>({ label:recallMapCommandLabel(command), command })), - recentActivity:[ - ...pendingProposals.slice(0,3).map((proposal)=>({ - kind:'proposal', - label:'Memory proposed', - detail:proposal.sourceLocator, - at:proposal.enqueuedAt - })), - ...activeFacts.slice(0,3).map((fact)=>({ - kind:'memory', - label:'Memory current', - detail:fact.sourceLocator, - at:fact.validFrom - })) - ].sort((left,right)=>String(right.at).localeCompare(String(left.at))).slice(0,5) - }; -} - -export function renderOverview(model) { - if (model.state==='loading') return `

Overview

${statePanel('loading',model.title,model.copy)}
`; - if (model.state==='error') return `

Overview

${renderApiErrorPanel(model.title,model.error)}
`; - const action=selectOverviewPrimaryAction(model); - const actionHtml=action?.action - ? `` - : action - ? `${esc(action.label)}` - : 'No action queued'; - const stateCopy=model.state==='stale' - ? statePanel('stale','Source changes need review','Repository evidence changed after the current handoff was pinned. Review the affected sources before sharing context.') - : model.state==='empty' - ? statePanel('empty','No JS/TS entry points yet','The map is live, but this workspace did not yield a bounded JS/TS entry point. Inspect supported coverage in Map before broadening the workspace.') - : model.state==='partial'||model.coverage.status==='partial' - ? statePanel('partial','Bounded coverage','The bounded scan completed, but the Map only indexes supported JS/TS metadata within its scan limits. Review its coverage notes before treating the repository picture as complete.') - : ''; - const detectionFailed=model.impact.detectionStatus==='unavailable'||model.impact.detectionStatus==='error'; - const omittedChangeEvidence=model.impact.detectionStatus==='available'&&model.impact.omittedChangedCount>0; - const changed=model.impact.changedLocators.length - ? `
    ${model.impact.changedLocators.map((locator)=>`
  • ${esc(locator)}
  • `).join('')}
${model.impact.omittedChangedCount?`

${model.impact.changedCount} shown · ${model.impact.omittedChangedCount} omitted by safety or scan bounds.

`:''}` - : detectionFailed - ? `
${model.impact.repositoryDirtyCount>0?`${model.impact.repositoryDirtyCount} changed entr${model.impact.repositoryDirtyCount===1?'y':'ies'}; `:''}file detection unavailable.

${esc(model.impact.detectionMessage)}

` - : omittedChangeEvidence - ? `

0 shown · ${model.impact.omittedChangedCount} omitted by safety or scan bounds.

` - : model.impact.detectionStatus==='available' - ? '

No changed files detected.

' - : '

Changed-file detection has not run.

'; - const attention=[ - model.memory.pendingCount?`${model.memory.pendingCount} pendingReview proposed memory`:'', - model.memory.staleCount?`${model.memory.staleCount} staleCheck source changes`:'', - model.handoff.state==='blocked'?`Handoff blockedRepair registry or pinned artifacts`:'', - model.handoff.state==='review'?`Handoff needs reviewUpdate changed sources`:'', - detectionFailed?``:'', - omittedChangeEvidence?`${model.impact.omittedChangedCount} change${model.impact.omittedChangedCount===1?'':'s'} omittedInspect safety and scan bounds`:'', - model.coverage.status==='partial'||model.coverage.diagnosticCount?`${model.coverage.diagnosticCount?`${model.coverage.diagnosticCount} coverage note${model.coverage.diagnosticCount===1?'':'s'}`:'Bounded coverage'}Inspect supported files and scan scope`:'' - ].filter(Boolean).join('')||'

Nothing needs review.

'; - const affected=model.impact.affectedSymbols.length - ? `
    ${model.impact.affectedSymbols.slice(0,6).map((entry)=>`
  1. ${esc(entry.label)}${esc(entry.locator ?? 'locator unavailable')}
  2. `).join('')}
` - : '

No focused impact set.

'; - const activity=model.recentActivity.length - ? `
    ${model.recentActivity.map((item)=>`
  1. ${esc(item.label)}${esc(item.detail ?? 'source unavailable')}
  2. `).join('')}
` - : '

No memory activity recorded.

'; - return `
-
-

${esc(model.repository.name)}

${esc(model.repository.branch ?? 'Branch unavailable')} · ${model.repository.dirtyCount} changed · scanned ${esc(model.generatedAt?date(model.generatedAt):'not yet')}

- ${actionHtml} -
- ${stateCopy} -
-

Changes

Open Map
${changed}
-

Needs attention

Open Memory
${attention}
-

Impact

${model.impact.affectedCount} affected
${affected}
-

Current handoff

Open Handoffs
State
${esc(model.handoff.state)}
Age
${esc(model.handoff.ageLabel)}
Source check
${esc(model.handoff.copy)}
-

Recent activity

${model.recentActivity.length}
${activity}
-
-
`; + const model=buildOrientationModel({ + report, + error, + loading:!report&&!error, + gitChanges, + handoff:pinnedHandoffStatus, + handoffError:pinnedHandoffError + }); + return model.state==='failure' ? Object.freeze({...model,state:'error'}) : model; } -export const renderRecallMapHome=renderOverview; - -function recallMapCommandLabel(command) { - if (command.startsWith('recall map')) return 'Refresh map locally'; - if (command.startsWith('recall handoff')) return 'Create Next-Agent Handoff'; - if (command.includes('mcp inspect')) return 'Inspect read-only MCP'; - if (command.includes('graph stats')) return 'Check source graph'; - return 'Copy command'; -} +export const renderOverview=renderOrientation; +export const renderRecallMapHome=renderOrientation; function renderRuns() { if (activeRunDetail) return renderRunDetail(activeRunDetail); @@ -1502,7 +1340,7 @@ function renderRunDetail(data) { function renderWorkflows() { const steps=['collect','normalize','analyze-patterns','compile-context','generate-angles','verify-recommendations','local-draft-outcome']; - return `

Content Intelligence

workflow:content-intelligence
    ${steps.map((step,index)=>`
  1. ${index+1}${step}${workflowStepCopy(step)}
  2. `).join('')}
`; + return `

Content analysis

workflow:content-intelligence
    ${steps.map((step,index)=>`
  1. ${index+1}${step}${workflowStepCopy(step)}
  2. `).join('')}
`; } export function buildLoopWorkbenchModel(report=null,{dashboard=null,error=null}={}) { @@ -2103,7 +1941,7 @@ function zeroCount(value) { function contextPackHarnessCommands(pack,usePlan=null,{memoryConfig=null}={}) { const packCommands=Array.isArray(pack?.handoff?.commands) ? pack.handoff.commands.filter((command)=>typeof command==='string'&&command.trim()) : []; if(packCommands.length){ - const commands=packCommands.map((command)=>({ label:contextPackCommandLabel(command), command })); + const commands=packCommands.map((command)=>({ label:contextPackCommandLabel(command), command:publicRecallCommand(command) })); insertContextPackReceiveCommand(commands,pack); const generated=[ {label:'Test local handoff',command:contextPackPreflightCommand(pack,{memoryConfig})}, @@ -2132,18 +1970,18 @@ function contextPackHarnessCommands(pack,usePlan=null,{memoryConfig=null}={}) { { label:'Test local handoff', command:contextPackPreflightCommand(pack,{memoryConfig}) }, { label:'Test handoff summary', command:contextPackPreflightCommand(pack,{memoryConfig,format:'summary'}) }, { label:'Copy impact command', command:contextPackImpactCommand(pack) }, - { label:'Rebuild from CLI', command:`npm run oaf -- context pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --dry-run --format markdown` }, - { label:'Pin locally', command:`npm run oaf -- context pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --write --pin --out context-packs/CONTEXT_PACK.md --format json` }, - { label:'Verify pin', command:'npm run oaf -- context registry status --read-only --format json' }, + { label:'Rebuild from CLI', command:`recall context pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --dry-run --format markdown` }, + { label:'Pin locally', command:`recall context pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --write --pin --out context-packs/CONTEXT_PACK.md --format json` }, + { label:'Verify pin', command:'recall context registry status --read-only --format json' }, { label:'Receive pinned pack', command:contextPackReceiveCommand(pack) }, { label:'Receive summary', command:contextPackReceiveSummaryCommand(pack) }, { label:'Start MCP bridge', command:oafCommand('mcp resources --read-only --stdio') }, - { label:'Preview harness setup', command:`npm run oaf -- harness setup plan --client ${setupClient} --server oaf --dry-run --format json` }, - { label:'Read use plan', command:'npm run oaf -- mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/use-plan/current --format json' }, - { label:'Read registry', command:'npm run oaf -- mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/registry/current --format json' }, - { label:'Read current context pack', command:`npm run oaf -- mcp resources --read-only --context-pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --uri oaf://workspace/ws_local/context-pack/current --format json` }, - { label:'Read MCP resources', command:'npm run oaf -- mcp resources --read-only --format json' }, - { label:'Read latest handoff', command:'npm run oaf -- mcp resources --read-only --uri oaf://workspace/ws_local/handoff/latest --format json' } + { label:'Preview harness setup', command:`recall harness setup plan --client ${setupClient} --server oaf --dry-run --format json` }, + { label:'Read use plan', command:'recall mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/use-plan/current --format json' }, + { label:'Read registry', command:'recall mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/registry/current --format json' }, + { label:'Read current context pack', command:`recall mcp resources --read-only --context-pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --uri oaf://workspace/ws_local/context-pack/current --format json` }, + { label:'Read MCP resources', command:'recall mcp resources --read-only --format json' }, + { label:'Read latest handoff', command:'recall mcp resources --read-only --uri oaf://workspace/ws_local/handoff/latest --format json' } ]; } @@ -2195,12 +2033,12 @@ function contextPackPreflightCommand(pack,{memoryConfig=null,format='json'}={}) function contextPackReceiveCommand(pack) { const target=String(pack?.targetHarness ?? 'generic'); - return `npm run oaf -- context receive --read-only --root . --target ${target} --format json`; + return `recall context receive --read-only --root . --target ${target} --format json`; } function contextPackReceiveSummaryCommand(pack) { const target=String(pack?.targetHarness ?? 'generic'); - return `npm run oaf -- context receive --read-only --root . --target ${target} --format summary`; + return `recall context receive --read-only --root . --target ${target} --format summary`; } function contextPackGeneratedUsePlanCommands(pack,usePlan=null) { @@ -2215,13 +2053,13 @@ function contextPackGeneratedUsePlanCommands(pack,usePlan=null) { const changed=(pack?.sourceGraph?.impact?.changedLocators ?? []).map((locator)=>` --changed ${quoteShell(locator.replace(/^workspace:\/\//u,''))}`).join(''); const uri=String(usePlan?.resource?.uri ?? 'oaf://workspace/ws_local/context-pack/use-plan/current'); return [ - { label:'Pin locally', command:`npm run oaf -- context pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --write --pin --out context-packs/CONTEXT_PACK.md --format json` }, - { label:'Verify pin', command:'npm run oaf -- context registry status --read-only --format json' }, + { label:'Pin locally', command:`recall context pack --from ${from} --root . --objective ${objective} --step ${step} --target ${target}${selected}${changed} --write --pin --out context-packs/CONTEXT_PACK.md --format json` }, + { label:'Verify pin', command:'recall context registry status --read-only --format json' }, { label:'Receive pinned pack', command:contextPackReceiveCommand(pack) }, { label:'Receive summary', command:contextPackReceiveSummaryCommand(pack) }, { label:'Start MCP bridge', command:oafCommand('mcp resources --read-only --stdio') }, - { label:'Read registry', command:'npm run oaf -- mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/registry/current --format json' }, - { label:'Read use plan', command:`npm run oaf -- mcp resources --read-only --uri ${uri} --format json` } + { label:'Read registry', command:'recall mcp resources --read-only --uri oaf://workspace/ws_local/context-pack/registry/current --format json' }, + { label:'Read use plan', command:`recall mcp resources --read-only --uri ${uri} --format json` } ]; } @@ -2252,6 +2090,12 @@ function oafCommand(args) { return `${OAF_CHECKOUT_COMMAND_PREFIX} ${args}`; } +function publicRecallCommand(command) { + return String(command ?? '') + .replace(/^npm\s+--silent\s+run\s+oaf\s+--\s+/u, 'recall ') + .replace(/^npm\s+run\s+oaf\s+--\s+/u, 'recall '); +} + function contextPackSetupClient(pack) { const target=String(pack?.targetHarness ?? 'codex'); return target === 'cursor' || target === 'claude-code' || target === 'codex' ? target : 'codex'; @@ -2286,11 +2130,12 @@ function contextPackFormDraft() { const payload=selectContextPackPinPayload({reviewedPayload:contextPackResult?.reviewedPayload ?? contextPackReviewedPayload}); const memoryConfig=normalizeMemoryWorkspaceConfig(contextPackResult?.memoryConfig ?? contextPackMemoryConfig); const sourceFamilies=String(payload?.from ?? 'codex').split(',').map((item)=>item.trim()).filter(Boolean); + const objectiveFromUrl=String(new URL(globalThis.location?.href??'http://127.0.0.1/handoffs').searchParams.get('objective')??'').trim().slice(0,2000); return { targetHarness:String(payload?.targetHarness ?? 'codex'), tokenBudget:String(Number(payload?.tokenBudget ?? 4096)), sourceFamilies:sourceFamilies.length ? sourceFamilies : ['codex'], - objective:String(payload?.objective ?? DEFAULT_CONTEXT_PACK_OBJECTIVE), + objective:objectiveFromUrl||String(payload?.objective ?? DEFAULT_CONTEXT_PACK_OBJECTIVE), step:String(payload?.step ?? DEFAULT_CONTEXT_PACK_STEP), userSelectedFiles:(payload?.userSelectedFiles ?? []).join('\n'), changedLocators:(payload?.changedLocators ?? []).map((locator)=>String(locator).replace(/^workspace:\/\//u,'')).join('\n'), @@ -2534,10 +2379,12 @@ function memoryConfigDownloadName() { } function renderSourceGraph() { - const errorPanel=sourceGraphError?renderApiErrorPanel('Source graph preview failed',sourceGraphError):''; - const query=recallMapSearchQuery(); - const globalResults=renderRepositorySearchState({query,report:recallMap,error:recallMapError}); - return `

Repository structure

Map

Find an entry point, trace a symbol, or inspect the impact of a changed file.

Read-only · bounded metadata
${globalResults}
No model or network calls
${errorPanel}${sourceGraphResult?renderSourceGraphResult(sourceGraphResult):statePanel('empty','No map results','Run the map to inspect files, symbols, import neighbors, and likely starting points.')}
`; + return renderSourceMap({ + state:parseMapUrl(globalThis.location?.href), + report:sourceGraphResult, + error:sourceGraphError, + loading:sourceGraphLoading + }); } function renderRecallMapSearchResults(report,query){const search=report?.architecture?.search;const results=Array.isArray(search?.results)?search.results:[];return `

Search results

${Number(search?.total??0)} matches for ${esc(query)}
${results.length?`
    ${results.map((item)=>`
  1. ${esc(item.label)}${esc(item.locator)}
  2. `).join('')}
`:'

No bounded source-graph matches.

'}
`} @@ -2548,133 +2395,11 @@ export function renderRepositorySearchState({query='',report=null,error=null}={} return renderRecallMapSearchResults(report,query); } -export function renderSourceGraphResult(report) { - const summary=report.graph?.summary ?? {}; - return `

Map results

Repo Map

Files
${Number(summary.fileCount??0)}
Symbols
${Number(summary.symbolCount??0)}
Relations
${Number(summary.edgeCount??0)}
Matches
${Number(report.search?.total??0)}
${renderRepoMap(report)}

Search results

${Number(report.search?.total??0)} matches
${sourceGraphSearchList(report.search?.results)}

Trace

${report.trace?.paths?.length??0} paths
${sourceGraphTraceList(report.trace?.paths)}

Changed impact

${report.impact?.affectedSymbols?.length??0} symbols
${sourceGraphImpactList(report.impact?.affectedSymbols)}
Scan details${sourceGraphSafeguards(report.safeguards)}

Sample nodes

${report.graph?.sampleNodes?.length??0}
${sourceGraphNodeList(report.graph?.sampleNodes)}${esc(shortFingerprint(report.graph?.graphFingerprint))}
`; -} - -function renderRepoMap(report) { - const nodes=report.graph?.sampleNodes ?? []; - const byId=new Map(nodes.map((node)=>[node.id,node])); - const files=nodes.filter((node)=>node.kind==='file').slice(0,6); - const symbols=uniqueBy([ - ...(report.graph?.summary?.entryPoints ?? []), - ...(report.graph?.summary?.hotspots ?? []), - ...nodes.filter((node)=>node.kind==='symbol') - ],(item)=>item.locator ?? item.label).slice(0,6); - const imports=(report.graph?.sampleEdges ?? []).filter((edge)=>edge.kind==='imports').map((edge)=>{ - const from=byId.get(edge.fromNodeId); - const to=byId.get(edge.toNodeId); - return from&&to?`${from.label} -> ${to.label}`:''; - }).filter(Boolean).slice(0,6); - const readFirst=(report.graph?.summary?.entryPoints ?? []).map((item)=>item.locator).filter(Boolean).slice(0,6); - return `

Repo Map

${files.length} files
${sourceGraphStartHere(report)}

Read first

${sourceGraphTextList(readFirst)}

Files

${sourceGraphNodeList(files)}
`; -} - -function uniqueBy(items,key) { - const seen=new Set(); - return items.filter((item)=>{const value=key(item);if(!value||seen.has(value))return false;seen.add(value);return true;}); -} - -function sourceGraphStartHere(report) { - const entry=report.graph?.summary?.entryPoints?.[0]; - const changed=report.impact?.representedChangedLocators?.[0] ?? report.impact?.changedLocators?.[0]; - const affected=report.impact?.affectedSymbols?.[0]; - if(!entry && !changed && !affected)return ''; - return `

Start here

${entry?`Begin at ${esc(entry.label)} (${esc(entry.locator)}).`:'Use the read-first list below.'}${changed?` Changed impact: ${esc(changed)}${affected?` touches ${esc(affected.name)}`:''}.`:''}

`; -} - -function sourceGraphSymbolList(symbols=[]) { - if(!symbols.length)return '

No key symbols in the bounded preview.

'; - return `
    ${symbols.map((symbol)=>`
  1. ${esc(symbol.label)}${esc(symbol.symbolKind??'symbol')} · ${esc(symbol.locator??`${Number(symbol.total??0)} links`)}
  2. `).join('')}
`; -} - -function sourceGraphTextList(items=[]) { - if(!items.length)return '

No bounded preview items.

'; - return `
    ${items.map((item)=>`
  1. ${esc(item)}
  2. `).join('')}
`; -} - -function sourceGraphSearchList(results=[]) { - if(!results.length)return '

No matching graph records.

'; - return `
    ${results.map((item)=>`
  1. ${esc(item.label)}${esc(item.kind)} · ${esc(item.locator??'no locator')} · ${Number(item.score??0).toFixed(3)}
  2. `).join('')}
`; -} - -function sourceGraphNodeList(nodes=[]) { - if(!nodes.length)return '

No sample nodes.

'; - return `
    ${nodes.map((node)=>`
  1. ${esc(node.label)}${esc(node.kind)} · ${esc(node.locator??node.sourceRef??node.id)}
  2. `).join('')}
`; -} - -function sourceGraphTraceList(paths=[]) { - if(!paths.length)return '

No trace paths for the selected symbol.

'; - return `
    ${paths.map((path)=>`
  1. ${esc(path.terminalLabel)}depth ${Number(path.depth??0)} · ${path.nodeIds?.length??0} nodes
  2. `).join('')}
`; -} - -function sourceGraphImpactList(symbols=[]) { - if(!symbols.length)return '

No impacted symbols for the supplied locator.

'; - return `
    ${symbols.map((symbol)=>`
  1. ${esc(symbol.name)}${esc(symbol.symbolKind)} · ${esc(symbol.locator)}
  2. `).join('')}
`; -} - -function sourceGraphSafeguards(safeguards={}) { - return `
Persisted
${safeguards.persisted?'yes':'no'}
Model calls
${Number(safeguards.modelCalls??0)}
Network
${Number(safeguards.networkCalls??0)}
Graph DB
${safeguards.graphDatabaseUsed?'yes':'no'}
Raw bodies
${safeguards.rawBodyIncluded?'included':'excluded'}
`; -} - function renderMemory() { if(memoryCockpitError)return statePanel('error','Memory cockpit unavailable',memoryCockpitError); return renderMemoryCockpit(memoryCockpit); } -export function buildMemoryGraphModel(report = null) { - if(!report?.graph)return {ready:false,summary:{nodeCount:0,edgeCount:0,communityCount:0},nodes:[],edges:[],focus:null}; - const nodes=Array.isArray(report.graph.nodes)?report.graph.nodes:[]; - const edges=Array.isArray(report.graph.edges)?report.graph.edges:[]; - const summary={nodeCount:nodes.length,edgeCount:edges.length,communityCount:0,...(report.summary??{})}; - return { - ready:true, - provider:report.provider??'provider:native:memory:sqlite', - mode:report.mode??'current', - generatedAt:report.generatedAt, - reportFingerprint:report.reportFingerprint, - communityMethod:report.communityMethod??'label-propagation', - summary, - nodes, - edges, - focus:report.focus, - safeguards:report.safeguards??{} - }; -} - -export function renderMemoryGraph(report = null, options = {}, error = null) { - if(error)return statePanel('error','Memory graph unavailable',error); - const model=buildMemoryGraphModel(report); - if(!model.ready)return statePanel('empty','No governed graph loaded','Ingest and approve temporal memory facts, then refresh this local graph view.'); - const query=options.query??''; - const historyChecked=options.history?' checked':''; - const communityChecked=options.communities?' checked':''; - const focus=model.focus?.nodes?.length?model.focus:null; - const focusList=focus - ? `
    ${focus.nodes.slice(0,12).map((node)=>`
  1. ${esc(node.name)}${esc(node.type)} · degree ${Number(node.degree??0)} · community ${Number(node.community??0)}
  2. `).join('')}
` - : '

Click a node or search for an entity to focus its governed neighborhood.

'; - const currentEdges=model.edges.filter((edge)=>edge.current).slice(0,12); - const staleEdges=model.edges.filter((edge)=>!edge.current).slice(0,12); - const currentList=currentEdges.length - ? memoryGraphEdgeList(currentEdges) - : '

No current graph facts are visible.

'; - const historyList=staleEdges.length - ? memoryGraphEdgeList(staleEdges) - : '

No superseded graph edges are visible in this mode.

'; - return `
${metric(model.summary.nodeCount,'Nodes',`${model.summary.currentNodeCount??0} current`)}${metric(model.summary.edgeCount,'Edges',`${model.summary.currentEdgeCount??0} current`)}${metric(model.summary.communityCount,'Communities',model.communityMethod)}${metric(model.summary.historyEdgeCount??0,'History edges',model.mode==='history'?'visible':'hidden')}

Governed knowledge graph

${esc(model.mode)} · ${esc(shortFingerprint(model.reportFingerprint))}
${memoryGraphLegend(model.nodes)}
`; -} - -function memoryGraphEdgeList(edges=[]) { - return `
    ${edges.map((edge)=>`
  1. ${esc(edge.from)} ${esc(edge.predicate)} ${esc(edge.to)}${edge.current?'Current':'Superseded'} · ${edge.supersededBy?`superseded by ${esc(edge.supersededBy)}`:'current winner'}Valid from ${date(edge.validFrom)} · Valid until ${edge.validUntil?date(edge.validUntil):'open'} · Provenance ${esc(edge.source)}
  2. `).join('')}
`; -} - -function memoryGraphLegend(nodes=[]) { - const types=[...new Set(nodes.map((node)=>node.type))].sort(); - if(!types.length)return '

No node types to render.

'; - return types.map((type)=>`${esc(type)}`).join(''); -} - export function renderMemoryIntakePanel(result=null,error=null,draft={sourceLocator:'memory/inbox.md',text:''}) { const rows=Array.isArray(result?.proposalFacts) && result.proposalFacts.length ? `
    ${result.proposalFacts.map((item)=>`
  1. ${esc(item.subject)} ${esc(item.predicate)}${esc(item.object)}${esc(item.id)} · ${esc(item.status)} · ${esc(item.sourceLocator)}
  2. `).join('')}
` @@ -2705,7 +2430,8 @@ export function renderMemoryCockpit(cockpit = null) { const toolStats=model.mcpStats.byTool.length ? `
    ${model.mcpStats.byTool.map((item)=>`
  1. ${esc(item.toolName)} · ${item.callCount}${item.deliveredTokens} delivered · ${item.tokensSaved} saved
  2. `).join('')}
` : '

No MCP delivery calls recorded for this workspace yet.

'; - return `

Governed repository memory

Memory

Review proposed facts, inspect current truth, and add explicit source-backed memory.

Pending
${model.summary.pendingProposalCount}
Active
${model.summary.activeFactCount}
Delivery
${deliveryChangeLabel(model.savings.percent)}

Review queue

${model.summary.pendingProposalCount} pending
${queue}

Active memory

${model.facts.length} facts
${facts}
Delivery details · ${deliveryChangeLabel(model.savings.percent)}
Active facts
${model.summary.activeFactCount}
Pending proposals
${model.summary.pendingProposalCount}
Naive baseline
${model.savings.beforeDeliveryTokens}
Memory Recall delivery
${model.savings.afterDeliveryTokens}
Delivery tokens saved
${model.savings.tokensSaved}
MCP calls
${model.mcpStats.callCount}
MCP delivered
${model.mcpStats.deliveredTokens}
MCP saved
${model.mcpStats.tokensSaved}
Provider billing
${model.savings.providerBillingClaimed||model.mcpStats.providerBillingClaimed?'claimed':'not claimed'}
Provider
${esc(model.provider)}
${toolStats}${history}

Delivery values are local estimates, not provider billing claims. No model, network, or raw source body is used on this route.

`; + const deliveryLabel=deliveryChangeLabel(model.savings.percent,model.savings.beforeDeliveryTokens); + return `

Governed repository memory

Memory

Review proposed facts, inspect current truth, and add explicit source-backed memory.

Pending
${model.summary.pendingProposalCount}
Active
${model.summary.activeFactCount}
Delivery
${deliveryLabel}

Review queue

${model.summary.pendingProposalCount} pending
${queue}

Active memory

${model.facts.length} facts
${facts}
Delivery details · ${deliveryLabel}
Active facts
${model.summary.activeFactCount}
Pending proposals
${model.summary.pendingProposalCount}
Naive baseline
${model.savings.beforeDeliveryTokens||'Not measured'}
Memory Recall delivery
${model.savings.afterDeliveryTokens}
Delivery tokens saved
${model.savings.beforeDeliveryTokens>0?model.savings.tokensSaved:'Not measured'}
MCP calls
${model.mcpStats.callCount}
MCP delivered
${model.mcpStats.deliveredTokens}
MCP saved
${model.mcpStats.tokensSaved}
Provider billing
${model.savings.providerBillingClaimed||model.mcpStats.providerBillingClaimed?'claimed':'not claimed'}
Provider
${esc(model.provider)}
${toolStats}${history}

${model.savings.beforeDeliveryTokens>0?'Delivery values are local estimates, not provider billing claims.':'Run a context delivery with a comparable baseline to measure reduction.'} No model, network, or raw source body is used on this route.

`; } function renderEvidence() { @@ -2743,22 +2469,12 @@ function renderHarnessSetupResult(report) { } function renderSettings() { - return `

Local system

Shared tokens
${[['Ink','--ink'],['Paper','--paper'],['Signal','--signal'],['Proof','--proof'],['Caution','--caution'],['Danger','--danger'],['Success','--success']].map(([name,token])=>`
${name}${token}
`).join('')}
`; + return `

Settings

Inspect the local paths, scan bounds, and privacy rules used by this workspace.

Local workspace

Local storage

Derived and governed state
Memory
.local/memory.sqlite
Source graph
Persistent native index; unavailable until explicitly built or refreshed
Context packs
context-packs/ when explicitly pinned

Scan limits

Bounded by default
Languages
14 Tier 1 languages; current coverage is reported per scan
File size
512 KiB default
Graph display
200 nodes / 400 relationships

Privacy

Offline default
${localBoundary()}

Map and read-only MCP responses expose bounded locators and metadata, never raw source bodies.

`; } function metric(value,label,copy){return `
${Number(value??0)}${label}${copy}
`} function statusChip(kind,label,description){return `${esc(label)}${esc(description)}`} function localBoundary(){return `
Residency
Local-only
Network
Denied by default
Writes
External writes disabled
Model
Deterministic offline default
`} -function renderApiErrorPanel(heading,error) { - const model=buildApiErrorUiModel(error); - return statePanel('error',heading,model.message,false,renderApiErrorRecovery(model)); -} -function renderApiErrorRecovery(model) { - const issues=model.issues.length ? `

Fix this field

    ${model.issues.map((issue)=>`
  • ${esc(issue.label)}${esc(issue.detail)}${esc(issue.path)} · ${esc(issue.code)}
  • `).join('')}
` : ''; - const correlation=model.correlationId ? `

Correlation ${esc(model.correlationId)}

` : ''; - return `${issues}${correlation}`; -} -function statePanel(kind,heading,copy,button=false,extra=''){return `

${esc(heading)}

${esc(copy)}

${extra}${button?'
':''}
`} export function renderSetupScreen(mode, copy, inputModel=null) { const model=inputModel ?? buildAuthViewModel({mode,copy,draft:authDraft,error:authError}); const isBootstrap = mode === 'bootstrap'; @@ -2871,10 +2587,11 @@ function keyValueFacts(items){ return `
${items.map((item)=>`
${esc(item.key)}
${esc(item.value)}
`).join('')}
`; } -function navigate(event) { +async function navigate(event) { event.preventDefault(); activeRunDetail=null; history.pushState({},'',event.currentTarget.getAttribute('href')); + if(currentRoute().id==='source-graph')await loadSourceMap(parseMapUrl(globalThis.location?.href)); render(); document.querySelector('#main').focus({preventScroll:true}); } @@ -2886,7 +2603,40 @@ function navigateLocal(event) { document.querySelector('#main').focus({preventScroll:true}); } -async function submitGlobalSearch(event){event.preventDefault();const input=event.currentTarget.elements.query;const query=String(input?.value??'').trim().slice(0,256);if(!query){input?.focus();return}const params=new URLSearchParams({query});const currentWorkspace=workspaceId();if(currentWorkspace!=='ws_local')params.set('workspaceId',currentWorkspace);history.pushState({},'',`/map?${params.toString()}`);document.querySelector('#live-status').textContent='Searching bounded repository metadata.';await loadRecallMap();render();document.querySelector('#main').focus({preventScroll:true});document.querySelector('#live-status').textContent=recallMapError?`Repository search failed. ${buildApiErrorUiModel(recallMapError).message}`:'Repository search loaded.'} +const COMMAND_TARGETS=Object.freeze({ + explain:(value)=>`/map?query=${encodeURIComponent(value)}`, + trace:(value)=>`/map?query=${encodeURIComponent(value)}&start=${encodeURIComponent(value)}`, + impact:(value)=>`/map?query=${encodeURIComponent(value)}&changed=${encodeURIComponent(value)}`, + handoff:(value)=>`/handoffs?objective=${encodeURIComponent(value)}` +}); + +async function submitGlobalSearch(event){ + event.preventDefault(); + const input=event.currentTarget.elements.query; + const query=String(input?.value??'').trim().slice(0,256); + if(!query){ + input?.focus(); + document.querySelector('#live-status').textContent='Enter a file, symbol, concept, or path.'; + return; + } + const intent=String(event.currentTarget.elements.intent?.value??'explain'); + const target=(COMMAND_TARGETS[intent]??COMMAND_TARGETS.explain)(query); + const url=new URL(target,'http://127.0.0.1'); + const currentWorkspace=workspaceId(); + if(currentWorkspace!=='ws_local')url.searchParams.set('workspaceId',currentWorkspace); + history.pushState({},'',`${url.pathname}${url.search}`); + if(intent==='handoff'){ + render(); + document.querySelector('#main').focus({preventScroll:true}); + document.querySelector('#live-status').textContent='Handoff objective filled in for review.'; + return; + } + document.querySelector('#live-status').textContent='Loading bounded repository metadata.'; + await loadSourceMap(parseMapUrl(globalThis.location?.href)); + render(); + document.querySelector('#main').focus({preventScroll:true}); + document.querySelector('#live-status').textContent=sourceGraphError?`Map request failed. ${buildApiErrorUiModel(sourceGraphError).message}`:'Map loaded.'; +} function selectFabricNode(event) { activeFabricNode=event.currentTarget.dataset.fabricNode ?? 'context'; @@ -3220,66 +2970,49 @@ async function pinCurrentContextPack(event) { } } -async function submitSourceGraph(event){ - event.preventDefault(); - const form=event.currentTarget; - const button=form.querySelector('button[type=submit]'); - const data=new FormData(form); - const query=String(data.get('query') ?? '').trim(); - const startName=String(data.get('startName') ?? '').trim(); - const changedLocator=String(data.get('changedLocator') ?? '').trim(); - const limit=Number(data.get('limit') ?? 8); - const depth=Number(data.get('depth') ?? 2); - const body={ - workspaceId:workspaceId(), - limit, - depth, - sampleLimit:6 - }; - if(query)body.query=query; - if(startName)body.startName=startName; - if(changedLocator)body.changedLocators=[changedLocator]; - button.disabled=true; - button.textContent='Previewing...'; - document.querySelector('#live-status').textContent='Previewing local source graph.'; - try{ - sourceGraphResult=await api('/api/context/graph/preview',{method:'POST',body:JSON.stringify(body)}); - sourceGraphError=null; - document.querySelector('#live-status').textContent='Source graph preview ready.'; - render(); - }catch(error){ - document.querySelector('#live-status').textContent=error.message; - sourceGraphError=error; - render(); - }finally{ - button.disabled=false; - button.textContent='Preview repo map'; - } +async function submitSourceGraph(state){ + state={...state,offset:0}; + const target=serializeMapUrl(state); + const current=`${globalThis.location?.pathname??''}${globalThis.location?.search??''}`; + if(target!==current)history.pushState({},'',target); + document.querySelector('#live-status').textContent='Loading the submitted map scope.'; + await loadSourceMap(state); + render(); + document.querySelector('#live-status').textContent=sourceGraphError?`Map request failed. ${buildApiErrorUiModel(sourceGraphError).message}`:'Map loaded.'; } -async function submitMemoryGraph(event){ - event.preventDefault(); - const form=event.currentTarget; - const button=form.querySelector('button[type=submit]'); - const data=new FormData(form); - const query=String(data.get('query')??'').trim(); - const history=data.get('history')==='on'; - const communities=data.get('communities')==='on'; - button.disabled=true; - button.textContent='Refreshing...'; +async function pageSourceGraph(offset){ + const state={...parseMapUrl(globalThis.location?.href),offset}; + const target=serializeMapUrl(state); + history.pushState({},'',target); + document.querySelector('#live-status').textContent='Loading the requested query page.'; + await loadSourceMap(state); + render(); + document.querySelector('#live-status').textContent=sourceGraphError?`Map request failed. ${buildApiErrorUiModel(sourceGraphError).message}`:'Query page loaded.'; +} + +async function refreshSourceGraph(){ + const state=parseMapUrl(globalThis.location?.href); + document.querySelector('#live-status').textContent='Refreshing the local source scan.'; + await loadSourceMap(state,{refresh:true}); + render(); + document.querySelector('#live-status').textContent=sourceGraphError?`Map refresh failed. ${buildApiErrorUiModel(sourceGraphError).message}`:'Map refreshed.'; +} + +async function submitMemoryGraph(options){ document.querySelector('#live-status').textContent='Refreshing governed memory graph.'; - await loadMemoryGraph({history,query,entity:'',communities}); + await loadMemoryGraph(options); render(); document.querySelector('#live-status').textContent=memoryGraphError??'Governed memory graph ready.'; } -async function toggleMemoryGraphHistory(event){ - await loadMemoryGraph({...memoryGraphOptions,history:event.currentTarget.checked,entity:''}); +async function toggleMemoryGraphHistory(checked){ + await loadMemoryGraph({...memoryGraphOptions,history:checked,entity:''}); render(); } -function toggleMemoryGraphCommunities(event){ - memoryGraphOptions={...memoryGraphOptions,communities:event.currentTarget.checked}; +function toggleMemoryGraphCommunities(checked){ + memoryGraphOptions={...memoryGraphOptions,communities:checked}; render(); } @@ -3512,12 +3245,7 @@ async function loadRunById(id,{push=true}={}){ } } -function esc(value){return String(value??'').replace(/[&<>'"]/g,char=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[char]))} -function relativeAge(from,to){const start=Date.parse(String(from??''));const end=Date.parse(String(to??''));if(!Number.isFinite(start)||!Number.isFinite(end)||endpart[0]?.toUpperCase()+part.slice(1)).join(' ')||'Step'} function labelize(value){return titleize(value).replace(/\bId\b/g,'ID')} function safeText(value){return String(value??'').replace(/[\r\n\t]+/g,' ').slice(0,160)} function previewText(value){return safeText(value).slice(0,220)} @@ -3530,165 +3258,11 @@ function memoryDisplayText(value){const text=String(value??'');return /sk-[A-Za- function safeKeyValueList(value){if(!value||typeof value!=='object'||Array.isArray(value))return [];return Object.entries(value).filter(([key])=>!/prompt|body|credential|token|secret|path|url|reasoning|sql/i.test(key)).slice(0,8).map(([key,raw])=>({key:labelize(key),value:Array.isArray(raw)?raw.slice(0,4).map(safeText).join(', '):safeText(raw)}))} function quoteShell(value){return `'${String(value??'').replaceAll("'","'\"'\"'")}'`} -function memoryGraphNodeColor(type,community=0,useCommunity=false){ - if(useCommunity){ - const palette=['#4cc9a6','#7aa2ff','#f7b955','#e56b8b','#b38cff','#62d3ff','#9bd66f','#f08f4f']; - return palette[Math.abs(Number(community??0))%palette.length]; - } - return ({project:'#4cc9a6',provider:'#7aa2ff',port:'#f7b955',decision:'#e56b8b',module:'#b38cff',entity:'#8a96a8'})[type]??'#8a96a8'; -} - -function memoryGraphVisiblePayload(report){ - const nodes=Array.isArray(report?.graph?.nodes)?report.graph.nodes:[]; - const edges=Array.isArray(report?.graph?.edges)?report.graph.edges:[]; - const focusNames=new Set((report?.focus?.nodes??[]).map((node)=>node.name)); - if(!focusNames.size)return {nodes,edges}; - return { - nodes:nodes.filter((node)=>focusNames.has(node.name)), - edges:edges.filter((edge)=>focusNames.has(edge.from)&&focusNames.has(edge.to)) - }; -} - -function layoutMemoryGraph(nodes,edges,width,height){ - const positions=new Map(); - const centerX=width/2; - const centerY=height/2; - const radius=Math.max(80,Math.min(width,height)*0.36); - nodes.forEach((node,index)=>{ - const angle=(Math.PI*2*index)/Math.max(1,nodes.length); - positions.set(node.id,{x:centerX+Math.cos(angle)*radius,y:centerY+Math.sin(angle)*radius,vx:0,vy:0,node}); - }); - const linked=edges.map((edge)=>({source:positions.get(edge.from),target:positions.get(edge.to),edge})).filter((item)=>item.source&&item.target); - for(let tick=0;tick<90;tick+=1){ - for(let i=0;inode.name)); - const query=String(options.query??'').toLowerCase(); - ctx.lineCap='round'; - for(const edge of edges){ - const source=positions.get(edge.from); - const target=positions.get(edge.to); - if(!source||!target)continue; - const focused=!focusNodes.size||focusNodes.has(edge.from)||focusNodes.has(edge.to); - ctx.globalAlpha=edge.current?(focused?0.72:0.32):0.18; - ctx.strokeStyle=edge.current?'#4f5d75':'#9aa3b2'; - ctx.lineWidth=edge.current?1.4:1; - ctx.setLineDash(edge.current?[]:[5,5]); - ctx.beginPath(); - ctx.moveTo(source.x,source.y); - ctx.lineTo(target.x,target.y); - ctx.stroke(); - ctx.setLineDash([]); - const labelX=(source.x+target.x)/2; - const labelY=(source.y+target.y)/2; - ctx.globalAlpha=edge.current?0.75:0.32; - ctx.fillStyle='#c7ced9'; - ctx.font='11px Inter, system-ui, sans-serif'; - ctx.fillText(edge.predicate.slice(0,28),labelX+4,labelY-4); - } - ctx.globalAlpha=1; - for(const node of nodes){ - const point=positions.get(node.id); - if(!point)continue; - const matched=query&&node.name.toLowerCase().includes(query); - const focused=!focusNodes.size||focusNodes.has(node.name); - const r=Number(node.size??10)+(matched?4:0); - ctx.globalAlpha=node.current?(focused?1:0.52):0.34; - ctx.fillStyle=memoryGraphNodeColor(node.type,node.community,options.communities); - ctx.beginPath(); - ctx.arc(point.x,point.y,r,0,Math.PI*2); - ctx.fill(); - if(node.governedDecision||matched){ - ctx.strokeStyle=node.governedDecision?'#ff7395':'#f7b955'; - ctx.lineWidth=3; - ctx.stroke(); - } - ctx.globalAlpha=node.current?0.92:0.46; - ctx.fillStyle='#f5f7fb'; - ctx.font='12px Inter, system-ui, sans-serif'; - ctx.fillText(node.name.slice(0,34),point.x+r+5,point.y+4); - } - ctx.globalAlpha=1; - canvas.onclick=async (event)=>{ - const box=canvas.getBoundingClientRect(); - const x=(event.clientX-box.left)*(width/box.width); - const y=(event.clientY-box.top)*(height/box.height); - let selected=null; - let best=Infinity; - for(const node of nodes){ - const point=positions.get(node.id); - if(!point)continue; - const distance=Math.hypot(point.x-x,point.y-y); - const hit=(Number(node.size??10)+8); - if(distance{if(event.key==='Escape'){const menu=document.querySelector('#repository-menu[open]');if(menu){menu.open=false;menu.querySelector('summary')?.focus()}}}); - window.addEventListener('popstate',async()=>{activeRunDetail=null;const runId=new URL(location.href).searchParams.get('run');if(currentRoute().id==='runs'&&runId)loadRunById(runId,{push:false});else if(currentRoute().id==='source-graph'){await loadRecallMap();render()}else render()}); + window.addEventListener('popstate',async()=>{activeRunDetail=null;const runId=new URL(location.href).searchParams.get('run');if(currentRoute().id==='runs'&&runId)loadRunById(runId,{push:false});else if(currentRoute().id==='source-graph'){await loadSourceMap(parseMapUrl(location.href));render()}else render()}); const runId=new URL(location.href).searchParams.get('run'); load().then(()=>{if(runId)loadRunById(runId,{push:false})}); } diff --git a/apps/web/graph-layout-worker.js b/apps/web/graph-layout-worker.js new file mode 100644 index 00000000..c196a747 --- /dev/null +++ b/apps/web/graph-layout-worker.js @@ -0,0 +1,71 @@ +export function layoutFocusedGraph(nodes = [], edges = [], width = 960, height = 560) { + if (nodes.length > 200 || edges.length > 400) throw new Error('graph_layout_bounds_exceeded'); + const ordered = [...nodes].sort((left, right) => String(left.id).localeCompare(String(right.id))); + const adjacency = new Map(ordered.map(({ id }) => [id, new Set()])); + for (const { fromNodeId, toNodeId } of edges) { + if (!adjacency.has(fromNodeId) || !adjacency.has(toNodeId)) continue; + adjacency.get(fromNodeId).add(toNodeId); + adjacency.get(toNodeId).add(fromNodeId); + } + + const roots = [...ordered].sort((left, right) => ( + adjacency.get(right.id).size - adjacency.get(left.id).size + || String(left.id).localeCompare(String(right.id)) + )); + const layerById = new Map(); + let componentOffset = 0; + for (const root of roots) { + if (layerById.has(root.id)) continue; + const queue = [{ id: root.id, layer: componentOffset }]; + let cursor = 0; + let componentMax = componentOffset; + while (cursor < queue.length) { + const current = queue[cursor++]; + if (layerById.has(current.id)) continue; + layerById.set(current.id, current.layer); + componentMax = Math.max(componentMax, current.layer); + for (const neighbor of [...adjacency.get(current.id)].sort()) { + if (!layerById.has(neighbor)) queue.push({ id: neighbor, layer: current.layer + 1 }); + } + } + componentOffset = componentMax + 2; + } + + const layers = new Map(); + for (const node of ordered) { + const layer = layerById.get(node.id) ?? 0; + if (!layers.has(layer)) layers.set(layer, []); + layers.get(layer).push(node.id); + } + const layerIds = [...layers.keys()].sort((left, right) => left - right); + const safeWidth = Math.max(160, Number(width) || 960); + const safeHeight = Math.max(160, Number(height) || 560); + const xStep = (safeWidth - 96) / Math.max(1, layerIds.length - 1); + const positions = {}; + layerIds.forEach((layer, column) => { + const ids = layers.get(layer); + const yStep = (safeHeight - 96) / Math.max(1, ids.length - 1); + ids.forEach((id, row) => { + positions[id] = { + x: 48 + column * xStep, + y: ids.length === 1 ? safeHeight / 2 : 48 + row * yStep + }; + }); + }); + return { + positions, + bounds: { minX: 48, minY: 48, maxX: safeWidth - 48, maxY: safeHeight - 48 } + }; +} + +if (typeof self !== 'undefined') { + self.onmessage = ({ data }) => { + const { requestId, nodes = [], edges = [], width = 960, height = 560 } = data ?? {}; + if (nodes.length > 200 || edges.length > 400) { + self.postMessage({ requestId, error: 'graph_layout_bounds_exceeded' }); + return; + } + const { positions, bounds } = layoutFocusedGraph(nodes, edges, width, height); + self.postMessage({ requestId, positions, bounds }); + }; +} diff --git a/apps/web/graph-viewport.js b/apps/web/graph-viewport.js new file mode 100644 index 00000000..08870501 --- /dev/null +++ b/apps/web/graph-viewport.js @@ -0,0 +1,356 @@ +export function createGraphViewport(canvas, outline, inspector, { + nodes = [], + edges = [], + onSelect = () => {}, + onError = () => {} +} = {}) { + if (!canvas) return inertViewport(); + if (nodes.length > 200 || edges.length > 400) { + onError('graph_layout_bounds_exceeded'); + return inertViewport(); + } + + const worker = new Worker(new URL('./graph-layout-worker.js', import.meta.url), { type: 'module' }); + const controller = new AbortController(); + const signal = controller.signal; + const context = canvas.getContext('2d'); + const nodeById = new Map(nodes.map((node) => [node.id, node])); + let positions = {}; + let bounds = null; + let requestId = 0; + let selectedNodeId = outline?.querySelector('[data-node-id][aria-current="true"]')?.dataset.nodeId ?? nodes[0]?.id ?? null; + let scale = 1; + let panX = 0; + let panY = 0; + let pointer = null; + let hoveredNodeId = null; + + const resizeObserver = typeof ResizeObserver === 'function' + ? new ResizeObserver(() => requestLayout()) + : null; + resizeObserver?.observe(canvas); + if (!resizeObserver && typeof window !== 'undefined') window.addEventListener('resize', requestLayout, { signal }); + + worker.addEventListener('message', ({ data }) => { + if (data?.requestId !== requestId) return; + if (data.error) { + onError(data.error); + return; + } + positions = data.positions ?? {}; + bounds = data.bounds ?? null; + canvas.dataset.layoutReady = 'true'; + reset(); + }, { signal }); + worker.addEventListener('error', () => onError('graph_layout_worker_failed'), { signal }); + + outline?.addEventListener('click', (event) => { + const button = event.target.closest('[data-node-id]'); + if (!button) return; + select(button.dataset.nodeId, { focusView: true }); + }, { signal }); + canvas.addEventListener('pointerdown', (event) => { + pointer = { id: event.pointerId, x: event.clientX, y: event.clientY, moved: false }; + canvas.setPointerCapture?.(event.pointerId); + }, { signal }); + canvas.addEventListener('pointermove', (event) => { + if (!pointer || pointer.id !== event.pointerId) { + const nextHoveredNodeId = hitTest(event); + if (nextHoveredNodeId !== hoveredNodeId) { + hoveredNodeId = nextHoveredNodeId; + canvas.style.cursor = hoveredNodeId ? 'pointer' : 'grab'; + draw(); + } + return; + } + const dx = event.clientX - pointer.x; + const dy = event.clientY - pointer.y; + if (Math.abs(dx) + Math.abs(dy) > 2) pointer.moved = true; + pointer.x = event.clientX; + pointer.y = event.clientY; + panX += dx; + panY += dy; + draw(); + }, { signal }); + canvas.addEventListener('pointerup', (event) => { + if (!pointer || pointer.id !== event.pointerId) return; + if (!pointer.moved) { + const nodeId = hitTest(event); + if (nodeId) select(nodeId); + } + pointer = null; + canvas.releasePointerCapture?.(event.pointerId); + }, { signal }); + canvas.addEventListener('pointercancel', () => { pointer = null; }, { signal }); + canvas.addEventListener('pointerleave', () => { + if (!pointer && hoveredNodeId) { + hoveredNodeId = null; + canvas.style.cursor = 'grab'; + draw(); + } + }, { signal }); + canvas.addEventListener('wheel', (event) => { + event.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const anchorX = event.clientX - rect.left; + const anchorY = event.clientY - rect.top; + const nextScale = clamp(scale * Math.exp(-event.deltaY * 0.0015), 0.5, 2.5); + const graphX = (anchorX - panX) / scale; + const graphY = (anchorY - panY) / scale; + scale = nextScale; + panX = anchorX - graphX * scale; + panY = anchorY - graphY * scale; + draw(); + }, { passive: false, signal }); + + const stage = canvas.closest('.source-map-stage'); + stage?.querySelector('[data-graph-action="fit"]')?.addEventListener('click', () => fit(), { signal }); + stage?.querySelector('[data-graph-action="reset"]')?.addEventListener('click', () => reset(), { signal }); + + if (selectedNodeId) select(selectedNodeId); + requestLayout(); + + function requestLayout() { + const rect = canvas.getBoundingClientRect(); + const width = Math.max(320, Math.round(rect.width || canvas.width || 960)); + const height = Math.max(280, Math.round(rect.height || canvas.height || 560)); + syncCanvas(width, height); + requestId += 1; + worker.postMessage({ requestId, nodes, edges, width, height }); + } + + function syncCanvas(width, height) { + const ratio = Math.min(2, globalThis.devicePixelRatio || 1); + canvas.width = Math.round(width * ratio); + canvas.height = Math.round(height * ratio); + canvas.dataset.pixelRatio = String(ratio); + draw(); + } + + function select(nodeId, { focusView = false } = {}) { + if (!nodeById.has(nodeId)) return; + selectedNodeId = nodeId; + outline?.querySelectorAll('[data-node-id]').forEach((button) => { + if (button.dataset.nodeId === nodeId) button.setAttribute('aria-current', 'true'); + else button.removeAttribute('aria-current'); + }); + if (inspector) inspector.dataset.selectedNodeId = nodeId; + onSelect(nodeById.get(nodeId)); + if (focusView) focus(nodeId); + else draw(); + } + + function focus(nodeId = selectedNodeId) { + const point = positions[nodeId]; + if (!point) return; + const { width, height } = cssSize(); + scale = Math.max(1, scale); + panX = width / 2 - point.x * scale; + panY = height / 2 - point.y * scale; + draw(); + } + + function fit() { + if (selectedNodeId && positions[selectedNodeId]) { + scale = 1.35; + focus(selectedNodeId); + return; + } + fitBounds(); + } + + function fitBounds() { + if (!bounds) return reset(); + const { width, height } = cssSize(); + const graphWidth = Math.max(1, bounds.maxX - bounds.minX); + const graphHeight = Math.max(1, bounds.maxY - bounds.minY); + scale = clamp(Math.min((width - 48) / graphWidth, (height - 48) / graphHeight), 0.5, 2.5); + panX = (width - (bounds.minX + bounds.maxX) * scale) / 2; + panY = (height - (bounds.minY + bounds.maxY) * scale) / 2; + draw(); + } + + function reset() { + scale = 1; + panX = 0; + panY = 0; + draw(); + } + + function draw() { + if (!context) return; + const ratio = Number(canvas.dataset.pixelRatio) || 1; + const { width, height } = cssSize(); + const palette = graphPalette(canvas); + context.setTransform(ratio, 0, 0, ratio, 0, 0); + context.clearRect(0, 0, width, height); + context.fillStyle = palette.panel; + context.fillRect(0, 0, width, height); + context.save(); + context.translate(panX, panY); + context.scale(scale, scale); + context.lineWidth = 1 / scale; + context.strokeStyle = palette.rule; + for (const edge of edges) { + const from = positions[edge.fromNodeId]; + const to = positions[edge.toNodeId]; + if (!from || !to) continue; + context.beginPath(); + context.moveTo(from.x, from.y); + context.lineTo(to.x, to.y); + context.stroke(); + } + context.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace'; + context.textBaseline = 'middle'; + const visibleLabelIds = visibleGraphLabelIds({ + nodes, + edges, + selectedNodeId, + hoveredNodeId, + scale, + budget: scale < 0.75 ? 5 : scale < 1.35 ? 8 : 14 + }); + for (const node of nodes) { + const point = positions[node.id]; + if (!point) continue; + const selected = node.id === selectedNodeId; + context.fillStyle = selected ? palette.accent : palette.ink; + context.beginPath(); + context.arc(point.x, point.y, selected ? 6 : 4, 0, Math.PI * 2); + context.fill(); + } + const placedLabelBoxes = []; + const labelNodes = nodes + .filter((node) => visibleLabelIds.has(node.id) && positions[node.id]) + .sort((left, right) => labelPriority(right.id, selectedNodeId, hoveredNodeId) - labelPriority(left.id, selectedNodeId, hoveredNodeId)); + for (const node of labelNodes) { + const point = positions[node.id]; + const label = String(node.label ?? '').slice(0, 32); + const labelWidth = context.measureText(label).width; + const placement = graphLabelPlacement({ + pointX: point.x, + labelWidth, + scale, + panX, + viewportWidth: width + }); + const anchorX = point.x + placement.offset; + const screenAnchorX = panX + anchorX * scale; + const screenY = panY + point.y * scale; + const scaledWidth = labelWidth * scale; + const box = { + left: placement.align === 'right' ? screenAnchorX - scaledWidth : screenAnchorX, + right: placement.align === 'right' ? screenAnchorX : screenAnchorX + scaledWidth, + top: screenY - 7, + bottom: screenY + 7 + }; + const forced = node.id === selectedNodeId || node.id === hoveredNodeId; + if (!forced && graphLabelBoxesOverlap(box, placedLabelBoxes)) continue; + placedLabelBoxes.push(box); + context.fillStyle = palette.ink; + context.textAlign = placement.align; + context.fillText(label, anchorX, point.y); + } + context.restore(); + } + + function hitTest(event) { + const rect = canvas.getBoundingClientRect(); + const x = ((event.clientX - rect.left) - panX) / scale; + const y = ((event.clientY - rect.top) - panY) / scale; + let selected = null; + let distance = 12 / scale; + for (const node of nodes) { + const point = positions[node.id]; + if (!point) continue; + const candidate = Math.hypot(point.x - x, point.y - y); + if (candidate <= distance) { + selected = node.id; + distance = candidate; + } + } + return selected; + } + + function cssSize() { + const ratio = Number(canvas.dataset.pixelRatio) || 1; + return { width: canvas.width / ratio, height: canvas.height / ratio }; + } + + return { + fit, + reset, + focus, + destroy() { + controller.abort(); + resizeObserver?.disconnect(); + worker.terminate(); + } + }; +} + +export function visibleGraphLabelIds({ nodes = [], edges = [], selectedNodeId = null, hoveredNodeId = null, scale = 1, budget = 8 } = {}) { + const nodeIds = new Set(nodes.map((node) => node.id)); + const limit = Math.max(1, Math.min(nodes.length, Math.round(Number(budget) || 8) + (Number(scale) >= 1.75 ? 4 : 0))); + const visible = new Set(); + const add = (nodeId) => { + if (visible.size < limit && nodeIds.has(nodeId)) visible.add(nodeId); + }; + add(selectedNodeId); + add(hoveredNodeId); + for (const anchor of [selectedNodeId, hoveredNodeId]) { + if (!anchor) continue; + for (const edge of edges) { + if (edge.fromNodeId === anchor) add(edge.toNodeId); + if (edge.toNodeId === anchor) add(edge.fromNodeId); + } + } + for (const node of nodes) add(node.id); + return visible; +} + +export function graphLabelBoxesOverlap(box, placedBoxes = [], gap = 4) { + const padding = Math.max(0, Number(gap) || 0); + return placedBoxes.some((placed) => !( + box.right + padding < placed.left + || box.left - padding > placed.right + || box.bottom + padding < placed.top + || box.top - padding > placed.bottom + )); +} + +function labelPriority(nodeId, selectedNodeId, hoveredNodeId) { + if (nodeId === selectedNodeId) return 2; + if (nodeId === hoveredNodeId) return 1; + return 0; +} + +export function graphLabelPlacement({ pointX = 0, labelWidth = 0, scale = 1, panX = 0, viewportWidth = 0 } = {}) { + const resolvedScale = Math.max(0.01, Number(scale) || 1); + const screenX = Number(panX) + Number(pointX) * resolvedScale; + const scaledLabelWidth = Math.max(0, Number(labelWidth) || 0) * resolvedScale; + const rightEdge = screenX + (10 * resolvedScale) + scaledLabelWidth; + const leftEdge = screenX - (10 * resolvedScale) - scaledLabelWidth; + return rightEdge > Number(viewportWidth) - 8 && leftEdge >= 8 + ? { align: 'right', offset: -10 } + : { align: 'left', offset: 10 }; +} + +function graphPalette(canvas) { + const styles = getComputedStyle(canvas); + const token = (name, fallback) => styles.getPropertyValue(name).trim() || fallback; + return { + panel: token('--color-panel', '#ffffff'), + rule: token('--color-rule-strong', '#a7a7a7'), + ink: token('--color-ink', '#202020'), + accent: token('--color-accent', '#345cff') + }; +} + +function clamp(value, minimum, maximum) { + return Math.max(minimum, Math.min(maximum, value)); +} + +function inertViewport() { + return { fit() {}, reset() {}, focus() {}, destroy() {} }; +} diff --git a/apps/web/index.html b/apps/web/index.html index 571712e3..7886bf2b 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -36,14 +36,22 @@
Branch unavailable Not scanned + Local only / External writes off
- Loading + Loading Settings
diff --git a/apps/web/memory-graph-view.js b/apps/web/memory-graph-view.js new file mode 100644 index 00000000..1ee7c18f --- /dev/null +++ b/apps/web/memory-graph-view.js @@ -0,0 +1,215 @@ +import { createGraphViewport } from './graph-viewport.js'; +import { escapeHtml, formatDate, renderApiErrorPanel, statePanel } from './ui-primitives.js'; + +export function buildMemoryGraphViewModel(report = null, options = {}, error = null) { + const sourceNodes = arrayValue(report?.graph?.nodes); + const sourceEdges = arrayValue(report?.graph?.edges); + const history = options.history === true; + const query = String(options.query ?? '').trim().slice(0, 512); + const groupRelated = options.communities === true; + const currentNodes = history ? sourceNodes : sourceNodes.filter(({ current }) => current !== false); + const currentEdges = history ? sourceEdges : sourceEdges.filter(({ current }) => current !== false); + const focusIds = new Set(arrayValue(report?.focus?.nodes).flatMap((node) => [node.id, node.name]).filter(Boolean)); + const focusedNodes = focusIds.size ? currentNodes.filter((node) => focusIds.has(node.id) || focusIds.has(node.name)) : currentNodes; + const { nodes, edges } = filterMemoryGraph(focusedNodes, currentEdges, query); + const orderedNodes = groupRelated + ? [...nodes].sort((left, right) => nonNegativeInteger(left.community) - nonNegativeInteger(right.community) || String(left.name ?? left.id).localeCompare(String(right.name ?? right.id))) + : nodes; + const boundedNodes = orderedNodes.length > 200 + ? [...orderedNodes].sort((left, right) => nonNegativeInteger(right.degree) - nonNegativeInteger(left.degree) || String(left.id).localeCompare(String(right.id))).slice(0, 200) + : orderedNodes; + const boundedEdges = edges.length > 400 + ? [...edges].sort((left, right) => Number(right.current !== false) - Number(left.current !== false) || String(left.id).localeCompare(String(right.id))).slice(0, 400) + : edges; + const nodeIds = new Set(boundedNodes.map(({ id }) => id)); + const viewportEdges = boundedEdges + .filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to)) + .map((edge) => ({ ...edge, fromNodeId: edge.from, toNodeId: edge.to, kind: edge.predicate })); + const summary = { + nodeCount: sourceNodes.length, + edgeCount: sourceEdges.length, + currentNodeCount: sourceNodes.filter(({ current }) => current !== false).length, + currentEdgeCount: sourceEdges.filter(({ current }) => current !== false).length, + historyNodeCount: sourceNodes.filter(({ current }) => current === false).length, + historyEdgeCount: sourceEdges.filter(({ current }) => current === false).length, + ...(report?.summary ?? {}) + }; + return Object.freeze({ + ready: Boolean(report?.graph), + error, + workspaceId: report?.workspaceId ?? 'ws_local', + provider: report?.provider ?? 'provider:native:memory:sqlite', + generatedAt: report?.generatedAt ?? null, + reportFingerprint: report?.reportFingerprint ?? null, + safeguards: report?.safeguards ?? {}, + summary, + query, + history, + groupRelated, + nodes: Object.freeze(boundedNodes.map((node) => Object.freeze({ ...node, label: node.name ?? node.id }))), + edges: Object.freeze(boundedEdges), + viewportEdges: Object.freeze(viewportEdges), + omittedNodeCount: Math.max(0, orderedNodes.length - boundedNodes.length), + omittedEdgeCount: Math.max(0, edges.length - boundedEdges.length), + sourceNodeCount: sourceNodes.length, + sourceEdgeCount: sourceEdges.length + }); +} + +export function renderMemoryGraphView(model) { + if (model.error) return `
${memoryGraphHeading()}${renderApiErrorPanel('Memory graph unavailable', model.error)}
`; + if (!model.ready) return `
${memoryGraphHeading()}${statePanel('empty', 'Memory graph not loaded', 'The local memory provider did not return a graph report.')}
`; + + const controls = renderMemoryGraphControls(model); + if (model.sourceNodeCount === 0) { + return `
${memoryGraphHeading()}${controls}

No governed memory yet

Checked workspace ${escapeHtml(model.workspaceId)} through ${escapeHtml(model.provider)}. No approved temporal facts are available.

Review or add a source-backed proposal on the Memory route, then refresh this view.

${memoryGraphBoundary(model)}
`; + } + if (model.nodes.length === 0) { + return `
${memoryGraphHeading()}${controls}${statePanel('empty', 'No matching memory', 'No governed facts match the current search and history scope.')}${memoryGraphBoundary(model)}
`; + } + + const first = model.nodes[0]; + return `
+ ${memoryGraphHeading()} + ${controls} +
+
+

Governed facts

${model.nodes.length} visible records / ${model.edges.length} visible relationships${model.omittedNodeCount || model.omittedEdgeCount ? ` / ${model.omittedNodeCount} records and ${model.omittedEdgeCount} relationships omitted by display bounds` : ''}

+ +
+
+ +
+
+

Fact history

${memoryEdgeList(model.edges)}
+ ${memoryGraphBoundary(model)} +
+
`; +} + +export function bindMemoryGraph(root, { + model, + onSubmit, + onHistoryChange, + onGroupChange, + onSelectNode +} = {}) { + if (!root) return () => {}; + const controller = new AbortController(); + const signal = controller.signal; + let viewport = null; + root.querySelector('#memory-graph-form')?.addEventListener('submit', (event) => { + event.preventDefault(); + onSubmit?.(memoryGraphOptionsFromForm(event.currentTarget), event); + }, { signal }); + root.querySelector('#memory-graph-history')?.addEventListener('change', (event) => onHistoryChange?.(event.currentTarget.checked, event), { signal }); + root.querySelector('#memory-graph-communities')?.addEventListener('change', (event) => onGroupChange?.(event.currentTarget.checked, event), { signal }); + + const canvas = root.querySelector('#memory-graph-canvas'); + if (canvas && model) { + viewport = createGraphViewport( + canvas, + root.querySelector('.memory-graph-outline'), + root.querySelector('#memory-graph-selection'), + { + nodes: model.nodes, + edges: model.viewportEdges, + onSelect: (node) => { + const selection = root.querySelector('#memory-graph-selection'); + if (selection) selection.innerHTML = memoryNodeSelection(node); + onSelectNode?.(node); + }, + onError: (code) => { + const failure = root.querySelector('[data-memory-graph-error]'); + if (!failure) return; + failure.hidden = false; + failure.textContent = code === 'graph_layout_bounds_exceeded' + ? 'The visible memory graph exceeds the 200-node or 400-relationship display limit.' + : 'Graph layout could not start. Use the governed memory outline.'; + } + } + ); + root.querySelector('[data-memory-graph-action="fit"]')?.addEventListener('click', () => viewport.fit(), { signal }); + root.querySelector('[data-memory-graph-action="reset"]')?.addEventListener('click', () => viewport.reset(), { signal }); + } + return () => { + controller.abort(); + viewport?.destroy(); + }; +} + +function memoryGraphHeading() { + return '

Graph

Inspect current and historical temporal facts from the local memory store.

Read-only / local SQLite
'; +} + +function renderMemoryGraphControls(model) { + return `
`; +} + +function memoryGraphOutline(nodes, groupRelated) { + let lastCommunity = null; + return `
    ${nodes.map((node, index) => { + const community = nonNegativeInteger(node.community); + const groupLabel = groupRelated && community !== lastCommunity ? `
  1. Related group ${community}
  2. ` : ''; + lastCommunity = community; + return `${groupLabel}
  3. `; + }).join('')}
`; +} + +function memoryNodeSelection(node) { + if (!node) return '

No selection.

'; + return `
Name
${escapeHtml(node.name ?? node.id)}
Status
${escapeHtml(memoryNodeStatus(node))}
Type
${escapeHtml(node.type ?? 'entity')}
Degree
${nonNegativeInteger(node.degree)}
${node.governedDecision ? '
Governance
Decision record
' : ''}
`; +} + +function memoryEdgeList(edges) { + if (!edges.length) return '

No relationships in the current scope.

'; + return `
    ${edges.slice(0, 16).map((edge) => `
  1. ${escapeHtml(edge.from)} ${escapeHtml(edge.predicate)} ${escapeHtml(edge.to)}${escapeHtml(edge.current === false ? 'Superseded' : 'Current')}Valid from ${escapeHtml(formatDate(edge.validFrom))} / Valid until ${edge.validUntil ? escapeHtml(formatDate(edge.validUntil)) : 'open'} / Provenance ${escapeHtml(edge.source ?? 'unavailable')}
  2. `).join('')}
`; +} + +function memoryGraphBoundary(model) { + const safeguards = model.safeguards ?? {}; + return `

Provider check

Provider
${escapeHtml(model.provider)}
Generated
${escapeHtml(formatDate(model.generatedAt))}
Read-only
${safeguards.readOnly === false ? 'no' : 'yes'}
Model calls
${nonNegativeInteger(safeguards.modelCalls)}
Network calls
${nonNegativeInteger(safeguards.networkCalls)}
External writes
${safeguards.externalWritesEnabled ? 'on' : 'off'}
`; +} + +function memoryGraphOptionsFromForm(form) { + const data = new FormData(form); + return { + query: String(data.get('query') ?? '').trim().slice(0, 512), + history: data.get('history') === 'on', + communities: data.get('communities') === 'on', + entity: '' + }; +} + +function filterMemoryGraph(nodes, edges, query) { + if (!query) return { nodes: [...nodes], edges: [...edges] }; + const needle = query.toLocaleLowerCase(); + const selectedIds = new Set(nodes.filter((node) => [node.id, node.name, node.type].some((value) => String(value ?? '').toLocaleLowerCase().includes(needle))).map(({ id }) => id)); + for (const edge of edges) { + const edgeMatches = [edge.from, edge.to, edge.predicate, edge.source].some((value) => String(value ?? '').toLocaleLowerCase().includes(needle)); + if (edgeMatches || selectedIds.has(edge.from) || selectedIds.has(edge.to)) { + selectedIds.add(edge.from); + selectedIds.add(edge.to); + } + } + return { + nodes: nodes.filter(({ id }) => selectedIds.has(id)), + edges: edges.filter((edge) => selectedIds.has(edge.from) && selectedIds.has(edge.to)) + }; +} + +function memoryNodeStatus(node) { + return node.current === false ? 'Historical' : 'Current'; +} + +function arrayValue(value) { + return Array.isArray(value) ? value : []; +} + +function nonNegativeInteger(value) { + const number = Number(value); + return Number.isFinite(number) && number >= 0 ? Math.floor(number) : 0; +} diff --git a/apps/web/orientation-model.js b/apps/web/orientation-model.js new file mode 100644 index 00000000..db2843de --- /dev/null +++ b/apps/web/orientation-model.js @@ -0,0 +1,575 @@ +import { buildApiErrorUiModel } from './ui-primitives.js'; +import { selectOverviewPrimaryAction } from './shell-model.js'; + +const MAX_GROUPS = 12; +const MAX_RELATIONS = 20; +const MAX_START_ITEMS = 3; +const START_REASON_PRIORITY = new Map([ + ['application_route', 0], + ['package_entry_point', 1], + ['changed_central_module', 2], + ['executable_command', 3], + ['inbound_dependency_hub', 4] +]); +const START_REASON_LABEL = new Map([ + ['application_route', 'application route'], + ['package_entry_point', 'package entry point'], + ['changed_central_module', 'changed central module'], + ['executable_command', 'executable command'], + ['inbound_dependency_hub', 'inbound dependency hub'] +]); + +export function buildOrientationModel({ + report = null, + loading = false, + error = null, + gitChanges = null, + handoff = null, + handoffError = null, + selectedGroupId = null, + now = null +} = {}) { + if (loading) return frozenState('loading', 'Loading repository map', 'Reading bounded local source metadata.'); + if (error) return Object.freeze({ + state: 'failure', + error: buildApiErrorUiModel(error), + title: 'Repository map unavailable', + copy: 'The local API did not return a map. Retry the read-only request.', + groups: Object.freeze([]), + relations: Object.freeze([]), + startHere: Object.freeze([]) + }); + if (!report) return frozenState('empty', 'Repository index unavailable', 'Build the local index, then reload this page.'); + + const architecture = objectValue(report.architecture); + const repository = normalizeRepository(report.repository); + const sourceGraph = objectValue(report.support?.sourceGraph); + const snapshot = objectValue(sourceGraph.snapshot); + const coverage = normalizeCoverage(sourceGraph, snapshot); + const rawGroups = arrayValue(architecture.groups).slice(0, MAX_GROUPS).map(normalizeGroup); + const groupIds = new Set(rawGroups.map(({ id }) => id)); + const relations = Object.freeze(arrayValue(architecture.groupRelations) + .map(normalizeRelation) + .filter((relation) => groupIds.has(relation.sourceGroupId) && groupIds.has(relation.targetGroupId)) + .sort(compareRelations) + .slice(0, MAX_RELATIONS)); + const groups = Object.freeze(layerOrientationGroups(rawGroups, relations)); + const impact = normalizeImpact(architecture.impact, gitChanges, repository); + const startHere = Object.freeze(rankStartHere(startCandidates(architecture, groups, impact))); + const memory = normalizeMemory(report.memory); + const handoffModel = normalizeHandoff({ report, handoff, handoffError, now }); + const selected = chooseSelectedGroup(groups, selectedGroupId); + const noArchitecture = groups.length === 0 + && startHere.length === 0 + && arrayValue(architecture.hotspots).length === 0 + && impact.changedLocators.length === 0 + && impact.affectedSymbols.length === 0; + const state = coverage.status === 'stale' || handoffModel.state === 'review' + ? 'stale' + : noArchitecture + ? 'empty' + : coverage.status === 'failed' + ? 'partial' + : 'success'; + const commands = normalizeCommands(report); + const recentActivity = normalizeActivity(report.memory); + const nativeIndex = arrayValue(sourceGraph.languages).length > 2; + const indexRecovery = sourceIndexRecovery(snapshot.reason); + const trust = Object.freeze({ + memory: Object.freeze({ + status: memory.pendingCount > 0 ? 'pending' : memory.staleCount > 0 ? 'stale' : memory.status === 'available' ? 'current' : memory.status, + activeCount: memory.activeCount, + pendingCount: memory.pendingCount, + staleCount: memory.staleCount, + conflictingCount: memory.conflictingCount + }), + handoff: Object.freeze({ + status: handoffModel.status, + state: handoffModel.state, + createdAt: handoffModel.createdAt, + ageLabel: handoffModel.ageLabel + }), + source: Object.freeze({ + status: coverage.status, + snapshotStatus: coverage.snapshotStatus, + reuse: coverage.reuse + }), + deliveryReduction: measuredDeliveryReduction(report) + }); + const model = { + state, + generatedAt: safeNullableText(report.generatedAt, 64), + repository, + coverage, + groups, + relations, + selectedGroupId: selected, + startHere, + impact, + trust, + index: Object.freeze({ + status: safeText(sourceGraph.status || 'unavailable', 32), + kind: sourceGraph.status === 'implemented' ? 'success' : 'error', + label: sourceGraph.status === 'implemented' ? (nativeIndex ? 'Native index ready' : 'JS/TS map indexed') : 'Source graph unavailable', + copy: sourceGraph.status === 'implemented' ? 'Bounded local metadata only; raw source bodies stay local.' : indexRecovery.copy, + recoveryCommand: sourceGraph.status === 'implemented' ? null : indexRecovery.command + }), + entryPoints: Object.freeze(arrayValue(architecture.entryPoints).map(normalizeStartItem)), + hotspots: Object.freeze(arrayValue(architecture.hotspots).map(normalizeStartItem)), + memory, + handoff: handoffModel, + safeguards: Object.freeze({ ...objectValue(report.safeguards) }), + commands, + recentActivity + }; + model.primaryAction = selectOverviewPrimaryAction(model); + return Object.freeze(model); +} + +function sourceIndexRecovery(reason) { + const code = String(reason ?? '').split(':').at(-1); + if (code === 'source_index_build_required') { + return Object.freeze({ copy: 'Build the local source index, then refresh this page.', command: 'recall graph index --write --engine native --root . --format summary' }); + } + if (code === 'source_index_refresh_required') { + return Object.freeze({ copy: 'Refresh the stale source index, then reload this page.', command: 'recall graph index --refresh --engine native --root . --format summary' }); + } + if (['source_index_repair_required', 'source_index_migration_required', 'source_index_wrong_repository'].includes(code)) { + return Object.freeze({ copy: 'Inspect the local source index and apply the exact repair command it reports.', command: 'recall graph index --doctor --engine native --root . --format summary' }); + } + if (code === 'source_index_schema_newer') { + return Object.freeze({ copy: 'Use a Memory Recall version compatible with this newer source index.', command: null }); + } + if (['native_platform_package_missing', 'native_engine_unavailable'].includes(code)) { + return Object.freeze({ copy: 'Install the matching Memory Recall native package, then reload this page.', command: 'npm install -g memory-recall' }); + } + return Object.freeze({ copy: 'The local source index could not be read. Check it, then reload this page.', command: 'recall graph index --doctor --engine native --root . --format summary' }); +} + +export function layerOrientationGroups(inputGroups = [], inputRelations = []) { + const groups = inputGroups.slice(0, MAX_GROUPS).map((group) => ({ ...group })); + const byId = new Map(groups.map((group) => [group.id, group])); + const adjacency = new Map(groups.map((group) => [group.id, []])); + for (const relation of inputRelations.slice(0, MAX_RELATIONS)) { + if (!byId.has(relation.sourceGroupId) || !byId.has(relation.targetGroupId)) continue; + adjacency.get(relation.sourceGroupId).push(relation.targetGroupId); + } + for (const targets of adjacency.values()) targets.sort(compareGroupIds(byId)); + const components = stronglyConnectedComponents(groups, adjacency); + const componentByGroup = new Map(); + components.forEach((component, index) => component.forEach((id) => componentByGroup.set(id, index))); + const predecessors = new Map(components.map((_, index) => [index, new Set()])); + for (const [source, targets] of adjacency) { + for (const target of targets) { + const sourceComponent = componentByGroup.get(source); + const targetComponent = componentByGroup.get(target); + if (sourceComponent !== targetComponent) predecessors.get(targetComponent).add(sourceComponent); + } + } + const componentLayers = new Map(); + const layerFor = (component) => { + if (componentLayers.has(component)) return componentLayers.get(component); + const previous = [...predecessors.get(component)].map(layerFor); + const layer = previous.length ? Math.max(...previous) + 1 : 0; + componentLayers.set(component, layer); + return layer; + }; + components.forEach((_, index) => layerFor(index)); + return groups + .map((group) => Object.freeze({ ...group, layer: componentLayers.get(componentByGroup.get(group.id)) ?? 0 })) + .sort((left, right) => left.layer - right.layer || left.prefix.localeCompare(right.prefix)); +} + +export function selectOrientationGroup(model, groupId) { + const selected = model?.groups?.some(({ id }) => id === groupId) ? groupId : model?.selectedGroupId ?? null; + return Object.freeze({ ...model, selectedGroupId: selected }); +} + +export function normalizeOrientationGitChanges(report = null, error = null) { + const empty = { changedLocators: [], totalCount: 0, omittedCount: 0, truncated: false }; + if (error) return { status: 'error', ...empty, reason: safeText(error?.code || 'git_detection_failed', 64), message: safeGitMessage(error?.message, 'Git change detection failed. Retry the local scan.') }; + if (report?.status === 'error') return { status: 'error', ...empty, reason: safeText(report.reason || 'git_detection_failed', 64), message: safeGitMessage(report.message, 'Git change detection failed. Retry the local scan.') }; + if (report?.status === 'unavailable') { + const reason = safeText(report.reason || 'git_unavailable', 64); + return { status: 'unavailable', ...empty, reason, message: gitUnavailableMessage(reason) }; + } + if (report?.status !== 'available') return { status: 'unknown', ...empty, reason: 'not_run', message: 'Git change detection has not run.' }; + const changedLocators = arrayValue(report.changedLocators).map((value) => safeText(value, 512).replace(/^workspace:\/\//u, '')).filter(Boolean).slice(0, 16); + const skippedCount = Math.max(0, finiteNumber(report.skippedCount)); + const totalBase = finiteNumber(report.totalCount ?? report.totalChangedLocatorCount ?? changedLocators.length); + const omittedBase = finiteNumber(report.omittedCount ?? report.omittedChangedLocatorCount ?? 0); + return { + status: 'available', + changedLocators, + totalCount: Math.max(changedLocators.length, totalBase + (report.totalCount == null ? skippedCount : 0)), + omittedCount: Math.max(0, omittedBase) + (report.omittedCount == null ? skippedCount : 0), + truncated: report.truncated === true + }; +} + +function normalizeRepository(value) { + const repository = objectValue(value); + return Object.freeze({ + name: safeText(repository.name || 'Local workspace', 160), + branch: safeNullableText(repository.branch, 160), + commitSha: safeNullableText(repository.commitSha, 64), + dirtyCount: finiteNumber(repository.dirtyCount), + gitStatusAvailable: repository.gitStatusAvailable === true, + reason: safeNullableText(repository.reason, 64) + }); +} + +function normalizeCoverage(sourceGraph, snapshot) { + const raw = objectValue(sourceGraph.coverage); + const rawStatus = safeText(raw.status || 'unavailable', 32); + const snapshotStatus = safeText(snapshot.status || (sourceGraph.status === 'implemented' ? 'fresh' : 'unavailable'), 32); + const status = snapshotStatus === 'stale' || rawStatus === 'stale' + ? 'stale' + : sourceGraph.status !== 'implemented' || rawStatus === 'unavailable' || snapshotStatus === 'unavailable' + ? 'failed' + : rawStatus === 'complete' + ? 'complete' + : 'partial'; + return Object.freeze({ + status, + rawStatus, + snapshotStatus, + reuse: safeText(snapshot.reuse || 'none', 32), + builtAt: safeNullableText(snapshot.builtAt, 64), + buildDurationMs: snapshot.buildDurationMs == null ? null : finiteNumber(snapshot.buildDurationMs), + analyzedFileCount: finiteNumber(raw.analyzedFileCount), + maxFiles: finiteNumber(raw.maxFiles), + label: `${finiteNumber(raw.analyzedFileCount)} / ${finiteNumber(raw.maxFiles)} files`, + diagnosticCount: finiteNumber(raw.diagnosticCount), + reasonCodes: Object.freeze(arrayValue(raw.reasonCodes).map((code) => safeText(code, 64)).filter(Boolean).slice(0, 16)), + reason: safeNullableText(snapshot.reason, 64), + lastValidSnapshotShown: status === 'stale' + }); +} + +function normalizeGroup(group, index) { + const value = objectValue(group); + const prefix = safeText(value.prefix || value.label || `group-${index + 1}`, 512); + return Object.freeze({ + id: safeText(value.id || `group_${index}`, 96), + label: safeText(value.label || prefix.split('/').at(-1) || prefix, 160), + prefix, + fileCount: finiteNumber(value.fileCount), + symbolCount: finiteNumber(value.symbolCount), + changedFileCount: finiteNumber(value.changedFileCount), + coverageStatus: safeText(value.coverageStatus || 'complete', 32), + entryPoints: Object.freeze(arrayValue(value.entryPoints).slice(0, 2).map(normalizeStartItem)) + }); +} + +function normalizeRelation(relation, index) { + const value = objectValue(relation); + return Object.freeze({ + id: safeText(value.id || `relation_${index}`, 96), + sourceGroupId: safeText(value.sourceGroupId || value.fromGroupId, 96), + targetGroupId: safeText(value.targetGroupId || value.toGroupId, 96), + sourcePrefix: safeText(value.sourcePrefix, 512), + targetPrefix: safeText(value.targetPrefix, 512), + kind: safeText(value.kind || dominantKind(value.edgeKindCounts), 32), + count: finiteNumber(value.count), + edgeKindCounts: Object.freeze({ ...objectValue(value.edgeKindCounts) }) + }); +} + +function normalizeImpact(rawImpact, rawGitChanges, repository) { + const impact = objectValue(rawImpact); + const changedLocators = Object.freeze(arrayValue(impact.changedLocators).map((value) => safeText(value, 512)).filter(Boolean).slice(0, 16)); + const representedChangedLocators = Object.freeze(arrayValue(impact.representedChangedLocators).map((value) => safeText(value, 512)).filter(Boolean).slice(0, 16)); + const affectedSymbols = Object.freeze(arrayValue(impact.affectedSymbols).map(normalizeStartItem).slice(0, 20)); + const detected = normalizeOrientationGitChanges(rawGitChanges); + const totalChangedCount = detected.status === 'available' ? detected.totalCount : changedLocators.length; + const unrepresentedChangedCount = Math.max(0, totalChangedCount - representedChangedLocators.length); + const status = detected.status === 'available' && totalChangedCount === 0 && repository.dirtyCount === 0 + ? 'clean' + : detected.status === 'unavailable' || detected.status === 'error' + ? 'unknown' + : totalChangedCount > 0 || changedLocators.length > 0 + ? 'changed' + : 'not scanned'; + return Object.freeze({ + status, + label: status === 'clean' ? 'No local changes detected' : status === 'changed' ? `${totalChangedCount} local change${totalChangedCount === 1 ? '' : 's'}` : status === 'unknown' ? 'Local change detection unavailable' : 'Local changes not scanned', + changedLocators, + representedChangedLocators, + affectedSymbols, + changedCount: changedLocators.length, + representedCount: representedChangedLocators.length, + unrepresentedChangedCount, + affectedCount: affectedSymbols.length, + totalChangedCount, + omittedChangedCount: detected.status === 'available' ? detected.omittedCount : 0, + truncated: detected.truncated === true, + detectionStatus: detected.status, + detectionReason: detected.reason, + detectionMessage: detected.message, + repositoryDirtyCount: repository.dirtyCount, + depth: finiteNumber(impact.depth) + }); +} + +function normalizeMemory(rawMemory) { + const memory = objectValue(rawMemory); + const activeFacts = Object.freeze(arrayValue(memory.activeFacts).slice(0, 20)); + const pendingProposals = Object.freeze(arrayValue(memory.pendingProposals).slice(0, 20)); + const staleCount = finiteNumber(memory.staleFactCount); + return Object.freeze({ + status: safeText(memory.status || 'unavailable', 32), + kind: memory.status === 'available' ? (staleCount > 0 ? 'stale' : 'success') : 'error', + activeFacts, + pendingProposals, + activeCount: activeFacts.length, + pendingCount: pendingProposals.length, + staleCount, + conflictingCount: finiteNumber(memory.conflictingFactCount), + unavailableReason: safeNullableText(memory.unavailableReason, 64) + }); +} + +function normalizeHandoff({ report, handoff, handoffError, now }) { + const raw = objectValue(handoff); + const current = objectValue(raw.current); + const status = safeText(raw.status || current.status || '', 32); + const entryId = current.entryId ?? raw.entryId ?? null; + const entry = arrayValue(raw.entries).find(({ id }) => id === entryId) ?? null; + const createdAt = safeNullableText(raw.createdAt ?? entry?.createdAt, 64); + const state = handoffError + ? 'blocked' + : status === 'verified' + ? 'ready' + : ['stale', 'review'].includes(status) + ? 'review' + : status === 'tampered' + ? 'blocked' + : report.readiness?.handoff?.status === 'available' + ? 'pending' + : 'blocked'; + const end = now ?? raw.generatedAt ?? report.generatedAt; + return Object.freeze({ + state, + status: status || (state === 'pending' ? 'available' : state), + kind: state === 'ready' ? 'success' : state === 'review' || state === 'pending' ? 'partial' : 'error', + command: safeText(report.readiness?.handoff?.command || 'recall handoff', 256), + createdAt, + ageLabel: relativeAge(createdAt, end), + copy: state === 'ready' + ? 'Pinned handoff is verified for the next coding agent.' + : state === 'review' + ? 'Pinned sources changed and need review before handoff.' + : state === 'pending' + ? 'The handoff command is available; no verified pinned packet is active.' + : 'Handoff verification is unavailable. Review the local registry before sharing context.' + }); +} + +function startCandidates(architecture, groups, impact) { + const changed = new Set(impact.changedLocators.map(fileLocator)); + const candidates = [ + ...arrayValue(architecture.entryPoints), + ...groups.flatMap(({ entryPoints }) => entryPoints), + ...arrayValue(architecture.hotspots).map((item) => ({ ...item, reasonCodes: item.reasonCodes ?? ['inbound_dependency_hub'], score: item.score ?? item.total ?? item.inbound ?? 0 })) + ]; + const seen = new Set(); + return candidates.map((item) => { + const normalized = normalizeStartItem(item); + const reasonCode = safeText(arrayValue(item?.reasonCodes)[0] || item?.reasonCode || inferStartReason(normalized, changed), 64); + return { ...normalized, reasonCode, score: finiteNumber(item?.score ?? item?.total ?? 0) }; + }).filter((item) => { + const key = `${item.locator}\u0000${item.label}`; + if (!item.locator || seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +function rankStartHere(items) { + const ranked = [...items] + .filter(({ locator = '' }) => !/(?:^|\/)(?:test|tests|fixtures|generated|vendor)(?:\/|$)/iu.test(locator.replace(/^workspace:\/\//u, ''))) + .sort((left, right) => ( + (START_REASON_PRIORITY.get(left.reasonCode) ?? 99) - (START_REASON_PRIORITY.get(right.reasonCode) ?? 99) + || right.score - left.score + || left.locator.localeCompare(right.locator) + || left.label.localeCompare(right.label) + )); + const selected = []; + const selectedReasons = new Set(); + for (const item of ranked) { + if (selectedReasons.has(item.reasonCode)) continue; + selected.push(item); + selectedReasons.add(item.reasonCode); + if (selected.length === MAX_START_ITEMS) break; + } + for (const item of ranked) { + if (selected.length === MAX_START_ITEMS) break; + if (!selected.includes(item)) selected.push(item); + } + return selected.map((item) => Object.freeze({ ...item, reason: START_REASON_LABEL.get(item.reasonCode) ?? 'ranked source entry point' })); +} + +function normalizeStartItem(value) { + const item = objectValue(value); + return Object.freeze({ + nodeId: safeNullableText(item.nodeId, 96), + label: safeText(item.label || item.name || 'Unnamed source', 240), + qualifiedLabel: safeNullableText(item.qualifiedLabel || item.qualifiedName, 240), + locator: safeNullableText(item.locator, 512), + symbolKind: safeText(item.symbolKind || item.kind || 'module', 32) + }); +} + +function inferStartReason(item, changed) { + const locator = String(item.locator ?? '').replace(/^workspace:\/\//u, '').split('#', 1)[0]; + if (/(?:^|\/)route\.(?:[cm]?[jt]sx?)$/iu.test(locator)) return 'application_route'; + if (/(?:^|\/)(?:index|main)\.(?:[cm]?[jt]sx?)$/iu.test(locator)) return 'package_entry_point'; + if (changed.has(fileLocator(locator))) return 'changed_central_module'; + if (/^(?:apps\/cli|bin\/)|\/bin\//u.test(locator)) return 'executable_command'; + return 'inbound_dependency_hub'; +} + +function stronglyConnectedComponents(groups, adjacency) { + let nextIndex = 0; + const stack = []; + const onStack = new Set(); + const indexById = new Map(); + const lowById = new Map(); + const components = []; + const visit = (id) => { + indexById.set(id, nextIndex); + lowById.set(id, nextIndex); + nextIndex += 1; + stack.push(id); + onStack.add(id); + for (const target of adjacency.get(id) ?? []) { + if (!indexById.has(target)) { + visit(target); + lowById.set(id, Math.min(lowById.get(id), lowById.get(target))); + } else if (onStack.has(target)) { + lowById.set(id, Math.min(lowById.get(id), indexById.get(target))); + } + } + if (lowById.get(id) !== indexById.get(id)) return; + const component = []; + let member; + do { + member = stack.pop(); + onStack.delete(member); + component.push(member); + } while (member !== id); + component.sort(compareGroupIds(new Map(groups.map((group) => [group.id, group])))); + components.push(component); + }; + [...groups].sort((left, right) => left.prefix.localeCompare(right.prefix)).forEach(({ id }) => { + if (!indexById.has(id)) visit(id); + }); + return components; +} + +function normalizeCommands(report) { + const commands = [ + 'recall map --root . --sqlite .local/memory.sqlite --format summary', + ...arrayValue(report.readiness?.nextCommands), + report.readiness?.mcp?.command + ].filter((command, index, all) => typeof command === 'string' && command.length > 0 && all.indexOf(command) === index).slice(0, 5); + return Object.freeze(commands.map((command) => Object.freeze({ label: commandLabel(command), command }))); +} + +function normalizeActivity(rawMemory) { + const memory = objectValue(rawMemory); + return Object.freeze([ + ...arrayValue(memory.pendingProposals).slice(0, 3).map((proposal) => ({ kind: 'proposal', label: 'Memory proposed', detail: safeNullableText(proposal.sourceLocator, 512), at: safeNullableText(proposal.enqueuedAt, 64) })), + ...arrayValue(memory.activeFacts).slice(0, 3).map((fact) => ({ kind: 'memory', label: 'Memory current', detail: safeNullableText(fact.sourceLocator, 512), at: safeNullableText(fact.validFrom, 64) })) + ].sort((left, right) => String(right.at).localeCompare(String(left.at))).slice(0, 5).map(Object.freeze)); +} + +function measuredDeliveryReduction(report) { + const measurement = objectValue(report.deliveryMeasurement ?? report.measurements?.delivery ?? report.memory?.deliveryMeasurement); + const before = finiteNumber(measurement.beforeDeliveryTokens ?? measurement.baselineTokens); + const after = finiteNumber(measurement.afterDeliveryTokens ?? measurement.deliveredTokens); + if (measurement.providerBillingClaimed !== false || before <= 0 || after < 0 || after > before) return null; + return Object.freeze({ before, after, tokensSaved: before - after, percent: Math.round(((before - after) / before) * 100), providerBillingClaimed: false }); +} + +function chooseSelectedGroup(groups, requested) { + if (groups.some(({ id }) => id === requested)) return requested; + return groups.find(({ changedFileCount }) => changedFileCount > 0)?.id ?? groups[0]?.id ?? null; +} + +function compareRelations(left, right) { + return right.count - left.count || left.sourceGroupId.localeCompare(right.sourceGroupId) || left.targetGroupId.localeCompare(right.targetGroupId); +} + +function compareGroupIds(byId) { + return (left, right) => (byId.get(left)?.prefix ?? left).localeCompare(byId.get(right)?.prefix ?? right); +} + +function dominantKind(counts) { + return Object.entries(objectValue(counts)).sort((left, right) => finiteNumber(right[1]) - finiteNumber(left[1]) || left[0].localeCompare(right[0]))[0]?.[0] ?? 'imports'; +} + +function commandLabel(command) { + if (command.startsWith('recall map')) return 'Refresh map locally'; + if (command.startsWith('recall handoff')) return 'Create handoff'; + if (command.includes('mcp inspect')) return 'Inspect read-only MCP'; + if (command.includes('graph stats')) return 'Check source graph'; + return 'Copy command'; +} + +function relativeAge(from, to) { + const start = Date.parse(String(from ?? '')); + const end = Date.parse(String(to ?? '')); + if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return 'Not pinned'; + const minutes = Math.floor((end - start) / 60_000); + if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} old`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'} old`; + const days = Math.floor(hours / 24); + return `${days} day${days === 1 ? '' : 's'} old`; +} + +function fileLocator(locator) { + return String(locator ?? '').replace(/^workspace:\/\//u, '').split('#', 1)[0]; +} + +function safeGitMessage(value, fallback) { + const message = safeText(value, 180); + return !message || /(?:\/Users|\/private|\/var\/folders|https?:|file:|token|secret|api[_-]?key|authorization|cookie)/iu.test(message) ? fallback : message; +} + +function gitUnavailableMessage(reason) { + return ({ + not_git_repository: 'Git change detection is unavailable because this workspace is not a Git repository.', + git_unavailable: 'Git change detection is unavailable because the local Git executable could not be used.', + git_status_failed: 'Git change detection is unavailable because local status could not be read.', + git_status_timeout: 'Git change detection is unavailable because local status timed out.' + })[reason] ?? 'Git change detection is unavailable. Retry the local scan.'; +} + +function frozenState(state, title, copy) { + return Object.freeze({ state, title, copy, groups: Object.freeze([]), relations: Object.freeze([]), startHere: Object.freeze([]) }); +} + +function arrayValue(value) { + return Array.isArray(value) ? value : []; +} + +function objectValue(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; +} + +function safeText(value, maxLength = 240) { + return String(value ?? '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, maxLength); +} + +function safeNullableText(value, maxLength) { + const text = safeText(value, maxLength); + return text || null; +} + +function finiteNumber(value) { + const number = Number(value ?? 0); + return Number.isFinite(number) ? Math.max(0, number) : 0; +} diff --git a/apps/web/orientation-view.js b/apps/web/orientation-view.js new file mode 100644 index 00000000..6b1902a8 --- /dev/null +++ b/apps/web/orientation-view.js @@ -0,0 +1,130 @@ +import { escapeHtml, formatDate, renderApiErrorPanel, statePanel } from './ui-primitives.js'; + +export function renderOrientation(model) { + if (model.state === 'loading') return orientationState('loading', model.title, model.copy, 'Scan repository'); + if (model.state === 'failure' || model.state === 'error') { + return `

Overview

${renderApiErrorPanel(model.title, model.error)}
`; + } + if (model.state === 'empty' && !model.repository) return orientationState('empty', model.title, model.copy, 'Scan repository'); + + const groups = model.groups ?? []; + const selectedGroup = groups.find(({ id }) => id === model.selectedGroupId) ?? groups[0] ?? null; + const boardGroups = architectureBoardGroups(groups, selectedGroup?.id); + const maxLayer = Math.max(0, ...boardGroups.map(({ layer = 0 }) => layer)); + const coverageLabel = coverageText(model.coverage); + const architecture = groups.length + ? `
${boardGroups.map((group) => groupButton(group, model.selectedGroupId)).join('')}${renderRelations(model.relations, boardGroups)}
${architectureDisclosure(groups.length, boardGroups.length)}${architectureOutline(groups, model.selectedGroupId)}` + : renderIndexState(model.index); + + return `
+
+

Overview

${escapeHtml(repositoryLine(model))}

+ ${escapeHtml(coverageLabel)} +
+
+
+

Architecture

${escapeHtml(selectedGroupCopy(selectedGroup))}

${selectedGroup ? `Open in Map` : ''}
+ ${architecture} +
+ +
+
`; +} + +function renderIndexState(index = {}) { + const command = index.recoveryCommand ? `

Run ${escapeHtml(index.recoveryCommand)}

` : ''; + const title = index.recoveryCommand ? (index.label || 'Source index unavailable') : 'No supported groups'; + return `${statePanel('empty', title, index.copy || 'The current index did not return repository groups inside its bounds.')}${command}`; +} + +export function bindOrientation(root, { onSelectGroup, onRefresh } = {}) { + if (!root) return () => {}; + const controller = new AbortController(); + const options = { signal: controller.signal }; + root.querySelectorAll('[data-group-id]').forEach((button) => button.addEventListener('click', () => onSelectGroup?.(button.dataset.groupId), options)); + root.querySelectorAll('[data-action="refresh-recall-map"]').forEach((button) => button.addEventListener('click', (event) => onRefresh?.(event), options)); + return () => controller.abort(); +} + +function orientationState(kind, title, copy, actionLabel) { + return `

Overview

${statePanel(kind, title, copy)}
`; +} + +function groupButton(group, selectedGroupId) { + const selected = group.id === selectedGroupId; + return ``; +} + +function renderRelations(relations = [], groups = []) { + const byId = new Map(groups.map((group) => [group.id, group])); + if (!relations.length) return ''; + return ``; +} + +function architectureBoardGroups(groups, selectedGroupId) { + const maximum = 6; + if (groups.length <= maximum) return groups; + const selected = groups.find(({ id }) => id === selectedGroupId); + if (!selected || groups.slice(0, maximum).some(({ id }) => id === selected.id)) return groups.slice(0, maximum); + return [...groups.slice(0, maximum - 1), selected]; +} + +function architectureDisclosure(total, shown) { + if (shown >= total) return ''; + return `

Showing ${shown} of ${total} groups. Open the outline for the complete bounded map.

`; +} + +function architectureOutline(groups, selectedGroupId) { + return `
Repository architecture outline
    ${groups.map((group) => `
  1. `).join('')}
`; +} + +function renderStartHere(items = []) { + const content = items.length + ? `
    ${items.map((item) => `
  1. ${escapeHtml(item.label)}${escapeHtml(item.locator)}${escapeHtml(item.reason)}
  2. `).join('')}
` + : '

No ranked entry points in the current bounds.

'; + return `

Start here

${items.length} ranked
${content}
`; +} + +function renderImpact(impact = {}) { + const affected = impact.affectedSymbols ?? []; + const detail = impact.status === 'clean' + ? '

No local changes detected.

' + : `

${escapeHtml(impact.label ?? 'Change state unavailable')}

${affected.length ? `
    ${affected.slice(0, 3).map((item) => `
  1. ${escapeHtml(item.label)}${escapeHtml(item.locator)}
  2. `).join('')}
` : ''}${impact.unrepresentedChangedCount ? `${Number(impact.unrepresentedChangedCount)} changed file${impact.unrepresentedChangedCount === 1 ? '' : 's'} outside the represented graph.` : ''}`; + return `

Current impact

${Number(impact.affectedCount ?? 0)} affected
${detail}
`; +} + +function renderTrust(trust = {}) { + const memory = trust.memory ?? {}; + const handoff = trust.handoff ?? {}; + const source = trust.source ?? {}; + return `

Trusted context

Source map
${escapeHtml(source.status ?? 'not scanned')}
Memory
${escapeHtml(memory.status ?? 'unavailable')}${memory.pendingCount ? `, ${Number(memory.pendingCount)} pending` : ''}
Handoff
${escapeHtml(handoff.status ?? 'not pinned')}
${trust.deliveryReduction ? `
Measured delivery
${Number(trust.deliveryReduction.percent)}% fewer tokens
` : ''}
`; +} + +function coverageText(coverage = {}) { + const status = coverage.status === 'failed' ? 'Unavailable' : titleCase(coverage.status || 'not scanned'); + const built = coverage.builtAt ? ` / built ${formatDate(coverage.builtAt)}` : ''; + return `${status}${built}`; +} + +function repositoryLine(model) { + const repository = model.repository ?? {}; + return [repository.name, repository.branch, model.coverage?.label].filter(Boolean).join(' / '); +} + +function selectedGroupCopy(group) { + return group ? `${group.prefix}. ${group.fileCount} files and ${group.symbolCount} symbols.` : 'Bounded local index structure.'; +} + +function titleCase(value) { + const text = String(value ?? '').replaceAll('-', ' '); + return text ? text[0].toUpperCase() + text.slice(1) : ''; +} diff --git a/apps/web/shell-model.js b/apps/web/shell-model.js index 2a7713ee..65915b90 100644 --- a/apps/web/shell-model.js +++ b/apps/web/shell-model.js @@ -1,8 +1,8 @@ export const PRIMARY_NAV = Object.freeze([ - { id: 'overview', routeId: 'home', path: '/', label: 'Overview' }, - { id: 'map', routeId: 'source-graph', path: '/map', label: 'Map' }, - { id: 'memory', routeId: 'memory', path: '/memory', label: 'Memory' }, - { id: 'handoffs', routeId: 'context-pack', path: '/handoffs', label: 'Handoffs' }, + { id: 'overview', routeId: 'home', path: '/', label: 'Start' }, + { id: 'map', routeId: 'source-graph', path: '/map', label: 'Explore code' }, + { id: 'memory', routeId: 'memory', path: '/memory', label: 'Review memory' }, + { id: 'handoffs', routeId: 'context-pack', path: '/handoffs', label: 'Prepare handoff' }, { id: 'settings', routeId: 'settings', path: '/settings', label: 'Settings' } ]); @@ -35,12 +35,13 @@ export function navigationItemsFor(mode) { } export function selectOverviewPrimaryAction(model = {}) { - if (model.state === 'loading' || model.state === 'error') return null; + if (model.state === 'loading' || model.state === 'error' || model.state === 'failure') return null; if (model.state === 'empty') return { label: 'Scan repository', route: '/', action: 'refresh-recall-map' }; - const pendingCount = Number(model.memory?.pendingCount ?? 0); + const pendingCount = Number(model.trust?.memory?.pendingCount ?? model.memory?.pendingCount ?? 0); if (pendingCount > 0) return { label: `Review ${pendingCount} proposal${pendingCount === 1 ? '' : 's'}`, route: '/memory', routeId: 'memory' }; - if (model.handoff?.state === 'blocked') return { label: 'Repair handoff', route: '/handoffs', routeId: 'context-pack' }; - if (model.state === 'stale' || model.handoff?.state === 'review') return { label: 'Update handoff', route: '/handoffs', routeId: 'context-pack' }; - if (model.handoff?.state === 'ready') return { label: 'View current handoff', route: '/handoffs', routeId: 'context-pack' }; + const handoffState = model.trust?.handoff?.state ?? model.handoff?.state; + if (handoffState === 'blocked') return { label: 'Repair handoff', route: '/handoffs', routeId: 'context-pack' }; + if (model.state === 'stale' || handoffState === 'review') return { label: 'Update handoff', route: '/handoffs', routeId: 'context-pack' }; + if (handoffState === 'ready') return { label: 'View current handoff', route: '/handoffs', routeId: 'context-pack' }; return null; } diff --git a/apps/web/source-map-view.js b/apps/web/source-map-view.js new file mode 100644 index 00000000..22dfc681 --- /dev/null +++ b/apps/web/source-map-view.js @@ -0,0 +1,367 @@ +import { escapeHtml, formatDate, renderApiErrorPanel, statePanel } from './ui-primitives.js'; +import { createGraphViewport } from './graph-viewport.js'; + +const DEFAULT_DEPTH = 2; +const DEFAULT_LIMIT = 20; + +export function parseMapUrl(input = '/map') { + const url = new URL(String(input || '/map'), 'http://127.0.0.1'); + const query = boundedText(url.searchParams.get('query'), 512); + const group = boundedText(url.searchParams.get('group'), 512); + const startName = boundedText(url.searchParams.get('start'), 240); + const changedLocator = boundedText(url.searchParams.get('changed'), 512); + const depth = boundedInteger(url.searchParams.get('depth'), DEFAULT_DEPTH, 1, 5); + const limit = boundedInteger(url.searchParams.get('limit'), DEFAULT_LIMIT, 1, 100); + const offset = boundedInteger(url.searchParams.get('offset'), 0, 0, 10000); + return { + query, + group, + startName, + changedLocator, + depth, + limit, + offset, + advanced: Boolean(startName || changedLocator || depth !== DEFAULT_DEPTH || limit !== DEFAULT_LIMIT) + }; +} + +export function serializeMapUrl(value = {}) { + const state = normalizeMapState(value); + const params = new URLSearchParams(); + if (state.query) params.set('query', state.query); + if (state.group) params.set('group', state.group); + if (state.startName) params.set('start', state.startName); + if (state.changedLocator) params.set('changed', state.changedLocator); + if (state.depth !== DEFAULT_DEPTH) params.set('depth', String(state.depth)); + if (state.limit !== DEFAULT_LIMIT) params.set('limit', String(state.limit)); + if (state.offset > 0) params.set('offset', String(state.offset)); + const query = params.toString(); + return query ? `/map?${query}` : '/map'; +} + +export function buildMapRequest(value = {}) { + const state = normalizeMapState(value); + return compactObject({ + query: state.query || null, + locatorPrefix: state.group || null, + startName: state.startName || null, + changedLocators: state.changedLocator ? [state.changedLocator] : null, + depth: state.depth, + limit: state.limit, + offset: state.offset, + sampleLimit: 50 + }); +} + +export function mapStateFromForm(form) { + const data = new FormData(form); + return normalizeMapState({ + query: data.get('query'), + group: data.get('group'), + startName: data.get('startName'), + changedLocator: data.get('changedLocator'), + depth: data.get('depth'), + limit: data.get('limit') + }); +} + +export function renderSourceMap({ state: value = {}, report = null, error = null, loading = false } = {}) { + const state = normalizeMapState(value); + const result = error + ? renderApiErrorPanel('Map request failed', error) + : loading + ? statePanel('loading', 'Reading repository map', 'Loading bounded source metadata from the local index.') + : report + ? renderMapResult(report, state) + : statePanel('empty', 'Map not loaded', 'Run the map to inspect bounded repository structure.'); + + return `
+

Map

Find an entry point, trace a symbol, or inspect a changed file.

Read-only / bounded metadata
+ ${renderMapForm(state)} + ${result} +
`; +} + +export function bindSourceMap(root, { report = null, onSubmit, onRefresh, onPage, onSelectNode } = {}) { + if (!root) return () => {}; + const controller = new AbortController(); + const options = { signal: controller.signal }; + let viewport = null; + root.querySelector('#source-graph-form')?.addEventListener('submit', (event) => { + event.preventDefault(); + onSubmit?.(mapStateFromForm(event.currentTarget), event); + }, options); + root.querySelector('[data-action="refresh-source-map"]')?.addEventListener('click', (event) => onRefresh?.(event), options); + root.querySelectorAll('[data-map-offset]').forEach((button) => button.addEventListener('click', () => { + onPage?.(boundedInteger(button.dataset.mapOffset, 0, 0, 10000)); + }, options)); + const canvas = root.querySelector('#source-map-canvas'); + if (canvas) { + const state = mapStateFromRoot(root); + const graph = sourceMapGraphData(report, state); + const nodes = graph.nodes; + viewport = createGraphViewport( + canvas, + root.querySelector('.source-map-outline'), + root.querySelector('#source-map-selection'), + { + nodes, + edges: graph.edges, + onSelect: (node) => { + const selection = root.querySelector('#source-map-selection'); + if (selection) selection.innerHTML = renderSelection(node); + onSelectNode?.(node.id); + }, + onError: (code) => { + const failure = root.querySelector('[data-graph-error]'); + if (failure) { + failure.hidden = false; + failure.textContent = code === 'graph_layout_bounds_exceeded' + ? 'Focused graph exceeds the 200-node or 400-relationship display limit.' + : 'Graph layout could not start. Use the outline to inspect records.'; + } + } + } + ); + } else { + root.querySelectorAll('[data-node-id]').forEach((button) => button.addEventListener('click', () => onSelectNode?.(button.dataset.nodeId), options)); + } + return () => { + controller.abort(); + viewport?.destroy(); + }; +} + +function renderMapForm(state) { + return `
+ +
+ Scope and trace +
+ + + + + +
+
+
No model, network, or external writes
+
`; +} + +function renderMapResult(report, state) { + if (report?.snapshot?.status === 'unavailable') return renderUnavailableMap(report); + const coverage = coverageModel(report); + const hasFocus = Boolean(state.query || state.group || state.startName || state.changedLocator); + const focusNodes = arrayValue(report.focus?.nodes); + const focusEdges = arrayValue(report.focus?.edges); + const groups = arrayValue(report.orientation?.groups); + const processes = arrayValue(report.orientation?.processes); + const outlineNodes = hasFocus ? focusNodes : groups; + const groupRelations = arrayValue(report.orientation?.relations ?? report.orientation?.groupRelations); + const content = hasFocus + ? renderFocus(focusNodes, focusEdges, report.focus) + : renderArchitecture(groups, groupRelations); + + return `
+

${hasFocus ? 'Focused map' : 'Repository architecture'}

${escapeHtml(coverage.summary)}

${snapshotLabel(report.snapshot)}
+ ${coverage.status === 'partial' ? renderCoverageWarning(coverage) : ''} + ${hasFocus ? renderQueryBounds(report.search, state) : ''} +
+
${content}
+ +
+ ${renderProcesses(processes)} +
`; +} + +function renderQueryBounds(search = {}, state) { + const count = arrayValue(search.results).length; + const offsetIncomplete = search.offsetIncomplete === true; + const reachedOffset = number(search.reachedOffset ?? state.offset); + const start = count ? reachedOffset + 1 : 0; + const end = reachedOffset + count; + const hasPrevious = state.offset > 0; + const hasMore = search.hasMore === true; + const truncatedWithoutCursor = search.truncated === true && !hasMore && !offsetIncomplete; + const copy = offsetIncomplete + ? `The requested offset ${formatNumber(state.offset)} was not reached within the bounded native read. The walk reached ${formatNumber(reachedOffset)}; this is not an empty result page.` + : truncatedWithoutCursor + ? 'The bounded query omitted additional evidence, and no continuation cursor is available.' + : count + ? `Query matches ${formatNumber(start)}-${formatNumber(end)} on this page.${hasMore ? ' More matches are available.' : ' End of the bounded query results.'}` + : hasPrevious + ? 'No query matches on this page. Previous results may still be available.' + : 'No query matches were returned.'; + const previousOffset = Math.max(0, state.offset - state.limit); + const nextOffset = Math.min(10000, state.offset + state.limit); + const detail = offsetIncomplete + ? search.continuationCursor + ? 'Use the returned continuation cursor through the API, or request an earlier page.' + : 'Request an earlier page; this bounded result has no continuation cursor.' + : truncatedWithoutCursor + ? 'The result is partial. Broaden the limit only within the documented query bounds.' + : 'The focused map expands relationships separately; its omitted node and relationship counts appear below.'; + return `
${offsetIncomplete ? 'Query page incomplete' : truncatedWithoutCursor ? 'Query results partial' : 'Query result bounds'}

${escapeHtml(copy)}

${detail}
${hasPrevious ? `` : ''}${!offsetIncomplete && hasMore && nextOffset > state.offset ? `` : ''}
`; +} + +function renderArchitecture(groups, relations) { + if (!groups.length) return statePanel('empty', 'No supported groups', 'No repository groups were represented inside the current index bounds.'); + return `
${number(groups.length)} groups${number(relations.length)} connections
`; +} + +function renderFocus(nodes, edges, focus = {}) { + if (!nodes.length) return statePanel('empty', 'No focused records', 'The submitted scope produced no bounded nodes. Broaden the query or remove a group filter.'); + return `
${number(nodes.length)} nodes${number(edges.length)} relationships${number(focus.omittedNodes ?? focus.omittedNodeCount) ? `${number(focus.omittedNodes ?? focus.omittedNodeCount)} nodes omitted` : ''}${number(focus.omittedEdges ?? focus.omittedEdgeCount) ? `${number(focus.omittedEdges ?? focus.omittedEdgeCount)} relationships omitted` : ''}Labels stay with the selection and nearby nodes. Use the outline for the complete bounded result.
`; +} + +function renderUnavailableMap(report) { + const recovery = sourceIndexRecovery(report?.snapshot?.reason); + const command = recovery.command ? `

${escapeHtml(recovery.command)}

` : ''; + return statePanel('partial', 'Source index unavailable', recovery.copy, false, `${command}

Map did not scan files or use another graph engine.

`); +} + +function sourceIndexRecovery(reason) { + const code = String(reason ?? '').split(':').at(-1); + if (code === 'source_index_build_required') { + return { copy: 'Build the local source index, then run the map again.', command: 'recall graph index --write --engine native --root . --format summary' }; + } + if (code === 'source_index_refresh_required') { + return { copy: 'Refresh the stale source index, then run the map again.', command: 'recall graph index --refresh --engine native --root . --format summary' }; + } + if (['native_platform_package_missing', 'native_engine_unavailable'].includes(code)) { + return { copy: 'Install a matching Memory Recall native package, then reload this page.', command: 'npm install -g memory-recall' }; + } + return { copy: 'Inspect the local source index, apply the reported repair, then run the map again.', command: 'recall graph index --doctor --engine native --root . --format summary' }; +} + +function renderProcesses(processes) { + if (!processes.length) return ''; + return `

Entry-to-sink processes

${number(processes.length)} bounded paths
    ${processes.map((process) => { + const steps = Math.max(1, arrayValue(process.nodeIds).length - 1); + return `
  1. ${escapeHtml(process.entryPoint?.label)}${escapeHtml(process.sink?.label)}

    ${escapeHtml(process.sinkKind)} · ${number(steps)} ${steps === 1 ? 'step' : 'steps'} · ${percent(process.confidence)}% confidence${process.truncated ? ' · bounded result' : ''}

    ${escapeHtml(process.entryPoint?.locator)}${escapeHtml(process.sink?.locator)}
  2. `; + }).join('')}
`; +} + +function renderMapOutline(items, focused) { + if (!items.length) return '

No records in the current outline.

'; + return `
    ${items.map((item, index) => `
  1. `).join('')}
`; +} + +export function sourceMapGraphData(report = {}, state = {}) { + const hasFocus = Boolean(state.query || state.group || state.startName || state.changedLocator); + if (hasFocus) return { + nodes: arrayValue(report.focus?.nodes), + edges: arrayValue(report.focus?.edges) + }; + return { + nodes: arrayValue(report.orientation?.groups).map((group) => ({ + ...group, + kind: 'repository-group', + label: group.prefix ?? group.label + })), + edges: arrayValue(report.orientation?.relations ?? report.orientation?.groupRelations).map((relation, index) => ({ + id: relation.id ?? `group_relation_${index}`, + kind: 'group-dependency', + fromNodeId: relation.sourceGroupId, + toNodeId: relation.targetGroupId, + count: number(relation.count) + })) + }; +} + +function mapStateFromRoot(root) { + const form = root.querySelector('#source-graph-form'); + return form ? mapStateFromForm(form) : normalizeMapState(); +} + +function renderSelection(item) { + if (!item) return '

No selection.

'; + return `
Name
${escapeHtml(item.label ?? item.prefix)}
Type
${escapeHtml(item.kind ?? 'repository group')}
${item.locator ? `
Locator
${escapeHtml(item.locator)}
` : ''}
`; +} + +function renderSourceTruth(report, coverage) { + const safeguards = report.safeguards ?? {}; + return `

Scan truth

Coverage
${escapeHtml(coverage.status)}
Represented
${number(coverage.representedFiles)} files
Model calls
${number(safeguards.modelCalls)}
Network calls
${number(safeguards.networkCalls)}
External writes
${safeguards.externalWritesEnabled ? 'on' : 'off'}
`; +} + +function renderCoverageWarning(coverage) { + const reasons = coverage.reasonCodes.map(reasonLabel); + return `
Partial coverage

${escapeHtml(coverage.summary)}

${reasons.length ? `
    ${reasons.map((reason) => `
  • ${escapeHtml(reason)}
  • `).join('')}
` : ''}
`; +} + +function coverageModel(report) { + const source = report.coverage ?? report.graph?.summary?.coverage ?? {}; + const representedFiles = number(source.representedFileCount ?? report.graph?.summary?.fileCount); + const omittedFiles = number(source.omittedFileCount ?? ( + number(source.skippedFileCount) + number(source.oversizedFileCount) + number(source.unsupportedFileCount) + )); + const omittedEdges = number(source.omittedEdgeCount); + const status = source.status === 'partial' ? 'partial' : 'complete'; + const parts = [`${formatNumber(representedFiles)} represented files`]; + if (omittedFiles) parts.push(`${formatNumber(omittedFiles)} omitted files`); + if (omittedEdges) parts.push(`${formatNumber(omittedEdges)} omitted relationships`); + return { status, representedFiles, omittedFiles, omittedEdges, reasonCodes: arrayValue(source.reasonCodes), summary: parts.join(' / ') }; +} + +function snapshotLabel(snapshot = {}) { + const status = escapeHtml(snapshot.status ?? 'unavailable'); + const reuse = snapshot.reuse && snapshot.reuse !== 'none' ? ` / ${escapeHtml(snapshot.reuse)}` : ''; + const built = snapshot.builtAt ? ` / ${escapeHtml(formatDate(snapshot.builtAt))}` : ''; + return `${status}${reuse}${built}`; +} + +function reasonLabel(value) { + const labels = { + file_budget_reached: 'File limit reached', + max_files_reached: 'File limit reached', + node_budget_reached: 'Node limit reached', + edge_budget_reached: 'Relationship limit reached' + }; + return labels[value] ?? String(value ?? '').replaceAll('_', ' '); +} + +function normalizeMapState(value) { + const query = boundedText(value.query, 512); + const group = boundedText(value.group, 512); + const startName = boundedText(value.startName, 240); + const changedLocator = boundedText(value.changedLocator, 512); + const depth = boundedInteger(value.depth, DEFAULT_DEPTH, 1, 5); + const limit = boundedInteger(value.limit, DEFAULT_LIMIT, 1, 100); + const offset = boundedInteger(value.offset, 0, 0, 10000); + return { + query, group, startName, changedLocator, depth, limit, offset, + advanced: Boolean(startName || changedLocator || depth !== DEFAULT_DEPTH || limit !== DEFAULT_LIMIT) + }; +} + +function boundedText(value, maximum) { + return String(value ?? '').trim().slice(0, maximum); +} + +function boundedInteger(value, fallback, minimum, maximum) { + const candidate = Number(value); + return Number.isInteger(candidate) && candidate >= minimum && candidate <= maximum ? candidate : fallback; +} + +function compactObject(value) { + return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== null && item !== undefined && item !== '')); +} + +function arrayValue(value) { + return Array.isArray(value) ? value : []; +} + +function number(value) { + const candidate = Number(value); + return Number.isFinite(candidate) && candidate >= 0 ? Math.floor(candidate) : 0; +} + +function percent(value) { + const candidate = Number(value); + return Math.round((Number.isFinite(candidate) ? Math.max(0, Math.min(1, candidate)) : 0) * 100); +} + +function formatNumber(value) { + return new Intl.NumberFormat('en-US').format(number(value)); +} diff --git a/apps/web/styles.css b/apps/web/styles.css index c6077022..6aef75e8 100644 --- a/apps/web/styles.css +++ b/apps/web/styles.css @@ -50,13 +50,16 @@ main{min-width:0} .repository-popover{position:absolute;z-index:30;top:calc(100% + var(--space-2xs));left:0;width:min(360px,calc(100vw - 32px));display:grid;gap:var(--space-2xs);padding:var(--space-sm);border:1px solid var(--color-rule-strong);border-radius:var(--radius-panel);background:var(--color-panel);box-shadow:0 12px 32px color-mix(in oklch,var(--color-ink) 18%,transparent)} .repository-popover p{margin:0;color:var(--color-muted)} .repository-popover code{padding:var(--space-2xs);background:var(--color-panel-muted);border-radius:var(--radius-control)} -.global-search{flex:1 1 280px;max-width:520px;display:grid;grid-template-columns:minmax(120px,1fr) auto;align-items:center} -.global-search input{min-height:44px;min-width:0;border:1px solid var(--color-rule-strong);border-radius:var(--radius-control) 0 0 var(--radius-control);padding:0 var(--space-xs);color:var(--color-ink);background:var(--color-panel)} +.global-search{flex:1 1 360px;max-width:620px;display:grid;grid-template-columns:148px minmax(140px,1fr) auto;align-items:center} +.global-search select,.global-search input{min-height:44px;min-width:0;border:1px solid var(--color-rule-strong);padding:0 var(--space-xs);color:var(--color-ink);background:var(--color-panel);font:inherit} +.global-search select{border-radius:var(--radius-control) 0 0 var(--radius-control)} +.global-search input{min-height:44px;border-left:0;border-radius:0} .global-search button{min-height:44px;border:1px solid var(--color-rule-strong);border-left:0;border-radius:0 var(--radius-control) var(--radius-control) 0;padding:0 var(--space-xs);color:var(--color-ink);background:var(--color-panel);cursor:pointer} .global-search button:hover{border-color:var(--color-accent)} .condition{color:var(--color-muted);text-transform:capitalize} .condition[data-state="error"],.condition[data-state="denied"]{color:var(--color-danger)} .condition[data-state="stale"],.condition[data-state="partial"]{color:var(--color-ink);text-decoration:underline;text-decoration-color:var(--color-warning);text-underline-offset:2px} +#repository-boundary[data-state="warning"]{color:var(--color-danger);font-weight:650} .icon-link{min-height:44px;display:inline-flex;align-items:center;color:var(--color-ink)} #view-root{padding:var(--space-lg) clamp(var(--space-sm),4vw,var(--space-xl)) var(--space-2xl)} .eyebrow{margin:0 0 var(--space-3xs);color:var(--slate);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em} @@ -66,11 +69,13 @@ h3{font-size:15px;line-height:1.3} .action-row{display:flex;gap:var(--space-8);flex-wrap:wrap} .button{min-height:40px;border:1px solid var(--color-rule-strong);border-radius:var(--radius-control);padding:var(--space-2xs) var(--space-xs);color:var(--color-ink);background:var(--color-panel);font:inherit;font-weight:650;cursor:pointer} a.button{text-decoration:none;display:inline-flex;align-items:center;justify-content:center} -.button.primary{min-height:44px;color:var(--color-accent-ink);background:var(--color-accent);border-color:var(--color-accent)} +.button.primary{min-height:44px;color:var(--color-canvas);background:var(--color-ink);border-color:var(--color-ink)} .button.secondary{background:var(--color-panel);color:var(--color-ink)} +.button.quiet{min-height:44px;border-color:transparent;background:transparent;color:var(--color-muted)} .button:hover:not(:disabled){border-color:var(--color-accent)} +.button.quiet:hover:not(:disabled){border-color:transparent;background:var(--color-panel-muted);color:var(--color-ink)} .button:active:not(:disabled){background:var(--color-panel-muted)} -.button.primary:active:not(:disabled){filter:brightness(.9)} +.button.primary:active:not(:disabled){background:color-mix(in oklch,var(--color-ink) 88%,var(--color-canvas))} .button:disabled{opacity:.55;cursor:not-allowed} .button:focus-visible,a:focus-visible,main:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,button:focus-visible,summary:focus-visible{outline:2px solid var(--color-accent);outline-offset:1px} .status-chip{display:inline-flex;align-items:center;gap:var(--space-8);border:1px solid var(--border);background:var(--surface-2);border-radius:999px;padding:5px 10px;min-height:32px} @@ -135,7 +140,7 @@ code,dd,.meta-row span,.reason{overflow-wrap:anywhere;word-break:break-word} .stacked-form{display:grid;gap:var(--space-16)} .field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-12)} .field{display:grid;gap:var(--space-3xs);min-width:0} -.field span{font-size:12px;font-weight:720;color:var(--slate);text-transform:uppercase;letter-spacing:.08em} +.field span{font-size:12px;font-weight:650;color:var(--slate)} .field small{color:var(--slate);font-size:12px;line-height:1.4} .field input,.field select,.field textarea{width:100%;min-height:44px;border:1px solid var(--border);border-radius:var(--radius-control);background:var(--surface-2);color:var(--paper);padding:var(--space-2xs) var(--space-xs);font:inherit;outline:2px solid transparent;outline-offset:1px} .field textarea{min-height:96px;resize:vertical} @@ -143,7 +148,7 @@ code,dd,.meta-row span,.reason{overflow-wrap:anywhere;word-break:break-word} .field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{outline-color:var(--focus)} .field input:disabled,.field select:disabled,.field textarea:disabled{opacity:.55;cursor:not-allowed;background:var(--color-panel-muted)} .source-family-field{display:grid;gap:var(--space-8);border:1px solid var(--border);border-radius:var(--radius-control);padding:var(--space-12);margin:0;background:var(--surface-2)} -.source-family-field legend{padding:0 var(--space-6,6px);font-size:12px;font-weight:720;color:var(--slate);text-transform:uppercase;letter-spacing:.08em} +.source-family-field legend{padding:0 var(--space-6,6px);font-size:12px;font-weight:650;color:var(--slate)} .source-family-options{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-8)} .source-family-options label{display:flex;align-items:center;gap:var(--space-8);min-height:42px;border:1px solid var(--border);border-radius:var(--radius-control);background:var(--surface-3);padding:8px 10px;font-weight:720} .source-family-options input{inline-size:18px;block-size:18px;accent-color:var(--signal)} @@ -298,6 +303,56 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .change-detection-warning{display:grid;gap:var(--space-2xs)} .change-detection-warning p{margin:0;color:var(--color-muted)} .change-detection-warning .button{justify-self:start} +.orientation-workbench{max-width:1440px;margin:0 auto} +.orientation-heading{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-sm);padding-bottom:var(--space-sm);border-bottom:1px solid var(--color-rule)} +.orientation-heading>div{min-width:0} +.orientation-heading h1{font-size:24px;letter-spacing:-.02em} +.orientation-heading p{margin:var(--space-3xs) 0 0;color:var(--color-muted)} +.coverage-label{color:var(--color-muted);font-size:12px;text-align:right} +.coverage-label[data-status="partial"],.coverage-label[data-status="stale"],.coverage-label[data-status="failed"]{color:var(--color-ink)} +.orientation-layout{display:grid;grid-template-columns:minmax(0,7fr) minmax(280px,3fr);gap:var(--space-lg);padding-top:var(--space-md);align-items:start} +.architecture-region{min-width:0} +.region-heading{display:flex;align-items:start;justify-content:space-between;gap:var(--space-sm);margin-bottom:var(--space-sm)} +.region-heading h2,.orientation-inspector h2{margin:0;font-size:15px} +.region-heading p{margin:var(--space-3xs) 0 0;color:var(--color-muted)} +.region-heading a{min-height:44px;display:inline-flex;align-items:center;white-space:nowrap} +.architecture-board{position:relative;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--space-xs);align-items:start;padding:var(--space-sm);border:1px solid var(--color-rule);border-radius:var(--radius-panel);background:var(--color-panel-muted)} +.orientation-group{min-height:64px;display:grid;align-content:center;gap:2px;min-width:0;border:1px solid var(--color-rule);border-radius:var(--radius-control);padding:var(--space-2xs) var(--space-xs);background:var(--color-panel);color:var(--color-ink);box-shadow:none;text-align:left;cursor:pointer} +.orientation-group:hover{border-color:var(--color-rule-strong)} +.orientation-group.is-selected{border-color:var(--color-accent);box-shadow:inset 3px 0 0 var(--color-accent)} +.orientation-group strong,.orientation-group span,.orientation-group small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.orientation-group span,.orientation-group small{color:var(--color-muted)} +.orientation-group small{font-size:11px} +.orientation-relations{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:var(--space-2xs) var(--space-sm);padding-top:var(--space-3xs);color:var(--color-muted);font-size:11px} +.orientation-relations span{display:inline-flex;align-items:center;gap:var(--space-3xs)} +.orientation-relations span:before{content:"";width:18px;border-top:1px solid var(--color-rule-strong)} +.orientation-relations b{font-weight:650;color:var(--color-ink)} +.architecture-disclosure{margin:var(--space-xs) 0 0;color:var(--color-muted);font-size:12px} +.architecture-outline{margin-top:var(--space-xs);border-top:1px solid var(--color-rule)} +.architecture-outline summary{min-height:44px;display:flex;align-items:center;cursor:pointer;font-weight:650} +.architecture-outline ol{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-2xs)} +.architecture-outline button{width:100%;min-height:44px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--space-xs);align-items:center;border:0;border-radius:var(--radius-control);padding:var(--space-2xs);background:transparent;color:var(--color-ink);text-align:left;cursor:pointer} +.architecture-outline button:hover{background:var(--color-panel-muted)} +.architecture-outline button span{color:var(--color-muted);font-size:11px;white-space:nowrap} +.orientation-inspector{min-width:0} +.orientation-inspector section{padding:var(--space-sm) 0;border-top:1px solid var(--color-rule)} +.orientation-inspector section:first-child{padding-top:0;border-top:0} +.orientation-inspector section>header{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-xs);margin-bottom:var(--space-xs)} +.orientation-inspector section>header span{color:var(--color-muted);font-size:12px} +.orientation-inspector ol{list-style:none;margin:0;padding:0} +.start-item a{min-height:44px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--space-xs);padding:var(--space-3xs) 0;border-top:1px solid var(--color-rule);color:inherit;text-decoration:none} +.start-item:first-child a{border-top:0} +.start-item code{grid-column:1/-1;color:var(--color-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.start-item span{color:var(--color-muted);font-size:12px;white-space:nowrap} +.orientation-inspector p{margin:0;color:var(--color-muted)} +.orientation-inspector small{display:block;margin-top:var(--space-2xs);color:var(--color-muted)} +.orientation-inspector section ol:not(:first-child){margin-top:var(--space-2xs)} +.orientation-inspector section ol li:not(.start-item){display:grid;gap:2px;padding:var(--space-3xs) 0} +.orientation-inspector section ol code{color:var(--color-muted)} +.trust-list{margin:0;display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2xs) var(--space-sm)} +.trust-list div{min-width:0} +.trust-list dt{color:var(--color-muted);font-size:11px} +.trust-list dd{margin:2px 0 0;font-weight:650;overflow-wrap:anywhere} .tool-workspace{max-width:1280px;margin:0 auto} .tool-page-heading{display:flex;align-items:end;justify-content:space-between;gap:var(--space-lg);padding-bottom:var(--space-md);margin-bottom:var(--space-md);border-bottom:1px solid var(--color-rule)} .tool-page-heading>div{min-width:0} @@ -305,6 +360,59 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .tool-page-heading p{margin:var(--space-2xs) 0 0;color:var(--color-muted);max-width:var(--layout-prose)} .tool-page-heading>span{color:var(--color-muted);white-space:nowrap} .tool-query{padding:0 0 var(--space-md);margin-bottom:var(--space-md);border-bottom:1px solid var(--color-rule)} +.source-map-form{display:grid;grid-template-columns:minmax(240px,1fr) minmax(280px,1.4fr);gap:var(--space-sm);align-items:end} +.map-advanced{min-width:0;border:0} +.map-advanced summary{min-height:44px;display:flex;align-items:center;border-bottom:1px solid var(--color-rule);cursor:pointer;font-weight:650} +.map-advanced[open] summary{margin-bottom:var(--space-xs)} +.map-advanced-fields{display:grid;grid-template-columns:minmax(140px,1fr) minmax(140px,1fr) minmax(160px,1.2fr) 76px 76px;gap:var(--space-xs);align-items:end} +.map-query-actions{grid-column:1/-1;display:flex;align-items:center;gap:var(--space-xs);flex-wrap:wrap} +.map-query-actions>span{color:var(--color-muted);font-size:12px} +.source-map-result{display:grid;gap:var(--space-sm)} +.map-result-heading{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-sm);padding-bottom:var(--space-sm);border-bottom:1px solid var(--color-rule)} +.map-result-heading h2{margin:0;font-size:24px} +.map-result-heading p{margin:var(--space-3xs) 0 0;color:var(--color-muted)} +.map-snapshot{color:var(--color-muted);white-space:nowrap} +.map-coverage-warning{display:grid;grid-template-columns:max-content minmax(0,1fr) auto;gap:var(--space-xs);align-items:start;padding:var(--space-xs);border:1px solid var(--color-warning);border-radius:var(--radius-control)} +.map-coverage-warning p,.map-coverage-warning ul{margin:0} +.map-coverage-warning ul{display:flex;flex-wrap:wrap;gap:var(--space-2xs) var(--space-xs);padding:0;list-style:none;color:var(--color-muted)} +.source-map-layout{display:grid;grid-template-columns:minmax(0,7fr) minmax(280px,3fr);gap:var(--space-lg);align-items:start} +.source-map-stage{min-width:0} +.source-map-inspector{min-width:0;padding-left:var(--space-md);border-left:1px solid var(--color-rule)} +.source-map-inspector>h2,.source-map-truth h2{margin:0 0 var(--space-xs);font-size:15px} +.source-map-inspector>hr{margin:var(--space-sm) 0;border:0;border-top:1px solid var(--color-rule)} +.map-groups{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--space-xs)} +.map-groups article{min-width:0;display:grid;gap:var(--space-3xs);padding:var(--space-xs);border:1px solid var(--color-rule);border-radius:var(--radius-control);background:var(--color-panel)} +.map-groups article span,.map-groups article small{color:var(--color-muted)} +.map-relations{margin:var(--space-sm) 0 0;padding:0;list-style:none;border-top:1px solid var(--color-rule)} +.map-relations li{min-height:44px;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr) auto;gap:var(--space-xs);align-items:center;border-bottom:1px solid var(--color-rule)} +.map-relations li span,.map-relations li small{color:var(--color-muted)} +.map-focus-summary{display:flex;flex-wrap:wrap;gap:var(--space-xs);padding-bottom:var(--space-xs);color:var(--color-muted)} +.map-focus-summary strong{color:var(--color-ink)} +.map-focus-summary small{flex-basis:100%;color:var(--color-muted);font-size:12px;line-height:1.4} +.map-index-command{margin:var(--space-xs) 0 0} +.map-index-command code{display:block;overflow:auto;padding:var(--space-2xs);background:var(--color-panel-muted);border-radius:var(--radius-control);white-space:pre-wrap} +.map-processes{margin-top:var(--space-md)} +.map-processes .section-heading{margin-bottom:0} +.map-processes ol{margin:0;padding:0;list-style:none} +.map-processes li{display:grid;grid-template-columns:minmax(220px,1.2fr) minmax(180px,.8fr) minmax(0,1fr) minmax(0,1fr);gap:var(--space-xs);align-items:center;min-height:56px;padding:var(--space-xs) 0;border-bottom:1px solid var(--color-rule)} +.map-process-route{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);gap:var(--space-2xs);align-items:center} +.map-process-route strong,.map-processes code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.map-processes p{margin:0;color:var(--color-muted)} +.map-processes code{color:var(--color-muted)} +.map-graph-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--space-xs);padding-bottom:var(--space-xs)} +.map-graph-toolbar>div:last-child{display:flex;gap:var(--space-2xs)} +.map-graph-error{margin:0 0 var(--space-xs);padding:var(--space-xs);border:1px solid var(--color-danger);border-radius:var(--radius-control);color:var(--color-danger)} +.source-map-canvas-wrap{height:clamp(380px,56vh,620px);overflow:hidden;border:1px solid var(--color-rule);border-radius:var(--radius-control);background:var(--color-panel);touch-action:none} +.source-map-canvas-wrap canvas{display:block;width:100%;height:100%;cursor:grab} +.source-map-canvas-wrap canvas:active{cursor:grabbing} +.map-focus-list,.source-map-outline{margin:0;padding:0;list-style:none;border-top:1px solid var(--color-rule)} +.map-focus-list button,.source-map-outline button{width:100%;min-height:44px;display:block;padding:var(--space-2xs);border:0;border-bottom:1px solid var(--color-rule);border-radius:0;background:transparent;color:var(--color-ink);text-align:left;cursor:pointer} +.source-map-outline-copy{min-width:0;display:grid;gap:2px} +.source-map-outline-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.source-map-outline-locator{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--color-muted);font-size:12px} +.map-focus-list button:hover,.source-map-outline button:hover{background:var(--color-panel-muted)} +.map-focus-list button span,.source-map-outline button span{color:var(--color-muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.source-map-outline button[aria-current="true"]{box-shadow:inset 2px 0 var(--color-accent)} .query-options{display:grid;grid-template-columns:100px 100px max-content 1fr;gap:var(--space-xs);align-items:end} .query-options>.muted{align-self:center} .tool-result-heading{display:flex;align-items:end;justify-content:space-between;gap:var(--space-md);margin-bottom:var(--space-md)} @@ -374,16 +482,37 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .fabric-links .is-active strong:before,.fabric-links .is-active strong:after{border-color:color-mix(in srgb,var(--signal) 70%,var(--border))} .fabric-links .is-blocked strong{color:var(--danger)} .fabric-context-flow{margin-bottom:0} -.memory-graph-shell{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,var(--layout-inspector));gap:var(--space-16);align-items:start;margin-bottom:var(--space-16)} -.memory-graph-toolbar{display:grid;grid-template-columns:minmax(220px,1fr) auto auto auto;gap:var(--space-10,10px);align-items:end;margin-bottom:var(--space-14,14px)} +.memory-graph-query{padding-bottom:var(--space-md);margin-bottom:var(--space-md);border-bottom:1px solid var(--color-rule)} +.memory-graph-toolbar{display:grid;grid-template-columns:minmax(220px,1fr) auto auto auto;gap:var(--space-xs);align-items:end} .memory-graph-search{min-width:0} .toggle-field{min-height:44px;display:inline-flex;align-items:center;gap:var(--space-8);border:1px solid var(--border);border-radius:var(--radius-control);background:var(--surface-2);padding:8px 12px;font-weight:720;color:var(--paper);white-space:nowrap} .toggle-field input{inline-size:18px;block-size:18px;accent-color:var(--signal)} -.memory-graph-canvas-wrap{width:100%;height:clamp(420px,58vh,700px);border:1px solid var(--border);border-radius:var(--radius-panel);overflow:hidden;background:#111826} -.memory-graph-canvas-wrap canvas{display:block;width:100%;height:100%} -.memory-graph-legend{display:flex;flex-wrap:wrap;gap:var(--space-8);margin-top:var(--space-10,10px)} -.memory-graph-legend span{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:999px;background:var(--surface-2);padding:5px 9px;color:var(--slate);font-size:12px;font-weight:720} -.memory-graph-legend i{width:10px;height:10px;border-radius:50%;display:inline-block} +.memory-graph-empty{max-width:720px;padding:var(--space-md) 0} +.memory-graph-empty h2{margin:0 0 var(--space-xs);font-size:24px} +.memory-graph-empty p{color:var(--color-muted)} +.memory-graph-layout{display:grid;grid-template-columns:minmax(0,7fr) minmax(280px,3fr);gap:var(--space-lg);align-items:start} +.memory-graph-stage{min-width:0} +.memory-graph-stage-heading{display:flex;align-items:end;justify-content:space-between;gap:var(--space-xs);padding-bottom:var(--space-xs)} +.memory-graph-stage-heading h2{margin:0} +.memory-graph-stage-heading p{margin:var(--space-3xs) 0 0;color:var(--color-muted)} +.memory-graph-stage-heading>div:last-child{display:flex;gap:var(--space-2xs)} +.memory-graph-canvas-wrap{width:100%;aspect-ratio:16/9;max-height:620px;border:1px solid var(--color-rule);border-radius:var(--radius-control);overflow:hidden;background:var(--color-panel);touch-action:none} +.memory-graph-canvas-wrap canvas{display:block;width:100%;height:100%;cursor:grab} +.memory-graph-canvas-wrap canvas:active{cursor:grabbing} +.memory-graph-inspector{min-width:0;padding-left:var(--space-md);border-left:1px solid var(--color-rule)} +.memory-graph-inspector>h2,.memory-graph-boundary h2{margin:0 0 var(--space-xs);font-size:15px} +.memory-graph-inspector>hr{margin:var(--space-sm) 0;border:0;border-top:1px solid var(--color-rule)} +.memory-graph-meta{display:grid;grid-template-columns:minmax(0,7fr) minmax(260px,3fr);gap:var(--space-lg);margin-top:var(--space-md);padding-top:var(--space-md);border-top:1px solid var(--color-rule);align-items:start} +.memory-graph-history h2{margin:0 0 var(--space-xs);font-size:15px} +.memory-graph-history .memory-fact-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:var(--space-md)} +.memory-graph-outline,.memory-fact-list{margin:0;padding:0;list-style:none;border-top:1px solid var(--color-rule)} +.memory-graph-outline button{width:100%;min-height:44px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--space-xs);align-items:center;padding:var(--space-2xs);border:0;border-bottom:1px solid var(--color-rule);border-radius:0;background:transparent;color:var(--color-ink);text-align:left;cursor:pointer} +.memory-graph-outline button:hover{background:var(--color-panel-muted)} +.memory-graph-outline button[aria-current="true"]{box-shadow:inset 2px 0 var(--color-accent)} +.memory-graph-outline button span{color:var(--color-muted);font-size:12px} +.memory-group-label{padding:var(--space-xs) var(--space-2xs) var(--space-3xs);color:var(--color-muted);font-size:11px;font-weight:650} +.memory-fact-list li{display:grid;gap:var(--space-3xs);padding:var(--space-xs) 0;border-bottom:1px solid var(--color-rule)} +.memory-fact-list span,.memory-fact-list small{color:var(--color-muted)} .flow-steps{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--space-12)} .flow-step{position:relative;border:1px solid var(--border);border-radius:var(--radius-card);background:var(--surface-2);padding:var(--space-14,14px);min-height:132px;display:grid;gap:var(--space-8)} .flow-step>span{width:28px;height:28px;border-radius:999px;background:var(--surface-3);display:grid;place-items:center;color:var(--slate);font-size:12px;font-weight:760} @@ -414,7 +543,8 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .use-path-grid{grid-template-columns:repeat(2,minmax(0,1fr))} .facts-wide{grid-template-columns:1fr} .fabric-hero,.fabric-layout{grid-template-columns:1fr} - .memory-graph-shell{grid-template-columns:1fr} + .memory-graph-layout{grid-template-columns:1fr} + .memory-graph-inspector{padding:var(--space-md) 0 0;border-left:0;border-top:1px solid var(--color-rule)} .memory-graph-toolbar{grid-template-columns:1fr 1fr} .memory-graph-toolbar .button,.memory-graph-search{grid-column:1/-1} .primary-flow{grid-template-columns:1fr} @@ -428,6 +558,13 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .tool-detail-grid>section:nth-child(odd){padding-left:0;border-right:1px solid var(--color-rule)} .tool-detail-grid>section:nth-child(even){padding-right:0} .tool-detail-grid>.tool-disclosure{padding-left:0;grid-column:1/-1} + .source-map-form{grid-template-columns:1fr} + .map-query-actions{grid-column:1} + .map-advanced-fields{grid-template-columns:repeat(2,minmax(0,1fr))} + .map-advanced-fields .field:nth-child(3){grid-column:1/-1} + .source-map-layout{grid-template-columns:1fr} + .source-map-inspector{padding:var(--space-md) 0 0;border-left:0;border-top:1px solid var(--color-rule)} + .map-processes li{grid-template-columns:minmax(0,1fr) minmax(0,1fr)} .flow-step:not(:last-child):after{display:none} } @@ -447,6 +584,9 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 @media(max-width:900px){ .overview-grid{grid-template-columns:1fr} .overview-section{grid-column:1;padding:var(--space-md) 0!important;border-right:0!important} + .orientation-layout{grid-template-columns:1fr} + .orientation-inspector{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-sm);border-top:1px solid var(--color-rule);padding-top:var(--space-sm)} + .orientation-inspector section,.orientation-inspector section:first-child{padding:0;border:0} } @media(max-width:700px){ @@ -454,13 +594,20 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .setup-screen{grid-template-columns:1fr;gap:var(--space-md)} .sidebar{display:none} main{min-height:0;overflow:auto} - .repository-bar{min-height:56px;padding:var(--space-2xs) var(--space-xs)} + .repository-bar{min-height:56px;display:grid;grid-template-columns:minmax(0,1fr) auto;padding:var(--space-2xs) var(--space-xs)} .repository-identity{display:none} + .repository-menu{grid-column:1;grid-row:1} .repository-menu summary{max-width:96px} - .global-search{flex-basis:130px;grid-template-columns:minmax(80px,1fr)} - .global-search button{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} - .global-search input{border-radius:var(--radius-control)} - #repository-condition{display:none} + .repository-actions{grid-column:2;grid-row:1} + .global-search{grid-column:1/-1;grid-row:2;width:100%;max-width:none;grid-template-columns:112px minmax(80px,1fr) auto} + .global-search select{width:auto;padding:0 var(--space-2xs);color:var(--color-ink)} + .global-search button{min-width:44px;padding:0 var(--space-2xs)} + .orientation-heading{align-items:flex-start} + .orientation-heading p{display:none} + .architecture-board{grid-template-columns:1fr;grid-auto-flow:row;padding:var(--space-xs)} + .orientation-group{grid-column:1!important} + .architecture-outline ol{grid-template-columns:1fr} + .orientation-inspector{grid-template-columns:1fr} #view-root{padding:var(--space-md) var(--space-xs) var(--space-xl)} h1{font-size:32px} .metric-strip{grid-template-columns:1fr 1fr} @@ -485,7 +632,9 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .fabric-scoreboard{grid-template-columns:repeat(2,minmax(0,1fr))} .memory-graph-toolbar{grid-template-columns:1fr} .memory-graph-toolbar .button,.memory-graph-search{grid-column:auto} - .memory-graph-canvas-wrap{height:460px} + .memory-graph-stage-heading{display:grid} + .memory-graph-canvas-wrap{aspect-ratio:4/3} + .memory-graph-meta,.memory-graph-history .memory-fact-list{grid-template-columns:1fr} .tool-page-heading,.tool-result-heading{display:grid;align-items:start} .tool-page-heading>span{white-space:normal} .tool-summary{justify-content:flex-start;gap:var(--space-sm)} @@ -493,6 +642,20 @@ th{color:var(--slate);font-size:12px;text-transform:uppercase;letter-spacing:.08 .query-options .button,.query-options>.muted{grid-column:1/-1} .tool-detail-grid{grid-template-columns:1fr} .tool-detail-grid>section,.tool-detail-grid>section:nth-child(odd),.tool-detail-grid>section:nth-child(even),.tool-detail-grid>.tool-disclosure{padding:var(--space-sm) 0;border-right:0;grid-column:1} + .map-advanced-fields{grid-template-columns:1fr} + .map-advanced-fields .field:nth-child(3){grid-column:1} + .map-query-actions .button{flex:1 1 auto} + .map-graph-toolbar{display:grid} + .map-graph-toolbar>div:last-child .button{flex:1} + .source-map-canvas-wrap{height:420px} + .map-result-heading{display:grid} + .map-snapshot{white-space:normal} + .map-coverage-warning{grid-template-columns:1fr} + .source-map-layout{grid-template-columns:1fr} + .source-map-inspector{padding:var(--space-sm) 0 0;border-left:0;border-top:1px solid var(--color-rule)} + .map-processes li{grid-template-columns:1fr;gap:var(--space-3xs)} + .map-relations li{grid-template-columns:minmax(0,1fr) auto minmax(0,1fr)} + .map-relations li small{grid-column:1/-1} .memory-review-grid{grid-template-columns:1fr} .memory-review-queue,.memory-intake-panel{padding:var(--space-md) 0;border-right:0} .memory-intake-panel{border-top:1px solid var(--color-rule)} diff --git a/apps/web/tokens.css b/apps/web/tokens.css index c36a248e..46161ced 100644 --- a/apps/web/tokens.css +++ b/apps/web/tokens.css @@ -7,7 +7,7 @@ --color-muted:oklch(48% 0.018 255); --color-rule:oklch(86% 0.01 255); --color-rule-strong:oklch(72% 0.018 255); - --color-accent:oklch(52% 0.19 258); + --color-accent:oklch(50% 0.12 258); --color-accent-ink:oklch(98% 0.005 258); --color-success:oklch(55% 0.13 150); --color-warning:oklch(67% 0.14 78); @@ -67,7 +67,7 @@ --color-muted:oklch(68% 0.014 255); --color-rule:oklch(31% 0.014 255); --color-rule-strong:oklch(43% 0.018 255); - --color-accent:oklch(67% 0.16 258); + --color-accent:oklch(70% 0.1 258); --color-accent-ink:oklch(17% 0.008 255); } } diff --git a/apps/web/ui-primitives.js b/apps/web/ui-primitives.js new file mode 100644 index 00000000..09ea9fe6 --- /dev/null +++ b/apps/web/ui-primitives.js @@ -0,0 +1,91 @@ +const API_ISSUE_HINTS = new Map([ + ['$.body.changedLocators', ['Changed files', 'Use workspace-relative paths under this repository, one per line. Keep the list bounded and review it before building.']], + ['$.body.userSelectedFiles', ['Explicit files', 'Use workspace-relative paths under this repository. Do not paste file bodies, absolute paths, credentials, or provider URLs.']], + ['$.body.memoryConfig.memoryPaths', ['Memory preflight sources', 'Use reviewed workspace-relative files only. Do not use absolute paths, URLs, credentials, or generated/local state directories.']], + ['$.body.client', ['Client', 'Choose a supported local harness client from the menu.']], + ['$.body.objective', ['Objective', 'Use a plain task summary. Do not include secrets, provider URLs, session tokens, absolute paths, or hidden reasoning.']], + ['$.body.step', ['Step', 'Use a short current-step label. Do not include secrets, provider URLs, session tokens, absolute paths, or hidden reasoning.']], + ['$.body.tokenBudget', ['Token budget', 'Use a positive number within the field limit.']], + ['$.body.sourceLocator', ['Source locator', 'Use a workspace-relative source file such as notes/memory.md.']], + ['$.body.text', ['Memory text', 'Use simple Fact or Decision lines with safe subject, predicate, and object text.']], + ['$.body.targetHarness', ['Target', 'Choose Codex, Claude Code, Cursor, or Generic agent.']], + ['$.body.from', ['Source families', 'Use supported source families only, such as codex, cursor, or claude-code.']], + ['$.body.workspaceId', ['Workspace', 'Use the current local workspace.']] +]); + +export function escapeHtml(value) { + return String(value ?? '').replace(/[&<>'"]/gu, (character) => ({ + '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' + })[character]); +} + +export function formatDate(value) { + if (!value) return '-'; + const parsed = new Date(value); + if (!Number.isFinite(parsed.getTime())) return '-'; + return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(parsed); +} + +export function shortFingerprint(value) { + return `${String(value ?? '').slice(0, 19)}...`; +} + +export function titleize(value) { + return String(value ?? '').split(/[-_]/u).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(' ') || 'Step'; +} + +export function safeErrorToken(value, fallback, maxLength = 120) { + const text = String(value ?? '').trim(); + if (!text || containsPrivateValue(text)) return fallback; + const normalized = text.replace(/[^\w$.[\]:-]/gu, '_').slice(0, maxLength); + return containsPrivateValue(normalized) ? fallback : normalized; +} + +export function buildApiErrorUiModel(errorLike) { + const error = typeof errorLike === 'object' && errorLike ? errorLike : { message: String(errorLike ?? 'Request failed.') }; + const issues = Array.isArray(error.issues) ? error.issues.slice(0, 5).map((issue) => { + const path = safeErrorToken(issue?.path, '$.body'); + const code = safeErrorToken(issue?.code, 'validation_failed', 64); + return { path, code, ...apiIssueHint(path) }; + }) : []; + return { + message: String(error.message ?? 'Request failed.'), + status: Number.isFinite(Number(error.status)) ? Number(error.status) : null, + code: safeErrorToken(error.code, '', 64), + correlationId: safeErrorToken(error.correlationId, '', 96), + issues + }; +} + +export function renderApiErrorPanel(heading, error) { + const model = buildApiErrorUiModel(error); + return statePanel('error', heading, model.message, false, renderApiErrorRecovery(model)); +} + +export function renderApiErrorRecovery(model) { + const issues = model.issues.length + ? `

Fix this field

    ${model.issues.map((issue) => `
  • ${escapeHtml(issue.label)}${escapeHtml(issue.detail)}${escapeHtml(issue.path)} · ${escapeHtml(issue.code)}
  • `).join('')}
` + : ''; + const correlation = model.correlationId + ? `

Correlation ${escapeHtml(model.correlationId)}

` + : ''; + return `${issues}${correlation}`; +} + +export function statePanel(kind, heading, copy, button = false, extra = '') { + const actions = button + ? '
' + : ''; + return `

${escapeHtml(heading)}

${escapeHtml(copy)}

${extra}${actions}
`; +} + +function apiIssueHint(path) { + const direct = API_ISSUE_HINTS.get(path); + if (direct) return { label: direct[0], detail: direct[1] }; + if (path.startsWith('$.body.')) return { label: titleize(path.slice('$.body.'.length)), detail: 'Review this field and use only supported local values.' }; + return { label: 'Request field', detail: 'Review the highlighted request field and retry with supported local values.' }; +} + +function containsPrivateValue(value) { + return /(?:\/Users|\/private|\/var\/folders|https?:|file:|token|secret|api[_-]?key|authorization|cookie)/iu.test(value); +} diff --git a/docs/adr/0023-production-rust-code-intelligence-engine.md b/docs/adr/0023-production-rust-code-intelligence-engine.md new file mode 100644 index 00000000..fcd3e4cc --- /dev/null +++ b/docs/adr/0023-production-rust-code-intelligence-engine.md @@ -0,0 +1,44 @@ +# ADR 0023: Production Rust code-intelligence engine + +## Status + +Accepted. + +## Decision + +Rust owns the production code-intelligence engine: repository discovery, +language detection, parsing, structural extraction, graph construction, and +incremental index updates. The engine emits a versioned, provider-neutral +JSON Lines protocol. It does not expose Tree-sitter node shapes, grammar crate +names, provider identifiers, absolute paths, or source bodies as public +identity. + +Node.js remains the product shell. It owns the CLI, loopback Control API, +governed memory workflow, read-only MCP surface, and web workbench. Node.js +starts the native engine, validates every protocol record, applies product +bounds and authorization, and converts results into Memory Recall contracts. +The process boundary keeps parser failure isolated from the long-running +product services. + +Indexes and caches produced by the engine are derived local state. Canonical +memory, approvals, handoffs, and provenance continue to use the existing +governed stores and protocols. A derived index may be deleted and rebuilt +without changing canonical product state. + +Published npm packages include signed platform binaries for supported targets. +Normal installation and use require no local Rust toolchain. Unsupported +platforms fail with a clear diagnostic instead of compiling during install or +silently falling back to a weaker parser. + +## Consequences + +- The existing Rust Tree-sitter implementation becomes the only production + parser path after compatibility and benchmark gates pass. +- The current JavaScript and TypeScript provider remains available during the + measured migration, then becomes an explicit fallback or is retired. +- The native protocol must be deterministic, bounded, versioned, and covered by + compatibility fixtures before the production switch. +- Parser and index data stay local by default. Network access is not part of + scanning, indexing, or querying. +- Native release artifacts require checksums, signatures, smoke tests, and npm + package verification for every supported platform. diff --git a/docs/adr/0024-rust-sqlite-source-index.md b/docs/adr/0024-rust-sqlite-source-index.md new file mode 100644 index 00000000..850568cf --- /dev/null +++ b/docs/adr/0024-rust-sqlite-source-index.md @@ -0,0 +1,54 @@ +# ADR 0024: Separate Rust SQLite source index + +## Status + +Accepted. + +## Decision + +The production code-intelligence index is a separate embedded SQLite database +owned by the Rust engine. Its default location is +`.local/source-index/index.v1.sqlite`. It stores derived structural records, +content hashes, evidence spans, unresolved relationships, coverage, generation +metadata, and health state. It never stores raw source bodies, absolute +checkout paths, credentials, environment values, or command output. + +The source index is not part of `oaf-store`. Governed memory, proposals, +approvals, supersession, handoffs, and canonical product state remain in their +existing memory stores. Source intelligence is derived and rebuildable, but a +corrupt index is preserved until an explicit repair plan is confirmed. + +Rust owns migrations, transactions, refresh, dependency invalidation, watcher +scheduling, health checks, and read-only structural queries. Node owns the CLI, +MCP, Control API, and browser boundaries. Node sends closed JSON Lines requests, +validates closed responses, enforces process and output limits, and never sends +raw SQL. + +Each successful writer transaction commits a complete generation and switches +the active generation atomically. Readers continue to use the previous active +generation until commit succeeds. A no-change refresh performs no writer +transaction. One previous valid generation is retained by default for bounded +rollback evidence. + +Read-only open never creates, migrates, repairs, or refreshes an index. Doctor +reports stable health codes and an explicit repair plan. Rebuild repair is a +separate confirmed writer operation and preserves the invalid database as a +bounded local backup. + +The current JS/TS JSON index remains a compatibility path during the measured +migration. Phase 3 does not change the public engine default, MCP behavior, or +npm binary packaging. Those changes require later distribution and benchmark +gates. + +## Consequences + +- Canonical memory cannot be mutated by source indexing or repair. +- All fourteen Tier 1 languages can use one normalized persistent store. +- Generation transactions isolate readers from interrupted refresh work. +- Index lifecycle and query operations need separate closed protocol schemas. +- Migration, corruption, interruption, concurrency, and process-restart tests + become release gates. +- JSON remains bounded export and compatibility output, not the scalable + production storage format. +- Reverting Phase 3 leaves the current JS graph, JSON index, MCP tools, and + governed memory available. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 6e39cd6b..f808a5ac 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -501,7 +501,7 @@ paths: schema: { $ref: '#/components/schemas/ContextGraphPreviewRequest' } responses: '200': - description: Bounded read-only native JS/TS source graph preview. + description: Bounded read-only source graph preview from a current native index, or a sanitized unavailable result with the exact native recovery reason. headers: x-correlation-id: { schema: { $ref: '#/components/schemas/CorrelationId' } } content: @@ -600,7 +600,7 @@ components: type: string minLength: 1 maxLength: 512 - pattern: '^(?!/)(?!.*(?:^|/)\\.\\.?($|/))(?!.*\\\\)(?!.*\\s)(?!.*://)(?!.*(?:^|/)(?:Users|private|node_modules|\\.git|\\.local)(?:/|$))(?!.*(?:^|/)var/folders(?:/|$))[A-Za-z0-9._@+~,-]+(?:/[A-Za-z0-9._@+~,-]+)*$' + pattern: "^(?!/)(?![^/]*%)(?![A-Za-z][A-Za-z0-9+.-]*:)(?!\\.\\.(?:/|$))(?!.*\\/\\.\\.(?:/|$))(?!.*%(?:2[eEfF]|3[aA]|5[cC]|25))[A-Za-z0-9._~!$&'()*+,;=@%/\\[\\]-]{1,512}(?:#L[0-9]+-L[0-9]+)?$" RecallMapQuery: type: string minLength: 1 @@ -610,7 +610,7 @@ components: type: object additionalProperties: false required: [workspaceId, changedLocators] - maxProperties: 3 + maxProperties: 4 properties: workspaceId: { $ref: '#/components/schemas/RecallMapWorkspaceId' } changedLocators: @@ -619,6 +619,7 @@ components: uniqueItems: true items: { $ref: '#/components/schemas/RecallMapChangedLocator' } query: { $ref: '#/components/schemas/RecallMapQuery' } + refresh: { type: boolean } RecallMapWorkspaceId: type: string maxLength: 128 @@ -870,7 +871,7 @@ components: type: string minLength: 1 maxLength: 512 - pattern: "^(workspace://)?(?!/)(?!.*\\.\\.)(?!.*\\\\)(?!.*\\s)(?!.*(?:^|/)Users(?:/|$))(?!.*(?:^|/)private(?:/|$))(?!.*(?:^|/)var/folders(?:/|$))[A-Za-z0-9._~!$&'()*+,;=:@%/-]{1,512}$" + pattern: "^(?:workspace://)?(?!/)(?![^/]*%)(?![A-Za-z][A-Za-z0-9+.-]*:)(?!\\.\\.(?:/|$))(?!.*\\/\\.\\.(?:/|$))(?!.*%(?:2[eEfF]|3[aA]|5[cC]|25))[A-Za-z0-9._~!$&'()*+,;=@%/\\[\\]-]{1,512}(?:#L[0-9]+-L[0-9]+)?$" tokenBudget: { type: integer, minimum: 1, maximum: 100000 } ContextPackResponse: type: object @@ -1236,6 +1237,7 @@ components: sampleLimit: { type: integer, minimum: 1, maximum: 50 } maxFiles: { type: integer, minimum: 1, maximum: 1000 } maxFileBytes: { type: integer, minimum: 1024, maximum: 1048576 } + refresh: { type: boolean } ContextGraphPreviewResponse: type: object additionalProperties: false @@ -1245,7 +1247,13 @@ components: previewVersion: { const: oaf-source-graph-preview-1.0.0 } workspaceId: { $ref: '#/components/schemas/WorkspaceId' } generatedAt: { type: string, format: date-time } - graph: { type: object } + graph: + type: object + properties: + summary: + type: object + properties: + coverage: { $ref: '#/components/schemas/SourceGraphCoverage' } search: { type: object } trace: { type: [object, 'null'] } impact: { type: [object, 'null'] } @@ -1265,6 +1273,62 @@ components: graphDatabaseUsed: { const: false } rawBodyIncluded: { const: false } sourceSlicesRead: { const: false } + SourceGraphCoverage: + type: object + properties: + status: { enum: [complete, partial] } + ignoredFileCount: { type: integer, minimum: 0, maximum: 1000000000 } + ignoredDirectoryCount: { type: integer, minimum: 0, maximum: 1000000000 } + ignoredSamples: + type: array + maxItems: 100 + items: { type: string, minLength: 1, maxLength: 512 } + ignoreRuleFingerprint: { type: string, pattern: '^sha256:[a-f0-9]{64}$' } + ignoreFileLocators: + type: array + maxItems: 100 + items: { type: string, minLength: 1, maxLength: 512 } + unsupportedExtensionCounts: + type: object + maxProperties: 32 + additionalProperties: { type: integer, minimum: 0, maximum: 1000000000 } + candidateNodeCount: { type: integer, minimum: 0, maximum: 1000000000 } + representedNodeCount: { type: integer, minimum: 0, maximum: 20000 } + omittedNodeCount: { type: integer, minimum: 0, maximum: 1000000000 } + candidateNodeKindCounts: { $ref: '#/components/schemas/SourceGraphNodeCountMap' } + representedNodeKindCounts: { $ref: '#/components/schemas/SourceGraphNodeCountMap' } + omittedNodeKindCounts: { $ref: '#/components/schemas/SourceGraphNodeCountMap' } + candidateEdgeCount: { type: integer, minimum: 0, maximum: 1000000000 } + representedEdgeCount: { type: integer, minimum: 0, maximum: 50000 } + omittedEdgeCount: { type: integer, minimum: 0, maximum: 1000000000 } + candidateEdgeKindCounts: { $ref: '#/components/schemas/SourceGraphEdgeCountMap' } + representedEdgeKindCounts: { $ref: '#/components/schemas/SourceGraphEdgeCountMap' } + omittedEdgeKindCounts: { $ref: '#/components/schemas/SourceGraphEdgeCountMap' } + reasonCodes: + type: array + maxItems: 16 + uniqueItems: true + items: { type: string, minLength: 1, maxLength: 64 } + SourceGraphNodeCountMap: + type: object + maxProperties: 4 + additionalProperties: false + properties: + file: { type: integer, minimum: 0, maximum: 1000000000 } + chunk: { type: integer, minimum: 0, maximum: 1000000000 } + symbol: { type: integer, minimum: 0, maximum: 1000000000 } + module: { type: integer, minimum: 0, maximum: 1000000000 } + SourceGraphEdgeCountMap: + type: object + maxProperties: 6 + additionalProperties: false + properties: + contains: { type: integer, minimum: 0, maximum: 1000000000 } + defined_in: { type: integer, minimum: 0, maximum: 1000000000 } + imports: { type: integer, minimum: 0, maximum: 1000000000 } + exports: { type: integer, minimum: 0, maximum: 1000000000 } + calls: { type: integer, minimum: 0, maximum: 1000000000 } + references: { type: integer, minimum: 0, maximum: 1000000000 } HarnessSetupPlanRequest: type: object additionalProperties: false diff --git a/docs/architecture/native-providers.md b/docs/architecture/native-providers.md index afbfbe88..5896b46d 100644 --- a/docs/architecture/native-providers.md +++ b/docs/architecture/native-providers.md @@ -15,8 +15,7 @@ Native providers make the local product useful without optional integrations. Th | `native.policy.deterministic` | on | Contextual policy decisions for resources, tools, data classes, approvals, and budgets | | `native.context-candidate.exact` | on | Workspace-scoped exact candidate lookup by canonical record ID | | `native.context-candidate.lexical` | on | Deterministic lexical candidate lookup over safe record fields | -| `native.context-candidate.ast-code` | on | Dependency-free JS/TS chunk and symbol candidate source | -| `native.context-candidate.graph` | on | Locator-only candidate source over the derived JS/TS source graph | +| `native.code-intelligence.rust` | on | Verified packaged Rust code intelligence over the explicit local SQLite index | | `native.context-manifest.local` | on | Workspace-scoped immutable local context manifest persistence | | `native.model.ollama` | off | Explicit loopback local-model generation | | `native.tool.brokered-local` | on | Reviewed checksum-pinned local tool execution behind brokers | @@ -130,7 +129,7 @@ external writes disabled. ## Context candidate-source providers -The native exact, lexical, and AST-code providers implement +The native exact and lexical providers implement `CandidateSourcePort` version `1.0.0`. Exact and lexical remain the conformance baselines for OAF-010 candidate generation. None of these sources is a storage engine or final selector. @@ -153,24 +152,17 @@ Both sources require contextual policy allow decisions before invocation and candidate-level policy allow decisions before output. Secret data is denied for model-context candidate generation by default. -The AST-code source is a dependency-free static JS/TS chunker and symbol index -for workspace files. It records parser version, workspace locator, byte and -line ranges, scope chain, symbol/import/export metadata, sibling locators, -signature hashes, exact reconstruction hashes, parse-error state, file -outlines, repository outlines, and content-hash journals without returning raw -source bodies or absolute local paths. It supports read-only definition, -reference, import, export, caller, callee, file-outline, and repository-outline -queries over the derived index. It is not a full semantic parser, language -server, graph index, executor, or Tree-sitter runtime. - OAF-011 preserves exact and lexical providers as conformance baselines. Hybrid fusion, global reranking, diversity, category caps, and token budgeting live in `packages/context-compiler/src/index.mjs`, not in candidate-source providers. Vector, temporal, preference, and episode source kinds remain declared but -unavailable until later tasks add explicit providers. The native graph source -is available now as a locator-only wrapper over the derived JS/TS source graph; -it is still not a graph database, semantic retrieval provider, or authority -surface. +unavailable until later tasks add explicit providers. + +The packaged Rust code-intelligence provider owns source structure, search, +trace, impact, and graph projections. It reads only a healthy, current local +SQLite index, returns locator-only bounded metadata, and requires an explicit +native build, refresh, or repair action when the index is unavailable. It is +not a graph database, semantic retrieval provider, or authority surface. ## Context manifest provider diff --git a/docs/architecture/protocol-bridges.md b/docs/architecture/protocol-bridges.md index 2c9b6b53..c8c54cf8 100644 --- a/docs/architecture/protocol-bridges.md +++ b/docs/architecture/protocol-bridges.md @@ -82,9 +82,16 @@ Dry-run is the default and writes nothing. The preview uses the same harness-setup planner and prints the ready-to-paste stdio server: ```text -npm --silent run recall -- mcp server --read-only --root . --stdio +npm --silent run recall -- mcp server --read-only --engine native --root . --stdio ``` +Installer-generated servers use strict native selection. The install +does not build or refresh a graph index: its report prints `indexBuildCommand` +as a separate explicit write. Without a healthy, current native index, the +read-only structural tools return the exact build, refresh, repair, schema, or +package action. They never silently switch engines. Direct `mcp server` commands +without `--engine` use the same native selection. + Applying the preview requires the matching plan fingerprint from that dry-run: ```bash diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 468858de..c52aa90f 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -3,9 +3,10 @@ This page is the source of truth for Memory Recall's public measurements. The artifact shape is stated for each benchmark below: most CLI benches emit JSON with a `reportFingerprint`, while the Context Recall script emits JSON without -that field and the product-proof commands deliberately render summaries. None -creates a versioned result artifact in this repository. Save the output yourself -if you need to retain a run. +that field and the product-proof commands deliberately render summaries. The +Phase 0 baseline, Phase 1 JS/TS compatibility receipt, Phase 2 Tier 1 audit, and +Phase 3 source-index receipt are the committed versioned code-intelligence +results. Save other output yourself if you need to retain a run. All token figures are local delivery estimates using `ceil(chars/4)`. They are not provider-billed tokens, production-cost estimates, or production-latency @@ -22,6 +23,202 @@ with `npm run recall --`. | Temporal current truth | 10/10 correct and clean current answers against the included evolving-fact fixture | Synthetic temporal fixture; not a token-saving result | | Structured-ingest sufficiency | 12/12 checkout-derived provider/default answers present after structured ingest | In-repo sufficiency check; not real-world QA | +## Retired JavaScript comparison receipts + +The Phase 0 baseline and Phase 1 JavaScript-versus-Rust comparison scripts and +receipts were retired with the duplicate JavaScript intelligence path. They are +not current benchmark evidence and do not support a parity or leadership claim. +Current evidence starts with the Tier 1 audit below. + +## Code-intelligence Phase 2 Tier 1 audit + +The committed aggregate is +`evals/code-intelligence/results/phase2-tier1-summary.json`. Reproduce it after +building the local release Rust binary: + +```bash +node scripts/code-intelligence-phase2-tier1.mjs --check +``` + +The aggregate binds five batch receipts covering 14 fixtures and all 43 pinned +repositories at their exact commits and bounded scopes. Each graph is built +twice with a 5,000-file, 512 KiB-per-file, 5,000-node, and 10,000-edge limit. +The stored audit records 50,664 nodes, 113,640 edges, 79,816,280 serialized +graph bytes, 22,794.19 ms summed first-run wall time, 23,570.05 ms summed +second-run wall time, and 254,752 KiB peak evaluator RSS on the recorded macOS +arm64 run. These machine-specific resource values are evidence receipts, not +performance promises. + +All 59 cases are deterministic and pass their reviewed truth: 0 duplicate +canonical symbols, 0 repository parse failures, 0 network calls, 0 model calls, +0 canonical-memory writes, and 0 workspace writes. Five repository scopes hit a +node or edge budget and store only safe reason/count diagnostics. The audit +does not hide those omissions. + +Across 154 Tier 1 capability cells, 75 meet the Phase 2 evidence floor, none has +a recorded floor failure, 78 applicable rows remain unmeasured, and one row is +explicitly not applicable. A row +requires reviewed evidence from its fixture and at least three distinct pinned +repositories before it can say `meets-floor`. Every Tier 1 language remains +overall `unmeasured`. Java, Go, Rust, Kotlin, C#, PHP, Ruby, Swift, C, and C++ +imports now have reviewed evidence from their fixtures plus three pinned +repositories per language. PHP evidence covers dotted namespace coordinates and +exact local-module resolution, Ruby preserves full `require` paths, Swift +preserves scoped coordinates, and C/C++ preserve angle-bracket header paths. +Go and Rust exports also meet the sampled floor across one fixture and three +pinned repositories each. Go export evidence omits exported fields and interface +methods. Rust exact evidence covers unrestricted top-level `pub` items and +simple-symbol `pub use`; grouped and glob re-exports remain outside the passing +sample. +Dart URI-level module re-exports also meet the sampled floor across one fixture +and three pinned repositories. Local relative exports and self-package +`package:` URIs resolve exactly when the scan root is a Dart package root or +its `lib` directory. `show` and `hide` symbol filtering remain unmeasured and +outside the passing sample. +Dart calls also meet the sampled fixture-plus-three-repository floor. In the +Flutter sample, `BookstoreAuth.of(context)` resolves to the workspace method and +the same-name `GoRouter.of(context)` decoy does not. This does not establish +general framework or monorepo call resolution. +Dart heritage meets the sampled fixture-plus-three-repository floor. Flutter +keeps `_BookstoreState` attached only to the direct outer `State` superclass, +rejects the generic argument `Bookstore` as heritage, and retains the sampled +mixin edge. Shelf's `RouterParams on Request` edge remains explicitly +unresolved, while HTTP's `BaseClient implements Client` edge resolves exactly. +Generic substitution, compiler-level inference, and broader framework heritage +remain unmeasured. +Kotlin calls meet the sampled floor for callsite-owner attribution. In Now in +Android, the `UserNewsResource` constructor owns the line-45 `map` call while the +line-57 `map` call is not attributed to it. The sampled target remains unresolved; +this does not establish general typed, framework, or monorepo call resolution. +Kotlin types meet the sampled fixture-plus-three-repository floor. Coroutines +binds `InlineList(element)` without mislabeling the generic `ArrayList(4)` +line, and Ktor binds `RoutingResolveTraceEntry(...)` without confusing it with +`RoutingResolveTrace`. Generic constructor calls and property initializer +constructions remain missed; `List(size) { ... }` can appear as an unresolved +construction. Full Kotlin type inference, generic substitution, nullability +flow, overload resolution, and compiler-equivalent analysis remain unmeasured. +C++ types meet the sampled fixture-plus-three-repository floor. The fmt case +keeps `utf8_system_category` as a class and rejects `FMT_STRING(...)` as a +construction edge. General preprocessor expansion, template analysis, and C++ +type resolution remain unmeasured. +C++ heritage meets the sampled fixture-plus-three-repository floor for direct +base specifiers. The reviewed cases bind `ItemService` to `ItemLoader`, +`ApproxMatcher` to `MatcherBase`, `utf8_system_category` to unresolved external +`error_category`, and `lexer` to `lexer_base`. Generic type arguments and a +class referenced from a body are explicit negative decoys. Template +substitution, alias expansion, dependent names, and compiler-equivalent +inheritance analysis remain unmeasured. +Grouped PHP imports such as `use Foo\{Bar, Baz};` remain unsupported and +unmeasured; they were not counted in the passing sample. Python has exact +framework-route evidence from narrow +FastAPI, Flask, and Django application scopes. The broader FastAPI and Flask +implementation scopes and the Requests client remain in the corpus for their +other reviewed capabilities. Documentation examples do not qualify as routes. +Only C heritage is recorded as not applicable because C has no language-level +inheritance, interface, trait, protocol, or mixin relationship. The native +engine stays an unbundled preview; the npm, MCP, and web defaults remain JS. +Competitors remain unmeasured, and the receipt makes no parity, leadership, +multi-repository, or scale claim. + +## Code-intelligence Phase 3 source index + +The committed receipt is +`evals/code-intelligence/results/phase3-source-index.json`. Verify its pinned +inputs, fingerprint, safety boundary, and pass decision with: + +```bash +node scripts/code-intelligence-phase3-index.mjs --check +``` + +Run the script without `--check` to fetch the same exact commits into temporary +directories and remeasure them. The receipt records the exact Memory Recall +checkout commit and whether that checkout was dirty at measurement time. The +current environment is macOS 25.5 arm64, Apple M2 Max, Node 22.22.3. It covers a +600-file TypeScript dependency fixture, the pinned +HashiCorp go-multierror repository, the pinned Express repository, and the +pinned TypeScript compiler-transformers scope. + +Across 781 files, 6,646 nodes, and 15,540 edges, the four cold builds had a +machine-specific p50 of 187.77 ms and maximum of 883.291 ms. Warm status p50 +was 21.763 ms; no-change refresh p50 was 21.367 ms. The 600-file fixture parsed +zero files on an exact no-op refresh, 11 files after the sampled isolated file +change, and 5 files after the sampled dependency-impact change. All no-op and +reader checks preserved the exact SQLite bytes and modification time. + +The receipt stores five-sample exact, neighborhood, impact, and trace timings, +response bytes, database sizes, changed/reused counts, evaluator RSS, omissions, +and safe diagnostics per case. These are machine-specific preview measurements, +not latency promises. The engine made no network or model calls; the benchmark +harness made three network fetches to obtain the pinned repositories. It does +not measure a packaged native binary, competitors, multi-repository behavior, or +million-node scale, so it makes no parity or leadership claim. + +## Code-intelligence Phase 4 intelligence + +The committed receipt is +`evals/code-intelligence/results/phase4-intelligence.json`. Verify its +fingerprint, pass decision, evidence integrity, and claim boundary with: + +```bash +node scripts/code-intelligence-phase4-intelligence.mjs --check +``` + +The local fixture builds four disconnected TypeScript areas plus one Next.js +route-to-handler call chain. Five repeated reads prove deterministic exact, +lexical, and one-hop structural search alongside bounded communities and an +evidence-backed process. The same fixture proves that an outbound depth-two +calls-only query retains both call steps and their source-backed endpoints while +excluding other edge kinds. Every query stays below the bounded two-second deadline +and preserves the exact SQLite bytes and modification time. The committed +receipt records the machine-specific p95 values. + +This is a deterministic local correctness and deadline gate, not a competitor +benchmark or a general latency promise. It makes no parity, leadership, +multi-repository, packaged-binary, or million-node claim. + +## Code-intelligence Phase 5 cross-service gate + +Verify `evals/code-intelligence/results/phase5-cross-service.json` with: + +```bash +node scripts/code-intelligence-phase5-cross-service.mjs --check +``` + +Five TypeScript fixture reads prove a gateway-to-orders import, call, trace, +and process. Negative cases and bounds pass, reads preserve SQLite, and the +recorded macOS arm64 p95 was 9.603 ms. This is single-repository evidence; no +registry, multi-repository, parity, leadership, or scale claim is made. + +## Native package consumer gate + +Run the checkout-only installed-product gate after a release build: + +```bash +cargo build --release -p oaf --manifest-path rust/Cargo.toml --locked +node scripts/native-code-intelligence-consumer-smoke.mjs +``` + +On the reviewed macOS arm64 run, the platform tarball contained exactly one +verified native binary and installed beside the root tarball without registry +access or install scripts. With Cargo and rustc unavailable at runtime, the +installed provider returned parser-produced graph evidence for all fourteen +Tier 1 languages and completed SQLite build, status, and query operations. The +gate also checks checksum/version selection, source and governed-memory +preservation, and unchanged installed package bytes. It is a current-platform +consumer gate, not cross-platform, signing, publication, parity, or leadership +evidence. + +The same gate removes both packages, verifies that the executable and package +roots are gone while the workspace SQLite bundle and governed memory are +unchanged, then installs the exact same tarballs into a fresh prefix and reopens +the existing generation for representative TypeScript, Python, and Go queries. +That proves same-version reinstall survivability, not cross-version downgrade. + +The five-target Rust CI matrix reuses this installed-consumer gate with the +exact unsigned tarball produced by each native runner. Only the macOS arm64 lane +has been reproduced locally; the workflow configuration does not count as a +passing result for the other four targets. + ## Dataset | Claim | Dataset | @@ -114,13 +311,20 @@ recall graph stats --root . --format summary recall mcp inspect --read-only --root . --format summary ``` -`map` and `graph stats` cover the implemented bounded JS/TS static graph. +`map` and `graph stats` cover the implemented bounded native SQLite index. `context handoff`, `memory refine`, and `mcp inspect` expose their own safeguards and status fields. Treat graph and handoff token reports as live local measurements, not fixed provider-billing or cross-repository claims. ## Methods without a headline number +`npm run source-graph:large-smoke` creates a temporary 1,100-file JS fixture, +exercises the 1,000-file default scan bound, builds and reloads the persistent +index, changes one represented file, and verifies that refresh parses one file +while reusing 999 shards. The JSON includes cold, warm, and one-file refresh +times plus graph and index sizes. Those values are machine-specific and are not +a production latency or million-node claim. + `recall bench locomo --read-only --root . --dataset evals/locomo/smoke.v1.json --budget 512 --limit 4 --format json` is a model-free retrieval-coverage method. The committed smoke fixture contains one conversation and three questions. Its report explicitly says it does not diff --git a/docs/implementation/MEMORY_RECALL_COMPLETION_MATRIX.md b/docs/implementation/MEMORY_RECALL_COMPLETION_MATRIX.md new file mode 100644 index 00000000..989f4636 --- /dev/null +++ b/docs/implementation/MEMORY_RECALL_COMPLETION_MATRIX.md @@ -0,0 +1,203 @@ +# Memory Recall completion matrix + +Snapshot date: 2026-07-19 + +Branch: `codex/memory-recall-orientation-workbench` + +Initial matrix commit: `dc8fb91` + +Current evidence commit: `82b10e0` + +Public registry rechecked 2026-07-18: `memory-recall@1.1.0`; all five `@memory-recall/native-*` packages returned npm `E404`. + +This is the controlling proof ledger for the polyglot code-intelligence and stable-release goal. A green narrow test does not close a broader row. The original completion criteria remain unchanged. + +## Status vocabulary + +| Status | Meaning | +| --- | --- | +| Proven | Current authoritative evidence covers the full stated row. | +| Incomplete | Some implementation or proof exists, but the full row is not established. | +| Contradicted | Current implementation or external state conflicts with the requirement. | +| Missing | No authoritative implementation or proof was found. | + +## Release identity and semantic version + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| V-1 | Ship one normal stable release, not a beta, preview, RC, or partial package. | Incomplete | `package.json` now selects stable major `2.0.0`; no publication is authorized or proven. The frozen tarball, five remote targets, registry proof, and published-package smoke still remain. | Pass every local and remote final gate, then publish exactly one stable `2.0.0` release in native-first order. | +| V-2 | Do not treat the approximately 89,000-line expansion as an assumed patch. | Proven | The published baseline is `1.1.0`; the compatibility audit records breaking distribution, MCP-config, Recall Map wire, default-engine, and shipped Rust-source contracts. The source line is explicitly `2.0.0`. | Keep every release artifact on the selected major line; do not downgrade the release classification. | +| V-3 | Preserve the exact original completion criteria. | Proven | This matrix maps the approved polyglot design, the active goal, and the ten stable-release priorities without redefining success. | Keep this ledger current after each slice. | + +## Product target + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| T-1 | Replace GitNexus for developer and coding-agent workflows. | Missing | `evals/code-intelligence/results/phase2-tier1-summary.json` explicitly denies parity; the Phase 8 test uses a fake GitNexus executable and one Go fixture. | Run identical pinned-corpus task and performance comparisons against the real permitted GitNexus artifact and file baselines. | +| T-2 | Meet or beat GitNexus across the fourteen Tier 1 languages. | Incomplete | Phase 2 has 14 fixtures and 43 repositories. Of 154 capability rows, 75 meet the sampled floor, 78 applicable rows are unmeasured, and one row is explicitly not applicable. | Resolve every applicable row with fixture and capability-specific evidence from at least three pinned repositories per language. | +| T-3 | Promote extra languages only after equal gates. | Proven | `PROJECT_STATUS.json` and `docs/usage/code-intelligence-support.md` keep Lua, Bash, SQL, Objective-C, Scala, R, Julia, and Zig experimental. | Do not promote them until the Tier 1 gate is closed and equivalent evidence exists. | +| T-4 | Keep installation to `npm install -g memory-recall` without Cargo or rustc. | Incomplete | `scripts/native-code-intelligence-consumer-smoke.mjs` proves the packed root plus native tarball on darwin-arm64; the other four platforms and registry install are unproven. | Pass clean packed and registry-shaped installs on all five targets with Cargo/rustc absent. | + +## Architecture and invariants + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| A-1 | Rust owns production code intelligence; Node owns transport, validation, memory, CLI, MCP, API, and UI. | Incomplete | ADR 0023 defines the boundary. CLI, MCP, Control API, and web select the packaged Rust SQLite index and fail closed with exact recovery guidance. Current-host packed browser proof confirms the native Control API and Map path. The duplicate JavaScript intelligence implementation and tests are deleted. | Prove all five release targets. | +| A-2 | Use one versioned provider-neutral graph and index schema. | Proven | `packages/protocol/schemas/code-intelligence-graph.schema.json`, index request/response schemas, provider wrapper, protocol validation, and compatibility tests. | Preserve schema compatibility through the final version audit. | +| A-3 | Use a persistent incremental embedded index; JSON is bounded export/debug output only. | Incomplete | Phase 3 proves SQLite generations and bounded incremental refresh over 781 files. Production CLI, MCP, API, and web reads use the SQLite index without mutating it; the current-host packed browser proof preserves the prebuilt SQLite bundle during normal and stale-index reads. No duplicate intelligence store remains. | Prove the final five-target distribution gate. | +| A-4 | Persist content hashes and handle changed, added, deleted, and renamed files. | Incomplete | Rust index refresh and focused tests prove changed/added/deleted invalidation; exact rename behavior is not separately evidenced in the current receipts. | Add one rename regression proving identity/history behavior and refresh equivalence. | +| A-5 | Dependency invalidation is bounded and correct. | Proven | Phase 3 dependency-closure refresh receipt and Rust index tests pass with zero unresolved/omitted rows in the measured cases. | Revalidate at the large-repository gate. | +| A-6 | Bounded watch mode works reliably. | Incomplete | Watcher coordination exists and focused tests pass; no cross-platform, crash, or large-repository watcher gate is recorded. | Prove debounce, concurrent changes, deletion/rename, restart, and all supported platforms. | +| A-7 | Index migrations and corruption recovery are safe. | Incomplete | SQLite migrations, doctor, and repair exist with focused tests. Packaged cross-version migration, downgrade, and corrupt-index recovery are not proven on all targets. | Add packaged prior-version migration, corruption, repair, downgrade refusal/rollback, and clean reinstall gates. | +| A-8 | Freshness is explicit and reads fail closed. | Proven | Native status hashes current source scope. Default MCP and the strict `auto`/`native-preview` aliases recheck freshness before structural calls, read only a healthy current committed generation, and return bounded actionable errors for every other state without building, repairing, or selecting JS. | Preserve the behavior after compatibility deletion and through packed cross-platform gates. | +| A-9 | Multi-repository indexes load lazily and remain bounded. | Incomplete | The Rust SQLite registry supports up to eight indexes and one exact Go path. A release-binary regression now proves isolated TypeScript and Python repositories with same-name symbols, bounded qualified results, stale-index partial results, missing-index partial results, and no registry writes during reads. Lazy eviction and larger multi-repository measurements are still unproven. | Prove bounded lazy opening/eviction and representative larger multi-repository measurements. | +| A-10 | Preserve local-first operation, workspace isolation, provenance, read-only MCP, no silent cloud fallback, and reviewed semantic memory. | Proven | Protocol schemas, 12-tool MCP tests, governed-memory tests, Phase 0-5 safeguards, and project defaults report zero model/network/canonical-memory writes for structural reads. | Re-run security and packed-product gates after the production-default cutover. | + +## Stable distribution chain + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| D-1 | Publish all five native packages before the root package. | Incomplete | The local Rust workflow now aggregates exactly five attested packages; the npm workflow publishes them in fixed order, verifies npm integrity, and reaches root publication only afterward. No remote run or publication is authorized or proven. | Run the exact five-target workflow at the frozen commit and prove the protected publication lane without bypass. | +| D-2 | No root package may reference unpublished native packages. | Incomplete | Root `optionalDependencies` still references five `@memory-recall/native-*@2.0.0` packages that currently return npm `E404`. The workflow and the exported/direct root publisher now require the exact signed five-package native artifact directory, validate one version and commit, and re-verify registry integrity, provenance, and npm signature audit immediately before any root lookup or publication. The local guard passes; no remote protected run or registry proof exists. | Prove the same fail-closed guard in the remote protected workflow and publish no root until all five native packages pass it. | +| D-3 | Root installation works without a Rust toolchain. | Incomplete | An isolated Darwin-arm64 lifecycle installs the single prepacked root tarball and its matching native tarball without Cargo/rustc, validates the installed package paths, exercises CLI and MCP, uninstalls, and reinstalls the same artifacts. Four platform lanes and registry installation remain unproven. | Run the identical consumer smoke on all five native runners using the exact release artifacts. | +| D-4 | Validate target OS, CPU, libc, binary version, and exact package contents. | Incomplete | `.github/workflows/rust.yml` defines five runner identities and validates tarball contents; only darwin-arm64 has a retained local receipt. | Obtain successful retained receipts from all five GitHub matrix jobs at the frozen commit. | +| D-5 | Verify binary and tarball checksums. | Incomplete | Native manifests and receipts carry SHA-256; the aggregate validator binds the exact tarball hash and npm integrity for all five packages before publication. Four remote receipts are missing. | Obtain and verify the complete frozen-commit artifact set from all five runners. | +| D-6 | Produce complete native provenance and SBOM. | Incomplete | The local workflow produces file-complete SPDX 2.3 documents for each native target and the exact root tarball, with SHA-1/SHA-256 file checksums, package verification codes, tarball checksums, and version-qualified namespaces. The workflow binds both artifact classes to signed SBOM predicates. No remote attestation set exists. | Obtain and verify all root and native provenance and SBOM attestations from one successful frozen-commit run. | +| D-7 | Sign supported native artifacts. | Incomplete | The local workflow uses `actions/attest@v4`; both the Rust aggregate and npm consumer verify exact repository, signer workflow, signer/source commit, and hosted runner. Remote attestations and any required platform code-signing proof remain absent. | Run the protected five-platform producer, retain the cryptographic attestations, and document whether macOS/Windows platform signing is additionally required. | +| D-8 | Prove uninstall and clean reinstall. | Incomplete | The isolated Darwin-arm64 exact-artifact lifecycle proves MCP entry removal, neighboring-config preservation, package removal, workspace-state preservation, and same-version clean reinstall that reopens the existing index without rebuilding. | Repeat on all five targets and add cross-version upgrade, downgrade-refusal, and rollback cases. | +| D-9 | Publish checksums, provenance, and SBOM with the stable release. | Incomplete | The protected workflow packs the root exactly once, records SHA-256 and SHA-512 integrity, uploads that artifact, attests its provenance and complete SPDX predicate, and later publishes those exact bytes. Registry verification requires exact root integrity plus both npm publish and SLSA predicates after the five equivalent native verifications. No remote evidence or authorized stable release exists. | Run the protected workflow at the frozen commit, retain the root and native evidence set, and attach it only during the authorized stable release. | + +## Fourteen-language support gate + +Phase 2 currently reports 75 `meets-floor` rows, 78 applicable `unmeasured` rows, and one explicit `not-applicable` row. No row is recorded as `does-not-meet-floor`, which means missing proof must not be relabeled as support. + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| L-1 | At least three pinned repositories exist for every Tier 1 language. | Proven | `evals/code-intelligence/corpus.v1.json` contains 43 pinned repositories; the corpus gate passes for all fourteen languages. | Preserve exact commits and source hashes in final comparison runs. | +| L-2 | Declarations and structural symbols meet recall floors. | Incomplete | All languages have sampled declaration evidence, but capability coverage remains uneven and partial scopes exist. | Expand reviewed declarations per capability and keep recall at or above 95% for every language. | +| L-3 | Packages, modules, imports, exports, and bindings resolve exactly. | Incomplete | All fourteen Tier 1 import rows meet the sampled floor. Go exports preserve specification-defined public identifiers; Rust exports preserve public visibility and re-export targets; Dart URI-level module re-exports preserve full coordinates and resolve local relative or self-package targets when scanning a package root or its `lib` directory. Each has fixture plus three-repository evidence. Nine export rows, Dart `show`/`hide`, broader Dart monorepo-root package discovery, and broader binding behavior remain unmeasured. | Add fixture plus three-repository truth for every remaining applicable package/export/binding row. | +| L-4 | Cross-file resolution is exact and source-backed. | Incomplete | Selected language fixtures and Phase 3 dependency cases pass; no all-language capability gate exists. | Prove cross-file positive, negative, same-name decoy, and unresolved diagnostics per language. | +| L-5 | Heritage, interfaces, traits, and protocols resolve. | Incomplete | Python, Dart, C++, and selected languages have sampled heritage. Dart has fixture plus three-repository evidence for a direct generic superclass, mixin, interface, and extension-on-type edge. C++ has the same breadth for direct bases while rejecting generic arguments and body references. Template substitution and most remaining heritage rows are unmeasured. | Add reviewed heritage truth for every remaining applicable language and explicit not-applicable decisions where the language lacks the construct. | +| L-6 | Type and receiver inference resolves calls correctly. | Incomplete | TS and Python have reviewed inference evidence. Kotlin now has sampled constructor evidence with distinct target/line decoys, but generic constructors and property initializers remain missed and a standard-library factory can appear as unresolved construction. C++ includes a real class plus a macro-shaped constructor decoy; general preprocessor expansion, templates, and C++ type resolution remain unmeasured. Several language type/call rows remain unmeasured. | Prove same-name receiver disambiguation and confidence for every applicable Tier 1 language. | +| L-7 | Calls meet at least 90% reviewed precision with confidence evidence. | Incomplete | Phase 2 sampled call precision passes where measured. Dart includes a typed Flutter call and a same-name wrong-callsite decoy. Kotlin includes fixture plus three-repository callsite-owner evidence and rejects a wrong-caller decoy, while the sampled target remains unresolved. Swift and several other call/type combinations remain unmeasured or governed-memory-limited. | Add reviewed positive/negative calls and confidence strategies per remaining language without weakening identity safety. | +| L-8 | Entry points are detected for applicable applications and frameworks. | Incomplete | Selected fixtures expose `entry_point`; there is no capability row or three-repository gate per language. | Define applicability and prove real entry points or explicit not-applicable results. | +| L-9 | Framework routes are detected without documentation/example false positives. | Incomplete | Python FastAPI, Flask, and Django meet a narrow reviewed floor; other applicable language/framework rows are unmeasured. | Add framework-specific truth for applicable languages and explicit unsupported diagnostics elsewhere. | +| L-10 | Configuration resources and build/package manifests are modeled. | Incomplete | Python config is measured; most config rows are unmeasured. | Define per-language applicability and add reviewed manifest/config truth. | +| L-11 | Impact is correct for each language. | Incomplete | Phase 4 now runs bounded impact probes on exact pinned JavaScript, TypeScript, and Go repositories, and Phase 5 proves one Go path. These are evidence-bearing probes, not reviewed impact truth across fourteen languages; Phase 2 impact rows remain unmeasured. | Prove forward/reverse impact with evidence and decoys on three repositories per applicable language. | +| L-12 | Search, context, trace, dependencies, and processes work for each language. | Incomplete | Native provider and MCP contracts exist. Phase 4 runs the workflow families on exact pinned Express, Nest cats-sample, and Gin scopes in addition to the four-file fixture. Eleven languages and task-level truth remain open, and three repositories total do not satisfy the per-language corpus gate. | Run the full workflow set on the per-language pinned corpus with bounded outputs and task-level truth. | +| L-13 | Results are deterministic and contain no duplicate canonical symbols. | Incomplete | Phase 2 sampled graphs are deterministic with zero recorded duplicates; five repository scopes report omissions and 78 applicable capability rows remain unmeasured. | Re-run determinism/duplicate checks for every completed capability and final packaged binaries. | +| L-14 | Partial, unsupported, and not-applicable behavior is explicit. | Incomplete | Tier 1 applicability is now independent of evidence presence. All applicable unsupported or missing rows remain non-green; C heritage is the sole explicit not-applicable row with a language-semantic rationale. Seventy-eight applicable rows remain unmeasured. | Prove or explicitly fail every remaining applicable row without converting missing evidence into not-applicable support. | + +### Current unmeasured rows + +| Language | Unmeasured | Capabilities | +| --- | ---: | --- | +| TypeScript | 5 | heritage, config, frameworks, impact, processes | +| JavaScript | 5 | heritage, config, frameworks, impact, processes | +| Python | 3 | exports, impact, processes | +| Java | 7 | exports, heritage, types, config, frameworks, impact, processes | +| Kotlin | 6 | exports, heritage, config, frameworks, impact, processes | +| C# | 7 | exports, heritage, types, config, frameworks, impact, processes | +| Go | 6 | heritage, types, config, frameworks, impact, processes | +| Rust | 5 | heritage, config, frameworks, impact, processes | +| PHP | 6 | exports, types, config, frameworks, impact, processes | +| Ruby | 7 | exports, heritage, types, config, frameworks, impact, processes | +| Swift | 6 | exports, calls, config, frameworks, impact, processes | +| C | 6 | exports, types, config, frameworks, impact, processes | +| C++ | 5 | exports, config, frameworks, impact, processes | +| Dart | 4 | config, frameworks, impact, processes | + +## Phase 4 intelligence + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| I-1 | Deterministic communities on real repositories. | Incomplete | The current Phase 4 receipt runs `label-propagation-v1` five times on exact pinned Express, Nest cats-sample, and Gin scopes (260 files, 4,564 nodes, 13,640 edges combined), records deterministic results, and proves two non-overlapping cursor pages for every repository. It does not contain reviewed community truth or large-repository stability evidence. | Add reviewed community truth and stability metrics on representative pinned repositories and large repos. | +| I-2 | Bounded entry-to-sink processes on real repositories. | Incomplete | All three exact repositories return deterministic `entry-path-v1` processes with at least two nodes, canonical relationship evidence, confidence of at least 0.75, continuous pagination, and p95 latency below the two-second deadline. The receipt has no reviewed process ground truth and does not cover cycles, incomplete paths, varied sinks, or representative language breadth. | Prove correct complete/incomplete paths, multiple entries/sinks, cycles, depth bounds, and language coverage on real repositories. | +| I-3 | Hybrid local search. | Incomplete | Native exact, lexical, and one-hop structural search passes the local fixture. Search is deterministic on Express, Nest, and Gin; each result has a workspace locator, all p95 values are below 74 ms, and receipts record delivered bytes/token estimates plus continuation where present. There is no reviewed relevance set, MRR/recall result, lexical-only comparison, or exact tokenizer measurement. | Add reviewed query sets, MRR/recall, lexical-only baseline, exercised search continuation, and exact delivered-token measurements on representative real repositories. | +| I-4 | Routes, impact, dependencies, trace, and safe graph queries. | Incomplete | All three exact repositories run every query family deterministically under the two-second deadline. Dependencies, safe query, and trace must traverse at least one real relationship; every returned relationship carries canonical endpoints, locator evidence, and confidence. Generic seeds and the absence of reviewed positive/negative truth prevent a correctness claim. | Add identical real-repository queries with reviewed positive/negative truth, decoys, and deadlines across representative languages. | +| I-5 | Evidence, confidence, pagination, token accounting, and deadlines. | Incomplete | The fixture proves relationship evidence, confidence propagation, and read queries preserving its SQLite bundle. All three real-repository runs prove located results, evidence-complete relationships, process evidence, community/process cursor continuity, per-query byte/token estimates, and two-second deadlines. Exact token accounting, token budgets, truncation truth, cancellation, and pagination for every query family remain unproven. | Add exact delivered-token accounting, budgets, truncation/cancellation cases, and continuation tests per query family. | +| I-6 | Preserve exactly twelve MCP tools unless a distinct capability requires another. | Proven | MCP inventory and packed consumer smoke verify twelve read-only tools. Phase 4/5 reuse those tools. | Keep the inventory stable through final packaging. | + +## Phase 5 multi-repository and cross-service intelligence + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| M-1 | General multi-repository search and traversal. | Incomplete | The registry now proves bounded qualified search across three TypeScript, Python, and Go repositories with deliberately ambiguous symbols, plus exact Go traversal between two independent Git repositories with an identical decoy rejected. Missing and stale indexes return isolated bounded partial results. | Broaden traversal and relationship evidence beyond the exact Go module path to representative cross-language and cross-service cases. | +| M-2 | Evidence-backed cross-repository and cross-service relationships. | Incomplete | One Go import/call/trace/impact path and one monorepo two-prefix fixture pass with decoy rejection. | Add general package coordinates, services, routes, data flow, reverse impact, and cross-language cases. | +| M-3 | Preserve repository/workspace isolation. | Proven | Registry/provider tests reject another workspace, keep unavailable and stale indexes isolated, preserve every queried SQLite bundle, and redact local paths. The packed consumer proof verifies independent Git repositories, decoy exclusion, and no package/config/source mutation. | Preserve these bounds through final cross-platform packaging. | + +## Scale and performance + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| S-1 | Index exactly one million committed nodes within the measured gate. | Proven | `evals/code-intelligence/results/million-node-rust-index.json` records exactly 1,000,000 committed nodes and 999,000 edges in 80,393 ms with zero omissions under the unchanged 300,000 ms build deadline. | Re-run only at the final frozen performance gate or after relevant Rust index changes. | +| S-2 | Warm open, no-change refresh, one-file refresh, and exact query work on the million-node index. | Proven | The same retained report records a 1,357 ms warm open, 3,851 ms no-change refresh with zero writes, 51,107 ms one-file refresh with 999 reused files, and twenty exact queries with 1,547 ms p95, stable results, and an unchanged SQLite fingerprint. | Re-run only at the final frozen performance gate or after relevant Rust index changes. | +| S-3 | Optimize only measured bottlenecks. | Proven | The measured reader bottleneck was removed without raising either deadline; the retained million-node report passes and records the exact binary hash, commit, RSS, database size, latency, and integrity evidence. | Preserve the gate and profile again before any further scale optimization. | + +## Production default and duplicate deletion + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| P-1 | Packaged Rust is the production default in CLI. | Proven | The current-host packed consumer gate installs the root and verified native package with Cargo and rustc unavailable, fails closed before indexing, builds the first SQLite index only through the explicit writer command, and then runs default native stats and search without mutating the index. `auto` and `native-preview` are strict native aliases; no JavaScript intelligence engine remains. | Re-run the same packed gate on the other four release targets and the final registry package under T-4. | +| P-2 | Packaged Rust is the production default in MCP. | Proven | The same clean packed install exposes exactly twelve read-only tools and executes all twelve against the current native index. The gate proves `context.pack` returns native-index evidence, confirms the retired JavaScript graph paths are absent from the installed root package, and verifies SQLite, WAL, SHM, memory, source, config, and package state remain unchanged. | Preserve this installed-package absence proof through all five release targets. | +| P-3 | Packaged Rust is the production default in web/Control API. | Incomplete | A clean packed Darwin-arm64 install proves the browser Map and Control API read the verified native index without Cargo, rustc, a C compiler, or any index mutation. After a real source change it returns `source_index_refresh_required` and renders the exact native refresh command; after deliberate index removal it returns `source_index_build_required` and renders the explicit writer command. Neither state falls back or creates SQLite side files. Missing-package and other-platform recovery remain unproven. | Prove the remaining recovery states and all five release targets. | +| P-4 | Delete duplicate JS intelligence and duplicate tests after default cutover. | Proven | The retired JavaScript source-graph modules, providers, compatibility mode, duplicate tests, and old comparison scripts are deleted. Current-host packed smoke proves the installed package excludes those paths while CLI and all twelve MCP tools read the native index. | Re-run the absence proof on the four remaining release targets and final registry tarballs. | + +## Large-repository UI + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| U-1 | First screen explains shape, coverage, subsystems, entry points, hotspots, changes, memory, and next action. | Proven | `c02e986` runs Chromium against a clean 1,225-tracked-file clone. Its first desktop screen has a bounded architecture board, current coverage, ranked entry points, current-change impact, memory and handoff truth, and a direct Map action, all within the viewport. | Re-run only after a first-screen behavior change. | +| U-2 | Map uses progressive disclosure for groups, communities, processes, neighborhoods, and evidence. | Proven | `c02e986` directly loads `/map?group=apps%2Fweb` against that clone and proves a focused native neighborhood, deterministic architectural groups, bounded entry-to-sink processes, evidence inspector, keyboard outline selection, and a six-group visual board with the full bounded outline disclosed separately. | Re-run only after Map behavior changes. | +| U-3 | Navigation is understandable and Map works directly. | Proven | The first-run browser flow reaches Map from Overview, submits and reloads a focused Map query, and `c02e986` proves a direct large-repository `/map` request without a second click. `82b10e0` covers the current public routes plus the Map and Handoffs aliases. | Re-run only after navigation behavior changes. | +| U-4 | No overlap or unreadable source-map outline. | Proven | `c02e986` retains desktop and 390px large-repository captures: canvas labels are bounded, outline labels and locators ellipsize safely, no desktop or mobile horizontal overflow occurs, and the last mobile outline record remains reachable. | Re-run only after graph layout or outline changes. | +| U-5 | Desktop, mobile, accessibility, overflow, and every control pass in a real browser. | Proven | `82b10e0` audits every enabled visible control across every public route plus Map and Handoffs for an accessible name, keyboard reachability, one main landmark, and one page heading; it also proves desktop/mobile overflow bounds, reduced-motion rendering, Map interactions, and no unexpected console/page errors. `c02e986` adds the same responsive and keyboard checks on a 1,225-file clone. | Re-run only after a user-facing browser control changes. | +| U-6 | Complete the anti-slop review before UI completion. | Proven | `docs/implementation/MEMORY_RECALL_UI_ANTI_SLOP_REVIEW.md` reviews the current first-screen, Map, memory, handoff, responsive, accessibility, motion, and information-density evidence at `c02e986`. It records the prior dense-graph and large-Map response fixes and rejects non-functional decoration. | Revisit only if a user-facing UI surface changes before the frozen release gate. | + +## Head-to-head proof + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| H-1 | Run identical pinned commits through Memory Recall, GitNexus, and file baselines. | Missing | The Phase 8 runner covers one Go fixture and the automated test substitutes a fake GitNexus executable. | Use the real permitted GitNexus artifact and the full pinned Tier 1 corpus with immutable versions/hashes. | +| H-2 | Measure structural, search, and task accuracy. | Missing | Memory Recall truth exists; there is no complete identical-product comparison report. | Define identical reviewed questions and compute recall, precision, MRR, task success, and unsupported cases. | +| H-3 | Measure indexing, latency, RSS, disk, MCP calls, and delivered tokens. | Missing | Separate local Memory Recall receipts exist; competitor/file measurements are absent. | Capture all metrics under identical limits and hardware, including warm/cold behavior and confidence intervals. | +| H-4 | Meet floors: symbol recall at least 95%, resolved-call precision at least 90%, zero duplicate symbols, determinism, explicit unsupported states, and real-repository proof. | Incomplete | Sampled Memory Recall rows meet floors where measured; 78 applicable rows and competitor runs are missing. | Close all applicability rows and execute the full comparison. | +| H-5 | Claim parity or leadership only from proven results. | Proven | Current receipts explicitly set parity and leadership false. | Keep claims false until H-1 through H-4 pass. | + +## Phase status + +| Phase | Requirement | Status | Evidence and remaining boundary | +| --- | --- | --- | --- | +| 0 | Matrix, ADR, schema, baseline. | Proven | ADR 0023, provider-neutral schemas, corpus, gates, and six-command baseline pass. | +| 1 | Unify Node and Rust. | Incomplete | Production reads now use the packaged Rust SQLite engine and the duplicate JS intelligence path is removed; remaining closure is five-platform distribution proof. | +| 2 | Pass fourteen Tier 1 languages. | Incomplete | 75/154 rows meet floor; 78 applicable rows remain unmeasured and one row is not applicable. All fourteen language-level statuses remain unmeasured. | +| 3 | Scalable index, watcher, and recovery. | Incomplete | Core SQLite lifecycle and the local one-million-node gate pass; cross-platform packaged migration/recovery remains unproven. | +| 4 | Search, communities, processes, routes, impact, query, and MCP. | Incomplete | The deterministic fixture and exact pinned Express, Nest cats-sample, and Gin slices pass, including evidence-backed relationship traversal, community/process pagination, all required query families, delivery estimates, and deadlines. The receipt explicitly keeps `phase4IntelligenceProven: false`; reviewed correctness, representative language breadth, large repositories, exact token budgets, cancellation, and full pagination remain missing. | +| 5 | Multi-repository and cross-service. | Incomplete | One Go cross-repo path and one monorepo fixture are insufficient. | +| 6 | Signed npm distribution. | Incomplete | The local producer/consumer chain is native-first, transports one exact root tarball, and verifies signed GitHub provenance, file-complete SPDX predicates, npm integrity, and root plus native npm attestation bundles. Remote five-platform proof and publication remain absent. | +| 7 | Polyglot large-repository UI. | Incomplete | The required release UI behavior is proven locally against a 1,225-file repository and every public route. Broader polyglot and packaged cross-platform UI characterization remains outside this release's deferred language scope. | +| 8 | Head-to-head benchmark and audits. | Missing | Only a fake-competitor one-fixture harness test exists; no full comparison receipt exists. | +| 9 | Promote extra languages and formats. | Missing | Correctly deferred; no promotion gate has been run. | + +## Final stable-release audit + +| ID | Requirement | Status | Authoritative evidence | Required closure | +| --- | --- | --- | --- | --- | +| R-1 | Full Node CI on frozen implementation. | Proven | `npm run ci` passes after `82b10e0`: repository checks, 208 protocol compatibility assertions, the Node suite, and evaluations. | Re-run only if the frozen commit changes. | +| R-2 | Full Rust workspace on frozen implementation. | Proven | `cargo test --locked --workspace` passes on the frozen implementation before the browser-audit-only `82b10e0` commit; no Rust source or Rust test changed afterward. | Re-run only if Rust files change. | +| R-3 | All five platform package jobs. | Missing | Workflow matrix exists; only darwin-arm64 is locally proven. | Run the GitHub `Rust` workflow `native-artifacts` matrix at the exact frozen commit and retain all receipts. | +| R-4 | Security and CodeQL. | Incomplete | CodeQL workflow exists; no final frozen-commit result is recorded here. | Run CodeQL and security audit at the frozen commit and bind run URLs/SHAs to release evidence. | +| R-5 | Packed clean installs. | Incomplete | The frozen Darwin-arm64 root-plus-native smoke proves a compiler-free exact-tarball install, the native default, all twelve MCP tools, uninstall, and same-version reinstall. The other four targets and the published registry package remain unproven. | Pass the five remote registry-consumer jobs from clean HOME without Cargo/rustc. | +| R-6 | Complete browser suite. | Proven | `npm run consumer:browser-smoke` passes at `82b10e0`, including setup, all public routes plus aliases, direct and reloaded Map, responsive overflow, control names/focus, reduced motion, console/page errors, and the large-repository proof remains green at `c02e986`. | Re-run only if a browser-facing file changes. | +| R-7 | Release readiness and handoff verification. | Proven | `npm run release:readiness`, `npm run release:readiness:check`, and `npm run verify:handoff` pass on the frozen local release inputs. The final remote artifact provenance is separately required by D-6 through D-9. | Re-run only if release inputs change. | +| R-8 | Do not push, merge, publish, or deploy without separate authorization. | Proven | All current work is local on `codex/memory-recall-orientation-workbench`; no remote mutation was performed in this goal continuation. | Report exact branch, commit, GitHub action, and publication order when local work is ready. | + +## Fastest dependency order + +1. Keep the local root-plus-five-native artifact chain guarded; defer its remote five-platform run until the implementation is frozen. +2. Close the 78 applicable language rows without weakening gates. +3. Finish reviewed Phase 4 correctness on real repositories, then general Phase 5. +4. Prove the clean packaged browser path, including native recovery states. +5. Complete the large-repository browser and anti-slop gates. +6. Run the real identical-corpus comparison. +7. Freeze implementation, re-run this semantic-version audit against the exact tarball, choose the stable version, obtain five platform receipts, run final audits, and only then request publication authorization. diff --git a/docs/implementation/MEMORY_RECALL_SEMVER_COMPATIBILITY_AUDIT.md b/docs/implementation/MEMORY_RECALL_SEMVER_COMPATIBILITY_AUDIT.md new file mode 100644 index 00000000..850bdf30 --- /dev/null +++ b/docs/implementation/MEMORY_RECALL_SEMVER_COMPATIBILITY_AUDIT.md @@ -0,0 +1,116 @@ +# Memory Recall semantic-version compatibility audit + +Audit date: 2026-07-19 + +Published baseline: `memory-recall@1.1.0` at commit `06a947fb52b5c23544a6e1b18ed8e28bd0735fb2` + +Audited worktree: `codex/memory-recall-orientation-workbench` through `fd153b2`, before the release-version commit + +## Decision + +The current worktree requires the **2.0.0 major stable line**. A patch or minor +release is rejected because every breaking boundary below remains intentional +and no compatibility layer restores it. Version selection is therefore +complete; publication remains blocked until the frozen tarball and final release +gates pass. + +The major-line conclusion comes from observed public contract breaks, not the +size of the change. + +The audited work includes native-first distribution, persistent Rust indexing, +the twelve-tool MCP surface, and the public Map workbench. None restore or +dual-serve the breaking boundaries recorded below, so `2.0.0` is the only +defensible stable line. + +## Evidence snapshot + +The public package was inspected directly: + +```bash +npm view memory-recall@1.1.0 name version dist.integrity dist.shasum dist.tarball --json +npm pack memory-recall@1.1.0 --pack-destination --json +``` + +The registry and packed artifact agreed on: + +| Field | Published value | +| --- | --- | +| Package | `memory-recall@1.1.0` | +| Tarball | `https://registry.npmjs.org/memory-recall/-/memory-recall-1.1.0.tgz` | +| Integrity | `sha512-jBrawYpI+wluYrTwSRSPiwa4m+Cm+JQS4pkpSIbsH1ALC5POd2X5RqfBPhSMd6+zi60iGImq72AMP1Ftx0/vzA==` | +| SHA-1 | `e343012deaa10520ce2064eea0554c4a0f8b55a6` | +| Packed size | 1,209,674 bytes | +| Unpacked size | 5,215,304 bytes | +| Entries | 828 | + +The published tarball contains `rust/Cargo.toml`, the `rust/oaf-*` sources, +`apps/cli/oaf.mjs`, and `docs/usage/rust-acceleration.md`. A current +`npm pack . --dry-run --json --ignore-scripts` check reported no `rust/` or +`native-packages/` entries in the root package. It did include the native binary +resolver. This proves the distribution-contract comparison without relying on +the source tree alone. + +## Compatibility findings + +| Public boundary | `1.1.0` contract | Current behavior | Result | Authoritative evidence | +| --- | --- | --- | --- | --- | +| Root npm distribution and Rust build path | The published package says Rust acceleration is opt-in, includes Rust source, and gives an exact local Cargo build path. | The root package excludes Rust source and resolves a binary through five platform-specific optional packages. | **Breaking**. Existing consumers lose the shipped, documented source-build contract and move to a different artifact topology. | Baseline `06a947fb52b5:docs/usage/rust-acceleration.md:3-11,29-46`; current `docs/usage/rust-acceleration.md:34-51`; `package.json:121,148-160`; registry and tarball inspection above. | +| Installed MCP configuration | `1.1.0` writes `mcp server --read-only --root ... --sqlite ... --stats ... --stdio`. | The owned signature now requires `--engine auto`. The old exact entry is classified as drifted; install refuses to upgrade it and uninstall refuses to remove it. | **Breaking**. A configuration written by the published release cannot follow the current managed upgrade or uninstall path. | Baseline `06a947fb52b5:apps/cli/oaf.mjs:7537-7561`; current `apps/cli/oaf.mjs:8786-8787,8836-8842,8901-8925,8987-8997,9016-9020`; `tests/cli.test.mjs:1769-1807,1877-1894,1915-1937`. | +| Recall Map wire format | The strict `1.0.0` schema and `memory-recall-map-1.1.0` report ID reject unknown architecture properties. | The same schema and report identifiers remain, but reports always emit `snapshot`, `groups`, `groupRelations`, and `processes`; the current schema was expanded in place. | **Breaking**. A strict consumer using the published schema rejects current output even though the version identifiers did not change. | Baseline `06a947fb52b5:packages/protocol/schemas/recall-map.schema.json:3,7-11,137-147`; current `packages/protocol/schemas/recall-map.schema.json:3,10-11,221-230`; `packages/recall-map/src/index.mjs:292-305,326-337`. | +| Default graph and MCP engine | The published public path defaults to the bounded JS/TS graph; Rust is explicit and locally built. | Graph and MCP reads use only the packaged native SQLite index; `auto` and `native-preview` are native aliases, and the duplicate JavaScript path is deleted. | **Breaking semantic default**. Identical commands now require a healthy native index and cannot select the former JavaScript implementation. | Baseline `06a947fb52b5:docs/usage/rust-acceleration.md:3-12`; current `docs/usage/rust-acceleration.md:3-12`; `apps/cli/oaf.mjs`; `tests/cli-graph-index.test.mjs`; `tests/mcp-code-intelligence.test.mjs`. | +| Shipped Rust source API and workspace | The published tarball exposes buildable Rust sources with `IngestOptions` and `IngestReport` shapes and workspace version `0.1.0`. | Public structs gain required fields such as `prefer_cpp_headers`, `recovered_files`, and `code_facts`; the workspace adds `oaf-index` and reports version `2.0.0`. | **Breaking source contract**. Existing Rust construction or deserialization against the shipped source may fail, and the workspace version change does not preserve the prior crate-level contract. | Baseline `06a947fb52b5:rust/oaf-ingest/src/lib.rs:20-38,60-78`; `06a947fb52b5:rust/Cargo.toml:1-8`; current `rust/oaf-ingest/src/lib.rs:20-40,239-260`; `rust/Cargo.toml:1-8`. | + +## Additive work + +The worktree also adds compatible capabilities: + +- the read-only MCP inventory expands from five tools to twelve; +- graph index and repository commands are added; +- managed MCP uninstall is added; +- isolated persistent SQLite source indexes are added; +- five native platform packages are defined for macOS, Linux, and Windows. + +These additions do not neutralize the breaks above. SemVer classification uses +the least-compatible public change in the release. + +Current evidence for the additive MCP surface is +`tests/mcp-code-intelligence.test.mjs:7-23,48-99`. The published five-tool +inventory is fixed by +`06a947fb52b5:tests/cli.test.mjs:1694-1710,2033`. + +## Repairs required before a minor line is defensible + +A minor line is possible only if all of these conditions are met: + +1. **Migrate legacy MCP entries.** Recognize the exact `1.1.0` server argument + signature as Memory Recall-owned. Install must offer a safe, confirmed + migration to the current signature, and uninstall must remove either owned + generation without touching neighboring configuration. +2. **Version or dual-serve wire output.** Keep a strict `1.1.0` Recall Map mode + that validates against the published schema, or introduce a new versioned + schema/report contract and negotiate it explicitly. Do not emit new fields + under the old identifiers to strict clients. +3. **Preserve or formally replace the Rust-source contract.** Continue shipping + the documented buildable source surface with compatible public structs, or + provide an explicit compatibility artifact and migration that preserves the + published local-build use case. +4. **Preserve or negotiate the engine default.** Keep JS as the behavior for + unversioned legacy invocations, or require an explicit/versioned negotiation + before `auto` can change the engine and output semantics. +5. **Bind each repair to the published artifact.** Add focused tests using the + exact `memory-recall@1.1.0` configuration, strict schema, command defaults, + and Rust source surface. The tests must prove upgrade, operation, uninstall, + and rollback against the packed current product. + +If any condition remains unresolved at release freeze, the stable release must +remain on a major line. Adding release notes or documenting the break does not +make a patch or minor release backward compatible. + +## Release stop condition + +`2.0.0` is selected for the release line. Re-run this audit against the frozen +release commit and exact root tarball before publication. The release can proceed +only when every row is either: + +- backward compatible and proven against `memory-recall@1.1.0`; or +- intentionally breaking and assigned to a major stable line. diff --git a/docs/implementation/MEMORY_RECALL_UI_ANTI_SLOP_REVIEW.md b/docs/implementation/MEMORY_RECALL_UI_ANTI_SLOP_REVIEW.md new file mode 100644 index 00000000..71b04042 --- /dev/null +++ b/docs/implementation/MEMORY_RECALL_UI_ANTI_SLOP_REVIEW.md @@ -0,0 +1,46 @@ +# Memory Recall UI anti-slop review + +Review date: 2026-07-19 +Reviewed commit: `c02e986` + +This review covers the shipped local workbench rather than a marketing surface. Its standard is practical orientation: a developer should see repository truth, a next action, and bounded evidence before any graph density or promotional framing. + +## Evidence inspected + +| Surface | Evidence | Result | +| --- | --- | --- | +| Overview, desktop | Clean 1,225-tracked-file clone in `scripts/large-repository-browser-smoke.mjs` | Six of twelve groups fit in the first viewport; the full bounded outline is explicitly disclosed. | +| Overview, mobile | `consumer-browser-smoke.mjs` responsive widths and bottom navigation checks | No horizontal overflow; controls retain 44px targets. | +| Focused Map, desktop and mobile | Clean-clone screenshots and keyboard/overflow assertions | Direct `/map?group=apps%2Fweb` loads, graph labels stay bounded, outline locators use ellipsis, and the last mobile outline item clears fixed navigation. | +| Memory and handoff | Browser screenshots and route/control audit | Forms and governed state remain factual, readable, and reachable without faux product framing. | +| Every published route | `consumer-browser-smoke.mjs` route/control audit | Each route has one main landmark, one page heading, named keyboard-reachable visible controls, no console/page errors, and no horizontal overflow. | + +## Composition and language + +- The first screen uses a compact title, factual coverage, architecture, ranked starts, current impact, and trusted context. It does not use a marketing hero, slogan, testimonial, pricing block, decorative dashboard metric strip, or an always-on graph canvas. +- The Map starts with a direct query and a bounded result. Its graph is a drill-down, not the first-screen visual. Complete bounded records remain available in the outline rather than as overlapping canvas labels. +- Copy is descriptive and local: `Read-only / bounded metadata`, coverage, freshness, omitted counts, and explicit recovery actions. It makes no AI-performance, parity, cloud, or automation claim. +- The primary navigation is five clear developer tasks on desktop and four fixed tasks on mobile. Active state is conveyed by text weight and restrained color, not a decorative dot or animated underline. + +## Visual system and motion + +- The workbench uses flat, warm-neutral surfaces, native/system typography, restrained blue for selection, and directional inset selection marks. It has no blue-purple gradients, atmospheric blobs, glows, faux browser windows, giant logos, or default fill-and-outline CTA pairs in the reviewed flows. +- Borders separate live controls and data records. They do not create a decorative card grid on the Overview. Status chips are retained only where they express actual memory or handoff state. +- Content is visible before script-driven interaction. The UI does not depend on entrance animation; reduced-motion checks bound any transition or animation duration. +- Graph canvases use progressive labels, an accessible outline, and a selection inspector. The review specifically rejects the prior dense all-label graph treatment. + +## Responsive, accessibility, and overflow review + +- Desktop and mobile screenshots show no clipped headings, control collisions, unreadable outline labels, or bottom-navigation obstruction in the reviewed Overview and Map states. +- Long locators and labels use overflow-safe ellipsis in the architecture cards and source-map outline. The graph does not rely on text placed beyond the canvas edge. +- Every reviewed interactive control has an accessible name and keyboard reachability. The source-map outline updates the canonical selection with Enter. + +## Fixes confirmed by this review + +1. Architecture cards no longer stack every same-layer group into one tall column. The first screen shows six cards, with an explicit disclosure for the remaining groups. +2. A large-repository Map cannot return a schema-validation 500 from unmapped hotspot references or one-edge process records. Unsupported projection fragments are omitted rather than misrepresented. +3. The Map no longer requires an extra click after a direct deep link, and its desktop/mobile outline remains readable at bounded density. + +## Decision + +No decorative redesign is warranted. The current workbench is intentionally compact, factual, and local-first; adding hero art, branding effects, a broader dashboard, extra cards, or cosmetic motion would reduce clarity without helping a developer complete a task. diff --git a/docs/implementation/OAF-031-context-intake-preview-note.md b/docs/implementation/OAF-031-context-intake-preview-note.md index f62b159e..071b139d 100644 --- a/docs/implementation/OAF-031-context-intake-preview-note.md +++ b/docs/implementation/OAF-031-context-intake-preview-note.md @@ -64,20 +64,13 @@ slice. - `npm run recall -- benchmark truth-floor --suite benchmark-truth-floor --dataset evals/benchmark-truth-floor/cases.v1.json --format json` exposes the schema-validated gold-evidence truth floor for native exact, full-context, lexical, and current-harness baselines. -- `providers/native/context-candidate-ast-code/` exposes a dependency-free - JS/TS static source index with definitions, references, imports, exports, - callers, callees, file outlines, repository outlines, exact slice hashes, and - content-hash journals. -- The same native provider now exposes a read-only derived source graph built - from that JS/TS source index, with schema-validated file/chunk/symbol/module - nodes, contains/defined/import/export/reference/call edges, lexical graph - search, call tracing, changed-file impact reports, and locator-only graph - candidate records for Context Compiler selection. The parser uses a - linear string/comment stripper, 256 KiB default static JS/TS file coverage, - a 1 MiB hard validation ceiling, and bounded reference/call fanout so large - template-heavy project files cannot spin the CLI, API, or browser preview. - Over-limit files remain hash/read-plan-only and keep changed-file coverage in - review instead of claiming symbol impact. +- The packaged Rust engine owns source parsing, the persistent SQLite index, + definitions, relationships, search, trace, impact, communities, and bounded + entry-to-sink processes for the fourteen shipped parsers. +- Context packs consume only the native index's locator-safe projection. Missing, + stale, corrupt, or unavailable indexes stay explicit and never trigger a + duplicate JavaScript scanner. Over-limit or unsupported evidence remains + partial and keeps changed-file coverage in review. - Durable context manifests now record explicit assembly representation tiers, token budget reports, manifest `etag`s, and `deltaFrom` summaries for repeated local runs. The token report separates selected-token ratio diff --git a/docs/open-source/release-engineering.md b/docs/open-source/release-engineering.md index 2c04e48e..a3cebfb0 100644 --- a/docs/open-source/release-engineering.md +++ b/docs/open-source/release-engineering.md @@ -22,18 +22,42 @@ external writes. `.github/workflows/npm-publish.yml` is the maintainer publication lane for the public npm package. It is manual-only (`workflow_dispatch`), requires the exact -`publish memory-recall@VERSION` confirmation text, runs CI, native smoke, -consumer smoke, release-readiness verification, and an npm publish dry run before -the real publish step, and uses the protected `npm-release` environment. +`publish memory-recall@VERSION` confirmation text and the run ID of a successful +`Rust` workflow for the same commit. The Rust run must produce all five native +packages, deterministic file-complete SPDX 2.3 SBOMs, signed GitHub provenance +for every tarball and receipt, and one exact +aggregate artifact. The protected `npm-release` job authenticates that run and +the signer workflow, verifies the complete release set, checksums, and signed +SBOM predicates, then +publishes the five native packages in fixed order. It verifies every native +version, npm integrity, and exact publish/SLSA attestation bundle before it can +dry-run or publish the root package. + +The validation job runs CI, native smoke, consumer smoke, and release-readiness +verification, then packs the root package exactly once. It records the tarball's +SHA-256 and SHA-512 integrity, generates a file-complete SPDX 2.3 document, signs +both provenance and SBOM attestations, and uploads the exact artifact. The +protected job downloads and validates those bytes, verifies both GitHub +attestations, installs the exact root tarball with its matching native tarball in +an isolated lifecycle, and publishes that same tarball only after all five +native registry records pass. It then requires exact root registry integrity and +both npm publish and SLSA attestations. No later bare `npm publish` repack is +permitted. Both jobs use Node 22.14.0 and npm 11.18.0 so validation, JSON +attestation verification, and publication do not drift with `npm@latest`. Prefer npm Trusted Publishing. Configure npm with GitHub Actions as the trusted publisher for repository `rebel0789/Memory-Recall` and workflow filename `npm-publish.yml`, then run the workflow with `auth_mode=trusted-publishing`. -This uses OIDC and does not require a long-lived npm token. If the maintainer -chooses token auth for the first release, set the repository secret `NPM_TOKEN` -and run the same workflow with `auth_mode=npm-token`. - -The workflow does not create tags, sign artifacts, submit marketplace manifests, -or publish from pull requests. Marketplace or plugin-registry submission remains -blocked until target registry requirements are known and a maintainer approves -the submission. +This uses OIDC and does not require a long-lived npm token. Each of the six npm +packages must authorize `npm-publish.yml` and the `npm-release` environment as +its trusted publisher. If the maintainer chooses token auth for the first +release, set the repository secret `NPM_TOKEN` and run the same workflow with +`auth_mode=npm-token`; the workflow requests npm provenance in token mode. + +The workflows do not create tags, submit marketplace manifests, publish from +pull requests, or publish without the protected environment. GitHub provenance +attestation, root and native SBOM binding, exact-artifact lifecycle proof, and npm +provenance verification are implemented locally, but the remote five-runner +result and stable publication remain unproven. +Marketplace or plugin-registry submission remains blocked until target registry +requirements are known and a maintainer approves the submission. diff --git a/docs/product/memory-recall-developer-first.md b/docs/product/memory-recall-developer-first.md index 75be5b63..f99eb060 100644 --- a/docs/product/memory-recall-developer-first.md +++ b/docs/product/memory-recall-developer-first.md @@ -16,17 +16,23 @@ recall handoff ``` `recall setup` initializes local Recall state without scanning the repository. -`recall map` is the explicit read-only first scan: it combines the implemented -JS/TS static graph with governed local-memory status. `recall handoff` produces -a read-only handoff for the next coding-agent session. +`recall map` is the explicit read-only first scan: it combines bounded source +metadata with governed local-memory status. Structural graph, MCP, API, and web +reads use the packaged Rust index. `recall handoff` produces a read-only handoff +for the next coding-agent session. ## Support contract -- Implemented: local JS/TS static graph, reviewed SQLite memory, read-only MCP. +- Implemented: native-default, freshness-gated graph reads, reviewed SQLite + memory, and twelve read-only MCP tools. Missing or stale native state fails + closed with the exact recovery action; JS/TS requires explicit compatibility + mode. - Implemented: bounded semantic plan and task packets, strict result import, pending proposals, and source-rechecked named approval. -- Experimental: Rust acceleration paths require a local build before explicit invocation. +- Experimental: the verified packaged Rust path has compiler-free local evidence across 14 Tier 1 fixtures, while full language and cross-platform release gates remain open. The bounded cross-repository path currently covers exact Go module resolution, one entry-to-service trace, and reverse impact across two explicitly registered repositories through the existing MCP tools. - Experimental: explicit one-shot Gemini and OpenAI-compatible semantic API execution. -- Unsupported: automatic transcript capture, write-capable MCP, hosted sync, and non-JS/TS source graph analysis. +- Unsupported: automatic transcript capture, write-capable MCP, hosted sync, + general cross-repository analysis beyond the measured exact Go path, and + unmeasured Tier 1 capability rows. - Unsupported: automatic harness invocation, arbitrary semantic providers, semantic retrieval, raw source-code upload, background semantic sync, and automatic semantic memory activation. diff --git a/docs/release/1.0-COMPATIBILITY-MATRIX.md b/docs/release/1.0-COMPATIBILITY-MATRIX.md index 86a67fc7..50fcfce7 100644 --- a/docs/release/1.0-COMPATIBILITY-MATRIX.md +++ b/docs/release/1.0-COMPATIBILITY-MATRIX.md @@ -17,9 +17,8 @@ | Provider | Contract | Locality | Enabled | | --- | --- | --- | --- | | provider:native:artifact:filesystem | ArtifactStorePort | in-process | true | -| provider:native:context-candidate:ast-code | CandidateSourcePort | in-process | true | +| provider:native:code-intelligence:rust | CodeIntelligencePort | loopback-process | true | | provider:native:context-candidate:exact | CandidateSourcePort | in-process | true | -| provider:native:context-candidate:graph | CandidateSourcePort | in-process | true | | provider:native:context-candidate:lexical | CandidateSourcePort | in-process | true | | provider:native:context-manifest:local | ContextManifestRepositoryPort@1.0.0 | in-process | true | | provider:native:identity:local | IdentityStorePort | in-process | true | @@ -54,7 +53,12 @@ External adapters enabled by default: 0. | Package | Version | License | Runtime deps | | --- | --- | --- | --- | -| memory-recall | 1.1.0 | Apache-2.0 | 0 | +| @memory-recall/native-darwin-arm64 | 2.0.0 | Apache-2.0 | 0 | +| @memory-recall/native-darwin-x64 | 2.0.0 | Apache-2.0 | 0 | +| @memory-recall/native-linux-arm64-gnu | 2.0.0 | Apache-2.0 | 0 | +| @memory-recall/native-linux-x64-gnu | 2.0.0 | Apache-2.0 | 0 | +| @memory-recall/native-win32-x64 | 2.0.0 | Apache-2.0 | 0 | +| memory-recall | 2.0.0 | Apache-2.0 | 0 | | @open-agent-fabric/adapter-contracts | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/agentpack | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/content-intelligence | 0.2.0-dev | Apache-2.0 | 0 | @@ -79,7 +83,6 @@ External adapters enabled by default: 0. | @open-agent-fabric/ui | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/workflow-runtime | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/native-artifact-filesystem | 0.2.0-dev | Apache-2.0 | 0 | -| @open-agent-fabric/native-context-candidate-ast-code | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/native-context-candidate-exact | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/native-context-candidate-lexical | 0.2.0-dev | Apache-2.0 | 0 | | @open-agent-fabric/provider-native-context-manifest-local | 0.2.0-dev | Apache-2.0 | 0 | diff --git a/docs/release/1.0-MARKETPLACE-MANIFEST.json b/docs/release/1.0-MARKETPLACE-MANIFEST.json index 74cbb7be..8f059113 100644 --- a/docs/release/1.0-MARKETPLACE-MANIFEST.json +++ b/docs/release/1.0-MARKETPLACE-MANIFEST.json @@ -6,9 +6,9 @@ "package": { "registry": "npm", "name": "memory-recall", - "version": "1.1.0", + "version": "2.0.0", "install": "npm install -g memory-recall", - "url": "https://www.npmjs.com/package/memory-recall/v/1.1.0", + "url": "https://www.npmjs.com/package/memory-recall/v/2.0.0", "bins": [ "recall", "oaf" diff --git a/docs/release/1.0-PROVENANCE.json b/docs/release/1.0-PROVENANCE.json index 9a7c6bda..cb834c0d 100644 --- a/docs/release/1.0-PROVENANCE.json +++ b/docs/release/1.0-PROVENANCE.json @@ -3,7 +3,7 @@ "predicateType": "https://github.com/rebel0789/Memory-Recall/provenance/release-readiness/v1", "subject": { "name": "memory-recall", - "version": "1.1.0", + "version": "2.0.0", "releaseCandidate": "1.0-readiness" }, "buildType": "local-deterministic-source-readiness", @@ -21,13 +21,13 @@ "mergePerformedByThisTask": false }, "sourceHashes": { - "package.json": "sha256:0b4ad199b2c9bcb0d4832f3da1747633cee59deb65ec94d3a33f83292bef5dff", - "package-lock.json": "sha256:66bac759b6c28f95c29eca52c0b42e9bb24237a4a926fb67a42093f9ee425ed7", - "PROJECT_STATUS.json": "sha256:66a4cea825b1893e0f234f0f8ae71906dae322756cdde7cccfb967a4d44e2a83", + "package.json": "sha256:e5ff8b5bee26f0403a31c2c15b119f4e9b9952db895ccfec9c1da2afd089f4c3", + "package-lock.json": "sha256:5a80cd7fc840b4df82d873ca398a68fef89d05c4f2194c2156b41c44aa6d579e", + "PROJECT_STATUS.json": "sha256:bf712102fafbb1634cc99e94897d95ce689481357f41ee8a32b4fad1fb14f4b1", "planning/backlog.json": "sha256:a404e5704ba3bf3be635a655d2d73d84eff7eba3ce43ab511b83967cceab6a5f", "adapters/catalog.json": "sha256:11c22a150f847a560be3e64c8280c7757368d813b5f70304938ab7915694c283", - "docs/usage/support-matrix.md": "sha256:dd4108828b742e8d06112e6abdfa4119f90a8b034b417b9e7e41f6924dcf26a6", - "docs/benchmarks.md": "sha256:d486819287f1f97c1f51efded6dd7bf6c8491dc8e7a135c9cfaba9ed8f14b212", + "docs/usage/support-matrix.md": "sha256:73844778b418c382828bc0877c554ac1592024e078fc2b65ef6a02670d7ac80e", + "docs/benchmarks.md": "sha256:975701efde58e54b64213b6904758f562118f29111eae4d77d90ae46dd5ad068", "THIRD_PARTY.md": "sha256:adbbffba498346ac25ff8f064db76ae93543d3c1990d37f1137d9122f0742dd7", "SECURITY.md": "sha256:af8c910a7f2092f3adc8a9be30f79618d040101c6eda0fda705fdd8f9327b472", "GOVERNANCE.md": "sha256:479e1fe15e56b58bd33b41cab67244de87b6f62b491bea0761337a5b87cd7e52" @@ -35,11 +35,11 @@ "publicEvidence": [ { "path": "docs/usage/support-matrix.md", - "sha256": "sha256:dd4108828b742e8d06112e6abdfa4119f90a8b034b417b9e7e41f6924dcf26a6" + "sha256": "sha256:73844778b418c382828bc0877c554ac1592024e078fc2b65ef6a02670d7ac80e" }, { "path": "docs/benchmarks.md", - "sha256": "sha256:d486819287f1f97c1f51efded6dd7bf6c8491dc8e7a135c9cfaba9ed8f14b212" + "sha256": "sha256:975701efde58e54b64213b6904758f562118f29111eae4d77d90ae46dd5ad068" } ], "generatedFiles": [ diff --git a/docs/release/1.0-READINESS-REPORT.md b/docs/release/1.0-READINESS-REPORT.md index e9609b0b..3ca102af 100644 --- a/docs/release/1.0-READINESS-REPORT.md +++ b/docs/release/1.0-READINESS-REPORT.md @@ -18,14 +18,14 @@ Publication state: npm-published. This evidence is prepared for a maintainer mer | Area | Evidence | | --- | --- | -| Release | 1.1.0 | -| Phase | 1.1-release-candidate-readiness | +| Release | 2.0.0 | +| Phase | 2.0.0-major-readiness | | Network default | deny | | External writes | false | | Model mode | deterministic | -| Quality snapshot date | 2026-07-15 | -| Recorded tests | 660 | -| Recorded protocol fixtures | 182 | +| Quality snapshot date | 2026-07-18 | +| Recorded tests | 715 | +| Recorded protocol fixtures | 208 | | Recorded evaluation assertions | 144 | | Quality snapshot note | Counts are a dated snapshot; command results and HANDOFF_VERIFICATION.json are authoritative. | @@ -33,8 +33,8 @@ Publication state: npm-published. This evidence is prepared for a maintainer mer | Page | Scope | Revision fingerprint | | --- | --- | --- | -| [Support matrix](../usage/support-matrix.md) | `docs/usage/support-matrix.md`: Implemented, experimental, and unsupported client paths with reversal boundaries. | `sha256:dd4108828b742e8d06112e6abdfa4119f90a8b034b417b9e7e41f6924dcf26a6` | -| [Benchmark proof](../benchmarks.md) | `docs/benchmarks.md`: Public local claims, datasets, baselines, commands, artifacts, and limitations. | `sha256:d486819287f1f97c1f51efded6dd7bf6c8491dc8e7a135c9cfaba9ed8f14b212` | +| [Support matrix](../usage/support-matrix.md) | `docs/usage/support-matrix.md`: Implemented, experimental, and unsupported client paths with reversal boundaries. | `sha256:73844778b418c382828bc0877c554ac1592024e078fc2b65ef6a02670d7ac80e` | +| [Benchmark proof](../benchmarks.md) | `docs/benchmarks.md`: Public local claims, datasets, baselines, commands, artifacts, and limitations. | `sha256:975701efde58e54b64213b6904758f562118f29111eae4d77d90ae46dd5ad068` | The support matrix is the public contract for install and reversal paths. The benchmark page is the public source of truth for local measurement claims and diff --git a/docs/release/1.0-REPRODUCIBILITY.md b/docs/release/1.0-REPRODUCIBILITY.md index ffa2a422..4b455cd8 100644 --- a/docs/release/1.0-REPRODUCIBILITY.md +++ b/docs/release/1.0-REPRODUCIBILITY.md @@ -24,16 +24,16 @@ Counts are a dated snapshot; command results and HANDOFF_VERIFICATION.json are a | Gate | Expected Count | | --- | --- | -| Tests | 660 | -| Protocol fixtures | 182 | +| Tests | 715 | +| Protocol fixtures | 208 | | Evaluation assertions | 144 | ## Public Claim Evidence | Page | Scope | SHA-256 | | --- | --- | --- | -| `docs/usage/support-matrix.md` | Implemented, experimental, and unsupported client paths with reversal boundaries. | `sha256:dd4108828b742e8d06112e6abdfa4119f90a8b034b417b9e7e41f6924dcf26a6` | -| `docs/benchmarks.md` | Public local claims, datasets, baselines, commands, artifacts, and limitations. | `sha256:d486819287f1f97c1f51efded6dd7bf6c8491dc8e7a135c9cfaba9ed8f14b212` | +| `docs/usage/support-matrix.md` | Implemented, experimental, and unsupported client paths with reversal boundaries. | `sha256:73844778b418c382828bc0877c554ac1592024e078fc2b65ef6a02670d7ac80e` | +| `docs/benchmarks.md` | Public local claims, datasets, baselines, commands, artifacts, and limitations. | `sha256:975701efde58e54b64213b6904758f562118f29111eae4d77d90ae46dd5ad068` | Regenerate these release artifacts after changing either public page so support boundaries and benchmark limitations remain tied to the same reviewed source. diff --git a/docs/release/1.0-SBOM.json b/docs/release/1.0-SBOM.json index 80200d08..c8e734a8 100644 --- a/docs/release/1.0-SBOM.json +++ b/docs/release/1.0-SBOM.json @@ -3,13 +3,58 @@ "format": "memory-recall-release-readiness-sbom", "releaseCandidate": "1.0-readiness", "project": "memory-recall", - "packageVersion": "1.1.0", + "packageVersion": "2.0.0", "dependencyPolicy": "bootstrap dependency-free; optional integrations remain disabled adapters", "packages": [ + { + "path": "native-packages/darwin-arm64/package.json", + "name": "@memory-recall/native-darwin-arm64", + "version": "2.0.0", + "license": "Apache-2.0", + "private": false, + "dependencyCount": 0, + "devDependencyCount": 0 + }, + { + "path": "native-packages/darwin-x64/package.json", + "name": "@memory-recall/native-darwin-x64", + "version": "2.0.0", + "license": "Apache-2.0", + "private": false, + "dependencyCount": 0, + "devDependencyCount": 0 + }, + { + "path": "native-packages/linux-arm64-gnu/package.json", + "name": "@memory-recall/native-linux-arm64-gnu", + "version": "2.0.0", + "license": "Apache-2.0", + "private": false, + "dependencyCount": 0, + "devDependencyCount": 0 + }, + { + "path": "native-packages/linux-x64-gnu/package.json", + "name": "@memory-recall/native-linux-x64-gnu", + "version": "2.0.0", + "license": "Apache-2.0", + "private": false, + "dependencyCount": 0, + "devDependencyCount": 0 + }, + { + "path": "native-packages/win32-x64/package.json", + "name": "@memory-recall/native-win32-x64", + "version": "2.0.0", + "license": "Apache-2.0", + "private": false, + "dependencyCount": 0, + "devDependencyCount": 0 + }, { "path": "package.json", "name": "memory-recall", - "version": "1.1.0", + "version": "2.0.0", "license": "Apache-2.0", "private": false, "dependencyCount": 0, @@ -231,15 +276,6 @@ "dependencyCount": 0, "devDependencyCount": 0 }, - { - "path": "providers/native/context-candidate-ast-code/package.json", - "name": "@open-agent-fabric/native-context-candidate-ast-code", - "version": "0.2.0-dev", - "license": "Apache-2.0", - "private": true, - "dependencyCount": 0, - "devDependencyCount": 0 - }, { "path": "providers/native/context-candidate-exact/package.json", "name": "@open-agent-fabric/native-context-candidate-exact", @@ -358,11 +394,11 @@ "contract": "ArtifactStorePort" }, { - "id": "provider:native:context-candidate:ast-code", - "category": "context-candidate", - "locality": "in-process", + "id": "provider:native:code-intelligence:rust", + "category": "code-intelligence", + "locality": "loopback-process", "enabledByDefault": true, - "contract": "CandidateSourcePort" + "contract": "CodeIntelligencePort" }, { "id": "provider:native:context-candidate:exact", @@ -371,13 +407,6 @@ "enabledByDefault": true, "contract": "CandidateSourcePort" }, - { - "id": "provider:native:context-candidate:graph", - "category": "context-candidate", - "locality": "in-process", - "enabledByDefault": true, - "contract": "CandidateSourcePort" - }, { "id": "provider:native:context-candidate:lexical", "category": "context-candidate", diff --git a/docs/superpowers/plans/2026-07-15-memory-recall-large-repository-graph-foundation.md b/docs/superpowers/plans/2026-07-15-memory-recall-large-repository-graph-foundation.md new file mode 100644 index 00000000..de93e6b1 --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-memory-recall-large-repository-graph-foundation.md @@ -0,0 +1,1061 @@ +# Memory Recall Large-Repository Graph Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make Recall Map and Map return truthful, bounded, reusable source-graph results for large JavaScript and TypeScript repositories. + +**Architecture:** Keep scanning and graph construction in the native AST provider, but split ignore matching into a focused module and put graph reuse behind an injected snapshot service. Bound nodes and edges before protocol validation, expose coverage and cache truth additively in the v1 schemas, and inject one snapshot service into the local Control API so Overview and Map share the same graph. + +**Tech Stack:** Node.js 22 ESM, dependency-free JavaScript, JSON Schema, Node test runner, loopback Control API. + +## Global Constraints + +- Preserve local-only, read-only behavior; no network calls, model calls, external writes, or raw source bodies. +- Add no runtime dependency, graph database, persistent index, frontend framework, or graph library. +- Support JavaScript and TypeScript only: `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, and `.tsx`. +- Keep `maxFiles` at 1,000 supported files; excluded and unsupported files do not consume that budget. +- Keep the public graph within 20,000 nodes and 50,000 edges; exceeding candidates produce partial coverage instead of an unavailable graph. +- Preserve structural edges before low-signal references in this order: `contains`, `defined_in`, `imports`, `exports`, `calls`, `references`. +- Accept ordinary repository-relative directories named `users`, `Users`, or `private`; continue rejecting absolute paths, traversal, URI schemes, and encoded traversal. +- Honor root and descendant `.gitignore` files plus root `.recallignore`; explicit includes may override non-security exclusions only. +- Repeated Overview and Map requests against an unchanged root must reuse one in-process snapshot and one in-flight build. +- Protocol changes are additive within v1 and require RFC, OpenAPI, examples, and compatibility-fixture updates. +- Preserve the last valid snapshot when a refresh fails; surface it as stale rather than reporting a false empty success. + +--- + +## File Map + +### Create + +- `providers/native/context-candidate-ast-code/src/ignore-rules.mjs`: parse and match dependency-free gitignore-style rules. +- `packages/source-graph/src/orientation.mjs`: build deterministic repository groups, inter-group relations, and bounded focus neighborhoods. +- `packages/source-graph/src/snapshot-service.mjs`: own cached graph snapshots, dirty generations, watchers, in-flight deduplication, and stale fallback. +- `tests/source-graph-discovery.test.mjs`: focused discovery, ignore, and supported-file budget tests. +- `tests/source-graph-snapshot-service.test.mjs`: focused cache, invalidation, concurrency, and stale fallback tests. +- `scripts/source-graph-large-repo-smoke.mjs`: generated and optional real-repository cold/cached smoke measurement. + +### Modify + +- `providers/native/context-candidate-ast-code/src/index.mjs`: apply discovery policy before accounting and enforce graph budgets. +- `packages/protocol/src/source-graph-locator.mjs`: separate relative locator safety from display-label safety. +- `packages/protocol/schemas/source-graph.schema.json`: add bounded omission and coverage fields. +- `packages/protocol/schemas/source-graph-preview.schema.json`: add snapshot state and bounded omission fields. +- `packages/protocol/schemas/recall-map.schema.json`: expose cache state and truthful coverage on Overview. +- `packages/source-graph/src/index.mjs`: query an injected snapshot and return fresh, cached, in-flight, stale, or unavailable state. +- `packages/recall-map/src/index.mjs`: consume the injected source-graph snapshot service. +- `services/control-api/src/server.mjs`: construct and close one snapshot service per server. +- `services/control-api/src/route-contracts.mjs`: keep route response validation aligned with additive schemas. +- `tests/source-graph-preview.test.mjs`: cover safe labels, bounded graphs, and stale diagnostics. +- `tests/recall-map.test.mjs`: cover cache and coverage projection. +- `tests/control-api-boundary.test.mjs`: prove Overview and Map share one graph build through the injectable server harness. +- `examples/protocol/source-graph.json`: include partial graph coverage fields. +- `examples/protocol/source-graph-preview.json`: include snapshot state. +- `examples/protocol/recall-map.json`: include source snapshot state. +- `examples/protocol/compatibility/invalid/source-graph-local-path.json`: retain an actually absolute invalid case. +- `examples/protocol/compatibility/invalid/source-graph-preview-local-path.json`: retain an actually absolute invalid case. +- `docs/api/openapi.yaml`: document additive snapshot and coverage fields. +- `docs/usage/recall-map.md`: document ignore rules, partial coverage, cache semantics, and refresh. +- `rfcs/0001-protocol-contracts.md`: record the additive v1 source-graph fields. +- `package.json`: add the large-repository smoke command. + +--- + +### Task 1: Fix locator and display-label safety without weakening path containment + +**Files:** +- Modify: `packages/protocol/src/source-graph-locator.mjs` +- Modify: `packages/protocol/src/index.mjs` +- Modify: `packages/protocol/schemas/source-graph.schema.json` +- Modify: `packages/protocol/schemas/source-graph-preview.schema.json` +- Modify: `examples/protocol/compatibility/invalid/source-graph-local-path.json` +- Modify: `examples/protocol/compatibility/invalid/source-graph-preview-local-path.json` +- Test: `tests/source-graph-preview.test.mjs` + +**Interfaces:** +- Consumes: `normalizeSourceGraphWorkspaceLocator(value, { stripFragment })`. +- Produces: unchanged locator-normalization signature plus `isSafeSourceGraphDisplayLabel(value): boolean`. + +- [x] **Step 1: Write the failing relative-directory regression tests** + +Add these cases to `tests/source-graph-preview.test.mjs`: + +```js +// Extend the existing node:fs/promises import with `rm` and import +// normalizeSourceGraphWorkspaceLocator from packages/protocol/src/source-graph-locator.mjs. +test('source graph accepts ordinary relative users and private directories', async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-relative-users-')); + t.after(() => rm(root, { recursive: true, force: true })); + await mkdir(path.join(root, 'apps', 'api', 'users', '[userRef]'), { recursive: true }); + await mkdir(path.join(root, 'src', 'private'), { recursive: true }); + await writeFile(path.join(root, 'apps', 'api', 'users', '[userRef]', 'route.ts'), 'export function GET(){ return "ok"; }\n'); + await writeFile(path.join(root, 'src', 'private', 'state.ts'), 'export const state = "local";\n'); + + const preview = await buildSourceGraphPreview({ root, workspaceId: 'ws_local' }); + + assert.equal(preview.graph.summary.fileCount, 2); + assert.equal(preview.graph.diagnostics.some(({ code }) => code.startsWith('source_graph_unavailable')), false); + assert(preview.graph.sampleNodes.some(({ locator }) => locator?.includes('/users/'))); + assert(preview.graph.sampleNodes.some(({ locator }) => locator?.includes('/private/'))); +}); + +test('source graph still rejects absolute and encoded traversal locators', () => { + for (const locator of [ + '/Users/rebel/project/src/app.js', + 'C:\\Users\\rebel\\project\\src\\app.js', + 'workspace:///Users/rebel/project/src/app.js', + 'workspace://src/%252e%252e/secret.js', + 'https://example.com/source.js' + ]) { + assert.throws( + () => normalizeSourceGraphWorkspaceLocator(locator), + /source_graph_workspace_locator_invalid/ + ); + } +}); +``` + +- [x] **Step 2: Run the focused test and verify the current false rejection** + +Run: + +```bash +node --test --test-name-pattern="ordinary relative users|absolute and encoded" tests/source-graph-preview.test.mjs +``` + +Expected: the relative `users` or `private` test fails with an unavailable graph or invalid locator while absolute and traversal cases remain rejected. + +- [x] **Step 3: Separate locator and label rules** + +In `packages/protocol/src/source-graph-locator.mjs`, remove the blanket `Users`, `private`, and `var/folders` segment exclusions from `SOURCE_GRAPH_WORKSPACE_LOCATOR_PATTERN`. Keep the leading slash, drive, URI-scheme, traversal, percent-encoding, length, and fragment constraints. Replace the absolute-directory exclusions in `SOURCE_GRAPH_SAFE_LABEL_PATTERN` with structural checks only, then export a named predicate: + +```js +export function isSafeSourceGraphDisplayLabel(value) { + return typeof value === 'string' && SOURCE_GRAPH_SAFE_LABEL_RE.test(value); +} +``` + +The locator prefix must remain: + +```js +String.raw`^workspace://(?!/)(?![^/]*%)(?![A-Za-z][A-Za-z0-9+.-]*:)(?!\.\.(?:/|$))(?!.*\/\.\.(?:/|$))` +``` + +Update the identical schema patterns in both source-graph schemas. Change the two invalid compatibility examples so they use `workspace:///Users/rebel/project/src/app.js`; do not keep a repository-relative `workspace://Users/...` example marked invalid. + +- [x] **Step 4: Run focused protocol and source-graph verification** + +Run: + +```bash +node --test --test-name-pattern="ordinary relative users|absolute and encoded|locator" tests/source-graph-preview.test.mjs +npm run protocol:validate +``` + +Expected: focused tests pass and all protocol fixtures validate. + +- [x] **Step 5: Commit the safety correction** + +```bash +git add packages/protocol/src/source-graph-locator.mjs packages/protocol/src/index.mjs packages/protocol/schemas/source-graph.schema.json packages/protocol/schemas/source-graph-preview.schema.json examples/protocol/compatibility/invalid/source-graph-local-path.json examples/protocol/compatibility/invalid/source-graph-preview-local-path.json tests/source-graph-preview.test.mjs +git commit -m "fix: accept safe repository-relative graph paths" +``` + +### Task 2: Apply ignore rules before supported-file accounting + +**Files:** +- Create: `providers/native/context-candidate-ast-code/src/ignore-rules.mjs` +- Create: `tests/source-graph-discovery.test.mjs` +- Modify: `providers/native/context-candidate-ast-code/src/index.mjs` + +**Interfaces:** +- Produces: `parseIgnoreFile(text, { base = '' }): IgnoreRule[]`. +- Produces: `isIgnoredPath(relativePath, { isDirectory, rules, explicitIncludes }): boolean`. +- Produces: `loadRootRecallIgnore(root): Promise`. +- `scanAstCodeWorkspace()` adds optional `explicitIncludes = []` and returns bounded ignore coverage. + +- [x] **Step 1: Write failing ignore and accounting tests** + +Create `tests/source-graph-discovery.test.mjs` with fixtures that prove exclusions happen before `maxFiles`: + +```js +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { scanAstCodeWorkspace } from '../providers/native/context-candidate-ast-code/src/index.mjs'; + +test('discovery honors nested gitignore and recallignore before maxFiles', async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-discovery-')); + t.after(() => rm(root, { recursive: true, force: true })); + await mkdir(path.join(root, 'src', 'generated'), { recursive: true }); + await mkdir(path.join(root, '.worktrees', 'old', 'src'), { recursive: true }); + await mkdir(path.join(root, '.venv', 'lib'), { recursive: true }); + await writeFile(path.join(root, '.gitignore'), 'src/generated/\n'); + await writeFile(path.join(root, '.recallignore'), 'src/ignored.ts\n'); + await writeFile(path.join(root, 'src', '.gitignore'), 'nested.ts\n'); + await writeFile(path.join(root, 'src', 'entry.ts'), 'export function entry(){ return 1; }\n'); + await writeFile(path.join(root, 'src', 'nested.ts'), 'export const nested = 1;\n'); + await writeFile(path.join(root, 'src', 'ignored.ts'), 'export const ignored = 1;\n'); + await writeFile(path.join(root, 'src', 'generated', 'output.ts'), 'export const generated = 1;\n'); + await writeFile(path.join(root, '.worktrees', 'old', 'src', 'copy.ts'), 'export const copy = 1;\n'); + await writeFile(path.join(root, '.venv', 'lib', 'tool.js'), 'export const tool = 1;\n'); + + const scan = await scanAstCodeWorkspace({ root, maxFiles: 1 }); + + assert.equal(scan.fileCount, 1); + assert.deepEqual(scan.coverage.representedJsTsLocators, ['workspace://src/entry.ts']); + assert.equal(scan.coverage.maxFilesReached, false); + assert.equal(scan.coverage.ignoredFileCount, 2); + assert.equal(scan.coverage.ignoredDirectoryCount, 1); + assert(scan.coverage.excludedDirectoryCount >= 2); +}); + +test('explicit include overrides non-security ignore but not root containment', async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-include-')); + t.after(() => rm(root, { recursive: true, force: true })); + await mkdir(path.join(root, 'src'), { recursive: true }); + await writeFile(path.join(root, '.recallignore'), 'src/kept.ts\n'); + await writeFile(path.join(root, 'src', 'kept.ts'), 'export const kept = true;\n'); + const scan = await scanAstCodeWorkspace({ root, explicitIncludes: ['src/kept.ts'] }); + assert.deepEqual(scan.coverage.representedJsTsLocators, ['workspace://src/kept.ts']); + await assert.rejects( + scanAstCodeWorkspace({ root, explicitIncludes: ['../outside.ts'] }), + /source_graph_explicit_include_invalid/ + ); +}); +``` + +- [x] **Step 2: Run the tests and verify missing ignore behavior** + +Run: + +```bash +node --test tests/source-graph-discovery.test.mjs +``` + +Expected: FAIL because `.worktrees`, `.venv`, nested `.gitignore`, `.recallignore`, and `explicitIncludes` are not implemented. + +- [x] **Step 3: Implement dependency-free ignore parsing** + +Create `ignore-rules.mjs`. Parse blank lines, comments, escaped `#`/`!`, negation, root anchoring, directory-only suffixes, `*`, `?`, and `**`. Store the ignore-file base directory so descendant `.gitignore` rules are relative to their own directory: + +```js +export function parseIgnoreFile(text, { base = '' } = {}) { + return String(text ?? '').split(/\r?\n/u).flatMap((raw, index) => { + const line = raw.trim(); + if (!line || line.startsWith('#')) return []; + const negated = line.startsWith('!'); + const pattern = (negated ? line.slice(1) : line).replace(/\\([#!])/gu, '$1'); + if (!pattern) return []; + return [{ base, pattern, negated, directoryOnly: pattern.endsWith('/'), index }]; + }); +} + +export function isIgnoredPath(relativePath, { isDirectory = false, rules = [], explicitIncludes = [] } = {}) { + const normalized = normalizeRelativePath(relativePath); + if (explicitIncludes.some((item) => item === normalized || item.startsWith(`${normalized}/`))) return false; + let ignored = false; + for (const rule of rules) { + if (rule.directoryOnly && !isDirectory) continue; + if (matchesIgnoreRule(normalized, rule)) ignored = !rule.negated; + } + return ignored; +} + +export async function loadRootRecallIgnore(root) { + try { + return parseIgnoreFile(await readFile(path.join(root, '.recallignore'), 'utf8'), { base: '' }); + } catch (error) { + if (error?.code === 'ENOENT') return []; + throw error; + } +} + +function normalizeRelativePath(value) { + const raw = String(value ?? ''); + const normalized = raw.replace(/^\.\//u, ''); + if (!normalized || raw.includes('\\') || normalized.startsWith('/') || /(?:^|\/)\.\.(?:\/|$)/u.test(normalized) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(normalized)) { + throw new Error('source_graph_explicit_include_invalid'); + } + return normalized; +} + +function matchesIgnoreRule(relativePath, rule) { + const raw = rule.pattern.replace(/\/$/u, ''); + const anchored = raw.startsWith('/'); + const pattern = anchored ? raw.slice(1) : raw; + const base = String(rule.base ?? '').replace(/\/$/u, ''); + const scoped = base ? `${base}/${pattern}` : pattern; + const glob = anchored || pattern.includes('/') + ? scoped + : base ? `${base}/**/${pattern}` : `**/${pattern}`; + return path.matchesGlob(relativePath, glob) + || (!pattern.includes('/') && path.basename(relativePath) === pattern) + || (rule.directoryOnly && (relativePath === scoped || relativePath.startsWith(`${scoped}/`))); +} +``` + +Import `readFile` from `node:fs/promises` and `path` from `node:path`. Use `path.matchesGlob()` only after normalization. Validate every explicit include with `normalizeRelativePath()` before scanning; security exclusions and root containment checks still run after a non-security ignore override. + +- [x] **Step 4: Integrate discovery policy into the scanner** + +In `scanAstCodeWorkspace()`: + +1. Expand default exclusions to `.worktrees`, `.venv`, `venv`, `site-packages`, `.cache`, `.pytest_cache`, `.turbo`, `.parcel-cache`, `.agents`, `.claude`, `test-results`, and generated build directories. +2. Load root `.recallignore` once. +3. When entering a directory, load its `.gitignore` and append rules for descendants. +4. Evaluate default exclusions and ignore rules before `lstat()` recursion and before unsupported-file counting. +5. Increment `visitedFiles` only for supported files that are actually read. +6. Delete `discoverExcludedDirectoriesAfterCap()`; the scan must not traverse excluded trees after reaching the supported-file cap. +7. Add bounded `ignoredFileCount`, `ignoredDirectoryCount`, `ignoredSamples` (maximum 100), and `unsupportedExtensionCounts` (maximum 32 entries) to scan coverage. Keep these internal to the scan result until Task 3 extends the public graph schemas. +8. Keep an internal `index.discoveryIdentity` with the normalized relevant ignore-file locators (maximum 100) and a SHA-256 fingerprint of their normalized rules plus explicit includes. Do not project these new fields into the public graph until Task 3 extends the schema. + +Keep coverage samples bounded with the existing `addBoundedLocator()` helper. + +- [x] **Step 5: Verify discovery and existing AST behavior** + +Run: + +```bash +node --test tests/source-graph-discovery.test.mjs tests/ast-code-candidate-source.test.mjs tests/source-graph-preview.test.mjs +``` + +Expected: all focused tests pass and supported AST/source-graph behavior remains unchanged. + +- [x] **Step 6: Commit discovery policy** + +```bash +git add providers/native/context-candidate-ast-code/src/ignore-rules.mjs providers/native/context-candidate-ast-code/src/index.mjs tests/source-graph-discovery.test.mjs +git commit -m "feat: bound source discovery before file accounting" +``` + +### Task 3: Bound graph construction and report omitted candidates + +**Files:** +- Modify: `providers/native/context-candidate-ast-code/src/index.mjs` +- Modify: `packages/protocol/schemas/source-graph.schema.json` +- Modify: `packages/protocol/schemas/source-graph-preview.schema.json` +- Modify: `examples/protocol/source-graph.json` +- Modify: `examples/protocol/source-graph-preview.json` +- Modify: `rfcs/0001-protocol-contracts.md` +- Modify: `docs/api/openapi.yaml` +- Test: `tests/source-graph-preview.test.mjs` + +**Interfaces:** +- `buildSourceGraphFromIndex(index, options)` adds `maxNodes = 20_000` and `maxEdges = 50_000`. +- `graph.summary.coverage` adds represented, candidate, and omitted node/edge counts by kind. + +- [x] **Step 1: Write a graph-budget regression test** + +Add this generated dense-source test to `tests/source-graph-preview.test.mjs`. Extend the existing provider import with `buildJsTsSourceIndex` and `buildSourceGraphFromIndex`: + +```js +test('graph budget preserves structural and call edges before references', async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-dense-graph-')); + t.after(() => rm(root, { recursive: true, force: true })); + await mkdir(path.join(root, 'src'), { recursive: true }); + const targets = Array.from({ length: 30 }, (_, index) => `export function target${index}(){ return ${index}; }`); + const calls = Array.from({ length: 30 }, (_, caller) => ( + `export function caller${caller}(){ return ${Array.from({ length: 30 }, (_, target) => `target${target}()`).join(' + ')}; }` + )); + await writeFile(path.join(root, 'src', 'dense.js'), `${targets.join('\n')}\n${calls.join('\n')}\n`); + + const index = await buildJsTsSourceIndex({ root, workspaceId: 'ws_local' }); + const graph = buildSourceGraphFromIndex(index, { + builtAt: '2026-07-15T10:00:00.000Z', + maxNodes: 200, + maxEdges: 300 + }); + + assert(graph.nodes.length <= 200); + assert(graph.edges.length <= 300); + assert.equal(graph.summary.coverage.status, 'partial'); + assert(graph.summary.coverage.omittedEdgeCount > 0); + assert(graph.summary.coverage.omittedEdgeKindCounts.references > 0); + assert(graph.summary.edgeKindCounts.contains > 0); + assert(graph.summary.edgeKindCounts.defined_in > 0); + assert(graph.summary.edgeKindCounts.calls > 0); + assert.equal(validateJsonSchema(sourceGraphSchema, graph).valid, true); +}); +``` + +- [x] **Step 2: Run the budget test and verify schema overflow** + +Run: + +```bash +node --test --test-name-pattern="graph budget" tests/source-graph-preview.test.mjs +``` + +Expected: FAIL because `buildSourceGraphFromIndex` has no budgets and the schema has no omission fields. + +- [x] **Step 3: Implement deterministic node and edge budgets** + +Add constants and normalize options: + +```js +export const DEFAULT_SOURCE_GRAPH_MAX_NODES = 20_000; +export const DEFAULT_SOURCE_GRAPH_MAX_EDGES = 50_000; +const EDGE_BUDGET_ORDER = Object.freeze(['contains', 'defined_in', 'imports', 'exports', 'calls', 'references']); +``` + +Build nodes in deterministic file, chunk, symbol, and module order. When the node budget is exhausted, count omitted nodes by kind and do not create edges to omitted nodes. Stage edge candidates by kind, sort each kind by `id`, then take them in `EDGE_BUDGET_ORDER` until `maxEdges`. Produce: + +```js +coverage: { + ...index.coverage, + ignoreRuleFingerprint: index.discoveryIdentity.ignoreRuleFingerprint, + ignoreFileLocators: index.discoveryIdentity.ignoreFileLocators, + status: omittedNodeCount || omittedEdgeCount || index.coverage.status === 'partial' ? 'partial' : 'complete', + candidateNodeCount, + representedNodeCount: nodes.length, + omittedNodeCount, + omittedNodeKindCounts, + candidateEdgeCount, + representedEdgeCount: edges.length, + omittedEdgeCount, + omittedEdgeKindCounts, + reasonCodes: uniqueSortedStrings([ + ...index.coverage.reasonCodes, + ...(omittedNodeCount ? ['node_budget_reached'] : []), + ...(omittedEdgeCount ? ['edge_budget_reached'] : []) + ]) +} +``` + +Do not truncate after graph construction. The returned graph itself must satisfy the public schema. + +- [x] **Step 4: Extend additive schemas, examples, OpenAPI, and RFC** + +Add optional bounded integer/count-map properties to the shared coverage definitions in both graph schemas. Also allow `ignoreRuleFingerprint` as a `sha256:` string and up to 100 safe workspace-relative `ignoreFileLocators`; these fields let the later snapshot service identify the discovery policy without exposing file bodies or absolute roots. Keep existing required fields and schema versions unchanged. Update `examples/protocol/source-graph.json` and `source-graph-preview.json` with a partial example. Add one paragraph to RFC 0001 explaining that additive v1 coverage fields report omitted candidates and never expand authority. Mirror the fields in `docs/api/openapi.yaml`. + +- [x] **Step 5: Run graph and protocol verification** + +Run: + +```bash +node --test tests/source-graph-preview.test.mjs tests/ast-code-candidate-source.test.mjs +npm run protocol:validate +``` + +Expected: graph-budget tests, existing graph tests, and compatibility fixtures pass. + +- [x] **Step 6: Commit bounded construction** + +```bash +git add providers/native/context-candidate-ast-code/src/index.mjs packages/protocol/schemas/source-graph.schema.json packages/protocol/schemas/source-graph-preview.schema.json examples/protocol/source-graph.json examples/protocol/source-graph-preview.json docs/api/openapi.yaml rfcs/0001-protocol-contracts.md tests/source-graph-preview.test.mjs +git commit -m "feat: return bounded partial source graphs" +``` + +### Task 4: Add deterministic orientation and focused-neighborhood projections + +**Files:** +- Create: `packages/source-graph/src/orientation.mjs` +- Modify: `packages/source-graph/src/index.mjs` +- Modify: `packages/recall-map/src/index.mjs` +- Modify: `packages/protocol/schemas/source-graph-preview.schema.json` +- Modify: `packages/protocol/schemas/recall-map.schema.json` +- Modify: `examples/protocol/source-graph-preview.json` +- Modify: `examples/protocol/recall-map.json` +- Test: `tests/source-graph-preview.test.mjs` +- Test: `tests/recall-map.test.mjs` + +**Interfaces:** +- Produces: `buildSourceGraphOrientation(graph, options): { groups, relations }`. +- Produces: `buildSourceGraphFocus(graph, options): { nodes, edges, omittedNodes, omittedEdges }`. +- `buildSourceGraphPreview()` adds top-level `orientation` and `focus` projections. +- `buildRecallMap()` projects `orientation.groups` and `orientation.relations` under `architecture`. + +- [x] **Step 1: Write failing deterministic orientation tests** + +Extend the provider import with `buildJsTsSourceGraph` and `rankArchitectureNodes`, and import both new projection functions from `orientation.mjs`. Add this test and helper: + +```js +test('source graph orientation is deterministic and bounded', async (t) => { + const root = await writeOrientationRepository(t); + const graph = await buildJsTsSourceGraph({ root, workspaceId: 'ws_local' }); + const ranking = rankArchitectureNodes(graph, { changedLocators: ['workspace://apps/web/app.js'], limit: 12 }); + const options = { + changedLocators: ['workspace://apps/web/app.js'], + entryPoints: ranking.entryPoints, + maxGroups: 12, + maxRelations: 20 + }; + const orientation = buildSourceGraphOrientation(graph, options); + assert.deepEqual(orientation.groups.map(({ prefix }) => prefix), [ + 'apps/web', + 'packages/source-graph', + 'providers/native/context-candidate-ast-code', + 'scripts', + 'services/control-api', + 'tests' + ]); + assert.equal(orientation.groups.find(({ prefix }) => prefix === 'apps/web').changedFileCount, 1); + assert.equal(orientation.groups.every(({ entryPoints }) => entryPoints.length <= 2), true); + assert.equal(orientation.relations.length <= 20, true); + assert.deepEqual(orientation, buildSourceGraphOrientation(structuredClone(graph), options)); +}); + +async function writeOrientationRepository(t) { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-orientation-')); + t.after(() => rm(root, { recursive: true, force: true })); + const files = new Map([ + ['apps/web/app.js', 'import { build } from "../../packages/source-graph/index.js";\nexport function renderOverview(){ return build(); }\n'], + ['packages/source-graph/index.js', 'import { serve } from "../../services/control-api/server.js";\nexport function build(){ return serve(); }\n'], + ['services/control-api/server.js', 'import { parse } from "../../providers/native/context-candidate-ast-code/index.js";\nexport function serve(){ return parse(); }\n'], + ['providers/native/context-candidate-ast-code/index.js', 'export function parse(){ return "ready"; }\n'], + ['scripts/smoke.js', 'import { renderOverview } from "../apps/web/app.js";\nexport const smoke = renderOverview();\n'], + ['tests/web.test.js', 'import { renderOverview } from "../apps/web/app.js";\nexport const expected = typeof renderOverview;\n'] + ]); + for (const [relative, body] of files) { + await mkdir(path.dirname(path.join(root, relative)), { recursive: true }); + await writeFile(path.join(root, relative), body); + } + return root; +} +``` + +- [x] **Step 2: Write failing focused-neighborhood tests** + +For a query result inside `apps/web`, reuse `writeOrientationRepository()` and assert the focus projection contains seed nodes and bounded adjacent edges but not the full graph: + +```js +test('source graph focus returns a bounded neighborhood', async (t) => { + const root = await writeOrientationRepository(t); + const graph = await buildJsTsSourceGraph({ root, workspaceId: 'ws_local' }); + const seed = graph.nodes.find(({ label }) => label === 'renderOverview'); + assert(seed); + const focus = buildSourceGraphFocus(graph, { + seedNodeIds: [seed.id], + locatorPrefix: 'workspace://apps/web', + nodeLimit: 50, + edgeLimit: 100 + }); + assert(focus.nodes.some(({ id }) => id === seed.id)); + assert(focus.nodes.every(({ locator }) => !locator || locator.startsWith('workspace://apps/web'))); + assert(focus.nodes.length <= 50); + assert(focus.edges.length <= 100); + assert(focus.omittedNodes > 0 || focus.nodes.length < graph.nodes.length); +}); +``` + +- [x] **Step 3: Run the projection tests and verify missing exports** + +Run: + +```bash +node --test --test-name-pattern="deterministic orientation|focused neighborhood" tests/source-graph-preview.test.mjs +``` + +Expected: FAIL because `orientation.mjs`, `buildSourceGraphOrientation`, and `buildSourceGraphFocus` do not exist. + +- [x] **Step 4: Implement deterministic group derivation** + +Create `orientation.mjs`. Convert node locators to repository-relative paths. Use two segments for known workspace roots (`apps`, `packages`, `services`, `plugins`, `examples`, `tests`, and `scripts`), three for `providers//` when available, and one segment otherwise. Never invent a group. + +```js +function groupPrefix(locator) { + const parts = relativeLocator(locator).split('/').filter(Boolean); + if (!parts.length) return null; + if (parts[0] === 'providers') return parts.slice(0, Math.min(3, parts.length - 1 || 1)).join('/'); + if (new Set(['apps', 'packages', 'services', 'plugins', 'examples', 'tests', 'scripts']).has(parts[0])) { + return parts.slice(0, Math.min(2, parts.length - 1 || 1)).join('/'); + } + return parts[0]; +} + +function relativeLocator(locator) { + const value = String(locator ?? '').split('#', 1)[0]; + return value.startsWith('workspace://') ? value.slice('workspace://'.length) : ''; +} +``` + +Count unique files and symbols per group. Assign up to two ranked entry points by locator prefix. Aggregate only inter-group `imports` and `calls` relations, sort by descending count then source/target prefix, and cap at 20. Create stable IDs with the existing hash helper; do not expose package-manager metadata or absolute paths. + +- [x] **Step 5: Implement a bounded one-hop focus projection** + +Start from query/search/trace/impact seed IDs. If `locatorPrefix` is present, include matching file and symbol nodes first. Add one-hop edges in priority order `calls`, `imports`, `exports`, `defined_in`, `contains`, `references`. Add the opposite endpoint only while under the node limit. Sort returned nodes/edges by ID and report omitted counts from the eligible neighborhood. + +```js +return Object.freeze({ + nodeLimit, + edgeLimit, + nodes: Object.freeze(selectedNodes), + edges: Object.freeze(selectedEdges), + omittedNodes: Math.max(0, eligibleNodeIds.size - selectedNodes.length), + omittedEdges: Math.max(0, eligibleEdges.length - selectedEdges.length) +}); +``` + +- [x] **Step 6: Wire projections into preview and Recall Map** + +Build `orientation` from the validated public graph. Build focus seeds from search result node IDs, trace path node IDs, and impact node IDs. For an unscoped empty query, return an empty focus so Overview uses groups rather than a raw file graph. Project the same group/relations objects into Recall Map: + +```js +architecture: { + ...existingArchitecture, + groups: preview.orientation.groups, + groupRelations: preview.orientation.relations +} +``` + +Add optional strict bounded definitions to both schemas: 12 groups, 20 relations, 200 focus nodes, and 400 focus edges. The producer always emits them, while the optional schema fields preserve additive v1 reader compatibility. Update both examples. + +- [x] **Step 7: Verify projection and protocol behavior** + +Run: + +```bash +node --test tests/source-graph-preview.test.mjs tests/recall-map.test.mjs +npm run protocol:validate +``` + +Expected: orientation, focus, Recall Map, and protocol tests pass. + +- [x] **Step 8: Commit orientation data** + +```bash +git add packages/source-graph/src/orientation.mjs packages/source-graph/src/index.mjs packages/recall-map/src/index.mjs packages/protocol/schemas/source-graph-preview.schema.json packages/protocol/schemas/recall-map.schema.json examples/protocol/source-graph-preview.json examples/protocol/recall-map.json tests/source-graph-preview.test.mjs tests/recall-map.test.mjs +git commit -m "feat: project bounded repository orientation data" +``` + +### Task 5: Add the injected in-process snapshot service + +**Files:** +- Create: `packages/source-graph/src/snapshot-service.mjs` +- Create: `tests/source-graph-snapshot-service.test.mjs` +- Modify: `packages/source-graph/src/index.mjs` +- Modify: `packages/protocol/schemas/source-graph-preview.schema.json` +- Modify: `examples/protocol/source-graph-preview.json` + +**Interfaces:** +- Produces: `createSourceGraphSnapshotService(options): SourceGraphSnapshotService`. +- Produces methods `getSnapshot(request)`, `markDirty(root, reason)`, `inspect(root)`, and `close()`. +- `buildSourceGraphPreview(options)` adds optional `snapshotService` and `refresh = false`. +- Snapshot identity is SHA-256 over canonical root, graph/parser version, ignore-rule fingerprint, and source-index fingerprint. + +- [x] **Step 1: Write failing cache, concurrency, invalidation, and stale tests** + +Create `tests/source-graph-snapshot-service.test.mjs` using an injected `buildGraph` counter and injected watcher: + +```js +test('snapshot service reuses, deduplicates, invalidates, and preserves last valid graph', async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-snapshot-service-')); + t.after(() => rm(root, { recursive: true, force: true })); + let builds = 0; + let fail = false; + let onChange = null; + const graph = fixtureGraph(); + const service = createSourceGraphSnapshotService({ + buildGraph: async () => { + builds += 1; + await Promise.resolve(); + if (fail) throw new Error('fixture_build_failed'); + return graph; + }, + watchRoot: (_root, listener) => { + onChange = listener; + return { close() {} }; + }, + clock: () => 1000 + }); + t.after(() => service.close()); + + const request = { root, workspaceId: 'ws_local', maxFiles: 1000, maxFileBytes: 524288 }; + const [first, concurrent] = await Promise.all([service.getSnapshot(request), service.getSnapshot(request)]); + assert.equal(builds, 1); + assert.equal(first.reuse, 'cold'); + assert.equal(concurrent.reuse, 'inflight'); + + const cached = await service.getSnapshot(request); + assert.equal(cached.reuse, 'cache'); + assert.equal(builds, 1); + assert.match(service.inspect(root).identity, /^sha256:[a-f0-9]{64}$/u); + + onChange('change', 'src/app.js'); + fail = true; + const stale = await service.getSnapshot(request); + assert.equal(stale.status, 'stale'); + assert.equal(stale.graph.graphFingerprint, graph.graphFingerprint); + assert.equal(builds, 2); +}); + +test('snapshot service falls back to a bounded metadata scan when recursive watch is unavailable', async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'recall-snapshot-fallback-')); + t.after(() => rm(root, { recursive: true, force: true })); + let builds = 0; + let manifest = 'manifest:a'; + const unavailable = Object.assign(new Error('recursive watch unavailable'), { code: 'ERR_FEATURE_UNAVAILABLE' }); + const service = createSourceGraphSnapshotService({ + buildGraph: async () => { + builds += 1; + return fixtureGraph(); + }, + watchRoot: () => { throw unavailable; }, + freshnessProbe: async () => manifest, + clock: () => 1000 + }); + t.after(() => service.close()); + const request = { root, workspaceId: 'ws_local', maxFiles: 1000, maxFileBytes: 524288 }; + + await service.getSnapshot(request); + const cached = await service.getSnapshot(request); + assert.equal(cached.reuse, 'cache'); + assert.equal(cached.validationMode, 'metadata-scan'); + assert.equal(builds, 1); + + manifest = 'manifest:b'; + const rebuilt = await service.getSnapshot(request); + assert.equal(rebuilt.reuse, 'cold'); + assert.equal(builds, 2); +}); + +function fixtureGraph() { + return Object.freeze({ + schemaVersion: '1.0.0', + workspaceId: 'ws_local', + graphVersion: 'memory-recall-test-1.0.0', + parserVersion: 'memory-recall-test-parser', + builtAt: '2026-07-15T10:00:00.000Z', + sourceIndexFingerprint: `sha256:${'a'.repeat(64)}`, + graphFingerprint: `sha256:${'b'.repeat(64)}`, + summary: { + fileCount: 1, + symbolCount: 0, + moduleCount: 0, + nodeCount: 0, + edgeCount: 0, + nodeKindCounts: {}, + edgeKindCounts: {}, + hotspots: [], + entryPoints: [], + coverage: { + ignoreRuleFingerprint: `sha256:${'c'.repeat(64)}`, + ignoreFileLocators: [] + } + }, + nodes: [], + edges: [], + diagnostics: [] + }); +} +``` + +Import `mkdtemp` and `rm` from `node:fs/promises`, plus `os` and `path`; the request root must be canonicalized by the service. + +- [x] **Step 2: Run the service test and verify the missing module** + +Run: + +```bash +node --test tests/source-graph-snapshot-service.test.mjs +``` + +Expected: FAIL with module-not-found for `snapshot-service.mjs`. + +- [x] **Step 3: Implement the snapshot service** + +Create `snapshot-service.mjs` with one entry per canonical root plus graph options. Use `node:fs.watch` recursively by default. Relevant supported-source, `.gitignore`, and `.recallignore` events increment `dirtyGeneration`; ignored/cache/build output events do not. The service result shape is: + +```js +Object.freeze({ + graph, + status: 'fresh', // or 'stale' + reuse: 'cold', // or 'cache' or 'inflight' + reason: null, + generation, + buildDurationMs, + builtAt: graph.builtAt +}) +``` + +Use a `Map` of entries with `{ graph, identity, manifest, dirty, generation, inFlight, watcher, validationMode, lastBuildDurationMs }`. Compute identity with `node:crypto` SHA-256 over canonical root, `graphVersion`, `parserVersion`, `graph.summary.coverage.ignoreRuleFingerprint`, and `sourceIndexFingerprint`. Set `inFlight` before awaiting the build. Clear it in `finally`. Replace `graph`, identity, and manifest only after a successful build. On failure with a last valid graph, return `status: 'stale'`, `reason: safe error code`, and the old graph. On first-build failure, rethrow. `close()` closes every watcher and clears the map. + +The default watcher uses `fs.watch(root, { recursive: true })`. If recursive watching reports `ERR_FEATURE_UNAVAILABLE` or `ERR_INVALID_ARG_VALUE`, set `validationMode` to `metadata-scan`. The default bounded freshness probe stats the canonical root, safe file-node locators, every directory prefix derived from those locators, and `ignoreFileLocators`, capped by the existing 1,000-file/100-ignore-file bounds; sort `{ locator, size, mtimeMs }` tuples and hash them. Before returning a cached result in this mode, compare the manifest and mark dirty on change. This fallback may scan metadata but must not read source bodies or walk outside the canonical root. + +- [x] **Step 4: Query the snapshot from the preview facade** + +In `buildSourceGraphPreview()`: + +```js +const snapshot = snapshotService + ? await snapshotService.getSnapshot({ root, workspaceId: safeWorkspaceId, maxFiles: boundedMaxFiles, maxFileBytes: boundedMaxFileBytes, refresh }) + : { graph: await buildJsTsSourceGraph({ root, workspaceId: safeWorkspaceId, maxFiles: boundedMaxFiles, maxFileBytes: boundedMaxFileBytes, clock: () => generatedAt }), status: 'fresh', reuse: 'cold', reason: null, generation: 0, buildDurationMs: null }; +graph = snapshot.graph; +``` + +Add a `snapshot` object to every preview response with `status`, `reuse`, `reason`, `generation`, `validationMode`, `buildDurationMs`, and `builtAt`. Define it as an optional additive field in the v1 schema so older valid producers remain readable. The unavailable response uses `{ status: 'unavailable', reuse: 'none', reason: code, generation: 0, validationMode: 'none', buildDurationMs: null, builtAt: null }`. The UI may label `metadata-scan` as a scan; it must not describe it as watcher-backed reuse. + +- [x] **Step 5: Verify snapshot and facade behavior** + +Run: + +```bash +node --test tests/source-graph-snapshot-service.test.mjs tests/source-graph-preview.test.mjs +npm run protocol:validate +``` + +Expected: service, facade, and protocol tests pass. + +- [x] **Step 6: Commit snapshot service** + +```bash +git add packages/source-graph/src/snapshot-service.mjs packages/source-graph/src/index.mjs packages/protocol/schemas/source-graph-preview.schema.json examples/protocol/source-graph-preview.json tests/source-graph-snapshot-service.test.mjs tests/source-graph-preview.test.mjs +git commit -m "feat: reuse source graph snapshots" +``` + +### Task 6: Share one snapshot across Recall Map and Map API operations + +**Files:** +- Modify: `packages/recall-map/src/index.mjs` +- Modify: `packages/protocol/schemas/recall-map.schema.json` +- Modify: `examples/protocol/recall-map.json` +- Modify: `services/control-api/src/server.mjs` +- Modify: `services/control-api/src/route-contracts.mjs` +- Modify: `tests/recall-map.test.mjs` +- Modify: `tests/control-api-boundary.test.mjs` + +**Interfaces:** +- `buildRecallMap(options)` adds `sourceGraphSnapshotService` and `refreshSourceGraph = false`. +- `createControlApiServer(options)` adds injectable `sourceGraphSnapshotService`. + +- [x] **Step 1: Write the failing shared-build Control API test** + +In `tests/control-api-boundary.test.mjs`, import `buildJsTsSourceGraph` and `createSourceGraphSnapshotService`. Use the existing `startServer()` injectable harness so the test exercises both authenticated routes against the same real service and temporary repository: + +```js +test('Recall Map and source preview share one source snapshot', async (t) => { + const sourceGraphRoot = await mkdtemp(path.join(os.tmpdir(), 'oaf-shared-source-snapshot-')); + t.after(async () => rm(sourceGraphRoot, { recursive: true, force: true })); + await mkdir(path.join(sourceGraphRoot, 'src'), { recursive: true }); + await writeFile(path.join(sourceGraphRoot, 'src', 'app.js'), 'export const sharedSnapshotFixture = true;\n'); + + let buildCount = 0; + const sourceGraphSnapshotService = createSourceGraphSnapshotService({ + buildGraph: async (options) => { + buildCount += 1; + return buildJsTsSourceGraph(options); + } + }); + t.after(() => sourceGraphSnapshotService.close()); + const api = await startServer(t, { sourceGraphRoot, sourceGraphSnapshotService }); + const authHeaders = { cookie: api.auth.cookie, origin: api.base }; + + const recall = await request(api.base, '/api/recall/map?workspaceId=ws_local', { + headers: authHeaders + }); + const map = await request(api.base, '/api/context/graph/preview', { + method: 'POST', + headers: { + ...authHeaders, + 'content-type': 'application/json', + 'x-csrf-token': api.auth.csrf + }, + body: JSON.stringify({ workspaceId: 'ws_local', sampleLimit: 3 }) + }); + + assert.equal(recall.status, 200, recall.text); + assert.equal(map.status, 200, map.text); + assert.equal(buildCount, 1); + assert.equal(recall.body.support.sourceGraph.snapshot.reuse, 'cold'); + assert.equal(map.body.snapshot.reuse, 'cache'); + assert.equal(map.body.graph.summary.fileCount > 0, true); +}); +``` + +The test owns and closes its injected service. The server must never close a caller-owned service. + +Also add a stale fixture to `tests/recall-map.test.mjs` and assert `support.sourceGraph.coverage.status === 'stale'`, not `unavailable` and not a zero-result ready state. + +- [x] **Step 2: Run focused tests and verify duplicate construction** + +Run: + +```bash +node --test --test-name-pattern="share one source snapshot|stale source snapshot" tests/control-api-boundary.test.mjs tests/recall-map.test.mjs +``` + +Expected: FAIL because Recall Map and preview build independently and Recall Map has no snapshot projection. + +- [x] **Step 3: Inject the service into Recall Map** + +Call `buildSourceGraphPreview({ snapshotService: sourceGraphSnapshotService, refresh: refreshSourceGraph, ... })`. Project snapshot truth under `support.sourceGraph.snapshot`: + +```js +snapshot: { + status: preview.snapshot.status, + reuse: preview.snapshot.reuse, + reason: preview.snapshot.reason, + validationMode: preview.snapshot.validationMode, + builtAt: preview.snapshot.builtAt, + buildDurationMs: preview.snapshot.buildDurationMs +} +``` + +Set coverage to `stale` when the last valid snapshot is shown after a failed refresh, `partial` for bounded omission, `complete` only for complete current coverage, and `unavailable` only when no graph exists. + +- [x] **Step 4: Own one service in the Control API server** + +Import `createSourceGraphSnapshotService`. In `createControlApiServer()`, use the injected service or create one: + +```js +const sourceSnapshots = sourceGraphSnapshotService ?? createSourceGraphSnapshotService(); +const ownsSourceSnapshots = !sourceGraphSnapshotService; +server.once('close', () => { + if (ownsSourceSnapshots) sourceSnapshots.close(); +}); +``` + +Pass `sourceSnapshots` to both `buildRecallMap()` cases and `buildSourceGraphPreview()` in `previewContextGraph`. Add an optional boolean `refresh` to the POST Recall Map and graph-preview request schemas. Pass it as `refreshSourceGraph` or `refresh`; a true value marks the root dirty before the request. GET remains cache-aware and read-only. Preserve all auth, rate-limit, validation, and side-effect behavior. + +- [x] **Step 5: Extend Recall Map schema and examples additively** + +Allow coverage states `complete`, `partial`, `stale`, and `unavailable`. Add the bounded snapshot object and optional request `refresh` flag. Update the example and route contract fixtures. Do not rename existing source-graph or readiness fields. + +- [x] **Step 6: Verify shared API behavior** + +Run: + +```bash +node --test tests/recall-map.test.mjs tests/control-api.test.mjs tests/control-api-boundary.test.mjs +npm run protocol:validate +``` + +Expected: all focused API, boundary, and protocol tests pass with one shared graph build. + +- [x] **Step 7: Commit Control API reuse** + +```bash +git add packages/recall-map/src/index.mjs packages/protocol/schemas/recall-map.schema.json examples/protocol/recall-map.json services/control-api/src/server.mjs services/control-api/src/route-contracts.mjs tests/recall-map.test.mjs tests/control-api-boundary.test.mjs +git commit -m "feat: share graph snapshots across local map routes" +``` + +### Task 7: Add generated and real-repository performance proof + +**Files:** +- Create: `scripts/source-graph-large-repo-smoke.mjs` +- Modify: `package.json` +- Modify: `docs/usage/recall-map.md` +- Test: `tests/source-graph-preview.test.mjs` + +**Interfaces:** +- Produces command: `npm run source-graph:large-smoke`. +- Optional environment: `MEMORY_RECALL_LARGE_REPO_ROOT=/absolute/read-only/repository`. +- Emits one JSON object with cold/cached timings, counts, coverage, cache reuse, and validation state. + +- [x] **Step 1: Write the smoke script assertions before implementation** + +Create the script entry with these terminal assertions: + +```js +must(first.graph.summary.fileCount > 0, 'large_repo_graph_empty'); +must(!first.graph.diagnostics.some(({ code }) => code.startsWith('source_graph_unavailable')), 'large_repo_graph_unavailable'); +must(first.graph.summary.nodeCount <= 20_000, 'large_repo_node_budget_exceeded'); +must(first.graph.summary.edgeCount <= 50_000, 'large_repo_edge_budget_exceeded'); +must(second.snapshot.reuse === 'cache', 'large_repo_snapshot_not_reused'); +must(cachedMs <= coldMs * 0.2, `large_repo_cache_not_80_percent_faster:${coldMs}:${cachedMs}`); +``` + +The generated fixture must include at least 1,100 supported files, more than 50,000 candidate relations, ordinary `users` paths, `.worktrees`, `.venv`, generated output, nested `.gitignore`, and `.recallignore`. Use bounded file bodies and remove the temporary fixture in `finally`. + +- [x] **Step 2: Add the package command and run it red** + +Add: + +```json +"source-graph:large-smoke": "node scripts/source-graph-large-repo-smoke.mjs" +``` + +Run: + +```bash +npm run source-graph:large-smoke +``` + +Expected before the preceding tasks are complete: FAIL on unavailable graph, edge budget, ignored-tree traversal, or cache reuse. + +- [x] **Step 3: Finish generated and optional real-repository measurement** + +Use one `createSourceGraphSnapshotService()` for both preview calls. When `MEMORY_RECALL_LARGE_REPO_ROOT` is present, run the same read-only checks against that canonical root without creating or editing files there. Emit: + +```js +console.log(JSON.stringify({ + schemaVersion: '1.0.0', + rootKind: configuredRoot ? 'configured' : 'generated', + coldMs, + cachedMs, + cacheReductionPercent: Number(((1 - cachedMs / coldMs) * 100).toFixed(2)), + snapshot: second.snapshot, + summary: first.graph.summary, + diagnostics: first.graph.diagnostics.map(({ code }) => code), + protocolValid: validateJsonSchema(sourceGraphPreviewSchema, first).valid +}, null, 2)); +``` + +- [x] **Step 4: Document operator behavior** + +Update `docs/usage/recall-map.md` with default exclusions, `.recallignore`, supported-file budgeting, partial coverage reason codes, automatic invalidation, explicit refresh, and the large-repository command. State that timings are local measurements, not universal claims. + +- [x] **Step 5: Run foundation verification** + +Run: + +```bash +npm run source-graph:large-smoke +MEMORY_RECALL_LARGE_REPO_ROOT=/Users/rebel/Desktop/polychads-clean npm run source-graph:large-smoke +node --test tests/source-graph-discovery.test.mjs tests/source-graph-snapshot-service.test.mjs tests/source-graph-preview.test.mjs tests/recall-map.test.mjs tests/control-api.test.mjs tests/control-api-boundary.test.mjs +npm run protocol:validate +``` + +Expected: both smoke modes return non-empty valid results, the cached path is at least 80% faster, and all focused tests pass. + +- [x] **Step 6: Commit performance proof** + +```bash +git add scripts/source-graph-large-repo-smoke.mjs package.json docs/usage/recall-map.md tests/source-graph-preview.test.mjs +git commit -m "test: prove large repository graph performance" +``` + +### Task 8: Run the graph-foundation release gate + +**Files:** +- Verify only; fix failures in the owning task before continuing. + +**Interfaces:** +- Produces a clean, independently testable graph foundation for the UI plan. + +- [x] **Step 1: Run all source, map, protocol, and consumer checks** + +```bash +npm run check +npm run protocol:validate +node --test tests/ast-code-candidate-source.test.mjs tests/source-graph-discovery.test.mjs tests/source-graph-snapshot-service.test.mjs tests/source-graph-preview.test.mjs tests/recall-map-ranking.test.mjs tests/recall-map.test.mjs tests/control-api.test.mjs tests/control-api-boundary.test.mjs +npm run source-graph:large-smoke +npm run consumer:smoke +``` + +Expected: every command exits 0. + +- [x] **Step 2: Confirm no security or side-effect drift** + +Inspect both generated and real-repository smoke JSON. Confirm: + +```text +networkCalls = 0 +modelCalls = 0 +externalWritesEnabled = false +rawBodyIncluded = false +graphDatabaseUsed = false +protocolValid = true +``` + +- [x] **Step 3: Record the foundation checkpoint** + +```bash +git status --short +git log --oneline -7 +``` + +Expected: clean worktree and one focused commit per task. Do not squash before the UI plan is verified. diff --git a/docs/superpowers/plans/2026-07-15-memory-recall-orientation-workbench-ui.md b/docs/superpowers/plans/2026-07-15-memory-recall-orientation-workbench-ui.md new file mode 100644 index 00000000..4ceb6628 --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-memory-recall-orientation-workbench-ui.md @@ -0,0 +1,1191 @@ +# Memory Recall Orientation Workbench UI Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the rejected Overview, Map, and memory-graph presentation with a compact, orientation-first workbench that shows repository structure, starting points, change impact, and trusted context without AI-style slogans or generic dashboard design. + +**Architecture:** Keep the existing static ESM shell and five-destination navigation. Split pure view models and route renderers out of `apps/web/app.js`, consume the bounded orientation/focus projections from the graph-foundation plan, run detailed graph layout in a worker, and provide semantic outlines as the canonical accessible representation. + +**Tech Stack:** Browser-native ESM, semantic HTML, CSS custom properties, Canvas 2D for data visualization, Web Workers, Node test runner, Playwright consumer browser smoke. + +## Prerequisite + +Complete and verify `docs/superpowers/plans/2026-07-15-memory-recall-large-repository-graph-foundation.md` first. This UI plan consumes `report.architecture.groups`, `report.architecture.groupRelations`, `preview.orientation`, `preview.focus`, and `preview.snapshot` exactly as defined there. + +## Global Constraints + +- Follow `DESIGN.md` and the approved `2026-07-15-memory-recall-orientation-workbench-design.md` specification. +- Preserve the existing five destinations, stable deep links, local-only boundary, setup flow, Memory review queue, Handoffs flow, and Settings behavior. +- Add no frontend framework, graph library, icon library, runtime dependency, model call, network service, or new product route. +- Use the existing native system sans, native monospace, warm off-white, graphite, restrained cobalt, 6 px controls, and 8 px bounded panels. +- Use lists, rules, aligned rows, and disclosures before cards; do not add gradients, glow, glass, heavy shadows, decorative art, ambient motion, or a metric-card wall. +- Minimal means fewer elements, not empty space: related gaps are 8 to 16 px and primary-region gaps use only 24 or 32 px tokens. +- No hero, slogan, promotional subtitle, fake chat, typing indicator, sparkle mark, robot imagery, or intelligence theater. +- Visible copy names an object, state, reason, or action. AI/model/provider terms appear only for a real configured provider, operation, setting, or technical boundary. +- Overview shows at most 12 deterministic groups, at most 20 inter-group relations, and exactly three `Start here` items when three are available. +- Detailed graphs render only a focused bounded payload and perform no unbounded quadratic layout on the main thread. +- Every visual graph has a keyboard-reachable outline with identical selection and inspector state. +- Preserve query and scope through submit, success, partial result, failure, reload, back, and forward. +- Empty governed memory renders no canvas and no zero-value metric strip. +- Meet WCAG 2.2 AA, visible focus, 44 px primary touch targets, reduced motion, light/dark modes, and no horizontal overflow at 320, 375, 414, 768, and 1440 px. + +--- + +## File Map + +### Create + +- `apps/web/api.js`: loopback JSON requests, CSRF handling, and bounded API errors. +- `apps/web/ui-primitives.js`: escaping, state panels, error recovery, date/fingerprint formatting, and status text. +- `apps/web/orientation-model.js`: pure Overview state, group layering, starting-point ranking, impact, and trust models. +- `apps/web/orientation-view.js`: Overview markup and selected-group interactions. +- `apps/web/source-map-view.js`: Map URL state, request payload, renderer, outline, inspector, and viewport controls. +- `apps/web/memory-graph-view.js`: governed-memory graph model, empty/populated rendering, outline, and inspector. +- `apps/web/graph-viewport.js`: shared canvas transform, hit testing, selection, outline parity, and worker lifecycle. +- `apps/web/graph-layout-worker.js`: deterministic bounded detailed-graph layout off the main thread. +- `tests/web-orientation.test.mjs`: Overview model and markup tests. +- `tests/web-source-map.test.mjs`: Map URL, focused graph, outline, and error-state tests. +- `tests/web-memory-graph.test.mjs`: empty/populated governed-memory graph tests. + +### Modify + +- `apps/web/app.js`: retain boot, routing, shared shell state, and delegation only for touched routes. +- `apps/web/index.html`: turn the existing repository search into the single deterministic command bar. +- `apps/web/styles.css`: add compact orientation, map, graph, outline, and responsive styles; remove obsolete touched-route styles. +- `apps/web/tokens.css`: reuse tokens; change only if a missing semantic alias is proven. +- `tests/web-shell.test.mjs`: update module imports and retain shell regression coverage. +- `scripts/consumer-browser-smoke.mjs`: exercise the new first-ten-seconds contract, Map, and memory graph across states and widths. +- `DESIGN.md`: change only if implementation finds an unrecorded canonical rule; do not weaken existing constraints. + +--- + +### Task 1: Extract API and shared UI primitives without visual change + +**Files:** +- Create: `apps/web/api.js` +- Create: `apps/web/ui-primitives.js` +- Modify: `apps/web/app.js` +- Modify: `tests/web-shell.test.mjs` + +**Interfaces:** +- Produces: `requestJson(path, options): Promise` and `ApiRequestError` from `api.js`. +- Produces: `escapeHtml`, `formatDate`, `shortFingerprint`, `buildApiErrorUiModel`, `renderApiErrorPanel`, and `statePanel` from `ui-primitives.js`. +- `app.js` imports and delegates to these functions; route behavior remains unchanged. + +- [x] **Step 0: Capture the rejected UI as baseline evidence** + +Before changing any visual markup or CSS, run the existing browser smoke and preserve the current Overview, Map, and Memory screenshots outside the files later runs overwrite: + +```bash +mkdir -p .scratch/ui-redesign/before +npm run consumer:browser-smoke +cp .scratch/ui-redesign/overview-desktop-light-1440.png .scratch/ui-redesign/before/overview-desktop-light-1440.png +cp .scratch/ui-redesign/map-desktop-1440.png .scratch/ui-redesign/before/map-desktop-1440.png +cp .scratch/ui-redesign/memory-desktop-1440.png .scratch/ui-redesign/before/memory-desktop-1440.png +``` + +Expected: the three baseline files exist and remain uncommitted for the final visual comparison. + +- [x] **Step 1: Write failing module-boundary tests** + +Add to `tests/web-shell.test.mjs`: + +```js +test('web API and UI primitives are focused modules', async () => { + const apiSource = await readFile(new URL('../apps/web/api.js', import.meta.url), 'utf8'); + const primitiveSource = await readFile(new URL('../apps/web/ui-primitives.js', import.meta.url), 'utf8'); + const appSource = await readFile(new URL('../apps/web/app.js', import.meta.url), 'utf8'); + assert.match(apiSource, /export async function requestJson/); + assert.match(apiSource, /export class ApiRequestError/); + assert.match(primitiveSource, /export function escapeHtml/); + assert.match(primitiveSource, /export function statePanel/); + assert.doesNotMatch(appSource, /async function api\(/); + assert.doesNotMatch(appSource, /function statePanel\(/); +}); +``` + +- [x] **Step 2: Run the boundary test and verify missing modules** + +Run: + +```bash +node --test --test-name-pattern="focused modules" tests/web-shell.test.mjs +``` + +Expected: FAIL because `api.js` and `ui-primitives.js` do not exist. + +- [x] **Step 3: Extract the API client** + +Move the existing CSRF lookup, request headers, JSON parsing, correlation ID, issue list, and error mapping into `apps/web/api.js`. Keep the public shape small: + +```js +export class ApiRequestError extends Error { + constructor(message, { status = null, code = '', correlationId = '', issues = [] } = {}) { + super(message); + this.name = 'ApiRequestError'; + this.status = status; + this.code = code; + this.correlationId = correlationId; + this.issues = issues; + } +} + +export async function requestJson(path, { method = 'GET', body, signal } = {}) { + const response = await fetch(path, { + method, + credentials: 'same-origin', + signal, + headers: requestHeaders(method), + body + }); + const payload = await readPayload(response); + if (!response.ok) throw apiError(response, payload); + return payload; +} + +function csrfToken() { + return /(?:^|;\s*)oaf_csrf=([^;]+)/u.exec(globalThis.document?.cookie ?? '')?.[1] ?? ''; +} + +function requestHeaders(method) { + const headers = new Headers(); + if (!['GET', 'HEAD'].includes(method)) { + headers.set('content-type', 'application/json'); + const token = csrfToken(); + if (token) headers.set('x-csrf-token', token); + } + return headers; +} + +async function readPayload(response) { + return response.clone().json().catch(() => null); +} + +function apiError(response, payload) { + const code = payload?.error?.code ?? ''; + const message = code === 'bootstrap_required' + ? 'Local owner setup is required.' + : code === 'invalid_credentials' + ? 'Username or password is incorrect.' + : response.status === 401 + ? 'Local authentication required.' + : payload?.error?.message ?? `Request failed with ${response.status}`; + return new ApiRequestError(message, { + status: response.status, + code, + correlationId: payload?.error?.correlationId ?? response.headers.get('x-correlation-id') ?? '', + issues: Array.isArray(payload?.error?.issues) ? payload.error.issues : [] + }); +} +``` + +Do not read storage, change auth semantics, retry automatically, or log response bodies. + +- [x] **Step 4: Extract safe shared render helpers** + +Move only pure formatting, escaping, state-panel, and API-error rendering into `ui-primitives.js`. Use dependency injection for action markup instead of importing route state. The escaping implementation remains: + +```js +export function escapeHtml(value) { + return String(value ?? '').replace(/[&<>'"]/gu, (character) => ({ + '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' + })[character]); +} +``` + +Export legacy aliases from `app.js` temporarily if existing tests or untouched routes import them. Do not duplicate implementation. + +- [x] **Step 5: Run shell regression tests** + +Run: + +```bash +node --test tests/web-shell.test.mjs tests/control-api-boundary.test.mjs +``` + +Expected: all existing shell and boundary tests pass with no visual behavior change. + +- [x] **Step 6: Commit the extraction** + +```bash +git add apps/web/api.js apps/web/ui-primitives.js apps/web/app.js tests/web-shell.test.mjs +git commit -m "refactor: extract web API and UI primitives" +``` + +### Task 2: Build the deterministic orientation model + +**Files:** +- Create: `apps/web/orientation-model.js` +- Create: `tests/web-orientation.test.mjs` +- Modify: `apps/web/app.js` +- Modify: `apps/web/shell-model.js` + +**Interfaces:** +- Produces: `buildOrientationModel(input): OrientationModel`. +- Produces: `layerOrientationGroups(groups, relations): LayeredGroup[]`. +- Produces: `selectOrientationGroup(model, groupId): OrientationModel`. +- Consumes graph-foundation fields `architecture.groups` and `architecture.groupRelations`. +- Input accepts `loading = false`, `error = null`, `gitChanges = null`, `handoff = null`, and an injected `now` timestamp for deterministic age labels. + +- [x] **Step 1: Write failing first-ten-seconds model tests** + +Create `tests/web-orientation.test.mjs`: + +```js +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { buildOrientationModel } from '../apps/web/orientation-model.js'; + +test('orientation model is deterministic and bounded', () => { + const report = orientationFixture({ groupCount: 16, entryPointCount: 8 }); + const first = buildOrientationModel({ report, gitChanges: changedFixture(), handoff: handoffFixture() }); + const second = buildOrientationModel({ report: structuredClone(report), gitChanges: changedFixture(), handoff: handoffFixture() }); + assert.deepEqual(first.groups, second.groups); + assert.equal(first.groups.length, 12); + assert.equal(first.relations.length <= 20, true); + assert.equal(first.startHere.length, 3); + assert.deepEqual(first.startHere.map(({ reason }) => reason), [ + 'application route', + 'package entry point', + 'changed central module' + ]); + assert.equal(first.repository.name, 'large-repository'); + assert.equal(first.repository.branch, 'main'); + assert.equal(first.coverage.status, 'partial'); + assert.equal(first.coverage.builtAt, '2026-07-15T09:59:00.000Z'); + assert.equal(first.impact.unrepresentedChangedCount, 1); + assert.equal(first.trust.memory.status, 'pending'); + assert.equal(first.trust.handoff.status, 'verified'); +}); + +test('small repositories keep real groups and do not invent filler', () => { + const model = buildOrientationModel({ report: orientationFixture({ groupCount: 3, entryPointCount: 2 }) }); + assert.equal(model.groups.length, 3); + assert.equal(model.startHere.length, 2); +}); + +test('orientation model distinguishes loading, clean, stale, empty, and failure truth', () => { + assert.equal(buildOrientationModel({ loading: true }).state, 'loading'); + assert.equal(buildOrientationModel({ report: null, loading: false }).state, 'empty'); + assert.equal(buildOrientationModel({ error: { message: 'Graph validation failed.' } }).state, 'failure'); + + const cleanReport = orientationFixture({ groupCount: 3, entryPointCount: 3 }); + cleanReport.repository.dirtyCount = 0; + const clean = buildOrientationModel({ report: cleanReport, gitChanges: { status: 'available', changedLocators: [], totalCount: 0, omittedCount: 0, truncated: false } }); + assert.equal(clean.impact.status, 'clean'); + assert.equal(clean.impact.label, 'No local changes detected'); + + const staleReport = orientationFixture({ groupCount: 3, entryPointCount: 3 }); + staleReport.support.sourceGraph.snapshot.status = 'stale'; + staleReport.support.sourceGraph.snapshot.reason = 'source_graph_refresh_failed'; + const stale = buildOrientationModel({ report: staleReport, now: '2026-07-15T10:00:00.000Z' }); + assert.equal(stale.state, 'stale'); + assert.equal(stale.coverage.lastValidSnapshotShown, true); +}); + +const GROUP_PREFIXES = [ + 'apps/web', 'apps/cli', 'packages/source-graph', 'packages/recall-map', + 'packages/protocol', 'services/control-api', 'providers/native/memory-sqlite', + 'providers/native/context-candidate-ast-code', 'scripts', 'tests', 'docs', + 'examples', 'evals', 'rfcs', 'deploy', 'planning' +]; + +function orientationFixture({ groupCount = 8, entryPointCount = 5 } = {}) { + const groups = GROUP_PREFIXES.slice(0, groupCount).map((prefix, index) => ({ + id: `group_${index}`, + label: prefix.split('/').at(-1), + prefix, + fileCount: 10 + index, + symbolCount: 30 + index, + changedFileCount: index === 0 ? 1 : 0, + coverageStatus: index === groupCount - 1 ? 'partial' : 'complete', + entryPoints: [] + })); + const reasons = ['application_route', 'package_entry_point', 'changed_central_module', 'executable_command', 'inbound_dependency_hub']; + const labels = ['route', 'createServer', 'renderOverview', 'recall', 'buildSourceGraphPreview']; + const entryPoints = Array.from({ length: entryPointCount }, (_, index) => ({ + nodeId: `sgnode_${String(index).padStart(32, '0')}`, + label: labels[index % labels.length], + locator: `workspace://${groups[index % groups.length].prefix}/entry-${index}.js#L1-L4`, + symbolKind: 'function', + reasonCodes: [reasons[index % reasons.length]], + score: 100 - index + })); + for (const item of entryPoints) { + const group = groups.find(({ prefix }) => item.locator.startsWith(`workspace://${prefix}/`)); + if (group && group.entryPoints.length < 2) group.entryPoints.push(item); + } + return { + schemaVersion: '1.0.0', + workspaceId: 'ws_local', + generatedAt: '2026-07-15T10:00:00.000Z', + repository: { name: 'large-repository', branch: 'main', commitSha: 'a'.repeat(40), dirtyCount: 2, gitStatusAvailable: true, reason: null }, + support: { + sourceGraph: { + status: 'implemented', + coverage: { status: 'partial', analyzedFileCount: 572, maxFiles: 1000, diagnosticCount: 2, reasonCodes: ['unsupported_extensions_skipped'] }, + snapshot: { status: 'fresh', reuse: 'cache', reason: null, builtAt: '2026-07-15T09:59:00.000Z', buildDurationMs: 15 } + } + }, + architecture: { + groups, + groupRelations: groups.slice(1).map((group, index) => ({ fromGroupId: groups[index].id, toGroupId: group.id, kind: 'imports', count: 2 })), + entryPoints, + hotspots: [], + impact: { changedLocators: ['workspace://apps/web/app.js'], representedChangedLocators: ['workspace://apps/web/app.js'], affectedSymbols: [], depth: 2 } + }, + memory: { status: 'available', activeFacts: [], pendingProposals: [{ id: 'mpq_1' }], staleFactCount: 0, conflictingFactCount: 0 }, + readiness: { handoff: { status: 'available', command: 'recall handoff' }, nextCommands: [] }, + safeguards: { readOnly: true, modelCalls: 0, networkCalls: 0, externalWritesEnabled: false } + }; +} + +function changedFixture() { + return { status: 'available', changedLocators: ['apps/web/app.js'], totalCount: 2, omittedCount: 1, truncated: false }; +} + +function handoffFixture() { + return { status: 'verified', createdAt: '2026-07-15T09:58:00.000Z' }; +} +``` + +Define fixture helpers in the same test file with complete repository, support, architecture, memory, safeguards, and readiness fields. + +- [x] **Step 2: Run the model test and verify the missing module** + +Run: + +```bash +node --test tests/web-orientation.test.mjs +``` + +Expected: FAIL with module-not-found for `orientation-model.js`. + +- [x] **Step 3: Implement bounded group and start-point selection** + +In `orientation-model.js`, normalize every external string and array. Cap groups at 12 and relations at 20. Rank start points with explicit reason priority: + +```js +const START_REASON_PRIORITY = new Map([ + ['application_route', 0], + ['package_entry_point', 1], + ['changed_central_module', 2], + ['executable_command', 3], + ['inbound_dependency_hub', 4] +]); +const START_REASON_LABEL = new Map([ + ['application_route', 'application route'], + ['executable_command', 'executable command'], + ['package_entry_point', 'package entry point'], + ['changed_central_module', 'changed central module'], + ['inbound_dependency_hub', 'inbound dependency hub'] +]); + +function rankStartHere(items) { + return [...items] + .filter(({ locator = '' }) => !/(?:^|\/)(?:test|tests|fixtures|generated|vendor)(?:\/|$)/iu.test(locator)) + .sort((left, right) => ( + (START_REASON_PRIORITY.get(left.reasonCode) ?? 99) - (START_REASON_PRIORITY.get(right.reasonCode) ?? 99) + || right.score - left.score + || left.locator.localeCompare(right.locator) + )) + .slice(0, 3) + .map((item) => ({ ...item, reason: START_REASON_LABEL.get(item.reasonCode) ?? 'ranked source entry point' })); +} +``` + +Collapse strongly connected group components before assigning left-to-right topological layers. Sort groups inside a layer by repository-relative prefix. Do not use randomness, measured DOM size, or force layout. + +- [x] **Step 4: Build truthful impact and trust models** + +Expose represented and unrepresented changed counts separately. Map graph snapshot states to `complete`, `partial`, `stale`, `failed`, or `not scanned`. Keep memory counts as active, pending, stale, and conflicting. Include token reduction only when the report contains a measured baseline and `providerBillingClaimed === false`; otherwise set it to `null`. + +Return a frozen model with: + +```js +{ + state, + repository, + coverage, + groups, + relations, + selectedGroupId, + startHere, + impact, + trust: { memory, handoff, source, deliveryReduction }, + primaryAction +} +``` + +- [x] **Step 5: Delegate Overview modeling from `app.js`** + +Replace `buildRecallMapHomeModel()` internals with an import/delegation to `buildOrientationModel()`. Keep a named re-export from `app.js` for compatibility until `tests/web-shell.test.mjs` imports the new module directly. Update `selectOverviewPrimaryAction()` only to consume the new model fields; keep its current route IDs and actions. + +- [x] **Step 6: Verify model and shell behavior** + +Run: + +```bash +node --test tests/web-orientation.test.mjs tests/web-shell.test.mjs +``` + +Expected: deterministic orientation tests and existing shell state tests pass. + +- [x] **Step 7: Commit orientation modeling** + +```bash +git add apps/web/orientation-model.js apps/web/app.js apps/web/shell-model.js tests/web-orientation.test.mjs tests/web-shell.test.mjs +git commit -m "feat: model repository orientation deterministically" +``` + +### Task 3: Render the compact Overview workbench + +**Files:** +- Create: `apps/web/orientation-view.js` +- Modify: `apps/web/index.html` +- Modify: `apps/web/app.js` +- Modify: `apps/web/styles.css` +- Modify: `tests/web-orientation.test.mjs` +- Modify: `tests/web-shell.test.mjs` + +**Interfaces:** +- Produces: `renderOrientation(model): string`. +- Produces: `bindOrientation(root, { onSelectGroup, onRefresh }): () => void`. +- Consumes: `OrientationModel` from Task 2 and primitives from Task 1. + +- [x] **Step 1: Write failing minimalist Overview markup tests** + +Add to `tests/web-orientation.test.mjs`: + +```js +import { readFile } from 'node:fs/promises'; +import { renderOrientation } from '../apps/web/orientation-view.js'; + +test('Overview renders the first-ten-seconds contract without dashboard slop', () => { + const html = renderOrientation(buildOrientationModel({ report: orientationFixture({ groupCount: 8, entryPointCount: 5 }) })); + for (const label of ['Architecture', 'Start here', 'Current impact', 'Trusted context']) assert.match(html, new RegExp(label)); + assert.equal((html.match(/class="orientation-group/g) ?? []).length, 8); + assert.equal((html.match(/class="start-item/g) ?? []).length, 3); + assert.match(html, /aria-label="Repository architecture outline"/); + assert.doesNotMatch(html, /class="metric-strip"/); + assert.doesNotMatch(html, /hero|tagline|AI-powered|intelligent|smart|magical|seamless|unlock|supercharge|next-generation/iu); +}); + +test('repository command bar exposes four deterministic intents', async () => { + const html = await readFile(new URL('../apps/web/index.html', import.meta.url), 'utf8'); + for (const intent of ['explain', 'trace', 'impact', 'handoff']) { + assert.match(html, new RegExp(`value="${intent}"`)); + } + assert.doesNotMatch(html, /chat|ask AI|thinking/iu); +}); + +test('repository truth bar names local and external-write state', async () => { + const html = await readFile(new URL('../apps/web/index.html', import.meta.url), 'utf8'); + assert.match(html, /id="repository-boundary"/); + assert.match(html, /Local only/); + assert.match(html, /External writes off/); +}); +``` + +- [x] **Step 2: Run the test and verify the missing renderer** + +Run: + +```bash +node --test --test-name-pattern="first-ten-seconds" tests/web-orientation.test.mjs +``` + +Expected: FAIL because `renderOrientation` does not exist. + +- [x] **Step 3: Implement semantic orientation markup** + +Render this macrostructure, with no wrapper cards around the three secondary sections: + +```html +
+
+

Overview

+ Partial · scanned 2 minutes ago +
+
+
+ +
+
+``` + +Group nodes are `