@cherishxn/agent-scaffold 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +30 -2
  2. package/THIRD_PARTY_NOTICES.md +4 -0
  3. package/dist/cli.js +9 -4
  4. package/dist/diagnostics.d.ts +10 -0
  5. package/dist/diagnostics.js +27 -0
  6. package/dist/generate.js +8 -3
  7. package/dist/model.d.ts +1 -1
  8. package/dist/model.js +1 -1
  9. package/dist/verify.d.ts +7 -1
  10. package/dist/verify.js +36 -22
  11. package/package.json +5 -2
  12. package/skills/scaffold-use/SKILL.md +20 -2
  13. package/templates/catalog.json +16 -16
  14. package/templates/next-fullstack-antd/.agents/skills/project-work/SKILL.md +6 -4
  15. package/templates/next-fullstack-antd/.claude/skills/project-work/SKILL.md +6 -4
  16. package/templates/next-fullstack-antd/.grok/skills/project-work/SKILL.md +6 -4
  17. package/templates/next-fullstack-antd/AGENTS.md +1 -1
  18. package/templates/next-fullstack-antd/README.md +14 -5
  19. package/templates/next-fullstack-antd/THIRD_PARTY_NOTICES.md +4 -0
  20. package/templates/next-fullstack-antd/docs/PROJECT.md +23 -0
  21. package/templates/next-fullstack-antd/docs/api.d.ts +32 -4
  22. package/templates/next-fullstack-antd/docs/openapi.json +25 -3
  23. package/templates/next-fullstack-antd/package-lock.json +54 -0
  24. package/templates/next-fullstack-antd/package.json +5 -0
  25. package/templates/next-fullstack-antd/playwright.config.ts +1 -1
  26. package/templates/next-fullstack-antd/scripts/api-check.mjs +18 -0
  27. package/templates/next-fullstack-antd/scripts/api-contract.mjs +46 -0
  28. package/templates/next-fullstack-antd/scripts/project.mjs +4 -2
  29. package/templates/next-fullstack-antd/tests/contract-client.ts +20 -0
  30. package/templates/next-fullstack-antd/tests/contract.spec.ts +4 -2
  31. package/templates/next-fullstack-shadcn/.agents/skills/project-work/SKILL.md +6 -4
  32. package/templates/next-fullstack-shadcn/.claude/skills/project-work/SKILL.md +6 -4
  33. package/templates/next-fullstack-shadcn/.grok/skills/project-work/SKILL.md +6 -4
  34. package/templates/next-fullstack-shadcn/AGENTS.md +1 -1
  35. package/templates/next-fullstack-shadcn/README.md +14 -5
  36. package/templates/next-fullstack-shadcn/THIRD_PARTY_NOTICES.md +4 -0
  37. package/templates/next-fullstack-shadcn/docs/PROJECT.md +23 -0
  38. package/templates/next-fullstack-shadcn/docs/api.d.ts +32 -4
  39. package/templates/next-fullstack-shadcn/docs/openapi.json +25 -3
  40. package/templates/next-fullstack-shadcn/package-lock.json +54 -0
  41. package/templates/next-fullstack-shadcn/package.json +5 -0
  42. package/templates/next-fullstack-shadcn/playwright.config.ts +1 -1
  43. package/templates/next-fullstack-shadcn/scripts/api-check.mjs +18 -0
  44. package/templates/next-fullstack-shadcn/scripts/api-contract.mjs +46 -0
  45. package/templates/next-fullstack-shadcn/scripts/project.mjs +4 -2
  46. package/templates/next-fullstack-shadcn/tests/contract-client.ts +20 -0
  47. package/templates/next-fullstack-shadcn/tests/contract.spec.ts +4 -2
  48. package/templates/react-fastapi-antd/.agents/skills/project-work/SKILL.md +6 -4
  49. package/templates/react-fastapi-antd/.claude/skills/project-work/SKILL.md +6 -4
  50. package/templates/react-fastapi-antd/.grok/skills/project-work/SKILL.md +6 -4
  51. package/templates/react-fastapi-antd/AGENTS.md +1 -1
  52. package/templates/react-fastapi-antd/README.md +14 -5
  53. package/templates/react-fastapi-antd/THIRD_PARTY_NOTICES.md +4 -0
  54. package/templates/react-fastapi-antd/docs/PROJECT.md +23 -0
  55. package/templates/react-fastapi-antd/docs/api.d.ts +32 -4
  56. package/templates/react-fastapi-antd/docs/openapi.json +25 -3
  57. package/templates/react-fastapi-antd/package-lock.json +54 -0
  58. package/templates/react-fastapi-antd/package.json +5 -0
  59. package/templates/react-fastapi-antd/playwright.config.ts +1 -1
  60. package/templates/react-fastapi-antd/scripts/api-check.mjs +18 -0
  61. package/templates/react-fastapi-antd/scripts/api-contract.mjs +46 -0
  62. package/templates/react-fastapi-antd/scripts/project.mjs +4 -2
  63. package/templates/react-fastapi-antd/tests/contract-client.ts +20 -0
  64. package/templates/react-fastapi-antd/tests/contract.spec.ts +4 -2
  65. package/templates/react-fastapi-shadcn/.agents/skills/project-work/SKILL.md +6 -4
  66. package/templates/react-fastapi-shadcn/.claude/skills/project-work/SKILL.md +6 -4
  67. package/templates/react-fastapi-shadcn/.grok/skills/project-work/SKILL.md +6 -4
  68. package/templates/react-fastapi-shadcn/AGENTS.md +1 -1
  69. package/templates/react-fastapi-shadcn/README.md +14 -5
  70. package/templates/react-fastapi-shadcn/THIRD_PARTY_NOTICES.md +4 -0
  71. package/templates/react-fastapi-shadcn/docs/PROJECT.md +23 -0
  72. package/templates/react-fastapi-shadcn/docs/api.d.ts +32 -4
  73. package/templates/react-fastapi-shadcn/docs/openapi.json +25 -3
  74. package/templates/react-fastapi-shadcn/package-lock.json +54 -0
  75. package/templates/react-fastapi-shadcn/package.json +5 -0
  76. package/templates/react-fastapi-shadcn/playwright.config.ts +1 -1
  77. package/templates/react-fastapi-shadcn/scripts/api-check.mjs +18 -0
  78. package/templates/react-fastapi-shadcn/scripts/api-contract.mjs +46 -0
  79. package/templates/react-fastapi-shadcn/scripts/project.mjs +4 -2
  80. package/templates/react-fastapi-shadcn/tests/contract-client.ts +20 -0
  81. package/templates/react-fastapi-shadcn/tests/contract.spec.ts +4 -2
  82. package/templates/react-nest-antd/.agents/skills/project-work/SKILL.md +6 -4
  83. package/templates/react-nest-antd/.claude/skills/project-work/SKILL.md +6 -4
  84. package/templates/react-nest-antd/.grok/skills/project-work/SKILL.md +6 -4
  85. package/templates/react-nest-antd/AGENTS.md +1 -1
  86. package/templates/react-nest-antd/README.md +14 -5
  87. package/templates/react-nest-antd/THIRD_PARTY_NOTICES.md +4 -0
  88. package/templates/react-nest-antd/docs/PROJECT.md +23 -0
  89. package/templates/react-nest-antd/docs/api.d.ts +32 -4
  90. package/templates/react-nest-antd/docs/openapi.json +25 -3
  91. package/templates/react-nest-antd/package-lock.json +54 -0
  92. package/templates/react-nest-antd/package.json +5 -0
  93. package/templates/react-nest-antd/playwright.config.ts +1 -1
  94. package/templates/react-nest-antd/scripts/api-check.mjs +18 -0
  95. package/templates/react-nest-antd/scripts/api-contract.mjs +46 -0
  96. package/templates/react-nest-antd/scripts/project.mjs +4 -2
  97. package/templates/react-nest-antd/tests/contract-client.ts +20 -0
  98. package/templates/react-nest-antd/tests/contract.spec.ts +4 -2
  99. package/templates/react-nest-shadcn/.agents/skills/project-work/SKILL.md +6 -4
  100. package/templates/react-nest-shadcn/.claude/skills/project-work/SKILL.md +6 -4
  101. package/templates/react-nest-shadcn/.grok/skills/project-work/SKILL.md +6 -4
  102. package/templates/react-nest-shadcn/AGENTS.md +1 -1
  103. package/templates/react-nest-shadcn/README.md +14 -5
  104. package/templates/react-nest-shadcn/THIRD_PARTY_NOTICES.md +4 -0
  105. package/templates/react-nest-shadcn/docs/PROJECT.md +23 -0
  106. package/templates/react-nest-shadcn/docs/api.d.ts +32 -4
  107. package/templates/react-nest-shadcn/docs/openapi.json +25 -3
  108. package/templates/react-nest-shadcn/package-lock.json +54 -0
  109. package/templates/react-nest-shadcn/package.json +5 -0
  110. package/templates/react-nest-shadcn/playwright.config.ts +1 -1
  111. package/templates/react-nest-shadcn/scripts/api-check.mjs +18 -0
  112. package/templates/react-nest-shadcn/scripts/api-contract.mjs +46 -0
  113. package/templates/react-nest-shadcn/scripts/project.mjs +4 -2
  114. package/templates/react-nest-shadcn/tests/contract-client.ts +20 -0
  115. package/templates/react-nest-shadcn/tests/contract.spec.ts +4 -2
  116. package/templates/react-spring-antd/.agents/skills/project-work/SKILL.md +6 -4
  117. package/templates/react-spring-antd/.claude/skills/project-work/SKILL.md +6 -4
  118. package/templates/react-spring-antd/.grok/skills/project-work/SKILL.md +6 -4
  119. package/templates/react-spring-antd/AGENTS.md +1 -1
  120. package/templates/react-spring-antd/README.md +14 -5
  121. package/templates/react-spring-antd/THIRD_PARTY_NOTICES.md +4 -0
  122. package/templates/react-spring-antd/backend/src/main/resources/openapi.json +25 -3
  123. package/templates/react-spring-antd/docs/PROJECT.md +23 -0
  124. package/templates/react-spring-antd/docs/api.d.ts +32 -4
  125. package/templates/react-spring-antd/docs/openapi.json +25 -3
  126. package/templates/react-spring-antd/package-lock.json +54 -0
  127. package/templates/react-spring-antd/package.json +5 -0
  128. package/templates/react-spring-antd/playwright.config.ts +1 -1
  129. package/templates/react-spring-antd/scripts/api-check.mjs +18 -0
  130. package/templates/react-spring-antd/scripts/api-contract.mjs +46 -0
  131. package/templates/react-spring-antd/scripts/project.mjs +4 -2
  132. package/templates/react-spring-antd/tests/contract-client.ts +20 -0
  133. package/templates/react-spring-antd/tests/contract.spec.ts +4 -2
  134. package/templates/react-spring-shadcn/.agents/skills/project-work/SKILL.md +6 -4
  135. package/templates/react-spring-shadcn/.claude/skills/project-work/SKILL.md +6 -4
  136. package/templates/react-spring-shadcn/.grok/skills/project-work/SKILL.md +6 -4
  137. package/templates/react-spring-shadcn/AGENTS.md +1 -1
  138. package/templates/react-spring-shadcn/README.md +14 -5
  139. package/templates/react-spring-shadcn/THIRD_PARTY_NOTICES.md +4 -0
  140. package/templates/react-spring-shadcn/backend/src/main/resources/openapi.json +25 -3
  141. package/templates/react-spring-shadcn/docs/PROJECT.md +23 -0
  142. package/templates/react-spring-shadcn/docs/api.d.ts +32 -4
  143. package/templates/react-spring-shadcn/docs/openapi.json +25 -3
  144. package/templates/react-spring-shadcn/package-lock.json +54 -0
  145. package/templates/react-spring-shadcn/package.json +5 -0
  146. package/templates/react-spring-shadcn/playwright.config.ts +1 -1
  147. package/templates/react-spring-shadcn/scripts/api-check.mjs +18 -0
  148. package/templates/react-spring-shadcn/scripts/api-contract.mjs +46 -0
  149. package/templates/react-spring-shadcn/scripts/project.mjs +4 -2
  150. package/templates/react-spring-shadcn/tests/contract-client.ts +20 -0
  151. package/templates/react-spring-shadcn/tests/contract.spec.ts +4 -2
