diff --git a/AGENTS.md b/AGENTS.md index 3c02822625..aafc47f93d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,7 +119,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, - **Trust TypeScript at typed same-process boundaries.** Do not add runtime validation, fallback behavior, or hostile-input tests solely for values the static interface requires; validate at parser/config, queued, model/tool JSON, durable/file, worker, process, and wire boundaries. - **Source plane vs artifact plane, never mixed.** Static gates and tests resolve workspace imports through tsconfig `paths` to `src` and pass on a clean tree; gates consuming built `lib/` declare that dependency ([layout](docs/development.md#typescript-project-layout)). - **Keep compiler faces explicit.** A package with both Host and Client programs exposes face-specific leaf configs and a solution-only root; repo-wide programs seed a face config, never the root solution ([layout](docs/development.md#typescript-project-layout)). -- **An empty `catch` names the swallowed error** and why; keep its `try` to one statement. +- **An empty `catch` names the error** and why; keep its `try` to one statement. - **Keep comments local.** Do not restate code, expand unrelated comments, or explain distant behavior without local need ([rationale](.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md)). - **Ban `prove` + `nance`** ([rule](.agents/notes/implemented/process/2026-08-26-ban-ambiguous-origin-label.md)). - **Prefer symmetry for parallel values**; unexplained asymmetry usually signals a missed extraction. diff --git a/packages/client/ui-conversation/src/client/contract/records.ts b/packages/client/ui-conversation/src/client/contract/records.ts index 48b67d54a3..f639a1f16a 100644 --- a/packages/client/ui-conversation/src/client/contract/records.ts +++ b/packages/client/ui-conversation/src/client/contract/records.ts @@ -73,7 +73,7 @@ export interface AssistantMessageNode { step: number blocks: readonly AssistantBlock[] usage?: unknown - providerInfo?: AssistantProviderMetadataView + providerMetadata?: AssistantProviderMetadataView requestConfig?: AssistantRequestConfig /** Timing derived from the recorded step/chunk/message event sequence. */ timing?: AssistantTiming diff --git a/packages/client/ui-conversation/src/client/contract/request-inspection.ts b/packages/client/ui-conversation/src/client/contract/request-inspection.ts index 8ff135b657..3f0d785b8e 100644 --- a/packages/client/ui-conversation/src/client/contract/request-inspection.ts +++ b/packages/client/ui-conversation/src/client/contract/request-inspection.ts @@ -133,7 +133,7 @@ interface RequestViewBase { error?: string /** Stable provider code for localized presentation of known failures. */ errorCode?: string - providerInfo?: AssistantProviderMetadataView + providerMetadata?: AssistantProviderMetadataView requestConfig?: AssistantRequestConfig usage?: unknown /** Assistant message or compaction summary sequence produced by this request. */ diff --git a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx index 2884d4f52e..4120779d9d 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx @@ -250,8 +250,8 @@ export function TrajectoryView({ const turn = request?.turn ?? node?.turn const step = request?.step ?? node?.step if (turn === undefined || step === undefined) continue - const provider = request?.providerInfo?.provider ?? node?.providerInfo?.provider - const model = request?.providerInfo?.model ?? node?.providerInfo?.model + const provider = request?.providerMetadata?.provider ?? node?.providerMetadata?.provider + const model = request?.providerMetadata?.model ?? node?.providerMetadata?.model const requestConfig = request?.requestConfig ?? node?.requestConfig numbered.push({ seq: entry.seq, @@ -292,12 +292,12 @@ export function TrajectoryView({ ...(request.error === undefined ? {} : { error: request.error }), ...(request.errorCode === undefined ? {} : { errorCode: request.errorCode }), resultSeq: request.startSeq, - ...(request.providerInfo?.provider === undefined + ...(request.providerMetadata?.provider === undefined ? {} - : { provider: request.providerInfo.provider }), - ...(request.providerInfo?.model === undefined + : { provider: request.providerMetadata.provider }), + ...(request.providerMetadata?.model === undefined ? {} - : { model: request.providerInfo.model }), + : { model: request.providerMetadata.model }), ...(request.requestConfig === undefined ? {} : { requestConfig: request.requestConfig }), ...(usage === undefined ? {} : { usage }), ...(cumulativeUsage === undefined ? {} : { cumulativeUsage }), diff --git a/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts b/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts index 792eb63bae..6b1d6cd004 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-assistant-definition.ts @@ -252,7 +252,7 @@ function finalNode( step: state.step, blocks: toAssistantBlocks(event.data.message.content), usage: event.data.usage, - providerInfo: { + providerMetadata: { provider: event.data.message.source.provider, model: event.data.message.source.model, }, @@ -309,7 +309,7 @@ function assistantRequest( ? {} : { resultSeq: node.seq, - ...(node.providerInfo === undefined ? {} : { providerInfo: node.providerInfo }), + ...(node.providerMetadata === undefined ? {} : { providerMetadata: node.providerMetadata }), }), ...(state.usage === undefined ? {} : { usage: state.usage }), } diff --git a/packages/client/ui-trajectory/src/client/trajectory-compaction-definition.ts b/packages/client/ui-trajectory/src/client/trajectory-compaction-definition.ts index 8e45be67a9..32916af141 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-compaction-definition.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-compaction-definition.ts @@ -64,7 +64,7 @@ function requestFromState( resultSeq: summary.seq, summary: summary.data.summary, ...(summary.data.rawOutput === undefined ? {} : { rawOutput: summary.data.rawOutput }), - providerInfo: { provider: summary.data.provider, model: summary.data.model }, + providerMetadata: { provider: summary.data.provider, model: summary.data.model }, requestConfig: { provider: summary.data.provider, model: summary.data.model, diff --git a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts index 6e2c89e911..4f900802a2 100644 --- a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts +++ b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts @@ -397,7 +397,7 @@ describe('Trajectory conversation Definitions', () => { purpose: 'assistant', resultSeq: 3, status: 'error', - providerInfo: { provider: 'test', model: 'test' }, + providerMetadata: { provider: 'test', model: 'test' }, }]) }) diff --git a/scripts/verify-concrete-terms.spec.ts b/scripts/verify-concrete-terms.spec.ts index 6f7b6fa500..9932703d0b 100644 --- a/scripts/verify-concrete-terms.spec.ts +++ b/scripts/verify-concrete-terms.spec.ts @@ -1,5 +1,8 @@ +import { mkdtempSync, rmSync, symlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import { findConcreteTermViolations } from './verify-concrete-terms.ts' +import { findConcreteTermViolations, readTrackedSource } from './verify-concrete-terms.ts' const blockedTerm = 'prove' + 'nance' @@ -21,6 +24,27 @@ describe('concrete terminology policy', () => { .toEqual([{ file: 'packages/example/src/source.ts', line: 1 }]) }) + it('normalizes compatibility characters before scanning', () => { + const fullwidthTerm = blockedTerm.split('') + .map(character => String.fromCodePoint(character.charCodeAt(0) + 0xfee0)) + .join('') + expect(findConcreteTermViolations('packages/example/src/source.ts', fullwidthTerm)) + .toEqual([{ file: 'packages/example/src/source.ts', line: 1 }]) + }) + + it.skipIf(process.platform === 'win32')('reads the target of a dangling tracked symlink', () => { + const repoRoot = mkdtempSync(join(tmpdir(), 'dsh-concrete-terms-')) + try { + symlinkSync(`../${blockedTerm}-target`, join(repoRoot, 'tracked-link')) + expect(findConcreteTermViolations( + 'tracked-link', + readTrackedSource(repoRoot, 'tracked-link') ?? '', + )).toEqual([{ file: 'tracked-link', line: 1 }]) + } finally { + rmSync(repoRoot, { recursive: true, force: true }) + } + }) + it('accepts exact replacement terms', () => { expect(findConcreteTermViolations( 'packages/example/src/origin.ts', diff --git a/scripts/verify-concrete-terms.ts b/scripts/verify-concrete-terms.ts index 4f16c79c86..b2b59eef40 100644 --- a/scripts/verify-concrete-terms.ts +++ b/scripts/verify-concrete-terms.ts @@ -1,7 +1,7 @@ /** Reject one ambiguous origin label from maintained tracked files. */ import { execFileSync } from 'node:child_process' -import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs' +import { lstatSync, readFileSync, readlinkSync } from 'node:fs' import { resolve } from 'node:path' import { pathToFileURL } from 'node:url' @@ -55,10 +55,16 @@ function trackedFiles(repoRoot: string): string[] { return files } -function readTrackedSource(repoRoot: string, file: string): string | undefined { +/** + * Read one tracked file without following a symlink to its target. + * @param repoRoot - Repository root containing the tracked path. + * @param file - Repository-relative tracked path. + * @returns File text, the symlink target, or undefined when the path is absent or not a file. + */ +export function readTrackedSource(repoRoot: string, file: string): string | undefined { const path = resolve(repoRoot, file) - if (!existsSync(path)) return undefined - const stat = lstatSync(path) + const stat = lstatSync(path, { throwIfNoEntry: false }) + if (stat === undefined) return undefined if (stat.isSymbolicLink()) return readlinkSync(path) return stat.isFile() ? readFileSync(path, 'utf8') : undefined } diff --git a/vitest.config.ts b/vitest.config.ts index e11bac9c82..8556b58442 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -243,7 +243,6 @@ export default defineConfig({ 'packages/client/ui-chat/src/client/conversation-nodes/*', 'packages/client/ui-chat/src/client/details/*', 'packages/client/ui-chat/src/client/model/*', - 'packages/client/ui-chat/src/client/contract/context-producer.ts', 'packages/client/ui-chat/src/client/contract/snapshot.ts', 'packages/client/ui-chat/src/client/historical-images.ts', 'packages/client/ui-primitives/src/DisclosureRow.tsx',