Browse Source

test(deps): enforce runtime dependency ownership

imccyu 1 tháng trước cách đây
mục cha
commit
34bdc81b47

+ 1 - 0
scripts/client-bundle-purity.spec.ts

@@ -95,6 +95,7 @@ describe('client bundle purity gate', () => {
     expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull()
     expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
     expect(resolveId('@deepseek-ai/dsh-deque')).toBeNull()
+    expect(resolveId('@deepseek-ai/dsh-util-values')).toBeNull()
     expect(resolveId('@deepseek-ai/dsh-token-meter/client')).toBeNull()
     expect(() => resolveId('@deepseek-ai/dsh-token-meter')).toThrow(/purity/)
     expect(() => resolveId('@deepseek-ai/dsh-token-meter/client/internal')).toThrow(/purity/)

+ 2 - 1
scripts/doc-standard.spec.ts

@@ -76,7 +76,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
   'packages/typert/generator': 'Build-time generator run outside any agent runtime.',
   'packages/typert/protocol': 'Compiler-independent protocol declarations.',
   'packages/util/atomic-write': 'Zero-dependency filesystem write utility.',
-  'packages/util/brand': 'Type-only branding primitive erased at compile time.',
+  'packages/util/brand': 'Stateless nominal-string and canonical-key constructors.',
   'packages/util/crypto': 'Zero-dependency identifier minting utility.',
   'packages/util/deque': 'Zero-dependency circular deque utility.',
   'packages/util/home-paths': 'Zero-dependency harness-home path resolver.',
@@ -85,6 +85,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
   'packages/util/output-retention': 'Zero-dependency retention utility.',
   'packages/util/time': 'Zero-dependency time-zone canonicalization utility.',
   'packages/util/timeout': 'Zero-dependency timeout utility.',
+  'packages/util/values': 'Stateless lossless-JSON and immutable-value helpers.',
   'packages/util/workspace-path': 'Zero-dependency Workspace path formatter.',
 }
 

+ 4 - 0
scripts/gen-cordis-catalog.ts

@@ -493,7 +493,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
   SubagentStartRequest: 'subagent.md',
   AssembleContext: 'system-prompt.md',
   PromptContext: 'system-prompt.md',
+  PromptContextOrderName: 'system-prompt.md',
   PromptSection: 'system-prompt.md',
+  PromptSectionOrderName: 'system-prompt.md',
   SystemPrompt: 'system-prompt.md',
   ToolProviderResult: 'system-prompt.md',
   JobDoneListener: 'jobs.md',
@@ -533,7 +535,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
   ToolRestriction: 'tools.md',
   ToolSchema: 'tools.md',
   SettingsNamespace: 'settings.md',
+  SettingsNamespaceInput: 'settings.md',
   SettingsRegisterOptions: 'settings.md',
+  SettingsSectionHooks: 'settings.md',
   SettingsScope: 'settings.md',
   SettingsDescriptor: 'settings.md',
   SettingsDescribeValue: 'settings.md',

+ 11 - 10
scripts/package-dependency-policy.ts

@@ -29,29 +29,28 @@ const CONFIGURATION_ONLY_DEV_DEPENDENCIES = {
   '@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
 } as const satisfies Readonly<Record<string, readonly string[]>>
 
+/** Workspace packages whose complete runtime surface is safe across duplicate installations. */
+const DUPLICATE_SAFE_PACKAGES: readonly string[] = [
+  '@deepseek-ai/dsh-brand',
+  '@deepseek-ai/dsh-typert-protocol',
+  '@deepseek-ai/dsh-util-crypto',
+  '@deepseek-ai/dsh-util-values',
+]
+
 /**
  * Runtime exports whose values remain valid when npm installs another package copy.
  */
 const SAFE_HOST_DEPENDENCY_EXPORTS = {
-  '@deepseek-ai/dsh-api-session-controller/remote-events': ['SESSION_CONTROLLER_REMOTE_EVENTS'],
   '@deepseek-ai/dsh-credentials': ['credentialKey'],
   '@deepseek-ai/dsh-deque': ['Deque'],
-  '@deepseek-ai/dsh-llm': ['MessageId', 'callConfigEquals', 'deepFreeze', 'freezeMessage'],
-  '@deepseek-ai/dsh-llm/brand': ['ToolCallId'],
-  '@deepseek-ai/dsh-session': ['isJsonValue'],
-  '@deepseek-ai/dsh-session/types': ['SessionId'],
-  '@deepseek-ai/dsh-settings': ['settingsNamespace'],
-  '@deepseek-ai/dsh-system-prompt': ['FIRST_PARTY_SECTION_ORDER'],
+  '@deepseek-ai/dsh-llm': ['callConfigEquals'],
   '@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
-  '@deepseek-ai/dsh-typert-protocol': ['RemoteError', 'remoteErrorOf'],
-  '@deepseek-ai/dsh-util-crypto': ['randomUUID'],
   '@deepseek-ai/schemastery': ['default'],
 } as const satisfies HostDependencyExports
 
 /** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
 const PEER_REQUIRED_HOST_EXPORTS = {
   '@deepseek-ai/dsh-scope': ['carrierKeyOf', 'scopeOf', 'scopeTarget'],
-  '@deepseek-ai/dsh-typert-protocol': ['Remote', 'TypertRemoteService', 'remoteMethods'],
 } as const satisfies HostDependencyExports
 
 /** Exact import specifier to reviewed runtime exports. */
@@ -63,6 +62,7 @@ export interface PackageDependencyPolicy {
   readonly clientFaceExclude: readonly string[]
   readonly hostPackages: readonly string[]
   readonly configurationOnlyDevDependencies: Readonly<Record<string, readonly string[]>>
+  readonly duplicateSafePackages?: readonly string[]
   readonly safeHostDependencyExports: HostDependencyExports
   readonly peerRequiredHostExports: HostDependencyExports
 }
@@ -73,6 +73,7 @@ export const PACKAGE_DEPENDENCY_POLICY: PackageDependencyPolicy = {
   clientFaceExclude: CLIENT_FACE_EXCLUDE,
   hostPackages: HOST_DEPENDENCY_PACKAGES,
   configurationOnlyDevDependencies: CONFIGURATION_ONLY_DEV_DEPENDENCIES,
+  duplicateSafePackages: DUPLICATE_SAFE_PACKAGES,
   safeHostDependencyExports: SAFE_HOST_DEPENDENCY_EXPORTS,
   peerRequiredHostExports: PEER_REQUIRED_HOST_EXPORTS,
 }

+ 66 - 32
scripts/verify-package-dependencies.spec.ts

@@ -87,6 +87,39 @@ function facts(manifest: PackageDependencyManifest): PackageDependencyFacts {
   }
 }
 