@@ -1,3 +1,3 @@
1
1
  import { defineConfig } from '@playwright/test';
2
2
  const port=process.env.WEB_PORT||'5173';
3
- export default defineConfig({testDir:'./tests',outputDir:process.env.SCAFFOLD_REPORT_DIR?process.env.SCAFFOLD_REPORT_DIR+'/playwright':'test-results',workers:1,retries:0,timeout:30000,reporter:[['line'],['junit',{outputFile:process.env.PLAYWRIGHT_JUNIT_OUTPUT_FILE||'reports/playwright.xml'}]],use:{baseURL:`http://127.0.0.1:${port}`,headless:true,trace:'off',screenshot:'off'},webServer:{command:'node scripts/project.mjs serve',url:`http://127.0.0.1:${port}/api/health`,reuseExistingServer:false,timeout:120000,stdout:'ignore',stderr:'pipe'},expect:{timeout:10000}});
3
+ export default defineConfig({testDir:'./tests',outputDir:process.env.SCAFFOLD_REPORT_DIR?process.env.SCAFFOLD_REPORT_DIR+'/playwright':'test-results',workers:1,retries:0,timeout:30000,reporter:[['line'],['junit',{outputFile:process.env.PLAYWRIGHT_JUNIT_OUTPUT_FILE||'reports/playwright.xml'}]],use:{baseURL:`http://127.0.0.1:${port}`,headless:true,trace:'off',screenshot:'off'},webServer:{command:'node scripts/project.mjs serve',url:`http://127.0.0.1:${port}/api/health`,reuseExistingServer:false,timeout:120000,stdout:'pipe',stderr:'pipe'},expect:{timeout:10000}});
@@ -0,0 +1,18 @@
1
+ import {mkdtemp,readFile,rm} from 'node:fs/promises';
2
+ import {tmpdir} from 'node:os';
3
+ import {join,resolve} from 'node:path';
4
+ import {spawnSync} from 'node:child_process';
5
+
6
+ // Generate outside the workspace: validation never repairs or overwrites source.
7
+ const directory=await mkdtemp(join(tmpdir(),'scaffold-api-check-'));
8
+ try{
9
+ const target=join(directory,'api.d.ts');
10
+ const result=spawnSync(process.execPath,['node_modules/openapi-typescript/bin/cli.js','docs/openapi.json','-o',target],{cwd:resolve('.'),encoding:'utf8'});
11
+ if(result.status!==0)throw new Error('API_CONTRACT_INVALID: OpenAPI generation failed. '+result.stderr);
12
+ const expected=await readFile(target,'utf8'),actual=await readFile('docs/api.d.ts','utf8');
13
+ if(expected!==actual){
14
+ const a=actual.split('\n'),e=expected.split('\n');let line=0;while(line<Math.max(a.length,e.length)&&a[line]===e[line])line++;
15
+ throw new Error(`API_CONTRACT_DRIFT: docs/api.d.ts differs from docs/openapi.json at line ${line+1}. Run npm run api:generate after reviewing the intended contract.`);
16
+ }
17
+ console.log('API contract and generated types match.');
18
+ }finally{await rm(directory,{recursive:true,force:true});}
@@ -0,0 +1,46 @@
1
+ import Ajv from 'ajv';
2
+ import addFormats from 'ajv-formats';
3
+
4
+ export function responseValidator(document){
5
+ const ajv=new Ajv({strict:false,allErrors:true});addFormats(ajv);
6
+ ajv.addSchema(document,'contract');
7
+ const operations=[];
8
+ function dereference(value,pointer){
9
+ const seen=new Set();
10
+ while(value?.$ref){
11
+ const ref=value.$ref;
12
+ if(!ref.startsWith('#/')||seen.has(ref))throw new Error('API_CONTRACT_INVALID: unsupported or circular response reference');
13
+ seen.add(ref);pointer=ref.slice(1);value=pointer.slice(1).split('/').map(s=>s.replaceAll('~1','/').replaceAll('~0','~')).reduce((at,key)=>at?.[key],document);
14
+ if(!value)throw new Error('API_CONTRACT_INVALID: unresolved response reference');
15
+ }
16
+ return {value,pointer};
17
+ }
18
+ const pointer=value=>value.replaceAll('~','~0').replaceAll('/','~1');
19
+ for(const [path,item] of Object.entries(document.paths))for(const [method,operation] of Object.entries(item)){
20
+ if(!['get','post','put','delete','patch','head','options'].includes(method))continue;
21
+ const statuses=new Map();
22
+ for(const [status,response] of Object.entries(operation.responses)){
23
+ const resolved=dereference(response,`/paths/${pointer(path)}/${method}/responses/${pointer(status)}`);
24
+ const content=resolved.value.content?.['application/json'];
25
+ if(resolved.value.content&&!content)throw new Error('API_CONTRACT_INVALID: only JSON response schemas are supported');
26
+ const validate=content?ajv.compile({$ref:`contract#${resolved.pointer}/content/application~1json/schema`}):null;
27
+ statuses.set(status,validate);
28
+ }
29
+ operations.push({path,parts:path.split('/'),method,statuses});
30
+ }
31
+ return (method,url,status,headers,body)=>{
32
+ const parts=new URL(url,'http://contract.invalid').pathname.split('/');
33
+ const operation=operations.find(o=>o.method===method.toLowerCase()&&o.parts.length===parts.length&&o.parts.every((part,i)=>part===parts[i]||/^\{[^}]+\}$/.test(part)&&parts[i]!==''));
34
+ const label=`API_RESPONSE_INVALID: ${method.toUpperCase()} ${operation?.path??'unlisted operation'} status ${status}`;
35
+ if(!operation)throw new Error(label+' is absent from docs/openapi.json');
36
+ const key=operation.statuses.has(String(status))?String(status):operation.statuses.has(`${Math.floor(status/100)}XX`)?`${Math.floor(status/100)}XX`:'default';
37
+ if(!operation.statuses.has(key))throw new Error(label+' is not declared');
38
+ const validate=operation.statuses.get(key);
39
+ if(status===204&&body.length)throw new Error(label+' must have an empty body');
40
+ if(validate){
41
+ if(!/^application\/json(?:;|$)/i.test(headers['content-type']??''))throw new Error(label+' requires application/json');
42
+ let data;try{data=JSON.parse(body);}catch{throw new Error(label+' is not valid JSON');}
43
+ if(!validate(data))throw new Error(label+' '+validate.errors.map(e=>`${e.instancePath||'/'} ${e.keyword}${e.params.missingProperty?' '+e.params.missingProperty:''}`).join('; '));
44
+ }
45
+ };
46
+ }
@@ -88,9 +88,10 @@ async function setup() {
88
88
  }
