P2.1: iOS Schema Versioning Strategy
- Added SCHEMA_VERSION constant and checkSchemaVersion() method in PersistenceController
- Version stored in NSPersistentStore metadata (observability contract, not migration gate)
- CoreData auto-migration remains authoritative; version mismatches logged, not blocked
- Documentation added to ios/Plugin/README.md with migration contract
P2.2: Combined Edge Case Tests
- Added 3 resilience test scenarios to DailyNotificationRecoveryTests.swift:
- test_combined_dst_boundary_duplicate_delivery_cold_start()
- test_combined_rollover_duplicate_delivery_cold_start()
- test_combined_schema_version_cold_start_recovery()
- All tests labeled with @resilience @combined-scenarios comments
- Tests verify idempotency and correctness under combined stressors
P2.3: Android Combined Tests Design
- Created P2.3-DESIGN.md with scope, invariants, and acceptance criteria
- Created P2.3-IMPLEMENTATION-CHECKLIST.md with step-by-step execution plan
- Design ready for implementation to achieve parity with iOS P2.2
Documentation Updates
- Fixed parity matrix: iOS invalid data handling now correctly shows "✅ Recovery tested" with test references
- Updated progress docs (00-STATUS.md, 01-CHANGELOG-WORK.md, 03-TEST-RUNS.md, 04-PARITY-MATRIX.md)
- Updated P2-DESIGN.md to reflect P2.3 scope (Android combined tests)
- Updated SYSTEM_INVARIANTS.md baseline tag references
Baseline Tag
- Created and pushed v1.0.11-p2-complete tag
- Tag represents P2.x completion (schema versioning + combined resilience tests)
All invariants preserved. CI passes. Tests runnable via xcodebuild on macOS.
428 lines
17 KiB
Markdown
428 lines
17 KiB
Markdown
# System Invariants
|
|
|
|
**Purpose:** Single authoritative document naming, explaining, and referencing all enforced invariants.
|
|
**Owner:** Development Team
|
|
**Last Updated:** 2025-12-22
|
|
**Status:** active
|
|
**Baseline:** `v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete`
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document defines the **invariants** (unchanging rules) that this project enforces. These invariants are **policy-as-code** — they are enforced by tooling, not just documented as conventions.
|
|
|
|
**Why this matters:**
|
|
- New contributors can understand "what not to break"
|
|
- Future work (P2, P3, etc.) has explicit constraints
|
|
- Violations are caught automatically, not discovered later
|
|
- The baseline tag (`v1.0.11-p0-p1.4-complete`) represents a state where all invariants are enforced
|
|
|
|
**How to use this document:**
|
|
- Before making changes, review relevant invariants
|
|
- If you violate an invariant, CI will fail with a clear error
|
|
- If you need to change an invariant, update this document and the enforcing code together
|
|
|
|
---
|
|
|
|
## 1. Packaging Invariants (P0)
|
|
|
|
### What
|
|
|
|
The npm package must not contain forbidden files, and packaging is controlled by a whitelist approach.
|
|
|
|
**Specific rules:**
|
|
- `npm pack --dry-run` must not contain:
|
|
- `xcuserdata/`, `*.xcuserstate`, `DerivedData/` (Xcode user state)
|
|
- `ios/App/` (test app, not library code)
|
|
- `.DS_Store`, `*.swp`, `*.swo`, `*.orig`, `*.rej` (editor/macOS junk)
|
|
- `package.json.files` whitelist is **authoritative** (primary control)
|
|
- `.npmignore` is secondary (belt-and-suspenders only)
|
|
|
|
### Why
|
|
|
|
- **Publish safety:** Prevents shipping developer-local files, test apps, and build artifacts
|
|
- **Package size:** Keeps published tarball clean and minimal
|
|
- **Security:** Avoids leaking local development state
|
|
- **Professionalism:** Published packages should only contain intended library code
|
|
|
|
### How
|
|
|
|
**Enforced by:** `scripts/verify.sh` → `check_package()` function
|
|
|
|
**Enforcement mechanism:**
|
|
1. Runs `npm pack --dry-run` to simulate package creation
|
|
2. Extracts file list from pack output (handles multiple npm output formats)
|
|
3. Scans for forbidden patterns using regex: `xcuserdata/|\.xcuserstate|DerivedData/|\.tgz|ios/App/|\.DS_Store|\.swp|\.swo|\.orig|\.rej`
|
|
4. **Hard-fails** if any forbidden files are found
|
|
5. Provides actionable error messages with remediation hints
|
|
|
|
**Location:** `scripts/verify.sh:216-316` (function `check_package()`)
|
|
|
|
**Verification command:**
|
|
```bash
|
|
./ci/run.sh # Includes package checks
|
|
# Or manually:
|
|
npm pack --dry-run | grep -E "xcuserdata|xcuserstate|DerivedData|ios/App/"
|
|
```
|
|
|
|
### Where
|
|
|
|
- **Enforcing code:** `scripts/verify.sh:216-316` (`check_package()`)
|
|
- **Policy definition:** `docs/progress/00-STATUS.md:104-113` (Packaging Invariants section)
|
|
- **Package configuration:** `package.json` (`files` field)
|
|
- **Secondary exclusion:** `.npmignore` (belt-and-suspenders)
|
|
|
|
---
|
|
|
|
## 2. Core Module Purity (P1.4)
|
|
|
|
### What
|
|
|
|
The `src/core/` module must remain platform-agnostic and portable. It cannot import platform-specific or Node.js built-in modules.
|
|
|
|
**Specific rules:**
|
|
- `src/core/` must not import:
|
|
- **Node builtins:** `fs`, `path`, `os`, `child_process`, `crypto`, `http`, `https`, `net`, `tls`, `zlib`, `stream`, `util`, `url`, `worker_threads`, `perf_hooks`, `vm`
|
|
- **Platform modules:** `@capacitor/*`, `react`, `capacitor`
|
|
- `package.json.exports['./core']` must exist and point to valid build artifacts
|
|
- Core types must remain platform-agnostic (no platform-specific types in core)
|
|
|
|
### Why
|
|
|
|
- **Portability:** Core module can be used in any JavaScript/TypeScript environment
|
|
- **Architectural separation:** Platform-specific code belongs in adapters, not core
|
|
- **Testability:** Core can be tested without platform dependencies
|
|
- **Reusability:** Core types/interfaces can be shared across platforms without coupling
|
|
|
|
### How
|
|
|
|
**Enforced by:** `scripts/verify.sh` → `check_core_source()` + `check_core_artifacts()`
|
|
|
|
**Source checks (pre-build):**
|
|
1. Verifies `src/core/` directory exists
|
|
2. Checks for required core files (`index.ts`, `errors.ts`, `enums.ts`, `events.ts`, `contracts.ts`, `guards.ts`)
|
|
3. Scans all files in `src/core/` for forbidden imports using comprehensive regex:
|
|
```bash
|
|
(from\s+['\"]|require\s*\(\s*['\"]|import\s*\(\s*['\"])(${NODE_BUILTINS}|react|@capacitor/|capacitor)['\"]
|
|
```
|
|
4. **Hard-fails** if forbidden imports are found
|
|
5. Prints offending lines and policy reminder
|
|
|
|
**Artifact checks (post-build):**
|
|
1. Verifies build artifacts exist: `dist/esm/core/index.js`, `dist/esm/core/index.d.ts`
|
|
2. Validates `package.json.exports['./core']` exists using Node.js script
|
|
3. **Hard-fails** if artifacts or exports are missing
|
|
|
|
**Location:**
|
|
- Source checks: `scripts/verify.sh:413-464` (function `check_core_source()`)
|
|
- Artifact checks: `scripts/verify.sh:467-496` (function `check_core_artifacts()`)
|
|
|
|
**Verification command:**
|
|
```bash
|
|
./ci/run.sh # Includes core module checks
|
|
# Or manually check source:
|
|
grep -RInE "(from\s+['\"]|require\s*\(\s*['\"]|import\s*\(\s*['\"])(${NODE_BUILTINS}|react|@capacitor/|capacitor)['\"]" src/core
|
|
```
|
|
|
|
### Where
|
|
|
|
- **Enforcing code:**
|
|
- Source checks: `scripts/verify.sh:413-464` (`check_core_source()`)
|
|
- Artifact checks: `scripts/verify.sh:467-496` (`check_core_artifacts()`)
|
|
- **Policy definition:** `docs/progress/P2-DESIGN.md:67-77` (Core Module Purity section)
|
|
- **Core module location:** `src/core/`
|
|
- **Package exports:** `package.json` (`exports['./core']` field)
|
|
|
|
---
|
|
|
|
## 3. CI Authority (P0)
|
|
|
|
### What
|
|
|
|
`./ci/run.sh` is the **only** supported CI entrypoint. All release gates, merge gates, and automation must invoke `./ci/run.sh`, not `npm run build` directly.
|
|
|
|
**Specific rules:**
|
|
- `./ci/run.sh` is the canonical CI command
|
|
- All gates (release, merge, automation) must call `./ci/run.sh`
|
|
- `npm run build` must not be called directly in gates (it doesn't include invariant checks)
|
|
- `./scripts/verify.sh` is an implementation detail (wrapped by `./ci/run.sh`)
|
|
|
|
### Why
|
|
|
|
- **Single source of truth:** One command that runs all checks
|
|
- **Invariant enforcement:** `verify.sh` (called by `ci/run.sh`) encodes packaging, core-purity, and export checks
|
|
- **Consistency:** All environments (local, CI, release) use the same verification
|
|
- **Debuggability:** Failures are actionable and consistent across environments
|
|
- **Policy-as-code:** The contract is explicit, not implicit
|
|
|
|
### How
|
|
|
|
**Enforced by:** `ci/README.md` (policy-as-code contract) + `githooks/pre-push` (optional automation)
|
|
|
|
**Enforcement mechanism:**
|
|
1. **Documentation contract:** `ci/README.md` explicitly states the policy (line 5-6)
|
|
2. **Git hook (optional):** `githooks/pre-push` calls `./ci/run.sh` before allowing pushes
|
|
3. **Makefile target:** `make ci` runs `./ci/run.sh` (convenience alias)
|
|
4. **Process enforcement:** Team must follow the contract (not automatically enforced, but CI will fail if invariants are violated)
|
|
|
|
**Location:**
|
|
- Policy contract: `ci/README.md:5-6` (Contract / Policy-as-code block)
|
|
- CI entrypoint: `ci/run.sh` (wraps `./scripts/verify.sh`)
|
|
- Git hook: `githooks/pre-push` (optional, calls `./ci/run.sh`)
|
|
|
|
**Verification command:**
|
|
```bash
|
|
./ci/run.sh # The canonical CI command
|
|
# Or:
|
|
make ci # Convenience alias
|
|
```
|
|
|
|
### Where
|
|
|
|
- **Policy contract:** `ci/README.md:5-6` (Contract / Policy-as-code block)
|
|
- **CI entrypoint:** `ci/run.sh` (wraps `./scripts/verify.sh`)
|
|
- **Verification script:** `scripts/verify.sh` (implementation detail)
|
|
- **Git hook:** `githooks/pre-push` (optional automation)
|
|
- **Makefile:** `Makefile` (`make ci` target)
|
|
- **Documentation:** `docs/progress/00-STATUS.md:115-117` (Local CI Policy section)
|
|
|
|
---
|
|
|
|
## 4. Export Correctness (P0)
|
|
|
|
### What
|
|
|
|
All `package.json.exports` paths must match actual build artifacts. Exported paths must exist after build.
|
|
|
|
**Specific rules:**
|
|
- `package.json.exports["./web"]` paths must match actual build artifacts
|
|
- `package.json.exports["./core"]` paths must match actual build artifacts
|
|
- All exported paths must exist after `npm run build`
|
|
- Build must succeed (TypeScript compilation + Rollup bundling)
|
|
|
|
### Why
|
|
|
|
- **Runtime correctness:** Broken exports cause import failures at runtime
|
|
- **Type safety:** Missing type definitions break TypeScript consumers
|
|
- **Publish safety:** Broken exports are discovered before publish, not after
|
|
- **Consumer trust:** Correct exports are a basic contract with package consumers
|
|
|
|
### How
|
|
|
|
**Enforced by:** `scripts/verify.sh` → `check_build()` function
|
|
|
|
**Enforcement mechanism:**
|
|
1. Runs `npm run build` to generate build artifacts
|
|
2. Verifies build succeeds (exit code check)
|
|
3. Checks for required build outputs:
|
|
- `dist/esm/web.d.ts`, `dist/esm/web.js`
|
|
- `dist/esm/core/index.d.ts`, `dist/esm/core/index.js`
|
|
4. **Hard-fails** if build fails or artifacts are missing
|
|
5. Core artifact validation also checks `package.json.exports['./core']` exists (via `check_core_artifacts()`)
|
|
|
|
**Location:** `scripts/verify.sh:191-214` (function `check_build()`)
|
|
|
|
**Verification command:**
|
|
```bash
|
|
./ci/run.sh # Includes build checks
|
|
# Or manually:
|
|
npm run build && ls -la dist/esm/web.* dist/esm/core/index.*
|
|
```
|
|
|
|
### Where
|
|
|
|
- **Enforcing code:** `scripts/verify.sh:191-214` (`check_build()`)
|
|
- **Export definitions:** `package.json` (`exports` field)
|
|
- **Build artifacts:** `dist/esm/` (generated by `npm run build`)
|
|
- **Policy definition:** `docs/progress/00-STATUS.md:111` (Export correctness requirement)
|
|
|
|
---
|
|
|
|
## 5. Documentation Structure (P1.5)
|
|
|
|
### What
|
|
|
|
Documentation must follow the index-first rule and maintain drift guards. New docs must be discoverable via the index or explicitly archived.
|
|
|
|
**Specific rules:**
|
|
- **Index-first rule:** New docs must be linked from `docs/00-INDEX.md` or placed in `_archive/`/`_reference/`
|
|
- **Progress docs are authoritative:** `docs/progress/` is the single source of truth for project state
|
|
- **Archive structure:** Historical docs go in `docs/_archive/` (underscore indicates "not active doc surface")
|
|
- **Drift guards:** Key docs have standard headers (Purpose, Owner, Last Updated, Status)
|
|
|
|
### Why
|
|
|
|
- **Discoverability:** Contributors can find docs via the index
|
|
- **Prevents sprawl:** Index-first rule prevents undocumented files
|
|
- **Maintainability:** Drift guards (Last Updated, Status) help identify stale docs
|
|
- **Audit trail:** Archive preserves history without cluttering active navigation
|
|
- **Authority:** Progress docs are clearly marked as "truth" vs "guides"
|
|
|
|
### How
|
|
|
|
**Enforced by:** `docs/00-INDEX.md` (index-first rule) + documentation process
|
|
|
|
**Enforcement mechanism:**
|
|
1. **Index-first rule:** Stated in `docs/00-INDEX.md:298-305` (Maintenance section)
|
|
2. **Process enforcement:** Team must add new docs to index (not automatically enforced, but discoverability suffers if not followed)
|
|
3. **Drift guards:** Standard header format in progress docs:
|
|
```markdown
|
|
**Purpose:** [one sentence]
|
|
**Owner:** Development Team
|
|
**Last Updated:** YYYY-MM-DD
|
|
**Status:** active|archived
|
|
```
|
|
4. **Archive structure:** `docs/_archive/` clearly separated from active docs
|
|
|
|
**Location:**
|
|
- Index: `docs/00-INDEX.md` (central navigation hub)
|
|
- Index-first rule: `docs/00-INDEX.md:298-305` (Maintenance section)
|
|
- Progress docs: `docs/progress/` (authoritative state)
|
|
- Archive: `docs/_archive/` (historical artifacts)
|
|
|
|
**Verification command:**
|
|
```bash
|
|
# Manual review:
|
|
# 1. Check that new docs are in index
|
|
# 2. Verify progress docs have drift guards
|
|
# 3. Confirm archive structure is standardized
|
|
```
|
|
|
|
### Where
|
|
|
|
- **Index:** `docs/00-INDEX.md` (central navigation hub)
|
|
- **Index-first rule:** `docs/00-INDEX.md:298-305` (Maintenance section)
|
|
- **Progress docs:** `docs/progress/` (authoritative state)
|
|
- **Archive structure:** `docs/_archive/` (historical artifacts)
|
|
- **Policy definition:** `docs/progress/P2-DESIGN.md:105-113` (Documentation Structure section)
|
|
|
|
---
|
|
|
|
## 6. Baseline Tag Integrity
|
|
|
|
### What
|
|
|
|
The baseline tag `v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete` represents a known-good architectural baseline where all invariants are enforced. Future work must not invalidate this baseline.
|
|
|
|
**Specific rules:**
|
|
- Baseline tag: `v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete`
|
|
- This tag represents:
|
|
- All P0 invariants enforced (packaging, CI authority, exports)
|
|
- All P1.4 invariants enforced (core module purity)
|
|
- All P1.5 invariants enforced (documentation structure)
|
|
- All P2.6 invariants enforced (type safety)
|
|
- All P2.7 invariants enforced (system invariants documentation)
|
|
- All tooling in place (`verify.sh`, `ci/run.sh`)
|
|
- Future work must not require rollback to this baseline
|
|
- Future work must not break any invariant enforced at baseline
|
|
|
|
### Why
|
|
|
|
- **Safety anchor:** Provides a known-good state to rollback to if needed
|
|
- **Reference point:** Future work can compare against baseline
|
|
- **Confidence:** Baseline represents a tested, stable state
|
|
- **Historical record:** Tag preserves the state where foundation was complete
|
|
|
|
### How
|
|
|
|
**Enforced by:** Git tag + process (not automatically enforced, but baseline must remain valid)
|
|
|
|
**Enforcement mechanism:**
|
|
1. **Git tag:** `v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete` exists in repository
|
|
2. **Process enforcement:** Team must not break baseline (CI will catch invariant violations)
|
|
3. **Validation:** Can verify baseline by checking out tag and running `./ci/run.sh` (should pass)
|
|
|
|
**Location:**
|
|
- Baseline tag: `v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete` (Git tag)
|
|
- Baseline description: `docs/progress/00-STATUS.md:126` (Baseline Tag section)
|
|
- Previous baseline: `v1.0.11-p0-p1.4-complete` (historical reference)
|
|
|
|
**Verification command:**
|
|
```bash
|
|
# Verify baseline is still valid:
|
|
git checkout v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete
|
|
./ci/run.sh # Should pass
|
|
git checkout - # Return to current branch
|
|
```
|
|
|
|
### Where
|
|
|
|
- **Baseline tag:** `v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete` (Git tag)
|
|
- **Baseline description:** `docs/progress/00-STATUS.md:126` (Baseline Tag section)
|
|
- **Previous baseline:** `v1.0.11-p0-p1.4-complete` (historical reference)
|
|
- **Status doc:** `docs/progress/00-STATUS.md:17-25` (What This Baseline Includes section)
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
### Invariant Enforcement Matrix
|
|
|
|
| Invariant | Enforced By | Hard-Fail? | Verification Command |
|
|
|-----------|-------------|------------|---------------------|
|
|
| Packaging | `verify.sh` → `check_package()` | ✅ Yes | `./ci/run.sh` |
|
|
| Core Purity | `verify.sh` → `check_core_source()` + `check_core_artifacts()` | ✅ Yes | `./ci/run.sh` |
|
|
| CI Authority | `ci/README.md` (contract) | ⚠️ Process | Manual review |
|
|
| Export Correctness | `verify.sh` → `check_build()` | ✅ Yes | `./ci/run.sh` |
|
|
| Documentation Structure | `docs/00-INDEX.md` (index-first rule) | ⚠️ Process | Manual review |
|
|
| Baseline Integrity | Git tag + process | ⚠️ Process | `git checkout v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete && ./ci/run.sh` |
|
|
|
|
**Legend:**
|
|
- ✅ **Hard-Fail:** CI automatically fails if violated
|
|
- ⚠️ **Process:** Enforced by process/documentation, not automatic
|
|
|
|
---
|
|
|
|
## For New Contributors
|
|
|
|
**Before making changes:**
|
|
1. Review relevant invariants above
|
|
2. Run `./ci/run.sh` to verify current state passes
|
|
3. Make your changes
|
|
4. Run `./ci/run.sh` again — it will catch invariant violations automatically
|
|
|
|
**If CI fails:**
|
|
- Read the error message — it explains which invariant was violated
|
|
- Check the "Where" section above for the enforcing code
|
|
- Fix the violation (or discuss changing the invariant if needed)
|
|
|
|
**If you need to change an invariant:**
|
|
1. Update this document (`docs/SYSTEM_INVARIANTS.md`)
|
|
2. Update the enforcing code (usually `scripts/verify.sh`)
|
|
3. Update any related documentation
|
|
4. Ensure the change is backward-compatible or properly versioned
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- **P2 Design:** `docs/progress/P2-DESIGN.md` — Defines P2 scope and constraints
|
|
- **Progress Status:** `docs/progress/00-STATUS.md` — Current status and packaging invariants
|
|
- **CI Documentation:** `ci/README.md` — Local CI usage and contract
|
|
- **Verification Script:** `scripts/verify.sh` — Implementation of invariant checks
|
|
|
|
---
|
|
|
|
**Last Updated:** 2025-12-22
|
|
**Maintained By:** Development Team
|
|
**Status:** active
|
|
|
|
---
|
|
|
|
## Type Safety Notes
|
|
|
|
**Policy:** All external boundaries use `unknown`, all data payloads use `Record<string, unknown>`. No `any` allowed except documented TypeScript limitations.
|
|
|
|
**Allowed Exception:**
|
|
- **`src/utils/PlatformServiceMixin.ts:258`** — `any[]` required for TypeScript mixin constructor pattern
|
|
- **Reason:** TypeScript's mixin pattern requires `any[]` for constructor arguments (language limitation, not design choice)
|
|
- **Status:** Documented with inline comment explaining the limitation
|
|
- **Verification:** `rg '\bany\b' src/` returns zero matches except this documented exception
|
|
|
|
**Verification:**
|
|
- Run `rg -n "\bany\b" src/ --type ts | grep -v "node_modules" | grep -v "test"` — should return only the documented exception
|
|
- All external boundaries (`src/web.ts`, plugin interfaces) use `unknown` for inputs
|
|
- All data payloads (`src/observability.ts`, `src/core/events.ts`) use `Record<string, unknown>`
|
|
|