沉淀生产收口规约与清理证据(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:18:59 +08:00
父节点 5fb91803fa
当前提交 79bf6cff83
共修改 12 个文件,包含 784 行新增和 3 行删除
@@ -169,3 +169,128 @@ if (acknowledgementCount > 0) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409 });
}
```
## Scenario: Safe Development Seed
### 1. Scope / Trigger
- Trigger: changing or running `server/src/scripts/seed-dev.js`, its safety
guard, development account defaults or the `seed:dev` npm command.
### 2. Signatures
- Command: `SEED_DEV_ALLOW=1 SEED_DEV_DATABASE_CONFIRM=<db> npm run seed:dev`.
- Optional reset: `SEED_DEV_RESET_PASSWORDS=1`.
- Target: `DATABASE_URL` parsed with the same `pg` semantics used by Prisma's
PostgreSQL adapter.
### 3. Contracts
- `NODE_ENV=production` is permanently refused.
- The explicit allow switch and exact database-name confirmation are both
required.
- The effective PostgreSQL host, including query-string overrides, must be
loopback; checking only the URI authority is insufficient.
- Existing passwords are preserved by default. Newly created accounts and
explicitly rotated accounts set `must_change_password=true`.
- The guard and seed logs never print credentials, complete targets or account
passwords.
### 4. Validation & Error Matrix
- Production -> `production_forbidden`.
- Missing allow switch -> `explicit_allow_required`.
- Invalid/non-PostgreSQL URL -> `database_url_invalid` or
`database_protocol_invalid`.
- Effective non-loopback host, including `?host=...` ->
`database_host_not_local`.
- Missing/mismatched confirmation -> `database_confirmation_mismatch`.
### 5. Good/Base/Bad Cases
- Good: loopback test database, exact confirmation, new accounts require a
password change.
- Base: rerun without reset preserves existing password hashes.
- Bad: accept `postgresql://[email protected]/db?host=remote.internal` because only
`URL.hostname` was checked.
### 6. Tests Required
- Pure guard tests for production, missing allow, remote authority, host-query
override, exact confirmation and redacted errors.
- Unit tests for existing-password preservation, explicit rotation and new
account first-login change.
- Do not validate refusal paths by connecting to a real database.
### 7. Wrong vs Correct
#### Wrong
```js
if (new URL(env.DATABASE_URL).hostname === "127.0.0.1") allowSeed();
```
#### Correct
```js
const client = new Client({ connectionString: env.DATABASE_URL });
if (!LOOPBACK_HOSTS.has(client.host)) refuse("database_host_not_local");
```
## Scenario: Database-Free App Contract Tests
### 1. Scope / Trigger
- Trigger: unit tests call the production `buildApp()` factory to verify health,
readiness, static serving or other app-level contracts in CI without a DB.
### 2. Signatures
- Production: `buildApp()`.
- Test seam: `buildApp({ prismaPlugin, startJobs })`.
### 3. Contracts
- Omitted dependencies always use the production Prisma plugin and job starter.
- Unit tests inject a `fastify-plugin`-wrapped Prisma decorator and a no-op job
starter; they must not rely on ESM `vi.mock` intercepting a CJS `require`.
- App-level test doubles expose only methods used by the tested requests.
- `DATABASE_URL='' JWT_SECRET=''` must not make isolated app tests access a
developer `.env` or local PostgreSQL instance.
### 4. Validation & Error Matrix
- Production call with missing DB target -> normal startup refusal.
- Test call with injected Prisma plugin -> no production client construction.
- Test double missing a method exercised by the request -> focused unit failure.
- Injected dependencies leaking into `buildApp()` callers without options ->
contract regression.
### 5. Good/Base/Bad Cases
- Good: health/static tests inject Prisma and pass with empty DB/JWT process
variables after setting their own test JWT.
- Base: API/Worker call `buildApp()` without options and retain production wiring.
- Bad: a hoisted `vi.mock("../prismaClient")` appears green only because a local
`.env` database is reachable.
### 6. Tests Required
- Run health and static tests with `DATABASE_URL='' JWT_SECRET=''`.
- Run the full `scripts/check-minimal.sh` under the same empty environment.
- Keep the production default path covered by normal startup/smoke checks.
### 7. Wrong vs Correct
#### Wrong
```js
vi.mock("../../src/prismaClient", () => ({ createPrismaClient: fake }));
await buildApp(); // CJS plugin may still construct the real client
```
#### Correct
```js
await buildApp({ prismaPlugin: testPrismaPlugin, startJobs: noOpStartJobs });
```
@@ -263,3 +263,87 @@ const REQUIRED_MAIN_NAV_KEYS = [
const REQUIRED_BACKUP_NAV_KEYS = ["weeklyReports", "announcements"];
const REQUIRED_ADMIN_NAV_KEYS = ["systemSettings"];
```
## Scenario: Immutable Deployment Provenance And Dual Build SHA
### 1. Scope / Trigger
- Trigger: changing the trialrun deployment entry, Docker build, Compose
wiring, `/health` or `/ready`, public bundle scanning or live readiness.
### 2. Signatures
- Provenance gate: `node deploy/trialrun/check-source-provenance.mjs`.
- Standard release: `SYNC_ENV=0 bash deploy/trialrun/deploy.sh`.
- Build/runtime inputs: `BUILD_SHA=<full-commit>` and
`VITE_BUILD_SHA=<12-char-commit>`.
- Runtime outputs: `/health.data.buildSha`, `/ready.data.buildSha` and the
frontend `html[data-build-sha]` marker.
### 3. Contracts
- Deployment rejects tracked and untracked dirty files before synchronization.
- By default HEAD must be contained in its upstream. The explicit unpublished
override is for controlled offline recovery and never relaxes clean-tree.
- Standard deployment refreshes the configured upstream remote before the
ancestor check; a stale local remote-tracking ref is not publication proof.
- Unpublished override requires an allowlisted `DEPLOY_UNPUBLISHED_REASON` and
must emit an `UNPUBLISHED-OVERRIDE` audit line.
- Synchronization comes from `git archive <verified-commit>`, not the working
tree.
- Full `BUILD_SHA` is injected at build and runtime; `VITE_BUILD_SHA` is
normalized before bundling so only 12 characters become public.
- Neither build key is persisted in `deploy/trialrun/.env`; direct Compose use
must explicitly provide both and must not publish `unknown`.
- Deployment compares public `/ready` and the public bundle marker with the
verified commit before reporting success.
### 4. Validation & Error Matrix
- Dirty tree -> `dirty_worktree`, deployment stops.
- No upstream or commit absent from upstream -> stop unless the explicit
unpublished override is approved.
- Upstream refresh fails -> stop in standard mode; override mode requires an
allowlisted reason and records `remoteVerified=false`.
- Missing runtime/build SHA input -> Compose interpolation fails.
- `/ready` SHA mismatch, bundle marker missing or full SHA exposed -> deployment
fails.
- Standalone readiness missing/mismatching observed SHA -> non-clean finding.
### 5. Good/Base/Bad Cases
- Good: clean published commit -> Git archive -> dual-SHA build -> matching
backend and public bundle checks.
- Base: local development build uses `unknown` but is never represented as a
release.
- Bad: rsync the current working tree or let `.env` override the image's SHA
with `unknown`.
### 6. Tests Required
- Temp-repo provenance tests for dirty, missing upstream, published and
unpublished-override cases, including a stale remote-tracking ref after a
forced remote update.
- `node deploy/trialrun/validate-env-contract.js` and Compose config with both
SHA inputs.
- Synthetic 40-character build: bundle contains the matching 12-character SHA
and not the full value.
- Local-stub readiness test for expected/observed SHA propagation.
- Public deployment still requires separate approval and post-deploy checks.
### 7. Wrong vs Correct
#### Wrong
```bash
rsync -az --delete ./ remote:/app
docker compose up -d --build server
```
#### Correct
```bash
node deploy/trialrun/check-source-provenance.mjs
BUILD_SHA=<verified-full-sha> VITE_BUILD_SHA=<matching-short-sha> \
docker compose -f deploy/trialrun/docker-compose.yml build server
```
+16 -1
查看文件
@@ -36,6 +36,9 @@ Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
- 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`.
@@ -47,6 +50,8 @@ Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
- 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.
### 4. Validation & Error Matrix
@@ -60,11 +65,15 @@ Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
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.
### 5. Good/Base/Bad Cases
- Good: `summary.json` contains `reportRef`, `childReports.<name>.status`,
`childReports.<name>.findingCount` and no absolute local path.
`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
@@ -81,6 +90,9 @@ Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
- 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.
### 7. Wrong vs Correct
@@ -107,6 +119,9 @@ Start from `docs/文档索引.md` and `docs/文档事实源状态矩阵.md`.
```json
{
"status": "PASS_WITH_FINDINGS",
"sourceCommit": "abcdef1234567890",
"observedBuildSha": "abcdef123456",
"buildShaMatches": true,
"reportRef": "artifacts/qa-flow/run",
"childReports": {
"oaOnlySmokeReadiness": {
+62 -2
查看文件
@@ -13,8 +13,9 @@ tasks after TRELLIS2.
- Put goal, scope, out-of-scope, acceptance and test plan in `prd.md`.
- Add `design.md` or `implement.md` for complex process, API, migration,
permission, deployment or workflow-governance changes.
- Curate `implement.jsonl` and `check.jsonl` with stable spec or research
context only.
- In sub-agent dispatch mode, curate `implement.jsonl` and `check.jsonl` with
stable spec or research context only. In Codex inline mode they may contain
no real entries because the main session reads specs directly.
- Give a concrete implementation plan before coding and wait for approval.
- Keep edits scoped to the current Trellis task.
- Commit in Chinese and include the Trellis task name or task ID.
@@ -60,3 +61,62 @@ rollback and future self-maintenance.
- Do not delete, bulk rewrite or move historical `issues/*.csv` files.
- New work may mention an old Issue CSV for traceability, but must not require
a new CSV unless the user explicitly requests legacy/export evidence.
## Scenario: Codex Inline Context Manifests
### 1. Scope / Trigger
- Trigger: creating, validating or finishing a Trellis task while
`.trellis/config.yaml` sets `codex.dispatch_mode: inline`.
### 2. Signatures
- Config: `codex.dispatch_mode: inline | sub-agent`.
- Context files: `<task>/implement.jsonl`, `<task>/check.jsonl`.
- Gates: `node scripts/qa/check-trellis-governance.mjs` and
`python3 ./.trellis/scripts/task.py validate <task>`.
### 3. Contracts
- Inline mode may keep both JSONL files at zero real entries; the seed
`_example` row does not count as injected context.
- If an inline task contains real entries, each entry must still be valid JSON
with non-empty `file` and `reason`; normal path and context warnings apply.
- Sub-agent mode requires at least one real entry in each manifest.
- Switching dispatch mode changes the validation policy, not historical task
evidence and not the requirement to read specs before implementation.
### 4. Validation & Error Matrix
- Inline + zero real entries -> pass with `*_inline_optional`.
- Inline + malformed real entry -> fail.
- Sub-agent + zero real entries -> fail with `*_empty`.
- Either mode + non-spec/non-research context -> warning for review.
### 5. Good/Base/Bad Cases
- Good: inline task uses the seed-only files and the main agent reads specs.
- Base: inline task keeps curated entries for audit; they remain validated.
- Bad: governance fabricates placeholder entries only to satisfy a rule meant
for sub-agent context injection.
### 6. Tests Required
- Fixture test for inline seed-only pass.
- Fixture test for sub-agent seed-only failure.
- Run both Trellis governance and `task.py validate` for the active task.
### 7. Wrong vs Correct
#### Wrong
```text
dispatch_mode=inline -> require at least one injected JSONL entry
```
#### Correct
```text
dispatch_mode=inline -> allow zero real entries; validate any real entries
dispatch_mode=sub-agent -> require curated real entries
```
+120
查看文件
@@ -155,3 +155,123 @@ return reply.send(stream);
originals in normal OA attachment areas.
- Customer original material, sensitive quotes, contract bodies or signed
packages outside authorized business flows.
## Scenario: External Delivery Error Logging
### 1. Scope / Trigger
- Trigger: logging SMTP, webhook, object-storage or other provider delivery
failures whose native error objects may contain recipients or payloads.
### 2. Signatures
- Email log metadata: `errorCode`, optional `providerCode`, optional
`smtpCommand`, optional numeric `smtpResponseCode`, request/business IDs and
recipient count.
- Forbidden input: raw provider `Error` objects in structured logs.
### 3. Contracts
- Log only explicitly allowlisted scalar metadata.
- Do not log `response`, `rejected`, `rejectedErrors`, `envelope`, addresses,
raw messages, headers, credentials or payloads.
- Provider codes must match a bounded token format; SMTP commands must come
from a fixed safe set; response codes must be bounded integers.
- Delivery failure counts remain observable without exposing recipient data.
### 4. Validation & Error Matrix
- Known safe provider code/command/response code -> include normalized fields.
- Unknown or free-form field -> omit it.
- Error with rejected address or SMTP response text -> log generic category and
safe numeric/token fields only.
- Missing logger -> return the delivery result without throwing a logging
error.
### 5. Good/Base/Bad Cases
- Good: record `EENVELOPE`, `RCPT TO`, `550`, request ID and failed count.
- Base: plain `Error` becomes only `errorCode=email_delivery_failed`.
- Bad: pass Nodemailer's raw `err` to Pino, which serializes rejected addresses.
### 6. Tests Required
- Mock a provider error containing a rejected full address, response and
envelope.
- Assert serialized log context contains none of those values and retains only
allowlisted metadata.
- Cover disabled, configured, success and failure delivery paths.
### 7. Wrong vs Correct
#### Wrong
```js
logger.error({ err: providerError, recipient }, "delivery failed");
```
#### Correct
```js
logger.error({ ...buildSafeErrorLog(providerError), requestId, failed }, "delivery failed");
```
## Scenario: Current Fact Source Sensitive Gate
### 1. Scope / Trigger
- Trigger: adding, registering or changing a CURRENT fact source, README,
deployment entry or environment template.
### 2. Signatures
- Gate: `node scripts/qa/check-sensitive-content.mjs`.
- Registry: `docs/文档事实源状态矩阵.md` rows whose status is `CURRENT` and
contract path is under `docs/`.
- Output: `fileRef`, `line`, `category`; never the matched text.
### 3. Contracts
- Scan fixed execution/deployment entrypoints plus every CURRENT docs contract
derived from the status matrix; do not maintain a partial duplicate docs list.
- Detect secret assignments, credential URLs, full accounts, personal workspace
paths, private keys and public IPv4 values.
- Allow explicit placeholders, environment interpolation and loopback addresses.
- Missing registry files remain a docs-audit concern; existing scanned files
must not be silently excluded.
### 4. Validation & Error Matrix
- Any sensitive match -> `FAIL` with non-zero `findingCount`.
- CURRENT doc added to the matrix -> automatically enters the scan set.
- EVIDENCE/HISTORICAL issue row -> does not enter the CURRENT docs scan set.
- Placeholder such as `<repo-root>` -> pass.
### 5. Good/Base/Bad Cases
- Good: all current docs and deploy entrypoints scan clean with metadata-only
output.
- Base: historical evidence stays outside this current-source blocking gate.
- Bad: hard-code six docs while the matrix contains eighteen CURRENT docs.
### 6. Tests Required
- Parser fixture with CURRENT doc, historical doc and CURRENT Issue row.
- Assertion that representative current manuals/specs appear in the resolved
default set.
- Repository gate must report `findingCount=0` and the expected expanded file
count after registry changes.
### 7. Wrong vs Correct
#### Wrong
```js
const files = ["docs/文档索引.md", "docs/产品设计规则.md"];
```
#### Correct
```js
const files = [...fixedEntrypoints, ...extractCurrentDocumentFiles(statusMatrix)];
```