@givolt/i3-composer-agent 2.3.2 → 2.3.4

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 (100) 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 +48 -9
  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 +49 -15
  12. package/docs/codebuddy-setup.md +43 -4
  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 +20 -2
  17. package/hooks/codebuddy-upstream.cjs +93 -0
  18. package/hooks/i3-prototype.cjs +12 -0
  19. package/hooks/manage.cjs +170 -0
  20. package/package.json +35 -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-buddy-lifecycle-test.js +89 -0
  31. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  32. package/scripts/ui-prototype-dependencies-test.js +13 -0
  33. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  34. package/scripts/ui-prototype-execution-gates-test.js +90 -0
  35. package/scripts/ui-prototype-execution-policy-test.js +53 -0
  36. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  37. package/scripts/ui-prototype-ide-source-test.js +73 -0
  38. package/scripts/ui-prototype-recovery-test.js +72 -0
  39. package/scripts/ui-prototype-registration-test.js +53 -0
  40. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  41. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  42. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  43. package/skills/i3-ui-prototype/SKILL.md +29 -7
  44. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  45. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  46. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +14 -3
  47. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  48. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +6 -1
  49. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  50. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
  51. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  52. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +17 -0
  53. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +4 -2
  54. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  55. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  56. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  57. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  58. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +1 -1
  59. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  60. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  61. package/skills/i3-ui-prototype/references/artifact-contract.md +1 -1
  62. package/skills/i3-ui-prototype/references/content-template.md +9 -4
  63. package/skills/i3-ui-prototype/references/entry-contract.md +1 -1
  64. package/skills/i3-ui-prototype/references/execution-evidence.md +83 -0
  65. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  66. package/skills/i3-ui-prototype/references/interaction-acceptance.md +5 -1
  67. package/skills/i3-ui-prototype/references/optional-skills.md +4 -0
  68. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  69. package/skills/i3-ui-prototype/references/template-adapters.md +6 -0
  70. package/skills/i3-ui-prototype/references/workflow-contract.md +4 -0
  71. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  72. package/skills/i3-ui-prototype/scripts/buddy-host.cjs +40 -0
  73. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  74. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +196 -0
  75. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  76. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +125 -0
  77. package/skills/i3-ui-prototype/scripts/content-template.cjs +2 -2
  78. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  79. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  80. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +168 -0
  81. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +62 -0
  82. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +52 -0
  83. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  84. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  85. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  86. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  87. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  88. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +20 -0
  89. package/skills/i3-ui-prototype/scripts/native-command.cjs +18 -0
  90. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +17 -5
  91. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +1 -0
  92. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +64 -18
  93. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +23 -3
  94. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  95. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +28 -6
  96. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  97. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +5 -7
  98. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +5 -1
  99. package/skills/i3-ui-prototype/scripts/template-contract.cjs +11 -2
  100. 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,7 @@ function defaultScope() {
32
33
  ],exemptions:[],openQuestions:[]};
33
34
  }
