@fx-data/ai-diagnostics 0.1.20261111112203 → 0.1.20261112024042

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/AGENT_SETUP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FX 数据诊断:Agent 安装与管理
2
2
 
3
- 本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261111112203`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
3
+ 本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261112024042`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
4
4
 
5
5
  用户明确要求安装、更新、回退、卸载或检查 FX 数据诊断时,也会触发薄 Skill,并直接执行对应管理命令,不先运行 `context` 或读取诊断规则;只有开始诊断时才按薄 Skill 加载内容。普通聊天不主动加载。
6
6
 
package/README.md CHANGED
@@ -40,4 +40,4 @@
40
40
 
41
41
  ---
42
42
 
43
- **给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261111112203/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
43
+ **给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261112024042/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
@@ -9,18 +9,18 @@
9
9
  支持快照的入口在本次输入查询的 `calc.build_query` 阶段写出正文(历史名为 `filter`);V2 查询也使用该阶段名,但不能据此认定已采集计算快照,须检查实际属性。入口保留检索 ID、结果和请求统计,名称、筛选正文与参数作用位置在快照中只保存一份;ID 在引用和检索位置出现不等于重复正文。两种入口的控件摘要统一为 `controls.*`,候选值输出来源另存 `control.option_source.bindings`,它不能代替实际依赖。历史绑定摘要只用于旧记录,不要求新版同时提供。
10
10
 
11
11
  - `calculation.panorama.status=CAPTURED` 表示快照已生成;同一 span 的 `part_count`、`part_0...part_N`、`length`(UTF-16 长度)和 `sha256` 用于核对完整性。`OVERSIZE`、`FAILED`、缺块、长度或摘要不符均不算完整;不能混用不同 span 的分块。