89
89
  const command=process.argv[2];
90
90
  try {
91
- mkdirSync('reports',{recursive:true});
91
+ if(command!=='api:check')mkdirSync('reports',{recursive:true});
92
92
  if(command==='install')await install();
93
93
  else if(command==='setup'){await install();await setup();}
94
+ else if(command==='check'){for(const task of ['api:check','typecheck','lint','build'])await run('node',['scripts/project.mjs',task]);}
94
95
  else if(command==='dev:setup')await setup();
95
96
  else if(command==='migrate')await migrate();
96
97
  else if(command==='seed')await seed();
@@ -113,6 +114,7 @@ try {
113
114
  else if(stack==='react-nest')await run('pnpm',['run','test'],'backend');
114
115
  else await run('pnpm',['run','test'],'web');
115
116
  } else if(command==='test:api'||command==='test:e2e')await run('npx',['--no-install','playwright','test',command==='test:api'?'tests/contract.spec.ts':'tests/ui.spec.ts']);
117
+ else if(command==='api:check')await run('node',['scripts/api-check.mjs']);
116
118
  else if(command==='api:generate')await run('npx',['--no-install','openapi-typescript','docs/openapi.json','-o','docs/api.d.ts']);
117
- else throw new Error('Use setup, install, dev:setup, dev, build, typecheck, lint, migrate, seed, test:native, test:api, test:e2e, api:generate.');
119
+ else throw new Error('Use setup, install, dev:setup, dev, check, build, typecheck, lint, migrate, seed, test:native, test:api, test:e2e, api:check, api:generate.');
118
120
  } catch(e) {stop();console.error(e instanceof Error?e.message:'Project command failed');process.exitCode=process.exitCode||1;}
