@antprofuse/saddle-islands-tools 0.1.6 → 0.1.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antprofuse/saddle-islands-tools",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Independent Saddle–Islands engineering tools",
5
5
  "files": [
6
6
  "tools"
package/tools/cli/main.py CHANGED
@@ -22,12 +22,12 @@ def main():
22
22
  if len(sys.argv) > 1 and sys.argv[1] == 'minifish':
23
23
  args = sys.argv[2:]
24
24
  if not args or args == ['--help']:
25
- print('minifish build|package|plugin|batch|run|report <explicit options>; see tools/docs/minifish-testing.md')
25
+ print('minifish compile-check|build|package|plugin|batch|run|report <explicit options>; see tools/docs/minifish-testing.md')
26
26
  return 0
27
27
  command = args[0]
28
- if command not in ('build', 'package', 'plugin', 'batch', 'run', 'report'):
28
+ if command not in ('compile-check', 'build', 'package', 'plugin', 'batch', 'run', 'report'):
29
29
  raise SystemExit('Unknown minifish command: ' + command)
30
- entry = 'minifish.mjs' if command in ('build', 'package', 'plugin') else 'minifish.py'
30
+ entry = 'minifish.mjs' if command in ('compile-check', 'build', 'package', 'plugin') else 'minifish.py'
31
31
  return subprocess.call(['node' if entry.endswith('.mjs') else sys.executable, str(Path(__file__).with_name(entry)), *args])
32
32
  parser = argparse.ArgumentParser(description=__doc__)
33
33
  parser.add_argument('command', choices=['identity', 'backend', 'minifish', 'iteration'], help='semantic identities, backend tests or Minifish tests')
@@ -8,7 +8,7 @@ import {createRequire,registerHooks} from 'node:module';
8
8
  import {pathToFileURL} from 'node:url';
9
9
  const [command,...args]=process.argv.slice(2),flags={};
10
10
  for(let i=0;i<args.length;i+=2){assert(args[i]?.startsWith('--')&&args[i+1],'Expected named option and value');assert(!Object.hasOwn(flags,args[i].slice(2)),'Duplicate option');flags[args[i].slice(2)]=args[i+1];}
11
- assert(['build','package','plugin'].includes(command),'Usage: minifish.mjs build|package|plugin --project-root ROOT --config FILE --output DIR --definition CASE | --plan JSON [--tool-node-modules DIR]');
11
+ assert(['compile-check','build','package','plugin'].includes(command),'Usage: minifish.mjs build|package|plugin --project-root ROOT --config FILE --output DIR --definition CASE | --plan JSON [--tool-node-modules DIR]');
12
12
  assert(flags['project-root']&&flags.config&&flags.output,'Explicit project root, config and output required');
13
13
  if(!process.execArgv.includes('--experimental-vm-modules')){const child=spawnSync(process.execPath,['--experimental-vm-modules',process.argv[1],...process.argv.slice(2)],{stdio:'inherit',env:process.env});process.exit(child.status??1);}
14
14
  const packageRoot=path.resolve(import.meta.dirname,'../..');
@@ -25,14 +25,20 @@ if(flags['tool-node-modules']){
25
25
  registerHooks({resolve(specifier,context,next){if(['typescript','@playwright/test'].includes(specifier)){let file=require.resolve(specifier);if(specifier==='@playwright/test')file=path.join(path.dirname(file),'index.mjs');return {url:pathToFileURL(file).href,shortCircuit:true};}return next(specifier,context);}});
26
26
  }
27
27
  registerHooks({resolve(specifier,context,next){const prefix='@antprofuse/saddle-islands-tools/';if(specifier.startsWith(prefix)){const file=path.resolve(packageRoot,specifier.slice(prefix.length));assert(file.startsWith(packageRoot+path.sep),'Public package import escaped');return {url:pathToFileURL(file).href,shortCircuit:true};}return next(specifier,context);}});
28
- const {buildCase,buildPlanPackage}=await import('../modules/minifish/compiler/build-case.mjs');
29
28
  const options={projectRoot:path.resolve(flags['project-root']),config:path.resolve(flags.config)};
30
29
  let result;
31
- if(command==='plugin'){
30
+ if(command==='compile-check'){
31
+ const {compileCheck}=await import('../modules/minifish/compiler/compile-check.mjs');
32
+ result=await compileCheck({...options,manifest:flags.manifest,output:flags.output});
33
+ if(result.failed)process.exitCode=3;
34
+ }else if(command==='plugin'){
32
35
  const {loadProject}=await import('../modules/minifish/compiler/config.mjs');
33
36
  const project=await loadProject(options.projectRoot,options.config);
34
37
  const {validatePluginEvents}=await import(pathToFileURL(await project.resolveAdapter(project.config.pluginAdapter)).href);
35
38
  const {buildPluginArtifact}=await import('../modules/minifish/harness/build-plugin-artifact.mjs');
36
39
  result=await buildPluginArtifact({planFile:flags.plan,output:flags.output,modules:flags['sdk-node-modules'],source:flags.source,repositoryRoot:path.resolve(project.root,project.config.repositoryRoot),validatePluginEvents});
37
- }else result=command==='build'?await buildCase(flags.definition,flags.output,options):await buildPlanPackage(JSON.parse(await readFile(flags.plan,'utf8')),flags.output,options);
40
+ }else{
41
+ const {buildCase,buildPlanPackage}=await import('../modules/minifish/compiler/build-case.mjs');
42
+ result=command==='build'?await buildCase(flags.definition,flags.output,options):await buildPlanPackage(JSON.parse(await readFile(flags.plan,'utf8')),flags.output,options);
43
+ }
38
44
  console.log(JSON.stringify(result));
@@ -10,8 +10,12 @@ python3 <frozen-package>/tools/cli/main.py backend --project <suite-root> --repo
10
10
  npm run selftest --prefix <frozen-package>/tools/modules/backend
11
11
  ```
12
12
 
13
+ 运行器消费已构建的可执行制品,不负责选择或安装Rust工具链。发起者为构建阶段准备框架要求的工具链及可解析的cargo/rustc入口,核版本后交接执行环境;不能依赖其他workspace的隐含PATH。
14
+
13
15
  所有根、适配、协议、Service、启动 app/alias 与制品均为显式输入。适配和协议预检真实路径,拒绝未声明根、符号链接逃逸、缺配置或缺制品。项目源依据与 Islands 公开生成工具仍由项目声明;本包不复制 Islands 或 Saddle 运行时。
14
16
 
17
+ `--out`须位于业务仓外且最终目录尚不存在。工具自动创建缺失父目录,但拒绝复用已有最终目录(即使为空),避免混入历史证据;目录准备失败时不启动业务进程。
18
+
15
19
  ## 适配合同
16
20
 
17
21
  适配模块导出 `project(projectRoot)`,返回 `scenarios`、`proto`、`rpc`、`process`、`frozen`、`inputFiles`、`database`、`databaseLogPolicy`、`prepare` 和 `execute`。
@@ -61,3 +61,5 @@ python3 tools/cli/main.py iteration archive \
61
61
  `kind`为reports或records。目标在持久迭代目录下,以新内容版本命名,不覆盖原件。逐文件摘要校验复制一致,拒绝符号链接,检查静态HTML/CSS引用是否缺失或逃出归档目录;保存归档清单到records/archives。不会生成ZIP,不删除源目录。源需已停止写入;先后摘要不同则拒绝交付。
62
62
 
63
63
  不执行或解析JavaScript生成的动态URL、不下载外部HTTP资源,也不证明图片能够解码或预览服务可达。最终检查点须实际打开用户预览链接、检查截图;动态资源在浏览器中核验。服务与持久文件分别管理。结束CoTask前归档并核验,不能结束后才尝试复制已回收文件。
64
+
65
+ MT(minifish-test)的stage-gate另需 `--compile-result`,指向当前候选通过公开 `minifish compile-check` 生成的全量编译收据。它与Git路径范围检查共同构成交付门禁,不启动产品;命令及清单格式见minifish-testing.md。
@@ -42,3 +42,11 @@ Legacy component-host integrations must supply an explicit `repositoryRoot` to `
42
42
  可选 `carrier` 默认为 `semantic-root`。`source-content-group` 在语义root内对可见文字/图像取最低公共元素祖先;`owned-text-block` 在该组内要求唯一不属于其他语义目标的文字分支。选择与期待样式、class、原型HTML标签无关,候选缺失或歧义明确失败;不适用的拓扑不得借此猜测观察目标。`viewportInsets` 可声明 left/right/top/bottom 到业务frame视口的设计长度,返回采样记录包含原root盒及是否选中root。
43
43
 
44
44
  按压快照目前只支持 `semantic-root`,其他carrier在按下前拒绝,不能释放后采样冒充按住状态。这是明确能力限制,不能默默降级。内容分组不覆盖背景装饰/无文字无图像部件或Shadow DOM;相应责任另用适用的公开观察合同。
45
+
46
+ ### 全量定义编译门禁
47
+
48
+ `minifish compile-check --tool-source TRUSTED_MANIFEST --project-root PROJECT --config PROJECT/minifish.config.json --manifest PROJECT/catalog/case-map.json --output NEW_EXTERNAL_DIR --tool-node-modules COMPILER/node_modules` 使用项目已声明的编译适配器,逐项检查当前活动定义、来源责任和准备祖先,不启动产品或模拟器,不生成独立运行包。
49
+
50
+ 项目配置的 `caseManifest` 指定唯一正式活动清单,命令的 `--manifest` 必须与其一致。活动清单包含 `cases`(definition、sourceCaseIds;定义路径为编译条目的唯一标识)及可选 `pending`(sourceCaseId、reason)。活动与非执行责任必须互斥、唯一且覆盖配置中的完整catalog。历史未激活定义不隐式加入;不可执行原因仍需Owner审核,不计编译通过或业务通过。输入须为干净候选提交内的文件。结果 `compile-result.json` 绑定候选提交、项目树、清单和目录摘要、已验证工具身份,并含逐case结果;有编译错误返回非零。
51
+
52
+ MT使用 `iteration stage-gate` 时必须提供 `--compile-result NEW_EXTERNAL_DIR/compile-result.json`,缺失、过期、漏项、身份不匹配或编译失败均拒绝交付;其他阶段不增加此检查。
@@ -0,0 +1,36 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {mkdtemp,mkdir,writeFile,readFile,rm,access} from 'node:fs/promises';
4
+ import {tmpdir} from 'node:os';
5
+ import {join} from 'node:path';
6
+ import {createExternalOutput} from './index.mjs';
7
+ test('fresh nested output is created; prior evidence and repository paths are rejected',async()=>{
8
+ const root=await mkdtemp(join(tmpdir(),'backend-output-'));
9
+ try {
10
+ const repo=join(root,'repo');await mkdir(repo);
11
+ const out=join(root,'missing','nested','run');assert.equal(await createExternalOutput(repo,out),out);
12
+ await assert.rejects(createExternalOutput(repo,out),{code:'EEXIST'});
13
+ await writeFile(join(out,'evidence.json'),'original');
14
+ await assert.rejects(createExternalOutput(repo,out),{code:'EEXIST'});
15
+ assert.equal(await readFile(join(out,'evidence.json'),'utf8'),'original');
16
+ const illegal=join(repo,'new','run');await assert.rejects(createExternalOutput(repo,illegal),/outside source repository/);
17
+ await assert.rejects(access(join(repo,'new')),{code:'ENOENT'});
18
+ const file=join(root,'file');await writeFile(file,'unchanged');
19
+ await assert.rejects(createExternalOutput(repo,join(file,'run')));
20
+ assert.equal(await readFile(file,'utf8'),'unchanged');
21
+ } finally {await rm(root,{recursive:true,force:true});}
22
+ });
23
+
24
+ test('runner rejects reused output before starting RPC or product',async()=>{
25
+ const {run}=await import('./run.mjs');
26
+ const root=await mkdtemp(join(tmpdir(),'backend-preflight-'));
27
+ try {
28
+ const repo=join(root,'repo');await mkdir(repo);
29
+ const proto=join(repo,'input.proto');await writeFile(proto,'intentionally invalid proto');
30
+ const adapter=join(repo,'adapter.mjs');
31
+ await writeFile(adapter,`export async function project(){return {rpc:{serviceName:'Synthetic'},proto:${JSON.stringify(proto)},process:{app:'synthetic',alias:'test'},inputFiles:[${JSON.stringify(proto)}],scenarios:[{id:'one'}],prepare(){},execute(){throw Error('business must not start')}}}`);
32
+ const out=join(root,'existing');await mkdir(out);
33
+ await assert.rejects(run({projectRoot:repo,repositoryRoot:repo,adapter,out,binary:process.execPath}),{code:'EEXIST'});
34
+ await assert.rejects(access(join(out,'report.json')),{code:'ENOENT'});
35
+ } finally {await rm(root,{recursive:true,force:true});}
36
+ });
@@ -8,7 +8,7 @@ import {within,preflight} from './paths.mjs';
8
8
  import {scenarioActions} from './actions.mjs';
9
9
  import {mock} from './grpc.mjs';
10
10
  import {product} from './product.mjs';
11
- import {externalOutput,sleep} from './util.mjs';
11
+ import {createExternalOutput,sleep} from './util.mjs';
12
12
  import {requestLogs} from './logs.mjs';
13
13
  import {assertRouteRejection} from './rejection-logs.mjs';
14
14
  import {classifyReport} from './report.mjs';
@@ -25,8 +25,8 @@ export async function run({projectRoot,repositoryRoot,adapter,out,binary,caseId,
25
25
  const all=config.scenarios;assert(Array.isArray(all)&&all.length,'empty scenarios');assert.equal(new Set(all.map(s=>s.id)).size,all.length,'duplicate scene id');
26
26
  const selected=caseId?all.filter(s=>s.id===caseId):all;assert(selected.length,'unknown case');
27
27
  if(list){await new Promise(done=>process.stdout.write(JSON.stringify(all.map(({id,operation,basis,scope})=>({id,operation,basis,scope:scope??'business'})),null,2)+'\n',done));return;}
28
- assert(binary&&out,'real binary and fresh external output required');await access(resolve(binary),constants.X_OK);await access(config.proto);const destination=externalOutput(repositoryRoot,out);
29
- await mkdir(destination);const report={...config.frozen,tool:identity,projectInputs,scenarioSetSha256:createHash('sha256').update(JSON.stringify(all)).digest('hex'),started:new Date().toISOString(),scenarios:[],database:config.database};
28
+ assert(binary&&out,'real binary and fresh external output required');await access(resolve(binary),constants.X_OK);await access(config.proto);const destination=await createExternalOutput(repositoryRoot,out);
29
+ const report={...config.frozen,tool:identity,projectInputs,scenarioSetSha256:createHash('sha256').update(JSON.stringify(all)).digest('hex'),started:new Date().toISOString(),scenarios:[],database:config.database};
30
30
  let rpc,app,stage='startup';const abort=new AbortController();const listeners=new Map();
31
31
  for(const signal of ['SIGINT','SIGTERM']){const handler=()=>{report.interrupted=signal;abort.abort();rpc?.release();};listeners.set(signal,handler);process.once(signal,handler);}
32
32
  try{
@@ -1,7 +1,7 @@
1
1
  import {spawn} from 'node:child_process';
2
2
  import {createServer} from 'node:net';
3
3
  import {mkdir,open} from 'node:fs/promises';
4
- import {resolve,relative} from 'node:path';
4
+ import {resolve,relative,dirname} from 'node:path';
5
5
  import {setTimeout as sleep} from 'node:timers/promises';
6
6
  export {sleep};
7
7
  export async function port() {
@@ -22,3 +22,11 @@ export async function command(binary,args,dir,env={}) {
22
22
  }
23
23
  export async function until(fn,timeout=30000) { const end=Date.now()+timeout;let error;while(Date.now()<end){try{return await fn();}catch(e){error=e;await sleep(100);}}throw error ?? Error('readiness timeout'); }
24
24
  export function externalOutput(project,out) { const p=resolve(out);const rel=relative(resolve(project),p);if(rel!=='..'&&!rel.startsWith('../')) throw Error('output must be outside source repository');return p; }
25
+
26
+ export async function createExternalOutput(repository,out) {
27
+ const destination=externalOutput(repository,out);
28
+ await mkdir(dirname(destination),{recursive:true});
29
+ // Keep the final creation exclusive: existing evidence must never be reused.
30
+ await mkdir(destination);
31
+ return destination;
32
+ }
@@ -0,0 +1,65 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import {readFile,realpath,mkdir,writeFile} from 'node:fs/promises';
4
+ import {execFileSync} from 'node:child_process';
5
+ import {createHash} from 'node:crypto';
6
+ import {pathToFileURL} from 'node:url';
7
+ import {performance} from 'node:perf_hooks';
8
+ import {loadProject,checkOutput} from './config.mjs';
9
+ import {toolIdentity} from './tool-identity.mjs';
10
+ const sha=x=>createHash('sha256').update(x).digest('hex');
11
+ export function activeCases(manifest,catalog){
12
+ assert(Array.isArray(manifest.cases)&&manifest.cases.length,'Nonempty active case manifest required');
13
+ const files=new Set(),covered=new Set();
14
+ const official=new Set(catalog.cases.map(c=>c.id));
15
+ assert.equal(official.size,catalog.cases.length,'Duplicate catalog responsibility');
16
+ for(const row of manifest.cases){
17
+ assert(typeof row.definition==='string'&&row.definition&&!files.has(row.definition),'Duplicate/missing definition');files.add(row.definition);
18
+ assert(Array.isArray(row.sourceCaseIds)&&row.sourceCaseIds.length,'Source responsibilities required');
19
+ for(const id of row.sourceCaseIds){assert(official.has(id)&&!covered.has(id),'Unknown/duplicate source responsibility');covered.add(id);}
20
+ }
21
+ for(const row of manifest.pending??[]){assert(typeof row.reason==='string'&&row.reason.trim(),'Nonexecutable responsibility requires reason');assert(official.has(row.sourceCaseId)&&!covered.has(row.sourceCaseId),'Unknown/duplicate pending responsibility');covered.add(row.sourceCaseId);}
22
+ assert.equal(covered.size,official.size,'Active manifest omits catalog responsibilities');
23
+ return manifest.cases;
24
+ }
25
+ export async function compileCheck(options){
26
+ const started=performance.now(),project=await loadProject(options.projectRoot,options.config),{root,config}=project;
27
+ const repo=await realpath(path.resolve(root,config.repositoryRoot));
28
+ const git=(...args)=>execFileSync('git',['-C',repo,...args],{encoding:'utf8'}).trim();
29
+ const head=git('rev-parse','HEAD');git('diff','--exit-code','HEAD','--');
30
+ const projectPath=path.relative(repo,root).split(path.sep).join('/');
31
+ assert(projectPath&&!projectPath.startsWith('../'),'Project must belong to Git repository');
32
+ const tracked=new Set(git('ls-files','-z','--',projectPath).split('\0'));
33
+ const readTracked=async file=>{const declared=path.resolve(file),full=await realpath(declared),relative=path.relative(repo,full).split(path.sep).join('/');assert.equal(full,declared,'Symlink input forbidden');assert(full.startsWith(root+path.sep),'Input escaped project');assert(tracked.has(relative),'Input must be committed: '+relative);return {relative,bytes:await readFile(full)};};
34
+ await readTracked(options.config);await readTracked(await project.resolveAdapter(config.compilerAdapter));
35
+ assert(typeof config.caseManifest==='string'&&config.caseManifest,'Project config caseManifest required');
36
+ assert.equal(path.resolve(options.manifest),path.resolve(root,config.caseManifest),'Use the configured complete case manifest');
37
+ const manifest=await readTracked(options.manifest),catalog=await readTracked(path.resolve(root,config.catalog));
38
+ const parsedManifest=JSON.parse(manifest.bytes),parsedCatalog=JSON.parse(catalog.bytes);
39
+ const rows=activeCases(parsedManifest,parsedCatalog);
40
+ const adapter=await import(pathToFileURL(await project.resolveAdapter(config.compilerAdapter)).href);
41
+ const definitionRoot=await realpath(path.resolve(root,config.definitions)),results=[];
42
+ for(const row of rows){
43
+ try{
44
+ const file=await realpath(path.resolve(root,row.definition));assert(file.startsWith(definitionRoot+path.sep),'Definition escaped definitions root');
45
+ const definition=JSON.parse((await readTracked(file)).bytes),fragments=[];
46
+ assert.deepEqual(definition.sourceCaseIds??[definition.caseId],row.sourceCaseIds,'Manifest/definition source IDs differ');
47
+ for(let dir=path.dirname(file);;dir=path.dirname(dir)){
48
+ try{fragments.unshift(JSON.parse((await readTracked(path.join(dir,'prepare.json'))).bytes));}catch(e){if(e.code!=='ENOENT')throw e;}
49
+ if(dir===definitionRoot)break;assert(dir.startsWith(definitionRoot+path.sep),'Preparation escaped root');
50
+ }
51
+ assert.deepEqual(definition.ancestors,fragments.map(f=>f.id),'Preparation ancestry mismatch');
52
+ await adapter.compile({projectRoot:root,config,catalog:parsedCatalog,definition,fragments});
53
+ results.push({id:row.definition,definition:row.definition,status:'passed'});
54
+ }catch(e){results.push({id:row.definition,definition:row.definition,status:'failed',error:e.message});}
55
+ }
56
+ git('diff','--exit-code','HEAD','--');assert.equal(git('rev-parse','HEAD'),head,'Candidate changed during compilation');
57
+ const result={schemaVersion:1,kind:'minifish-full-compile',head,projectPath,projectTree:git('rev-parse',head+':'+projectPath),
58
+ manifest:{path:manifest.relative,sha256:sha(manifest.bytes)},catalog:{path:catalog.relative,sha256:sha(catalog.bytes)},
59
+ toolIdentity:await toolIdentity(config.toolSource,path.resolve(root,config.toolSourceManifest)),
60
+ total:results.length,passed:results.filter(r=>r.status==='passed').length,failed:results.filter(r=>r.status==='failed').length,
61
+ nonExecutable:(parsedManifest.pending??[]).map(r=>({id:r.sourceCaseId,reason:r.reason})),
62
+ durationMs:performance.now()-started,productExecution:false,results};
63
+ const destination=await checkOutput(options.output,project);await mkdir(destination,{recursive:false});
64
+ await writeFile(path.join(destination,'compile-result.json'),JSON.stringify(result,null,2)+'\n');return result;
65
+ }
@@ -0,0 +1,51 @@
1
+ """Bind a full MT compilation receipt to the candidate's committed active catalog."""
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+ from .iteration import Invalid
6
+ from .diff_review import git
7
+
8
+
9
+ def verify_compile(repo, head, bizdir, receipt_file):
10
+ if not receipt_file:
11
+ raise Invalid('MT requires --compile-result from minifish compile-check')
12
+ r = json.loads(Path(receipt_file).read_text())
13
+ project = bizdir + '/code/minifish-tests'
14
+ def check(ok, message):
15
+ if not ok:
16
+ raise Invalid(message)
17
+ check(r.get('schemaVersion') == 1 and r.get('kind') == 'minifish-full-compile', 'Invalid compile receipt')
18
+ check(r.get('head') == head and r.get('projectPath') == project, 'Stale or wrong-project compile receipt')
19
+ check(r.get('projectTree') == git(repo, 'rev-parse', head + ':' + project).decode().strip(), 'Compile tree mismatch')
20
+ def read_bound(key):
21
+ entry = r[key]
22
+ from pathlib import PurePosixPath
23
+ p = PurePosixPath(entry['path'])
24
+ check(not p.is_absolute() and '..' not in p.parts and entry['path'].startswith(project + '/'), 'Compile input outside project')
25
+ raw = git(repo, 'show', head + ':' + entry['path'])
26
+ check(hashlib.sha256(raw).hexdigest() == entry['sha256'], 'Compile input hash mismatch')
27
+ return json.loads(raw)
28
+ manifest, catalog = read_bound('manifest'), read_bound('catalog')
29
+ config = json.loads(git(repo, 'show', head + ':' + project + '/minifish.config.json'))
30
+ check(r['manifest']['path'] == project + '/' + config['caseManifest'], 'Unexpected active manifest')
31
+ check(r['catalog']['path'] == project + '/' + config['catalog'], 'Unexpected source catalog')
32
+ identity = r.get('toolIdentity', {})
33
+ check(identity.get('kind') == 'verified-content', 'Missing verified compiler identity')
34
+ for key, value in config['toolSource'].items():
35
+ check(identity.get(key) == value, 'Compiler identity mismatch: ' + key)
36
+ lock = git(repo, 'show', head + ':' + project + '/' + config['toolSourceManifest'])
37
+ check(identity.get('manifestSha256') == hashlib.sha256(lock).hexdigest(), 'Compiler manifest mismatch')
38
+ rows, results = manifest['cases'], r.get('results', [])
39
+ check(bool(rows) and len(rows) == len(results), 'Incomplete compile results')
40
+ check(len({x['definition'] for x in rows}) == len(rows), 'Duplicate active definition')
41
+ check([(x['definition'], x['definition']) for x in rows] == [(x.get('id'), x.get('definition')) for x in results], 'Compile case set mismatch')
42
+ official = [x['id'] for x in catalog['cases']]
43
+ covered = [i for row in rows for i in row['sourceCaseIds']]
44
+ pending = manifest.get('pending', [])
45
+ check(all(isinstance(x.get('reason'), str) and x['reason'].strip() for x in pending), 'Missing nonexecutable reason')
46
+ covered += [x['sourceCaseId'] for x in pending]
47
+ check(len(covered) == len(set(covered)) and len(official) == len(set(official)) and set(covered) == set(official), 'Incomplete catalog coverage')
48
+ check(r.get('nonExecutable') == [{'id': x['sourceCaseId'], 'reason': x['reason']} for x in pending], 'Nonexecutable decisions mismatch')
49
+ check(r.get('productExecution') is False, 'Not a static compilation receipt')
50
+ check(r.get('total') == len(rows) and r.get('passed') == len(rows) and r.get('failed') == 0 and all(x.get('status') == 'passed' for x in results), 'Full compilation did not pass')
51
+ return f"\n\nMT全量编译:{len(rows)}/{len(rows)} 通过;非执行责任 {len(pending)} 项保留独立处置。\n"
@@ -196,6 +196,7 @@ def main(argv=None):
196
196
  parser.add_argument('--repository')
197
197
  parser.add_argument('--base')
198
198
  parser.add_argument('--head')
199
+ parser.add_argument('--compile-result', help='Full static MT compilation receipt')
199
200
  parser.add_argument('--stage-id', action='append')
200
201
  parser.add_argument('--registry', help='Initiator workspace persistent iterations directory')
201
202
  parser.add_argument('--expected-sha256')
@@ -230,6 +231,9 @@ def main(argv=None):
230
231
  require('repository','head')
231
232
  from .diff_review import review
232
233
  checked=review(args.repository,config['inputCommit'],args.head,scope_nodes(config,args.workflow))
234
+ if config['stage'] == 'minifish-test':
235
+ from .compile_gate import verify_compile
236
+ checked['markdown'] += verify_compile(args.repository,args.head,config['bizdir'],args.compile_result)
233
237
  target.write_text(checked['markdown']); return 0 if checked['passed'] else 3
234
238
  result=render(config,args.workflow)
235
239
  if args.action == 'scope':