12
- - `tables` 是本次涉及的表,`dependencies` 是实际查询构建时观察到的来源到使用者关系,包含直接读取的来源,不额外查询祖先血缘。`nodes` 与 `conditionEdges` 保留本次条件对象及传播关系;节点 `id` 区分同表不同实例,`tableId` 对应表。条件传播和实际扫描是不同事实;条件涉及但未读取的表不能认作本次重算。
13
- - `conditions` 集中记录筛选正文,节点的 `fieldFilters`、`componentFilters`、`linkageFilters` 以及步骤只引用筛选 ID;节点另有 `parameters` 和 `dynamicFields`。筛选保留字段、运算符与 AND/OR/NOT 组合;`valueState` 的 `REDACTED`、`UNAVAILABLE`、`TRUNCATED` 分别表示脱敏、未取得、截断,不能补猜值。
14
- - 条件的 `fieldTransforms` 保留字段变换,如按年、按月、求和及请求中的分组。HTML 把变换写在字段旁;`UNAVAILABLE`、`TRUNCATED` 要明确显示细节未记录或未采集完,不能简化成原字段比较。历史快照缺少此字段时,不补猜变换。
15
- - `tables[].fields` 按表保存本次涉及字段的显示名;先去掉已确认的所属表前缀,再按本表字典解析,不能用别表同名字段补名。`kind=INLINE_QUERY` 是实际计算中的内联子查询,其 ID 是计算实体标识,不能当业务表去查名称。`componentName` 是本次请求携带的组件名称;名称为空不能用另一个组件或结果表名替代。
16
- - 条件 `parameterIds` 是原始筛选翻译时确认的参数引用,`valueState=PARAMETER` 表示整个比较值就是参数,可直接显示“金额 ≥ 参数「最低金额」”。组合条件自身的参数引用只说明整组涉及该参数,不能分配给未知的子条件。前后 N 项也遵守相同脱敏规则,展示为“金额 取前 参数「展示数量」 项”或“金额 取后 [值已脱敏] 项”。其他结构不补写未知公式。`dynamicDefinitions` 给出命中的动态项名称和默认字段,节点 `dynamicFields` 给出本次选择;据此显示“分组:区域 → 渠道”。`controls` 集中保存实际使用的来源名称、原请求 ID 和类别,区分普通控件、行权限、组件联动、值联动、参数控件和动态字段控件。`conditionControls` 通过条件 ID 说明来源;参数及动态字段覆盖项的 `controlIds` 说明由哪个控件改变,同请求键的不同来源不能借用名称。历史快照没有覆盖项来源时不补猜;权限条件用“行权限”说明,业务值继续脱敏。
17
- - 节点参数 `id` 对应快照 `parameters` 名称表;`sources=TABLE` 表示分配给该表的覆盖输入,`COMPONENT` 表示组件带入,同一参数可同时有两种来源,`UNKNOWN` 保留未知。是否实际用于筛选,需要结合条件引用与读取方式,不能仅凭分配就认定已生效。根节点可能合并两类参数,不能把组件参数自动认作目标表的配置绑定。旧 `filter_conditions.parameter_ids` 只是全树集合,`controls.parameter_bindings` 是筛选器到参数关系,均不能代替逐表对应关系。
18
- - `tables[].definition` 保存本次读取方式和表内筛选。`READ_MATERIALIZED` 表示直接读取已有结果,`EXPANDED` 表示重新展开;`REUSED_INPUT` 表示复用已有查询输入,其内部条件不可得时用 `REUSED_INPUT_DETAILS_UNAVAILABLE:<表 ID>` 标明缺口,不能把未执行的准备算子当作本次筛选。`filters` 列表只保存非空筛选组的条件引用,不记录步骤 ID、算子类型或无筛选步骤。同表不同读取方式或筛选组保存在 `variants`,展示时全部保留,不能只取第一份;相同记录不重复输出。`componentDefinition` 保存实际执行的组件追加筛选,组件追加阶段复用已有输入而跳过准备算子时记录 `COMPONENT_INPUT_REUSED`。缺失只说明记录中没有对应读取方式,不能证明本次没有读取,也不能用当前或发布配置补造本次事实。公式、子查询内部条件不猜测展开。
19
- - `scope=QUERY_BUILT` 表示查询构建成功,不表示引擎已执行成功;`BUILD_FAILED` 保留失败前确认的事实,图必须标明未完成。重复构建或失败后重试时,保留预编译输入的参数和控件关联,实际依赖、读取方式和表内筛选只来自本轮;参数字典只覆盖本轮引用与保留输入,不包含未使用的预取条件或前次运行条件。
20
- - `componentStages` 的 `source=REQUEST` 表示本次请求明确配置的组件筛选,`DETAIL` 为汇总前明细筛选,`RESULT` 为组件结果筛选,作用于根表,条件正文仍引用 `conditions`。这些输入在同步构建阶段记录,不代表后续编译或执行已经成功;实际展开仍以 `dependencies`、读取方式及表内筛选为准。历史记录缺少 `source` 时沿用当时的编译条件含义。未提供这些字段的入口明确说明缺口。
21
- - 服务端补名失败或资源已不存在时,`TABLE_NAMES_MISSING` / `PARAMETER_NAMES_MISSING` 表示名称尚未补齐,不能把快照称作完整计算全景;展示时保留 ID 并明确缺口。`gaps`、`omittedNodes`、`omittedEdges`、条件截断及 `notCaptured` 必须随图展示。表与条件对象各最多 512,关系、算子检查、筛选内容及覆盖项在采集时有预算限制,达到上限标记 `*_LIMIT`,不影响原计算继续执行;省略数量不一定可知。正文最多 16 块,每块不超过 8000 个 UTF-16 单元,过大明确记 `OVERSIZE`。空数组仅在对应采集未省略的范围内证明该类直接条件为零。
22
-
23
- 历史 `calculation-panorama-v1` 的依赖来自逻辑血缘,`INPUT` 是根表输入配置,`PUBLISHED` 仅为发布配置对照,不能当作本次实际展开。更旧记录仍可使用已有[条件树](filter-tree.md#筛选树还原与交付);新观测不能补回历史缺失的事实。已有性能聚合指标保留原用途,不能代替业务关系。
12
+ - `tables` 是本次涉及的业务表。`dependencies` 记录本次查询构建时观察到的来源到使用者关系,包含直接读取的来源,不额外查询祖先血缘。合并等步骤的内联分支折成业务表之间的直接连线,共享来源保留每个使用者。`nodes` 与 `conditionEdges` 保留本次条件对象及传播关系,节点 `id` 区分同表不同实例,`tableId` 对应表。判断是否重算时,以实际扫描和读取方式为准。
13
+ - `conditions` 集中记录表上的筛选正文,节点的 `fieldFilters` 和 `linkageFilters` 通过 ID 引用。节点另有 `parameters` 和 `dynamicFields`。筛选保留字段、运算符与 AND/OR/NOT 组合。`valueState` 的 `REDACTED`、`UNAVAILABLE`、`TRUNCATED` 分别表示脱敏、未取得、截断。
14
+ - 条件的 `fieldTransforms` 保留已知的字段变换,如按年、按月和求和。HTML 把变换写在字段旁,遇到 `UNAVAILABLE`、`TRUNCATED` 时标明细节未记录或未采集完。历史快照缺少该字段时,沿用记录中的条件描述。
15
+ - `tables[].fields` 按表保存本次条件与动态选择引用字段的显示名。先去掉已确认的所属表前缀,再按本表字典解析,不能用别表同名字段补名。`componentName` 是本次请求携带的组件名称。名称为空时可单独标明结果表名称。
16
+ - 条件 `parameterIds` 是原始筛选翻译时确认的参数引用,`valueState=PARAMETER` 表示整个比较值就是参数,可直接显示“金额 ≥ 参数「最低金额」”。组合条件自身的参数引用只说明整组涉及该参数,不能分配给未知的子条件。前后 N 项也遵守相同脱敏规则,展示为“金额 取前 参数「展示数量」 项”或“金额 取后 [值已脱敏] 项”。其他结构不补写未知公式。`dynamicDefinitions` 给出命中的动态项名称和默认字段,节点 `dynamicFields` 给出本次选择。两者以快照内短引用关联,历史快照可能还包含步骤 ID。`controls` 集中保存实际使用的来源名称、原请求 ID 和类别,条件通过短引用定位来源,区分普通控件、行权限、组件联动、值联动、参数控件和动态字段控件。`conditionControls` 通过条件 ID 说明来源;参数及动态字段覆盖项的 `controlIds` 说明由哪个控件改变,同请求键的不同来源不能借用名称。历史快照没有覆盖项来源时不补猜;权限条件用“行权限”说明,业务值继续脱敏。
17
+ - 节点参数 `id` 对应快照 `parameters` 名称表。保留分配给该表的覆盖输入和该表筛选引用的参数。`sources=TABLE` 表示分配给表,`COMPONENT` 表示组件带入,`UNKNOWN` 表示分配来源未确认。结合条件引用与读取方式判断参数的用途。
18
+ - `tables[].definition` 保存本次读取方式。`READ_MATERIALIZED` 表示直接读取已有结果,`EXPANDED` 表示重新展开,`REUSED_INPUT` 表示复用已有查询输入。复用输入的内部构建详情不可得时用 `REUSED_INPUT_DETAILS_UNAVAILABLE:<表 ID>` 标明缺口。同表的其他读取方式保存在 `variants`。读取方式缺失时保持未知。
19
+ - `scope=QUERY_BUILT` 表示查询构建成功,不表示引擎已执行成功。`BUILD_FAILED` 保留失败前确认的事实,图标明未完成。重复构建或失败后重试时,保留预编译输入的参数和控件关联。实际依赖和读取方式来自本轮,参数字典只覆盖保留的条件引用与输入覆盖。
20
+ - 旧 v2 快照可能有 `componentDefinition`、`componentStages` 和 `componentFilters`,分别描述组件追加步骤、请求配置和组件阶段条件。新版快照省去组件自己的筛选、表内原有筛选及仅由它们引用的参数。新版 `componentFilters` 和表定义里的 `filters` 是供旧 v2 读取器识别的空数组。
21
+ - 名称优先复用计算已经加载的元数据,结束时批量补齐缺名项。`TABLE_NAMES_MISSING` / `PARAMETER_NAMES_MISSING` 表示名称尚未补齐。图随 `gaps`、`omittedNodes`、`omittedEdges` 和条件截断标注缺口。表与条件对象各最多 512,关系、筛选内容及覆盖项有采集预算,达到上限标记 `*_LIMIT`,计算继续执行。正文最多 16 块,每块不超过 8000 个 UTF-16 单元,过大记 `OVERSIZE`。
22
+
23
+ 早期 `calculation-panorama-v2` 可能含 `INLINE_QUERY` 节点和表内筛选。生成图时按已记录的关系折叠内联分支,省去表内筛选及其独有参数。历史 `calculation-panorama-v1` 的依赖来自逻辑血缘,`INPUT` 是根表输入配置,`PUBLISHED` 仅为发布配置对照,不能当作本次实际展开。更旧记录仍可使用已有[条件树](filter-tree.md#筛选树还原与交付),新观测不能补回历史缺失的事实。已有性能聚合指标保留原用途,不能代替业务关系。
24
24
 
25
25
  ## 计算关系图
26
26
 
@@ -29,8 +29,8 @@
29
29
  名称缺失时用 ID,关系不全时展示能证实的局部范围并标注缺口;生成图不需要先确认唯一原因。完全没有可证实的关系或文件生成受阻时,说明具体限制,不编图或声称已经生成。
30
30
 
31
31
  - **范围与关系**:注明业务对象、执行和来源,画全所选范围内已知的节点与边,方向为“上游来源 → 下游使用者 → 当前结果”。同表的不同条件对象保持可区分,共享上游不等于重复执行;旧树的身份、计数与环路按[筛选树](filter-tree.md#选定结构与完整性)核对。条件展开树不冒充完整产品血缘。
32
- - **图形主视图**:只保留按上下游排布的表节点和带箭头的连线。同一张表只画一次,多个下游直接连到它,不另设共享区、共享编号或引用卡片。表格、路径或缩进列表只作补充。大图可缩放、滚动或查找,全部节点与明细应能访问。
33
- - **逐表条件**:节点内只展示本节点直接记录的字段筛选、参数、动态字段和联动,优先展示名称与可读条件,不只写数量。参数要说明是本次传入还是本表实际使用;有明确关联时写进对应的筛选或公式,没有关联就标明“具体用途未记录”,不按名称或数值猜用途。不汇总或重复列出上游条件,上下游关系由连线表达。ID、条件对象和步骤信息收起查看;同表不同条件对象分组保留,不合并成一组同时生效的条件。条件是否直接作用于本表,按[名称、绑定与展示](filter-tree.md#名称绑定与展示)判断。全局请求参数只作上下文,不自动分配到表节点。
32
+ - **图形主视图**:业务表由左到右排列。同一张表画一次,箭头指向每个使用者。只给已确认展开的表加标记,悬停可查看已记录的读取方式。大图可缩放、滚动或查找。
33
+ - **逐表条件**:卡片展示直接作用于该表的字段筛选、联动和动态字段。表级参数覆盖在确认展开时单独标出,参数确实被表上的筛选引用时可连同来源写入该筛选。触发本表展开的覆盖,以及有实际依赖和条件传播指向下游展开的筛选,使用展开原因标记。内部编号用于连接记录,卡片只显示业务含义。同表不同条件对象分组保留,条件作用位置按[名称、绑定与展示](filter-tree.md#名称绑定与展示)核对。
34
34
  - **缺失与推测**:随图展示采集缺口、截断和未采集范围,不补造关系。可疑路径或优化建议与已确认关系分开,并注明依据与业务前提;不要求每张图都附原因判断或优化方案。
35
35
 
36
36
  HTML 应能独立打开,数据、样式和脚本内嵌;保存到本次临时产物或客户端交付目录,按[诊断材料边界](../../../AGENTS.md#3-诊断原则)脱敏,不把真实业务材料写入仓库,日志与参数按文本安全转义。生成方法见[计算全景查询与生成](../queries.md#计算全景)。
@@ -2,20 +2,124 @@
2
2
  <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>计算血缘图</title>
5
+ <title>计算关系图</title>
6
6
  <style>
7
- :root{font:14px/1.6 system-ui,"Microsoft YaHei",sans-serif;color:#24364b;background:#f5f7fa}*{box-sizing:border-box}body{margin:0;height:100vh;display:flex;flex-direction:column;overflow:hidden}button,input{font:inherit}button{cursor:pointer;background:white;color:inherit;border:1px solid #d9e0e9;border-radius:7px;padding:6px 12px}button:hover{border-color:#7193c5}header{padding:22px 28px 16px;background:#fff;border-bottom:1px solid #e0e6ed;flex-shrink:0}h1{margin:0;font-size:24px;font-weight:650}.subtitle{color:#64758b;font-size:13px;margin-top:5px}.toolbar{display:flex;align-items:center;gap:8px;padding:12px 28px;background:#fff;border-bottom:1px solid #e0e6ed;flex-wrap:wrap;flex-shrink:0}.toolbar input{width:270px;max-width:100%;padding:7px 10px;border:1px solid #d9e0e9;border-radius:7px}.toolbar .direction{margin-left:auto;color:#718096;font-size:12px}#match{color:#64758b;font-size:12px}#viewport{overflow:auto;flex:1;min-height:200px;padding:0;position:relative}#space{position:relative}#canvas{position:relative;transform-origin:0 0}svg{position:absolute;inset:0;overflow:visible;pointer-events:none}.edge{fill:none;stroke:#a6b6ca;stroke-width:1.7}.edge.active{stroke:#477bbd;stroke-width:2.5}.edge.dim{opacity:.12}.node{position:absolute;top:0;left:0;width:280px;background:#fff;border:1px solid #d6dfe9;border-radius:10px;padding:14px 16px;box-shadow:0 2px 7px #24364b08;overflow-wrap:anywhere}.node.result{border-top:3px solid #4c7cba}.node.selected{outline:2px solid #7098cb;outline-offset:3px}.node.dim{opacity:.23}.node h2{margin:0 0 9px;font-size:15px;line-height:1.5;font-weight:650}.node .result-label{display:inline-block;font-size:10px;font-weight:500;color:#5279a7;padding:1px 5px;border-radius:4px;background:#edf3fb;margin-left:7px}.condition{margin:6px 0;line-height:1.7;font-size:12px}.condition b{color:#65768b;font-weight:500;margin-right:7px}.muted{font-size:12px;color:#8290a0}.case{border-top:1px solid #e7ecf2;padding-top:6px;margin-top:10px}.case-label{color:#6d7d91;font-size:11px}.node details{margin-top:9px;font-size:11px;color:#718096}.node details summary{cursor:pointer}.node details p{margin:5px 0}.node details ul{padding-left:17px;margin:5px 0}#notes{padding:10px 28px;background:#fff;border-top:1px solid #e0e6ed;font-size:12px;color:#718096;max-height:40vh;overflow:auto;flex-shrink:0}#notes summary{cursor:pointer}#notes p{margin:7px 0;white-space:pre-wrap}.warning{color:#9a651b}#zoom{min-width:40px;font-size:12px;text-align:center;color:#65768b}@media(max-width:600px){header{padding:16px}.toolbar{padding:10px 16px}.toolbar .direction{width:100%;margin:0}h1{font-size:20px}#notes{padding:12px 16px}}
7
+ :root {
8
+ color-scheme: light;
9
+ font: 14px/1.5 Inter, system-ui, "Microsoft YaHei", sans-serif;
10
+ color: #1e293b;
11
+ background: #eef1f6;
12
+ --border: #e1e7ef;
13
+ --muted: #607087;
14
+ }
15
+ * { box-sizing: border-box; }
16
+ body { margin: 0; padding: 18px; height: 100vh; height: 100dvh; }
17
+ .workspace {
18
+ display: flex; flex-direction: column; height: 100%; overflow: hidden;
19
+ background: #fff; border: 1px solid #dbe2ed; border-radius: 15px; box-shadow: 0 6px 26px #25375812;
20
+ }
21
+ .masthead {
22
+ display: flex; align-items: center; justify-content: space-between; gap: 10px 20px; flex-wrap: wrap;
23
+ padding: 12px 17px; border-bottom: 1px solid var(--border); flex-shrink: 0;
24
+ }
25
+ .head-title { display: flex; align-items: center; gap: 9px; min-width: 0; }
26
+ h1 { margin: 0; color: #23334a; font-size: 16px; font-weight: 720; }
27
+ .head-mark { color: #127e74; font-size: 19px; }
28
+ .head-meta { min-width: 0; }
29
+ .subtitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: min(49vw, 620px); }
30
+ .subtitle, .graph-counts, #match, #zoom { color: var(--muted); font-size: 12px; }
31
+ .graph-counts, #zoom { font-variant-numeric: tabular-nums; }
32
+ .toolbar, .zoom-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
33
+ button, input { font: inherit; }
34
+ button, .toolbar input {
35
+ color: #31445d; background: #fff; border: 1px solid var(--border); border-radius: 7px; font-size: 12px; min-height: 32px;
36
+ }
37
+ button { padding: 4px 10px; cursor: pointer; }
38
+ button:hover { border-color: #bdc9dd; background: #f5f7fc; }
39
+ button:focus-visible, input:focus-visible, summary:focus-visible, .node:focus-visible { outline: 2px solid #7061df; outline-offset: 2px; }
40
+ .toolbar input { width: 181px; max-width: 100%; padding: 5px 9px; }
41
+ #zoom { min-width: 34px; text-align: center; }
42
+ #viewport {
43
+ overflow: auto; flex: 1; min-height: 0; position: relative;
44
+ background-color: #f7f8fb;
45
+ background-image: linear-gradient(to right, #e9edf3 1px, transparent 1px), linear-gradient(to bottom, #e9edf3 1px, transparent 1px);
46
+ background-size: 23px 23px;
47
+ }
48
+ #space, #canvas { position: relative; }
49
+ #space { overflow: hidden; }
50
+ #canvas { transform-origin: 0 0; }
51
+ svg { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
52
+ .edge { fill: none; stroke: #a5b2c5; stroke-width: 1.35; }
53
+ .edge.active { stroke: #6d60df; stroke-width: 2.25; }
54
+ .edge.dim { opacity: .19; }
55
+ .node {
56
+ position: absolute; top: 0; left: 0; width: 252px; padding: 11px 12px;
57
+ background: #fff; border: 1px solid #dfe6f0; border-radius: 10px; box-shadow: 0 2px 5px #263b5510;
58
+ overflow-wrap: anywhere; cursor: pointer;
59
+ }
60
+ .node.expanding { border-left: 2px solid #7463ec; }
61
+ .node.has-cause { border-top-color: #b9d9d1; }
62
+ .node.expanding.has-cause { border-left-color: #208779; }
63
+ .node.dim { opacity: .35; }
64
+ .node.selected { outline: 2px solid #7463ec; outline-offset: 3px; }
65
+ .node-top { display: flex; align-items: flex-start; gap: 7px; }
66
+ .node-icon {
67
+ flex: 0 0 22px; height: 22px; text-align: center; line-height: 20px;
68
+ color: #687e9a; background: #f5f7fb; border: 1px solid #e5eaf1; border-radius: 5px; font-size: 13px;
69
+ }
70
+ .expanding .node-icon { color: #6d58cc; background: #f3f0ff; border-color: #e9e1fd; }
71
+ .node h2 { flex: 1; min-width: 0; margin: 1px 0 0; color: #1e293b; font-size: 13px; line-height: 1.43; }
72
+ .tag {
73
+ display: inline-block; width: fit-content; max-width: 100%; padding: 0 5px; border-radius: 4px;
74
+ color: var(--tone); background: var(--tint); border: 1px solid var(--stroke); font-size: 10px; font-weight: 680;
75
+ }
76
+ .expanded { --tone: #5e46b2; --tint: #f2edff; --stroke: #ded3fa; white-space: nowrap; }
77
+ .filter { --tone: #225b93; --tint: #f0f6fd; --stroke: #c9dcef; }
78
+ .parameter { --tone: #603b95; --tint: #f5f0fc; --stroke: #e1d3f2; }
79
+ .dynamic { --tone: #13675d; --tint: #edf8f5; --stroke: #c3e4da; }
80
+ .linkage { --tone: #893c58; --tint: #fbf0f4; --stroke: #eac9d6; }
81
+ .condition { margin: 8px 0 0; padding: 7px 8px; border-radius: 6px; background: var(--tint); font-size: 11px; }
82
+ .condition-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; color: var(--tone); }
83
+ .condition-name { font-weight: 650; color: #26384e; }
84
+ .reason { color: var(--tone); border: 1px solid var(--stroke); border-radius: 4px; padding: 0 4px; font-size: 10px; white-space: nowrap; }
85
+ .condition-change { margin-top: 3px; color: #26384e; white-space: pre-line; }
86
+ .origin { display: flex; align-items: baseline; gap: 6px; margin-top: 3px; color: var(--muted); }
87
+ .origin-label { flex-shrink: 0; font-weight: 650; }
88
+ .param-ref { color: #603b95; font-weight: 680; }
89
+ .case-label { color: var(--muted); font-size: 11px; margin: 7px 0 0; }
90
+ .case+.case { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 3px; }
91
+ .case-label { font-weight: 700; }
92
+ #notes summary { width: fit-content; cursor: pointer; }
93
+ #notes { padding: 6px 17px; border-top: 1px solid var(--border); font-size: 12px; color: var(--muted); max-height: 35vh; overflow: auto; flex-shrink: 0; }
94
+ #notes p { margin: 7px 0; white-space: pre-wrap; }
95
+ .warning { color: #8c480e; font-weight: 650; }
96
+ @media (max-width: 650px) {
97
+ body { padding: 6px; }
98
+ .masthead { padding: 9px 10px; }
99
+ .toolbar input { flex: 1; min-width: 122px; }
100
+ .subtitle { max-width: 77vw; }
101
+ }
8
102
  </style>
9
103
  </head>
10
104
  <body>
11
- <header><h1>计算血缘图</h1><div id="subtitle" class="subtitle"></div></header>
12
- <div class="toolbar">
13
- <input id="search" aria-label="查找表、字段或参数" placeholder="查找表、字段或参数"><span id="match"></span>
14
- <button id="minus" aria-label="缩小">−</button><span id="zoom">100%</span><button id="plus" aria-label="放大">+</button>
15
- <button id="fit">查看全图</button><button id="reset">原始大小</button><span class="direction">箭头从上游表指向下游表;条件直接写在表内</span>
16
- </div>
17
- <main id="viewport"><div id="space"><div id="canvas" aria-label="计算依赖全景"></div></div></main>
18
- <details id="notes"><summary id="notes-title">计算信息与采集说明</summary><p id="identity"></p><p id="summary"></p><p id="coverage"></p><p id="evidence"></p></details>
105
+ <div class="workspace">
106
+ <header class="masthead">
107
+ <div class="head-title">
108
+ <span class="head-mark" aria-hidden="true">▦</span>
109
+ <div class="head-meta"><h1>计算关系图</h1><div id="subtitle" class="subtitle"></div></div>
110
+ </div>
111
+ <div class="toolbar">
112
+ <span id="graph-counts" class="graph-counts"></span>
113
+ <input id="search" type="search" aria-label="查找表或条件来源" placeholder="查找表或条件来源"><span id="match" aria-live="polite"></span>
114
+ <div class="zoom-tools">
115
+ <button id="minus" aria-label="缩小">−</button><span id="zoom">100%</span><button id="plus" aria-label="放大">+</button>
116
+ </div>
117
+ <button id="fit">查看全图</button><button id="reset">原始大小</button>
118
+ </div>
119
+ </header>
120
+ <main id="viewport"><div id="space"><div id="canvas" aria-label="计算依赖全景"></div></div></main>
121
+ <details id="notes"><summary id="notes-title">计算信息与采集说明</summary><p id="identity"></p><p id="summary"></p><p id="coverage"></p><p id="evidence"></p></details>
122
+ </div>
19
123
  <script type="application/json" id="panorama-data">__PANORAMA_DATA__</script>
20
124
  <script>
21
125
  'use strict';
@@ -24,10 +128,11 @@ const $=id=>document.getElementById(id);
24
128
  const el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n};
25
129
  const actual=data.schema==='calculation-panorama-v2',legacy=data.schema==='legacy-condition-tree';
26
130
  const tables=new Map(data.tables.map(t=>[t.id,t]));
27
- const parameterNames=new Map((data.parameters||[]).map(p=>[p.id,p.name||p.id]));
131
+ const parameterNames=new Map((data.parameters||[]).map(p=>[p.id,p.name]));
132
+ const parameterLabel=id=>'参数「'+(parameterNames.get(id)||'名称未记录')+'」';
28
133
  const controls=new Map((data.controls||[]).map(c=>[c.id,c]));
29
134
  const dynamicDefinitions=new Map((data.dynamicDefinitions||[]).map(d=>[JSON.stringify([d.tableId,d.operatorId,d.itemId]),d]));
30
- const tableName=id=>tables.get(id)?.kind==='INLINE_QUERY'?'内联子查询':tables.get(id)?.name||id;
135
+ const tableName=id=>tables.get(id)?.name||id;
31
136
  const limited=!!(data.omittedNodes||data.omittedEdges||(data.gaps||[]).some(g=>g.includes('_LIMIT')));
32
137
  const operators={In:'属于',NotIn:'不属于',Between:'介于',NotBetween:'不在区间',Null:'为空',NotNull:'不为空',LinkageIn:'属于联动选中的记录',SubQueryIn:'属于子查询结果',NumberLarge:'>',NumberSmall:'<',NumberEqual:'=',NumberNoEqual:'≠',NumberLargeOrEqual:'≥',NumberSmallOrEqual:'≤',StringBelong:'属于',StringNoBelong:'不属于',StringContain:'包含',StringNoContain:'不包含'};
33
138
  const requestOperators={Large:'>',Small:'<',Equal:'=',NoEqual:'≠',LargeOrEqual:'≥',SmallOrEqual:'≤',Belong:'属于',NotBelong:'不属于',NoBelong:'不属于',Contain:'包含',NoContain:'不包含',After:'晚于',Before:'早于',Between:'介于',NoBetween:'不在区间',Null:'为空',NotNull:'不为空',TopN:'前 N 项',BottomN:'后 N 项',Formula:'公式筛选'};
@@ -45,77 +150,128 @@ function conditionField(c,tableId){
45
150
  }
46
151
  function conditionText(c,tableId){
47
152
  if(c.operator==='LinkageIn')return '联动选中的记录';
48
- const parameters=(c.parameterIds||[]).map(id=>'参数「'+(parameterNames.get(id)||id)+'」').join('、');
153
+ const parameters=(c.parameterIds||[]).map(parameterLabel).join('、');
49
154
  if(c.kind==='COMBINED')return '('+(c.children||[]).map(child=>conditionText(child,tableId)).join(c.operator==='OR'?' 或 ':' 且 ')+')'+(parameters?'(涉及'+parameters+')':'')+(c.valueState==='TRUNCATED'?'(条件未采集完)':'');
50
155
  const requestType=c.kind==='REQUEST'?(c.operator||'').replace(/^(ComposeDate|Date|String|Number|Json)/,''):null;
51
156
  const value=requestType==='Null'||requestType==='NotNull'?'':c.valueState==='PARAMETER'?parameters:c.valueState==='REDACTED'?'[值已脱敏]':c.valueState==='UNAVAILABLE'?'[内容未记录]':c.valueState==='TRUNCATED'?'[条件未采集完]':'';
52
157
  if(c.operator==='TOP'||c.operator==='BOTTOM'||requestType==='TopN'||requestType==='BottomN')return [conditionField(c,tableId),c.operator==='TOP'||requestType==='TopN'?'取前':'取后',value||'N','项'].filter(Boolean).join(' ');
53
158
  return [conditionField(c,tableId),operators[c.operator]||(requestType?requestOperators[requestType]||'筛选类型未展开':c.operator)||(c.kind==='UNKNOWN'?'条件内容未记录':c.kind),value].filter(Boolean).join(' ')+(parameters&&c.valueState!=='PARAMETER'?'(涉及'+parameters+')':'');
54
159
  }
55
- function controlNames(ids){return [...new Set((ids||[]).map(id=>controls.get(id)).filter(Boolean).map(source=>{const kinds={COMPONENT_LINKAGE:'组件联动',VALUE_LINKAGE:'值联动',PARAMETER:'参数控件',DYNAMIC_FIELD:'动态字段控件'};return source.kind==='ROW_PERMISSION'?'行权限':kinds[source.kind]?kinds[source.kind]+'「'+(source.name||'名称未记录')+'」':source.name||'筛选器名称未记录'}))]}
56
- function conditionDescription(c,tableId){const sources=controlNames(data.conditionControls?.[c._traceConditionId]);return conditionText(c,tableId)+(sources.length?'('+sources.join('、')+')':'')}
57
- function row(parent,label,text){if(!text)return;const p=el('p',undefined,'condition');p.append(el('b',label),document.createTextNode(text));parent.append(p)}
58
- function knownCount(node,key,oldKey){return Array.isArray(node[key])?node[key].length:node.counts?.[oldKey]===undefined?null:Number(node.counts[oldKey])}
59
- function dynamicText(node,tableId){return (node.dynamicFields||[]).map(d=>{const definition=dynamicDefinitions.get(JSON.stringify([tableId,d.operatorId,d.itemId])),sources=controlNames(d.controlIds);return (definition?.name||d.name||d.itemName||'名称未记录')+':'+(definition?.defaultFieldId?fieldName(definition.defaultFieldId,tableId)+' → ':'')+fieldName(d.fieldName||d.fieldId,tableId)+(sources.length?'(来自'+sources.join('、')+')':'')}).join(';')}
60
- const filterKeys=[['fieldFilters','字段筛选','field'],['componentFilters','组件筛选','advanced_component'],['linkageFilters','联动筛选','linkage']];
61
- // 一个表只画一个节点。同表有不同的读取条件时逐组展示,不把不同读取拼成一个 AND。
62
- const nodes=data.tables.map(t=>({id:t.id,table:t,cases:data.nodes.filter(n=>n.tableId===t.id)}));
63
- const edges=legacy?[...new Map(data.conditionEdges.map(e=>{const a=data.nodes.find(n=>n.id===e.source),b=data.nodes.find(n=>n.id===e.target);return [a.tableId+'\0'+b.tableId,{source:a.tableId,target:b.tableId}]})).values()]:data.dependencies;
160
+ function controlNames(ids){return [...new Set((ids||[]).map(id=>controls.get(id)).filter(Boolean).map(source=>{
161
+ const kinds={CONTROL:'筛选器',COMPONENT_LINKAGE:'组件联动',VALUE_LINKAGE:'值联动',PARAMETER:'参数控件',DYNAMIC_FIELD:'动态字段控件'};
162
+ return source.kind==='ROW_PERMISSION'?'行权限':(kinds[source.kind]||'筛选器')+(source.name?'「'+source.name+'」':'(名称未记录)');
163
+ }))]}
164
+ function dynamicSelection(d,tableId){
165
+ const definition=dynamicDefinitions.get(JSON.stringify([tableId,d.operatorId,d.itemId]));
166
+ const name=definition?.name||d.name||d.itemName||'名称未记录';
167
+ const selection=(definition?.defaultFieldId?fieldName(definition.defaultFieldId,tableId)+' → ':'')+fieldName(d.fieldName||d.fieldId,tableId);
168
+ return {name,selection};
169
+ }
170
+ function knownCount(input,key,oldKey){return Array.isArray(input[key])?input[key].length:input.counts?.[oldKey]===undefined?null:Number(input.counts[oldKey])}
171
+ const filterKeys=[['fieldFilters','字段筛选','field','filter'],['linkageFilters','联动筛选','linkage','linkage']];
172
+ const readLabels={READ_MATERIALIZED:'直读',EXPANDED:'展开',REUSED_INPUT:'复用输入'};
173
+ const nodes=data.tables.map(t=>{
174
+ const modes=[...new Set([t.definition,...(t.variants||[])].filter(Boolean).map(item=>item.state))];
175
+ return {id:t.id,cases:data.nodes.filter(n=>n.tableId===t.id),modes,expanded:actual&&modes.includes('EXPANDED')};
176
+ });
177
+ const conditionNodes=new Map(data.nodes.map(n=>[n.id,n]));
178
+ const edges=legacy?[...new Map(data.conditionEdges.map(e=>{
179
+ const a=conditionNodes.get(e.source),b=conditionNodes.get(e.target);
180
+ return [a.tableId+'\0'+b.tableId,{source:a.tableId,target:b.tableId}];
181
+ })).values()]:data.dependencies;
64
182
  const byId=new Map(nodes.map(n=>[n.id,n]));
65
183
  const incoming=new Map(nodes.map(n=>[n.id,[]])),outgoing=new Map(nodes.map(n=>[n.id,[]]));
66
184
  for(const e of edges){incoming.get(e.target).push(e.source);outgoing.get(e.source).push(e.target)}
67
- function renderConditions(n,card){
68
- const definitions=[n.table.definition,...(n.table.variants||[])].filter(Boolean);
69
- const usedParameters=new Set();function remember(c){for(const id of c.parameterIds||[])usedParameters.add(id);for(const child of c.children||[])remember(child)}
70
- const describedConditions=[...definitions.flatMap(d=>(d.filters||[]).flatMap(s=>s.conditions||[])),...n.cases.flatMap(c=>filterKeys.flatMap(([key])=>c[key]||[]))];
71
- if(n.id===data.rootTableId){describedConditions.push(...(data.componentStages||[]).flatMap(s=>s.conditions||[]));describedConditions.push(...(data.componentDefinition?.filters||[]).flatMap(s=>s.conditions||[]))}describedConditions.forEach(remember);
72
- for(const [index,definition] of definitions.entries()){
73
- const conditions=(definition.filters||[]).flatMap(s=>s.conditions||[]);
74
- if(conditions.length)row(card,definitions.length>1?'表内筛选(记录 '+(index+1)+')':'表内筛选',conditions.map(c=>conditionDescription(c,n.id)).join(';'));
185
+ const filtersUsedByExpanded=new Set();
186
+ if(actual)for(const edge of data.conditionEdges){
187
+ const source=conditionNodes.get(edge.source),target=conditionNodes.get(edge.target);
188
+ if(byId.get(target.tableId).expanded&&incoming.get(target.tableId).includes(source.tableId))filtersUsedByExpanded.add(source.id);
189
+ }
190
+ function conditionParameters(condition,ids=new Set()){
191
+ for(const id of condition.parameterIds||[])ids.add(id);
192
+ for(const child of condition.children||[])conditionParameters(child,ids);
193
+ return ids;
194
+ }
195
+ function tableConditions(n){
196
+ const triggersExpansion=use=>n.expanded&&use.sources?.includes('TABLE');
197
+ function describeFilter(condition,label,kind,parameters,reason){
198
+ const controlIds=[...(data.conditionControls?.[condition._traceConditionId]||[])];
199
+ const referenced=conditionParameters(condition);
200
+ for(const id of referenced){
201
+ const uses=(parameters||[]).filter(p=>p.id===id);
202
+ if(uses.length!==1||triggersExpansion(uses[0]))continue;
203
+ controlIds.push(...(uses[0].controlIds||[]));
204
+ }
205
+ return {label,kind,change:conditionText(condition,n.id),sources:controlNames(controlIds),reason,showSource:referenced.size>0};
75
206
  }
76
- for(const [index,c] of n.cases.entries()){
77
- const group=n.cases.length>1?el('div',undefined,'case'):card;
78
- if(group!==card){group.append(el('div','本表条件组 '+(index+1),'case-label'));card.append(group)}
79
- for(const [key,label,oldKey] of filterKeys){
80
- if(c[key]?.length)row(group,label,c[key].map(condition=>conditionDescription(condition,n.id)).join(';'));
81
- else if(!Array.isArray(c[key])&&knownCount(c,key,oldKey)!==0){const count=knownCount(c,key,oldKey);row(group,label,count===null?'未记录':count+' 项,具体内容未记录')}
207
+ return n.cases.map(input=>{
208
+ const entries=(input.dynamicFields||[]).map(d=>{
209
+ const {name,selection}=dynamicSelection(d,n.id);
210
+ return {kind:'dynamic',label:'动态字段',name,change:selection,sources:controlNames(d.controlIds),
211
+ reason:n.expanded?'触发展开':'',showSource:true};
212
+ });
213
+ const reason=filtersUsedByExpanded.has(input.id)?'影响下游展开':'';
214
+ for(const [key,label,oldKey,kind] of filterKeys){
215
+ for(const condition of input[key]||[]){
216
+ entries.push(describeFilter(condition,label,kind,input.parameters,reason));
217
+ }
218
+ if(!Array.isArray(input[key])){
219
+ const count=knownCount(input,key,oldKey);
220
+ if(count>0)entries.push({label,kind,change:count+' 项,具体内容未记录'});
221
+ }
222
+ }
223
+ for(const p of input.parameters||[]){
224
+ if(!triggersExpansion(p))continue;
225
+ entries.push({kind:'parameter',label:'参数覆盖',change:parameterLabel(p.id),sources:controlNames(p.controlIds),
226
+ reason:'触发展开',showSource:true});
82
227
  }
83
- if(c.parameters?.length){for(const p of c.parameters){
84
- const controlSources=controlNames(p.controlIds);if(controlSources.length)row(group,'参数来源','参数「'+(parameterNames.get(p.id)||p.id)+'」来自'+controlSources.join('、'));
85
- if(usedParameters.has(p.id))continue;
86
- const name='「'+(parameterNames.get(p.id)||p.id)+'」',sources=p.sources||[],table=sources.includes('TABLE'),input=sources.includes('COMPONENT');
87
- if(table)row(group,'参数输入',(input?'本次由组件传入的表级参数':'本次传入的表级参数')+name+'(具体用途未记录)');
88
- else if(input)continue;
89
- else row(group,'参数记录',name+'的来源和具体用途未记录');
90
- }}
91
- else if(!Array.isArray(c.parameters)&&knownCount(c,'parameters','parameter')!==0){const count=knownCount(c,'parameters','parameter');row(group,'参数记录',count===null?'名称和用途未记录':count+' 个,名称和具体用途未记录')}
92
- if(c.dynamicFields?.length)row(group,'动态字段',dynamicText(c,n.id));
93
- else if(!Array.isArray(c.dynamicFields)&&knownCount(c,'dynamicFields','dynamic')!==0){const count=knownCount(c,'dynamicFields','dynamic');row(group,'动态字段',count===null?'未记录':count+' 项,名称与选择未记录')}
228
+ const count=knownCount(input,'dynamicFields','dynamic');
229
+ if(!Array.isArray(input.dynamicFields)&&count>0)entries.push({label:'动态字段',kind:'dynamic',change:count+' 项,具体内容未记录'});
230
+ return entries;
231
+ }).filter(entries=>entries.length);
232
+ }
233
+ function conditionCard(entry){
234
+ const block=el('section',undefined,'condition '+entry.kind),head=el('div',undefined,'condition-head');
235
+ head.append(el('strong',entry.label));
236
+ if(entry.name)head.append(el('span',entry.name,'condition-name'));
237
+ if(entry.reason)head.append(el('span',entry.reason,'reason'));
238
+ const change=el('div',undefined,'condition-change');
239
+ for(const [i,part] of entry.change.split(/(参数「[^」]+」)/g).entries()){
240
+ change.append(i%2?el('span',part,'param-ref'):document.createTextNode(part));
241
+ }
242
+ block.append(head,change);
243
+ if(entry.sources?.length||entry.showSource||entry.reason){
244
+ const origin=el('div',undefined,'origin');
245
+ origin.append(el('span','来源','origin-label'),el('span',entry.sources?.join('、')||'未记录'));
246
+ block.append(origin);
94
247
  }
95
- if(n.id===data.rootTableId&&data.componentDefinition){for(const step of data.componentDefinition.filters||[])if(step.conditions?.length)row(card,'组件步骤筛选',step.conditions.map(c=>conditionDescription(c,n.id)).join(';'))}
96
- if(n.id===data.rootTableId)for(const stage of data.componentStages||[])if(stage.conditions?.length)row(card,(stage.source==='REQUEST'?'组件请求':'组件')+(stage.stage==='DETAIL'?'明细筛选':'结果筛选'),stage.conditions.map(c=>conditionDescription(c,n.id)).join(';'));
97
- if(!legacy&&!card.querySelector('.condition'))card.append(el('p',actual?(definitions.some(d=>d.state==='REUSED_INPUT')?'本次复用已有输入,来源筛选未记录':'本次记录中没有附加条件'):'条件未记录','muted'));
98
- if(legacy&&n.cases.every(c=>filterKeys.every(([key,label,oldKey])=>knownCount(c,key,oldKey)===0)&&knownCount(c,'parameters','parameter')===0&&knownCount(c,'dynamicFields','dynamic')===0))card.append(el('p','本次记录的附加条件计数为 0','muted'));
248
+ return block;
99
249
  }
100
250
  const canvas=$('canvas'),ns='http://www.w3.org/2000/svg';
101
251
  const svg=document.createElementNS(ns,'svg');svg.setAttribute('aria-label','上游到下游依赖连线');
102
252
  svg.innerHTML='<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10Z" fill="#8c9fb9"/></marker></defs>';canvas.append(svg);
103
253
  for(const n of nodes){
104
- const card=el('article',undefined,'node'+(n.id===data.rootTableId?' result':''));n.card=card;card.dataset.id=n.id;card.setAttribute('aria-label',tableName(n.id));
105
- const title=el('h2',tableName(n.id));if(n.id===data.rootTableId)title.append(el('span','本次结果','result-label'));card.append(title);renderConditions(n,card);canvas.append(card);
106
- }
107
- // 节点只展示自身条件,上下游关系由连线表达。
108
- for(const n of nodes){
109
- // 查找直接作用于本表的条件;折叠区中的组件传入参数不代表本表实际使用。
110
- n.searchText=n.card.textContent;
111
- const technical=el('details');technical.append(el('summary','ID 与读取信息'));technical.append(el('p','表 ID:'+n.id));
112
- if(n.cases.length)technical.append(el('p','条件记录:'+n.cases.map(c=>c.id).join('、')));
113
- function fields(c){return [c.field,...(c.children||[]).flatMap(fields)].filter(Boolean)}
114
- const fieldIds=[...new Set(n.cases.flatMap(c=>filterKeys.flatMap(([key])=>(c[key]||[]).flatMap(fields))))];
115
- if(fieldIds.length)technical.append(el('p','条件字段原标识:'+fieldIds.join('、')));
116
- if(n.table.definition)technical.append(el('p',actual?({READ_MATERIALIZED:'本次直接读取已有计算结果',REUSED_INPUT:'本次复用已有查询输入,来源内部条件未记录',EXPANDED:'本次重新展开来源计算'}[n.table.definition.state]||'本次读取方式未记录'):n.table.definition.state==='PUBLISHED'?'发布配置,仅供对照':'输入配置'));
117
- for(const c of n.cases){for(const p of c.parameters||[])technical.append(el('p','参数 '+(parameterNames.get(p.id)||p.id)+':'+p.id));for(const d of c.dynamicFields||[])technical.append(el('p','动态字段步骤:'+d.operatorId+';项:'+d.itemId+';选中字段:'+d.fieldId))}
118
- n.card.append(technical);n.card.onclick=e=>{if(!e.target.closest('details')){selected=selected===n.id?null:n.id;highlight()}};
254
+ const groups=tableConditions(n);
255
+ const hasCause=groups.some(group=>group.some(entry=>entry.reason));
256
+ const card=el('article',undefined,'node'+(n.expanded?' expanding':'')+(hasCause?' has-cause':''));n.card=card;card.dataset.id=n.id;
257
+ const name=tableName(n.id),mode=n.modes.map(read=>readLabels[read]).filter(Boolean).join('、');
258
+ card.tabIndex=0;card.setAttribute('aria-label',mode?name+',读取方式 '+mode:name);
259
+ card.title=mode?name+'\n读取方式 '+mode:name;
260
+ const top=el('div',undefined,'node-top'),icon=el('span','▦','node-icon');
261
+ icon.setAttribute('aria-hidden','true');top.append(icon,el('h2',name));
262
+ if(n.expanded)top.append(el('span','展开','tag expanded'));
263
+ card.append(top);
264
+ groups.forEach((entries,index)=>{
265
+ const group=el('div',undefined,'case');
266
+ if(n.cases.length>1&&groups.length>1)group.append(el('p','条件组合 '+(index+1),'case-label'));
267
+ entries.forEach(entry=>group.append(conditionCard(entry)));
268
+ card.append(group);
269
+ });
270
+ n.searchText=card.textContent;
271
+ const select=()=>{selected=selected===n.id?null:n.id;highlight()};
272
+ card.onclick=select;
273
+ card.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();select()}};
274
+ canvas.append(card);
119
275
  }
120
276
  let graphWidth=700,graphHeight=600,scale=1,selected=null;
121
277
  function layout(){
@@ -124,35 +280,59 @@ function layout(){
124
280
  function visit(id){indices.set(id,index);low.set(id,index++);stack.push(id);on.add(id);for(const next of outgoing.get(id)){if(!indices.has(next)){visit(next);low.set(id,Math.min(low.get(id),low.get(next)))}else if(on.has(next))low.set(id,Math.min(low.get(id),indices.get(next)))}if(low.get(id)===indices.get(id)){const group=[];let next;do{next=stack.pop();on.delete(next);groupOf.set(next,groups.length);group.push(next)}while(next!==id);groups.push(group)}}
125
281
  for(const n of nodes)if(!indices.has(n.id))visit(n.id);
126
282
  const rank=new Map();function level(group){if(rank.has(group))return rank.get(group);const parents=[...new Set(groups[group].flatMap(id=>incoming.get(id).map(p=>groupOf.get(p))).filter(p=>p!==group))];const value=parents.length?1+Math.max(...parents.map(level)):0;rank.set(group,value);return value}groups.forEach((g,i)=>level(i));
127
- const units=new Map(),layers=[];for(const n of nodes){n.rank=rank.get(groupOf.get(n.id));n.w=280;n.h=n.card.offsetHeight;units.set(n.id,n);(layers[n.rank]??=[]).push(n)}
283
+ const units=new Map(),layers=[];for(const n of nodes){n.rank=rank.get(groupOf.get(n.id));n.w=n.card.offsetWidth;n.h=n.card.offsetHeight;units.set(n.id,n);(layers[n.rank]??=[]).push(n)}
128
284
  const paths=edges.map((e,i)=>{const a=byId.get(e.source),b=byId.get(e.target),chain=[a];for(let r=a.rank+1;r<b.rank;r++){const dummy={id:'edge-'+i+'-'+r,rank:r,w:0,h:18};units.set(dummy.id,dummy);layers[r].push(dummy);chain.push(dummy)}chain.push(b);return {edge:e,chain}});
129
285
  const previous=new Map([...units.keys()].map(id=>[id,[]])),next=new Map([...units.keys()].map(id=>[id,[]]));for(const p of paths)for(let i=1;i<p.chain.length;i++){previous.get(p.chain[i].id).push(p.chain[i-1].id);next.get(p.chain[i-1].id).push(p.chain[i].id)}
130
286
  const position=new Map();function order(){layers.forEach(layer=>layer.forEach((u,i)=>position.set(u.id,i)))}order();
131
287
  for(let pass=0;pass<6;pass++){const forward=pass%2===0,sequence=forward?layers:layers.toReversed(),neighbors=forward?previous:next;for(const layer of sequence){const score=u=>{const ns=neighbors.get(u.id);return ns.length?ns.reduce((s,id)=>s+position.get(id),0)/ns.length:position.get(u.id)};layer.sort((a,b)=>score(a)-score(b));order()}}
132
- const heights=layers.map(layer=>layer.reduce((s,u)=>s+u.h,0)+Math.max(0,layer.length-1)*42);graphHeight=Math.max(250,...heights)+64;
133
- let left=32;for(const [r,layer] of layers.entries()){let top=(graphHeight-heights[r])/2;const columnWidth=Math.max(...layer.map(u=>u.w));for(const u of layer){u.x=left;u.y=top;u.columnWidth=columnWidth;top+=u.h+42;if(u.card){u.card.style.left=u.x+'px';u.card.style.top=u.y+'px'}}left+=columnWidth+72}graphWidth=left-40;
288
+ const heights=layers.map(layer=>layer.reduce((s,u)=>s+u.h,0)+Math.max(0,layer.length-1)*36);
289
+ graphHeight=Math.max(280,...heights)+56;
290
+ let left=28;
291
+ for(const [r,layer] of layers.entries()){
292
+ let top=(graphHeight-heights[r])/2;
293
+ const columnWidth=Math.max(...layer.map(u=>u.w));
294
+ for(const u of layer){
295
+ u.x=left+(columnWidth-u.w)/2;u.y=top;top+=u.h+36;
296
+ if(u.card){u.card.style.left=u.x+'px';u.card.style.top=u.y+'px'}
297
+ else{u.laneStart=left-4;u.laneEnd=left+columnWidth+4}
298
+ }
299
+ left+=columnWidth+60;
300
+ }
301
+ graphWidth=left-32;
134
302
  canvas.style.width=graphWidth+'px';canvas.style.height=graphHeight+'px';svg.setAttribute('width',graphWidth);svg.setAttribute('height',graphHeight);svg.querySelectorAll('.edge').forEach(p=>p.remove());
135
303
  for(const p of paths){const first=p.chain[0];let from={x:first.x+first.w,y:first.y+first.h/2},d='M'+from.x+','+from.y;
136
- if(p.chain[0].rank>=p.chain.at(-1).rank){const a=p.chain[0],b=p.chain.at(-1),lane=Math.max(a.x+a.w,b.x+b.w)+20;d='M'+(a.x+a.w)+','+(a.y+a.h/2)+' H'+lane+' V'+(b.y+b.h/2)+' H'+(b.x+b.w)}
137
- else for(let i=1;i<p.chain.length;i++){const u=p.chain[i],to={x:u.x,y:u.y+u.h/2},middle=(from.x+to.x)/2;d+=' C'+middle+','+from.y+' '+middle+','+to.y+' '+to.x+','+to.y;if(i<p.chain.length-1){to.x+=u.columnWidth;d+=' H'+to.x}from=to}
304
+ if(first.rank>=p.chain.at(-1).rank){const b=p.chain.at(-1),lane=Math.max(from.x,b.x+b.w)+20;d+=' H'+lane+' V'+(b.y+b.h/2)+' H'+(b.x+b.w)}
305
+ else for(let i=1;i<p.chain.length;i++){
306
+ const u=p.chain[i],to={x:u.card?u.x:u.laneStart,y:u.y+u.h/2},middle=(from.x+to.x)/2;
307
+ d+=' C'+middle+','+from.y+' '+middle+','+to.y+' '+to.x+','+to.y;
308
+ if(!u.card){to.x=u.laneEnd;d+=' H'+to.x}
309
+ from=to;
310
+ }
138
311
  const path=document.createElementNS(ns,'path');path.setAttribute('d',d);path.setAttribute('class','edge');path.setAttribute('marker-end','url(#arrow)');path.dataset.source=p.edge.source;path.dataset.target=p.edge.target;svg.append(path)}
139
312
  zoom(scale);highlight();
140
313
  }
141
- function zoom(value){scale=Math.max(.12,Math.min(2,value));canvas.style.transform='scale('+scale+')';const spaceWidth=Math.max($('viewport').clientWidth,Math.ceil(graphWidth*scale));$('space').style.width=spaceWidth+'px';$('space').style.height=Math.ceil(graphHeight*scale)+'px';canvas.style.left=Math.max(0,(spaceWidth-graphWidth*scale)/2)+'px';$('zoom').textContent=Math.round(scale*100)+'%'}
314
+ function zoom(value){
315
+ scale=Math.max(.12,Math.min(2,value));canvas.style.transform='scale('+scale+')';
316
+ const viewport=$('viewport'),width=Math.max(viewport.clientWidth,Math.ceil(graphWidth*scale)),height=Math.max(viewport.clientHeight,Math.ceil(graphHeight*scale));
317
+ $('space').style.width=width+'px';$('space').style.height=height+'px';
318
+ canvas.style.left=(width-graphWidth*scale)/2+'px';canvas.style.top=(height-graphHeight*scale)/2+'px';
319
+ $('zoom').textContent=Math.round(scale*100)+'%';
320
+ }
142
321
  function fit(){zoom(Math.min(1,($('viewport').clientWidth-24)/graphWidth,($('viewport').clientHeight-24)/graphHeight));$('viewport').scrollTo(0,0)}
143
322
  function highlight(){const query=$('search').value.trim().toLocaleLowerCase(),matches=new Set(nodes.filter(n=>(tableName(n.id)+' '+n.id+' '+n.searchText).toLocaleLowerCase().includes(query)).map(n=>n.id));const focus=query?matches:selected?new Set([selected,...incoming.get(selected),...outgoing.get(selected)]):null;for(const n of nodes){n.card.classList.toggle('dim',!!focus&&!focus.has(n.id));n.card.classList.toggle('selected',n.id===selected||!!query&&matches.has(n.id))}for(const p of svg.querySelectorAll('.edge')){const active=selected&&(p.dataset.source===selected||p.dataset.target===selected);p.classList.toggle('active',!!active);p.classList.toggle('dim',!!focus&&(!focus.has(p.dataset.source)||!focus.has(p.dataset.target)))}$('match').textContent=query?'找到 '+matches.size+' 张表':''}
144
- const gapNames={NON_FILTER_STEP_CONDITIONS:'未展开公式、子查询等内部条件',PHYSICAL_EXECUTION:'本图展示查询依赖,不展示引擎物理执行计划',RAW_VALUES:'trace 中的业务值已脱敏',COMPONENT_INPUT_REUSED:'组件追加阶段复用了已有输入,未执行准备的追加筛选'};
145
- const inlineCount=nodes.filter(n=>n.table.kind==='INLINE_QUERY').length;
146
- $('subtitle').textContent=[data.componentName||'组件名称未记录',(nodes.length-inlineCount)+' 张表',inlineCount?inlineCount+' 个内联查询':null,edges.length+' 条关系'].filter(Boolean).join(' · ');
147
- $('identity').textContent=['Trace:'+(data.traceId||'未记录'),data.time,'组件 ID:'+(data.componentId||'未记录'),'来源 span:'+(data.sourceSpan||'输入快照')].filter(Boolean).join('\n');
323
+ const gapNames={TABLE_DEFINITION_FILTERS:'图中展示本次取数的附加条件',NON_FILTER_STEP_CONDITIONS:'公式等步骤内部的条件未采集',PHYSICAL_EXECUTION:'依赖关系来自查询构建',RAW_VALUES:'trace 中的业务值已脱敏',COMPONENT_INPUT_REUSED:'组件追加阶段复用了已有输入,未执行准备的追加筛选'};
324
+ $('subtitle').textContent=data.componentName||'结果表 '+tableName(data.rootTableId);
325
+ $('subtitle').title=$('subtitle').textContent;
326
+ $('graph-counts').textContent=nodes.length+' 张表,'+edges.length+' 条依赖';
327
+ $('identity').textContent=[data.traceId?'Trace '+data.traceId:null,data.time].filter(Boolean).join('\n');
148
328
  $('summary').textContent=data.summary||'';
149
- const coverage=[legacy?'旧 trace 仅记录条件展开关系,缺失的名称和条件内容保留为未记录。':actual?'范围:本次查询构建。查询构建完成不代表引擎执行成功。':'范围:历史逻辑依赖,不能当作本次实际展开。',...(data.gaps||[]).map(g=>g.startsWith('REUSED_INPUT_DETAILS_UNAVAILABLE:')?'复用输入的来源条件未记录:'+tableName(g.slice(g.indexOf(':')+1)):gapNames[g]||g),...(data.notCaptured||[]).map(g=>gapNames[g]||g)];
329
+ const coverage=[legacy?'旧 trace 只记录了条件传播关系。':actual?'本图依据本次查询构建记录,业务值已脱敏。':'历史快照记录的是逻辑依赖。',...(data.gaps||[]).map(g=>g.startsWith('REUSED_INPUT_DETAILS_UNAVAILABLE:')?'复用输入的来源条件未记录('+tableName(g.slice(g.indexOf(':')+1))+')':gapNames[g]||g)];
150
330
  if(data.omittedNodes||data.omittedEdges)coverage.push('已省略 '+data.omittedNodes+' 个条件对象、'+data.omittedEdges+' 条条件关系');
151
331
  $('coverage').textContent=coverage.join('\n');$('evidence').textContent=(data.evidence||[]).join('\n');
152
- if(legacy||data.scope==='BUILD_FAILED'||data.gaps?.length||limited){$('notes-title').textContent='采集有缺口,部分信息未记录 · 展开说明';$('notes-title').classList.add('warning')}
332
+ if(legacy||data.scope==='BUILD_FAILED'||data.gaps?.length||limited){$('notes-title').textContent='部分计算信息未记录';$('notes-title').classList.add('warning')}
153
333
  $('search').oninput=()=>{highlight();if($('search').value.trim())canvas.querySelector('.node.selected')?.scrollIntoView({block:'center',inline:'center'})};$('minus').onclick=()=>zoom(scale/1.2);$('plus').onclick=()=>zoom(scale*1.2);$('fit').onclick=fit;$('reset').onclick=()=>zoom(1);
154
- // 条件展开后重新测量卡片,连线跟随高度变化,不遮住新展开的内容。
155
- canvas.addEventListener('toggle',()=>requestAnimationFrame(layout),true);layout();zoom(Math.max(.7,Math.min(1,($('viewport').clientWidth-24)/graphWidth)));
334
+ window.addEventListener('resize',()=>zoom(scale));
335
+ layout();zoom(Math.max(.75,Math.min(1,($('viewport').clientWidth-24)/graphWidth)));
156
336
  </script>
157
337
  </body>
158
338
  </html>
@@ -235,8 +235,83 @@ def normalize(source, span_id=None, context=None):
235
235
  return snapshot
236
236
 
237
237
 
238
+ def graph_dependencies(tables, dependencies, inline):
239
+ """历史快照的内联分支只作中转,遍历到下一张业务表就停止。"""
240
+ consumers = {}
241
+ for edge in dependencies:
242
+ consumers.setdefault(edge['source'], []).append(edge['target'])
243
+ result = []
244
+ for table in tables:
245
+ source = table['id']
246
+ pending = list(reversed(consumers.get(source, [])))
247
+ seen = set()
248
+ while pending:
249
+ target = pending.pop()
250
+ if target in seen:
251
+ continue
252
+ seen.add(target)
253
+ if target in inline:
254
+ pending.extend(reversed(consumers.get(target, [])))
255
+ elif target != source:
256
+ if len(result) == 4096:
257
+ return result, True
258
+ result.append({'source': source, 'target': target})
259
+ return result, False
260
+
261
+
262
+ def graph_data(snapshot):
263
+ """把已核验快照整理成业务表关系与直接作用于表的条件。"""
264
+ graph = dict(snapshot)
265
+ graph.pop('componentDefinition', None)
266
+ graph.pop('componentStages', None)
267
+ graph['nodes'] = [{key: value for key, value in node.items() if key != 'componentFilters'}
268
+ for node in snapshot['nodes']]
269
+ inline = {table['id'] for table in snapshot['tables'] if table.get('kind') == 'INLINE_QUERY'}
270
+ tables = []
271
+ expanded = set()
272
+ for table in snapshot['tables']:
273
+ if table['id'] in inline:
274
+ continue
275
+ modes = dict.fromkeys(item['state'] for item in [table.get('definition'), *table.get('variants', [])] if item)
276
+ if 'EXPANDED' in modes:
277
+ expanded.add(table['id'])
278
+ definitions = [{'state': mode} for mode in modes]
279
+ tables.append(dict(table, definition=definitions[0] if definitions else None, variants=definitions[1:]))
280
+ graph['tables'] = tables
281
+ if inline:
282
+ graph['dependencies'], limited = graph_dependencies(tables, snapshot['dependencies'], inline)
283
+ if limited:
284
+ graph['gaps'] = list(dict.fromkeys([*snapshot.get('gaps', []), 'SCAN_LIMIT']))
285
+
286
+ if snapshot.get('schema') == 'calculation-panorama-v2':
287
+ references, parameter_ids, control_ids = set(), set(), set()
288
+ for node in graph['nodes']:
289
+ filters = [condition for key in ('fieldFilters', 'linkageFilters') for condition in node.get(key) or []]
290
+ references.update(condition.get('_traceConditionId') for condition in filters)
291
+ filter_parameters = set()
292
+ while filters:
293
+ condition = filters.pop()
294
+ filter_parameters.update(condition.get('parameterIds', []))
295
+ filters.extend(condition.get('children', []))
296
+ node['parameters'] = [item for item in node.get('parameters') or []
297
+ if item['id'] in filter_parameters
298
+ or (node['tableId'] in expanded and 'TABLE' in (item.get('sources') or []))]
299
+ parameter_ids.update(filter_parameters)
300
+ parameter_ids.update(item['id'] for item in node['parameters'])
301
+ for inputs in (node['parameters'], node.get('dynamicFields') or []):
302
+ control_ids.update(source for item in inputs for source in item.get('controlIds', []))
303
+ controls = {ref: ids for ref, ids in snapshot.get('conditionControls', {}).items() if ref in references}
304
+ control_ids.update(source for ids in controls.values() for source in ids)
305
+ graph['parameters'] = [item for item in snapshot['parameters'] if item['id'] in parameter_ids]
306
+ graph['conditionControls'] = controls
307
+ graph['controls'] = [item for item in snapshot.get('controls', []) if item['id'] in control_ids]
308
+ graph.pop('conditions', None) # HTML 使用各作用位置已解析的条件,避免携带被省略的旧正文。
309
+ return graph
310
+
311
+
238
312
  def render(snapshot, output):
239
313
  """将已校验的展示数据嵌入离线 HTML;业务文本转义后写入,防止结束 script 标签。"""
314
+ snapshot = graph_data(snapshot)
240
315
  template = Path(__file__).with_name('assets').joinpath('calculation-panorama.html').read_text(encoding='utf-8')
241
316
  payload = json.dumps(snapshot, ensure_ascii=False).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
242
317
  document = template.replace('__PANORAMA_DATA__', payload)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "version": "0.1.20261111112203",
4
- "source_commit": "235b53a9ec7ec63f9df0a607fa7a9b07d6149c2b",
3
+ "version": "0.1.20261112024042",
4
+ "source_commit": "98b379cb3e75bc9b1f389a2ef9d7fe31fcab605a",
5
5
  "source_dirty": false,
6
6
  "cli_sha256": "4a153c76eb03a90bcec67a5431d471cbdda09a1e7188bb6cefee818d43e89e26",
7
7
  "files": {
@@ -45,7 +45,7 @@
45
45
  "knowledge/dashboard/conclusions.md": "08541d4e3b678c5da0faa348d9ba8bedb23a6ad2ed67a13f4bd3f329df426420",
46
46
  "knowledge/dashboard/queries.md": "ef676ae4df22a35ca76c16bb8f155c0e8bd30d7d356d6429ee48c05eb7a06a40",
47
47
  "knowledge/dashboard/signals.md": "b968b762a1f5db9bce52e34be14e6870c6bc14322273c9059602caa6652df127",
48
- "knowledge/dashboard/topics/calculation-panorama.md": "d41e8d9c17a5b7cf81f81198c91adc6967d8a8ee2219eff68488d2a4165099e6",
48
+ "knowledge/dashboard/topics/calculation-panorama.md": "19c5e8e51b1aeacd5adb78a7100aee1905d4832cbaea2ed59854ff7777d35c7c",
49
49
  "knowledge/dashboard/topics/component-comparison.md": "c67e699adf92891f973c29bd691a8fec4b3ce1f543a286f08f31be1484b6c4de",
50
50
  "knowledge/dashboard/topics/dynamic-fields.md": "2483a89bbeaa49cc317cd7cd69a7157f0254a96daf553183c109dd48cec4bbda",
51
51
  "knowledge/dashboard/topics/filter-tree.md": "c373b24fd57204cad492224cd85ef7e017fe6dca3b29054e4fdcacf3d99e9550",
@@ -83,11 +83,11 @@
83
83
  "knowledge/update/topics/lifecycle.md": "6fbe2b705226b8de08dd5b960dc2c98b626f12e9837b62604c51d3867bca63b4",
84
84
  "knowledge/update/topics/scene-producers.md": "fbe64e070dc2e0f0aaa28d4ad4985626bc758fcaf0c97d6bd2cf72788556f857",
85
85
  "knowledge/update/workflow.md": "977d871c7088dfb3b033dba3c361e45d3ca05e673a428ba41e6f7771e9353b25",
86
- "scripts/assets/calculation-panorama.html": "9bc09f9c30c4e9b6903df2526cc66278db12080c90c91fb1cdb64758e810fc74",
86
+ "scripts/assets/calculation-panorama.html": "76b7e2241d4bdc1e03128e75b741262c8012f33425fa284b62e0b9cb310d2943",
87
87
  "scripts/enterprise_update_report.template.html": "c589820c0beb117d9663f70b58d721f011d6b55cfe5d853d652961a32ab281ba",
88
88
  "scripts/install_signoz_mcp.ps1": "a50ff759df00aa45332aede4ebee55a1778d3ba4d48ed384c8ae08c68e5cd94e",
89
89
  "scripts/install_signoz_mcp.py": "ae54d74787ee80b55a1aaa84cf621857698ecf990dbf6f2b1a41379a9215abf7",
90
- "scripts/render_calculation_panorama.py": "564bfb3787f2d8796c56044babf95112e9e5815cb20f8b834507c92eaacc848e",
90
+ "scripts/render_calculation_panorama.py": "728c0663204db6425be0602d3345e679ec92c8554a6f8bab656b71c7946d62dc",
91
91
  "scripts/render_enterprise_update_report.py": "8ab58e0803c0a5f46fa4565468a38a6f564b76f39035ed9987f25f11053983c1"
92
92
  }
93
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fx-data/ai-diagnostics",
3
- "version": "0.1.20261111112203",
3
+ "version": "0.1.20261112024042",
4
4
  "description": "FX数据诊断:Agent 诊断技能、知识与本地脚本",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",