Commit Graph

44 Commits

Author SHA1 Message Date
Martin Vogel 8eabe191d2 fix(install,daemon): unbreak npx clients, group-writable homes, and legacy updaters
Five field reports in the 24 hours after v0.10.0 all pointed at the same thing:
gates that were right in principle refused real, ordinary setups, and then
failed to say why. Per the consolidated strictness decision, each gate keeps the
protection that matters and drops the part that was refusing legitimate users —
and every refusal now names what it refused and how to proceed.

**Daemon image gate: npx and every ephemeral install path (#1539, #1383).**
The admission check treated "the peer's image hashes differently" and "the
peer's image cannot be examined at all" as one failure. The second is what
`npx codebase-memory-mcp` always produces (ephemeral cache path,
unfingerprintable), so every npx-invoked client was rejected — and, because the
client never reported it, agents saw a transport that closed mid-handshake with
zero bytes on stdout. Reported by @wassolles with the admission path already
read and the fix space mapped.

An unverifiable image is now admitted: the rendezvous HELLO immediately above it
has already proven semantic version, build fingerprint, and protocol/store/
feature ABI, and the image check was trading that real proof for an unavailable
one. It logs daemon.client_image_unverifiable_admitted so the weaker check is
never invisible. A fingerprint MISMATCH — the tamper case the gate exists for —
still rejects hard. Separate test seams keep the two modes testable apart.

**Client bootstrap failures are no longer silent (#1539).**
An MCP client that cannot reach the daemon now emits a JSON-RPC error on stdout
naming the reason, plus the same text on stderr. Previously the reason sat in
bootstrap_result.message and the process exited having written nothing at all.

**POSIX activation: group-writable ancestors (#1535, discussion #1526).**
activation_directory_secure required no group or other write bit on the install
directory AND every ancestor. WSL2 ships ~ and ~/.local at 0775, as do several
distro skeletons and any site using a shared primary group, so install.sh failed
for a large fraction of Linux users — reporting a policy refusal as "activation
transaction I/O failed", which sent reporters after disk errors and filesystem
types. Root-caused by @AmirF194 in a clean ubuntu container; @shochdoerfer and
@iandol confirmed independently.

World-writable ancestors are still refused (any local user could swap a path
component mid-transaction). Group-writable ancestors are now warned about and
admitted. The LEAF directory stays strictly owner-private — that is where the
binary is published, and group write there would let another account replace the
executable between validation and exec. Refusals now name the directory, its
mode, and which rule refused.

**The obsolete ui/standard chooser (#1538, from discussion #1526).**
v0.10.0 consolidated to one archive per platform with the UI always embedded,
but `update` still offered a variant choice: "ui" could only 404, and "standard"
quietly WAS the UI build. Reported by @iandol upgrading 0.9.0 -> 0.10.0. The
chooser, its --standard/--ui flags, and the ui- URL plumbing are removed, along
with the CBM_VARIANT=ui remnant in the npm installer.

Already-released 0.9.x binaries cannot be fixed retroactively, so the release
workflow now publishes byte-identical ui-*-named alias assets — their updaters
work again with no user action. The aliases are uploaded AFTER the VirusTotal
gate: they are the same bytes as archives it already cleared, and uploading them
earlier would duplicate every object in the scan set and the provenance manifest.

**macOS install noise and attribution (#1537).**
install.sh silenced the "No such xattr: com.apple.quarantine" line, which is
what happens when a curl-downloaded archive carries no quarantine attribute —
harmless, and it became the title of a bug report about an unrelated failure.
The session-stop refusal now points at `daemon status` to list the client
processes actually holding the daemon, instead of asserting sessions exist and
leaving the reader to guess. Reported by @listepo.

**Riders.** hatchling is pinned in pkg/pypi (an unpinned backend resolved fresh
inside `python -m build` is what emitted Metadata-Version 2.5 and broke the
v0.10.1 publish); SECURITY.md's supported-versions table moves to 0.10.x.

Tests: separate seams for unverifiable vs mismatched peer images with a test per
outcome; activation refusal must name directory + mode + rule; a group-writable
ancestor must stage successfully. The update tests drop the flag that no longer
exists. Verified against each reporter's environment shape.

**Open security alerts (all three, OSSF Scorecard).**
- HIGH, binary artifact: an 8.8 MB compiled Go ELF wrapper had been committed at
  pkg/go/codebase-memory-mcp by accident. Removed, and both it and its .exe
  sibling are gitignored so `go build` in that directory cannot repeat it.
- HIGH, GHSA-2v37-7h3g-55p8: nanoid < 3.3.17 loops forever when a custom
  generator is called with size 0. It reaches us transitively (postcss -> vite),
  so it is pinned through the existing graph-ui overrides block rather than
  promoted to a direct dependency; the lockfile resolves 3.3.18.
- MEDIUM, unpinned pip command: the publish step installed build/twine by
  version only, leaving the whole transitive graph resolved at run time.
  pkg/pypi/requirements-publish.txt now hash-pins the complete toolchain (316
  hashes), generated on a linux/amd64 python:3.12 image so the wheels match what
  ubuntu-latest resolves, and the step runs pip with --require-hashes. Verified
  by installing from it in that same image.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-11 13:37:32 +02:00
Martin Vogel 63a244a83c Merge pull request #1313 from lukiod/fix/accept-language-q-ranking
fix(graph-ui): rank Accept-Language by q instead of substring match
2026-07-31 02:40:39 +02:00
Martin Vogel 98a2856a90 fix(security): patch postcss and pin the glibc-floor image
Two open security alerts.

Dependabot #13 (GHSA-r28c-9q8g-f849, high): postcss path traversal via
sourceMappingURL auto-loading. Transitive dev dependency of vite, so it never
ships in the binary, but the fix is a clean lockfile bump -- vite requires
^8.5.3 and the patch floor is 8.5.18, so 8.5.24 satisfies it with no dependency
graph change. Verified: npm ci resolves and the UI still builds.

Scorecard #76 (PinnedDependenciesID): Dockerfile.glibc22 used a floating
`ubuntu:22.04`. Every other venue image is digest-pinned; this one was missed.
It matters more here than the checkbox suggests -- that image IS the glibc-floor
assertion, the oldest userland we claim the portable binary runs on. A floating
tag means the floor silently drifts to whatever 22.04 points at, and a floor
that moves is not a floor.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-29 17:04:13 +02:00
lukiod e5c3200f43 fix(graph-ui): rank Accept-Language by q instead of substring match
detectLanguage tested whether "zh" appeared anywhere in the header, so any
visitor whose Accept-Language mentions Chinese got the Chinese UI regardless of
preference.

    en-US,en;q=0.9,zh;q=0.5  ->  zh, expected en
    zh;q=0, en               ->  zh, expected en

The first is a bilingual visitor who prefers English and lists Chinese as a
fallback. The second sets q=0, which RFC 7231 defines as not acceptable.

Now parses q, drops q=0, sorts descending, and matches the primary subtag rather
than a substring. Chinese still wins whenever it is actually preferred.

The two existing assertions are unchanged and still pass.

Signed-off-by: lukiod <mohakgupta0981@gmail.com>
2026-07-28 08:50:52 +05:30
Martin Vogel 0baddf7887 daemon: permanent lifecycle, daemon-backed CLI/hooks, real-Windows hardening, long-path launcher transactions
Daemon lifecycle and Windows correctness, verified on a real Windows 11
ARM64 VM through the maintained test-infrastructure/vm drivers, plus the
macOS and Linux arm64 suites and the container lint gate.

Daemon lifecycle:

- daemon start/stop/status subcommands. `daemon start` launches a
  PERMANENT daemon (spawn shape is byte-exact argv; survives idle
  periods and session ends) and reports an already-active daemon
  instead of failing. Permanence is honored at every stop latch:
  last-committed-client disconnect, host initial-client window,
  coordinator release, and application final-session close — a
  permanent daemon also keeps admitting new sessions after its last
  one closes.
- daemon stop refuses while sessions are active and lists the blocking
  peers (pid/role) that must finish first; an idle daemon drains
  through the activation-shutdown machinery with the ACK ordered after
  connection interrupts. A second stop is idempotent. The wire ops are
  no-cohort first-frame requests with peer fingerprint authentication,
  so stop/status never conflict with an exact-build admission gate.
- One-shot CLI commands now execute through the daemon (index workers
  keep their local supervised path). A cold CLI run that had to spawn a
  temporary daemon prints a hint that `daemon start` removes the
  per-command startup tax; a warm daemon is recycled silently.
- Hooks are connect-only fail-open: with no daemon present the hook
  emits a visible, rate-limited notice (Claude-dialect systemMessage
  plus stderr for other dialects) and always exits 0 — augmentation is
  never allowed to block the caller's tool use.
- Version skew: a newer-build client automatically drains an
  older-build permanent daemon (strict semantic-version triples only;
  dev builds never auto-drain) and the build-conflict message names
  `cbm daemon stop` as the manual escape hatch.

Windows IPC/runtime (real-VM verified):

- ipc(win): persistent pending overlapped ConnectNamedPipe. The accept
  path used to destroy its listening pipe instance on every 20 ms poll
  timeout; a client attaching in the teardown window was severed or left
  on an orphaned pipe object whose HELLO no server handle could ever
  read, absorbing the connect until the client's own timeout expired.
  The pending connect now survives poll timeouts and nothing is
  destroyed while a client could be attaching.
- ipc(win): drain-before-close for final responses. Closing a named-pipe
  server handle can discard a just-sent response before the peer reads
  it (POSIX stream sockets never lose buffered data on close). A bounded
  cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX,
  immediate on interrupted connections) now precedes close in
  runtime_worker_finish and runtime_reject_inline, so hello-conflict,
  capacity and disconnect acknowledgements reliably reach the peer.
- runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no
  transport half-close, so close_begin now announces departure with an
  explicit frame (ordered after APPLICATION_CANCEL, before the local
  interrupt); the server releases the client's admission on receipt
  instead of waiting for the handle to close. Admission-drop timing is
  now identical to POSIX shutdown() semantics on every platform.
- runtime(win): client close cancellation. close_begin serializes with
  request publication under the send lock, best-effort sends the active
  token's APPLICATION_CANCEL frame, then interrupts local I/O; the
  server cancels MCP/subprocess work promptly. Contract tests accept
  both correct outcomes (interrupted transport or decoded CANCELLED).
- runtime: activation acknowledgement ordering. The activation ACK is
  the requester's license to act on "snapshotted and draining", so every
  connection interrupt is now initiated before the ACK is sent; a
  session could previously get one more request serviced after the
  requester observed the ACK.
- service(win): deadline-bounded private-file prepare. The conflict-log
  prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms
  timer granularity) burned ~1.6 s against permanently obstructed paths,
  stalling hello rejections past the client's timeout. The retry budget
  is now a 250 ms deadline; transient share collisions still retry.
- subprocess(win): cmd.exe /C payload encoder quotes metacharacters
  correctly (root cause of the git-on-Windows failure cluster).
- watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native
  Windows stop/unwatch cancellation test with exact-image verification.
- httpd: send_all writes in bounded 64 KiB slices. A single giant
  nonblocking send() on Windows is absorbed wholesale into AFD kernel
  buffering regardless of SO_SNDBUF, so send deadlines and interrupts
  could never engage against a slow peer (and the full payload was
  pinned in nonpaged pool). Slicing restores a deterministic
  backpressure point; a test hook pins SO_SNDBUF for the deadline and
  interrupt tests.
- ui/http: shutdown lifecycle — interrupt checks, response-wide send
  deadline, explicit connection states, refusal to free a server while
  a listener-owned connection is active.

Windows long-path support:

- Central path-aware wide conversion (canonicalize via GetFullPathNameW
  and prepend the extended-length prefix for absolute paths >=240) at
  the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite
  store opens, and the daemon build-fingerprint/log paths. Deep managed
  installs (a 64-hex generation directory routinely exceeds MAX_PATH)
  now index, stage and activate correctly.
- activation transaction: its own file APIs and the component-walking
  ancestry validators now operate in the extended-length namespace;
  the launcher path is canonicalized (and prefixed when deep) once at
  entry so every downstream exact-string comparison stays
  form-consistent.
- Executable self-resolution uses the wide APIs (GetModuleFileNameW,
  GetFileAttributesW) so non-ASCII install paths survive argv[0]
  resolution.

Windows launcher install/uninstall transaction:

- FileRenameInfoEx names are NUL-terminated in an over-allocated
  buffer. FileNameLength governs per the contract, but filter drivers
  read FileName as NUL-terminated and appended adjacent heap bytes to
  created names — a flaky, garbage-suffixed rename target. Both the CLI
  and the launcher rename helpers are fixed.
- Uninstall retires state via rename-aside (.cbm ->
  .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex
  chars so the bare rename target stays under the FileRenameInfoEx
  NT-conversion ceiling at guard depths; 64 bits still uniquely
  identify the generation.
- When the running launcher's mapped generation backings pin .cbm
  against rename, the backings are relocated to activation-<pid>-N
  .retired tombstones beside the install (a mapped image may be renamed,
  never deleted; the launcher's liveness-guarded sweep reclaims stale
  tombstones). Every relocation is recorded, and a FAILED uninstall
  reverses the moves after restoring .cbm — via MoveFileExW with
  extended-length paths on both arguments, since the deep generation
  target is beyond the handle-based rename's bare-path reach — so a
  restored install keeps its generation backings and stays runnable.
- After a committed uninstall the retired tree's backings are relocated
  out so the tree is shallow enough for the detached cleanup's rd, and
  the cleanup's working directory strips the extended-length prefix
  (CreateProcessW lpCurrentDirectory silently ignores prefixed paths).
- Files created under Administrators-default-owner directories
  (CopyFileW destinations, CREATE_NEW tombstones, probe directories)
  are explicitly owner-stamped so the exact-owner validators hold on
  runner images; guard fixtures stamp hand-built trees the same way.

Diagnostics, tests and infra:

- diagnostics: discovery is now an always-delivered JSON control record
  (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and
  paths containing spaces; placement honors $TMPDIR with /tmp fallback
  via a diagnostics-local helper; the soak parser reads the JSON record;
  documented in docs/CONFIGURATION.md. Red-first coverage for suppressed
  log levels, TMPDIR-with-spaces, and native Windows output-contract
  assertions.
- tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint
  parents with production-shaped ancestry (LocalAppData on Windows, via
  th_secure_runtime_parent_new) — the runtime ancestry validation
  correctly refuses temp roots whose ancestors grant mutation rights to
  Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and
  drive the documented startup-owner publication flow before reading
  generation-bound endpoint addresses. This turns the 26 Windows
  failures previously visible in CI's full-test job green without
  weakening any validation.
- tests(win): the launcher guard covers the full permanent-launcher
  contract including failed-uninstall restore and immediate reinstall
  after uninstall; new daemon lifecycle and reworked hook-augment
  guards run the start/recycle/stop flow end to end.
- tests: CBM_SKIP_PERF is now actually consumed by the test runner
  (it was set by CI but never read, so perf suites ran everywhere);
  four throughput/bench suites are classified as perf, the heavy
  store_arch suite moved to the slow-timeout tier, and two
  wall-clock-sensitive assertions were rewritten as invariant checks
  with coarse hang-detector backstops.
- build/test infra: build-dir safety contract, UI dev-proxy security
  contract, soak daemon-recovery contract, path-safety helper, the
  Windows VM worktree-sync contract wired into scripts/test.sh, and
  vm/win.sh guards building its clean embedded-UI product in an
  isolated BUILD_DIR so it cannot clobber the incremental test build.
  provision-windows.sh now installs Node.js for the guards UI build.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-21 15:32:15 +02:00
Martin Vogel 0e00ef5702 feat: coordinate concurrent CBM sessions
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-16 19:20:46 +02:00
ShiroKSH d4b0eeba9b fix(graph-ui): clear ADR content on delete
Signed-off-by: ShiroKSH <kushidashiro@gmail.com>
2026-07-09 21:31:28 +03:00
Martin Vogel 3eb294fa11 fix(ui): serve the upstream issues URL from the backend
DCO / dco (push) Has been cancelled
The UI security audit (security-ui.sh, layer A1) forbids hardcoded
external URLs in graph-ui source — the callout's GitHub issues link
tripped it on all pr-smoke legs. Follow the established pattern for
external targets (/api/repo-info deep-links): /api/ui-config now
carries upstream_issues_url and the callout consumes it, rendering the
issue button only when the backend provides an https URL. The protocol
check uses a regex literal on purpose: a bare protocol string in source
also aborts the audit's URL extraction.

Verified: security-ui.sh passes locally; /api/ui-config serves the URL;
the callout opens with the prefilled link in the browser; frontend
build + tests green.

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 03:38:34 +02:00
Martin Vogel 660b90d94a feat(ui): report-an-edge-case callout when the missed skeleton is selected
Clicking a node of the missed skeleton now opens a dedicated right-panel
callout instead of the standard node panel (code snippet and
callers/callees are meaningless for a not-fully-indexed file):

- explains the gap in plain words (best-effort detection; the file
  content itself is ground truth)
- asks the user to have their agent summarize what fails to parse and
  report it upstream so the edge case can be handled
- two working actions: a prefilled upstream GitHub issue (title
  'Indexing gap: <file>'; body carries ONLY the file path and project
  name, with an explicit add-snippets-only-if-shareable note) and a
  copy-to-clipboard agent prompt (index_status -> summarize flagged
  ranges -> file the issue) with visible copied feedback

Verified in the browser: skeleton click opens the callout with both
actions; frontend build + tests green.

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 02:04:58 +02:00
Martin Vogel afc649f4e8 feat(ui): missed skeleton beside the code galaxy with click-to-focus
Render the miss graph side by side with the code graph instead of
swapping layouts:

- /api/layout (code graph) now also attaches "missed_graph": {nodes,
  edges, offset} — the shadow-project layout placed below the primary
  cluster (same satellite pattern as linked_projects; -Y slot so
  cross-repo satellites collide last). graph=missed stays the isolated
  view for API users
- the UI paints the skeleton white and ghostly beside the galaxy,
  auto-frames BOTH clusters on load, and navigates naturally: clicking
  the skeleton flies the camera into it (file labels + detail panel),
  clicking a code node flies back to the code side, and clicking empty
  space while the skeleton has focus returns to the overview (the
  galaxy can be entirely off-screen at that point)
- the sidebar toggle now shows/hides the skeleton (with a missed-file
  count) rather than swapping the layout

Verified in the browser: overview composition, skeleton focus with
split.c labeled, and the empty-space return flight; ui/httpd suites and
frontend build + tests green.

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-08 20:07:01 +02:00
Martin Vogel 664313e4ac feat(index): missed graph in UI + read-time coverage hook + recovery subtraction
Four refinements to the coverage signal:

- extraction: subtract DEFINITE recovery before flagging. Tree-sitter
  error recovery plus the ERROR-descending def walker often re-extract
  constructs inside a failed region (verified: `def broken(:` comes
  back as a def); a region whose every line is covered by definitions
  that START inside it is not a miss, and a fully recovered file is not
  flagged at all. Container defs (Module/Package) don't count as
  evidence, and partially covered regions stay flagged — the
  #ifdef-split case keeps its flag because the first branch's function
  is genuinely lost
- naming: the query_graph option is graph="missed" (the graph shows
  ONLY misses — "coverage" was misleading); shadow project renamed to
  "<name>::missed"; tool descriptions updated so agents discover both
  the option and its semantics
- hook: the CLI-installed PreToolUse augmenter now also matches Read
  and injects a coverage note when the file being read is listed as not
  fully indexed ("line ranges X-Y could not be parsed — the file
  content you are reading is ground truth"). Safe against the old
  issue-362 hazard: the augmenter is structurally non-blocking (always
  exit 0, additionalContext only), mirroring the Gemini matcher that
  already includes read_file; matcher upgrade bookkeeping updated
- ui: "Missed files" toggle in the graph sidebar renders the miss
  graph as a second graph option — /api/layout gains graph=missed
  (same db file, shadow-project scoping; base project name validated
  as before)

Tests: recovery-subtraction cases (recovered def unflagged, garbage
region flagged, trailing recovered defs keep the flag), CLI matcher
tests updated, e2e resilience fixtures switched to an unrecovered
miss; verified end-to-end: hook emits the note for a flagged file and
stays silent otherwise, /api/layout?graph=missed serves the miss graph
and the code graph is unchanged, frontend build + tests green.

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-08 19:13:25 +02:00
Martin Vogel 8e65f8f1be feat(graph-ui): density-aware contrast + Display controls
DCO / dco (push) Has been cancelled
High-density graphs washed out to a cyan-white blob: edges blend additively
and the per-node boost rewarded luminance, so ~80k overlapping edges plus the
white/yellow mid-tier stars saturated the center.

Compensate by density so contrast stays roughly constant as the graph grows:
- edges dim by ~1/sqrt(edgeCount) (they cause the blob) while nodes and bloom
  stay at full strength up to 25k nodes and only ease gently past that, so the
  bright-star look is preserved on moderate graphs;
- the per-node glow is now colour-aware by channel dominance instead of
  luminance: blue hubs (high-degree) glow brightest, red leaves modestly,
  white/yellow least — matching the star-class meaning and giving the graph
  depth instead of a uniform white core.

Add a Display menu (edge brightness / node glow / bloom multipliers, 0–3×,
persisted) layered on top of the adaptive defaults, so the look is tunable
live. A selection is never density-scaled and stays bright against the
dimmed rest.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-07 23:34:22 +02:00
Martin Vogel 9969253bdc feat(graph-ui): type-in node budget with live loading progress
Add a node-budget control next to Refresh: type any count (snapped to
5,000 steps, 5,000 default, 10M max) and the graph reloads with that many
nodes plus every edge between them. The choice persists per project.

Loading gets real feedback: the layout response streams with a live
MB counter under an animated constellation (static under
prefers-reduced-motion), replacing the bare spinner.

Rendering scales with the budget: instance matrices are rebuilt only when
the node set or highlight changes instead of every frame, sphere
tessellation steps down as counts grow, and past 75k nodes the cloud
switches to soft point sprites — one position per node — so six-figure
budgets stay interactive. The truncation notice now names the loaded edge
count and points at the budget control.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-07 23:09:20 +02:00
Martin Vogel c091ffdb05 fix(graph-ui): avoid external-URL literals in doc comments + test fixtures (UI security audit)
DCO / dco (push) Has been cancelled
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 23:00:50 +02:00
Martin Vogel ca261d44c1 Merge remote-tracking branch 'origin/main' into distill/789-deadcode-ui
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	graph-ui/src/components/FilterPanel.tsx
2026-07-04 22:45:07 +02:00
Martin Vogel a27680c3a9 feat(graph-ui): dead-code filtering, node code preview, GitHub deep-links
Distilled from #789 (safe features only). Adds backend dead-code classification
(status + in_calls in the layout JSON), a GET /api/repo-info endpoint for GitHub
deep-links, and the frontend dead-code filters, node code preview, and deep-links.

The render-cap revert was dropped (kept 2000 for DEFAULT_MAX_NODES/HARD_MAX_NODES
and GRAPH_RENDER_NODE_LIMIT); the sidebar regex-search refactor was omitted.

Security fixes over the original: repo-info strips credentials from any returned
remote_url; the legitimate https blob-URL construction is allow-listed so the
static gate passes; libgit2 is not re-initialized/shutdown per request (reuses the
process-wide init from cbm_alloc_init); deep-link path segments are URL-encoded.

Co-authored-by: Andy Zehady <azehady@ciroos.ai>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 22:19:53 +02:00
Martin Vogel 9987a4eae0 Merge pull request #740 from rarepops/feat/graph-ui-routing-and-filter-ux
feat(graph-ui): URL routing, filter panel UX, and camera fly-to fix
2026-07-04 21:36:29 +02:00
Martin Vogel 093eaf17de Merge pull request #839 from sahil-mangla/main
test(graph-ui): fix locale-dependent number-format assertion in GraphTab.test.ts
2026-07-04 19:34:33 +02:00
Zadak c751240dc0 fix(graph-ui): keep the camera on the focused node after fly-to
The fly-to animation moved only the camera and called lookAt, leaving the OrbitControls pivot at the origin, so OrbitControls re-centred the view on the next frame and snapped the camera back once the animation ended.

Lerp the controls target to the focus point as well so the view stays on the clicked node or cluster and orbits around it afterwards.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 16:47:06 +02:00
Zadak dc8c5be587 fix(graph-ui): clarify graph HUD labels and empty state
Rename the ambiguous Clear button to Clear selection so it reads distinctly from Refresh, and point the empty-state hint at the Projects tab instead of the renamed Stats tab.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 16:47:06 +02:00
Zadak 0c7883eb2f feat(graph-ui): sync active tab and project to the URL
Store the active tab and selected project in the URL query string so the view survives refreshes and can be bookmarked or shared, syncing back on browser back and forward via popstate.

Query params are used rather than path segments because the embedded server only serves index.html at the root path. Opening the Projects tab now clears the active project, and the Graph tab stays disabled until a project is selected.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 16:47:06 +02:00
Zadak 8ea42911a4 feat(graph-ui): scrollable filter panel with clearer grouping
Cap the filter panel height and move the node and edge chips into a scroll area so a long type list no longer pushes the folder tree off screen.

Regroup the chips under Node types and Relationships, pin the show-labels toggle as a footer, and add a Folders heading (en and zh) above the folder tree so the section is clearly labelled.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 16:47:06 +02:00
Martin Vogel c7374135ac Merge pull request #847 from DeusData/distill/ui-label-and-ts-repro
fix(graph-ui): honest Project-ID label; RED repro for TS inherited-method gap
2026-07-04 15:31:27 +02:00
Martin Vogel b56cb592e9 fix(graph-ui): label Project name honestly as permanent ID
The New Index modal presented the project name as an optional display
name, but the value becomes permanent identity: the database filename,
the projects primary key, and the QN prefix of every node. It can never
be renamed afterwards.

Implements the decision from the #805 review: keep the field, make it
honest. The label now reads "Project ID (optional — permanent, cannot
be renamed)", the placeholder and a new help line explain that the ID
becomes the database name and query prefix and that leaving it blank
derives it from the path. Both EN and zh locales updated, test asserting
the old label adjusted.

Co-authored-by: rarepops <rarepops@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 11:35:41 +02:00
sahil-mangla 00b2a0e044 test(graph-ui): fix locale-dependent number-format assertion in GraphTab.test.ts
Pin formatGraphLimitNotice to 'en-US' to resolve unit test failures under non-en_US (e.g. German de_*) locales.

Fixes #825

Signed-off-by: sahil-mangla <manglasahil2017@gmail.com>
2026-07-04 14:32:29 +05:30
Zadak d72a84ff40 fix(graph-ui): scope typed-path picker refresh to Windows drive paths
The debounced folder-list refresh (and Enter-to-navigate) on the Repository path field now only fires for Windows drive paths (e.g. 'D:/'), where typing is the mechanism for switching drives. POSIX path navigation is left exactly as before.

Add a regression test asserting a typed POSIX path does not trigger a re-browse.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 09:39:28 +02:00
Zadak b15dff53a4 fix(graph-ui): drop the empty '/' root on Windows, offer the drive instead
On Windows the POSIX '/' quick-jump root is meaningless (browsing it returns an empty listing), yet the picker showed a '/' button whenever the backend did not enumerate drives (older builds return roots=['/']). Clicking it stranded the user on an empty view.

Derive Windows-aware quick-jump roots: drop non-drive roots and always include the current drive (parsed from the browsed path), so the button lists the drive root. Other drives remain reachable by typing. Add a regression test.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 09:39:28 +02:00
Zadak f50bf902aa fix(graph-ui): refresh folder list when a path is typed in the picker
Typing a path into the Repository path field updated currentPath (and thus the breadcrumb) but never re-fetched the directory listing, so switching drives by typing (e.g. 'D:/') left the previous location's folders showing. This is the only way to change drives when the backend does not enumerate drive roots.

Debounce a silent /api/browse refresh when the typed path changes (keeping the last good listing instead of flashing errors mid-typing), and navigate immediately on Enter. Add a regression test.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 09:39:28 +02:00
Zadak 4efa22cd95 fix(graph-ui): navigate Windows drive breadcrumbs to real paths
The index file picker built breadcrumb targets as '/' + segments, so on a Windows drive path (C:/Users/rap) clicking a crumb browsed to '/C:/...', which the backend rejected as 'not a directory'. Only the '.. (up)' button worked.

Build drive-aware crumb targets (C:/, C:/Users) and drop the bogus unified '/' root crumb on Windows drive paths; POSIX behavior is unchanged. Add a regression test for Windows breadcrumb navigation and cleanup() for test isolation.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-07-04 09:39:28 +02:00
Martin Vogel cfc73f8d0b fix(graph-ui): surface indexing-job failures instead of silently completing
The backend /api/index-status reports status:"error" plus an error
message for failed indexing jobs, but IndexProgress treated any
non-"indexing" state as successful completion: the spinner vanished
with no feedback (e.g. after an OOM-killed indexer subprocess), the
project never appeared, and no error was shown.

Render a visible error banner (path + error text) with a Dismiss
button instead, and keep the success flow unchanged. Beyond the
original PR:

- Restore the empty-jobs guard the PR dropped: the backend keeps
  finished jobs listed as "done"/"error" (handle_index_status only
  skips idle slots), so an empty list mid-index only occurs on
  transient state loss and must not be treated as completion.
- Route the new user-facing strings through the i18n system
  (projects.indexingFailed, common.dismiss; EN + zh entries).
- Cover error banner + dismiss, success flow and the empty-jobs
  guard with vitest cases on the now-exported IndexProgress.

Distilled from #549.

Refs #524

Co-authored-by: sahil-mangla <manglasahil2017@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:28:27 +02:00
Zadak a0e970d78d fix(graph-ui): keep filter sidebar visible when all nodes are filtered out
Clicking "None" (disable all filters) clears every label and edge type,
making filteredData.nodes empty. The early return then replaced the whole
layout — including the left filter sidebar — with a centered "All nodes
filtered out / Reset Filters" message, leaving no way to re-enable
individual filters without resetting everything.

Narrow the early return to the genuinely-empty / no-data case and render
the "all filtered out" state inside the graph area instead, so the
FilterPanel and Sidebar stay mounted. As a side effect, a project with
zero nodes now correctly reads "No nodes in this project" rather than
"All nodes filtered out".

Add a regression test that disables all filters via "None" and asserts the
Filters sidebar remains in the document.

Signed-off-by: Zadak <rarepops@protonmail.com>
2026-06-30 21:27:41 +02:00
Martin Vogel d5414111b3 feat(ui): add i18n and index picker UX
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:21:32 +02:00
Martin Vogel d9d70656b4 fix(ui): cap graph rendering and index spawn
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 20:57:14 +02:00
Martin Vogel a3336bf346 chore(security): resolve flagged Scorecard + Dependabot alerts
- graph-ui: bump vite ^6.4.2 -> ^6.4.3 and add overrides pinning the transitive
  form-data >=4.0.6 and @babel/core >=7.29.6 (all dev-scope build/test deps, not
  shipped in the binary). Clears the 4 open Dependabot alerts and Scorecard's
  VulnerabilitiesID. `npm audit` now reports 0 vulnerabilities.
- codeql.yml: move `security-events: write` from the workflow top level to the
  `analyze` job (top level is now `contents: read`), resolving Scorecard's
  TokenPermissionsID least-privilege finding. CodeQL still uploads results from the
  job-scoped token.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-24 00:34:05 +02:00
Nan 550a2cc65c fix(ui): render graph labels without remote font fetches (#496)
Signed-off-by: Nan <630971265@qq.com>
2026-06-22 22:04:54 +02:00
Martin Vogel 952eea9f1f Extend the license gate to ancestry coverage and the UI dependency tree
Structural layer: drop the hardcoded component list — any directory
containing vendored source or data files must now be covered by a
license file in itself or an ancestor within the vendored tree, so
newly vendored code without a license fails immediately. New third
layer: the graph-UI npm production tree is resolved (postinstall
scripts disabled) and every bundled package must be on the policy
allow-list; unknown licenses fail. Reclassifies @tailwindcss/vite as a
devDependency — it is a build plugin and was inflating the production
tree with native tooling.
2026-06-12 13:03:12 +02:00
Martin Vogel 64be28083e fix(deps): bump vitest 3 -> 4.1.8 (GHSA-5xrq-8626-4rwp)
Resolves the critical Dependabot/Scorecard alert for vitest <4.1.0 (UI server
arbitrary file read/exec). graph-ui has no test files, so the major bump is
risk-free here (CI uses 'vitest run'); lockfile regenerated, vitest 4.1.8.
2026-06-01 22:57:54 +02:00
Martin Vogel d1b676fc0d fix(deps): bump ws to 8.21.0 (CVE-2026-45736, GHSA-58qx-3vcg-4xpx)
ws 8.19.0 had a moderate uninitialized-memory-disclosure issue in websocket.close() (fixed in 8.20.1). Transitive dev dependency; the existing ^8.18.0 range already permits the fix, so this is a lockfile-only bump. npm audit now reports 0 vulnerabilities.
2026-05-29 00:36:24 +02:00
Martin Vogel 7290c1fd2b feat(ui): satellite galaxy radius spacing + cross-galaxy edge rendering
Two visualization gaps in the cross-repo graph view:

1. Fixed LAYOUT_GALAXY_SPACING=600 buried satellite galaxies inside the
   primary cluster on any non-trivial project — a 1000-node primary
   cluster has bounding radius ~1500. Added layout_radius() (max distance
   from origin across all node positions) and compute the per-satellite
   distance as primary_radius + sat_radius + LAYOUT_GALAXY_PAD, with the
   old constant retained as a lower bound for tiny projects.

2. cross_edges was hard-coded to an empty array (TODO placeholder), so
   inter-galaxy CROSS_* edges never rendered even when the matcher had
   produced them. Populate from a join on the source store's CROSS_*
   edges → Route node qualified_name, then resolve the canonical QN in
   the linked store to get its node id. The Route QN is the cross-repo
   matching contract; properties.target_function isn't unique.

Required keeping both `store` and `lp_store` open through the linked-
projects loop instead of closing them right after layout compute.
Added cbm_store_close to all early-error paths to balance the lifetime.

Frontend side:
- EdgeLines accepts an optional targetNodes prop so a single component
  can render edges with source in one node array and target in another
  (offset-adjusted satellite nodes).
- GraphScene renders an EdgeLines layer per linked project for its
  cross_edges, using primary nodes as source and offsetNodes as target.
- GraphTab folds linked-project labels and edge types into the filter
  init / enableAll / filteredData paths so cross_edges respect the
  current filter state.
- Edge color palette extended with GRPC/GRAPHQL/TRPC/CROSS_* tones.

Co-authored-by: sponger94 <45746997+sponger94@users.noreply.github.com>
2026-05-10 18:56:30 +02:00
Martin Vogel 4fdcdd4239 fix(security): close open scanner alerts
Dependabot/CodeQL #33 — postcss XSS GHSA-qx2v-qp2m-jg93:
  npm audit fix in graph-ui (8.5.8 -> 8.5.14, above the <8.5.10 vuln range)

CodeQL #39 — TOCTOU race in artifact.c ensure_gitattributes():
  Replace stat() + fopen() with open(O_WRONLY|O_CREAT|O_EXCL).
  Atomic create-only-if-absent closes the check-vs-write window.
  Falls through to merge driver setup if file already exists.

CodeQL #55 — pip install not pinned in release.yml:
  Pin build==1.3.0 and twine==6.2.0. Comment explains why
  --require-hashes is not used (transitive-deps overhead).

Dismissed (won't-fix):
  - #56 contents: write — required for 'gh release edit --draft=false';
    no narrower permission exists.
  - #54-51 Crystal grammar warnings — vendored upstream code.
  - #50-40 Agda grammar warnings — vendored upstream code.
2026-05-05 00:51:52 +02:00
test b6eefe014c Add multi-galaxy UI layout and cross-repo architecture summary
Backend (http_server.c):
- Layout API detects CROSS_* edges and loads linked project graphs
- Each linked project computed at radial offset as satellite galaxy
- Response includes linked_projects with nodes, edges, offset

Frontend:
- LinkedProject type in types.ts
- GraphScene renders satellite galaxies at offset positions
- NodeCloud/EdgeLines accept opacity prop for dimmed rendering

Tool integration (mcp.c):
- get_architecture includes cross_repo_links summary
- Extract append_cross_repo_summary helper for linter compliance
2026-04-15 23:53:42 +02:00
DeusData e21eba6348 Bump vite to 6.4.2 to fix CVE GHSA-4w7w-66w2-5vf9 and GHSA-p9ff-h696-f583
Fixes arbitrary file read via dev server WebSocket and path traversal
in optimized deps .map handling.
2026-04-12 19:06:28 +01:00
Martin Vogel 1ad2568d66 Fix smoke 8o: OpenCode command array + update picomatch (dependabot) 2026-03-25 23:08:02 +01:00
Martin Vogel 18fa9979ee Replace vmem with mimalloc global allocator, add extraction-phase prescan, fix __init__.py QN collision
Memory management:
- Vendor mimalloc v2.1.9 as global allocator (MI_OVERRIDE=1 in prod)
- New mem.h/mem.c: RSS-based budget tracking via mi_process_info()
- Remove vmem.c/vmem.h (mmap-based budget tracking)
- Remove slab tier2 bump allocator (~300 LOC); >64B goes to mimalloc
- Slab tier1 pages from malloc (= mimalloc) instead of vmem
- Arena blocks from malloc instead of vmem
- Budget raised from 35% to 50% RAM (no more untracked C++ heap)

Extraction-phase prescan (eliminates disk re-reads):
- HTTP call sites: keyword check + URL extraction during extraction
- HTTP routes: decorator + source-based extraction during extraction
- Config file refs: regex scan during extraction
- httplinks: 41.8s → 13ms on Linux kernel (3,212x faster)
- configlink: 41.4s → 0.8s on Linux kernel (54x faster)
- Linux kernel fast-mode total: 2m38s → 1m18s

Bug fixes:
- __init__.py Module QN no longer collides with Folder QN
- index.ts same fix for JS/TS packages
- 13 regression tests for QN collision at FQN + extraction layers
- search_graph/search_code default limit raised from 10 to 500k
- Resolve all clang-tidy, cppcheck, and clang-format warnings

Repo cleanup:
- tree-sitter-form, tree-sitter-magma moved to tools/
- .gitignore: build/, node_modules/, graph-ui/dist/, TEST_PLAN.md
2026-03-17 22:05:57 +01:00