feat(loader): interpolate the entry disabled field
The Windows platform layer disables tool-bash and inserts the pwsh stack, but the shipped presets each mount a tool-bash row that re-enabled the tool on win32 — the session had both a PowerShell-backed bash tool and tool-pwsh, silently, because no spec pinned the composed preset layer. The Loader now evaluates a disabled: !!js expression against the loader context at every mount decision; disabled is the only interpolated metadata field, and the raw node stays in the options so write-back keeps the !!js form. The standard/code/cordis presets gate tool-bash with process.platform === 'win32', verify-cordis-config allows expressions in disabled only, and the windows-shell spec pins the preset-level invariant.
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-11-loader-entry-disabled-interpolation.md
|
||||
2026-08-11-loader-entry-disabled-interpolation.md: d2ddbc7d00c8f3e493912f826ee573f355b3b71d
|
||||
2026-08-11-loader-entry-disabled-interpolation.zh.md: cc139202e4ea79dfbae1c89cef5d7eda4ab2011f
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: Loader interpolates the entry `disabled` field
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-11-loader-entry-disabled-interpolation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Windows platform layer (`packages/bundle/base/windows.cordis.patch.yml`) disables `tool-bash` on win32, but the shipped presets each mount a `tool-bash` row. Preset rows compose last, so the same-id row re-enabled the tool on Windows — the session had both `tool-bash` (PowerShell-backed) and `tool-pwsh`, silently, because no spec pinned the composed preset layer. Entry metadata had no conditional mechanism: `!!js` interpolates only under plugin `config`, and [postmortem 0002](../../../../docs/postmortem/0002-js-expression-disabled-filesystem-tools.md) documents that `disabled: !!js ...` stays a truthy expression object, disabling the row everywhere.
|
||||
|
||||
## Decision
|
||||
|
||||
The Loader interpolates the entry `disabled` field (`vendor/loader/src/config/entry.ts`): a `!!js` expression evaluates against the loader context at every mount decision. `disabled` is the only interpolated metadata field; `id`, `name`, `group`, and `inject` stay static. The raw node stays in the options, so write-back keeps the `!!js` form. The shipped presets (standard, code, cordis) gate `tool-bash` with `disabled: !!js process.platform === 'win32'`, and `verify-cordis-config` now allows expressions in `disabled` only.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A declarative `platform` field on the row.** Static and gate-checkable, but a second composition mechanism beside `!!js`, and platform is only today's condition.
|
||||
|
||||
**Preset-level platform overlays.** Rejected: the condition belongs on the row it governs.
|
||||
|
||||
## Consequences
|
||||
|
||||
A row can gate itself on platform or environment; a bad expression fails loud at boot. Every other metadata field remains literal and the gate keeps rejecting expressions there — the postmortem-0002 hazard is closed for `disabled` by evaluation, not prohibition. The `minimal` preset's missing win32 PTY stack is a preset-metadata follow-up.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Agent Note:Loader 插值条目 `disabled` 字段
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-11-loader-entry-disabled-interpolation.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Windows 平台层(`packages/bundle/base/windows.cordis.patch.yml`)在 win32 上禁用 `tool-bash`,但 shipped 预设各自挂载了一行 `tool-bash`。预设行最后组合,同名行在 Windows 上重新启用了该工具——会话同时拥有 `tool-bash`(PowerShell 后端)与 `tool-pwsh`,且是静默的,因为没有 spec pin 组合后的预设层。条目元数据没有条件机制:`!!js` 只在插件 `config` 下插值,[postmortem 0002](../../../../docs/postmortem/0002-js-expression-disabled-filesystem-tools.md) 记录了 `disabled: !!js ...` 保持真值表达式对象、在所有平台上禁用该行的事故。
|
||||
|
||||
## 决策
|
||||
|
||||
Loader 插值条目 `disabled` 字段(`vendor/loader/src/config/entry.ts`):`!!js` 表达式在每次挂载决策时基于 loader 上下文求值。`disabled` 是唯一被插值的元数据字段;`id`、`name`、`group`、`inject` 保持静态。原始节点保留在 options 中,写回保持 `!!js` 形式。shipped 预设(standard、code、cordis)用 `disabled: !!js process.platform === 'win32'` 门控 `tool-bash`,`verify-cordis-config` 现在只允许 `disabled` 中的表达式。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**行上的声明式 `platform` 字段。** 静态且可被门禁检查,但它是 `!!js` 之外的第二种组合机制,且平台只是今天的条件。
|
||||
|
||||
**预设级平台 overlay。** 被否:条件应当属于它所治理的行。
|
||||
|
||||
## 后果
|
||||
|
||||
行可以按平台或环境门控自身;错误的表达式在启动时响亮失败。其余元数据字段保持字面值,门禁继续拒绝那里的表达式——`disabled` 上的 postmortem-0002 隐患以「求值」而非「禁止」关闭。`minimal` 预设缺失的 win32 PTY 栈是预设元数据的后续工作。
|
||||
@@ -92,7 +92,7 @@ Run checks before pushes via [dsh-pre-push-checks](.agents/skills/dsh-pre-push-c
|
||||
|
||||
## Secrets / .env
|
||||
|
||||
Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and root `.env`. cordis.yml allows `!!js` (never `!js`) only under plugin `config`; Loader metadata is static, so conditional composition uses overlays ([primer](docs/cordis-primer.md#loader-configuration)). Never commit credentials. CI e2e skips without a key; [testing.md](docs/testing.md) owns key policy.
|
||||
Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and root `.env`. cordis.yml allows `!!js` (never `!js`) under plugin `config` and entry `disabled`; other metadata stays literal, so conditional composition also uses overlays ([primer](docs/cordis-primer.md#loader-configuration)). Never commit credentials. CI e2e skips without a key; [testing.md](docs/testing.md) owns key policy.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -46,10 +46,12 @@
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# from here; the executor behind it is host-plane too, where the sandbox policy
|
||||
# owns it.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
# POSIX-only: the Windows platform layer swaps the bash stack for the pwsh stack.
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -40,10 +40,12 @@
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# from here; the executor behind it is host-plane too, where the sandbox policy
|
||||
# owns it.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
# POSIX-only: the Windows platform layer swaps the bash stack for the pwsh stack.
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# from here; the executor behind it is host-plane too, where the sandbox policy
|
||||
# owns it.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
# POSIX-only: the Windows platform layer swaps the bash stack for the pwsh stack.
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
@@ -137,3 +140,38 @@ describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
expect(baseWarnings).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('shipped agent presets keep tool-bash off the win32 roster', () => {
|
||||
const presetRoot = resolve(fileURLToPath(new URL('../package.json', import.meta.url)), '..', 'config', 'agent-presets')
|
||||
|
||||
it.each(['standard', 'code', 'cordis'])('preset %s gates its tool-bash row by platform', (preset) => {
|
||||
const entries: unknown = yaml.load(
|
||||
readFileSync(join(presetRoot, preset, 'agent.cordis.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
)
|
||||
if (!Array.isArray(entries)) throw new TypeError(`preset ${preset} must parse to an entry array`)
|
||||
const row = entries.find((entry): entry is Record<string, unknown> => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === 'tool-bash'
|
||||
))
|
||||
if (row === undefined) throw new TypeError(`preset ${preset} must mount tool-bash`)
|
||||
expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string })
|
||||
// The platform patch disables the host's tool-bash row on win32; the
|
||||
// preset row must not re-enable it there. Evaluate the shipped expression
|
||||
// with a platform-scoped context (the `with` scope shadows the global
|
||||
// `process`) so both outcomes pin on every host.
|
||||
const expression = (row.disabled as { __jsExpr: string }).__jsExpr
|
||||
expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression))).toBe(true)
|
||||
expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression))).toBe(false)
|
||||
})
|
||||
|
||||
it('minimal mounts no tool-bash row at all (its shell is the PTY stack)', () => {
|
||||
const entries: unknown = yaml.load(
|
||||
readFileSync(join(presetRoot, 'minimal', 'agent.cordis.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
)
|
||||
if (!Array.isArray(entries)) throw new TypeError('minimal preset must parse to an entry array')
|
||||
expect(entries.some(entry => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === 'tool-bash'
|
||||
))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-primer.md
|
||||
cordis-primer.md: d1e7c5fd8eaaa89fe448d238359389d945cd6346
|
||||
cordis-primer.zh.md: d6ce0f2024f65b006c9505daffaa06a08bb56875
|
||||
cordis-primer.md: c57055e9657ebc8a0c3f537825ddcbdda1ced68a
|
||||
cordis-primer.zh.md: 45cce2abb2117aef44028ab53a9836d24fab91d6
|
||||
@@ -35,7 +35,7 @@ For single-decision events, short-circuiting is the design. A policy listener ca
|
||||
|
||||
## Loader Configuration
|
||||
|
||||
`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates only an entry's `config`, after declared injections activate, against that plugin context (`ctx.serviceName`); Include preserves nested row expressions until target activation. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, `isolate`) stays literal, so `disabled: !!js ...` always disables the entry. Use overlays when the environment selects plugins.
|
||||
`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates an entry's `config` (after declared injections activate, against that plugin context — `ctx.serviceName`) and its `disabled` field (at every mount decision, against the loader context); Include preserves nested row expressions until target activation. Other entry metadata stays literal. Use overlays when the environment selects plugins.
|
||||
|
||||
## Practical Rules
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
|
||||
|
||||
## Loader 配置
|
||||
|
||||
`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 只在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`;Include 会保留嵌套行表达式,直到目标行激活。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept`、`isolate`)保持字面值,因此 `disabled: !!js ...` 始终禁用该条目。由环境选择插件时,请使用 overlay。
|
||||
`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`,并在每次挂载决策时基于 loader 上下文插值其 `disabled` 字段;Include 会保留嵌套行表达式,直到目标行激活。其余条目元数据保持字面值。由环境选择插件时,请使用 overlay。
|
||||
|
||||
## 实践规则
|
||||
|
||||
|
||||
@@ -187,6 +187,74 @@ describe('Loader config interpolation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loader entry disabled interpolation', () => {
|
||||
it('evaluates a !!js disabled expression against the loader context', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), [
|
||||
'- id: expr-off',
|
||||
' name: ./noop.mjs',
|
||||
' disabled: !!js process.version.length > 0',
|
||||
'- id: expr-on',
|
||||
' name: ./noop.mjs',
|
||||
' disabled: !!js process.version.length === 0',
|
||||
'',
|
||||
].join('\n'))
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
try {
|
||||
const off = [...ctx.loader.entries()].find(entry => entry.options.id === 'expr-off')
|
||||
const on = [...ctx.loader.entries()].find(entry => entry.options.id === 'expr-on')
|
||||
expect(off?.disabled).toBe(true)
|
||||
expect(off?.fiber).toBeUndefined()
|
||||
expect(on?.disabled).toBe(false)
|
||||
expect(on?.fiber).toBeDefined()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the raw expression in the options so write-back preserves the !!js form', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: expr\n name: ./noop.mjs\n disabled: !!js process.platform === "win32"\n')
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
try {
|
||||
const entry = [...ctx.loader.entries()].find(item => item.options.id === 'expr')
|
||||
// The evaluated boolean drives the mount decision; the serialized
|
||||
// expression node stays in the options for the file-backed tree.
|
||||
expect(entry?.options.disabled).toEqual({ __jsExpr: 'process.platform === "win32"' })
|
||||
expect(entry?.disabled).toBe(process.platform === 'win32')
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('re-evaluates when update() replaces the expression, mounting and unmounting', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: expr\n name: ./noop.mjs\n disabled: !!js process.version.length === 0\n')
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
try {
|
||||
const entry = [...ctx.loader.entries()].find(item => item.options.id === 'expr')
|
||||
expect(entry?.disabled).toBe(false)
|
||||
expect(entry?.fiber).toBeDefined()
|
||||
// The expression form is the file dialect; the typed programmatic API
|
||||
// carries booleans. Include reapplication feeds the raw node through
|
||||
// the untyped file path — simulated here with the serialized shape.
|
||||
const disabledTrue = { __jsExpr: 'process.version.length > 0' } as unknown as boolean
|
||||
const disabledFalse = { __jsExpr: 'process.version.length === 0' } as unknown as boolean
|
||||
await entry?.update({ disabled: disabledTrue })
|
||||
expect(entry?.disabled).toBe(true)
|
||||
expect(entry?.fiber).toBeUndefined()
|
||||
await entry?.update({ disabled: disabledFalse })
|
||||
expect(entry?.disabled).toBe(false)
|
||||
expect(entry?.fiber).toBeDefined()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('boot with user patches', () => {
|
||||
it('applies id-targeted overrides, inserts, and interpolates !!js from the environment', async () => {
|
||||
const dir = tmp()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* The verify-cordis-config metadata contract: `disabled` is the one entry
|
||||
* metadata field whose `!!js` expression the Loader interpolates; every other
|
||||
* metadata field must stay static, and a disabled expression must parse.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { metadataExpressionErrors } from './verify-cordis-config.ts'
|
||||
|
||||
describe('verify-cordis-config metadata expressions', () => {
|
||||
it('accepts a disabled !!js expression', () => {
|
||||
const problems = metadataExpressionErrors(
|
||||
{ id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', disabled: { __jsExpr: "process.platform === 'win32'" } },
|
||||
'[0]',
|
||||
)
|
||||
expect(problems).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an expression in a static metadata field', () => {
|
||||
const problems = metadataExpressionErrors({ id: { __jsExpr: 'process.platform' }, name: 'pkg' }, '[0]')
|
||||
expect(problems).toContain('[0].id: !!js is not interpolated here')
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Validate Cordis Loader entry metadata and package resolution.
|
||||
*
|
||||
* The Loader interpolates only a plugin entry's `config`; expression objects in
|
||||
* fields such as `disabled` remain truthy data and silently change composition.
|
||||
* Example configs and the dsh Web composition resolve named plugins from their
|
||||
* owning workspace manifests. Local example packages must also be in the root
|
||||
* TypeScript project graph.
|
||||
* The Loader interpolates a plugin entry's `config` and the entry `disabled`
|
||||
* field (both evaluate against the loader context; `disabled` at tree build).
|
||||
* Every other entry metadata field stays static, so an expression there
|
||||
* remains truthy data and silently changes composition. Example configs and
|
||||
* the dsh Web composition resolve named plugins from their owning workspace
|
||||
* manifests. Local example packages must also be in the root TypeScript
|
||||
* project graph.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
@@ -35,7 +37,7 @@ const appOverlayFiles = new Set([
|
||||
'examples/web-cordis/cordis.yml',
|
||||
...globSync('examples/mcp-memory/*.cordis.yml', { cwd: root }),
|
||||
])
|
||||
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
|
||||
const metadataFields = ['id', 'name', 'group', 'inject', 'intercept', 'isolate'] as const
|
||||
|
||||
/** The adaptive directory-picker chooser package (mounts a backend row at boot). */
|
||||
const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||
@@ -60,32 +62,35 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
})
|
||||
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
const files = cordisConfigFiles(root)
|
||||
const errors: string[] = []
|
||||
const pluginReferences: PluginReference[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
if (!isUnknownArray(document)) {
|
||||
errors.push(`${file}: root must be a Loader entry array`)
|
||||
continue
|
||||
}
|
||||
for (let index = 0; index < document.length; index++) {
|
||||
validateEntry(document[index], file, `[${index}]`)
|
||||
}
|
||||
}
|
||||
if (import.meta.main) {
|
||||
const files = cordisConfigFiles(root)
|
||||
|
||||
errors.push(...validateExampleResolution())
|
||||
errors.push(...validateAppResolution())
|
||||
errors.push(...validateSourcePlaneResolution())
|
||||
errors.push(...validatePresetPlaneSeparation())
|
||||
for (const file of files) {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
if (!isUnknownArray(document)) {
|
||||
errors.push(`${file}: root must be a Loader entry array`)
|
||||
continue
|
||||
}
|
||||
for (let index = 0; index < document.length; index++) {
|
||||
validateEntry(document[index], file, `[${index}]`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
|
||||
for (const error of errors) console.error(`- ${error}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`verify-cordis-config: ${files.length} config files passed.`)
|
||||
errors.push(...validateExampleResolution())
|
||||
errors.push(...validateAppResolution())
|
||||
errors.push(...validateSourcePlaneResolution())
|
||||
errors.push(...validatePresetPlaneSeparation())
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
|
||||
for (const error of errors) console.error(`- ${error}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`verify-cordis-config: ${files.length} config files passed.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,12 +380,27 @@ function packageNameFromSpecifier(specifier: string): string | undefined {
|
||||
}
|
||||
|
||||
function validateMetadata(entry: Record<string, unknown>, file: string, path: string): void {
|
||||
for (const problem of metadataExpressionErrors(entry, path)) {
|
||||
errors.push(`${file}${problem}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expression-node diagnostics for one entry. `disabled` is the single
|
||||
* interpolated metadata field; every other metadata field must stay static.
|
||||
* @param entry - one loader entry (or patch row).
|
||||
* @param path - the entry's diagnostic path prefix.
|
||||
* @returns one diagnostic per offending expression.
|
||||
*/
|
||||
export function metadataExpressionErrors(entry: Record<string, unknown>, path: string): string[] {
|
||||
const problems: string[] = []
|
||||
for (const field of metadataFields) {
|
||||
if (!(field in entry)) continue
|
||||
const expressionPaths: string[] = []
|
||||
collectExpressionPaths(entry[field], `${path}.${field}`, expressionPaths)
|
||||
for (const expressionPath of expressionPaths) errors.push(`${file}${expressionPath}: !!js is not interpolated here`)
|
||||
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
function collectExpressionPaths(value: unknown, path: string, output: string[]): void {
|
||||
|
||||
Vendored
+1
@@ -48,6 +48,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
16. **In-memory Loader entry activation in `loader/src/config/entry.ts`**: an invocation can activate a row shipped with `disabled: true` without mutating its serialized options. The override belongs to the mounted entry object, survives Include config reapplication, respects disabled ancestors, and disappears with the entry. Covered by `packages/boot/cmdline/tests/cmdline.spec.ts` and `apps/web/tests/hmr-live.e2e.ts`.
|
||||
17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table's `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for('schemastery')` and Schemastery's `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table's two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).
|
||||
18. **`cordis/package.json` publishes `src`**: added `src` to the `files` list, joining the other eight vendored packages. Cordis declares `"./src/*": "./src/*"` in its exports, so a tarball without `src` publishes an export map pointing at absent files; the release change judgement also reads `files` to decide whether a diff reaches the payload, and a package whose only published paths are build output has no tracked path to match.
|
||||
19. **Entry `disabled` interpolation in `loader/src/config/entry.ts`**: a `disabled: !!js` expression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the `!!js` form. `disabled` is the only interpolated metadata field. Covered by `packages/boot/app-boot/tests/user-patches.spec.ts` and `apps/cli/tests/windows-shell.spec.ts`.
|
||||
|
||||
## Sync procedure
|
||||
|
||||
|
||||
Vendored
+13
-3
@@ -3,7 +3,7 @@ import { deepEqual, isNullable } from '@deepseek-ai/cosmokit'
|
||||
import { Loader } from '../index.ts'
|
||||
import { EntryGroup } from './group.ts'
|
||||
import { EntryTree } from './tree.ts'
|
||||
import { evaluate } from './utils.ts'
|
||||
import { evaluate, isJsExpr } from './utils.ts'
|
||||
|
||||
/** Static plugin hook for resolving a container config while preserving nested entry configs. */
|
||||
export const EntryConfigResolver = Symbol.for('cordis.loader.entry-config-resolver')
|
||||
@@ -101,15 +101,25 @@ export class Entry {
|
||||
private _disabled(options: EntryOptions) {
|
||||
// group is always enabled
|
||||
if (options.group) return false
|
||||
if (options.disabled && !this.runtimeEnabled) return true
|
||||
if (this.disabledOf(options) && !this.runtimeEnabled) return true
|
||||
let entry = this.parent.ctx.fiber.entry
|
||||
while (entry) {
|
||||
if (entry.options.disabled && !entry.runtimeEnabled) return true
|
||||
if (this.disabledOf(entry.options) && !entry.runtimeEnabled) return true
|
||||
entry = entry.parent.ctx.fiber.entry
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective disabled state: a `!!js` expression evaluates against the loader
|
||||
* context. The raw node stays in the options, so write-back keeps the form.
|
||||
*/
|
||||
private disabledOf(options: EntryOptions): boolean {
|
||||
return isJsExpr(options.disabled)
|
||||
? Boolean(this.evaluate(options.disabled.__jsExpr))
|
||||
: Boolean(options.disabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable this in-memory entry without rewriting its configured `disabled`
|
||||
* value; the override survives config reapplication for this entry object.
|
||||
|
||||
Reference in New Issue
Block a user