@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
@@ -4,6 +4,7 @@ const {fs,path,crypto,assert,hash,json,id,inside,walk,write,entries,excluded}=F;
4
4
  const skillRoot=path.resolve(__dirname,'..');
5
5
  const frameworkRoot=path.join(skillRoot,'assets/frameworks/vue-antdv-next');
6
6
  const templates=()=>require('./template-contract.cjs');
7
+ const projectRegistry=require('./project-registry.cjs');
7
8
  function metadata(directory) { const data=json(inside(directory,'prototype.json')); assert.equal(data.schemaVersion,2,'Not a native v2 prototype'); assert.equal(data.runtime.kind,'vue-antdv-next'); return data; }
8
9
  function effectiveReview(data,current) {
9
10
  const review=data.review;
@@ -32,6 +33,9 @@ function defaultScope() {
32
33
  ],exemptions:[],openQuestions:[]};
33
34
  }
34
35
  function createPrototype(options) {
36
+ const installedFile=path.resolve(__dirname,'../../../.i3-composer-agent.json');
37
+ if(fs.existsSync(installedFile))options=require('./execution-policy.cjs').creationOptions(options,json(installedFile));
38
+ if(options.executionPolicy!==undefined){assert.equal(options.executionPolicy,require('./execution-policy.cjs').ID,'Unsupported execution policy');assert.equal(options.host,'codebuddy','Explicit CodeBuddy host required for A+B');}
35
39
  assert.ok(options.framework===undefined||options.framework==='vue-antdv-next','Unsupported framework / 不支持的框架');
36
40
  assert.ok(options.title===undefined||(typeof options.title==='string'&&options.title.trim()),'Title must be a nonempty string');
37
41
  const project=path.resolve(options.project||'.'); assert.ok(fs.statSync(project).isDirectory(),'Project must exist'); id(options.id);
@@ -41,24 +45,29 @@ function createPrototype(options) {
41
45
  provider=require('./prototype-lib.cjs').checkDesignProvider({...options,project});
42
46
  if(provider.availability!=='available') return {status:'needs-design-provider',provider,sideEffects:[],choices:['locate-or-repair','explicit-template-mode','analyze']};
43
47
  }
44
- const template=options.templateRoot?templates().loadTemplate(path.resolve(options.templateRoot)):templates().loadBundledTemplate();
48
+ assert.ok(!(options.templateRoot && options.template), 'Choose a template ID or an explicit template root');
49
+ const template=options.templateRoot?templates().loadTemplate(path.resolve(options.templateRoot)):templates().loadBundledTemplate(options.template);
45
50
  const reference=template.kind==='content-reference';
46
51
  assert.ok(!reference||options.scope,'A sourced scope file is required for business generation; no default device demo');
47
52
  const scope=options.scope?json(path.resolve(options.scope)):defaultScope();
53
+ const appliesTo=projectRegistry.moduleIds(scope);
48
54
  const selection=reference?require('./content-template.cjs').scopeInputs(template,scope,templates()):null;
55
+ const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(template,scope):null;
49
56
  const content=reference?{pageTitle:scope.title||'业务原型'}:templates().projectAdminListContent(template);
50
57
  if(options.title!==undefined)content.pageTitle=options.title;
51
58
  const profile=json(path.join(frameworkRoot,'profile.json'));
52
59
  const target=inside(project,`prototypes/${options.id}`); assert.ok(!fs.existsSync(target),'Prototype exists; revise without reinitializing');
53
- const lockFile=inside(project,'.i3-ui-prototypes.lock'); const lock=fs.openSync(lockFile,'wx');
60
+ return projectRegistry.transaction(project,store=>{
54
61
  let stage=null,published=false;
55
62
  try {
56
63
  fs.mkdirSync(path.dirname(target),{recursive:true}); stage=fs.mkdtempSync(path.join(path.dirname(target),'.native-'));
57
- const registryPath=inside(project,'.i3-ui-prototypes.json'); const registry=fs.existsSync(registryPath)?json(registryPath):{schemaVersion:1,prototypes:[]};
64
+ const registry=store.registry;
58
65
  assert.equal(registry.schemaVersion,1); assert.ok(Array.isArray(registry.prototypes)); assert.ok(!registry.prototypes.some(p=>p.id===options.id),'Duplicate prototype ID');
59
66
  const frameworkFiles=entries(frameworkRoot,walk(frameworkRoot,'',excluded));
60
67
  const instanceId=crypto.randomUUID();
61
68
  const data={schemaVersion:2,id:options.id,title:options.title||content.pageTitle||'设备清单原型',revision:'r001',artifactKind:'framework-prototype',runtime:{kind:'vue-antdv-next',version:'1',...(reference?{contentReference:true}:{})},...(reference?{business:{status:'needs-adaptation',source:'inputs/scope.json'}}:{}),template:{id:template.id,version:template.version,digest:template.digest},framework:{id:'vue-antdv-next',version:profile.version||'1',digest:hash(JSON.stringify(frameworkFiles))},expertMode:mode,expert:{status:provider?'pending':'skipped',provider},verification:{status:'not-run'},review:{status:'pending-review'},instanceId};
69
+ if(generation)data.runtime.generationContract=1;
70
+ if(options.executionPolicy){data.runtime.executionPolicy=options.executionPolicy;write(stage,'inputs/execution-policy.json',require('./execution-policy.cjs').create({host:options.host,mode,officialCallable:!options.officialSourceFallback}));}
62
71
  write(stage,'prototype.json',data);
63
72
  for(const file of template.files) {
64
73
  assert.equal(file.encoding,'base64');
@@ -66,28 +75,35 @@ function createPrototype(options) {
66
75
  }
67
76
  write(stage,'inputs/template-snapshot.json',template);
68
77
  write(stage,'inputs/scope.json',scope);
78
+ if(generation)write(stage,'inputs/generation.json',generation);
69
79
  write(stage,'inputs/capabilities.json',selection?selection.fixed:{schemaVersion:1,definition:template.definition,bindings:template.index.bindings,regions:template.index.regions,decisions:[]});
70
- write(stage,'inputs/framework.json',{profile,componentMap:json(path.join(frameworkRoot,'component-map.json')),acceptance:json(path.join(frameworkRoot,'acceptance.json')),files:frameworkFiles});
80
+ write(stage,'inputs/framework.json',{profile,componentMap:generation?require('./generation-evidence.cjs').framework():json(path.join(frameworkRoot,'component-map.json')),acceptance:json(path.join(frameworkRoot,'acceptance.json')),files:frameworkFiles});
71
81
  const checked=templates().checkInstanceCapabilities(path.join(stage,'inputs')); assert.equal(checked.status,'passed',checked.errors?.join('; '));
72
- const requiredScenarios=checked.requiredScenarios.map(s=>typeof s==='string'?s:s.id);
73
- write(stage,'inputs/acceptance.json',{schemaVersion:1,requiredScenarios,scenarioDetails:checked.requiredScenarios,frameworkScenarios:reference?['framework-responsive','framework-instance-isolation','template-content-projection',...(selection.fixed.regions.some(region=>region.mode==='edit-form')?['framework-drawer-motion','framework-mask-blur','framework-focus-scroll']:[])]:['framework-drawer-motion','framework-mask-blur','framework-focus-scroll','framework-states','framework-responsive','framework-instance-isolation','template-content-projection'],browserReview:json(path.join(frameworkRoot,'acceptance.json')).browserReview});
82
+ const scenarioDetails=[...checked.requiredScenarios,...(generation?.scenarios||[])],requiredScenarios=scenarioDetails.map(s=>typeof s==='string'?s:s.id);
83
+ const hasDrawer=reference&&(selection.fixed.regions.some(region=>region.mode==='edit-form')||generation?.bindings.some(binding=>binding.capabilityId==='dialog'&&binding.parameters.presentation==='drawer'));
84
+ write(stage,'inputs/acceptance.json',{schemaVersion:1,requiredScenarios,scenarioDetails,frameworkScenarios:reference?['framework-responsive','framework-instance-isolation','template-content-projection',...(hasDrawer?['framework-drawer-motion','framework-mask-blur','framework-focus-scroll']:[])]:['framework-drawer-motion','framework-mask-blur','framework-focus-scroll','framework-states','framework-responsive','framework-instance-isolation','template-content-projection'],browserReview:json(path.join(frameworkRoot,'acceptance.json')).browserReview});
85
+ if(options.executionPolicy){const all=json(inside(stage,'inputs/acceptance.json'));assert.ok(options.executionAssertions,`Strict policy requires --execution-assertions with fixed expected JSON facts for: ${[...all.requiredScenarios,...all.frameworkScenarios].join(', ')}`);const fixed=require('./execution-assertions.cjs').validate(json(path.resolve(options.executionAssertions)),[...all.requiredScenarios,...all.frameworkScenarios]);write(stage,'inputs/execution-assertions.json',fixed);}
74
86
  const requirements=json(path.join(stage,'inputs/scope.json')).requirements;
75
- write(stage,'requirements-map.json',{schemaVersion:2,requirements:requirements.map(r=>({...r,pages:reference?r.pages:template.index.pages.map(page=>page.id),scenarios:reference?checked.requiredScenarios.filter(s=>s.bindingId.startsWith('app-shell/')||r.pages.some(page=>s.bindingId.startsWith(page+'/'))).map(s=>s.id):requiredScenarios})),bindings:selection?selection.fixed.bindings:template.index.bindings});
87
+ write(stage,'requirements-map.json',{schemaVersion:2,requirements:requirements.map(r=>({...r,pages:reference?r.pages:template.index.pages.map(page=>page.id),scenarios:reference?scenarioDetails.filter(s=>s.bindingId.startsWith('app-shell/')||r.pages.some(page=>s.bindingId.startsWith(page+'/'))).map(s=>s.id):requiredScenarios})),bindings:[...(selection?selection.fixed.bindings:template.index.bindings),...(generation?.bindings||[])]});
76
88
  const starter=path.join(frameworkRoot,reference?profile.starter:(profile.legacyStarter||profile.starter));
77
89
  fs.cpSync(starter,path.join(stage,'app'),{recursive:true,filter:file=>!excluded(`starter/${path.relative(starter,file).split(path.sep).join('/')}`,path.basename(file))});
78
90
  const themeBinding=(selection?selection.fixed.bindings:template.index.bindings).find(binding=>binding.capabilityId==='theme');
79
- write(stage,'app/public/prototype-config.json',{instanceId,storageNamespace:`i3-native-${options.id}-${instanceId}`,mockVersion:reference?'business-v1':'admin-list-v1',title:data.title,...(reference?{pages:scope.pages.map(page=>({id:page.id,title:page.title}))}:{content}),...(themeBinding?{theme:templates().resolveThemeParameters(themeBinding.parameters)}:{})});
91
+ write(stage,'app/public/prototype-config.json',{instanceId,storageNamespace:`i3-native-${options.id}-${instanceId}`,mockVersion:reference?'business-v1':'admin-list-v1',title:data.title,...(reference?{pages:scope.pages.map(page=>({id:page.id,title:page.title}))}:{content}),...(generation?{presentation:generation.presentation,navigation:generation.navigation,generation:{pages:generation.pages}}:{}),...(themeBinding?{theme:templates().resolveThemeParameters(themeBinding.parameters)}:{})});
80
92
  write(stage,'mocks/contract.json',reference?{schemaVersion:1,instanceId,status:'needs-business-data',requirements:scope.requirements.map(r=>r.id)}:{schemaVersion:1,seed:'admin-list-v1',count:21,pageSize:10,selection:'current-page-clear-on-page-or-filter',instanceId});
81
93
  write(stage,'design-system/MASTER.md',`# ${data.title}\n\n使用 antDv-next 原生组件与主题。管理列表采用紧凑信息层级,正文与操作可读;错误在字段附近呈现。抽屉保留进入与退出过程,显式开启遮罩模糊。\n\n本地模拟,不代表生产权限。\n`);
94
+ if(generation)write(stage,'design-system/MASTER.md',require('./generation-evidence.cjs').designSummary(generation,`# ${data.title}\n\nPrototypeShell 提供已选原生外壳;按逐区域/字段/操作合同实现业务,补齐真实源码映射和浏览器证据。骨架不是业务完成。本地模拟不代表生产权限。`));
95
+ if(generation)write(stage,'design-system/implementation.json',require('./generation-evidence.cjs').initialize(generation));
82
96
  write(stage,'design-system/decisions.json',{schemaVersion:1,records:[],reviewStatus:mode==='template'?'skipped':'pending'});
83
97
  write(stage,'qa/design-advice.json',{schemaVersion:2,provider,queries:[],records:[],status:mode==='template'?'skipped':'pending'});
84
98
  write(stage,'HANDOFF.md',`# ${data.title}\n\n本次为 ${mode} 模式;来源 ${template.id} ${template.version}。运行形态为真实 Vue+antDv-next。读取 inputs 中固定合同和实际源码,勿从安装目录更新旧合同。\n\n使用 native-pilot.cjs inspect/build/preview。当前未验证,待用户评审;不要把检查通过当用户认可。\n`);
85
99
  const candidate=manifest(stage); data.inputs={digest:candidate.inputDigest}; write(stage,'prototype.json',data);
86
100
  fs.renameSync(stage,target); published=true;
87
- registry.prototypes.push({id:options.id,directory:`prototypes/${options.id}`,appliesTo:[],schemaVersion:2}); write(project,'.i3-ui-prototypes.json',registry);
88
- return {status:'created',directory:target,revision:'r001',digest:manifest(target).digest};
101
+ const digest=manifest(target).digest;
102
+ registry.prototypes.push({id:options.id,directory:`prototypes/${options.id}`,appliesTo,schemaVersion:2}); store.save();
103
+ return {status:'created',directory:target,revision:'r001',digest};
89
104
  } catch(error) { if(published) fs.rmSync(target,{recursive:true,force:true}); throw error; }
90
- finally { if(stage&&fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true}); fs.closeSync(lock); fs.unlinkSync(lockFile); }
105
+ finally { if(stage&&fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true}); }
106
+ });
91
107
  }
