mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): add bounded trusted run comparison
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
最新增量证据(2026-08-13):
|
||||
|
||||
- D-308/ADR-0400(已接受):首个只读 Copilot 的“最近成功/失败运行对比”不再依赖 Prompt 自由拼接两个查询。`qinglong.run.compare@1.0.0` 作为第二个受信内建 Tool,固定 `read/low`、`run.read`、`database.read` 和 5 秒 deadline;只有当前 Project Tool snapshot 显式包含 reviewed Definition、产品 composition 显式绑定 adapter 后才可执行。它按 baseline→candidate 串行复用两次有界 Run 点查,absent 与 cross-Project 均为 `found:false`,只输出低敏 Run projection、固定 changed fields 和可证明的 queue/execution/total duration delta;任一时间戳不完整或结束早于开始时不生成对应差值。输出明确标记 `ordered_independent_point_reads`,不冒充数据库事务快照。实现位于既有 `runtime-core/tool-execution/builtin-run-compare/`,只提供两个显式 subpath,不从 package root 导出,不新增 workspace package、依赖、表、migration、连接、timer、listener、watcher、cache 或低配设备常驻开销。最终 18-package clean build/test 退出 0;backend 1,208 项为 1,206 pass/2 条件 skip/0 fail;package/dependency/Edge/Cluster deployment 审计零 finding。Edge、Edge AI、Edge MCP 制品为 2,589,812 / 3,121,108 / 7,209,862 bytes,均低于各自上限且未装配 subpath 被发布投影裁掉。
|
||||
- D-307/ADR-0399(已接受):物理 Edge release archive Gate 使用外部 Ed25519 签发、QingLong verify-only 的两阶段协议。`prepare` exact 重建 owner-private 统一物理报告,要求 direct release service start 已通过,并把 repository、40 位 Git revision、设备/boot、物理报告、release archive、实机 artifact tree/metadata/entrypoint 与 Node digest/version 编入无换行 canonical payload;私钥始终位于 HSM/KMS/离线 operator。`finalize` 以 operator-pinned SPKI 公钥重算 fingerprint,稳定读取并复核所有输入后验证 64-byte detached signature,任一 source/archive/evidence 漂移均失败关闭,输出 `0600` no-replace envelope。通过只把 `release_archive_signature` 替换为 `release_archive_signature_or_attestation`,`supported:false` 与其余 firmware、整机 flash、migration、断电、固定实机采集和 Cluster 容量 Gate 全部保留。基础 importer 同时把 Edge、SQLite 与 Plugin Package 三个 workload 的 platform/architecture 精确绑定到统一物理观测,release verifier 再要求 observed/Edge/SQLite 为完整 recorder shape 且 Node identity 相同,拒绝跨主机拼接与重算外层摘要后的最小伪造。实现不新增 package、依赖、daemon、listener、timer、watcher 或设备常驻负担;18-package clean build/test 退出 0,backend 1,208 项为 1,206 pass/2 条件 skip/0 fail,package/dependency/Edge/service bridge/Cluster deployment 审计零 finding。
|
||||
- D-303/ADR-0391(已接受)
|
||||
Cluster operator context 在离线 `validate` 之后增加显式、只读的 `ql3-cluster-admin context probe`。probe 先完整预检全部
|
||||
@@ -6896,6 +6897,13 @@ Copilot 默认不能直接提交修改或运行命令。写操作通过 Tool Reg
|
||||
- 限制单次日志上下文大小,优先在本地提取错误片段和统计摘要。
|
||||
- 远程模型不可用时,普通任务管理、调度、日志和通知仍完整可用。
|
||||
|
||||
`next` 已由 ADR-0162/ADR-0400 提供两个 profile-neutral 受信只读原语:
|
||||
`qinglong.run.get@1.0.0` 和 `qinglong.run.compare@1.0.0`。后者只接受两个不同 Run ID,
|
||||
按固定顺序执行 Project-scoped 点查并返回低敏差异;它明确不声称事务快照,也不读取
|
||||
日志、error、command、Artifact 或 Secret。两者目前仍是产品组合显式装配的 Tool,
|
||||
不是默认向所有 Project、HTTP、MCP 或模型开放的 ambient capability。“最近成功与失败”
|
||||
的有界选择、日志 range/redaction 和最终 Copilot Prompt 仍需后续产品 Gate。
|
||||
|
||||
## 17. Tool Registry
|
||||
|
||||
### 17.1 ToolDefinition
|
||||
@@ -6960,9 +6968,12 @@ ApprovalRequest、immutable dispatch、StepRun、Audit 和受审 handler registr
|
||||
SQLite v27 与 PostgreSQL v28 都只允许同 Project、同 Run、`ready` 或
|
||||
`waiting_approval` 的 Tool StepRun 执行 append/exact replay。PostgreSQL runtime
|
||||
仍不能 SELECT 全量 Audit,只能在同一事务写 Audit、Trace 和保存完整 audit JSON 的
|
||||
receipt,再由 foreign key 与不可变 ACL 证明审计事实存在。该 repository 没有 execute
|
||||
seam;plan/dispatch、StepRun、Trace、Audit 尚未进入同一个 start barrier,因此本节
|
||||
规则仍是 production gate,而不是已经开放的 Tool 调用路径。
|
||||
receipt,再由 foreign key 与不可变 ACL 证明审计事实存在。后续 ADR-0158 至 ADR-0163
|
||||
已经把 immutable plan/input/preview、同事务 start barrier、snapshot-specific executable
|
||||
binding、加密 result Artifact、StepRun terminal transition、RunEvent、Trace/Audit completion
|
||||
串成双方言耐久链;response loss 只读取已提交 result,不重复执行 adapter。该完成事实
|
||||
不等于默认开放产品入口:Project snapshot、Policy、binding、repository、Artifact key 与
|
||||
transport/MCP/Copilot composition 仍必须逐 Profile 显式装配。
|
||||
|
||||
## 18. MCP
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# ADR-0400:有界、受信的 Run 对比 Tool
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-14
|
||||
- 关联 RFC:QL-RFC-0001 D-308、Phase 2
|
||||
- 关联 ADR:ADR-0133、ADR-0162、ADR-0163
|
||||
|
||||
## 问题
|
||||
|
||||
QingLong 3.0 首个稳定版本要求只读 Copilot 能比较最近成功与失败运行,但现有受信内建
|
||||
Tool 只有 `qinglong.run.get@1.0.0`。让模型连续自由调用两次 `run.get` 会把选择、字段
|
||||
裁剪、差值算法和一致性声明留给 Prompt;另建 Copilot 专用数据库查询又会绕过 Tool
|
||||
Definition、Project Policy、durable start barrier、结果 Artifact 与 Trace/Audit 链。
|
||||
|
||||
对比能力还必须同时适用于低配路由设备和 Cluster。它不能引入新 package、连接池、
|
||||
缓存、watcher 或后台采集器,也不能把 error、command、Artifact、PID、lease、Secret
|
||||
或跨 Project 存在性送入模型上下文。
|
||||
|
||||
## 决策
|
||||
|
||||
1. 在既有 `@qinglong/runtime-core` 的
|
||||
`tool-execution/builtin-run-compare/` 领域目录内实现
|
||||
`qinglong.run.compare@1.0.0`,只从
|
||||
`/builtin-run-compare-tool` 与 `/builtin-run-compare-projection` 两个显式 subpath
|
||||
导出。它不进入 package 根入口,也不新增 workspace package。
|
||||
2. Definition 固定为 `effect=read`、`risk=low`、权限 `run.read`、5 秒 deadline;
|
||||
executable binding 只允许 `database.read`,适用于 `edge|standalone|cluster-control`,
|
||||
recovery mode 为 `retry_safe_read`。Definition 必须先作为 `qinglong` Package 的普通
|
||||
contribution 进入当前 Project Tool snapshot;平台不会隐式授予任何 Project。
|
||||
3. 输入只接受两个不同、各不超过 128 字符的 `baselineRunId` 与 `candidateRunId`。
|
||||
adapter 复用现有 `RunRepositoryReader.findRunById`,按 baseline → candidate 固定顺序
|
||||
串行执行两次有界点查;不新增 transaction 或并发读。任一点查损坏或不可用时,整个
|
||||
调用返回稳定 unavailable。
|
||||
4. absent 与 cross-Project 对调用方统一为 `{found:false}`。可见 Run 只包含既有低敏
|
||||
projection:Run/Task revision identity、状态/version/event sequence/priority、执行来源/
|
||||
owner 与生命周期时间;request、trigger、input/output Artifact、executor handle、PID、
|
||||
lease、error 和 Secret 一律不输出。
|
||||
5. 只有两侧都可见时才计算 `taskId|taskRevision|status|priority|executionOrigin|
|
||||
executionOwner` 的固定顺序 changed fields,以及可证明的 queue、execution、total
|
||||
duration delta。任一时间戳缺失、非安全整数或结束早于开始时,对应差值字段不出现,
|
||||
不以 `0` 冒充未知。
|
||||
6. 输出永久携带 `consistency=ordered_independent_point_reads`。本协议不声称两次读取属于
|
||||
同一数据库 snapshot;每个 projection 的 version/eventSequence 供上层展示观察边界。
|
||||
若未来需要事务快照,必须由 repository 新契约独立评审,不能静默改变 v1 语义。
|
||||
7. 该 Tool 复用既有 durable start → current binding → encrypted input Artifact → key →
|
||||
adapter → encrypted result Artifact → StepRun/RunEvent/Trace/Audit completion 链。本 ADR
|
||||
不开放新的 HTTP/MCP/Copilot route;产品组合仍必须显式装配 current snapshot、binding、
|
||||
repository 和 key authority。
|
||||
|
||||
## 低配与集群影响
|
||||
|
||||
- Edge/Standalone 空闲时零新增模块加载、timer、listener、数据库写入或后台唤醒;每次
|
||||
调用最多增加两次串行 Run 点查和一个调用期 deadline。
|
||||
- Cluster 复用现有 PostgreSQL Run reader 与调用方 Pool;不新增表、migration、role、
|
||||
Pool、controller、Job 或缓存。
|
||||
- package 数保持 18;新源码位于有稳定领域名的二级目录,不回到 `src` 根平铺,也不为
|
||||
两个文件制造微型 package。Runtime Core source 由 160 增至 162,新增量全部为 nested,
|
||||
根层仍精确为一个 160 行公开 export 文件。
|
||||
|
||||
## 被否决方案
|
||||
|
||||
1. **让 Prompt 自由组合两次 `run.get`**:不能冻结字段、顺序、差值和一致性声明。
|
||||
2. **在 AI package 直连 Run repository**:会让模型路径绕过受信 Tool 和 durable evidence。
|
||||
3. **一次并发读取两个 Run**:没有事务快照时并发不能提供更强一致性,却增加低配设备
|
||||
瞬时数据库并发。
|
||||
4. **返回 error summary、日志或 Artifact**:它们需要独立授权、范围、脱敏和字节预算。
|
||||
5. **找不到一侧时区分 absent/cross-Project**:会泄露跨租户 Run 存在性。
|
||||
6. **新增 `ql3-run-compare` package**:没有独立部署、依赖或权限生命周期收益。
|
||||
|
||||
## 当前验证
|
||||
|
||||
- 定向 5/5:覆盖有界差值、固定读取顺序、跨 Project/absent 等价、时间戳缺失、别名与
|
||||
unknown input、损坏/不可用 repository、Definition/binding/authority 漂移、显式 subpath
|
||||
和根入口零导出。
|
||||
- Runtime Core 全量 553/553 通过;TypeScript clean build 通过。
|
||||
- 最终 18-package clean build/test 退出 0;backend 1,208 项为 1,206 pass、2 条平台条件
|
||||
skip、0 fail。package boundary 保持 18 个 package,`singleSourcePackages=[]`、
|
||||
`shallowSourcePackages=[]`;cluster dependency、Edge import 与 Cluster deployment 审计
|
||||
均无 finding。
|
||||
- 基础 Edge、Edge AI、Edge MCP 最终制品分别为 2,589,812 / 3,121,108 /
|
||||
7,209,862 bytes,低于各自 4 / 5 / 16 MiB 上限;未装配的 compare subpath 由 Profile
|
||||
runtime export projection 裁掉,不产生默认常驻模块或发布闭包强依赖。
|
||||
- GitNexus 对复用的 `executeBoundedRunReadProjection` 报告 LOW:1 个直接调用者、4 个
|
||||
总影响、0 条 execution flow。`RunRepositoryReader` 接口覆盖 11 个直接/33 个总依赖,
|
||||
为 MEDIUM,但本切片没有修改该接口,只消费既有 `findRunById`。
|
||||
- 刷新索引后,新 `executeBuiltInRunCompareTool` 为 LOW:1 个直接调用者、6 个总影响、
|
||||
0 条 execution flow;adapter class 与 binding factory 均为 0 个产品调用者,精确反映
|
||||
“契约已提供、产品组合尚未默认装配”的状态。
|
||||
|
||||
## 后续门禁
|
||||
|
||||
1. 在 Local 与 Cluster 产品 composition 中装配 Definition/binding,并以真实 SQLite/
|
||||
PostgreSQL 完成 durable start→result completion 纵切面;
|
||||
2. 增加受 Policy 保护的“按 Task 选择最近成功/失败 Run”,但不得把无界 search 交给模型;
|
||||
3. 日志解释必须另设 range、redaction、Artifact 与 Prompt-injection 边界;
|
||||
4. 固定物理 Edge 的单次对比延迟/RSS,以及 Cluster 多副本读取与 PostgreSQL failover 证据。
|
||||
@@ -403,6 +403,7 @@
|
||||
| [ADR-0397](./ADR-0397-explicit-cluster-secret-action-manual-recovery.md) | Cluster Secret Action 显式人工恢复 | Accepted(实现、单节点 PostgreSQL、完整 workspace/后端/边界与 physical HA 门完成) |
|
||||
| [ADR-0398](./ADR-0398-pre-activation-plugin-package-candidate-qualification.md) | Plugin Package 激活前候选资格校验与自动保留旧版本 | Proposed(v2 私有报告/离线审计已编排、全量/HA 门完成;远端 Kubernetes 成功记录与物理低配门待闭合) |
|
||||
| [ADR-0399](./ADR-0399-external-release-attestation-for-physical-edge-evidence.md) | 物理 Edge 证据的外部发布归档证明 | Accepted(exact recorder 重建与离线验签已实现;固定实机 ceremony 待执行) |
|
||||
| [ADR-0400](./ADR-0400-bounded-trusted-run-comparison-tool.md) | 有界、受信的 Run 对比 Tool | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -236,6 +236,12 @@
|
||||
"builtin-run-read-projection": [
|
||||
"dist/tool-execution/builtin-run-read/builtInRunReadProjection.d.ts"
|
||||
],
|
||||
"builtin-run-compare-tool": [
|
||||
"dist/tool-execution/builtin-run-compare/builtInRunCompareTool.d.ts"
|
||||
],
|
||||
"builtin-run-compare-projection": [
|
||||
"dist/tool-execution/builtin-run-compare/builtInRunCompareProjection.d.ts"
|
||||
],
|
||||
"run": [
|
||||
"dist/run/run.d.ts"
|
||||
],
|
||||
@@ -652,6 +658,16 @@
|
||||
"require": "./dist/tool-execution/builtin-run-read/builtInRunReadProjection.js",
|
||||
"default": "./dist/tool-execution/builtin-run-read/builtInRunReadProjection.js"
|
||||
},
|
||||
"./builtin-run-compare-tool": {
|
||||
"types": "./dist/tool-execution/builtin-run-compare/builtInRunCompareTool.d.ts",
|
||||
"require": "./dist/tool-execution/builtin-run-compare/builtInRunCompareTool.js",
|
||||
"default": "./dist/tool-execution/builtin-run-compare/builtInRunCompareTool.js"
|
||||
},
|
||||
"./builtin-run-compare-projection": {
|
||||
"types": "./dist/tool-execution/builtin-run-compare/builtInRunCompareProjection.d.ts",
|
||||
"require": "./dist/tool-execution/builtin-run-compare/builtInRunCompareProjection.js",
|
||||
"default": "./dist/tool-execution/builtin-run-compare/builtInRunCompareProjection.js"
|
||||
},
|
||||
"./secret-reference": {
|
||||
"types": "./dist/secret/secretReference.d.ts",
|
||||
"require": "./dist/secret/secretReference.js",
|
||||
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
import { EXECUTION_ORIGINS, RUN_STATUSES } from '../../run/run';
|
||||
import type { RunRepositoryReader } from '../../run/runRepository';
|
||||
import {
|
||||
BoundedRunReadProjectionUnavailableError,
|
||||
executeBoundedRunReadProjection,
|
||||
type BoundedRunReadProjection,
|
||||
} from '../../run/projection/boundedRunReadProjection';
|
||||
import {
|
||||
normalizeToolDefinition,
|
||||
type ToolJsonValue,
|
||||
} from '../tool-registry/toolRegistry';
|
||||
|
||||
export const BUILTIN_RUN_COMPARE_TOOL = Object.freeze({
|
||||
name: 'qinglong.run.compare',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_RUN_COMPARE_TIMEOUT_SECONDS = 5;
|
||||
|
||||
const MAX_INT = 2_147_483_647;
|
||||
const MIN_INT = -2_147_483_648;
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
const COMPARABLE_FIELDS = Object.freeze([
|
||||
'taskId',
|
||||
'taskRevision',
|
||||
'status',
|
||||
'priority',
|
||||
'executionOrigin',
|
||||
'executionOwner',
|
||||
] as const);
|
||||
|
||||
const RUN_PROJECTION_SCHEMA = Object.freeze({
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
found: { type: 'boolean' as const },
|
||||
id: { type: 'string' as const, minLength: 1, maxLength: 128 },
|
||||
taskId: { type: 'string' as const, minLength: 1, maxLength: 255 },
|
||||
taskRevision: { type: 'string' as const, minLength: 1, maxLength: 255 },
|
||||
status: {
|
||||
type: 'string' as const,
|
||||
maxLength: 32,
|
||||
enum: RUN_STATUSES,
|
||||
},
|
||||
version: { type: 'integer' as const, minimum: 0, maximum: MAX_INT },
|
||||
eventSequence: {
|
||||
type: 'integer' as const,
|
||||
minimum: 0,
|
||||
maximum: MAX_INT,
|
||||
},
|
||||
priority: {
|
||||
type: 'integer' as const,
|
||||
minimum: MIN_INT,
|
||||
maximum: MAX_INT,
|
||||
},
|
||||
executionOrigin: {
|
||||
type: 'string' as const,
|
||||
maxLength: 32,
|
||||
enum: EXECUTION_ORIGINS,
|
||||
},
|
||||
executionOwner: {
|
||||
type: 'string' as const,
|
||||
maxLength: 16,
|
||||
enum: ['legacy', 'runtime'] as const,
|
||||
},
|
||||
createdAtMs: {
|
||||
type: 'integer' as const,
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
queuedAtMs: {
|
||||
type: 'integer' as const,
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
startedAtMs: {
|
||||
type: 'integer' as const,
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
finishedAtMs: {
|
||||
type: 'integer' as const,
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
},
|
||||
required: ['found'] as const,
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const BUILTIN_RUN_COMPARE_TOOL_DEFINITION = normalizeToolDefinition({
|
||||
name: BUILTIN_RUN_COMPARE_TOOL.name,
|
||||
version: BUILTIN_RUN_COMPARE_TOOL.version,
|
||||
description:
|
||||
'Compare two low-sensitive Project-scoped Run projections using bounded point reads',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
baselineRunId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
candidateRunId: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
},
|
||||
required: ['baselineRunId', 'candidateRunId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
baseline: RUN_PROJECTION_SCHEMA,
|
||||
candidate: RUN_PROJECTION_SCHEMA,
|
||||
comparable: { type: 'boolean' },
|
||||
sameTask: { type: 'boolean' },
|
||||
sameTaskRevision: { type: 'boolean' },
|
||||
changedFields: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: COMPARABLE_FIELDS,
|
||||
},
|
||||
maxItems: COMPARABLE_FIELDS.length,
|
||||
},
|
||||
queueDelayDeltaMs: {
|
||||
type: 'integer',
|
||||
minimum: -Number.MAX_SAFE_INTEGER,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
executionDurationDeltaMs: {
|
||||
type: 'integer',
|
||||
minimum: -Number.MAX_SAFE_INTEGER,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
totalDurationDeltaMs: {
|
||||
type: 'integer',
|
||||
minimum: -Number.MAX_SAFE_INTEGER,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
consistency: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
enum: ['ordered_independent_point_reads'],
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'baseline',
|
||||
'candidate',
|
||||
'comparable',
|
||||
'sameTask',
|
||||
'sameTaskRevision',
|
||||
'changedFields',
|
||||
'consistency',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
effect: 'read',
|
||||
risk: 'low',
|
||||
requiredPermissions: ['run.read'],
|
||||
timeoutSeconds: BUILTIN_RUN_COMPARE_TIMEOUT_SECONDS,
|
||||
});
|
||||
|
||||
export class InvalidBuiltInRunCompareToolError extends TypeError {
|
||||
readonly code = 'BUILTIN_RUN_COMPARE_TOOL_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Built-in Run compare Tool is invalid: ${message}`);
|
||||
this.name = 'InvalidBuiltInRunCompareToolError';
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltInRunCompareToolUnavailableError extends Error {
|
||||
readonly code = 'BUILTIN_RUN_COMPARE_TOOL_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Built-in Run compare Tool is unavailable');
|
||||
this.name = 'BuiltInRunCompareToolUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInRunCompareToolError(message);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximum: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= maximum &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function timestampDelta(
|
||||
baselineStart: unknown,
|
||||
baselineEnd: unknown,
|
||||
candidateStart: unknown,
|
||||
candidateEnd: unknown,
|
||||
): number | undefined {
|
||||
if (
|
||||
!Number.isSafeInteger(baselineStart) ||
|
||||
!Number.isSafeInteger(baselineEnd) ||
|
||||
!Number.isSafeInteger(candidateStart) ||
|
||||
!Number.isSafeInteger(candidateEnd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
Number(baselineEnd) < Number(baselineStart) ||
|
||||
Number(candidateEnd) < Number(candidateStart)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const baselineDuration = Number(baselineEnd) - Number(baselineStart);
|
||||
const candidateDuration = Number(candidateEnd) - Number(candidateStart);
|
||||
const delta = candidateDuration - baselineDuration;
|
||||
return Number.isSafeInteger(delta) ? delta : undefined;
|
||||
}
|
||||
|
||||
function compareProjections(
|
||||
baseline: BoundedRunReadProjection,
|
||||
candidate: BoundedRunReadProjection,
|
||||
): Readonly<Record<string, ToolJsonValue>> {
|
||||
const comparable = baseline.found === true && candidate.found === true;
|
||||
const changedFields = Object.freeze(
|
||||
comparable
|
||||
? COMPARABLE_FIELDS.filter((field) => baseline[field] !== candidate[field])
|
||||
: [],
|
||||
);
|
||||
const queueDelayDeltaMs = comparable
|
||||
? timestampDelta(
|
||||
baseline.createdAtMs,
|
||||
baseline.queuedAtMs,
|
||||
candidate.createdAtMs,
|
||||
candidate.queuedAtMs,
|
||||
)
|
||||
: undefined;
|
||||
const executionDurationDeltaMs = comparable
|
||||
? timestampDelta(
|
||||
baseline.startedAtMs,
|
||||
baseline.finishedAtMs,
|
||||
candidate.startedAtMs,
|
||||
candidate.finishedAtMs,
|
||||
)
|
||||
: undefined;
|
||||
const totalDurationDeltaMs = comparable
|
||||
? timestampDelta(
|
||||
baseline.createdAtMs,
|
||||
baseline.finishedAtMs,
|
||||
candidate.createdAtMs,
|
||||
candidate.finishedAtMs,
|
||||
)
|
||||
: undefined;
|
||||
return Object.freeze({
|
||||
baseline,
|
||||
candidate,
|
||||
comparable,
|
||||
sameTask: comparable && baseline.taskId === candidate.taskId,
|
||||
sameTaskRevision:
|
||||
comparable && baseline.taskRevision === candidate.taskRevision,
|
||||
changedFields,
|
||||
...(queueDelayDeltaMs === undefined ? {} : { queueDelayDeltaMs }),
|
||||
...(executionDurationDeltaMs === undefined
|
||||
? {}
|
||||
: { executionDurationDeltaMs }),
|
||||
...(totalDurationDeltaMs === undefined ? {} : { totalDurationDeltaMs }),
|
||||
consistency: 'ordered_independent_point_reads',
|
||||
});
|
||||
}
|
||||
|
||||
export async function executeBuiltInRunCompareTool(
|
||||
runs: Pick<RunRepositoryReader, 'findRunById'>,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
): Promise<Readonly<Record<string, ToolJsonValue>>> {
|
||||
const inputRecord =
|
||||
input && typeof input === 'object' && !Array.isArray(input)
|
||||
? (input as Readonly<Record<string, ToolJsonValue>>)
|
||||
: null;
|
||||
if (
|
||||
!runs ||
|
||||
typeof runs.findRunById !== 'function' ||
|
||||
!boundedText(projectId, 128) ||
|
||||
!inputRecord ||
|
||||
Reflect.ownKeys(inputRecord).length !== 2 ||
|
||||
!boundedText(inputRecord.baselineRunId, 128) ||
|
||||
!boundedText(inputRecord.candidateRunId, 128) ||
|
||||
inputRecord.baselineRunId === inputRecord.candidateRunId
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
try {
|
||||
const baseline = await executeBoundedRunReadProjection(
|
||||
runs,
|
||||
projectId,
|
||||
inputRecord.baselineRunId,
|
||||
);
|
||||
const candidate = await executeBoundedRunReadProjection(
|
||||
runs,
|
||||
projectId,
|
||||
inputRecord.candidateRunId,
|
||||
);
|
||||
return compareProjections(baseline, candidate);
|
||||
} catch (error) {
|
||||
if (!(error instanceof BoundedRunReadProjectionUnavailableError)) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
throw new BuiltInRunCompareToolUnavailableError();
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import type { DeploymentProfile } from '../../cluster-control/clusterControlActivation';
|
||||
import type { RunRepositoryReader } from '../../run/runRepository';
|
||||
import {
|
||||
normalizeProjectToolDefinitionSnapshot,
|
||||
type ProjectToolDefinitionSnapshot,
|
||||
} from '../tool-registry/projectToolDefinitionSnapshot';
|
||||
import {
|
||||
ToolDefinitionRegistry,
|
||||
type ToolJsonValue,
|
||||
} from '../tool-registry/toolRegistry';
|
||||
import {
|
||||
createTrustedToolHandlerBinding,
|
||||
normalizeTrustedToolHandlerBinding,
|
||||
type TrustedToolHandlerBinding,
|
||||
} from '../trustedToolInvocation';
|
||||
import type {
|
||||
TrustedToolExecutionAdapter,
|
||||
TrustedToolExecutionAdapterContext,
|
||||
} from '../trustedToolExecution';
|
||||
import {
|
||||
BUILTIN_RUN_COMPARE_TOOL,
|
||||
BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
|
||||
BUILTIN_RUN_COMPARE_TIMEOUT_SECONDS,
|
||||
InvalidBuiltInRunCompareToolError,
|
||||
executeBuiltInRunCompareTool,
|
||||
} from './builtInRunCompareProjection';
|
||||
|
||||
export {
|
||||
BUILTIN_RUN_COMPARE_TOOL,
|
||||
BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
|
||||
BUILTIN_RUN_COMPARE_TIMEOUT_SECONDS,
|
||||
BuiltInRunCompareToolUnavailableError,
|
||||
InvalidBuiltInRunCompareToolError,
|
||||
executeBuiltInRunCompareTool,
|
||||
} from './builtInRunCompareProjection';
|
||||
|
||||
export const BUILTIN_RUN_COMPARE_ADAPTER = Object.freeze({
|
||||
id: 'builtin.qinglong.run-compare',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_RUN_COMPARE_REDACTION_CONTRACT = Object.freeze({
|
||||
id: 'redaction.qinglong.run-compare',
|
||||
version: '1.0.0',
|
||||
});
|
||||
export const BUILTIN_RUN_COMPARE_AUDIT_CONTRACT = Object.freeze({
|
||||
id: 'audit.qinglong.tool-call',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidBuiltInRunCompareToolError(message);
|
||||
}
|
||||
|
||||
function sameValue(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, maximum: number): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= maximum &&
|
||||
!CONTROL_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
export function createBuiltInRunCompareToolHandlerBinding(
|
||||
snapshotValue: ProjectToolDefinitionSnapshot,
|
||||
profiles: readonly DeploymentProfile[],
|
||||
): Readonly<TrustedToolHandlerBinding> {
|
||||
const snapshot = normalizeProjectToolDefinitionSnapshot(snapshotValue);
|
||||
const definition = snapshot.definitions.find(
|
||||
(entry) =>
|
||||
entry.definition.name === BUILTIN_RUN_COMPARE_TOOL.name &&
|
||||
entry.definition.version === BUILTIN_RUN_COMPARE_TOOL.version,
|
||||
)?.definition;
|
||||
if (!definition || !sameValue(definition, BUILTIN_RUN_COMPARE_TOOL_DEFINITION)) {
|
||||
return invalid('reviewed Tool definition is absent or changed');
|
||||
}
|
||||
return createTrustedToolHandlerBinding(snapshot, {
|
||||
tool: BUILTIN_RUN_COMPARE_TOOL,
|
||||
adapter: BUILTIN_RUN_COMPARE_ADAPTER,
|
||||
executionClass: 'builtin_in_process',
|
||||
profiles,
|
||||
authorities: ['database.read'],
|
||||
timeoutSeconds: BUILTIN_RUN_COMPARE_TIMEOUT_SECONDS,
|
||||
redactionContract: BUILTIN_RUN_COMPARE_REDACTION_CONTRACT,
|
||||
auditContract: BUILTIN_RUN_COMPARE_AUDIT_CONTRACT,
|
||||
});
|
||||
}
|
||||
|
||||
export class BuiltInRunCompareToolAdapter
|
||||
implements TrustedToolExecutionAdapter
|
||||
{
|
||||
readonly binding!: Readonly<TrustedToolHandlerBinding>;
|
||||
readonly profile!: DeploymentProfile;
|
||||
readonly recoveryMode = 'retry_safe_read' as const;
|
||||
readonly #runs!: Pick<RunRepositoryReader, 'findRunById'>;
|
||||
|
||||
constructor(
|
||||
bindingValue: TrustedToolHandlerBinding,
|
||||
profile: DeploymentProfile,
|
||||
definitions: ToolDefinitionRegistry,
|
||||
runs: Pick<RunRepositoryReader, 'findRunById'>,
|
||||
) {
|
||||
const binding = normalizeTrustedToolHandlerBinding(bindingValue);
|
||||
if (!(definitions instanceof ToolDefinitionRegistry)) {
|
||||
return invalid('Tool Definition registry is invalid');
|
||||
}
|
||||
let definition;
|
||||
try {
|
||||
definition = definitions.resolve(
|
||||
BUILTIN_RUN_COMPARE_TOOL.name,
|
||||
BUILTIN_RUN_COMPARE_TOOL.version,
|
||||
);
|
||||
} catch {
|
||||
return invalid('reviewed Tool definition is unavailable');
|
||||
}
|
||||
if (
|
||||
!sameValue(binding.tool, BUILTIN_RUN_COMPARE_TOOL) ||
|
||||
!sameValue(binding.adapter, BUILTIN_RUN_COMPARE_ADAPTER) ||
|
||||
binding.executionClass !== 'builtin_in_process' ||
|
||||
!sameValue(binding.authorities, ['database.read']) ||
|
||||
binding.timeoutSeconds !== BUILTIN_RUN_COMPARE_TIMEOUT_SECONDS ||
|
||||
!sameValue(
|
||||
binding.redactionContract,
|
||||
BUILTIN_RUN_COMPARE_REDACTION_CONTRACT,
|
||||
) ||
|
||||
!sameValue(binding.auditContract, BUILTIN_RUN_COMPARE_AUDIT_CONTRACT) ||
|
||||
!binding.profiles.includes(profile) ||
|
||||
!sameValue(definition, BUILTIN_RUN_COMPARE_TOOL_DEFINITION)
|
||||
) {
|
||||
return invalid('binding does not match the reviewed adapter contract');
|
||||
}
|
||||
if (!runs || typeof runs.findRunById !== 'function') {
|
||||
return invalid('Run repository is invalid');
|
||||
}
|
||||
this.binding = binding;
|
||||
this.profile = profile;
|
||||
this.#runs = runs;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
async execute(
|
||||
context: Readonly<TrustedToolExecutionAdapterContext>,
|
||||
input: ToolJsonValue,
|
||||
): Promise<unknown> {
|
||||
if (
|
||||
!context ||
|
||||
typeof context !== 'object' ||
|
||||
!boundedText(context.projectId, 128)
|
||||
) {
|
||||
return invalid('execution context or input is invalid');
|
||||
}
|
||||
return executeBuiltInRunCompareTool(this.#runs, context.projectId, input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
BUILTIN_RUN_COMPARE_ADAPTER,
|
||||
BUILTIN_RUN_COMPARE_TOOL,
|
||||
BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
|
||||
BuiltInRunCompareToolAdapter,
|
||||
BuiltInRunCompareToolUnavailableError,
|
||||
InvalidBuiltInRunCompareToolError,
|
||||
createBuiltInRunCompareToolHandlerBinding,
|
||||
executeBuiltInRunCompareTool,
|
||||
} = require('../dist/tool-execution/builtin-run-compare/builtInRunCompareTool');
|
||||
const {
|
||||
createPluginPackageResourceGenerationFromReferences,
|
||||
} = require('../dist/plugin-package/pluginPackageResourceGeneration');
|
||||
const {
|
||||
createProjectToolDefinitionSnapshot,
|
||||
projectToolDefinitionRegistry,
|
||||
} = require('../dist/tool-execution/tool-registry/projectToolDefinitionSnapshot');
|
||||
|
||||
const DIGEST_A = 'a'.repeat(64);
|
||||
const DIGEST_B = 'b'.repeat(64);
|
||||
const DIGEST_C = 'c'.repeat(64);
|
||||
|
||||
function run(id, overrides = {}) {
|
||||
return {
|
||||
id,
|
||||
projectId: 'project-compare',
|
||||
taskId: 'task-backup',
|
||||
taskRevision: 'task-backup@4',
|
||||
triggerType: 'schedule',
|
||||
executionOrigin: 'scheduled_system',
|
||||
executionOwner: 'runtime',
|
||||
status: 'succeeded',
|
||||
version: 4,
|
||||
eventSequence: 8,
|
||||
priority: 10,
|
||||
createdAtMs: 1_000,
|
||||
queuedAtMs: 1_020,
|
||||
startedAtMs: 1_050,
|
||||
finishedAtMs: 1_150,
|
||||
requestId: 'must-not-cross-tool-output',
|
||||
inputRef: 'artifact:must-not-cross-tool-output',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function repository(records, calls = []) {
|
||||
return {
|
||||
async findRunById(runId) {
|
||||
calls.push(runId);
|
||||
return records.get(runId) ?? null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(definition = BUILTIN_RUN_COMPARE_TOOL_DEFINITION) {
|
||||
const generation = createPluginPackageResourceGenerationFromReferences({
|
||||
installationId: 'install-qinglong-compare',
|
||||
projectId: 'project-compare',
|
||||
packageName: 'qinglong',
|
||||
lockDigest: DIGEST_A,
|
||||
generation: 1,
|
||||
previousActiveLockDigest: null,
|
||||
contentDigest: DIGEST_B,
|
||||
resources: [],
|
||||
});
|
||||
return createProjectToolDefinitionSnapshot({
|
||||
projectId: 'project-compare',
|
||||
contributions: [
|
||||
{
|
||||
generation,
|
||||
revisionDigest: DIGEST_C,
|
||||
definitions: [definition],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
test('compares two ordered Project-scoped Run point reads with bounded deltas', async () => {
|
||||
const calls = [];
|
||||
const baseline = run('run-success');
|
||||
const candidate = run('run-failure', {
|
||||
taskRevision: 'task-backup@5',
|
||||
status: 'failed',
|
||||
version: 6,
|
||||
eventSequence: 12,
|
||||
createdAtMs: 2_000,
|
||||
queuedAtMs: 2_040,
|
||||
startedAtMs: 2_100,
|
||||
finishedAtMs: 2_350,
|
||||
requestId: 'must-also-not-cross-tool-output',
|
||||
});
|
||||
const output = await executeBuiltInRunCompareTool(
|
||||
repository(
|
||||
new Map([
|
||||
[baseline.id, baseline],
|
||||
[candidate.id, candidate],
|
||||
]),
|
||||
calls,
|
||||
),
|
||||
'project-compare',
|
||||
{ baselineRunId: baseline.id, candidateRunId: candidate.id },
|
||||
);
|
||||
|
||||
assert.deepEqual(calls, ['run-success', 'run-failure']);
|
||||
assert.equal(output.comparable, true);
|
||||
assert.equal(output.sameTask, true);
|
||||
assert.equal(output.sameTaskRevision, false);
|
||||
assert.deepEqual(output.changedFields, ['taskRevision', 'status']);
|
||||
assert.equal(Object.isFrozen(output.changedFields), true);
|
||||
assert.equal(output.queueDelayDeltaMs, 20);
|
||||
assert.equal(output.executionDurationDeltaMs, 150);
|
||||
assert.equal(output.totalDurationDeltaMs, 200);
|
||||
assert.equal(output.consistency, 'ordered_independent_point_reads');
|
||||
assert.equal(output.baseline.requestId, undefined);
|
||||
assert.equal(output.candidate.inputRef, undefined);
|
||||
|
||||
const registry = projectToolDefinitionRegistry(snapshot());
|
||||
assert.deepEqual(
|
||||
registry.normalizeOutput(
|
||||
BUILTIN_RUN_COMPARE_TOOL.name,
|
||||
BUILTIN_RUN_COMPARE_TOOL.version,
|
||||
output,
|
||||
),
|
||||
output,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not compare an absent or cross-Project Run and omits duration claims', async () => {
|
||||
const output = await executeBuiltInRunCompareTool(
|
||||
repository(
|
||||
new Map([
|
||||
['run-visible', run('run-visible')],
|
||||
[
|
||||
'run-foreign',
|
||||
run('run-foreign', { projectId: 'project-someone-else' }),
|
||||
],
|
||||
]),
|
||||
),
|
||||
'project-compare',
|
||||
{ baselineRunId: 'run-visible', candidateRunId: 'run-foreign' },
|
||||
);
|
||||
|
||||
assert.equal(output.baseline.found, true);
|
||||
assert.deepEqual(output.candidate, { found: false });
|
||||
assert.equal(output.comparable, false);
|
||||
assert.equal(output.sameTask, false);
|
||||
assert.equal(output.sameTaskRevision, false);
|
||||
assert.deepEqual(output.changedFields, []);
|
||||
assert.equal(output.queueDelayDeltaMs, undefined);
|
||||
assert.equal(output.executionDurationDeltaMs, undefined);
|
||||
assert.equal(output.totalDurationDeltaMs, undefined);
|
||||
|
||||
const invalidTimeline = await executeBuiltInRunCompareTool(
|
||||
repository(
|
||||
new Map([
|
||||
['run-baseline', run('run-baseline')],
|
||||
[
|
||||
'run-reversed',
|
||||
run('run-reversed', { startedAtMs: 3_000, finishedAtMs: 2_900 }),
|
||||
],
|
||||
]),
|
||||
),
|
||||
'project-compare',
|
||||
{ baselineRunId: 'run-baseline', candidateRunId: 'run-reversed' },
|
||||
);
|
||||
assert.equal(invalidTimeline.comparable, true);
|
||||
assert.equal(invalidTimeline.executionDurationDeltaMs, undefined);
|
||||
});
|
||||
|
||||
test('rejects aliases, unknown input, malformed records, and repository failure', async () => {
|
||||
const runs = repository(new Map());
|
||||
await assert.rejects(
|
||||
executeBuiltInRunCompareTool(runs, 'project-compare', {
|
||||
baselineRunId: 'same-run',
|
||||
candidateRunId: 'same-run',
|
||||
}),
|
||||
InvalidBuiltInRunCompareToolError,
|
||||
);
|
||||
await assert.rejects(
|
||||
executeBuiltInRunCompareTool(runs, 'project-compare', {
|
||||
baselineRunId: 'run-a',
|
||||
candidateRunId: 'run-b',
|
||||
injected: true,
|
||||
}),
|
||||
InvalidBuiltInRunCompareToolError,
|
||||
);
|
||||
await assert.rejects(
|
||||
executeBuiltInRunCompareTool(
|
||||
repository(
|
||||
new Map([
|
||||
['run-a', run('run-a')],
|
||||
['run-b', run('run-b', { version: -1 })],
|
||||
]),
|
||||
),
|
||||
'project-compare',
|
||||
{ baselineRunId: 'run-a', candidateRunId: 'run-b' },
|
||||
),
|
||||
BuiltInRunCompareToolUnavailableError,
|
||||
);
|
||||
await assert.rejects(
|
||||
executeBuiltInRunCompareTool(
|
||||
{
|
||||
async findRunById() {
|
||||
throw new Error('database DSN must not escape');
|
||||
},
|
||||
},
|
||||
'project-compare',
|
||||
{ baselineRunId: 'run-a', candidateRunId: 'run-b' },
|
||||
),
|
||||
BuiltInRunCompareToolUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('binds the exact reviewed definition to a retry-safe database-read adapter', async () => {
|
||||
const currentSnapshot = snapshot();
|
||||
const binding = createBuiltInRunCompareToolHandlerBinding(currentSnapshot, [
|
||||
'edge',
|
||||
'standalone',
|
||||
'cluster-control',
|
||||
]);
|
||||
assert.deepEqual(binding.tool, BUILTIN_RUN_COMPARE_TOOL);
|
||||
assert.deepEqual(binding.adapter, BUILTIN_RUN_COMPARE_ADAPTER);
|
||||
assert.deepEqual(binding.authorities, ['database.read']);
|
||||
|
||||
const definitions = projectToolDefinitionRegistry(currentSnapshot);
|
||||
const adapter = new BuiltInRunCompareToolAdapter(
|
||||
binding,
|
||||
'edge',
|
||||
definitions,
|
||||
repository(
|
||||
new Map([
|
||||
['run-a', run('run-a')],
|
||||
['run-b', run('run-b', { status: 'failed' })],
|
||||
]),
|
||||
),
|
||||
);
|
||||
assert.equal(adapter.recoveryMode, 'retry_safe_read');
|
||||
const output = await adapter.execute(
|
||||
{ projectId: 'project-compare' },
|
||||
{ baselineRunId: 'run-a', candidateRunId: 'run-b' },
|
||||
);
|
||||
assert.deepEqual(output.changedFields, ['status']);
|
||||
|
||||
const changed = {
|
||||
...BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
|
||||
description: 'unreviewed changed definition',
|
||||
};
|
||||
assert.throws(
|
||||
() =>
|
||||
createBuiltInRunCompareToolHandlerBinding(snapshot(changed), ['edge']),
|
||||
InvalidBuiltInRunCompareToolError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
new BuiltInRunCompareToolAdapter(
|
||||
{ ...binding, authorities: ['database.read', 'network.client'] },
|
||||
'edge',
|
||||
definitions,
|
||||
repository(new Map()),
|
||||
),
|
||||
/handler authority is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('publishes only explicit compare subpaths and keeps runtime-core root unchanged', () => {
|
||||
const tool = require('@qinglong/runtime-core/builtin-run-compare-tool');
|
||||
const projection = require('@qinglong/runtime-core/builtin-run-compare-projection');
|
||||
const root = require('@qinglong/runtime-core');
|
||||
|
||||
assert.equal(tool.BUILTIN_RUN_COMPARE_TOOL.name, 'qinglong.run.compare');
|
||||
assert.equal(
|
||||
projection.BUILTIN_RUN_COMPARE_TOOL.name,
|
||||
'qinglong.run.compare',
|
||||
);
|
||||
assert.equal(root.BUILTIN_RUN_COMPARE_TOOL, undefined);
|
||||
assert.equal(root.executeBuiltInRunCompareTool, undefined);
|
||||
});
|
||||
@@ -299,10 +299,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
||||
rootSourceFileRoles: runtimeCore.rootSourceFileRoles,
|
||||
},
|
||||
{
|
||||
sourceFiles: 160,
|
||||
sourceFiles: 162,
|
||||
rootSourceFiles: 1,
|
||||
rootSourceLines: 160,
|
||||
nestedSourceFiles: 159,
|
||||
nestedSourceFiles: 161,
|
||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user