dsh-scout 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -3,8 +3,8 @@
3
3
  [![ci](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/ci.yml/badge.svg)](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/ci.yml)
4
4
  [![license MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
5
5
  [![node >=22.19](https://img.shields.io/badge/node-%3E%3D22.19-brightgreen)](package.json)
6
- [![dsh-tools 0.1.0-rc.6](https://img.shields.io/badge/dsh-tools-0.1.0--rc.6-4b32c3)](package.json)
7
- [![tests 32 passing](https://img.shields.io/badge/tests-32%20passing-green)](tests/model.test.mjs)
6
+ [![DSH nightly compatibility](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/compatibility-nightly.yml/badge.svg)](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/compatibility-nightly.yml)
7
+ [![tests 34 passing](https://img.shields.io/badge/tests-34%20passing-green)](tests/model.test.mjs)
8
8
  [![中文 README](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-2ea44f)](README.md)
9
9
 
10
10
  **司察(Scout)** — evidence-driven company & job due-diligence plugin for [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness).
@@ -49,7 +49,7 @@ pnpm test
49
49
  pnpm run check:release
50
50
  ```
51
51
 
52
- The tests cover the conservative decision default, evidence-level constraints, identity verification, session isolation, report rendering, export/import round-trips, auto-persist, question generation, and tool cleanup on unload. `check:release` additionally packs the plugin, installs it into an isolated temporary DSH profile, verifies `--dump-config`, checks the mounted tools, and observes the Cordis unload disposer. The gate uses `DSH_BIN` or a local `dsh` binary when available; otherwise it downloads the exact official CLI version `0.1.0-rc.6` through `npx`.
52
+ The 34 tests cover the conservative decision default, evidence-level constraints, identity verification, session isolation, report rendering, export/import round-trips, auto-persist, question generation, and tool cleanup on unload. `check:release` additionally packs the plugin, installs it into isolated temporary DSH profiles for the four exact versions in the table below, verifies `--dump-config`, checks the mounted tools, observes the Cordis unload disposer, and checks package contents. `test:profile` uses `DSH_BIN` when explicitly set; otherwise it downloads the exact `DSH_VERSION` (default `0.2.0-rc.2`) through `npx`.
53
53
 
54
54
  ## Install into a DSH profile
55
55
 
@@ -60,7 +60,20 @@ dsh plugin --profile scout-demo add github:MaxHou-infinity/dsh-scout#<commit>
60
60
  dsh --profile scout-demo --dump-config
61
61
  ```
62
62
 
63
- Git installs fetch source and run `prepare`. pnpm may require an explicit `allowBuilds` entry for `dsh-scout`; only allow a pinned source you have reviewed. The current package targets `@deepseek-ai/dsh-tools` `0.1.0-rc.6` and `@deepseek-ai/cordis` `4.0.x`.
63
+ Git installs fetch source and run `prepare`. pnpm may require an explicit `allowBuilds` entry for `dsh-scout`; only allow a pinned source you have reviewed. The `@deepseek-ai/dsh-tools` peer range is `>=0.1.0-rc.6 <0.3.0-0`; the `@deepseek-ai/cordis` peer range is `>=4.0.2 <5`.
64
+
65
+ ### DSH compatibility
66
+
67
+ DSH checks a plugin's `@deepseek-ai/dsh-*` peers at install and startup, including prereleases. The `<0.3.0-0` upper bound excludes `0.3.0-alpha` and later releases. **Verified on 2026-10-12** with the 0.9.0 candidate tarball in isolated profiles, covering installation, cold startup, tool mounting, and unload:
68
+
69
+ | DSH version | npm channel on that date | Result |
70
+ | --- | --- | --- |
71
+ | `0.1.0-rc.6` | Declared lower bound | Pass |
72
+ | `0.1.2-rc.1` | Older release regression | Pass |
73
+ | `0.2.0-rc.2` | `latest` / `next` | Pass |
74
+ | `0.2.1-alpha.2` | `alpha` | Pass |
75
+
76
+ Future `0.2.x` versions still require runtime verification. The [nightly compatibility workflow](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/compatibility-nightly.yml) resolves npm's `latest`, `next`, and `alpha` tags to exact versions before testing each one. The badge shows the latest workflow result; this table records versions already verified.
64
77
 
65
78
  ## Configuration (optional)
66
79
 
@@ -87,4 +100,4 @@ See [the product contract](docs/dsh-scout-product-contract.md) for the full MVP
87
100
 
88
101
  ## Community
89
102
 
90
- This is an independent community plugin for DeepSeek Harness. The repository uses the `dsh-plugin`, `deepseek-harness`, `due-diligence`, `company-research`, `job-research`, `hr-tech`, and `evidence-based` topics for discovery.
103
+ This is an independent community plugin for DeepSeek Harness. The repository uses the `dsh-plugin`, `deepseek-harness`, `due-diligence`, `company-research`, `job-research`, `hr-tech`, and `evidence-based` topics for discovery. For install or startup failures, use the [compatibility issue template](https://github.com/MaxHou-infinity/dsh-scout/issues/new?template=compatibility.yml) with exact versions and redacted logs.
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  [![ci](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/ci.yml/badge.svg)](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/ci.yml)
4
4
  [![license MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
5
5
  [![node >=22.19](https://img.shields.io/badge/node-%3E%3D22.19-brightgreen)](package.json)
6
- [![dsh-tools 0.1.0-rc.6](https://img.shields.io/badge/dsh-tools-0.1.0--rc.6-4b32c3)](package.json)
7
- [![tests 32 passing](https://img.shields.io/badge/tests-32%20passing-green)](tests/model.test.mjs)
6
+ [![DSH 兼容性夜间检查](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/compatibility-nightly.yml/badge.svg)](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/compatibility-nightly.yml)
7
+ [![tests 34 passing](https://img.shields.io/badge/tests-34%20passing-green)](tests/model.test.mjs)
8
8
  [![English README](https://img.shields.io/badge/README-English-blue)](README.en.md)
9
9
 
10
10
  **司察(Scout)** —— 面向 [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) 的**证据驱动型公司与岗位尽调 / 背调插件**。
@@ -119,7 +119,20 @@ dsh plugin --profile scout-demo add github:MaxHou-infinity/dsh-scout#<commit>
119
119
  dsh --profile scout-demo --dump-config # 验证工具挂载
120
120
  ```
121
121
 
122
- 要求:Node ≥ 22.19;面向 `@deepseek-ai/dsh-tools` `0.1.0-rc.6` 与 `@deepseek-ai/cordis` `4.0.x`。Git 安装会运行 `prepare`,pnpm 可能需要为 `dsh-scout` 显式添加 `allowBuilds`(只放行你审查过的固定版本)。
122
+ 要求:Node ≥ 22.19;`@deepseek-ai/dsh-tools` peer 范围为 `>=0.1.0-rc.6 <0.3.0-0`,`@deepseek-ai/cordis` 为 `>=4.0.2 <5`。Git 安装会运行 `prepare`,pnpm 可能需要为 `dsh-scout` 显式添加 `allowBuilds`(只放行你审查过的固定版本)。
123
+
124
+ ### DSH 兼容性
125
+
126
+ DSH 安装和启动时会检查插件的 `@deepseek-ai/dsh-*` peer;预发布版本也参与匹配。上界 `<0.3.0-0` 排除 `0.3.0-alpha` 及之后的版本。以下是 **2026-10-12 在隔离 profile 中用 0.9.0 候选 tarball 实测**的版本,覆盖安装、冷启动、工具挂载和卸载:
127
+
128
+ | DSH 版本 | npm 渠道(核对当日) | 结果 |
129
+ | --- | --- | --- |
130
+ | `0.1.0-rc.6` | 兼容范围下界 | 通过 |
131
+ | `0.1.2-rc.1` | 旧版回归 | 通过 |
132
+ | `0.2.0-rc.2` | `latest` / `next` | 通过 |
133
+ | `0.2.1-alpha.2` | `alpha` | 通过 |
134
+
135
+ 未来的 `0.2.x` 版本虽落在声明范围内,仍需实际验证。[夜间兼容检查](https://github.com/MaxHou-infinity/dsh-scout/actions/workflows/compatibility-nightly.yml)每天解析 npm 的 `latest`、`next`、`alpha` 到精确版本,再运行测试和隔离 profile 检查。徽章显示该工作流最近一次运行结果;以上表格记录已完成的精确版本验证。
123
136
 
124
137
  ## 配置(可选)
125
138
 
@@ -162,10 +175,12 @@ report.md 当前报告快照
162
175
 
163
176
  ```sh
164
177
  pnpm install
165
- pnpm test # 32 个测试:决策默认值/证据约束/主体核验/会话隔离/报告渲染/导出导入往返/自动持久化/面试问题/采集→主张流转
178
+ pnpm test # 34 个测试:决策默认值/证据约束/主体核验/会话隔离/报告渲染/导出导入往返/自动持久化/面试问题/采集→主张流转
166
179
  pnpm run check:release
167
180
  ```
168
181
 
182
+ `check:release` 在运行 34 项测试后,分别用表中的四个精确 DSH 版本安装候选 tarball 并检查 profile 生命周期,最后检查打包内容。
183
+
169
184
  ## 路线图
170
185
 
171
186
  - ✅ v0.1:证据纪律、三态决策、结构化报告、会话隔离
@@ -178,4 +193,4 @@ pnpm run check:release
178
193
 
179
194
  ## 社区
180
195
 
181
- 面向 DeepSeek Harness 的独立社区插件(Topic:`dsh-plugin`、`deepseek-harness`、`due-diligence`、`company-research`、`job-research`、`hr-tech`、`evidence-based`)。欢迎 Issues / PR。
196
+ 面向 DeepSeek Harness 的独立社区插件(Topic:`dsh-plugin`、`deepseek-harness`、`due-diligence`、`company-research`、`job-research`、`hr-tech`、`evidence-based`)。安装或启动遇到问题时,请用[兼容性问题模板](https://github.com/MaxHou-infinity/dsh-scout/issues/new?template=compatibility.yml)提供精确版本和脱敏日志;也欢迎其他 Issues / PR。
package/dist/index.d.ts CHANGED
@@ -1,11 +1,23 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
2
3
  export declare const name = "dsh-scout";
3
4
  export declare const inject: string[];
4
- export interface ScoutConfig {
5
+ export interface Config {
5
6
  /** Default directory for case exports; case files land under `<scoutDir>/<caseId>/`. */
6
7
  scoutDir?: string;
7
8
  /** Persist the five-file export after every mutation when `scoutDir` is set. */
8
9
  autoPersist?: boolean;
10
+ /** Trusted authority host suffixes used by the E3 evidence policy. */
11
+ authorityHostSuffixes?: readonly string[];
9
12
  }
10
- export declare function apply(ctx: Context, config?: ScoutConfig): void;
13
+ export declare const Config: z<Schemastery.ObjectS<{
14
+ scoutDir: z<string, string>;
15
+ autoPersist: z<boolean, boolean>;
16
+ authorityHostSuffixes: z<string[], string[]>;
17
+ }>, Schemastery.ObjectT<{
18
+ scoutDir: z<string, string>;
19
+ autoPersist: z<boolean, boolean>;
20
+ authorityHostSuffixes: z<string[], string[]>;
21
+ }>>;
22
+ export declare function apply(ctx: Context, config?: Config): void;
11
23
  export * from './model.js';
package/dist/index.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
- import { addClaim, addSource, appendEvent, CLAIM_DIMENSIONS, CLAIM_IMPACTS, CLAIM_STATUSES, createCase, decideCase, EVIDENCE_LEVELS, EXPORT_FILE_NAMES, exportCaseFiles, generateInterviewQuestions, importCaseFromFiles, inferEvidenceLevel, inferSourceType, renderComparison, renderReport, SOURCE_TYPES, verifyClaim, verifyIdentity, } from './model.js';
2
+ import z from '@deepseek-ai/schemastery';
3
+ import { addClaim, addSource, appendEvent, CLAIM_DIMENSIONS, CLAIM_IMPACTS, CLAIM_STATUSES, createCase, decideCase, DEFAULT_AUTHORITY_HOST_SUFFIXES, EVIDENCE_LEVELS, EXPORT_FILE_NAMES, exportCaseFiles, generateInterviewQuestions, importCaseFromFiles, inferEvidenceLevel, inferSourceType, renderComparison, renderReport, SOURCE_TYPES, verifyClaim, verifyIdentity, } from './model.js';
3
4
  export const name = 'dsh-scout';
4
- export const inject = ['tools', 'fs', 'web'];
5
+ export const inject = ['tools'];
6
+ export const Config = z.object({
7
+ scoutDir: z.string().default('dsh-scout'),
8
+ autoPersist: z.boolean().default(false),
9
+ authorityHostSuffixes: z.array(z.string()).default([...DEFAULT_AUTHORITY_HOST_SUFFIXES]),
10
+ });
5
11
  export function apply(ctx, config = {}) {
6
12
  const cases = new Map();
7
13
  const caseKey = (caseId, agentId) => `${String(agentId ?? 'unscoped')}::${caseId}`;
@@ -11,8 +17,11 @@ export function apply(ctx, config = {}) {
11
17
  throw new Error(`Unknown case: ${key.split('::').at(-1)}`);
12
18
  return scoutCase;
13
19
  };
14
- const fs = ctx.fs;
15
- const web = ctx.web;
20
+ // Optional host services: unavailable hosts degrade gracefully (tools report
21
+ // the missing service instead of failing to register).
22
+ const fs = ctx.get('fs');
23
+ const web = ctx.get('web');
24
+ const authoritySuffixes = config.authorityHostSuffixes ?? DEFAULT_AUTHORITY_HOST_SUFFIXES;
16
25
  const defaultScoutDir = config.scoutDir?.replace(/\/+$/, '') || 'dsh-scout';
17
26
  const caseExportDir = (caseId) => `${defaultScoutDir}/${caseId}`;
18
27
  const persistFiles = async (scoutCase, targetDir) => {
@@ -45,7 +54,7 @@ export function apply(ctx, config = {}) {
45
54
  detail,
46
55
  });
47
56
  };
48
- const registerSource = (scoutCase, source) => recordEvent(addSource(scoutCase, source), 'source_added', {
57
+ const registerSource = (scoutCase, source) => recordEvent(addSource(scoutCase, source, authoritySuffixes), 'source_added', {
49
58
  sourceId: source.sourceId,
50
59
  type: source.type,
51
60
  title: source.title,
@@ -199,8 +208,8 @@ export function apply(ctx, config = {}) {
199
208
  errors.push({ url, error: `invalid evidenceLevel: ${String(item.evidenceLevel)}` });
200
209
  continue;
201
210
  }
202
- const typeGuess = inferSourceType(url, explicitType);
203
- const levelGuess = inferEvidenceLevel(url, typeGuess.type, explicitLevel);
211
+ const typeGuess = inferSourceType(url, explicitType, authoritySuffixes);
212
+ const levelGuess = inferEvidenceLevel(url, typeGuess.type, explicitLevel, authoritySuffixes);
204
213
  let sourceId = `src-${nextId}`;
205
214
  while (scoutCase.sources.some(source => source.sourceId === sourceId)) {
206
215
  nextId += 1;
@@ -349,8 +358,8 @@ export function apply(ctx, config = {}) {
349
358
  return;
350
359
  }
351
360
  try {
352
- const typeGuess = inferSourceType(url);
353
- const levelGuess = inferEvidenceLevel(url, typeGuess.type);
361
+ const typeGuess = inferSourceType(url, undefined, authoritySuffixes);
362
+ const levelGuess = inferEvidenceLevel(url, typeGuess.type, undefined, authoritySuffixes);
354
363
  let sourceId = `src-${nextId}`;
355
364
  while (scoutCase.sources.some(source => source.sourceId === sourceId)) {
356
365
  nextId += 1;
@@ -542,7 +551,7 @@ export function apply(ctx, config = {}) {
542
551
  evidenceLevel: args.evidenceLevel,
543
552
  status: 'captured',
544
553
  };
545
- let nextCase = recordEvent(addSource(scoutCase, source), 'source_added', {
554
+ let nextCase = recordEvent(addSource(scoutCase, source, authoritySuffixes), 'source_added', {
546
555
  sourceId: source.sourceId,
547
556
  evidenceLevel: source.evidenceLevel,
548
557
  });
@@ -577,7 +586,7 @@ export function apply(ctx, config = {}) {
577
586
  legalRepresentative: args.legalRepresentative,
578
587
  brandRelationship: args.brandRelationship,
579
588
  sourceIds: args.sourceIds.split(',').map(value => value.trim()).filter(Boolean),
580
- })), 'identity_verified', {
589
+ }, authoritySuffixes)), 'identity_verified', {
581
590
  legalEntity: args.legalEntity,
582
591
  registrationNumber: args.registrationNumber,
583
592
  });
package/dist/model.d.ts CHANGED
@@ -17,6 +17,8 @@ export interface ScoutEvent {
17
17
  at: string;
18
18
  detail: Record<string, unknown>;
19
19
  }
20
+ /** Default trusted authority host suffixes used by the E3 evidence policy. */
21
+ export declare const DEFAULT_AUTHORITY_HOST_SUFFIXES: readonly ["gov", "gov.cn", "gov.uk", "gov.hk", "gov.au", "gc.ca", "brreg.no", "handelsregister.de", "hkexnews.hk", "sse.com.cn", "szse.cn", "neeq.com.cn"];
20
22
  export interface ScoutSource {
21
23
  sourceId: string;
22
24
  type: SourceType;
@@ -80,15 +82,15 @@ export declare function createCase(input: {
80
82
  location?: string;
81
83
  decisionObjective?: string;
82
84
  }): ScoutCase;
83
- export declare function addSource(scoutCase: ScoutCase, source: ScoutSource): ScoutCase;
84
- export declare function isTrustedAuthorityUrl(value: string | null): boolean;
85
+ export declare function addSource(scoutCase: ScoutCase, source: ScoutSource, authoritySuffixes?: readonly string[]): ScoutCase;
86
+ export declare function isTrustedAuthorityUrl(value: string | null, authoritySuffixes?: readonly string[]): boolean;
85
87
  /** Guess a source category from its URL when the caller did not provide one. */
86
- export declare function inferSourceType(url: string | null, explicit?: SourceType): {
88
+ export declare function inferSourceType(url: string | null, explicit?: SourceType, authoritySuffixes?: readonly string[]): {
87
89
  type: SourceType;
88
90
  inferred: boolean;
89
91
  };
90
92
  /** Guess an evidence level from the URL and category when not provided explicitly. */
91
- export declare function inferEvidenceLevel(url: string | null, type: SourceType, explicit?: EvidenceLevel): {
93
+ export declare function inferEvidenceLevel(url: string | null, type: SourceType, explicit?: EvidenceLevel, authoritySuffixes?: readonly string[]): {
92
94
  evidenceLevel: EvidenceLevel;
93
95
  inferred: boolean;
94
96
  };
@@ -107,7 +109,7 @@ export declare function verifyIdentity(scoutCase: ScoutCase, input: {
107
109
  legalRepresentative: string;
108
110
  brandRelationship: string;
109
111
  sourceIds: string[];
110
- }): ScoutCase;
112
+ }, authoritySuffixes?: readonly string[]): ScoutCase;
111
113
  export declare function decideCase(scoutCase: ScoutCase): ScoutCase;
112
114
  /** Derive a deduplicated, prioritized interview question list from the case. */
113
115
  export declare function generateInterviewQuestions(scoutCase: ScoutCase): string[];
package/dist/model.js CHANGED
@@ -56,7 +56,8 @@ const JOB_PLATFORM_HOST_HINTS = [
56
56
  'nowcoder.com',
57
57
  'jobs.feishu.cn',
58
58
  ];
59
- const AUTHORITY_HOST_SUFFIXES = [
59
+ /** Default trusted authority host suffixes used by the E3 evidence policy. */
60
+ export const DEFAULT_AUTHORITY_HOST_SUFFIXES = [
60
61
  'gov',
61
62
  'gov.cn',
62
63
  'gov.uk',
@@ -111,17 +112,17 @@ export function createCase(input) {
111
112
  ],
112
113
  };
113
114
  }
114
- export function addSource(scoutCase, source) {
115
+ export function addSource(scoutCase, source, authoritySuffixes = DEFAULT_AUTHORITY_HOST_SUFFIXES) {
115
116
  if (scoutCase.sources.some(item => item.sourceId === source.sourceId)) {
116
117
  throw new Error(`Duplicate source: ${source.sourceId}`);
117
118
  }
118
119
  if (source.evidenceLevel === 'E3'
119
- && (!IDENTITY_SOURCE_TYPES.has(source.type) || !isTrustedAuthorityUrl(source.url))) {
120
+ && (!IDENTITY_SOURCE_TYPES.has(source.type) || !isTrustedAuthorityUrl(source.url, authoritySuffixes))) {
120
121
  throw new Error('E3 sources require a supported registry, regulator, or official filing HTTPS origin');
121
122
  }
122
123
  return { ...scoutCase, sources: [...scoutCase.sources, source] };
123
124
  }
124
- export function isTrustedAuthorityUrl(value) {
125
+ export function isTrustedAuthorityUrl(value, authoritySuffixes = DEFAULT_AUTHORITY_HOST_SUFFIXES) {
125
126
  if (!value)
126
127
  return false;
127
128
  try {
@@ -131,21 +132,21 @@ export function isTrustedAuthorityUrl(value) {
131
132
  if (url.port && url.port !== '443')
132
133
  return false;
133
134
  const hostname = url.hostname.toLowerCase().replace(/\.$/, '');
134
- return AUTHORITY_HOST_SUFFIXES.some(suffix => hostname === suffix || hostname.endsWith(`.${suffix}`));
135
+ return authoritySuffixes.some(suffix => hostname === suffix || hostname.endsWith(`.${suffix}`));
135
136
  }
136
137
  catch {
137
138
  return false;
138
139
  }
139
140
  }
140
141
  /** Guess a source category from its URL when the caller did not provide one. */
141
- export function inferSourceType(url, explicit) {
142
+ export function inferSourceType(url, explicit, authoritySuffixes = DEFAULT_AUTHORITY_HOST_SUFFIXES) {
142
143
  if (explicit && SOURCE_TYPES.includes(explicit))
143
144
  return { type: explicit, inferred: false };
144
145
  if (url) {
145
146
  try {
146
147
  const hostname = new URL(url).hostname.toLowerCase().replace(/\.$/, '');
147
148
  // Trusted authority origins (official registry/regulator) take priority.
148
- if (isTrustedAuthorityUrl(url)) {
149
+ if (isTrustedAuthorityUrl(url, authoritySuffixes)) {
149
150
  const isRegistry = REGISTRY_HOST_SUFFIXES.some(suffix => hostname === suffix || hostname.endsWith(`.${suffix}`));
150
151
  return { type: isRegistry ? 'company_registry' : 'regulator', inferred: true };
151
152
  }
@@ -161,10 +162,10 @@ export function inferSourceType(url, explicit) {
161
162
  return { type: 'other', inferred: true };
162
163
  }
163
164
  /** Guess an evidence level from the URL and category when not provided explicitly. */
164
- export function inferEvidenceLevel(url, type, explicit) {
165
+ export function inferEvidenceLevel(url, type, explicit, authoritySuffixes = DEFAULT_AUTHORITY_HOST_SUFFIXES) {
165
166
  if (explicit && EVIDENCE_LEVELS.includes(explicit))
166
167
  return { evidenceLevel: explicit, inferred: false };
167
- if (IDENTITY_SOURCE_TYPES.has(type) && isTrustedAuthorityUrl(url)) {
168
+ if (IDENTITY_SOURCE_TYPES.has(type) && isTrustedAuthorityUrl(url, authoritySuffixes)) {
168
169
  return { evidenceLevel: 'E3', inferred: true };
169
170
  }
170
171
  if (type === 'user_provided') {
@@ -228,7 +229,7 @@ export function verifyClaim(scoutCase, input) {
228
229
  claims[claimIndex] = verified;
229
230
  return { ...scoutCase, claims };
230
231
  }
231
- export function verifyIdentity(scoutCase, input) {
232
+ export function verifyIdentity(scoutCase, input, authoritySuffixes = DEFAULT_AUTHORITY_HOST_SUFFIXES) {
232
233
  const requiredValues = [
233
234
  input.legalEntity,
234
235
  input.registrationNumber,
@@ -250,7 +251,7 @@ export function verifyIdentity(scoutCase, input) {
250
251
  });
251
252
  const hasAuthoritativeSource = sources.some(source => source.evidenceLevel === 'E3'
252
253
  && IDENTITY_SOURCE_TYPES.has(source.type)
253
- && isTrustedAuthorityUrl(source.url));
254
+ && isTrustedAuthorityUrl(source.url, authoritySuffixes));
254
255
  if (!hasAuthoritativeSource) {
255
256
  throw new Error('Company identity verification requires a linked E3 registry, regulator, or official filing source');
256
257
  }
package/package.json CHANGED
@@ -1,19 +1,29 @@
1
1
  {
2
2
  "name": "dsh-scout",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Evidence-driven company and job intelligence plugin for DeepSeek Harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
9
17
  "files": [
10
18
  "dist",
11
19
  "cordis.patch.yml",
12
- "scripts",
20
+ "scripts/check-release.mjs",
21
+ "scripts/profile-smoke.mjs",
13
22
  "README.md",
14
23
  "LICENSE",
15
- "docs",
16
- "tests",
24
+ "docs/dsh-scout-product-contract.md",
25
+ "docs/fixtures/dsh-scout/snapmaker-hr-head.json",
26
+ "tests/model.test.mjs",
17
27
  "README.en.md"
18
28
  ],
19
29
  "scripts": {
@@ -21,15 +31,15 @@
21
31
  "test": "npm run build && node --test tests/*.test.mjs",
22
32
  "check": "npm test",
23
33
  "test:profile": "node scripts/profile-smoke.mjs",
24
- "check:release": "npm test && npm run test:profile && pnpm pack --dry-run",
34
+ "check:release": "node scripts/check-release.mjs",
25
35
  "prepare": "npm run build"
26
36
  },
27
37
  "engines": {
28
38
  "node": ">=22.19.0"
29
39
  },
30
40
  "peerDependencies": {
31
- "@deepseek-ai/cordis": ">=4.0.1 <5",
32
- "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <0.2.0"
41
+ "@deepseek-ai/cordis": ">=4.0.2 <5",
42
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <0.3.0-0"
33
43
  },
34
44
  "peerDependenciesMeta": {
35
45
  "@deepseek-ai/cordis": {
@@ -40,10 +50,13 @@
40
50
  }
41
51
  },
42
52
  "devDependencies": {
43
- "@deepseek-ai/cordis": "4.0.1",
44
- "@deepseek-ai/dsh-tools": "0.1.0-rc.6",
53
+ "@deepseek-ai/cordis": "4.0.4",
54
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
45
55
  "typescript": "^5.8.3"
46
56
  },
57
+ "dependencies": {
58
+ "@deepseek-ai/schemastery": "^3.18.2"
59
+ },
47
60
  "dsh": {
48
61
  "bundle": {
49
62
  "patch": "./cordis.patch.yml"
@@ -0,0 +1,23 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ const command = name => process.platform === 'win32' ? `${name}.cmd` : name
4
+ const verifiedDshVersions = [
5
+ '0.1.0-rc.6',
6
+ '0.1.2-rc.1',
7
+ '0.2.0-rc.2',
8
+ '0.2.1-alpha.2',
9
+ ]
10
+
11
+ function run(program, args, env = process.env) {
12
+ const result = spawnSync(command(program), args, { env, stdio: 'inherit' })
13
+ if (result.error) throw result.error
14
+ if (result.status !== 0) {
15
+ throw new Error(`${program} ${args.join(' ')} failed with exit code ${result.status}`)
16
+ }
17
+ }
18
+
19
+ run('npm', ['test'])
20
+ for (const version of verifiedDshVersions) {
21
+ run('npm', ['run', 'test:profile'], { ...process.env, DSH_VERSION: version })
22
+ }
23
+ run('pnpm', ['pack', '--dry-run'])
@@ -1,18 +1,14 @@
1
1
  import assert from 'node:assert/strict'
2
- import { existsSync } from 'node:fs'
3
2
  import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4
3
  import { tmpdir } from 'node:os'
5
4
  import { join } from 'node:path'
6
5
  import { spawnSync } from 'node:child_process'
7
6
 
8
- const DSH_VERSION = '0.1.0-rc.6'
7
+ const DSH_VERSION = process.env.DSH_VERSION ?? '0.2.0-rc.2'
9
8
  const command = name => process.platform === 'win32' ? `${name}.cmd` : name
10
- const localDsh = join(process.cwd(), 'node_modules', '.bin', command('dsh'))
11
9
  const dshCommand = process.env.DSH_BIN
12
10
  ? { program: process.env.DSH_BIN, prefix: [] }
13
- : existsSync(localDsh)
14
- ? { program: localDsh, prefix: [] }
15
- : { program: 'npx', prefix: ['--yes', `@deepseek-ai/dsh@${DSH_VERSION}`] }
11
+ : { program: 'npx', prefix: ['--yes', `@deepseek-ai/dsh@${DSH_VERSION}`] }
16
12
 
17
13
  function run(program, args, options = {}) {
18
14
  const result = spawnSync(command(program), args, {
@@ -95,7 +91,7 @@ export async function apply(ctx) {
95
91
  ], { capture: true, env: gateEnv })
96
92
  assert.match(lifecycle, /DSH_SCOUT_LOAD/)
97
93
  assert.match(lifecycle, /DSH_SCOUT_UNLOAD/)
98
- console.log('dsh-scout profile smoke: PASS')
94
+ console.log(`dsh-scout profile smoke: PASS (dsh ${DSH_VERSION})`)
99
95
  } finally {
100
96
  await rm(gateRoot, { recursive: true, force: true })
101
97
  }
@@ -18,7 +18,7 @@ import {
18
18
  verifyClaim,
19
19
  verifyIdentity,
20
20
  } from '../dist/model.js'
21
- import { apply, inject, name } from '../dist/index.js'
21
+ import { apply, Config, inject, name } from '../dist/index.js'
22
22
 
23
23
  test('starts conservatively until company identity is verified', () => {
24
24
  const scoutCase = createCase({
@@ -327,6 +327,7 @@ test('only proceeds after identity and all required role dimensions are verified
327
327
  test('isolates identical case ids by DSH agent session identity', async () => {
328
328
  const registered = []
329
329
  apply({
330
+ get(name) { return this[name] },
330
331
  tools: {
331
332
  register(tool) {
332
333
  registered.push(tool)
@@ -413,6 +414,7 @@ test('exports a disposable DSH plugin tool surface', async () => { const regist
413
414
  const disposed = []
414
415
  const effects = []
415
416
  apply({
417
+ get(name) { return this[name] },
416
418
  tools: {
417
419
  register(tool) {
418
420
  registered.push(tool)
@@ -425,7 +427,7 @@ test('exports a disposable DSH plugin tool surface', async () => { const regist
425
427
  })
426
428
 
427
429
  assert.equal(name, 'dsh-scout')
428
- assert.deepEqual(inject, ['tools', 'fs', 'web'])
430
+ assert.deepEqual(inject, ['tools'])
429
431
  assert.deepEqual(
430
432
  registered.map(tool => tool.name).sort(),
431
433
  [
@@ -521,6 +523,7 @@ test('exports and imports a case through the fs-backed tools', async () => {
521
523
  },
522
524
  }
523
525
  apply({
526
+ get(name) { return this[name] },
524
527
  tools: {
525
528
  register(tool) {
526
529
  registered.push(tool)
@@ -651,6 +654,7 @@ test('auto-persists each mutation when configured, and exports to the default di
651
654
  },
652
655
  }
653
656
  apply({
657
+ get(name) { return this[name] },
654
658
  tools: {
655
659
  register(tool) {
656
660
  registered.push(tool)
@@ -690,6 +694,7 @@ test('auto-persists each mutation when configured, and exports to the default di
690
694
  test('scout_questions tool returns the derived question list', async () => {
691
695
  const registered = []
692
696
  apply({
697
+ get(name) { return this[name] },
693
698
  tools: {
694
699
  register(tool) {
695
700
  registered.push(tool)
@@ -718,6 +723,7 @@ test('auto-persist and default export dir degrade gracefully without fs or scout
718
723
  // autoPersist enabled but no fs service: mutations still succeed
719
724
  const noFs = []
720
725
  apply({
726
+ get(name) { return this[name] },
721
727
  tools: { register(tool) { noFs.push(tool); return () => undefined } },
722
728
  effect(execute) { Array.from(execute()) },
723
729
  }, { scoutDir: '/nowhere', autoPersist: true })
@@ -743,6 +749,7 @@ test('auto-persist and default export dir degrade gracefully without fs or scout
743
749
  async writeText(target, content) { store.set(target.displayPath, content); return { operation: 'create' } },
744
750
  }
745
751
  apply({
752
+ get(name) { return this[name] },
746
753
  tools: { register(tool) { registered.push(tool); return () => undefined } },
747
754
  fs: mockFs,
748
755
  effect(execute) { Array.from(execute()) },
@@ -854,6 +861,7 @@ test('infers source type and evidence level from URLs', () => {
854
861
  test('scout_ingest batch-registers sources with inference and isolates errors', async () => {
855
862
  const registered = []
856
863
  apply({
864
+ get(name) { return this[name] },
857
865
  tools: { register(tool) { registered.push(tool); return () => undefined } },
858
866
  effect(execute) { Array.from(execute()) },
859
867
  })
@@ -906,6 +914,7 @@ test('scout_ingest isolates null items and invalid explicit enums, and avoids su
906
914
 
907
915
  const registered = []
908
916
  apply({
917
+ get(name) { return this[name] },
909
918
  tools: { register(tool) { registered.push(tool); return () => undefined } },
910
919
  effect(execute) { Array.from(execute()) },
911
920
  })
@@ -999,6 +1008,7 @@ test('scout_compare tool compares cases and inference rejects invalid explicit e
999
1008
 
1000
1009
  const registered = []
1001
1010
  apply({
1011
+ get(name) { return this[name] },
1002
1012
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1003
1013
  effect(execute) { Array.from(execute()) },
1004
1014
  })
@@ -1020,6 +1030,7 @@ test('scout_compare tool compares cases and inference rejects invalid explicit e
1020
1030
  test('scout_compare deduplicates case ids and empty claims render cleanly', async () => {
1021
1031
  const registered = []
1022
1032
  apply({
1033
+ get(name) { return this[name] },
1023
1034
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1024
1035
  effect(execute) { Array.from(execute()) },
1025
1036
  })
@@ -1068,6 +1079,7 @@ test('scout_ingest drafts claims from items alongside sources', async () => {
1068
1079
 
1069
1080
  const registered = []
1070
1081
  apply({
1082
+ get(name) { return this[name] },
1071
1083
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1072
1084
  effect(execute) { Array.from(execute()) },
1073
1085
  })
@@ -1115,6 +1127,7 @@ test('scout_ingest drafts claims from items alongside sources', async () => {
1115
1127
  test('scout_ingest rejects invalid claim enums explicitly and keeps claim ids collision-safe', async () => {
1116
1128
  const registered = []
1117
1129
  apply({
1130
+ get(name) { return this[name] },
1118
1131
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1119
1132
  effect(execute) { Array.from(execute()) },
1120
1133
  })
@@ -1167,6 +1180,7 @@ test('covers registry boundaries, non-object claims, and verified-E0 rejection v
1167
1180
  const registered = []
1168
1181
  const store = new Map()
1169
1182
  apply({
1183
+ get(name) { return this[name] },
1170
1184
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1171
1185
  fs: {
1172
1186
  async resolve(path) { return { targetKey: path, displayPath: path } },
@@ -1211,6 +1225,7 @@ test('scout_search registers provider results as sources and requires the web se
1211
1225
  const registered = []
1212
1226
  const searched = []
1213
1227
  apply({
1228
+ get(name) { return this[name] },
1214
1229
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1215
1230
  web: {
1216
1231
  async search(request) {
@@ -1259,6 +1274,7 @@ test('scout_search registers provider results as sources and requires the web se
1259
1274
  // web service missing -> explicit error
1260
1275
  const noWeb = []
1261
1276
  apply({
1277
+ get(name) { return this[name] },
1262
1278
  tools: { register(tool) { noWeb.push(tool); return () => undefined } },
1263
1279
  effect(execute) { Array.from(execute()) },
1264
1280
  })
@@ -1277,6 +1293,7 @@ test('scout_search handles provider edge cases and limit bounds', async () => {
1277
1293
  const providerCalls = []
1278
1294
  let failSearch = false
1279
1295
  apply({
1296
+ get(name) { return this[name] },
1280
1297
  tools: { register(tool) { registered.push(tool); return () => undefined } },
1281
1298
  web: {
1282
1299
  async search(request) {
@@ -1339,6 +1356,7 @@ test('scout_search handles provider edge cases and limit bounds', async () => {
1339
1356
  // non-array sources -> reported as an error, tool still succeeds
1340
1357
  const noArray = []
1341
1358
  apply({
1359
+ get(name) { return this[name] },
1342
1360
  tools: { register(tool) { noArray.push(tool); return () => undefined } },
1343
1361
  web: { async search() { return { sources: 'nope' } } },
1344
1362
  effect(execute) { Array.from(execute()) },
@@ -1354,3 +1372,50 @@ test('scout_search handles provider edge cases and limit bounds', async () => {
1354
1372
  assert.equal(nonArray.added.length, 0)
1355
1373
  assert.match(nonArray.errors[0].error, /non-array sources/)
1356
1374
  })
1375
+
1376
+ test('exports a schemastery Config with sensible defaults', () => {
1377
+ const parsed = Config({})
1378
+ assert.equal(parsed.scoutDir, 'dsh-scout')
1379
+ assert.equal(parsed.autoPersist, false)
1380
+ assert.ok(parsed.authorityHostSuffixes.includes('gov.cn'))
1381
+ assert.ok(parsed.authorityHostSuffixes.includes('gsxt.gov.cn') === false)
1382
+ })
1383
+
1384
+ test('custom authority host suffixes extend the E3 trust boundary', async () => {
1385
+ const registered = []
1386
+ apply({
1387
+ get(name) { return this[name] },
1388
+ tools: {
1389
+ register(tool) {
1390
+ registered.push(tool)
1391
+ return () => undefined
1392
+ },
1393
+ },
1394
+ effect(execute) {
1395
+ Array.from(execute())
1396
+ },
1397
+ }, {
1398
+ authorityHostSuffixes: ['example-registry.example'],
1399
+ })
1400
+ const tools = new Map(registered.map(tool => [tool.name, tool]))
1401
+ const start = tools.get('scout_start')
1402
+ const addSource = tools.get('scout_add_source')
1403
+ await start.execute({
1404
+ caseId: 'custom-trust',
1405
+ companyName: 'Custom Co',
1406
+ roleTitle: 'Role',
1407
+ location: '',
1408
+ }, { agent: { id: 'session-a' } })
1409
+ // The configured suffix makes this registry origin a valid E3 source.
1410
+ const result = await addSource.execute({
1411
+ caseId: 'custom-trust',
1412
+ sourceId: 'registry',
1413
+ sourceType: 'company_registry',
1414
+ title: 'Custom registry',
1415
+ url: 'https://registry.example-registry.example/rec/1',
1416
+ evidenceLevel: 'E3',
1417
+ }, { agent: { id: 'session-a' } })
1418
+ assert.match(result, /"evidenceLevel": "E3"/)
1419
+ // The default trust boundary still rejects the same origin without the override.
1420
+ assert.equal(isTrustedAuthorityUrl('https://registry.example-registry.example/rec/1'), false)
1421
+ })