project — Status
Last updated: 2026-06-05 Update this file at the end of each session.
Completed (this session)
-
Behavioral contracts documented (
docs/use-cases.md): replaced the generic scaffold with 8 use cases — one per endpoint group (UC-001 registration/login, UC-002 current user, UC-003 profiles+follow, UC-004 articles CRUD, UC-005 feed+pagination, UC-006 favorites, UC-007 comments, UC-008 tags) — each with Actor / Precondition / Steps / Postcondition / Error Cases and machine-checkable Acceptance Criteria (AC-NNN.M). Derived from the shippedsrc/http+src/servicesbehavior and cross-checked against the Hurl conformance suite (13/13). Captures the post-conformance contract: DELETE article/comment →204empty body (thebf9beffconformance fix superseded the earlier200 {}); follow/unfollow and favorite/unfavorite →200idempotent; login failure →401{ errors: { credentials: ['invalid'] } }; list/feed omitbody. - Integration & hardening pass (final pre-release gate). All 7 GS properties re-verified against the real
.husky/pre-commitgate:tsc --noEmit(0),eslint(0),npm audit --audit-level=high(0 high; 4 moderate dev-only),jest --coverage(211 passed, 4 DB-gated skipped),tscbuild emits.- Layering audit (Composable): confirmed no
src/httpproduction module imports a repository or@prisma/client— every handler delegates to a service. Routes mention Prisma only in docstrings. - Error envelope (api.md): all
AppErrors map to{ errors: { body:[…] } };ValidationError→422with field-keyed messages (envelope prefix stripped). - Error-path coverage added for the remaining reachable edges: service author-resolution
404(ArticleService/CommentService.resolveAuthor), JWT empty-userIdrejection, username/image partial-update branch, and the HTTP-controller defensive guards (identity →401, missing path segment →404) viasrc/http/controllerGuards.test.ts. Branch coverage 86.5% → 95.3%; HTTP layer 100%/100%; services branch 97.1%. .forgecraft/gate-violations.jsonl(generated tooling log) added to.gitignore;CHANGELOG.mdupdated with the delivered feature slices.
- Layering audit (Composable): confirmed no
- Tags vertical slice (RealWorld tag contract):
GET /api/tags(no auth) returns{ tags }— the distinct, de-duplicated, alphabetically-sorted set of tags appearing on any non-deleted article.- Derived from the article aggregate, no tag store: new
IArticleRepository.listTags()(distinct union over live articles), implemented inInMemoryArticleRepository+PrismaArticleRepository;ArticleService.listTags()exposes it; new thinTagController/tagRoutes/tagPresenterslice wired atsrc/http/app.ts(reuses the already-injectedarticleService, soserver.tsis unchanged). - Removed the scaffolded
ITagRepositoryport (separatelistAll()/ensure()registration store): its registration model contradicts “tags that appear on any article” — registered tags would outlive their last (soft-)deleted article. Spec decision record:docs/specs/tags.md. No new ADR (reuses ADR-0002 layering); no schema change (tagListalready onArticle). - Verification gate green:
tsc --noEmit(0),eslint(0),npm audit --audit-level=high(0 high),jest --coverage(199 passed, 4 DB-gated skipped; new tag files 100%, ≥80%/80% gate met),tscbuild emits.
- Derived from the article aggregate, no tag store: new
- Comments vertical slice (RealWorld comment contract):
GET /api/articles/:slug/comments(optional auth),POST /api/articles/:slug/comments(auth,201), andDELETE /api/articles/:slug/comments/:id(auth, author-only → 403).- New
CommentServiceoverICommentRepository+IArticleRepository(resolve slug → article id,404) +IUserRepository(embed author) +IProfileRepository(authorfollowing);InMemoryCommentRepositoryfake for tests/dev andPrismaCommentRepositoryfor prod, wired atsrc/server.ts. NewcommentRoutes/CommentController/commentSchemas/commentPresenter;'comment'added tovalidation.tsenvelope keys so field errors stay flat (body, notcomment.body). Commentmodel added to the Prisma schema (deletedAtsoft delete per Tier 3, mirroring articles; cascade FKs to article + author); client regenerated andPrismaCommentRepositoryexcluded from the coverage denominator. Comments list newest first.- Spec decision record:
docs/specs/comments.md;data-model.mdextended with the Comment entity and ERD. No new ADR (reuses ADR-0002 layering + the article soft-delete decision). - Verification gate green:
tsc --noEmit(0),eslint(0),npm audit --audit-level=high(0 high),jest --coverage(192 passed, 4 DB-gated skipped; 96.96% stmts / 86.47% branch, ≥80%/80% gate met),tscbuild emits.
- New
- Articles vertical slice (RealWorld article + favorites contract):
GET /api/articles(optional auth; filters tag/author/favorited + limit/offset pagination),GET /api/articles/feed(auth; followed authors),GET /api/articles/:slug(optional auth),POST /api/articles(auth,201),PUT/DELETE /api/articles/:slug(auth, author-only → 403), andPOST/DELETE /api/articles/:slug/favorite(auth, idempotent).- New
ArticleServiceoverIArticleRepository+IUserRepository(resolve filter usernames → ids) +IProfileRepository(authorfollowing- feed author ids);
InMemoryArticleRepositoryfake for tests/dev andPrismaArticleRepositoryfor prod, wired atsrc/server.ts.
- feed author ids);
Article+ArticleFavoritemodels added to the Prisma schema (favoritesCountderived via_count;deletedAtsoft delete); client regenerated. ScaffoldedIArticleRepositoryport refined so filters/feed take ids, keeping the adapter ignorant of users/follows. Shared boundary-validationparseOrThrowextracted tosrc/http/validation.ts.- List/feed omit the
bodyfield (2024-08-16 performance spec); the presenter splits a body-less summary from the full article. - Spec decision record:
docs/specs/articles.md;data-model.mdextended with Article + ArticleFavorite entities and ERD. No new ADR (reuses ADR-0002 layering). - Verification gate green:
tsc --noEmit(0),eslint(0),npm audit --audit-level=high(0 high),jest --coverage(164 passed, 4 DB-gated skipped; 97.3% stmts / 87.0% branch, ≥80%/80% gate met),tscbuild emits.
- New
- User profiles vertical slice (RealWorld profile contract):
GET /api/profiles/:username(optional auth),POST /api/profiles/:username/followandDELETE .../follow(auth required).- New
ProfileServiceoverIUserRepository(resolve username) +IProfileRepository(follow graph);InMemoryProfileRepositoryfake for tests/dev andPrismaProfileRepositoryfor prod, wired atsrc/server.ts. Followmodel added to the Prisma schema (composite PK, cascade delete); client regenerated.optionalAuthenticatemiddleware added (sharesreadBearerTokenwithauthenticate) so the public GET reflects a known viewer’sfollowingflag without rejecting anonymous/invalid tokens.- Spec decision record:
docs/specs/profiles.md;data-model.mdpopulated with User + Follow entities and ERD. No new ADR (reuses ADR-0002 layering). - Verification gate green:
tsc --noEmit(0),eslint(0),npm audit --audit-level=high(0 high),jest --coverage(96 passed, 4 DB-gated skipped; 97.5% stmts / 85.7% branch, ≥80%/70% gate met).
- New
- User authentication vertical slice (RealWorld user contract):
POST /api/users(register),POST /api/users/login,GET /api/user,PUT /api/user.- Layered ports & adapters:
UserServiceoverIUserRepository/IPasswordHasher(Argon2id) /ITokenService(JWT HS256); wired atsrc/server.ts. PrismaUsermodel +PrismaUserRepositoryfor prod;InMemoryUserRepositoryfake for tests/dev. - Zod validation at the boundary,
AppError→ RealWorld error-envelope middleware,authenticatemiddleware (Token/Bearer). - 66 tests pass (4 DB tests skipped — see below). Coverage 97.8% stmts / 85.7% branch.
- Layered ports & adapters:
- Verification gates green:
tsc --noEmit(0 errors),eslint(0),npm audit --audit-level=high(0 high),jest --coverage(≥80% gate met),tscbuild emits. - Spec decision record:
docs/specs/auth.md. Pitfalls documented in.claude/standards/protocols.md§ Known Pitfalls (jsonwebtokenexpiresIncast + CommonJS default import).
In Progress
- (none)
Next
- Provision Postgres (
docker composeon port 5453 per.env) and run the real-DB suite withRUN_DB_TESTS=1+prisma db push(now also coversfollows,articles,article_favorites, andcomments— addPrismaProfileRepository.test.ts,PrismaArticleRepository.test.ts, andPrismaCommentRepository.test.tsunder the same gate).
Decisions Made (this session)
-
Tags (see
docs/specs/tags.md):GET /api/tags→200 { tags }, no auth; tags are derived from live articles (distinct, sorted), not a separate store — so a tag drops out when its only article is soft-deleted. The scaffoldedITagRepository(registration model) was removed as incompatible with that requirement;IArticleRepository.listTags()is the source instead. - Status codes (see
docs/specs/auth.md): register201; duplicate →409(ConflictError, perAppError.ts’s documented purpose); bad creds →401. RealWorld reference servers sometimes use200/422here — flagged for revisit if a conformance suite asserts otherwise. tmp@^0.2.7npm override to clear a transitive HIGH CVE from the dev-only stryker chain without a breaking major bump (docs/approved-packages.md§ Transitive Overrides).- Articles (see
docs/specs/articles.md): create →201, others →200, DELETE →200 {}; author-only update/delete →403(enforced in the service, not the route); unknown slug →404; a filter naming an unknown user → empty page (not404);articlesCountis the pre-pagination total. Soft delete (deletedAt+ read-time filtering) reconciles the RealWorld hard-DELETEcontract with operation-classification Tier 3 (“no hard delete of domain entities”). List/feed omitbodyper the 2024-08-16 performance change. - Comments (see
docs/specs/comments.md): create →201, list →200, DELETE →200 {}; author-only delete →403(enforced in the service); unknown slug or comment id →404(a comment under a mismatched article slug is also404). Soft delete (deletedAt+ read-time filtering) reuses the article Tier-3 decision. List is newest-first; the embedded authorfollowingflag is viewer-relative via the sameIProfileRepository.isFollowing. - Profiles (see
docs/specs/profiles.md): all three endpoints succeed with200; unknown username →404; follow/unfollow idempotent. GET uses optional auth — a missing/malformed/invalid token serves the anonymous view (no401), matching the RealWorldauth.optionalsemantics. Self-follow is not special-cased.
Assumptions (recorded per Clarification Protocol — clarifying question was dismissed)
- Integration tests run over the in-memory repository fake (a Fake, endorsed by
testing.md; not a mock), because no Postgres is reachable in this environment (port 5453 refused, Docker daemon down). The real-DB adapter is shipped and covered by a gated test. Re-run that test where a DB exists to fully satisfy the “real DB integration” standard.
Blockers / Dependencies
- No Postgres available locally → real-DB integration test (
PrismaUserRepository.test.ts) is skipped untilRUN_DB_TESTS=1with a reachable database.
Note on prompt references (doc drift)
The task referenced “CLAUDE.md § Known Type Pitfalls” and “§ Verification Protocol (7 steps)”, which do not exist verbatim. Mapped to the real artifacts: the JWT cast is now a real entry in protocols.md § Known Pitfalls; verification was run against the actual pre-commit gate (typecheck → lint → audit → coverage) and the constitution’s 7 GS Properties. Consider adding an explicit “Verification Protocol” section to CLAUDE.md to remove the ambiguity.