@@ -0,0 +1,20 @@
1
+ import {request as raw,type APIRequestContext,type APIResponse} from '@playwright/test';
2
+ import document from '../docs/openapi.json' with {type:'json'};
3
+ import {responseValidator} from '../scripts/api-contract.mjs';
4
+ const validate=responseValidator(document);
5
+ export const requests={
6
+ async newContext(options?:Parameters<typeof raw.newContext>[0]):Promise<APIRequestContext>{
7
+ const context=await raw.newContext(options);
8
+ return new Proxy(context,{
9
+ get(target,key){
10
+ const value=Reflect.get(target,key,target);
11
+ if(['get','post','put','delete','patch','head'].includes(String(key)))return async(...args:unknown[])=>{
12
+ const response:APIResponse=await value.apply(target,args);
13
+ validate(String(key),response.url(),response.status(),response.headers(),(await response.body()).toString('utf8'));
14
+ return response;
15
+ };
16
+ return typeof value==='function'?value.bind(target):value;
17
+ }
18
+ });
19
+ }
20
+ };
@@ -1,14 +1,16 @@
1
- import {test,expect,request as requests,type APIRequestContext} from '@playwright/test';
1
+ import {requests} from './contract-client';
2
+ import {test,expect,type APIRequestContext} from '@playwright/test';
2
3
  import project from '../project.json' with {type:'json'};
3
4
  const baseURL=`http://127.0.0.1:${process.env.WEB_PORT||5173}`;
4
5
  const origin=process.env.APP_ORIGIN||baseURL;
5
6
  async function login(email:string){const context=await requests.newContext({baseURL,extraHTTPHeaders:{Origin:origin}});const res=await context.post('/api/auth/login',{data:{email,password:process.env.SEED_PASSWORD}});expect(res.status()).toBe(200);const auth=await res.json();if(auth.token)await context.dispose();const client:APIRequestContext=auth.token?await requests.newContext({baseURL,extraHTTPHeaders:{Origin:origin,Authorization:`Bearer ${auth.token}`}}):context;return {client,auth};}
