Onboard recipe-app to @recipe/contract #35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "onboard-recipe-platform-contract"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements
docs/plans/onboarding-integration.md(all steps 1-9).Summary
@recipe/contractfile:dependency and arecipe-platformsibling clone step to all three Forgejo workflows, perrecipe-platform/docs/adr/2026-09-19-contract-ci-strategy.md.docs/audit-domains.mddiffs every contract domain against this repo's DTOs and the live backend. Confirms this repo'sAbstractModelDtoMapperdoes not have the Date/string base-mapper blocker that limitedrecipe-backendto migrating onlyhealth-- dates are already ISO strings throughout this repo's DTO layer.TagRestResource.tsnow consumes the{ tags: [...] }wrapper fromGET /tag/all/GET /tag/enrichedinstead of a bare array. The backend's matching change has already landed onrecipe-backend'sonboard-recipe-platform-contractbranch -- these two PRs should merge close together.LoginResponse.expiryDatewas typedDatebut never actually held one (fixed tostring, matching the wire format);HealthResponse.commit/.datewere typed non-nullablestringbut the backend actually sendsnullin local dev (fixed tostring | null).@recipe/contractimports:shared,health,auth,tags,comments,ratings,recipes,compact-recipes,users,meal-plans,ai,passkeys(partial, see below), plusimages(not applicable -- no local DTO exists for that domain in this repo). Contract renames applied:MealPlanSummaryDto→CompactMealPlanDto,EffectiveMealPlanPermission→MealPlanEffectivePermission,RecipeFromImagesResponseDto→RecipeImportResponse,PasskeyDeviceType'sSingleDevice/MultiDevice→SINGLE_DEVICE/MULTI_DEVICE.options/responsefields asunknown(documented in its own JSDoc, to avoid a@simplewebauthndependency in the shared package), while this repo needs the concrete@simplewebauthn/browsertypes to pass directly intostartAuthentication()/startRegistration().FrontendBuildInfo(frontend-only, no contract equivalent), which is now an interface.check-platformskill: mirrorsrecipe-platform'scheck-siblingsin reverse -- on-demand check for../recipe-platformandapi-contractinside it, with consent before cloning..claude/settings.json: committed baseline permissions (previously only a gitignored local file existed).CLAUDE.md: new "Multi-Repo Context" section naming the sibling repos, the contract dependency and migration status, thecheck-platformskill, and pointing atrecipe-platform/docs/api-conventions.mdfor wire-format rules.Out of scope
Per the plan: repo-specific agent definitions (
architect.md/planner.md/implementer.md/reviewer.mdin.claude/agents/) are a separate follow-up initiative, not part of this PR.Coordination note
The
tagswire-format fix in this PR only makes sense paired withrecipe-backend's matching change (already on that repo'sonboard-recipe-platform-contractbranch). Please sequence the merges close together.Test plan
npm run build-- passesnpm run lint-- passes (2 pre-existing, unrelated warnings)npm run test-- 187/187 tests passDeletes the local TagDto, EnrichedTagDto, and DeleteTagRequest types. TagRestResource now consumes TagListResponse/EnrichedTagListResponse (the { tags: [...] } wrapper) from GET /tag/all and GET /tag/enriched instead of a bare array, matching the list-wrapper convention in docs/api-conventions.md. The backend's matching change already landed on recipe-backend's onboard-recipe-platform-contract branch -- these two PRs need to merge close together.@ -5,0 +18,4 @@breaks both sibling repos at compile time. Run `/check-platform` if `npm install`/`npm ci`fails to resolve it (checks whether `recipe-platform` and `api-contract` are present at theexpected path, offering to clone with consent). Migration status: no domain has beenmigrated to `@recipe/contract` types yet — see `docs/audit-domains.md` for the fullWe removed the file from that location. And is it really necessary for claude to read that one later after the migration is complete? Seems like a good way to fill context for nothing...
@ -5,0 +22,4 @@field-by-field audit against every contract domain, including which domains are ready tomigrate and which need a coordinated backend change first.**Cross-repo feature workflow** (spec → contract → architecture → plans → implementation →Does the frontend require knowledge on the platform skills? I don't think so. The agent simply needs to know where the plans live. If I want to start an agent in frontend alone and do local planning here, that's my decision. And if we plan from the platform repo, a subagent in this repo will be spawned reading this md. I don't think this paragraph is wise.
@ -11,2 +9,4 @@* Fetches all tags as plain models.* Used where only the tag ID and description are needed (e.g. recipe editor).** Consumes the `{ tags: TagDto[] }` wrapper per the list-wrapper conventionNo history in comments! Describe the now and give a reason where necessary. No need here...
@ -16,3 +16,2 @@...this.mapDtoToBaseModel(dto),body: dto.body,recipeId: dto.recipeId,// The contract types body/recipeId as optional to cover write payloads too,I'm unsure what you mean by that comment. Elaborate.
@ -20,3 +20,3 @@return {...this.mapDtoToBaseModel(dto),title: dto.title,// title is optional on the contract type but always populated on reads.Why should it be optional? Title is a mandatory field for a recipe. We should check this!
@ -15,2 +15,2 @@ingredientList: dto.ingredients.sort((a, b) => a.sortOrder - b.sortOrder)// ingredients is optional on the contract type but always populated on reads.ingredientList: (dto.ingredients ?? [])same here. Ingredients are mandatory...
@ -8,2 +8,3 @@...this.mapDtoToBaseModel(dto),text: dto.text,// text is optional on the contract type but always populated on reads.text: dto.text!,This should be mandatory as well. We should check the contract...
@ -25,3 +27,3 @@recipeTitle: dto.recipeTitle,recipeAmountDescription: dto.recipeAmountDescription,servings: dto.servings,servings: dto.servings!,we should check the contract here. I'm pretty sure that servings is mandatory.
@ -32,3 +32,3 @@// Base fields: id, createdAt, updatedAt, createdBy, version...this.mapDtoToBaseModel(dto),title: dto.title,// title is optional on the contract type but always populated by the backend.We need to check the contract. THis should be mandatory.
@ -26,1 +26,3 @@sharedWithUserId: dto.sharedWithUserId,// sharedWithUserId/permission are optional on the contract type but always// populated by the backend on reads.sharedWithUserId: dto.sharedWithUserId!,Check...
@ -17,3 +17,2 @@...this.mapDtoToBaseModel(dto),rating: dto.rating,recipeId: dto.recipeId,// rating/recipeId are optional on the contract type (shared with write payloads)Check contract. They shouldn't be optional
@ -20,3 +20,1 @@url: `${API_BASE_URL}${dto.url}`,mimeType: dto.mimeType,recipeId: dto.recipeId,// url/mimeType/recipeId are optional on the contract type but alwayscheck contract.
@ -24,3 +24,2 @@},instructionStepList: dto.instructions.sort((a, b) => a.sortOrder - b.sortOrder)// instructions/ingredientGroups are optional on the contract type (it alsocheck contract.
@ -22,2 +21,3 @@...this.mapDtoToBaseModel(dto),description: dto.description,// description is optional on the contract type but always populated on reads.description: dto.description!,check contract.
@ -22,3 +22,2 @@...this.mapDtoToBaseModel(dto),userName: dto.userName,email: dto.email,// userName/email are optional on the contract type but always populated on reads.check contract.
@ -1,5 +1,4 @@import type {RecipeImportStatus} from "../api/enums/RecipeImportStatus.ts";import type {RecipeImportReason} from "../api/enums/RecipeImportReason.ts";import type {RecipeImportStatus, RecipeImportReason} from "@recipe/contract";Hm, using API enums in the model carries APIdependencies to the model. Question is: is it worth duplicating all the enums and mapping to avoid just that.
Pushed
82aeb0baddressing the open review items:Comment 363 (TagRestResource JSDoc) — Fixed. Removed the "Consumes the wrapper" lines from both
fetchAllTagsandfetchEnrichedTags. The unwrapping is visible in the code itself.Comment 366 (IngredientGroupMapper — ingredients mandatory?) — No code change. Checked the contract:
RecipeIngredientGroupDto.ingredientsis typedingredients?: RecipeIngredientDto[]— explicitly optional. The?? []fallback and the comment are correct. If the backend always populates it on reads and the contract should enforce that, the fix belongs inrecipe-platform/api-contract, not here.Comment 373 (RecipeMapper — check contract) — The
?? []fallbacks forinstructionsandingredientGroupsare correct:RecipeDtomarks both as optional (the DTO is shared between write payloads and read responses). Thetitle!assertion on line 20 remains —RecipeDto.titleistitle?: stringin the contract, so the!is required by TypeScript. The business logic says title is NOT NULL, so the contract should mark ittitle: string— that is arecipe-platformfix, not frontend.Comment 376 (contract enums in models) — Addressed by
22ec77bwhich documents the decision with an ADR. The enums stay; the rationale is recorded.Bonus fixes also in
82aeb0b: Two stray!assertions that6b55bf5missed —randomImage.url!inCompactRecipeMapper(url is required per contract) anddto.description!inTagMapper.mapEnrichedDtoToModel(EnrichedTagDto.description is also required per contract), plus a misleading comment claiming it was optional.