以下情境为教学假设,人物与数字不代表经核实的真实案例。
争议发生后,独立开发者最常问的问题是「我能不能告他」。更前面的问题其实是「我能不能证明」。合同、变更单、验收记录、沟通记录和日志,决定了你能主张什么金额、走哪条程序、以及对方有没有空间否认。证据链不闭合时,即使实体上占理,期望净得也会被回收概率吃掉。止损则是在证据已经足够、但现金还在持续流出时,决定什么时候停下来的动作。
证据链的五段:从「有合同」到「能证明」
把证据按「证明什么」拆成五段,比按文件类型堆放更有用。每一段都有权重,权重是教学假设,不是证据法上的证明力评级。
| 证据 | 权重 | 证明什么 | 缺失后的后果 |
|---|---|---|---|
| 签署合同 | 3,000 | 双方、范围、价格、账期 | 连主张的基数都不确定 |
| 变更单 | 2,000 | 范围如何变化、谁批准的 | 新增工作可能被当成免费赠送 |
| 验收记录 | 2,000 | 什么算交付、何时交付 | 延期和瑕疵的起算点说不清 |
| 沟通记录 | 1,500 | 对方知道什么、承诺过什么 | 口头承诺无法还原 |
| 系统日志 | 1,500 | 实际使用、上线时间、故障 | 交付与影响缺客观时间线 |
五段加起来是 10,000 个基点。权重不是法律上的概率,而是一个用于自查的完整度分数:缺一段,就知道哪一块主张会变弱。
签署合同是基数。没有合同,不是完全不能主张,而是金额、范围、账期都要靠聊天记录和发票去拼;每拼一项,回收概率就下降一点。变更单是独立开发者最容易漏的一段:客户在微信里说「再加一个小功能」,你做了,但没有书面确认;到了争议阶段,对方可以说这属于原范围,也可以说从来没要求过。验收记录决定「什么时候算交付」,直接影响迟延违约金从哪天起算。沟通记录和日志则是把「对方知道」和「实际发生」变成时间线。
这五段不是并列的。合同和变更单决定「能不能主张」,验收记录和日志决定「金额算不算得清」,沟通记录决定「对方有没有机会否认」。缺前两段,后面的证据再完整,也只是在证明一个没有基数的故事。
证据缺口改变的是回收概率
证据分数不直接等于胜诉率,但它会改变你在程序里的回收概率。用一个教学映射:完整文件按 70% 的仲裁回收概率算,缺变更单和日志的 6,500 分文件按 55% 算。
Nord 的 11,264 应收,两种证据状态下的仲裁期望净得:
| 证据状态 | 分数 | 回收概率 | 期望回收 | 总成本 | 期望净得 |
|---|---|---|---|---|---|
| 完整 | 10,000 | 70% | 7,884.80 | 9,520 | −1,635.20 |
| 缺变更单和日志 | 6,500 | 55% | 6,195.20 | 9,520 | −3,324.80 |
同一笔钱、同一个程序,证据缺口让期望净得下降 1,689.60。这个下降不是「少赢一点」,而是把一个本来已经为负的仲裁变得更负。如果争议金额是 20,000,完整证据下仲裁净得 4,480,6,500 分证据下只有 20,000 × 55% − 9,520 = 1,480;证据缺口吃掉了 3,000,相当于把一笔可做的仲裁变成勉强可做。
更常见的情形是证据缺口改变程序选择。11,264 的争议在完整证据下,调解净得 5,811.60;如果证据只有 6,500 分,调解的回收概率也要下调,假设从 65% 降到 50%,净得变成 11,264 × 50% − 1,510 = 4,122,仍然为正,但比仲裁的 −3,324.80 好得多。结论没有反转,差距拉大了:证据越弱,越应该走便宜的程序,而不是指望一个更正式的程序把事实查清。
这也是「先补证据,再选程序」的原因。补一段变更单可能只花 1 小时,但它把回收概率从 55% 拉回 70%,在 20,000 的争议上是 3,000 的期望差。补证据的投入产出比,通常高于换一个更贵的程序。
止损的三个动作与现金影响
止损不是「不干了」这一个动作,而是三个可以分别计价的动作:停止投入、发出书面异议、保全证据。三者的时点和现金影响不同。
| 动作 | 建议时点 | 直接现金影响 | 证据影响 |
|---|---|---|---|
| 停止投入 | 逾期超过约定账期且对方无付款计划 | 每月少发生 1,530 的交付成本 | 保留「我没有继续扩大损失」的记录 |
| 书面异议 | 发现违约或范围争议的当周 | 1–2 小时,按 85 美元计 85–170 | 固定主张,避免被当成默认接受 |
| 保全证据 | 停止投入的同一周 | 约 4 小时,340 | 导出日志、邮件、验收记录,防止丢失 |
停止投入的直接现金影响,是把每月 1,530 的新增交付成本(18 小时 × 85)停下来。但停工会放弃这部分收入,所以它不是无条件正确;下一节的 90 天比较会给出条件。
书面异议的作用不是马上收钱,而是把「你什么时候知道、主张什么」固定下来。商业上,继续无异议地服务,容易被对方用来主张你已经接受了变更或放弃了主张;书面异议的成本只有 85–170,但它保护的是后面几千美元的立场。这里不展开函件写法,也不构成法律意见;财务上只需记住,这是一个低成本、高杠杆的动作。
保全证据是把沟通记录、验收记录和日志导出到你可控的位置。成本约 340,收益是防止账号被停、聊天记录过期或对方删除共享文件。证据保全做得早,后面选程序时才有得选;做得晚,连调解的 65% 都保不住。
LedgerLite 的对照在证据来源:订阅记录、发票和用量日志由系统生成,完整度高;但点击同意的条款没有谈判式变更单,范围争议少。单客户年费 924,保全证据的 340 占 37%,止损动作要先看金额,再决定投入多少小时。
90 天现金差:继续服务 vs 停工
Nord 的应收是 11,264,逾期率 8%,逾期部分约 901。假设逾期客户每月仍占用 18 小时(约 110 小时咨询产能的 16%,与前 3 家客户占 48% 的口径一致),单价 85 美元/小时,考察 90 天。
两种选择的假设如下,全部为教学假设:
| 参数 | 继续服务 | 停工并转投其他客户 |
|---|---|---|
| 90 天新增账单 | 18 × 3 × 85 = 4,590 | 0 |
| 新增账单回收概率 | 40% | 不适用 |
| 逾期余额回收概率 | 60% | 50% |
| 释放工时的可计费率 | 不适用 | 70% |
| 转投工作的回收概率 | 不适用 | 95% |
| 交付成本率 | 15.6% | 15.6% |
交付成本率来自 Nord 的直接成本 24,000 ÷ 年收入 154,320 = 15.6%。继续服务时,90 天新增账单 4,590,交付成本 4,590 × 15.6% = 713.75;期望回收包括逾期余额 901 × 60% = 540.60,加上新增账单 4,590 × 40% = 1,836。净现金:
继续服务 = 540.60 + 1,836 − 713.75 = 1,662.85
停工后,54 小时被释放,其中 70% 可计费,即 37.8 小时,按 85 美元/小时是 3,213;按 95% 回收是 3,052.35,交付成本 3,213 × 15.6% = 499.62。逾期余额按 50% 回收是 450.50。净现金:
停工 = 450.50 + 3,052.35 − 499.62 = 3,003.23
90 天现金差是 3,003.23 − 1,662.85 = 1,340.38,停工多留下约 1,340。这个结论依赖两个关键假设:释放的工时能按 70% 计费,且转投工作的回收率是 95%。如果这两个假设不成立,结论会变。
| 情景 | 停工净现金 | 继续净现金 | 差额 |
|---|---|---|---|
| 基准(可计费 70%,转投回收 95%) | 3,003.23 | 1,662.85 | +1,340.38 |
| 可计费率降到 50% | 2,273.88 | 1,662.85 | +611.03 |
| 继续服务的回收率升到 50% | 3,003.23 | 2,121.85 | +881.38 |
| 两个不利同时发生 | 2,273.88 | 2,121.85 | +152.03 |
| 可计费率降到 30% | 1,544.53 | 1,662.85 | −118.32 |
可计费率的平衡点大约在 33%:低于它,停工反而不如继续服务。这就是止损的边界——不是「对方逾期就立刻停」,而是「释放出来的时间有没有更高价值的去处」。如果团队没有其他客户可接,停工的现金优势会消失,剩下的只是证据和关系上的考虑。
还要注意,继续服务的期望净现金为正(1,662.85),并不意味着应该继续。它的正数来自 40% 的新增账单回收概率;如果这个概率降到 25%,继续服务净现金变成 540.60 + 1,147.50 − 713.75 = 974.35,停工的差额扩大到 2,028.88。逾期客户的付款行为本身就是回收概率的最好样本:已经逾期 60 天以上的客户,再用 40% 去假设新增账单,往往偏乐观。
写成代码:evidence.ts
下面的 evidence.ts 有两个函数。assessEvidence 把五段证据按权重加总,返回分数、强度档位和缺失清单;compareStopLoss 把继续服务和停工两条路径的 90 天净现金算出来,差额为正表示停工更优。
// file: src/contract/evidence.ts
// tier: L1
// verify: node --test test/contract/evidence.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type EvidenceItem = {
name: string;
weightBps: Bps;
present: boolean;
};
export type EvidenceAssessment = {
scoreBps: Bps;
strength: "weak" | "moderate" | "strong";
missing: string[];
};
export type StopLossInput = {
overdueCents: Cents;
monthlyHours: number;
hourlyRateCents: Cents;
horizonMonths: number;
deliveryCostBps: Bps;
existingContinueBps: Bps;
existingStopBps: Bps;
continueCollectionBps: Bps;
redeployBillableBps: Bps;
redeployCollectionBps: Bps;
};
export type StopLossResult = {
continueNetCents: Cents;
stopNetCents: Cents;
differenceCents: Cents;
newBillingCents: Cents;
redeployedBillingCents: Cents;
};
function checkAmount(value: Cents, label: string): Result<true> {
if (!Number.isInteger(value) || value < 0) {
return fail(`${label} must be a non-negative integer amount in cents`);
}
return ok(true);
}
function checkRate(value: Bps, label: string): Result<true> {
if (!Number.isInteger(value) || value < 0 || value > 10000) {
return fail(`${label} must be between 0 and 10000 bps`);
}
return ok(true);
}
/**
* Evidence is scored by the share of the claim each item supports. The
* weights are teaching assumptions; what matters is that a missing change
* order or acceptance record is visible as a hole, not discovered after the
* demand letter is sent. A strong file does not guarantee recovery, but a
* weak file changes which procedures are worth paying for.
*/
export function assessEvidence(items: EvidenceItem[]): Result<EvidenceAssessment> {
if (items.length === 0) {
return fail("at least one evidence item is required");
}
let scoreBps = 0;
const missing: string[] = [];
for (const item of items) {
if (item.name.trim() === "") {
return fail("evidence name cannot be empty");
}
const weight = checkRate(item.weightBps, `${item.name} weight`);
if (!weight.ok) {
return weight;
}
if (item.present) {
scoreBps += item.weightBps;
} else {
missing.push(item.name);
}
}
scoreBps = Math.min(10000, scoreBps);
const strength =
scoreBps >= 8000 ? "strong" : scoreBps >= 5000 ? "moderate" : "weak";
return ok({ scoreBps, strength, missing });
}
/**
* Continuing to serve a payer who is already overdue adds new billing and new
* delivery cost. Stopping frees the hours for other work but lowers the
* expected recovery on the old balance. Comparing the two over the same
* horizon turns "when do we stop?" into a cash number.
*/
export function compareStopLoss(input: StopLossInput): Result<StopLossResult> {
const overdue = checkAmount(input.overdueCents, "overdue balance");
if (!overdue.ok) {
return overdue;
}
if (!Number.isFinite(input.monthlyHours) || input.monthlyHours <= 0) {
return fail("monthly hours must be greater than zero");
}
if (!Number.isInteger(input.hourlyRateCents) || input.hourlyRateCents <= 0) {
return fail("hourly rate must be a positive integer amount in cents");
}
if (!Number.isFinite(input.horizonMonths) || input.horizonMonths <= 0) {
return fail("horizon months must be greater than zero");
}
const rates: Array<[number, string]> = [
[input.deliveryCostBps, "delivery cost rate"],
[input.existingContinueBps, "existing continue rate"],
[input.existingStopBps, "existing stop rate"],
[input.continueCollectionBps, "continue collection rate"],
[input.redeployBillableBps, "redeploy billable rate"],
[input.redeployCollectionBps, "redeploy collection rate"],
];
for (const [value, label] of rates) {
const checked = checkRate(value, label);
if (!checked.ok) {
return checked;
}
}
const newBillingCents = Math.round(
input.monthlyHours * input.horizonMonths * input.hourlyRateCents,
);
const continueDeliveryCents = applyRate(
newBillingCents,
input.deliveryCostBps,
);
const continueNetCents =
applyRate(input.overdueCents, input.existingContinueBps) +
applyRate(newBillingCents, input.continueCollectionBps) -
continueDeliveryCents;
const freedHours = input.monthlyHours * input.horizonMonths;
const redeployedHours =
(freedHours * input.redeployBillableBps) / 10000;
const redeployedBillingCents = Math.round(
redeployedHours * input.hourlyRateCents,
);
const redeployedDeliveryCents = applyRate(
redeployedBillingCents,
input.deliveryCostBps,
);
const stopNetCents =
applyRate(input.overdueCents, input.existingStopBps) +
applyRate(redeployedBillingCents, input.redeployCollectionBps) -
redeployedDeliveryCents;
return ok({
continueNetCents,
stopNetCents,
differenceCents: stopNetCents - continueNetCents,
newBillingCents,
redeployedBillingCents,
});
}
把 Nord 的数字换成美分代进去:完整证据五项全在,返回 10,000 分、strong、缺失清单为空;去掉变更单和日志,返回 6,500 分、moderate、缺失两项。止损比较的输入是逾期 90,100 美分、每月 18 小时、时价 8,500 美分、3 个月、交付成本率 1,555 基点、继续服务时逾期回收 6,000 基点、停工后逾期回收 5,000 基点、新增账单回收 4,000 基点、转投可计费 7,000 基点、转投回收 9,500 基点;返回继续净现金 166,285 美分、停工净现金 300,323 美分、差额 134,038 美分。
代码只算完整度和现金差,不判断证据能不能被采纳,也不处理保全措施的法律程序。分数和权重是教学设定;换成自己的证据清单时,只要保持权重总和为 10,000,强度档位就仍然可比。
小结
- 证据链五段:合同、变更单、验收记录、沟通记录、日志;权重合计 10,000 分。
- 合同和变更单决定能不能主张,验收和日志决定金额算不算得清,沟通记录决定对方能否否认。
- 完整证据按 70% 回收,缺变更单和日志的 6,500 分文件按 55%;11,264 的仲裁净得从 −1,635.20 降到 −3,324.80。
- 证据越弱,越应走便宜的程序;补一段变更单的投入产出比通常高于换程序。
- 止损有三个动作:停止投入、书面异议、保全证据;成本分别是每月 1,530 的交付成本、85–170、340。
- 90 天比较:继续服务净现金 1,662.85,停工 3,003.23,差额 1,340.38。
- 差额依赖释放工时的可计费率;可计费率低于约 33% 时,停工不再占优。
- 继续服务的期望净现金为正,不代表应该继续;逾期客户的付款行为本身就是回收概率的样本。
阅读导航:上一节:5.2 争议解决:管辖、仲裁与调解 · 下一节:6.1 E&O、网络安全险与雇主责任 。
常见错误与边界
- 以为「有理就能赢」。 能不能主张取决于能不能证明;证据缺口会先吃掉回收概率,再谈实体对错。
- 只留合同,不留变更单。 口头新增的工作在争议时容易被当成原范围或从未发生;变更单权重 2,000 不是装饰。
- 验收记录只写「通过」。 没有日期、版本和验收人,迟延和瑕疵的起算点仍然说不清。
- 用聊天记录代替书面确认。 沟通记录能证明「对方知道」,但不一定能证明「对方批准了范围变更」。
- 日志只留在对方系统里。 账号被停或数据过期后,客观时间线就没了;保全证据要导出到可控位置。
- 把证据分数当胜诉率。 10,000 分只表示清单完整,不表示法院或仲裁庭一定采信。
- 逾期后继续无异议地服务。 商业上容易被对方用来主张你已接受变更;书面异议的成本只有 85–170。
- 把停工作为情绪反应。 停工的现金优势来自释放工时的再利用;没有其他客户可接时,停工可能更差。
- 忽略交付成本率。 继续服务不是「多收钱」,而是「多发生 15.6% 的交付成本」;只算收入会高估继续服务的价值。
- 用一次比较代替持续复盘。 可计费率、回收概率和逾期余额都会变;90 天窗结束后要重算,而不是沿用旧结论。
本节的边界是「证据完整度与止损现金比较」:只覆盖五类证据的权重、证据分数对回收概率的影响,以及继续服务与停工两条路径的 90 天净现金,不展开证据采纳规则、保全措施的法律程序、诉讼时效和跨境取证。所有权重、概率和成本率均为教学假设;具体证据策略因司法辖区而异,本节不构成法律意见。违约形态的金额量化见 5.1 违约的四种形态 ,程序成本比较见 5.2 争议解决:管辖、仲裁与调解 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。