DSH / Atlas
2026-09-20implementedfeature

Config schema dump validation policy

配置 schema dump 的验证策略

`dsh --profile <name> --dump-config-schema` projects native Schemastery Config declarations into JSON Schema without mounting plugins. Native validation mutates its input, evaluates `!!js` expressions, and runs transform callbacks; a static document cannot reproduce all of that. Three choices were contested in review and would be reopened without a record: what `complete` and the exit code mean when part of the proje

English

Problem

dsh --profile <name> --dump-config-schema projects native Schemastery Config declarations into JSON Schema without mounting plugins. Native validation mutates its input, evaluates !!js expressions, and runs transform callbacks; a static document cannot reproduce all of that. Three choices were contested in review and would be reopened without a record: what complete and the exit code mean when part of the projection is approximate, which rows the collector inspects, and how the projection treats native behavior it cannot model.

Decision

  • Partial projection exits 1. x-cordis.complete is false and the CLI exits 1 for any error diagnostic, any partial, unsupported, or error entry, or one plugin name resolving to several Config definitions, even when stdout holds a valid and useful schema. The exit code answers whether the document can replace native validation, not whether output was produced. Callers that only need the schema read stdout and ignore the exit code; the diagnostics under x-cordis name each limitation at every entry it affects.
  • Discovery is declaration-oriented and includes disabled rows. The collector walks every row the Loader would receive, including rows disabled literally or by expression, and imports their modules. A disabled row's import or include failure therefore makes a bootable profile incomplete. The one exception follows the Loader: a disabled group or include without group: true never creates its children, so a missing carrier config is recorded as a childless tree and the entry schema does not validate its child declarations.
  • Widen instead of simulating native mutation. Where native resolution can alter the input before later validation reads it (adapted values written back by a container, renamed dictionary keys, loose fallbacks, lazy metadata propagation into shared nodes), the projection keeps the available declaration detail beside an unrestricted alternative and records a limitation. It never emulates the mutation. Widening is scoped to the mechanism: only containers write adapted values back, so a bare primitive transform as an earlier union branch keeps anyOf.

The CLI reference documents the resulting behavior; app-boot owns the collector API.

Alternatives considered

Exit 0 with a valid document and warnings only. Rejected: a consumer feeding the schema to an editor or agent would treat an approximate document as authoritative. A nonzero exit forces the caller to decide explicitly.

Skip disabled rows during discovery. Rejected: a disabled row is a declaration the user is likely to re-enable, and its schema, id target, and import failures are what an editing agent needs. Skipping would also change configRef targets whenever disabled toggles.

A relaxed child-list variant for ancestor-disabled children. Rejected for now: it needs a parallel entryList and entry rule set with requiredness removed. Children under a disabled group: true row and patch insertions into a disabled group are validated as enabled, and the reference says so.

Emulating native mutation in the projection. Rejected: transform callbacks are plugin code the dump must not execute, and reproducing writeback order would couple the projector to Schemastery internals. A widening with a limitation is cheaper to maintain and cannot reject inputs that native validation accepts.

Consequences

A shipped profile whose rows use an unsupported carrier reports complete: false and exits 1 while still emitting usable schemas, so consumers must read x-cordis.diagnostics to distinguish an approximate document from an unusable one. Every widening is paired with a limitation diagnostic, and generated schemas validate the --dump-config output of every shipped profile.

中文

Problem

dsh --profile <name> --dump-config-schema 在不挂载插件的情况下,把原生 Schemastery Config 声明投影为 JSON Schema。原生验证会修改输入、求值 !!js 表达式并执行 transform 回调;静态文档无法完整重现这些行为。评审中有三项选择存在争议,若不记录会被反复重提:投影部分近似时 complete 和退出码的含义、收集器检查哪些行,以及投影如何处理无法建模的原生行为。

Decision

  • 部分投影以 1 退出。 只要存在错误诊断、任何 partial、unsupported 或 error 条目,或同一插件名解析出多个 Config 定义,x-cordis.complete 即为 false 且 CLI 以 1 退出,即使 stdout 已包含有效且有用的 schema。退出码回答的是该文档能否替代原生验证,而不是是否产生了输出。只需要 schema 的调用方读取 stdout 并忽略退出码;x-cordis 下的诊断会在受影响的每个条目上说明各项限制。
  • 发现面向声明,并包含禁用行。 收集器遍历 Loader 将接收的每一行,包括字面量禁用或表达式禁用的行,并导入它们的模块。因此禁用行的导入或 include 失败会使可启动的 profile 被标为不完整。唯一的例外遵循 Loader:未设置 group: true 的禁用 group 或 include 从不创建其子项,所以缺失的承载 config 记录为没有子项的树,entry schema 也不验证其子声明。
  • 放宽而不是模拟原生修改。 当原生解析可能在后续验证读取前改变输入(容器写回适配值、字典键改名、loose 回退、lazy 元数据传播到共享节点)时,投影保留可获得的声明细节,同时并列一个不受限制的备选项,并记录一条限制。它绝不模拟该修改。放宽的范围限定于该机制:只有容器会写回适配值,因此作为前序 union 分支的裸原始类型 transform 保留 anyOf。

CLI 参考记录了由此产生的行为;app-boot 负责收集器 API。

Alternatives considered

输出有效文档并仅给出警告,以 0 退出。 被否决:把 schema 交给编辑器或 agent 的消费者会把近似文档当作权威。非零退出迫使调用方显式决定。

发现时跳过禁用行。 被否决:禁用行是用户很可能重新启用的声明,其 schema、id 目标和导入失败正是编辑 agent 所需。跳过还会导致 configRef 目标随 disabled 切换而变化。

为祖先被禁用的子项提供放宽的子列表变体。 暂被否决:它需要一套去掉必填约束的并行 entryList 和 entry 规则。禁用的 group: true 行之下的子项以及插入到禁用 group 的 patch 按启用状态验证,参考文档已如此说明。

在投影中模拟原生修改。 被否决:transform 回调是 dump 不得执行的插件代码,而重现写回顺序会把投影器与 Schemastery 内部实现耦合。带限制说明的放宽维护成本更低,也不会拒绝原生验证接受的输入。

Consequences

使用不受支持承载插件的已发布 profile 会报告 complete: false 并以 1 退出,同时仍输出可用的 schema,因此消费者必须读取 x-cordis.diagnostics 来区分近似文档与不可用文档。每次放宽都配有一条限制诊断,生成的 schema 能验证每个已发布 profile 的 --dump-config 输出。