@givolt/i3-composer-agent 2.3.2 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +5 -1
  6. package/bin/agent-targets.json +4 -2
  7. package/bin/i3-composer-agent.cjs +44 -8
  8. package/bin/install.cjs +2 -1
  9. package/bin/native-layout.cjs +1 -1
  10. package/bin/tool-selector.cjs +24 -7
  11. package/bin/wizard.cjs +89 -15
  12. package/docs/codebuddy-setup.md +27 -2
  13. package/docs/codex-setup.md +3 -1
  14. package/docs/other-hosts.md +1 -1
  15. package/docs/ui-prototype-native/USAGE.md +4 -2
  16. package/docs/workbuddy-setup.md +3 -1
  17. package/hooks/codebuddy-upstream.cjs +93 -0
  18. package/hooks/i3-prototype.cjs +12 -0
  19. package/hooks/manage.cjs +162 -0
  20. package/package.json +31 -1
  21. package/plugin.json +1 -1
  22. package/scripts/prototype-cache-test.js +55 -0
  23. package/scripts/prototype-generation-entry-test.js +6 -0
  24. package/scripts/prototype-generation-evidence-test.js +10 -0
  25. package/scripts/prototype-generation-native-test.js +78 -0
  26. package/scripts/prototype-generation-test.js +99 -0
  27. package/scripts/prototype-inventory-test.js +43 -0
  28. package/scripts/prototype-locations-test.js +21 -0
  29. package/scripts/prototype-project-test.js +183 -0
  30. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  31. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  32. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  33. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  34. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  35. package/scripts/ui-prototype-ide-source-test.js +73 -0
  36. package/scripts/ui-prototype-registration-test.js +53 -0
  37. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  38. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  39. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  40. package/skills/i3-ui-prototype/SKILL.md +27 -7
  41. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  42. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  43. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +14 -3
  44. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  45. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +6 -1
  46. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  47. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
  48. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  49. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  50. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +4 -2
  51. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  52. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  53. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  54. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  55. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +1 -1
  56. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  57. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  58. package/skills/i3-ui-prototype/references/artifact-contract.md +1 -1
  59. package/skills/i3-ui-prototype/references/content-template.md +9 -4
  60. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  61. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  62. package/skills/i3-ui-prototype/references/interaction-acceptance.md +4 -0
  63. package/skills/i3-ui-prototype/references/optional-skills.md +4 -0
  64. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  65. package/skills/i3-ui-prototype/references/template-adapters.md +6 -0
  66. package/skills/i3-ui-prototype/references/workflow-contract.md +4 -0
  67. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  68. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  69. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  70. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  71. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  72. package/skills/i3-ui-prototype/scripts/content-template.cjs +2 -2
  73. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  74. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  75. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  76. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  77. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  78. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  79. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  80. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  81. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  82. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  83. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +17 -5
  84. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +1 -0
  85. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +66 -18
  86. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +18 -2
  87. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  88. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  89. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +5 -7
  90. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +5 -1
  91. package/skills/i3-ui-prototype/scripts/template-contract.cjs +11 -2
  92. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
