Skip to content

feat: %TYPE/%ROWTYPE 锚定建成 [T] 引用边(detail/impact 可见,lineage 不污染) (#158) - #164

Merged
c2j merged 48 commits into
mainfrom
feat/issue-158
Sep 8, 2026
Merged

c2j merged 48 commits into
mainfrom
feat/issue-158

Conversation

@c2j

@c2j c2j commented Sep 8, 2026

Copy link
Copy Markdown
Owner

概要

把 PL/SQL %TYPE / 表级 %ROWTYPE 编译期 schema 锚定建成 AnchorsOn 引用边EdgeCategory::Reference,CLI 标签 [T]):detail / trace / impact 可见,lineage / conflicts / --summarize-tables / community 自动排除。

Closes #158

实现要点

  • Edge::AnchorsOn { kind, column, site, location }:追加在 Edge 枚举末尾(bincode 序号稳定);kind: PercentType | PercentRowTypesite: ReturnType | Param | Variable | NestedType。8 处穷尽 match 补臂(category / edge_type_tag / edge_weight / JSON / NDJSON / DOT / Mermaid / location_line)。
  • AnchorExtractor visitor:结构化 AST(变量/嵌套 TYPE/record 全是 PlDataType)+ 扁平签名串兜底解析(容忍 par_sys_purchase. purchase_days% type 式 token 空格与大小写混乱);守卫:cursor 名与局部声明名(变量/TYPE/RECORD)命中即跳过,防伪表锚。
  • 建边create_object_ref_edges / collect_package_object_ref_edgestable_index 既有解析路径(无 DDL → inferred table*);包级变量锚到 Package 节点,包级 cursor 名注入成员例程;routine 内 (object, column, kind, site) 去重。
  • edge_label_for 聚合:同对节点平行边标签合并为 [R,T](修复原先只取首边的显示缺口;petgraph LIFO 迭代经 .rev() 还原创建顺序)。
  • STORE_VERSION 8→9:新变体改变 bincode 布局;旧 store 拒载并提示重建,analyze fast-path 自愈(peek_version 门禁复用 fix(analyze): store 版本校验缺失导致升级二进制后 stale 缓存死循环 #157 机制)。
  • dedupanchors_on 专分支按 (kind, column小写, site) 保留不同锚定组合(同表多列 %TYPE 不被折叠)。

验收对照(issue §验收)

  • detail CALLEES 同表 [R]+[T] 共存 → issue_158_dml_and_anchor_edges_coexist_on_read_table
  • ✅ 无 DML 表以 [T] 可见(inferred table*)→ issue_158_type_only_reference_produces_inferred_table_with_anchor_only
  • ✅ lineage 无多余 hop、conflicts 不计锁 → issue_158_lineage_ignores_anchor_edges / issue_158_conflicts_ignore_anchor_edges
  • impact 从纯锚定表可达函数 → issue_158_impact_reaches_via_anchor_edge
  • cursor%ROWTYPE 不误建表边 → issue_158_no_cursor_rowtype_anchor_edges_present
  • ✅ 旧 store 拒载 → should_reject_store_with_stale_version

另含 33+ 单元测试(解析/抽取/建边/序列化),关键路径均做变异验证(注入实现错误 → 测试失败)。

披露

  • 新 helper collect_routine_anchor_edges 沿用本文件既有惯例加了 #[allow(clippy::too_many_arguments)](builder.rs/main.rs 已有 8 处同类)。
  • 锚定边 line 暂取 routine 起始行(AST 无 span)。
  • Follow-up(本期 Non-goals):游标 RETURN t%ROWTYPE 锚定、CGEF 回导白名单扩展 anchors_on、ogsql-parser 将签名类型结构化为 PlDataType

门禁(rebase 到 origin/main 后实测)

cargo build --features full                        → 0 errors
cargo clippy --features full -- -D warnings        → clean
cargo fmt --all -- --check                         → clean
cargo test --features full -- --skip test_path_mapping_applied --skip test_serve_
                                                   → 716 lib + 全部集成套件,0 failed

🤖 Generated with Claude Code

c2j and others added 24 commits September 8, 2026 11:26
- create_object_ref_edges 的 CreateProcedure/CreateFunction 分支:每 statement
  新建 AnchorExtractor 实例 walk 例程 block,消费 anchors 建 AnchorsOn 边;
  routine 内以 (object 小写, column, kind, site) 去重,覆盖签名锚定(Param/
  ReturnType)与变量/嵌套锚定(Variable/NestedType)可能撞同列的场景。
- collect_package_object_ref_edges:新增 table_index/package_index 参数。
  - PackageItem::Variable:锚到包节点(site=Variable)——包级变量属于包,
    不属于某个例程;用包级 cursor 名集合守卫 %ROWTYPE。
  - PackageItem::Cursor:收集包级 cursor 名,注入每个成员例程的
    AnchorExtractor(新增 pub fn register_cursor_name),保证包级 cursor
    也能守卫例程体内的 %ROWTYPE。
  - PackageItem::Procedure/Function:补齐签名 Param/ReturnType 扁平串锚定
    (原代码只做 body 内 ReferencesType/UsesSequence)+ body walk 新
    AnchorExtractor 实例;同 routine 级去重。
- extractor.rs:抽出 anchor_from_pl_data_type 自由函数(object/column/kind
  判定,不含守卫),AnchorExtractor::visit_pl_data_type 复用;新增
  register_cursor_name 注入接口;补两句 doc caveat(declare-before-use 假设
  + 遮蔽同名表时保守跳过是有意行为)。
- AnchorKind/AnchorSite 补 derive(Hash)(去重 HashSet key 需要)。
- 新增 4 个 builder 单测 + 集成测试 tests/regress_issue_158_type_anchor_edges.rs
  (issue #158 简化等价样例:RETURN/RESULT/变量三处锚定同列去重为 1 条,
  第二张表仅通过变量锚定、无 DML,验证 inferred table* + AnchorsOn 独立于
  TableAccess 共存)。

测试:
- cargo test --features full -- --skip test_path_mapping_applied --skip test_serve_
  696 passed, 0 failed, 3 ignored (pre-existing)
- cargo build --features full: clean
- cargo clippy --features full -- -D warnings: clean
- cargo fmt --all -- --check: clean
…ified_key (#158)

- 新增 GraphBuilder::collect_routine_anchor_edges:统一「params 扁平串解析→
  return_type 解析→新 AnchorExtractor walk block」序列,消除
  CreateProcedure/CreateFunction/包成员例程三处逐字重复(~35-40 行 x3)。
  顶层调用传 pkg_cursor_names=&[],包成员传包级 cursor 名。
- 新增模块级 type alias AnchorDedupKey,消除 4 处重复的 6 行元组类型拼写
  (anchor_dedup_key 返回类型 + 原 3 处局部 HashSet 声明,后者随重复代码
  一起被消除)。
- 包级变量锚定分支改为直接调用既有 pkg_qualified_key(pkg_name) helper,
  去掉内联重复实现的 schema.pkg_name 限定 key 拼接逻辑。
- fixture tests/regress/issue_158_type_anchor_edges/cases/anchor_edges.sql
  顶部补 -- Issue #158 说明块,对齐 issue_140/issue_120 sibling fixture 惯例。
- 纯行为保持重构:全部既有 db88aba 测试(4 builder + 3 集成)作为安全网,
  重构前后逐一确认无回归。

测试:
- cargo test --test regress_issue_158_type_anchor_edges: 3 passed
- cargo test --features full cursor_rowtype /
  should_keep_table_access_and_anchor / should_anchor_package_level: 全绿
- cargo test --features full -- --skip test_path_mapping_applied
  --skip test_serve_: 696 passed, 0 failed, 3 ignored(与重构前完全一致)
- cargo build --features full: clean
- cargo fmt --all -- --check: clean
- cargo clippy --features full -- -D warnings: clean

builder.rs: 8054 → 7986 行(净减 68 行,含新增 ~65 行 helper+doc)
局部 TYPE ... IS TABLE OF/RECORD(...) 的类型名,以及独立 plain RECORD
变量声明名,此前未登记进 var_names 守卫集合,导致锚定到这些本地标识符
的 %TYPE(如 v_list typ_list%TYPE、v2 rec2%TYPE)被误判为表锚,产生
伪造的 inferred table* 节点/AnchorsOn 边。

真实 AST 形态(ogsql-parser src/ast/plpgsql.rs):
- PlDeclaration::Type(PlTypeDecl) 四个变体(Record/TableOf/VarrayOf/
  RefCursor)均有独立 name 字段,已有 pl_type_decl_name() helper 提取。
- plain RECORD 变量是独立的 PlDeclaration::Record(PlRecordDecl { name }),
  非 PlDeclaration::Variable。

修复:AnchorExtractor::visit_pl_declaration 补两处登记 —— Record(r) 的
r.name、Type(t) 的 pl_type_decl_name(t) 均 insert 进 var_names,早于
push_anchor 的守卫检查生效。

新测试:
- should_skip_type_anchored_to_local_type_declaration
- should_skip_type_anchored_to_plain_record_variable
均先红(缺失守卫产生伪表锚)后绿。

回归:should_collect_*(5 个)/ should_skip_*(3 个既有)/
should_keep_table_rowtype_when_cursor_exists_elsewhere 全绿,无回归。
AnchorsOn 边去重键 (kind, column, site) 的 column 分量此前按原始大小写
比较:GraphBuilder::anchor_dedup_key(同 routine 内)与 GraphStore::dedup
的 anchors_on 专分支(跨阶段合并后)均未归一化,导致 emp.id%TYPE 与
emp.ID%TYPE(openGauss 对未加引号标识符的大小写折叠语义下是同一列)
被当作两条不同的边保留,产生重复。

修复:两处 column 分量统一 .map(|c| c.to_lowercase())。

新测试:should_dedupe_anchor_edges_case_insensitively_by_column
(graph::store::tests)——同 (proc, table) 两条 AnchorsOn,column
Some("id") / Some("ID"),先红(剩 2 条)后绿(剩 1 条)。

回归:should_keep_distinct_anchor_edges_through_dedup(不同列仍保留)+
should_roundtrip_anchors_on_edge_through_bincode_store 全绿,无回归。

同时在 docs/plans/2026-09-07-issue-158-type-anchor-edges.md 执行备注
末尾补记本次外部 review 修复。
* feat(lineage): 标量子查询作为 INSERT 值源时解析其首表达式 (fix #142 部分)

* feat(lineage): 表锚定 %ROWTYPE 记录字段解析为表列 (fix #142 部分)

* feat(lineage): SELECT * 游标 + %ROWTYPE 记录字段归因到游标表 (fix #142 部分)

* feat(lineage): 整记录 INSERT VALUES r 按游标源列位置展开 (fix #142 部分)

* test(lineage): 锁定游标 %ROWTYPE 记录字段写入的穿透行为 (fix #142)

* docs: add issue #142 column-lineage penetration plan

* fix(lineage): 标量子查询首表达式复用 classify_value_expr 分类 (review #153-1)

* fix(lineage): 拦截通用 walker 递归子查询,杜绝 join 状态泄漏 (review #153-2)

* style: cargo fmt (review #153-2 follow-up)

* fix(lineage): FETCH 将 %ROWTYPE 记录锚定重绑到实际游标 (review #153-3)

* fix(lineage): 整记录 SELECT* catch-all 不再猜测列名,避免重排错归因 (review #153-5)

* test(lineage): 锁定记录字段子查询穿透并修正局限文档 (review #153-4 #153-6)

* fix(lineage): 多列 SET = 子查询按位置对齐各目标列 (review 5136742683)

* fix(lineage): 容器子查询先收集左操作数列再跳过嵌套 SELECT (review 5136742683)
…(对齐 table*) (#160)

* feat(graph): 无 CREATE SEQUENCE 时为 seq.nextval 建 inferred seq* 节点与 UsesSequence 边

Node::Sequence 增加 explicit 标记与 Option<location>(对齐 Table/View 模式),推断节点由 builder 在 DDL 缺失时 or_insert 生成;schema 限定引用仅精确匹配 full key,不回退短名别名,避免跨 schema 误绑定。STORE_VERSION 8 -> 9。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test: issue #159 序列推测节点集成回归(store 落盘 / 增量 / 无重复边)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: add issue #159 inferred-sequence implementation plan

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(graph): 推测序列索引提升到构建上下文,CREATE SEQUENCE 原位升级

inferred_sequence_index 原为 create_object_ref_edges 局部缓存,跨 chunk(>100 SQL 文件)即失效:同一序列产生重复 seq* 节点,且后置 CREATE SEQUENCE 只查 sequence_index 造出兄弟 explicit 节点不升级。现提升为 GraphBuildContext 字段跨 chunk 共享;DDL 命中推测节点时原位改写 explicit/location(保留 NodeIndex 与既有边),升级遵守精确 key 纪律——限定 DDL 只升级限定推测节点,杜绝短名模糊匹配跨 schema 误绑定。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(store): pick_richer_node 补 Sequence 臂,优先保留带 DDL 位置的节点

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: add PR #160 inferred-sequence cross-chunk fix plan

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
--files 仍只列出调用链源文件。加上 --related-ddl 后,把链上表/视图的
索引、同义词、触发器文件合并进同一段 FILES,不改变调用树。

Closes #161
- Comprehensive functional comparison across 5 dimensions
  (SQL parsing, graph model, query, export, deployment)
- Performance benchmarks on shared ANSI SQL corpus (3 tiers)
  and codeweb-only PL/pgSQL corpus (2 tiers)
- Results: codeweb 19x faster on large ANSI SQL parsing,
  44x faster on JSON export, 5x less memory
- Deep dive into flowScope column-level lineage implementation
  (architecture, accuracy analysis, edge types, limitations)
- Scenario-based tool selection recommendations
- Improvement suggestions for codeweb
Comment thread src/parser/extractor.rs
use ogsql_parser::ast::plpgsql::PlDataType;
match dt {
PlDataType::PercentType { table, column } => {
Some((table.clone(), Some(column.clone()), AnchorKind::PercentType))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] anchor_from_pl_data_type treats every PlDataType::PercentType as a table.column anchor, including the single-identifier form that ogsql-parser encodes as column: "". In v0.10.0 (parse_pl_data_type), v1%TYPE becomes PercentType { table: "v1", column: String::new() }. The flat parser already rejects that shape (PercentType requires ≥2 identifiers), but the AST path does not. Combined with a skip set that only knows previously declared local variables/cursors, this creates inferred table* nodes for common non-table anchors: v2 v1%TYPE at package level (builder only checks pkg_cursor_names), and v p_id%TYPE where p_id is a parameter (never inserted into var_names). Those fake tables then show up in detail/trace/impact as [T] edges.

Suggestion: Return None for PercentType when column is empty/whitespace, matching parse_anchor_from_type_string. Also seed the skip set with routine parameter names and package-level variable/type names (see Issue 2) so rec.field%TYPE against a record param/package var is still suppressed. Add tests for v p_id%TYPE and package-level v2 v1%TYPE.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 1a53283anchor_from_pl_data_typecolumn.trim().is_empty()PercentType 返回 None(对齐扁平解析器的 ≥2 段语义)。测试:should_reject_empty_column_percent_type_from_ast(空串/空白/正常表列/PercentRowType 四形态)。

Comment thread src/graph/builder.rs

let mut anchor_extractor = AnchorExtractor::new();
for cname in pkg_cursor_names {
anchor_extractor.register_cursor_name(cname);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] Member-routine (and top-level) AnchorExtractor walks are only seeded with package cursor names. Parameter names, package-level variables, and package-level type names are not registered, even though collect_package_call_edges already collects pkg_var_names and PackageItem::Type for call extraction. So v p_emp.empno%TYPE (parameter is p_emp employees%ROWTYPE) and v pkg_rec.col%TYPE (package-level record) still have a non-empty column and pass Issue 1’s empty-column filter, then become inferred tables. Package-level variables have the same hole: collect_package_object_ref_edges only skips pkg_cursor_names (line 2104), not earlier package vars/types.

Suggestion: Add register_var_name (or a generic register_local_name) next to register_cursor_name. Before walk_pl_block, register every parameters[].name, every PackageItem::Variable name, and every pl_type_decl_name from PackageItem::Type. At package-variable site, skip if the object matches any package var/type/cursor name collected from the same pkg_items list.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 d1e249e:新增 AnchorExtractor::register_var_namecollect_routine_anchor_edges 内注册全部 parameters[].name,签名扩展 pkg_var_type_names: &[String](包级 Variable 名 + pl_type_decl_name,收集于锚定循环前);包级变量锚定点守卫扩为 pkg_cursor_names ∪ pkg_var_type_names;成员例程注入双集合。测试:should_skip_type_anchored_to_param_name(v p_emp.empno%TYPE)、should_skip_package_var_anchored_to_earlier_package_varshould_skip_type_anchored_to_package_level_record + e2e issue_158_param_name_anchor_suppressed_end_to_end

Comment thread src/graph/builder.rs
),
PackageItem::Raw(_)
| PackageItem::Variable(_)
| PackageItem::Type(_)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] PackageItem::Type(_) is continued, so package-level TYPE t IS TABLE OF x.col%TYPE / VARRAY OF / RECORD (f t.col%TYPE) never produce AnchorsOn edges. Issue #158 lists nested TYPE/record-field anchors in scope, and the routine-body visitor already handles the same PlTypeDecl variants. In real PL/SQL those nested types almost always live in the package spec/body, not in a routine DECLARE. Package-level variables in the same loop are already turned into package-node anchors, so this is an inconsistent hole in the same declare section, not a documented non-goal (D3 is cursor RETURN only).

Suggestion: Walk PackageItem::Type with the same PlTypeDecl match used in AnchorExtractor (TableOf/VarrayOf/Record fields), apply the package-level cursor/var/type skip set, and attach surviving edges to the package node with site=NestedType. Add a builder test for TYPE t_list IS TABLE OF some_table.col%TYPE inside PACKAGE BODY.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 e165644PackageItem::Type 不再 continue——提取共享入口 pub fn anchor_targets_in_pl_type_decl(复用 anchor_from_pl_data_type,TableOf{elem_type,index_by}/VarrayOf/Record fields 全覆盖),AnchorExtractor 的 Type 臂同步改用该入口(无重复 match);包节点锚定 site=NestedType,守卫含本 TYPE 名(TYPE t_bad IS TABLE OF t_list.col%TYPE 被抑制)。测试:should_anchor_package_level_nested_type + e2e issue_158_package_nested_type_anchor_end_to_end

Comment thread src/graph/builder.rs Outdated
/// passes its package's cursor names so `rec pkg_cursor%ROWTYPE` inside
/// the body is guarded the same way a routine-local cursor would be.
/// Shared across the three call sites (top-level procedure, top-level
/// function, package member) that previously duplicated this sequence.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The doc comment on collect_routine_anchor_edges narrates refactor history (“that previously duplicated this sequence”) rather than a current invariant. Same class of leakage as the long “Caveats” / “Task N” commentary elsewhere in the diff.

Suggestion: Keep one sentence on why pkg_cursor_names is injected; drop the “previously duplicated” / task-history wording.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 c47fa7e:doc 改为陈述当前不变量(为何注入包级名集),移除 "previously duplicated" 历史叙事;另在 5eaf87f 补充遮蔽 caveat 明确覆盖 register_var_name 注入的参数名(参数与真表同名的保守跳过是文档化行为)。

c2j and others added 5 commits September 8, 2026 13:08
…y tags (#154) (#163)

* fix(lineage): hint column-level needs an existing table in missing-table note (#154)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(graph): pin jsp/jspsql node-key tags under the jsp feature (#154)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Comment thread src/graph/builder.rs
) {
let mut anchor_seen: HashSet<AnchorDedupKey> = HashSet::new();

for param in parameters {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] Previous issue 2 (fake table anchors from non-table names) is only fixed for the body walk. collect_routine_anchor_edges still turns every signature %TYPE/%ROWTYPE string into an AnchorsOn edge with no cursor/var/TYPE/parameter skip. pkg_cursor_names / pkg_var_type_names are injected only after this loop (and only into AnchorExtractor), so they never apply here. Concrete false positives: PROCEDURE p(p_rec c%ROWTYPE) when c is a package cursor; FUNCTION f(p_id emp_rec.empno%TYPE) when emp_rec is a package RECORD type; PROCEDURE p(p_id v_emp.empno%TYPE) when v_emp is a package-level %ROWTYPE variable. Each creates an inferred table* named c / emp_rec / v_emp. The new tests (should_skip_type_anchored_to_param_name, issue_158_param_name_anchor_suppressed_end_to_end) only cover a body variable v p_emp.empno%TYPE, not a parameter/RETURN whose type string itself points at a local name.

Suggestion: Before add_anchor_edge, drop a parsed signature anchor whose lowercased object is in pkg_cursor_names, pkg_var_type_names, or the current routine's parameter names (excluding the parameter currently being declared, so employees employees%ROWTYPE as a param still anchors the table). Add a builder test whose parameter type is a package cursor/TYPE/variable name.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 e70728acollect_routine_anchor_edges 参数/RETURN 锚定在建边前检查 pkg_cursor_names ∪ pkg_var_type_names ∪ 参数名集(小写全串对比);排除当前参数自身名——PROCEDURE p(employees employees%ROWTYPE) 惯用法仍锚表(should_still_anchor_param_named_after_table 锁定);RETURN 无 self 排除(保守)。测试:should_skip_signature_anchor_to_package_cursor / should_skip_signature_anchor_to_package_type_and_variable

Comment thread src/graph/builder.rs
// variables below, and are injected into every member routine's
// AnchorExtractor so `rec pkg_cursor%ROWTYPE` inside a routine body
// is guarded the same way a routine-local cursor would be (#158).
let pkg_cursor_names: Vec<String> = pkg_items

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] The new skip sets (pkg_cursor_names, pkg_var_type_names) are built only from the current statement's pkg_items. create_sql_edges already knows spec and body are independent statements and therefore indexes SPEC items (spec_items_by_pkg) so a BODY can inherit public Variable/Type/Cursor names (see the comment at builder.rs:1624–1636 and the BODY call at 1652–1662). create_object_ref_edges / collect_package_object_ref_edges do not. A SPEC-only CURSOR c, TYPE rec_t IS RECORD (...), or package variable used from a BODY member (rec c%ROWTYPE, v rec_t.col%TYPE) is therefore unguarded and becomes an inferred table*. The new tests keep TYPE/cursor/var and the member routine in the same BODY, so they never exercise this split.

Suggestion: Mirror the call-edge path: index CreatePackage items by pkg_qualified_key, pass them as inherited_items when walking CreatePackageBody, and union them into pkg_cursor_names / pkg_var_type_names (and into any signature skip from Issue 1). Cover with a two-statement SPEC+BODY fixture.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 0697f37 + 73b50c6collect_package_object_ref_edges 新增 inherited_items 参数,create_object_ref_edges 预扫描构建 spec_items_by_pkg(镜像调用边 :1629 模式,后提取为 build_spec_items_index 共享 helper),BODY 调用传 SPEC items;pkg_cursor_names/pkg_var_type_names 收集 .chain(inherited)。测试:should_inherit_spec_names_for_body_anchor_guards(SPEC+BODY 双语句)+ e2e issue_158_spec_body_inherited_guards_end_to_end(伪表 c/rec_t/v_emp 缺席 + 对照组锚定)。

Comment thread src/parser/extractor.rs Outdated
self.cursor_names.insert(c.name.to_lowercase());
}
PlDeclaration::Variable(v) => {
self.var_names.insert(v.name.to_lowercase());

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] The skip set includes the identifier currently being declared, so the standard PL/SQL idiom emp emp%ROWTYPE / emp emp.empno%TYPE never produces an AnchorsOn edge. visit_pl_declaration inserts v.name (and the TYPE name at 1241) before reading the %TYPE/%ROWTYPE target; package-level code has the same hole because pkg_var_type_names is the full item list including self (builder.rs:2115–2131). Cursor %ROWTYPE does not need this: CURSOR c; rec c%ROWTYPE is already filtered by cursor_names. Empty-column v2 v1%TYPE is already rejected by anchor_from_pl_data_type. The extractor caveat that “shadowing means the name resolves to the local declaration” is wrong at the declaration site — Oracle’s own examples use employees employees%ROWTYPE, and %ROWTYPE looks up a table/view/cursor, not the variable whose type is being written. Same-name later siblings (v employees%ROWTYPE then a package var named employees) are also over-skipped by the full-set package guard; declare-before-use (previous names only, insert-after-visit) is enough for v_id v_emp.empno%TYPE.

Suggestion: Register the current variable/TYPE name after visiting its PlDataType / nested fields. For package-level items, skip only names declared earlier in pkg_items (plus inherited SPEC names from Issue 2), never the item’s own name. Add a builder test for emp emp%ROWTYPE asserting an AnchorsOn to table emp, and keep the existing v_empv_id v_emp.empno%TYPE guard test.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 6b07c1d:extractor 改 insert-after-visit(Variable/Type 名在自身类型访问后注册)——emp emp%ROWTYPE 惯用法正向测试 should_anchor_variable_self_named_after_table;包级循环改增量 declared_earlier 集(种子 = SPEC 继承名,永不含 self)+ should_anchor_package_var_self_named_after_table;成员例程注入维持全集(包体整体可见);cursor 名维持全集(如 review 所述不需要)。doc caveat 同步改写(声明点自名解析为表)。回归:v2 v1%TYPE/t_bad→t_list/v_id v_emp.empno%TYPE 守卫全部保持。

Comment thread src/graph/builder.rs
continue;
};

Self::collect_routine_anchor_edges(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] collect_routine_anchor_edges is invoked for every package member that already has a proc_idx, including SPEC signatures whose block is None. Node creation (create_package_nodes) skips body-less SPEC items, but create_sql_nodes finishes the whole file (SPEC then BODY) before create_object_ref_edges runs, so by the time SPEC is walked for anchors the BODY has already inserted the shared RoutineId node. SPEC then emits Param/ReturnType AnchorsOn edges; BODY emits the same edges again. anchor_seen is local to a single call, and neither GraphBuilder::build nor project::analyze runs store.dedup(), so JSON/detail consumers see two identical anchors_on edges on the standard Oracle pattern (CREATE PACKAGE … PROCEDURE p(t t%ROWTYPE); + matching BODY). Round-2 tests never declare the procedure in both SPEC and BODY, so they miss it.

Suggestion: Dedup signature anchors across SPEC and BODY for the same proc_idx (a pass-level (proc_idx, AnchorDedupKey) set, or collect signatures only from SPEC when a matching SPEC exists and let BODY contribute DECLARE-site anchors only). Add a SPEC+BODY fixture that asserts exactly one AnchorsOn from p to the table.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 002366banchor_seen 上移为调用方持有 HashSet<(NodeIndex, AnchorDedupKey)>(proc 维度入键),collect_package_object_ref_edges 一个集贯穿 SPEC+BODY——签名相同折叠、不同保留(不偏向 SPEC)。测试:should_dedupe_signature_anchors_across_spec_and_body(SPEC+BODY 双声明恰好 1 条 Param 边 + BODY 独有 Variable 边保留)。

Comment thread src/parser/extractor.rs
}
self.var_names.insert(pl_type_decl_name(t).to_lowercase());
}
_ => {}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] AnchorExtractor::visit_pl_declaration falls through NestedProcedure / NestedFunction to _ => {} and returns Continue. The default walker then recurses into the nested block on the same extractor: nested-routine parameters are never register_var_name’d (they are not PlDeclarations), so PROCEDURE inner(p_emp VARCHAR2) IS v p_emp.empno%TYPE; still mints inferred table* p_emp — the same hole round 1 closed for top-level params. Nested locals/cursors also leak into the outer skip sets. CallExtractor in this same file already save/restores scope and injects nested parameters via begin_routine_scope.

Suggestion: Mirror CallExtractor: on NestedProcedure/NestedFunction, save cursor_names/var_names, register_var_name each nested parameter, walk the nested block, then restore. Cover with should_skip_type_anchored_to_nested_proc_param.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 6370485:镜像 CallExtractor——NestedProcedure/NestedFunctionmem::take 保存 cursor/var 集 → 注册嵌套参数 → 手动 walk 嵌套块 → restore → SkipChildren 防默认递归双走。测试:should_skip_type_anchored_to_nested_proc_param(伪造 p_emp 被守卫)+ should_not_leak_nested_routine_locals_into_outer_scope(行为化:嵌套后用嵌套专属名锚定,正向断言泄漏未发生;变异自证 restore 移除即 Red)。

Comment thread src/graph/store.rs
}
if tag == "anchors_on" {
// `AnchorsOn` edges on the same (proc, table) pair are not
// interchangeable duplicates: distinct params/vars can each

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] GraphStore::dedup() now keeps distinct AnchorsOn edges that share (src, dst) but differ in (kind, column, site). GraphStore::merge still keys seen_edges on (src_key, dst_key, edge_type_tag) only ("anchors_on"), so p1 emp.id%TYPE + p2 emp.name%TYPE in one input store collapse to a single edge when merged. That reintroduces the exact invariant should_keep_distinct_anchor_edges_through_dedup locked for dedup().

Suggestion: Apply the same (kind, lowercased column, site) key inside merge’s per-store edge loop (and when combining with edges already in the accumulator). A merge-path unit test parallel to the dedup one would lock it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 d667927 + e74f41fanchors_on 边在 merge 用扩展键(kind/小写 column/site,贯穿全部 store 的 seen_anchor_keys,天然覆盖累加器已存在边),非锚定边维持 per-store 原语义。中途曾把全局 seen 泛化到全部边类型、破坏 TableAccess 跨 store 模式并集——review 捕获后回退并加回归锁:should_union_table_access_modes_across_stores_on_merge(Read+Write → Read|Write)+ should_keep_distinct_anchor_edges_through_merge / should_dedupe_identical_anchor_edge_across_stores

Comment thread src/graph/builder.rs Outdated
// variable/TYPE, or another parameter's name. The *current*
// parameter's own name is excluded from the "other param" check so
// the Oracle self-naming idiom (`p(employees employees%ROWTYPE)`)
// still anchors to the real table (PR #164 review round 2 issue 1).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Several new comments narrate review history rather than the current invariant: “PR #164 review round 2 issue 1” (here), “PR #164 review round 2” on the SPEC-inheritance skip sets, “PR #164 review round 2 issue 3” on declared_earlier. c47fa7e already stripped that style once; the follow-up commits put it back.

Suggestion: Keep the self-naming / SPEC-inheritance / earlier-only rules; drop the PR-round citations.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复 f4de1c9:全仓 grep 清理 "PR #164 review round N" 叙事(builder/extractor/store 23 处),保留规则语义;bc07b6d 顺带修正两处陈旧前瞻描述与 doc rewrap。

Comment thread src/parser/extractor.rs Outdated
// `NestedFunction` arms. Nested RETURN-type anchoring is not
// done — a nested routine has no independent graph node.
PlDeclaration::NestedProcedure(p) => {
let saved_cursors = std::mem::take(&mut self.cursor_names);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] AnchorExtractor’s new NestedProcedure / NestedFunction arms save scope with std::mem::take, which clears cursor_names / var_names for the nested walk and only restores them afterward. That correctly stops nested locals from leaking outward (the previous finding, covered by should_not_leak_nested_routine_locals_into_outer_scope) and nested parameters are now registered (covered by should_skip_type_anchored_to_nested_proc_param). It also wipes names the nested routine is supposed to see. PL/SQL nested subprograms inherit the enclosing DECLARE / parameter list, and collect_routine_anchor_edges injects package-level cursor/var/TYPE names plus the outer routine’s parameters onto this same extractor before walk_pl_block. After take(), a nested body like rec c%ROWTYPE (outer/package cursor c) or v p_emp.empno%TYPE (outer param p_emp) is unguarded and still mints inferred table* c / table* p_emp attached to the outer node. CallExtractor uses the same take() because local_vars is a call-vs-identifier set that starts fresh per routine; copying that mechanic here is the wrong scope rule for %TYPE/%ROWTYPE guards. The struct comment only claims leak-out isolation; the implementation is stronger than that invariant.

Suggestion: Clone-and-restore instead of take-and-restore (saved = self.cursor_names.clone(); … self.cursor_names = saved;, same for var_names), then register nested parameters (so they can shadow). Add a test that an outer/package cursor or outer param visible in a nested body does not become a table anchor (CURSOR c IS …; PROCEDURE inner IS rec c%ROWTYPE;). Same change on the NestedFunction arm at line 1279.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d0353f7: both NestedProcedure/NestedFunction arms now clone() (not take()) cursor_names/var_names before registering the nested routine's own parameters and walking — outer/package cursor and parameter names stay guarded inside the nested body (lexical inheritance), while the save/restore-to-snapshot afterward is unchanged, so nested-local declarations still don't leak outward. Added two RED\u2192GREEN regression tests: should_skip_nested_body_anchor_using_outer_cursor (outer CURSOR c + nested rec c%ROWTYPE) and should_skip_nested_function_body_anchor_using_outer_param (nested-within-nested: a nested routine's own parameter guarding a further-nested body's %TYPE reference \u2014 the literal top-level-signature-parameter variant doesn't exercise this code path since extract_anchors()'s test harness never registers a CREATE FUNCTION's own signature parameters, only graph::builder::collect_routine_anchor_edges does downstream). Verified both new tests fail against the old take() code (fabricated table* c / table* p_emp anchors) before the fix, and pass after. Existing guards (should_skip_type_anchored_to_nested_proc_param, should_not_leak_nested_routine_locals_into_outer_scope) and the full regress_issue_158_type_anchor_edges suite (10/10) remain green.

Comment thread src/graph/store.rs Outdated
/// every other edge type (in particular `TableAccess`) must stay on the
/// per-store generic key or `merge_duplicate_table_access_edges`'s
/// cross-store `AccessMode` union never gets a second edge to union
/// (regression fixed in commit following d667927: a global generic key

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] anchor_merge_key’s doc-comment still narrates the review/commit history (“regression fixed in commit following d667927: a global generic key silently dropped…”) rather than stating the current invariant. The useful part is already there: AnchorsOn uses a cross-store (src, dst, kind, column, site) set; every other edge type stays on the per-store (src, dst, tag) key so merge_duplicate_table_access_edges can union modes. The commit-hash sentence is the same class of history-narration the previous pass asked to drop from builder.rs (those sites are clean now). The test at should_union_table_access_modes_across_stores_on_merge also leads with “commit d667927”.

Suggestion: Keep the invariant (separate cross-store AnchorsOn key vs per-store generic key, because TableAccess union needs both stores’ edges present). Drop the commit hash and the “what we broke last time” recap from production comments; the regression test name/body is enough history.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 90bde2a: both doc comments dropped the commit-hash / "what we broke last time" narration while keeping the invariant statement. anchor_merge_key's doc now states: AnchorsOn uses a merge-spanning (src, dst, kind, lowercased column, site) key so identical anchors collapse across stores while distinct columns survive, and every other edge type keeps the per-store (src, dst, tag) key so merge_duplicate_table_access_edges can still union TableAccess modes across stores. should_union_table_access_modes_across_stores_on_merge's doc now reads "Regression guard (#158): a whole-merge generic key would drop the second store's TableAccess edge before mode union; per-store keys + the dedicated AnchorsOn key above keep both behaviors" \u2014 no commit hash. grep -rn "d667927" src/ \u2192 0 hits.

c2j and others added 4 commits September 8, 2026 18:51
* docs(plans): #165-169 列级分析查询面实施计划(Momus 两轮审核通过)

覆盖 #165 columns 查询面 / #166 文档 / #167 PL 谓词 / #168 记录字段跨表键 / #169 transform 白名单;决策 D1-D6 全部锁定(方案A 合并、RecordField 变体、独立 predicates 命令、合并版本 bump、双变体白名单、store 侧表方案)。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(parser): 函数包裹列字面量过滤 transform 白名单 + WHERE/JOIN %ROWTYPE 记录字段跨表等值键 (fix #169, fix #168)

- HardFilter 增加 transform(FilterTransform,serde "fn");白名单 {substr,substring,nvl,trim,upper,lower},FunctionCall+SpecialFunction 双变体;六个比较操作符统一支持
- transform 序列化采用 is_human_readable() 分支手写实现(bincode 固定布局 / JSON 省略 None)
- JoinConditionSource 新增 RecordField 变体;column_source 记录字段解析抽取为 record_field_source/resolve_record_field 复用;等值一侧为已解析记录字段时产出跨表 JoinCondition
- 诊断类型补 Hash derive(方案A 并集合并前置)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(parser): PL IF/CASE 条件解析为表列谓词(branch-aware 提取器) (fix #167)

- PredicateExtractor 走整个 PlBlock 保留分支结构(extract_body_sql 摊平前收集)
- 置信度规则:记录字段/裸列→high;SELECT INTO 主表变量→medium;维表变量→low + param_table_hint;函数/动态 SQL→low 保留 origin,绝不静默 high
- 条件转换复用 #169 column_transform_of 与 #168 记录字段解析;PredicateClause.transform 沿用 HardFilter 的 is_human_readable 序列化模式
- 过程内 SELECT INTO 变量源追踪;SELECT INTO 目标/变量数不匹配时 parse_log 告警;游标 WHERE HardFilter 不混入谓词列表

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(store): procedure_predicates 侧表与版本链 v10→v12 (#167)

- v10: merge_table_access_edges 诊断字段并集(#165 方案A)+ transform 预留(#169)
- v11: procedure_predicates 侧表(HashMap<NodeKey, Vec<PlPredicate>>,serde default,merge 时并集)
- v12: PredicateClause.transform(bincode 布局变更)
- 版本拒绝基线测试不变;版本戳字面量测试同步至 12

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(builder): TableAccess 诊断字段并集合并 + 过程谓词收集(归一化存储键) (fix #165)

- merge_table_access_edges 对 join_conditions/hard_filters/enum_mappings/select_into/insert_columns/update_columns/column_refs 做保序 HashSet 并集,alias_map 首见优先;column_mappings/read_tables 语义不变(方案A,修复同过程多语句同表时诊断字段「保留第一条」丢失)
- 过程构建期走整个 PlBlock 收集谓词,procedure_predicates 经 RoutineId::normalized() 以与 NodeKey::from_node 一致的小写键写入(修复大小写不匹配查找 miss)
- project/mod.rs 接线 ctx.procedure_predicates → GraphStore

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(graph): 按过程/包聚合 ColumnAnalysis 查询后端 (fix #165)

column_analysis_of_routine/column_analysis_of_package 扫入边+出边逐字段去重聚合,--table 过滤;AggregatedColumnAnalysis 字段与 ColumnAnalysis 1:1(schema_version 1),供 CLI/MCP/HTTP 三面共享,不另造 schema

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(cli): codeweb columns / predicates 子命令 (fix #165, fix #167)

- columns --procedure|--package [--table] --format json:旧 store(<v10) 软提示重跑 analyze;unknown 过程非零退出不静默
- predicates --procedure --format json:旧 store(<v12) 软提示;输出 {schema_version, procedure, predicates}
- regress_columns / regress_predicates 端到端回归(含大写过程名大小写无关解析、记录字段跨表键、param_table_hint)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(serve,mcp): columns/lineage MCP 工具与 HTTP 端点,共享 lineage 目标解析 (fix #165)

- MCP 新增 codeweb_column_analysis / codeweb_lineage(与 CLI JSON 字段 1:1,空图守卫与错误措辞对齐既有工具);工具清单测试 6→8
- HTTP 新增 GET /api/v1/columns、GET /api/v1/lineage(400 非法输入 / 404 未命中,同 serde 结构直出)
- lineage 目标解析抽取为 graph::lineage::parse_lineage_target 纯函数(6 个单测),cmd_lineage 行为不变

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: 补齐 lineage/columns/predicates 用户与开发者文档 (fix #166)

- README 中英 CLI/HTTP/MCP 三表补齐(示例与 --help 逐字一致)
- user-guide §6 新增 lineage/columns/predicates 小节;DeveloperGuide 增加 ColumnAnalysis 字段表(含 transform/RecordField)与 mock 造数消费场景
- getting-started(_zh) 增加可照跑的列级血缘示例;serve-api-guide 补 /columns 与 /lineage 端点(真实 JSON 输出)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(predicates): ELSIF/简单 CASE 采集 + 函数包裹操作数断链修复 (PR#170 review F1/F2)

- If 臂遍历 elsifs 逐条产出谓词;简单 CASE(expression: Some)合成 expression = WHEN 值比较,不再把裸字面量降级为 Low
- condition_operand 移除 expr_name(expr)? 提前断链:记录字段 → var_sources(expr_name 或 transform 目标列名)→ sole-table fallback 顺序保持;裸列 substr 与 SELECT INTO 变量 substr 均正确解析
- Derived 臂携带 transform(删除硬编码 None);PL 变量的 transform 提取打通

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(cli,serve,mcp): 谓词身份对齐 columns、歧义显式失败、空谓词语义化 (PR#170 review F3/F4/F5)

- predicates JSON 的 procedure 取 RoutineId 裸名并新增 package 字段,与 columns 可 join(此前包内过程为 pkg.prc)
- columns/predicates/MCP/HTTP 四处新查询面 fail_on_multiple=true:多匹配非零/400/error JSON 显式失败,消灭静默取首个(MCP 区分 Empty 与 Ambiguous 文案;HTTP 歧义 400、未命中 404)
- resolved 无分支过程返回 predicates: [] exit 0,非零保留给未解析/歧义
- cmd_lineage 改用共享 parse_lineage_target(行为不变,regress_lineage 套件守护);文档同步

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor: 精简谓词/提取器注释,去除 issue 叙事(PR#170 review F6)

仅注释:保留非显性约束(bincode 固定字段数等)并压缩篇幅,删除控制流复述与评审/issue 编号叙事

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs(plans): PR#170 评审修复计划(Momus 审核通过)

六项评审发现的核实记录与 F1-F6 修复方案

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(cli): columns --package 歧义子串显式失败,对齐 --procedure (PR#170 review follow-up)

package 臂 resolve_single_node 补上 fail_on_multiple=true(此前仍静默取首个匹配,Ambiguous 臂不可达);新增 columns_ambiguous_package_fails_explicitly 防回归

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Comment thread src/parser/extractor.rs
/// reference, not self-declaration, so it resolves to the local, not the
/// table, at the point of use.
/// - A nested routine (`PlDeclaration::NestedProcedure`/`NestedFunction`)
/// gets its own isolated scope: its parameters are injected into

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] After the take→clone fix, nested-routine scope is lexical inheritance (outer/package names remain visible; nested params shadow on top; restore only prevents leak-out). The struct-level caveat still says a nested routine "gets its own isolated scope". That was true of mem::take and is now false; it contradicts the arm comment and the new tests. A later change that "restores isolation" by switching back to take would reintroduce the round-4 bug.

Suggestion: Rewrite the bullet to the current invariant: nested bodies inherit the enclosing (and injected package) guard sets, nested parameters are registered on top, and the pre-nesting snapshot is restored so nested locals do not leak out.

Comment thread src/parser/extractor.rs
// nested locals outward and never registering the nested
// parameters as guarded names. We prevent this by returning
// SkipChildren and manually walking the nested block with a
// save/restore barrier: *clone* (not take) the guard sets so

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The NestedProcedure/NestedFunction comment still narrates the review fix (*clone* (not take)) and claims the arms "Mirror CallExtractor", but CallExtractor still uses mem::take + begin_routine_scope (a fresh scope). The follow-up that de-historicized anchor_merge_key introduced the same class of history-narration here, and the CallExtractor parallel is now inaccurate.

Suggestion: State the current contract only: clone the guard sets, register nested params, walk, restore, return SkipChildren. Drop the take/clone post-mortem and the CallExtractor claim (or note that CallExtractor intentionally uses a fresh call-resolution scope).

Comment thread src/graph/builder.rs

/// Issue #158: a routine parameter's flat
/// `%ROWTYPE` signature anchor bypasses the guard entirely — the
/// signature loop in `collect_routine_anchor_edges` never consults

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] should_skip_signature_anchor_to_package_cursor asserts that a package cursor does guard a signature %ROWTYPE (and collect_routine_anchor_edges now consults pkg_cursor_set / pkg_var_type_set / other param names). The test comment still speaks in the present tense as if the signature loop "bypasses the guard entirely" and "never consults" those sets — the old hole, not the current invariant.

Suggestion: Rephrase as the locked behavior (signature anchors must consult the same cursor/var/param guards as the body walk), not as a description of a bypass that the test then contradicts.

@c2j
c2j merged commit 0e36795 into main Sep 8, 2026
2 checks passed
@c2j
c2j deleted the feat/issue-158 branch September 8, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: %TYPE/%ROWTYPE 锚定建成 [T] 引用边(detail/impact 可见,lineage 不污染)

1 participant