92
108
  function inspectDesignReview(directory,current=manifest(directory)) {
93
109
  const data=metadata(directory);
@@ -112,12 +128,16 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
112
128
  assert.equal(acceptedInputDigest??data.inputs.digest,current.inputDigest,'Input contract changed; explicitly revise fixed inputs');
113
129
  const capability=templates().checkInstanceCapabilities(path.join(directory,'inputs')); assert.equal(capability.status,'passed',capability.errors?.join('; '));
114
130
  const acceptance=json(inside(directory,'inputs/acceptance.json'));
131
+ require('./execution-runtime.cjs').policy(directory);
115
132
  require('./native-devtools.cjs').readPolicy(directory);
116
133
  for(const scenario of capability.requiredScenarios.map(s=>typeof s==='string'?s:s.id)) assert.ok(acceptance.requiredScenarios.includes(scenario),`Required scenario omitted: ${scenario}`);
117
134
  const fixedFramework=json(inside(directory,'inputs/framework.json'));
118
135
  if(fixedFramework.acceptance?.scenarios?.some(scenario=>scenario.id==='template-content-projection'&&scenario.required))assert.ok(acceptance.frameworkScenarios.includes('template-content-projection'),'Required template-content-projection scenario omitted from fixed framework acceptance');
119
136
  const template=json(inside(directory,'inputs/template-snapshot.json')),mapping=json(inside(directory,'requirements-map.json')),scope=json(inside(directory,'inputs/scope.json'));
120
- const pageIds=new Set(data.runtime.contentReference?['app-shell',...scope.pages.map(page=>page.id)]:template.index.pages.map(page=>page.id)),bindingIds=new Set(json(inside(directory,'inputs/capabilities.json')).bindings.map(binding=>binding.id));
137
+ const generation=scope.schemaVersion===3?json(inside(directory,'inputs/generation.json')):null;
138
+ if(generation){assert.equal(data.runtime.generationContract,1,'Generation runtime identity is required');require('./generation-contract.cjs').verify(template,scope,generation);for(const scenario of generation.scenarios)assert.ok(acceptance.requiredScenarios.includes(scenario.id),`Generation scenario omitted: ${scenario.id}`);}
139
+ projectRegistry.moduleIds(scope);
140
+ const pageIds=new Set(data.runtime.contentReference?['app-shell',...scope.pages.map(page=>page.id)]:template.index.pages.map(page=>page.id)),bindingIds=new Set([...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])].map(binding=>binding.id));
121
141
  for(const requirement of scope.requirements)assert.ok(mapping.requirements.some(item=>item.id===requirement.id),`Requirement omitted from mapping: ${requirement.id}`);
