Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ review_cycle_days: 21
### kb-server

- Exposes health/readiness and notes/publish APIs.
- Exposes retrieval/context APIs for server-side note discovery and bundling.
- Enforces path + extension safety for note files.
- Routes writes by source:
- `source=api`: queued to PR branch workflow.
Expand Down Expand Up @@ -76,6 +77,13 @@ review_cycle_days: 21
3. API returns composed content + source branches.
4. Writes to `view=current` are rejected.

### Retrieval Flow

1. Client requests search or a context bundle.
2. `kb-server` builds a deterministic note graph over the visible view.
3. Results return ranked note candidates, excerpts, and provenance.
4. Content remains read-only until a later explicit write operation.

## Invariants

- `main` remains approved truth.
Expand All @@ -91,4 +99,3 @@ review_cycle_days: 21
- `docs/product-specs/vault-sync.md`
- `docs/SECURITY.md`
- `docs/RELIABILITY.md`

2 changes: 2 additions & 0 deletions docs/RELIABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ review_cycle_days: 21
## Service Expectations

- `kb-server` readiness requires database and Git-backed vault access.
- `kb-server` retrieval endpoints should rebuild or refresh in-process graph state when visible note state changes.
- Autosave worker should tolerate transient Git/network failures.
- `vault-sync` should converge after temporary API outages.

Expand All @@ -31,6 +32,7 @@ review_cycle_days: 21
## Reliability Signals

- API health and readiness checks.
- Retrieval endpoint latency/error rate and cache rebuild logs.
- Job/event tables for write and publish operations.
- Sync logs for pull/push loop success and retries.

Expand Down
17 changes: 17 additions & 0 deletions docs/exec-plans/active/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
owner: platform
status: draft
last_verified: 2026-03-12
source_of_truth:
- ../completed/README.md
related_code:
- ../../../scripts/docs_lint.py
related_tests:
- ../../../kb-server/tests
- ../../../mcp-server/tests
review_cycle_days: 30
---

# Active Plans

Place in-progress execution plans here while work is underway.
9 changes: 6 additions & 3 deletions docs/generated/api-surface.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
owner: platform
status: generated
last_verified: 2026-03-07
last_verified: 2026-03-12
source_of_truth:
- ../../kb-server/app/api/routes/health.py
- ../../kb-server/app/api/routes/context.py
- ../../kb-server/app/api/routes/notes.py
- ../../kb-server/app/api/routes/publish.py
related_code:
Expand All @@ -15,16 +16,18 @@ review_cycle_days: 7

# API Surface (Generated)

Generated on `2026-03-07` from route handlers.
Generated on `2026-03-12` from route handlers.

| Method | Path |
| --- | --- |
| `GET` | `/health` |
| `GET` | `/ready` |
| `POST` | `/search` |
| `POST` | `/bundle` |
| `GET` | `/` |
| `GET` | `/{path:path}` |
| `PUT` | `/{path:path}` |
| `DELETE` | `/{path:path}` |
| `POST` | `/publish` |

Do not edit manually. Regenerate with `python3 scripts/generate_context_artifacts.py`.
Do not edit manually. Regenerate with `python3 scripts/generate_context_artifacts.py`.
6 changes: 3 additions & 3 deletions docs/generated/env-catalog.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
owner: platform
status: generated
last_verified: 2026-03-07
last_verified: 2026-03-06
source_of_truth:
- ../../kb-server/.env.example
- ../../kb-server/app/core/config.py
Expand All @@ -16,7 +16,7 @@ review_cycle_days: 7

# Environment Catalog (Generated)

Generated on `2026-03-07` from settings and env sources.
Generated on `2026-03-06` from settings and env sources.

## kb-server `.env.example`

Expand Down Expand Up @@ -70,4 +70,4 @@ Generated on `2026-03-07` from settings and env sources.
| `sync_debounce_seconds` | `2.0` |
| `sync_pull_interval_seconds` | `30.0` |

Do not edit manually. Regenerate with `python3 scripts/generate_context_artifacts.py`.
Do not edit manually. Regenerate with `python3 scripts/generate_context_artifacts.py`.
4 changes: 3 additions & 1 deletion docs/product-specs/kb-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Provide a file-first API over a Git-backed vault with explicit approval boundari
- `GET /notes` and `GET /notes/{path}` support:
- `view=main` (default approved state)
- `view=current` (approved + pending composed state)
- `POST /context/search` returns ranked note candidates for a query.
- `POST /context/bundle` returns a token-bounded context bundle with excerpts, optional full content, and provenance.
- `PUT /notes/{path}` and `DELETE /notes/{path}` support:
- `source=api` for PR-based pending writes
- `source=human` for direct approved writes
Expand All @@ -42,11 +44,11 @@ Provide a file-first API over a Git-backed vault with explicit approval boundari
- Allowed file extensions: `.md`, `.markdown`, `.txt`.
- No absolute paths and no traversal outside vault root.
- API key auth enforced when configured.
- Retrieval is read-only and respects the same view/provenance semantics as note reads.

## Related Operational Docs

- `../../kb-server/README.md`
- `../../kb-server/BRANCHING_AND_CURRENT_VIEW.md`
- `../SECURITY.md`
- `../RELIABILITY.md`

103 changes: 103 additions & 0 deletions kb-server/app/api/routes/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from enum import Enum

from fastapi import APIRouter, Depends, HTTPException

