Merge remote-tracking branch 'origin/master' into codex/dsh-home-path
# Conflicts: # packages/ui/app-boot/README.i18n.yaml # packages/ui/app-boot/README.md # packages/ui/app-boot/README.zh.md # packages/ui/app-boot/src/index.ts
This commit is contained in:
449 files changed
+13346
-4834
No files matched your search
@@ -12,7 +12,7 @@ import { basename, dirname, join, resolve } from 'node:path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { Context, type FiberState } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import Include, { applyEntryPatches, entryListSchema, type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
// Side-effect type import: resolves `ctx.get('systemPrompt')` to the service.
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
@@ -67,16 +67,12 @@ export function loadEnv(
|
||||
/** File inside the Harness home holding the personal loader overlay patches. */
|
||||
export const PERSONAL_CONFIG_FILENAME = 'config.yaml'
|
||||
|
||||
// The include's YAML dialect: `!!js` scalars become expression nodes the
|
||||
// Loader interpolates against each entry's context at mount time. Personal
|
||||
// patches are parsed with the same schema so they may reference `process.env`.
|
||||
// Load-only: this schema never dumps, so no `predicate`/`represent`.
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: data => typeof data === 'string',
|
||||
construct: data => ({ __jsExpr: String(data) }),
|
||||
})
|
||||
const personalPatchesSchema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
// The include's YAML dialect (`!!js` scalars become expression nodes the
|
||||
// Loader interpolates against each entry's context at mount time), imported
|
||||
// from the include itself so patch parsing and config dumping can never drift
|
||||
// from what the include mounts. Personal patches share it so they may
|
||||
// reference `process.env`.
|
||||
const personalPatchesSchema = entryListSchema
|
||||
|
||||
/**
|
||||
* Load the optional personal overlay patches (`config.yaml` under the Harness
|
||||
@@ -156,6 +152,141 @@ function parsePatchList(
|
||||
return parsed as PatchOptions[]
|
||||
}
|
||||
|
||||
/** One overlay patch list with the label provenance comments print for it. */
|
||||
export interface ConfigDumpLayer {
|
||||
/** Source name shown in provenance comments (a file basename or path). */
|
||||
label: string
|
||||
/** The layer's patches, from {@link loadOverlayPatches} / {@link loadPersonalPatches}. */
|
||||
patches: PatchOptions[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the effective entry list exactly as `boot()` would mount it: parse
|
||||
* the base config file with the include's entry-list dialect, apply every
|
||||
* layer's patches as ONE flattened list through the include's own patch
|
||||
* algorithm (`applyEntryPatches`) — the same single call `boot()` makes, so
|
||||
* even patch-visibility corner cases (a later layer targeting a group child a
|
||||
* plain config replacement introduced, which the single-pass id index never
|
||||
* sees) compose identically — then render the result as YAML in the same
|
||||
* dialect (`!!js` expressions print verbatim, unevaluated).
|
||||
*
|
||||
* Every run of rows with the same provenance is preceded by a `# ==` comment
|
||||
* naming the file that contributed the rows and any layers that patched them,
|
||||
* so the output stays a loadable YAML document while showing which section
|
||||
* comes from which file. Provenance is derived from single-call prefix
|
||||
* snapshots (base + layers 1..k), diffed positionally: the patch algorithm
|
||||
* only rewrites rows in place or appends, so a top-level index identifies one
|
||||
* row across snapshots, and a layer whose addition changes the row (config
|
||||
* replacement, disable, group insert) is listed as having patched it.
|
||||
*
|
||||
* A patch that matches no row is reported through `warn` with its layer
|
||||
* label, mirroring the Loader's boot-time warning. Earlier layers' patches
|
||||
* see an identical preceding state in every snapshot that includes them, so
|
||||
* each snapshot's warning list extends the previous one and the new tail
|
||||
* belongs to the added layer.
|
||||
* @param binName - the diagnostic prefix on read/parse errors.
|
||||
* @param absoluteConfigPath - the base config file `boot()` would include.
|
||||
* @param layers - overlay layers in application order (later wins).
|
||||
* @param warn - sink for skipped-patch diagnostics; defaults to stderr.
|
||||
* @returns the composed entry list rendered as a YAML document with
|
||||
* provenance comment separators.
|
||||
*/
|
||||
export function renderConfigDump(
|
||||
binName: string,
|
||||
absoluteConfigPath: string,
|
||||
layers: ConfigDumpLayer[],
|
||||
warn: (line: string) => void = line => void process.stderr.write(`${line}\n`),
|
||||
): string {
|
||||
let content: string
|
||||
try {
|
||||
content = readFileSync(absoluteConfigPath, 'utf8')
|
||||
} catch (error) {
|
||||
throw new Error(`${binName}: failed to read config ${absoluteConfigPath}: ${String(error)}`)
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = yaml.load(content, { schema: entryListSchema })
|
||||
} catch (error) {
|
||||
throw new Error(`${binName}: failed to parse config ${absoluteConfigPath}: ${String(error)}`)
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error(`${binName}: config ${absoluteConfigPath} must be a top-level YAML array of entries`)
|
||||
}
|
||||
const baseLabel = basename(absoluteConfigPath)
|
||||
// The YAML boundary yields untyped rows; the include validates entry shape
|
||||
// at mount, and the dump prints whatever the file holds, so `EntryOptions`
|
||||
// here is structural trust in the same file `boot()` would include.
|
||||
const base = parsed as Parameters<typeof applyEntryPatches>[0]
|
||||
// snapshot_k = ONE application of layers 1..k flattened — boot's exact call
|
||||
// shape for that prefix. snapshot_N is therefore the mounted composition.
|
||||
// The patches are cloned per call: applyEntryPatches detaches the entry
|
||||
// list but pushes `insert` rows by reference from the patch list, so
|
||||
// sharing patch objects across snapshot calls would leak a later
|
||||
// snapshot's mutations into an earlier one's result.
|
||||
const snapshot = (count: number, warnings: string[]): ReturnType<typeof applyEntryPatches> => {
|
||||
const flattened = structuredClone(layers.slice(0, count).flatMap(layer => layer.patches))
|
||||
return applyEntryPatches(base, flattened, (message: string, ...args: unknown[]) => {
|
||||
// The include logs through cordis's printf-style logger (`%C` = code); a
|
||||
// dump has no logger, so substitute inline for a plain line.
|
||||
let index = 0
|
||||
warnings.push(message.replace(/%C/g, () => JSON.stringify(args[index++])))
|
||||
})
|
||||
}
|
||||
let previous = base
|
||||
let previousWarnings: string[] = []
|
||||
const provenance: { origin: string; patchedBy: string[] }[] = base.map(() => ({ origin: baseLabel, patchedBy: [] }))
|
||||
let composed = base
|
||||
for (let count = 1; count <= layers.length; count += 1) {
|
||||
const layer = layers[count - 1]
|
||||
/* v8 ignore next -- count iterates 1..length, so the slot exists */
|
||||
if (layer === undefined) continue
|
||||
const warnings: string[] = []
|
||||
composed = snapshot(count, warnings)
|
||||
for (const line of warnings.slice(previousWarnings.length)) {
|
||||
warn(`${binName}: [${layer.label}] ${line}`)
|
||||
}
|
||||
const before = previous.map(entry => JSON.stringify(entry))
|
||||
for (let index = 0; index < composed.length; index += 1) {
|
||||
if (index >= before.length) provenance.push({ origin: layer.label, patchedBy: [] })
|
||||
else if (JSON.stringify(composed[index]) !== before[index]) provenance[index]?.patchedBy.push(layer.label)
|
||||
}
|
||||
previous = composed
|
||||
previousWarnings = warnings
|
||||
}
|
||||
return groupedDump(composed, provenance)
|
||||
}
|
||||
|
||||
/** Render the composed rows grouped under one provenance comment per contiguous run. */
|
||||
function groupedDump(
|
||||
composed: readonly unknown[],
|
||||
provenance: readonly { origin: string; patchedBy: string[] }[],
|
||||
): string {
|
||||
const lines: string[] = []
|
||||
let currentLabel: string | undefined
|
||||
let group: unknown[] = []
|
||||
const flush = (): void => {
|
||||
if (currentLabel === undefined || group.length === 0) return
|
||||
lines.push(`# == ${currentLabel}`)
|
||||
lines.push(yaml.dump(group, { schema: entryListSchema, noRefs: true }).trimEnd())
|
||||
group = []
|
||||
}
|
||||
for (let index = 0; index < composed.length; index += 1) {
|
||||
const record = provenance[index]
|
||||
/* v8 ignore next -- provenance is index-aligned with composed by construction */
|
||||
if (record === undefined) continue
|
||||
const label = record.patchedBy.length === 0
|
||||
? record.origin
|
||||
: `${record.origin}, patched by ${record.patchedBy.join(', ')}`
|
||||
if (label !== currentLabel) {
|
||||
flush()
|
||||
currentLabel = label
|
||||
}
|
||||
group.push(composed[index])
|
||||
}
|
||||
flush()
|
||||
return lines.join('\n') + '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* The slice of `process` {@link installFailLoud} needs — injectable so tests
|
||||
* exercise the handler without registering on (or exiting) the real process.
|
||||
|
||||
Reference in New Issue
Block a user