122
142
  for(const requirement of mapping.requirements) {
123
143
  assert.ok(scope.requirements.some(item=>item.id===requirement.id),`Unknown mapped requirement: ${requirement.id}`);
@@ -131,14 +151,17 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
131
151
  if(data.runtime.contentReference) {
132
152
  assert.deepEqual(config.pages,scope.pages.map(page=>({id:page.id,title:page.title})),'Business configuration differs from fixed page scope');
133
153
  assert.ok(['needs-adaptation','adapted'].includes(data.business?.status),'Unknown business implementation status');
134
- const fixedBindings=json(inside(directory,'inputs/capabilities.json')).bindings;
154
+ const fixedBindings=[...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])];
135
155
  assert.deepEqual((mapping.bindings||[]).map(binding=>binding.id).sort(),fixedBindings.map(binding=>binding.id).sort(),'Business mapping omits fixed capabilities');
136
156
  }
137
157
  const themeBinding=json(inside(directory,'inputs/capabilities.json')).bindings.find(binding=>data.runtime.contentReference?binding.capabilityId==='theme':binding.id==='devices/device-list/theme');
138
158
  if(themeBinding)assert.deepEqual(config.theme,templates().resolveThemeParameters(themeBinding.parameters),'Theme configuration differs from fixed input binding');
139
159
  else assert.equal(config.theme,undefined,'Theme requires an explicit optional capability binding');
160
+ if(generation){assert.deepEqual(config.presentation,generation.presentation,'Customer presentation differs from fixed choices');assert.deepEqual(config.navigation,generation.navigation,'Navigation differs from fixed generation');assert.deepEqual(config.generation,{pages:generation.pages},'Business page patterns differ from fixed generation');}
140
161
  const deps=json(inside(directory,'app/package.json')).dependencies; assert.ok(deps['antdv-next'] && deps.vue,'Native framework dependencies missing');
162
+ if(generation)require('./generation-evidence.cjs').validateImplementation(directory,generation,json(inside(directory,'design-system/implementation.json')),{complete:!!options.acceptance,frameworkMap:fixedFramework.componentMap});
141
163
  if(options.acceptance) {
164
+ if(generation){assert.equal(generation.pendingDecisions.length,0,'Reference applicability decisions remain unresolved');assert.equal(generation.pendingReferenceElements.length,0,'Reference functional elements remain unaccounted for');}
142
165
  if(data.runtime.contentReference)assert.equal(data.business?.status,'adapted','Business implementation pending; a scaffold cannot pass acceptance');
143
166
  const adoptedAdvice=inspectDesignReview(directory,current).adoptedAdvice;
144
167
  const report=options.report||json(inside(directory,json(inside(directory,'qa/latest.json')).report));
@@ -149,7 +172,9 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
149
172
  assert.deepEqual(report.consoleErrors,[],'Browser errors remain');
150
173
  const required=[...acceptance.requiredScenarios,...acceptance.frameworkScenarios];
151
174
  for(const scenario of required) assert.ok(report.journeys.some(j=>j.id===scenario && j.status==='passed'),`Missing browser scenario: ${scenario}`);
175
+ if(generation)require('./generation-evidence.cjs').validateObservations(directory,generation,report);
152
176
  for(const record of adoptedAdvice)assert.ok(report.journeys.some(j=>j.id===record.verificationScenario && j.status==='passed'),`Advice verification missing: ${record.resultIdentity}`);
177
+ require('./execution-runtime.cjs').requireStage(directory,'delivery',module.exports,{businessReport:report});
153
178
  }
154
179
  return {status:'passed',level:options.acceptance?'acceptance':'structure',revision:current.revision,digest:current.digest,verification:data.verification.status,review:effectiveReview(data,current).status};
