Skip to content

feat(control-plane): add module living spec graph routing and AST interface boundary checks - #7347

Closed
hhyykk wants to merge 1 commit into
loopx-project:mainfrom
hhyykk:feat/ts-living-spec-graph
Closed

hhyykk wants to merge 1 commit into
loopx-project:mainfrom
hhyykk:feat/ts-living-spec-graph

Conversation

@hhyykk

@hhyykk hhyykk commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add a deterministic TypeScript capability module under loopx/control_plane/capabilities/repository_context/ for co-located module living specifications (SPEC.md), budgeted write_scope dependency-graph routing, and TypeScript AST public interface & dependency boundary checking:

  1. Co-located Module Knowledge Graph (module_graph.ts):
    • parseModuleSpecMarkdown() and buildModuleKnowledgeGraph() parse module-level SPEC.md documents (Responsibilities, Public Interfaces, Allowed Dependencies, Invariants), validate declared dependency targets, and compute canonical digests via the single-owner canonicalAuthoritySha256 helper.
  2. Budgeted write_scope & Shortest-Hop Dependency Context Routing (context_route.ts):
    • routeRepositoryContext() routes module summaries by write_scope ownership ( depth = 0, owning_write_scope), direct dependencies (depth = 1, direct_dependency), and optional configurable maxDependencyDepth (depth >= 2, transitive_dependency) in deterministic shortest-hop BFS order under a strict UTF-8 byte budget (maxBytes).
    • buildTurnContextContributionInput() packages the routed context and omitted/stale readback metadata into a host-validated turn-context contribution (validateTurnContextContribution) without creating a competing authority surface.
  3. TypeScript Compiler AST Interface & Dependency Drift Detection (interface_boundary.ts):
    • extractTypeScriptExportsAndImports() and comparePublicInterfaces() use typescript AST parsing (ts.createSourceFile) to detect missing declared exports, undeclared public exports, and imports outside allowedDependencies while ignoring comment/string syntax traps.

Multi-Arm Evaluation & Ablations

Added a deterministic multi-arm evaluation benchmark (examples/living_specs_benchmark_corpus.ts and examples/repository-context-living-specs-comparison.ts) across 15 five-layer modules, 36 routing tasks with independent RoutingTaskOracle ground truth, 4 byte budgets (2, 4, 8, 16 KiB), and 80 interface boundary cases:

