diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index b938b34..b6f3522 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -148,7 +148,7 @@ | directive 解析指令 | `terminal` | stage 阶段 | `layers` | run 执行 | `square-terminal` | | copy 复制 | `copy` | setting 设置 | `sliders-horizontal` | label 标签 | `tag` | | health 健康检查 | `activity` | start 启动 | `play` | trait 特质 | `network` | - | impl 实现 | `square-function` | | | | | + | impl 实现 | `square-function` | module 模块 | `layers` | | | 库规则的种类用规则给的图标(`KindBadge.icon`),跟在语言的种类图标后面:SQLAlchemy 数据表 `database`、数据类 `package`、Pydantic 数据模型 `braces`(和类型块同一个图标,颜色不同)、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`、React 组件 `atom`、副作用 `zap`、事件处理 `mouse-pointer-click`、serde 可序列化 `braces`、thiserror 错误 `triangle-alert`、axum 接口 `globe`、sqlx 查询 `database`、tauri 桌面命令 `square-function`。数据类写了 `frozen=True` 再加一把锁(`lock`,悬停「不可修改」)。实现块再由规则 `rust.impl` 换成卡片,左边因此有两个「实现」图标。 @@ -574,7 +574,7 @@ ```ts export type BlockKind = | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable' - | 'trait' | 'impl' // Rust:特质、实现(属性表) + | 'trait' | 'impl' | 'module' // Rust:特质、实现、模块(属性表) | 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment' | 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' // shell:解释器、选项(属性表),分情况、命令、管道(一行译文) | 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start' // Dockerfile:解析指令、阶段、健康检查、启动(属性表),执行、复制、设置、标签(一行译文) @@ -1748,8 +1748,11 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 - `enum` → enum。变体表「变体 | 携带的数据」:没带数据是「—」,`Failed(String)` 写类型,`Running { pid: u32 }` 写字段原文。 - `fn` → function。有才显示:说明、异步(只有 `async fn` 写「是」)、可见性、输入参数表「名称 | 类型」、返回(没写是 `()`)。函数体里直接写的 `match` 收成「分情况」表「分支 | 做什么」,N 支:优先用这一支里的注释,否则是 `return` 那句或这一支的第一句,不展开成子块。 - `trait` → trait(图标 `network`,颜色同声明关键字的蓝)。方法表「名称 | 参数 | 返回」,只列签名,不展开函数体。 -- `impl` → impl(图标 `square-function`,颜色同函数)。标题是「某类型 的方法」(`impl Trait for Type` 取 Type)。方法是子块。不看导入的规则 `rules/rustImpl.ts`(`libraries: []`,种类 `rust.impl`)把每张方法卡放进方法表那一行,点方法名展开,和 React 事件处理同一条路。方法卡的参数表叫「参数」。 -- 其余(`let`、`const`、宏调用)→ statement。`//` 注释单独成块。 +- `impl` → impl(图标 `square-function`,颜色同函数)。标题是「某类型 的方法」(`impl Trait for Type` 取 Type)。方法是子块。不看导入的规则 `rules/rustImpl.ts`(`libraries: []`,种类 `rust.impl`)把每张方法卡放进方法表那一行,点方法名展开,和 React 事件处理同一条路。方法卡的参数表叫「参数」。身体里的 `const`、`static` 不收成语句,跟文件顶层同一条路(见下)。 +- `mod 名字 { … }`(`mod_item` 有 `body`)→ module(中文「模块」,图标 `layers`,颜色同阶段)。卡片行:名称、说明(有文档注释才有)、可见性(没写也是本模块,跟结构体一样总有这一行)。身体用和文件顶层同一套规则拆成子块,排在这张卡片下面,不另做一张表把子块塞进某一行。模块自己的文档注释留在模块卡片上,身体里每项自己的文档注释留在那一项上。`mod 名字;` 没有身体,不拆,仍是一条 statement。 +- `const`(`const_item`)→ const,`static`(`static_item`)→ variable。属性表不另写一套译文,走中文模板里常量 / 变量那一套:名称、说明、访问级别(公开才有,模板里叫「公共」)、类型、值(`static` 的值叫「初始值」)。文件顶层、模块身体、impl 身体同一条路。 +- `extern "…" { … }`(`foreign_mod_item`)不单独成一块源码。里面的函数签名(`function_signature_item`,没有函数体)是函数卡片,跟普通函数一样有名称、说明、可见性、参数、返回,作为所在这一层(模块、文件顶层或 impl)的子块。 +- 其余(`let`、宏调用)→ statement。不拆函数体。`//` 注释单独成块。模块身体里宏调用后面多出来的那个空 `;`(`empty_statement`)不成块。 ### 属性表信息(facts)从哪来 @@ -1765,7 +1768,7 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令 - Shell:解释器取 `#!` 后面的整句,第一个词是 `env` 的是按 PATH 找、程序名是它后面那个词,否则是写死的路径、程序名是路径最后一节。选项:每个开关一项,单字母换成 bash 的长名字(e → errexit、u → nounset、x → xtrace……),`-o 名字` 照写,`-` 开、`+` 关。变量:`variable_assignment` 的 name / value,声明里没给值的只有 name。函数:name 是函数名,doc 是紧贴在上面的注释;`positional` 是主体里(不进嵌套的函数)用到的 `$N` / `${N}` / `${N:-…}`,照位置排,`local x="$N"`、`x=$N` 给它起名字;`prints` 是没重定向到别处的 `echo` / `printf`(进 if、循环、分情况,不进 `$(…)`、管道、嵌套的函数);`captured` 看同一文件里有没有 `$(函数名 …)`;`returns` 是主体里的 `return` 命令。 - Dockerfile:解析指令是每行注释 `# 名字=值` 的名字(小写)和值;阶段取 `image_spec` 的 name / tag / digest 字段、`as` 字段(image_alias),stage 看镜像名是不是前面某个阶段的名字(不分大小写),选项是 `param`、`mount_param` 节点的原文 `--名字=值`(值在语法树里没有单独的节点,按原文拆,记下值从哪开始);变量是 `env_pair` 的 name / value、`arg_instruction` 的 name / default;健康检查的 none 看有没有 NONE,检查命令是里面的 `cmd_instruction`;数组写法取 `json_string_array`,shell 写法取 `shell_command` 里各个 `shell_fragment`(`line_continuation` 不算),只有一个 `< = { class: 'box', trait: 'network', impl: 'square-function', + module: 'layers', type: 'braces', enum: 'list-ordered', exception: 'triangle-alert', diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index 0808462..bff2019 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -109,4 +109,74 @@ describe('rust 语言卡片', () => { items: { changed: { 1: { added: true } }, removed: [] }, }) }) + + it('带身体的模块拆成子块,常量和外部函数是卡片', async () => { + const source = [ + '/// 平台相关。', + 'pub mod platform {', + ' /// 读入。', + ' use std::io::Read;', + ' /// 上限。', + ' pub const LIMIT: u32 = 8;', + ' static NAME: &str = "a";', + ' pub fn open() -> u32 { LIMIT }', + ' extern "C" {', + ' /// 外部。', + ' pub fn bar(x: i32) -> i32;', + ' }', + ' let local = 1;', + ' println!("hi");', + '}', + 'mod other;', + '', + 'impl Agent {', + ' pub const N: u8 = 2;', + '}', + '', + ].join('\n') + const list = await views(source) + const mod = list.find(block => block.kind === 'module') + expect(mod?.kind).toBe('module') + expect(list.some(block => block.kind === 'statement' && block.code.includes('const LIMIT'))).toBe(false) + expect(list.filter(block => block.kind === 'statement').map(block => block.code)).toEqual(['mod other;']) + expect(mod?.children.map(block => block.kind)).toEqual(['import', 'const', 'variable', 'function', 'function', 'statement', 'statement']) + expect(viewLines(mod ? [mod] : [])).toEqual([ + 'module 1', + ' 名称: platform', + ' 说明: 平台相关。', + ' 可见性: 公开', + ' import 3', + ' 来源: std::io', + ' 导入项: 1 个', + ' Read | —', + ' const 5', + ' 名称: LIMIT', + ' 说明: 上限。', + ' 访问级别: 公共', + ' 类型: u32', + ' 值: 8', + ' variable 7', + ' 名称: NAME', + ' 类型: &str', + ' 初始值: "a"', + ' function 8', + ' 名称: open', + ' 可见性: 公开', + ' 返回: u32', + ' function 10', + ' 名称: bar', + ' 说明: 外部。', + ' 可见性: 公开', + ' 输入参数: 1 个', + ' x | i32', + ' 返回: i32', + ' statement 13: 令 local = 1;', + ' statement 14: println!("hi")', + ]) + const impl = list.find(block => block.kind === 'rust.impl') + const inner = impl?.children.find(block => block.kind === 'const') + expect(inner?.kind).toBe('const') + expect(viewLines(inner ? [inner] : []).join('\n')).toContain('名称: N') + expect(viewLines(inner ? [inner] : []).join('\n')).toMatch(/类型: u8|值: 2/) + }) }) diff --git a/vscode/src/codeReader/languages/rust.ts b/vscode/src/codeReader/languages/rust.ts index 61c3887..b3f1f35 100644 --- a/vscode/src/codeReader/languages/rust.ts +++ b/vscode/src/codeReader/languages/rust.ts @@ -1,4 +1,4 @@ -import type { Access, ClassBlock, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, TokenRole, TraitBlock } from '../contract' +import type { Access, ClassBlock, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, ModuleBlock, Node, ParameterFacts, TokenRole, TraitBlock, VariableBlock } from '../contract' import * as path from 'node:path' import { leadingAttributes, visibilityFacts } from '../rules/rustTree' import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, named } from './shared' @@ -15,26 +15,35 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter { } } +/** extern 块本身不成一块源码,里面的签名跟所在这一层的别的项走同一条路。 */ +function unwrapExtern(item: Node): Node[] { + if (item.type !== 'foreign_mod_item') + return [item] + const body = item.childForFieldName('body') + return body ? named(body).flatMap(unwrapExtern) : [] +} + function topBlocks(items: Node[]): CodeBlock[] { + const list = items.flatMap(unwrapExtern) const blocks: CodeBlock[] = [] let index = 0 - while (index < items.length) { - const item = items[index] + while (index < list.length) { + const item = list[index] if (isDoc(item)) { const docs = [item] let next = index + 1 - while (next < items.length && isDoc(items[next]) && items[next].startPosition.row <= docs[docs.length - 1].endPosition.row + 1) - docs.push(items[next++]) - while (next < items.length && items[next].type === 'attribute_item') + while (next < list.length && isDoc(list[next]) && list[next].startPosition.row <= docs[docs.length - 1].endPosition.row + 1) + docs.push(list[next++]) + while (next < list.length && list[next].type === 'attribute_item') next++ - const target = items[next] - if (target && target.type !== 'line_comment' && target.type !== 'block_comment') { + const target = list[next] + if (target && target.type !== 'line_comment' && target.type !== 'block_comment' && target.type !== 'empty_statement') { blocks.push(withPreamble(itemBlock(target), docs)) index = next + 1 continue } } - if (item.type === 'attribute_item') { + if (item.type === 'attribute_item' || item.type === 'empty_statement') { index++ continue } @@ -59,13 +68,50 @@ function itemBlock(item: Node): CodeBlock { return implBlock(item) case 'trait_item': return traitBlock(item) + case 'mod_item': + return moduleBlock(item) + case 'const_item': + return valueBlock(item, 'const') + case 'static_item': + return valueBlock(item, 'variable') case 'function_item': + case 'function_signature_item': return functionBlock(item) default: return leafBlock('statement', item) } } +/** 没有身体的 `mod 名字;` 不拆。有身体的模块,身体和文件顶层同一套规则。 */ +function moduleBlock(node: Node): CodeBlock { + const body = node.childForFieldName('body') + if (!body) + return leafBlock('statement', node) + const { access, attributes } = visibilityFacts(node) + return { + kind: 'module', + ...compoundShape(node, body.startIndex, named(body), items => topBlocks(items)), + ...(attributes && { attributes }), + facts: classFacts(node, access), + } +} + +/** const 是常量,static 是变量;名称、类型、值交给现成的常量 / 变量属性表。 */ +function valueBlock(node: Node, kind: 'const' | 'variable'): CodeBlock { + const name = node.childForFieldName('name') + if (!name) + return leafBlock('statement', node) + const type = node.childForFieldName('type') ?? undefined + const value = node.childForFieldName('value') ?? undefined + const { access, attributes } = visibilityFacts(node) + return { + kind, + ...leafShape(node), + ...(attributes && { attributes }), + facts: { access, declarators: [{ name, type, value }] }, + } +} + /** 文档注释和写在前面的属性算进头部。文档注释的第一句是说明。 */ function withPreamble(block: CodeBlock, docs: Node[]): CodeBlock { const [doc] = docs @@ -79,8 +125,8 @@ function withPreamble(block: CodeBlock, docs: Node[]): CodeBlock { return next } -function isDocumentedKind(block: CodeBlock): block is FunctionBlock | ClassBlock | TraitBlock | ImplBlock | EnumBlock { - return block.kind === 'function' || block.kind === 'class' || block.kind === 'trait' || block.kind === 'impl' || block.kind === 'enum' +function isDocumentedKind(block: CodeBlock): block is FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | VariableBlock { + return block.kind === 'function' || block.kind === 'class' || block.kind === 'trait' || block.kind === 'impl' || block.kind === 'module' || block.kind === 'enum' || block.kind === 'const' || block.kind === 'variable' } function isDoc(node: Node): boolean { @@ -178,21 +224,7 @@ function traitBlock(node: Node): TraitBlock { function implBlock(node: Node): ImplBlock { const { access, attributes } = visibilityFacts(node) const body = node.childForFieldName('body') - const children: CodeBlock[] = [] - let docs: Node[] = [] - for (const child of named(body)) { - if (isDoc(child)) { - docs.push(child) - continue - } - if (child.type === 'attribute_item') - continue - if (child.type === 'function_item') - children.push(withPreamble(functionBlock(child), docs)) - else if (child.type === 'const_item' || child.type === 'type_item') - children.push(leafBlock('statement', child)) - docs = [] - } + const children = body ? topBlocks(named(body)) : [] return { kind: 'impl', ...compoundShape(node, body?.startIndex ?? node.endIndex, [], () => children), diff --git a/vscode/src/codeReader/languages/shared.ts b/vscode/src/codeReader/languages/shared.ts index 2335b45..c61fa93 100644 --- a/vscode/src/codeReader/languages/shared.ts +++ b/vscode/src/codeReader/languages/shared.ts @@ -211,6 +211,7 @@ function ownDoc(block: CodeBlock): Node | undefined { case 'class': case 'trait': case 'impl': + case 'module': case 'enum': case 'exception': case 'const': diff --git a/vscode/src/codeReader/rules/rustRows.ts b/vscode/src/codeReader/rules/rustRows.ts index 40e8c07..ef2b990 100644 --- a/vscode/src/codeReader/rules/rustRows.ts +++ b/vscode/src/codeReader/rules/rustRows.ts @@ -1,4 +1,4 @@ -import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, PropertyItem, PropertyRow, TokenRole, TraitBlock } from '../contract' +import type { ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, TokenRole, TraitBlock } from '../contract' import type { RustVisibility } from './rustTree' import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree' @@ -64,6 +64,14 @@ export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] { ] } +export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] { + return [ + nameRow(block.facts.name, say, 'variable'), + ...docRow(block.facts.doc), + visibilityRow(block), + ] +} + export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] { return [ nameRow(block.facts.name, say, 'type'), diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts index 3b9ca65..0b89988 100644 --- a/vscode/src/codeReader/translators/rustZh.ts +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -1,66 +1,68 @@ import type { FactBlock, Node, PropertyRow, SourceDocument, Translator } from '../contract' -import { enumRows, functionRows, implRows, importRows, plainSay, structRows, traitRows } from '../rules/rustRows' +import { enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows' import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate' const say = plainSay() +const template = createZhTemplateTranslator({ + id: 'rust.zh-template', + label: '中文直译', + languageIds: ['rust'], + keywords: { + fn: '函数', + let: '令', + mut: '可变', + pub: '公开', + struct: '结构体', + enum: '枚举', + impl: '实现', + trait: '特质', + use: '导入', + mod: '模块', + match: '匹配', + if: '如果', + else: '否则', + for: '遍历', + while: '当', + loop: '循环', + return: '返回', + async: '异步', + await: '等待', + break: '跳出', + continue: '继续', + where: '其中', + type: '类型', + const: '常量', + static: '静态', + unsafe: '不安全', + as: '作为', + in: '在', + ref: '引用', + move: '移动', + dyn: '动态', + crate: '本 crate', + super: '父模块', + self: '自己', + Self: '本类型', + }, + literals: { true: '真', false: '假' }, + builtinTypes: {}, + isTypePosition: () => false, + operators: {}, + operatorParents: [], + emptyLiterals: {}, + mapLiterals: {}, + stringNodes: ['string_literal'], + commentText, + nodeTemplates: {}, +}) + /** * Rust 的中文直译。属性表的用词按代码阅读的 Rust 原型:结构体、可见性、变体、分情况。 - * 关键字和注释仍走中文直译那一套。 + * 关键字和注释仍走中文直译那一套。常量和变量的行不另写一套,走模板里现成的那套。 */ export const rustZhTranslator: Translator = { - ...createZhTemplateTranslator({ - id: 'rust.zh-template', - label: '中文直译', - languageIds: ['rust'], - keywords: { - fn: '函数', - let: '令', - mut: '可变', - pub: '公开', - struct: '结构体', - enum: '枚举', - impl: '实现', - trait: '特质', - use: '导入', - mod: '模块', - match: '匹配', - if: '如果', - else: '否则', - for: '遍历', - while: '当', - loop: '循环', - return: '返回', - async: '异步', - await: '等待', - break: '跳出', - continue: '继续', - where: '其中', - type: '类型', - const: '常量', - static: '静态', - unsafe: '不安全', - as: '作为', - in: '在', - ref: '引用', - move: '移动', - dyn: '动态', - crate: '本 crate', - super: '父模块', - self: '自己', - Self: '本类型', - }, - literals: { true: '真', false: '假' }, - builtinTypes: {}, - isTypePosition: () => false, - operators: {}, - operatorParents: [], - emptyLiterals: {}, - mapLiterals: {}, - stringNodes: ['string_literal'], - commentText, - nodeTemplates: {}, - }), + ...template, attributeNames: { attributes: { ...ZH_ATTRIBUTE_NAMES.attributes, access: '可见性' }, values: { @@ -78,7 +80,7 @@ export const rustZhTranslator: Translator = { describe, } -function describe(block: FactBlock, _document: SourceDocument): PropertyRow[] { +function describe(block: FactBlock, document: SourceDocument): PropertyRow[] { switch (block.kind) { case 'import': return importRows(block, say) @@ -92,6 +94,11 @@ function describe(block: FactBlock, _document: SourceDocument): PropertyRow[] { return traitRows(block, say) case 'impl': return implRows(block, say) + case 'module': + return moduleRows(block, say) + case 'const': + case 'variable': + return template.describe(block, document) default: return [] } diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index edb2d77..f25840e 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -214,6 +214,7 @@ export const KIND_ZH: Record = { class: '类', trait: '特质', impl: '实现', + module: '模块', enum: '枚举', exception: '异常', type: '类型', @@ -708,6 +709,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator { case 'class': case 'trait': case 'impl': + case 'module': return classRows(block.facts, phrases) case 'enum': return enumRows(block.facts, phrases) diff --git a/vscode/webview-ui/src/codeReader/codeReader.css b/vscode/webview-ui/src/codeReader/codeReader.css index 82765a5..d862325 100644 --- a/vscode/webview-ui/src/codeReader/codeReader.css +++ b/vscode/webview-ui/src/codeReader/codeReader.css @@ -74,7 +74,7 @@ body.vscode-high-contrast-light { [data-kind="exception"] { --kind: var(--cr-kind-exception); } :is([data-kind="field"], [data-kind="const"], [data-kind="variable"], [data-kind="label"]) { --kind: var(--cr-kind-data); } :is([data-kind="if"], [data-kind="else"], [data-kind="case"]) { --kind: var(--cr-kind-branch); } -[data-kind="stage"] { --kind: var(--cr-kind-stage); } +:is([data-kind="stage"], [data-kind="module"]) { --kind: var(--cr-kind-stage); } [data-kind="loop"] { --kind: var(--cr-kind-loop); } :is([data-kind="try"], [data-kind="catch"], [data-kind="finally"], [data-kind="health"]) { --kind: var(--cr-kind-error); } :is([data-kind="return"], [data-kind="start"]) { --kind: var(--cr-kind-return); } diff --git a/vscode/webview-ui/src/codeReader/kindIcons.test.tsx b/vscode/webview-ui/src/codeReader/kindIcons.test.tsx index 54a1e99..81ab434 100644 --- a/vscode/webview-ui/src/codeReader/kindIcons.test.tsx +++ b/vscode/webview-ui/src/codeReader/kindIcons.test.tsx @@ -23,6 +23,7 @@ describe('块的种类图标', () => { class: 'box', trait: 'network', impl: 'square-function', + module: 'layers', type: 'braces', enum: 'list-ordered', exception: 'triangle-alert', diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 4dec0ee..c71e033 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -4,7 +4,7 @@ export type BlockKind = | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable' - | 'trait' | 'impl' + | 'trait' | 'impl' | 'module' | 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment' | 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' | 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start'