Skip to content

feat(api): implement project routes with mock manager/store#47

Merged
illegalcall merged 28 commits into
mainfrom
lenovo/phase3-project-apis
May 31, 2026
Merged

feat(api): implement project routes with mock manager/store#47
illegalcall merged 28 commits into
mainfrom
lenovo/phase3-project-apis

Conversation

@Vaibhaav-Tiwari
Copy link
Copy Markdown
Collaborator

Summary

Implements the Phase 3 /api/v1/projects API surface
on top of PR #24 route shell by replacing 501 Not Implemented stubs with working handlers backed by a
mock in-memory project manager/store.

What Changed

  • Implemented project route handlers in backend/ internal/httpd/controllers/projects.go:
    • GET /api/v1/projects
    • POST /api/v1/projects
    • GET /api/v1/projects/{id}
    • PATCH /api/v1/projects/{id}
    • DELETE /api/v1/projects/{id}
    • POST /api/v1/projects/{id}/repair
    • POST /api/v1/projects/reload
  • Added mock project service layer:
    • backend/internal/project/manager.go
    • backend/internal/project/memory_store.go
    • backend/internal/project/errors.go
  • Wired default mock manager in backend/internal/ httpd/api.go when project deps are not provided.
  • Replaced route-shell 501 tests with behavior tests
    in backend/internal/httpd/controllers/ projects_test.go.
  • Fixed Windows compile issue in backend/internal/ runfile/rename_windows.go (MoveFileExW via
    kernel32.dll) so backend tests run on Windows.

Behavior / Validation

  • Returns structured API errors (INVALID_JSON,
    PATH_REQUIRED, NOT_A_GIT_REPO,
    INVALID_PROJECT_ID, PROJECT_NOT_FOUND,
    IDENTITY_FROZEN, PATH_ALREADY_REGISTERED,
    ID_ALREADY_REGISTERED, REPAIR_NOT_AVAILABLE).
  • Enforces identity freeze for PATCH on: projectId,
    path, repo, defaultBranch.
  • Mock DB is in-memory only (no durable persistence
    yet).

Testing

Ran locally:

cd backend
go test ./...
go vet ./...

All passing.

## Scope Notes

- This PR intentionally uses mocked project storage
  as assigned for Phase 3.

- Follow-up work will wire real persistence (SQLite
  store) and implement real repair/reload/delete side
  effects (session/workspace cleanup).