6
7
  test('HTTP identity, persistent CRUD, pagination and owner isolation',async()=>{
8
+ const health=await requests.newContext({baseURL});expect((await health.get('/api/health')).status()).toBe(200);await health.dispose();
7
9
  const anonymous=await requests.newContext({baseURL,extraHTTPHeaders:{Origin:origin}});
8
10
  expect((await anonymous.get('/api/records')).status()).toBe(401);
9
11
  expect((await anonymous.post('/api/records',{data:{title:'x',content:''}})).status()).toBe(401);
10
12
  expect((await anonymous.post('/api/auth/login',{data:{email:process.env.SEED_A_EMAIL,password:'invalid-password'}})).status()).toBe(401);
11
- const {client:a}=await login(process.env.SEED_A_EMAIL!);const {client:b}=await login(process.env.SEED_B_EMAIL!);
13
+ const {client:a}=await login(process.env.SEED_A_EMAIL!);const {client:b}=await login(process.env.SEED_B_EMAIL!);expect((await a.get('/api/auth/me')).status()).toBe(200);
12
14
  const body=process.env.TEST_BODY_MARKER||'synthetic-content';
13
15
  const res=await a.post('/api/records',{data:{title:' Contract record ',content:body}});expect(res.status()).toBe(201);const row=await res.json();const path=`/api/records/${row.id}`;
14
16
  expect(row.title).toBe('Contract record');expect((await a.get(path)).status()).toBe(200);
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: project-work
3
- description: Implement or fix this generated project using its documented commands and verify the requested behavior.
3
+ description: Implement features, fix defects, or change API/data/UI behavior in this generated project, using its task map and real validation commands.
4
4
  ---
5
5
 
6
- Read AGENTS.md, docs/PROJECT.md and README.md. Check Git changes and the current request. Implement only the requested behavior. Run the relevant project checks, inspect the diff, and report files, commands and actual results. A failed or unrun check is not a pass. Follow the task's existing authorization for external writes.
7
-
8
- For UI work, read root DESIGN.md before editing. Follow its source/sync workflow, use the bundled design-contract Skill, run `npm run design:check` and `npm run design:verify`, and verify affected business flows in the browser.
6
+ 1. Read AGENTS.md, README.md and the task map in docs/PROJECT.md. Check project.json for the selected stack and inspect the current Git changes.
7
+ 2. State the requested observable behavior and failure cases. Follow the task map to the implementation and callers; use existing interfaces and test conventions.
8
+ 3. Implement the requested change. API or data changes include the contract, a new migration when needed, and affected authorization tests. Run `npm run api:generate` after changing OpenAPI; `npm run api:check` detects stale types without modifying source, and HTTP tests validate response schemas. UI changes first read DESIGN.md and use design-contract for source, sync and browser checks.
9
+ 4. Run the affected checks, then `npm run check` for type/lint/build feedback. For API, data, auth or complete delivery, follow README's isolated `verify` command; UI work also needs design:check and design:verify. Check both exit status and the report's overallStatus/validity.
10
+ 5. Inspect the final diff. Report the behavior, actual commands/results and remaining limits. A quick check, doctor, historical report or generated file is not a current business pass. Keep credentials and raw runtime data local; apply the user's existing authorization to external writes.
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: project-work
3
- description: Implement or fix this generated project using its documented commands and verify the requested behavior.
3
+ description: Implement features, fix defects, or change API/data/UI behavior in this generated project, using its task map and real validation commands.
4
4
  ---
5
5
 
6
- Read AGENTS.md, docs/PROJECT.md and README.md. Check Git changes and the current request. Implement only the requested behavior. Run the relevant project checks, inspect the diff, and report files, commands and actual results. A failed or unrun check is not a pass. Follow the task's existing authorization for external writes.
7
-
8
- For UI work, read root DESIGN.md before editing. Follow its source/sync workflow, use the bundled design-contract Skill, run `npm run design:check` and `npm run design:verify`, and verify affected business flows in the browser.
6
+ 1. Read AGENTS.md, README.md and the task map in docs/PROJECT.md. Check project.json for the selected stack and inspect the current Git changes.
7
+ 2. State the requested observable behavior and failure cases. Follow the task map to the implementation and callers; use existing interfaces and test conventions.
8
+ 3. Implement the requested change. API or data changes include the contract, a new migration when needed, and affected authorization tests. Run `npm run api:generate` after changing OpenAPI; `npm run api:check` detects stale types without modifying source, and HTTP tests validate response schemas. UI changes first read DESIGN.md and use design-contract for source, sync and browser checks.
9
+ 4. Run the affected checks, then `npm run check` for type/lint/build feedback. For API, data, auth or complete delivery, follow README's isolated `verify` command; UI work also needs design:check and design:verify. Check both exit status and the report's overallStatus/validity.
10
+ 5. Inspect the final diff. Report the behavior, actual commands/results and remaining limits. A quick check, doctor, historical report or generated file is not a current business pass. Keep credentials and raw runtime data local; apply the user's existing authorization to external writes.
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: project-work
3
- description: Implement or fix this generated project using its documented commands and verify the requested behavior.
3
+ description: Implement features, fix defects, or change API/data/UI behavior in this generated project, using its task map and real validation commands.
4
4
  ---
5
5
 
6
- Read AGENTS.md, docs/PROJECT.md and README.md. Check Git changes and the current request. Implement only the requested behavior. Run the relevant project checks, inspect the diff, and report files, commands and actual results. A failed or unrun check is not a pass. Follow the task's existing authorization for external writes.
7
-
8
- For UI work, read root DESIGN.md before editing. Follow its source/sync workflow, use the bundled design-contract Skill, run `npm run design:check` and `npm run design:verify`, and verify affected business flows in the browser.
6
+ 1. Read AGENTS.md, README.md and the task map in docs/PROJECT.md. Check project.json for the selected stack and inspect the current Git changes.
7
+ 2. State the requested observable behavior and failure cases. Follow the task map to the implementation and callers; use existing interfaces and test conventions.
8
+ 3. Implement the requested change. API or data changes include the contract, a new migration when needed, and affected authorization tests. Run `npm run api:generate` after changing OpenAPI; `npm run api:check` detects stale types without modifying source, and HTTP tests validate response schemas. UI changes first read DESIGN.md and use design-contract for source, sync and browser checks.
9
+ 4. Run the affected checks, then `npm run check` for type/lint/build feedback. For API, data, auth or complete delivery, follow README's isolated `verify` command; UI work also needs design:check and design:verify. Check both exit status and the report's overallStatus/validity.
10
+ 5. Inspect the final diff. Report the behavior, actual commands/results and remaining limits. A quick check, doctor, historical report or generated file is not a current business pass. Keep credentials and raw runtime data local; apply the user's existing authorization to external writes.
@@ -6,6 +6,6 @@
6
6
 
7
7
  仅修改任务范围,保留其他改动。秘密只放本地环境,不提交或打印;模板 verify 使用隔离数据库。推送、发布和外部写入依任务授权执行。
8
8
 
9
- 需要开发指引时使用 project-work Skill;未自动发现时按以上规则和 README 执行。
9
+ 实现功能、修复缺陷时使用 project-work Skill;先按 docs/PROJECT.md 的任务表定位实现、迁移和测试入口。未自动发现 Skill 时按该表和 README 执行。
10
10
 
11
11
  修改 UI 前先读根目录 DESIGN.md。使用 design-contract Skill;设计源、同步命令和一致性检查以该文件为准。交付前运行 design:check 与 design:verify,报告实际覆盖范围。
@@ -15,14 +15,23 @@ npm run dev
15
15
 
16
16
  ## 验证
17
17
 
18
- 安装依赖后,由已安装的 CLI 执行:
18
+ 安装前可只检查工具;doctor 不会安装依赖或启动服务:
19
19
 
20
20
  ```bash
21
- scaffold doctor --dir . --json
22
- scaffold verify --dir . --profile baseline --json
23
- scaffold report --dir . --json
21
+ npx @cherishxn/agent-scaffold@__CLI_VERSION__ doctor --dir . --tools-only --json
24
22
  ```
25
23
 
24
+ 安装依赖后,先做快速检查,再按需要验证完整业务:
25
+
26
+ ```bash
27
+ npm run check
28
+ npx @cherishxn/agent-scaffold@__CLI_VERSION__ doctor --dir . --json
29
+ npx @cherishxn/agent-scaffold@__CLI_VERSION__ verify --dir . --profile baseline --json
30
+ npx @cherishxn/agent-scaffold@__CLI_VERSION__ report --dir . --json
31
+ ```
32
+
33
+ `check` 顺序执行类型、Lint、构建,任一步失败即停止;它不创建数据库、不执行迁移或业务测试。
34
+
26
35
  verify 创建自己的临时 PostgreSQL 并注入连接,不使用开发 DATABASE_URL;完成后释放本次容器。单独运行项目命令时需要显式提供本地环境:
27
36
 
28
37
  ```bash
@@ -35,7 +44,7 @@ node scripts/project.mjs test:e2e
35
44
  node scripts/project.mjs api:generate
36
45
  ```
37
46
 
38
- 接口:docs/openapi.json;结构:docs/PROJECT.md;规则:AGENTS.md。按需运行 api:generate 更新 TypeScript 客户端类型,不在 verify 中修改源码。
47
+ 接口:docs/openapi.json;结构:docs/PROJECT.md;规则:AGENTS.md。修改契约后运行 npm run api:generate 更新 TypeScript 类型。npm run api:check 在临时目录重新生成并只读比较,check/verify 自动执行;test:api 同时按 OpenAPI 检查真实响应状态、必填字段和类型。验证不会自动修改源码。
39
48
 
40
49
  ## 认证边界
41
50
 
@@ -12,3 +12,7 @@ Templates are maintained derivatives, not unmodified upstream distributions. Exa
12
12
  | Ant Design | npm 6.6.6 | Installed dependency; no copied source | MIT (package license) |
13
13
 
14
14
  Package dependencies carry their own licenses. Generated apps include these notices; template authors must carry applicable notices when changing upstream-derived files. Names and marks remain their respective owners'.
15
+
16
+ ## Design and workflow reference
17
+
18
+ VinkyDev/vibestart at commit `5768fc561340422bf7736499fe69b1aae263790a` (MIT) informed the preview, task-oriented Agent guidance, quick checks and template-layer collision rules. These changes are independently implemented for Agent Scaffold; no VibeStart source, bundled skills, branding or UI assets are redistributed. The source review and selected adaptations are recorded in docs/reference/VIBESTART-REVIEW.md.
@@ -7,3 +7,26 @@
7
7
  project.json 标识技术栈和 UI;web/ 是界面或 Next 全栈应用;独立后端位于 backend/。scripts/project.mjs 提供实际安装、开发、测试、构建命令。数据库结构以迁移为准,接口见 docs/openapi.json 和生成的 docs/api.d.ts。
8
8
 
9
9
  后续需求在团队已有权威来源或本文记录,不要求接入特定任务平台。名称和简介来自创建输入,修改 project.json 可更新界面展示。
10
+
11
+
12
+ ## 从任务找到改动位置
13
+
14
+ 先核对 project.json 的实际技术栈;以下路径只描述本项目。读取命中的实现和调用方后再修改,不将基础示例自动扩展成用户未要求的业务。
15
+
16
+ | 任务 | 入口 | 改动约束 |
17
+ | --- | --- | --- |
18
+ | 页面交互 | `web/src/App.tsx`、`web/src/workspace-model.ts` | 业务状态放模型,页面负责绑定;身份结束时清空用户数据。 |
19
+ | 设计与组件 | `DESIGN.md`、`design/`、`web/src/ui.tsx` | 先改共享设计意图,再改适配器;按 design-contract 执行同步与浏览器检查。 |
20
+ | API 与身份隔离 | `backend/src/app.module.ts`、`backend/src/password.ts` | 查询始终限定当前用户;JWT 保留在浏览器内存。 |
21
+ | 模型与迁移 | `backend/prisma/schema.prisma`、`backend/prisma/migrations/` | 新增 Prisma 迁移,保留已经运行过的迁移。 |
22
+ | 后端回归 | `backend/test/api.test.ts` | 用真实 PostgreSQL 验证数据和越权行为。 |
23
+ | HTTP 契约 | `docs/openapi.json`、`docs/api.d.ts`、`tests/contract.spec.ts` | 修改输入输出时同步契约与真实 HTTP 回归;用 api:generate 生成类型。 |
24
+ | 用户流程 | `tests/ui.spec.ts` | 覆盖真实操作、持久化和失败反馈,测试仅使用专用数据。 |
25
+ | 启动与依赖 | `package.json`、`scripts/project.mjs`、`compose.yaml` | 使用锁文件和项目命令;配置、账号及运行报告留在本地。 |
26
+
27
+ ## 修改与验证
28
+
29
+ 1. 将当前需求转成可观察行为与失败场景,复用现有契约和测试入口。
30
+ 2. 做能贯通当前需求的最小改动,先运行相关检查;`npm run check` 按类型、Lint、构建快速失败。
31
+ 3. API、数据、认证或完整交付使用 README 中的 `verify`,它创建隔离数据库执行原生/API/浏览器检查。UI 还需运行设计契约检查。
32
+ 4. 查看差异并报告实际结果。类型或构建通过不等于业务通过;历史报告为 STALE 时按当前内容复验。
@@ -27,7 +27,7 @@ export interface paths {
27
27
  };
28
28
  content: {
29
29
  "application/json": {
30
- status?: string;
30
+ status: string;
31
31
  };
32
32
  };
33
33
  };
@@ -73,13 +73,13 @@ export interface paths {
73
73
  };
74
74
  content: {
75
75
  "application/json": {
76
- user?: {
76
+ user: {
77
77
  id: string;
78
78
  email: string;
79
79
  };
80
80
  token?: string;
81
81
  /** @enum {unknown} */
82
- mode?: "jwt" | "session";
82
+ mode: "jwt" | "session";
83
83
  };
84
84
  };
85
85
  };
@@ -135,7 +135,7 @@ export interface paths {
135
135
  };
136
136
  content: {
137
137
  "application/json": {
138
- user?: {
138
+ user: {
139
139
  id: string;
140
140
  email: string;
141
141
  };
@@ -205,6 +205,13 @@ export interface paths {
205
205
  };
206
206
  content?: never;
207
207
  };
208
+ /** @description Cross-origin session write rejected */
209
+ 403: {
210
+ headers: {
211
+ [name: string]: unknown;
212
+ };
213
+ content?: never;
214
+ };
208
215
  /** @description Record absent or owned by another user */
209
216
  404: {
210
217
  headers: {
@@ -316,6 +323,13 @@ export interface paths {
316
323
  };
317
324
  content?: never;
318
325
  };
326
+ /** @description Cross-origin session write rejected */
327
+ 403: {
328
+ headers: {
329
+ [name: string]: unknown;
330
+ };
331
+ content?: never;
332
+ };
319
333
  /** @description Record absent or owned by another user */
320
334
  404: {
321
335
  headers: {
@@ -424,6 +438,13 @@ export interface paths {
424
438
  };
425
439
  content?: never;
426
440
  };
441
+ /** @description Cross-origin session write rejected */
442
+ 403: {
443
+ headers: {
444
+ [name: string]: unknown;
445
+ };
446
+ content?: never;
447
+ };
427
448
  /** @description Record absent or owned by another user */
428
449
  404: {
429
450
  headers: {
@@ -466,6 +487,13 @@ export interface paths {
466
487
  };
467
488
  content?: never;
468
489
  };
490
+ /** @description Cross-origin session write rejected */
491
+ 403: {
492
+ headers: {
493
+ [name: string]: unknown;
494
+ };
495
+ content?: never;
496
+ };
469
497
  /** @description Record absent or owned by another user */
470
498
  404: {
471
499
  headers: {
@@ -19,7 +19,10 @@
19
19
  "status": {
20
20
  "type": "string"
21
21
  }
22
- }
22
+ },
23
+ "required": [
24
+ "status"
25
+ ]
23
26
  }
24
27
  }
25
28
  }
@@ -85,7 +88,11 @@
85
88
  "session"
86
89
  ]
87
90
  }
88
- }
91
+ },
92
+ "required": [
93
+ "user",
94
+ "mode"
95
+ ]
89
96
  }
