mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:51:57 +00:00
9d8b41430e
- 完善口径编辑、只读试运行、统一质量验证与执行语义指纹 - 增加可执行指标约束、有预算的主动调查和过程评测 - 提取发布检查、证据读取与起草服务,支持任务停止、重试和检查点恢复 - 增加版本传递影响分析,修复并发覆盖、敏感值遮罩、异常评分及页面状态问题 - 修复 SQLite 复合查询追溯和 microVM 可选依赖缺失处理 - 纳入当前设计文档及部署配置调整 验证:相关主回归 3822 项通过,4 项跳过、8 项预期失败;补充回归通过。 前端构建、文案检查及 git diff --check 通过。 全量检查仍有环境权限、PostgreSQL 可选依赖和既有部署配置、SQLite 文件基线差异导致的未通过项。
112 lines
6.3 KiB
Python
112 lines
6.3 KiB
Python
"""口径的内容指纹:同一位置的口径定义变没变,按定义本身的哈希认,不看运行时算出来的值。
|
||
|
||
口径的身份原先是「工作流 id@版本」(ledger/labels.caliber_key)。同一版不会变(版本快照不可变),但复制、改名、迁移之后
|
||
就认不回是同一份口径了;后续做「定义变更影响分析」也需要一份内容指纹。这里给的就是这份指纹:
|
||
|
||
- definition_hash:一张口径卡的指标定义(caliber、caliber_version 和每个指标的语义字段)的 sha256;
|
||
- version_hash:一份口径版本里各卡的内容哈希排序后再取 sha256,也就是整份版本的定义指纹;
|
||
- semantic_hash:口径卡及上游执行依赖的指纹,含 SQL、数据源与绑定,排除布局;
|
||
- card_hash_of:从 {node_id, metrics, caliber?, caliber_version?} 这种卡形状取哈希(运行时指标卡、账本里源头口径卡的
|
||
两种写法共用)。
|
||
|
||
**定义指纹只取声明字段。** 指标只留 SEMANTIC_FIELDS 中存在的键(含类型配置、粒度、过滤和可执行约束),
|
||
按 id 排序(顺序不影响哈希);空字符串、None、空的列表和字典不写进规范 JSON。值、渲染后的字、输入出处、状态这些都是
|
||
运行时产物,不进哈希——同一份定义重跑一次,或者补了无关的字段,哈希不变。
|
||
|
||
**这是内容指纹,不是跨图稳定的 ID。** 表达式里写的是节点 id(cell(nodes.fetch, 0, 'gmv')),取数节点改了名,式子跟着改,
|
||
哈希就会变;复制到另一张图、指标定义一字不差时哈希相同,正是用来认回同一份定义。要跨图认同一份口径,仍靠版本引用
|
||
(caliber_from)和「工作流@版本」。
|
||
|
||
**口径名、版本进不进哈希。** 两个函数的入参可以是空串:运行时指标卡和账本两侧算指标集指纹时都不查库、不拿名字,
|
||
只按 metrics 算(空串时规范 JSON 里省略这两个键),这样两边的哈希才对得上。名字、版本要不要算进去由调用方决定。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Iterable
|
||
from typing import Any
|
||
|
||
#: 指标的语义字段:只有这几样算定义,运行时值(value、rendered、inputs……)不参与哈希
|
||
SEMANTIC_FIELDS = ("id", "name", "expression", "unit", "decimals", "format", "kind", "definition", "params", "external",
|
||
"rule", "assessment", "grain", "time_field", "numerator", "denominator", "dimensions", "filters",
|
||
"contract")
|
||
|
||
|
||
def _blank(value: Any) -> bool:
|
||
"""空值、缺省键:None、空字符串、空的列表和字典不写进规范 JSON。0、False 是有意义的取值,照写。"""
|
||
return value is None or value == "" or (isinstance(value, (list, dict, tuple)) and not value)
|
||
|
||
|
||
def _metric_content(metric: Any) -> dict[str, Any]:
|
||
"""一个指标的语义内容:只留 SEMANTIC_FIELDS 里存在的键,空值略去。"""
|
||
if not isinstance(metric, dict):
|
||
return {}
|
||
return {field: metric[field] for field in SEMANTIC_FIELDS if field in metric and not _blank(metric[field])}
|
||
|
||
|
||
def definition_hash(caliber: str, caliber_version: str, metrics: Iterable[dict[str, Any]]) -> str:
|
||
"""一张口径卡的指标定义 → 内容指纹。指标按 id 排序,顺序不影响哈希;caliber / caliber_version 为空时不进规范 JSON
|
||
(运行时和账本两侧只按 metrics 算,见模块说明)。"""
|
||
from app.core.artifact_store import canonical_json, content_hash
|
||
|
||
content: dict[str, Any] = {}
|
||
if not _blank(caliber):
|
||
content["caliber"] = str(caliber)
|
||
if not _blank(caliber_version):
|
||
content["caliber_version"] = str(caliber_version)
|
||
kept = [_metric_content(metric) for metric in metrics or []]
|
||
content["metrics"] = sorted(kept, key=lambda metric: str(metric.get("id") or ""))
|
||
return content_hash(canonical_json(content))
|
||
|
||
|
||
def card_hash_of(card: dict[str, Any]) -> str:
|
||
"""一张口径卡(card)的内容指纹:{node_id, metrics, caliber?, caliber_version?}。没有 caliber 时按空串算
|
||
(即只按 metrics)。"""
|
||
if not isinstance(card, dict):
|
||
return definition_hash("", "", [])
|
||
return definition_hash(str(card.get("caliber") or ""), str(card.get("caliber_version") or ""),
|
||
card.get("metrics") or [])
|
||
|
||
|
||
def version_hash(card_hashes: Iterable[str]) -> str:
|
||
"""一份口径版本的内容指纹:各卡的内容哈希(card_hash_of 的结果)排序后取 sha256,和卡片的先后无关。"""
|
||
from app.core.artifact_store import content_hash
|
||
|
||
return content_hash("\n".join(sorted(str(h) for h in card_hashes or () if h)))
|
||
|
||
|
||
def semantic_hash(graph: dict[str, Any], node_id: str) -> str:
|
||
"""口径及其上游执行依赖的指纹:包含 SQL、数据源、入口绑定和固定版本引用。
|
||
|
||
definition_hash 继续表示指标定义自身;此指纹才覆盖取数语义。保守保留 SQL 原文,
|
||
不把未证明等价的改写判为相同;画布坐标和无关的下游写作节点不参与。
|
||
"""
|
||
import re
|
||
|
||
from app.core.artifact_store import canonical_json, content_hash, execution_content
|
||
|
||
content = execution_content(graph)
|
||
nodes = {n["id"]: n for n in content["nodes"]}
|
||
wanted, pending = set(), [node_id]
|
||
while pending:
|
||
current = pending.pop()
|
||
if current in wanted:
|
||
continue
|
||
wanted.add(current)
|
||
pending.extend(e["source"] for e in content["edges"] if e["target"] == current)
|
||
node = nodes.get(current)
|
||
if node:
|
||
# 非相邻节点也能被表达式引用。保守扫描配置,包含节点参数中的模板引用。
|
||
config = canonical_json(node["data"].get("config") or {})
|
||
pending.extend(re.findall(r"\bnodes\.([\w-]+)", config))
|
||
pending.extend(re.findall(r"\bnodes\[['\"]([\w-]+)['\"]\]", config.replace('\\"', '"')))
|
||
inputs = {n["id"] for n in nodes.values() if n["type"] == "input"}
|
||
wanted |= inputs
|
||
return content_hash(canonical_json({
|
||
"schema_version": 1, "defaults": content["defaults"],
|
||
"nodes": [n for n in content["nodes"] if n["id"] in wanted],
|
||
"edges": [e for e in content["edges"] if e["source"] in wanted and e["target"] in wanted],
|
||
}))
|
||
|
||
|
||
__all__ = ["SEMANTIC_FIELDS", "card_hash_of", "definition_hash", "version_hash"]
|