Skip to content
Merged
8 changes: 8 additions & 0 deletions .changeset/read-only-codegen-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@croco/openapi-spec": patch
"@croco/problems-core": patch
"@croco/rpc-codegen": patch
"create-croco-app": patch
---

Detect stale committed OpenAPI and RPC outputs without rewriting them, make generated app contract verification use the read-only checks, and scaffold Next.js applications with the patched 15.5.21 release.
4 changes: 2 additions & 2 deletions docs/problem-code-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -9566,7 +9566,7 @@
"sources": [
{
"file": "packages/rpc-codegen/src/libs/generate.ts",
"line": 105,
"line": 110,
"column": 5,
"kind": "problem-constructor"
}
Expand Down Expand Up @@ -9626,7 +9626,7 @@
"sources": [
{
"file": "packages/rpc-codegen/src/libs/generate.ts",
"line": 111,
"line": 116,
"column": 5,
"kind": "problem-constructor"
}
Expand Down
7 changes: 6 additions & 1 deletion packages/create-croco-app/src/tests/e2e-generation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2252,7 +2252,7 @@ describe("E2E: generate()", () => {
build: "turbo build",
test: "turbo test",
"contract:verify":
"pnpm contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check && pnpm contract:openapi && pnpm contract:client && pnpm --filter @test/provider-rpc typecheck",
"pnpm contract:diff && pnpm contract:check && pnpm project-map:check && pnpm contract:openapi:check && pnpm contract:client:check && pnpm --filter @test/provider-rpc typecheck",
"di:graph": "pnpm --filter @test/api-server di:graph",
"di:check": "croco di check .croco/build/di-graph.manifest.json",
"di:assert": "node scripts/assert-di-graph.mjs .croco/build/di-graph.manifest.json",
Expand All @@ -2265,6 +2265,11 @@ describe("E2E: generate()", () => {
});
expect(rootPackageJson.scripts?.["contract:client"]).toContain("--strict-schemas");
expect(rootPackageJson.scripts?.["contract:openapi"]).toContain("--strict-schemas");
expect(rootPackageJson.scripts?.["contract:client:check"]).toContain("--output-check");
expect(rootPackageJson.scripts?.["contract:openapi:check"]).toContain("--output-check");
expect(rootPackageJson.scripts?.codegen).toBe(
"pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
);
expect(manifest).toMatchObject({
schemaVersion: 1,
projectName: "my-saas-api",
Expand Down
25 changes: 19 additions & 6 deletions packages/create-croco-app/src/tests/templates-build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function checkSpaBeSplitStructure() {
/^croco project map[\s\S]*--check --manifest croco\.project-map\.json --manifest-bundle \.croco\/manifest$/,
),
"contract:verify": expect.stringMatching(
/^pnpm contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check && pnpm contract:openapi && pnpm contract:client && pnpm --filter \{\{scope\}\}\/provider-rpc typecheck$/,
/^pnpm contract:diff && pnpm contract:check && pnpm project-map:check && pnpm contract:openapi:check && pnpm contract:client:check$/,
),
"ci:contracts": "pnpm contract:verify",
"di:graph": "pnpm --filter {{scope}}/api-server di:graph",
Expand All @@ -187,7 +187,11 @@ function checkSpaBeSplitStructure() {
"contract:client": expect.stringMatching(
/^pnpm contract:check &&[\s\S]*croco-rpc-codegen[\s\S]*--strict-schemas[\s\S]*--problem-runtime frontend-problems --manifest-bundle \.croco\/manifest[\s\S]*provider-rpc typecheck$/,
),
codegen: expect.any(String),
"contract:openapi:check": expect.stringMatching(/croco-openapi-spec[\s\S]*--output-check$/),
"contract:client:check": expect.stringMatching(
/croco-rpc-codegen[\s\S]*--output-check[\s\S]*provider-rpc typecheck$/,
),
codegen: "pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
lint: "biome lint .",
test: "turbo test",
}),
Expand Down Expand Up @@ -321,7 +325,7 @@ function checkAdminConsoleStructure() {
/croco project map[\s\S]*--check[\s\S]*--manifest-bundle \.croco\/manifest/,
),
"contract:verify": expect.stringMatching(
/contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check/,
/contract:diff[\s\S]*contract:openapi:check && pnpm contract:client:check/,
),
"di:graph": "pnpm --filter {{scope}}/api-server di:graph",
"di:check": "croco di check .croco/build/di-graph.manifest.json",
Expand All @@ -333,6 +337,9 @@ function checkAdminConsoleStructure() {
"contract:client": expect.stringMatching(
/admin\.ts,users\.ts,problems\.ts[\s\S]*--strict-schemas[\s\S]*--problem-runtime frontend-problems --manifest-bundle \.croco\/manifest/,
),
"contract:openapi:check": expect.stringMatching(/croco-openapi-spec[\s\S]*--output-check$/),
"contract:client:check": expect.stringMatching(/croco-rpc-codegen[\s\S]*--output-check/),
codegen: "pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
typecheck: "pnpm contract:client && turbo typecheck",
build: "pnpm contract:client && turbo build",
}),
Expand Down Expand Up @@ -549,7 +556,7 @@ function checkSaasStructure() {
expect(rootPackageJson).toMatchObject({
scripts: expect.objectContaining({
"contract:check": expect.stringMatching(
/^pnpm contract:client && pnpm --filter \{\{scope\}\}\/provider-rpc typecheck$/,
/^croco-rpc-codegen[\s\S]*--check[\s\S]*--strict-schemas[\s\S]*--fail-on-diagnostics$/,
),
"contract:snapshot": expect.stringMatching(
/^croco contracts check[\s\S]*--strict-schemas[\s\S]*--json --out contract-graph\.snapshot\.json$/,
Expand All @@ -567,7 +574,7 @@ function checkSaasStructure() {
/^croco project map[\s\S]*--runtime-policy croco-runtime-policy\.manifest\.json[\s\S]*--provider-profile croco-saas-profile\.manifest\.json[\s\S]*--check --manifest croco\.project-map\.json --manifest-bundle \.croco\/manifest$/,
),
"contract:verify": expect.stringMatching(
/^pnpm contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check && pnpm contract:openapi && pnpm contract:client && pnpm --filter \{\{scope\}\}\/provider-rpc typecheck$/,
/^pnpm contract:diff && pnpm contract:check && pnpm project-map:check && pnpm contract:openapi:check && pnpm contract:client:check && pnpm --filter \{\{scope\}\}\/provider-rpc typecheck$/,
),
"ci:contracts": "pnpm contract:verify",
"di:graph": "pnpm --filter {{scope}}/api-server di:graph",
Expand All @@ -583,6 +590,9 @@ function checkSaasStructure() {
"contract:openapi": expect.stringMatching(
/^pnpm contract:check && croco-openapi-spec[\s\S]*--strict-schemas[\s\S]*--out openapi\.json[\s\S]*--manifest-bundle \.croco\/manifest$/,
),
"contract:openapi:check": expect.stringMatching(/croco-openapi-spec[\s\S]*--output-check$/),
"contract:client:check": expect.stringMatching(/^croco-rpc-codegen[\s\S]*--output-check$/),
codegen: "pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
"demo:seed": expect.any(String),
"profile:check": "pnpm --filter {{scope}}/api-server profile:check",
"architecture-policy:check": "croco architecture-policy check --manifest croco.arch.json",
Expand Down Expand Up @@ -875,7 +885,7 @@ function checkAiSaasStructure() {
/^croco project map[\s\S]*--check --manifest croco\.project-map\.json --manifest-bundle \.croco\/manifest$/,
),
"contract:verify": expect.stringMatching(
/^pnpm contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check/,
/^pnpm contract:diff[\s\S]*contract:openapi:check && pnpm contract:client:check/,
),
"ci:contracts": "pnpm contract:verify",
"di:graph": "pnpm --filter {{scope}}/api-server di:graph",
Expand All @@ -888,6 +898,9 @@ function checkAiSaasStructure() {
"contract:openapi": expect.stringMatching(
/--strict-schemas[\s\S]*AI SaaS API[\s\S]*--manifest-bundle \.croco\/manifest$/,
),
"contract:openapi:check": expect.stringMatching(/croco-openapi-spec[\s\S]*--output-check$/),
"contract:client:check": expect.stringMatching(/^croco-rpc-codegen[\s\S]*--output-check$/),
codegen: "pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
}),
});
expect(Object.values(rootPackageJson.scripts ?? {})).not.toEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pnpm dev:web

- API unavailable: resource table과 timeline은 마지막 성공 상태를 유지하고 상단 alert에 네트워크 실패를 표시합니다.
- Missing user: generated `getUserResult` client가 declared Problem branch를 반환하며 UI는 `admin-console/user-not-found` code와 recovery action을 표시합니다.
- Contract drift: `pnpm contract:verify`가 snapshot diff, consumer coverage report, Project Map manifest bundle, OpenAPI generation, generated client typecheck를 순서대로 실행합니다.
- Contract drift: `pnpm contract:verify`가 snapshot diff, canonical graph, Project Map manifest bundle, OpenAPI/client output drift, generated client typecheck를 쓰기 없이 검사합니다. 의도한 출력 변경은 `pnpm codegen`으로 반영합니다.
- DI graph drift: 의도적으로 갱신할 때 `pnpm di:graph`를 실행합니다. `pnpm di:verify`는 기존 `.croco/build/di-graph.manifest.json`과 Project Map 산출물을 변경하지 않은 채 검사하고 `croco doctor --json`을 실행합니다.

## Contract Commands
Expand All @@ -43,7 +43,7 @@ pnpm codegen

`contract:client`는 admin controller 계약에서 React Query hook을 포함한 fetch client를 생성합니다. Admin web은 `{{scope}}/provider-rpc`의 `adminClient`와 generated output types를 직접 사용하므로 `typecheck`와 `build`는 codegen을 먼저 실행합니다.
`contract:coverage`는 같은 controller 계약에서 `contract-graph.coverage.json`을 써서 OpenAPI/RPC consumer coverage diagnostics를 남깁니다.
`contract:verify`는 `project-map:write`와 `project-map:check`를 실행해 `.croco/manifest` inspectable bundle을 갱신하고 검증합니다. Generated OpenAPI/RPC outputs는 같은 bundle 경로를 source reference로 남깁니다.
`codegen`은 Project Map과 `.croco/manifest` inspectable bundle을 갱신한 뒤 OpenAPI/RPC outputs를 생성합니다. `contract:verify`는 이 산출물들을 다시 쓰지 않고 검사합니다.
Generated OpenAPI/RPC commands run strict ContractGraph schema checks by default and pass `--fail-on-diagnostics`, so strict Problem contract diagnostics block generated artifacts. `--compatibility-schemas` and `--compatibility-problems` are migration-only opt-outs for legacy routes, not generated app CI settings.

## Structure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
"contract:coverage": "croco contracts check --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --strict-schemas --json --out contract-graph.coverage.json",
"project-map:write": "croco project map --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --out croco.project-map.json --manifest-bundle .croco/manifest",
"project-map:check": "croco project map --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --check --manifest croco.project-map.json --manifest-bundle .croco/manifest",
"contract:verify": "pnpm contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check && pnpm contract:openapi && pnpm contract:client && pnpm --filter {{scope}}/provider-rpc typecheck",
"contract:verify": "pnpm contract:diff && pnpm contract:check && pnpm project-map:check && pnpm contract:openapi:check && pnpm contract:client:check",
"ci:contracts": "pnpm contract:verify",
"di:graph": "pnpm --filter {{scope}}/api-server di:graph",
"di:check": "croco di check .croco/build/di-graph.manifest.json",
"di:assert": "node scripts/assert-di-graph.mjs .croco/build/di-graph.manifest.json",
"doctor": "croco doctor --json",
"di:verify": "pnpm di:check && pnpm di:assert && pnpm project-map:check && pnpm doctor",
"contract:openapi": "pnpm contract:check && croco-openapi-spec --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --strict-schemas --fail-on-diagnostics --out openapi.json --title \"{{projectName}} Admin Console API\" --version 0.1.0 --server http://localhost:3000 --manifest-bundle .croco/manifest",
"contract:openapi:check": "pnpm contract:check && croco-openapi-spec --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --strict-schemas --fail-on-diagnostics --out openapi.json --title \"{{projectName}} Admin Console API\" --version 0.1.0 --server http://localhost:3000 --manifest-bundle .croco/manifest --output-check",
"contract:client": "pnpm contract:check && croco-rpc-codegen --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --strict-schemas --fail-on-diagnostics --out libs/shared/provider-rpc/src --react-query --problem-runtime frontend-problems --manifest-bundle .croco/manifest && pnpm --filter {{scope}}/provider-rpc typecheck",
"codegen": "pnpm contract:client",
"contract:client:check": "pnpm contract:check && croco-rpc-codegen --controllers \"apps/api-server/src/{controllers/**/*.ts,admin.ts,users.ts,problems.ts}\" --strict-schemas --fail-on-diagnostics --out libs/shared/provider-rpc/src --react-query --problem-runtime frontend-problems --manifest-bundle .croco/manifest --output-check && pnpm --filter {{scope}}/provider-rpc typecheck",
"codegen": "pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
"build": "pnpm contract:client && turbo build",
"lint": "biome lint .",
"test": "turbo test",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-croco-app/templates/ai-saas/README.md.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pnpm demo:smoke

`pnpm demo:smoke` runs the base SaaS demo, operational smoke checks, and the AI SaaS smoke flow. `pnpm ai:smoke` runs only the AI portion.

Intentional API contract changes should update and commit `contract-graph.snapshot.json` with `pnpm contract:snapshot`. CI should run `pnpm contract:verify` or `pnpm ci:contracts`; it does not rewrite the snapshot before diffing, writes `contract-graph.coverage.json`, regenerates and checks `.croco/manifest`, then regenerates `openapi.json` and `libs/shared/provider-rpc/src` from the accepted controller contract. The manifest bundle is referenced by generated OpenAPI and RPC outputs. Commit `contract-graph.coverage.json` only when audit artifacts need it. Commit `openapi.json` only when external consumers or deployment artifacts need it; the provider-rpc client is generated from the server contract and is typechecked by the verification gate.
Intentional API contract changes should update `contract-graph.snapshot.json` with `pnpm contract:snapshot`, then update and commit the Project Map, manifest bundle, `openapi.json`, and provider-rpc client with `pnpm codegen`. CI should run `pnpm contract:verify` or `pnpm ci:contracts`; it checks these committed artifacts without rewriting them. Commit `contract-graph.coverage.json` only when audit artifacts need it.
Run `pnpm di:graph` when intentionally regenerating the DI graph. `pnpm di:verify` validates the existing DI graph and Project Map artifacts without rewriting them, asserts required manifest fields, and runs `croco doctor --json`.

Generated OpenAPI/RPC commands run strict ContractGraph schema checks by default and pass `--fail-on-diagnostics`, so strict Problem contract diagnostics block generated artifacts. `--compatibility-schemas` and `--compatibility-problems` are migration-only opt-outs for legacy routes, not generated app CI settings.
Expand Down
8 changes: 5 additions & 3 deletions packages/create-croco-app/templates/ai-saas/package.json.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@
"packageManager": "pnpm@11.9.0",
"scripts": {
"dev:api": "pnpm --filter {{scope}}/api-server dev",
"contract:check": "pnpm contract:client && pnpm --filter {{scope}}/provider-rpc typecheck",
"contract:check": "croco-rpc-codegen --controllers \"apps/api-server/src/controllers/**/*.ts\" --check --strict-schemas --fail-on-diagnostics",
"contract:snapshot": "croco contracts check --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas --json --out contract-graph.snapshot.json",
"contract:diff": "croco contracts diff --baseline contract-graph.snapshot.json --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas",
"contract:coverage": "croco contracts check --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas --json --out contract-graph.coverage.json",
"project-map:write": "croco project map --controllers \"apps/api-server/src/controllers/**/*.ts\" --out croco.project-map.json --manifest-bundle .croco/manifest",
"project-map:check": "croco project map --controllers \"apps/api-server/src/controllers/**/*.ts\" --check --manifest croco.project-map.json --manifest-bundle .croco/manifest",
"contract:verify": "pnpm contract:diff && pnpm contract:coverage && pnpm project-map:write && pnpm project-map:check && pnpm contract:openapi && pnpm contract:client && pnpm --filter {{scope}}/provider-rpc typecheck",
"contract:verify": "pnpm contract:diff && pnpm contract:check && pnpm project-map:check && pnpm contract:openapi:check && pnpm contract:client:check && pnpm --filter {{scope}}/provider-rpc typecheck",
"ci:contracts": "pnpm contract:verify",
"di:graph": "pnpm --filter {{scope}}/api-server di:graph",
"di:check": "croco di check .croco/build/di-graph.manifest.json",
"di:assert": "node scripts/assert-di-graph.mjs .croco/build/di-graph.manifest.json",
"doctor": "croco doctor --json",
"di:verify": "pnpm di:check && pnpm di:assert && pnpm project-map:check && pnpm doctor",
"contract:openapi": "pnpm contract:check && croco-openapi-spec --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas --fail-on-diagnostics --out openapi.json --title \"{{projectName}} AI SaaS API\" --version 0.1.0 --server http://localhost:3000 --manifest-bundle .croco/manifest",
"contract:openapi:check": "pnpm contract:check && croco-openapi-spec --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas --fail-on-diagnostics --out openapi.json --title \"{{projectName}} AI SaaS API\" --version 0.1.0 --server http://localhost:3000 --manifest-bundle .croco/manifest --output-check",
"contract:client": "croco-rpc-codegen --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas --fail-on-diagnostics --out libs/shared/provider-rpc/src --manifest-bundle .croco/manifest",
"codegen": "pnpm contract:client",
"contract:client:check": "croco-rpc-codegen --controllers \"apps/api-server/src/controllers/**/*.ts\" --strict-schemas --fail-on-diagnostics --out libs/shared/provider-rpc/src --manifest-bundle .croco/manifest --output-check",
"codegen": "pnpm project-map:write && pnpm contract:openapi && pnpm contract:client",
"demo:seed": "pnpm --filter {{scope}}/api-server demo:seed",
"demo:smoke": "pnpm contract:check && pnpm --filter {{scope}}/api-server demo:smoke && pnpm --filter {{scope}}/api-server ops:smoke && pnpm --filter {{scope}}/api-server ai:smoke",
"ops:smoke": "pnpm --filter {{scope}}/api-server ops:smoke",
Expand Down
Loading
Loading