Archived
feat(activity-registry): 配方静态审 —— 写错地址在装包那一刻就拒
与 refersTo 目标检查同款:代码错是必然的错,而必然的错只有在装的时候被拦下才算好查。 台账地址 / 列名 / 模型名 / 视图模版 / submit 的验收清单逐条核; analyze 的出参没人消费报 warning —— 那是白烧一次模型调用。 顺带(R8,Task 7 评审挖出):全 auto 的步没有 guide 是正常形态, guide.min(1) 放宽为 z.string() + superRefine —— 没 auto 又不写 guide 照旧拒装。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -348,3 +348,116 @@ describe('refersTo 目标要在本活动的声明里 —— 写错的地址会
|
||||
expect(found.map((f) => f.code)).toEqual(['reference-target-unbound']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ── 配方静态审(2026-08-29)──────────────────────────────────────────
|
||||
*
|
||||
* `autoRecipeFindings` 读的是**整份** `ActivityManifest`(不像本文件其余审读只收窄到
|
||||
* 字段级),所以这里不能再用局部的 `manifest()` / `table()` 这类窄类型 helper ——
|
||||
* 借 `activityManifestSchema.parse()` 把六区的默认值一次性填满,只手写真正必填的那几处。
|
||||
*/
|
||||
import { activityManifestSchema } from '@contracts';
|
||||
import { autoRecipeFindings } from '../internal/domain/static-audit.ts';
|
||||
|
||||
/** 台账地址与列名全部真实声明过 —— `demo.record` 只有 `case_ref` / `note` 两列 */
|
||||
function manifestFixture(): ActivityManifest {
|
||||
return activityManifestSchema.parse({
|
||||
identity: {
|
||||
activityId: 'demo-activity',
|
||||
name: '演示活动',
|
||||
packageVersion: '1.0.0',
|
||||
category: '演示',
|
||||
sdkVersion: '1.0.0',
|
||||
checksum: 'chk',
|
||||
},
|
||||
contract: {
|
||||
activityId: 'demo-activity',
|
||||
version: '1.0.0',
|
||||
input: { data: { type: 'object', properties: {} } },
|
||||
output: {},
|
||||
authorization: { humanCallBudget: 0 },
|
||||
resources: { budget: { steps: 10, tokens: 10000, activeMinutes: 10, humanCalls: 0 } },
|
||||
},
|
||||
declarations: {
|
||||
models: ['m'],
|
||||
ledger: [
|
||||
{
|
||||
domain: 'demo',
|
||||
object: 'record',
|
||||
kind: 'record',
|
||||
caseKey: 'case_ref',
|
||||
label: '演示记录',
|
||||
fields: [
|
||||
{ name: 'case_ref', type: 'text', primaryKey: true },
|
||||
{ name: 'note', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
harness: {},
|
||||
},
|
||||
assets: {
|
||||
businessModel: {
|
||||
cmmnXml: '<caseFileItem id="S1" name="x"/>',
|
||||
narrative: '演示说明书',
|
||||
dmn: { drd: [], tables: [] },
|
||||
steps: [],
|
||||
},
|
||||
},
|
||||
samples: [{ name: '样例', input: {}, expect: { ports: ['done'] } }],
|
||||
entry: { file: 'index.js', checksum: 'chk' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('配方静态审:写错地址 = 每一单都错,装的时候就拒', () => {
|
||||
const withRecipe = (auto: unknown) => {
|
||||
const base = manifestFixture();
|
||||
return {
|
||||
...base,
|
||||
assets: {
|
||||
...base.assets,
|
||||
businessModel: {
|
||||
...base.assets.businessModel,
|
||||
steps: [{ item: 'S1', guide: '', writes: [], reads: [], tools: [], auto }],
|
||||
},
|
||||
},
|
||||
} as never;
|
||||
};
|
||||
|
||||
it('台账地址不在 declarations.ledger 里 → error', () => {
|
||||
const findings = autoRecipeFindings(withRecipe({ do: [{ kind: 'project', into: 'nope.nope', from: { once: true }, fields: { a: 'b' } }] }));
|
||||
expect(findings.some((f) => f.severity === 'error' && f.message.includes('nope.nope'))).toBe(true);
|
||||
});
|
||||
|
||||
it('列名不在该对象的声明里 → error(列名写错 = 那一格永远是空的)', () => {
|
||||
const findings = autoRecipeFindings(
|
||||
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { once: true }, fields: { 不存在的列: 'x' } }] }),
|
||||
);
|
||||
expect(findings.some((f) => f.severity === 'error' && f.message.includes('不存在的列'))).toBe(true);
|
||||
});
|
||||
|
||||
it('each / lookup 的地址同样要在声明里', () => {
|
||||
const findings = autoRecipeFindings(
|
||||
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { each: 'mine:nope.nope' }, fields: {} }] }),
|
||||
);
|
||||
expect(findings.some((f) => f.message.includes('nope.nope'))).toBe(true);
|
||||
});
|
||||
|
||||
it('in: 与 result: 不是台账地址,不该被当成台账来审', () => {
|
||||
const findings = autoRecipeFindings(
|
||||
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { each: 'in:pns' }, fields: {} }] }),
|
||||
);
|
||||
expect(findings).toEqual([]);
|
||||
});
|
||||
|
||||
it('analyze 的出参没人消费 → warning(白烧一次模型调用)', () => {
|
||||
const findings = autoRecipeFindings(
|
||||
withRecipe({ do: [{ kind: 'analyze', model: 'm', into: 'items', instruction: 'x', input: { from: { once: true }, fields: {} }, output: { type: 'object' } }] }),
|
||||
);
|
||||
expect(findings.some((f) => f.severity === 'warn' && f.message.includes('items'))).toBe(true);
|
||||
});
|
||||
|
||||
it('submit 那一笔所在的步没有人工验收清单 → error(系统提交不比模型提交宽松)', () => {
|
||||
const findings = autoRecipeFindings(withRecipe({ do: [{ kind: 'submit', rationale: 'x' }] }));
|
||||
expect(findings.some((f) => f.severity === 'error' && f.message.includes('验收清单'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user