@givolt/i3-composer-agent 2.3.2 → 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 (92) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +5 -1
  6. package/bin/agent-targets.json +4 -2
  7. package/bin/i3-composer-agent.cjs +44 -8
  8. package/bin/install.cjs +2 -1
  9. package/bin/native-layout.cjs +1 -1
  10. package/bin/tool-selector.cjs +24 -7
  11. package/bin/wizard.cjs +89 -15
  12. package/docs/codebuddy-setup.md +27 -2
  13. package/docs/codex-setup.md +3 -1
  14. package/docs/other-hosts.md +1 -1
  15. package/docs/ui-prototype-native/USAGE.md +4 -2
  16. package/docs/workbuddy-setup.md +3 -1
  17. package/hooks/codebuddy-upstream.cjs +93 -0
  18. package/hooks/i3-prototype.cjs +12 -0
  19. package/hooks/manage.cjs +162 -0
  20. package/package.json +31 -1
  21. package/plugin.json +1 -1
  22. package/scripts/prototype-cache-test.js +55 -0
  23. package/scripts/prototype-generation-entry-test.js +6 -0
  24. package/scripts/prototype-generation-evidence-test.js +10 -0
  25. package/scripts/prototype-generation-native-test.js +78 -0
  26. package/scripts/prototype-generation-test.js +99 -0
  27. package/scripts/prototype-inventory-test.js +43 -0
  28. package/scripts/prototype-locations-test.js +21 -0
  29. package/scripts/prototype-project-test.js +183 -0
  30. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  31. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  32. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  33. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  34. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  35. package/scripts/ui-prototype-ide-source-test.js +73 -0
  36. package/scripts/ui-prototype-registration-test.js +53 -0
  37. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  38. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  39. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  40. package/skills/i3-ui-prototype/SKILL.md +27 -7
  41. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  42. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  43. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +14 -3
  44. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  45. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +6 -1
  46. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  47. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
  48. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  49. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  50. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +4 -2
  51. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  52. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  53. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  54. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  55. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +1 -1
  56. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  57. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  58. package/skills/i3-ui-prototype/references/artifact-contract.md +1 -1
  59. package/skills/i3-ui-prototype/references/content-template.md +9 -4
  60. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  61. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  62. package/skills/i3-ui-prototype/references/interaction-acceptance.md +4 -0
  63. package/skills/i3-ui-prototype/references/optional-skills.md +4 -0
  64. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  65. package/skills/i3-ui-prototype/references/template-adapters.md +6 -0
  66. package/skills/i3-ui-prototype/references/workflow-contract.md +4 -0
  67. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  68. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  69. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  70. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  71. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  72. package/skills/i3-ui-prototype/scripts/content-template.cjs +2 -2
  73. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  74. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  75. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  76. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  77. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  78. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  79. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  80. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  81. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  82. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  83. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +17 -5
  84. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +1 -0
  85. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +66 -18
  86. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +18 -2
  87. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  88. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  89. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +5 -7
  90. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +5 -1
  91. package/skills/i3-ui-prototype/scripts/template-contract.cjs +11 -2
  92. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Original scripts stay unchanged; this adapter only maps host paths and input fields.