155
180
  } catch(error) { return {status:'failed',errors:[error.message]}; }
@@ -186,12 +211,35 @@ function revise(directory,options) {
186
211
  assert.equal(checked.status,'passed',checked.errors?.join('; '));
187
212
  } else assert.equal(data.inputs.digest,current.inputDigest,'Input contract changed; revise --accept-inputs must explicitly validate the new contract');
188
213
  assert.equal(manifest(directory).digest,current.digest,'Candidate changed while validating fixed inputs');
189
- if(options.acceptInputs)data.inputs={digest:current.inputDigest};
190
- data.revision=options.revision; data.verification={status:'not-run'}; data.review={status:'pending-review'};
191
- write(directory,'prototype.json',data); return manifest(directory);
214
+ const scope=json(inside(directory,'inputs/scope.json')); projectRegistry.moduleIds(scope);
215
+ return projectRegistry.withInstanceAssociation(directory,scope,()=>{
216
+ if(options.acceptInputs)data.inputs={digest:current.inputDigest};
217
+ data.revision=options.revision; data.verification={status:'not-run'}; data.review={status:'pending-review'};
218
+ write(directory,'prototype.json',data); return manifest(directory);
219
+ });
220
+ });
221
+ }
222
+ function replan(directory,options) { const owner=options.owner||`replan-${crypto.randomUUID()}`;return mutate(directory,{...options,owner},()=>require('./generation-replan.cjs').run(directory,{...options,owner},module.exports)); }
223
+ function build(directory,options={}) { const gates=require('./execution-runtime.cjs');gates.requireStage(directory,'implementation',module.exports);gates.requireStage(directory,'design',module.exports);return require('./native-build.cjs').build(directory,options,{manifest,inspect}); }
224
+ function synchronizeIndex(options={}) {
225
+ id(options.id);
226
+ const index=projectRegistry.read(options.project),entry=index.registry.prototypes.find(item=>item.id===options.id);
227
+ assert.ok(entry,'Prototype is not registered');
228
+ const directory=inside(index.project,entry.directory),scope=json(inside(directory,'inputs/scope.json'));
229
+ assert.ok(scope.moduleIds!==undefined||scope.schemaVersion===2,'Legacy scope has no explicit moduleIds; do not invent or rewrite old input contracts');
230
+ const current=manifest(directory);
231
+ return mutate(directory,{...options,expectedRevision:current.revision,expectedDigest:current.digest},()=>{
232
+ const checked=inspect(directory);assert.equal(checked.status,'passed',checked.errors?.join('; '));
233
+ return projectRegistry.transaction(index.project,store=>{
234
+ const selected=store.registry.prototypes.find(item=>item.id===options.id);
235
+ assert.ok(selected&&selected.directory===entry.directory,'Prototype index entry changed during synchronization');
236
+ const fixed=json(inside(directory,'inputs/scope.json')),appliesTo=projectRegistry.moduleIds(fixed);
237
+ assert.equal(manifest(directory).digest,current.digest,'Candidate changed during synchronization');
238
+ selected.appliesTo=appliesTo;store.save();
239
+ return {status:'synchronized',index:store.file,id:selected.id,appliesTo};
240
+ });
192
241
  });
193
242
  }
194
- function build(directory,options={}) { return require('./native-build.cjs').build(directory,options,{manifest,inspect}); }
195
243
  function verifyBuild(directory,buildId) { return require('./native-build.cjs').verifyBuild(directory,buildId,{manifest}); }
196
244
  function preview(directory,options={}) { return require('./native-build.cjs').preview(directory,options,{manifest}); }
197
245
  function recordRun(directory,report) {
@@ -241,4 +289,4 @@ function compareBaseline(directory) {
241
289
  }
242
290
  function state(directory,options={}) { return require('./native-delivery.cjs').state(directory,options,module.exports); }
243
291
  function finalize(directory,options={}) { return require('./native-delivery.cjs').finalize(directory,options,module.exports); }
244
- module.exports={analyze,prepareTemplate,createPrototype,manifest,inspect,inspectDesignReview,effectiveReview,claim,release,revise,build,verifyBuild,preview,recordRun,confirmBaseline,compareBaseline,state,finalize};
292
+ module.exports={analyze,prepareTemplate,createPrototype,manifest,inspect,inspectDesignReview,effectiveReview,claim,release,revise,replan,synchronizeIndex,build,verifyBuild,preview,recordRun,confirmBaseline,compareBaseline,state,finalize};
@@ -5,14 +5,29 @@ const fs=require('node:fs');
5
5
  const lib=require('./native-pilot-lib.cjs');
6
6
  async function main(argv=process.argv.slice(2)) {
7
7
  const operation=argv.shift()||'analyze'; const positional=[]; const options={};
8
- while(argv.length) { const value=argv.shift(); if(value.startsWith('--')) { const key=value.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); options[key]=!argv.length||argv[0].startsWith('--')?true:argv.shift(); } else positional.push(value); }
8
+ while(argv.length) { const value=argv.shift(); if(value==='--') { options.command=argv.splice(0); break; } if(value.startsWith('--')) { const key=value.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); options[key]=!argv.length||argv[0].startsWith('--')?true:argv.shift(); } else positional.push(value); }
9
9
  const root=positional[0]?path.resolve(positional[0]):null;
10
10
  switch(operation) {
11
11
  case 'analyze': return lib.analyze(options);
12
+ case 'paths': { const context=require('./prototype-locations.cjs').resolveLocations(options); return {...require('./project-registry.cjs').paths(options.project,options.id),host:context.host,scope:context.scope,projectKey:context.projectKey,toolsDirectory:context.toolsDirectory,cacheDirectory:context.cacheDirectory}; }
13
+ case 'index': return require('./project-registry.cjs').query(options);
14
+ case 'sync-index': return lib.synchronizeIndex(options);
15
+ case 'migrate-index': return require('./project-registry.cjs').migrate(options);
16
+ case 'browser-cache': return require('./browser-workspace.cjs').prepare(options);
17
+ case 'browser-cli': return require('./browser-workspace.cjs').runCli(options);
18
+ case 'archive-browser-output': return require('./browser-workspace.cjs').archive(options);
12
19
  case 'prepare-template': return lib.prepareTemplate(options);
13
20
  case 'sync-template': { const templates=require('./template-contract.cjs'); return options.check?templates.loadTemplate(root).index:templates.syncTemplate(root); }
14
- case 'template-page': { const templates=require('./template-contract.cjs'); return require('./content-template.cjs').pageContent(root?templates.loadTemplate(root):templates.loadBundledTemplate(),options.page); }
21
+ case 'template-page': { const templates=require('./template-contract.cjs'); return require('./content-template.cjs').pageContent(root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),options.page); }
22
+ case 'generation-options': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),meta=require('./generation-contract.cjs').resource(snapshot,'generation.json');return {status:'passed',readOnly:true,template:snapshot.id,...meta,patterns:require('./generation-contract.cjs').definition().patterns}; }
23
+ case 'generation-plan': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template);const contract=require('./generation-contract.cjs');return contract.view(contract.compile(snapshot,JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'))),options); }
24
+ case 'inventory': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),value=require('./template-inventory.cjs').verifySnapshot(snapshot);return options.details?value:{status:'passed',readOnly:true,template:snapshot.id,files:Object.keys(value.files).length,routes:value.routes.length,pages:value.pages.length,fragments:value.fragments.length,components:Object.keys(value.components).length,events:value.eventCount}; }
25
+ case 'sync-generation-assets': return require('./generation-assets.cjs').refresh(path.resolve(__dirname,'..'),{check:!!options.check});
15
26
  case 'create': return lib.createPrototype(options);