90
97
  }
91
98
  }
@@ -127,7 +134,10 @@
127
134
  }
128
135
  }
129
136
  }
130
- }
137
+ },
138
+ "required": [
139
+ "user"
140
+ ]
131
141
  }
132
142
  }
133
143
  }
@@ -158,6 +168,9 @@
158
168
  },
159
169
  "422": {
160
170
  "description": "Invalid input or protected field"
171
+ },
172
+ "403": {
173
+ "description": "Cross-origin session write rejected"
161
174
  }
162
175
  }
163
176
  }
@@ -277,6 +290,9 @@
277
290
  },
278
291
  "422": {
279
292
  "description": "Invalid input or protected field"
293
+ },
294
+ "403": {
295
+ "description": "Cross-origin session write rejected"
280
296
  }
281
297
  }
282
298
  }
@@ -362,6 +378,9 @@
362
378
  },
363
379
  "422": {
364
380
  "description": "Invalid input or protected field"
381
+ },
382
+ "403": {
383
+ "description": "Cross-origin session write rejected"
365
384
  }
366
385
  }
367
386
  },
@@ -378,6 +397,9 @@
378
397
  },
379
398
  "422": {
380
399
  "description": "Invalid input or protected field"
400
+ },
401
+ "403": {
402
+ "description": "Cross-origin session write rejected"
381
403
  }