4
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
5
+ const M=require('./manage.cjs');
6
+ function projectKey(project){return (path.basename(project).replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,32)||'project')+'-'+M.sha(project).slice(0,16);}
7
+ function effective({project,product,group,home=os.homedir(),env=process.env,read=file=>M.modules().R.fileIO.read(file),load=base=>registration(base)}) {
8
+ const projectBase=path.join(project,'.codebuddy'),globalBase=M.modules().P.globalBase({home,env});
9
+ const projectText=read(path.join(projectBase,'settings.json')),local=read(path.join(projectBase,'settings.local.json'));
10
+ if(local){const settings=M.config(local);assert.ok(!settings.hooks&&!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Local Hook override requires review');}
11
+ for(const base of [projectBase,globalBase]){const settings=M.config(read(path.join(base,'settings.json')));assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents Hooks');}
12
+ const bases=product==='ide'&&projectText!==null?[projectBase]:[projectBase,globalBase];
13
+ for(const base of [...new Set(bases)]){const record=load(base);if(record?.groups[group]){assert.equal(record.product,product,'Conflicting CodeBuddy products');return record;}}
14
+ return null;
15
+ }
16
+ function projectFor(options,payload,env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=(file,session)=>require(path.join(M.modules().scripts,'execution-evidence.cjs')).ideWorkspace(file,session)) {
17
+ // CLI 2.169 omits cwd on SessionStart. Use the actual startup Hook process
18
+ // directory only for that event; other events still require native cwd.
19
+ const cwd=payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);
20
+ assert.ok(path.isAbsolute(cwd||''),'Native Hook cwd required');
21
+ const ideRoot=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&cwd==='/';
22
+ const project=real(options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):env.CODEBUDDY_PROJECT_DIR||env.CLAUDE_PROJECT_DIR||cwd);
23
+ const relative=path.relative(project,real(ideRoot?project:cwd));assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Hook working directory is outside the registered project');return project;
24
+ }
25
+ function invocation({group,event,project,cwd=project,base,payload},source) {
26
+ const value=structuredClone(payload),cache=path.join(base,'cache/i3-composer-agent/hooks',projectKey(project));
27
+ if(group==='sdd-cache') {
28
+ if(value.tool_name!=='WebFetch'||!['PreToolUse','PostToolUse'].includes(event))return null;
29
+ const before='${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache';assert.equal(source.split(before).length,3,'Upstream cache path changed; review the adapter');
30
+ source=source.replaceAll(before,'${I3_HOOK_CACHE}/sdd-cache');
31
+ } else if(group==='simplify-ignore') {
32
+ if(event==='Stop')value.tool_name='';
33
+ if(event!=='Stop') {
34
+ if(event==='PreToolUse'?value.tool_name!=='Read':!['Edit','Write'].includes(value.tool_name))return null;
35
+ const file=value.tool_input?.file_path||value.tool_input?.filePath;if(typeof file!=='string')return null;
36
+ const resolved=path.resolve(cwd,file),relative=path.relative(project,resolved);
37
+ if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))return null;
38
+ value.tool_input.file_path=resolved;
39
+ }
40
+ const before='${CLAUDE_PROJECT_DIR:-.}/.claude/.simplify-ignore-cache';assert.equal(source.split(before).length,2,'Upstream simplify cache path changed; review the adapter');
41
+ source=source.replace(before,'${I3_HOOK_CACHE}/simplify-ignore');
42
+ }else assert.ok(group==='session-start'&&event==='SessionStart','Unknown upstream Hook event');
43
+ return {source,payload:value,cache,env:{CLAUDE_PROJECT_DIR:project,CODEBUDDY_PROJECT_DIR:project,I3_HOOK_CACHE:cache}};
44
+ }
45
+ function bundle(directory=__dirname) {
46
+ const files=['manage.cjs','codebuddy-upstream.cjs','session-start.sh','sdd-cache-pre.sh','sdd-cache-post.sh','simplify-ignore.sh'];
47
+ return M.sha(JSON.stringify(files.map(file=>{let text=fs.readFileSync(path.join(directory,file),'utf8');if(file==='session-start.sh')text=text.replace(/^SKILLS_DIR=.*$/m,'SKILLS_DIR=<native-layout>');return [file,M.sha(text)];})));
48
+ }
49
+ function registration(base) {
50
+ const io=M.modules().R.fileIO,raw=io.read(path.join(base,'state/i3-composer-agent/hooks.json'));if(raw===null)return null;
51
+ const record=JSON.parse(raw),settingsText=io.read(path.join(base,'settings.json'));
52
+ M.validateOwned(M.config(settingsText),record,base,record.scope);
53
+ for(const item of Object.values(record.groups))assert.equal(item.scriptDigest,bundle(),'Upstream Hook adapter changed; refresh registration');
54
+ return {...record,settingsDigest:M.sha(settingsText)};
55
+ }
56
+ function observe(base,project,group,payload,record,result) {
57
+ const io=M.modules().R.fileIO,file=path.join(base,'state/i3-composer-agent/hook-observations',projectKey(project),group+'.json'),unlock=io.lock(file);
58
+ try {
59
+ const before=io.read(file),old=before?JSON.parse(before):null;
60
+ const same=old?.settingsDigest===record.settingsDigest&&old?.scriptDigest===record.groups[group].scriptDigest;
61
+ const value={schemaVersion:1,group,base,project,sessionId:payload.session_id||null,event:payload.hook_event_name,at:new Date().toISOString(),
62
+ events:[...new Set([...(same?old.events:[]),payload.hook_event_name])],exitCode:result.status,settingsDigest:record.settingsDigest,scriptDigest:record.groups[group].scriptDigest,invocationVerified:false};
63
+ io.write(file,M.json(value),M.digest(before));
64
+ }finally{unlock();}
65
+ }
66
+ function main(argv=process.argv.slice(2),raw=fs.readFileSync(0,'utf8')) {
67
+ assert.ok(Buffer.byteLength(raw)<=4*1024*1024,'Hook input too large');const options={};
68
+ while(argv.length){const flag=argv.shift();assert.ok(flag.startsWith('--')&&argv.length,'Invalid Hook argument');assert.ok(['registration','group','base','scope','product','event'].includes(flag.slice(2)),'Unknown Hook argument');options[flag.slice(2)]=argv.shift();}
69
+ assert.equal(options.registration,M.OWNER,'Invalid Hook registration');assert.ok(path.isAbsolute(options.base||''),'Absolute Hook base required');assert.ok(['project','global'].includes(options.scope),'Invalid Hook scope');
70
+ const item=M.catalog.find(x=>x.id===options.group&&x.origin==='upstream');assert.ok(item?.events.includes(options.event),'Invalid Hook group/event');
71
+ const payload=JSON.parse(raw);assert.equal(payload.hook_event_name,options.event,'Hook payload event differs');const project=projectFor(options,payload);
72
+ const expected=options.scope==='project'?path.join(project,'.codebuddy'):M.modules().P.globalBase();assert.equal(options.base,expected,'Hook installation scope differs');
73
+ const record=registration(options.base);
74
+ if(!record?.groups[options.group])return {status:0,stdout:'{"continue":true}\n',stderr:''};
75
+ assert.equal(record.product,options.product,'Hook product differs');
76
+ if(effective({project,product:options.product,group:options.group})?.base!==options.base)return {status:0,stdout:'{"continue":true}\n',stderr:''};
77
+ const filename=options.group==='session-start'?'session-start.sh':options.group==='simplify-ignore'?'simplify-ignore.sh':options.event==='PreToolUse'?'sdd-cache-pre.sh':'sdd-cache-post.sh';
78
+ const cwd=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/'?project:payload.cwd||project;
79
+ const script=path.join(__dirname,filename),prepared=invocation({...options,project,cwd,payload},fs.readFileSync(script,'utf8'));
80
+ if(!prepared)return {status:0,stdout:'{"continue":true}\n',stderr:''};
81
+ if(options.group==='simplify-ignore'&&options.event!=='Stop'&&fs.existsSync(prepared.payload.tool_input.file_path)) {
82
+ const real=fs.realpathSync(prepared.payload.tool_input.file_path),relative=path.relative(project,real);assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Protected file symlink escapes the project');
83
+ }
84
+ const unlock=M.modules().R.fileIO.lock(path.join(prepared.cache,'.'+options.group+'.event'));
85
+ let result;try {
86
+ result=require('node:child_process').spawnSync(record.groups[options.group].bashPath,['-c',prepared.source,script],{cwd:project,env:{...process.env,...prepared.env},input:JSON.stringify(prepared.payload),encoding:'utf8',timeout:15000,maxBuffer:8*1024*1024});
87
+ if(result.error||result.status===null)throw new Error(result.error?.message||'Upstream Hook did not complete');
88
+ }finally{unlock();}
89
+ try{observe(options.base,project,options.group,payload,record,result);}catch(error){result.stderr=(result.stderr||'')+`\ni3 Hook observation unavailable: ${error.message}\n`;}
90
+ return result;
91
+ }
92
+ module.exports={projectKey,projectFor,effective,invocation,bundle,registration,observe,main};
93
+ if(require.main===module)try{const result=main();process.stdout.write(result.stdout||'');process.stderr.write(result.stderr||'');process.exitCode=result.status;}catch(error){process.stderr.write(`i3 CodeBuddy Hook unavailable: ${error.message}\n`);process.exitCode=1;}
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Stable host Hook entry; the workflow and A+B implementation remain in the Skill.
4
+ const fs=require('node:fs'),path=require('node:path');
5
+ function main(argv=process.argv.slice(2),raw=fs.readFileSync(0,'utf8'),load=()=>require(path.join(require('./manage.cjs').modules().scripts,'hosts/codebuddy.cjs'))) {
6
+ return load().main(argv,raw);
7
+ }
8
+ module.exports={main};
9
+ if(require.main===module)try {
10
+ const result=main();process.stdout.write(JSON.stringify(result.output)+'\n');
11
+ if(result.code===2)process.stderr.write(result.output.reason+'\n');process.exitCode=result.code;
12
+ }catch(error){process.stderr.write(`i3 prototype audit unavailable: ${error.message}. Strict A gates remain unverified.\n`);process.exitCode=1;}
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // CodeBuddy settings protocol: https://www.codebuddy.cn/docs/cli/hooks
4
+ // IDE override semantics: https://www.codebuddy.cn/docs/ide/Features/Hooks
5
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),crypto=require('node:crypto'),assert=require('node:assert/strict');
6
+ const OWNER='i3-composer-agent-codebuddy-v1';
7
+ const catalog=[
8
+ {id:'session-start',name:'Skill 发现与路由',origin:'upstream',files:['session-start.sh'],events:['SessionStart'],dependencies:['bash','jq'],condition:'明确 skill-routing=hook;原生路由使用 native,避免重复注入'},
9
+ {id:'sdd-cache',name:'官方文档请求缓存',origin:'upstream',files:['sdd-cache-pre.sh','sdd-cache-post.sh'],events:['PreToolUse','PostToolUse'],dependencies:['bash','jq','curl','sha256'],condition:'实际 WebFetch;仅源站 304 复用'},
10
+ {id:'simplify-ignore',name:'代码块简化保护',origin:'upstream',files:['simplify-ignore.sh'],events:['PreToolUse','PostToolUse','Stop'],dependencies:['bash','jq','sha1'],condition:'使用 simplify-ignore 标记;会在运行期间替换标记块并在 Stop 恢复'},
11
+ {id:'prototype',name:'原型执行审计',origin:'i3',events:['SessionStart','UserPromptSubmit','PreToolUse','PostToolUse','Stop'],dependencies:['node'],condition:'真实 CodeBuddy IDE/CLI;默认从原生 Hook 自动识别日志,支持手动目录限制'},
12
+ ];
13
+ const digest=value=>crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
14
+ const sha=bytes=>crypto.createHash('sha256').update(bytes).digest('hex');
15
+ const json=value=>JSON.stringify(value,null,2)+'\n';
16
+ function selection(value,action='check') {
17
+ const ids=value===undefined?(['check','list'].includes(action)?catalog.map(x=>x.id):['prototype']):value==='all'?catalog.map(x=>x.id):value.split(',');
18
+ assert.ok(ids.length&&ids.every(id=>catalog.some(item=>item.id===id)),'Unknown Hook group; choose session-start,sdd-cache,simplify-ignore,prototype or all');
19
+ return [...new Set(ids)];
20
+ }
21
+ function quote(value){assert.ok(typeof value==='string'&&!/[\x00\r\n]/.test(value),'Unsafe Hook argument');return "'"+value.replaceAll("'","'\\''")+"'";}
22
+ function config(raw){const value=raw===null?{}:JSON.parse(raw);assert.ok(value&&typeof value==='object'&&!Array.isArray(value),'Settings must be an object');assert.ok(value.hooks===undefined||value.hooks&&typeof value.hooks==='object'&&!Array.isArray(value.hooks),'Invalid hooks configuration');for(const groups of Object.values(value.hooks||{})){assert.ok(Array.isArray(groups),'Invalid Hook groups');for(const group of groups)assert.ok(group&&Array.isArray(group.hooks),'Invalid Hook group');}return value;}
23
+ function modules() {
24
+ const roots=[path.resolve(__dirname,'..'),path.resolve(__dirname,'../..')];
25
+ const candidates=roots.flatMap(root=>['i3-ui-prototype',['ui','prototype'].join('-')].map(id=>path.join(root,'skills',id,'scripts')));
26
+ const scripts=candidates.find(dir=>fs.existsSync(path.join(dir,'prototype-hooks.cjs')));assert.ok(scripts,'Prototype audit implementation is missing; install the complete package');
27
+ return {scripts,R:require(path.join(scripts,'codebuddy-registration.cjs')),P:require(path.join(scripts,'prototype-hooks.cjs'))};
28
+ }
29
+ function owned(group){return group.hooks.some(h=>typeof h.command==='string'&&h.command.includes(OWNER));}
30
+ function validateOwned(settings,record,base,scope) {
31
+ if(record){assert.equal(record.owner,OWNER,'Unknown Hook ownership');assert.equal(record.schemaVersion,1,'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');assert.equal(record.scope,scope,'Hook scope differs');assert.ok(record.groups&&typeof record.groups==='object'&&!Array.isArray(record.groups),'Invalid owned Hook groups');
32
+ for(const [id,item] of Object.entries(record.groups)){const known=catalog.find(x=>x.id===id&&x.origin==='upstream');assert.ok(known&&item&&item.groups,'Unknown owned Hook group');assert.deepEqual(Object.keys(item.groups).sort(),[...known.events].sort(),'Owned Hook events changed');}
33
+ }
34
+ const found=new Set();
35
+ for(const [event,groups] of Object.entries(settings.hooks||{}))for(const group of groups.filter(owned)) {
36
+ const ids=Object.keys(record?.groups||{}).filter(id=>record.groups[id].groups[event]&&group.hooks.some(h=>h.command?.includes(`--group' '${id}'`)));
37
+ assert.equal(ids.length,1,'Hook ownership is missing; preserve the unknown command');const key=ids[0]+':'+event;
38
+ assert.ok(!found.has(key),'Duplicate owned Hook');assert.deepEqual(group,record.groups[ids[0]].groups[event],'Owned Hook was modified; preserve user changes');found.add(key);
39
+ }
40
+ for(const [id,item] of Object.entries(record?.groups||{}))for(const event of Object.keys(item.groups))assert.ok(found.has(id+':'+event),'Owned Hook registration is missing');
41
+ }
42
+ function requirements(id,deps) {
43
+ for(const name of catalog.find(x=>x.id===id).dependencies){if(name==='sha256')assert.ok(deps.shasum||deps.sha256sum,'Missing shasum or sha256sum');else if(name==='sha1')assert.ok(deps.shasum||deps.sha1sum,'Missing shasum or sha1sum');else assert.ok(deps[name],`Missing ${name}; install it separately before enabling ${id}`);}
44
+ }
45
+ function planUpstream(options) {
46
+ const {base,scope,product,action,record=null,settingsText=null,nodePath,hookDirectory,scriptDigest,dependencies={},skillRouting}=options;
47
+ assert.ok(['keep','enable','disable'].includes(action),'Invalid Hook action');assert.ok(['project','global'].includes(scope),'Specify project or global scope');
48
+ const settings=config(settingsText);validateOwned(settings,record,base,scope);
49
+ const chosen=selection(options.groups,action).filter(id=>id!=='prototype'),enabled=new Set(Object.keys(record?.groups||{}));
50
+ if(action==='enable')for(const id of chosen){if(id==='session-start')assert.equal(skillRouting,'hook','session-start requires explicit skill-routing=hook; native routing must not be duplicated');requirements(id,dependencies);enabled.add(id);}
51
+ if(action==='disable')for(const id of chosen)enabled.delete(id);
52
+ if(enabled.size){assert.ok(['ide','cli'].includes(product),'Specify CodeBuddy product ide or cli');if(record)assert.equal(product,record.product,'Disable the previous product profile before changing it');assert.match(scriptDigest,/^[a-f0-9]{64}$/);for(const p of [base,nodePath,hookDirectory])assert.ok(path.isAbsolute(p),'Absolute registration paths required');}
53
+ // Remove only exact owned groups after validation. All user Hook groups survive.
54
+ for(const event of Object.keys(settings.hooks||{}))settings.hooks[event]=settings.hooks[event].filter(g=>!owned(g));
55
+ const groups={};
56
+ for(const item of catalog.filter(x=>enabled.has(x.id))) {
57
+ const entry={product,scriptDigest,nodePath,bashPath:dependencies.bash||record?.groups[item.id]?.bashPath,skillRouting:item.id==='session-start'?'hook':null,groups:{}};
58
+ for(const event of item.events) {
59
+ const matcher=item.id==='sdd-cache'?'^WebFetch$':event==='PreToolUse'?'^Read$':'^(Edit|Write)$';
60
+ const argv=[nodePath,path.join(hookDirectory,'codebuddy-upstream.cjs'),'--registration',OWNER,'--group',item.id,'--base',base,'--scope',scope,'--product',product,'--event',event];
61
+ const group={...(event.endsWith('ToolUse')?{matcher}:{}),hooks:[{type:'command',command:argv.map(quote).join(' '),timeout:item.id==='sdd-cache'?15:20}]};
62
+ entry.groups[event]=group;settings.hooks||={};(settings.hooks[event]||=[]).push(group);
63
+ }
64
+ groups[item.id]=entry;
65
+ }
66
+ const recordAfter=enabled.size?{schemaVersion:1,owner:OWNER,base,scope,product,productVersion:options.productVersion||record?.productVersion||'unreported',groups}:null;
67
+ const afterText=!record&&!chosen.length||!record&&action!=='enable'?settingsText:json(settings);
68
+ return {afterText,recordAfter};
69
+ }
70
+ function apply(change,io=modules().R.fileIO) {
71
+ const unlock=io.lock?io.lock(change.changes[0].file):()=>{},written=[];
72
+ try {
73
+ for(const entry of change.changes)assert.equal(digest(io.read(entry.file)),digest(entry.before),'Hook configuration changed after planning');
74
+ try {for(const entry of change.changes)if(entry.before!==entry.after){io.write(entry.file,entry.after,digest(entry.before));written.push(entry);}}
75
+ catch(error){for(const entry of written.reverse())if(io.read(entry.file)===entry.after)io.write(entry.file,entry.before,digest(entry.after));throw error;}
76
+ return change.summary;
77
+ }finally{unlock();}
78
+ }
79
+ function executable(name,env=process.env) {
80
+ for(const folder of (env.PATH||'').split(path.delimiter)){const file=path.resolve(folder,name);try{if(fs.statSync(file).isFile()){fs.accessSync(file,fs.constants.X_OK);return file;}}catch{}}
81
+ return null;
82
+ }
83
+ function dependencies(){return Object.fromEntries(['bash','jq','curl','shasum','sha256sum','sha1sum'].map(name=>[name,executable(name)]));}
84
+ function location(options) {
85
+ assert.equal(options.host||'codebuddy','codebuddy','Unified Hooks currently support CodeBuddy only');assert.ok(['project','global'].includes(options.scope),'Specify project or global scope');
86
+ const project=fs.realpathSync(path.resolve(options.project||'.'));assert.ok(fs.statSync(project).isDirectory(),'Project must be a directory');
87
+ return {project,base:options.scope==='project'?path.join(project,'.codebuddy'):modules().P.globalBase(options)};
88
+ }
89
+ function unmanaged(settings) {
90
+ const files={ 'session-start':['session-start.sh'],'sdd-cache':['sdd-cache-pre.sh','sdd-cache-post.sh'],'simplify-ignore':['simplify-ignore.sh'] },result=[];
91
+ for(const [event,groups] of Object.entries(settings.hooks||{}))for(const group of groups)for(const hook of group.hooks) {
92
+ if(typeof hook.command!=='string'||hook.command.includes(OWNER))continue;
93
+ for(const [id,names] of Object.entries(files))if(names.some(name=>hook.command.includes(name)))result.push({id,event,command:hook.command});
94
+ }
95
+ return result;
96
+ }
97
+ function pendingSimplify(base) {
98
+ const parent=path.join(base,'cache/i3-composer-agent/hooks');if(!fs.existsSync(parent))return [];
99
+ return fs.readdirSync(parent,{withFileTypes:true}).filter(entry=>entry.isDirectory()&&!entry.isSymbolicLink()).flatMap(entry=>{
100
+ const dir=path.join(parent,entry.name,'simplify-ignore');if(!fs.existsSync(dir))return [];
101
+ assert.ok(!fs.lstatSync(dir).isSymbolicLink(),'Simplify cache symlink requires review');return fs.readdirSync(dir).filter(file=>file.endsWith('.bak')).map(file=>path.join(dir,file));
102
+ });
103
+ }
104
+ function prepare(options,runtime={}) {
105
+ const {P,R}=runtime.modules||modules(),{project,base}=runtime.location||location(options),io=R.fileIO,action=options.action||'keep',ids=selection(options.groups,action);
106
+ const settingsPath=path.join(base,'settings.json'),recordPath=path.join(base,'state/i3-composer-agent/hooks.json'),prototypePath=path.join(base,'state/i3-prototype/installation.json');
107
+ const before=io.read(settingsPath),recordText=io.read(recordPath),record=recordText?JSON.parse(recordText):null,prototypeText=io.read(prototypePath),prototype=prototypeText?JSON.parse(prototypeText):null;
108
+ if(action!=='disable'&&(action!=='keep'||record||prototype))assert.ok(process.platform!=='win32','CodeBuddy Hook quoting on Windows has not been validated; default installation without Hooks remains available');
109
+ const product=options.product||record?.product||prototype?.product,settings=config(before);
110
+ if(action!=='disable'&&(action!=='keep'||record||prototype)) {
111
+ assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic Hooks; preserve the policy');
112
+ const manifest=io.read(path.join(base,'.i3-composer-agent.json'));if(manifest)assert.ok(!JSON.parse(manifest).agents?.includes('workbuddy'),'Shared WorkBuddy scope cannot identify CodeBuddy safely');
113
+ assert.ok(!options.agents?.includes('workbuddy'),'Use an exclusive CodeBuddy scope for Hooks');
114
+ }
115
+ if(action==='enable')for(const id of ids)assert.ok(!unmanaged(settings).some(item=>item.id===id),`Unmanaged ${id} Hook already exists; preserve it and resolve the duplicate before enabling`);
116
+ if(ids.includes('simplify-ignore')&&['enable','disable'].includes(action)) {
117
+ for(const candidate of [...new Set([base,path.join(project,'.codebuddy'),P.globalBase?P.globalBase(options):path.join(options.home||os.homedir(),'.codebuddy')])])assert.equal((runtime.pendingSimplify||pendingSimplify)(candidate).length,0,'Simplify protection is still active; finish the session and restore blocks before changing Hooks');
118
+ }
119
+ const prototypeAction=ids.includes('prototype')?action:'keep';
120
+ const prototypeChange=action==='disable'&&!ids.includes('prototype')?{unchanged:true}:P.prepare({...options,project,product,action:prototypeAction,entryPath:path.join(base,'i3-composer-agent/hooks/i3-prototype.cjs'),entrySource:path.join(__dirname,'i3-prototype.cjs')});
121
+ const upstream=planUpstream({...options,base,product,action,record,settingsText:prototypeChange.unchanged?before:prototypeChange.afterText,nodePath:process.execPath,
122
+ hookDirectory:path.join(base,'i3-composer-agent/hooks'),scriptDigest:runtime.scriptDigest||require('./codebuddy-upstream.cjs').bundle(),dependencies:runtime.dependencies||dependencies()});
123
+ const changes=[{file:settingsPath,before,after:upstream.afterText},{file:recordPath,before:recordText,after:upstream.recordAfter?json(upstream.recordAfter):null},
124
+ {file:prototypePath,before:prototypeText,after:prototypeChange.unchanged?prototypeText:prototypeChange.recordAfter?json(prototypeChange.recordAfter):null}];
125
+ return {changes,summary:{host:'codebuddy',scope:options.scope,base,product:product||null,action,groups:ids,changed:changes.some(item=>item.before!==item.after),
126
+ enabledGroups:[...Object.keys(upstream.recordAfter?.groups||{}),...((prototypeChange.unchanged?prototype:prototypeChange.recordAfter)?['prototype']:[])],
127
+ ...(prototypeChange.summary?.transcript?{transcript:prototypeChange.summary.transcript}:{}),
128
+ next:product==='cli'?'Review /hooks in CodeBuddy Code and start a new session.':'Reload CodeBuddy and verify actual Hook events. Registration is not activation.'}};
129
+ }
130
+ function check(options) {
131
+ const {P,R}=modules(),{project,base}=location(options),io=R.fileIO,ids=selection(options.groups,options.action||'check'),settingsText=io.read(path.join(base,'settings.json')),settings=config(settingsText),deps=dependencies(),items=[];
132
+ let record=null,error=null;try{record=require('./codebuddy-upstream.cjs').registration(base);}catch(e){error=e.message;}
133
+ for(const id of ids) {
134
+ const item=catalog.find(x=>x.id===id),entry=path.join(base,'i3-composer-agent/hooks',id==='prototype'?'i3-prototype.cjs':'codebuddy-upstream.cjs');
135
+ if(id==='prototype'){try{const result=P.check({...options,project});items.push({...item,...result,...(result.effective===false&&result.status!=='failed'?{status:'shadowed'}:{}),entryPath:entry});}catch(e){items.push({...item,status:'failed',reason:e.message,entryPath:entry});}continue;}
136
+ const missingFiles=[entry,...item.files.map(file=>path.join(base,'i3-composer-agent/hooks',file))].filter(file=>!fs.existsSync(file));
137
+ const row={...item,entryPath:entry,filesPresent:missingFiles.length===0,missingFiles,status:missingFiles.length?'not-installed':'files-present',invocationVerified:false,nativeValidation:'pending-user'};
138
+ try{requirements(id,deps);}catch(e){row.dependencyIssue=e.message;}
139
+ if(error){row.status='failed';row.reason=error;}
140
+ else if(record?.groups[id])try {
141
+ const U=require('./codebuddy-upstream.cjs'),selected=U.effective({project,product:record.product,group:id,home:options.home,env:options.env});row.effective=selected?.base===base;
142
+ row.status=!row.effective?'shadowed':record.product==='cli'?'pending-host-review':'registered';
143
+ const raw=io.read(path.join(base,'state/i3-composer-agent/hook-observations',U.projectKey(project),id+'.json'));
144
+ if(raw){const observation=JSON.parse(raw);if(observation.settingsDigest===record.settingsDigest&&observation.scriptDigest===record.groups[id].scriptDigest){row.observation=observation;row.status=row.effective?'triggered':row.status;}}
145
+ row.product=record.product;row.productVersion=record.productVersion;
146
+ }catch(e){row.status='failed';row.reason=e.message;}
147
+ items.push(row);
148
+ }
149
+ return {host:'codebuddy',scope:options.scope,base,readOnly:true,changed:false,status:items.some(item=>item.status==='failed')?'failed':'checked',hooks:items,unmanagedHooks:unmanaged(settings),
150
+ next:'files-present is not enabled; registered is not triggered; triggered is not business acceptance. CLI: review /hooks; IDE: reload.'};
151
+ }
152
+ function configure(options){const change=prepare(options);return options.preview?change.summary:apply(change);}
153
+ function main(argv=process.argv.slice(2)) {
154
+ if(argv.includes('--help'))return {usage:'node manage.cjs list|check|enable|disable --host codebuddy --scope project|global --project <directory> [--groups <ids>|all] [--product ide|cli] [--skill-routing hook] [--transcript-root <native-directory>|auto]',hooks:catalog,default:'keep; enable/disable default prototype; list/check default all; transcripts auto for new registrations',nativeValidation:'pending-user'};
155
+ const options={action:argv.shift()||'list',host:'codebuddy',project:process.cwd()};
156
+ assert.ok(['list','check','enable','disable','keep'].includes(options.action),'Choose list, check, enable or disable');
157
+ while(argv.length){const arg=argv.shift();assert.ok(arg.startsWith('--')&&argv[0]&&!argv[0].startsWith('--'),'Hook option needs a value');const key=arg.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());assert.ok(['host','scope','project','product','productVersion','transcriptRoot','groups','skillRouting'].includes(key),'Unknown Hook option');options[key]=argv.shift();}
158
+ if(options.skillRouting!==undefined)assert.ok(['native','hook'].includes(options.skillRouting),'skill-routing must be native or hook');
159
+ return ['list','check'].includes(options.action)?check(options):configure(options);
160
+ }
161
+ module.exports={OWNER,catalog,digest,sha,json,quote,config,selection,modules,validateOwned,requirements,planUpstream,apply,dependencies,location,unmanaged,pendingSimplify,prepare,check,configure,main};
162
+ if(require.main===module)try{const result=main();process.stdout.write(json(result));if(result.status==='failed')process.exitCode=1;}catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
package/package.json CHANGED
@@ -183,17 +183,32 @@
183
183
  "scripts/lib/prototype-fixture.cjs",