neversettle17-101 and others added 13 commits May 27, 2026 19:23
…ul shutdown (#10)

Phase 1a of the Go HTTP daemon lane (#10). Stands up the loopback-only
sidecar skeleton the later REST/SSE/WS/static surfaces build on:

- config: env-driven (AO_HOST/PORT/ENV/timeouts/run-file) with zero-config
  defaults; binds 127.0.0.1:3001; validates and fails fast on bad input.
- httpd: chi router with the recoverer → request-id → logger → real-ip
  middleware stack and /healthz + /readyz probes. Per-request timeout is
  carried in config but intentionally not global — it scopes to /api/v1 in
  Phase 1b so it never throttles SSE/WS/health.
- runfile: atomic PID + port handshake (running.json) for the Electron
  supervisor, with a dead-PID stale check so a crashed predecessor doesn't
  block startup while a live one fails fast.
- server: bind-before-publish (port conflict fails fast), graceful shutdown
  on SIGINT/SIGTERM via signal.NotifyContext with a 10s hard timeout, and
  run-file cleanup on exit.

Why: the daemon must be safely supervisable as a child process — the
supervisor needs a discoverable PID/port and the daemon must not leave a
half-started process or stale handshake behind. Locking the lifecycle down
now keeps the future port split a small change rather than a rewrite.

Tests cover config defaults/overrides/validation, run-file round-trip and
live/dead PID detection, health probes, full Run lifecycle, and port-conflict
fail-fast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review on #14: AO_ENV / Config.Env / IsProduction() weren't load-bearing
for Phase 1a — they only switched the slog handler. Removing them now keeps
the surface minimal; the env knob can come back later when a real consumer
needs it.

- config: remove Env field, AO_ENV parsing, and IsProduction helper.
- main: collapse newLogger to a single text-handler path.
- httpd: drop the env field from the listening log line.
- tests: drop the env assertions and AO_ENV fixture.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- config: drop AO_HOST entirely — the daemon is loopback-only by design,
  so making the bind host env-configurable was a security footgun
- config: use net.JoinHostPort in Addr() so IPv6 literals stay valid
- config: reject zero/negative AO_REQUEST_TIMEOUT and AO_SHUTDOWN_TIMEOUT
  (time.ParseDuration accepts both; either would silently break the
  daemon — instant request expiry / no graceful drain)
- runfile: split processAlive into unix/windows build-tagged files so
  liveness detection is reliable on both platforms (Windows uses
  OpenProcess; POSIX keeps signal 0)
- runfile: document os.Rename overwrite semantics (atomic on POSIX,
  REPLACE_EXISTING on Windows) so the temp-then-rename pattern's
  cross-platform behaviour is explicit
- httpd tests: give probe/waitForHealth clients an explicit per-request
  timeout so a stalled connect can't hang the test on the outer deadline
gofmt CI was failing because removing the orphan processAlive doc
comment left an extra newline at EOF.
- runfile: introduce build-tagged atomicReplace — POSIX rename(2) on
  Unix, MoveFileEx with MOVEFILE_REPLACE_EXISTING on Windows. The Go
  runtime happens to do the Windows call internally already, but
  invoking it directly makes the cross-platform contract explicit
  instead of a runtime implementation detail
- runfile: tighten process_unix.go build tag from `!windows` to `unix`
  so plan9/js/wasm fail to build rather than silently using a broken
  signal-0 probe
- runfile: add TestWriteOverwritesExisting covering the stale run-file
  replace path that none of the previous tests exercised
- config: anchor the loopback-only decision in the LoopbackHost doc so
  the next contributor doesn't reintroduce AO_HOST without the security
  rationale
chi's middleware.Logger writes via stdlib log to stdout, but the
daemon's slog logger writes to stderr — so REST traffic and daemon
logs landed on different streams in different formats. Replace it
with a small slog-backed requestLogger that:

- Wraps the response writer via middleware.NewWrapResponseWriter so
  status/bytes are accurate even when handlers return without an
  explicit WriteHeader.
- Reads the request id off the context set by middleware.RequestID
  (kept mounted just before this middleware so the id is available).
- Emits one structured Info line per request with method, path,
  status, bytes, duration, and remote — same key=value shape as the
  rest of the daemon, one stream for the Electron supervisor to
  capture.
Mounts the /api/v1 surface on the skeleton router (#10·1a) and registers
the 7 canonical project routes as 501 stubs that emit a structured
PlannedRoute body documenting the future contract. Shared scaffolding
landed here (api.go, errors.go, stubs/, controllers/) so #21/#22 plug in
without re-touching the wiring.

WHY: opens the route-shell PRs in the Go HTTP daemon lane. Doing it
interface-first lets the dashboard team build against the contract
before any handler logic exists; the locked APIError envelope and
PlannedRoute shape become #19's OpenAPI source-of-truth.

REST audit corrections vs the legacy TS surface:
  R3 PUT /projects/:id alias of PATCH: PUT not registered → 405.
  R4 POST /projects/:id repair overload: canonical /repair; legacy 405.
  R5 degraded GET returns 200 with error field: discriminator status.
  R6 ok/success flag flips: drop on 2xx; return affected resource.
  R9 bare {error: msg}: locked {error,code,message,requestId,details?}.

Legacy paths are deliberately NOT registered; each canonical handler
carries PlannedRoute.Legacy so consumers can discover the migration.

Zod schemas (TrackerConfig, SCMConfig, AgentConfig, ReactionConfig,
LocalProjectConfig, RoleAgentConfig) ported to typed Go structs with an
Extra map reserved for .passthrough() round-tripping in later PRs.

Closes part of #18; targets feat/issue-10 until #14 merges.
Controllers now depend on ONE inbound interface per resource — ports.ProjectManager —
mirroring the existing ports.SessionManager + LifecycleManager pattern.
Whether the manager impl reaches into the registry, the LCM, an outbound
port, or all three is its own concern; the HTTP layer no longer has to
know any of that.

WHY: the original split named the boundary type "ProjectService" and put
it in a sibling services.go. That implied a second category of port
distinct from inbound.go's *Manager interfaces, even though they play
the same role (things HTTP/CLI call into the core). Per review feedback,
collapse them onto one Manager-per-resource pattern.

Mechanical changes:
- ports/inbound.go gains ProjectManager next to SessionManager.
- ports/services.go renamed to projects.go; keeps only the DTOs the
  ProjectManager methods take/return.
- ProjectsController.Svc renamed to Mgr; APIDeps.Projects type bumped
  to ports.ProjectManager.

All tests pass unchanged; no behavioural change.
The first cut of the route shell duplicated each route's contract twice:
once as a Go literal (stubs.PlannedRoute{...}) in the controller, and
implicitly in the PR description. The Go literal was ~230 LoC of pure
throwaway that would be deleted in handler-impl PRs.

This commit eliminates the duplication:

  - backend/internal/httpd/apispec/openapi.yaml: full OpenAPI 3.1 doc
    covering the 7 project routes + shared schemas (Project, APIError,
    config types). x-replaces records the legacy → canonical mapping
    REST-audit corrections produced.
  - apispec/apispec.go: //go:embed the YAML, expose Operation(method,
    path) → the spec slice as a map, NotImplemented(w, r, method, path)
    → 501 with that slice embedded as `spec`.
  - controllers/projects.go: each of 7 handlers is now a one-liner:
    apispec.NotImplemented(w, r, "GET", "/api/v1/projects").
  - /api/v1/openapi.yaml serves the embedded document so tooling
    (SDK gen, the validator slated for #19, dashboard dev tools) can
    fetch the whole spec from the same origin as the routes.
  - stubs/ package deleted.

When a real handler lands, only the apispec.NotImplemented line goes
away — nothing else does. The spec stays as documentation; consumers
never had to know it was throwaway. #19 (OpenAPI follow-up) is now
half-folded into this PR; the validation middleware remains its own
follow-up.

Tests reshaped: assert envelope + spec.operationId + spec.x-replaces
(replaces the old planned.legacy assertion); add TestOpenAPIYAMLServed
to cover the static spec serve; add apispec_test.go for embed/lookup
behaviour.
Pilots the feature-package layout the backend is migrating toward: a
resource's inbound interface and its DTOs live with the resource, not in
a central ports/ catch-all.

WHY: review flagged ports/ as vague. It conflates three jobs — the
outbound capability seam (legit), single-impl inbound interfaces (Go
idiom wants these consumer-side), and DTOs that aren't ports at all.
This moves the projects contract out as the reference shape #21/#22
follow; the merged session/lifecycle/outbound contracts are left
untouched and migrated separately.

Scope: INTERFACE ONLY. No implementation — handlers still answer via
apispec.NotImplemented and the injected project.Manager stays nil. The
impl lands in a later handler-impl PR.

Changes:
- new internal/project: project.go (Manager interface, 7 endpoints) +
  dto.go (AddInput/GetResult/UpdateConfigInput/RemoveResult/ReloadResult,
  moved verbatim from ports/projects.go, Project-prefix dropped).
- ports/projects.go deleted; ProjectManager removed from ports/inbound.go.
  outbound.go and facts.go untouched.
- controllers/projects.go and httpd/api.go depend on project.Manager.

Domain entities (Project, ProjectSummary, DegradedProject, config types)
stay in domain/ as shared vocabulary.

go build/vet/test/gofmt all clean; no behavioural change.
Addresses PR review: (1) "why are config_types required at the moment?"
and (2) "project objects already defined in project/ — how do we
differentiate?"

Both had the same root cause: project types were split across domain/
and project/. Fix — keep ALL project types in the project package; only
domain.ProjectID (shared with sessions/lifecycle/workspace) stays in
domain.

- domain/project.go → project/types.go: Project, Summary, Degraded
  (renamed from ProjectSummary/DegradedProject; the package name carries
  the "Project" prefix now).
- domain/config_types.go deleted. Kept only the 4 shapes the projects
  API actually exposes — TrackerConfig, SCMConfig, SCMWebhookConfig,
  ReactionConfig — moved into project/types.go. Dropped AgentConfig,
  AgentPermission, RoleAgentConfig, LocalProjectConfig (zero references)
  and the speculative `Extra map[string]any` passthrough fields (no
  marshaller existed, so they silently dropped data — premature).
- project/dto.go + project/project.go reference the local types; ids
  stay domain.ProjectID.

Net: one home for project types, no dead code. go build/vet/test/gofmt
clean; no behavioural change (handlers still 501 via apispec).
@Vaibhaav-Tiwari
Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the /api/v1/projects REST surface in the Go daemon by replacing route-shell stubs with working handlers backed by an in-memory (mock) project manager/store, and rounds out the backend daemon skeleton (config, server lifecycle, run-file handshake, OpenAPI serving) with tests.

Changes:

  • Implemented full projects API handlers (list/add/get/update/delete/repair/reload) backed by a mock project.Manager + MemoryStore.
  • Added daemon lifecycle infrastructure: loopback bind config, graceful Server.Run, and atomic running.json run-file handshake (incl. Windows support).
  • Embedded/served OpenAPI YAML and updated/added unit tests for API behavior, server lifecycle, and spec helpers.

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
README.md Documents how to run/test the backend daemon and its env configuration.
backend/main.go Turns main into a real daemon entrypoint (config, logger, run-file check, server run).
backend/internal/runfile/runfile.go Implements atomic run-file write/read/remove + stale PID detection.
backend/internal/runfile/runfile_test.go Adds run-file unit tests (round trip, overwrite, stale/live PID behavior).
backend/internal/runfile/rename_windows.go Implements atomic replace on Windows via MoveFileExW.
backend/internal/runfile/rename_unix.go Implements atomic replace on Unix via os.Rename.
backend/internal/runfile/process_windows.go Windows PID liveness check via OpenProcess.
backend/internal/runfile/process_unix.go Unix PID liveness check via kill(pid, 0) semantics.
backend/internal/project/types.go Defines project entities and behavior-config DTO shapes exposed via API.
backend/internal/project/project.go Introduces the project.Manager interface (service contract).
backend/internal/project/memory_store.go Adds concurrency-safe in-memory project store implementing Store.
backend/internal/project/manager.go Implements mock Manager behavior over the store (validation/conflicts/reload).
backend/internal/project/errors.go Adds typed manager errors for controller translation to API envelopes.
backend/internal/project/dto.go Adds manager request/response DTOs used by controllers.
backend/internal/httpd/server.go Implements daemon HTTP server lifecycle (bind, serve, graceful shutdown, run-file).
backend/internal/httpd/server_test.go Tests health probes, server lifecycle + run-file behavior, and port conflict.
backend/internal/httpd/router.go Builds router + middleware stack, mounts health, mounts API surface.
backend/internal/httpd/log.go Adds slog-based structured access logging middleware.
backend/internal/httpd/json.go Adds shared JSON response writer helper for the httpd package.
backend/internal/httpd/errors.go Defines the locked APIError envelope and writer for router-level errors.
backend/internal/httpd/controllers/projects.go Implements /api/v1/projects handlers and error translation for projects routes.
backend/internal/httpd/controllers/projects_test.go Replaces route-shell 501 tests with behavior tests for the projects API + OpenAPI serving.
backend/internal/httpd/apispec/openapi.yaml Adds embedded OpenAPI 3.1 spec describing the /api/v1/projects surface.
backend/internal/httpd/apispec/apispec.go Embeds/parses OpenAPI YAML and serves operation slices in 501 stubs + YAML serving.
backend/internal/httpd/apispec/apispec_test.go Tests embedded spec loading, operation lookup behavior, and YAML serving.
backend/internal/httpd/api.go Wires API surface and defaults missing deps to a memory project.Manager; mounts OpenAPI route.
backend/internal/config/config.go Adds env-based daemon config with loopback-only binding and default timeouts/run-file path.
backend/internal/config/config_test.go Adds config unit tests for defaults, overrides, and invalid env values.
backend/go.mod Adds module requirements for chi and yaml.
backend/go.sum Adds sums for new Go module dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/internal/httpd/apispec/apispec.go
Comment thread backend/internal/httpd/apispec/apispec.go Outdated
Comment thread backend/internal/httpd/apispec/openapi.yaml Outdated
Comment thread backend/internal/project/memory_store.go Outdated
Comment on lines +199 to +215
type apiError struct {
Error string `json:"error"`
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"requestId,omitempty"`
Details map[string]any `json:"details,omitempty"`
}

func writeAPIError(w http.ResponseWriter, r *http.Request, status int, kind, code, message string, details map[string]any) {
writeJSON(w, status, apiError{
Error: kind,
Code: code,
Message: message,
RequestID: middleware.GetReqID(r.Context()),
Details: details,
})
}
Comment thread backend/internal/httpd/apispec/apispec.go
Comment thread backend/internal/httpd/apispec/apispec.go Outdated
Comment thread backend/internal/httpd/apispec/openapi.yaml Outdated
Comment thread backend/internal/project/memory_store.go Outdated
Comment on lines +199 to +215
type apiError struct {
Error string `json:"error"`
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"requestId,omitempty"`
Details map[string]any `json:"details,omitempty"`
}

func writeAPIError(w http.ResponseWriter, r *http.Request, status int, kind, code, message string, details map[string]any) {
writeJSON(w, status, apiError{
Error: kind,
Code: code,
Message: message,
RequestID: middleware.GetReqID(r.Context()),
Details: details,
})
}
Vaibhaav-Tiwari and others added 8 commits May 31, 2026 03:34
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…-apis

# Conflicts:
#	backend/go.mod
#	backend/go.sum
#	backend/internal/httpd/router.go
#	backend/internal/runfile/rename_windows.go
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented May 30, 2026

@copilot resolve the merge conflicts in this pull request

Resolved and pushed. I merged origin/main into this PR branch, fixed all merge conflicts, and validated backend tests. Addressed in commit b438925.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 31, 2026

Greptile Summary

This PR implements the Phase 3 /api/v1/projects REST surface, replacing 501 stubs with working handlers backed by a mock in-memory manager and store. The HTTP wiring, error mapping, OpenAPI spec embedding, and identity-freeze guard are all well-structured; the Windows MoveFileExW fix is correct.

  • Mock store bugs (already flagged): Archive never removes entries from the paths index, so deleted projects permanently poison their path and ID slots; double-DELETE succeeds instead of 404; Get returns archived rows, creating a list/get inconsistency.
  • Silent PATCH drop (already flagged): UpdateConfig discards all fields in patch (the parameter is _) and immediately returns 501, giving callers no indication their supplied values were ignored.
  • New P2 findings: isGitRepo contains dead code (return top == path is always false after the strings.EqualFold guard); projectFromRow hardcodes DefaultBranch: "main" with no corresponding ProjectRow field, leaving no column for the real default branch when persistence lands.

Confidence Score: 3/5

Not safe to merge without addressing the store mutation bugs: deleted projects permanently block re-registration at both the same path and the same ID, double-deletes silently succeed, and every PATCH request discards all supplied config fields.

The HTTP wiring, routing, and error envelope layers are solid, but the in-memory store and manager that back every write operation carry four confirmed defects on the mutation paths: the paths index is never cleaned on archive (making path slots unrecoverable), archived entries still pass the id-conflict check (making id slots unrecoverable), Archive is idempotent (second delete returns 200 instead of 404), and UpdateConfig silently discards all patch fields. These are all exercised by the handlers added in this PR and will persist into the SQLite implementation if not corrected in the Store/Manager contract now.

backend/internal/project/memory_store.go and backend/internal/project/manager.go — the archive/re-registration and silent-patch bugs live here and affect every project mutation route.

Important Files Changed

Filename Overview
backend/internal/project/manager.go New mock manager implementing the project.Manager interface; contains previously flagged P1 bugs (UpdateConfig silently drops patch, archived IDs block re-registration) plus a dead-code branch in isGitRepo.
backend/internal/project/memory_store.go New in-memory Store implementation; previously flagged P1 bugs: Archive never clears paths index (path poisoned forever) and double-DELETE succeeds instead of 404.
backend/internal/httpd/controllers/projects.go Implements all 7 project route handlers; correctly reads body twice for frozen-field guard with proper reset, maps project.Error kinds to HTTP status codes, and falls back to 501 stubs when Mgr is nil.
backend/internal/httpd/controllers/projects_test.go Good behavioural test coverage for list/add/get/reload, validation errors, conflicts, delete and repair; explicitly validates the archived-project visibility asymmetry between GET list and GET by-id.
backend/internal/httpd/apispec/apispec.go Parses embedded OpenAPI YAML once via sync.Once, serves per-operation slices in 501 responses; previously flagged P2: YAML() ignores the receiver and always returns the global embedded bytes.
backend/internal/runfile/rename_windows.go Replaces syscall.MoveFileEx with a lazy-loaded kernel32.dll call to fix a Windows build issue; unsafe.Pointer/uintptr usage follows the standard Windows syscall pattern and is safe within a single Call invocation.

Sequence Diagram

sequenceDiagram
    participant C as HTTP Client
    participant R as chi Router
    participant PC as ProjectsController
    participant M as manager (Manager)
    participant S as MemoryStore (Store)

    C->>R: POST /api/v1/projects
    R->>PC: add()
    PC->>M: Add(ctx, AddInput)
    M->>S: FindByPath(ctx, path)
    S-->>M: (row, ok, err)
    M->>S: Get(ctx, id)
    S-->>M: (row, ok, err)
    M->>S: Upsert(ctx, ProjectRow)
    S-->>M: nil
    M-->>PC: "Project{}"
    PC-->>C: "201 {project}"

    C->>R: "GET /api/v1/projects/{id}"
    R->>PC: get()
    PC->>M: Get(ctx, id)
    M->>S: Get(ctx, id)
    Note over S: Returns archived rows too
    S-->>M: (row, true, nil)
    M-->>PC: "GetResult{Status:ok}"
    PC-->>C: "200 {status, project}"

    C->>R: "DELETE /api/v1/projects/{id}"
    R->>PC: remove()
    PC->>M: Remove(ctx, id)
    M->>S: Archive(ctx, id, now)
    Note over S: paths index not cleared
    Note over S: re-archive succeeds
    S-->>M: (true, nil)
    M-->>PC: "RemoveResult{}"
    PC-->>C: "200 {projectId, removedStorageDir}"

    C->>R: "PATCH /api/v1/projects/{id}"
    R->>PC: updateConfig()
    PC->>PC: containsFrozenIdentityField()
    PC->>M: UpdateConfig(ctx, id, patch)
    Note over M: patch arg ignored
    M-->>PC: notImplemented error
    PC-->>C: 501 PROJECT_CONFIG_NOT_IMPLEMENTED
Loading

Reviews (4): Last reviewed commit: "style(project): gofmt git repo validatio..." | Re-trigger Greptile

Copy link
Copy Markdown
Collaborator

@illegalcall illegalcall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have 2 PRs that recommend the backend architecture and library choices:

  • Architecture / code structure: #41
  • Technical stack / libraries: #25

Let’s try to follow those.

If you disagree with any part of those recommendations, let’s comment/contribute there and keep those PRs as the source of truth.

Based on that, I recommend these changes here:

  • Keep the internal/project feature-package direction, but make sure the mock store does not become the production source of truth by accident.
  • Keep HTTP as the protocol adapter: controllers should call project.Manager, and storage/config/runtime side effects should stay behind the project feature layer.
  • If project registration validates a real git repo, use the recommended Git CLI path instead of filesystem-only .git checks.
  • Make PATCH/DELETE semantics honest: either persist the requested mock state consistently, or return a clear not-implemented/unsupported response until the real project store lands.
  • Align the OpenAPI spec with actual behavior, especially around config passthrough and archived/deleted project visibility.

Comment thread backend/internal/httpd/api.go Outdated
// per-request timeout so the REST group can apply it without re-reading the
// environment.
func NewAPI(cfg config.Config, deps APIDeps) *API {
if deps.Projects == nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting nil deps to project.NewMemoryManager() makes the production router expose a successful but process-local project API whenever callers forget to wire a real manager. That makes POST /projects look durable even though the data disappears on restart. I think the mock manager should be injected explicitly by tests/dev wiring, or nil should keep the 501 behavior until a real project manager is available.

Comment thread backend/internal/project/manager.go Outdated
return Project{}, notFound("PROJECT_NOT_FOUND", "Unknown project")
}

// The PR #37 schema stores only registry identity columns. Behaviour config
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns 200 for PATCH while deliberately dropping every requested config field. That is worse than a stub because clients will believe agent, runtime, tracker, etc. were accepted. For the mock phase, can we either persist these fields in the in-memory row/read-model, or return a clear unsupported/not-implemented error until a config store exists?

if err := validateProjectID(id); err != nil {
return GetResult{}, err
}
row, ok, err := m.store.Get(ctx, string(id))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Remove archives the row and Get does not check ArchivedAt, a project remains readable as status: ok after DELETE. That conflicts with the route contract’s “remove/unregister” semantics. Please either treat archived rows as not found/degraded at the API layer, or update the architecture/spec discussion to say deleted projects remain fetchable.

return filepath.Clean(abs), nil
}

func isGitRepo(path string) bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stack PR recommends using the real git CLI for Git behavior. A filesystem .git check accepts fake repos and misses some edge cases that git -C <path> rev-parse --is-inside-work-tree would handle. If this endpoint is doing real validation, it should go through the Git CLI path; otherwise keep this explicitly mock-only.

degradedCount: { type: integer }

# ---- Behaviour config blobs (ported from the TS Zod schemas) ----
# All carry .passthrough() semantics — extra keys are preserved on
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec says extra config keys are preserved and re-emitted, but the Go DTOs do not carry an Extra/raw field and UpdateConfig currently drops the whole patch. Please align the spec with the implementation, or implement passthrough preservation before advertising it as part of the contract.

Copy link
Copy Markdown
Collaborator

@illegalcall illegalcall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked latest head d46bf77. The earlier API semantics comments are partly addressed, but go test ./... now fails in backend/internal/httpd/controllers because valid git init fixtures are rejected as NOT_A_GIT_REPO on macOS.

Checked:

  • gofmt -l . passes
  • go vet ./... passes
  • go test ./... fails in controllers

if err != nil {
return false
}
top := filepath.Clean(strings.TrimSpace(string(out)))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This equality check rejects valid repos on macOS when the input path is under /var/...: git rev-parse --show-toplevel returns the canonical /private/var/... path, while normalizePath keeps /var/..., so the same repo fails this comparison. That is why the latest git init fixtures fail with NOT_A_GIT_REPO. Please canonicalize both paths with filepath.EvalSymlinks before comparing, or treat a successful git -C path rev-parse --show-toplevel as sufficient for repo validation if nested worktrees are acceptable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll canonicalize both paths with filepath.EvalSymlinks before comparing.

@Vaibhaav-Tiwari
Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

Copy link
Copy Markdown
Collaborator

@illegalcall illegalcall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked latest head 0b0b136. The /var vs /private/var git repo fixture issue is fixed now: go test ./... and go vet ./... pass locally.

Still not ready to merge:

  • GitHub currently marks this PR as CONFLICTING / DIRTY against latest main after PR #37 merged. Please rebase/merge latest main and resolve the backend/go.mod / backend/go.sum conflict.
  • gofmt -l . returns backend/internal/project/manager.go.

After that, rerun the backend checks on the rebased branch.

Comment thread backend/internal/project/manager.go Outdated
top := filepath.Clean(strings.TrimSpace(string(out)))
path = filepath.Clean(path)
top, err = filepath.EvalSymlinks(top)
if err != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new block needs gofmt; gofmt -l . currently reports backend/internal/project/manager.go.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep working on that only

Copy link
Copy Markdown
Collaborator

@illegalcall illegalcall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked latest head 5046ea8. The previous merge conflict and gofmt issues are resolved.

Checked locally:

  • gofmt -l . passes
  • go vet ./... passes
  • go test ./... passes
  • go test -race ./... passes

GitHub now reports the PR as MERGEABLE / CLEAN.

@illegalcall illegalcall merged commit 9a10eac into main May 31, 2026
2 checks passed
AgentWrapper pushed a commit that referenced this pull request May 31, 2026
PR was building, leaving #24 conflicting on nearly every file. The route shell
itself is now redundant, but two pieces of #24 are genuinely net-new and absent
from main — salvage them here, rebuilt on top of #47's merged code:

- Code-first OpenAPI: apispec/specgen reflects the controllers' request/response
  types and project DTOs (the same types the handlers use at runtime) into
  openapi.yaml via swaggest. `cmd/genspec` + `go:generate` regenerate the
  committed, embedded spec; a drift test (TestBuild_MatchesEmbedded) and a route
  parity test (TestRouteSpecParity) fail CI if the spec and the code disagree.
  This replaces main's hand-maintained openapi.yaml so the "single source of
  truth" claim is actually enforced, not aspirational.

- Typed frontend client: frontend/src/api/schema.d.ts is generated from that
  spec via openapi-typescript (`npm run gen:api`), consumed by a small
  openapi-fetch client. The frontend now gets its types from the daemon
  contract instead of hand-maintaining them.

specgen lives outside apispec (which controllers import for the 501 stub) to
avoid an import cycle. Handlers now encode named response DTOs
(controllers/dto.go) instead of map[string]any so the generator reflects the
real wire shapes. A gen-verify CI job regenerates both artifacts and fails on a
stale commit.

Tradeoff: the generated spec drops the hand-authored examples / x-rest-audit
notes from #47's openapi.yaml; those can be re-added as operation metadata in
specgen if wanted. Behaviour-only patch (no handler logic changes).

Supersedes the codegen + frontend parts of #24. Refs #20, #47.
neversettle17-101 added a commit that referenced this pull request Jun 1, 2026
Squashed feat/20 for a clean rebase onto main (which now carries #47's parallel
implementation). Keeps our architecture: httpx transport, swaggest-generated
openapi.yaml + frontend TS types, CI drift guard, 5 REST-corrected routes,
500 SERVICE_UNAVAILABLE while the Manager is nil.
neversettle17-101 added a commit that referenced this pull request Jun 1, 2026
Implement the existing 5-method project.Manager backed directly by the sqlite
project store (no extra store/port/adapter, no memory store). Correcting #47:
reuses our interface, drops reload/repair, and uses one error type.

- project/manager.go: NewManager(*sqlite.Store) calling project_store.go
  directly. Add validates path (git rev-parse) + derives/validates the id +
  path/id conflict checks (suggestedProjectId); List/Get/Remove are real;
  Get always status:"ok" and UpdateConfig returns a typed error until config
  persistence exists (registry-only scope).
- httpx.APIErr: one typed error (status+kind+code+message+details) with
  BadRequest/NotFound/Conflict/Internal constructors. The manager returns these;
  controllers translate via writeErr. Replaces #47's separate project/errors.go.
- Wire the real Manager into main.go (httpd.New now takes APIDeps).
- Tests: manager_test.go drives all 5 methods + validation/conflict cases
  against a real temp-dir sqlite store; controller test covers the typed→HTTP
  translation. Verified live: real 201/200/400/404/409 over the daemon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
PR was building, leaving #24 conflicting on nearly every file. The route shell
itself is now redundant, but two pieces of #24 are genuinely net-new and absent
from main — salvage them here, rebuilt on top of #47's merged code:

- Code-first OpenAPI: apispec/specgen reflects the controllers' request/response
  types and project DTOs (the same types the handlers use at runtime) into
  openapi.yaml via swaggest. `cmd/genspec` + `go:generate` regenerate the
  committed, embedded spec; a drift test (TestBuild_MatchesEmbedded) and a route
  parity test (TestRouteSpecParity) fail CI if the spec and the code disagree.
  This replaces main's hand-maintained openapi.yaml so the "single source of
  truth" claim is actually enforced, not aspirational.

- Typed frontend client: frontend/src/api/schema.d.ts is generated from that
  spec via openapi-typescript (`npm run gen:api`), consumed by a small
  openapi-fetch client. The frontend now gets its types from the daemon
  contract instead of hand-maintaining them.

specgen lives outside apispec (which controllers import for the 501 stub) to
avoid an import cycle. Handlers now encode named response DTOs
(controllers/dto.go) instead of map[string]any so the generator reflects the
real wire shapes. A gen-verify CI job regenerates both artifacts and fails on a
stale commit.

Tradeoff: the generated spec drops the hand-authored examples / x-rest-audit
notes from #47's openapi.yaml; those can be re-added as operation metadata in
specgen if wanted. Behaviour-only patch (no handler logic changes).

Supersedes the codegen + frontend parts of #24. Refs #20, #47.
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
…ory store

The project Manager now runs only against the durable backend store: remove the
process-local MemoryStore (and NewMemoryManager), and require a real Store. The
daemon already wires the sqlite store; tests now build a real temp-dir sqlite
store instead of the mock.

- Move Row + the Store port to project/store.go. The Store interface stays
  because it is the dependency-inversion port that lets the manager reach the
  backend without an import cycle (storage imports project.Row), not an extra
  mock layer — there is no longer any in-memory implementation.
- NewManager requires a non-nil Store (no in-memory fallback).
- Add project/manager_test.go: List/Add/Get/Remove happy paths +
  PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED,
  PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real
  sqlite store (the service-logic tests #47 lacked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
* refactor(project): manager talks to the sqlite store; drop the in-memory store

The project Manager now runs only against the durable backend store: remove the
process-local MemoryStore (and NewMemoryManager), and require a real Store. The
daemon already wires the sqlite store; tests now build a real temp-dir sqlite
store instead of the mock.

- Move Row + the Store port to project/store.go. The Store interface stays
  because it is the dependency-inversion port that lets the manager reach the
  backend without an import cycle (storage imports project.Row), not an extra
  mock layer — there is no longer any in-memory implementation.
- NewManager requires a non-nil Store (no in-memory fallback).
- Add project/manager_test.go: List/Add/Get/Remove happy paths +
  PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED,
  PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real
  sqlite store (the service-logic tests #47 lacked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(project): trim routes, consolidate package, add code-first OpenAPI

- Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their
  Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult,
  UpdateConfigInput) — not needed at this stage
- Merge Manager interface into manager.go; delete project.go (single-impl
  split served no purpose)
- Remove dead notImplemented helper from errors.go
- Port PR #59 code-first OpenAPI generation: controllers/dto.go named
  response types, specgen/build.go (4 routes), parity + drift tests,
  cmd/genspec, go generate wiring; regenerate openapi.yaml
- Add swaggest deps; add YAML() method to apispec.Spec

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(project): address PR review comments

- t.Skipf → t.Fatalf in gitRepo helper: git failures now hard-fail
  instead of silently skipping manager tests on a misconfigured runner
- FindProjectByPath: add AND archived_at IS NULL so archived paths don't
  permanently block re-registration (update queries/projects.sql and
  generated gen/projects.sql.go)
- Add TestManager_ReaddAfterRemove to lock the fix

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fixed lint and fmt

* addressed greptile comments

* Apply suggestions from code review

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* project tests fix

* project_tests fix

* fix: Linting and formatting fix

* refactor: move project manager into service layer (#68)

* refactor: split service package by resource (#68)

* fix: ignore archived project id conflicts (#68)

* refactor: move pr manager into service layer (#68)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants