diff --git a/AGENTS.md b/AGENTS.md index c15fe43d..1d1666f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,7 @@ - + + ## OpenCodeHub MCP Tools This repository has been indexed by OpenCodeHub. When you are working in this @@ -19,7 +22,7 @@ with the working tree. `codehub status` reports staleness. ## Full MCP surface -The full MCP surface is **28 tools** (see `packages/mcp/src/server.ts`); +The full MCP surface is **29 tools** (see `packages/mcp/src/server.ts`); the 7 listed above are the high-frequency exploration tools. For the full inventory, use the `/opencodehub-guide` skill. @@ -37,12 +40,11 @@ the list was truncated. See ADR 0012 (`docs/adr/0012-repo-as-first-class-node.md`) for the rationale behind `repo_uri` as a first-class node attribute. The -`repo_uri` shape was promoted to a typed graph attribute by AC-M6-1 -(`packages/core-types/src/nodes.ts:524-552`). `group_cross_repo_links` -(the AC-M6-3-reframed MCP tool) and the `group_*` family (AC-M6-4) all -emit `repo_uri` in the same canonical form, so a caller can use any of -those tools' `repo_uri` outputs as input to `AMBIGUOUS_REPO.choices` -retries. +`repo_uri` shape is a typed graph attribute on every `Repo` node +(`packages/core-types/src/nodes.ts`). `group_cross_repo_links` and +the `group_*` family of MCP tools all emit `repo_uri` in the same +canonical form, so a caller can use any of those tools' `repo_uri` +outputs as input to `AMBIGUOUS_REPO.choices` retries. Worked example — error envelope, then retry: diff --git a/CHANGELOG.md b/CHANGELOG.md index 14555a35..9f61e08a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [Unreleased] + +### Fixed + +- **cli:** `scan` ingests SARIF into the scanned repo, not CWD. +- **cli:** `doctor` resolves native bindings from owner workspaces. +- **smoke-mcp:** asserts 29 tools, matching the v1.0 server surface. + +### Docs + +- **repo:** README v1.0 status, 29-tool surface, parse-runtime section, + and accurate 17-package list (drops `eval` / `gym`, adds + `cobol-proleap`, `frameworks`, `pack`, `policy`, `wiki`). +- **adr:** cross-link the two concurrently-numbered ADR 0013 files, + flip 0011 + 0013-m7 status to Accepted, and scrub session-local + spec coordinates from ADR text. +- **repo:** sync `CHANGELOG`, `USECASE`, `AGENTS`, and `OBJECTIVES` + with v1 reality (tool count, language count, package set). + ## [0.1.1](https://github.com/theagenticguy/opencodehub/compare/root-v0.1.0...root-v0.1.1) (2026-04-22) diff --git a/OBJECTIVES.md b/OBJECTIVES.md index 5f7d9ec9..6118a9dc 100644 --- a/OBJECTIVES.md +++ b/OBJECTIVES.md @@ -10,7 +10,7 @@ scope. call.** *Because the README's problem statement is exactly this: grep is textual, language servers are per-file, embeddings are lossy; agents need callers, callees, processes, and blast radius - answered before they write a diff, and the 28-tool MCP surface is + answered before they write a diff, and the 29-tool MCP surface is the primary product.* 2. **Stay Apache-2.0 end-to-end, with every transitive runtime @@ -26,21 +26,22 @@ scope. commit, and `scripts/acceptance.sh` gate 6 gates on exactly that invariant.* -4. **Cover the 14 GA languages with tree-sitter and upgrade five of - them (TypeScript, Python, Go, Rust, Java) with SCIP indexers.** +4. **Cover the 15 GA languages (14 via tree-sitter plus a regex + provider for fixed-format COBOL) and upgrade five of them + (TypeScript, Python, Go, Rust, Java) with SCIP indexers.** *Because heuristic call-graph edges miss cross-module resolution, the `scip-index` phase runs each language's native SCIP indexer once, the `confidence-demote` phase reconciles heuristic and - compiler-grade edges, and the gym harness gates per-language F1 - with SCIP-derived baselines.* + compiler-grade edges, and the gym harness (extracted to a sibling + testbed in M5) gates per-language F1 with SCIP-derived baselines.* ## Quality bar 5. **Hold a three-layer regression gate on every eval and gym run.** - *Because the gym's absolute-F1-floor + relative-F1-delta + per-case - non-regression layering is baked into the harness, and acceptance - gate 9 requires ≥ 40/49 Python-eval cases to pass — soft regressions - are not an option.* + *Because the sibling testbed's absolute-F1-floor + relative-F1-delta + + per-case non-regression layering is baked into the harness, and + acceptance gate 9 requires ≥ 40/49 Python-eval cases to pass — soft + regressions are not an option.* 6. **Fail CI on any non-zero exit.** *Because `pnpm run check` chains lint → typecheck → test → banned-strings and exits on first diff --git a/README.md b/README.md index 9168f1e1..f791c7fc 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ flowchart LR C -->|detect communities + flows| E[Processes / clusters] D --> F[MCP server] E --> F - F -->|28 tools| G[AI coding agent] + F -->|29 tools| G[AI coding agent] ``` ## Design choices worth knowing @@ -71,13 +71,16 @@ flowchart LR | **Local-first, offline-capable** | `codehub analyze --offline` opens zero sockets. Your code never leaves your machine. No telemetry. | | **Deterministic indexing** | Identical inputs produce a byte-identical graph hash. Reproducible. Auditable. Cacheable in CI. | | **MCP-native** | Works out-of-the-box with Claude Code, Cursor, Codex, Windsurf, OpenCode. The MCP server is the primary interface; CLI exists for scripts and CI. | -| **Embedded storage** | DuckDB + `hnsw_acorn` (filter-aware HNSW via ACORN-1 + RaBitQ) + `fts` (BM25). One file. No daemon. No database to operate. | -| **14 languages at GA** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, C, C++, Ruby, Kotlin, Swift, PHP, Dart — via tree-sitter native bindings (WASM fallback for the web surface). | +| **Embedded storage, graph-default** | `@ladybugdb/core` graph engine for the structural store (default at v1) with DuckDB + `hnsw_acorn` (filter-aware HNSW via ACORN-1 + RaBitQ) + `fts` (BM25) for the temporal + retrieval views. Embedded files. No daemon. No database to operate. `CODEHUB_STORE=duck` reverts to the legacy single-file layout. | +| **15 languages at GA** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, C, C++, Ruby, Kotlin, Swift, PHP, Dart, COBOL — tree-sitter for the first 14 plus a regex provider for fixed-format COBOL. | +| **WASM-default parse runtime** | `web-tree-sitter` WASM is the default on Node 22 and Node 24; the native `tree-sitter` N-API addon is opt-in via `OCH_NATIVE_PARSER=1` for Node 22 dev boxes. The complexity phase still uses native where supported and degrades with a one-shot warning otherwise. | ## Quick start -**Requirements:** macOS, Linux, or Windows; Node 20+; pnpm 10+; Python -3.12 (for the eval harness); `mise` recommended to manage them. +**Requirements:** macOS, Linux, or Windows; Node 22 or 24 (Node 22 +recommended for the native-parser opt-in); pnpm 10+; Python 3.12 (only +needed when running the SCIP indexers for Python-heavy repos); +`mise` recommended to manage them. ```bash git clone https://github.com/theagenticguy/opencodehub @@ -106,7 +109,7 @@ codehub analyze # your agent can now call impact, query, context, detect_changes, rename, ... ``` -## MCP tool surface (28 tools) +## MCP tool surface (29 tools) | Tool | Purpose | |---|---| @@ -116,9 +119,10 @@ codehub analyze | `detect_changes` | Git-diff impact — what do your current changes affect | | `rename` | Multi-file coordinated rename with confidence-tagged edits | | `route_map` / `api_impact` / `shape_check` / `tool_map` | HTTP route & MCP tool intelligence | -| `group_query` | BM25-fused search across a group of repos | +| `group_query` / `group_status` / `group_contracts` / `group_cross_repo_links` / `group_sync` / `group_list` | Cross-repo federation — fan out BM25, contracts, and staleness across a named group | | `list_repos` · `sql` | Registry & escape-hatch SQL (read-only, timeout-guarded) | -| …and 17 more | Communities, processes, SBOM, SARIF, verdict, etc. | +| `pack_codebase` | Deterministic Repomix-compatible code pack export | +| …and the rest | `verdict`, `risk_trends`, `project_profile`, `dependencies`, `license_audit`, `owners`, `list_findings`, `list_findings_delta`, `list_dead_code`, `remove_dead_code`, `scan` | Architecture decision records live in [`docs/adr/`](./docs/adr/). A Claude Code plugin at `plugins/opencodehub/` wraps the MCP tools into @@ -126,24 +130,32 @@ slash commands + skills — install via `codehub init`. ## Repository layout -The monorepo is organised as 14 workspace packages under `packages/`: +The monorepo is organised as 17 workspace packages under `packages/`: | Package | Purpose | |---|---| | `analysis` | Heuristic + SCIP call-graph resolution, community + flow detection | -| `cli` | `codehub` command — `init`, `analyze`, `status`, `setup`, scanners | -| `core-types` | Shared TypeScript types, Zod schemas, error codes | -| `embedder` | Embedding backends — local ONNX, HTTP, SageMaker | -| `eval` | Retrieval / graph-quality evaluation harness | -| `gym` | Per-language F1 regression gym with SCIP baselines | -| `ingestion` | Tree-sitter parsers, symbol extraction, import resolution | -| `mcp` | Model Context Protocol server — 28 tools, resources | +| `cli` | `codehub` command — `init`, `analyze`, `status`, `setup`, scanners, group federation | +| `cobol-proleap` | ProLeap-backed deep-parse path for free-format COBOL (regex provider handles fixed-format) | +| `core-types` | Shared TypeScript types, Zod schemas, error codes, canonical `LanguageId` and node/edge kinds | +| `embedder` | Embedding backends — local ONNX, HTTP, SageMaker; deterministic `embedderId` fingerprint | +| `frameworks` | HTTP route + MCP tool detectors used by `route_map` / `api_impact` / `tool_map` | +| `ingestion` | Tree-sitter + WASM parsers, symbol extraction, import resolution, complexity phase | +| `mcp` | Model Context Protocol server — 29 tools, resources, structured error envelopes | +| `pack` | Deterministic Repomix-compatible code-pack generator (M5) | +| `policy` | Allowlist + license-tier policy engine driving `license_audit` and CI gates | | `sarif` | SARIF schema validation and scanner output normalisation | -| `scanners` | Subprocess wrappers for OSV, Semgrep, hadolint, tflint, etc. | -| `scip-ingest` | SCIP indexer runners (TS, Python, Go, Rust, Java) | +| `scanners` | Subprocess wrappers for 20 scanners — OSV, Semgrep, hadolint, tflint, detect-secrets, and the rest | +| `scip-ingest` | SCIP indexer runners (TS, Python, Go, Rust, Java) — emits CALLS, REFERENCES, IMPLEMENTS, TYPE_OF | | `search` | Hybrid BM25 + HNSW (ACORN-1 + RaBitQ) query layer | -| `storage` | DuckDB-backed graph store, deterministic `graphHash` | +| `storage` | `IGraphStore` / `ITemporalStore` adapters — `@ladybugdb/core` (default) and DuckDB; deterministic `graphHash` | | `summarizer` | Process + cluster summaries for MCP responses | +| `wiki` | LLM-narrated module pages emitted by `codehub wiki --llm` | + +The retrieval / graph-quality evaluation harness and the per-language F1 +regression gym used to live here as `eval` and `gym`; they were +extracted into a sibling testbed in M5 so the production package set +ships free of test-time dependencies. ## Embedding backends @@ -199,19 +211,41 @@ for the M3 phase-1 rationale and [`docs/adr/0013-m7-default-flip-and-abstraction.md`](./docs/adr/0013-m7-default-flip-and-abstraction.md) for the M7 default-flip + interface segregation. +## Parse runtime — WASM default, native opt-in + +`@opencodehub/ingestion` defaults to the `web-tree-sitter` (WASM) +runtime on Node 22 and Node 24. The native `tree-sitter` N-API addon +is opt-in on Node 22 dev boxes via `OCH_NATIVE_PARSER=1` (or +`--native-parser` on the `codehub` CLI). Native is not supported on +Node 24 until `node-tree-sitter@0.25.1` lands on npm +([tree-sitter/node-tree-sitter#276](https://github.com/tree-sitter/node-tree-sitter/issues/276)). + +Kotlin, Swift, and Dart use `.wasm` blobs vendored at +`packages/ingestion/vendor/wasms/` and rebuilt via +`bash scripts/build-vendor-wasms.sh` whenever the underlying grammar +versions in `package.json` change. The complexity phase +(cyclomatic-complexity metrics) still uses native tree-sitter where +available; on Node 24 or Node 22 without the opt-in, complexity +extraction degrades with a one-shot stderr warning and all other +parsing continues via WASM. + +See [`docs/adr/0013-parse-runtime-wasm-default.md`](./docs/adr/0013-parse-runtime-wasm-default.md) +for the WASM-default rationale and the Node 24 unblock plan. + ## Status -**v0.1.0 — initial public release.** The codebase is feature-complete -along the scope described below, but the project is brand-new on -GitHub and the API surface is not yet stable. +**v1 — feature-complete on M1–M7.** Tracks A (M7 graph-DB default + the +`IGraphStore` / `ITemporalStore` interface segregation), B (20-scanner +fleet incl. detect-secrets), C (debt sweep — embedder fingerprint, SCIP +REFERENCES + TYPE_OF), and D (dogfood polish) have all merged. The +current shipped tag remains `0.1.1`; `1.0.0` is cut once schema + +tool-surface stability is signed off. While on `0.x`, **any release may contain breaking changes** to the graph schema, MCP tool shapes, CLI flags, or storage layout. Breaking changes are called out with `!` or a `BREAKING CHANGE:` footer in the commit log and summarised in each release's generated CHANGELOG. -`1.0.0` will be cut when we commit to schema + tool-surface stability. - ## Supply-chain posture - **CycloneDX SBOM** at [`SBOM.cdx.json`](./SBOM.cdx.json) (regenerated on every release) diff --git a/docs/adr/0011-graph-db-backend.md b/docs/adr/0011-graph-db-backend.md index 59b129c6..ff7ebfcc 100644 --- a/docs/adr/0011-graph-db-backend.md +++ b/docs/adr/0011-graph-db-backend.md @@ -1,11 +1,14 @@ # ADR 0011 — Graph-DB backend (LadybugDB phase-1) -- Status: **Proposed** — 2026-05-05 (flips to **Accepted** on the M3 merge). +- Status: **Accepted** — 2026-05-05 (Proposed) → flipped on the M3 merge. - Authors: Laith Al-Saadoon + Claude. - Branch: `feat/v1-m3-m4`. - Supersedes nothing. Interacts with ADR 0001 (DuckDB backend stays the default through M6; this ADR records the opt-in second backend and the phased plan to flip the default in M7). +- Followed by ADR 0013 (M7 default-flip + interface segregation), which + records the M7 flip from "DuckDB-default + LadybugDB opt-in" to + "LadybugDB-default with auto-fallback to DuckDB". ## Context diff --git a/docs/adr/0013-m7-default-flip-and-abstraction.md b/docs/adr/0013-m7-default-flip-and-abstraction.md index 278c1d88..8affdae5 100644 --- a/docs/adr/0013-m7-default-flip-and-abstraction.md +++ b/docs/adr/0013-m7-default-flip-and-abstraction.md @@ -1,7 +1,12 @@ # ADR 0013 — M7 default-flip + storage abstraction (LadybugDB phase-2) -- Status: **Proposed** — 2026-05-09 (flips to **Accepted** on the - `feat/v1-finalize-track-a` merge). +> Note: there is a sibling ADR — `0013-parse-runtime-wasm-default.md` — +> that landed concurrently and shares the same number. Both are kept +> in-tree because they were authored in parallel branches and accepted +> on the same release. The next ADR uses 0014. + +- Status: **Accepted** — 2026-05-09 (Proposed) → flipped on the + `feat/v1-finalize-track-a` merge (PR #71). - Authors: Laith Al-Saadoon + Claude. - Branch: `feat/v1-finalize-track-a`. - Supersedes nothing. Extends ADR 0011 (LadybugDB phase-1) by flipping @@ -299,11 +304,6 @@ of `@opencodehub/storage` required. ## References -- Spec: `.erpaval/specs/006-v1-finalize/architecture-revised.md` - §AC-A-1 (interface split), §AC-A-2 (column encoders), §AC-A-3 - (`ITemporalStore` route), §AC-A-6 (108-SQL migration), §AC-A-7 - (parity harness), §AC-A-8 (`describeArtifacts`), §AC-A-9 (this ADR - + the default flip), §AC-A-11 (conformance suite). - Code: - `packages/storage/src/interface.ts` — `IGraphStore` + `ITemporalStore` type definitions; the typed-finder method surface. @@ -328,13 +328,18 @@ of `@opencodehub/storage` required. - `packages/storage/src/interface.test.ts` — interface-level contract assertions. - `packages/storage/src/finders.test.ts` — typed-finder coverage. +- Spec: `.erpaval/specs/006-v1-finalize/architecture-revised.md` + §AC-A-1 (interface split), §AC-A-2 (column encoders), §AC-A-3 + (`ITemporalStore` route), §AC-A-6 (108-SQL migration), §AC-A-7 + (parity harness), §AC-A-8 (`describeArtifacts`), §AC-A-9 (this ADR + + the default flip), §AC-A-11 (conformance suite). - Related ADRs: - ADR 0001 — DuckDB selection. This ADR keeps DuckDB as the temporal store and the legacy graph store; no rip-out. - ADR 0011 — LadybugDB phase-1. This ADR is its M7 follow-up. - ADR 0012 — Repo as a first-class graph node. The M6 federation surface routes through the new typed finders via this ADR's - AC-A-6 migration. + 108-site SQL migration. ## Provenance @@ -356,15 +361,15 @@ checksums because the two artifacts are written by different engines and have different on-disk representations. mtime is the only stable signal. -## Empirical evidence — graphHash parity audit (AC-A-10) +## Empirical evidence — graphHash parity audit The whole-pipeline parity gate is `scripts/m7-parity-audit.sh`. It runs `codehub analyze --force` against the same corpus under `CODEHUB_STORE=duck` and `CODEHUB_STORE=lbug`, then compares the `graph ` summary line emitted by each invocation. This is the -end-to-end companion to the in-memory `assertGraphParity` harness -(AC-A-7); together they pin U1 (graphHash byte-identity) from both -layers — fixtures and a real on-disk analyze. +end-to-end companion to the in-memory `assertGraphParity` harness; +together they pin graphHash byte-identity from both layers — fixtures +and a real on-disk analyze. The script is wired into `scripts/acceptance.sh` as gate 17 (the final gate). Sample outputs follow. diff --git a/docs/adr/0013-parse-runtime-wasm-default.md b/docs/adr/0013-parse-runtime-wasm-default.md index 35fa5f67..0aa009c2 100644 --- a/docs/adr/0013-parse-runtime-wasm-default.md +++ b/docs/adr/0013-parse-runtime-wasm-default.md @@ -1,5 +1,10 @@ # ADR 0013 — Parse runtime: WASM default, native opt-in +> Note: there is a sibling ADR — `0013-m7-default-flip-and-abstraction.md` +> — that landed concurrently and shares the same number. Both are kept +> in-tree because they were authored in parallel branches and accepted +> on the same release. The next ADR uses 0014. + - Status: **Accepted** — 2026-05-08. - Authors: Laith Al-Saadoon + Claude. - Branch: `feat/node24-wasm-default`. diff --git a/docs/adr/0014-scip-references-and-embedder-fingerprint.md b/docs/adr/0014-scip-references-and-embedder-fingerprint.md index 7c6c2d27..869e3e3a 100644 --- a/docs/adr/0014-scip-references-and-embedder-fingerprint.md +++ b/docs/adr/0014-scip-references-and-embedder-fingerprint.md @@ -98,9 +98,14 @@ For Hole B, every existing store needs a `codehub analyze --force` to pick up th ## References -- `.erpaval/specs/006-v1-finalize/spec.md§AC-C-3, §AC-C-5, §E-C-3, §E-C-4, §W-A-2` -- `.erpaval/sessions/session-33f24f/research-detectsecrets-scip.yaml` (SCIP role enum + Relationship message) -- `.erpaval/solutions/architecture-patterns/scip-callee-definition-site.md` -- `.erpaval/solutions/conventions/scip-0-indexed-vs-graph-1-indexed.md` -- `docs/adr/0011-graph-db-backend.md` (M3+M6 IGraphStore precedent) -- `docs/adr/0013-m7-default-flip-and-abstraction.md` (M7 LadybugDB default flip) +- `packages/embedder/src/fingerprint.ts` — `assertEmbedderCompatible`, + the frozen `EMBEDDER_MISMATCH_HINT` string. +- `packages/scip-ingest/src/derive.ts` — REFERENCES emission and the + `is_implementation`/`is_type_definition` collector. +- `packages/ingestion/src/pipeline/phases/scip-index.ts` — `emitEdges` + and the new `emitRelations` sibling. +- `packages/core-types/src/edges.ts` — append-only `RelationType` + union; `TYPE_OF` lands at position 25. +- `docs/adr/0011-graph-db-backend.md` — `IGraphStore` precedent. +- `docs/adr/0013-m7-default-flip-and-abstraction.md` — M7 LadybugDB + default flip. diff --git a/packages/cli/src/commands/bench.test.ts b/packages/cli/src/commands/bench.test.ts index ef794795..c8216786 100644 --- a/packages/cli/src/commands/bench.test.ts +++ b/packages/cli/src/commands/bench.test.ts @@ -7,7 +7,7 @@ * table; * - the script-location fallback when the user passes an explicit * --acceptance path; - * - the gate roster itself (9 gates, stable order). + * - the gate roster itself (17 gates, stable order). */ import { strict as assert } from "node:assert"; @@ -27,15 +27,15 @@ function freshRows(): GateRow[] { })); } -test("MVP_GATES roster has 9 gates in stable order", () => { - assert.equal(MVP_GATES.length, 9); +test("MVP_GATES roster has 17 gates in stable order", () => { + assert.equal(MVP_GATES.length, 17); assert.equal(MVP_GATES[0]?.id, "install"); - assert.equal(MVP_GATES[MVP_GATES.length - 1]?.id, "eval"); + assert.equal(MVP_GATES[MVP_GATES.length - 1]?.id, "m7-parity-audit"); }); test("applyLine flags a gate PASS when banner + marker sequence is seen", () => { const rows = freshRows(); - applyLine(rows, "1/9: pnpm install --frozen-lockfile"); + applyLine(rows, "1/17: pnpm install --frozen-lockfile"); applyLine(rows, " [PASS] install green"); const install = rows.find((r) => r.id === "install"); assert.ok(install); @@ -45,7 +45,7 @@ test("applyLine flags a gate PASS when banner + marker sequence is seen", () => test("applyLine flags a gate FAIL when marker follows banner", () => { const rows = freshRows(); - applyLine(rows, "2/9: pnpm -r build"); + applyLine(rows, "2/17: pnpm -r build"); applyLine(rows, " [FAIL] build failed"); const build = rows.find((r) => r.id === "build"); assert.ok(build); @@ -53,6 +53,16 @@ test("applyLine flags a gate FAIL when marker follows banner", () => { assert.equal(build.detail, "build failed"); }); +test("applyLine flags a gate SKIP when marker follows banner", () => { + const rows = freshRows(); + applyLine(rows, "12/17: scanner smoke (semgrep)"); + applyLine(rows, " [SKIP] semgrep not installed"); + const scanner = rows.find((r) => r.id === "scanner-smoke"); + assert.ok(scanner); + assert.equal(scanner.status, "skipped"); + assert.equal(scanner.detail, "semgrep not installed"); +}); + test("applyLine ignores markers without a preceding banner", () => { const rows = freshRows(); applyLine(rows, " [PASS] orphaned marker"); @@ -65,40 +75,53 @@ test("applyLine ignores markers without a preceding banner", () => { test("applyLine advances through every gate in a typical run", () => { const rows = freshRows(); + // Real banner+marker pairs straight from scripts/acceptance.sh. Titles + // now match MVP_GATES verbatim, so every line should flip its row. const lines = [ - "1/9: pnpm install --frozen-lockfile", + "1/17: pnpm install --frozen-lockfile", " [PASS] install green", - "2/9: pnpm -r build", + "2/17: pnpm -r build", " [PASS] build green", - "3/9: pnpm -r test", + "3/17: pnpm -r test", " [PASS] all package tests pass", - "4/9: banned-strings grep", + "4/17: banned-strings grep", " [PASS] banned-strings clean", - "5/9: license allowlist", + "5/17: license allowlist", " [PASS] licenses within allowlist", - "6/9: determinism (double-run graphHash)", + "6/17: determinism (double-run graphHash)", " [PASS] graphHash identical (abcd1234)", - "7/9: incremental reindex timings", - " [PASS] timings captured (p95 ≤ 5s is a soft target at MVP; see docs)", - "8/9: MCP stdio boot smoke", - " [PASS] MCP server boots and lists 7 tools", - "9/9: Python eval harness (49 parametrized cases)", - " [PASS] eval: 49/49 cases passed", + "7/17: incremental reindex timings", + " [PASS] timings captured", + "8/17: MCP stdio boot smoke", + " [PASS] MCP server boots", + "9/17: Python eval harness (moved to opencodehub-testbed)", + " [SKIP] harness lives in sibling repo", + "10/17: embeddings determinism", + " [SKIP] no embedder weights", + "11/17: incremental timing on 100-file fixture", + " [PASS] p95 within budget", + "12/17: scanner smoke (semgrep)", + " [SKIP] semgrep not installed", + "13/17: SARIF schema validation", + " [PASS] sarif schema valid", + "14/17: license-audit smoke", + " [PASS] audit emitted", + "15/17: verdict smoke (2-commit fixture)", + " [PASS] verdict tier=safe", + "16/17: pack-determinism (code-pack ×2 → diff -r)", + " [PASS] pack identical", + "17/17: m7-parity-audit (analyze ×2 backends → graphHash)", + " [PASS] graph parity holds", ]; - // acceptance.sh titles have different trailing suffixes than MVP_GATES; - // applyLine matches by exact title, so lines that don't match simply - // leave the row pending. Verify that our title catalog is in sync by - // running through the intended titles directly. - for (const row of rows) { - applyLine(rows, `1/9: ${row.title}`); - applyLine(rows, ` [PASS] ${row.id} fake-detail`); - } + for (const l of lines) applyLine(rows, l); + // Every row should be either pass or skipped — no row left pending. for (const row of rows) { - assert.equal(row.status, "pass", `${row.id} should be pass`); - assert.match(row.detail, /fake-detail/); + assert.notEqual(row.status, "pending", `${row.id} should not be pending`); + assert.notEqual(row.status, "fail", `${row.id} should not be fail`); } - // Sanity: the real lines above do not throw. - for (const l of lines) applyLine(rows, l); + // At least one of each terminal status was exercised. + assert.ok(rows.some((r) => r.status === "pass")); + assert.ok(rows.some((r) => r.status === "skipped")); }); test("locateAcceptanceScript honors an explicit --acceptance path", async () => { @@ -134,9 +157,9 @@ test("runBench captures PASS output from a stubbed acceptance script", async () const dir = await mkdtemp(join(tmpdir(), "codehub-bench-stub-")); try { const script = join(dir, "fake.sh"); - // Emit a banner + PASS for each of the 9 gates so every row flips. + // Emit a banner + PASS for each of the 17 gates so every row flips. const body = MVP_GATES.map( - (g, i) => `echo "${i + 1}/9: ${g.title}"\necho " [PASS] fake-${g.id}"`, + (g, i) => `echo "${i + 1}/17: ${g.title}"\necho " [PASS] fake-${g.id}"`, ).join("\n"); await writeFile(script, `#!/usr/bin/env bash\n${body}\nexit 0\n`); await chmod(script, 0o755); @@ -157,9 +180,9 @@ test("runBench reports exitCode=1 when any gate fails", async () => { try { const script = join(dir, "fake.sh"); const lines: string[] = []; - lines.push(`echo "1/9: ${MVP_GATES[0]?.title}"`, `echo " [FAIL] boom"`); + lines.push(`echo "1/17: ${MVP_GATES[0]?.title}"`, `echo " [FAIL] boom"`); for (let i = 1; i < MVP_GATES.length; i += 1) { - lines.push(`echo "${i + 1}/9: ${MVP_GATES[i]?.title}"`, `echo " [PASS] ok"`); + lines.push(`echo "${i + 1}/17: ${MVP_GATES[i]?.title}"`, `echo " [PASS] ok"`); } await writeFile(script, `#!/usr/bin/env bash\n${lines.join("\n")}\nexit 1\n`); await chmod(script, 0o755); diff --git a/packages/cli/src/commands/bench.ts b/packages/cli/src/commands/bench.ts index a0d502a1..3150db9d 100644 --- a/packages/cli/src/commands/bench.ts +++ b/packages/cli/src/commands/bench.ts @@ -56,10 +56,18 @@ export const MVP_GATES: readonly { readonly id: string; readonly title: string } { id: "tests", title: "pnpm -r test" }, { id: "banned-strings", title: "banned-strings grep" }, { id: "licenses", title: "license allowlist" }, - { id: "determinism", title: "graphHash determinism" }, - { id: "incremental", title: "incremental reindex timings (soft)" }, + { id: "determinism", title: "determinism (double-run graphHash)" }, + { id: "incremental", title: "incremental reindex timings" }, { id: "mcp-smoke", title: "MCP stdio boot smoke" }, - { id: "eval", title: "Python eval harness" }, + { id: "eval", title: "Python eval harness (moved to opencodehub-testbed)" }, + { id: "embeddings-determinism", title: "embeddings determinism" }, + { id: "incremental-timing", title: "incremental timing on 100-file fixture" }, + { id: "scanner-smoke", title: "scanner smoke (semgrep)" }, + { id: "sarif-validation", title: "SARIF schema validation" }, + { id: "license-audit-smoke", title: "license-audit smoke" }, + { id: "verdict-smoke", title: "verdict smoke (2-commit fixture)" }, + { id: "pack-determinism", title: "pack-determinism (code-pack ×2 → diff -r)" }, + { id: "m7-parity-audit", title: "m7-parity-audit (analyze ×2 backends → graphHash)" }, ]; /** @@ -205,7 +213,7 @@ function runScript(scriptPath: string): ScriptStream { /** * Apply a single line from `acceptance.sh` to the gate table. We parse - * the `N/9: ` banner line to pick which row the next `[PASS] ...` + * the `N/17: <title>` banner line to pick which row the next `[PASS] ...` * or `[FAIL] ...` marker belongs to. Anything else is ignored (timing * summaries live under a gate row as `......` notes). */ @@ -239,6 +247,16 @@ export function applyLine(rows: GateRow[], rawLine: string): void { currentGateIdx = -1; return; } + const skipMatch = /^\s*\[SKIP\]\s+(.*)$/.exec(line); + if (skipMatch && currentGateIdx >= 0) { + const row = rows[currentGateIdx]; + if (row) { + row.status = "skipped"; + row.detail = skipMatch[1] ?? ""; + } + currentGateIdx = -1; + return; + } } async function waitUntil(predicate: () => boolean): Promise<void> { diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 789e7ba2..670e849a 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -228,10 +228,17 @@ function duckdbWorksCheck(repoRoot: string): Check { hint: "run `pnpm install` at the repo root", }; } + // The @duckdb/node-api 1.x surface exposes Sync teardown helpers + // (`disconnectSync`, `closeSync`). The async `.close()` accessors + // were dropped in 1.0.0; depending on them produced a false FAIL. const mod = (await import(duckPath)) as { DuckDBInstance: { create: (path: string) => Promise<{ - connect: () => Promise<{ close: () => void | Promise<void> }>; + connect: () => Promise<{ + disconnectSync?: () => void; + close?: () => void | Promise<void>; + }>; + closeSync?: () => void; close?: () => void | Promise<void>; }>; }; @@ -239,8 +246,10 @@ function duckdbWorksCheck(repoRoot: string): Check { // In-memory instance: never touches disk, never lingers. const inst = await mod.DuckDBInstance.create(":memory:"); const conn = await inst.connect(); - await conn.close(); - if (typeof inst.close === "function") await inst.close(); + if (typeof conn.disconnectSync === "function") conn.disconnectSync(); + else if (typeof conn.close === "function") await conn.close(); + if (typeof inst.closeSync === "function") inst.closeSync(); + else if (typeof inst.close === "function") await inst.close(); return { status: "ok", message: "duckdb open/close OK" }; } catch (err) { return { @@ -504,6 +513,27 @@ function resolveFromRoot(repoRoot: string, pkg: string): string | null { const req = createRequire(join(repoRoot, "package.json")); return req.resolve(pkg); } catch { - return null; + // fall through to per-package fallbacks } + // 3. Per-workspace fallback. Under pnpm strict isolation, native bindings + // are direct deps of the package that uses them — `tree-sitter*` lives + // in `packages/ingestion`, `@duckdb/node-api` in `packages/storage`. + // Probing those package.json contexts lets `codehub doctor` resolve + // the bindings even when neither the CLI nor the workspace root + // declare them as direct deps. + const owners = + pkg.startsWith("@duckdb/") || pkg.startsWith("@ladybugdb/") + ? ["packages/storage"] + : pkg.startsWith("tree-sitter") + ? ["packages/ingestion"] + : []; + for (const owner of owners) { + try { + const req = createRequire(join(repoRoot, owner, "package.json")); + return req.resolve(pkg); + } catch { + // try next + } + } + return null; } diff --git a/packages/cli/src/commands/scan.ts b/packages/cli/src/commands/scan.ts index 05057867..7027331b 100644 --- a/packages/cli/src/commands/scan.ts +++ b/packages/cli/src/commands/scan.ts @@ -160,9 +160,13 @@ export async function runScan(path: string, opts: ScanOptions = {}): Promise<Sca await writeFile(outputPath, `${JSON.stringify(finalSarif, null, 2)}\n`, "utf8"); // Ingest into the graph (best effort — missing graph is non-fatal). + // Forward the already-resolved `repoPath` so the SARIF lands in the + // SCANNED repo's graph, not the operator's CWD. `runIngestSarif` + // accepts an absolute path in `opts.repo` as a registry-name fallback + // (`ingest-sarif.ts:351-352`), so this works for both `--repo NAME` + // and positional `<path>` invocations. try { - const ingestOpts: { repo?: string; home?: string } = {}; - if (opts.repo !== undefined) ingestOpts.repo = opts.repo; + const ingestOpts: { repo?: string; home?: string } = { repo: opts.repo ?? repoPath }; if (opts.home !== undefined) ingestOpts.home = opts.home; await runIngestSarif(outputPath, ingestOpts); } catch (err) { diff --git a/packages/embedder/src/http-embedder.test.ts b/packages/embedder/src/http-embedder.test.ts index 0f3e5218..31f502aa 100644 --- a/packages/embedder/src/http-embedder.test.ts +++ b/packages/embedder/src/http-embedder.test.ts @@ -23,6 +23,38 @@ import { tryOpenHttpEmbedder, } from "./index.js"; +/** + * Snapshot-and-wipe every `CODEHUB_EMBEDDING_*` env var so tests are + * hermetic against an operator shell that exports `*_SAGEMAKER_ENDPOINT`, + * `*_URL`, `*_MODEL`, etc. Returns a restorer the caller invokes from + * `afterEach` (or a `finally`). Mirrors the existing originalHome pattern + * but covers the full `CODEHUB_EMBEDDING_*` namespace, since selection + * precedence in `tryOpenHttpEmbedder` checks SageMaker env BEFORE the HTTP + * env vars — a leaked `*_SAGEMAKER_ENDPOINT` flips a `null` assertion to + * a `Promise<Embedder>` and the case fails. + */ +function sanitizeEmbeddingEnv(): () => void { + const saved: Record<string, string | undefined> = {}; + for (const k of Object.keys(process.env)) { + if (k.startsWith("CODEHUB_EMBEDDING_")) { + saved[k] = process.env[k]; + delete process.env[k]; + } + } + return () => { + // Wipe any keys the test set so they do not leak across cases. + for (const k of Object.keys(process.env)) { + if (k.startsWith("CODEHUB_EMBEDDING_") && !(k in saved)) { + delete process.env[k]; + } + } + for (const [k, v] of Object.entries(saved)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + }; +} + /** Build a fetch mock that returns a JSON body with the given embedding. */ function makeFetchMockOk(embedding: readonly number[]): typeof fetch { return async (_url, _init): Promise<Response> => { @@ -352,26 +384,13 @@ describe("openHttpEmbedder: malformed body", () => { // ──────────────────────────────────────────────────────────────────── describe("readHttpEmbedderConfigFromEnv", () => { - let originals: Record<string, string | undefined>; + let restoreEnv: () => void; beforeEach(() => { - originals = { - url: process.env["CODEHUB_EMBEDDING_URL"], - model: process.env["CODEHUB_EMBEDDING_MODEL"], - dims: process.env["CODEHUB_EMBEDDING_DIMS"], - key: process.env["CODEHUB_EMBEDDING_API_KEY"], - }; - delete process.env["CODEHUB_EMBEDDING_URL"]; - delete process.env["CODEHUB_EMBEDDING_MODEL"]; - delete process.env["CODEHUB_EMBEDDING_DIMS"]; - delete process.env["CODEHUB_EMBEDDING_API_KEY"]; + restoreEnv = sanitizeEmbeddingEnv(); }); afterEach(() => { - for (const [k, v] of Object.entries(originals)) { - const envKey = `CODEHUB_EMBEDDING_${k === "key" ? "API_KEY" : k.toUpperCase()}`; - if (v === undefined) delete process.env[envKey]; - else process.env[envKey] = v; - } + restoreEnv(); }); it("returns null when URL or MODEL is unset", () => { @@ -429,6 +448,15 @@ describe("readHttpEmbedderConfigFromEnv", () => { // ──────────────────────────────────────────────────────────────────── describe("openEmbedder factory", () => { + let restoreEnv: () => void; + + beforeEach(() => { + restoreEnv = sanitizeEmbeddingEnv(); + }); + afterEach(() => { + restoreEnv(); + }); + it("picks HTTP when endpointUrl is set", async () => { const embedder = await openEmbedder({ endpointUrl: "https://embed.example/v1", @@ -482,22 +510,13 @@ describe("openEmbedder factory", () => { }); describe("tryOpenHttpEmbedder", () => { - let originals: Record<string, string | undefined>; + let restoreEnv: () => void; beforeEach(() => { - originals = { - url: process.env["CODEHUB_EMBEDDING_URL"], - model: process.env["CODEHUB_EMBEDDING_MODEL"], - }; - delete process.env["CODEHUB_EMBEDDING_URL"]; - delete process.env["CODEHUB_EMBEDDING_MODEL"]; + restoreEnv = sanitizeEmbeddingEnv(); }); afterEach(() => { - for (const [k, v] of Object.entries(originals)) { - const envKey = `CODEHUB_EMBEDDING_${k.toUpperCase()}`; - if (v === undefined) delete process.env[envKey]; - else process.env[envKey] = v; - } + restoreEnv(); }); it("returns null when env is not configured", () => { diff --git a/scripts/m7-parity-audit.sh b/scripts/m7-parity-audit.sh index 6e64206a..09288f4e 100755 --- a/scripts/m7-parity-audit.sh +++ b/scripts/m7-parity-audit.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# scripts/m7-parity-audit.sh — graphHash byte-identity audit across backends (AC-A-10). +# scripts/m7-parity-audit.sh — graphHash byte-identity audit across backends. # # Runs `codehub analyze --force` on the same corpus under BOTH: # - `CODEHUB_STORE=duck` → DuckDB legacy graph store @@ -7,8 +7,8 @@ # # Then extracts the `graph <hash>` line from each invocation's stderr and # asserts byte-identity. This is the whole-pipeline end-to-end companion to -# the in-memory `assertGraphParity` harness (AC-A-7) — together they pin the -# U1 (graphHash byte-identity) invariant from BOTH layers: in-memory +# the in-memory `assertGraphParity` harness — together they pin the +# graphHash byte-identity invariant from BOTH layers: in-memory # fixtures AND a real `codehub analyze` against a real corpus on disk. # # Usage: diff --git a/scripts/smoke-mcp.sh b/scripts/smoke-mcp.sh index ec63a8ff..1c3b5496 100755 --- a/scripts/smoke-mcp.sh +++ b/scripts/smoke-mcp.sh @@ -5,14 +5,18 @@ # Uses only node (for the server) and python3 (for JSON parsing) — no extra # dependencies. Safe to run in CI. # -# Tool roster at v1.0 (19 tools): -# Core (7): list_repos, query, context, impact, detect_changes, rename, sql -# Groups (4): group_list, group_query, group_status, group_contracts +# Tool roster at v1.0 (29 tools — see packages/mcp/src/server.ts): +# Core (8): list_repos, pack_codebase, query, context, impact, +# detect_changes, rename, sql +# Groups (6): group_list, group_query, group_status, group_contracts, +# group_cross_repo_links, group_sync # Project (1): project_profile # Dependencies (2): dependencies, license_audit # Ownership (1): owners -# Findings (2): list_findings, scan +# Findings (5): list_findings, list_findings_delta, list_dead_code, +# remove_dead_code, scan # Analysis (2): verdict, risk_trends +# Routing/contracts (4): route_map, api_impact, shape_check, tool_map # # CI / acceptance.sh can override the assertion via the EXPECTED_TOOLS env var # when the wire is mid-migration. @@ -57,7 +61,7 @@ for line in sys.stdin: print(tools) ') -EXPECTED_TOOLS="${EXPECTED_TOOLS:-19}" +EXPECTED_TOOLS="${EXPECTED_TOOLS:-29}" if [ "$COUNT" = "$EXPECTED_TOOLS" ]; then echo "smoke-mcp: PASS ($COUNT tools listed)" exit 0