34
35
  function createPrototype(options) {
36
+ if(options.executionPolicy!==undefined){const policy=require('./execution-policy.cjs');assert.ok([policy.ID,policy.AUTO_ID].includes(options.executionPolicy),'Unsupported execution policy');assert.ok(['codebuddy','workbuddy'].includes(options.host),'Explicit Buddy host required for A+B');if(options.executionPolicy===policy.ID)assert.equal(options.host,'codebuddy','Legacy policy is CodeBuddy only');}
35
37
  assert.ok(options.framework===undefined||options.framework==='vue-antdv-next','Unsupported framework / 不支持的框架');
36
38
  assert.ok(options.title===undefined||(typeof options.title==='string'&&options.title.trim()),'Title must be a nonempty string');
37
39
  const project=path.resolve(options.project||'.'); assert.ok(fs.statSync(project).isDirectory(),'Project must exist'); id(options.id);
@@ -41,24 +43,29 @@ function createPrototype(options) {
41
43
  provider=require('./prototype-lib.cjs').checkDesignProvider({...options,project});
42
44
  if(provider.availability!=='available') return {status:'needs-design-provider',provider,sideEffects:[],choices:['locate-or-repair','explicit-template-mode','analyze']};
43
45
  }
44
- const template=options.templateRoot?templates().loadTemplate(path.resolve(options.templateRoot)):templates().loadBundledTemplate();
46
+ assert.ok(!(options.templateRoot && options.template), 'Choose a template ID or an explicit template root');
47
+ const template=options.templateRoot?templates().loadTemplate(path.resolve(options.templateRoot)):templates().loadBundledTemplate(options.template);
45
48
  const reference=template.kind==='content-reference';
46
49
  assert.ok(!reference||options.scope,'A sourced scope file is required for business generation; no default device demo');
47
50
  const scope=options.scope?json(path.resolve(options.scope)):defaultScope();
51
+ const appliesTo=projectRegistry.moduleIds(scope);
48
52
  const selection=reference?require('./content-template.cjs').scopeInputs(template,scope,templates()):null;
53
+ const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(template,scope):null;
49
54
  const content=reference?{pageTitle:scope.title||'业务原型'}:templates().projectAdminListContent(template);
50
55
  if(options.title!==undefined)content.pageTitle=options.title;
51
56
  const profile=json(path.join(frameworkRoot,'profile.json'));
52
57
  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');
58
+ return projectRegistry.transaction(project,store=>{
54
59
  let stage=null,published=false;
55
60
  try {
56
61
  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:[]};
62
+ const registry=store.registry;
58
63
  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
64
  const frameworkFiles=entries(frameworkRoot,walk(frameworkRoot,'',excluded));
60
65
  const instanceId=crypto.randomUUID();
61
66
  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};
67
+ if(generation)data.runtime.generationContract=1;
68
+ if(options.executionPolicy){data.runtime.executionPolicy=options.executionPolicy;write(stage,'inputs/execution-policy.json',require('./execution-policy.cjs').create({id:options.executionPolicy,host:options.host,mode,officialCallable:!options.officialSourceFallback}));}
62
69
  write(stage,'prototype.json',data);
63
70
  for(const file of template.files) {
64
71
  assert.equal(file.encoding,'base64');
@@ -66,28 +73,35 @@ function createPrototype(options) {
66
73
  }
67
74
  write(stage,'inputs/template-snapshot.json',template);
68
75
  write(stage,'inputs/scope.json',scope);
76
+ if(generation)write(stage,'inputs/generation.json',generation);
69
77
  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});
78
+ 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
79
  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});
80
+ const scenarioDetails=[...checked.requiredScenarios,...(generation?.scenarios||[])],requiredScenarios=scenarioDetails.map(s=>typeof s==='string'?s:s.id);
81
+ const hasDrawer=reference&&(selection.fixed.regions.some(region=>region.mode==='edit-form')||generation?.bindings.some(binding=>binding.capabilityId==='dialog'&&binding.parameters.presentation==='drawer'));
82
+ 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});
83
+ 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
84
  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});
85
+ 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
86
  const starter=path.join(frameworkRoot,reference?profile.starter:(profile.legacyStarter||profile.starter));
77
87
  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
88
  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)}:{})});
89
+ 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
90
  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
91
  write(stage,'design-system/MASTER.md',`# ${data.title}\n\n使用 antDv-next 原生组件与主题。管理列表采用紧凑信息层级,正文与操作可读;错误在字段附近呈现。抽屉保留进入与退出过程,显式开启遮罩模糊。\n\n本地模拟,不代表生产权限。\n`);
92
+ if(generation)write(stage,'design-system/MASTER.md',require('./generation-evidence.cjs').designSummary(generation,`# ${data.title}\n\nPrototypeShell 提供已选原生外壳;按逐区域/字段/操作合同实现业务,补齐真实源码映射和浏览器证据。骨架不是业务完成。本地模拟不代表生产权限。`));
93
+ if(generation)write(stage,'design-system/implementation.json',require('./generation-evidence.cjs').initialize(generation));
82
94
  write(stage,'design-system/decisions.json',{schemaVersion:1,records:[],reviewStatus:mode==='template'?'skipped':'pending'});
