@givolt/i3-composer-agent 2.3.1 → 2.3.3

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 (173) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +5 -7
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +5 -7
  7. package/README.md +16 -7
  8. package/bin/agent-targets.json +4 -2
  9. package/bin/i3-composer-agent.cjs +74 -7
  10. package/bin/install.cjs +16 -1
  11. package/bin/native-layout.cjs +1 -1
  12. package/bin/native-migration.cjs +27 -3
  13. package/bin/tool-selector.cjs +24 -7
  14. package/bin/wizard.cjs +89 -15
  15. package/commands/i3-prototype.toml +5 -7
  16. package/docs/codebuddy-setup.md +91 -3
  17. package/docs/codex-setup.md +6 -0
  18. package/docs/other-hosts.md +1 -1
  19. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  20. package/docs/ui-prototype-native/USAGE.md +93 -0
  21. package/docs/workbuddy-setup.md +6 -0
  22. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  23. package/evals/cases/i3-ui-prototype.json +109 -0
  24. package/evals/cases/i3-using-agent-skills.json +28 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  27. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  28. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  29. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  30. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  31. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  32. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  33. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  34. package/hooks/codebuddy-upstream.cjs +93 -0
  35. package/hooks/i3-prototype.cjs +12 -0
  36. package/hooks/manage.cjs +162 -0
  37. package/package.json +61 -1
  38. package/plugin.json +1 -1
  39. package/scripts/design-provider-test.js +7 -0
  40. package/scripts/enterprise-template-test.js +27 -1
  41. package/scripts/lib/prototype-fixture.cjs +17 -0
  42. package/scripts/prototype-cache-test.js +55 -0
  43. package/scripts/prototype-generation-entry-test.js +6 -0
  44. package/scripts/prototype-generation-evidence-test.js +10 -0
  45. package/scripts/prototype-generation-native-test.js +78 -0
  46. package/scripts/prototype-generation-test.js +99 -0
  47. package/scripts/prototype-inventory-test.js +43 -0
  48. package/scripts/prototype-locations-test.js +79 -0
  49. package/scripts/prototype-project-test.js +183 -0
  50. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  51. package/scripts/ui-prototype-content-test.js +123 -0
  52. package/scripts/ui-prototype-dependencies-test.js +213 -0
  53. package/scripts/ui-prototype-devtools-test.js +53 -0
  54. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  55. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  56. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  57. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  58. package/scripts/ui-prototype-ide-source-test.js +73 -0
  59. package/scripts/ui-prototype-native-test.js +504 -0
  60. package/scripts/ui-prototype-registration-test.js +53 -0
  61. package/scripts/ui-prototype-template-test.js +261 -0
  62. package/scripts/ui-prototype-test.js +10 -0
  63. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  64. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  65. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  66. package/skills/i3-ui-prototype/SKILL.md +158 -19
  67. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  68. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  77. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  78. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  79. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  80. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  81. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  82. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  83. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  84. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  85. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  86. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  87. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  88. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  89. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  90. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  91. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  92. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  93. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  94. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  95. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
  96. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  97. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
  98. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  99. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  100. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  101. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
  102. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  103. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  104. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  105. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  106. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  107. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
  108. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  109. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  110. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  111. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  112. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  113. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  114. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  115. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  116. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  117. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  118. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  119. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
  120. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  121. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  122. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  123. package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
  124. package/skills/i3-ui-prototype/references/content-template.md +81 -0
  125. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  126. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  127. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  128. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  129. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  130. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  131. package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
  132. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  133. package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
  134. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  135. package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
  136. package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
  137. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  138. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  139. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  140. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  141. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  142. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  143. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  144. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  145. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  146. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  147. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  148. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  149. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  150. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  151. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  152. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  153. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  154. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  155. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  156. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  157. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  158. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
  159. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
  160. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  161. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
  162. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
  163. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  164. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  165. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  166. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  167. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
  168. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
  169. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  170. package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
  171. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
  172. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  173. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+ const {spawnSync}=require('node:child_process');
