完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
+77
查看文件
@@ -16,3 +16,80 @@ task before modifying approval behavior.
not drift because rules changed later.
- Reviewer review is required after changes touching workflow, status machine,
approval rules, permissions or migrations.
## Scenario: System Operator Is Not A Business Approver
### 1. Scope / Trigger
- Trigger: adding or changing OA account administration, role assignment,
approval eligibility, payment review or emergency-superuser behavior.
### 2. Signatures
- Daily responsibility roles: `manager`, `ceo`, `finance`, `system_admin`.
- Emergency role: `admin` (`*`), hidden from ordinary assignment UI.
- System permissions: `system.accounts.manage`, `system.roles.manage`.
- Payment reviewer permission: `payment.batch.review`.
### 3. Contracts
- `system_admin` manages ordinary accounts and system configuration only; it
does not imply leadership, finance, payroll, personnel-sensitive access or
business approval authority.
- A payment reviewer must have both `payment.batch.review` and an explicit
`manager` or `ceo` role. `admin` or `system_admin` alone is not a normal
reviewer.
- `manager` does not imply that every employee reports to that account;
reporting scope remains in `direct_manager_id`.
- A non-emergency system administrator cannot edit their own roles or grant,
revoke or import `admin` / `system_admin`. Role changes remain audited.
- Employee aggregate endpoints must not treat `system.settings` as personnel-
sensitive authority. Salary summaries require `payroll.manage`; performance
summaries require `system.users_sensitive`; unauthorized branches issue no
salary/performance query and return only the non-sensitive summary shape.
### 4. Validation & Error Matrix
- System administrator changes own roles -> `403 PERM_001` before role writes.
- System administrator changes a protected role -> `403 PERM_001`, zero role
and audit writes.
- Reviewer has permission but lacks `manager|ceo` -> `403 PERM_001`.
- Batch creator attempts review -> deterministic `409 VALID_001`.
- `system_admin` opens ordinary employee statistics -> salary is null,
performance is empty and both sensitive query families remain untouched.
### 5. Good/Base/Bad Cases
- Good: one person may explicitly hold both `manager` and `system_admin`, while
each route evaluates the responsibility required for that action.
- Base: `system_admin` creates or disables an ordinary account without seeing
payroll, bank, identity or employee-contract content.
- Bad: treat `admin` as the default company leader or use `system_admin` as an
approval fallback because the team is small.
### 6. Tests Required
- Role matrix for employee, manager, CEO, finance, system administrator and
emergency administrator.
- Negative tests for self-role change, protected-role assignment/import and
permission-only payment review.
- Negative employee-statistics test using the real `system_admin` permission
set, plus positive tests for explicit payroll/personnel-sensitive authority.
- Reviewer read-only review after changing role or approval semantics.
### 7. Wrong vs Correct
#### Wrong
```js
if (hasPermission(actor, "*")) allowPaymentReview();
```
#### Correct
```js
if (!hasPermission(actor, "payment.batch.review")
|| !actor.roleCodes.some((role) => role === "manager" || role === "ceo")) {
throw forbidden();
}
```
@@ -383,3 +383,82 @@ const currentSession = await tx.authSession.findUnique({ where: { id: sessionId
if (!currentSession || currentSession.revoked_at) return invalidRefresh();
await rotateSession(tx, currentSession);
```
## Scenario: Small-Team Payment Batch Consistency
### 1. Scope / Trigger
- Trigger: changing payment-batch creation, review, execution, retry,
cancellation, receipt evidence or the legacy direct-payment path.
### 2. Signatures
- Candidate API: `GET /api/payment-batches/candidates?q=&page=&pageSize=`.
- State APIs: `POST /api/payment-batches/:id/review`,
`POST /api/payment-batches/:id/execute`,
`POST /api/payment-batches/:id/cancel` with `{ reason }`.
- Receipt invalidation accepts `{ reason }` and retains the record.
- DB: `payment_batches.cancelled_reason TEXT NULL`.
- Isolated concurrency gate:
`bash scripts/qa/run-web-gates.sh payment-concurrency-local`.
### 3. Contracts
- Normal flow is finance maker/executor plus one independent `manager|ceo`
reviewer; the maker cannot review their own batch.
- Lock the batch before state checks, then lock source entities in stable order
and recheck status, amount and reservation inside the same transaction.
- When `payment_batch_v1` is enabled, an approved reimbursement can enter only
one active batch and the legacy direct-payment route must not form a second
write path.
- Retry processes only `pending|failed` items. Existing `executed` items never
repeat payment writes, follow-ups or notifications. Persisted execution
summary is recomputed from every final item state; the audit separately
records the current attempt.
- Cancellation requires a 2-500 character reason. Receipt evidence is voided
with audit history, not physically deleted.
### 4. Validation & Error Matrix
- Missing review, maker-reviewer conflict or terminal state -> `409 VALID_001`.
- Concurrent loser after a valid winner -> deterministic `409`, zero duplicate
state, audit or notification writes.
- Missing/invalid cancellation or void reason -> `400 VALID_001`.
- Source already paid, amount changed or reserved by another active batch ->
`409 VALID_001` and no partial batch mutation.
### 5. Good/Base/Bad Cases
- Good: one executor wins, a concurrent cancel or second executor receives
`409`, and the final payment/audit/notification counts remain one.
- Base: a failed batch retries only failed items and reaches `executed` with an
all-item final summary.
- Bad: count only the retry attempt in `execution_summary_json`, or allow the
direct route to pay while the same source is in a valid batch.
### 6. Tests Required
- Focused route tests for candidate authority, maker-checker, retry summary,
cancellation, receipt void and zero-side-effect refusals.
- Real disposable PostgreSQL races for double execute, execute/cancel, double
create and direct-payment/batch competition; assert unique audits and
notifications and zero residual test databases.
- Prisma validate, clean empty-database migration application and a read-only
reviewer after payment-state changes.
### 7. Wrong vs Correct
#### Wrong
```js
summary.successCount = successesInThisRetry;
batch.status = failuresInThisRetry ? "failed" : "executed";
```
#### Correct
```js
const finalSummary = summarizeFinalItemStatuses(finalItemStatuses);
batch.execution_summary_json = finalSummary;
batch.status = finalSummary.failedCount > 0 ? "failed" : "executed";
```
@@ -442,3 +442,77 @@ validate_isolated_target_before_writes
run_mutating_auth_regression
drop_owned_database_or_fail
```
## Scenario: Repository Local Candidate Gate
### 1. Scope / Trigger
- Trigger: declaring a checkout ready for internal deployment planning without
deploying it or using public, production or real-account evidence.
### 2. Signatures
- Full gate: `bash scripts/qa/run-local-candidate.sh full`.
- Browser-only rerun: `bash scripts/qa/run-local-candidate.sh focused`.
- Parse-only check: `bash scripts/qa/run-local-candidate.sh list`.
- Route contract: `scripts/qa/route-manifest.json`.
- Result verifier: `scripts/qa/verify-local-candidate-results.mjs`.
### 3. Contracts
- `full` alone may print `LOCAL_CANDIDATE_PASS`; `focused` prints
`FOCUSED_BROWSER_PASS`, and `list` prints `LIST_ONLY`.
- The gate accepts only loopback HTTP origins and canonical TCP ports. It uses
local synthetic fixtures, excludes `manual/live` specs and does not consume
historical Issue CSV as a candidate prerequisite.
- Every registered route is classified. Every core bundle has one stable,
source-bound witness; candidate spec basenames are unique.
- Every selected Playwright result must pass. Missing, duplicate, unknown,
skipped, multi-tag or wrong-source witnesses and any failed/skipped
acceptance test make the gate fail.
- The script's full pass proves only its local scope. A task changing payment
concurrency also requires the isolated PostgreSQL payment gate; deployment,
public checks and real-device behavior remain separate states.
### 4. Validation & Error Matrix
- Non-loopback URL, credentialed URL, path/query target or invalid port -> fail
before Vite/Playwright use.
- Unclassified/orphan route or missing core behavior/visual witness -> non-zero
scanner result.
- Playwright suite error, unexpected, failed or skipped test -> non-zero result
gate with basename-only redacted diagnostics.
- `focused` succeeds -> useful rerun evidence, but not a full candidate claim.
### 5. Good/Base/Bad Cases
- Good: full base checks pass, all source-bound witnesses and acceptance specs
execute, and the runner prints one full-pass label.
- Base: use `list` before a long run to prove assets parse, while recording that
no behavior has run.
- Bad: reuse a prior browser pass after parser changes, or report focused/list
output as `LOCAL_CANDIDATE_PASS`.
### 6. Tests Required
- Negative tests for route gaps, manual/live evidence, duplicate basenames,
invalid loopback inputs, witness result/source drift and mode labels.
- Current-checkout `full` run with zero skipped/unexpected results.
- For payment-state changes, run the disposable PostgreSQL gate and prove
cleanup left no matching temporary databases.
### 7. Wrong vs Correct
#### Wrong
```bash
bash scripts/qa/run-local-candidate.sh list
echo LOCAL_CANDIDATE_PASS
```
#### Correct
```bash
bash scripts/qa/run-local-candidate.sh full
# Only this successful branch emits LOCAL_CANDIDATE_PASS.
```
@@ -938,6 +938,69 @@ validation prevent repeated re-entry and reduce support/debugging cost.
- Focused Playwright coverage for draft save, submit validation, unsaved-change
protection and server-validation failure paths when changing form behavior.
## Scenario: Data-Router Unsaved-Change Protection
### 1. Scope / Trigger
- Trigger: a React form can lose unsaved work through in-app navigation,
browser back/forward, reload, closing its panel or switching records.
### 2. Signatures
- Router: `createBrowserRouter(...)` rendered through `RouterProvider`.
- Navigation blocker: React Router `useBlocker(isDirty)`.
- Reload/close guard: `beforeunload` while `isDirty === true`.
- Shared UI contract: one unsaved-change dialog with continue-editing and
discard actions.
### 3. Contracts
- Block navigation before the route or selected record changes; do not restore
old UI state after navigation has already occurred.
- `blocker.reset()` keeps the current route and form values. `blocker.proceed()`
is called only after explicit discard confirmation.
- Record switching and panel closing use the same pending-action contract as
route navigation. Successful save or explicit discard clears dirty state.
- Register `beforeunload` only while dirty and remove it during cleanup.
### 4. Validation & Error Matrix
- Clean form navigation -> continue without a dialog.
- Dirty in-app navigation or browser back -> block and show one dialog.
- Continue editing -> URL, record selection and values remain unchanged.
- Discard -> execute exactly one pending navigation/selection and clear dirty.
- Reload/close while dirty -> browser-level unload warning remains active.
### 5. Good/Base/Bad Cases
- Good: reimbursement, procurement, contract and asset forms reuse one guard
while keeping their own save and dirty-field logic.
- Base: a short non-editable filter form does not register the guard.
- Bad: listen to history after navigation, then push the previous URL back; the
user can see state flicker or lose form data before confirmation.
### 6. Tests Required
- Playwright covers link navigation, browser back, reload and record switch.
- Assert cancel preserves URL and values; discard changes target once; save
removes the warning.
- Frontend typecheck and build after router or shared guard changes.
### 7. Wrong vs Correct
#### Wrong
```ts
window.addEventListener("popstate", () => navigate(previousUrl));
```
#### Correct
```ts
const blocker = useBlocker(isDirty);
// reset to keep editing; proceed only after explicit discard.
```
## Convention: Detail Evidence Layout
**What**: Detail pages use one evidence-first structure so users can understand