《独立开发者的商业指南·合同、应收与风险》5.3 证据链与止损

能不能主张,不取决于你有多生气,而取决于证据链是否完整闭合。本节把合同、变更单、验收记录、沟通记录和日志折成分数,说明证据缺口如何改变回收概率,并用 Nord 的 901 美元逾期应收,算出继续服务与停工两种选择在 90 天里的实际现金差。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

争议发生后,独立开发者最常问的问题是「我能不能告他」。更前面的问题其实是「我能不能证明」。合同、变更单、验收记录、沟通记录和日志,决定了你能主张什么金额、走哪条程序、以及对方有没有空间否认。证据链不闭合时,即使实体上占理,期望净得也会被回收概率吃掉。止损则是在证据已经足够、但现金还在持续流出时,决定什么时候停下来的动作。

证据链的五段:从「有合同」到「能证明」

把证据按「证明什么」拆成五段,比按文件类型堆放更有用。每一段都有权重,权重是教学假设,不是证据法上的证明力评级。

证据权重证明什么缺失后的后果
签署合同3,000双方、范围、价格、账期连主张的基数都不确定
变更单2,000范围如何变化、谁批准的新增工作可能被当成免费赠送
验收记录2,000什么算交付、何时交付延期和瑕疵的起算点说不清
沟通记录1,500对方知道什么、承诺过什么口头承诺无法还原
系统日志1,500实际使用、上线时间、故障交付与影响缺客观时间线

五段加起来是 10,000 个基点。权重不是法律上的概率,而是一个用于自查的完整度分数:缺一段,就知道哪一块主张会变弱。

签署合同是基数。没有合同,不是完全不能主张,而是金额、范围、账期都要靠聊天记录和发票去拼;每拼一项,回收概率就下降一点。变更单是独立开发者最容易漏的一段:客户在微信里说「再加一个小功能」,你做了,但没有书面确认;到了争议阶段,对方可以说这属于原范围,也可以说从来没要求过。验收记录决定「什么时候算交付」,直接影响迟延违约金从哪天起算。沟通记录和日志则是把「对方知道」和「实际发生」变成时间线。

这五段不是并列的。合同和变更单决定「能不能主张」,验收记录和日志决定「金额算不算得清」,沟通记录决定「对方有没有机会否认」。缺前两段,后面的证据再完整,也只是在证明一个没有基数的故事。

证据缺口改变的是回收概率

证据分数不直接等于胜诉率,但它会改变你在程序里的回收概率。用一个教学映射:完整文件按 70% 的仲裁回收概率算,缺变更单和日志的 6,500 分文件按 55% 算。

Nord 的 11,264 应收,两种证据状态下的仲裁期望净得:

证据状态分数回收概率期望回收总成本期望净得
完整10,00070%7,884.809,520−1,635.20
缺变更单和日志6,50055%6,195.209,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,5900
新增账单回收概率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.231,662.85+1,340.38
可计费率降到 50%2,273.881,662.85+611.03
继续服务的回收率升到 50%3,003.232,121.85+881.38
两个不利同时发生2,273.882,121.85+152.03
可计费率降到 30%1,544.531,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、网络安全险与雇主责任 。

常见错误与边界

  1. 以为「有理就能赢」。 能不能主张取决于能不能证明;证据缺口会先吃掉回收概率,再谈实体对错。
  2. 只留合同,不留变更单。 口头新增的工作在争议时容易被当成原范围或从未发生;变更单权重 2,000 不是装饰。
  3. 验收记录只写「通过」。 没有日期、版本和验收人,迟延和瑕疵的起算点仍然说不清。
  4. 用聊天记录代替书面确认。 沟通记录能证明「对方知道」,但不一定能证明「对方批准了范围变更」。
  5. 日志只留在对方系统里。 账号被停或数据过期后,客观时间线就没了;保全证据要导出到可控位置。
  6. 把证据分数当胜诉率。 10,000 分只表示清单完整,不表示法院或仲裁庭一定采信。
  7. 逾期后继续无异议地服务。 商业上容易被对方用来主张你已接受变更;书面异议的成本只有 85–170。
  8. 把停工作为情绪反应。 停工的现金优势来自释放工时的再利用;没有其他客户可接时,停工可能更差。
  9. 忽略交付成本率。 继续服务不是「多收钱」,而是「多发生 15.6% 的交付成本」;只算收入会高估继续服务的价值。
  10. 用一次比较代替持续复盘。 可计费率、回收概率和逾期余额都会变;90 天窗结束后要重算,而不是沿用旧结论。

本节的边界是「证据完整度与止损现金比较」:只覆盖五类证据的权重、证据分数对回收概率的影响,以及继续服务与停工两条路径的 90 天净现金,不展开证据采纳规则、保全措施的法律程序、诉讼时效和跨境取证。所有权重、概率和成本率均为教学假设;具体证据策略因司法辖区而异,本节不构成法律意见。违约形态的金额量化见 5.1 违约的四种形态 ,程序成本比较见 5.2 争议解决:管辖、仲裁与调解 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查