chore(web): remove command description gate

This commit is contained in:
Kaige-Gao
2026-09-08 13:01:29 +08:00
parent 8b94ed9c21
commit ea60e1c8a7
5 changed files with 8 additions and 314 deletions
-1
View File
@@ -110,7 +110,6 @@
"verify-npm-install-layout": "tsx scripts/verify-npm-install-layout.ts",
"verify-client-packages": "tsx scripts/verify-client-packages.ts",
"verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts",
"verify-host-command-descriptions": "tsx scripts/verify-host-command-descriptions.ts",
"verify-no-bare-dispatcher": "tsx scripts/verify-no-bare-dispatcher.ts",
"verify-vendored-links": "tsx scripts/verify-vendored-links.ts",
"verify-cordis-config": "tsx scripts/verify-cordis-config.ts",
+2 -2
View File
@@ -184,8 +184,8 @@ describe('gate graph validation', () => {
expect(ids).toEqual([
'rescope-vendor', 'publint', 'constraints', 'package-dependencies', 'application-entrypoints',
'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'host-command-descriptions',
'no-bare-dispatcher', 'cordis-config', 'runtime-closure', 'vendored-links',
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'no-bare-dispatcher', 'cordis-config',
'runtime-closure', 'vendored-links',
])
expect(defaultConcurrency('hygiene', ids.length, 8)).toEqual({
workers: 4,
+6 -11
View File
@@ -290,15 +290,6 @@ export function gatesForMode(selected: Mode): Gate[] {
}
}
function clientStaticGates(): Gate[] {
return [
pnpmScript('client-packages', 'verify-client-packages', { label: 'client packages' }),
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
pnpmScript('host-command-descriptions', 'verify-host-command-descriptions', { label: 'Host command descriptions' }),
pnpmScript('no-bare-dispatcher', 'verify-no-bare-dispatcher', { label: 'proxy-aware dispatchers' }),
]
}
function ciSharedStaticGates(): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
@@ -311,7 +302,9 @@ function ciSharedStaticGates(): Gate[] {
pnpmScript('optional-dependency-imports', 'verify-optional-dependency-imports', {
label: 'optional dependency imports',
}),
...clientStaticGates(),
pnpmScript('client-packages', 'verify-client-packages', { label: 'client packages' }),
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
pnpmScript('no-bare-dispatcher', 'verify-no-bare-dispatcher', { label: 'proxy-aware dispatchers' }),
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
]
}
@@ -705,7 +698,9 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
pnpmScript('optional-dependency-imports', 'verify-optional-dependency-imports', {
label: 'optional dependency imports',
}),
...clientStaticGates(),
pnpmScript('client-packages', 'verify-client-packages', { label: 'client packages' }),
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
pnpmScript('no-bare-dispatcher', 'verify-no-bare-dispatcher', { label: 'proxy-aware dispatchers' }),
]
}
@@ -1,73 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
findHostCommandRegistrations,
hostCommandDescriptionViolations,
} from './verify-host-command-descriptions.ts'
const locales = `
export const en = {
'description.compact': 'Compact older conversation history',
'description.plan': 'Enter or leave plan mode',
}
`
const service = `
const HOST_DESCRIPTION_KEYS = new Map<string, string>([
['compact', 'description.compact'],
['plan', 'description.plan'],
])
`
describe('Host command description alignment', () => {
it('discovers direct object registrations through any commands receiver', () => {
const registrations = findHostCommandRegistrations('packages/example/src/index.ts', `
yield ctx.commands.register({ name: 'compact', description: 'Compact older conversation history', handler })
commandCtx.commands.register({ name: \`plan\`, description: \`Enter or leave plan mode\`, handler })
commandUi.register({ name: 'client-only', description: 'Client contribution' })
ctx.commands.register(definition)
`)
expect(registrations).toEqual([
{
file: 'packages/example/src/index.ts',
line: 2,
name: 'compact',
description: 'Compact older conversation history',
},
{
file: 'packages/example/src/index.ts',
line: 3,
name: 'plan',
description: 'Enter or leave plan mode',
},
{ file: 'packages/example/src/index.ts', line: 5 },
])
})
it('accepts exact Host, English dictionary, and allowlist alignment', () => {
const registrations = findHostCommandRegistrations('packages/example/src/index.ts', `
ctx.commands.register({ name: 'compact', description: 'Compact older conversation history' })
ctx.commands.register({ name: 'plan', description: 'Enter or leave plan mode' })
`)
expect(hostCommandDescriptionViolations(registrations, locales, service)).toEqual([])
})
it('rejects non-literals, missing allowlist entries, drift, duplicates, and stale entries', () => {
const registrations = [
...findHostCommandRegistrations('packages/example/src/a.ts', `
ctx.commands.register({ name: commandName, description: 'dynamic' })
ctx.commands.register({ name: 'compact', description: 'Changed upstream' })
ctx.commands.register({ name: 'compact', description: 'Changed upstream' })
`),
]
expect(hostCommandDescriptionViolations(
registrations,
'export const en = { \'description.compact\': \'Old copy\' }',
'const HOST_DESCRIPTION_KEYS = new Map([[\'compact\', \'description.compact\'], [\'stale\', \'description.stale\']])',
)).toEqual([
'packages/example/src/a.ts:2 Host command name and description must be string literals.',
'packages/example/src/a.ts:4 duplicates /compact, first registered at packages/example/src/a.ts:3.',
'packages/client/ui-commands/src/client/locales.ts description.compact must equal the Host description "Changed upstream".',
'packages/client/ui-commands/src/client/service.ts HOST_DESCRIPTION_KEYS contains stale /stale.',
])
})
})
-227
View File
@@ -1,227 +0,0 @@
/**
* Keep first-party Host command descriptions aligned with the Client's
* English locale dictionary and localization allowlist.
*/
import { globSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ts from 'typescript'
const root = resolve(import.meta.dirname, '..')
const MINIMUM_HOST_SOURCES = 1_500
const MINIMUM_HOST_COMMANDS = 6
const LOCALES_FILE = 'packages/client/ui-commands/src/client/locales.ts'
const SERVICE_FILE = 'packages/client/ui-commands/src/client/service.ts'
/** One literal `*.commands.register({ name, description })` call. */
export interface HostCommandRegistration {
file: string
line: number
name?: string
description?: string
}
function stringProperty(object: ts.ObjectLiteralExpression, key: string): string | undefined {
for (const property of object.properties) {
if (!ts.isPropertyAssignment(property)) continue
const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
? property.name.text
: undefined
if (name !== key) continue
return ts.isStringLiteral(property.initializer) || ts.isNoSubstitutionTemplateLiteral(property.initializer)
? property.initializer.text
: undefined
}
return undefined
}
function isCommandsRegister(call: ts.CallExpression): boolean {
const register = call.expression
return ts.isPropertyAccessExpression(register)
&& register.name.text === 'register'
&& ts.isPropertyAccessExpression(register.expression)
&& register.expression.name.text === 'commands'
}
function unwrapExpression(expression: ts.Expression): ts.Expression {
let current = expression
while (ts.isParenthesizedExpression(current)
|| ts.isAsExpression(current)
|| ts.isSatisfiesExpression(current)
|| ts.isTypeAssertionExpression(current)) {
current = current.expression
}
return current
}
/**
* Find literal Host command registrations in one production source.
* @param file - Repository-relative path used in diagnostics.
* @param sourceText - TypeScript source.
* @returns Registration calls in source order; missing literal fields remain undefined.
*/
export function findHostCommandRegistrations(file: string, sourceText: string): HostCommandRegistration[] {
const source = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
const registrations: HostCommandRegistration[] = []
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node) && isCommandsRegister(node)) {
const argument = node.arguments[0]
const position = source.getLineAndCharacterOfPosition(node.getStart(source))
const registration: HostCommandRegistration = { file, line: position.line + 1 }
if (argument !== undefined && ts.isObjectLiteralExpression(argument)) {
const name = stringProperty(argument, 'name')
const description = stringProperty(argument, 'description')
if (name !== undefined) registration.name = name
if (description !== undefined) registration.description = description
}
registrations.push(registration)
}
ts.forEachChild(node, visit)
}
visit(source)
return registrations
}
/** Read string properties from a named object-literal variable. */
function objectDictionary(sourceText: string, variableName: string): Map<string, string> {
const source = ts.createSourceFile(variableName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
const values = new Map<string, string>()
const visit = (node: ts.Node): void => {
if (ts.isVariableDeclaration(node)
&& ts.isIdentifier(node.name)
&& node.name.text === variableName
&& node.initializer !== undefined) {
const initializer = unwrapExpression(node.initializer)
if (!ts.isObjectLiteralExpression(initializer)) return
for (const property of initializer.properties) {
if (!ts.isPropertyAssignment(property)) continue
const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
? property.name.text
: undefined
const value = ts.isStringLiteral(property.initializer) || ts.isNoSubstitutionTemplateLiteral(property.initializer)
? property.initializer.text
: undefined
if (key !== undefined && value !== undefined) values.set(key, value)
}
}
ts.forEachChild(node, visit)
}
visit(source)
return values
}
/** Read `[command, localeKey]` tuples from `HOST_DESCRIPTION_KEYS`. */
function hostDescriptionKeys(sourceText: string): Map<string, string> {
const source = ts.createSourceFile(SERVICE_FILE, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
const values = new Map<string, string>()
const visit = (node: ts.Node): void => {
if (ts.isVariableDeclaration(node)
&& ts.isIdentifier(node.name)
&& node.name.text === 'HOST_DESCRIPTION_KEYS'
&& node.initializer !== undefined
&& ts.isNewExpression(node.initializer)) {
const entries = node.initializer.arguments?.[0]
if (entries !== undefined && ts.isArrayLiteralExpression(entries)) {
for (const entry of entries.elements) {
if (!ts.isArrayLiteralExpression(entry) || entry.elements.length !== 2) continue
const name = entry.elements[0]
const key = entry.elements[1]
if (name !== undefined && key !== undefined && ts.isStringLiteral(name) && ts.isStringLiteral(key)) {
values.set(name.text, key.text)
}
}
}
}
ts.forEachChild(node, visit)
}
visit(source)
return values
}
/**
* Compare Host registrations with the Client's English strings and allowlist.
* @param registrations - Syntax-aware production registrations.
* @param localesSource - `ui-commands` locale dictionary source.
* @param serviceSource - `ui-commands` service source owning the allowlist.
* @returns Human-readable violations.
*/
export function hostCommandDescriptionViolations(
registrations: readonly HostCommandRegistration[],
localesSource: string,
serviceSource: string,
): string[] {
const violations: string[] = []
const commands = new Map<string, HostCommandRegistration>()
for (const registration of registrations) {
const where = `${registration.file}:${registration.line}`
if (registration.name === undefined || registration.description === undefined) {
violations.push(`${where} Host command name and description must be string literals.`)
continue
}
const prior = commands.get(registration.name)
if (prior !== undefined) {
violations.push(`${where} duplicates /${registration.name}, first registered at ${prior.file}:${prior.line}.`)
continue
}
commands.set(registration.name, registration)
}
const english = objectDictionary(localesSource, 'en')
const keys = hostDescriptionKeys(serviceSource)
for (const [name, registration] of commands) {
const expectedKey = `description.${name}`
const key = keys.get(name)
if (key !== expectedKey) {
violations.push(`${registration.file}:${registration.line} /${name} must map to ${expectedKey} in HOST_DESCRIPTION_KEYS.`)
continue
}
const localizedEnglish = english.get(key)
if (localizedEnglish !== registration.description) {
violations.push(
`${LOCALES_FILE} ${key} must equal the Host description ${JSON.stringify(registration.description)}.`,
)
}
}
for (const name of keys.keys()) {
if (!commands.has(name)) violations.push(`${SERVICE_FILE} HOST_DESCRIPTION_KEYS contains stale /${name}.`)
}
return violations
}
function sourceFiles(): string[] {
return globSync('packages/*/*/src/**/*.{ts,tsx}', { cwd: root })
.map(file => file.replaceAll('\\', '/'))
.filter(file => !file.endsWith('.d.ts'))
.sort()
}
function main(): void {
const files = sourceFiles()
if (files.length < MINIMUM_HOST_SOURCES) {
throw new Error(
`verify-host-command-descriptions: discovery narrowed to ${files.length} source file(s); expected at least ${MINIMUM_HOST_SOURCES}.`,
)
}
const registrations = files.flatMap(file =>
findHostCommandRegistrations(file, readFileSync(resolve(root, file), 'utf8')))
if (registrations.length < MINIMUM_HOST_COMMANDS) {
throw new Error(
`verify-host-command-descriptions: discovery found ${registrations.length} Host command registration(s); expected at least ${MINIMUM_HOST_COMMANDS}.`,
)
}
const violations = hostCommandDescriptionViolations(
registrations,
readFileSync(resolve(root, LOCALES_FILE), 'utf8'),
readFileSync(resolve(root, SERVICE_FILE), 'utf8'),
)
if (violations.length > 0) {
console.error(`verify-host-command-descriptions: ${violations.length} violation(s):`)
for (const violation of violations) console.error(` ${violation}`)
process.exitCode = 1
return
}
console.log(
`verify-host-command-descriptions: ${registrations.length} Host command description(s) match the Client English dictionary.`,
)
}
if (import.meta.filename === resolve(process.argv[1] ?? '')) main()