diff --git a/plugins/hetaoBackend/mcode-dynamic-workflows/VERIFICATION.md b/plugins/hetaoBackend/mcode-dynamic-workflows/VERIFICATION.md index 789d7f4..f5f9e85 100644 --- a/plugins/hetaoBackend/mcode-dynamic-workflows/VERIFICATION.md +++ b/plugins/hetaoBackend/mcode-dynamic-workflows/VERIFICATION.md @@ -16,3 +16,16 @@ Process lifecycle regression checks use real, bounded Node CLI/descendant fixtur Additional CI review: three focused dependency-boundary checks cover the exact CodeQL findings documented in `SECURITY_REVIEW.md`. The two failing repository Python argument-validation tests also pass locally with Pillow installed. CI now explicitly installs Pillow and a CJK font; Ubuntu confirmation comes from the PR check results. Not verified: paid model execution, account authorization, real Windows/Linux MCode installation, or every supported host/plugin-loader version. Passing these checks does not establish correctness of model-generated findings or safety of side effects initiated by an authorized agent task. + +## Mechanical claims + +The machine-recheckable claims are FIXED ARGV DATA in `scripts/verify-claims.mjs` (spawned directly, no shell; `node` resolves to the running executable). Run `node scripts/verify-claims.mjs` from the plugin directory: one PASS/FAIL line per claim, exit 0 only when every claim matches its expected exit status (1 on the first mismatch, 2 on a tool error). The table below is a human-readable **mirror** of that data; `checks/claims.check.mjs` strictly validates the mirror (header, order, uniqueness, columns, full consumption — any malformed or smuggled row fails the suite). V-02/V-03 need development dependencies (`npm ci` first); V-04 runs after V-03 on a committed tree and detects drifted assets. Portability: POSIX/macOS (direct spawn of node/npm/git; Windows npm.cmd resolution is not claimed). Prose claims that are not mechanically expressible intentionally stay prose. + +```verify +| id | command | expect | +|----|---------|--------| +| V-01 | node --test test/package.test.mjs | exit 0 | +| V-02 | npm test | exit 0 | +| V-03 | npm run build | exit 0 | +| V-04 | git diff --exit-code -- dist web THIRD_PARTY_NOTICES.txt | exit 0 | +``` diff --git a/plugins/hetaoBackend/mcode-dynamic-workflows/checks/claims.check.mjs b/plugins/hetaoBackend/mcode-dynamic-workflows/checks/claims.check.mjs new file mode 100644 index 0000000..5149980 --- /dev/null +++ b/plugins/hetaoBackend/mcode-dynamic-workflows/checks/claims.check.mjs @@ -0,0 +1,37 @@ +import test from 'node:test';import assert from 'node:assert/strict';import {readFile} from 'node:fs/promises'; +import {CLAIMS,parseMirror} from '../scripts/verify-claims.mjs'; +const mirror=async()=>parseMirror(await readFile('VERIFICATION.md','utf8')); +const wrap=rows=>['```verify','| id | command | expect |','|----|---------|--------|',...rows,'```'].join('\n'); +test('the VERIFICATION.md mirror equals the executable claims exactly, in order',async()=>{ + const rows=await mirror(); + assert.deepEqual(rows.map(r=>({id:r.id,display:r.display,expect:r.expect})),CLAIMS.map(c=>({id:c.id,display:c.display,expect:c.expect}))); +}); +test('claims data is internally valid',()=>{ + assert.ok(CLAIMS.length>=1);assert.deepEqual(CLAIMS.map(c=>c.id),[...new Set(CLAIMS.map(c=>c.id))],'ids unique'); + for(const c of CLAIMS){assert.match(c.id,/^[A-Z][A-Z0-9-]*$/);assert.ok(Array.isArray(c.argv)&&c.argv.length>0);assert.ok(Number.isInteger(c.expect));} +}); +test('negative: a malformed header is rejected, not skipped',()=>{ + const md=['```verify','| id | command | wanted |','|----|---------|--------|','| V-01 | npm test | exit 0 |','```'].join('\n'); + assert.throws(()=>parseMirror(md),/bad header/); +}); +test('negative: an unparseable row fails the whole parse (no silent omission)',()=>{ + const md=wrap(['| V-01 | npm test | exit zero |']); + assert.throws(()=>parseMirror(md),/unparseable row/); +}); +test('negative: duplicate IDs are rejected',()=>{ + const md=wrap(['| V-01 | npm test | exit 0 |','| V-01 | npm test | exit 0 |']); + assert.throws(()=>parseMirror(md),/duplicate id/); +}); +test('negative: a smuggled extra row beyond the claims data breaks mirror equality',async()=>{ + const rows=await mirror(); + assert.notDeepEqual([...rows.map(r=>r.id),'V-99'],CLAIMS.map(c=>c.id)); +}); +test('negative: reordered mirror rows break equality even with identical members',async()=>{ + const rows=await mirror(); + if(rows.length<2) return; + const reordered=[...rows.slice(1),rows[0]]; + assert.notDeepEqual(reordered.map(r=>r.id),CLAIMS.map(c=>c.id)); +}); +test('negative: a missing verify block is an error',()=>{ + assert.throws(()=>parseMirror('# no block here'),/no .*verify block/); +}); diff --git a/plugins/hetaoBackend/mcode-dynamic-workflows/checks/examples.check.mjs b/plugins/hetaoBackend/mcode-dynamic-workflows/checks/examples.check.mjs new file mode 100644 index 0000000..739aec2 --- /dev/null +++ b/plugins/hetaoBackend/mcode-dynamic-workflows/checks/examples.check.mjs @@ -0,0 +1,22 @@ +import test from 'node:test';import assert from 'node:assert/strict';import {mkdtemp,rm,readFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os';import {join} from 'node:path';import {setTimeout as delay} from 'node:timers/promises'; +import {Store} from '../src/store.mjs';import {Engine} from '../src/engine.mjs';import {validateScript} from '../src/common.mjs'; +async function fixture(execute){const dir=await mkdtemp(join(tmpdir(),'wf-examples-'));const store=new Store(dir),engine=new Engine(store,{workspace:dir,execute});return {dir,store,engine,cleanup:async()=>{await engine.close();store.close();await rm(dir,{recursive:true,force:true});}};} +async function finish(engine,id){for(let i=0;i<300;i++){if(!engine.active.has(id))return engine.snapshot(id);await delay(20);}throw Error('timeout');} +test('reflection example: independent agents each receive the original task and material (input contract)',async()=>{ + const script=(await readFile('examples/reflection.js','utf8')); + assert.deepEqual(validateScript(script),{valid:true,scriptHash:validateScript(script).scriptHash,dslVersion:1}); + const inputs=[];const f=await fixture(async s=>{inputs.push({id:s.id,input:s.input});return {output:{text:`${s.id} text`,openIssues:[]}};}); + try{ + const started=await f.engine.start({requestId:crypto.randomUUID(),name:'Reflection example',executor:'demo',script,input:{task:'write a release note',material:'changelog.md contents'}}); + await f.engine.approve(started.id,{revision:1});const end=await finish(f.engine,started.id); + assert.equal(end.status,'succeeded',end.error); + const by={};for(const {id,input} of inputs)by[id]=input; + for(const id of ['draft','critique','revise'])assert.ok(by[id],`${id} executed`); + for(const id of ['draft','critique','revise']){assert.equal(by[id].task,'write a release note',`${id} gets the original task`);assert.equal(by[id].material,'changelog.md contents',`${id} gets the original material`);} + assert.ok(by.critique.draft,'critique sees the draft');assert.equal(by.critique.draft.text,'draft text'); + assert.ok(by.revise.draft&&by.revise.critique,'revision sees draft and critique'); + assert.ok(by.revise.critique.text==='critique text','revision can independently check the critique against the source'); + assert.deepEqual(end.result,{text:'revise text',openIssues:[],critiqueCount:0}); + }finally{await f.cleanup();} +}); diff --git a/plugins/hetaoBackend/mcode-dynamic-workflows/examples/reflection.js b/plugins/hetaoBackend/mcode-dynamic-workflows/examples/reflection.js new file mode 100644 index 0000000..cb99623 --- /dev/null +++ b/plugins/hetaoBackend/mcode-dynamic-workflows/examples/reflection.js @@ -0,0 +1,18 @@ +await ctx.phase({id:'draft',label:'初稿'}); +await ctx.phase({id:'critique',label:'独立批判'}); +await ctx.phase({id:'revise',label:'修订定稿'}); +const schema={type:'object',properties:{text:{type:'string'},openIssues:{type:'array',items:{type:'string'}}},required:['text','openIssues'],additionalProperties:false}; +await ctx.log('反思编队:初稿、批判、修订由互相独立的 Agent 承担;批判与修订都拿到原始任务与材料。',{phase:'draft'}); +const draft=await ctx.agent({id:'draft',label:'初稿',phase:'draft',schema, + prompt:'根据输入任务与材料写出一版初稿。证据不足之处如实写入 openIssues,不要编造。',input:{task:input.task,material:input.material}}); +if(draft.status!=='succeeded')throw Error(draft.error); +await ctx.checkpoint('draft-snapshot',draft.output); +await ctx.log('初稿完成并冻结快照,进入独立批判。',{stepId:'critique',phase:'critique'}); +const critique=await ctx.agent({id:'critique',label:'独立批判',phase:'critique',dependsOn:['draft'],schema, + prompt:'只挑毛病:核对初稿与原始任务和材料,指出无证据的断言、任务覆盖缺口与遗漏,写入 openIssues;不要重写初稿。',input:{task:input.task,material:input.material,draft:draft.output}}); +if(critique.status!=='succeeded')throw Error(critique.error); +const revise=await ctx.agent({id:'revise',label:'修订定稿',phase:'revise',dependsOn:['draft','critique'],schema, + prompt:'针对批判意见逐条修订初稿;修订须对照原始任务与材料独立核验每条批判是否有据,不采纳的意见保留在 openIssues 中并说明理由。',input:{task:input.task,material:input.material,draft:draft.output,critique:critique.output}}); +if(revise.status!=='succeeded')throw Error(revise.error); +await ctx.log(`定稿完成,遗留问题 ${revise.output.openIssues.length} 条。`,{stepId:'revise',phase:'revise'}); +return {text:revise.output.text,openIssues:revise.output.openIssues,critiqueCount:critique.output.openIssues.length}; diff --git a/plugins/hetaoBackend/mcode-dynamic-workflows/scripts/verify-claims.mjs b/plugins/hetaoBackend/mcode-dynamic-workflows/scripts/verify-claims.mjs new file mode 100644 index 0000000..45d731f --- /dev/null +++ b/plugins/hetaoBackend/mcode-dynamic-workflows/scripts/verify-claims.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +// Mechanical claims runner. The executable claims are FIXED ARGV DATA in this +// file — never parsed out of the documentation. Commands are spawned directly +// (no shell), `node` is resolved to the running executable for portability. +// VERIFICATION.md carries a human-readable mirror of this table; checks/claims.check.mjs +// strictly validates that mirror against this data (any drift, malformed, duplicate, +// or smuggled row fails the suite). +// Exit codes: 0 all claims pass; 1 first mismatch; 2 tool/claims-definition error. +// Portability: runs wherever node, npm, and git are directly spawnable (POSIX/macOS; +// Windows needs npm.cmd resolution and is not claimed). +import {spawnSync} from 'node:child_process'; +import {fileURLToPath, pathToFileURL} from 'node:url'; +import {realpathSync} from 'node:fs'; +import {join,dirname} from 'node:path'; + +export const CLAIMS = [ + {id:'V-01', expect:0, argv:[process.execPath,'--test','test/package.test.mjs'], display:'node --test test/package.test.mjs'}, + {id:'V-02', expect:0, argv:['npm','test'], display:'npm test'}, + {id:'V-03', expect:0, argv:['npm','run','build'], display:'npm run build'}, + {id:'V-04', expect:0, argv:['git','diff','--exit-code','--','dist','web','THIRD_PARTY_NOTICES.txt'], display:'git diff --exit-code -- dist web THIRD_PARTY_NOTICES.txt'}, +]; + +// Strict parser for the VERIFICATION.md mirror table. Throws on ANY anomaly: +// missing block, wrong header, malformed separator, unparseable row, wrong column +// count, duplicate IDs, or rows that are not exact CLAIMS members in order. +// Nothing is ever silently skipped. +const ROW = /^\| ([A-Z][A-Z0-9-]*) \| (.+?) \| exit (\d+) \|$/; +const HEADER = '| id | command | expect |'; +const SEPARATOR = '|----|---------|--------|'; +export function parseMirror(markdown) { + const block = markdown.match(/```verify\r?\n([\s\S]*?)```/); + if (!block) throw new Error('mirror: no ```verify block found'); + const lines = block[1].replace(/\r/g,'').split('\n'); + if (lines[0] !== HEADER) throw new Error(`mirror: bad header: ${JSON.stringify(lines[0])}`); + if (lines[1] !== SEPARATOR) throw new Error(`mirror: bad separator: ${JSON.stringify(lines[1])}`); + const seen = new Set(); const rows = []; + for (let i = 2; i < lines.length; i++) { + const line = lines[i]; + if (line === '' && i === lines.length - 1) continue; // trailing newline only + const m = ROW.exec(line); + if (!m) throw new Error(`mirror: unparseable row ${i+1}: ${JSON.stringify(line)}`); + if (seen.has(m[1])) throw new Error(`mirror: duplicate id ${m[1]}`); + seen.add(m[1]); + rows.push({id:m[1], display:m[2], expect:Number(m[3])}); + } + if (!rows.length) throw new Error('mirror: no claim rows'); + return rows; +} + +const invokedDirectly = (() => { try { return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href; } catch { return false; } })(); +if (invokedDirectly) { + const root = join(dirname(fileURLToPath(import.meta.url)),'..'); + for (const claim of CLAIMS) { + const result = spawnSync(claim.argv[0], claim.argv.slice(1), {cwd: root, stdio: 'inherit'}); + if (result.error) { console.error(`[verify-claims] ERROR ${claim.id}: ${result.error.message}`); process.exit(2); } + if (result.status !== claim.expect) { console.error(`[verify-claims] FAIL ${claim.id}: ${claim.display} (exit ${result.status}, expected ${claim.expect})`); process.exit(1); } + console.log(`[verify-claims] PASS ${claim.id}`); + } + console.log(`[verify-claims] ${CLAIMS.length} claims verified`); +} \ No newline at end of file