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:
@@ -376,6 +376,125 @@ export function loopFindings(manifest: ActivityManifest): StaticAuditFinding[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* **配方静态审**(2026-08-29)—— 与 `refersTo` 目标检查同款:**写错地址 = 每一单都错**。
|
||||
*
|
||||
* 代码错是必然的错,模型错是概率的错。必然的错好查 —— 前提是它在装包那一刻就被拦下,
|
||||
* 而不是等到第一单跑起来才现形。
|
||||
*/
|
||||
export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFinding[] {
|
||||
const out: StaticAuditFinding[] = [];
|
||||
const ledgers = new Map(
|
||||
manifest.declarations.ledger.map((l) => [`${l.domain}.${l.object}`, new Set(l.fields.map((f) => f.name))]),
|
||||
);
|
||||
const templates = new Set(manifest.assets.viewTemplates.map((t) => t.name));
|
||||
|
||||
/** 台账地址:剥掉 `mine:` 前缀;`in:` / `result:` 不是台账,跳过 */
|
||||
const checkAddress = (raw: string, at: string): void => {
|
||||
if (raw.startsWith('in:') || raw.startsWith('result:')) return;
|
||||
const address = raw.startsWith('mine:') ? raw.slice('mine:'.length) : raw;
|
||||
if (!ledgers.has(address)) {
|
||||
out.push({
|
||||
code: 'schema-invalid',
|
||||
severity: 'error',
|
||||
message: `配方引的台账地址不在声明里:${address}(写的授权面是台账声明)`,
|
||||
at,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** 表达式里也可能藏着 lookup 地址,递归找一遍 */
|
||||
const walkExpr = (expr: unknown, at: string): void => {
|
||||
if (typeof expr !== 'object' || expr === null) return;
|
||||
const node = expr as Record<string, unknown>;
|
||||
if (typeof node['lookup'] === 'string') {
|
||||
checkAddress(node['lookup'], at);
|
||||
for (const value of Object.values((node['by'] ?? {}) as Record<string, unknown>)) walkExpr(value, at);
|
||||
if (node['else'] !== undefined) walkExpr(node['else'], at);
|
||||
}
|
||||
if (Array.isArray(node['cat'])) for (const part of node['cat'] as unknown[]) walkExpr(part, at);
|
||||
if (node['fmt'] !== undefined) walkExpr(node['fmt'], at);
|
||||
if (node['then'] !== undefined) walkExpr(node['then'], at);
|
||||
if (typeof node['when'] === 'string' && node['else'] !== undefined) walkExpr(node['else'], at);
|
||||
};
|
||||
|
||||
const walkSource = (source: unknown, at: string): void => {
|
||||
if (typeof source !== 'object' || source === null) return;
|
||||
const node = source as Record<string, unknown>;
|
||||
if (typeof node['each'] === 'string') checkAddress(node['each'], at);
|
||||
for (const value of Object.values((node['where'] ?? {}) as Record<string, unknown>)) walkExpr(value, at);
|
||||
if (typeof node['then'] === 'object' && node['then'] !== null) walkSource(node['then'], at);
|
||||
if (Array.isArray(node['rows'])) {
|
||||
for (const row of node['rows'] as unknown[]) {
|
||||
for (const value of Object.values(row as Record<string, unknown>)) walkExpr(value, at);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const step of manifest.assets.businessModel.steps ?? []) {
|
||||
const recipe = step.auto;
|
||||
if (recipe === undefined) continue;
|
||||
const at = `assets.businessModel.steps[${step.item}].auto`;
|
||||
const consumed = new Set<string>();
|
||||
|
||||
for (const [index, action] of recipe.do.entries()) {
|
||||
const where = `${at}.do[${index}]`;
|
||||
if (action.kind === 'project') {
|
||||
checkAddress(action.into, where);
|
||||
walkSource(action.from, where);
|
||||
const columns = ledgers.get(action.into);
|
||||
for (const [name, expr] of Object.entries(action.fields)) {
|
||||
walkExpr(expr, where);
|
||||
if (columns !== undefined && !columns.has(name)) {
|
||||
out.push({ code: 'schema-invalid', severity: 'error', message: `配方写的列不在 ${action.into} 的声明里:${name}`, at: where });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.kind === 'analyze') {
|
||||
walkSource(action.input.from, where);
|
||||
for (const expr of Object.values(action.input.fields)) walkExpr(expr, where);
|
||||
if (!manifest.declarations.models.includes(action.model)) {
|
||||
out.push({ code: 'schema-invalid', severity: 'error', message: `配方用的模型没在 declarations.models 里声明:${action.model}`, at: where });
|
||||
}
|
||||
}
|
||||
if (action.kind === 'card') {
|
||||
for (const address of action.gateLedgers ?? []) checkAddress(address, where);
|
||||
if (action.as === 'decide' && (action.options ?? []).length === 0) {
|
||||
out.push({ code: 'schema-invalid', severity: 'error', message: '决策卡一个选项都没有', at: where });
|
||||
}
|
||||
}
|
||||
if (action.kind === 'submit' && (step.acceptanceChecklist ?? []).length === 0) {
|
||||
out.push({
|
||||
code: 'schema-invalid',
|
||||
severity: 'error',
|
||||
message: 'submit 那一步必须声明人工验收清单(acceptanceChecklist)—— 系统提交不比模型提交宽松',
|
||||
at: where,
|
||||
});
|
||||
}
|
||||
/* 找出 `result:xxx` 被谁消费了 —— analyze 的出参没人用,就是白烧一次模型调用 */
|
||||
const text = JSON.stringify(action);
|
||||
for (const hit of text.matchAll(/"result:([A-Za-z0-9_]+)"/g)) consumed.add(hit[1] ?? '');
|
||||
}
|
||||
|
||||
for (const action of recipe.do) {
|
||||
if (action.kind !== 'analyze') continue;
|
||||
if (!consumed.has(action.into)) {
|
||||
out.push({
|
||||
code: 'schema-invalid',
|
||||
severity: 'warn',
|
||||
message: `analyze 的出参 result:${action.into} 没有任何一笔消费它 —— 白烧一次模型调用`,
|
||||
at,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step.view !== undefined && step.view !== '' && !templates.has(step.view)) {
|
||||
out.push({ code: 'schema-invalid', severity: 'error', message: `剧本引的视图模版不存在:${step.view}`, at });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function staticAudit(manifest: ActivityManifest, external: ExternalChecks): StaticAuditFinding[] {
|
||||
const out: StaticAuditFinding[] = [];
|
||||
|
||||
@@ -425,6 +544,7 @@ export function staticAudit(manifest: ActivityManifest, external: ExternalChecks
|
||||
out.push(...ledgerKindFindings(manifest));
|
||||
out.push(...acceptanceLevelFindings(manifest));
|
||||
out.push(...loopFindings(manifest));
|
||||
out.push(...autoRecipeFindings(manifest));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user