27
+ case 'execution-begin': return require('./execution-runtime.cjs').begin(root,lib);
28
+ case 'execution-assertions-template': return require('./execution-assertions.cjs').template(options);
29
+ case 'execution-template': return require('./execution-runtime.cjs').template(root,lib);
30
+ case 'execution-check': return require('./execution-runtime.cjs').check(root,options.stage||'delivery',lib);
16
31
  case 'manifest': return lib.manifest(root);
17
32
  case 'inspect': return lib.inspect(root,{acceptance:!!options.acceptance});
18
33
  case 'state': return lib.state(root,options);
@@ -22,6 +37,7 @@ async function main(argv=process.argv.slice(2)) {
22
37
  case 'build': return lib.build(root,options);
23
38
  case 'preview': { const running=await lib.preview(root,options); process.stdout.write(JSON.stringify({url:running.url,buildId:running.build.buildId})+'\n'); return null; }
24
39
  case 'revise': return lib.revise(root,options);
40
+ case 'replan': return lib.replan(root,options);
25
41
  case 'claim': return lib.claim(root,options);
26
42
  case 'release': return lib.release(root,options.owner);
27
43
  case 'record-run': return lib.recordRun(root,JSON.parse(fs.readFileSync(path.resolve(options.report),'utf8')));
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+ const { fs, path, crypto, hash, assert, json, id, inside, write } = require('./native-files.cjs');
3
+ function paths(project, prototypeId) {
4
+ const root = fs.realpathSync(path.resolve(project || '.'));
5
+ assert.ok(fs.statSync(root).isDirectory(), 'Project must be a directory');
6
+ if (prototypeId !== undefined) id(prototypeId);
7
+ return { project: root, output: inside(root, 'prototypes'), index: inside(root, 'prototypes/index.json'),
8
+ legacyIndex: inside(root, '.i3-ui-prototypes.json'), lock: inside(root, 'prototypes/.index.lock'), legacyLock: inside(root, '.i3-ui-prototypes.lock'),
9
+ ...(prototypeId === undefined ? {} : { directory: inside(root, `prototypes/${prototypeId}`), preparation: inside(root, `prototypes/.preparation/${prototypeId}`), scopeFile: inside(root, `prototypes/.preparation/${prototypeId}/scope.json`) }) };
10
+ }
11
+ function moduleIds(scope = {}) {
12
+ if (scope.schemaVersion !== undefined) assert.ok([1, 2, 3].includes(scope.schemaVersion), 'Unsupported scope schemaVersion');
13
+ if (scope.moduleIds === undefined && ![2, 3].includes(scope.schemaVersion)) return [];
14
+ assert.ok(Array.isArray(scope.moduleIds), 'scope.moduleIds must be an array; use [] for unbound scope');
15
+ for (const value of scope.moduleIds) assert.ok(typeof value === 'string' && value.length <= 128 && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value), 'Invalid module identifier in scope.moduleIds; use stable PRD module IDs, not paths or requirement text');
16
+ return [...new Set(scope.moduleIds)];
17
+ }
18
+ function validate(registry, project) {
19
+ assert.equal(registry.schemaVersion, 1, 'Invalid project prototype registry version');
20
+ assert.ok(Array.isArray(registry.prototypes), 'Invalid project prototype registry');
21
+ const ids = new Set();
22
+ for (const entry of registry.prototypes) {
23
+ id(entry.id); assert.ok(!ids.has(entry.id), 'Duplicate prototype ID in registry'); ids.add(entry.id);
24
+ inside(project, entry.directory);
25
+ assert.ok(entry.appliesTo === undefined || (Array.isArray(entry.appliesTo) && entry.appliesTo.every(value => typeof value === 'string')), 'Invalid appliesTo array');
26
+ }
27
+ return registry;
28
+ }
29
+ function read(project) {
30
+ const locations = paths(project), modern = fs.existsSync(locations.index), legacy = fs.existsSync(locations.legacyIndex);
31
+ if (modern && legacy) assert.deepEqual(json(locations.index), json(locations.legacyIndex), 'Conflicting prototype indices; resolve or explicitly migrate before writing');
32
+ const file = modern || !legacy ? locations.index : locations.legacyIndex;
33
+ const bytes = fs.existsSync(file) ? fs.readFileSync(file) : null;
34
+ return { ...locations, file, legacy: !modern && legacy, duplicateLegacy: modern && legacy,
35
+ bytes, registry: validate(bytes ? JSON.parse(bytes.toString('utf8')) : { schemaVersion: 1, prototypes: [] }, locations.project) };
36
+ }
37
+ function transaction(project, operation, { allowDuplicate = false } = {}) {
38
+ const initial = read(project);
39
+ assert.ok(allowDuplicate || !initial.duplicateLegacy, 'Duplicate active indices; explicitly resolve before writing');
40
+ assert.ok(!fs.existsSync(initial.legacyLock), 'Legacy prototype writer lock is active');
41
+ const createdOutput = !fs.existsSync(initial.output), locks = [];
42
+ fs.mkdirSync(initial.output, { recursive: true });
43
+ const acquire = file => { const fd = fs.openSync(file, 'wx'); locks.push({ file, fd }); };
44
+ try {
45
+ acquire(initial.lock);
46
+ if (initial.legacy || initial.duplicateLegacy) acquire(initial.legacyLock);
47
+ else assert.ok(!fs.existsSync(initial.legacyLock), 'Legacy prototype writer lock is active');
48
+ const current = read(project);
49
+ assert.equal(current.file, initial.file, 'Prototype index location changed while acquiring lock');
50
+ assert.ok(allowDuplicate || !current.duplicateLegacy, 'Duplicate active indices');
51
+ return operation({ ...current, save() {
52
+ const latest = read(project);
53
+ assert.equal(latest.file, current.file, 'Prototype index location changed');
54
+ assert.deepEqual(latest.bytes, current.bytes, 'Prototype index changed outside the active writer');
55
+ validate(current.registry, current.project);
56
+ write(current.project, path.relative(current.project, current.file).split(path.sep).join('/'), current.registry);
57
+ } });
58
+ } finally {
59
+ for (const { file, fd } of locks.reverse()) { fs.closeSync(fd); fs.unlinkSync(file); }
60
+ if (createdOutput && fs.existsSync(initial.output) && !fs.readdirSync(initial.output).length) fs.rmdirSync(initial.output);
61
+ }
62
+ }
63
+ function query(options = {}) {
64
+ const current = read(options.project), prototypes = current.registry.prototypes;
65
+ if (options.module !== undefined) assert.ok(typeof options.module === 'string' && options.module.trim(), 'Provide a nonempty module identifier or existing legacy label');
66
+ const errors = [], invalid = new Set();
67
+ for (const entry of prototypes) {
68
+ try {
69
+ const scopeFile = inside(current.project, `${entry.directory}/inputs/scope.json`);
70
+ if (fs.existsSync(scopeFile)) {
71
+ const scope = json(scopeFile);
72
+ if (scope.schemaVersion === 2 || scope.moduleIds !== undefined) assert.deepEqual(entry.appliesTo || [], moduleIds(scope), 'appliesTo differs from fixed scope.moduleIds; explicitly revise or synchronize the index');
73
+ }
74
+ } catch (error) { invalid.add(entry.id); errors.push(`${entry.id}: ${error.message}`); }
75
+ }
76
+ const matches = options.module === undefined ? [] : prototypes.filter(entry => !invalid.has(entry.id) && entry.appliesTo?.includes(options.module));
77
+ return { status: errors.length ? 'failed' : 'checked', index: current.file, legacy: current.legacy, prototypes, errors,
78
+ matches, unbound: prototypes.filter(entry => !invalid.has(entry.id) && !entry.appliesTo?.length), selection: matches.length > 1 ? 'ambiguous' : matches.length ? 'matched' : 'none', readOnly: true,
79
+ note: 'Module association does not prove implementation, current verification or human approval.' };
80
+ }
81
+ function withInstanceAssociation(directory, scope, operation) {
82
+ const parent = path.dirname(path.resolve(directory));
83
+ if (path.basename(parent) !== 'prototypes' || (scope.schemaVersion !== 2 && scope.moduleIds === undefined)) return operation();
84
+ const project = path.dirname(parent), current = read(project);
85
+ if (!current.registry.prototypes.some(entry => path.resolve(project, entry.directory) === path.resolve(directory))) return operation();
86
+ const appliesTo = moduleIds(scope), metadataFile = inside(directory, 'prototype.json'), before = fs.readFileSync(metadataFile);
87
+ return transaction(project, store => {
88
+ const entry = store.registry.prototypes.find(item => path.resolve(project, item.directory) === path.resolve(directory));
89
+ assert.ok(entry, 'Prototype entry disappeared during revision');
90
+ try { const result = operation(); entry.appliesTo = appliesTo; store.save(); return result; }
91
+ catch (error) { write(directory, 'prototype.json', before.toString('utf8')); throw error; }
92
+ });
93
+ }
94
+ function migrate(options = {}) {
95
+ assert.ok(options.check === undefined || typeof options.check === 'boolean', '--check must be a boolean flag');
96
+ const initial = read(options.project);
97
+ if (!fs.existsSync(initial.legacyIndex)) return { status: 'current', index: initial.index, sideEffects: [] };
98
+ const original = fs.readFileSync(initial.legacyIndex), digest = hash(original);
99
+ if (options.expectedDigest !== undefined) assert.equal(options.expectedDigest, digest, 'Legacy index changed since preview');
100
+ const plan = { source: initial.legacyIndex, destination: initial.index, sourceDigest: digest, prototypes: initial.registry.prototypes.length };
101
+ if (options.check) return { status: 'migration-preview', ...plan, sideEffects: [] };
102
+ return transaction(options.project, store => {
103
+ assert.deepEqual(fs.readFileSync(store.legacyIndex), original, 'Legacy index changed during migration');
104
+ const folder = inside(store.project, `prototypes/.migration/index-${crypto.randomUUID()}`);
105
+ fs.mkdirSync(folder, { recursive: true });
106
+ const backup = inside(folder, 'legacy-index.json'), temp = inside(store.project, `prototypes/.index-${crypto.randomUUID()}.tmp`);
107
+ fs.writeFileSync(backup, original, { flag: 'wx' });
108
+ write(folder, 'source.json', { ...plan, backup, purpose: 'Source snapshot before explicit index migration' });
109
+ let created = false;
110
+ try {
111
+ if (!fs.existsSync(store.index)) {
112
+ fs.writeFileSync(temp, original, { flag: 'wx' });
113
+ try { fs.linkSync(temp, store.index); created = true; }
114
+ finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
115
+ } else assert.deepEqual(fs.readFileSync(store.index), original, 'Destination index already exists with different content');
116
+ assert.deepEqual(fs.readFileSync(backup), original, 'Migration backup mismatch');
117
+ assert.deepEqual(fs.readFileSync(store.legacyIndex), original, 'Legacy index changed before commit');
118
+ fs.unlinkSync(store.legacyIndex);
119
+ return { status: 'migrated', ...plan, backup };
120
+ } catch (error) {
121
+ if (created && fs.existsSync(store.index) && fs.readFileSync(store.index).equals(original)) fs.unlinkSync(store.index);
122
+ throw error;
123
+ }
124
+ }, { allowDuplicate: true });
125
+ }
126
+ module.exports = { paths, moduleIds, read, transaction, query, withInstanceAssociation, migrate };
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Explicit configuration lifecycle. check is read-only and never invokes the installer.
4
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
5
+ const R=require('./codebuddy-registration.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs');
6
+ function context(options) {
7
+ assert.equal(options.host||'codebuddy','codebuddy','A+B supports CodeBuddy only');
8
+ const project=fs.realpathSync(path.resolve(options.project||'.')),scope=options.scope;
9
+ assert.ok(['project','global'].includes(scope),'Specify project or global installation scope');
10
+ const base=scope==='project'?path.join(project,'.codebuddy'):C.globalBase(options);
11
+ const recordText=R.fileIO.read(path.join(base,'state/i3-prototype/installation.json'));
12
+ const record=recordText?JSON.parse(recordText):null;
13
+ const unified=path.join(base,'i3-composer-agent/hooks/i3-prototype.cjs');
14
+ const scriptPath=options.entryPath||(fs.existsSync(unified)?unified:path.join(base,'skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs'));
15
+ return {project,scope,base,record,scriptPath};
16
+ }
17
+ function prepare(options,runtime={}) {
18
+ const current=runtime.current||context(options),readText=runtime.readText||R.fileIO.read,action=options.action||'keep';
19
+ assert.ok(['keep','enable','disable'].includes(action),'Invalid Hook configuration action');
20
+ if(['keep','disable'].includes(action)&&!current.record){
21
+ if(action==='disable'){const settings=R.config(readText(path.join(current.base,'settings.json')));assert.ok(!Object.values(settings.hooks||{}).flat().some(g=>g.hooks.some(h=>h.command?.includes(R.OWNER))),'Unknown owned-looking Hook has no ownership record; preserve it for review');}
22
+ return {summary:{status:'not-enabled',scope:current.scope,changed:false},unchanged:true};
23
+ }
24
+ const product=options.product||current.record?.product;assert.ok(['ide','cli'].includes(product),'Use --codebuddy-product ide or cli when enabling Hooks');
25
+ if(action!=='disable') {
26
+ const manifest=readText(path.join(current.base,'.i3-composer-agent.json'));
27
+ if(manifest)assert.ok(!JSON.parse(manifest).agents?.includes('workbuddy'),'Shared CodeBuddy/WorkBuddy registration cannot safely identify the product; preserve settings and use an exclusive CodeBuddy scope');
28
+ assert.ok(!options.agents?.includes('workbuddy')||current.scope!=='project','Do not enable CodeBuddy audit in a shared WorkBuddy project target');
29
+ }
30
+ const explicit=options.transcriptRoot,roots=explicit==='auto'?[]:explicit?[path.resolve(explicit)]:current.record?.transcriptRoots||[];
31
+ const mode=explicit?(explicit==='auto'?'auto':'manual'):current.record?.transcriptMode||(roots.length?'manual':'auto');
32
+ if(action!=='disable') {
33
+ for(const root of roots){const relative=path.relative(current.project,root);assert.ok(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative),'Native log root must be outside the business project; QA exports are not native provenance');}
34
+ }
35
+ const settingsText=readText(path.join(current.base,'settings.json')),settings=R.config(settingsText);
36
+ if(action!=='disable')assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic auditing; preserve the existing policy');
37
+ return R.plan({action,...current,product,nodePath:process.execPath,scriptDigest:runtime.scriptDigest||C.bundle(options.entrySource||(path.basename(current.scriptPath)==='i3-prototype.cjs'?current.scriptPath:path.join(__dirname,'hosts/codebuddy.cjs'))),currentText:settingsText,
38
+ transcriptMode:mode,transcriptRoots:roots,productVersion:options.productVersion||current.record?.productVersion||'unreported'});
39
+ }
40
+ function configure(options) {
41
+ const change=prepare(options);if(change.unchanged||options.preview)return change.summary;
42
+ return R.apply(change);
43
+ }
44
+ function check(options,runtime={}) {
45
+ const current=runtime.current||context(options),profile=require('../assets/hosts/codebuddy/profile.json');
46
+ const result={scope:current.scope,base:current.base,scriptPath:current.scriptPath,filesPresent:fs.existsSync(current.scriptPath),profile:profile.id,nativeValidation:profile.nativeValidation,readOnly:true,sideEffects:[]};
47
+ if(process.platform==='win32')return {...result,status:'unsupported',reason:'CodeBuddy Hook quoting on Windows has not been validated'};
48
+ if(!current.record){
49
+ const settings=R.config(R.fileIO.read(path.join(current.base,'settings.json')));
50
+ if(Object.values(settings.hooks||{}).flat().some(group=>group.hooks.some(h=>h.command?.includes(R.OWNER))))return {...result,status:'failed',errors:['Unknown prototype Hook registration has no ownership record; preserve it for review']};
51
+ return {...result,status:result.filesPresent?'files-present':'not-enabled'};
52
+ }
53
+ try {
54
+ const record=(runtime.registration||C.registration)(current.base),effective=(runtime.select||C.select)({project:current.project,product:record.product,home:options.home,env:options.env});
55
+ Object.assign(result,{product:record.product,productVersion:record.productVersion,registrationDigest:record.registrationDigest,effective:effective?.base===current.base,transcriptRoots:record.transcriptRoots,
56
+ transcript:{mode:record.transcriptMode||(record.transcriptRoots?.length?'manual':'auto'),status:'awaiting-native-event',invocationVerified:false}});
57
+ let observation=null;
58
+ const parent=path.join(current.base,'state/i3-prototype',C.projectKey(current.project));
59
+ const entries=runtime.entries||[];
60
+ if(!runtime.entries&&fs.existsSync(parent))for(const name of fs.readdirSync(parent)) {
61
+ if(!/^[a-f0-9]{24}$/.test(name))continue;
62
+ const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
63
+ entries.push(...Object.values(JSON.parse(raw).bindings||{}));
64
+ }
65
+ for(const entry of entries)if(entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
66
+ if(observation) {
67
+ result.transcript.path=observation.source.path;
68
+ C.prefix(observation.source.path,observation.source.bytes,observation.source.digest);
69
+ C.source({session_id:observation.sessionId,transcript_path:observation.source.path},record,current.project);
70
+ Object.assign(result.transcript,{status:'verified',invocationVerified:true});
71
+ result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
72
+ }
73
+ const recent=observation&&Date.now()-Date.parse(observation.lastHook.at)>=0&&Date.now()-Date.parse(observation.lastHook.at)<30*60*1000;
74
+ return {...result,status:result.effective&&recent?'active':record.product==='cli'?'pending-host-review':'registered',
75
+ activationScope:'Only the displayed project/session/configuration and adapter bytes; not all projects or business acceptance',
76
+ next:record.product==='cli'?'Review /hooks in CodeBuddy Code, then use a new session.':'Reload CodeBuddy and verify a real prototype write task.'};
77
+ }catch(error){if(result.transcript)Object.assign(result.transcript,{status:'unverified',invocationVerified:false});return {...result,status:'failed',errors:[error.message]};}
78
+ }
79
+ function main(argv=process.argv.slice(2)) {
80
+ const central=[path.resolve(__dirname,'../../../i3-composer-agent/hooks/manage.cjs'),path.resolve(__dirname,'../../../hooks/manage.cjs')].find(file=>fs.existsSync(file));
81
+ if(central)return require(central).main([...argv,'--groups','prototype']);
82
+ const options={action:argv.shift()||'check',host:'codebuddy',project:process.cwd()};
83
+ 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'].includes(key),'Unknown Hook option');options[key]=argv.shift();}
84
+ return options.action==='check'?check(options):configure(options);
85
+ }
86
+ module.exports={context,globalBase:C.globalBase,prepare,configure,check,main};
87
+ if(require.main===module)try{const result=main();process.stdout.write(JSON.stringify(result,null,2)+'\n');if(result.status==='failed')process.exitCode=1;}catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
@@ -5,6 +5,7 @@ const crypto = require('node:crypto');
5
5
  const { spawnSync } = require('node:child_process');
6
6
  const templateRegistry = require('./template-adapters.cjs').createTemplateRegistry();
7
7
  const { discoverProviderCandidates } = require('./design-provider-discovery.cjs');
8
+ const projectRegistry = require('./project-registry.cjs');
8
9
  const hash = value => crypto.createHash('sha256').update(value).digest('hex');
9
10
  const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
10
11
  function ensure(condition, message) { if (!condition) throw new Error(message); }
@@ -113,11 +114,8 @@ function initPrototype(options, registry = templateRegistry) {
113
114
  ensure(fs.existsSync(project) && fs.statSync(project).isDirectory(), 'Project directory does not exist');
114
115
  const directory = contained(project, `prototypes/${id}`);
115
116
  ensure(!fs.existsSync(directory), 'Prototype already exists / 原型已存在,使用 revise 或另选标识');
116
- const lockPath = contained(project, '.i3-ui-prototypes.lock');
117
- const lock = fs.openSync(lockPath, 'wx');
118
- try {
119
- const registryPath = contained(project, '.i3-ui-prototypes.json');
120
- const registry = fs.existsSync(registryPath) ? json(registryPath) : { schemaVersion: 1, prototypes: [] };
117
+ return projectRegistry.transaction(project, store => {
118
+ const registry = store.registry;
121
119
  ensure(registry.schemaVersion === 1 && Array.isArray(registry.prototypes), 'Invalid project prototype registry');
122
120
  ensure(!registry.prototypes.some(entry => entry.id === id), 'Prototype registry already contains identifier');
123
121
  const preset = selected.preset;
@@ -142,11 +140,11 @@ function initPrototype(options, registry = templateRegistry) {
142
140
  fs.renameSync(staging, directory);
143
141
  created = true;
144
142
  registry.prototypes.push({ id, directory: `prototypes/${id}`, appliesTo: [] });
145
- atomicJson(project, '.i3-ui-prototypes.json', registry);
143
+ store.save();
146
144
  } catch (error) { if (created) fs.rmSync(directory, { recursive: true, force: true }); throw error; }
147
145
  finally { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); }
148
146
  return { status: 'created', directory, template: template.id, expertMode };
149
- } finally { fs.closeSync(lock); fs.unlinkSync(lockPath); }
147
+ });
150
148
  }