83
95
  write(stage,'qa/design-advice.json',{schemaVersion:2,provider,queries:[],records:[],status:mode==='template'?'skipped':'pending'});
84
96
  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
97
  const candidate=manifest(stage); data.inputs={digest:candidate.inputDigest}; write(stage,'prototype.json',data);
86
98
  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};
99
+ const digest=manifest(target).digest;
100
+ registry.prototypes.push({id:options.id,directory:`prototypes/${options.id}`,appliesTo,schemaVersion:2}); store.save();
101
+ return {status:'created',directory:target,revision:'r001',digest};
89
102
  } 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); }
103
+ finally { if(stage&&fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true}); }
104
+ });
91
105
  }
92
106
  function inspectDesignReview(directory,current=manifest(directory)) {
93
107
  const data=metadata(directory);
@@ -112,12 +126,16 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
112
126
  assert.equal(acceptedInputDigest??data.inputs.digest,current.inputDigest,'Input contract changed; explicitly revise fixed inputs');
113
127
  const capability=templates().checkInstanceCapabilities(path.join(directory,'inputs')); assert.equal(capability.status,'passed',capability.errors?.join('; '));
114
128
  const acceptance=json(inside(directory,'inputs/acceptance.json'));
129
+ require('./execution-runtime.cjs').policy(directory);
115
130
  require('./native-devtools.cjs').readPolicy(directory);
116
131
  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
132
  const fixedFramework=json(inside(directory,'inputs/framework.json'));
118
133
  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
134
  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));