+function hostRuntimeFixture(): {
+  provider: WorkspacePackageManifest
+  workspaceNames: Set<string>
+  consumerFacts: PackageDependencyFacts
+} {
+  const consumer = pkg('@f/consumer', 'packages/core/consumer/package.json')
+  const provider = pkg('@f/provider', 'packages/core/provider/package.json')
+  const sourcePath = 'packages/core/consumer/src/index.ts'
+  const specifier = `${provider.name}/api`
+  const workspaceNames = new Set([CORDIS, consumer.name, provider.name])
+  const consumerFacts: PackageDependencyFacts = {
+    manifestPath: consumer.manifestPath,
+    role: 'configured-host',
+    manifest: consumer.manifest,
+    workspaceNames,
+    allSourceUses: new Map(),
+    hostRuntimeSourceUses: new Map([[provider.name, [sourcePath]]]),
+    hostRuntimeExportUses: [{
+      packageName: provider.name,
+      specifier,
+      exportName: 'safeValue',
+      sourcePath,
+      line: 1,
+      column: 10,
+      sourceLine: `import { safeValue } from '${specifier}'`,
+    }],
+    peerRequiredHostDependencies: new Set(),
+    configurationOnlyDevDependencies: new Set(),
+    clientInject: new Set(),
+  }
+  return { provider, workspaceNames, consumerFacts }
+}
+
 describe('package dependency scope', () => {
   it('keeps the measured Host relay roster explicit', () => {
     expect(PACKAGE_DEPENDENCY_POLICY.clientFaceExclude).toEqual([
@@ -109,20 +142,20 @@ describe('package dependency scope', () => {
       '@deepseek-ai/dsh-client-ui-theme': ['@deepseek-ai/dsh-api-remotes'],
       '@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
     })
+    expect(PACKAGE_DEPENDENCY_POLICY.duplicateSafePackages).toEqual([
+      '@deepseek-ai/dsh-brand',
+      '@deepseek-ai/dsh-typert-protocol',
+      '@deepseek-ai/dsh-util-crypto',
+      '@deepseek-ai/dsh-util-values',
+    ])
     expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-deque']).toEqual(['Deque'])
     expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
-    expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toEqual([
-      'SessionId',
-    ])
-    expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-typert-protocol']).toEqual([
-      'RemoteError', 'remoteErrorOf',
-    ])
+    expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toBeUndefined()
+    expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
     expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-scope']).toEqual([
       'carrierKeyOf', 'scopeOf', 'scopeTarget',
     ])
-    expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toEqual([
-      'Remote', 'TypertRemoteService', 'remoteMethods',
-    ])
+    expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
   })
 
   it('discovers the Client directory, dsh.client declarations, and configured Host packages', () => {
@@ -175,29 +208,7 @@ describe('package dependency scope', () => {
   })
 
   it('rejects stale, duplicate, and unbounded safe Host export entries', () => {
-    const consumer = pkg('@f/consumer', 'packages/core/consumer/package.json')
-    const provider = pkg('@f/provider', 'packages/core/provider/package.json')
-    const workspaceNames = new Set([CORDIS, consumer.name, provider.name])
-    const consumerFacts: PackageDependencyFacts = {
-      manifestPath: consumer.manifestPath,
-      role: 'configured-host',
-      manifest: consumer.manifest,
-      workspaceNames,
-      allSourceUses: new Map(),
-      hostRuntimeSourceUses: new Map([[provider.name, ['packages/core/consumer/src/index.ts']]]),
-      hostRuntimeExportUses: [{
-        packageName: provider.name,
-        specifier: `${provider.name}/api`,
-        exportName: 'safeValue',
-        sourcePath: 'packages/core/consumer/src/index.ts',
-        line: 1,
-        column: 10,
-        sourceLine: "import { safeValue } from '@f/provider/api'",
-      }],
-      peerRequiredHostDependencies: new Set(),
-      configurationOnlyDevDependencies: new Set(),
-      clientInject: new Set(),
-    }
+    const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
 
     expect(collectHostDependencyExportPolicyViolations(
       [consumerFacts],
@@ -217,6 +228,29 @@ describe('package dependency scope', () => {
       expect.stringContaining('appears in both Host export classifications'),
     ]))
   })
+
+  it('applies a duplicate-safe package classification to its subpaths', () => {
+    const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
+
+    expect(collectHostDependencyExportPolicyViolations(
+      [consumerFacts],
+      workspaceNames,
+      {
+        duplicateSafePackages: [provider.name],
+        safeHostDependencyExports: {},
+        peerRequiredHostExports: {},
+      },
+    )).toEqual([])
+    expect(collectHostDependencyExportPolicyViolations(
+      [consumerFacts],
+      workspaceNames,
+      {
+        duplicateSafePackages: [provider.name],
+        safeHostDependencyExports: { [`${provider.name}/api`]: ['safeValue'] },
+        peerRequiredHostExports: {},
+      },
+    )).toContain(`safeHostDependencyExports redundantly classifies duplicate-install-safe package ${provider.name}/api`)
+  })
 })
 
 describe('face-aware source classification', () => {

+ 13 - 1
scripts/verify-package-dependencies.ts

@@ -374,10 +374,19 @@ export function readPackageDependencyFacts(
 export function collectHostDependencyExportPolicyViolations(
   facts: readonly PackageDependencyFacts[],
   workspaceNames: ReadonlySet<string>,
-  policy: Pick<PackageDependencyPolicy, 'peerRequiredHostExports' | 'safeHostDependencyExports'>,
+  policy: Pick<PackageDependencyPolicy, 'duplicateSafePackages' | 'peerRequiredHostExports' | 'safeHostDependencyExports'>,
 ): string[] {
   const violations: string[] = []
   const allRuntimeUses = facts.flatMap(fact => fact.hostRuntimeExportUses)
+  const duplicateSafePackages = new Set(policy.duplicateSafePackages ?? [])
+  for (const packageName of duplicates(policy.duplicateSafePackages ?? [])) {
+    violations.push(`duplicateSafePackages lists ${packageName} more than once`)
+  }
+  for (const packageName of duplicateSafePackages) {
+    if (!workspaceNames.has(packageName)) {
+      violations.push(`duplicateSafePackages names unknown workspace package ${packageName}`)
+    }
+  }
   const classifications = [
     ['safeHostDependencyExports', policy.safeHostDependencyExports],
     ['peerRequiredHostExports', policy.peerRequiredHostExports],
@@ -387,6 +396,8 @@ export function collectHostDependencyExportPolicyViolations(
       const provider = packageNameOf(specifier)
       if (provider === undefined || !workspaceNames.has(provider)) {
         violations.push(`${field} specifier ${specifier} is not a workspace package`)
+      } else if (duplicateSafePackages.has(provider)) {
+        violations.push(`${field} redundantly classifies duplicate-install-safe package ${specifier}`)
       }
       if (exportNames.length === 0) {
         violations.push(`${field} lists no exports for ${specifier}`)
@@ -414,6 +425,7 @@ export function collectHostDependencyExportPolicyViolations(
     for (const use of fact.hostRuntimeExportUses) {
       if (use.packageName === fact.manifest.name || use.packageName === CORDIS) continue
       if (!workspaceNames.has(use.packageName)) continue
+      if (duplicateSafePackages.has(use.packageName)) continue
       if (policy.safeHostDependencyExports[use.specifier]?.includes(use.exportName) === true) continue
       if (policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true) continue
       violations.push(

+ 1 - 1
scripts/verify-package-readme-limitations.ts

@@ -16,7 +16,7 @@ const CANONICAL = '## Known Limitations and Deferred Work'
 
 /** Packages audited as having no limitations section, keyed by repo-relative directory. */
 const NO_LIMITATIONS: Readonly<Record<string, string>> = {
-  'packages/util/brand': 'Type-only nominal-branding primitive with no runtime behavior or deferred work.',
+  'packages/util/brand': 'Stateless nominal-string and canonical-key helpers have no deferred work.',
 }
 
 /** A heading that reads as a limitations section — canonical or drifted. */

+ 2 - 1
scripts/verify-package-readme-model-experience.ts

@@ -31,10 +31,11 @@ interface SentenceContract {
  */
 const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
   'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
-  'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
+  'packages/util/brand': 'The package only constructs plain string values and registers nothing model-facing.',
   'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
   'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
   'packages/util/workspace-path': 'The package only formats Workspace paths for browser UI; it never constructs model input.',
+  'packages/util/values': 'The package only validates, snapshots, compares, freezes, or rejects caller-owned values; consumers own every model-facing use.',
 }
 
 /**