merge: 合入候选分支(09-17借款/ERP退役、09-18编号、09-19预算并发、完整10-05错误码迁移,均经确认)

# Conflicts:
#	.agents/skills/trellis-before-dev/SKILL.md
#	.agents/skills/trellis-brainstorm/SKILL.md
#	.agents/skills/trellis-continue/SKILL.md
#	.agents/skills/trellis-start/SKILL.md
#	.trellis/tasks/08-23-isolated-backup-restore-drill/design.md
#	.trellis/tasks/08-23-isolated-backup-restore-drill/prd.md
#	.trellis/tasks/09-05-error-explainability-payment-readiness/prd.md
#	.trellis/tasks/09-05-error-explainability-payment-readiness/task.json
#	.trellis/tasks/09-09-existing-deploy-seed-separation/implement.md
#	.trellis/tasks/archive/2026-09/09-05-trellis-upgrade-rule-cleanup/task.json
#	.trellis/workflow.md
#	scripts/qa/error-reason-legacy-baseline.json
#	server/src/routes/apOperations.js
#	server/src/routes/portal.js
#	server/src/routes/todos.js
#	server/tests/unit/fixtures/error-reason-messages.json
#	server/tests/unit/routes/apOperations.permissions.test.js
这个提交包含在:
2026-09-26 07:54:10 +08:00
当前提交 46a70aa552
共修改 395 个文件,包含 23086 行新增和 13407 行删除
@@ -1,105 +1,21 @@
# Code Reuse Thinking Guide
> **Purpose**: Stop and think before creating new code - does it already exist?
Consult when adding a helper/component or changing behavior shared by multiple
consumers. Search the relevant source and tests with `rg` before creating a
second implementation; widen the search when ownership is uncertain.
---
## Decide From The Contract
## The Problem
- Compare responsibilities, inputs, failure behavior, permissions and lifecycle, not only similar syntax or visual shape.
- Reuse or extend an existing owner when the semantics agree. Keep distinct rules separate when combining them creates flags, coupling or misleading names.
- Share constants only when they represent the same business fact. A fixed repetition count or similarity percentage does not justify an abstraction.
- For a shared change, identify callers and assert the affected behaviors, including exceptions. After a batch edit, check for missed consumers and accidental out-of-scope changes.
**Duplicated code is the #1 source of inconsistency bugs.**
## Asymmetric Producers
When you copy-paste or rewrite existing logic:
- Bug fixes don't propagate
- Behavior diverges over time
- Codebase becomes harder to understand
---
## Before Writing New Code
### Step 1: Search First
```bash
# Search for similar function names
grep -r "functionName" .
# Search for similar logic
grep -r "keyword" .
```
### Step 2: Ask These Questions
| Question | If Yes... |
|----------|-----------|
| Does a similar function exist? | Use or extend it |
| Is this pattern used elsewhere? | Follow the existing pattern |
| Could this be a shared utility? | Create it in the right place |
| Am I copying code from another file? | **STOP** - extract to shared |
---
## Common Duplication Patterns
### Pattern 1: Copy-Paste Functions
**Bad**: Copying a validation function to another file
**Good**: Extract to shared utilities, import where needed
### Pattern 2: Similar Components
**Bad**: Creating a new component that's 80% similar to existing
**Good**: Extend existing component with props/variants
### Pattern 3: Repeated Constants
**Bad**: Defining the same constant in multiple files
**Good**: Single source of truth, import everywhere
---
## When to Abstract
**Abstract when**:
- Same code appears 3+ times
- Logic is complex enough to have bugs
- Multiple people might need this
**Don't abstract when**:
- Only used once
- Trivial one-liner
- Abstraction would be more complex than duplication
---
## After Batch Modifications
When you've made similar changes to multiple files:
1. **Review**: Did you catch all instances?
2. **Search**: Run grep to find any missed
3. **Consider**: Should this be abstracted?
---
## Gotcha: Asymmetric Mechanisms Producing Same Output
**Problem**: When two different mechanisms must produce the same file set (e.g., recursive directory copy for init vs. manual `files.set()` for update), structural changes (renaming, moving, adding subdirectories) only propagate through the automatic mechanism. The manual one silently drifts.
**Symptom**: Init works perfectly, but update creates files at wrong paths or misses files entirely.
**Prevention checklist**:
- [ ] When migrating directory structures, search for ALL code paths that reference the old structure
- [ ] If one path is auto-derived (glob/copy) and another is manually listed, the manual one needs updating
- [ ] Add a regression test that compares outputs from both mechanisms
---
## Checklist Before Commit
- [ ] Searched for existing similar code
- [ ] No copy-pasted logic that should be shared
- [ ] Constants defined in one place
- [ ] Similar patterns follow same structure
When automatic discovery/copy and a manually listed producer promise the same
output (for example init/update, screen/export or live/snapshot inputs), a
rename or added field can update only one side. Inspect both producers and
compare their observable output contract. Prefer a focused parity regression
over another copied list; do not assume generated output proves every producer
was exercised.
@@ -1,162 +1,46 @@
# Cross-Layer Thinking Guide
> **Purpose**: Think through data flow across layers before implementing.
Use for changes crossing data, authority or state boundaries. Trace the affected
path from input through transformation/storage to display or export. Identify
consumers and the format, ownership, errors and invariants at each boundary.
Do not require a diagram or new document when a short task note suffices.
---
## Validation And Round Trips
## The Problem
Client validation helps the user; the server still enforces authentication,
authorization and untrusted input. Mutable business facts may need rechecking
under the transaction/lock that protects the write. Reuse pure validation rules
where semantics agree, but never remove a trust-boundary check merely because
another layer validated earlier.
**Most bugs happen at layer boundaries**, not within layers.
Check null/omitted/empty distinctions, dates/time zones, precision, serialization
and error handling on affected consumers. Verify meaningful round trips and
negative cases; test depth follows the changed contract, not a fixed layer count.
The owning domain spec defines exact business behavior.
Common cross-layer bugs:
- API returns format A, frontend expects format B
- Database stores X, service transforms to Y, but loses data
- Multiple layers implement the same logic differently
## Generated Runtime Inputs
---
For generated files that are also parsed at runtime, identify readers as well
as writers. Workflow tag/platform markers outside a managed block may affect
phase routing even when the block itself updates correctly.
## Before Implementing Cross-Layer Features
In this installed project, use the pinned target CLI, preserve overlays and
exercise the actual context/hook parsers; see
[upgrade compatibility](../hyb-oa/project-governance.md#scenario-trellis-project-upgrade-compatibility).
Only an authorized upstream-template task should modify other platform copies
or test upstream init/update generation with older pristine fixtures. Do not
assume this repository contains `src/templates` or enable other platforms.
### Step 1: Map the Data Flow
## Probe-Dependent Modes
Draw out how data moves:
When a mode depends on an external probe, check normal, non-interactive and
shortcut paths. Distinguish missing resources from transient failures; fail
clearly or retry within the authorized budget, never silently switch modes
after a timeout or transport error. Preserve that error
quality in downstream helpers and reset cached state when the source changes.
```
Source → Transform → Store → Retrieve → Transform → Display
```
For each arrow, ask:
- What format is the data in?
- What could go wrong?
- Who is responsible for validation?
### Step 2: Identify Boundaries
| Boundary | Common Issues |
|----------|---------------|
| API ↔ Service | Type mismatches, missing fields |
| Service ↔ Database | Format conversions, null handling |
| Backend ↔ Frontend | Serialization, date formats |
| Component ↔ Component | Props shape changes |
### Step 3: Define Contracts
For each boundary:
- What is the exact input format?
- What is the exact output format?
- What errors can occur?
---
## Common Cross-Layer Mistakes
### Mistake 1: Implicit Format Assumptions
**Bad**: Assuming date format without checking
**Good**: Explicit format conversion at boundaries
### Mistake 2: Scattered Validation
**Bad**: Validating the same thing in multiple layers
**Good**: Validate once at the entry point
### Mistake 3: Leaky Abstractions
**Bad**: Component knows about database schema
**Good**: Each layer only knows its neighbors
---
## Checklist for Cross-Layer Features
Before implementation:
- [ ] Mapped the complete data flow
- [ ] Identified all layer boundaries
- [ ] Defined format at each boundary
- [ ] Decided where validation happens
After implementation:
- [ ] Tested with edge cases (null, empty, invalid)
- [ ] Verified error handling at each boundary
- [ ] Checked data survives round-trip
---
## Cross-Platform Template Consistency
In Trellis, command templates (e.g., `record-session.md`) exist in **multiple platforms** with identical or near-identical content. This is a cross-layer boundary.
### Checklist: After Modifying Any Command Template
- [ ] Find all platforms with the same command: `find src/templates/*/commands/trellis/ -name "<command>.*"`
- [ ] Update all platform copies (Markdown `.md` and TOML `.toml`)
- [ ] For Gemini TOML: adapt line continuations (`\\` vs `\`) and triple-quoted strings
- [ ] Run `/trellis:check-cross-layer` to verify nothing was missed
**Real-world example**: Updated `record-session.md` in Claude to use `--mode record`, but forgot iFlow, Kilo, OpenCode, and Gemini — caught by cross-layer check.
---
## Generated Runtime Template Upgrade Consistency
Some generated files are both documentation and runtime input. In Trellis,
`.trellis/workflow.md` is parsed by `get_context.py`, `workflow_phase.py`,
SessionStart filters, and per-turn hooks. Template changes must be validated
against both fresh init and upgrade paths.
### Checklist: After Modifying A Runtime-Parsed Template
- [ ] Identify every runtime parser that reads the template, not just the file
writer that installs it
- [ ] Check whether relevant syntax lives outside obvious managed regions
such as tag blocks
- [ ] Verify fresh `init` output and a versioned `update` scenario that writes
the older `.trellis/.version`
- [ ] Add an upgrade regression using an older pristine template fixture, then
assert the installed file reaches the current packaged shape
- [ ] Update the backend spec that owns the runtime contract
**Real-world example**: Codex inline mode changed workflow platform markers from
`[Codex]` / `[Kilo, Antigravity, Windsurf]` to `[codex-sub-agent]` /
`[codex-inline, Kilo, Antigravity, Windsurf]`. Fresh init was correct, but
`trellis update` only merged `[workflow-state:*]` blocks and preserved stale
markers outside those blocks. Result: upgraded projects got new hook scripts
but old workflow routing, so `get_context.py --mode phase --platform codex`
could return empty Phase 2.1 detail.
---
## Mode-Detection Probe Checklist
When a CLI auto-detects a mode by probing a remote resource (e.g., checking if `index.json` exists to decide marketplace vs direct download):
### Before implementing:
- [ ] Probe runs in **ALL** code paths that use the result (interactive, `-y`, `--flag` combos)
- [ ] 404 vs transient error are distinguished — don't treat both as "not found"
- [ ] Transient errors **abort or retry**, never silently switch modes
- [ ] Shared state (caches, prefetched data) is **reset** when context changes (e.g., user switches source)
- [ ] **Shortcut paths** (e.g., `--template` skipping picker) must have the same error-handling quality as the probed path — check that downstream functions don't call catch-all wrappers
### After implementing:
- [ ] Trace every path from probe result to the mode-decision branch — no fallthrough
- [ ] External format contracts (giget URI, raw URLs) are tested or at least documented as comments
- [ ] Metadata reads consume a complete response or use a streaming parser — never parse a fixed-size prefix as full JSON
- [ ] When reconstructing a composite identifier from parsed parts, verify **all** fields are included and in the **correct position** (e.g., `provider:repo/path#ref` not `provider:repo#ref/path`)
- [ ] Verify that **action functions** called after a shortcut don't internally use the old catch-all fetch — they must use the probe-quality variant when error distinction matters
**Real-world example**: Custom registry flow had 8 bugs across 3 review rounds: (1) probe only ran in interactive mode, (2) transient errors fell through to wrong mode, (3) giget URI had `#ref` in wrong position, (4) prefetched templates leaked across source switches, (5) `--template` shortcut bypassed probe but `downloadTemplateById` internally used catch-all `fetchTemplateIndex`, turning timeouts into "Template not found".
**Real-world example**: Agent-session update hints fetched npm `latest` metadata with `response.read(4096)` and then parsed it as complete JSON. The `@mindfoldhq/trellis` package metadata exceeded 4 KB, so the JSON was truncated, parse failed silently, and the first session injection showed no update hint. Fix: read the complete response before parsing, and add a regression where `version` is followed by an 8 KB metadata tail.
---
## When to Create Flow Documentation
Create detailed flow docs when:
- Feature spans 3+ layers
- Multiple teams are involved
- Data format is complex
- Feature has caused bugs before
Parse complete metadata or use a real streaming parser; a fixed-length prefix
is not a complete JSON document. For reconstructed identifiers, preserve all
fields and their ordering (such as path before fragment). Exercise a large
metadata response and failure/shortcut cases only when those mechanisms are
part of the change. Do not contact real external systems merely to read this guide.
+10 -68
查看文件
@@ -1,72 +1,14 @@
# Thinking Guides
> **Purpose**: Expand your thinking to catch things you might not have considered.
Use a guide only when its trigger applies and its relevant context is not
already loaded. Stable implementation contracts remain in the domain specs;
these guides help locate the questions that matter.
---
| Guide | Trigger |
| --- | --- |
| [Code reuse](./code-reuse-thinking-guide.md) | A new helper/component, repeated behavior, or a shared constant/contract change |
| [Cross-layer behavior](./cross-layer-thinking-guide.md) | Data, authority or state crosses a boundary; multiple consumers depend on a change |
## Why Thinking Guides?
**Most bugs and tech debt come from "didn't think of that"**, not from lack of skill:
- Didn't think about what happens at layer boundaries → cross-layer bugs
- Didn't think about code patterns repeating → duplicated code everywhere
- Didn't think about edge cases → runtime errors
- Didn't think about future maintainers → unreadable code
These guides help you **ask the right questions before coding**.
---
## Available Guides
| Guide | Purpose | When to Use |
|-------|---------|-------------|
| [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md) | Identify patterns and reduce duplication | When you notice repeated patterns |
| [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) | Think through data flow across layers | Features spanning multiple layers |
---
## Quick Reference: Thinking Triggers
### When to Think About Cross-Layer Issues
- [ ] Feature touches 3+ layers (API, Service, Component, Database)
- [ ] Data format changes between layers
- [ ] Multiple consumers need the same data
- [ ] You're not sure where to put some logic
→ Read [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md)
### When to Think About Code Reuse
- [ ] You're writing similar code to something that exists
- [ ] You see the same pattern repeated 3+ times
- [ ] You're adding a new field to multiple places
- [ ] You are changing a shared constant or configuration contract
- [ ] **You're creating a new utility/helper function** ← Search first!
→ Read [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md)
---
## Reference Checks By Risk
Before changing shared contracts, cross-layer constants or configuration, inspect their consumers. A local literal or wording correction does not require a repository-wide search. Read these guides only when the task matches their triggers; do not repeat an unchanged checklist every turn.
---
## How to Use This Directory
1. **Before coding**: Skim the relevant thinking guide
2. **During coding**: If something feels repetitive or complex, check the guides
3. **After bugs**: Add new insights to the relevant guide (learn from mistakes)
---
## Contributing
Found a new "didn't think of that" moment? Add it to the relevant guide.
---
**Core Principle**: 30 minutes of thinking saves 3 hours of debugging.
Search affected consumers for a shared-contract change. A local wording or
literal correction does not require a full-repository search. Add a reusable
insight at its existing owner; keep one-off findings in the task.
+19
查看文件
@@ -17,6 +17,16 @@ task before modifying approval behavior.
- Reviewer review is required after changes touching workflow, status machine,
approval rules, permissions or migrations.
## Verification: Required Manager Missing
`server/scripts/regression-workflow-actions.sh` scenario 8 follows the current
required-manager contract: submission returns 409 `VALID_001`, details contain
`workflow=no_actionable_approver`, `reason=no_manager_found` and
`reasonCode=workflow.manager_unavailable`; the reimbursement stays draft with
no workflow instance. Do not restore the obsolete automatic skip-to-finance
expectation. Optional-step skipping remains independently covered in workflow
unit tests. Synthetic relationship changes do not resolve real employee setup.
## Verification: Direct Manager And CEO Are The Same Employee
`scripts/qa/test-invoice-first-http.cjs` supports opt-in
@@ -786,3 +796,12 @@ const semantics = buildPaymentProcessingSemantics({
// Correct: the UI renders only the server candidate contract.
const candidates = await fetchPaymentBatchCandidates({ q, page, pageSize });
```
## Scenario: Pending Payment Names And Delivery Evidence
- Batch pending stages may project current active eligible reviewers/finance names, excluding the maker/reviewer as appropriate. Keep completed stages' actual actor, and label the candidate list as 可复核/可办理 rather than claiming a named exclusive assignment.
- Separately show recipients of the persisted responsibility notification. Creation selects one CEO-first reviewer; current eligibility and historical notification recipient can differ after role changes. Loan review additionally excludes submitter and verified lender OA identity.
- Personal WeCom sending requires a successful delivery with result code `wecom_app_sent`; generic success, mock, webhook group success and no-bound-recipient skip are not personal sending evidence. Success does not establish reading.
- Shared dispatch stores a batch attempt on one anchor notification. Multiple intended recipients plus one success cannot prove every person received the message; use the bounded “存在发送成功记录,接收范围需核对” projection. Do not read raw delivery payloads or channel secrets for the progress display.
- Verify eligible-vs-notified separation, inactive/self/admin exclusions, completed actor preservation, no attempt/group/mock/unbound/failed and multiple-recipient cases; desktop/mobile use the same server progress projection.
+170 -1
查看文件
@@ -1,5 +1,20 @@
# Backend Fastify / Prisma
## Shared Budget Pool Write Transactions
- Entity row locks do not serialize different reimbursements, purchases or card entries sharing a budget pool. Before an authoritative budget decision or usage mutation, call `lockBudgetPoolsForUpdate(tx, poolIds)` on the interactive transaction client. It parameterizes UUIDs, deduplicates/sorts all required IDs and uses `ORDER BY id FOR NO KEY UPDATE`; do not substitute process-local locks or absolute writes based on unlocked reads. Prisma 7 supports nested `$transaction`; use the root-only `$connect` capability to reject root clients, not presence of `$transaction`.
- Maintain caller entity/workflow → budget pool lock order; the helper must never acquire entity locks. Card refunds that later synchronize an authorization must lock that authorization before taking a pool lock, matching authorization imports. For multi-pool releases, lock the complete pool set in stable order, then reread the ledger; reject a newly appearing pool instead of acquiring it out of order.
- `resolveApplicableBudgetPoolByContext(tx, context, { forUpdate: true, additionalPoolIds })` selects, locks and rereads the candidate. A different/missing candidate after waiting returns 409 with `budget.pool_changed`; previews omit `forUpdate`. Submission passes the locked record (or explicit null) as `budgetPoolForWrite` to `resolveEntityPrecheck`, preserving the single reimbursement entity read and keeping the checked/charged pool identical. Undefined means ordinary preview resolution; never accept this internal argument from a public request or cache it across transactions.
- Pool usage, applicable blocking decisions and ledger changes remain in the same business transaction. Purchase submission must recompute and recheck inside that transaction, not reuse a pre-transaction preview. Preserve control-mode/feature gates, Decimal comparisons at exact capacity, duplicate behavior, release floor and existing audit atomicity. Atomic increments alone do not repair stale over-budget decisions.
- Verify with `RUN_ISOLATED_BUDGET_DB_TEST=1 node scripts/qa/test-budget-pool-db.cjs`: real services/routes, synthetic authorized identities and isolated PostgreSQL, observed lock waits, mixed writers, multi-pool order, metadata changes while waiting and fault rollback. These fixtures do not prove production balances, authentication, frozen travel availability or publication eligibility; ordinary full-gate and independent-review obligations remain.
## Composing Visibility And Activity Filters
- `GET /delegation-grants` keeps an ordinary caller within records where they are the principal or delegate. The existing `system.users` check alone allows manager-selected principal/delegate filters; do not broaden this to `system.settings` or change the separate legacy revoke API.
- Visibility and activity each contain `OR`. Compose them as `AND: [baseWhere, activeWhere]`; object spread would overwrite the first `OR` and expose unrelated active grants. `includeInactive=true` removes only activity constraints, retaining `baseWhere`.
- Preserve `revoked_at=null`, `valid_from<=now`, and `(valid_to=null OR valid_to>now)`. A grant ending exactly now is inactive; one starting exactly now is eligible. Keep user-supplied personnel filters from widening an ordinary caller's scope.
- Test actual route query consumption with synthetic own/other, active/future/expired/revoked grants, both includeInactive modes, and administrator explicit filters. An unconditional mocked `findMany` result cannot detect a dropped visibility predicate. In-memory evaluation is not evidence of production data, PostgreSQL behavior or authentication.
## Scope
Server work lives under `server/`. Prefer existing route, service, utility and
@@ -20,13 +35,84 @@ QA script patterns over new abstractions.
### Calendar Date Inputs
- For reimbursement, payment-batch, travel-request and asset date-only input schemas,
- For reimbursement, payment-batch, travel-request, asset, contract-registry,
purchase delivery, travel payment/reconciliation, weekly-report and presence date-only input schemas,
combine the YYYY-MM-DD shape with isValidDateOnlyString. Reject calendar overflow
before persistence; a regex alone accepts February 30 and JavaScript normalizes it.
- Keep optional and nullable wrappers outside the string validator. Validate leap-day,
month-end, invalid month and empty/null boundaries without changing stored historical dates.
- parseDateOnlyString retains its legacy conversion contract for unaudited consumers;
it is not validation. Do not claim the shared parser or other domains are all hardened.
- Validate weekly-report weekStart before Monday normalization, including the GET
that may create a draft. Invalid dates must not create/update a report.
- Contract expiry filters share contractRegistryFilters with exports. Presence
history exports validate their own dateFrom/dateTo schema; do not silently
broaden unrelated export filters. Keep the original 409 for a reversed travel
reconciliation period after both endpoints pass calendar validation.
- Todo create/patch dueDate, employee-directory hireDateFrom/To (users and
employees routes), holiday-calendar day replacement and the audited employee,
dashboard and AP-operation export date-only fields also use the strict predicate.
Preserve todo null clearing versus omitted-date retention and original permissions.
- Validate every holiday item before entering the replacement transaction: one invalid
date must not delete existing days or write the successful audit. Duplicate dates
retain 409; an empty item list still intentionally clears the calendar.
- Audit-export timestamps and payroll months are separate contracts. Adding calendar
checks does not change AP ledger reversed-range behavior or authorize historical
data edits. Exercise both CSV and XLSX rejection before reads/audit and valid UTC bounds.
- User create/patch and CSV/XLSX hireDate, employee profile/lifecycle/archive
dates, AP ledger queries and dashboard queries also use the strict predicate.
Validate before business reads/writes; preserve feature/permission ordering and
original range responses. Import apply rejects the whole invalid batch (409),
while dry-run still records its original preview audit.
- Read user CSV cells as text before normalization. ExcelJS automatic date coercion
uses local time; converting that Date to UTC can move a valid hireDate to the
preceding day. CSV and XLSX tests must assert exact date values, blank defaults,
preserved textual identifiers and mixed-batch rejection, not only HTTP success.
- Payroll profile effectiveDate, budget creation periods, performance cycle
create/patch dates, attendance correction workDate, attendance sync request
ranges and leave create/edit/preview/list dates follow the same input contract.
Keep payroll YYYY-MM and timestamp/session semantics separate. Legacy
attendance_v1 is permanently disabled: its 409 guard remains authoritative;
never enable the gate just to exercise a schema in tests.
### HR Import Calendar Contract
1. Scope / trigger: `integrations/wecomHrMaster` normalizes external hire dates before department/account mutations. Do not let malformed supplied dates use the missing-date default.
2. Signatures: `normalizeWecomHrProfile({contact, staff, departmentMap})` returns canonical `hireDate` plus boolean `hireDateInvalid`; `runWecomHrMasterSync({prisma, config, mode, sourceProfiles, fetchImpl, ...})` rechecks normalized offline input.
3. Contract: separated/compact calendar dates and positive seconds/milliseconds remain supported. Invalid canonical or expanded-year dates are rejected. Missing dates retain the run's `now`; existing employee updates still omit `hire_date`. No new env, DB or public request field is introduced.
4. Validation/error matrix: invalid date → item `action=skip`, `status=skipped`, `errorCode=invalid_hire_date`, fixed safe message; no department, employee, role, identity or manager access. Run/item audit is retained. Missing required fields and inactive-source guards keep their existing priority.
5. Good/base/bad: `2024/2/29` → `2024-02-29`; blank → original create default; `2026-02-29`/`20260230` → skip, never February overflow or default today. Valid items in a mixed batch continue normally.
6. Tests: `wecomHrMaster.calendar.test.js` exercises the actual run service with mock DB and rejecting fetch; assert exact UTC dates, preserved existing hire date, audit-only skips, dry-run/apply and mixed-batch behavior. This is not real WeCom/PG evidence.
7. Wrong/correct: `hireDate = parse(raw) || null` alone loses invalidity. Preserve `hireDateInvalid` and guard it before `resolveDepartment`; independently validate any directly supplied normalized `hireDate`.
### Shared Read Filter Error Contract
1. Scope / trigger: employee directory, contract-registry filters and presence-history ranges, including their export consumers.
2. Signatures: `parseEmployeeDirectoryFilters`, `parseContractRegistryFilters`, `parseContractRegistryExpiringQuery`, `resolvePresenceHistoryRange`; export `parseQuery(schema, query, reasonCode?)` / `parseBody(schema, body, reasonCode?)` accept an explicit optional registered reason.
3. Contract: retain HTTP 400, `VALID_001`, original flatten/field details and guard order; add only `details.reasonCode`. Shared legacy helpers used by other write/conflict branches keep their contract.
4. Validation/error matrix: employee schema/reversed hire range → `employee_directory.query_invalid` / `hire_date_range_invalid`; contract schema/reversed expiry/days → `contract_registry.query_invalid` / `expiry_date_range_invalid` / `expiring_query_invalid`; history reversed/>31 days → `presence_history.date_range_invalid` / `date_range_too_large`.
5. Good/base/bad: 31 inclusive history days valid; absent history endpoints preserve 7-day default; contract expiring days 1–365 valid, 0/366 invalid. Employee/contract export outer schemas use their domain query reason, including invalid CSV/XLSX format.
6. Tests: actual Fastify negative requests assert exact reasons and legacy details before business queries/success audit; CSV/XLSX and unaffected export paths regress together. Registry/client/fixed message fixture must agree.
7. Wrong/correct: migrating only the shared filter misses outer export validation. Pass its domain reason explicitly; never infer it from ambiguous detail keys. Dynamic helper reasons must use registry validation (`getErrorReason(reasonCode).code`); only remove disappeared AST baseline entries, not still-used legacy helpers.
### Import Files, Announcement Times And Payroll Errors
1. Scope / trigger: `users` CSV/XLSX import; announcement create/patch/acknowledge; payroll maintenance and month/export queries. These errors retain status/details and business guard order.
2. Signatures: `readUserImportPayload(request)` must catch `FST_REQ_FILE_TOO_LARGE` both at `request.file()` and `filePart.toBuffer()`. Announcement/payroll `parseBody(schema, body, reasonCode)` take the operation's explicit reason; generic exports/attendance parsers retain optional reason parameters for unmigrated callers.
3. Contracts: import oversize remains production HTTP 400/VALID_001 with `file=file_too_large`, now adding `user.import_file_too_large`; leave other read exceptions unchanged. CSV/XLSX parsing follows the narrow boundary below. Preserve the 500-row cap and original preview audit. Announcement DATE_TIME_SCHEMA retains its old ISO/date-T-prefix alternatives, then requires a real calendar date prefix and finite parsed Date; nullable/optional, local/offset interpretation and valid 24:00 rollover remain compatible.
4. Validation/error matrix: seven file failures → `user.import_*`; announcement input/id/query/disabled/statement/ack guards → `announcement.*`; payroll month/item/profile/payslip conflicts → `payroll.*`; unified attendance month errors → `attendance.month_*`. Keep original 400/409 and nested amount-index/missing-profile details; fixed client text must never interpolate raw file content, staff lists, or amounts.
5. Good/base/bad: 500 import rows may preview; 501 rejects before business access. Announcement `2024-02-29T23:30:00-12:00` is valid even though UTC is March 1; February 30 or an unparseable T suffix must reject before reads/writes. Payroll/attendance month 00/13 already fail in downstream parsers: preserve that behavior instead of adding contradictory schema rules. Low years 0000–0099 still have legacy Date.UTC mapping and require a separate accepted-year/DB compatibility decision.
6. Tests required: actual multipart dry-run/apply and limit rejection; announcement create/patch across all three dates and signature guards; payroll salary item/profile/generate/edit/issue and P2002 simulation with no successful audit or subsequent mutation. Preserve originals/permission checks and assert exact reason plus legacy fields. P2002 mocks do not prove PostgreSQL uniqueness/rollback. Run bcrypt-heavy import regressions without concurrent frontend builds; do not remove hashing or business assertions to meet a timeout.
7. Wrong/correct: catching only `request.file()` misses stream-size errors from `toBuffer()`. Route-local catch both using the same registered reason. Replacing a domain builder globally can mislabel input vs state conflicts: pass explicit reasons per operation, validate dynamic codes through the registry, and delete a legacy helper only after all its callers migrate. Announcement mutation toasts consume `resolveErrorMessage(error, fallback)` so page callbacks do not erase the registered explanation; no new visual design is needed.
### User Import Parser Failures
- Catch only the in-memory ExcelJS `csv.read` and `xlsx.load` awaits. Known format failures use `user.import_file_invalid`, HTTP 400 / VALID_001, with only fixed `file=invalid_format` plus reasonCode; no filename, cell, sheet, parser message or cause.
- The installed fast-csv/JSZip/saxes versions expose plain Error messages rather than codes. Keep format-specific, anchored message allowlists supported by real malformed-file tests. Different error types, error codes/statuses and unknown messages remain 500; replace parser exceptions with a fresh fixed internal Error so the global logger receives no original message, stack, cause or custom properties. Do not map every exception to invalid input.
- Keep `request.file`, `toBuffer`, worksheet normalization, permission, DB, transaction and hashing outside that catch. Apply's preview reads already happen inside its transaction; do not claim all internal failures occur before transaction entry. Format rejection itself must precede all preview/transaction/business access.
- Test both multipart endpoints with actual malformed CSV, ZIP and XML and the production error-handler callback; verify zero business access and no raw exception logging. Regress valid quoted multiline CSV, XLSX, exact date text/defaults, 500/501 rows, original file guards and real hashing. Unknown failures and parser-like messages thrown by reads/DB/hash must retain their separate classifications. Use only synthetic input; mock transactions do not prove PostgreSQL rollback.
### Payment Account And Invoice Read Errors
@@ -216,6 +302,8 @@ Wrong: treat a notice count or pending-batch aggregate as the complete finance w
Fixed expected text lives in `server/tests/unit/fixtures/error-reason-messages.json`
and is asserted against both registry and client rendering; do not replace
these assertions with broad message regexes or expectations derived at runtime.
Also check registry → fixture completeness for every `audience=user` reason;
iterating fixture entries alone cannot detect a newly registered code omitted from the fixture.
- Authentication explanations must retain the same `auth.code_invalid_or_expired`
reason for missing, inactive, locked and wrong-code accounts; do not introduce
account-enumerating recovery messages or include submitted secrets in errors.
@@ -225,6 +313,12 @@ Wrong: treat a notice count or pending-batch aggregate as the complete finance w
- Workflow input-error migration preserves parsers, permission order and transaction
boundaries. Invalid comments, task identifiers and batch requests must fail before
business transactions; changing error metadata does not authorize state-machine edits.
- Notification list/delivery/channel validation uses notification.* reasons; todo
input/id/query/gate errors use todo.* and the portal gate uses portal.disabled.
Preserve legacy fieldErrors/id/featureFlag/reason and 400/409 statuses. A missing
channel encryption key must direct original-key repair without exposing input
secrets or suggesting a replacement key. Error migration must not send messages,
change channel encryption rules, enable flags or loosen assignment permissions.
- Invoice date-only input must survive UTC date round-trip unchanged. Reject invalid
timestamps and calendar overflow (including non-leap February 29) before updates
or audits; JavaScript Date construction alone is not calendar validation.
@@ -1016,3 +1110,78 @@ if (hasPermission(employee, "expense.view_all")) return reimbursement;
const where = buildReadableReimbursementWhere({ employee, scope: "all", filters });
await canReadReimbursementBusinessFact(prisma, reimbursement, employee.id);
```
## Company Loan Principal And Retired ERP Consumers
- `09-17-company-loan-principal-repayment` adds the explicit `loan_repayment` payment source: one retained repayment/batch/item, multiple allocations to confirmed same-company/lender/CNY loans. Preserve original principal and confirmed opening evidence; Decimal reservations and settlements must not exceed opening outstanding principal. SQL checks/deferred triggers complement locked application validation.
- Loan management requires an explicit finance/finance_mgr role plus loan.manage. Independent review requires active manager/CEO with review permission and excludes maker, submitter and lender OA identity; admin wildcard cannot replace these identities. External lenders require explicit verified no-OA identity. Account changes revoke identity confirmation and invalidate old unpaid snapshots.
- Creation locks lender, sorted loans, then day sequence; it never waits for a previously existing batch. Existing settlement locks batch, global receipt identity, attachments, lender/reviewer, sorted loans. Source maintenance never locks an existing batch. Claims, allocation settlement, batch/item payment facts and audit commit atomically.
- Export/upload marks bank outcome unknown without reducing principal. Unknown/in-transit cannot release reservations. A never-sent cancellation or independently reviewed, current and exactly matching failed-bank evidence may release; preserve the old request. Paid and terminal allocation/history facts are immutable. Accounting handoff is a separate audited reference, not a second payment state.
- Repayment BIFF8 uses encrypted account-name snapshot and purpose 还款/归还借款本金; receipt matching/manual review explicitly supports loan source and shares the reimbursement claim/alias namespace. Real bank/OCR acceptance is not established by synthetic recognition tests.
- Loan evidence uses governed PDF/image bytes, random object keys, bounded content/hash checks and audit-before-download. Finance reads the ledger; reviewers can download only materials in an eligible payment's allocations, not the full ledger.
- ERP-specific API/adapters/jobs/enqueues are retired by `09-17-erp-exclusive-retirement`. Shared outbox/notification/WeCom and historical records remain. New ERP enqueue and direct dispatch reject; pending selection excludes `erp.*`; historical retry returns `integration.erp_retired` without mutating records. Never restore these retired consumers solely to satisfy old success-path tests.
## Purchase draft transaction and response hydration
- Keep purchase draft creation and its audit atomic. Inside the create transaction, select only identifiers needed for the audit; hydrate the original detail relation projection after commit.
- Draft PATCH likewise hydrates the same response projection after commit. Keep FOR UPDATE, ownership/draft guards, master-data and project validation, item replacement, amount calculation, both existing parent updates and audit in their original transactional order. This response is current committed detail, not a relationship snapshot protected by the earlier write lock; never move a read used for a business decision outside the transaction.
- Cover omitted, empty and populated item lists; fail writes/items/audit/commit before any response read. A failed response read after commit is an error response, not a rollback, and must never trigger automatic patch retry. Review each write entry separately: a creation fix does not establish update/submit/withdraw/archive coverage.
- Do not increase transaction deadlines, connection pool size or retries to conceal a measured P2028. Preserve permission/cutover/default fields and audit ordering.
- Creation or audit failure must prevent post-commit hydration. A hydration failure after commit is not a rollback and must not trigger automatic draft creation retry.
- Test ordering and failures with stubs, and separately exercise the actual create/approve/archive flow in the managed synthetic database. Stub rollback assertions do not prove PostgreSQL fault-injection rollback.
## Asset and system-user creation under local load
- Asset creation keeps reference validation, number allocation, asset and optional assignment writes in the original transaction. Return the scalar audit/assignment fields, then hydrate the unchanged detail projection after commit. Preserve the existing post-commit, nonblocking asset audit and its independent failure logger; do not copy purchase's in-transaction audit semantics into this route.
- Failed asset/assignment/commit must not hydrate or emit a successful creation audit. Post-commit detail failure does not roll back the asset and must not retry creation; missing detail follows the existing asset.not_found contract. The detail represents current committed state, not an atomic relationship snapshot from the write.
- Single-user creation performs the unchanged cost12 password hash after existing permission/input/protected-role/reference guards but before opening the account/role/audit transaction. Only server code supplies the internal passwordHash argument; never accept a client-provided hash. Preserve generated credentials, forced password change, unique-conflict handling and post-commit directory sync. Batch imports retain their existing transaction-local hash position.
- Keep original transaction deadlines, password cost, retries and worker count. Test with real hashing for the pre-transaction boundary, plus hash/write/role/audit/commit/read failures and original import/permission flows. Mock transaction ordering is not PostgreSQL rollback or whole-load reliability evidence; retain a fresh complete managed run.
## Reimbursement submit precheck reads
- Resolve the reimbursement entity and ordered item/type projection once within each precheck call, and share only that server-read record between private budget, expense-standard and operational checks. Public helper calls still load their own records. Keep rule/assignee/evidence checks, missing-record failures and per-call freshness; do not accept client snapshots or introduce a cross-request cache.
- Submit retains its FOR UPDATE, aggregate total recomputation, budget/approval/history/audit transaction and original deadline. Reuse the already-read item category for travel-summary validation; a linked travel request still requires that summary. Only the legacy invoice gate needs a whole-form count when required-invoice items exist; the enabled per-item gate remains authoritative.
- Test both budget flag states with actual enabled flag fixtures, preserved financial/timeliness/evidence results, missing/read-failure boundaries, non-reimbursement branches and subsequent-call freshness. Travel allowance fixtures need real travel-summary fields even when testing invoice exemptions. Query reduction is bounded optimization, not proof that an intermittent P2028 or complete-load reliability is resolved.
## Scenario: Versioned Business Numbering
### 1. Scope / Trigger
New in-scope internal numbers use businessNumbering v2 only after an explicitly activated policy. Reimbursement is monthly; PB/JK/HK/CG/PD/automatic PRJ annual; FA and existing EA prefixes permanent. Contracts, employees, external source identifiers and frozen travel stay outside this mechanism.
### 2. Signatures
- `resolveNumberingContext(tx, { version?, now?, readOnly? })` returns the pinned rule version and instant. Only internal callers supply version/now. Config reads use readOnly; mutations require the caller's interactive transaction.
- `issueBusinessNumber(tx, { family, now, timeZone, prefix? })` returns persisted documentNumber, businessDate and serial; `issueDocumentNumber` selects legacy/v2, accepting internal numberingVersion and assetType.
- Tables: business_number_policies (version 2, immutable effective_at), business_number_counters (key + bigint high-water); purchase_requests.numbering_version defaults to immutable 1 for old/unversioned drafts.
- `node server/src/scripts/manage-business-numbering.js --help` documents status/activate/pause/resume. It does not load .env, accepts only explicit timestamp/actor/change reference and requires the operator's compatible-writers assertion; this flag is not automated proof of fleet readiness.
### 3. Contracts
- Migration adds structures, never activates or rewrites business numbers. New purchases pin eligibility at creation but obtain date/number on first submission; historical missing reimbursement numbers explicitly use version 1. Existing numbers, UUIDs, source snapshots and exported bank row identity remain unchanged.
- Policy shared lock `(1003, 0)` precedes policy read; management takes the exclusive lock and waits for in-flight writes. Counters lock namespace1004, family keys 1..8 from BUSINESS_NUMBER_FAMILIES; asset prefixes share key8 but retain separate counters. Keep lender→sorted loans→shared policy→HK counter→repayment→PB counter order. HK and its PB share the same context.
- Scan matching occupied numbers and persistent high-water under the counter lock, then save in the same transaction as the object. Four digits are minimum padding, never a truncation limit. No reuse after object cancellation/deletion; failed uncommitted transactions roll back. Matching manual project numbers reserve high-water in routes and importer without renaming formal codes.
- Policy effective time, old draft version and counter high-water are protected against changes/decreases/deletion by migration triggers. Pause/resume preserves all issued numbers and counters; pinned v2 drafts still issue v2 while policy is paused. Roll back only to a compatible application; do not remove these structures.
### 4. Validation / Error Matrix
- Missing/inactive/future policy → legacy; failed policy access or missing migration/model → error, never a silent legacy fallback.
- Unsupported version/family or invalid prefix → fixed error before writes. Capacity beyond PostgreSQL bigint → fixed exhaustion error, no truncation.
- Default activation time is read after waiting for the exclusive policy lock and compatibility reads; do not capture it in a parameter default before waiting. Existing legacy reimbursement YYYYMM / purchase or inventory YYYY configurations conservatively block activation/resume for a specific coexistence review; status lists only affected types, never silently edits the old rules. Existing same-format source rows are separately handled by the locked maximum.
- Old effective date or repeated activation → rejected; missing compatibility assertion/actor/reference → rejected. Policy update and audit share one transaction.
- Counter decrease/deletion, effective-time rewrite or draft version rewrite → PostgreSQL 23514. Unique violation → complete transaction rollback, never catch-and-retry within the aborted transaction.
### 5. Good / Base / Bad
Good: old PB-20260917-001 remains; new PB-2026-0001 can contain both BX date formats. Base: no activation means legacy behavior. Bad: reformat existing PB because its derived bank customer serial would change, reset a counter, or select a draft's version on submission day.
### 6. Tests Required
`businessNumbering.service.test.js` plus affected create/submit/payment/file routes cover formats, fixed time, timezone, boundaries, overflow, legacy and active policy. `RUN_ISOLATED_NUMBERING_DB_TEST=1 node scripts/qa/test-business-numbering-db.cjs` uses an owned network-none PG, all migrations, real concurrent writes/unique rollback, occupied numbers, import and CLI, immutable history and shared/exclusive locks, then authenticated loan/repayment/payment HTTP regression. Preserve its failures; unit mocks do not prove DB locking. G20 browser release acceptance and production activation remain separate.
### 7. Wrong / Correct
Wrong: `SUBSTRING(code FROM $1)` with an untyped bound offset can resolve PostgreSQL's text-regex overload and miss occupied numbers. Correct: `SUBSTRING(code FROM $1::int)::bigint`, with static allowlisted identifiers and bound regex/prefix values. Keep a real occupied-record integration assertion, not just a stub returning a maximum. Do not use count+1, no-lock MAX+1, or reset old DocumentSequence rows to implement periods.
+154 -3
查看文件
@@ -1,5 +1,30 @@
# Deployment / Trial Run
## Convention: Local Release Candidate Receipt
- `bash scripts/qa/run-release-gate.sh verify-candidate` reuses existing static and managed full-local stages and never calls deployment. The approved R1 local integration requires this receipt in every write entry; actual push/deployment still needs separate authorization.
- `release-gate-result.mjs` owns an exclusive empty directory under `artifacts/release-gate/`, records INCOMPLETE before execution, and derives final results from fixed ordered phases, source/index/tracked and untracked input digests, current policy/toolchain, owned environment/cleanup, G19 full discovery and every local result plus witnesses/acceptance specs. Missing, truncated, linked, changed, interrupted or partial evidence is not PASS.
- `qa-stage-record.mjs` records full-local phases through failure cleanup. Keep plain shell calls under errexit: wrapping compound helpers in `if`/`||` can suppress their internal failures. Preserve the original failing exit code if evidence cleanup also fails.
- Persist actual browser selection separately from discovery: exact derived spec arguments, supplementary arguments, mode, retries, forbid-only and worker count. Ordinary release verification rejects supplementary filters, shards, missing projects, changed budgets, skips, retries, expected failures and inconsistent statistics/time ranges.
- Native environment probes retain separate Chromium/WebKit observations even on launch or cleanup failure. Record the failing stage and allowlisted error category, never raw browser messages, endpoints or paths. Cleanup failures append without replacing the primary cause; every failure still rejects the environment. A Chromium trace cannot diagnose WebKit. A standalone blank-page diagnostic is not a managed gate receipt or evidence of business coverage.
- Prepare owned browser caches with `python3 scripts/qa/copy-browser-cache.py <source> <new-destination> <new-receipt>`. Use an explicitly selected physical cache directory. Preserve internal relative links to regular files; reject absolute, external, broken, cyclic or directory links and special files. Verify source before/after, copied bytes/modes/link structure and independent file inodes; matching expanded bytes alone is insufficient for native library aliases. Never flatten an entire bundle or execute from a historical shared cache. Keep Unix-socket runtime roots short and private; evidence directories need not be runtime directories. Copy receipts confer no release eligibility.
- A dirty development tree may have `localChecksPassed=true`, but returns PARTIAL/exit 2 and `deploymentEligible=false`. Offline `verify` re-reads and hashes evidence and recomputes against the current checkout. Local proof does not discharge the 12 same-candidate public obligations, independent reproduction, or operator approval.
- Existing project-wide skip flags accept only exact 0/1. Each or both require nonempty reason/operator/approval reference, persist affected stages, and end PARTIAL/exit 2. Other release modes reject them before stage side effects. These metadata do not grant authorization; no approval time is invented.
- Validate with pure receipt counterexamples, actual shell/fake-tool invocation, source mutations, and existing deployment lifecycle/safety regressions. Fake tools must intercept every external action. Evidence is auditable, not a cryptographic signature.
- Explicit consumption uses `RELEASE_GATE_RECEIPT_DIR` (absolute or repository-relative, confined beneath `artifacts/release-gate/`); `RELEASE_GATE_ARTIFACT_DIR` only selects new output. Never search for a latest receipt. `release-gate-result.mjs require` re-reads all evidence and current source/policy/toolchain; caller PASS/skip flags do not grant eligibility. The three deployment entries are part of the policy digest.
- `ci-release/all` reuse a supplied eligible receipt or run the verification-only child once, then call production identity bootstrap. `all` retains its extra smoke-local before that verification. Explicit invalid/empty receipts fail without fallback tests. `deploy` and direct wrappers require a receipt and never start full tests themselves. Repeated lower-level assertions only revalidate evidence.
- Pin the returned commit across bootstrap and wrapper preflights with `RELEASE_GATE_EXPECTED_COMMIT`; it is an additional equality constraint, never an override. Match provenance/archived `SOURCE_COMMIT` to it. Recheck before each lower entry and before the first remote write (the mkdir preceding rsync), not merely before container work. Initial rejection must precede SSH/config/provenance-fetch access; do not invoke failure collectors that read real configuration for a rejected publication.
- Both read-only flags accept only exact 0/1. Bootstrap identity-only exits before database lookup; bootstrap/existing preflight-only exits before deploy.sh. Release-runner publication modes and raw deploy.sh reject either read-only flag rather than treating it as write permission or publication success.
- Add boundary checks with synthetic disposable repos, private child HOME/PATH, audited fake config and fail-closed SSH/rsync/curl/git-fetch/Docker tools. Execute actual local entry scripts; remote-body lifecycle tests remain separate. Source mutations must expose an observable violation, not just remove a matching string. Freeze and commit the final reviewed candidate before the single full release run; task-only records and old partial receipts cannot substitute for it.
## Convention: Managed OCR And Concrete Visual Coverage
- After a successfully owned local server start, `run-web-gates.sh` recalculates `PLAYWRIGHT_OCR_MOCK` from that startup's `OCR_PROVIDER=mock` and `JOBS_ENABLED=true`; a caller's flag cannot enable unsupported capability. Each startup appends non-secret provider/job facts to `server-capabilities.jsonl` in the run report. Actual missing-field correction still has to pass; configuration alone is not behavior evidence.
- R108 uses an active synthetic reimbursement submitted through formal APIs, with a valid PDF and explicit item binding. Initialization fails the test rather than skipping it. Preserve the employee approval refusal, current approve/return and no-reject contract, offline upload queue, 390/414 layouts, and independent output paths. An incomplete visual report is not PASS just because findings is empty.
- The existing project visual matrix explicitly lists all 14 current system routes. Unknown wildcards/parameters and unowned system routes fail route-plan preparation. AP uses finance, system/audit uses admin, approval uses manager, and login uses an anonymous context. Require exact target URL plus a page-specific title or workspace anchor; 403/404/login fallback and same-origin API 403/404 are failures, not target page coverage. Mock response controls must declare UTF-8 so Chinese error headings exercise the intended guard.
- Per-route arrival and full audit completion are separate: record completion only after the screenshot and all DOM checks. The approved matrix is 25 PC + 8 mobile pages, grouped into seven role/viewport tests with 180 seconds each and one timed synthetic preparation. Preserve all assertions, screenshots, configured workers and zero retries; only this file's groups remain serial as before. Compare exact planned/completed route-role-viewport identities, reject missing/duplicate/unexpected pages and wrong arrivals, and require all seven actual test statuses passed. Failed or skipped serial successors must leave the report incomplete; unrelated guard tests cannot overwrite it. Matrix coverage does not mean every product page. Live/frozen obligations remain in full discovery; focused passes do not change ordinary eligibility.
## Public OA Boundary
Public OA is quasi-production. Deployments, remote config changes and live data
@@ -169,6 +194,12 @@ rollback cannot reverse migrations or initialization data.
source copy, including a hung inspect; never shorten the production constant.
- Fake binaries and whitelisted PATH/env exist only in test subprocesses. Unknown
commands fail, real Docker/SSH/production credentials remain inaccessible.
- Avoid a cold Node runtime per fake command: a per-test private Unix socket
reuses the same command parser and trace semantics. The isolated Python stdlib
client preserves argv, requested stdin and status; transport failure exits 97
without a real-command fallback. Record timeout before directly executing GNU
timeout, and close all owned connections before removing the temporary fixture.
Keep the original workers, deadlines and full-chain/mutation assertions.
- Run `server/tests/unit/scripts/deployment-db-lifecycle.test.js` in static gates
and retain existing `deployment-safety-contract.test.js` coverage.
@@ -789,6 +820,55 @@ BUILD_SHA=<verified-full-sha> VITE_BUILD_SHA=<matching-short-sha> \
docker compose -f deploy/trialrun/docker-compose.yml build server
```
## Scenario: Managed Local Gates With Synthetic Password Preparation
### 1. Scope / Trigger
- Prepare ordinary full-local or revalidate explicitly selected browser/server contracts in a fresh, run-owned local database. Focused diagnosis has no normal release eligibility; none of these entries activates R1 or authorizes production access.
### 2. Signatures
- Entries: ordinary `bash scripts/qa/run-web-gates.sh full-local`; focused `retained-local` with explicit `QA_RETAINED_SPECS` and optional `QA_RETAINED_GREP`; `retained-server-local` with required `QA_RETAINED_SERVER_SUITE=workflow-actions|server-regressions`. The shared shell helpers are `qa_seed_synthetic_credentials` and `qa_prepare_synthetic_accounts`.
- Account owner: `scripts/qa/prepare-local-e2e-accounts.mjs`; validators `validateAccountPreparation` and `prepareAccounts`.
- Receipt: `synthetic-account-preparation.json`, selection, complete Playwright JSON, managed environment and before/after source/repository snapshots.
### 3. Contracts
- The existing managed namespace, native-online/no-public-egress topology, exclusive PostgreSQL and owner-verified cleanup apply. Accept only existing local spec basenames; no traversal or implicit full-suite selection.
- Seed all eight synthetic roles once with a per-run random initial password and the real must-change flag. Prove the initial browser change-password gate, then serially use official login/change-password endpoints for each role; the old credential must fail and the new public test fixture credential must log in without another change requirement.
- The preparation CLI verifies the managed context, exact loopback HTTP origin and synthetic database prefix before writes. Do not read operator credentials, print auth payloads, clear flags with SQL, alter production seed behavior or reset shared accounts from concurrent login helpers.
- All three entries seed per-run initial credentials and prepare once before business cases. Ordinary full-local qualifies the guest entry after server startup, then prepares before workflow-actions/server regressions; the later job-enabled restart does not reset accounts. Remove initial-password and seed-password environment variables after preparation. Keep full discovery/PWA inputs, fail-fast, zero retries and configured file concurrency. The diagnostic inventory mode remains a separate contract and is not silently migrated.
- Build Vite clients with `NODE_ENV=production` while the isolated application server remains in test mode. No real notifications, bank calls or external OCR. Ordinary full-local and the focused server-regressions profile retain IM_NOTIFIER_ENABLED=true for the existing mock/loopback-failure notifier contract; focused browser/workflow-only profiles keep it false. The fresh seeded database contains no real channels; managed no-egress and disabled real WeCom/email remain required. The managed inner runner fixes `SAMPLE_FILE` to a nonexistent run-owned scratch path so the smoke script generates its synthetic fallback instead of reading a shared temporary sample.
### 4. Validation & Error Matrix
- Missing/invalid spec selection or managed environment, remote origin or invalid database identity -> fail before account preparation.
- Initial change gate missing, change rejected, old credential accepted or fresh login still requiring change -> fail before business cases, including ordinary server regressions. Cleanup still owns this run’s database. Removing preparation must make the command-capture failure test fail.
- Missing final browser JSON, interrupted run, changed source/repository or failed cleanup -> no complete pass. Owner-verified container removal is recorded independently from an interrupted inner database-drop failure.
- Focused pass -> selected contracts only; `normalReleaseEligible=false` remains mandatory. A focused server selection cannot replace full-local; only the two fixed script mappings are accepted, with no shell command interpolation. Ordinary discovery/browser calls use `--forbid-only` and `--retries=0`.
### 5. Good/Base/Bad Cases
- Good: one owned empty database, serial official password lifecycle, then unchanged shared login helpers and complete selected results.
- Base: browser runtime absent -> environment limitation; preserve the original observation and record NOT_RUN.
- Bad: disable must-change, mutate the same user's password from parallel tests, or promote a green filtered run into a full release receipt.
### 6. Tests Required
- `scripts/qa/tests/prepare-local-e2e-accounts.test.mjs`: accepted lifecycle, old-password rejection and invalid origin/database/initial-password behavior; no credential values in receipts.
- Managed browser exercise: first-login change page, eight successful account lifecycle records, at least one full real-auth business case, zero retries and verified cleanup/source/repository invariants.
- Keep PWA production-build command regression and the real A/B upgrade witness separate from mocked page checks.
- Mock API specs must explicitly block service workers in their actual contexts; production workers can bypass page.route. Keep real PWA A/B witnesses enabled and retain their original budgets. A focused pass after a concurrent failure is not proof of the performance root cause.
- Regression fixtures use current contracts: explicit non-travel expense type for a notification-only reimbursement, explicit invoice item association, management scope for employee lifecycle fields, and save/precheck/submit responses observed separately. Keep blocked prechecks rejected and avoid logging raw payloads.
- Synthetic notifier-secret checks use wecom_webhook with a fixed loopback failure address: encrypted storage, redacted return and provider failure are distinct from mock delivery. Never use real channel configuration.
- Retire a duplicate entry only after proving assertion containment and retaining its canonical manifest/runner entry. Preserve original discovery/results. Visual wildcard route tokens must not be reported as coverage of every matching page; record actual routes and unresolved ownership.
- Copy or hash temporary fixture inputs before the owner cleanup can remove them. A successful list/coverage stage followed by a receipt-collection failure must retain both outcomes; discovery alone never confers release eligibility.
### 7. Wrong vs Correct
- Wrong: turn off the seed change-password flag so the shared login helper reaches the portal.
- Correct: retain the flag, change the password once through the official API in the owned pre-test stage, verify both rejection and re-login, then leave authentication unchanged during concurrent tests.
## Scenario: Isolated Authentication Security Gate
### 1. Scope / Trigger
@@ -885,7 +965,8 @@ The independent all-spec owner is `bash scripts/qa/run-web-gates.sh full-local`,
not this route-manifest subset. Full-local prepares temporary PWA A/B production
builds before Playwright collection using the shared PWA fixture helper; all
non-manual spec/test files under the current test directory remain assigned to
full-local, including dedicated-input specs and filenames containing `live`.
full-local discovery, including dedicated-input specs and filenames containing `live`.
The approved G19 contract below partitions execution; public obligations stay mandatory after deployment.
The existing local/live runtime mode restrictions remain intact; ownership is
not authorization to use live accounts or contact production.
@@ -907,13 +988,20 @@ to diagnostics. No boolean environment marker grants release eligibility.
Both full-local entries own the Linux `isolated-online-v1` environment through
`full-local-environment.mjs`: a fresh network namespace, an up internal veth
pair without an external/default route, and an owned offline PostgreSQL container
pair with no external path, and an owned offline PostgreSQL container
connected only through its Unix socket and loopback proxy. No host fallback,
image pull, arbitrary outer wrapper or boolean online assertion is accepted.
Unsupported capabilities are environment failures, not business test failures.
Reports separate inherited, runner-created and browser namespace fingerprints;
live ownership/topology and current policy digests are revalidated between stages
and at completion. The native blank-page probe must observe actual online=true.
and at completion. The only default route is `default dev qaonline scope link`:
no gateway/via/multipath, no IPv6 default, and no other interfaces. Verify both
veth endpoints are in the owned namespace by reciprocal peer names/indexes,
`info_kind=veth`, and no external namespace/master link. This closed link lets
Linux WebKit detect availability without an external path. The native blank-page
probe must observe actual online=true in both Chromium and WebKit; each browser
process must match the owned namespace. Never patch navigator or count browser
network emulation as a kernel observation.
Before business browser tests, a fresh unmocked context opens the real candidate
login page: listeners precede navigation, cookies/storage are empty, the form
appears, no refresh or login request occurs, and native online samples are true.
@@ -1352,6 +1440,32 @@ STORAGE_HOST_DIR=/srv/oa-data/storage DEPLOY_DATA_MODE=existing \
`storage.tar.gz` and a final `manifest.json`. The manifest contains only safe
relative names, byte sizes, SHA-256 hashes, batch time, actual build SHA when
known and actual source image digest when supplied.
- Bind the canonical batch directory to the verified manifest `runId`, never
derive it from `createdAt`: run start and manifest creation can differ by
seconds. A cross-host manifest hash match does not replace checking the
selected directory identity on the actual restore host.
- An observation used to prepare a restore plan must return the observed
`runId` and relative `batchName`, not just a timestamp/hash. Pin the intended
manifest and reject a changed newest canonical candidate; do not skip a
newer symlink or silently fall back to an older matching batch. A successful
directory observation is not execution-time preflight or restore approval.
- A separately approved cross-platform image rebuild pins the source commit
and tree, native target platform and verified local base digest. Export only
the approved tracked paths, reject secrets/unsafe archive entries, and record
source/lock/context hashes plus output `BUILD_SHA` and OCI revision. Build
cache reuse is not a clean dependency install; same source is not the original
image's byte identity. Building alone authorizes no application startup.
- Use an exclusively created attempt receipt before a bounded remote build;
an existing receipt or uncertain timeout stops without automatic retry.
Keep prior images/caches and immutable review bundles. Attach new image/source
observations outside the frozen bundle; they do not clear pending content
review, source-directory binding or execution-time freshness gates.
- Rebinding reviewed recovery code uses distinct `reviewManifestSha256` and
`executionManifestSha256` identities. Verify embedded code/helpers against
the original review manifest and restrict bundle changes to the approved
source/image binding fields before importing the runner. Preserve the full
migration map and historical exception, not merely their counts. Review of
the old code does not imply independent review of a new binding envelope.
- `BUILD_SHA` supplied by the caller takes precedence over local Git fallback.
On a non-Git runtime host, at least `SOURCE_IMAGE_DIGEST=sha256:<64-hex>` is
mandatory. Never label a legacy backup with the commit planned for deployment.
@@ -1390,6 +1504,15 @@ STORAGE_HOST_DIR=/srv/oa-data/storage DEPLOY_DATA_MODE=existing \
Require the expected unique key count, then normalize both `table=count`
collections with `LC_ALL=C sort` (or an equivalent map comparison) before
equality checking; database collation order is not an evidence contract.
- A documented historical missing attachment is not an implicit restore
exception. A separately approved exception must pin record/path identities
and disposition evidence, recheck uniqueness, archive time and absence of
business associations inside the restored database, and prove actual file
absence. Keep every reference in the original strict validator input.
- Report `totalReferences`, `rawMismatches`, `acceptedKnownMissing` and
`unexplainedMismatches` separately. An accepted missing file stays incomplete;
it may allow only the explicitly approved later read-only stage and a
`PARTIAL` outcome, never storage-integrity `PASS`. Preserve earlier `NO_GO`.
### 4. Validation & Error Matrix
@@ -1418,6 +1541,15 @@ STORAGE_HOST_DIR=/srv/oa-data/storage DEPLOY_DATA_MODE=existing \
- Empty, duplicate or missing table-count output -> classify as a verification
execution failure first; do not report production-data drift until stdin was
delivered and every expected key was returned exactly once.
- Historical target identity/archive/association drift, duplicate target,
file substitution or any additional unexplained mismatch -> stop before the
application stage. Never use `archived_at IS NOT NULL` as a blanket exclusion.
- Expired or not-yet-rebound source -> do not render an executable restore
entry; code review and synthetic tests may continue with an explicit binding
gate and a non-executable review bundle.
- Build source/base/platform/resource mismatch -> stop before `docker build`;
a successful build with wrong output revision/platform remains unusable for
restore. A rebuild never extends the source backup's freshness window.
### 5. Good/Base/Bad Cases
@@ -1440,6 +1572,10 @@ STORAGE_HOST_DIR=/srv/oa-data/storage DEPLOY_DATA_MODE=existing \
- Shell syntax checks for all backup/restore scripts.
- Synthetic Docker/PostgreSQL and temporary-storage round trip.
- For a narrowly approved missing-file exception, assert exact identity,
archive time, every association and actual absence against synthetic SQL;
assert new missing files, wrong size/hash, duplicate targets and symlinks
still block, and that raw mismatch counts and `PARTIAL` are preserved.
- Negative assertions for wrong DB confirmation, corrupt DB/tar, non-empty
target and dangerous tar path.
- Validate manifest relative paths, absence of connection secrets and matching
@@ -1451,6 +1587,10 @@ STORAGE_HOST_DIR=/srv/oa-data/storage DEPLOY_DATA_MODE=existing \
the pass result while a changed count still fails.
- Real-instance restore and `/ready` remain separately approved evidence and
must never be fabricated from the local drill.
- For a cross-platform build wrapper, assert exact base/revision/platform,
minimal Dockerfile substitution, secret/path/link rejection, resource
thresholds, failed-preflight zero builds, exclusive receipt/no retry, and
successful image inspection without `docker run` or deletion of old images.
### 7. Wrong vs Correct
@@ -3372,3 +3512,14 @@ derive unit identity from installer -> timer trigger + service Result/exit
-> bind retained diagnostics to the safe failure phase; unavailable clocks stay unavailable
-> partial difference <= min(5% limit, 64 MiB) before calling it similar
```
## G19 layered browser acceptance (approved 2026-09-18)
- `verify-full-local-discovery.mjs` still discovers every non-manual source spec. `full-local-coverage-contract.json` fixes 12 post-deployment obligations and two retained frozen travel chains by file/case identity and source digest; do not edit the old sources to hide failure.
- `full-local-coverage.mjs plan` derives local files/cases from actual full discovery. Missing sources/protections, duplicate identities, changed exception source or an unregistered case in an exception file fail before business execution. Other discovered cases are locally mandatory.
- The ordinary runner uses exact derived file regexes and zero retries/default concurrency; callers cannot narrow it. `verify-local` checks source identity and every actual case, rejecting missing/unknown/duplicate/skipped/retried/expected-failure results even when Playwright returns zero. Failure receipts remain on disk.
- Local browser PASS always carries post-deploy `NOT_RUN_LOCAL`, delivery INCOMPLETE and no release eligibility. Frozen protection PASS means the new boundary assertions passed, never that the old travel chain ran.
- The offline post-deploy validator needs live profile, the same source candidate/deployed candidate, and complete original Playwright results for all public obligations. It does not establish remote identity independently, grant release authority or run network calls. R1 stage/source/environment enforcement remains a separate open task.
- Focused diagnosis stays focused. Keep original failed ordinary runs; later focused passes do not rewrite them. Actual owners and public evidence remain pending until supplied through authorized work.
- Travel is retained/frozen, not retired: current employee/finance mutation permissions and shared payment blocker are protected without restoring access. The managed synthetic runner enables travel sync for existing low-level regressions; do not claim all integration APIs are disabled or that finance managers lack `integration.manage`.
+99 -188
查看文件
@@ -2,276 +2,187 @@
## Source Docs
Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
Use `docs/文档索引.md` and `docs/文档事实源状态矩阵.md` to locate the current fact owner.
## Rules
- Prefer updating existing fact sources, module specs, manuals, evidence docs
or the current Trellis task.
- New documents need a reason and must be registered in both the index and
status matrix.
- Old Issue CSV files are historical audit evidence and should not be rewritten
unless the current Trellis task explicitly calls for a legacy/export update.
- For generated evidence, store report paths and conclusions, not raw secrets,
raw payloads, full account identifiers, invoice originals or confidential
business content.
- Update the existing fact source, module spec, manual or task. A new document needs a distinct purpose and registration in both the docs index and status matrix.
- Keep a verified reusable rule in its owning section; keep run chronology, counts, candidate hashes and unresolved hypotheses in task/evidence. Consolidate repetition without losing unique conditions, exceptions or references. If there is no new reusable lesson, no spec edit is needed; a fixed seven-section layout is not required unless the task/delivery contract requires it.
- Preserve historical Issue CSVs, findings and original receipts. New evidence adds a dated/sourced delta; it does not rewrite the old result. A matching selected-code digest does not establish whole-tree or publication coverage.
- Record relative report references and safe conclusions, not raw payloads, secrets, full accounts, invoice originals or confidential business content. Static validation, runtime behavior, independent review and public/real-data verification remain distinct.
## Scenario: Registered Local Evidence Integrity
### 1. Scope / Trigger
Protect the 14 historical screenshots registered in the archived 07-18
small-team internal-readiness evidence table. Not being an automated snapshot
baseline does not make human-reviewed, digest-registered evidence disposable.
small-team internal-readiness evidence table. Human-reviewed digest-registered
artifacts remain evidence even when automated snapshot tests do not read them.
### 2. Signatures
`node scripts/qa/check-trellis-governance.mjs` calls `validateRegisteredEvidence`.
The code-owned registry supplies document, artifact directory and expected count;
the document's `截图 | SHA-256` table supplies filenames and approved hashes.
Its code-owned registry defines document, artifact directory and expected count;
the document's `截图 | SHA-256` table defines filenames and approved hashes.
### 3. Contracts
Validate regular local files without following symlink artifacts or ancestor
redirects. Preserve original documents/hashes. Before restoring an accidentally
overwritten artifact, preserve the new runtime copy and its digest separately.
Do not remove registered evidence from the index merely because tests never read it.
Historical private restore-runner candidate hashes without a public repository
file mapping are not local-file checks; do not fetch private/production inputs
or claim universal verification of every SHA literal in Trellis documents.
Require regular local files, without artifact symlinks or ancestor redirects.
Preserve the registered documents/hashes. Before restoring an overwritten file,
save the runtime copy and its digest separately. Do not unregister evidence to
make a check pass. Private restore-candidate hashes without a public repository
file mapping are outside this local check; do not fetch private/production inputs
or claim every historical SHA was verified.
### 4. Validation / Error Matrix
Missing document/table/file, changed digest, invalid row/path, duplicate entry,
symlink or reduced registered count is a governance error. A matching table
reports the exact number verified; no silent table-shrink acceptance is allowed.
symlink or reduced registered count fails governance. A match reports the exact
verified count; silently shrinking the table is not an acceptable repair.
### 5. Good / Base / Bad
Good: verify 14/14 archived hashes while new screenshots go to isolated run roots.
Base: local governance verifies registered public artifacts, with scope explicit.
Bad: ignore all output paths, silently replace archived hashes, or call unverifiable
private historical candidates locally verified.
Verify the registered 14/14 separately from new isolated screenshots. Never
replace approved hashes with runtime outputs or claim unmapped private evidence
was locally checked.
### 6. Tests Required
`node --test scripts/qa/tests/registered-evidence.test.mjs` covers intact evidence,
overwrites, missing files/tables/rows, duplicate rows, unsafe paths and symlinks.
Visual path and full-repository snapshot tests separately prevent the producer
from overwriting history and detect tracked mutations even when already dirty.
`node --test scripts/qa/tests/registered-evidence.test.mjs`: intact evidence,
overwrite/missing document/table/file/row, duplicate rows, unsafe paths and
symlinks. Visual-output isolation and repository snapshot tests separately check
the producer and tracked mutations, including an already dirty checkout.
### 7. Wrong / Correct
Wrong: selected source digest unchanged, therefore the worktree is unchanged.
Correct: report selected-source and repository/index facts separately; tracked
output remains subject to the same whole-repository integrity requirements.
Report selected-source, whole-repository and index integrity separately; an
unchanged selected-source digest does not prove the worktree is unchanged.
## Scenario: Finding Lifecycle And Audit Closeout
### 1. Scope / Trigger
- Trigger: a review, audit or trial-run task records findings and later code,
deployment or external evidence changes their current state.
- Applies to Trellis PRDs, review reports, finding ledgers and management
summaries.
Use when later code, deployment or external evidence changes a recorded review,
audit or trial-run finding's current disposition.
### 2. Signatures
- Canonical ledger fields: `findingId`, `dimension`, `discoverySeverity`,
`confidence`, `discoveryStatus`, `remediationStatus`, `deploymentStatus`,
`externalVerificationStatus`, `evidenceRef`, `businessImpact`,
`controlObjective`, `safeVerification`, `followUpWorkPackage`.
- Delta fields: `discoveryCommit`, `reviewedCommit`, changed-commit list,
validation command, result and explicit environment boundary.
Canonical ledger fields: `findingId`, `dimension`, `discoverySeverity`,
`confidence`, `discoveryStatus`, `remediationStatus`, `deploymentStatus`,
`externalVerificationStatus`, `evidenceRef`, `businessImpact`, `controlObjective`,
`safeVerification`, `followUpWorkPackage`. A delta records `discoveryCommit`,
`reviewedCommit`, changed commits, command/result and environment boundary.
### 3. Contracts
- Preserve the discovery baseline, discovery severity and discovery status.
Later remediation must add a delta; it must not rewrite history as if the
finding never existed.
- Record remediation, deployment and external-behavior verification as three
independent states. A local test pass does not prove deployment, and a
deployed build marker does not prove role-specific or real-device behavior.
- A closeout task must have one canonical finding ledger. Component reports
may retain their original wording, but duplicate IDs and reclassified items
must point to the canonical ledger.
- When HEAD moves after discovery, record the exact old and new commits,
classify the delta and rerun the closeout gates. Do not relabel an old report
as evidence for the new HEAD.
- Use repo-relative evidence references and synthetic fixtures. Unknown or
unauthorized deployment, public, production-data and real-device checks stay
explicit as pending or externally gated.
Preserve discovery baseline/severity/status. Maintain one canonical finding
ledger per closeout; duplicate or reclassified component findings retain source
references and map to exactly one canonical entry. Record remediation,
deployment and external behavior independently: local PASS does not establish
deployment, and a build marker does not establish role/device behavior.
When HEAD changes, identify old/new commits, classify the delta and rerun the
applicable closeout gates; never relabel old reports as evidence for new inputs.
Use relative evidence refs and synthetic fixtures. Unknown or unauthorized
production/public/real-device checks remain explicitly pending or externally gated.
### 4. Validation & Error Matrix
- Local fix tested, not deployed -> remediation may pass; deployment remains
pending and external behavior remains pending when applicable.
- Finding open with no fix artifact -> deployment is `NO_FIX_TO_DEPLOY`, not
`DEPLOYMENT_PENDING`.
- Static/tooling-only finding -> deployment may be `NOT_APPLICABLE`; the
remediation state still records whether the contract itself is fixed.
- Duplicate component finding -> preserve its source reference and map it to
exactly one canonical ledger entry.
| Condition | Recorded state |
| --- | --- |
| Local fix verified, not deployed | Remediation may pass; deployment and applicable external behavior remain pending |
| Open finding with no fix artifact | `NO_FIX_TO_DEPLOY`, not `DEPLOYMENT_PENDING` |
| Static/tooling-only finding | Deployment may be `NOT_APPLICABLE`; record remediation independently |
| External verification not required | `NOT_REQUIRED` only where that verification is inapplicable |
| Duplicate component finding | Source retained; exactly one canonical ledger mapping |
### 5. Good/Base/Bad Cases
- Good: a historical P0 remains P0/confirmed while local remediation passes,
deployment is pending and external behavior is pending.
- Base: an open local-only QA finding uses `NO_FIX_TO_DEPLOY`,
`NOT_APPLICABLE` deployment or `NOT_REQUIRED` external verification as
appropriate.
- Bad: rename the discovery status to `RESOLVED` and omit whether the fix was
deployed or observed externally.
A confirmed historical P0 stays P0/confirmed after local remediation. Do not
replace discovery status with `RESOLVED` or a single `resolved` field that hides
deployment or observation.
### 6. Tests Required
- Task validation and Trellis governance.
- Sensitive-content scan and document registry audit.
- `git diff --check` plus a manual check that the recorded HEAD matches the
evidence-producing checkout.
Task validation, Trellis governance/sensitive scan, document registry audit and
`git diff --check`; verify that the recorded HEAD identifies the evidence-producing
checkout. Select additional checks from the changed contract.
### 7. Wrong vs Correct
#### Wrong
```json
{"findingId":"F-001","status":"resolved"}
```
`{"findingId":"F-001","status":"resolved"}` loses the discovery and verification states.
#### Correct
```json
{
"findingId": "F-001",
"discoverySeverity": "P0",
"discoveryStatus": "CONFIRMED",
"remediationStatus": "LOCAL_FIX_VERIFIED",
"deploymentStatus": "DEPLOYMENT_PENDING",
"externalVerificationStatus": "EXTERNAL_BEHAVIOR_PENDING"
}
```
Keep `discoverySeverity=P0`, `discoveryStatus=CONFIRMED`,
`remediationStatus=LOCAL_FIX_VERIFIED`, `deploymentStatus=DEPLOYMENT_PENDING`,
`externalVerificationStatus=EXTERNAL_BEHAVIOR_PENDING` when those facts apply.
## Scenario: QA Readiness Report Summaries
### 1. Scope / Trigger
- Trigger: QA shell runners aggregate child reports for launch, patrol,
smoke or readiness gates.
- Applies to generated `summary.json`, step JSON and privacy findings under
`artifacts/qa-flow/` or Trellis task research directories.
Shell runners aggregating child launch/patrol/smoke/readiness reports into
`summary.json`, step JSON or privacy findings under `artifacts/qa-flow/` or task research.
### 2. Signatures
- Parent shell runner writes `summary.json`.
- Child runner summary input: `<child-report-dir>/summary.json`.
- Step summary input/output: `<step-name>.step.json`.
Parent output: `summary.json`; child input: `<child-report-dir>/summary.json`;
step input/output: `<step-name>.step.json`.
### 3. Contracts
- Use repo-relative `reportRef` / `logRef` when a path is inside the repo.
- Do not write absolute local workspace paths into `summary.json`.
- Parent summaries must include child report status and child finding counts.
- Live-readiness summaries must distinguish the local `sourceCommit` from the
public `observedBuildSha`; a missing or mismatched public build marker cannot
produce a clean pass.
- Parent `findingCount` must become non-zero when any child summary reports
`PASS_WITH_FINDINGS`, `SKIP`, `PARTIAL`, `PAUSED`, `MISSING`, blocker-like
status or child `findingCount > 0`.
- Machine-readable reports may include status, count, mode, conclusion,
boolean operational flags and relative report refs.
- Machine-readable reports must not include amounts, prices, costs, balances,
bank details, invoice values, full accounts, tokens, secrets, raw payloads,
raw form values or full local paths.
- Credential/readiness presence reports must use safe business labels, not raw
env key names. For example, emit `adminCredentialReady: true` instead of a
field named after a credential-value environment variable.
- `check-pending-ocr.mjs` must reuse the adapter-backed Prisma client factory
and close both Prisma and its external PostgreSQL pool.
Use repo-relative `reportRef`/`logRef` for repository paths. Include each child's
status and finding count; any child finding or non-clean status makes parent
`findingCount` non-zero. Live readiness records local `sourceCommit` separately
from public `observedBuildSha`; absent/mismatched public identity cannot yield
a clean pass attributed to local HEAD.
Allow status/count/mode/conclusion, operational booleans and relative refs.
Exclude amounts/prices/costs/balances, bank/invoice details, full accounts,
tokens/secrets, raw payload/form values and absolute local paths. Presence
reports use safe labels such as `adminCredentialReady`, never secret-like raw
environment-key field names, even with boolean values.
`check-pending-ocr.mjs` reuses the adapter-backed Prisma client factory and
closes both Prisma and its external PostgreSQL pool.
### 4. Validation & Error Matrix
- Child `PASS` + `findingCount=0` -> parent may stay pass.
- Child `PASS_WITH_FINDINGS` -> parent records a finding.
- Child `SKIP` / `PARTIAL` / `PAUSED` / `MISSING` -> parent records
`manual_or_config`.
- Child `FAIL` / `BLOCKED` / `ERROR` -> parent records a finding or blocker
according to whether the parent step is required.
- Privacy smoke match -> write redacted finding metadata and make final
summary reflect the failure.
- Secret-like field names in generated reports -> privacy finding, even when
the value is only a boolean presence flag.
- Public `/ready` has no usable build SHA -> `manual_or_config` finding.
- Expected and observed public SHA differ -> `manual_or_config` finding or a
deployment blocker; never a clean pass attributed to local HEAD.
| Child/observation | Parent behavior |
| --- | --- |
| `PASS` and `findingCount=0` | May remain pass |
| `PASS_WITH_FINDINGS` or any child `findingCount>0` | Record a finding |
| `SKIP`, `PARTIAL`, `PAUSED`, `MISSING` | Record `manual_or_config` |
| `FAIL`, `BLOCKED`, `ERROR` or blocker-like status | Record finding/blocker according to whether the step is required |
| Privacy smoke match or secret-like report field | Redacted finding metadata; final summary reflects failure |
| Missing/unusable public `/ready` build SHA | `manual_or_config` finding |
| Expected/observed public SHA mismatch | `manual_or_config` or deployment blocker, never clean pass |
### 5. Good/Base/Bad Cases
- Good: `summary.json` contains `reportRef`, `childReports.<name>.status`,
`childReports.<name>.findingCount`, `sourceCommit`, `observedBuildSha` and no
absolute local path.
- Base: Missing runtime credentials produce `PASS_WITH_FINDINGS` or
`manual_or_config`, not a clean pass.
- Bad: Parent command exits 0 for a child `PASS_WITH_FINDINGS` and the parent
summary still says clean pass.
A parent report exposes child status/count and separate source/public identities.
Missing credentials produce `PASS_WITH_FINDINGS` or `manual_or_config`/blocking,
not clean readiness; exit 0 alone cannot erase a child's findings.
### 6. Tests Required
- `bash -n <runner>.sh`
- Run the parent runner in a missing-credential scenario and assert:
- parent status is `PASS_WITH_FINDINGS` or blocked, not clean pass;
- child finding count is visible in parent summary;
- privacy step is present in parent summary;
- `summary.json` has no full workspace path or business amount fields.
- For live/readiness env reports, assert the JSON contains safe readiness
labels and does not contain raw secret-like env key names.
- Run the matching launch/readiness aggregator when it consumes the report.
- Build a synthetic 40-character SHA and assert the public bundle contains only
its 12-character marker; use a local HTTP stub to assert readiness records and
matches the observed marker.
- `bash -n <runner>.sh`; exercise the parent/consuming aggregator with missing credentials. Assert parent `PASS_WITH_FINDINGS` or blocked, visible child count and privacy step, and absence of full paths/business amount fields.
- Assert safe readiness labels and no raw secret-like environment-key fields.
- Use a synthetic 40-character SHA to assert the public bundle exposes only its 12-character marker; a local HTTP stub verifies readiness reads and compares that marker.
### 7. Wrong vs Correct
#### Wrong
```json
{
"status": "PASS_OA_REAL_USE_DAILY_READY",
"reportDir": "/home/user/project/artifacts/qa-flow/run",
"childReports": {}
}
```
```json
{
"liveSmoke": {
"RAW_CREDENTIAL_ENV_KEY": true
}
}
```
A clean parent status with an empty `childReports` hides a child's findings;
a raw credential environment-key name remains unsuitable even with value `true`.
#### Correct
```json
{
"status": "PASS_WITH_FINDINGS",
"sourceCommit": "abcdef1234567890",
"observedBuildSha": "abcdef123456",
"buildShaMatches": true,
"reportRef": "artifacts/qa-flow/run",
"childReports": {
"oaOnlySmokeReadiness": {
"status": "PASS_WITH_FINDINGS",
"reportRef": "artifacts/qa-flow/run/oa-only-smoke-readiness/summary.json",
"findingCount": 7
}
}
}
```
```json
{
"liveSmoke": {
"adminCredentialReady": true
}
}
```
A `PASS_WITH_FINDINGS` summary records `sourceCommit`, `observedBuildSha`,
`buildShaMatches`, relative `reportRef`, and `childReports.<name>` with its
`status`, relative `reportRef` and `findingCount`. Credential presence uses
`liveSmoke.adminCredentialReady`, not a raw credential key.
@@ -1,5 +1,13 @@
# Frontend React / Vite / Design
## Convention: Invoice Detail Selection Has One URL State Source
- `InvoicePoolPage` reads its selected invoice from `useSearchParams().get("id")`. Upload success, list detail selection and return-to-list update that parameter through the router's functional setter, preserving other parameters and the existing replace semantics.
- Do not mirror the ID into `useState` and synchronize both directions with effects depending on both values. An upload changes one side first; opposing effects then exchange old values, repeatedly unmounting the detail and making manual correction unusable. A visible OCR banner alone does not prove a usable detail.
- Keep OCR polling, form edits and invoice permissions unchanged. The management correction case uses a finance identity with `invoice.manage`; ordinary employee reimbursement uploads remain separate coverage.
- The retained `invoice-ocr-flow.spec.ts` correction case must execute in the managed mock/jobs profile: missing fields reject premature availability, correcting them makes the invoice available without reload or forced click, and returning to the list then reopening preserves the result and selected URL. A conditional skip cannot serve as this regression's evidence.
## Receipt Verification Dialog
- When readiness is unmet and an active receipt has a system/key/snapshot failure,
@@ -318,6 +326,7 @@ test.beforeEach(async ({ page }) => {
- Standalone runner: `bash scripts/qa/run-pwa-cross-version-upgrade.sh`.
- Full-suite owner: `bash scripts/qa/run-web-gates.sh full-local`; both runners use `scripts/qa/pwa-cross-version-fixture.sh` to prepare the same temporary A/B inputs. Missing inputs remain a collection error, never a skip or implicit exclusion.
- Build inputs: two distinct 12-character synthetic `VITE_BUILD_SHA` values, written to temporary outDirs rather than `client/dist`.
- Every Vite production-build subprocess sets `NODE_ENV=production` explicitly, including A/B fixtures and the managed client build. The application server may remain in `NODE_ENV=test`; inheriting it into Vite can emit `import.meta.env.DEV=true` and suppress real Service Worker registration. Keep the application registration guard and upgrade assertions intact.
- Screenshot artifacts must not overwrite registered historical evidence. The internal-readiness and finance-wave4 specs use `QA_VISUAL_OUTPUT_DIR` as a root and create unique per-spec directories with `mkdtempSync`; standalone defaults also use fresh ignored directories, never the historical 07-18 directory. These are output-path changes, not authentication or assertion changes. Both discovery and full-browser runners supply the current report's visual root.
- Full-local's global environment is runner-owned and natively online without public egress. Its separate unmocked fresh-context login probe requires a visible form, zero refresh requests and independently recorded native online samples. A visible form after refresh 401 is not equivalent evidence. Do not instrument AuthContext or override navigator.onLine for this check; explicit offline business contexts remain legitimate. Environment probes are not additional business cases, and invalid environment observations cannot be promoted into a product failure inventory.
- Browser contract: `serviceWorkers: "allow"`, one dedicated random-port loopback HTTP origin and one BrowserContext; the spec declares serial execution. The standalone runner keeps one worker; full-local retains its configured file-level concurrency and explicitly passes zero retries even under CI. Do not serialize the whole suite without evidence of interference and separate approval.
@@ -334,6 +343,7 @@ test.beforeEach(async ({ page }) => {
### 4. Validation & Error Matrix
- Build-command capture tests must prove both A/B subprocesses receive `NODE_ENV=production`, even when the parent is a test server. A static command check does not replace the real same-profile browser upgrade witness.
- A build artifacts are not distinct -> `PAUSED`; the harness cannot prove an upgrade.
- A controller or precache is absent -> `PAUSED`; B would be a fresh-install observation.
- A is established and B does not converge without a prohibited action -> `PRODUCT_FINDING`; open a separate PWA product task before changing runtime code.
@@ -2416,11 +2426,11 @@ button text squeeze.
- Event name: `hyb-oa:pwa-update-ready`.
- Event detail: `{ applyUpdate?: () => void | Promise<void> }`.
- Route predicate: auth input pages must be checked by pathname before calling
`updateServiceWorker(true)`.
the captured controller-update refresh handler.
### 3. Contracts
- Auth input pages must not auto-reload when `onNeedRefresh()` fires.
- Auth input pages must not auto-reload when a replacement native controller activates.
- Auth input pages must show an explicit update prompt with a user-triggered
refresh action.
- Non-auth pages may keep automatic service-worker update behavior when the
@@ -2469,15 +2479,16 @@ button text squeeze.
#### Wrong
```ts
onNeedRefresh() {
void updateServiceWorker?.(true);
}
serviceWorker.register("/sw.js").then(() => {
// Too late: navigation may have installed and activated the worker already.
serviceWorker.addEventListener("controllerchange", reload);
});
```
#### Correct
```ts
onNeedRefresh() {
function onActivatedReplacementController(applyUpdate: () => void) {
if (isAuthInputPath(window.location.pathname)) {
window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate } }));
return;
@@ -2983,3 +2994,13 @@ authorization and independent workbook parsing tests separate from browser mocks
Wrong: a visible action or a generated file proves money was paid.
Correct: file generation, bank execution and OA payment registration are distinct
events. Synthetic browser/backend/container checks do not prove real bank acceptance.
## Native PWA controller ownership (09-18 local concurrency fix)
- Capture the current controller and subscribe to native `controllerchange` before asynchronous `register()`. Use one registration owner (`injectRegister: false`); do not combine generated registration with application reload ownership.
- Wait for the observed controller to activate and verify it is still current. Ignore redundant/superseded/duplicate events. Initial installation dispatches offline-ready without reload; an existing controlled page applies a replacement once.
- On auth input routes, dispatch the existing update-ready event and retain typed input until explicit user refresh. Background-to-foreground build freshness keeps its existing user prompt.
- Controller update clears only app API/static runtime caches. Workbox owns precache revisions. Explicit logout/manual refresh can retain the broader existing cache-clear helper.
- Failure witnesses record observations or null, not invented default false/zero. Build B evidence requires completed performance entries for B-only resources plus B-only cache entries; SPA navigation events are not a document reload count.
- Validate native lifecycle unit cases, full auth UX and the same-profile A/B browser witness. Old running client code cannot be retroactively fixed; first rollout still needs freshness/refresh verification.
+26 -16
查看文件
@@ -1,25 +1,35 @@
# hyb-oa Project Specs
These specs are the Trellis entry point for hyb-oa. They summarize stable
development rules and point back to the current Trellis task and `docs/`
fact sources.
Stable development contracts live here; current requirements and status live in
the Trellis task, while `docs/` remains the business fact source.
## Read Order
1. `project-governance.md`
2. The domain file matching the task.
3. The current `.trellis/tasks/<task>/prd.md` and `docs/文档索引.md`.
1. Start with the current PRD and latest implementation status. Resolve a stale/missing pointer with `task.py current --source`; inspect related tasks before creating one.
2. Reuse governance already loaded from AGENTS/workflow; consult `project-governance.md` for the relevant operation.
3. Select the affected domain below. Use `docs/文档索引.md` only to locate unresolved business facts.
## Domain Specs
| Spec | Purpose |
| Spec | Read when touching |
| --- | --- |
| `project-governance.md` | Official Trellis task contract, approvals, commit rules |
| `backend-fastify-prisma.md` | Server implementation and migration boundaries |
| `frontend-react-vite-design.md` | React/Vite UI and design-source rules |
| `approval-workflow.md` | OA approval/workflow/self-approval boundaries |
| `reimbursement-invoice-ocr.md` | Reimbursement, invoice, OCR and attachment rules |
| `wecom-boundary.md` | Enterprise WeChat ownership and API/write gates |
| `deployment-trialrun.md` | Public deployment, smoke, live-readiness and rollback gates |
| `docs-and-evidence.md` | Documentation reuse, evidence, Trellis task and status matrix |
| `security-privacy.md` | Privacy, secret handling and sensitive-data exclusions |
| `project-governance.md` | Task/approval rules; Trellis Project Upgrade Compatibility; Codex Inline Context Manifests |
| `backend-fastify-prisma.md` | Server APIs, transactions, migrations and data contracts |
| `frontend-react-vite-design.md` | React behavior, page permissions and design-source rules |
| `approval-workflow.md` | Approval states, self-approval and payment handoffs |
| `reimbursement-invoice-ocr.md` | Reimbursement, invoice, OCR and attachment handling |
| `wecom-boundary.md` | Enterprise WeChat ownership, APIs and write gates |
| `deployment-trialrun.md` | The specific candidate gate, deployment or recovery operation |
| `docs-and-evidence.md` | Evidence integrity, reporting and closeout |
| `security-privacy.md` | Authentication, sensitive-data access and privacy boundaries |
## Large Specs And Reusable Lessons
Locate sections with `rg -n '^#{1,3} ' .trellis/spec/hyb-oa/<file>.md`, then read
general rules, complete applicable scenarios and required references. Expand
for shared/cross-layer contracts; no search hit does not prove no rule applies.
Reuse unchanged sections already loaded instead of reading all domains.
For maintaining lessons and distinguishing evidence states, use
[Docs And Evidence rules](./docs-and-evidence.md#rules). A model upgrade does
not waive authorization, privacy, validation or independent review.
+9 -1
查看文件
@@ -80,6 +80,13 @@ tasks after TRELLIS2.
acceptance decisions require approval, not every intermediate action.
- Read the current phase and affected-layer contracts on demand. Reuse
unchanged context; do not require every document or thinking guide per turn.
- Resolve task identity with `task.py current --source` before broad session
dumps. A missing pointer does not authorize guessing another session's task
or creating a duplicate; inspect directly related task contracts.
- Use the package index and heading search for large specs, then read the
applicable general rules and complete scenarios. Do not treat a truncated
search excerpt as full contract coverage. Keep skills as small entry points,
with one owner for phase routing and reusable lessons.
- Prefer an available native plan tool. If unavailable, keep the checklist
in the current task implementation notes rather than stopping or creating
another planning system.
@@ -277,7 +284,8 @@ records the adopted scope and first reversible navigation reduction.
- Compile every Python file under `.trellis/scripts` and `.codex/hooks`.
- Exercise task validate/current/list and run Trellis governance.
- Archive the final active task and assert Trellis governance still passes with
zero active task directories.
zero active task directories in a disposable synthetic project only. This
runtime check does not authorize archiving a real task.
- Record pre-existing backup directory names before update; confirm no `.new`,
newly generated backup, bytecode or out-of-scope platform assets are tracked
or left for the sensitive scanner. Do not interpret a historical backup as