Budget Arm Required Recall % Complete Task Coverage % Direct Contract Coverage % Transitive Coverage % Irrelevant Noise % Mean Bytes Budget Violations
8,192 B R0 (Repo order) 48.15% 22.22% 21.88% 25.00% 79.58% 8,180 B 0
8,192 B R1 (Owner + path proximity) 69.44% 36.11% 37.50% 25.00% 74.47% 7,993 B 0
8,192 B R2 (Owner + BM25 lexical) 92.13% 77.78% 75.00% 100.00% 62.78% 7,832 B 0
8,192 B R3-no-deps (maxDependencyDepth: 0) 58.56% 27.78% 31.25% 0.00% 0.00% 1,504 B 0
8,192 B R3 (maxDependencyDepth: 1, default) 96.30% 88.89% 100.00% 0.00% 15.42% 4,353 B 0
8,192 B R3-2hop (maxDependencyDepth: 2) 100.00% 100.00% 100.00% 100.00% 33.44% 6,138 B 0
  • Tight-budget stability (2 KiB & 4 KiB): Because BFS packs depth 0 (owning_write_scope) and depth 1 (direct_dependency) before depth 2 (transitive_dependency), R3-2hop exhibits zero regression relative to R3 at 2 KiB and 4 KiB.
  • Boundary extraction (80 cases): TypeScript AST (B1) achieves 100.00% precision, 100.00% recall, and 0.00% FPR across the 70 supported symbol/import cases (eliminating B0 regex's 100% false-positive rate on comment/string syntax traps and 83.33% FPR on destructured/aliased exports), while explicitly reporting the 10 signature-only compatibility cases (0/10) as outside symbol-name AST scope.

Validation

  • npm run typecheck:control-plane
  • node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/control_plane_ts/repository_context_living_specs.test.ts
  • node --no-warnings --experimental-strip-types examples/repository-context-living-specs-comparison.ts
  • uv run python scripts/generate_semantic_inventory.py --changed-from HEAD (0 candidates)
  • pytest tests/architecture/test_content_digest_single_owner.py tests/architecture/test_content_digest_production_owner.py tests/architecture/test_semantic_development_probe.py (289 passed)

@mergify

mergify Bot commented Oct 25, 2026

Copy link
Copy Markdown

Hi @hhyykk, the DCO Sign-off check did not pass. Please inspect
its details first: checkout, fetch, timeout or infrastructure errors
need their own recovery, not a rewrite of otherwise signed commits.

If the log confirms a missing Signed-off-by trailer, amend the
affected commit with git commit --amend -s; for multiple commits,
use an interactive rebase against the current base from the correct
base-repository remote and sign off each affected commit. Push the
rewritten PR branch with git push --force-with-lease origin HEAD.

@songoow songoow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer: model_agent · GPT-6 Astra Pro · OpenAI · self_reported

审查提交:5dbd86891414a225d481858499284d44b18ca945;实际 PR 差异基线:59464f9741fe177381743eadaaffebbb54e5472e;对照当前 main:2b72d30fca7f452a71dffb723ebf6d1e2e878484。结论:REQUEST_CHANGES。以下四项实现问题均已直接调用新增 API 复现;另有一项生产交付边界尚未证明。

动机

目标使用者是在已有仓库中修改代码的 Agent,以及需要核对模块边界的维护者。

具体场景是修改订单 API 时,需要同时取得所属模块与依赖模块的约束,并识别新增的越界依赖;本 PR 希望用模块说明文件的图路由和静态检查,减少逐个查找说明及人工核对边界的工作。当前普通 Turn 路径尚未接入这些新模块,因此这个前后对比是拟交付能力,不能写成已经改善了实际用户任务。

已验证的结果是确定性的图构建、预算选择和有限语料上的提取行为;真实上下文消费、任务完成率、长期收益及完整成本仍未证明。

本次边界不包含签名兼容性分析、自动发现过期说明、模型任务实验或新增写权限;这些没有被当作本 PR 必须补齐的完整产品路线图。

改动思路

代码先从调用方提供的 SPEC.md 内容解析职责、公开名称、允许依赖及不变量,复用 canonicalAuthoritySha256 计算摘要;图构建拒绝重复模块、缺失依赖和环。路由以写入范围找到 owner,再按最短依赖跳数选择模块,先 owner、再直接依赖、最后可选间接依赖,同层保持路径顺序并计算 UTF-8 JSON 字节。另一路比较说明中的名称与 TypeScript AST 观察到的导出、导入。

buildTurnContextContributionInput 复用既有 validateTurnContextContribution,并核对 Goal/Agent 身份;它生成 advisory context,不产生新的调度、写权限或已执行 review gate。这个复用方向合理,但“构造器通过验证”与“真实 Turn 已经注册并消费该贡献”是两个不同的可观察结果。

目前四个新增生产文件只有测试和示例消费者,缺少普通 Turn 的注册、说明来源和消费路径;因此生产机制的放置与交付价值仍未证明。

本 PR 的最小可接受边界是接通一个已有合法调用方并验证输入到消费的闭环,或明确采用维护者接受的研究边界并将未使用的机制保留在研究侧;无需为此建立新的框架或扩展到完整路线图。

具体改动

实际差异为 8 个文件,+1,123/-0:生产模块 327 行、两个合成评测文件 695 行、测试 100 行、tsconfig 纳入测试 1 行。没有将主分支后续提交与旧 PR head 的直接差异误算为本 PR 的删除。

关键代码讲解

  1. parseModuleSpec / buildModuleKnowledgeGraph:忽略 fenced 示例,仅解析二级标题与 bullet;保留独立声明文本、计算摘要并验证图。接口名称提取仍使用局部正则,导致 F4。
  2. routeRepositoryContext:默认一跳,支持显式零跳/两跳,拒绝负预算和矛盾参数;预算只覆盖 selected JSON array。它把 scope 模式当作实体路径,导致 F3。
  3. buildTurnContextContributionInput:绑定已有 envelope、构造 context/readback 并调用共享 validator。没有注册实际 hook,也没有绕过共享 64 KiB 结果上限。
  4. extractTypeScriptExportsAndImports / comparePublicInterfaces:支持若干声明、解构、具名 re-export,显式拒绝需 resolver 的 wildcard re-export,再比较名称集合和依赖路径。固定 TSX 模式及顶层扫描分别导致 F1、F2。
  5. repository-context-living-specs-comparison.ts 与 corpus:手写任务/边界真值独立于算法输出;适配层与实际比较器的相等断言不是期望值来源。测试覆盖常规图、预算、深度、贡献验证及名称/静态 import 漂移,tsconfig 将该测试纳入类型检查。

没有找到本功能另行链接的已接受 RFC/issue。通用交付依据为 docs/development/README.md 的 Scope-Fit Review,审查版本 2b72d30fca7f452a71dffb723ebf6d1e2e878484;这些条款及共享 scope/context owner 在 PR 基线、head 与此 main 之间没有变化。以下编号仅用于映射该文档已有条款:

条款映射 当前结论
SF-active-caller:新增模块须有 shipped behavior、active call site 或明确兼容合约 not_met:当前只被测试与示例调用,见 F5;需最小实际调用链或被接受的研究边界。
SF-independent-oracle:期望值来自独立语义,不能由被测输出生成 implemented:手写 corpus 标签可追溯且汇总可独立复算;有限样本的覆盖不足由 F1–F4 指明。

已复现的实现问题

F1 · P2:按真实文件类型解析,且不能在解析失败后返回完整性结论。

定位:interface_boundary.ts:30。合法 .ts:

export const identity = <T>(value: T): T => value;
export const secret = 1;

TypeScript compiler 对这段代码零诊断,公开名称为 identity, secret;新 extractor 固定使用 module.tsx / ScriptKind.TSX,只返回 identity。SPEC 只声明 identity 时错误返回 hasDrift=false;声明两个名称时又错误报告 secret missing。尖括号断言后的静态 import 也能被恢复树吞掉。当前仓库 quota/context_delivery.ts:14 就有同形泛型箭头。请从已有 ModuleSourceFile.filePath 传入实际 ScriptKind,并明确处理 parse diagnostics;回归覆盖以上两种 SPEC 和断言后的 import。

F2 · P2:递归收集 literal dynamic import 与 import type。

定位:interface_boundary.ts:33–40。设置 allowedDependencies=[],并分别声明 submit / Order:

export async function submit() {
  return (await import("../forbidden/index.ts")).secret;
}
// 独立的第二个用例:
export type Order = import("../forbidden/index.ts").Secret;

两个用例均经真实 compiler 零诊断验证,且成功解析 forbidden 文件;当前比较器却都返回 forbiddenImports=[]、requiresGateReview=false。原因是只识别顶层 statement,没有访问表达式与 ImportTypeNode。仓库的 effect_runtime_handlers.ts 和 task_lease_acquire.ts 已使用这些语法。请递归收集可静态确定的 import literal,再复用现有依赖判定;不支持的动态目标应显式保留证据不完整状态。

F3 · P2:保持既有 write_scope 的 glob 语义。

定位:context_route.ts:49–55。图中包含 src/api、src/api/orders、src/service,预算 16 KiB 足够时,writeScope=["src/"] 选择三个模块,等价覆盖范围 writeScope=["src/**"] 却选择空集;["**"] 也为空,["src/api/**"] 只选父模块而漏掉嵌套模块。共享 canonicalWriteScope 与 overlap owner 接受这些模式,公开 overlap 调用也确认它们覆盖模块内文件。请复用已有 scope 语义选择模块,增加递归目录、全仓与嵌套模块回归。影响是应提供的说明/依赖上下文缺失,并被误标为 outside_write_scope;没有据此推断写权限扩大。

F4 · P2:SPEC 声明解析不能把关键字当作公开名称。

定位:module_graph.ts:66–69。SPEC bullet 和源码都写 export enum Mode { On, Off } 时,SPEC 名称被解析为 enum,AST 名称为 Mode,最终错误报告 missing enum、undeclared Mode、requiresGateReview=true。const enum、abstract class、declare function、namespace 也复现同一根因,源码均零语法诊断。请统一说明声明与源码的名称提取规则;若有明确不支持的声明,解析时拒绝它,而不是接受错误名称。回归应断言一致的 SPEC/source 不产生 drift。

对主干的风险

F5 · P2 交付缺口:新增生产结构没有实际生产消费者。 全仓引用搜索只找到本 PR 的测试、benchmark 和 barrel re-export;attach_turn_context 仍只提供 memory registration。没有普通调用路径生成/加载模块说明、选择本路由器、消费该 context 或执行 requiresGateReview。这限制了当前回归的线上影响,也意味着不能把功能接入视为完成。请按前述最小边界补齐调用及输入/消费验证,或明确被接受的研究定位;仅新增目录、导出和绿色 helper 测试不足以满足 scope-fit 条款。

F1/F2 会漏报新增 API 所声称检查的边界,F3 会漏给相关上下文,F4 会误报完全合规的实现。它们目前不会自动阻断线上任务,也没有证据表明已有权限或持久化被改变;修复后应同时验证合法输入和违规输入,避免只修漏报却扩大误报。现有 source→context 注册路径并未被此差异修改,但没有实际 feature enable/disable 调用可供执行,因此不将“尚无消费者”当作已完成默认关闭集成测试。

语义与 CI 对齐

write_scope 是现有共享语义,F3 应复用该 owner,而不是新建一套模式解释。selection/omission 字符串目前只是新模块本地投影;requiresGateReview 是返回值,没有证明某个执行门会消费它。diff-scoped semantic probe 对实际基线返回 0 candidates,但工具明确不覆盖所有语义形式,不能据此宣称没有新增语义风险。

已执行:npm run -s typecheck:control-plane 通过;repository_context_living_specs.test.ts 与 turn_capability_context.test.ts 两个测试文件通过;三份 content-digest/semantic 架构测试 289 passed。架构测试首次在受限环境中因本地 Effect runtime IPC 权限产生 1 个启动失败,允许该本地 IPC 后原命令 289 项全部通过;这不是 PR 修复记录。独立反例直接调用新增 API,其中 F1/F2 的五个 TypeScript 输入还经真实 compiler 零诊断验证。

合成评测完整运行并独立复算:864 个路由观察、160 个边界观察;8 KiB 下默认 R3 required recall 96.30%,两跳 100%,B1 在所选 70 个 supported cases 中 TP=39、FP=0、TN=31、FN=0。10 个 signature-only cases 检出 0/10 已被原文披露,不另当作缺陷。指标本身可复现,新增反例说明该 corpus 没有覆盖完整合法语法和标准 scope。

远端 CI 另有失败,不能宣称全部通过;实际测试 checkout 是当前 main 与该 PR 的 merge commit e2a695e3f7bb99cee588789a260729203980c02c。Sign-off 日志确认当前提交缺少有效 DCO trailer;kernel-static-checks 的 Ruff 错误位于未被本 PR 修改的 tests/presentation/test_linked_goal_sources.py:23,当前 main 也复现该错误,而 PR 原始 head 的同文件检查通过。build 另有 dashboard smoke:chat-model-menu 断言失败;本地未重新执行该 dashboard 环境,归因保留未验证。其余部分红灯是上游检查失败/跳过后的聚合门,并非四组已执行的产品测试反例。以上 CI 状态与本次四个独立复现的代码问题分开记录,不要求本 PR 静默承担未确认的主干问题。

补测应在 tests/control_plane_ts/repository_context_living_specs.test.ts 固化 F1–F4,复跑上述 TS 测试、类型检查及 node --no-warnings --experimental-strip-types examples/repository-context-living-specs-comparison.ts;F5 需补实际调用方的输入、消费和失败恢复验证。预算还有独立边界:totalBytes 只计算 selected array,omitted readback 加上 envelope 仍可能超出共享 64 KiB 上限;原 benchmark 已披露该预算口径,本次将其保留为集成风险,不重复列一个实现 blocker。

我的整体评价

这份实现复用了已有摘要和 turn-context 验证 owner,图遍历、字节记账及合成评测有可复现的正面证据;没有必要否认这些结果,也没有必要为此次机制审查运行付费模型实验。

但是,公开 API 对当前仓库已经使用的合法语法和 scope 有确定性错误,且生产消费者与说明生产/更新链路尚缺,因此当前交付判断为 not_yet_proven。长期任务收益和真实用户体验改进均未证明;测试/图/贡献对象存在不能代替实际消费与结果。本 PR 未改动渲染界面,视觉检查不适用;未验证范围是实际 Host 接入、模型采用、长期结果与完整成本,而非要求增加 UI。

兼容性方面没有新增版本分支或历史数据迁移要保留;更有价值的局部简化是共享 scope 语义并统一声明名称解析,避免第二套规则。327 行新生产结构的维护成本需要实际调用方或被接受的研究边界来支持。修复 F1–F4、明确 F5 边界并补相应回归后,再按新 head 复审;本次不授予合并资格,也不把现有 CI 红灯改写成已通过。

English verdict: REQUEST_CHANGES — head 5dbd86891414a225d481858499284d44b18ca945. Four deterministic API defects reproduce: forced TSX parsing loses valid TS declarations/imports; literal dynamic imports and import types are missed; canonical write-scope globs omit owned modules; SPEC declaration keywords are misread as export names. The production caller/delivery boundary also remains unproven. Typecheck, both focused TS test files, 289 architecture tests and the synthetic benchmark pass, but the additional counterexamples fail the intended invariants. Existing remote CI failures are reported separately; no production gate bypass or end-user benefit is claimed.

* incomplete public interface. Named re-exports are supported without IO.
*/
export function extractTypeScriptExportsAndImports(sourceCode: string): {exports: string[]; imports: string[]} {
const source = ts.createSourceFile("module.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 请按真实文件类型解析,并处理 parse diagnostics。合法 .ts 的 export const identity = <T>(value: T): T => value; export const secret = 1; 在真实 TS compiler 下零诊断且导出两个名称;固定 TSX 解析这里只返回 identity。SPEC 只声明 identity 时错误返回 hasDrift=false,声明两者时又错误报告 secret missing。请从 ModuleSourceFile.filePath 传入 ScriptKind,并补两方向回归。完整证据与影响边界见本 review F1。

Comment on lines +33 to +40
for (const statement of source.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
imports.add(statement.moduleSpecifier.text);
}
if (ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)) {
const expression = statement.moduleReference.expression;
if (expression && ts.isStringLiteral(expression)) imports.add(expression.text);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 顶层 statement 扫描遗漏 literal dynamic import 和 import type。export async function submit() { return (await import("../forbidden/index.ts")).secret; } / export type Order = import("../forbidden/index.ts").Secret; 分别声明公开名称并设置 allowedDependencies=[],两者都返回 forbiddenImports=[]、requiresGateReview=false。真实 compiler 均零诊断且解析到该文件。请递归收集这两类 AST 节点,再应用已有依赖规则。见 F2。

Comment on lines +49 to +55
for (const entry of request.writeScope) {
const path = normalized(entry);
const matching = graph.modules.filter(module => within(path, module.modulePath));
const longest = matching.sort((a, b) => b.modulePath.length - a.modulePath.length)[0];
if (longest) owners.add(longest.modulePath);
// A directory scope also authorizes modules nested beneath that directory.
for (const module of graph.modules) if (within(module.modulePath, path)) owners.add(module.modulePath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] write_scope 模式不能当作实体路径。现有 canonicalWriteScope 接受 src/** 和 **,共享 overlap owner 确认其覆盖 src/api/orders/index.ts;此处在16KiB足够预算下,src/能选中 src/api、src/api/orders、src/service,而src/和都选空,src/api/**只选父模块。请复用现有glob语义并补递归/全仓/嵌套模块回归。这是上下文漏选,不是权限扩大。见 F3。

Comment on lines +66 to +69
const publicInterfaces = (sections.get("Public Interfaces") ?? []).map(declaration => {
const name = /^(?:export\s+)?(?:(?:async\s+)?function\s+|class\s+|interface\s+|type\s+|const\s+|let\s+|var\s+)?([$\p{ID_Start}][$\p{ID_Continue}]*)/u.exec(unquote(declaration))?.[1];
if (!name) throw new Error(`Invalid public interface in ${input.specPath}: ${declaration}`);
return {name, declaration};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 这段正则会把合法声明关键字当作接口名。SPEC bullet与source同为 export enum Mode { On, Off } 时,publicInterfaces.name变成enum,而AST导出Mode,因此合规代码被报missing enum、undeclared Mode且requiresGateReview=true。const enum、abstract class、declare function、namespace也可复现。请统一名称提取语法或显式拒绝不支持的声明,补相同SPEC/source的无漂移回归。见 F4。

@hhyykk hhyykk closed this Oct 26, 2026
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.

2 participants