135
+ const generation=scope.schemaVersion===3?json(inside(directory,'inputs/generation.json')):null;
136
+ 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}`);}
137
+ projectRegistry.moduleIds(scope);
138
+ 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
139
  for(const requirement of scope.requirements)assert.ok(mapping.requirements.some(item=>item.id===requirement.id),`Requirement omitted from mapping: ${requirement.id}`);
122
140
  for(const requirement of mapping.requirements) {
123
141
  assert.ok(scope.requirements.some(item=>item.id===requirement.id),`Unknown mapped requirement: ${requirement.id}`);
@@ -131,14 +149,17 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
131
149
  if(data.runtime.contentReference) {
132
150
  assert.deepEqual(config.pages,scope.pages.map(page=>({id:page.id,title:page.title})),'Business configuration differs from fixed page scope');
133
151
  assert.ok(['needs-adaptation','adapted'].includes(data.business?.status),'Unknown business implementation status');
134
- const fixedBindings=json(inside(directory,'inputs/capabilities.json')).bindings;
152
+ const fixedBindings=[...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])];
135
153
  assert.deepEqual((mapping.bindings||[]).map(binding=>binding.id).sort(),fixedBindings.map(binding=>binding.id).sort(),'Business mapping omits fixed capabilities');
136
154
  }
137
155
  const themeBinding=json(inside(directory,'inputs/capabilities.json')).bindings.find(binding=>data.runtime.contentReference?binding.capabilityId==='theme':binding.id==='devices/device-list/theme');
138
156
  if(themeBinding)assert.deepEqual(config.theme,templates().resolveThemeParameters(themeBinding.parameters),'Theme configuration differs from fixed input binding');
139
157
  else assert.equal(config.theme,undefined,'Theme requires an explicit optional capability binding');
158
+ 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
159
  const deps=json(inside(directory,'app/package.json')).dependencies; assert.ok(deps['antdv-next'] && deps.vue,'Native framework dependencies missing');
160
+ if(generation)require('./generation-evidence.cjs').validateImplementation(directory,generation,json(inside(directory,'design-system/implementation.json')),{complete:!!options.acceptance,frameworkMap:fixedFramework.componentMap});
141
161
  if(options.acceptance) {
162
+ 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
163
  if(data.runtime.contentReference)assert.equal(data.business?.status,'adapted','Business implementation pending; a scaffold cannot pass acceptance');
143
164
  const adoptedAdvice=inspectDesignReview(directory,current).adoptedAdvice;
144
165
  const report=options.report||json(inside(directory,json(inside(directory,'qa/latest.json')).report));
@@ -149,7 +170,9 @@ function inspectWithInputs(directory,options={},acceptedInputDigest) {
149
170
  assert.deepEqual(report.consoleErrors,[],'Browser errors remain');
150
171
  const required=[...acceptance.requiredScenarios,...acceptance.frameworkScenarios];
151
172
  for(const scenario of required) assert.ok(report.journeys.some(j=>j.id===scenario && j.status==='passed'),`Missing browser scenario: ${scenario}`);
173
+ if(generation)require('./generation-evidence.cjs').validateObservations(directory,generation,report);
152
174
  for(const record of adoptedAdvice)assert.ok(report.journeys.some(j=>j.id===record.verificationScenario && j.status==='passed'),`Advice verification missing: ${record.resultIdentity}`);
175
+ require('./execution-runtime.cjs').requireStage(directory,'delivery',module.exports,{businessReport:report});
153
176
  }
154
177
  return {status:'passed',level:options.acceptance?'acceptance':'structure',revision:current.revision,digest:current.digest,verification:data.verification.status,review:effectiveReview(data,current).status};
155
178
  } catch(error) { return {status:'failed',errors:[error.message]}; }
@@ -186,12 +209,35 @@ function revise(directory,options) {
186
209
  assert.equal(checked.status,'passed',checked.errors?.join('; '));
187
210
  } else assert.equal(data.inputs.digest,current.inputDigest,'Input contract changed; revise --accept-inputs must explicitly validate the new contract');
188
211
  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);
212
+ const scope=json(inside(directory,'inputs/scope.json')); projectRegistry.moduleIds(scope);
213
+ return projectRegistry.withInstanceAssociation(directory,scope,()=>{
214
+ if(options.acceptInputs)data.inputs={digest:current.inputDigest};
215
+ data.revision=options.revision; data.verification={status:'not-run'}; data.review={status:'pending-review'};
216
+ write(directory,'prototype.json',data); return manifest(directory);
217
+ });
218
+ });
219
+ }
220
+ 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)); }
221
+ 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}); }
222
+ function synchronizeIndex(options={}) {
223
+ id(options.id);
224
+ const index=projectRegistry.read(options.project),entry=index.registry.prototypes.find(item=>item.id===options.id);
225
+ assert.ok(entry,'Prototype is not registered');
226
+ const directory=inside(index.project,entry.directory),scope=json(inside(directory,'inputs/scope.json'));
227
+ assert.ok(scope.moduleIds!==undefined||scope.schemaVersion===2,'Legacy scope has no explicit moduleIds; do not invent or rewrite old input contracts');
228
+ const current=manifest(directory);
229
+ return mutate(directory,{...options,expectedRevision:current.revision,expectedDigest:current.digest},()=>{
230
+ const checked=inspect(directory);assert.equal(checked.status,'passed',checked.errors?.join('; '));
231
+ return projectRegistry.transaction(index.project,store=>{
232
+ const selected=store.registry.prototypes.find(item=>item.id===options.id);
233
+ assert.ok(selected&&selected.directory===entry.directory,'Prototype index entry changed during synchronization');
234
+ const fixed=json(inside(directory,'inputs/scope.json')),appliesTo=projectRegistry.moduleIds(fixed);
235
+ assert.equal(manifest(directory).digest,current.digest,'Candidate changed during synchronization');
236
+ selected.appliesTo=appliesTo;store.save();
237
+ return {status:'synchronized',index:store.file,id:selected.id,appliesTo};
238
+ });
192
239
  });
193
240
  }
194
- function build(directory,options={}) { return require('./native-build.cjs').build(directory,options,{manifest,inspect}); }
195
241
  function verifyBuild(directory,buildId) { return require('./native-build.cjs').verifyBuild(directory,buildId,{manifest}); }
196
242
  function preview(directory,options={}) { return require('./native-build.cjs').preview(directory,options,{manifest}); }
197
243
  function recordRun(directory,report) {
@@ -241,4 +287,4 @@ function compareBaseline(directory) {
241
287
  }
242
288
  function state(directory,options={}) { return require('./native-delivery.cjs').state(directory,options,module.exports); }
243
289
  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};
290
+ 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,33 @@ 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); }
15
- case 'create': return lib.createPrototype(options);
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});
26
+ case 'create': {
27
+ const installedFile=path.resolve(__dirname,'../../../.i3-composer-agent.json');
28
+ const effective=fs.existsSync(installedFile)?require('./execution-policy.cjs').creationOptions(options,JSON.parse(fs.readFileSync(installedFile,'utf8'))):options;
29
+ return lib.createPrototype(effective);
30
+ }
31
+ case 'execution-begin': return require('./execution-runtime.cjs').begin(root,lib);
32
+ case 'execution-assertions-template': return require('./execution-assertions.cjs').template(options);
33
+ case 'execution-template': return require('./execution-runtime.cjs').template(root,lib);
34
+ case 'execution-check': return require('./execution-runtime.cjs').check(root,options.stage||'delivery',lib);
16
35
  case 'manifest': return lib.manifest(root);
17
36
  case 'inspect': return lib.inspect(root,{acceptance:!!options.acceptance});
18
37
  case 'state': return lib.state(root,options);
@@ -22,6 +41,7 @@ async function main(argv=process.argv.slice(2)) {
22
41
  case 'build': return lib.build(root,options);
23
42
  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
43
  case 'revise': return lib.revise(root,options);
44
+ case 'replan': return lib.replan(root,options);
25
45
  case 'claim': return lib.claim(root,options);
26
46
  case 'release': return lib.release(root,options.owner);
27
47
  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 };
@@ -31,10 +31,32 @@ function antdvTool(project, options = {}) {
31
31
  if (result.status !== 0) throw new Error(result.error?.message || result.stderr || 'CLI query failed');
32
32
  const data = JSON.parse(result.stdout);
33
33
  if (!data || data.name !== 'Table' || !Array.isArray(data.props) || data.error) throw new Error('CLI returned no Table knowledge');
34
- return { availability: 'available', root, source, entry, version: pkg.version, command: [process.execPath, ...args], knowledgeVersion: '1.5.2', componentVersion: '1.5.3', exactKnowledgeMatch: false,
34
+ return { availability: 'available', root, source, entry, nodePath:process.execPath, version: pkg.version, command: [process.execPath, ...args], invocationVerified:false, knowledgeProbe:'Table-only', knowledgeVersion: '1.5.2', componentVersion: '1.5.3', exactKnowledgeMatch: false,
35
35
  limitation: 'CLI knowledge 1.5.2 is reference only; verify target 1.5.3 package types/source.' };
36
36
  } catch (error) { return { availability: 'unusable', root, source, reason: error.message }; }
37
37
  }
38
+ function query(options={}) {
39
+ const kinds=['info','demo','token','semantic','doc','list','design.md'];
40
+ assertQuery(kinds.includes(options.kind),'Choose a read-only antDv query: '+kinds.join(', '));
41
+ const locations=resolveLocations(options),tool=antdvTool(locations.project,{...options,host:locations.host,scope:locations.scope});
42
+ assertQuery(tool.availability==='available',`Local antDv CLI ${tool.availability}: ${tool.reason||'use prototype-deps check; installation requires separate authorization'}`);
43
+ const component=options.component,version=options.version||tool.knowledgeVersion;
44
+ assertQuery(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version),'Explicit safe knowledge version required');
45
+ const args=[tool.entry,options.kind];
46
+ if(!['list','design.md'].includes(options.kind)){
47
+ assertQuery(typeof component==='string'&&/^[A-Za-z][A-Za-z0-9-]{0,99}$/.test(component),'Component name required');args.push(component);
48
+ }
49
+ if(options.demo!==undefined){assertQuery(options.kind==='demo'&&/^[A-Za-z0-9_-]{1,100}$/.test(options.demo),'Safe demo name required');args.push(options.demo);}
50
+ args.push('--ver',version,'--format','json');
51
+ const result=spawnSync(tool.nodePath,args,{cwd:locations.project,env:{...(options.env||process.env),ANTDV_NO_AUTO_REPORT:'1'},encoding:'utf8',timeout:15000,maxBuffer:5*1024*1024});
52
+ let data=null;try{data=JSON.parse(result.stdout);}catch{/* Some official doc/demo commands return text. */}
53
+ const stdout=result.stdout||'',stderr=result.stderr||'';
54
+ const invalid=result.status!==0||!stdout.trim()||/^\s*(?:Error\b|错误[::])/i.test(stdout)||data?.error||data?.success===false||data?.status==='failed'||options.kind==='info'&&(!data||data.name!==component||!Array.isArray(data.props));
55
+ return {status:invalid?'failed':'queried',host:locations.host,scope:locations.scope,command:[tool.nodePath,...args],source:tool.source,cliVersion:tool.version,knowledgeVersion:version,componentVersion:tool.componentVersion,exactKnowledgeMatch:version===tool.componentVersion,
56
+ component:component||null,kind:options.kind,exitCode:result.status,stdout,stderr,...(data?{data}:{}),...(result.error?{error:result.error.message}:{}),skillInvocationVerified:false,
57
+ note:'This is a CLI query only. Load the official Skill through the host before business queries; native Hooks must correlate the actual request/result. Verify target package types/source when knowledge versions differ.'};
58
+ }
59
+ function assertQuery(condition,message){if(!condition)throw new Error(message);}
38
60
  function check(options = {}) {
39
61
  const locations = resolveLocations(options), { project, host, scope } = locations;
40
62
  const roots = scopeBases(locations).map(([base]) => path.join(base, 'skills/ui-ux-pro-max'));
@@ -62,8 +84,8 @@ function check(options = {}) {
62
84
  }
63
85
  function parse(argv) {
64
86
  const operation = argv.shift() || 'check', options = {};
65
- if (!['check', 'install'].includes(operation)) throw new Error('Use check or install');
66
- const flags = new Set(['project', 'host', 'scope', 'skills', 'provider', 'python', 'tool']);
87
+ if (!['check', 'install','query'].includes(operation)) throw new Error('Use check or install or query');
88
+ const flags = new Set(['project', 'host', 'scope', 'skills', 'provider', 'python', 'tool',...(operation==='query'?['kind','component','version','demo']:[])]);
67
89
  while (argv.length) {
68
90
  const key = argv.shift();
69
91
  if (key === '--migrate-tools' && operation === 'install') { options.migrateTools = true; continue; }
@@ -74,8 +96,8 @@ function parse(argv) {
74
96
  }
75
97
  async function main(argv = process.argv.slice(2)) {
76
98
  const { operation, options } = parse([...argv]);
77
- return operation === 'check' ? check(options) : require('./optional-skill-install.cjs').install(options);
99
+ return operation === 'check' ? check(options) : operation==='query'?query(options):require('./optional-skill-install.cjs').install(options);
78
100
  }
79
- module.exports = { check, skillAt, antdvTool, parse, main };
80
- if (require.main === module) main().then(result => process.stdout.write(JSON.stringify(result, null, 2) + '\n'))
101
+ module.exports = { check, skillAt, antdvTool, query, parse, main };
102
+ if (require.main === module) main().then(result => {process.stdout.write(JSON.stringify(result, null, 2) + '\n');if(result.status==='failed')process.exitCode=1;})
81
103
  .catch(error => { process.stderr.write(JSON.stringify({ status: 'failed', errors: [error.message] }) + '\n'); process.exitCode = 1; });
@@ -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
+ const host=options.host||'codebuddy';assert.ok(['codebuddy','workbuddy'].includes(host),'A+B supports CodeBuddy and WorkBuddy');
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,host};
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
+ assert.ok(options.product===undefined||['auto','ide','cli'].includes(options.product),'Invalid Buddy product profile');
25
+ const product='auto'; // Legacy product arguments are hints, never a second new registration.
26
+ const explicit=options.transcriptRoot,roots=explicit==='auto'?[]:explicit?[path.resolve(explicit)]:current.record?.transcriptRoots||[];
27
+ const mode=explicit?(explicit==='auto'?'auto':'manual'):current.record?.transcriptMode||(roots.length?'manual':'auto');
28
+ if(action!=='disable') {
29
+ 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');}
30
+ }
31
+ const settingsText=readText(path.join(current.base,'settings.json')),settings=R.config(settingsText);
32
+ if(action!=='disable')assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic auditing; preserve the existing policy');
33
+ return R.plan({action,...current,product,hosts:options.hosts,agents:options.agents,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,
34
+ transcriptMode:mode,transcriptRoots:roots,productVersion:options.productVersion||current.record?.productVersion||'unreported'});
35
+ }
36
+ function configure(options) {
37
+ const change=prepare(options);if(change.unchanged||options.preview)return change.summary;
38
+ return R.apply(change);
39
+ }
40
+ function check(options,runtime={}) {
41
+ const current=runtime.current||context(options),profile=require('../assets/hosts/codebuddy/profile.json');
42
+ 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:[]};
43
+ if(process.platform==='win32')return {...result,status:'unsupported',reason:'CodeBuddy Hook quoting on Windows has not been validated'};
44
+ if(!current.record){
45
+ const settings=R.config(R.fileIO.read(path.join(current.base,'settings.json')));
46
+ 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']};
47
+ return {...result,status:result.filesPresent?'files-present':'not-enabled'};
48
+ }
49
+ try {
50
+ const record=(runtime.registration||C.registration)(current.base),host=options.host||'codebuddy',products=record.product==='auto'?(host==='workbuddy'?['ide']:['ide','cli']):[record.product];
51
+ const effectiveByProduct=Object.fromEntries(products.map(product=>[product,(runtime.select||C.select)({project:current.project,product,host,home:options.home,env:options.env})?.base===current.base]));
52
+ Object.assign(result,{host,product:record.product,productVersion:record.productVersion,registrationDigest:record.registrationDigest,effective:Object.values(effectiveByProduct).every(Boolean),effectiveByProduct,transcriptRoots:record.transcriptRoots,
53
+ transcript:{mode:record.transcriptMode||(record.transcriptRoots?.length?'manual':'auto'),status:'awaiting-native-event',invocationVerified:false}});
54
+ let observation=null;
55
+ const parent=path.join(current.base,'state/i3-prototype',C.projectKey(current.project));
56
+ const entries=runtime.entries||[];
57
+ if(!runtime.entries&&fs.existsSync(parent))for(const name of fs.readdirSync(parent)) {
58
+ if(!/^[a-f0-9]{24}$/.test(name))continue;
59
+ const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
60
+ entries.push(...Object.values(JSON.parse(raw).bindings||{}));
61
+ }
62
+ for(const entry of entries)if((entry.host||'codebuddy')===host&&entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
63
+ if(observation) {
64
+ result.transcript.path=observation.source.path;
65
+ C.prefix(observation.source.path,observation.source.bytes,observation.source.digest);
66
+ C.source({session_id:observation.sessionId,transcript_path:observation.source.path},record,current.project);
67
+ Object.assign(result.transcript,{status:'verified',invocationVerified:true});
68
+ result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
69
+ const observedProduct=observation.product||(record.product!=='auto'?record.product:null);
70
+ assert.ok(['ide','cli'].includes(observedProduct),'Observed native product identity missing');
71
+ Object.assign(result,{product:observedProduct,productVersion:observation.productVersion||'unreported',effective:effectiveByProduct[observedProduct]===true,identitySource:observation.identitySource||'legacy-profile',recovery:observation.recovery||null});
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:'CLI: review /hooks; desktop: reload. Verify an actual Skill invocation and missing-Skill recovery in the chosen host.'};
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 (['CODEBUDDY_CONFIG_DIR','WORKBUDDY_CONFIG_DIR'].includes(config.env) && (!path.isAbsolute(base) || /[\x00\r\n]/.test(base))) throw new Error(`${config.env} 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) {