dsh-repo-scanner 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 约定。
4
+
5
+ ## Unreleased
6
+
7
+ - 新增 `prepublishOnly` 钩子:npm 发布前自动运行测试门禁。
8
+
9
+ ## 0.1.0 - 2026-09-02
10
+
11
+ 首个对外版本。开发过程曾自标 `1.0.0`(2026-08-23 完成度快照),因从未对外发布、且 v1.0 验收目标(arch-doc / dsh-refactor-insight 经适配层复用内核)尚未达成,按语义化版本回落为 `0.1.0` 首发。
12
+
13
+ ### DSH bundle 契约对齐(2026-09-02)
14
+
15
+ - `package.json` 声明 `dsh.bundle.patch`(现行 harness 只激活声明该字段的包),`main` 改指 `plugin/index.js`;扫描内核经 exports 子路径 `dsh-repo-scanner/scanner` 暴露,CLI bin 不变。
16
+ - `cordis.patch.yml` 重写为 config-tree `- insert:` 补丁格式,与 arch-doc / dsh-data-insight / dsh-refactor-insight / dsh-preset-scaffold 一致。
17
+ - `plugin/index.js` 重写为官方 `FileSystemSkillProvider` 标准模式(providerName / includeDefaultRoots: false / customSkillDirs + dispose effect),与 arch-doc 同构。
18
+ - `skills/repo-scanner-runbook/SKILL.md` 补充 frontmatter(`name` + `description`),满足现行技能识别规则(frontmatter 必填、name kebab-case)。
19
+ - 新增 optional peerDependencies `@deepseek-ai/dsh-skill-filesystem`。
20
+ - 版本统一:`package.json` 与 `TOOL_VERSION` 均为 `0.1.0`(`schema_version: 1.0` 为输出契约版本,独立于包版本,不受影响)。
21
+ - 文档清理:移除内部路线图遗留的 v0.2 / v0.3 / v1.0 特性标记;库接口示例统一改为 `dsh-repo-scanner/scanner` 导入。
22
+
23
+ ### 修复(2026-08-23 完成度快照)
24
+
25
+ - 修复 `src/dependencies.mjs` 编码损坏导致整个包无法加载的 SyntaxError(注释与代码被合并到同一行)。
26
+ - `npm run check` 现在校验全部 `src/*.mjs`、`bin` 与 `plugin`,不再漏检源码模块。
27
+ - `working_tree_clean` 语义明确:仅当提供 statusText 时判定(空输出 = 干净),其余场景为 null。
28
+ - 修复行数统计对以换行结尾文件多算 1 行的问题;读取时剥离 UTF-8 BOM。
29
+ - `--hash` 的 sha256 改为按原始字节计算(此前对二进制文件哈希的是 UTF-8 转码结果)。
30
+ - 修复 tsconfig 位于子目录且 baseUrl 非根时别名解析基准目录错误的问题。
31
+ - 修复 `normalizeDirList` 将 `..` 规整为 `.` 绕过越界检查的问题。
32
+ - 修复 pyproject `[project]` / `[project.scripts]` 为最后一个 TOML 段时解析不到的问题。
33
+ - CLI 补齐已实现选项的旗标:`--follow-symlinks`、`--cache`、`--cache-dir`、`--parsers`、符号查询、性能预算;`--base/--head` 记录到 `git.compare`。
34
+
35
+ ### 能力
36
+
37
+ - 增量扫描缓存:`cache: true`(按 mtime/size 失效,sha256 结果一并缓存;只写系统临时目录)。
38
+ - TS/JS import 别名解析增强:tsconfig `extends` 链、一个 pattern 多 target、JSONC 注释、baseUrl 裸模块名。
39
+ - OpenAPI/GraphQL/数据库迁移文件识别并呈现到 `project.openapi_files` / `graphql_files` / `db_migration_files`。
40
+ - 符号查询:`querySymbols` / `symbolQuery` 选项 / CLI `--symbol-name/--symbol-file/--symbol-module`。
41
+ - 可插拔解析器接口:`registerParser` / `resolveParserChain`;内置 heuristic;`tree-sitter` 为可选依赖,缺失时自动回退并写 `E_PARSER_UNAVAILABLE` 警告。
42
+ - 图分析模式 `--graphs`:模块级调用图 + 符号级引用图(命名导入解析到目标导出符号,带 confidence)。
43
+ - 统一 `dsh-analysis-schema`:输出外壳携带 `analysis_schema` 标识,供多个分析插件共享报告结构。
44
+ - 性能预算:`performance` 字段(elapsed_ms/files_indexed/bytes_read/cache 命中/budgets/budget_exceeded),默认 60000ms,超限写 `E_PERF_BUDGET_EXCEEDED` 警告。
45
+ - 大仓库增量索引:符号结果按文件缓存,未变化文件跳过重复解析。
46
+ - 可执行安全审计测试(`test/security.test.mjs`):路径越界、符号链接逃逸、脱敏、内建模块白名单、无子进程/网络红线。
47
+ - 完整事实 API:`getChangeImpactFacts`(反向依赖传播)、`getTestInsightFacts`(测试↔源码映射与模块覆盖)、`getDocSyncFacts`(文档引用与过期引用)。
48
+ - 稳定 schema 兼容策略:已发布字段不删除,新增字段只做向后兼容增加。
49
+ - `extractSymbols` 为 async(解析器链可能加载可选依赖);支持 heuristic 解析 `export { a as b }` 语句。
50
+ - fixtures:`ts-alias-app`(extends/多 target 别名)、`security-app`(脱敏)、monorepo API 契约文件、node-app README。
51
+ - CI:`.github/workflows/ci.yml`(Node 18/20/22 × Windows/Ubuntu)。
52
+ - 输出契约:schema 字段文档、扫描规则、示例输出与 README 同步。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 dsh-repo-scanner contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PUBLISHING.md ADDED
@@ -0,0 +1,23 @@
1
+ # Publishing
2
+
3
+ 1. 运行 `npm test` 和 `npm run check`,确保全部通过。
4
+ 2. 运行 `npm pack --dry-run`,确认包含 `plugin/index.js`、`cordis.patch.yml`、`skills/`、`src/`、`bin/`、`docs/`、`examples/`、`README.md`、`CHANGELOG.md`、`LICENSE`、`PUBLISHING.md`。
5
+ 3. 发布前确认 `docs/output-schema.md` 与 `examples/sample-output.json` 与实际输出一致(`tool.version` 同步为当前版本号)。
6
+ 4. 提升 `package.json`、`src/options.mjs` 的 `TOOL_VERSION`、`CHANGELOG.md` 与 git tag 版本号,保持一致。
7
+
8
+ ## DSH bundle(已对齐 2026-09 现行契约)
9
+
10
+ - `package.json` 声明 `dsh.bundle.patch: ./cordis.patch.yml`——harness 只激活声明该字段的包。
11
+ - `cordis.patch.yml` 为 config-tree `- insert:` 补丁格式;harness 加载 `main`(`plugin/index.js`)。
12
+ - `plugin/index.js` 经官方 `@deepseek-ai/dsh-skill-filesystem` 的 `FileSystemSkillProvider` 注册 `skills/` 为技能根(includeDefaultRoots: false)。
13
+ - `skills/repo-scanner-runbook/SKILL.md` frontmatter 必填 `name`(kebab-case)+ `description`。
14
+ - 扫描内核经 exports 子路径 `dsh-repo-scanner/scanner` 暴露;CLI bin 不变。
15
+
16
+ ## 发布渠道
17
+
18
+ 1. push `main`,确认 GitHub Actions CI 全绿(Node 18/20/22 × Windows/Ubuntu)。(2026-09-02 首推已绿)
19
+ 2. 打 tag `v0.1.0` 并推送。(已完成)
20
+ 3. (可选)`npm publish --access public`。
21
+ 4. 提交收录:awesome-dsh-plugin(`data/plugins/duyanta123__dsh-repo-scanner.yml`)与 awesome-deepseek-harness(en/zh README 条目,同一 PR)。
22
+
23
+ > 注:GitHub 仓库原名 `-dsh-repo-scanner`(建仓笔误)已于 2026-09-02 更名为 `dsh-repo-scanner`,旧链接自动重定向。
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # dsh-repo-scanner
2
+
3
+ 面向 DeepSeek Harness 分析型插件(`arch-doc`、`dsh-refactor-insight`、`dsh-change-impact`、`dsh-test-insight`)的统一、可复现、只读的代码库事实扫描内核。
4
+
5
+ 只读:不修改目标仓库,不安装依赖,不执行项目代码。
6
+
7
+ ## 作为 DSH 插件安装
8
+
9
+ 本包按 DSH bundle 规范打包(`package.json` 声明 `dsh.bundle.patch`),安装后自动注册 `repo-scanner-runbook` 技能:
10
+
11
+ ```sh
12
+ dsh plugin --profile web add "github:duyanta123/dsh-repo-scanner#main"
13
+ ```
14
+
15
+ 安装后重启 `dsh --profile web`,技能即可被发现;技能只在需要时加载 runbook,扫描本身通过 shell 调用 CLI 完成。上层插件(dsh-change-impact / dsh-test-insight 等)以库形式依赖本包时,经 exports 子路径 `dsh-repo-scanner/scanner` 引入扫描内核。
16
+
17
+ ## 快速开始
18
+
19
+ ```bash
20
+ node bin/repo-scanner.mjs <repo_path> --probe
21
+ node bin/repo-scanner.mjs <repo_path> --files
22
+ node bin/repo-scanner.mjs <repo_path> --scan
23
+ node bin/repo-scanner.mjs <repo_path> --deps
24
+ node bin/repo-scanner.mjs <repo_path> --entry
25
+ node bin/repo-scanner.mjs <repo_path> --symbols
26
+ node bin/repo-scanner.mjs <repo_path> --graphs
27
+ node bin/repo-scanner.mjs <repo_path> --git --diff-text-file diff.txt
28
+ node bin/repo-scanner.mjs <repo_path> --all --json
29
+ ```
30
+
31
+ 库接口:
32
+
33
+ ```js
34
+ import { scanRepository } from 'dsh-repo-scanner/scanner';
35
+
36
+ const report = await scanRepository({
37
+ repoPath: '.',
38
+ modes: ['probe', 'modules', 'dependencies', 'entries', 'symbols', 'graphs', 'git'],
39
+ maxDepth: 3,
40
+ cache: true, // 增量扫描缓存
41
+ parsers: ['heuristic'], // 可插拔解析器(tree-sitter 为可选依赖)
42
+ symbolQuery: { name: 'auth' }, // 符号查询
43
+ git: { diffText },
44
+ });
45
+ ```
46
+
47
+ ### 事实 API
48
+
49
+ ```js
50
+ import {
51
+ getChangeImpactFacts, // 变更影响:反向依赖传播 + 受影响模块/符号
52
+ getTestInsightFacts, // 测试洞察:测试↔源码映射 + 模块覆盖
53
+ getDocSyncFacts, // 文档同步:文档引用 + 过期引用
54
+ } from 'dsh-repo-scanner/scanner';
55
+
56
+ const impact = await getChangeImpactFacts({
57
+ repoPath: '.',
58
+ git: { statusText }, // 或 diffText / changedFiles
59
+ });
60
+ ```
61
+
62
+ ## 输出示例
63
+
64
+ ```json
65
+ {
66
+ "schema_version": "1.0",
67
+ "analysis_schema": { "name": "dsh-analysis-schema", "version": "1.0" },
68
+ "tool": { "name": "dsh-repo-scanner", "version": "0.1.0" },
69
+ "input": { "repo_path": ".", "resolved_path": "C:/work/app", "options": {} },
70
+ "limits": { "max_depth": 3, "max_files": 2000, "max_file_bytes": 256000, "truncated": false, "warnings": [] },
71
+ "project": {},
72
+ "files": [],
73
+ "modules": [],
74
+ "dependencies": { "internal": [], "external": [] },
75
+ "entry_points": [],
76
+ "run_methods": [],
77
+ "symbols": [],
78
+ "graphs": null,
79
+ "risks": [],
80
+ "errors": [],
81
+ "performance": null,
82
+ "git": null
83
+ }
84
+ ```
85
+
86
+ 完整字段说明见 `docs/output-schema.md`;扫描规则见 `docs/scanning-rules.md`。
87
+
88
+ ## 参数与退出码
89
+
90
+ | 参数 | 默认 | 说明 |
91
+ | --- | --- | --- |
92
+ | `--max-depth N` | 3 | 最大目录深度 |
93
+ | `--max-files N` | 2000 | 最大文件数 |
94
+ | `--max-file-bytes N` | 256000 | 单文件内容读取上限 |
95
+ | `--include-dirs a,b` | 空 | 只扫描这些目录 |
96
+ | `--exclude-dirs a,b` | 内置默认 | 追加排除目录 |
97
+ | `--language LANG` | 空 | 语言过滤 |
98
+ | `--format json\|jsonl` | json | 输出格式 |
99
+ | `--hash` | 关 | 计算文件 sha256(按原始字节) |
100
+ | `--strict` | 关 | 存在错误或警告时非零退出 |
101
+ | `--follow-symlinks` | 关 | 跟随符号链接(目标必须在仓库内) |
102
+ | `--cache` / `--cache-dir DIR` | 关 | 增量扫描缓存(只写临时目录) |
103
+ | `--parsers a,b` | heuristic | 符号解析器链(tree-sitter 为可选依赖,缺失自动回退) |
104
+ | `--symbol-name/file/module` | 空 | 符号查询过滤 |
105
+ | `--perf-budget-ms N` | 60000 | 性能预算(0 关闭;超限写 warning) |
106
+
107
+ 退出码:`0` 成功;`1` 存在错误,或 `--strict` 下存在警告;`2` 参数错误或仓库路径无效;`3` 输出失败或契约错误。
108
+
109
+ ## 安全红线
110
+
111
+ - 不写入目标仓库;不做 checkout/reset/clean。
112
+ - 不安装依赖、不执行目标项目代码或脚本。
113
+ - 路径规范化后拒绝越界;默认不跟随符号链接。
114
+ - 不 spawn 子进程获取文件或 Git 事实(由可执行安全审计测试保障)。
115
+ - 只把能解析到仓库内部的导入归为 internal;动态 import/require 记入 `risks`。
116
+ - 输出对 token/密码/连接串/JWT 自动脱敏。
117
+
118
+ ## 与 arch-doc / dsh-refactor-insight 的关系
119
+
120
+ 两个插件都有各自的 `arch-profile.mjs`。本包提取其扫描逻辑,通过 npm dependency 或 CLI 被它们复用。迁移顺序与字段映射见 `docs/migration-guide.md`。
121
+
122
+ ## 开发
123
+
124
+ ```bash
125
+ npm test # 功能测试 + 安全审计测试
126
+ npm run check
127
+ ```
128
+
129
+ Node >=18。
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { scanRepository, TOOL_NAME, TOOL_VERSION, SCHEMA_VERSION, validateReport, normalizeOptions } from '../src/index.mjs';
4
+ import { serializeReport } from '../src/output.mjs';
5
+
6
+ const USAGE = `dsh-repo-scanner ${TOOL_VERSION}
7
+
8
+ Usage:
9
+ node bin/repo-scanner.mjs <repo_path> [mode] [options]
10
+
11
+ Modes:
12
+ --probe 仓库探测:语言、仓库类型、技术栈、manifest
13
+ --files 文件索引
14
+ --scan 模块识别(等价 modes: scan/modules)
15
+ --deps 依赖关系(internal/external)
16
+ --entry 入口与运行方式
17
+ --symbols 符号索引
18
+ --graphs 图分析:模块调用图 + 符号引用图
19
+ --git Git 事实(HEAD、分支、变更文件;可用 --diff-text-file/--status-text-file 提供只读 diff 文本)
20
+ --all 全部模式(默认)
21
+
22
+ Options:
23
+ --max-depth N 最大目录深度(默认 3)
24
+ --max-files N 最大文件数(默认 2000)
25
+ --max-file-bytes N 单文件内容读取上限(默认 256000)
26
+ --include-dirs a,b 只扫描这些目录(root 文件始终保留)
27
+ --exclude-dirs a,b 追加排除目录(默认排除 .git/node_modules/dist 等)
28
+ --language LANG 按语言过滤文件
29
+ --format json|jsonl 输出格式(默认 json)
30
+ --hash 计算文件 sha256
31
+ --strict 本应当失败的错误(输出校验、路径越界、存在错误)或警告时非零退出
32
+ --follow-symlinks 跟随符号链接(默认关闭,链接目标必须在仓库内)
33
+ --cache 启用增量扫描缓存(只写系统临时目录)
34
+ --cache-dir DIR 缓存目录(默认 os.tmpdir()/dsh-repo-scanner-cache)
35
+ --parsers a,b 符号解析器(heuristic | tree-sitter;tree-sitter 为可选依赖)
36
+ --symbol-name NAME 符号查询:按名称过滤(不区分大小写子串)
37
+ --symbol-file FILE 符号查询:按文件过滤(精确或后缀匹配)
38
+ --symbol-module NAME 符号查询:按模块过滤(模块路径或名称)
39
+ --perf-budget-ms N 性能预算毫秒数(默认 60000,超限写 warning;0 关闭)
40
+ --base <ref> Git base ref(记录到 git.compare,供变更影响分析)
41
+ --head <ref> Git head ref(记录到 git.compare,供变更影响分析)
42
+ --diff-text-file FILE 只读读取 diff 全文(供 --git)
43
+ --status-text-file FILE 只读读取 git status 文本(供 --git)
44
+ --json 等价 --format json
45
+ --version 打印版本
46
+ --help 打印帮助
47
+ `;
48
+
49
+ export function parseArgs(argv) {
50
+ const args = [...argv];
51
+ const positional = [];
52
+ const modes = [];
53
+ const options = {};
54
+ let help = false;
55
+ let version = false;
56
+
57
+ const readValue = (name, fallback) => {
58
+ if (args.length === 0) {
59
+ throw new Error(`missing value for ${name}`);
60
+ }
61
+ const value = args.shift();
62
+ return value ?? fallback;
63
+ };
64
+
65
+ while (args.length > 0) {
66
+ const arg = args.shift();
67
+ switch (arg) {
68
+ case '--probe':
69
+ modes.push('probe');
70
+ break;
71
+ case '--files':
72
+ modes.push('files');
73
+ break;
74
+ case '--scan':
75
+ modes.push('scan');
76
+ break;
77
+ case '--deps':
78
+ modes.push('deps');
79
+ break;
80
+ case '--entry':
81
+ modes.push('entry');
82
+ break;
83
+ case '--symbols':
84
+ modes.push('symbols');
85
+ break;
86
+ case '--graphs':
87
+ modes.push('graphs');
88
+ break;
89
+ case '--git':
90
+ modes.push('git');
91
+ break;
92
+ case '--all':
93
+ modes.push('all');
94
+ break;
95
+ case '--max-depth':
96
+ options.maxDepth = readValue('--max-depth');
97
+ break;
98
+ case '--max-files':
99
+ options.maxFiles = readValue('--max-files');
100
+ break;
101
+ case '--max-file-bytes':
102
+ options.maxFileBytes = readValue('--max-file-bytes');
103
+ break;
104
+ case '--include-dirs':
105
+ options.includeDirs = readValue('--include-dirs');
106
+ break;
107
+ case '--exclude-dirs':
108
+ options.excludeDirs = readValue('--exclude-dirs');
109
+ break;
110
+ case '--language':
111
+ options.language = readValue('--language');
112
+ break;
113
+ case '--format':
114
+ options.format = readValue('--format');
115
+ break;
116
+ case '--json':
117
+ options.format = 'json';
118
+ break;
119
+ case '--hash':
120
+ options.hash = true;
121
+ break;
122
+ case '--strict':
123
+ options.strict = true;
124
+ break;
125
+ case '--follow-symlinks':
126
+ options.followSymlinks = true;
127
+ break;
128
+ case '--cache':
129
+ options.cache = true;
130
+ break;
131
+ case '--cache-dir':
132
+ options.cacheDir = readValue('--cache-dir');
133
+ break;
134
+ case '--parsers':
135
+ options.parsers = readValue('--parsers');
136
+ break;
137
+ case '--symbol-name':
138
+ options.symbolQuery = options.symbolQuery || {};
139
+ options.symbolQuery.name = readValue('--symbol-name');
140
+ break;
141
+ case '--symbol-file':
142
+ options.symbolQuery = options.symbolQuery || {};
143
+ options.symbolQuery.file = readValue('--symbol-file');
144
+ break;
145
+ case '--symbol-module':
146
+ options.symbolQuery = options.symbolQuery || {};
147
+ options.symbolQuery.module = readValue('--symbol-module');
148
+ break;
149
+ case '--perf-budget-ms':
150
+ options.perfBudgetMs = readValue('--perf-budget-ms');
151
+ break;
152
+ case '--base':
153
+ options.git = options.git || {};
154
+ options.git.base = readValue('--base');
155
+ break;
156
+ case '--head':
157
+ options.git = options.git || {};
158
+ options.git.head = readValue('--head');
159
+ break;
160
+ case '--diff-text-file': {
161
+ const file = readValue('--diff-text-file');
162
+ options.git = options.git || {};
163
+ options.git.diffText = readOwnFile(file);
164
+ break;
165
+ }
166
+ case '--status-text-file': {
167
+ const file = readValue('--status-text-file');
168
+ options.git = options.git || {};
169
+ options.git.statusText = readOwnFile(file);
170
+ break;
171
+ }
172
+ case '--help':
173
+ case '-h':
174
+ help = true;
175
+ break;
176
+ case '--version':
177
+ case '-v':
178
+ version = true;
179
+ break;
180
+ default:
181
+ if (arg.startsWith('--') && arg.includes('=')) {
182
+ const [name, value] = [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)];
183
+ args.unshift(value);
184
+ args.unshift(name);
185
+ continue;
186
+ }
187
+ if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`);
188
+ positional.push(arg);
189
+ break;
190
+ }
191
+ }
192
+
193
+ return { positional, modes, options, help, version };
194
+ }
195
+
196
+ function readOwnFile(filePath) {
197
+ try {
198
+ return fs.readFileSync(filePath, 'utf8');
199
+ } catch (err) {
200
+ throw new Error(`cannot read file ${filePath}: ${err.code || err.message}`);
201
+ }
202
+ }
203
+
204
+ function printError(error, exitCode) {
205
+ const payload = {
206
+ schema_version: SCHEMA_VERSION,
207
+ tool: { name: TOOL_NAME, version: TOOL_VERSION },
208
+ error: {
209
+ code: error.code || 'E_SCANNER',
210
+ message: error.message || String(error),
211
+ },
212
+ };
213
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
214
+ }
215
+
216
+ async function main() {
217
+ let parsed;
218
+ try {
219
+ parsed = parseArgs(process.argv.slice(2));
220
+ } catch (err) {
221
+ printError(err, 2);
222
+ process.exit(2);
223
+ }
224
+
225
+ if (parsed.help) {
226
+ process.stdout.write(USAGE);
227
+ process.exit(0);
228
+ }
229
+ if (parsed.version) {
230
+ process.stdout.write(`${TOOL_NAME} ${TOOL_VERSION}\n`);
231
+ process.exit(0);
232
+ }
233
+
234
+ if (parsed.positional.length === 0) {
235
+ printError(new Error('missing <repo_path> argument'), 2);
236
+ process.exit(2);
237
+ }
238
+ if (parsed.positional.length > 1) {
239
+ printError(new Error(`unexpected extra argument: ${parsed.positional[1]}`), 2);
240
+ process.exit(2);
241
+ }
242
+
243
+ const input = {
244
+ repoPath: parsed.positional[0],
245
+ modes: parsed.modes.length > 0 ? parsed.modes : ['all'],
246
+ ...parsed.options,
247
+ };
248
+
249
+ try {
250
+ const report = await scanRepository(input);
251
+ let output;
252
+ try {
253
+ output = serializeReport(report, parsed.options.format || 'json');
254
+ } catch (validationErr) {
255
+ printError(validationErr, 3);
256
+ process.exit(3);
257
+ }
258
+ process.stdout.write(`${output}\n`);
259
+
260
+ const warnings = report.limits?.warnings?.length || 0;
261
+ const errors = report.errors?.length || 0;
262
+ // 仅错误始终非零退出;警告默认不影响退出码,仅在 strict 下使命令失败。
263
+ if (errors > 0 || (parsed.options.strict && warnings > 0)) {
264
+ process.exit(1);
265
+ }
266
+ process.exit(0);
267
+ } catch (err) {
268
+ const code = err?.code;
269
+ if (code === 'E_INVALID_REPO_PATH' || code === 'E_PATH_TRAVERSAL') {
270
+ printError(err, 2);
271
+ process.exit(2);
272
+ }
273
+ if (code === 'E_OUTPUT_VALIDATION') {
274
+ printError(err, 3);
275
+ process.exit(3);
276
+ }
277
+ printError(err, 3);
278
+ process.exit(3);
279
+ }
280
+ }
281
+
282
+ await main();
@@ -0,0 +1,11 @@
1
+ # dsh-repo-scanner — DSH bundle patch。
2
+ #
3
+ # 当某个 profile 在 dsh.profile.bundles 中列出本包时(dsh plugin add 即完成
4
+ # 此事),dsh 启动时把本补丁打入配置树:插入本包插件行,加载 package.json
5
+ # 的 main(plugin/index.js),将自带 skills/ 注册为技能根。
6
+ #
7
+ # 人设与工具不在此处重复声明——宿主 profile(standard/web)已提供文件、
8
+ # shell、计划模式、web 搜索、子代理等 runbook 引用的全部工具。
9
+ - insert:
10
+ - id: dsh-repo-scanner
11
+ name: dsh-repo-scanner
@@ -0,0 +1,40 @@
1
+ # 从 arch-doc / dsh-refactor-insight 迁移
2
+
3
+ 两个插件当前各自维护 `arch-profile.mjs`。迁移顺序:
4
+
5
+ 1. 冻结旧输出字段,建立快照测试。
6
+ 2. 确认旧字段在新 schema 中的映射(见下表)。
7
+ 3. 引入 `dsh-repo-scanner` 作为 npm dependency,通过库接口或 CLI 调用。
8
+ 4. 旧命令保留兼容包装一段时间。
9
+ 5. 两侧输出稳定后删除重复实现。
10
+
11
+ ## 字段映射
12
+
13
+ | 旧字段 | 新字段 | 备注 |
14
+ | --- | --- | --- |
15
+ | mainLanguage | project.language | 附 `language_evidence` |
16
+ | repoType | project.repo_type | 附 `repo_type_evidence` |
17
+ | files[] | files[] | 路径统一 POSIX `/` |
18
+ | modules[] | modules[] | 不推断职责,保留 path/file_count/key_files/evidence |
19
+ | internalDeps[] | dependencies.internal[] | 仅可解析内部路径 |
20
+ | externalDeps[] | dependencies.external[] | 含 manifest 版本 |
21
+ | entrypoints/frameworks | entry_points[] | type 使用 web/cli/worker/scheduler/library |
22
+ | runCommands[] | run_methods[] | 必须标注 `source` |
23
+
24
+ ## 调用示例
25
+
26
+ ```js
27
+ import { scanRepository } from 'dsh-repo-scanner/scanner';
28
+
29
+ const report = await scanRepository({ repoPath, modes: ['probe', 'scan', 'deps'] });
30
+ ```
31
+
32
+ CLI 方式:
33
+
34
+ ```bash
35
+ node bin/repo-scanner.mjs <repo_path> --probe --scan --deps --json
36
+ ```
37
+
38
+ ## 兼容包装
39
+
40
+ 旧命令可以先保留,并把结果转换为旧字段形状;不要在旧包装中重新实现扫描逻辑。
@@ -0,0 +1,111 @@
1
+ # Output Schema v1.0
2
+
3
+ 所有命令输出统一外壳。字段名使用 snake_case;路径使用 POSIX `/`;行号从 1 开始。
4
+
5
+ ## 兼容策略(v1.0)
6
+
7
+ - 已发布字段不删除、不改语义;新增字段只做向后兼容增加。
8
+ - 只有发生不兼容变化时才升级 `schema_version` 主版本。
9
+ - `schema_version: "1.0"` 自 v0.1 起保持稳定;v1.0 新增 `analysis_schema`、`graphs`、`performance` 均为增量字段。
10
+
11
+ ## 外壳
12
+
13
+ | 字段 | 类型 | 说明 |
14
+ | --- | --- | --- |
15
+ | schema_version | string | 固定 `"1.0"` |
16
+ | analysis_schema | object | `dsh-analysis-schema` 标识(name/version),供多个分析插件共享报告结构 |
17
+ | tool.name | string | `"dsh-repo-scanner"` |
18
+ | tool.version | string | 包版本 |
19
+ | input.repo_path | string | 调用方传入的原始 repo_path |
20
+ | input.resolved_path | string | 规范化后的绝对路径 |
21
+ | input.options | object | 当前扫描选项摘要(modes/language/hash/strict/follow_symlinks/cache/parsers/symbol_query) |
22
+ | limits.max_depth | number | 最大目录深度 |
23
+ | limits.max_files | number | 最大文件数 |
24
+ | limits.max_file_bytes | number | 单文件内容读取上限 |
25
+ | limits.truncated | boolean | 是否因上限发生截断 |
26
+ | limits.warnings | array | 可恢复警告(含解析器回退、性能预算超限) |
27
+ | project | object | probe 结果;无能力时可为空对象 |
28
+ | files | array | 文件索引 |
29
+ | modules | array | 模块识别结果 |
30
+ | dependencies.internal | array | 仓库内依赖,必须能解析到内部路径 |
31
+ | dependencies.external | array | 第三方依赖导入与 manifest 声明 |
32
+ | entry_points | array | 入口分类(web/cli/worker/scheduler/library) |
33
+ | run_methods | array | 运行命令与来源(命令经过脱敏) |
34
+ | symbols | array | 符号索引,`parser` 字段标注实际使用的解析器 |
35
+ | graphs | object/null | graphs 模式时输出模块调用图与符号引用图,否则为 null |
36
+ | risks | array | 无法可靠解析的依赖等风险 |
37
+ | errors | array | 扫描过程中无法恢复的错误(默认保持空) |
38
+ | performance | object/null | 实测性能指标与预算(elapsed_ms/files_indexed/bytes_read/cache/budgets/budget_exceeded) |
39
+ | git | object/null | git 模式时输出,否则为 null |
40
+
41
+ ## project 字段(probe)
42
+
43
+ | 字段 | 类型 | 说明 |
44
+ | --- | --- | --- |
45
+ | name / description | string/null | 来自 manifest 或目录名 |
46
+ | language | string/null | 主语言 |
47
+ | repo_type | string | monolith/monorepo/library/service/cli/unknown |
48
+ | repo_type_evidence | array | 判定证据 |
49
+ | tech_stack | array | 技术栈标签 |
50
+ | manifest_files | array | manifest 文件路径 |
51
+ | openapi_files | array | OpenAPI/Swagger 契约文件 |
52
+ | graphql_files | array | GraphQL schema 文件 |
53
+ | db_migration_files | array | 数据库迁移文件 |
54
+ | file_count / source_file_count | number | 文件统计 |
55
+ | language_evidence | array | 主语言证据文件 |
56
+ | confidence | string | high/medium |
57
+
58
+ ## graphs 字段
59
+
60
+ ```json
61
+ {
62
+ "module_call_graph": {
63
+ "nodes": [{"name": "api", "path": "src/api", "file_count": 1}],
64
+ "edges": [{"source": "src/api", "target": "src/auth", "weight": 2, "imports": [{"source": "...", "target": "..."}]}]
65
+ },
66
+ "symbol_references": [
67
+ {"source_file": "src/api/server.ts", "target_file": "src/auth/service.ts", "symbol": "AuthService", "kind": "named-import", "line": 2, "confidence": "high"}
68
+ ]
69
+ }
70
+ ```
71
+
72
+ - `weight`:聚合到模块维度的导入语句数量。
73
+ - `symbol_references.kind`:named-import / default-import / namespace-import / from-import / star-import。
74
+ - 只输出可解析的事实边;无法确认的绑定不强行猜测。
75
+
76
+ ## git 字段
77
+
78
+ | 字段 | 类型 | 说明 |
79
+ | --- | --- | --- |
80
+ | available | boolean | 是否找到 .git 目录 |
81
+ | head_ref / branch / head_sha | string/null | 从 .git/HEAD 只读解析 |
82
+ | working_tree_clean | boolean/null | 仅当提供 statusText 时可判定;不可判定时为 null |
83
+ | changed_files | array | 调用方提供的 diff/status/清单解析结果 |
84
+ | compare | object | 调用方提供的 base/head ref(供变更影响分析记录) |
85
+ | method | string/null | changed_files 的来源方式 |
86
+ | warnings | array | Git 相关警告 |
87
+
88
+ ## 事实 API 输出
89
+
90
+ `getChangeImpactFacts` / `getTestInsightFacts` / `getDocSyncFacts` 返回独立外壳(含 schema_version、analysis_schema、tool),字段:
91
+
92
+ - 变更影响:`changed_files`、`impacted_files`(path/via/depth,最大传播深度 3)、`impacted_modules`、`impacted_symbols`。
93
+ - 测试洞察:`test_files`(test_file→target_file 映射)、`orphan_tests`、`module_test_coverage`(module/name/source_file_count/has_tests/tested_files)、`source_files_without_tests`。
94
+ - 文档同步:`docs`(doc/referenced_source_files/reference_count)、`docs_with_stale_references`、`stale_references`(doc/path)、`source_files_without_doc_references`。
95
+
96
+ ## 空值约定
97
+
98
+ - 空集合统一返回 `[]`。
99
+ - 不存在的可选对象使用 `null`。
100
+ - 语言无法识别时为 `null`。
101
+ - `lines`/`sha256` 未读取时为 `null`。
102
+
103
+ ## 稳定路径示例
104
+
105
+ ```json
106
+ {
107
+ "path": "src/auth/service.ts"
108
+ }
109
+ ```
110
+
111
+ 禁止出现反斜杠路径;Windows 扫描必须转换为 POSIX 分隔符。