184
184
  "scripts/lib/skill-lint-test.js",
185
185
  "scripts/lib/skill-lint.js",
186
+ "scripts/prototype-cache-test.js",
187
+ "scripts/prototype-generation-entry-test.js",
188
+ "scripts/prototype-generation-evidence-test.js",
189
+ "scripts/prototype-generation-native-test.js",
190
+ "scripts/prototype-generation-test.js",
191
+ "scripts/prototype-inventory-test.js",
186
192
  "scripts/prototype-locations-test.js",
193
+ "scripts/prototype-project-test.js",
187
194
  "scripts/run-evals-test.js",
188
195
  "scripts/run-evals.js",
189
196
  "scripts/run-regressions.js",
190
197
  "scripts/template-adapter-test.js",
198
+ "scripts/ui-prototype-codebuddy-hook-test.js",
191
199
  "scripts/ui-prototype-content-test.js",
192
200
  "scripts/ui-prototype-dependencies-test.js",
193
201
  "scripts/ui-prototype-devtools-test.js",
202
+ "scripts/ui-prototype-execution-evidence-test.js",
203
+ "scripts/ui-prototype-execution-gates-test.js",
204
+ "scripts/ui-prototype-execution-policy-test.js",
205
+ "scripts/ui-prototype-execution-runtime-test.js",
206
+ "scripts/ui-prototype-ide-source-test.js",
194
207
  "scripts/ui-prototype-native-test.js",
