Merge remote-tracking branch 'origin/master' into feat/loader-entry-disabled-interpolation
# Conflicts: # vendor/README.md # vendor/loader/src/config/entry.ts
This commit is contained in:
@@ -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 packages/boot/app-boot/README.md
|
||||
README.md: 9639f1c0a2ffe91fd509a2ffdf04be5f0895b700
|
||||
README.zh.md: e8bf0374aad2be2311b6e72e91e41f02f403b48a
|
||||
README.md: 4c82aa749edbeada0a344b0b119d1644544d9732
|
||||
README.zh.md: 38298e091af4aa1b09c31dfa8141ce68fa1d3f50
|
||||
|
||||
@@ -42,7 +42,7 @@ User-level machine-local preferences also live in the Harness home:
|
||||
- **`.env`** — the product CLI's ordinary environment layers: the invoking directory's file outranks the Harness-home file, and both sit below the inherited environment. `loadLayeredEnv` snapshots each value's source, rejects [bootstrap-only file variables](../../../.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md#decision) case-insensitively, and materializes accepted values into `process.env` for Loader expressions and third-party libraries. Managed credentials live separately in [`.credentials.yaml`](../../credentials/credentials-local/README.md); a credential left in either `.env` remains a lower-priority fallback.
|
||||
- **`cordis.patch.yml`** (home level) and **`profiles/<name>/cordis.patch.yml`** — the user patch layers, applied after every bundle layer (per-profile first, then the home-level file, which therefore outranks it): an id-targeted patch replaces the named entry's whole `config` (restate unchanged fields), `insert` adds entries, and `!!js` expressions interpolate at mount. A patch naming an entry id absent from the composed tree is a stderr warning. An empty or comments-only file throws (it parses to nothing, not to a list); disable the layer with `[]`.
|
||||
|
||||
Long-lived surfaces keep `cordis.patch.yml` live through `watchUserPatches`; one-shot runs read only the startup value. The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlays above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts `hmr/config-update-failed(filename, Error)` after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh.
|
||||
Every profile boot keeps `cordis.patch.yml` live through `watchUserPatches` (a one-shot surface disposes the watcher through its bounded shutdown). The watcher targets the exact path even when the file or immediate parent does not exist, serializes bursts, and recomposes the user patches inside the caller's layer order (bundle layers below, overlays above). A rejected read, parse, or Loader candidate leaves the last good tree running and the HMR service broadcasts `hmr/config-update-failed(filename, Error)` after logging it; observer failures are contained. Disposing the context closes the watcher and drains an active refresh.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(Harness home 由 [`
|
||||
- **`.env`**:产品 CLI 的普通环境层;调用目录的文件优先于 Harness home 的文件,两者都低于继承环境。`loadLayeredEnv` 记录每个值的来源,按不区分大小写的方式拒绝 [bootstrap-only 文件变量](../../../.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md#decision),并把其余值物化进 `process.env`,供 Loader 表达式和第三方库使用。受管凭据另存于 [`.credentials.yaml`](../../credentials/credentials-local/README.md);留在任一 `.env` 中的凭据仍是低优先级后备值。
|
||||
- **`cordis.patch.yml`**(home 级)与 **`profiles/<name>/cordis.patch.yml`**:用户 patch 层,应用在所有组合包层之后(先应用逐 profile 的文件,再应用 home 级文件,因此后者优先级更高):按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值。如果 patch 指定的条目 id 不在组合后的树中,则输出一条 stderr 警告。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用该层,请使用 `[]`。
|
||||
|
||||
长期运行的界面会持续应用 `cordis.patch.yml` 的变更,具体由 `watchUserPatches` 负责;一次性运行只读取启动时的值。即使该文件或其直接父目录不存在,监视器仍会监视确切路径;它会串行处理突发变更,并按调用方的层次顺序重新组合用户 patch(组合包层在下、overlay 在上)。读取失败、解析失败或 Loader 候选被拒时,最后一个可用树会继续运行;HMR 服务记录错误后广播 `hmr/config-update-failed(filename, Error)`,并隔离观察方的失败。上下文 dispose 时会关闭 watcher,并等待进行中的刷新结束。
|
||||
每次 profile 启动都由 `watchUserPatches` 持续应用 `cordis.patch.yml` 的变更(一次性 surface 经由有界关闭 dispose 监视器)。即使该文件或其直接父目录不存在,监视器仍会监视确切路径;它会串行处理突发变更,并按调用方的层次顺序重新组合用户 patch(组合包层在下、overlay 在上)。读取失败、解析失败或 Loader 候选被拒时,最后一个可用树会继续运行;HMR 服务记录错误后广播 `hmr/config-update-failed(filename, Error)`,并隔离观察方的失败。上下文 dispose 时会关闭 watcher,并等待进行中的刷新结束。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ function ensureSymlink(link: string, target: string): void {
|
||||
// Concurrent launches heal the same fallback; losing the race to a
|
||||
// process writing the identical link is success, anything else is not.
|
||||
// The window between the lstat miss above and this write cannot be
|
||||
// staged deterministically from the public surface.
|
||||
// staged deterministically from the public API.
|
||||
/* v8 ignore next 4 */
|
||||
if ((error as NodeJS.ErrnoException).code !== 'EEXIST'
|
||||
|| !lstatSync(link).isSymbolicLink() || readlinkSync(link) !== target) {
|
||||
|
||||
@@ -110,21 +110,33 @@ function entryConfig(ctx: Context, id: string): unknown {
|
||||
}
|
||||
|
||||
describe('Loader config interpolation', () => {
|
||||
it("resolves Include's own !!js options", async () => {
|
||||
it("keeps Include's config literal — a nested row's !!js belongs to that row's fiber", async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n')
|
||||
writeFileSync(join(dir, 'reader.mjs'), [
|
||||
'export const name = "reader"',
|
||||
'export function apply(ctx, config) { ctx.provide("observedValue", config.value) }',
|
||||
'',
|
||||
].join('\n'))
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: reader\n name: ./reader.mjs\n')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins.include = Include
|
||||
ctx.provide('includePath', pathToFileURL(join(dir, 'cordis.yml')).href)
|
||||
ctx.provide('answer', 42)
|
||||
try {
|
||||
// The include is a tree carrier: its own config (path, patches) stays
|
||||
// literal, and the expression nested inside the patched row's config
|
||||
// resolves against the row's fiber, not the include's.
|
||||
await ctx.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: { path: { __jsExpr: "ctx.get('includePath')" } },
|
||||
config: {
|
||||
path: pathToFileURL(join(dir, 'cordis.yml')).href,
|
||||
patches: [{ id: 'reader', name: './reader.mjs', config: { value: { __jsExpr: "ctx.get('answer')" } } }],
|
||||
},
|
||||
})
|
||||
await ctx.loader.await()
|
||||
expect([...ctx.loader.entries()].some(entry => entry.options.id === 'noop')).toBe(true)
|
||||
const reader = [...ctx.loader.entries()].find(entry => entry.options.id === 'reader')
|
||||
expect(reader?.options.config).toEqual({ value: { __jsExpr: "ctx.get('answer')" } })
|
||||
expect(ctx.get('observedValue')).toBe(42)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user