@liuhange/dsh-data-asset-inventory-scan 3.0.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.
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AssetTripleConditionScreener } from '../assetTripleConditionScreener.js';
3
+ import { OwnershipClueAnnotator } from '../ownershipClueAnnotator.js';
4
+ describe('AssetTripleConditionScreener', () => {
5
+ const config = {
6
+ pastTransactionKeywords: ['购买', '合同', '交易'],
7
+ ownershipControlKeywords: ['自有', '控制', '拥有'],
8
+ economicBenefitKeywords: ['收益', '价值', '利润'],
9
+ };
10
+ const screener = new AssetTripleConditionScreener(config);
11
+ it('三条件全满足→初筛通过', () => {
12
+ const result = screener.screen({
13
+ desc: '通过购买合同取得,企业自有控制,预期带来收益',
14
+ });
15
+ expect(result.passed).toBe(true);
16
+ expect(result.missingConditions).toHaveLength(0);
17
+ });
18
+ it('缺少过去交易→不通过', () => {
19
+ const result = screener.screen({
20
+ desc: '企业自有控制,预期带来收益',
21
+ });
22
+ expect(result.passed).toBe(false);
23
+ expect(result.missingConditions).toContain('过去交易或事项形成');
24
+ });
25
+ });
26
+ describe('OwnershipClueAnnotator', () => {
27
+ const rules = {
28
+ holdingRightFields: ['owner', 'holder'],
29
+ usageRightFields: ['license', 'usage'],
30
+ operationRightFields: ['admin', 'operator'],
31
+ };
32
+ const annotator = new OwnershipClueAnnotator(rules);
33
+ it('正确标注权属线索', () => {
34
+ const result = annotator.annotate({
35
+ owner: '某科技公司',
36
+ license: '授权使用',
37
+ admin: '管理部门',
38
+ });
39
+ expect(result.holdingRight).toBe('某科技公司');
40
+ expect(result.usageRight).toBe('授权使用');
41
+ expect(result.operationRight).toBe('管理部门');
42
+ });
43
+ it('无匹配时返回空线索', () => {
44
+ const result = annotator.annotate({
45
+ unknown: '无',
46
+ });
47
+ expect(result.holdingRight).toBe('');
48
+ expect(result.usageRight).toBe('');
49
+ expect(result.operationRight).toBe('');
50
+ });
51
+ });
52
+ //# sourceMappingURL=inventoryScan.test.js.map
@@ -0,0 +1,24 @@
1
+ export class AssetTripleConditionScreener {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ screen(metadata) {
7
+ const text = JSON.stringify(metadata).toLowerCase();
8
+ const missing = [];
9
+ const hasPastTransaction = this.config.pastTransactionKeywords.some(kw => text.includes(kw.toLowerCase()));
10
+ if (!hasPastTransaction)
11
+ missing.push('过去交易或事项形成');
12
+ const hasOwnershipControl = this.config.ownershipControlKeywords.some(kw => text.includes(kw.toLowerCase()));
13
+ if (!hasOwnershipControl)
14
+ missing.push('企业拥有或控制');
15
+ const hasEconomicBenefit = this.config.economicBenefitKeywords.some(kw => text.includes(kw.toLowerCase()));
16
+ if (!hasEconomicBenefit)
17
+ missing.push('预期带来经济利益');
18
+ return {
19
+ passed: missing.length === 0,
20
+ missingConditions: missing,
21
+ };
22
+ }
23
+ }
24
+ //# sourceMappingURL=assetTripleConditionScreener.js.map
package/lib/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { ScanExecutor } from './scanExecutor.js';
2
+ import { validateScanArgs } from './invariant.js';
3
+ export const name = '@liuhange/dsh-data-asset-inventory-scan';
4
+ export const inject = ['tools'];
5
+ export function apply(ctx) {
6
+ ctx.tools.register({
7
+ name: 'scan_data_assets',
8
+ description: '数据资产深度扫描:全量元数据扫描+资产三条件初筛+权属线索标注+政策依据输出',
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ scanSource: { type: 'string', description: '目录路径或数据库连接配置JSON' },
13
+ sourceType: { type: 'string', enum: ['directory', 'database'], description: '扫描源类型' },
14
+ outputPathDir: { type: 'string', description: '报告输出目录(可选)' },
15
+ },
16
+ required: ['scanSource', 'sourceType'],
17
+ },
18
+ async execute(args) {
19
+ try {
20
+ const validated = validateScanArgs(args);
21
+ let rulesConfig;
22
+ try {
23
+ const rulesPath = process.env.BUSINESS_RULES_PATH || 'config/business-rules.json';
24
+ const { readFileSync } = await import('node:fs');
25
+ rulesConfig = JSON.parse(readFileSync(rulesPath, 'utf-8'));
26
+ }
27
+ catch {
28
+ return JSON.stringify({ error: 'SCAN_RULES_MISSING', message: '无法加载business-rules.json' });
29
+ }
30
+ const inventoryConfig = rulesConfig.inventoryScan;
31
+ if (!inventoryConfig) {
32
+ return JSON.stringify({ error: 'SCAN_RULES_MISSING', message: 'inventoryScan配置段缺失' });
33
+ }
34
+ let policyDocuments;
35
+ try {
36
+ const policyPath = process.env.POLICY_REFS_PATH || 'config/policy-references.json';
37
+ const { readFileSync } = await import('node:fs');
38
+ const policyConfig = JSON.parse(readFileSync(policyPath, 'utf-8'));
39
+ const stage = policyConfig.policyReferences?.find((s) => s.stage === 'INVENTORY_SCAN');
40
+ policyDocuments = stage?.documents ?? [];
41
+ }
42
+ catch {
43
+ policyDocuments = [];
44
+ }
45
+ if (!policyDocuments || policyDocuments.length === 0) {
46
+ return JSON.stringify({ error: 'POLICY_REFERENCES_EMPTY', message: 'INVENTORY_SCAN阶段政策依据缺失' });
47
+ }
48
+ const executorConfig = {
49
+ tripleCondition: inventoryConfig.assetTripleCondition,
50
+ ownershipClueRules: inventoryConfig.ownershipClueRules,
51
+ scanScaleLimit: inventoryConfig.scanScaleLimit ?? 10000,
52
+ policyDocuments,
53
+ };
54
+ const executor = new ScanExecutor(executorConfig);
55
+ const report = executor.execute(validated.scanSource, validated.sourceType, validated.outputPathDir);
56
+ return JSON.stringify(report, null, 2);
57
+ }
58
+ catch (e) {
59
+ const err = e;
60
+ return JSON.stringify({
61
+ error: err.message.split(':')[0] ?? 'UNKNOWN_ERROR',
62
+ message: err.message,
63
+ });
64
+ }
65
+ },
66
+ });
67
+ }
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,25 @@
1
+ export function invariant(condition, message) {
2
+ if (!condition) {
3
+ throw new Error(`INVARIANT_VIOLATION: ${message}`);
4
+ }
5
+ }
6
+ export function validateScanArgs(args) {
7
+ if (!args || typeof args !== 'object') {
8
+ throw new Error('INVALID_ARGS: args must be an object');
9
+ }
10
+ if (!args.scanSource || typeof args.scanSource !== 'string') {
11
+ throw new Error('INVALID_ARGS: scanSource is required and must be a string');
12
+ }
13
+ if (!args.sourceType || !['directory', 'database'].includes(args.sourceType)) {
14
+ throw new Error('INVALID_ARGS: sourceType is required and must be "directory" or "database"');
15
+ }
16
+ const result = {
17
+ scanSource: args.scanSource,
18
+ sourceType: args.sourceType,
19
+ };
20
+ if (typeof args.outputPathDir === 'string') {
21
+ result.outputPathDir = args.outputPathDir;
22
+ }
23
+ return result;
24
+ }
25
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,23 @@
1
+ export class OwnershipClueAnnotator {
2
+ rules;
3
+ constructor(rules) {
4
+ this.rules = rules;
5
+ }
6
+ annotate(metadata) {
7
+ return {
8
+ holdingRight: this._extractFirst(metadata, this.rules.holdingRightFields),
9
+ usageRight: this._extractFirst(metadata, this.rules.usageRightFields),
10
+ operationRight: this._extractFirst(metadata, this.rules.operationRightFields),
11
+ };
12
+ }
13
+ _extractFirst(metadata, fields) {
14
+ for (const field of fields) {
15
+ const value = metadata[field];
16
+ if (value !== undefined && value !== null && String(value).trim() !== '') {
17
+ return String(value);
18
+ }
19
+ }
20
+ return '';
21
+ }
22
+ }
23
+ //# sourceMappingURL=ownershipClueAnnotator.js.map
@@ -0,0 +1,38 @@
1
+ import { ScanSourceAdapterFactory } from './scanSourceAdapter.js';
2
+ import { AssetTripleConditionScreener } from './assetTripleConditionScreener.js';
3
+ import { OwnershipClueAnnotator } from './ownershipClueAnnotator.js';
4
+ import { ScanReportGenerator } from './scanReportGenerator.js';
5
+ export class ScanExecutor {
6
+ config;
7
+ screener;
8
+ annotator;
9
+ reportGenerator;
10
+ constructor(config) {
11
+ this.config = config;
12
+ this.screener = new AssetTripleConditionScreener(config.tripleCondition);
13
+ this.annotator = new OwnershipClueAnnotator(config.ownershipClueRules);
14
+ this.reportGenerator = new ScanReportGenerator(config.policyDocuments);
15
+ }
16
+ execute(scanSource, sourceType, outputPathDir) {
17
+ const adapter = ScanSourceAdapterFactory.create(sourceType);
18
+ const metadata = adapter.scan(scanSource, this.config.scanScaleLimit);
19
+ const assetItems = metadata.map(obj => {
20
+ const screening = this.screener.screen(obj);
21
+ const ownershipClues = this.annotator.annotate(obj);
22
+ const item = {
23
+ id: obj.id,
24
+ name: obj.name,
25
+ sourceType,
26
+ metadata: obj,
27
+ initialScreening: screening.passed ? '初筛通过' : '初筛不通过',
28
+ ownershipClues,
29
+ };
30
+ if (screening.missingConditions.length > 0) {
31
+ item.missingConditions = screening.missingConditions;
32
+ }
33
+ return item;
34
+ });
35
+ return this.reportGenerator.generate(assetItems, scanSource, sourceType, outputPathDir);
36
+ }
37
+ }
38
+ //# sourceMappingURL=scanExecutor.js.map
@@ -0,0 +1,35 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export class ScanReportGenerator {
4
+ policyDocuments;
5
+ constructor(policyDocuments) {
6
+ this.policyDocuments = policyDocuments;
7
+ }
8
+ generate(assetItems, scanSource, sourceType, outputPathDir) {
9
+ if (!this.policyDocuments || this.policyDocuments.length === 0) {
10
+ throw new Error('POLICY_REFERENCES_EMPTY: policy documents must not be empty');
11
+ }
12
+ for (const doc of this.policyDocuments) {
13
+ if (!doc.name || doc.name === '依据文件待补充') {
14
+ throw new Error(`POLICY_REFERENCE_PLACEHOLDER: ${doc.name || 'undefined'}`);
15
+ }
16
+ }
17
+ const report = {
18
+ assetItems,
19
+ scanSource,
20
+ sourceType,
21
+ policyReferences: this.policyDocuments,
22
+ timestamp: new Date().toISOString(),
23
+ status: 'SUCCESS',
24
+ };
25
+ if (outputPathDir) {
26
+ const outputPath = join(outputPathDir, 'scan-report.json');
27
+ writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
28
+ }
29
+ return report;
30
+ }
31
+ formatPolicyReference(doc) {
32
+ return `依据:《${doc.name}》(${doc.docNumber})——${doc.coreRequirement}`;
33
+ }
34
+ }
35
+ //# sourceMappingURL=scanReportGenerator.js.map
@@ -0,0 +1,77 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join, extname, basename } from 'node:path';
3
+ export class DirectoryScanAdapter {
4
+ scan(dirPath, scaleLimit) {
5
+ const results = [];
6
+ this._scanRecursive(dirPath, results, scaleLimit);
7
+ if (results.length > scaleLimit) {
8
+ throw new Error(`SCAN_SCALE_EXCEEDED: ${results.length} > ${scaleLimit}`);
9
+ }
10
+ return results;
11
+ }
12
+ _scanRecursive(dir, results, limit) {
13
+ if (results.length >= limit)
14
+ return;
15
+ let entries;
16
+ try {
17
+ entries = readdirSync(dir);
18
+ }
19
+ catch {
20
+ throw new Error(`SCAN_SOURCE_UNREACHABLE: ${dir}`);
21
+ }
22
+ for (const entry of entries) {
23
+ if (results.length >= limit)
24
+ return;
25
+ const fullPath = join(dir, entry);
26
+ let stat;
27
+ try {
28
+ stat = statSync(fullPath);
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ if (stat.isDirectory()) {
34
+ this._scanRecursive(fullPath, results, limit);
35
+ }
36
+ else {
37
+ results.push({
38
+ id: `${results.length + 1}`,
39
+ name: basename(fullPath),
40
+ path: fullPath,
41
+ size: stat.size,
42
+ type: extname(fullPath).slice(1) || 'unknown',
43
+ createdAt: stat.birthtime.toISOString(),
44
+ modifiedAt: stat.mtime.toISOString(),
45
+ });
46
+ }
47
+ }
48
+ }
49
+ }
50
+ export class DatabaseScanAdapter {
51
+ scan(config, _scaleLimit) {
52
+ let dbConfig;
53
+ try {
54
+ dbConfig = JSON.parse(config);
55
+ }
56
+ catch {
57
+ throw new Error('INVALID_JSON: database config is not valid JSON');
58
+ }
59
+ const required = ['host', 'port', 'database'];
60
+ for (const field of required) {
61
+ if (!(field in dbConfig)) {
62
+ throw new Error(`INVALID_CONFIG: missing field ${field}`);
63
+ }
64
+ }
65
+ throw new Error('DATABASE_SCAN_NOT_IMPLEMENTED: use directory scan or provide database driver');
66
+ }
67
+ }
68
+ export class ScanSourceAdapterFactory {
69
+ static create(sourceType) {
70
+ if (sourceType === 'directory')
71
+ return new DirectoryScanAdapter();
72
+ if (sourceType === 'database')
73
+ return new DatabaseScanAdapter();
74
+ throw new Error(`INVALID_SOURCE_TYPE: ${sourceType}`);
75
+ }
76
+ }
77
+ //# sourceMappingURL=scanSourceAdapter.js.map
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@liuhange/dsh-data-asset-inventory-scan",
3
+ "description": "Data asset inventory scan plugin: deep metadata scan + triple condition screening + ownership clue annotation",
4
+ "version": "3.0.0",
5
+ "publishConfig": { "access": "public" },
6
+ "repository": { "type": "git", "url": "git+https://github.com/liuhange789/data-asset-inspector.git", "directory": "packages/data-asset/data-asset-inventory-scan" },
7
+ "type": "module",
8
+ "main": "lib/index.js",
9
+ "types": "lib/types/index.d.ts",
10
+ "exports": {
11
+ ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
12
+ "./src/*": "./src/*",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": ["lib/**/*.js", "lib/types/**/*.d.ts"],
16
+ "license": "MIT",
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run",
20
+ "build": "tsc"
21
+ },
22
+ "peerDependencies": {
23
+ "@deepseek-ai/cordis": "^4.0.0",
24
+ "@deepseek-ai/dsh-tools": "0.0.1-rc.1",
25
+ "@liuhange/dsh-data-asset-shared": "^3.0.0"
26
+ },
27
+ "dsh": { "bundle": { "patch": true } }
28
+ }