From ad2679dc24299e420f2c157198e95584bf674838 Mon Sep 17 00:00:00 2001
From: qnbs <155236708+qnbs@users.noreply.github.com>
Date: Wed, 9 Sep 2026 04:17:12 +0200
Subject: [PATCH 01/15] fix(docs): upgrade CHANGELOG completeness check,
backfill Unreleased
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
audit F-2: scanUnreleasedTruth's own gate fired only when [Unreleased]
was entirely empty, so a single unrelated doc-sync bullet satisfied
"meaningful content" forever — 13 real feat/fix commits since v1.28.4
went completely undocumented, and the gate never noticed.
- scanUnreleasedTruth now requires every post-tag feat|fix|perf commit
to be individually referenced in [Unreleased], either by its
trailing PR number (the mid-subject issue ref some commits also
carry, e.g. "(#553) (#621)", is correctly ignored in favor of the
true trailing PR number) or by a bounded subject-slug match (>=60%
of its most identifying words) for the few commits merged without
one. Missing commits are named in the failure output.
- Backfilled CHANGELOG.md's [Unreleased] with all 13 currently-
undocumented commits since v1.28.4 (project schema-version
classification Slices A/B, the canonical document-projection
foundation, the legacy-to-v1 admission primitive, filesystem-
admission convergence, i18n startup-copy fixes, PR-size governance
hardening, the attribution guard, and this session's own PR-1
security-doc fix).
- Pulled TODO.md's Current Sprint forward: the 5 landed ledger-row-9
PRs are now named, still correctly marked not-complete (no target
release invented — none exists yet).
- 6 new regression tests for the completeness logic; README
test-count resynced.
---
CHANGELOG.md | 56 +++++++++++++++++++
README.md | 8 +--
TODO.md | 18 +++++--
scripts/check-doc-metrics.mjs | 87 +++++++++++++++++++++++++++---
tests/unit/checkDocMetrics.test.ts | 69 ++++++++++++++++++++++++
5 files changed, 225 insertions(+), 13 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9036b59cd..12fa4d6ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- **Project schema-version classification (Slice A, #553):** `PROJECT_SCHEMA_V1` as a fresh
+ production version marker, disjoint classification
+ (`LEGACY_UNVERSIONED`/`SUPPORTED_OLDER`/`UNSUPPORTED_OLDER`/`CURRENT`/`FUTURE`/`MALFORMED`),
+ raw/header parsing before typed parsing so a `FUTURE` document with a breaking shape still
+ classifies `FUTURE` rather than `MALFORMED`, and the `LEGACY_TO_V1` in-memory admission
+ primitive. PR #618.
+- **Schema-version classification observed on IDB project load (Slice B, 1/N, #553):** the first
+ ingress path wired to Slice A's classifiers, observation-only (classify and log, never alter
+ load behavior, never throw), matching the established Core shadow-validation pattern. PR #619.
+- **Canonical document projection foundation:** establishes the canonical parser/import/admission
+ boundary that retains the original JSON text alongside a bounded typed projection, the
+ foundation Slice C's admission primitive builds on.
+- **Explicit legacy-to-v1 admission primitive (#653):** the non-destructive `LEGACY_TO_V1`
+ in-memory admission path recognizes, fully validates, losslessly overlays the V1 version marker,
+ and revalidates the destination. PR #653.
+- **Filesystem admission converged after #654 (#658):** carries forward canonical filesystem
+ admission and closes legacy editable-admission authority gaps. PR #658.
+
+### Fixed
+
+- **`schemaVersion` now enforces raw integer grammar (#553):** rejects rounded fractional schema
+ versions and non-number schema-version tokens instead of silently coercing them. PR #621.
+- **Canonical document projection now reuses its own parsed input** instead of re-parsing,
+ removing a redundant-parse divergence risk between the raw and typed projections.
+- **Projection failure now retains the raw header verdict** instead of discarding the
+ already-classified version header when the typed projection itself fails.
+- **Unsupported-project startup copy added** alongside the existing migration-gap copy, so a
+ project whose schema version this build cannot open at all gets its own distinct startup
+ message rather than reusing the migration-gap wording.
+- **Migration-gap startup copy now distinguished from the unsupported-project case (#656):** the
+ two failure classes previously shared wording; each now gets copy specific to its actual cause.
+ PR #656.
+- **PR-size exception governance hardened (#657):** closes control gaps in how per-PR size-ceiling
+ exceptions are recorded and validated. PR #657.
+- **Security docs no longer cite closed PR #356 as active desktop-encryption remediation:**
+ `docs/SECURITY-THREAT-MODEL.md` and `docs/IDB-ENCRYPTION.md` both asserted PR #356 was the
+ active/pending remediation for desktop plaintext storage after it was closed as superseded on
+ 2026-08-18. Rewritten to anchor on the living Ledger-row-9/R-15 state instead of a PR number, and
+ `check-doc-metrics.mjs` now mechanically rejects an unqualified live/pending-remediation claim
+ tied to a bare PR number in these two files. PR #673.
+- **CHANGELOG completeness-check upgrade, backfill Unreleased (audit F-2):** `scanUnreleasedTruth`
+ previously accepted any non-empty `[Unreleased]` section forever, so a single unrelated bullet
+ let arbitrarily many later `feat`/`fix`/`perf` commits go completely undocumented — 13 real
+ commits since `v1.28.4` had gone unrecorded. It now requires every governed commit to be
+ individually referenced by PR number or subject slug, naming any that aren't; this section was
+ backfilled with all 13 currently-undocumented entries above.
+
+### Security
+
+- **AI/session attribution now rejected in commits and PRs:** a fail-closed guard
+ (`check-commit-attribution.mjs`, a commit-msg hook, a pre-push scan of outgoing commits, and a
+ CI check on the commit range and PR title/body) blocks Claude/Anthropic attribution trailers,
+ session URLs, and generated-by footers before they can land in repository history. PR #672.
+
### Documentation
- **Post-release v1.28.4 truth sync:** removed the now-stale release-candidate markers from
diff --git a/README.md b/README.md
index 381806a19..11eb7a6fb 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-
+
@@ -511,7 +511,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
-| **Testing** | Vitest 4.x (7593+ tests / 603 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
+| **Testing** | Vitest 4.x (7599+ tests / 603 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
@@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
-│ ├── unit/ # Vitest unit tests (7593+ tests, 603 files) — count spans tests/, components/, packages/*/tests/, not just this folder
+│ ├── unit/ # Vitest unit tests (7599+ tests, 603 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
@@ -714,7 +714,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**.
**Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):**
-- **7593+ unit tests** across **603 test files** — CI is authoritative for pass/fail
+- **7599+ unit tests** across **603 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)
diff --git a/TODO.md b/TODO.md
index be2beae0b..0d12e63ff 100644
--- a/TODO.md
+++ b/TODO.md
@@ -35,9 +35,21 @@ Status: 🔄 in progress | ⬜ open | ✅ done
`TODO.md`/`README.md` reconciled, `v1.28.4` tag and GitHub Release published with all expected
Desktop artifacts; post-merge main CI and CodeQL green (CodeQL has no tag trigger), tag-triggered
CI/Tauri/Docker all green, and `AUDIT.md` updated with real evidence.
-- ⬜ Close the outstanding Wave 2 prerequisite (ledger row 9: project state-shape compatibility
- adapter) — currently in progress, not complete. Wave 3/4 R-15 implementation stays blocked
- (`S5_IMPLEMENTATION_READY=NO`) until this and `S5_TERMINAL=YES` are both true.
+- ✅ Attribution-hygiene guard merged (PR #672): fail-closed rejection of AI/session attribution
+ trailers and footers in commits, tags, and PR titles/bodies (commit-msg hook, pre-push scan, CI).
+- ✅ Post-audit truth/governance remediation PR #673 merged: stale `#356`-active-remediation
+ citations in `docs/SECURITY-THREAT-MODEL.md`/`docs/IDB-ENCRYPTION.md` rewritten to anchor on the
+ living ledger state, with a mechanical `check-doc-metrics.mjs` guard against recurrence (first
+ slice of the 2026-09-08 external audit's S1–S10 remediation sequence; further slices in progress).
+- 🔄 Ledger row 9 (project state-shape compatibility adapter) — five `feat`/`fix` PRs landed
+ (#618 Slice A persisted schema-version classification, #619 Slice B IDB-load observation, #621
+ raw-integer-grammar hardening, #653 the non-destructive `LEGACY_TO_V1` admission primitive, #658
+ filesystem-admission convergence) but **still not complete**: durable source-generation fencing,
+ writer integration, and universal ingress/egress remain open per
+ `docs/native/CORE-MIGRATION-LEDGER.md` row 9. Wave 3/4 R-15 implementation stays blocked
+ (`S5_IMPLEMENTATION_READY=NO`, recorded on row 10) until row 9 and `S5_TERMINAL=YES` are both
+ true — no target release is set for that yet (tracked, not invented; see the S1–S10 remediation
+ plan's exit-criterion work).
- ⬜ #614 (narrow concurrent-first-install multi-tab race, requires cross-tab coordination) and
#532 (WelcomePortal E2E entry nondeterminism root cause) remain open, tracked separately —
not part of this sprint unless they directly block release or R-15 work.
diff --git a/scripts/check-doc-metrics.mjs b/scripts/check-doc-metrics.mjs
index 7bd124321..77dd541e2 100644
--- a/scripts/check-doc-metrics.mjs
+++ b/scripts/check-doc-metrics.mjs
@@ -330,19 +330,84 @@ export function scanReadmeReleaseTruth(readme, taggedVersions) {
return findings;
}
-function hasMeaningfulUnreleasedContent(changelog) {
+// QNBS-v3 (audit F-2): shared by the presence check below and the completeness check further
+// down — both need the raw [Unreleased] section text with HTML comments stripped.
+function getUnreleasedSectionText(changelog) {
const heading = /^## \[Unreleased\]\s*$/m.exec(changelog);
- if (!heading) return false;
+ if (!heading) return '';
const afterHeading = changelog.slice(heading.index + heading[0].length);
const nextHeading = afterHeading.search(/^##\s/m);
const section = nextHeading === -1 ? afterHeading : afterHeading.slice(0, nextHeading);
- const withoutHtmlComments = section.replace(/|$)/g, '');
- return withoutHtmlComments.split('\n').some((line) => {
+ return section.replace(/|$)/g, '');
+}
+
+function hasMeaningfulUnreleasedContent(changelog) {
+ const section = getUnreleasedSectionText(changelog);
+ return section.split('\n').some((line) => {
const trimmed = line.trim();
return trimmed.length > 0 && !trimmed.startsWith('