382
404
  }
383
405
  }
@@ -9,6 +9,8 @@
9
9
  "version": "1.0.0",
10
10
  "devDependencies": {
11
11
  "@playwright/test": "1.64.1",
12
+ "ajv": "8.20.0",
13
+ "ajv-formats": "3.0.1",
12
14
  "openapi-typescript": "7.13.0"
13
15
  }
14
16
  },
@@ -109,6 +111,41 @@
109
111
  "node": ">= 14"
110
112
  }
111
113
  },
114
+ "node_modules/ajv": {
115
+ "version": "8.20.0",
116
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
117
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
118
+ "dev": true,
119
+ "license": "MIT",
120
+ "dependencies": {
121
+ "fast-deep-equal": "^3.1.3",
122
+ "fast-uri": "^3.0.1",
123
+ "json-schema-traverse": "^1.0.0",
124
+ "require-from-string": "^2.0.2"
125
+ },
126
+ "funding": {
127
+ "type": "github",
128
+ "url": "https://github.com/sponsors/epoberezkin"
129
+ }
130
+ },
131
+ "node_modules/ajv-formats": {
132
+ "version": "3.0.1",
133
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
134
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
135
+ "dev": true,
136
+ "license": "MIT",
137
+ "dependencies": {
138
+ "ajv": "^8.0.0"
139
+ },
140
+ "peerDependencies": {
141
+ "ajv": "^8.0.0"
142
+ },
143
+ "peerDependenciesMeta": {
144
+ "ajv": {
145
+ "optional": true
146
+ }
147
+ }
148
+ },
112
149
  "node_modules/ansi-colors": {
113
150
  "version": "4.1.3",
114
151
  "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -182,6 +219,23 @@
182
219
  "dev": true,
183
220
  "license": "MIT"
184
221
  },
222
+ "node_modules/fast-uri": {
223
+ "version": "3.1.9",
224
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
225
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
226
+ "dev": true,
227
+ "funding": [
228
+ {
229
+ "type": "github",
230
+ "url": "https://github.com/sponsors/fastify"
231
+ },
232
+ {
233
+ "type": "opencollective",
234
+ "url": "https://opencollective.com/fastify"
235
+ }
236
+ ],
237
+ "license": "BSD-3-Clause"
238
+ },
185
239
  "node_modules/https-proxy-agent": {
186
240
  "version": "7.0.6",
187
241
  "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -4,6 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "api:check": "node scripts/project.mjs api:check",
8
+ "api:generate": "node scripts/project.mjs api:generate",
9
+ "check": "node scripts/project.mjs check",
7
10
  "setup": "node scripts/project.mjs setup",
8
11
  "design:verify": "node scripts/design-verify.mjs",
9
12
  "design:sync": "node scripts/design.mjs sync",
@@ -15,6 +18,8 @@
15
18
  "test:e2e": "node scripts/project.mjs test:e2e"
16
19
  },
17
20
  "devDependencies": {
21
+ "ajv": "8.20.0",
22
+ "ajv-formats": "3.0.1",
18
23
  "@playwright/test": "1.64.1",
19
24
  "openapi-typescript": "7.13.0"
20
25
  }
@@ -1,3 +1,3 @@
1
1
  import { defineConfig } from '@playwright/test';
2
2
  const port=process.env.WEB_PORT||'5173';