151
149
  function contentManifest(directory, registry = templateRegistry) {
152
150
  const metadata = json(contained(directory, 'prototype.json'));
@@ -2,6 +2,7 @@
2
2
  const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const os = require('node:os');
5
+ const crypto = require('node:crypto');
5
6
  // Reviewed mirror of i3's installer target table; maintenance tests require byte equality.
6
7
  const registry = require('../assets/optional-skills/host-targets.json');
7
8
  const aliases = { claude: 'claude-code', gemini: 'gemini-cli', copilot: 'github-copilot' };
@@ -11,6 +12,7 @@ function globalSkillsDir(agent, home, env) {
11
12
  let base = home;
12
13
  if (config.base === 'config') base = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
13
14
  if (config.base === 'env') base = env[config.env]?.trim() || path.join(home, config.fallback);
15
+ if (config.env === 'CODEBUDDY_CONFIG_DIR' && (!path.isAbsolute(base) || /[\x00\r\n]/.test(base))) throw new Error('CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
14
16
  if (config.alternatives && !fs.existsSync(path.join(home, path.dirname(config.path)))) {
15
17
  const alternative = config.alternatives.find(relative => fs.existsSync(path.join(home, path.dirname(relative))));
16
18
  if (alternative) return path.join(home, alternative);
@@ -41,8 +43,10 @@ function resolveLocations(options = {}) {
41
43
  const projectBase = path.dirname(projectSkills), globalBase = globalSkills ? path.dirname(globalSkills) : null;
42
44
  // Some hosts put skills directly in the workspace; tools still get a host directory.
43
45
  const projectToolsBase = projectBase === project ? path.join(project, `.${host}`) : projectBase;
46
+ const projectKey = `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 32) || 'project'}-${crypto.createHash('sha256').update(project).digest('hex').slice(0, 16)}`;
47
+ const cacheBase = path.join(scope === 'global' ? globalBase : projectToolsBase, 'cache/i3-prototype');
44
48
  return { host, scope, project, home, skillsDirectory, base, projectSkills, globalSkills,
45
- projectBase, globalBase, projectToolsBase,
49
+ projectBase, globalBase, projectToolsBase, projectKey, cacheDirectory: scope === 'global' ? path.join(cacheBase, projectKey) : cacheBase,
46
50
  toolsDirectory: path.join(scope === 'global' ? globalBase : projectToolsBase, 'tools/i3-prototype') };
47
51
  }
48
52
  function scopeBases(context) {
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const assert = require('node:assert/strict');
2
3
  const fs = require('node:fs');
3
4
  const path = require('node:path');
4
5
  const crypto = require('node:crypto');
@@ -300,6 +301,7 @@ function checkCapabilities({ definition, bindings = [], regions = [], scope = {}
300
301
  }
301
302
  function syncTemplate(directory, { write = true } = {}) {
302
303
  directory = path.resolve(directory);
304
+ if (write && fs.existsSync(path.join(directory, 'component-inventory.json'))) fs.writeFileSync(path.join(directory, 'template-inventory.json'), JSON.stringify(require('./template-inventory.cjs').scan(directory), null, 2) + '\n');
303
305
  const files = filesUnder(directory);
304
306
  const manifest = readJson(localFile(directory, 'template.json'));
305
307
  identifier(manifest.id, 'template');
@@ -345,8 +347,15 @@ function loadTemplate(directory) {
345
347
  if (!stored || canonical(stored) !== canonical(expected)) throw new Error('Template index is stale / 源和索引不同步; run sync-template.cjs');
346
348
  return templateSnapshot(directory, expected);
347
349
  }
348
- function loadBundledTemplate() {
349
- const directory = path.join(ASSETS, 'templates/enterprise-admin');
350
+ function loadBundledTemplate(templateId, { templatesRoot = path.join(ASSETS, 'templates') } = {}) {
351
+ const catalog = readJson(localFile(templatesRoot, 'catalog.json'));
352
+ templateId ||= catalog.defaultTemplate; identifier(templateId, 'template');
353
+ const selected = catalog.templates.find(item => item.id === templateId);
354
+ assert.ok(selected?.availability === 'available' && selected.contentRoot, `Unsupported or not available native template: ${templateId}`);
355
+ const directory = localFile(templatesRoot, selected.contentRoot);
356
+ const manifest = readJson(localFile(directory, 'template.json'));
357
+ assert.equal(manifest.kind, 'content-reference', 'Native templates require an explicit content-reference manifest');
358
+ assert.equal(manifest.id, templateId, 'Template catalog identity differs from its manifest');
350
359
  return templateSnapshot(directory, syncTemplate(directory, { write: false }));
351
360
  }
352
361
  function prepareTemplate({ project, id = 'enterprise-admin' }) {