208
+ "scripts/ui-prototype-registration-test.js",
195
209
  "scripts/ui-prototype-template-test.js",
196
210
  "scripts/ui-prototype-test.js",
211
+ "scripts/ui-prototype-transcript-auto-test.js",
197
212
  "scripts/validate-artifact-paths-test.js",
198
213
  "scripts/validate-artifact-paths.js",
199
214
  "scripts/validate-commands-test.js",
@@ -386,17 +401,32 @@
386
401
  "scripts/lib/prototype-fixture.cjs",
387
402
  "scripts/lib/skill-lint-test.js",
388
403
  "scripts/lib/skill-lint.js",
404
+ "scripts/prototype-cache-test.js",
405
+ "scripts/prototype-generation-entry-test.js",
406
+ "scripts/prototype-generation-evidence-test.js",
407
+ "scripts/prototype-generation-native-test.js",
408
+ "scripts/prototype-generation-test.js",
409
+ "scripts/prototype-inventory-test.js",
389
410
  "scripts/prototype-locations-test.js",
411
+ "scripts/prototype-project-test.js",
390
412
  "scripts/run-evals-test.js",
391
413
  "scripts/run-evals.js",
392
414
  "scripts/run-regressions.js",
393
415
  "scripts/template-adapter-test.js",
416
+ "scripts/ui-prototype-codebuddy-hook-test.js",
394
417
  "scripts/ui-prototype-content-test.js",
