Skip to content
dsh.fish
Bundle

dsh-contract-stance

合同条款立场台账核对(按立场、风险等级与退让底线核对台账自洽,仅提示差异,不作出定性结论)

Source
PerryLink
License
Apache-2.0
Updated
Updated 7 days ago

Readme

# dsh-contract-stance — Contract clause stance register completeness and self-consistency check

[![DSH Market](https://raw.githubusercontent.com/2BingLing/dsh-market/master/assets/readme/badge-listed-en.svg)](https://dsh.market/)

`dsh-contract-stance` reads one clause-stance register — the contract header plus one row per clause — and checks that register's own completeness and self-consistency: that each clause's text is recorded, that your stance comes from your own vocabulary, that the risk grade does too, that a must-have clause records both a fallback position and an owner, that clause numbers are unique, that the register names the contract and your side, and that no unreplaced placeholder survives in the clause text.

## What it looks like

![Terminal demo of dsh-contract-stance: real output over its CS-001 fixture](https://raw.githubusercontent.com/PerryLink/dsh-contract-stance/main/docs/assets/dsh-contract-stance-demo.png)

Real output from this plugin over its own `CS-001` test fixture — not a mock-up. The rule pack ships no invented quotations, so a finding names both the clause it applied and the fact that the clause text was not obtained.

## What it answers

| You ask | What it answers |
|---|---|
| A clause row carries a topic, but the clause-text column is blank. Is that reported? | Yes. `CS-001` requires the `text` column on every row: without the wording, a stance cannot be checked against what it is a stance on. The rule checks only that the wording is recorded — it does not judge whether the clause should be accepted or whether it carries legal risk. |
| The stance and the risk grade are both filled in, yet every row comes back as `skipped`. Why? | Both value lists ship empty. `CS-002` accepts a stance only from the list configured for it, and `CS-003` does the same for the risk grade; with no list configured neither rule can run, so each reports itself in `skipped` rather than passing quietly. `CS-002` checks only that the value is on your list, not that the stance is appropriate; `CS-003` checks only that the grade is on your list, not how high the actual risk of that clause is. |
| A clause is marked must-have, but nobody wrote how far we can concede or who decides. | `CS-004` reads the register's own must-have column: for a row marked `是`, `Y`, `yes`, `true`, `必保` or `√` (the values set by `conditionValues`) it requires both the `fallback` and the `owner` column to be filled. It checks only that those two are filled, not whether the fallback is reasonable; which clauses count as must-have depends entirely on the project, and the rule never decides that. |
| The same clause number appears on two rows, one per negotiating draft. Is that a finding? | `CS-005` reports a repeated clause number, comparing with whitespace ignored. A repeated number makes it impossible to point at one clause precisely. One clause appearing once per draft is a normal shape: distinguish it in the version column instead of reusing the number. No clause requires clause numbers to be unique — uniqueness is what makes the register referable. |
| The header does not say which contract this is, nor which side we are. | `CS-006` requires the material's header to state `contractName` and `party`: without those two, a stance cannot be traced to a specific transaction and party. If your form carries a version column for negotiating drafts, add it to that rule's `fields`. The rule only checks that the header declares those two. |
| The clause text was copied straight from a model form and still contains 【】 or TBD. | `CS-007` reports clause text that still holds any of its placeholder terms — `【`, `】`, `{{`, `}}`, `XXX`, `xxx`, `待填`, `待补充`, `TBD`, `todo`, `示例` — which can be adjusted to your own template. Referring to a model form is allowed; the danger is that a template placeholder is read as wording already reviewed. The rule only looks for those terms in the `text` column, not at the clause itself. |

## Standards it follows

| Document | Number | Cited by rules |
|---|---|---|
| 《中华人民共和国民法典》 | 现行版本与条号本次未核实 | CS-001, CS-002, CS-003, CS-004, CS-005, CS-006, CS-007 |

**Boundary:** this plugin checks a **合同条款立场台账** for what a register can be held to — that each clause's
text is recorded, that your stance comes from your vocabulary, that the risk grade does too, that a must-have
clause records both a fallback position and an owner, that clause numbers are unique, that the register names
the contract and your side, and that no placeholder survives. It does **not** decide whether a clause should be
accepted, whether a risk is bearable, whether to sign, or whether a negotiating strategy is sound. **Those
depend on the deal, the counterparty's leverage and your own risk appetite.**

> ### ⚠️ What the citation rests on — and one thing this plugin must never be given
>
> **《中华人民共和国民法典》was obtained and read verbatim**, and `rules/evidence/clause-verification.md`
> records what was quoted: article 465 (a contract lawfully formed is protected by law), article 470 (contract
> content **一般包括** the parties, subject matter, quantity, quality, price, performance terms and liability for
> breach), article 509, article 577 (the consequences of breach) and article 584 (damages include expected
> profits but are capped by **foreseeability**).
>
> **The contract part does not regulate a negotiation register.** It governs formation, validity, performance and
> liability; the register's columns come from your enterprise's own contract management rules. So the `excerpt`
> fields still say "本次未取得" and every rule stays `warn` or `info`: **"this column is blank" must not be read
> as "this breaches the Civil Code".** Note also that article 470 says **一般包括**, not 应当包括 — one more
> reason `CS-001` does not claim a direct obligation.
>
> ⚠️ **Do not add a limitation-period rule to this plugin.** The Code *does* fix numbers — article 188 sets a
> three-year limitation (twenty years at the outside) and article 203 rolls a period forward off a public
> holiday — which makes "just compare the dates" look tempting. It is not workable here: the period runs from
> **when the creditor knew or ought to have known** of the harm and the debtor, a finding of fact the register
> does not record; the period is also subject to suspension (article 194) and interruption (article 195), and
> article 196 lists claims the limitation does not apply to at all. A date-arithmetic rule would dress a factual
> finding up as a calculation, which is precisely the over-claim this family avoids.
>
> **No judgement about the substance of a clause is made anywhere in this plugin.** It reads your own columns
> and checks only that they are filled and drawn from your vocabulary. Two vocabularies ship **empty** — the
> stance list (`CS-002`) and the risk grades (`CS-003`) — so with nothing configured those rules report
> themselves in `skipped` rather than passing quietly.

## Compatibility

| Surface | Status |
|---|---|
| Harness | Peer range `>=0.1.2-rc.1 <0.2.0 \|\| >=0.2.0-0 <0.3.0` — verified to accept both `0.2.0-rc.2` and `0.2.1-alpha.1`. `engines.dsh` is deliberately not declared: it has no reader and cannot reject a host |
| Node | `^22.19.0 || >=24.0.0` |
| Platforms | All (plain ESM; no native code, no network, no model call) |
| Tool mode | Works in `native`, `ptc` and `both`; for a full contract use `ptc` |

## What it does

Registers the `contract_stance` tool. It reads one clause-position register — the contract header plus one row
per clause — applies a versioned rule pack, and returns a report.

| Rule | Check | Severity | Basis kind |
|---|---|---|---|
| `CS-001` | every clause's text is recorded | warn | principle |
| `CS-002` | the stance comes from your vocabulary (off by default) | info | principle |
| `CS-003` | the risk grade comes from your vocabulary (off by default) | info | direct |
| `CS-004` | a must-have clause records a fallback and an owner | warn | direct |
| `CS-005` | clause numbers are unique | warn | principle |
| `CS-006` | the register names the contract and your side | warn | direct |
| `CS-007` | the clause text holds no unreplaced placeholder | warn | principle |
## Install

```sh
dsh plugin --profile <name> add dsh-contract-stance
dsh --profile <name> --dump-config | grep 'dsh-contract-stance'
```

## Configuration

| Key | Type | Default | Description |
|---|---|---|---|
| `rulesFile` | string | `rules/contract-stance.yaml` | Rule-pack path, relative to the package root |
| `disabledRules` | string[] | `[]` | Rule ids to stop running; each appears in `skipped` |
| `onlyRules` | string[] | `[]` | Run only these rule ids; empty runs every rule |
| `skipNotes` | string | `""` | Note appended to every `skipped` reason |
| `timeoutMs` | number | `120000` | Cooperative tool timeout budget |

Rule-level parameters worth knowing:

- `CS-002` `values` — your stance vocabulary, e.g. `[坚持, 可谈, 接受, 须删除]`. Empty means no check.
- `CS-003` `values` — your risk grades, e.g. `[高, 中, 低]`. Empty means no check.
- `CS-004` `conditionValues` — the values in your 是否必保 column that mark a clause must-have, by default
  `[是, Y, yes, true, 必保, √]`.
- `CS-007` `terms` — the placeholders to look for.

## Material format

The tool accepts JSON or YAML:

```yaml
contractNo: HT-2026-018
contractName: 某某设备采购合同
party: 某某公司
version: 第 3 轮谈判稿
rows:
  - { 条款号: 第 8.2 条, 条款主题: 逾期付款违约金,
      条款原文: 买方逾期付款的,每逾期一日按未付金额的万分之五向卖方支付违约金。,
      本方立场: 可谈, 风险等级: 中, 风险说明: 违约金比例高于本方惯例且未设上限,
      退让底线: 比例可降至万分之三,须增加总额不超过合同额 5% 的上限,
      是否必保: 是, 谈判责任人: 王经理 }
```

Column names are matched case-insensitively and ignoring spaces, underscores and hyphens; the register's own
column names are kept, so a finding names the column it read.

## Rule sources

Rule data lives in `rules/contract-stance.yaml`. The pack's header states the citation gap in full, and each
rule's `note` repeats the part that matters for that rule. The load-time guard that normally enforces "an
excerpt must be a real quotation of at least eight characters" cannot tell a quotation from a description —
so this pack leans on the header, the per-rule notes and a test that asserts every `excerpt` admits the gap.

## Troubleshooting

- **`CS-002` or `CS-003` reports itself as skipped.** Its vocabulary is empty. Stance and risk wording is your
  institution's convention, and the plugin will not guess it.
- **`CS-004` fires on a clause marked must-have.** It records no fallback or no owner. A must-have clause
  without a stated floor cannot be negotiated on the spot.
- **`CS-001` fires although the clause is quoted elsewhere.** The `text` column itself is empty. The check
  reads the register, not the contract document.
- **`CS-005` fires on one clause twice.** That is legitimate across negotiation rounds — distinguish the rows
  in the version column rather than reusing the clause number.
- **The plugin installs but the tool never appears.** Check that `main` resolves to `lib/index.mjs` and
  that `pnpm run build` produced it; a wrong `main` makes the loader skip the entry silently.
- **`dsh plugin add` refuses the package as incompatible.** The peer range covers `0.1.x` and `0.2.x`; if
  your runtime sits outside it, grant an explicit exemption:
  `dsh plugin --profile <name> allow-version dsh-contract-stance@0.1.0 --dsh-version <runtime> --accept-risk`
- **`check` reports `manifest-peers` as failed.** The static checker compares against a hard-coded peer
  range that predates the 0.2 line. The runtime enforces peer compatibility at install time, so the
  declared range is the correct one; this is a known upstream issue in `dsh-plugin-dev`.

## Development

```sh
pnpm install
pnpm run typecheck   # tsc --noEmit
pnpm test            # vitest, the shared table-plugin suite plus paired fixtures
pnpm run build       # tsdown -> lib/index.mjs + lib/index.d.mts
node ../scripts/sync-shared.mjs dsh-contract-stance   # refresh src/shared from ../_shared
```

The plugin is **data-only**: `src/model.ts` declares the table shape, the shared kit supplies the reader and
the check engine, and the rule pack declares every check.

## License

[Apache License 2.0](LICENSE) © 2026 dsh-contract-stance contributors.

Install

dsh plugin --profile web add github:PerryLink/dsh-contract-stance

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source