from app.api.deps import require_api_key
from app.schemas.context import (
ContextBundleItem,
ContextBundleRequest,
ContextBundleResponse,
ContextSearchRequest,
ContextSearchResponse,
ContextSearchResult,
)
from app.services import retrieval_service

router = APIRouter(
prefix="/context",
tags=["context"],
dependencies=[Depends(require_api_key)],
)


class ViewType(str, Enum):
main = "main"
current = "current"


@router.post("/search", response_model=ContextSearchResponse)
def search_context(body: ContextSearchRequest):
if body.view not in {ViewType.main.value, ViewType.current.value}:
raise HTTPException(status_code=400, detail="Unsupported view")

query = body.query.strip()
if not query:
raise HTTPException(status_code=400, detail="Query must not be blank")

try:
results = retrieval_service.search_notes(
query=query,
view=body.view,
limit=body.limit,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))

return ContextSearchResponse(
query=query,
view=body.view,
results=[
ContextSearchResult(
path=result.path,
title=result.title,
score=result.score,
reasons=result.reasons,
excerpt=result.excerpt,
view=result.view,
sources=result.sources,
)
for result in results
],
)


@router.post("/bundle", response_model=ContextBundleResponse)
def build_context_bundle(body: ContextBundleRequest):
if body.view not in {ViewType.main.value, ViewType.current.value}:
raise HTTPException(status_code=400, detail="Unsupported view")

query = body.query.strip()
if not query:
raise HTTPException(status_code=400, detail="Query must not be blank")

try:
items, used_tokens = retrieval_service.build_context_bundle(
query=query,
view=body.view,
limit=body.limit,
token_budget=body.token_budget,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))

return ContextBundleResponse(
query=query,
view=body.view,
token_budget=body.token_budget,
used_tokens=used_tokens,
items=[
ContextBundleItem(
path=item.path,
title=item.title,
score=item.score,
reasons=item.reasons,
excerpt=item.excerpt,
view=item.view,
sources=item.sources,
content=item.content,
content_tokens=item.content_tokens,
truncated=item.truncated,
)
for item in items
],
)
2 changes: 2 additions & 0 deletions kb-server/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ def create_app() -> FastAPI:
app.add_middleware(APIKeyMiddleware)

from app.api.routes.health import router as health_router
from app.api.routes.context import router as context_router
from app.api.routes.notes import router as notes_router
from app.api.routes.publish import router as publish_router

app.include_router(health_router)
app.include_router(context_router)
app.include_router(notes_router)
app.include_router(publish_router)

Expand Down
44 changes: 44 additions & 0 deletions kb-server/app/schemas/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from pydantic import BaseModel, Field


class ContextSearchRequest(BaseModel):
query: str = Field(min_length=1, max_length=500)
view: str = "current"
limit: int = Field(default=10, ge=1, le=50)


class ContextSearchResult(BaseModel):
path: str
title: str
score: float
reasons: list[str]
excerpt: str
view: str
sources: list[str] | None = None


class ContextSearchResponse(BaseModel):
query: str
view: str
results: list[ContextSearchResult]


class ContextBundleRequest(BaseModel):
query: str = Field(min_length=1, max_length=500)
view: str = "current"
limit: int = Field(default=10, ge=1, le=50)
token_budget: int = Field(default=4000, ge=1, le=50000)


class ContextBundleItem(ContextSearchResult):
content: str | None = None
content_tokens: int = 0
truncated: bool = False


class ContextBundleResponse(BaseModel):
query: str
view: str
token_budget: int
used_tokens: int
items: list[ContextBundleItem]
14 changes: 10 additions & 4 deletions kb-server/app/services/current_view_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def _pending_branches() -> list[str]:
return git_service.list_branches(pattern=f"{prefix}/*")


def read_note_current(relative_path: str) -> tuple[str, datetime, list[str]]:
def read_note_current(
relative_path: str,
pending_branches: list[str] | None = None,
) -> tuple[str, datetime, list[str]]:
"""Read a note from the *current* view.

Returns ``(content, modified_at, sources)`` where *sources* is the
Expand All @@ -56,7 +59,7 @@ def read_note_current(relative_path: str) -> tuple[str, datetime, list[str]]:
main_branch = settings.git_branch
main_content = git_service.show_file(main_branch, relative_path)

pending = _pending_branches()
pending = pending_branches if pending_branches is not None else _pending_branches()
winning_content: str | None = main_content
sources: list[str] = []
if main_content is not None:
Expand All @@ -75,14 +78,17 @@ def read_note_current(relative_path: str) -> tuple[str, datetime, list[str]]:
return winning_content, now, sources


def list_notes_current(prefix: str = "") -> list[tuple[str, datetime, list[str]]]:
def list_notes_current(
prefix: str = "",
pending_branches: list[str] | None = None,
) -> list[tuple[str, datetime, list[str]]]:
"""List notes visible in the *current* view.

Returns ``[(relative_path, modified_at, sources), ...]`` sorted by
path. Each entry includes which branches provide that file.
"""
main_branch = settings.git_branch
pending = _pending_branches()
pending = pending_branches if pending_branches is not None else _pending_branches()

path_sources: dict[str, list[str]] = {}

Expand Down
5 changes: 5 additions & 0 deletions kb-server/app/services/git_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ def current_sha() -> str:
return _run("rev-parse", "HEAD").stdout.strip()


def resolve_ref(ref: str) -> str:
"""Return the SHA for *ref*."""
return _run("rev-parse", ref).stdout.strip()


def show_file(branch: str, path: str) -> str | None:
"""Read file content from a branch without checking it out.

Expand Down
Loading
Loading