@liuhange/dsh-data-asset-valuation 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.
- package/lib/__tests__/valuation.test.js +66 -0
- package/lib/costBasedValuation.js +34 -0
- package/lib/incomeBasedValuation.js +25 -0
- package/lib/index.js +87 -0
- package/lib/invariant.js +48 -0
- package/lib/types.js +2 -0
- package/lib/valuationReportGenerator.js +38 -0
- package/package.json +28 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { calculateCostBasedValue } from '../costBasedValuation.js';
|
|
3
|
+
import { calculateIncomeBasedValue } from '../incomeBasedValuation.js';
|
|
4
|
+
describe('costBasedValuation', () => {
|
|
5
|
+
const config = {
|
|
6
|
+
costCategories: ['acquisition', 'processing', 'storage', 'maintenance', 'labor', 'infrastructure'],
|
|
7
|
+
depreciationYears: 5,
|
|
8
|
+
depreciationMethod: 'straight-line',
|
|
9
|
+
};
|
|
10
|
+
it('正确汇总各类成本', () => {
|
|
11
|
+
const result = calculateCostBasedValue([
|
|
12
|
+
{ category: 'acquisition', amount: 100000, year: 2024 },
|
|
13
|
+
{ category: 'processing', amount: 50000, year: 2024 },
|
|
14
|
+
{ category: 'storage', amount: 20000, year: 2024 },
|
|
15
|
+
], config);
|
|
16
|
+
expect(result.totalCost).toBe(170000);
|
|
17
|
+
expect(result.breakdown.acquisition).toBe(100000);
|
|
18
|
+
expect(result.breakdown.processing).toBe(50000);
|
|
19
|
+
expect(result.breakdown.storage).toBe(20000);
|
|
20
|
+
});
|
|
21
|
+
it('直线折旧法正确计算折旧后价值', () => {
|
|
22
|
+
const result = calculateCostBasedValue([
|
|
23
|
+
{ category: 'acquisition', amount: 100000, year: 2024 },
|
|
24
|
+
], config);
|
|
25
|
+
expect(result.annualDepreciation).toBe(20000);
|
|
26
|
+
expect(result.depreciatedValue).toBe(80000);
|
|
27
|
+
});
|
|
28
|
+
it('忽略不在costCategories中的类别', () => {
|
|
29
|
+
const result = calculateCostBasedValue([
|
|
30
|
+
{ category: 'unknown', amount: 999999, year: 2024 },
|
|
31
|
+
], config);
|
|
32
|
+
expect(result.totalCost).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('incomeBasedValuation', () => {
|
|
36
|
+
const config = {
|
|
37
|
+
defaultDiscountRate: 0.08,
|
|
38
|
+
defaultScenarioYears: 3,
|
|
39
|
+
scenarioTemplates: ['direct_sale', 'subscription'],
|
|
40
|
+
};
|
|
41
|
+
it('DCF正确计算现值', () => {
|
|
42
|
+
const result = calculateIncomeBasedValue([
|
|
43
|
+
{ type: 'subscription', annualRevenue: 100000, annualCost: 20000, years: 3 },
|
|
44
|
+
], config);
|
|
45
|
+
expect(result.scenarios).toHaveLength(1);
|
|
46
|
+
expect(result.scenarios[0].netCashFlow).toBe(80000);
|
|
47
|
+
expect(result.presentValue).toBeGreaterThan(0);
|
|
48
|
+
expect(result.discountRate).toBe(0.08);
|
|
49
|
+
});
|
|
50
|
+
it('多场景现值累加', () => {
|
|
51
|
+
const result = calculateIncomeBasedValue([
|
|
52
|
+
{ type: 'direct_sale', annualRevenue: 50000, annualCost: 10000, years: 2 },
|
|
53
|
+
{ type: 'subscription', annualRevenue: 30000, annualCost: 5000, years: 3 },
|
|
54
|
+
], config);
|
|
55
|
+
expect(result.scenarios).toHaveLength(2);
|
|
56
|
+
expect(result.presentValue).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
it('自定义折现率生效', () => {
|
|
59
|
+
const result = calculateIncomeBasedValue([
|
|
60
|
+
{ type: 'direct_sale', annualRevenue: 100000, annualCost: 0, years: 1 },
|
|
61
|
+
], config, 0.1);
|
|
62
|
+
expect(result.discountRate).toBe(0.1);
|
|
63
|
+
expect(result.scenarios[0].presentValue).toBeCloseTo(90909.09, -1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=valuation.test.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function calculateCostBasedValue(costItems, config) {
|
|
2
|
+
const breakdown = {};
|
|
3
|
+
for (const category of config.costCategories) {
|
|
4
|
+
breakdown[category] = 0;
|
|
5
|
+
}
|
|
6
|
+
let totalCost = 0;
|
|
7
|
+
for (const item of costItems) {
|
|
8
|
+
if (!config.costCategories.includes(item.category)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
breakdown[item.category] = (breakdown[item.category] ?? 0) + item.amount;
|
|
12
|
+
totalCost += item.amount;
|
|
13
|
+
}
|
|
14
|
+
const depreciationYears = config.depreciationYears;
|
|
15
|
+
let depreciatedValue;
|
|
16
|
+
let annualDepreciation;
|
|
17
|
+
if (config.depreciationMethod === 'straight-line') {
|
|
18
|
+
annualDepreciation = totalCost / depreciationYears;
|
|
19
|
+
depreciatedValue = totalCost - annualDepreciation;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
annualDepreciation = (totalCost * 2) / depreciationYears;
|
|
23
|
+
depreciatedValue = totalCost - annualDepreciation;
|
|
24
|
+
}
|
|
25
|
+
if (depreciatedValue < 0)
|
|
26
|
+
depreciatedValue = 0;
|
|
27
|
+
return {
|
|
28
|
+
totalCost: Math.round(totalCost * 100) / 100,
|
|
29
|
+
depreciatedValue: Math.round(depreciatedValue * 100) / 100,
|
|
30
|
+
breakdown,
|
|
31
|
+
annualDepreciation: Math.round(annualDepreciation * 100) / 100,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=costBasedValuation.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function calculateIncomeBasedValue(scenarios, config, customDiscountRate) {
|
|
2
|
+
const discountRate = customDiscountRate ?? config.defaultDiscountRate;
|
|
3
|
+
const scenarioResults = [];
|
|
4
|
+
let totalPresentValue = 0;
|
|
5
|
+
for (const scenario of scenarios) {
|
|
6
|
+
const years = scenario.years > 0 ? scenario.years : config.defaultScenarioYears;
|
|
7
|
+
const netCashFlow = scenario.annualRevenue - scenario.annualCost;
|
|
8
|
+
let pv = 0;
|
|
9
|
+
for (let year = 1; year <= years; year++) {
|
|
10
|
+
pv += netCashFlow / Math.pow(1 + discountRate, year);
|
|
11
|
+
}
|
|
12
|
+
scenarioResults.push({
|
|
13
|
+
type: scenario.type,
|
|
14
|
+
netCashFlow: Math.round(netCashFlow * 100) / 100,
|
|
15
|
+
presentValue: Math.round(pv * 100) / 100,
|
|
16
|
+
});
|
|
17
|
+
totalPresentValue += pv;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
presentValue: Math.round(totalPresentValue * 100) / 100,
|
|
21
|
+
scenarios: scenarioResults,
|
|
22
|
+
discountRate,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=incomeBasedValuation.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { calculateCostBasedValue } from './costBasedValuation.js';
|
|
2
|
+
import { calculateIncomeBasedValue } from './incomeBasedValuation.js';
|
|
3
|
+
import { generateValuationReport } from './valuationReportGenerator.js';
|
|
4
|
+
import { validateValuationArgs } from './invariant.js';
|
|
5
|
+
export const name = '@liuhange/dsh-data-asset-valuation';
|
|
6
|
+
export const inject = ['tools'];
|
|
7
|
+
export function apply(ctx) {
|
|
8
|
+
ctx.tools.register({
|
|
9
|
+
name: 'value_data_asset',
|
|
10
|
+
description: '数据资产估值:成本法(重置成本-折旧)+ 收益法(DCF折现)双模型,输出建议估值与定价区间',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
assetName: { type: 'string', description: '数据资产名称' },
|
|
15
|
+
costItems: {
|
|
16
|
+
type: 'array',
|
|
17
|
+
description: '成本明细列表',
|
|
18
|
+
items: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
category: { type: 'string', description: '成本类别(acquisition/processing/storage/maintenance/labor/infrastructure)' },
|
|
22
|
+
amount: { type: 'number', description: '金额(元)' },
|
|
23
|
+
year: { type: 'number', description: '年份' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
incomeScenarios: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
description: '收益场景列表',
|
|
30
|
+
items: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
type: { type: 'string', description: '场景类型(direct_sale/subscription/api_call/data_sharing/analytics_service)' },
|
|
34
|
+
annualRevenue: { type: 'number', description: '年收益(元)' },
|
|
35
|
+
annualCost: { type: 'number', description: '年成本(元)' },
|
|
36
|
+
years: { type: 'number', description: '持续年限' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
discountRate: { type: 'number', description: '自定义折现率(可选,默认从配置读取)' },
|
|
41
|
+
qualityScore: { type: 'number', description: '质量评分(可选,影响价值等级判定)' },
|
|
42
|
+
},
|
|
43
|
+
required: ['assetName', 'costItems', 'incomeScenarios'],
|
|
44
|
+
},
|
|
45
|
+
async execute(args) {
|
|
46
|
+
try {
|
|
47
|
+
const validated = validateValuationArgs(args);
|
|
48
|
+
const { readFileSync } = await import('node:fs');
|
|
49
|
+
let rulesConfig;
|
|
50
|
+
try {
|
|
51
|
+
rulesConfig = JSON.parse(readFileSync(process.env.BUSINESS_RULES_PATH || 'config/business-rules.json', 'utf-8'));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return JSON.stringify({ error: 'VALUATION_RULES_MISSING', message: '无法加载business-rules.json' });
|
|
55
|
+
}
|
|
56
|
+
const valuationConfig = rulesConfig.valuation;
|
|
57
|
+
if (!valuationConfig) {
|
|
58
|
+
return JSON.stringify({ error: 'VALUATION_RULES_MISSING', message: 'valuation配置段缺失' });
|
|
59
|
+
}
|
|
60
|
+
let policyDocuments;
|
|
61
|
+
try {
|
|
62
|
+
const policyConfig = JSON.parse(readFileSync(process.env.POLICY_REFS_PATH || 'config/policy-references.json', 'utf-8'));
|
|
63
|
+
const stage = policyConfig.policyReferences?.find((s) => s.stage === 'VALUATION');
|
|
64
|
+
policyDocuments = stage?.documents ?? [];
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
policyDocuments = [];
|
|
68
|
+
}
|
|
69
|
+
if (!policyDocuments || policyDocuments.length === 0) {
|
|
70
|
+
return JSON.stringify({ error: 'POLICY_REFERENCES_EMPTY', message: 'VALUATION阶段政策依据缺失' });
|
|
71
|
+
}
|
|
72
|
+
const costBased = calculateCostBasedValue(validated.costItems, valuationConfig.costBased);
|
|
73
|
+
const incomeBased = calculateIncomeBasedValue(validated.incomeScenarios, valuationConfig.incomeBased, validated.discountRate);
|
|
74
|
+
const report = generateValuationReport(validated.assetName, costBased, incomeBased, valuationConfig.pricingReference, validated.qualityScore, policyDocuments);
|
|
75
|
+
return JSON.stringify(report, null, 2);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
const err = e;
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
error: err.message.split(':')[0] ?? 'UNKNOWN_ERROR',
|
|
81
|
+
message: err.message,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function invariant(condition, message) {
|
|
2
|
+
if (!condition) {
|
|
3
|
+
throw new Error(`INVARIANT_VIOLATION: ${message}`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export function validateValuationArgs(args) {
|
|
7
|
+
if (!args || typeof args !== 'object') {
|
|
8
|
+
throw new Error('INVALID_ARGS: args must be an object');
|
|
9
|
+
}
|
|
10
|
+
if (!args.assetName || typeof args.assetName !== 'string') {
|
|
11
|
+
throw new Error('INVALID_ARGS: assetName is required and must be a string');
|
|
12
|
+
}
|
|
13
|
+
if (!Array.isArray(args.costItems)) {
|
|
14
|
+
throw new Error('INVALID_ARGS: costItems is required and must be an array');
|
|
15
|
+
}
|
|
16
|
+
for (const item of args.costItems) {
|
|
17
|
+
if (typeof item.category !== 'string' || typeof item.amount !== 'number' || typeof item.year !== 'number') {
|
|
18
|
+
throw new Error('INVALID_ARGS: each costItem must have category(string), amount(number), year(number)');
|
|
19
|
+
}
|
|
20
|
+
if (item.amount < 0) {
|
|
21
|
+
throw new Error(`INVALID_ARGS: cost amount cannot be negative for category "${item.category}"`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(args.incomeScenarios)) {
|
|
25
|
+
throw new Error('INVALID_ARGS: incomeScenarios is required and must be an array');
|
|
26
|
+
}
|
|
27
|
+
for (const scenario of args.incomeScenarios) {
|
|
28
|
+
if (typeof scenario.type !== 'string' || typeof scenario.annualRevenue !== 'number' || typeof scenario.annualCost !== 'number') {
|
|
29
|
+
throw new Error('INVALID_ARGS: each incomeScenario must have type(string), annualRevenue(number), annualCost(number)');
|
|
30
|
+
}
|
|
31
|
+
if (scenario.annualRevenue < 0 || scenario.annualCost < 0) {
|
|
32
|
+
throw new Error(`INVALID_ARGS: revenue/cost cannot be negative for scenario "${scenario.type}"`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const result = {
|
|
36
|
+
assetName: args.assetName,
|
|
37
|
+
costItems: args.costItems,
|
|
38
|
+
incomeScenarios: args.incomeScenarios,
|
|
39
|
+
};
|
|
40
|
+
if (typeof args.discountRate === 'number') {
|
|
41
|
+
result.discountRate = args.discountRate;
|
|
42
|
+
}
|
|
43
|
+
if (typeof args.qualityScore === 'number') {
|
|
44
|
+
result.qualityScore = args.qualityScore;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=invariant.js.map
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function generateValuationReport(assetName, costBased, incomeBased, pricingConfig, qualityScore, policyDocuments) {
|
|
2
|
+
const recommendedValue = Math.round((costBased.depreciatedValue * 0.4 + incomeBased.presentValue * 0.6) * 100) / 100;
|
|
3
|
+
const thresholds = pricingConfig.valueLevelThresholds;
|
|
4
|
+
let valueLevel;
|
|
5
|
+
const effectiveScore = qualityScore ?? 60;
|
|
6
|
+
if (effectiveScore >= thresholds.high) {
|
|
7
|
+
valueLevel = 'high';
|
|
8
|
+
}
|
|
9
|
+
else if (effectiveScore >= thresholds.medium) {
|
|
10
|
+
valueLevel = 'medium';
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
valueLevel = 'low';
|
|
14
|
+
}
|
|
15
|
+
const pricingSuggestion = buildPricingSuggestion(recommendedValue, valueLevel, costBased, incomeBased);
|
|
16
|
+
return {
|
|
17
|
+
assetName,
|
|
18
|
+
costBased,
|
|
19
|
+
incomeBased,
|
|
20
|
+
recommendedValue,
|
|
21
|
+
pricingSuggestion,
|
|
22
|
+
valueLevel,
|
|
23
|
+
policyReferences: policyDocuments,
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function buildPricingSuggestion(recommended, level, costBased, incomeBased) {
|
|
28
|
+
const levelLabel = level === 'high' ? '高价值' : level === 'medium' ? '中价值' : '低价值';
|
|
29
|
+
const parts = [];
|
|
30
|
+
parts.push(`建议估值: ${recommended} 元(${levelLabel})`);
|
|
31
|
+
parts.push(`成本法估值: ${costBased.depreciatedValue} 元(折旧后)`);
|
|
32
|
+
parts.push(`收益法估值: ${incomeBased.presentValue} 元(折现率${(incomeBased.discountRate * 100).toFixed(1)}%)`);
|
|
33
|
+
if (recommended > 0) {
|
|
34
|
+
parts.push(`定价区间: ${Math.round(recommended * 0.8)} ~ ${Math.round(recommended * 1.2)} 元`);
|
|
35
|
+
}
|
|
36
|
+
return parts.join(';');
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=valuationReportGenerator.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liuhange/dsh-data-asset-valuation",
|
|
3
|
+
"description": "Data asset valuation plugin: cost-based (replacement cost - depreciation) + income-based (DCF) dual model",
|
|
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-valuation" },
|
|
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
|
+
}
|