3
- export default defineConfig({testDir:'./tests',outputDir:process.env.SCAFFOLD_REPORT_DIR?process.env.SCAFFOLD_REPORT_DIR+'/playwright':'test-results',workers:1,retries:0,timeout:30000,reporter:[['line'],['junit',{outputFile:process.env.PLAYWRIGHT_JUNIT_OUTPUT_FILE||'reports/playwright.xml'}]],use:{baseURL:`http://127.0.0.1:${port}`,headless:true,trace:'off',screenshot:'off'},webServer:{command:'node scripts/project.mjs serve',url:`http://127.0.0.1:${port}/api/health`,reuseExistingServer:false,timeout:120000,stdout:'ignore',stderr:'pipe'},expect:{timeout:10000}});
3
+ export default defineConfig({testDir:'./tests',outputDir:process.env.SCAFFOLD_REPORT_DIR?process.env.SCAFFOLD_REPORT_DIR+'/playwright':'test-results',workers:1,retries:0,timeout:30000,reporter:[['line'],['junit',{outputFile:process.env.PLAYWRIGHT_JUNIT_OUTPUT_FILE||'reports/playwright.xml'}]],use:{baseURL:`http://127.0.0.1:${port}`,headless:true,trace:'off',screenshot:'off'},webServer:{command:'node scripts/project.mjs serve',url:`http://127.0.0.1:${port}/api/health`,reuseExistingServer:false,timeout:120000,stdout:'pipe',stderr:'pipe'},expect:{timeout:10000}});
@@ -0,0 +1,18 @@
1
+ import {mkdtemp,readFile,rm} from 'node:fs/promises';
2
+ import {tmpdir} from 'node:os';
3
+ import {join,resolve} from 'node:path';
4
+ import {spawnSync} from 'node:child_process';
5
+
6
+ // Generate outside the workspace: validation never repairs or overwrites source.
7
+ const directory=await mkdtemp(join(tmpdir(),'scaffold-api-check-'));
8
+ try{
9
+ const target=join(directory,'api.d.ts');
10
+ const result=spawnSync(process.execPath,['node_modules/openapi-typescript/bin/cli.js','docs/openapi.json','-o',target],{cwd:resolve('.'),encoding:'utf8'});
11
+ if(result.status!==0)throw new Error('API_CONTRACT_INVALID: OpenAPI generation failed. '+result.stderr);
12
+ const expected=await readFile(target,'utf8'),actual=await readFile('docs/api.d.ts','utf8');
13
+ if(expected!==actual){
14
+ const a=actual.split('\n'),e=expected.split('\n');let line=0;while(line<Math.max(a.length,e.length)&&a[line]===e[line])line++;
15
+ throw new Error(`API_CONTRACT_DRIFT: docs/api.d.ts differs from docs/openapi.json at line ${line+1}. Run npm run api:generate after reviewing the intended contract.`);
16
+ }
17
+ console.log('API contract and generated types match.');
18
+ }finally{await rm(directory,{recursive:true,force:true});}
@@ -0,0 +1,46 @@
1
+ import Ajv from 'ajv';
2
+ import addFormats from 'ajv-formats';
3
+
4
+ export function responseValidator(document){
5
+ const ajv=new Ajv({strict:false,allErrors:true});addFormats(ajv);
6
+ ajv.addSchema(document,'contract');
7
+ const operations=[];
8
+ function dereference(value,pointer){
9
+ const seen=new Set();
10
+ while(value?.$ref){
11
+ const ref=value.$ref;
12
+ if(!ref.startsWith('#/')||seen.has(ref))throw new Error('API_CONTRACT_INVALID: unsupported or circular response reference');
13
+ seen.add(ref);pointer=ref.slice(1);value=pointer.slice(1).split('/').map(s=>s.replaceAll('~1','/').replaceAll('~0','~')).reduce((at,key)=>at?.[key],document);
14
+ if(!value)throw new Error('API_CONTRACT_INVALID: unresolved response reference');
15
+ }
16
+ return {value,pointer};
17
+ }
18
+ const pointer=value=>value.replaceAll('~','~0').replaceAll('/','~1');
19
+ for(const [path,item] of Object.entries(document.paths))for(const [method,operation] of Object.entries(item)){
20
+ if(!['get','post','put','delete','patch','head','options'].includes(method))continue;
21
+ const statuses=new Map();
22
+ for(const [status,response] of Object.entries(operation.responses)){
23
+ const resolved=dereference(response,`/paths/${pointer(path)}/${method}/responses/${pointer(status)}`);
24
+ const content=resolved.value.content?.['application/json'];
25
+ if(resolved.value.content&&!content)throw new Error('API_CONTRACT_INVALID: only JSON response schemas are supported');
26
+ const validate=content?ajv.compile({$ref:`contract#${resolved.pointer}/content/application~1json/schema`}):null;
27
+ statuses.set(status,validate);
28
+ }
29
+ operations.push({path,parts:path.split('/'),method,statuses});
30
+ }
31
+ return (method,url,status,headers,body)=>{
32
+ const parts=new URL(url,'http://contract.invalid').pathname.split('/');
33
+ const operation=operations.find(o=>o.method===method.toLowerCase()&&o.parts.length===parts.length&&o.parts.every((part,i)=>part===parts[i]||/^\{[^}]+\}$/.test(part)&&parts[i]!==''));
34
+ const label=`API_RESPONSE_INVALID: ${method.toUpperCase()} ${operation?.path??'unlisted operation'} status ${status}`;
35
+ if(!operation)throw new Error(label+' is absent from docs/openapi.json');
36
+ const key=operation.statuses.has(String(status))?String(status):operation.statuses.has(`${Math.floor(status/100)}XX`)?`${Math.floor(status/100)}XX`:'default';
37
+ if(!operation.statuses.has(key))throw new Error(label+' is not declared');
38
+ const validate=operation.statuses.get(key);
39
+ if(status===204&&body.length)throw new Error(label+' must have an empty body');
40
+ if(validate){
41
+ if(!/^application\/json(?:;|$)/i.test(headers['content-type']??''))throw new Error(label+' requires application/json');
42
+ let data;try{data=JSON.parse(body);}catch{throw new Error(label+' is not valid JSON');}
43
+ if(!validate(data))throw new Error(label+' '+validate.errors.map(e=>`${e.instancePath||'/'} ${e.keyword}${e.params.missingProperty?' '+e.params.missingProperty:''}`).join('; '));
44
+ }
45
+ };
46
+ }