Merge remote-tracking branch 'origin/master' into worktree/web-background-tasks-display-258f7e
This commit is contained in:
@@ -97,14 +97,14 @@ function repositoryState(root: string): Record<string, string> {
|
||||
}
|
||||
|
||||
describe('change-scope', () => {
|
||||
it('uses an explicit base on a fresh branch without a same-name remote and after its first push', () => {
|
||||
it('uses an explicit base on a fresh branch without a same-name remote and after its first push', { timeout: 20_000 }, () => {
|
||||
const { root } = fixture()
|
||||
git(root, ['switch', '-c', 'feature'])
|
||||
git(root, ['branch', '--set-upstream-to=origin/master'])
|
||||
const headSha = commit(root, 'feature.txt', 'feature\n')
|
||||
|
||||
const fresh = jsonReport(root, 'origin/master')
|
||||
expect(fresh.repositoryRoot).toBe(realpathSync(root))
|
||||
expect(realpathSync.native(fresh.repositoryRoot)).toBe(realpathSync.native(root))
|
||||
expect(fresh.resolved).toEqual({
|
||||
baseSha: git(root, ['rev-parse', 'origin/master']),
|
||||
headSha,
|
||||
@@ -122,7 +122,7 @@ describe('change-scope', () => {
|
||||
const { root } = fixture('worktree ')
|
||||
const report = jsonReport(root, 'HEAD')
|
||||
|
||||
expect(report.repositoryRoot).toBe(realpathSync(root))
|
||||
expect(realpathSync.native(report.repositoryRoot)).toBe(realpathSync.native(root))
|
||||
expect(report.paths).toEqual({ committed: [], staged: [], unstaged: [], untracked: [] })
|
||||
})
|
||||
|
||||
|
||||
@@ -119,12 +119,16 @@ function workspaceManifests(): WorkspaceManifest[] {
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib;
|
||||
// dsh-base also ships the win32 shell platform layer the launcher reads.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml', 'windows.cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
|
||||
'@deepseek-ai/dsh-helper': ['lib/assets'],
|
||||
// The argv-prefix runner entry ships beside the lib as its own bundle;
|
||||
// sandbox-local resolves it through the package's ./runner export.
|
||||
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js'],
|
||||
'@deepseek-ai/dsh-skill-badge': ['assets'],
|
||||
'@deepseek-ai/dsh-subprocess-local': ['scripts/ensure-spawn-helper.mjs'],
|
||||
'@deepseek-ai/dsh-scripts': [
|
||||
|
||||
@@ -26,6 +26,63 @@ describe('CI workflow', () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps Wine blocking while native Windows reports independently', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
if (!isRecord(workflow.jobs)
|
||||
|| !isRecord(workflow.jobs.windows)
|
||||
|| !isRecord(workflow.jobs['windows-native'])
|
||||
|| !isRecord(workflow.jobs['all-checks-passed'])) {
|
||||
throw new TypeError('CI workflow must define Wine, native Windows, and aggregate jobs')
|
||||
}
|
||||
|
||||
const windows = workflow.jobs.windows
|
||||
const windowsNative = workflow.jobs['windows-native']
|
||||
const aggregate = workflow.jobs['all-checks-passed']
|
||||
if (!Array.isArray(windows.steps) || !Array.isArray(windowsNative.steps) || !Array.isArray(aggregate.needs)) {
|
||||
throw new TypeError('Windows jobs must define steps and the aggregate must define needs')
|
||||
}
|
||||
const nativeCommandSteps = windowsNative.steps.filter((step): step is Record<string, unknown> & { run: string } => (
|
||||
isRecord(step) && typeof step.run === 'string'
|
||||
))
|
||||
|
||||
expect(windows['runs-on']).toBe('ubuntu-latest')
|
||||
expect(windows.name).toBe('windows node 24 / wine blocking')
|
||||
expect(windows.if).toBe("github.event_name == 'pull_request'")
|
||||
expect(JSON.stringify(windows)).toContain('bash scripts/wine-windows-gates.sh')
|
||||
expect(workflow.jobs).toHaveProperty('wine-apt-cache')
|
||||
expect(windowsNative['runs-on']).toBe('dsh-windows-2025-16core')
|
||||
expect(windowsNative.name).toBe('windows node 24 / native complete')
|
||||
expect(windowsNative['timeout-minutes']).toBe(60)
|
||||
expect(windowsNative.if).toBe("github.event_name == 'pull_request'")
|
||||
expect(windowsNative.env).toMatchObject({
|
||||
DSH_COVERAGE_MAX_WORKERS: '2',
|
||||
DSH_GATE_CONCURRENCY: '2',
|
||||
DSH_PUBLINT_CONCURRENCY: '8',
|
||||
})
|
||||
expect(windowsNative).not.toHaveProperty('continue-on-error')
|
||||
expect(nativeCommandSteps).toHaveLength(3)
|
||||
expect(nativeCommandSteps.every(step => step.shell === 'pwsh')).toBe(true)
|
||||
expect(nativeCommandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete')
|
||||
expect(JSON.stringify(windowsNative)).not.toMatch(/wine/i)
|
||||
expect(aggregate.needs).toContain('windows')
|
||||
expect(aggregate.needs).not.toContain('windows-native')
|
||||
})
|
||||
|
||||
it('keeps supported LSP source under native Windows coverage', () => {
|
||||
const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8')
|
||||
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/connection.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/index.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/instance.ts')
|
||||
})
|
||||
|
||||
it('keeps every Vitest project process-isolated on native Windows', () => {
|
||||
const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8')
|
||||
|
||||
expect(config).not.toContain("pool: process.platform === 'win32' ? 'threads' : 'forks'")
|
||||
expect(config.match(/pool: 'forks'/g)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('E2B e2e workflow', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Regression coverage for source declarations owned by the client test aggregate. */
|
||||
|
||||
import { existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -14,6 +14,7 @@ function clientCssDeclarations(): string[] {
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => resolve(clientRoot, entry.name, 'src/css-modules.d.ts'))
|
||||
.filter(existsSync)
|
||||
.map(file => file.replaceAll(sep, '/'))
|
||||
.sort()
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ describe('client TypeScript aggregate', () => {
|
||||
}
|
||||
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root)
|
||||
const loaded = parsed.fileNames
|
||||
.map(file => file.replaceAll(sep, '/'))
|
||||
.filter(file => file.endsWith('/src/css-modules.d.ts'))
|
||||
.sort()
|
||||
expect(loaded).toEqual(clientCssDeclarations())
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"AGENTS.md": 1782,
|
||||
"AGENTS.md": 1900,
|
||||
"docs/AGENTS.md": 1320,
|
||||
"docs/architecture.md": 2160,
|
||||
"docs/architecture.md": 2400,
|
||||
"docs/cordis-primer.md": 600,
|
||||
"docs/defensive-patterns.md": 550,
|
||||
"docs/testing.md": 1150,
|
||||
"examples/AGENTS.md": 310,
|
||||
"packages/AGENTS.md": 675,
|
||||
"packages/README.md": 936
|
||||
"packages/README.md": 980
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ export { REGION_BEGIN, REGION_END }
|
||||
*/
|
||||
export const SERVICE_PAGE: Record<string, string> = {
|
||||
agentLoop: 'core.md',
|
||||
agentDefaultModel: 'core.md',
|
||||
agentPresets: 'core.md',
|
||||
agents: 'core.md',
|
||||
approval: 'approval.md',
|
||||
bash: 'bash.md',
|
||||
@@ -120,6 +122,8 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
|
||||
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the surface',
|
||||
command: 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the surface',
|
||||
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the surface',
|
||||
conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface',
|
||||
conversationViews: 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface',
|
||||
layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the surface',
|
||||
locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the surface',
|
||||
models: 'client-side interface-typed browser service — packages/client/ui-model/README.md owns the surface',
|
||||
@@ -196,6 +200,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
AgentCancelCause: 'core.md',
|
||||
AgentFactory: 'core.md',
|
||||
AgentHandle: 'core.md',
|
||||
ModelSelection: 'core.md',
|
||||
AgentOptions: 'core.md',
|
||||
AgentStatus: 'core.md',
|
||||
ContentBlock: 'llm-streaming.md',
|
||||
@@ -260,6 +265,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
FsEditOutcome: 'filesystem.md',
|
||||
FsEditRequest: 'filesystem.md',
|
||||
FsInfo: 'filesystem.md',
|
||||
FsObservation: 'filesystem.md',
|
||||
FsPathInfo: 'filesystem.md',
|
||||
FsPolicyExec: 'filesystem.md',
|
||||
FsTarget: 'filesystem.md',
|
||||
@@ -275,6 +281,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
CreateGoalResult: 'goal.md',
|
||||
CommandDefinition: 'commands.md',
|
||||
CommandDescriptor: 'commands.md',
|
||||
CommandId: 'commands.md',
|
||||
CommandResult: 'commands.md',
|
||||
CommandSurface: 'commands.md',
|
||||
LlmAdapter: 'llm-streaming.md',
|
||||
@@ -340,6 +347,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SkillProvider: 'skills.md',
|
||||
SkillProviderObservation: 'skills.md',
|
||||
SkillRegistration: 'skills.md',
|
||||
SkillViewOptions: 'skills.md',
|
||||
SkillSummary: 'skills.md',
|
||||
SaveTextSpill: 'spill.md',
|
||||
SpillRef: 'spill.md',
|
||||
@@ -383,6 +391,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ToolExecutionResult: 'tools.md',
|
||||
ToolExecutionToken: 'tools.md',
|
||||
ToolGuard: 'tools.md',
|
||||
ToolPresentationMode: 'tools.md',
|
||||
ToolRegistry: 'tools.md',
|
||||
ToolRestriction: 'tools.md',
|
||||
ToolSchema: 'tools.md',
|
||||
@@ -457,6 +466,9 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
ConsumeTokenRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
InsertTextRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
AgentHandle: 'agent ownership handle is owned by packages/core/agent/README.md',
|
||||
AgentPreset: 'discovered preset record is owned by packages/preset/agent-presets/README.md',
|
||||
PresetMetadata: 'preset display text is owned by packages/preset/agent-presets/README.md',
|
||||
BashEnvContributor: 'service-local extension type is owned by packages/bash/tool-bash/src/index.ts',
|
||||
BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts',
|
||||
CompactAgentContext: 'compaction service input is owned by packages/compact/compact/src/index.ts',
|
||||
|
||||
@@ -267,6 +267,13 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
mode: 'core',
|
||||
note: 'Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions.',
|
||||
},
|
||||
{
|
||||
key: 'agentPresets',
|
||||
pkg: 'agent-presets',
|
||||
title: 'Per-session agent composition',
|
||||
mode: 'core',
|
||||
note: 'Discovers preset directories over trusted and user-authored roots and mounts one preset cordis.yml under an agent scope during creation, rejecting a row that never activates or that publishes into the root service realm.',
|
||||
},
|
||||
{
|
||||
key: 'commands',
|
||||
pkg: 'commands',
|
||||
@@ -307,6 +314,14 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['agent-loop', 'acp', 'subagent-inprocess'],
|
||||
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
|
||||
},
|
||||
{
|
||||
key: 'agentDefaultModel',
|
||||
pkg: 'agent-default-model',
|
||||
title: 'Default Agent model selection',
|
||||
mode: 'core',
|
||||
consumers: ['headless', 'host-apiproxy'],
|
||||
note: 'Layers the default ModelSelection through settings so direct and Host-backed Agent front doors share one state owner.',
|
||||
},
|
||||
{
|
||||
key: 'agentLoop',
|
||||
pkg: 'agent-loop',
|
||||
@@ -426,7 +441,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
mode: 'seam',
|
||||
implementations: ['compact-basic'],
|
||||
consumers: ['compact-basic'],
|
||||
note: 'The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred.',
|
||||
note: 'The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool.',
|
||||
},
|
||||
{
|
||||
key: 'subagents',
|
||||
@@ -1109,7 +1124,7 @@ function renderEventRelations(pkgs: Pkg[], events: readonly EventEntry[]): strin
|
||||
const maintenance = 'generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program'
|
||||
const lines = generatedHeader('Event Producer And Consumer Matrix')
|
||||
lines.push(
|
||||
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Receiver and event-name types also cover contained dispatch sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.',
|
||||
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. Events are many-to-many, so the dense relation data is presented as a table rather than one large graph. Receiver and event-name types also cover contained dispatch sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.',
|
||||
'',
|
||||
'| Event | Mode | Declared in | Dispatchers | Listeners |',
|
||||
'| --- | --- | --- | --- | --- |',
|
||||
@@ -1222,7 +1237,7 @@ function renderLifecycle(): string {
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
|
||||
'```',
|
||||
'',
|
||||
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
|
||||
'The `assistant/message` event records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history, while the durable event keeps usage and `sourceEventSeqs` listing the exact `assistant/chunk` events, including an explicit empty list.',
|
||||
'',
|
||||
'`dsh-compact-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'',
|
||||
|
||||
@@ -18,8 +18,11 @@ const OUT = 'docs/persistence-catalog.md'
|
||||
* doc-typecheck, since their imported types are not standalone-compilable). */
|
||||
const FENCE = 'ts persistence-catalog'
|
||||
|
||||
/** The package whose module id plugin merges augment (`declare module '…'`). */
|
||||
const SESSION_MODULE = '@deepseek-ai/dsh-session'
|
||||
/** The package that owns the durable event vocabulary. */
|
||||
const SESSION_PACKAGE = '@deepseek-ai/dsh-session'
|
||||
|
||||
/** The type-only module that plugin declaration merges augment. */
|
||||
const SESSION_TYPES_MODULE = '@deepseek-ai/dsh-session/types'
|
||||
|
||||
/** Event-envelope declarations rendered before the per-event vocabulary. */
|
||||
const EVENT_ENVELOPE_TYPE_NAMES = [
|
||||
@@ -115,7 +118,7 @@ function declarationText(text: string, sf: ts.SourceFile, node: ts.Node): string
|
||||
/**
|
||||
* Every `interface SessionEventMap` declaration in a source file: the owning
|
||||
* top-level declaration (in `@deepseek-ai/dsh-session`) and any declaration
|
||||
* merge inside a `declare module '@deepseek-ai/dsh-session'` block. Both forms
|
||||
* merge inside a `declare module '@deepseek-ai/dsh-session/types'` block. Both forms
|
||||
* declare members of the SAME merged interface, so both are catalogued
|
||||
* uniformly. `topLevel` distinguishes the owning form so the caller can verify
|
||||
* it actually lives in the owning package — an unrelated local interface that
|
||||
@@ -125,7 +128,7 @@ function sessionEventMapDecls(sf: ts.SourceFile): { decl: ts.InterfaceDeclaratio
|
||||
const decls: { decl: ts.InterfaceDeclaration; topLevel: boolean }[] = []
|
||||
for (const stmt of sf.statements) {
|
||||
if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === 'SessionEventMap') decls.push({ decl: stmt, topLevel: true })
|
||||
if (ts.isModuleDeclaration(stmt) && ts.isStringLiteral(stmt.name) && stmt.name.text === SESSION_MODULE
|
||||
if (ts.isModuleDeclaration(stmt) && ts.isStringLiteral(stmt.name) && stmt.name.text === SESSION_TYPES_MODULE
|
||||
&& stmt.body && ts.isModuleBlock(stmt.body)) {
|
||||
for (const inner of stmt.body.statements) {
|
||||
if (ts.isInterfaceDeclaration(inner) && inner.name.text === 'SessionEventMap') decls.push({ decl: inner, topLevel: false })
|
||||
@@ -174,8 +177,8 @@ export function collectLogEvents(scanRoot: string = root): LogEventEntry[] {
|
||||
// the owning package. Same-named interfaces elsewhere are different
|
||||
// types and must not enter the on-disk catalog.
|
||||
const pkg = packageNameFor(rel, scanRoot)
|
||||
if (pkg !== SESSION_MODULE) {
|
||||
violations.push(`top-level interface SessionEventMap (${declSrc}) is outside ${SESSION_MODULE} (package ${pkg ?? 'unknown'}). Rename the interface, or contribute events via declare module '${SESSION_MODULE}'.`)
|
||||
if (pkg !== SESSION_PACKAGE) {
|
||||
violations.push(`top-level interface SessionEventMap (${declSrc}) is outside ${SESSION_PACKAGE} (package ${pkg ?? 'unknown'}). Rename the interface, or contribute events via declare module '${SESSION_TYPES_MODULE}'.`)
|
||||
continue
|
||||
}
|
||||
const exported = decl.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
||||
@@ -243,7 +246,7 @@ export function collectEventEnvelopeTypes(scanRoot: string = root): EventEnvelop
|
||||
const abs = resolve(scanRoot, rel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
if (!EVENT_ENVELOPE_TYPE_NAMES.some(name => text.includes(name))) continue
|
||||
if (packageNameFor(rel, scanRoot) !== SESSION_MODULE) continue
|
||||
if (packageNameFor(rel, scanRoot) !== SESSION_PACKAGE) continue
|
||||
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true)
|
||||
for (const stmt of sf.statements) {
|
||||
if (!ts.isTypeAliasDeclaration(stmt) || !wanted.has(stmt.name.text)) continue
|
||||
@@ -352,7 +355,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
|
||||
'',
|
||||
'# Session Persistence Event Catalog',
|
||||
'',
|
||||
'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the generated region of [session.md](subsystems/session.md#cordis-surface) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).',
|
||||
'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge into `@deepseek-ai/dsh-session/types` in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](subsystems/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](subsystems/persistence.md) (how the log is made durable), and the generated region of [session.md](subsystems/session.md#cordis-surface) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).',
|
||||
'',
|
||||
'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).',
|
||||
'',
|
||||
|
||||
@@ -221,7 +221,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolPwsh)
|
||||
},
|
||||
note:
|
||||
'The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus the sandbox surface — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session; ConPTY is roadmap work), with native `C:\\...` paths and `$env:NAME` variables.',
|
||||
'The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus the sandbox surface — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\\...` paths and `$env:NAME` variables.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-cordis',
|
||||
@@ -253,7 +253,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
dir: 'tool-str-replace-editor',
|
||||
source: 'packages/fs/tool-str-replace-editor/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.fs'],
|
||||
writes: ['tool/call', 'fs/observed after successful file operations', 'tool/result'],
|
||||
writes: ['tool/call', 'fs/observed after view presence/absence, edit absence, or successful mutation', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalFileSystem)
|
||||
await ctx.plugin(ToolStrReplaceEditor)
|
||||
@@ -266,7 +266,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
dir: 'tool-fs',
|
||||
source: 'packages/fs/tool-fs/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after successful file operations', 'tool/result'],
|
||||
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after read presence/absence or successful mutation', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The tool needs `fs`; the bare provider is sufficient because policy
|
||||
// changes behavior, not schema shape.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Regression drive for the unified hero composer (0729-0357-hero-unify):
|
||||
// Regression drive for the unified hero composer:
|
||||
// cold start with zero workspaces -> create a workspace -> type. Asserts the
|
||||
// composer textarea is the SAME DOM node across the disabled->live flip (a
|
||||
// remount drops the __heroMark marker property) — the session-maybe
|
||||
|
||||
@@ -610,7 +610,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('sibling dormant worktree config')
|
||||
expect(result.stderr).toContain(linkedConfig)
|
||||
expect(result.stderr).toContain(JSON.stringify(linkedConfig))
|
||||
expect(gitResult(fixture, fixture.main, ['config', '--get', 'extensions.worktreeConfig']).status).toBe(1)
|
||||
expect(gitResult(fixture, fixture.linked, ['config', '--get', 'core.hooksPath']).status).toBe(1)
|
||||
expect(git(fixture, fixture.main, ['config', '--file', linkedConfig, '--get', 'core.hooksPath'])).toBe(linkedHooks)
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Oxlint repository rule fingerprint', () => {
|
||||
const overrides: readonly unknown[] = parsed.overrides
|
||||
|
||||
it('pins the complete override shape', () => {
|
||||
expect(overrides).toHaveLength(6)
|
||||
expect(overrides).toHaveLength(8)
|
||||
})
|
||||
|
||||
it.each(Object.entries(profiles))('pins the %s rule profile', (_name, profile) => {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { join, relative } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||
const eslintCli = fileURLToPath(new URL('../node_modules/eslint/bin/eslint.js', import.meta.url))
|
||||
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
|
||||
const tsxCli = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url))
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
@@ -18,11 +19,11 @@ function isUnknownArray(value: unknown): value is unknown[] {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
function runStagedFormatter(paths: readonly string[]) {
|
||||
return spawnSync(process.execPath, [eslintCli, '--config', 'eslint.format.config.mjs', '--fix', '--no-warn-ignored', ...paths], {
|
||||
function runRepositoryOxlint(args: readonly string[], env: NodeJS.ProcessEnv = {}) {
|
||||
return spawnSync(process.execPath, [tsxCli, 'scripts/run-oxlint.ts', ...args], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
env: { ...process.env, NO_COLOR: '1', ...env },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
}
|
||||
}, 20_000)
|
||||
|
||||
it('keeps formatter rules aligned with Oxlint validation', async () => {
|
||||
it('keeps the complete stylistic contract in Oxlint', async () => {
|
||||
const oxlintPath = join(repositoryRoot, '.oxlintrc.json')
|
||||
const result = parseConfigFileTextToJson(oxlintPath, await readFile(oxlintPath, 'utf8'))
|
||||
if (result.error !== undefined) {
|
||||
@@ -160,27 +161,67 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
if (!isRecord(parsed) || !isUnknownArray(parsed.overrides)) {
|
||||
throw new Error('.oxlintrc.json must contain an overrides array')
|
||||
}
|
||||
expect(parsed.ignorePatterns).toEqual(expect.arrayContaining([
|
||||
'packages/typert/generator/tests/fixtures/type-model/**',
|
||||
]))
|
||||
const stylisticOverride = parsed.overrides.find((value: unknown) =>
|
||||
isRecord(value) && isRecord(value.rules) && '@stylistic/max-len' in value.rules)
|
||||
if (!isRecord(stylisticOverride) || !isRecord(stylisticOverride.rules)) {
|
||||
throw new Error('.oxlintrc.json must contain the @stylistic validator override')
|
||||
}
|
||||
const validatorRules = { ...stylisticOverride.rules }
|
||||
const maxLen = validatorRules['@stylistic/max-len']
|
||||
delete validatorRules['@stylistic/max-len']
|
||||
expect(stylisticOverride.rules).toMatchObject({
|
||||
'@stylistic/indent': ['error', 2],
|
||||
'@stylistic/semi': ['error', 'never'],
|
||||
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
|
||||
'@stylistic/comma-dangle': ['error', 'always-multiline'],
|
||||
'@stylistic/eol-last': ['error', 'always'],
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
|
||||
'@stylistic/member-delimiter-style': ['error', {
|
||||
multiline: { delimiter: 'none' },
|
||||
singleline: { delimiter: 'semi', requireLast: false },
|
||||
}],
|
||||
'@stylistic/max-len': ['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }],
|
||||
})
|
||||
const typeGraphOverride = parsed.overrides.find((value: unknown) =>
|
||||
isRecord(value)
|
||||
&& isUnknownArray(value.files)
|
||||
&& value.files.includes('packages/typert/generator/tests/fixtures/type-model/packages/host/src/models.ts'))
|
||||
expect(typeGraphOverride).toMatchObject({
|
||||
rules: { '@stylistic/quotes': 'off' },
|
||||
})
|
||||
})
|
||||
|
||||
const formatterUrl = pathToFileURL(join(repositoryRoot, 'eslint.format.config.mjs')).href
|
||||
const formatterModule = await import(formatterUrl) as unknown
|
||||
if (!isRecord(formatterModule) || !isUnknownArray(formatterModule.default)) {
|
||||
throw new Error('eslint.format.config.mjs must default-export a config array')
|
||||
}
|
||||
const formatterOverride = formatterModule.default.find((value: unknown) => isRecord(value) && isRecord(value.rules))
|
||||
if (!isRecord(formatterOverride) || !isRecord(formatterOverride.rules)) {
|
||||
throw new Error('eslint.format.config.mjs must contain a rules object')
|
||||
it('checks preserved TypeGraph syntax without type-aware analysis', () => {
|
||||
const result = runOxlint([
|
||||
'--config',
|
||||
'.oxlintrc.staged.json',
|
||||
'packages/typert/generator/tests/fixtures/type-model',
|
||||
])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, normalizedOutput(result)).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps repository lint workflows Oxlint-only', async () => {
|
||||
const packageJson = JSON.parse(await readFile(join(repositoryRoot, 'package.json'), 'utf8')) as unknown
|
||||
if (!isRecord(packageJson) || !isRecord(packageJson.scripts) || !isRecord(packageJson.devDependencies)) {
|
||||
throw new Error('package.json must contain scripts and devDependencies objects')
|
||||
}
|
||||
|
||||
expect(validatorRules).toStrictEqual(formatterOverride.rules)
|
||||
expect(maxLen).toStrictEqual(['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }])
|
||||
expect(packageJson.scripts['lint:contracts-ready']).toBe('tsx scripts/run-oxlint.ts .')
|
||||
expect(packageJson.scripts['lint:fix:contracts-ready']).toBe(
|
||||
'tsx scripts/run-oxlint.ts --config .oxlintrc.staged.json packages/typert/generator/tests/fixtures/type-model --fix && tsx scripts/run-oxlint.ts . --fix',
|
||||
)
|
||||
expect(packageJson.devDependencies).not.toHaveProperty('eslint')
|
||||
expect(packageJson.devDependencies).not.toHaveProperty('@typescript-eslint/parser')
|
||||
expect(existsSync(join(repositoryRoot, 'eslint.format.config.mjs'))).toBe(false)
|
||||
|
||||
const lefthook = await readFile(join(repositoryRoot, 'lefthook.yml'), 'utf8')
|
||||
expect(lefthook).toContain('scripts/run-oxlint.ts --config .oxlintrc.staged.json --fix')
|
||||
expect(lefthook).not.toContain('node_modules/.bin/eslint')
|
||||
expect(lefthook).not.toContain('eslint.format.config.mjs')
|
||||
})
|
||||
|
||||
it('reports an unused suppression', async () => {
|
||||
@@ -208,7 +249,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
})
|
||||
}, 20_000)
|
||||
|
||||
it('accepts an ignored-only staged selection', () => {
|
||||
const result = runOxlint([
|
||||
@@ -227,10 +268,13 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
if (result.error !== undefined) {
|
||||
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
|
||||
}
|
||||
expect(result.config).toMatchObject({
|
||||
const stagedConfig = result.config as unknown
|
||||
if (!isRecord(stagedConfig)) throw new Error('.oxlintrc.staged.json must contain a config object')
|
||||
expect(stagedConfig).toMatchObject({
|
||||
extends: ['./.oxlintrc.json'],
|
||||
options: { typeAware: false },
|
||||
})
|
||||
expect(stagedConfig.ignorePatterns).not.toContain('packages/typert/generator/tests/fixtures/type-model/**')
|
||||
|
||||
const suffix = randomUUID()
|
||||
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
|
||||
@@ -254,30 +298,76 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
}
|
||||
})
|
||||
|
||||
it('applies staged stylistic fixes before Oxlint validation', async () => {
|
||||
it('preserves successful fix output channels', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const directory = join(repositoryRoot, 'scripts', `.oxlint-contract-${suffix}`)
|
||||
const path = join(directory, 'fix.ts')
|
||||
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
|
||||
|
||||
try {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(path, 'const value={answer:1}; \nconsole.log(value)\n')
|
||||
|
||||
const relativePath = relative(repositoryRoot, path)
|
||||
const formatResult = runStagedFormatter([relativePath])
|
||||
const lintResult = runOxlint(['--config', relative(repositoryRoot, configPath), '--fix', relativePath])
|
||||
|
||||
expect(formatResult.error).toBeUndefined()
|
||||
expect(formatResult.status, normalizedOutput(formatResult)).toBe(0)
|
||||
expect(lintResult.error).toBeUndefined()
|
||||
expect(lintResult.status, normalizedOutput(lintResult)).toBe(0)
|
||||
await expect(readFile(path, 'utf8')).resolves.toBe('const value={ answer:1 }\nconsole.log(value)\n')
|
||||
} finally {
|
||||
await Promise.all([
|
||||
rm(directory, { recursive: true, force: true }),
|
||||
rm(configPath, { force: true }),
|
||||
await writeFile(path, '// oxlint-disable-next-line no-console\nexport const value = 1\n')
|
||||
const result = runRepositoryOxlint([
|
||||
'--config',
|
||||
'.oxlintrc.staged.json',
|
||||
'--format',
|
||||
'unix',
|
||||
'--fix',
|
||||
relative(repositoryRoot, path),
|
||||
])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, normalizedOutput(result)).toBe(0)
|
||||
expect(result.stdout).toContain('Unused oxlint-disable directive')
|
||||
expect(result.stderr).toBe('')
|
||||
} finally {
|
||||
await rm(path, { force: true })
|
||||
}
|
||||
}, 20_000)
|
||||
})
|
||||
|
||||
it('prints only the final diagnostics when a fix retry still fails', async () => {
|
||||
const suffix = randomUUID()
|
||||
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
|
||||
|
||||
try {
|
||||
await writeFile(path, `export const longProbe = ${'1 + '.repeat(80)}1\n`)
|
||||
const result = runRepositoryOxlint([
|
||||
'--config',
|
||||
'.oxlintrc.staged.json',
|
||||
'--format',
|
||||
'unix',
|
||||
'--fix',
|
||||
relative(repositoryRoot, path),
|
||||
])
|
||||
const output = normalizedOutput(result)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, output).toBe(1)
|
||||
expect(output.match(/@stylistic\(max-len\)/g)).toHaveLength(1)
|
||||
} finally {
|
||||
await rm(path, { force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['--fix', '--fix-suggestions', '--fix-dangerously'])(
|
||||
'converges overlapping staged stylistic fixes through Oxlint under %s',
|
||||
async (fixFlag) => {
|
||||
const suffix = randomUUID()
|
||||
const directory = join(repositoryRoot, 'scripts', `.oxlint-contract-${suffix}`)
|
||||
const path = join(directory, 'fix.ts')
|
||||
|
||||
try {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(path, 'const value={answer:1}; \nconsole.log(value)\n')
|
||||
|
||||
const relativePath = relative(repositoryRoot, path)
|
||||
const lintResult = runRepositoryOxlint(['--config', '.oxlintrc.staged.json', fixFlag, relativePath])
|
||||
|
||||
expect(lintResult.error).toBeUndefined()
|
||||
expect(lintResult.status, normalizedOutput(lintResult)).toBe(0)
|
||||
expect(normalizedOutput(lintResult)).not.toContain('@stylistic')
|
||||
await expect(readFile(path, 'utf8')).resolves.toBe('const value={ answer:1 }\nconsole.log(value)\n')
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
},
|
||||
20_000,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('rewriteMarkdown', () => {
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: (absPath) => {
|
||||
const name = absPath.split('/').pop() ?? ''
|
||||
const name = basename(absPath)
|
||||
placed.push(name)
|
||||
return `./${name}`
|
||||
},
|
||||
@@ -167,7 +167,7 @@ describe('rewriteMarkdown', () => {
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: absPath => `./${absPath.split('/').pop() ?? ''}`,
|
||||
placeImage: absPath => `./${basename(absPath)}`,
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
@@ -251,7 +251,7 @@ describe('rewriteMarkdown', () => {
|
||||
})
|
||||
|
||||
describe('docsPages locale routes', () => {
|
||||
it('publishes every route in both locales and selects paired sources', () => {
|
||||
it('publishes every route in both locales and uses every available Chinese counterpart', () => {
|
||||
const byRoute = new Map(docsPages.map(page => [page.route, page]))
|
||||
for (const page of docsPages.filter(page => page.locale === 'root')) {
|
||||
const counterpart = byRoute.get(`en/${page.route}`)
|
||||
@@ -265,6 +265,11 @@ describe('docsPages locale routes', () => {
|
||||
} else {
|
||||
expect(counterpart?.source).toBe(page.source)
|
||||
expect(counterpart?.contentLocale).toBe(page.contentLocale)
|
||||
const chineseSource = page.source.replace(/\.md$/, '.zh.md')
|
||||
expect(
|
||||
existsSync(resolve(repositoryRoot, chineseSource)),
|
||||
`${page.route} has a Chinese counterpart but projects English`,
|
||||
).toBe(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -282,39 +287,68 @@ describe('docsPages locale routes', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('projects translated subsystem pages while retaining explicit English fallbacks', () => {
|
||||
it('projects every published subsystem page in Chinese', () => {
|
||||
const rootPages = docsPages.filter(page => (
|
||||
page.locale === 'root' && page.route.startsWith('reference/subsystems/')
|
||||
))
|
||||
const translated = rootPages.filter(page => page.contentLocale === 'zh-CN')
|
||||
const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US')
|
||||
|
||||
expect(translated).toHaveLength(39)
|
||||
expect(translated).toHaveLength(42)
|
||||
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
|
||||
expect(fallbacks.map(page => page.source).sort()).toEqual([
|
||||
'docs/subsystems/commands.md',
|
||||
'docs/subsystems/goal.md',
|
||||
'docs/subsystems/pty.md',
|
||||
])
|
||||
expect(fallbacks).toEqual([])
|
||||
})
|
||||
|
||||
it('publishes the Cordis core API under matching locale structures', () => {
|
||||
const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md', 'inherited.md']
|
||||
const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md']
|
||||
for (const file of files) {
|
||||
const root = docsPages.find(page => page.route === `reference/cordis-api/${file}`)
|
||||
const english = docsPages.find(page => page.route === `en/reference/cordis-api/${file}`)
|
||||
expect(root?.source).toBe(`docs/cordis-api/${file}`)
|
||||
expect(root?.source).toBe(`docs/cordis-api/${file.replace(/\.md$/, '.zh.md')}`)
|
||||
expect(root?.contentLocale).toBe('zh-CN')
|
||||
expect(root?.section).toBe('Cordis API')
|
||||
expect(english?.source).toBe(root?.source)
|
||||
expect(english?.source).toBe(`docs/cordis-api/${file}`)
|
||||
expect(english?.contentLocale).toBe('en-US')
|
||||
expect(english?.section).toBe('Cordis Core API')
|
||||
}
|
||||
})
|
||||
|
||||
it('includes persistence event headings in both locale outlines', () => {
|
||||
const pages = docsPages.filter(page => page.source === 'docs/persistence-catalog.md')
|
||||
it('keeps Cordis inherited on the English fallback in both locales', () => {
|
||||
const pages = docsPages.filter(page => page.route.endsWith('reference/cordis-api/inherited.md'))
|
||||
expect(pages).toHaveLength(2)
|
||||
expect(pages.every(page => page.source === 'docs/cordis-api/inherited.md')).toBe(true)
|
||||
expect(pages.every(page => page.contentLocale === 'en-US')).toBe(true)
|
||||
})
|
||||
|
||||
it('includes persistence event headings in both locale outlines', () => {
|
||||
const pages = docsPages.filter(page => page.route.endsWith('reference/persistence-catalog.md'))
|
||||
expect(pages).toHaveLength(2)
|
||||
expect(pages.map(page => page.source).sort()).toEqual([
|
||||
'docs/persistence-catalog.md',
|
||||
'docs/persistence-catalog.zh.md',
|
||||
])
|
||||
expect(pages.map(page => page.outline)).toEqual(['deep', 'deep'])
|
||||
})
|
||||
|
||||
it('projects reviewed generated counterparts into root locale routes', () => {
|
||||
// module-graph, event-producer-consumer, and graph-atlas are paired but intentionally unpublished.
|
||||
const routes = [
|
||||
'reference/capability-seams.md',
|
||||
'reference/agent-lifecycle.md',
|
||||
'reference/tool-execution-pipeline.md',
|
||||
'reference/config-catalog.md',
|
||||
'reference/tool-catalog.md',
|
||||
'reference/persistence-catalog.md',
|
||||
'reference/cordis-api/context.md',
|
||||
'reference/cordis-api/events.md',
|
||||
'reference/cordis-api/fiber.md',
|
||||
'reference/cordis-api/registry.md',
|
||||
'reference/cordis-api/service.md',
|
||||
]
|
||||
const pages = routes.map(route => docsPages.find(page => page.route === route))
|
||||
expect(pages.every(page => page?.contentLocale === 'zh-CN')).toBe(true)
|
||||
expect(pages.every(page => page?.source.endsWith('.zh.md'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addProjectionFrontmatter', () => {
|
||||
|
||||
@@ -83,6 +83,15 @@ describe('gate graph validation', () => {
|
||||
expect(ids).toContain('public-repository-links')
|
||||
})
|
||||
|
||||
it('keeps native Windows coverage blocking while portability inventory remains observational', () => {
|
||||
const gates = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
|
||||
const byId = new Map(gates.map(subject => [subject.id, subject]))
|
||||
|
||||
expect(byId.get('coverage')?.allowFailure).not.toBe(true)
|
||||
expect(byId.get('coverage-exempt-heavy')?.allowFailure).not.toBe(true)
|
||||
expect(byId.get('duplication')?.allowFailure).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['empty', [], /gate graph has no gates/],
|
||||
['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/],
|
||||
|
||||
@@ -435,6 +435,7 @@ function ciWindowsCompleteGates(): Gate[] {
|
||||
return [
|
||||
pnpmScript('build', 'build'),
|
||||
pnpmScript('windows-site', 'docs:build', { label: 'production site' }),
|
||||
...coverageGates(),
|
||||
...observational,
|
||||
]
|
||||
}
|
||||
@@ -442,7 +443,7 @@ function ciWindowsCompleteGates(): Gate[] {
|
||||
function ciWindowsObservationalGates(): Gate[] {
|
||||
return [
|
||||
...ciStaticGates({ ownsBuild: true }),
|
||||
// Linux owns required lint, coverage, and snapshots; Windows omits those duplicates.
|
||||
// Linux owns required lint and snapshots; Windows omits those duplicates.
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
|
||||
@@ -3,6 +3,12 @@ import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
|
||||
const MAX_CAPTURED_OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
const FIX_FLAGS = new Set(['--fix', '--fix-dangerously', '--fix-suggestions'])
|
||||
|
||||
function isFixInvocation(args: readonly string[]): boolean {
|
||||
return args.some(arg => FIX_FLAGS.has(arg))
|
||||
}
|
||||
|
||||
/** Complete Oxlint child-process arguments and environment. */
|
||||
export interface OxlintInvocation {
|
||||
@@ -32,14 +38,50 @@ export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.Pro
|
||||
}
|
||||
}
|
||||
|
||||
function completeFrom(result: { readonly signal: NodeJS.Signals | null; readonly status: number | null }): void {
|
||||
if (result.signal !== null) {
|
||||
process.kill(process.pid, result.signal)
|
||||
return
|
||||
}
|
||||
process.exitCode = result.status ?? 1
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const invocation = resolveOxlintInvocation(process.argv.slice(2), process.env)
|
||||
const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
|
||||
if (!isFixInvocation(invocation.args)) {
|
||||
const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
|
||||
env: invocation.env,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
if (result.error !== undefined) throw result.error
|
||||
completeFrom(result)
|
||||
return
|
||||
}
|
||||
|
||||
const first = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
|
||||
encoding: 'utf8',
|
||||
env: invocation.env,
|
||||
maxBuffer: MAX_CAPTURED_OUTPUT_BYTES,
|
||||
})
|
||||
if (first.error !== undefined) throw first.error
|
||||
if (first.signal !== null) {
|
||||
completeFrom(first)
|
||||
return
|
||||
}
|
||||
if (first.status === 0) {
|
||||
process.stdout.write(first.stdout)
|
||||
process.stderr.write(first.stderr)
|
||||
process.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Overlapping JS-plugin fixes can expose one more fixable diagnostic after the first pass.
|
||||
const second = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
|
||||
env: invocation.env,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
if (result.error !== undefined) throw result.error
|
||||
process.exitCode = result.status ?? 1
|
||||
if (second.error !== undefined) throw second.error
|
||||
completeFrom(second)
|
||||
}
|
||||
|
||||
const entrypoint = process.argv[1]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,10 +5,9 @@
|
||||
* changed Markdown units, heading sections, whole document), the terminology
|
||||
* rows those changes touch, first-occurrence movement notes, and a digest of
|
||||
* the binding update rules. The unit mapping, mechanical code splice, and
|
||||
* first-occurrence tracking adopt the planner mechanics validated in the
|
||||
* incremental-pipeline work (PR #684). The CLI wrapper is
|
||||
* `scripts/gen-translation-brief.ts`; the workflow that consumes the
|
||||
* briefing is `.agents/skills/dsh-translate-docs/SKILL.md`.
|
||||
* first-occurrence tracking follow the incremental-pipeline planner mechanics.
|
||||
* The CLI wrapper is `scripts/gen-translation-brief.ts`; the workflow that
|
||||
* consumes the briefing is `.agents/skills/dsh-translate-docs/SKILL.md`.
|
||||
*/
|
||||
|
||||
import type { Nodes } from 'mdast'
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url))
|
||||
const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url))
|
||||
const workspaceRoot = fileURLToPath(new URL('../', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx/esm'))
|
||||
const tsxLoader = import.meta.resolve('tsx/esm')
|
||||
const fixtures: string[] = []
|
||||
|
||||
interface Fixture {
|
||||
@@ -119,6 +119,12 @@ const otherSource = baseSource.replace('Beta base.', 'Beta other.')
|
||||
const otherZh = baseZh.replace('乙基础。', '乙对侧。')
|
||||
const mergedSource = currentSource.replace('Beta base.', 'Beta other.')
|
||||
const mergedZh = currentZh.replace('乙基础。', '乙对侧。')
|
||||
const generatedBaseSource = '# Module graph\n\nAlpha base.\n\nBeta base.\n'
|
||||
const generatedBaseZh = '# 模块图\n\n[English](module-graph.md) | 中文\n\n甲基础。\n\n乙基础。\n'
|
||||
const generatedCurrentSource = generatedBaseSource.replace('Alpha base.', 'Alpha current.')
|
||||
const generatedCurrentZh = generatedBaseZh.replace('甲基础。', '甲当前。')
|
||||
const generatedOtherSource = generatedBaseSource.replace('Beta base.', 'Beta other.')
|
||||
const generatedOtherZh = generatedBaseZh.replace('乙基础。', '乙对侧。')
|
||||
const manualBaseSource = baseSource.replace('guide.zh.md', 'manual.zh.md')
|
||||
const manualBaseZh = baseZh.replace('guide.md', 'manual.md')
|
||||
const manualCurrentSource = manualBaseSource.replace('Alpha base.', 'Alpha current.')
|
||||
@@ -225,7 +231,7 @@ function expectMergedPair(fixture: Fixture): void {
|
||||
)
|
||||
}
|
||||
|
||||
describe('translation pairing merge composition', () => {
|
||||
describe('translation pairing merge composition', { timeout: 15_000 }, () => {
|
||||
it('rejects a pairing-record path outside the repository', () => {
|
||||
const fixture = createFixture(false)
|
||||
|
||||
@@ -257,6 +263,60 @@ describe('translation pairing merge composition', () => {
|
||||
expect(result.zhHash).toBe(gitBlobHash(Buffer.from(mergedZh)))
|
||||
})
|
||||
|
||||
it('merges a generated source without an English language switcher', () => {
|
||||
const fixture = createFixture(false)
|
||||
const ancestor = record(fixture.root, 'docs/module-graph.md', generatedBaseSource, generatedBaseZh)
|
||||
const current = record(fixture.root, 'docs/module-graph.md', generatedCurrentSource, generatedCurrentZh)
|
||||
const other = record(fixture.root, 'docs/module-graph.md', generatedOtherSource, generatedOtherZh)
|
||||
|
||||
const result = mergeTranslationPairingRecords(
|
||||
fixture.root,
|
||||
'docs/module-graph.i18n.yaml',
|
||||
ancestor,
|
||||
current,
|
||||
other,
|
||||
)
|
||||
|
||||
expect(result.sourceContent.toString('utf8')).toBe(
|
||||
generatedCurrentSource.replace('Beta base.', 'Beta other.'),
|
||||
)
|
||||
expect(result.zhContent.toString('utf8')).toBe(generatedCurrentZh.replace('乙基础。', '乙对侧。'))
|
||||
})
|
||||
|
||||
it('rejects an authored source without an English language switcher', () => {
|
||||
const fixture = createFixture(false)
|
||||
const source = baseSource.replace('English | [中文](guide.zh.md)\n\n', '')
|
||||
const ancestor = record(fixture.root, 'docs/guide.md', source, baseZh)
|
||||
const current = record(fixture.root, 'docs/guide.md', source, baseZh)
|
||||
const other = record(fixture.root, 'docs/guide.md', source, baseZh)
|
||||
|
||||
expect(() => mergeTranslationPairingRecords(
|
||||
fixture.root,
|
||||
'docs/guide.i18n.yaml',
|
||||
ancestor,
|
||||
current,
|
||||
other,
|
||||
)).toThrow('docs/guide.md clean merge lost its language-switcher link to guide.zh.md')
|
||||
})
|
||||
|
||||
it('rejects generated Chinese content without its English backlink', () => {
|
||||
const fixture = createFixture(false)
|
||||
const zh = generatedBaseZh.replace('[English](module-graph.md) | 中文\n\n', '')
|
||||
const ancestor = record(fixture.root, 'docs/module-graph.md', generatedBaseSource, zh)
|
||||
const current = record(fixture.root, 'docs/module-graph.md', generatedBaseSource, zh)
|
||||
const other = record(fixture.root, 'docs/module-graph.md', generatedBaseSource, zh)
|
||||
|
||||
expect(() => mergeTranslationPairingRecords(
|
||||
fixture.root,
|
||||
'docs/module-graph.i18n.yaml',
|
||||
ancestor,
|
||||
current,
|
||||
other,
|
||||
)).toThrow(
|
||||
'docs/module-graph.zh.md clean merge lost its language-switcher link to module-graph.md',
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves owner-content conflicts for a human', () => {
|
||||
const fixture = createFixture(false)
|
||||
const ancestor = record(fixture.root, 'docs/guide.md', baseSource, baseZh)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
linksTo,
|
||||
isTranslationScopeFile,
|
||||
parseTranslationMarkdown,
|
||||
requiresSourceLanguageSwitcher,
|
||||
translationStructureDiff,
|
||||
translationStructureSignature,
|
||||
} from './translation-pairing.ts'
|
||||
@@ -163,7 +164,7 @@ function loadRecordOwners(
|
||||
function assertMergedPairStructure(paths: TranslationPairPaths, source: Buffer, zh: Buffer): void {
|
||||
const sourceTree = parseTranslationMarkdown(source.toString('utf8'))
|
||||
const zhTree = parseTranslationMarkdown(zh.toString('utf8'))
|
||||
if (!linksTo(sourceTree, basename(paths.zh))) {
|
||||
if (requiresSourceLanguageSwitcher(paths.source) && !linksTo(sourceTree, basename(paths.zh))) {
|
||||
throw new Error(`${paths.source} clean merge lost its language-switcher link to ${basename(paths.zh)}`)
|
||||
}
|
||||
if (!linksTo(zhTree, basename(paths.source))) {
|
||||
|
||||
@@ -4,18 +4,9 @@
|
||||
".agents/notes/implemented/AGENTS.md",
|
||||
".agents/notes/implemented/CLAUDE.md",
|
||||
"docs/AGENTS.md",
|
||||
"docs/agent-lifecycle.md",
|
||||
"docs/capability-seams.md",
|
||||
"docs/config-catalog.md",
|
||||
"docs/cordis-api/",
|
||||
"docs/event-producer-consumer.md",
|
||||
"docs/graph-atlas.md",
|
||||
"docs/cordis-api/inherited.md",
|
||||
"docs/i18n/style-samples.md",
|
||||
"docs/i18n/terminology.md",
|
||||
"docs/i18n/translation-prompt.md",
|
||||
"docs/module-graph.md",
|
||||
"docs/persistence-catalog.md",
|
||||
"docs/tool-catalog.md",
|
||||
"docs/tool-execution-pipeline.md"
|
||||
"docs/i18n/translation-prompt.md"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
parseTranslationPairingCliArgs,
|
||||
parseTranslationPairingManifest,
|
||||
partitionGeneratedRegions,
|
||||
requiresSourceLanguageSwitcher,
|
||||
translationStructureDiff,
|
||||
translationStructureSignature,
|
||||
} from './translation-pairing.ts'
|
||||
@@ -142,6 +143,16 @@ describe('translation pairing manifest', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('translation pairing switchers', () => {
|
||||
it('exempts only paired generated English sources from reciprocal switchers', () => {
|
||||
expect(requiresSourceLanguageSwitcher('docs/config-catalog.md')).toBe(false)
|
||||
expect(requiresSourceLanguageSwitcher('docs/cordis-api/context.md')).toBe(false)
|
||||
expect(requiresSourceLanguageSwitcher('docs/cordis-api/inherited.md')).toBe(false)
|
||||
expect(requiresSourceLanguageSwitcher('docs/architecture.md')).toBe(true)
|
||||
expect(requiresSourceLanguageSwitcher('packages/core/session/README.md')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('translation pairing records', () => {
|
||||
const paths = translationPairPaths('docs/foo.md')
|
||||
const record = {
|
||||
|
||||
@@ -311,6 +311,28 @@ export function linksTo(tree: Nodes, target: string): boolean {
|
||||
return found
|
||||
}
|
||||
|
||||
/** Generated English sources cannot carry a switcher without making their generator stale. */
|
||||
export function requiresSourceLanguageSwitcher(source: string): boolean {
|
||||
return ![
|
||||
'docs/agent-lifecycle.md',
|
||||
'docs/capability-seams.md',
|
||||
'docs/config-catalog.md',
|
||||
'docs/cordis-api/context.md',
|
||||
'docs/cordis-api/events.md',
|
||||
'docs/cordis-api/fiber.md',
|
||||
// Excluded from pairing, but kept here for generated-category completeness and direct spec coverage.
|
||||
'docs/cordis-api/inherited.md',
|
||||
'docs/cordis-api/registry.md',
|
||||
'docs/cordis-api/service.md',
|
||||
'docs/event-producer-consumer.md',
|
||||
'docs/graph-atlas.md',
|
||||
'docs/module-graph.md',
|
||||
'docs/persistence-catalog.md',
|
||||
'docs/tool-catalog.md',
|
||||
'docs/tool-execution-pipeline.md',
|
||||
].includes(source)
|
||||
}
|
||||
|
||||
/** Collect the ordered structural signature, skipping one switcher target. */
|
||||
export function translationStructureSignature(tree: Nodes, switcherTarget: string): TranslationStructureSignature {
|
||||
const sig: TranslationStructureSignature = { headings: [], code: [], tables: [], lists: [], links: [] }
|
||||
|
||||
@@ -114,12 +114,12 @@
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "InboxTarget",
|
||||
"source": "packages/core/agent/src/inbox.ts"
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "CancelOptions",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
@@ -129,22 +129,22 @@
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "Agent",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "PreStepDecision",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "RequestErrorAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "SessionStartSource",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/scope.md",
|
||||
@@ -960,6 +960,11 @@
|
||||
"symbol": "FsVersion",
|
||||
"source": "packages/fs/fs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/filesystem.md",
|
||||
"symbol": "FsObservation",
|
||||
"source": "packages/fs/fs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/filesystem.md",
|
||||
"symbol": "FsInfo",
|
||||
@@ -1055,6 +1060,11 @@
|
||||
"symbol": "SkillLookupOptions",
|
||||
"source": "packages/skill/skill/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/skills.md",
|
||||
"symbol": "SkillViewOptions",
|
||||
"source": "packages/skill/skill/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/skills.md",
|
||||
"symbol": "SkillProviderObservation",
|
||||
@@ -1693,12 +1703,12 @@
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "AgentStatus",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "AgentOptions",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Enforce intra-package domain layering inside `packages/client/*\/src/client/`.
|
||||
* verify-module-graph covers package-level edges; this gate covers the
|
||||
* directory level the future package split will land on: domain directories
|
||||
* may import `contract/` and never each other, and only the assembly point
|
||||
* (`apply.ts` / `index.ts`) may import across domains.
|
||||
* directory level: domain directories may import `contract/` and never each
|
||||
* other, and only the assembly point (`apply.ts` / `index.ts`) may import
|
||||
* across domains.
|
||||
*
|
||||
* Layer model (lower may not import higher):
|
||||
* 0 contract/ shared contract surface (types + slot declarations)
|
||||
* 1 <domain>/ + service domain implementations (skeleton/, chat/, ...)
|
||||
* 2 apply.ts, index.ts assembly point and re-export shell
|
||||
*
|
||||
* Not yet wired into the gate sequence (loose-gate window); run directly:
|
||||
* Run directly:
|
||||
* pnpm exec tsx scripts/verify-client-domain-graph.ts
|
||||
*/
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ for (const file of files) {
|
||||
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:')
|
||||
@@ -87,6 +88,76 @@ if (errors.length > 0) {
|
||||
console.log(`verify-cordis-config: ${files.length} config files passed.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* No shipped agent preset may repeat a row the host composition still runs.
|
||||
*
|
||||
* A preset contributes what ONE session adds to the host's registries. A row
|
||||
* active on both planes is therefore mounted twice — once per process and once
|
||||
* per session — and what that costs depends on what the row does: a provider
|
||||
* behind an `isolate` realm shadows the host's for its own consumers, so a host
|
||||
* contributor to that service reaches nobody; a row that registers into a host
|
||||
* singleton registers once per live session, so the second one collides.
|
||||
*
|
||||
* Both have happened. `bash-env` in a preset realm left `DSH_WEB_URL` reaching
|
||||
* no shell, and `tool-subagent-report` handed every child `report` once per live
|
||||
* session until the second registration threw. Neither changes a tool catalog,
|
||||
* so no catalog assertion can see them — and the shipped presets are near-copies
|
||||
* of each other, so a fix applied to three of four is the normal failure.
|
||||
* @returns one diagnostic per preset row that is also active on the host plane.
|
||||
*/
|
||||
function validatePresetPlaneSeparation(): string[] {
|
||||
const problems: string[] = []
|
||||
// The shipped Web surface is two bundle patch layers over an empty root.
|
||||
const hostFile = 'packages/bundle/base/cordis.patch.yml'
|
||||
const overlayFile = 'packages/bundle/web-app/cordis.patch.yml'
|
||||
const hostRows = rowIds(hostFile)
|
||||
const overlay = loadEntries(overlayFile)
|
||||
const disabled = new Set<string>()
|
||||
for (const entry of overlay) {
|
||||
if (!isRecord(entry)) continue
|
||||
if (entry.disabled === true && typeof entry.id === 'string') disabled.add(entry.id)
|
||||
}
|
||||
// The overlay's own inserts are host-plane too; its disables take them back out.
|
||||
const active = new Set([...hostRows, ...rowIds(overlayFile)].filter(id => !disabled.has(id)))
|
||||
for (const file of globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root })) {
|
||||
for (const id of rowIds(file)) {
|
||||
if (!active.has(id)) continue
|
||||
problems.push(
|
||||
`${file}: row "${id}" is also active in the host composition; `
|
||||
+ 'a row belongs to exactly one plane',
|
||||
)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
/** Every entry of one config file, or an empty list when it is not an entry array. */
|
||||
function loadEntries(file: string): unknown[] {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
return isUnknownArray(document) ? document : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Row ids declared anywhere in one config file, including inside group `config`
|
||||
* lists — a preset nests most of its rows in `isolate` groups.
|
||||
* @param file - repository-relative config path.
|
||||
* @returns the declared ids.
|
||||
*/
|
||||
function rowIds(file: string): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
const walk = (value: unknown): void => {
|
||||
if (isUnknownArray(value)) {
|
||||
for (const item of value) walk(item)
|
||||
return
|
||||
}
|
||||
if (!isRecord(value)) return
|
||||
if (typeof value.id === 'string' && typeof value.name === 'string') ids.add(value.id)
|
||||
for (const child of Object.values(value)) walk(child)
|
||||
}
|
||||
walk(loadEntries(file))
|
||||
return ids
|
||||
}
|
||||
|
||||
function validateEntry(value: unknown, file: string, path: string): void {
|
||||
if (!isRecord(value)) {
|
||||
errors.push(`${file}${path}: entry must be an object`)
|
||||
|
||||
@@ -47,7 +47,11 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/bash/bash-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-bash.' },
|
||||
'packages/bash/pwsh-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-pwsh.' },
|
||||
'packages/code-runtime/code-runtime': { kind: 'indirect', reason: 'The service interface delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/core/agent-tool-mode': { kind: 'indirect', reason: 'The row only selects between the two projections dsh-tools owns; it registers no prompt, schema, or result of its own.' },
|
||||
'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' },
|
||||
'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' },
|
||||
'packages/preset/agent-presets': { kind: 'indirect', reason: 'The mount installs a preset\'s own plugins, which own every model-facing registration it makes visible.' },
|
||||
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
|
||||
'packages/typert/loader': { kind: 'none', reason: 'Loader integration only registers generated artifacts; consumers own any model-visible projection.' },
|
||||
'packages/e2b/e2b': { kind: 'none', reason: 'The shared remote-runtime owner registers no model context; provider adapters and consumers own rendered effects.' },
|
||||
@@ -69,7 +73,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/client/ui-task': { kind: 'none', reason: 'Browser-side read-only projection of ctx.tasks records; dsh-tool-tasks owns the model-facing surface.' },
|
||||
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
|
||||
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the host snapshots the target at the next prompt-assembly boundary and owns the model-visible effect.' },
|
||||
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the Host snapshots the selection at the next prompt-assembly boundary and owns the model-visible effect.' },
|
||||
'packages/client/ui-goal': { kind: 'indirect', reason: 'The strip verbs route goal.* mutations; the host GoalService owns the model-visible goal/change context message.' },
|
||||
'packages/client/ui-permission': { kind: 'indirect', reason: 'The picker submits the host /permission command; the knob events it appends own the model-visible effect through the sandbox/approval consumers.' },
|
||||
'packages/client/ui-plan': { kind: 'indirect', reason: 'The chip dispatches /plan off; dsh-plan-mode owns the model-visible policy, exit tool, and logged state.' },
|
||||
@@ -95,7 +99,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers no model surface.' },
|
||||
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers no model surface.' },
|
||||
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model surface.' },
|
||||
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base/web bundles.' },
|
||||
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base and headless bundles.' },
|
||||
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
|
||||
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
|
||||
'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
|
||||
@@ -104,6 +108,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/e2b/subprocess-e2b': { kind: 'indirect', reason: 'The remote spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
|
||||
'packages/sandbox/sandbox-windows-acl': { kind: 'indirect', reason: 'The provider backend delegates model rendering to the bash/pwsh sandbox executors and their tools.' },
|
||||
'packages/scaffold/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
|
||||
'packages/scaffold/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
|
||||
'packages/scaffold/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
parseTranslationPairingCliArgs,
|
||||
parseTranslationPairingManifest,
|
||||
partitionGeneratedRegions,
|
||||
requiresSourceLanguageSwitcher,
|
||||
isTranslationScopeFile,
|
||||
TRANSLATION_SCOPE_GLOB_EXCLUDES,
|
||||
translationStructureDiff,
|
||||
@@ -254,7 +255,7 @@ for (const source of [...pairAnchors].sort()) {
|
||||
if (!linksTo(zhTree, basename(source))) {
|
||||
errors.push(`${zh}: missing language switcher — no link to ${basename(source)}`)
|
||||
}
|
||||
if (!linksTo(sourceTree, basename(zh))) {
|
||||
if (requiresSourceLanguageSwitcher(source) && !linksTo(sourceTree, basename(zh))) {
|
||||
errors.push(`${source}: missing language switcher — no link back to ${basename(zh)}`)
|
||||
}
|
||||
for (const divergence of translationStructureDiff(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# Run the blocking Windows gates (workspace build, production site) with real
|
||||
# win-x64 Node.js under Wine — the same script the pull-request `windows` job
|
||||
# in ci.yml executes and the optional local gate `pnpm run check:windows-wine`
|
||||
# wraps. Owning rationale, fidelity limits, and measured timings:
|
||||
# .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
|
||||
# wraps. Owning rationale and fidelity limits:
|
||||
# .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
|
||||
#
|
||||
# The working tree is never mutated: tracked plus untracked-unignored files
|
||||
# are snapshotted into a scratch directory, the Wine-specific pnpm overrides
|
||||
@@ -74,19 +74,54 @@ trap cleanup EXIT
|
||||
mkdir -p "$cache_dir" "$scratch/logs"
|
||||
|
||||
# ---- provision Windows Node, boot Wine, snapshot + install concurrently ----
|
||||
curl_metadata_args=(
|
||||
--fail --silent --show-error --location
|
||||
--retry 3 --retry-all-errors --retry-delay 2
|
||||
--http1.1 --connect-timeout 10 --max-time 30 --retry-max-time 120
|
||||
)
|
||||
|
||||
download_node_archive() {
|
||||
local version="$1" output="$2" attempt status=0
|
||||
local archive="node-$version-win-x64.zip"
|
||||
local primary_url="https://nodejs.org/dist/$version/$archive"
|
||||
local mirror_url="https://npmmirror.com/mirrors/node/$version/$archive"
|
||||
|
||||
if curl --fail --silent --show-error --location --http1.1 \
|
||||
--connect-timeout 10 --max-time 300 --speed-limit 1024 --speed-time 30 \
|
||||
-o "$output" "$primary_url"; then
|
||||
return 0
|
||||
fi
|
||||
echo 'wine-windows-gates: nodejs.org archive transfer stalled; resuming from the checksum-untrusted transport mirror' >&2
|
||||
for attempt in 1 2 3; do
|
||||
if curl --fail --silent --show-error --location --http1.1 \
|
||||
--continue-at - --connect-timeout 10 --max-time 300 \
|
||||
--speed-limit 1024 --speed-time 30 \
|
||||
-o "$output" "$mirror_url"; then
|
||||
return 0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
(( attempt < 3 )) || break
|
||||
echo "wine-windows-gates: mirror transfer failed (exit $status) on attempt $attempt; resuming partial download" >&2
|
||||
done
|
||||
return "$status"
|
||||
}
|
||||
|
||||
provision_node() {
|
||||
# Latest release of the primary line, checksum-verified against the same
|
||||
# dist directory. Offline runs fall back to the newest cached zip, loudly.
|
||||
# dist directory. Bound and retry every transfer so a stalled nodejs.org
|
||||
# response cannot consume the entire CI job. Offline runs fall back to the
|
||||
# newest cached zip, loudly.
|
||||
local version zip
|
||||
version="$(curl -fsSL --max-time 30 https://nodejs.org/dist/index.json 2> /dev/null \
|
||||
version="$(curl "${curl_metadata_args[@]}" https://nodejs.org/dist/index.json 2> /dev/null \
|
||||
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const v=JSON.parse(d).find(r=>r.version.startsWith('v$node_major.'));if(v)console.log(v.version)})" \
|
||||
|| true)"
|
||||
if [ -n "$version" ]; then
|
||||
zip="$cache_dir/node-$version-win-x64.zip"
|
||||
if [ ! -f "$zip" ]; then
|
||||
curl -fsSL -o "$zip.tmp" "https://nodejs.org/dist/$version/node-$version-win-x64.zip"
|
||||
download_node_archive "$version" "$zip.tmp"
|
||||
local expected
|
||||
expected="$(curl -fsSL "https://nodejs.org/dist/$version/SHASUMS256.txt" \
|
||||
expected="$(curl "${curl_metadata_args[@]}" "https://nodejs.org/dist/$version/SHASUMS256.txt" \
|
||||
| awk -v a="node-$version-win-x64.zip" '$2 == a { print $1; exit }')"
|
||||
[ -n "$expected" ] || { echo "wine-windows-gates: no SHASUMS256 entry for node-$version-win-x64.zip" >&2; exit 1; }
|
||||
verify_sha256 "$expected" "$zip.tmp"
|
||||
|
||||
Reference in New Issue
Block a user