Skip to content

Commit c7069bd

Browse files
authored
fix: stabilize diagnostics and low-end hooks (#441)
* refactor(core): isolate diagnostics sink boundary Keep structured log construction and GDPR redaction renderer-neutral, then pass the typed LogEntry to the existing IDB, Tauri JSONL, and development-console adapters. Preserve all logger APIs and sink behavior; this slice does not switch production authority or migrate callers.\n\nValidation: targeted Vitest 22/22, Biome, tsgo, docs metrics, native-readiness, desktop import guardrail, and diff checks passed. No new dependency or suppression was added. Local hooks remain bypassed only because pnpm reports ERR_PNPM_VERIFY_DEPS_BEFORE_RUN on the existing install; CI remains authoritative. * docs(native): keep diagnostics maturity evidence honest Record the new typed sink boundary as locally proven and CI merge-gated until this PR lands; retain the existing CI-proven Core/redaction parity claims. No architecture or authority status changes.\n\nValidation: docs metrics, native-readiness, and diff checks passed. Local hook bypass remains limited to the known ERR_PNPM_VERIFY_DEPS_BEFORE_RUN install mismatch. * fix(diagnostics): redact nested sensitive context Address Amazon Q security finding PRRT_kwDOQOeAgc6bGOWM: nested password/token values could bypass the previous top-level-only redaction. Recursively sanitize JSON-like objects and arrays in TypeScript and Rust, preserve non-mutating behavior, and update the shared parity fixture.\n\nValidation: targeted Vitest 22/22, tsgo, Biome, cargo fmt --check, and worldscript-diagnostics unit plus shared-fixture tests passed. The full local Cargo test also reached only the separate doctest, which could not create its temporary directory because this environment exposes /tmp as read-only; the test-only command passed. No dependency, suppression, authority switch, or baseline change. Local hooks remain bypassed only for ERR_PNPM_VERIFY_DEPS_BEFORE_RUN. * fix(diagnostics): route sinks through desktop platform * chore(tooling): stabilize low-end git hooks * fix(desktop): preserve adapter diagnostics context * fix: close release and translation quality blind spots * test: stabilize tauri runtime mock initialization * fix: close diagnostics review and coverage gaps * style: apply rustfmt to diagnostics redaction * test: close diagnostics coverage gaps * fix: allow tagless release-truth checks * fix: align native IV redaction
1 parent adb92bd commit c7069bd

43 files changed

Lines changed: 1707 additions & 648 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ The app supports a multi-provider AI stack (Gemini, OpenAI, Claude, Grok, OpenRo
3333
```bash
3434
pnpm run ci:prepush
3535
```
36-
This gate is mandatory before every push and after every local correction before re-pushing; all three checks must exit successfully. A targeted test or changed-file lint run alone is not push evidence. The pre-commit hook checks staged files with Biome, while the pre-push gate runs the full repository lint before typecheck and i18n validation. If branch switching or a lockfile/package-manifest change makes pnpm report dependency verification errors, repair first with `pnpm install --frozen-lockfile` and rerun the complete pre-push gate.
36+
This gate is mandatory before every push and after every local correction before re-pushing; it runs sequentially with a single-checker project typecheck, i18n parity/quality and bundle checks, release/doc truth, and lightweight native guardrails. The pre-commit hook separately runs staged-file Biome checks. Full repository lint, coverage, E2E, Storybook, Lighthouse, and mutation checks belong to cloud CI. If branch switching or a lockfile/package-manifest change makes pnpm report dependency verification errors, run `node scripts/dependency-state.mjs reconcile` and rerun the complete pre-push gate.
3737
Optional targeted smoke test: `pnpm exec vitest run <path>` **without** `--coverage`.
38-
**Hard rule:** Never invoke `pnpm test`, `npm run test`, or a bare Vitest wrapper; always use an explicit `pnpm exec vitest run <path>` command to avoid watch-mode hangs on constrained hardware.
38+
**Hard rule:** Never invoke `pnpm test`, `npm run test`, or a bare Vitest wrapper; always use an explicit `pnpm exec vitest run <path>` command to avoid watch-mode hangs on constrained hardware. Never start multiple heavyweight processes concurrently.
3939
4. **Audit cloud CI logs, fix locally, then re-push** – If the cloud CI run fails, inspect the logs via GitHub web UI or `gh run watch`, reproduce the specific failing test or lint error in isolation, fix it locally (quick tier to verify), commit, and push again for another cloud CI run.
4040
5. **Sequential execution** – Do not parallelize builds, tests, or processes locally. Use single-threaded modes and avoid background tasks that compete for RAM/CPU.
4141
6. **Resource budget** – Avoid spinning up the dev server (`pnpm run dev`) for extended periods if not needed. Prefer one-off commands (`pnpm run build`, `pnpm run typecheck`) and stop the server when done.
@@ -169,6 +169,7 @@ pnpm run lint # Biome lint (--error-on-warnings)
169169
pnpm run lint:fix # Biome check --write (lint + format)
170170
pnpm run format # Biome format --write
171171
pnpm run typecheck # tsgo --project tsconfig.tsgo.json --noEmit --checkers 4
172+
pnpm run typecheck:single # local low-end typecheck: one checker, sequential
172173
pnpm run i18n:check # Locale key parity vs English + rebuild bundles + content guard
173174
pnpm run parity:check # Feature parity audit
174175
pnpm run suppressions:check # Biome-ignore count ratchet
@@ -261,8 +262,10 @@ On any non-trivial change, add a single-line comment explaining **why**, not wha
261262
### Commit Messages
262263

263264
Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`.
264-
After an explicit `pnpm run hooks:install`, the pre-commit hook runs `biome check --write` on staged
265-
files via `simple-git-hooks` + `lint-staged`, and the pre-push hook runs `pnpm run ci:prepush`.
265+
After an explicit `pnpm run hooks:install`, the pre-commit hook runs `lint-staged` on staged files
266+
through `node scripts/hooks/pre-commit.mjs`; the pre-push hook runs
267+
`node scripts/hooks/pre-push.mjs`. These direct Node entrypoints verify the content fingerprint
268+
and invoke local binaries without pnpm's workspace-state preflight in the hook path.
266269
The pre-commit hook is not a substitute for the complete pre-push gate; CI remains mandatory when
267270
hooks are not installed.
268271

@@ -273,12 +276,21 @@ hooks are not installed.
273276
### Philosophy
274277

275278
- **Cloud CI-first:** The canonical quality gate is GitHub Actions. Low-end local machines should run only the "Quick" tier.
276-
- **Quick tier (local, before every push):** `pnpm run ci:prepush` runs the full repository lint, then
277-
the exact CI typecheck and i18n checks sequentially. Staged-file Biome validation still runs in
278-
pre-commit. Run the gate
279-
again after every correction before re-pushing; do not push based only on a targeted test or a
280-
changed-file lint run. Optionally: `pnpm exec vitest run <path>`
281-
**without** `--coverage`.
279+
- **Quick tier (local, before every push):** `pnpm run ci:prepush` runs the project typecheck with
280+
one checker, i18n parity/quality/bundle/content checks, release/doc truth, and lightweight desktop guardrails sequentially;
281+
the pre-commit hook separately runs staged-file Biome checks. Run the gate again after every
282+
correction before re-pushing; do not
283+
push based only on a targeted test or a changed-file lint run. Optionally:
284+
`pnpm exec vitest run <path>` **without** `--coverage`.
285+
- **Dependency state:** `pnpm run deps:verify` compares a content fingerprint of dependency
286+
manifests, workspace package manifests, and patches. After a dependency-related branch switch,
287+
run `node scripts/dependency-state.mjs reconcile` (or `pnpm run deps:reconcile` when pnpm can
288+
start); never use `--no-verify` as the
289+
normal recovery path.
290+
- **Low-resource policy:** Never run Biome, multiple TypeScript checkers, Vitest, Cargo, Vite,
291+
Storybook, or other heavyweight processes concurrently on the development workstation. Full
292+
repository lint/tests, E2E, coverage, Lighthouse, and mutation testing are cloud-CI work unless
293+
the user explicitly requests a narrowly scoped local run.
282294
- **Vitest watch-mode hard rule:** Never invoke `pnpm test`, `npm run test`, or a bare Vitest wrapper; use an explicit targeted `pnpm exec vitest run <path>` command so constrained hardware never waits on watch mode.
283295
- **Heavy tier (CI):** Vitest with coverage thresholds, Playwright E2E (desktop + mobile emulation), Lighthouse CI, Stryker mutation, Storybook static build, bundle budget + analyze.
284296

@@ -375,6 +387,8 @@ Never commit directly to `main` — always a feature branch + PR, even for a sin
375387

376388
**Review-comment completeness — check three independent channels before declaring a PR review-clean, every time:** (1) GraphQL `reviewThreads` for inline per-line comments; (2) `gh api repos/<owner>/<repo>/issues/<PR>/comments` for plain top-level bot comments (qodo-code-review posts its real findings only here, never as `reviewThreads`); (3) `gh api repos/<owner>/<repo>/pulls/<PR>/reviews`, reading each review's full `.body` text (CodeRabbit's "🧹 Nitpick comments" and outside-diff-range findings live here, collapsed, invisible to the other two channels). A bot using one channel on a PR doesn't mean the others are covered.
377389

390+
**Squash-merge verification:** Never infer a missing merge from `git merge-base --is-ancestor` alone. For every PR, verify `state=MERGED`, `merged_at`, and the resulting `merge_commit_sha`; compare the base commit immediately before the merge with that resulting `main` commit, including changed files, additions, and deletions. Use patch/tree equivalence as an optional corroboration. This is required because GitHub squash merges intentionally produce a new commit whose SHA is not the PR head SHA.
391+
378392
**Known review bots on this repo** (confirm still installed — this list can drift): CodeRabbit (`@coderabbitai review` to re-trigger), CodeAnt AI (5 CI status checks only — `CodeAnt - Quality Gates/SAST/SCA/SCR/Test Coverage` — not inline PR comments here), qodo-code-review (top-level comments, see above), Amazon Q Developer (`/q review` as a fresh top-level comment — not inside an existing thread; quota-conscious — call it once CodeRabbit/CodeAnt's own loop has already reached quiescence, not after every fix commit), Graphite AI Reviews (automatic, no confirmed manual trigger), chatgpt-codex-connector (intermittent/quota-limited availability — verify it's currently active rather than assuming silence means "nothing to report"). A bot's silence is not a clean pass by itself — for security/sandbox/IPC/FFI/packaging-adjacent PRs, verify at least one bot produced real review output (its actual comment/review text), not just a green check-run.
379393

380394
**PR size:** keep every PR's changed-file count under ~100 — several review bots skip inline comments above that threshold. Check with `git diff --name-only <base>...HEAD | wc -l` before pushing; split into the fewest stacked PRs that stay under the limit if needed.

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [1.28.0] — 2026-08-21
11-
1210
### Added
1311

1412
- **Cloud model catalog defense-in-depth:** current Anthropic, OpenAI, and xAI model IDs now
@@ -38,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3836

3937
### Changed
4038

39+
- **Diagnostics sink boundary:** structured log construction and recursive redaction now remain
40+
portable before serialized IDB, Tauri JSONL, and development-console adapters receive the record;
41+
legacy logger APIs remain compatible and no sink authority switch is claimed.
4142
- **Coverage scope and threshold calibration:** coverage now measures the root application/PWA
4243
shell; CI recalibrated floors to L80/F72/B66/S78 from the expanded-scope measurement, and the
4344
token audit baseline was ratcheted down from 160 to the verified current count of 159.
@@ -46,6 +47,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4647
retires CEF from the target architecture.
4748
- **DesktopPlatform boundary:** direct Tauri imports are mechanically constrained by the zero-
4849
tolerance guardrail while the renderer-neutral contract becomes the native transition surface.
50+
- **Diagnostics parity and task contracts:** Rust/TypeScript wire-shape and redaction fixtures now
51+
cover the diagnostics boundary, and TaskSupervisor requests/results reject unsupported contract
52+
versions before native execution (#437, #439, #440).
53+
- **Native maturity evidence:** the G1 qualification ledger now records the partial, evidence-backed
54+
status of the native migration without claiming a production authority switch (#438).
4955

5056
### Fixed
5157

CONTRIBUTING.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ Run the smallest gate that matches your change — sequentially, one heavy comma
189189

190190
| Your change… | Run locally before pushing |
191191
|---|---|
192-
| **Always** | `pnpm run lint` · `pnpm run typecheck` · targeted `pnpm exec vitest run <path>` (no `--coverage`) |
192+
| **Always** | `pnpm run ci:prepush` |
193193
| **Touched any locale JSON** | `pnpm run i18n:check` (parity + bundle rebuild) |
194194
| **Added/removed a feature flag** | `pnpm exec tsx scripts/audit-feature-parity.ts` (must report 0 drifts) |
195195
| **Touched `packages/ai-core`, `workers/`, or `vite.config.ts`** | `pnpm run build && pnpm run smoke:prod` (prod-only crash guard) |
@@ -200,7 +200,9 @@ Coverage, E2E, Lighthouse, Stryker, and Storybook test-runner are **CI gate jobs
200200

201201
### Local vs CI (low-end friendly)
202202

203-
- **Before every push (recommended):** `pnpm run lint`, `pnpm run typecheck`, `pnpm run i18n:check`. Optional: targeted `pnpm exec vitest run <path>` for a quick smoke.
203+
- **Before every push:** `pnpm run ci:prepush` (sequential single-checker typecheck, i18n parity/quality, release/doc truth, and lightweight guardrails); the pre-commit hook runs staged Biome checks. Optional: targeted `pnpm exec vitest run <path>` for a quick smoke.
204+
- **Full local gate (manual):** `pnpm run ci:local:full`; full tests, coverage, E2E, Storybook, Lighthouse, and mutation testing remain CI-owned on low-end hardware.
205+
- **Dependency recovery:** run `node scripts/dependency-state.mjs reconcile` when pnpm reports stale dependency state. It performs the frozen install and records the content fingerprint atomically; `pnpm run deps:reconcile` is the convenience wrapper when pnpm can launch.
204206
- **Vitest hard rule:** Never invoke `pnpm test`, `npm run test`, or a bare Vitest wrapper; watch mode hangs constrained hardware. Use an explicit targeted `pnpm exec vitest run <path>` command.
205207
- **Full gate:** GitHub Actions runs Vitest **with** coverage thresholds, Playwright (desktop + mobile emulation in CI), Lighthouse, etc. A **green CI run** is the merge bar — you are **not** required to pass full E2E or LHCI on a weak laptop.
206208
- **Optional local E2E:** `CI=true pnpm run test:e2e` when debugging; optional mobile project: `RUN_MOBILE_E2E=1` (see [`docs/CI.md`](docs/CI.md)).
@@ -387,13 +389,11 @@ Open a **focused PR per theme** (storage vs. i18n vs. collaboration) to keep rev
387389

388390
1. Fork the repository and create a feature branch
389391
2. Write or update tests for your changes
390-
3. Let CI run the full test suite; locally use only targeted `pnpm exec vitest run <path>`.
391-
4. Ensure Biome passes: `pnpm run lint`
392-
5. Ensure i18n parity: `pnpm run i18n:check`
393-
6. Ensure types compile: `pnpm run typecheck`
394-
7. Ensure the build succeeds when build-affecting files changed; GitHub Actions runs the canonical build gate for every PR.
395-
8. Submit a PR against `main` with a clear description
396-
9. Request review from at least one maintainer
392+
3. Run the sequential local gate: `pnpm run ci:prepush` (and the optional targeted `pnpm exec vitest run <path>` when useful).
393+
4. Let CI run the full lint and test suite; it is authoritative for the heavy tier.
394+
5. Ensure the build succeeds when build-affecting files changed; GitHub Actions runs the canonical build gate for every PR.
395+
6. Submit a PR against `main` with a clear description
396+
7. Request review from at least one maintainer
397397

398398
The CI pipeline will automatically run lint, i18n check, typecheck, tests, and build on every PR.
399399

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<img src="https://img.shields.io/badge/TypeScript-7.x_(tsgo)-3178C6?logo=typescript&logoColor=white" alt="TypeScript 7 (tsgo)">
1010
<img src="https://img.shields.io/badge/AI-Gemini_%7C_OpenAI_%7C_OpenRouter_%7C_Ollama_%7C_WebLLM-4285F4?logo=google" alt="Gemini · OpenAI · OpenRouter · Ollama · WebLLM">
1111
<img src="https://img.shields.io/badge/Local_AI-WebGPU_%7C_ONNX_%7C_Transformers.js-8B5CF6" alt="WebGPU · ONNX · Transformers.js">
12-
<img src="https://img.shields.io/badge/Version-v1.28.0-6366F1" alt="v1.28.0">
12+
<img src="https://img.shields.io/badge/Next-v1.28.0-6366F1" alt="Next v1.28.0 (unreleased)">
1313
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
1414
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
1515
<img src="https://img.shields.io/badge/i18n-19_locales-2922_keys-0EA5E9" alt="i18n 19 locales — 2922 keys">

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
}
126126
},
127127
{
128-
"includes": ["services/logger.ts"],
128+
"includes": ["services/logger.ts", "services/diagnostics/logSinks.ts"],
129129
"linter": {
130130
"rules": {
131131
"suspicious": {

crates/worldscript-diagnostics/src/lib.rs

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,32 @@ pub struct LogEntry {
3232
pub context: Option<Map<String, Value>>,
3333
}
3434

35-
/// Redact sensitive top-level context keys using the existing TypeScript contract.
35+
/// Redact sensitive keys recursively in JSON objects and arrays.
3636
///
37-
/// The current TS implementation intentionally redacts keys only, not values nested under
38-
/// otherwise-safe keys. Keeping that scope exact avoids an unreviewed wire-contract change.
37+
/// This mirrors the TypeScript diagnostics boundary so sensitive values cannot bypass redaction
38+
/// by being placed below an otherwise-safe object key.
3939
pub fn sanitize_context(context: &Map<String, Value>) -> Map<String, Value> {
4040
context
4141
.iter()
4242
.map(|(key, value)| {
4343
let sanitized = if is_sensitive_key(key) {
4444
Value::String(REDACTED.to_string())
4545
} else {
46-
value.clone()
46+
sanitize_value(value)
4747
};
4848
(key.clone(), sanitized)
4949
})
5050
.collect()
5151
}
5252

53+
fn sanitize_value(value: &Value) -> Value {
54+
match value {
55+
Value::Object(object) => Value::Object(sanitize_context(object)),
56+
Value::Array(items) => Value::Array(items.iter().map(sanitize_value).collect()),
57+
_ => value.clone(),
58+
}
59+
}
60+
5361
/// Return a sanitized copy of a structured log entry without mutating its input.
5462
pub fn sanitize_entry(entry: &LogEntry) -> LogEntry {
5563
LogEntry {
@@ -60,10 +68,43 @@ pub fn sanitize_entry(entry: &LogEntry) -> LogEntry {
6068

6169
fn is_sensitive_key(key: &str) -> bool {
6270
let lowercase = key.to_ascii_lowercase();
71+
// QNBS-v3: normalize separator variants so native redaction matches the shared diagnostics contract.
72+
let compact = lowercase.replace(['_', '-'], "");
6373
lowercase.contains("key")
6474
|| lowercase.contains("token")
6575
|| lowercase.contains("password")
6676
|| lowercase.contains("passphrase")
77+
|| is_iv_key(key)
78+
|| compact.contains("initializationvector")
79+
|| compact.contains("initialvector")
80+
}
81+
82+
fn is_iv_key(key: &str) -> bool {
83+
let bytes = key.as_bytes();
84+
let is_iv = |index: usize| {
85+
bytes.get(index).is_some_and(|b| *b == b'i' || *b == b'I')
86+
&& bytes
87+
.get(index + 1)
88+
.is_some_and(|b| *b == b'v' || *b == b'V')
89+
};
90+
for index in 0..bytes.len().saturating_sub(1) {
91+
if !is_iv(index) {
92+
continue;
93+
}
94+
let before_ok = index == 0
95+
|| bytes[index - 1] == b'_'
96+
|| bytes[index - 1] == b'-'
97+
|| bytes[index - 1].is_ascii_lowercase();
98+
let after = bytes.get(index + 2).copied();
99+
let after_ok = after.is_none()
100+
|| after == Some(b'_')
101+
|| after == Some(b'-')
102+
|| after.is_some_and(|b| b.is_ascii_uppercase());
103+
if before_ok && after_ok {
104+
return true;
105+
}
106+
}
107+
false
67108
}
68109

69110
#[cfg(test)]
@@ -90,10 +131,7 @@ mod tests {
90131
assert_eq!(sanitized["myPassword"], json!(REDACTED));
91132
assert_eq!(sanitized["recoveryPassphrase"], json!(REDACTED));
92133
assert_eq!(sanitized["userId"], json!(42));
93-
assert_eq!(
94-
sanitized["nested"],
95-
json!({ "token": "still-owned-by-the-nested-value" })
96-
);
134+
assert_eq!(sanitized["nested"], json!({ "token": REDACTED }));
97135
assert_eq!(context["apiKey"], json!("secret"));
98136
}
99137

@@ -140,4 +178,34 @@ mod tests {
140178
);
141179
assert_eq!(entry.context.as_ref().unwrap()["apiKey"], json!("secret"));
142180
}
181+
182+
#[test]
183+
fn redacts_iv_variants_without_redacting_ordinary_words() {
184+
let context = serde_json::from_value(json!({
185+
"iv": "one",
186+
"ivHex": "two",
187+
"encryptionIv": "three",
188+
"initializationVector": "four",
189+
"initialization_vector": "five",
190+
"initialization-vector": "six",
191+
"initial_vector": "seven",
192+
"initial-vector": "eight",
193+
"ivory": "safe",
194+
"privilege": "safe"
195+
}))
196+
.expect("fixture context is an object");
197+
198+
let sanitized = sanitize_context(&context);
199+
200+
assert_eq!(sanitized["iv"], json!(REDACTED));
201+
assert_eq!(sanitized["ivHex"], json!(REDACTED));
202+
assert_eq!(sanitized["encryptionIv"], json!(REDACTED));
203+
assert_eq!(sanitized["initializationVector"], json!(REDACTED));
204+
assert_eq!(sanitized["initialization_vector"], json!(REDACTED));
205+
assert_eq!(sanitized["initialization-vector"], json!(REDACTED));
206+
assert_eq!(sanitized["initial_vector"], json!(REDACTED));
207+
assert_eq!(sanitized["initial-vector"], json!(REDACTED));
208+
assert_eq!(sanitized["ivory"], json!("safe"));
209+
assert_eq!(sanitized["privilege"], json!("safe"));
210+
}
143211
}

0 commit comments

Comments
 (0)