395
418
  "scripts/ui-prototype-dependencies-test.js",
396
419
  "scripts/ui-prototype-devtools-test.js",
420
+ "scripts/ui-prototype-execution-evidence-test.js",
421
+ "scripts/ui-prototype-execution-gates-test.js",
422
+ "scripts/ui-prototype-execution-policy-test.js",
423
+ "scripts/ui-prototype-execution-runtime-test.js",
424
+ "scripts/ui-prototype-ide-source-test.js",
397
425
  "scripts/ui-prototype-native-test.js",
426
+ "scripts/ui-prototype-registration-test.js",
398
427
  "scripts/ui-prototype-template-test.js",
399
428
  "scripts/ui-prototype-test.js",
429
+ "scripts/ui-prototype-transcript-auto-test.js",
400
430
  "scripts/validate-artifact-paths-test.js",
401
431
  "scripts/validate-artifact-paths.js",
402
432
  "scripts/validate-commands-test.js",
@@ -408,7 +438,7 @@
408
438
  "scripts/validate-versions.js"
409
439
  ]
410
440
  },
411
- "version": "2.3.2",
441
+ "version": "2.3.3",
412
442
  "repository": {
413
443
  "type": "git",
414
444
  "url": "https://github.com/49780522/i3-composer-agent.git"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const installation = path.join(__dirname, 'lib/installation.js');
8
+ const skills = fs.existsSync(installation) ? require(installation).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
9
+ const scripts = path.join(skills, 'i3-ui-prototype/scripts'), cache = require(path.join(scripts, 'browser-workspace.cjs'));
10
+ function fixture(t, scope = 'project') {
11
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'i3-browser-output-'))), project = path.join(root, '中文 项目'), home = path.join(root, 'isolated-home');
12
+ fs.mkdirSync(project); fs.mkdirSync(home); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
13
+ return { root, project, home, host: 'codebuddy', scope, runId: 'output-test', env: {} };
14
+ }
15
+ for (const scope of ['project', 'global']) test(`${scope} browser output is prepared only under its host cache and can be reused without overwriting files`, t => {
16
+ const options = fixture(t, scope), result = cache.prepare(options);
17
+ assert.equal(result.status, 'prepared');
18
+ assert.ok(result.directory.startsWith(scope === 'global' ? options.home : path.join(options.project, '.codebuddy')));
19
+ assert.equal(fs.existsSync(path.join(options.project, '.playwright-cli')), false);
20
+ assert.equal(fs.existsSync(path.join(options.project, 'prototypes')), false);
21
+ const config = JSON.parse(fs.readFileSync(result.configFile));
22
+ assert.equal(config.outputDir, result.directory); assert.equal(config.outputMode, 'file');
23
+ fs.writeFileSync(path.join(result.directory, 'keep.txt'), 'keep');
24
+ assert.equal(cache.prepare(options).directory, result.directory);
25
+ assert.equal(fs.readFileSync(path.join(result.directory, 'keep.txt'), 'utf8'), 'keep');
26
+ if (scope === 'global') assert.deepEqual(fs.readdirSync(options.project), []);
27
+ });
28
+ test('CLI wrapper uses an existing explicit Node entry, cache cwd and configured output without installing anything', t => {
29
+ const options = fixture(t), entry = path.join(options.root, 'fixture-cli.cjs');
30
+ fs.writeFileSync(entry, 'const f=require("node:fs"),p=require("node:path");const c=JSON.parse(f.readFileSync(process.argv[process.argv.indexOf("--config")+1]));if(c.outputDir!==process.cwd())process.exit(5);f.writeFileSync(p.join(c.outputDir,"snapshot.yml"),"fixture snapshot");console.log("fixture command executed");');
31
+ const result = cache.runCli({ ...options, cli: entry, command: ['snapshot'] });
32
+ assert.equal(result.status, 'executed'); assert.equal(result.exitCode, 0);
33
+ assert.ok(fs.existsSync(path.join(result.directory, 'snapshot.yml')));
34
+ assert.equal(fs.existsSync(path.join(options.project, '.playwright-cli')), false);
35
+ assert.throws(() => cache.runCli({ ...options, cli: entry, command: ['--config=other.json', 'snapshot'] }), /config/i);
36
+ });
37
+ test('explicit evidence archiving preserves candidate identity and survives cache removal', t => {
38
+ const options = fixture(t), output = cache.prepare(options), native = require(path.join(scripts, 'native-pilot-lib.cjs'));
39
+ const scope = { schemaVersion: 2, moduleIds: ['assets'], title: '资产', pages: [{ id: 'assets', title: '资产台账', referencePage: 'models-page', regions: [{ id: 'list', mode: 'management-list' }] }], requirements: [{ id: 'A-01', text: '资产操作', pages: ['assets'], source: { type: 'prd', reference: 'fixture/A-01', statement: 'Synthetic fixture, not real acceptance.' } }], exemptions: [] };
40
+ const file = path.join(options.root, 'scope.json'); fs.writeFileSync(file, JSON.stringify(scope));
41
+ const prototype = native.createPrototype({ project: options.project, id: 'assets', scope: file, expertMode: 'template' }).directory, before = native.manifest(prototype);
42
+ fs.writeFileSync(path.join(output.directory, 'page.yml'), 'synthetic snapshot, not acceptance');
43
+ const archived = cache.archive({ ...options, id: 'assets', files: ['page.yml'] });
44
+ assert.equal(archived.status, 'archived'); assert.equal(archived.verification, 'not-assessed');
45
+ assert.deepEqual(native.manifest(prototype), before);
46
+ fs.rmSync(output.directory, { recursive: true });
47
+ assert.equal(fs.readFileSync(path.join(archived.directory, 'page.yml'), 'utf8'), 'synthetic snapshot, not acceptance');
48
+ assert.throws(() => cache.archive({ ...options, id: 'assets', files: ['../scope.json'] }), /path|cache|exist/i);
49
+ });
50
+ test('unsafe run identifiers and absent CLI entries fail before cache creation', t => {
51
+ const options = fixture(t);
52
+ assert.throws(() => cache.prepare({ ...options, runId: '../outside' }), /identifier|path/i);
53
+ assert.throws(() => cache.runCli({ ...options, cli: path.join(options.root, 'missing.cjs'), command: ['snapshot'] }));
54
+ assert.deepEqual(fs.readdirSync(options.project), []);
55
+ });
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm');
3
+ const install=path.join(__dirname,'lib/installation.js'),skills=fs.existsSync(install)?require(install).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills');const root=path.join(skills,'i3-ui-prototype'),lib=require(path.join(root,'scripts/native-pilot.cjs'));
4
+ const hash=()=>require('node:crypto').createHash('sha256').update(fs.readFileSync(path.join(root,'assets/templates/enterprise-admin/template-inventory.json'))).digest('hex');
5
+ test('template discovery and inventory commands remain read-only and expose real supported choices',async()=>{const before=hash();const options=await lib.main(['generation-options','--template','enterprise-admin']);assert.equal(options.readOnly,true);assert.equal(Object.keys(options.choices.layout).length,4);assert.equal(Object.keys(options.choices.style).length,3);assert.ok(options.patterns['tree-table']);const inventory=await lib.main(['inventory']);assert.equal(inventory.events,595);assert.equal(inventory.components,116);assert.equal(hash(),before);await assert.rejects(()=>lib.main(['generation-options','--template','../bad']));});
6
+ test('offline preview uses metadata-derived local classic scripts and has valid JavaScript',()=>{const templates=path.join(root,'assets/templates'),context={};vm.runInNewContext(fs.readFileSync(path.join(templates,'generation-options.js'),'utf8'),context);assert.equal(context.I3_GENERATION_OPTIONS[0].id,'enterprise-admin');const html=fs.readFileSync(path.join(templates,'presentation.html'),'utf8');assert.ok(!/type="module"|https?:\/\/|fetch\(/.test(html));for(const block of html.matchAll(/<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/g))new vm.Script(block[1]);assert.equal(require(path.join(root,'scripts/generation-assets.cjs')).refresh(root,{check:true}).status,'passed');});
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path');
3
+ const install=path.join(__dirname,'lib/installation.js');const skills=fs.existsSync(install)?require(install).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills');
4
+ const script=path.join(skills,'i3-ui-prototype/scripts/generation-evidence.cjs');
5
+ function fixture(t){const directory=fs.mkdtempSync(path.join(os.tmpdir(),'i3-generation-evidence-'));t.after(()=>fs.rmSync(directory,{recursive:true,force:true}));fs.mkdirSync(path.join(directory,'app/src'),{recursive:true});fs.writeFileSync(path.join(directory,'app/src/Actual.vue'),'<template><a-tree /></template>');return directory;}
6
+ const generation={bindings:[{id:'items/tree/hierarchy',capabilityId:'hierarchy'}],scenarios:[{id:'generation/items/tree/hierarchy/expand',bindingId:'items/tree/hierarchy'}]};
7
+ test('each capability maps to implementation and its exact scenarios before acceptance',t=>{const lib=require(script),dir=fixture(t),fixed=lib.initialize(generation);assert.equal(lib.validateImplementation(dir,generation,fixed).status,'pending');assert.throws(()=>lib.validateImplementation(dir,generation,fixed,{complete:true}),/pending/i);const item=fixed.bindings[0];item.status='implemented';item.components=['Tree'];item.files=[{path:'app/src/Actual.vue',locator:'a-tree'}];assert.equal(lib.validateImplementation(dir,generation,fixed,{complete:true}).status,'implemented');item.scenarioIds=[];assert.throws(()=>lib.validateImplementation(dir,generation,fixed),/scenario/i);});
8
+ test('deleting a binding, pointing to absent code, or using input documents cannot pass',t=>{const lib=require(script),dir=fixture(t),fixed=lib.initialize(generation);fixed.bindings=[];assert.throws(()=>lib.validateImplementation(dir,generation,fixed),/binding/i);fixed.bindings=lib.initialize(generation).bindings;Object.assign(fixed.bindings[0],{status:'implemented',components:['Tree'],files:[{path:'inputs/generation.json',locator:'Tree'}]});assert.throws(()=>lib.validateImplementation(dir,generation,fixed),/implementation|source/i);fixed.bindings[0].files[0].path='app/src/Missing.vue';assert.throws(()=>lib.validateImplementation(dir,generation,fixed),/ENOENT|Missing/);});
9
+ test('browser observations need every scenario, actual actions and immutable evidence bytes',t=>{const lib=require(script),dir=fixture(t);assert.throws(()=>lib.validateObservations(dir,generation,{}),/observation/i);fs.mkdirSync(path.join(dir,'qa'));fs.writeFileSync(path.join(dir,'qa/observation.txt'),'Synthetic test evidence, never a browser result.');const hash=require('node:crypto').createHash('sha256').update(fs.readFileSync(path.join(dir,'qa/observation.txt'))).digest('hex');const report={generationObservations:[{scenarioId:generation.scenarios[0].id,bindingId:generation.bindings[0].id,status:'passed',steps:[{action:'fixture action',target:'fixture tree',observed:'fixture result'}],toolCallIds:['fixture-call'],evidence:[{path:'qa/observation.txt',sha256:hash}]}]};assert.equal(lib.validateObservations(dir,generation,report).invocationVerified,false);report.generationObservations[0].steps=[];assert.throws(()=>lib.validateObservations(dir,generation,report),/step/i);report.generationObservations[0].steps=[{action:'fixture',target:'fixture',observed:'fixture'}];fs.appendFileSync(path.join(dir,'qa/observation.txt'),'changed');assert.throws(()=>lib.validateObservations(dir,generation,report),/evidence/i);});
10
+ test('framework mapping covers all semantic capabilities without device sample assumptions',()=>{const lib=require(script),catalog=require(path.join(skills,'i3-ui-prototype/assets/capabilities/generation.json')),map=lib.framework();assert.deepEqual(Object.keys(map.capabilities).sort(),Object.keys(catalog.capabilities).sort());assert.ok(!JSON.stringify(map).includes('useDevices'));assert.ok(!JSON.stringify(map).includes('21 deterministic'));});
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+ const test = require('node:test'), assert = require('node:assert/strict'), fs = require('node:fs'), os = require('node:os'), path = require('node:path');
3
+ const install = path.join(__dirname, 'lib/installation.js');
4
+ const skills = fs.existsSync(install) ? require(install).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
5
+ const root = path.join(skills, 'i3-ui-prototype'), templates = require(path.join(root, 'scripts/template-contract.cjs')), native = require(path.join(root, 'scripts/native-pilot-lib.cjs'));
6
+ const read = file => JSON.parse(fs.readFileSync(file));
7
+ function fixture(t) {
8
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-generation-native-')); t.after(() => fs.rmSync(project, { recursive: true, force: true }));
9
+ const scope = { schemaVersion: 3, moduleIds: ['assets'], title: '资产', pages: [{ id: 'assets', title: '资产台账', referencePage: 'models-page', regions: [{ id: 'list', mode: 'content', pattern: 'tree-list' }] }], requirements: [{ id: 'A-01', text: '资产层级台账', pages: ['assets'], source: { type: 'prd', reference: 'fixture/A-01', statement: 'Synthetic source fixture only, not acceptance.' } }], generation: { presentation: { layout: 'mixed', style: 'soft', density: 'compact' } } };
10
+ const file = path.join(project, 'scope.json');fs.writeFileSync(file, JSON.stringify(scope));
11
+ return { project, scope, file, create: () => native.createPrototype({ project, id: 'assets', scope: file, expertMode: 'template' }) };
12
+ }
13
+ test('schema v3 creation freezes pattern contracts, customer choices and per-region acceptance', t => {
14
+ const { create, project } = fixture(t), result = create(), inputs = path.join(result.directory, 'inputs');
15
+ assert.equal(native.inspect(result.directory).status, 'passed');
16
+ const fixed = read(path.join(inputs, 'generation.json')), config = read(path.join(result.directory, 'app/public/prototype-config.json'));
17
+ assert.deepEqual(config.presentation, { layout: 'mixed', style: 'soft', density: 'compact' });
18
+ assert.equal(fixed.pages[0].regions[0].pattern, 'tree-list');
19
+ assert.ok(read(path.join(inputs, 'acceptance.json')).requiredScenarios.some(id => id.includes('/hierarchy/')));
20
+ assert.deepEqual(read(path.join(project, 'prototypes/index.json')).prototypes[0].appliesTo, ['assets']);
21
+ assert.equal(read(path.join(result.directory, 'prototype.json')).business.status, 'needs-adaptation');
22
+ assert.ok(fs.existsSync(path.join(result.directory, 'app/src/PrototypeShell.vue')));
23
+ });
24
+ test('generation constraints cannot be removed together with their acceptance IDs', t => {
25
+ const { create } = fixture(t), dir = create().directory, file = path.join(dir, 'inputs/generation.json');
26
+ const fixed = read(file);fixed.bindings = fixed.bindings.filter(item => item.capabilityId !== 'hierarchy');fs.writeFileSync(file, JSON.stringify(fixed));
27
+ const candidate = native.manifest(dir);
28
+ assert.throws(() => native.revise(dir, { revision: 'r002', expectedRevision: candidate.revision, expectedDigest: candidate.digest, acceptInputs: true }), /generation|contract/i);
29
+ });
30
+ test('unsupported and planned native templates are rejected rather than falling back to enterprise', t => {
31
+ fixture(t);
32
+ assert.throws(() => templates.loadBundledTemplate('ai-workbench'), /available|Unsupported|planned/);
33
+ assert.throws(() => templates.loadBundledTemplate('../outside'), /identifier|template/i);
34
+ });
35
+ test('legacy sourced scopes keep the original input contract and no new presentation is forced', t => {
36
+ const { scope, file, create } = fixture(t); scope.schemaVersion = 2; delete scope.generation; delete scope.pages[0].regions[0].pattern;
37
+ fs.writeFileSync(file, JSON.stringify(scope)); const dir = create().directory;
38
+ assert.equal(fs.existsSync(path.join(dir, 'inputs/generation.json')), false);
39
+ assert.equal(read(path.join(dir, 'app/public/prototype-config.json')).presentation, undefined);
40
+ assert.equal(native.inspect(dir).status, 'passed');
41
+ });
42
+ test('another registered content template uses its own structure and defaults without a core ID branch', t => {
43
+ const { project, scope, file } = fixture(t), pack = path.join(project, 'template-pack'), directory = path.join(pack, 'alternate');fs.mkdirSync(directory, { recursive: true });
44
+ fs.writeFileSync(path.join(pack, 'catalog.json'), JSON.stringify({ defaultTemplate: 'alternate', templates: [{ id: 'alternate', availability: 'available', contentRoot: 'alternate' }] }));
45
+ fs.writeFileSync(path.join(directory, 'template.json'), JSON.stringify({ id: 'alternate', version: '1.0.0', kind: 'content-reference', entry: 'index.html', definition: 'capability-definition.json' }));
46
+ fs.copyFileSync(path.join(root, 'assets/capabilities/management.json'), path.join(directory, 'capability-definition.json'));
47
+ fs.writeFileSync(path.join(directory, 'index.html'), '<script id="prototype-offline-routes">[{"path":"/items","component":"./Items.js"}]</script>\n<template data-source="reference/Items.html">\n<main><h1>另一套内容</h1><template data-component="Button" data-on-click="view">查看</template></main>\n</template>\n');
48
+ fs.writeFileSync(path.join(directory, 'component-inventory.json'), JSON.stringify({ schemaVersion: 1, components: { Button: { kind: 'framework', capabilities: ['interaction'] } } }));
49
+ const meta = read(path.join(root, 'assets/templates/enterprise-admin/generation.json'));meta.templateId = 'alternate';meta.shell = null;meta.defaults = { layout: 'topbar', style: 'minimal', density: 'comfortable' };meta.patterns = ['content'];
50
+ fs.writeFileSync(path.join(directory, 'generation.json'), JSON.stringify(meta));templates.syncTemplate(directory);
51
+ assert.equal(templates.loadBundledTemplate('alternate', { templatesRoot: pack }).id, 'alternate');
52
+ scope.pages[0].referencePage = 'items';scope.pages[0].regions[0].pattern = 'content';delete scope.generation.presentation;fs.writeFileSync(file, JSON.stringify(scope));
53
+ const result = native.createPrototype({ project, id: 'alternate-result', scope: file, templateRoot: directory, expertMode: 'template' });
54
+ assert.equal(read(path.join(result.directory, 'prototype.json')).template.id, 'alternate');
55
+ assert.equal(read(path.join(result.directory, 'inputs/generation.json')).presentation.layout, 'topbar');
56
+ assert.equal(native.inspect(result.directory).status, 'passed');
57
+ });
58
+ test('marking a scaffold adapted cannot bypass the new implementation acceptance gate',t=>{
59
+ const {create}=fixture(t),dir=create().directory,file=path.join(dir,'prototype.json'),meta=read(file);meta.business.status='adapted';fs.writeFileSync(file,JSON.stringify(meta));
60
+ const checked=native.inspect(dir,{acceptance:true});assert.equal(checked.status,'failed');assert.match(checked.errors.join(' '),/Implementation pending/);
61
+ fs.unlinkSync(path.join(dir,'design-system/implementation.json'));assert.equal(native.inspect(dir).status,'failed');
62
+ });
63
+ test('replan refreshes the complete fixed contract atomically and refuses invalid scope or stale identity',t=>{
64
+ const {create,scope,file}=fixture(t),dir=create().directory,before=native.manifest(dir);assert.equal(typeof native.replan,'function');
65
+ scope.generation.presentation.layout='topbar';fs.writeFileSync(file,JSON.stringify(scope));
66
+ const next=native.replan(dir,{scope:file,revision:'r002',expectedRevision:before.revision,expectedDigest:before.digest});assert.equal(next.revision,'r002');
67
+ assert.equal(read(path.join(dir,'app/public/prototype-config.json')).presentation.layout,'topbar');assert.ok(fs.readFileSync(path.join(dir,'design-system/MASTER.md'),'utf8').includes('布局 topbar'));assert.equal(native.inspect(dir).status,'passed');
68
+ const stable=native.manifest(dir);scope.generation.presentation.layout='bad-layout';fs.writeFileSync(file,JSON.stringify(scope));
69
+ assert.throws(()=>native.replan(dir,{scope:file,revision:'r003',expectedRevision:stable.revision,expectedDigest:stable.digest}),/choice/);assert.equal(native.manifest(dir).digest,stable.digest);
70
+ assert.throws(()=>native.replan(dir,{scope:file,revision:'r003',expectedRevision:before.revision,expectedDigest:before.digest}),/Stale|changed/);
71
+ });
72
+ test('replan restores fixed bytes if revision admission fails after input writes',t=>{
73
+ const {create,scope,file}=fixture(t),dir=create().directory,before=native.manifest(dir);scope.generation.presentation.layout='topbar';fs.writeFileSync(file,JSON.stringify(scope));fs.mkdirSync(path.join(dir,'baselines/r002'),{recursive:true});
74
+ assert.throws(()=>native.replan(dir,{scope:file,revision:'r002',expectedRevision:before.revision,expectedDigest:before.digest}),/frozen/);assert.deepEqual(native.manifest(dir),before);assert.equal(native.inspect(dir).status,'passed');
75
+ });
76
+ test('v3 state keeps recovery compact while exposing pending decisions and exact commands',t=>{
77
+ const {create}=fixture(t),dir=create().directory,state=native.state(dir,{});assert.equal(state.nextAction,'resolve-generation-decisions');assert.ok(Buffer.byteLength(JSON.stringify(state))<25000);assert.ok(state.commands.replan);assert.ok(state.generation.referenceReview.pendingCount>0);assert.equal(state.scopes.sourceFile,'inputs/scope.json');
78
+ });