3
+ const http=require('node:http');
4
+ const F=require('./native-files.cjs');
5
+ const {fs,path,crypto,assert,hash,json,id,inside,walk,write,entries}=F;
6
+ function verifyBuild(directory,buildId,{manifest}) {
7
+ id(buildId); const root=inside(directory,`builds/${buildId}`); const data=json(path.join(root,'manifest.json'));
8
+ assert.equal(data.buildId,buildId,'Build ID mismatch');
9
+ for(const step of ['typecheck','test','build'])assert.ok(Object.hasOwn(data.checks||{},step)&&typeof data.checks[step]==='string',`Required build check missing: ${step}`);
10
+ assert.deepEqual(entries(path.join(root,'site'),walk(path.join(root,'site'))),data.files,'Build bytes changed');
11
+ assert.equal(hash(JSON.stringify(data.files)),data.buildDigest,'Build digest mismatch');
12
+ assert.equal(data.candidateDigest,manifest(directory).digest,'Build is for an old candidate / 旧构建');
13
+ const scripts=Object.keys(data.files).filter(file=>/\.[cm]?js$/.test(file));
14
+ assert.ok(data.files['index.html'] && data.files['prototype-config.json'] && scripts.length,'Native build entry, config and JavaScript required');
15
+ assert.ok(scripts.some(file=>fs.readFileSync(inside(path.join(root,'site'),file),'utf8').includes(data.candidateDigest)),'Built JavaScript must embed its candidate identity / 构建标识');
16
+ return {...data,directory:root};
17
+ }
18
+
19
+ function verifyBrowserReport(directory,report,build) {
20
+ const start=Date.parse(report.startedAt),end=Date.parse(report.completedAt);
21
+ assert.ok(Number.isFinite(start)&&Number.isFinite(end)&&end>=start,'Browser run timestamps must be valid and ordered');
22
+ const base=new URL(report.previewBaseURL);
23
+ assert.ok(['http:','https:'].includes(base.protocol)&&!base.username&&!base.password&&!base.search&&!base.hash&&base.pathname.endsWith('/'),'Invalid preview base URL');
24
+ assert.ok(Array.isArray(report.observedResources)&&report.observedResources.length,'Observed browser resources required');
25
+ const observations=new Map();
26
+ for(const resource of report.observedResources) {
27
+ assert.ok(Object.hasOwn(build.files,resource.path),`Observed resource is outside the fixed build: ${resource.path}`);
28
+ const actual=new URL(resource.url),expected=new URL(resource.path==='index.html'?'':resource.path,base);
29
+ const entryAlias=resource.path==='index.html'&&actual.pathname===new URL('index.html',base).pathname;
30
+ assert.ok(actual.origin===base.origin&&(actual.pathname===expected.pathname||entryAlias)&&!actual.username&&!actual.password,`Observed resource has a different preview URL: ${resource.path}`);
31
+ assert.equal(resource.status,200,`Observed resource did not return 200: ${resource.path}`);
32
+ assert.equal(resource.sha256,build.files[resource.path],`Observed resource bytes differ: ${resource.path}`);
33
+ assert.ok(typeof resource.resourceType==='string'&&resource.resourceType,'Observed resource type required');
34
+ const type=resource.path==='index.html'?'document':/\.[cm]?js$/.test(resource.path)?'script':resource.path.endsWith('.css')?'stylesheet':resource.path==='prototype-config.json'?'fetch':null;
35
+ if(type)assert.equal(resource.resourceType,type,`Observed resource request type differs: ${resource.path}`);
36
+ observations.set(resource.path,resource);
37
+ }
38
+ const required=Object.keys(build.files).filter(file=>['index.html','prototype-config.json'].includes(file)||/\.(?:[cm]?js|css)$/.test(file));
39
+ for(const file of required)assert.ok(observations.has(file),`Browser did not observe required build resource: ${file}`);
40
+ assert.ok(Array.isArray(report.screenshots)&&report.screenshots.length>=2&&Array.isArray(report.viewports)&&report.viewports.length>=2,'Responsive screenshots required');
41
+ const png=Buffer.from([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]);
42
+ for(const file of report.screenshots) {
43
+ const bytes=fs.readFileSync(inside(directory,file));
44
+ assert.ok(bytes.length>24&&bytes.subarray(0,8).equals(png),`Screenshot must be PNG: ${file}`);
45
+ }
46
+ const trace=fs.readFileSync(inside(directory,report.trace));
47
+ assert.ok(trace.length>=22&&trace[0]===0x50&&trace[1]===0x4b&&[[3,4],[5,6],[7,8]].some(pair=>trace[2]===pair[0]&&trace[3]===pair[1]),'Browser trace must be a ZIP artifact');
48
+ }
49
+ function build(directory,options,{manifest,inspect}) {
50
+ const checked=inspect(directory); assert.equal(checked.status,'passed',checked.errors?.join('; '));
51
+ const before=manifest(directory); const app=inside(directory,'app');
52
+ function run(args,env={}) {
53
+ const result=spawnSync(process.platform==='win32'?'npm.cmd':'npm',args,{cwd:app,env:{...process.env,...env},encoding:'utf8',timeout:180000,maxBuffer:5*1024*1024});
54
+ assert.equal(result.status,0,`npm ${args.join(' ')} failed: ${result.error?.message||result.stderr||result.stdout}`);
55
+ return result.stdout;
56
+ }
57
+ if(options.install) run(['ci','--ignore-scripts','--no-audit','--no-fund']);
58
+ assert.ok(fs.existsSync(path.join(app,'node_modules','vite')),'Local dependencies missing; explicitly build --install');
59
+ const typecheck=run(['run','typecheck']);
60
+ const test=run(['test']);
61
+ fs.mkdirSync(inside(directory,'builds'),{recursive:true}); const stage=fs.mkdtempSync(path.join(directory,'builds','.build-'));
62
+ try {
63
+ const output=run(['run','build','--','--outDir',path.join(stage,'site'),'--emptyOutDir'],{VITE_CANDIDATE_ID:before.digest});
64
+ assert.equal(manifest(directory).digest,before.digest,'Source changed during build');
65
+ const files=entries(path.join(stage,'site'),walk(path.join(stage,'site'))); const buildDigest=hash(JSON.stringify(files)); const buildId=`b-${buildDigest.slice(0,20)}`;
66
+ const data={schemaVersion:1,buildId,buildDigest,candidateDigest:before.digest,inputDigest:before.inputDigest,revision:before.revision,files,toolchain:{node:process.version},checks:{typecheck,test,build:output}};
67
+ write(stage,'manifest.json',data);
68
+ const destination=inside(directory,`builds/${buildId}`);
69
+ if(fs.existsSync(destination)) verifyBuild(directory,buildId,{manifest}); else fs.renameSync(stage,destination);
70
+ write(directory,'last-build.json',{buildId,buildDigest,candidateDigest:before.digest});
71
+ return {status:'built',...data,directory:destination};
72
+ } finally { if(fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true}); }
73
+ }
74
+ async function preview(directory,options,{manifest}) {
75
+ const buildId=options.buildId||json(inside(directory,'last-build.json')).buildId;
76
+ const checked=verifyBuild(directory,buildId,{manifest}); const site=path.join(checked.directory,'site');
77
+ const mime={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.woff2':'font/woff2'};
78
+ const server=http.createServer((req,res)=>{
79
+ try {
80
+ const pathname=decodeURIComponent(new URL(req.url,'http://127.0.0.1').pathname);
81
+ res.setHeader('Cache-Control','no-store');
82
+ if(pathname==='/__prototype-build.json') { const current=verifyBuild(directory,buildId,{manifest}); res.setHeader('Content-Type','application/json'); res.end(JSON.stringify({buildId,currentCandidate:current.candidateDigest,buildDigest:current.buildDigest,files:current.files})); return; }
83
+ const relative=pathname==='/'?'index.html':pathname.replace(/^\//,''); const file=inside(site,relative);
84
+ if(!fs.existsSync(file)||!fs.statSync(file).isFile()) { res.writeHead(404); res.end('Not found'); return; }
85
+ res.setHeader('Content-Type',mime[path.extname(file)]||'application/octet-stream'); res.end(fs.readFileSync(file));
86
+ } catch(error) { res.writeHead(409); res.end(error.message); }
87
+ });
88
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(options.port||0,'127.0.0.1',resolve);});
89
+ return {server,url:`http://127.0.0.1:${server.address().port}/`,build:checked};
90
+ }
91
+ module.exports={build,verifyBuild,preview,verifyBrowserReport};
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+ const F=require('./native-files.cjs');
3
+ const {fs,path,crypto,assert,json,inside,write}=F;
4
+ const read=(root,file)=>fs.existsSync(inside(root,file))?json(inside(root,file)):null;
5
+ const quote=value=>`'${String(value).replace(/'/g,"'\\''")}'`;
6
+
7
+ function state(directory,options,lib) {
8
+ directory=path.resolve(directory);
9
+ const current=lib.manifest(directory),meta=json(inside(directory,'prototype.json'));
10
+ const review=lib.effectiveReview(meta,current);
11
+ const structure=lib.inspect(directory),advice=read(directory,'qa/design-advice.json'),decisions=read(directory,'design-system/decisions.json');
12
+ let design;
13
+ try { const reviewed=lib.inspectDesignReview(directory,current); design={status:reviewed.status,queries:reviewed.queries,records:reviewed.records,adopted:reviewed.adoptedAdvice.length}; }
14
+ catch(error) { design={status:advice?.status==='pending'||decisions?.reviewStatus==='pending'?'pending':'failed',queries:advice?.queries?.length||0,records:advice?.records?.length||0,errors:[error.message]}; }
15
+ let build={status:'not-run'};
16
+ const pointer=read(directory,'last-build.json');
17
+ if(pointer) {
18
+ try { const checked=lib.verifyBuild(directory,pointer.buildId); build={status:'current',buildId:checked.buildId,buildDigest:checked.buildDigest,checks:Object.keys(checked.checks)}; }
19
+ catch(error) { build={status:pointer.candidateDigest!==current.digest?'stale':'failed',buildId:pointer.buildId,errors:[error.message]}; }
20
+ }
21
+ let browser={status:'not-run'};
22
+ const latest=read(directory,'qa/latest.json');
23
+ if(latest) {
24
+ try {
25
+ const report=json(inside(directory,latest.report)),checked=lib.inspect(directory,{acceptance:true});
26
+ browser={status:report.candidateDigest!==current.digest||report.revision!==current.revision?'stale':checked.status==='passed'?'current':'failed',report:latest.report,reportDigest:F.hash(fs.readFileSync(inside(directory,latest.report))),runId:report.id,executor:report.executor,...(checked.errors?{errors:checked.errors}:{})};
27
+ } catch(error) { browser={status:'failed',errors:[error.message]}; }
28
+ }
29
+ const devtoolsReview=require('./native-devtools.cjs').check(directory,lib);
30
+ const execution=require('./execution-runtime.cjs').check(directory,'delivery',lib);
31
+ const ownership=read(directory,'.prototype-write.lock');
32
+ const savedDelivery=read(directory,'qa/delivery.json');
33
+ const deliveryCurrent=savedDelivery?.schemaVersion===1&&savedDelivery.status==='complete'&&savedDelivery.id===meta.id&&savedDelivery.revision===current.revision&&savedDelivery.candidateDigest===current.digest&&savedDelivery.inputDigest===current.inputDigest&&savedDelivery.buildId===build.buildId&&savedDelivery.buildDigest===build.buildDigest&&savedDelivery.report===browser.report&&savedDelivery.review===review.status&&savedDelivery.humanConfirmationRequired===(review.status!=='approved')&&browser.status==='current'&&((!devtoolsReview.required&&savedDelivery.browserReportDigest===undefined)||savedDelivery.browserReportDigest===browser.reportDigest)&&(!devtoolsReview.required||(devtoolsReview.status==='current'&&savedDelivery.devtoolsReviewDigest===devtoolsReview.reportDigest));
34
+ const executionCurrent=execution.status==='not-required'||execution.status==='passed'&&savedDelivery?.runId===execution.runId&&savedDelivery?.executionEvidenceDigest===F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));
35
+ const delivery=savedDelivery?{status:deliveryCurrent&&executionCurrent?'current':'stale',path:'qa/delivery.json',revision:savedDelivery.revision,...(deliveryCurrent&&executionCurrent?{result:savedDelivery}:{})}:null;
36
+ let runtime;
37
+ try { const provider=require('./native-browser.cjs').resolvePlaywright(directory,options); runtime={availability:'available',...provider,launchVerified:false}; }
38
+ catch(error) { runtime={availability:'missing',reason:error.message}; }
39
+ const cli=path.join(__dirname,'native-pilot.cjs');
40
+ const command=(operation,args=[])=>{const argv=[process.execPath,cli,operation,directory,...args];return {argv,shell:argv.map(quote).join(' ')};};
41
+ let nextRevision=/^r(\d+)$/.test(current.revision)?`r${String(Number(current.revision.slice(1))+1).padStart(3,'0')}`:`${current.revision}-next`;
42
+ while(fs.existsSync(inside(directory,`baselines/${nextRevision}`))) nextRevision+='-next';
43
+ const expected=['--expected-revision',current.revision,'--expected-digest',current.digest];
44
+ const browserArgs=[];
45
+ const browserProvider=options.playwrightModule||process.env.PLAYWRIGHT_MODULE||(runtime.availability==='available'?runtime.packageRoot:null);
46
+ if(browserProvider) browserArgs.push('--playwright-module',browserProvider);
47
+ if(options.browserChannel) browserArgs.push('--browser-channel',options.browserChannel);
48
+ if(options.expectedCategories) browserArgs.push('--expected-categories',typeof options.expectedCategories==='string'?options.expectedCategories:JSON.stringify(options.expectedCategories));
49
+ if(options.expectedDescription) browserArgs.push('--expected-description',options.expectedDescription);
50
+ if(options.verifyBlurValidation===true) browserArgs.push('--verify-blur-validation');
51
+ if(options.verifyAtomicStatus===true) browserArgs.push('--verify-atomic-status');
52
+ const commands={state:command('state'),revise:command('revise',['--revision',nextRevision,...expected]),build:command('build',fs.existsSync(inside(directory,'app/node_modules/vite'))?[]:['--install']),browser:command('browser',browserArgs),finalize:command('finalize',expected)};
53
+ if(meta.runtime.contentReference) {
54
+ delete commands.browser;
55
+ commands.recordRun=command('record-run',['--report',path.join(directory,'qa/business-browser-input.json')]);
56
+ }
57
+ if(build.status==='current')commands.preview=command('preview',['--build-id',build.buildId]);
58
+ if(devtoolsReview.required)commands.recordDevtoolsReview=command('record-devtools-review',['--report',path.join(directory,'qa/devtools-review-input.json')]);
59
+ if(meta.runtime.executionPolicy){commands.executionBegin=command('execution-begin');commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
60
+ let generation=null;
61
+ if(meta.runtime.generationContract===1)try{const fixed=json(inside(directory,'inputs/generation.json'));const plan=require('./generation-contract.cjs').view(fixed);generation={presentation:fixed.presentation,pendingDecisions:fixed.pendingDecisions,pendingReferenceElements:fixed.pendingReferenceElements.length,referenceReview:plan.referenceReview,implementation:require('./generation-evidence.cjs').validateImplementation(directory,fixed,json(inside(directory,'design-system/implementation.json')),{frameworkMap:json(inside(directory,'inputs/framework.json')).componentMap})};}catch(error){generation={status:'failed',errors:[error.message]};}
62
+ const nextAction=ownership?'wait-for-owner':structure.status!=='passed'?'repair-structure':generation&&(generation.pendingDecisions.length||generation.pendingReferenceElements)?'resolve-generation-decisions':generation?.implementation.status==='pending'?'implement-business':meta.runtime.contentReference&&meta.business?.status!=='adapted'?'implement-business':!['reviewed','no-match','skipped'].includes(design.status)?'review-design':build.status!=='current'?'build':browser.status!=='current'?(meta.runtime.contentReference?'business-browser-review':'browser'):devtoolsReview.required&&devtoolsReview.status!=='current'?'devtools-review':deliveryCurrent?'return-result':'finalize';
63
+ if(generation)commands.replan=command('replan',['--scope',path.join(directory,'inputs/scope.json'),'--revision',nextRevision,...expected]);
64
+ const fixedScope=json(inside(directory,'inputs/scope.json'));
65
+ const scopes=meta.runtime.generationContract===1?{sourceFile:'inputs/scope.json',schemaVersion:fixedScope.schemaVersion,title:fixedScope.title,moduleIds:fixedScope.moduleIds,pages:fixedScope.pages.map(page=>({id:page.id,title:page.title,referencePage:page.referencePage,regions:page.regions.map(region=>({id:region.id,pattern:region.pattern||region.mode,fields:region.fields?.length||0,actions:region.actions?.length||0}))})),requirements:fixedScope.requirements.map(item=>({id:item.id,pages:item.pages,source:{type:item.source.type,reference:item.source.reference}}))}:fixedScope;
66
+ return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction:meta.runtime.executionPolicy&&execution.status==='failed'?'resolve-execution-evidence':nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed commands; no source-code inspection is required to recover flags or digests.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy invocation fields retain their original meaning. New policy checks are reported separately in execution; native-source correlation is not tamper-proof attestation.']};
67
+ }
68
+
69
+ function finalize(directory,options,lib) {
70
+ directory=path.resolve(directory);
71
+ const before=lib.manifest(directory),checked=lib.inspect(directory,{acceptance:true});
72
+ assert.equal(checked.status,'passed',`Current acceptance required / 当前实际验收未通过: ${checked.errors?.join('; ')}`);
73
+ const devtools=require('./native-devtools.cjs');
74
+ const requiredReview=devtools.check(directory,lib);assert.ok(!requiredReview.required||requiredReview.status==='current',`Current DevTools review required: ${requiredReview.errors?.join('; ')||requiredReview.status}`);
75
+ require('./execution-runtime.cjs').requireStage(directory,'delivery',lib);
76
+ const owner=`finalize-${crypto.randomUUID()}`;
77
+ lib.claim(directory,{owner,expectedRevision:options.expectedRevision||before.revision,expectedDigest:options.expectedDigest||before.digest});
78
+ try {
79
+ const again=lib.inspect(directory,{acceptance:true});assert.equal(again.status,'passed',again.errors?.join('; '));
80
+ const devtoolsReview=devtools.check(directory,lib);assert.ok(!devtoolsReview.required||devtoolsReview.status==='current','Current DevTools review required');
81
+ assert.equal(lib.manifest(directory).digest,before.digest,'Candidate changed before final handoff');
82
+ const meta=json(inside(directory,'prototype.json')),scope=json(inside(directory,'inputs/scope.json'));
83
+ const review=lib.effectiveReview(meta,before);
84
+ const latest=json(inside(directory,'qa/latest.json')),report=json(inside(directory,latest.report));
85
+ const delivery={schemaVersion:1,status:'complete',operation:'finalize',id:meta.id,revision:before.revision,candidateDigest:before.digest,inputDigest:before.inputDigest,framework:meta.framework,template:meta.template,expertMode:meta.expertMode,expert:meta.expert.status,verification:'passed',review:review.status,checks:{structure:'passed',acceptance:'passed',build:'current',browser:'current'},buildId:report.buildId,buildDigest:report.buildDigest,report:latest.report,executor:report.executor,tool:report.tool,handoff:path.join(directory,'HANDOFF.md'),receipt:'qa/delivery.json',openQuestions:scope.openQuestions||[],humanConfirmationRequired:review.status!=='approved'};
86
+ const execution=require('./execution-runtime.cjs').requireStage(directory,'delivery',lib);
87
+ delivery.invocationVerification=execution.invocationVerification;delivery.tamperProof=false;
88
+ if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));}
89
+ delivery.browserReportDigest=F.hash(fs.readFileSync(inside(directory,latest.report)));
90
+ if(devtoolsReview.required){delivery.devtoolsReview=devtoolsReview.report;delivery.devtoolsReviewDigest=devtoolsReview.reportDigest;delivery.checks.devtoolsReview='current';delivery.invocationEvidence=execution.status==='passed'?'Native response IDs correlated with the configured host transcript; not tamper-proof.':'Verify actual Skill/MCP calls independently in the host transcript';}
91
+ const start='<!-- native-delivery:start -->',end='<!-- native-delivery:end -->';
92
+ const block=[start,'## 当前真实交付状态',`- 原型:${delivery.id} / ${delivery.revision}`,`- candidateDigest:${delivery.candidateDigest}`,`- inputDigest:${delivery.inputDigest}`,`- 构建:${delivery.buildId} / ${delivery.buildDigest}`,`- 浏览器:${latest.report};实际执行者 ${report.executor};${report.tool}`,`- 专业模式:${delivery.expertMode} / ${delivery.expert}`,`- 调用核验:${delivery.invocationVerification};runId:${delivery.runId||'原合同'};非不可篡改认证。`,`- verification:passed;review:${delivery.review}。本工具不确认或冻结基线。`,`- 待确认问题:${delivery.openQuestions.length?JSON.stringify(delivery.openQuestions):'无新增;仍需用户评审。'}`,end].join('\n');
93
+ const file=inside(directory,'HANDOFF.md'),previous=fs.existsSync(file)?fs.readFileSync(file,'utf8'):'';
94
+ const begin=previous.indexOf(start),finish=previous.indexOf(end);
95
+ assert.ok((begin===-1&&finish===-1)||(begin>=0&&finish>begin&&previous.indexOf(start,begin+1)===-1&&previous.indexOf(end,finish+1)===-1),'Ambiguous generated handoff markers; preserve the user file');
96
+ const updated=begin===-1?`${previous.trimEnd()}\n\n${block}\n`:`${previous.slice(0,begin)}${block}${previous.slice(finish+end.length)}`;
97
+ assert.equal(lib.manifest(directory).digest,before.digest,'Candidate changed while finalizing');
98
+ if(updated!==previous) write(directory,'HANDOFF.md',updated);
99
+ write(directory,'qa/delivery.json',delivery);
100
+ return delivery;
101
+ } finally { lib.release(directory,owner); }
102
+ }
103
+ module.exports={state,finalize};
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+ // Checks evidence completeness and current identity, not the truth of a model's
3
+ // claims. Actual Skill/MCP invocation must be checked against the host transcript.
4
+ const F=require('./native-files.cjs');
5
+ const {fs,path,assert,hash,json,inside,write,crypto}=F;
6
+ const zlib=require('node:zlib');
7
+ const KINDS=['dom','screenshot','console','network','accessibility','performance'];
8
+ const METHODS=['evaluate_script','take_screenshot','list_console_messages','list_network_requests','take_snapshot','performance_start_trace','performance_stop_trace'];
9
+ const limitations='Evidence structure, file bytes and current identity checked; verify actual Skill/MCP calls and findings independently in the raw host transcript.';
10
+ function policy(){return {schemaVersion:1,required:true,skill:'i3-browser-testing-with-devtools',provider:'chrome-devtools-mcp',observations:[...KINDS]};}
11
+ function readPolicy(directory){
12
+ const configured=json(inside(directory,'inputs/acceptance.json')).browserReview;
13
+ const frameworkFile=inside(directory,'inputs/framework.json');
14
+ if(fs.existsSync(frameworkFile)){const fixed=json(frameworkFile).acceptance?.browserReview;if(fixed!==undefined)assert.deepEqual(configured,fixed,'DevTools browser review policy differs from its fixed framework input');}
15
+ if(configured===undefined)return null;
16
+ assert.equal(configured.schemaVersion,1,'Unknown DevTools review policy');assert.equal(configured.required,true,'DevTools review policy cannot silently opt out');
17
+ assert.deepEqual(configured.observations,KINDS,'Required DevTools observations changed');assert.equal(configured.skill,policy().skill);assert.equal(configured.provider,'chrome-devtools-mcp');return configured;
18
+ }
19
+ const text=(value,label)=>assert.ok(typeof value==='string'&&value.trim(),`${label} required`);
20
+ const crcTable=Array.from({length:256},(_,value)=>{for(let bit=0;bit<8;bit++)value=(value>>>1)^((value&1)?0xedb88320:0);return value>>>0;});
21
+ function crc32(bytes){let crc=0xffffffff;for(const byte of bytes)crc=(crc>>>8)^crcTable[(crc^byte)&255];return (crc^0xffffffff)>>>0;}
22
+ function verifyPng(bytes){
23
+ assert.ok(bytes.length>=57&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])),'Complete PNG screenshot required');
24
+ let offset=8,header=null,ended=false;const image=[];
25
+ while(offset<bytes.length){
26
+ assert.ok(offset+12<=bytes.length,'Truncated PNG chunk');const size=bytes.readUInt32BE(offset),end=offset+size+12;assert.ok(end<=bytes.length,'Truncated PNG data');
27
+ const kind=bytes.toString('ascii',offset+4,offset+8),data=bytes.subarray(offset+8,offset+8+size);assert.equal(crc32(bytes.subarray(offset+4,offset+8+size)),bytes.readUInt32BE(offset+8+size),'Invalid PNG chunk CRC');
28
+ if(!header){assert.equal(kind,'IHDR','PNG header must be first');assert.equal(size,13,'Invalid PNG IHDR');header=data;}
29
+ else assert.notEqual(kind,'IHDR','Duplicate PNG header');
30
+ if(kind==='IDAT')image.push(data);
31
+ if(kind==='IEND'){assert.equal(size,0,'Invalid PNG end');assert.equal(end,bytes.length,'Trailing PNG bytes');ended=true;break;}offset=end;
32
+ }
33
+ assert.ok(ended&&image.length,'PNG needs image data and IEND');
34
+ const width=header.readUInt32BE(0),height=header.readUInt32BE(4),depth=header[8],channels={0:1,2:3,3:1,4:2,6:4}[header[9]];
35
+ assert.ok(width&&height&&channels&&[1,2,4,8,16].includes(depth),'Invalid PNG dimensions or color format');assert.equal(header[10],0,'Unsupported PNG compression');assert.equal(header[11],0,'Unsupported PNG filter method');assert.equal(header[12],0,'Expected noninterlaced Chrome screenshot PNG');
36
+ const stride=Math.ceil(width*channels*depth/8)+1,expected=stride*height;assert.ok(Number.isSafeInteger(expected)&&expected<=64*1024*1024,'PNG decoded image is too large');
37
+ let pixels;try{pixels=zlib.inflateSync(Buffer.concat(image),{maxOutputLength:64*1024*1024});}catch{throw new Error('Invalid PNG compressed image data');}
38
+ assert.equal(pixels.length,expected,'Incomplete PNG pixels');for(let row=0;row<height;row++)assert.ok(pixels[row*stride]<=4,'Invalid PNG scanline filter');
39
+ }
40
+ function validate(directory,report,current,build,configured){
41
+ assert.equal(report.schemaVersion,1);assert.equal(report.status,'passed','DevTools review has not passed');
42
+ assert.equal(report.revision,current.revision,'DevTools review revision is stale');assert.equal(report.candidateDigest,current.digest,'DevTools review candidate is stale');
43
+ assert.equal(report.buildId,build.buildId,'DevTools review build is stale');assert.equal(report.buildDigest,build.buildDigest,'DevTools review build bytes differ');
44
+ text(report.executor,'DevTools executor');const start=Date.parse(report.startedAt),end=Date.parse(report.completedAt);assert.ok(Number.isFinite(start)&&Number.isFinite(end)&&end>=start,'DevTools review timestamps required');
45
+ const url=new URL(report.previewBaseURL);assert.ok(url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname)&&!url.username&&!url.password,'DevTools preview must be the known local build');
46
+ assert.equal(report.skill?.name,configured.skill,'Required browser Skill missing');text(report.skill.toolCallId,'Skill tool call ID');text(report.skill.evidence,'Skill host transcript locator');
47
+ assert.equal(report.mcp?.name,'chrome-devtools-mcp');text(report.mcp.version,'MCP version');text(report.mcp.evidence,'MCP host transcript locator');
48
+ assert.ok(Array.isArray(report.mcp.calls)&&report.mcp.calls.length,'Actual MCP call locators required');
49
+ for(const call of report.mcp.calls){assert.ok(typeof call.tool==='string'&&call.tool.startsWith('mcp__'),'Native MCP tool name required');text(call.toolCallId,'MCP tool call ID');}
50
+ assert.equal(new Set(report.mcp.calls.map(call=>call.toolCallId)).size,report.mcp.calls.length,'Duplicate MCP call IDs');
51
+ for(const method of METHODS)assert.ok(report.mcp.calls.some(call=>call.tool.endsWith(`__${method}`)),`Missing DevTools method: ${method}`);
52
+ assert.ok(Array.isArray(report.openItems),'DevTools openItems required');assert.ok(!report.openItems.some(item=>item?.blocking===true),'Blocking DevTools findings remain');
53
+ const evidenceDigests={},seen=new Set();
54
+ for(const kind of KINDS){
55
+ const observed=report.observations?.[kind];assert.equal(observed?.status,'passed',`Missing passed DevTools observation: ${kind}`);text(observed.summary,`${kind} finding`);
56
+ assert.ok(typeof observed.evidence==='string'&&observed.evidence.startsWith('qa/')&&!seen.has(observed.evidence),'Each DevTools observation needs a distinct QA evidence path');seen.add(observed.evidence);
57
+ const file=inside(directory,observed.evidence);assert.ok(fs.statSync(file).isFile(),`${kind} evidence is not a file`);const bytes=fs.readFileSync(file);assert.ok(bytes.length&&bytes.length<=64*1024*1024,`${kind} evidence is empty or too large`);
58
+ if(kind==='screenshot')verifyPng(bytes);
59
+ if(kind==='performance'){
60
+ assert.ok(!bytes.subarray(0,2).equals(Buffer.from('PK')),'Playwright ZIP is not a DevTools trace');
61
+ let trace;try{const decoded=bytes[0]===31&&bytes[1]===139?zlib.gunzipSync(bytes,{maxOutputLength:64*1024*1024}):bytes;trace=JSON.parse(decoded.toString('utf8'));}catch{throw new Error('Parseable DevTools trace JSON required');}
62
+ const events=Array.isArray(trace)?trace:trace?.traceEvents;assert.ok(Array.isArray(events)&&events.length&&events.some(event=>typeof event.name==='string'),'Nonempty DevTools trace events required');
63
+ }
64
+ evidenceDigests[observed.evidence]=hash(bytes);
65
+ }
66
+ if(report.evidenceDigests)assert.deepEqual(report.evidenceDigests,evidenceDigests,'DevTools evidence bytes changed');
67
+ return evidenceDigests;
68
+ }
69
+ function check(directory,lib){
70
+ try{
71
+ const configured=readPolicy(directory);if(!configured)return {status:'not-required',required:false};
72
+ const file=inside(directory,'qa/devtools-review.json');if(!fs.existsSync(file))return {status:'not-run',required:true,missing:[...KINDS],limitations};
73
+ const report=json(file),current=lib.manifest(directory),build=lib.verifyBuild(directory,report.buildId);
74
+ const latestFile=inside(directory,'last-build.json');if(fs.existsSync(latestFile))assert.equal(json(latestFile).buildId,report.buildId,'DevTools review is not for the latest build');
75
+ const browserFile=inside(directory,'qa/latest.json');if(fs.existsSync(browserFile)){const supplemental=json(inside(directory,json(browserFile).report));assert.equal(supplemental.buildId,report.buildId,'DevTools review and browser regression build differ');assert.equal(supplemental.buildDigest,report.buildDigest,'DevTools review and browser regression bytes differ');}
76
+ const evidenceDigests=validate(directory,report,current,build,configured);assert.ok(report.evidenceDigests,'Recorded DevTools evidence hashes required');
77
+ return {status:'current',required:true,report:'qa/devtools-review.json',reportDigest:hash(fs.readFileSync(file)),revision:report.revision,candidateDigest:report.candidateDigest,buildId:report.buildId,buildDigest:report.buildDigest,evidenceDigests,invocationVerified:false,limitations};
78
+ }catch(error){return {status:'failed',required:true,errors:[error.message],limitations};}
79
+ }
80
+ function record(directory,report,lib){
81
+ const configured=readPolicy(directory);assert.ok(configured,'This fixed input has no DevTools review policy; use its original contract');
82
+ const current=lib.manifest(directory),build=lib.verifyBuild(directory,report.buildId),copy=JSON.parse(JSON.stringify(report));
83
+ const latestFile=inside(directory,'last-build.json');if(fs.existsSync(latestFile))assert.equal(json(latestFile).buildId,report.buildId,'DevTools review is not for the latest build');
84
+ copy.evidenceDigests=validate(directory,copy,current,build,configured);
85
+ require('./execution-runtime.cjs').requireStage(directory,'browser',lib,{devtoolsReport:copy});
86
+ const owner=`devtools-${crypto.randomUUID()}`;lib.claim(directory,{owner,expectedRevision:current.revision,expectedDigest:current.digest});
87
+ try{
88
+ assert.equal(lib.manifest(directory).digest,current.digest,'Candidate changed during DevTools review');validate(directory,copy,current,lib.verifyBuild(directory,copy.buildId),configured);
89
+ write(directory,'qa/devtools-review.json',copy);return {status:'passed',report:'qa/devtools-review.json',candidateDigest:current.digest,buildId:copy.buildId,invocationVerified:false,limitations};
90
+ }finally{lib.release(directory,owner);}
91
+ }
92
+ module.exports={KINDS,policy,readPolicy,validate,check,record};
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const crypto = require('node:crypto');
5
+ const assert = require('node:assert/strict');
6
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
7
+ const json = file => JSON.parse(fs.readFileSync(file,'utf8'));
8
+ const id = value => { assert.match(value || '',/^[a-z0-9]+(?:-[a-z0-9]+)*$/,'Invalid identifier'); return value; };
9
+ function inside(root,relative) {
10
+ assert.ok(typeof relative==='string' && relative && !path.isAbsolute(relative) && !path.win32.isAbsolute(relative) && !relative.includes('\\') && relative.split('/').every(p=>p && p!=='.' && p!=='..'),'Unsafe prototype path');
11
+ let current=path.resolve(root);
12
+ for(const part of relative.split('/')) { current=path.join(current,part); if(fs.existsSync(current)) assert.ok(!fs.lstatSync(current).isSymbolicLink(),'Prototype symlink refused'); }
13
+ return current;
14
+ }
15
+ function walk(root,prefix='',exclude=()=>false) {
16
+ const location=prefix?inside(root,prefix):root;
17
+ if(!fs.existsSync(location)) return [];
18
+ return fs.readdirSync(location,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name)).flatMap(entry=>{
19
+ const relative=prefix?`${prefix}/${entry.name}`:entry.name;
20
+ if(exclude(relative,entry.name)) return [];
21
+ assert.ok(!entry.isSymbolicLink(),`Prototype symlink refused: ${relative}`);
22
+ return entry.isDirectory()?walk(root,relative,exclude):entry.isFile()?[relative]:[];
23
+ });
24
+ }
25
+ function write(root,relative,value) {
26
+ const file=inside(root,relative); fs.mkdirSync(path.dirname(file),{recursive:true});
27
+ const tmp=`${file}.${crypto.randomUUID()}.tmp`;
28
+ try { fs.writeFileSync(tmp,typeof value==='string'?value:JSON.stringify(value,null,2)+'\n',{flag:'wx'}); fs.renameSync(tmp,file); }
29
+ finally { if(fs.existsSync(tmp)) fs.unlinkSync(tmp); }
30
+ }
31
+ function entries(root,files) { return Object.fromEntries([...new Set(files)].sort().map(file=>[file,hash(fs.readFileSync(inside(root,file)))])); }
32
+ const cacheRoots = new Set(['node_modules','dist','.vite','.git','coverage',...['app','starter'].flatMap(root=>['node_modules','dist','.vite','.git','coverage'].map(name=>`${root}/${name}`))]);
33
+ // A source/public subdirectory named "dist", or an imported .log file, is still source.
34
+ const excluded = (relative,name) => cacheRoots.has(relative) || name==='.DS_Store' || ['app/tsconfig.tsbuildinfo','starter/tsconfig.tsbuildinfo'].includes(relative);
35
+ module.exports={fs,path,crypto,assert,hash,json,id,inside,walk,write,entries,excluded};