3
+ const R=require('./codebuddy-registration.cjs'),E=require('./execution-evidence.cjs');
4
+ const FILES=['execution-policy.cjs','execution-evidence.cjs','codebuddy-ide-source.cjs','execution-gates.cjs','execution-assertions.cjs','execution-runtime.cjs','codebuddy-context.cjs','codebuddy-registration.cjs','codebuddy-hook.cjs','prototype-hooks.cjs','hosts/codebuddy.cjs','native-pilot.cjs','native-pilot-lib.cjs','native-delivery.cjs','native-devtools.cjs','native-files.cjs','native-build.cjs'];
5
+ const read=file=>R.fileIO.read(file);
6
+ function scriptsForEntry(scriptPath) {
7
+ if(path.basename(scriptPath)!=='i3-prototype.cjs')return path.dirname(path.dirname(scriptPath));
8
+ const root=path.resolve(path.dirname(scriptPath),'..');
9
+ const candidates=[root,path.dirname(root)].flatMap(directory=>['i3-ui-prototype',['ui','prototype'].join('-')].map(id=>path.join(directory,'skills',id,'scripts')));
10
+ const scripts=candidates.find(dir=>fs.existsSync(path.join(dir,'hosts/codebuddy.cjs')));assert.ok(scripts,'Unified Hook entry has no prototype Skill');return path.resolve(scripts);
11
+ }
12
+ function bundle(scriptPath) {
13
+ const scripts=scriptsForEntry(scriptPath);
14
+ const files=FILES.map(file=>[file,E.sha(fs.readFileSync(path.join(scripts,file)))]);
15
+ files.push(['profile',E.sha(fs.readFileSync(path.join(scripts,'../assets/hosts/codebuddy/profile.json')))]);
16
+ if(path.basename(scriptPath)==='i3-prototype.cjs') {
17
+ files.push(['unified-entry',E.sha(fs.readFileSync(scriptPath))]);
18
+ files.push(['unified-manager',E.sha(fs.readFileSync(path.join(path.dirname(scriptPath),'manage.cjs')))]);
19
+ }
20
+ return E.sha(JSON.stringify(files));
21
+ }
22
+ function registration(base) {
23
+ const raw=read(path.join(base,'state/i3-prototype/installation.json'));if(!raw)return null;
24
+ const record=JSON.parse(raw);assert.equal(record.owner,R.OWNER,'Unknown Hook ownership');assert.equal(record.schemaVersion,1,'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');
25
+ const settingsText=read(path.join(base,'settings.json')),settings=R.config(settingsText);
26
+ for(const event of R.EVENTS)assert.equal((settings.hooks?.[event]||[]).filter(group=>JSON.stringify(group)===JSON.stringify(record.groups[event])).length,1,'Registered Hook was removed, duplicated, or edited');
27
+ assert.equal(bundle(record.scriptPath),record.scriptDigest,'Audit adapter bytes changed; refresh owned registration');
28
+ assert.ok(fs.existsSync(record.nodePath),'Registered Node executable is missing');
29
+ return {...record,settingsDigest:E.sha(settingsText),registrationDigest:E.sha(JSON.stringify(record))};
30
+ }
31
+ function globalBase({home=os.homedir(),env=process.env}={}) {
32
+ const configured=env.CODEBUDDY_CONFIG_DIR?.trim();
33
+ if(!configured)return path.join(home,'.codebuddy');
34
+ assert.ok(path.isAbsolute(configured)&&!/[\x00\r\n]/.test(configured),'CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
35
+ return path.normalize(configured);
36
+ }
37
+ function select({project,product,home=os.homedir(),env=process.env,readText=read,loadRegistration=registration}) {
38
+ const projectBase=path.join(project,'.codebuddy'),userBase=globalBase({home,env});
39
+ const projectText=readText(path.join(projectBase,'settings.json')),localText=readText(path.join(projectBase,'settings.local.json'));
40
+ if(localText){const local=R.config(localText);assert.ok(!local.hooks&&!local.disableAllHooks&&!local.allowManagedHooksOnly,'Local Hook overrides are not supported by this profile');}
41
+ const projectSettings=R.config(projectText);assert.ok(!projectSettings.disableAllHooks&&!projectSettings.allowManagedHooksOnly,'Project Hook policy prevents automatic auditing');
42
+ const globalText=readText(path.join(userBase,'settings.json')),globalSettings=R.config(globalText);assert.ok(!globalSettings.disableAllHooks&&!globalSettings.allowManagedHooksOnly,'Global Hook policy prevents automatic auditing');
43
+ // IDE documents project settings as an override. Do not silently fall back through a shadowing file.
44
+ const bases=product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
45
+ for(const base of [...new Set(bases)]) {
46
+ const record=loadRegistration(base);if(!record)continue;
47
+ assert.equal(record.product,product,'Conflicting CodeBuddy product profiles');return record;
48
+ }
49
+ return null;
50
+ }
51
+ function projectKey(project){return `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,32)||'project'}-${E.sha(project).slice(0,16)}`;}
52
+ function sessionDirectory(record,project,sessionId) {
53
+ assert.ok(typeof sessionId==='string'&&sessionId,'Native session ID required');
54
+ return path.join(record.base,'state/i3-prototype',projectKey(project),E.sha(sessionId).slice(0,24));
55
+ }
56
+ function source(payload,record,project) {
57
+ assert.ok(typeof payload.transcript_path==='string'&&typeof payload.session_id==='string','Native transcript and session fields required');
58
+ const roots=record.transcriptRoots||[],mode=record.transcriptMode||(roots.length?'manual':'auto');
59
+ const value=E.readSource(payload.transcript_path,roots,{mode,project});
60
+ assert.ok(value.prompts.length,'Original user task is absent from native transcript');
61
+ assert.ok(value.calls.every(call=>call.sessionId===payload.session_id),'Native session identity missing or mismatched');
62
+ assert.ok(value.prompts.every(prompt=>!prompt.sessionId||prompt.sessionId===payload.session_id),'Native prompt session differs');
63
+ return value;
64
+ }
65
+ function readOnly(prompt) {
66
+ if(/不(?:要)?(?:改代码|修改(?:任何)?代码|实施|生成原型)|\b(?:analysis only|do not (?:edit|modify|implement))\b/i.test(prompt))return true;
67
+ if(/(?:只|仅).{0,6}(?:分析|讨论|梳理|检查|解释)/.test(prompt))return true;
68
+ if(/先.{0,4}(?:分析|讨论|梳理|检查|解释)/.test(prompt)&&!/(?:然后|再|后).{0,8}(?:实现|实施|修改|继续)/.test(prompt))return true;
69
+ return /^\s*(?:请|先)?(?:暂停|停止|取消)(?:[,。!\s]|(?:这|当前|实施|工作|修改|生成))|^\s*(?:stop|cancel|pause)\b/i.test(prompt);
70
+ }
71
+ function prefix(file,bytes,digest) {
72
+ assert.ok(Number.isSafeInteger(bytes)&&bytes>0&&bytes<=64*1024*1024,'Invalid source cursor');
73
+ const raw=E.cursorBytes(file);assert.ok(raw.length>=bytes,'Native transcript was truncated');assert.equal(E.sha(raw.subarray(0,bytes)),digest,'Native transcript prefix changed');
74
+ }
75
+ module.exports={read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,prefix};
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict');
3
+ const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs'),G=require('./execution-gates.cjs');
4
+ const X=require('./execution-runtime.cjs');
5
+ function tokens(command) {
6
+ if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
7
+ const values=[];let value='',quote=null,started=false;
8
+ for(let i=0;i<command.length;i++) {const c=command[i];if(quote){if(c===quote){quote=null;started=true;}else if(c==='\\'&&quote==='"'&&i+1<command.length)value+=command[++i];else value+=c;}
9
+ else if(c==='"'||c==="'"){quote=c;started=true;}else if(c==='\\'&&i+1<command.length){value+=command[++i];started=true;}else if(/\s/.test(c)){if(started){values.push(value);value='';started=false;}}else if(/[;&|<>]/.test(c))return null;else {value+=c;started=true;}}
10
+ if(quote)return null;if(started)values.push(value);return values;
11
+ }
12
+ function nativeCommand(tool,input,cwd) {
13
+ if(!['Bash','execute_command','run_terminal_command'].includes(tool))return null;
14
+ const argv=tokens(input?.command);if(!argv)return null;const index=argv.findIndex(arg=>path.basename(arg)==='native-pilot.cjs');if(index<1)return null;
15
+ const operation=argv[index+1],root=argv[index+2]&&!argv[index+2].startsWith('--')?path.resolve(cwd,argv[index+2]):null;
16
+ return {operation,root,argv};
17
+ }
18
+ function eventCall(payload,source) {
19
+ const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
20
+ const partial=(before,after)=>typeof before==='string'?typeof after==='string'&&after.startsWith(before):Array.isArray(before)?Array.isArray(after)&&before.length<=after.length&&before.every((value,i)=>partial(value,after[i])):before&&typeof before==='object'?after&&typeof after==='object'&&Object.keys(before).every(key=>Object.hasOwn(after,key)&&partial(before[key],after[key])):before===after;
21
+ const matches=source.calls.filter(call=>{
22
+ if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||payload.tool_use_id&&call.toolCallId!==payload.tool_use_id)return false;
23
+ if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
24
+ if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
25
+ // IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
26
+ // actual callback with one pending native call in the exact generation. The
27
+ // pending call remains unsuccessful until a native result is persisted.
28
+ return source.format==='codebuddy-ide-index-v1'&&payload.client==='CodeBuddyIDE'&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
29
+ });
30
+ const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
31
+ assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
32
+ }
33
+ function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
34
+ const toolEvent=['PreToolUse','PostToolUse'].includes(payload.hook_event_name);
35
+ // IDE can persist the current call only AFTER returning from both Hooks. Its
36
+ // current native prompt/generation still binds the callback. Never synthesize
37
+ // a successful call; A checks the persisted request/result at a later stage.
38
+ const attempts=toolEvent&&record.product==='ide'&&payload.client==='CodeBuddyIDE'?8:1;
39
+ for(let attempt=1;attempt<=attempts;attempt++) {
40
+ try {
41
+ const source=readSource(payload,record,project);
42
+ let nativeRequestStatus='not-tool-event';
43
+ if(toolEvent) {
44
+ if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&payload.client==='CodeBuddyIDE') {
45
+ const latest=source.prompts.at(-1);
46
+ assert.ok(typeof payload.generation_id==='string'&&latest?.nativeRequestId===payload.generation_id,'IDE callback differs from the current native task generation');
47
+ const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===payload.tool_name);
48
+ if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
49
+ else nativeRequestStatus='deferred-until-native-persistence';
50
+ }else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
51
+ }
52
+ return {...source,nativeReadAttempts:attempt,nativeRequestStatus};
53
+ }catch(error){if(attempt===attempts)throw error;pause(100);}
54
+ }
55
+ }
56
+ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false}) {
57
+ if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
58
+ if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
59
+ const front=E.loaded(source.calls,'frontend').filter(c=>c.sessionId===binding?.sessionId&&c.requestLine>source.contextLine&&(source.stableThroughLine===undefined||c.resultLine<=source.stableThroughLine));
60
+ if(!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine||!front.length)return {deny:true,reason:'Run execution-begin for the current task and load the frontend engineering Skill successfully in this session/context before writing prototype UI. Reading SKILL.md is insufficient.'};
61
+ }
62
+ const final=source.latestAssistant||'',claim=/(?:已完成|已经完成|全部通过|可以交付|\b(?:completed|ready for review|all checks passed)\b)/i.test(final)&&!/(?:未完成|尚未|待验证|阻塞|等待|进度|blocked|awaiting|not complete|in progress)/i.test(final);
63
+ if(event==='Stop'&&binding&&!deliveryCurrent&&!stopHookActive&&claim)return {feedback:true,reason:'This prototype run has no current verified receipt. Run execution-check and resolve the missing evidence, or report blocked / awaiting-input / progress without claiming completion.'};
64
+ return {continue:true};
65
+ }
66
+ function prototypeForWrite(project,tool,input) {
67
+ if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
68
+ const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
69
+ let current=path.dirname(path.resolve(project,file));const prototypes=path.join(project,'prototypes');
70
+ while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
71
+ }
72
+ function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
73
+ const ideRoot=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/';
74
+ const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):env.CODEBUDDY_PROJECT_DIR||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
75
+ const project=real(root);
76
+ // IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
77
+ // or session-bound native workspace above; reject all other outside cwd.
78
+ if(payload.cwd&&!ideRoot){const relative=path.relative(project,real(payload.cwd));assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Hook cwd is outside project');}return project;
79
+ }
80
+ function saveIndex(directory,index) {
81
+ for(const entry of Object.values(index.bindings))F.write(path.dirname(directory),`runs/${entry.runId}/binding.json`,entry);
82
+ F.write(directory,'session.json',index);
83
+ }
84
+ function toolCwd(payload,record,project) {
85
+ return record.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/'?project:payload.cwd||project;
86
+ }
87
+ function handle(payload,options) {
88
+ assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
89
+ const project=safeProject(payload,options),record=C.select({project,product:options.product});
90
+ if(!record||record.base!==options.base)return {continue:true};
91
+ const cwd=toolCwd(payload,record,project);
92
+ // SessionStart and analysis do not create persistent per-project/session state.
93
+ if(['SessionStart','UserPromptSubmit'].includes(options.event))return {continue:true};
94
+ let source;
95
+ try {
96
+ source=eventSource(payload,record,project);
97
+ }catch(error){
98
+ const target=options.event==='PreToolUse'?prototypeForWrite(project,payload.tool_name,payload.tool_input):null;
99
+ if(target&&X.policy(target)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},target))return {deny:true,reason:`Native evidence unavailable for strict prototype UI write: ${error.message}`};
100
+ throw error;
101
+ }
102
+ const latest=source.prompts.at(-1);
103
+ if(C.readOnly(latest.text)) {
104
+ const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
105
+ if(options.event==='PreToolUse'&&(native&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(native.operation)||root&&X.policy(root)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},root)))return {deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'};
106
+ return {continue:true};
107
+ }
108
+ const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
109
+ const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
110
+ const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
111
+ if(options.event==='PreToolUse'&&command?.operation==='create'&&!command.argv.includes('--execution-policy')&&(E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text)))return {deny:true,reason:'New CodeBuddy prototypes require --host codebuddy --execution-policy codebuddy-ab-v1 and fixed --execution-assertions. Existing artifacts keep their original contracts.'};
112
+ const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
113
+ const existing=directory?index.bindings[directory]:null;
114
+ if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
115
+ assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
116
+ const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
117
+ const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
118
+ assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
119
+ const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
120
+ const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
121
+ index.bindings[directory]={schemaVersion:1,runId,sessionId:payload.session_id,project,projectKey:C.projectKey(project),directory,instanceId:meta.instanceId,policyDigest:fixed.digest,inputDigest:current.inputDigest,entryLine:latest.line,promptDigest:latest.digest,contextLine:source.contextLine,
122
+ operation:meta.revision==='r001'?'create':'revise',product:record.product,productVersion:record.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
123
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
124
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
125
+ try {
126
+ const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
127
+ saveIndex(stateDirectory,fresh);
128
+ F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,product:record.product});
129
+ }finally{fs.rmdirSync(lock);}
130
+ return {continue:true};
131
+ }
132
+ if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
133
+ const answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd});if(answer.deny)return answer;
134
+ if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)&&!existing)return {deny:true,reason:'Begin this strict prototype run with execution-begin before implementation or delivery.'};
135
+ }
136
+ if(!Object.keys(index.bindings).length)return {continue:true};
137
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
138
+ try{fs.mkdirSync(lock);}catch(error){if(error.code==='EEXIST')throw new Error('Concurrent audit callback; no success evidence recorded. Retry the stage.');throw error;}
139
+ try {
140
+ const fresh=JSON.parse(C.read(file));let changed=false;
141
+ for(const entry of Object.values(fresh.bindings)) {
142
+ if(entry.entryLine!==latest.line)continue;
143
+ const eventKey=E.sha(JSON.stringify([options.event,payload.tool_use_id||null,payload.tool_name||null,payload.tool_input||null,source.digest]));
144
+ if(entry.lastEventKey===eventKey)continue;
145
+ entry.source={path:source.path,bytes:source.bytes,digest:source.digest};entry.lastEventKey=eventKey;entry.lastHook={event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus};changed=true;
146
+ if(options.event==='Stop') {
147
+ const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
148
+ const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
149
+ const feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
150
+ if(feedback.feedback){entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return feedback;}
151
+ }
152
+ }
153
+ if(changed)saveIndex(stateDirectory,fresh);
154
+ }finally{fs.rmdirSync(lock);}
155
+ return {continue:true};
156
+ }
157
+ module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+ // Observed CodeBuddy IDE history format: Hook-provided index.json and only the
3
+ // UUID message files explicitly referenced by that index. No directory scan.
4
+ const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
5
+ const ID=/^[a-f0-9]{32}$/;
6
+ const aliases={use_skill:'Skill',execute_command:'Bash',write_to_file:'Write',read_file:'Read',replace_in_file:'Edit'};
7
+ function read(file) {
8
+ assert.equal(path.basename(file),'index.json','Expected native IDE index');
9
+ const directory=path.dirname(file),sessionId=path.basename(directory);
10
+ assert.match(sessionId,ID,'Invalid IDE session directory');
11
+ let total=0;
12
+ const load=target=>{
13
+ const stat=fs.lstatSync(target);assert.ok(stat.isFile()&&!stat.isSymbolicLink(),'Invalid IDE history file');
14
+ total+=stat.size;assert.ok(total<=64*1024*1024,'IDE history exceeds size limit');
15
+ return JSON.parse(fs.readFileSync(target,'utf8'));
16
+ };
17
+ const index=load(file);assert.ok(Array.isArray(index.messages)&&Array.isArray(index.requests),'Unsupported IDE index');
18
+ assert.ok(index.messages.length<=10000&&index.requests.length<=10000,'IDE history index exceeds limit');
19
+ const folder=path.join(directory,'messages');assert.ok(fs.lstatSync(folder).isDirectory()&&!fs.lstatSync(folder).isSymbolicLink(),'Invalid IDE message directory');
20
+ const requestIds=new Map(),seen=new Set(),events=[];let latestAssistant='',workspace;
21
+ for(const request of index.requests){
22
+ assert.match(request.id,ID,'Invalid IDE request ID');assert.ok(Array.isArray(request.messages),'Invalid IDE request messages');
23
+ for(const id of request.messages){assert.match(id,ID,'Invalid IDE message reference');assert.ok(!requestIds.has(id),'Duplicate IDE message request');requestIds.set(id,request.id);}
24
+ }
25
+ for(const item of index.messages){
26
+ assert.match(item.id,ID,'Invalid IDE message ID');assert.ok(!seen.has(item.id),'Duplicate IDE message ID');seen.add(item.id);
27
+ const row=load(path.join(folder,item.id+'.json'));
28
+ assert.equal(row.id,item.id,'IDE message identity differs');assert.equal(row.role,item.role,'IDE message role differs');
29
+ const message=JSON.parse(row.message),extra=JSON.parse(row.extra||'{}');
30
+ assert.equal(message.role,row.role,'IDE serialized message role differs');
31
+ assert.ok(requestIds.has(item.id),'Unbound IDE message');
32
+ // The index is the primary request membership. During execution the SDK can
33
+ // write message content before its duplicate extra.requestId metadata.
34
+ const requestId=requestIds.get(item.id);
35
+ if(extra.requestId!==undefined)assert.equal(extra.requestId,requestId,'IDE request identity differs');
36
+ assert.ok(Array.isArray(message.content),'Invalid IDE message content');
37
+ const base={sessionId,nativeMessageId:item.id,nativeRequestId:requestId};
38
+ if(row.role==='user'&&!extra.isHelperMessage){
39
+ const hostText=message.content[0]?.type==='text'?message.content[0].text:'';
40
+ const context=typeof hostText==='string'?hostText.match(/^<user_info>\s*([\s\S]*?)<\/user_info>/):null;
41
+ const nativeFolder=context?.[1].match(/^Workspace Folder:\s*(\/[^\r\n]+)$/m)?.[1]?.trim();
42
+ if(nativeFolder){
43
+ assert.ok(path.isAbsolute(nativeFolder)&&!/[\x00\r\n]/.test(nativeFolder),'Invalid native IDE workspace');
44
+ if(workspace)assert.equal(workspace,nativeFolder,'Native IDE workspace changed within one session');
45
+ workspace=nativeFolder;
46
+ }
47
+ assert.ok(Array.isArray(extra.sourceContentBlocks),'Original IDE user task is missing');
48
+ const text=extra.sourceContentBlocks.filter(b=>b.type==='text'&&typeof b.text==='string').map(b=>b.text).join('\n');
49
+ if(text.trim())events.push({...base,type:'user',message:{content:[{type:'text',text}]}});
50
+ }else if(row.role==='assistant'){
51
+ const text=message.content.filter(b=>b.type==='text').map(b=>b.text||'').join('\n');
52
+ if(text)latestAssistant=text;
53
+ for(const block of message.content.filter(b=>b.type==='tool-call')){
54
+ assert.ok(block.args&&typeof block.args==='object'&&!Array.isArray(block.args),'Invalid IDE tool arguments');
55
+ events.push({...base,type:'assistant',message:{content:[{type:'tool_use',id:block.toolCallId,name:aliases[block.toolName]||block.toolName,input:block.args}]}});
56
+ }
57
+ }else if(row.role==='tool'){
58
+ for(const block of message.content.filter(b=>b.type==='tool-result')){
59
+ const result=block.result,success=!block.isError&&result?.success===true&&result.status==='success';
60
+ events.push({...base,type:'user',message:{content:[{type:'tool_result',tool_use_id:block.toolCallId,
61
+ tool_name:aliases[block.toolName]||block.toolName,content:result,is_error:!success}]}});
62
+ }
63
+ }
64
+ }
65
+ // Only stable prompt/call/result facts form the prefix cursor. Streaming text,
66
+ // reasoning, request state and mutable toolStatus do not invalidate old facts.
67
+ return {text:events.map(row=>JSON.stringify(row)).join('\n')+'\n',latestAssistant,workspace,format:'codebuddy-ide-index-v1'};
68
+ }
69
+ module.exports={read};
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+ // Pure configuration planning. This module does not install or activate a host.
3
+ const assert = require('node:assert/strict');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+ const crypto = require('node:crypto');
7
+ const { digest } = require('./execution-policy.cjs');
8
+ const OWNER = 'i3-prototype-codebuddy-v1';
9
+ const EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'];
10
+ function quote(value) {
11
+ assert.ok(typeof value === 'string' && !/[\x00\r\n]/.test(value), 'Unsafe Hook argument');
12
+ return "'" + value.replaceAll("'", "'\\''") + "'";
13
+ }
14
+ function config(text) {
15
+ if (text === null) return {};
16
+ let value; try { value = JSON.parse(text); } catch { throw new Error('Settings must be valid JSON; preserve the original file'); }
17
+ assert.ok(value && !Array.isArray(value) && typeof value === 'object', 'Settings must be a JSON object');
18
+ assert.ok(!value.hooks || (typeof value.hooks === 'object' && !Array.isArray(value.hooks)), 'Invalid hooks configuration');
19
+ for (const groups of Object.values(value.hooks || {})) {
20
+ assert.ok(Array.isArray(groups), 'Hook groups must be arrays');
21
+ for (const group of groups) assert.ok(group && Array.isArray(group.hooks), 'Invalid Hook group');
22
+ }
23
+ return value;
24
+ }
25
+ function ownedMarker(group) { return group.hooks.some(hook => typeof hook.command === 'string' && hook.command.includes(OWNER)); }
26
+ function plan(options) {
27
+ const { action = 'keep', scope, product, base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
28
+ transcriptRoots = record?.transcriptRoots || [], transcriptMode = record?.transcriptMode || (transcriptRoots.length ? 'manual' : 'auto'), productVersion = record?.productVersion || 'unreported' } = options;
29
+ assert.ok(['keep', 'enable', 'disable'].includes(action), 'Invalid Hook action');
30
+ assert.ok(['project', 'global'].includes(scope), 'Invalid installation scope');
31
+ assert.ok(['ide', 'cli'].includes(product), 'Specify CodeBuddy product ide or cli');
32
+ for (const item of [base, project, nodePath, scriptPath]) assert.ok(typeof item === 'string' && path.isAbsolute(item) && !/[\x00\r\n]/.test(item), 'Absolute safe registration paths required');
33
+ if (process.platform === 'win32') throw new Error('CodeBuddy A+B Windows Hook quoting is not yet verified');
34
+ const before = config(currentText), after = structuredClone(before), activeRecord = record && record.owner === OWNER ? record : null;
35
+ if (record) {
36
+ assert.ok(activeRecord && activeRecord.schemaVersion === 1, 'Unknown registration ownership');
37
+ assert.equal(activeRecord.scope, scope, 'Registration scope changed');
38
+ assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
39
+ assert.equal(activeRecord.base, base, 'Registration base changed');
40
+ }
41
+ const found = {};
42
+ for (const [event, groups] of Object.entries(after.hooks || {})) {
43
+ for (const group of groups.filter(ownedMarker)) {
44
+ assert.ok(activeRecord?.groups[event], 'Hook ownership is missing; do not adopt an unknown command');
45
+ assert.deepEqual(group, activeRecord.groups[event], 'Owned Hook was modified; preserve the user changes');
46
+ assert.ok(!found[event], 'Duplicate owned Hook registration'); found[event] = group;
47
+ }
48
+ }
49
+ if (activeRecord) for (const event of Object.keys(activeRecord.groups)) assert.ok(found[event], 'Owned Hook registration changed or is missing');
50
+ const unchanged = action === 'keep' && !activeRecord;
51
+ let recordAfter = null;
52
+ if (!unchanged) {
53
+ after.hooks ||= {};
54
+ for (const event of Object.keys(found)) after.hooks[event] = after.hooks[event].filter(group => group !== found[event]);
55
+ if (action !== 'disable') {
56
+ assert.match(scriptDigest || '', /^[a-f0-9]{64}$/, 'Hook script digest required');
57
+ const groups = {};
58
+ for (const event of EVENTS) {
59
+ const argv = [nodePath, scriptPath, '--registration', OWNER, '--base', base, '--scope', scope, '--product', product, '--event', event];
60
+ const group = { ...(event.endsWith('ToolUse') ? { matcher: '' } : {}), hooks: [{ type: 'command', command: argv.map(quote).join(' '), timeout: 10 }] };
61
+ groups[event] = group; (after.hooks[event] ||= []).push(group);
62
+ }
63
+ assert.ok(Array.isArray(transcriptRoots)&&transcriptRoots.every(root=>typeof root==='string'&&path.isAbsolute(root)&&!/[\x00\r\n]/.test(root)),'Absolute native transcript roots required');
64
+ assert.ok(['auto','manual'].includes(transcriptMode),'Invalid native transcript mode');
65
+ assert.ok(transcriptMode==='manual'?transcriptRoots.length>0:transcriptRoots.length===0,'Manual transcript mode requires roots; auto uses only the native callback file');
66
+ recordAfter = { schemaVersion: 1, owner: OWNER, scope, product, base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
67
+ }
68
+ }
69
+ const afterText = unchanged ? currentText : JSON.stringify(after, null, 2) + '\n';
70
+ return { action, scope, product, base, project, settingsPath: path.join(base, 'settings.json'),
71
+ recordPath: path.join(base, 'state/i3-prototype/installation.json'), beforeDigest: digest(currentText), beforeRecordDigest: digest(record), afterText, recordAfter,
72
+ summary: { status: unchanged || action === 'disable' ? 'not-enabled' : product === 'cli' ? 'pending-host-review' : 'registered',
73
+ ...(recordAfter?{transcript:{mode:transcriptMode,status:'awaiting-native-event',invocationVerified:false}}:{}),
74
+ scope, product, settingsPath: path.join(base, 'settings.json'), events: recordAfter ? EVENTS : [], changed: afterText !== currentText || digest(recordAfter)!==digest(record),
75
+ next: product === 'cli' && recordAfter ? 'Review the external settings change in CodeBuddy /hooks; then start a new session.' : 'Actual Hook invocation must be checked in the host; registration is not activation.' } };
76
+ }
77
+ function safePath(file) {
78
+ let current = path.parse(path.resolve(file)).root;
79
+ for (const part of path.resolve(file).slice(current.length).split(path.sep)) {
80
+ current = path.join(current, part);
81
+ if (fs.existsSync(current)) assert.ok(!fs.lstatSync(current).isSymbolicLink(), 'Hook settings symlink refused');
82
+ }
83
+ }
84
+ const fileIO = {
85
+ lock(file) {safePath(file);fs.mkdirSync(path.dirname(file),{recursive:true});const lock=file+'.i3-prototype.lock';try{fs.mkdirSync(lock);}catch{throw new Error('Hook configuration is being updated; retry without overwriting the other change');}return ()=>fs.rmdirSync(lock);},
86
+ read(file) {
87
+ safePath(file); if (!fs.existsSync(file)) return null;
88
+ assert.ok(fs.statSync(file).isFile() && fs.statSync(file).size <= 4 * 1024 * 1024, 'Invalid or oversized Hook configuration');
89
+ return fs.readFileSync(file, 'utf8');
90
+ },
91
+ write(file, text, expected) {
92
+ assert.equal(digest(this.read(file)), expected, 'Hook configuration changed concurrently');
93
+ if (text === null) { if (fs.existsSync(file)) fs.unlinkSync(file); return; }
94
+ fs.mkdirSync(path.dirname(file), { recursive: true }); safePath(file);
95
+ const staging = file + '.' + crypto.randomUUID() + '.new';
96
+ try {
97
+ fs.writeFileSync(staging, text, { flag: 'wx', mode: fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600 });
98
+ assert.equal(digest(this.read(file)), expected, 'Hook configuration changed before publication');
99
+ fs.renameSync(staging, file);
100
+ } finally { if (fs.existsSync(staging)) fs.unlinkSync(staging); }
101
+ },
102
+ };
103
+ function apply(change, io = fileIO) {
104
+ const unlock=io.lock?io.lock(change.settingsPath):()=>{};
105
+ try {return applyLocked(change,io);}finally{unlock();}
106
+ }
107
+ function applyLocked(change, io) {
108
+ const before = io.read(change.settingsPath), recordBefore = io.read(change.recordPath);
109
+ assert.equal(digest(before), change.beforeDigest, 'Hook configuration changed after preview');
110
+ assert.equal(digest(recordBefore === null ? null : JSON.parse(recordBefore)), change.beforeRecordDigest, 'Hook ownership changed after preview');
111
+ const recordAfter = change.recordAfter ? JSON.stringify(change.recordAfter, null, 2) + '\n' : null;
112
+ if (change.afterText === before && recordAfter === recordBefore) return change.summary;
113
+ let settingsWritten = false;
114
+ try {
115
+ if (change.afterText !== before) { io.write(change.settingsPath, change.afterText, digest(before)); settingsWritten = true; }
116
+ if (recordAfter !== recordBefore) io.write(change.recordPath, recordAfter, digest(recordBefore));
117
+ } catch (error) {
118
+ if (settingsWritten && io.read(change.settingsPath) === change.afterText) io.write(change.settingsPath, before, digest(change.afterText));
119
+ throw error;
120
+ }
121
+ return change.summary;
122
+ }
123
+ module.exports = { OWNER, EVENTS, quote, config, plan, apply, fileIO };
@@ -56,8 +56,8 @@ function scopeInputs(snapshot, scope, contract) {
56
56
  assert.ok(page.regions?.length, `Declare the actual region modes: ${page.id}`);
57
57
  for (const region of page.regions) {
58
58
  assert.match(region.id || '', /^[a-z][a-z0-9-]{0,63}$/);
59
- assert.ok(['management-list', 'edit-form', 'content'].includes(region.mode), `Unknown business region mode: ${region.mode}`);
60
- regions.push({ pageId: page.id, id: region.id, mode: region.mode });
59
+ assert.ok(['management-list', 'edit-form', 'content'].includes(region.mode) || (scope.schemaVersion === 3 && require('./generation-contract.cjs').resource(snapshot, 'generation.json').patterns.includes(region.pattern || region.mode)), `Unknown business region mode: ${region.mode}`);
60
+ regions.push({ pageId: page.id, id: region.id, mode: scope.schemaVersion === 3 ? 'content' : region.mode });
61
61
  }
62
62
  }
63
63
  for (const region of regions) for (const capability of snapshot.definition.capabilities) {
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+ const assert=require('node:assert/strict'),E=require('./execution-evidence.cjs');
3
+ const METHODS=['evaluate_script','take_snapshot','list_console_messages','list_network_requests','performance_stop_trace'];
4
+ function validate(value,scenarioIds) {
5
+ assert.equal(value?.schemaVersion,1,'Fixed native assertions schema required');assert.ok(Array.isArray(value.scenarios),'Fixed native assertion scenarios required');
6
+ assert.deepEqual(value.scenarios.map(s=>s.scenarioId).sort(),[...scenarioIds].sort(),'Fixed native assertion scenario coverage differs');
7
+ for(const scenario of value.scenarios) {
8
+ assert.ok(scenario.checks?.length,'Each fixed scenario needs actual result assertions before implementation');
9
+ for(const check of scenario.checks) {
10
+ assert.ok(METHODS.includes(check.method),'Unsupported native assertion method');
11
+ assert.ok(typeof check.pointer==='string'&&/^\/(?:[^~]|~[01])*$/.test(check.pointer)&&!/(?:__proto__|constructor|prototype)/.test(check.pointer),'Safe JSON pointer required');
12
+ assert.ok(['equals','includes','gte','lte'].includes(check.operator),'Unsupported native assertion operator');
13
+ assert.ok(Object.hasOwn(check,'expected')&&check.expected!==null,'Explicit fixed expected fact required');
14
+ if(['gte','lte'].includes(check.operator))assert.equal(typeof check.expected,'number','Numeric fixed expectation required');
15
+ }
16
+ }
17
+ return value;
18
+ }
19
+ function resultData(result) {
20
+ const text=E.contentText(result).trim();let value;
21
+ try{value=JSON.parse(text);}catch{const blocks=[...text.matchAll(/```(?:json)?\s*\n([\s\S]*?)\n```/g)];assert.equal(blocks.length,1,'One actual JSON result or JSON code block required');value=JSON.parse(blocks[0][1]);}
22
+ assert.ok(value&&typeof value==='object','Native JSON fact result required');return value;
23
+ }
24
+ function extract(value,pointer) {
25
+ for(const part of pointer.slice(1).split('/').map(p=>p.replaceAll('~1','/').replaceAll('~0','~'))) {assert.ok(value&&Object.hasOwn(value,part),'Native expected fact is absent');value=value[part];}return value;
26
+ }
27
+ function compare(actual,check) {
28
+ if(check.operator==='equals')assert.deepEqual(actual,check.expected,'Native fact differs from fixed expectation');
29
+ else if(check.operator==='includes')assert.ok((typeof actual==='string'||Array.isArray(actual))&&actual.includes(check.expected),'Native fact does not include fixed expectation');
30
+ else {assert.equal(typeof actual,'number','Native numeric fact required');assert.ok(check.operator==='gte'?actual>=check.expected:actual<=check.expected,'Native numeric fact differs from fixed expectation');}
31
+ }
32
+ function template(options) {
33
+ const fs=require('node:fs'),path=require('node:path'),T=require('./template-contract.cjs');
34
+ assert.ok(options.scope,'Sourced scope file required');const scope=JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'));
35
+ const snapshot=options.templateRoot?T.loadTemplate(path.resolve(options.templateRoot)):T.loadBundledTemplate(options.template);
36
+ const selection=snapshot.kind==='content-reference'?require('./content-template.cjs').scopeInputs(snapshot,scope,T):null;
37
+ const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(snapshot,scope):null;
38
+ const scenarios=[...(selection?.result.requiredScenarios||snapshot.index.requiredScenarios),...(generation?.scenarios||[])];
39
+ const drawer=selection&&(selection.fixed.regions.some(r=>r.mode==='edit-form')||generation?.bindings.some(b=>b.capabilityId==='dialog'&&b.parameters.presentation==='drawer'));
40
+ const framework=selection?['framework-responsive','framework-instance-isolation','template-content-projection',...(drawer?['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'];
41
+ return {schemaVersion:1,scopeDigest:E.sha(JSON.stringify(scope)),templateDigest:snapshot.digest,scenarios:[...scenarios.map(s=>({scenarioId:typeof s==='string'?s:s.id,expectedSummary:s.expected||null,checks:[]})),...framework.map(scenarioId=>({scenarioId,checks:[]}))]};
42
+ }
43
+ module.exports={validate,resultData,extract,compare,template};