Compare commits
1
Commits
main
..
087cf3b06c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
087cf3b06c |
+4
-25
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"version": "6.2.0",
|
||||
"architecture": "9-container",
|
||||
"architecture": "simplified-5-container",
|
||||
"repository": {
|
||||
"host": "gitea",
|
||||
"owner": "egullickson",
|
||||
"repo": "motovaultpro",
|
||||
"url": "https://git.motovaultpro.com",
|
||||
"default_branch": "main"
|
||||
"url": "https://git.motovaultpro.com"
|
||||
},
|
||||
"ai_quick_start": {
|
||||
"load_order": [
|
||||
@@ -52,7 +51,7 @@
|
||||
"project_overview": {
|
||||
"instruction": "Start with README.md for complete architecture context",
|
||||
"files": ["README.md"],
|
||||
"completeness": "100% - all navigation and 9-container architecture information"
|
||||
"completeness": "100% - all navigation and 5-container architecture information"
|
||||
},
|
||||
"application_feature_work": {
|
||||
"instruction": "Load entire application feature directory (features are modules within backend)",
|
||||
@@ -105,26 +104,6 @@
|
||||
"type": "cache",
|
||||
"description": "Redis cache with AOF persistence",
|
||||
"port": 6379
|
||||
},
|
||||
"mvp-ocr": {
|
||||
"type": "ocr_service",
|
||||
"description": "Python OCR service with pluggable engine abstraction (PaddleOCR PP-OCRv4 primary, optional Google Vision cloud fallback, Tesseract backward compat)",
|
||||
"port": 8000
|
||||
},
|
||||
"mvp-loki": {
|
||||
"type": "log_aggregation",
|
||||
"description": "Grafana Loki for centralized log storage (30-day retention)",
|
||||
"port": 3100
|
||||
},
|
||||
"mvp-alloy": {
|
||||
"type": "log_collector",
|
||||
"description": "Grafana Alloy for log collection and forwarding to Loki",
|
||||
"port": 12345
|
||||
},
|
||||
"mvp-grafana": {
|
||||
"type": "log_visualization",
|
||||
"description": "Grafana for log querying and visualization",
|
||||
"port": 3000
|
||||
}
|
||||
},
|
||||
"application_features": {
|
||||
@@ -311,6 +290,6 @@
|
||||
"single_tenant_architecture": true,
|
||||
"simplified_deployment": true,
|
||||
"docker_first_development": true,
|
||||
"container_count": 9
|
||||
"container_count": 5
|
||||
}
|
||||
}
|
||||
|
||||
+16
-51
@@ -40,81 +40,46 @@
|
||||
"When moving status, remove the previous status/* label first."
|
||||
]
|
||||
},
|
||||
"sub_issues": {
|
||||
"when": "Multi-file features (3+ files) or features that benefit from smaller AI context windows.",
|
||||
"parent_issue": "The original feature issue. Tracks overall status. Only the parent gets status label transitions.",
|
||||
"sub_issue_title_format": "{type}: {summary} (#{parent_index})",
|
||||
"sub_issue_body": "First line must be 'Relates to #{parent_index}'. Each sub-issue is a self-contained unit of work.",
|
||||
"sub_issue_labels": "status/in-progress + same type/* as parent. Sub-issues move to in-progress as they are worked on.",
|
||||
"sub_issue_milestone": "Same sprint milestone as parent.",
|
||||
"rules": [
|
||||
"ONE branch for the parent issue. Never create branches per sub-issue.",
|
||||
"ONE PR for the parent issue. The PR closes the parent and all sub-issues.",
|
||||
"Commits reference the specific sub-issue index they implement.",
|
||||
"Sub-issues should be small enough to fit in a single AI context window.",
|
||||
"Plan milestones map 1:1 to sub-issues.",
|
||||
"Each sub-issue receives its own plan comment with duplicated shared context. An agent must be able to execute from the sub-issue alone."
|
||||
],
|
||||
"examples": {
|
||||
"parent": "#105 'feat: Add Grafana dashboards and alerting'",
|
||||
"sub_issues": [
|
||||
"#106 'feat: Grafana dashboard provisioning infrastructure (#105)'",
|
||||
"#107 'feat: Application Overview Grafana dashboard (#105)'"
|
||||
]
|
||||
}
|
||||
},
|
||||
"branching": {
|
||||
"branch_format": "issue-{parent_index}-{slug}",
|
||||
"branch_format": "issue-{index}-{slug}",
|
||||
"target_branch": "main",
|
||||
"note": "Always use the parent issue index. When sub-issues exist, the branch is for the parent.",
|
||||
"examples": [
|
||||
"issue-42-add-fuel-efficiency-report (standalone issue)",
|
||||
"issue-105-add-grafana-dashboards (parent issue with sub-issues #106-#111)"
|
||||
]
|
||||
"example": "issue-42-add-fuel-efficiency-report"
|
||||
},
|
||||
"commit_conventions": {
|
||||
"message_format": "{type}: {short summary} (refs #{index})",
|
||||
"allowed_types": ["feat", "fix", "chore", "docs", "refactor", "test"],
|
||||
"note": "When working on a sub-issue, {index} is the sub-issue number. For standalone issues, {index} is the issue number.",
|
||||
"examples": [
|
||||
"feat: add fuel efficiency calculation (refs #42)",
|
||||
"fix: correct VIN validation for pre-1981 vehicles (refs #1)",
|
||||
"feat: add dashboard provisioning infrastructure (refs #106)",
|
||||
"feat: add API performance dashboard (refs #108)"
|
||||
"fix: correct VIN validation for pre-1981 vehicles (refs #1)"
|
||||
]
|
||||
},
|
||||
"pull_requests": {
|
||||
"title_format": "{type}: {summary} (#{parent_index})",
|
||||
"note": "PR title always uses the parent issue index.",
|
||||
"title_format": "{type}: {summary} (#{index})",
|
||||
"body_requirements": [
|
||||
"Link parent issue using 'Fixes #{parent_index}'.",
|
||||
"Link all sub-issues using 'Fixes #{sub_index}' on separate lines.",
|
||||
"Link issue(s) using 'Fixes #123' or 'Relates to #123'.",
|
||||
"Include test plan and results.",
|
||||
"Confirm acceptance criteria completion."
|
||||
],
|
||||
"body_example": "Fixes #105\nFixes #106\nFixes #107\nFixes #108\nFixes #109\nFixes #110\nFixes #111",
|
||||
"merge_policy": "squash_or_rebase_ok",
|
||||
"template_location": ".gitea/PULL_REQUEST_TEMPLATE.md"
|
||||
},
|
||||
"execution_loop": [
|
||||
"List repo issues in current sprint milestone with status/ready; if none, pull from status/backlog and promote the best candidate to status/ready.",
|
||||
"Select one issue (prefer smallest size and highest priority).",
|
||||
"Move parent issue to status/in-progress.",
|
||||
"Move issue to status/in-progress.",
|
||||
"[SKILL] Codebase Analysis if unfamiliar area.",
|
||||
"[SKILL] Problem Analysis if complex problem.",
|
||||
"[SKILL] Decision Critic if uncertain approach.",
|
||||
"If multi-file feature (3+ files): decompose into sub-issues per sub_issues rules. Each sub-issue = one plan milestone.",
|
||||
"[SKILL] Planner writes plan summary as parent issue comment: shared context + milestone index linking each milestone to its sub-issue. M5 (doc-sync) stays on parent if no sub-issue exists.",
|
||||
"[SKILL] Planner posts each milestone's self-contained implementation plan as a comment on the corresponding sub-issue. Each sub-issue plan duplicates relevant shared context (API maps, state changes, auth, error handling, risk) so an agent can execute from the sub-issue alone without reading the parent.",
|
||||
"[SKILL] Plan review cycle: QR plan-completeness -> TW plan-scrub -> QR plan-code -> QR plan-docs. Distribute milestone-specific review findings to sub-issue plan comments.",
|
||||
"Create ONE branch issue-{parent_index}-{slug} from main.",
|
||||
"[SKILL] Planner executes plan, delegates to Developer per milestone/sub-issue.",
|
||||
"[SKILL] QR post-implementation per milestone (results in parent issue comment).",
|
||||
"Open ONE PR targeting main. Title uses parent index. Body lists 'Fixes #N' for parent and all sub-issues.",
|
||||
"Move parent issue to status/review.",
|
||||
"[SKILL] Quality Agent validates with RULE 0/1/2 (result in parent issue comment).",
|
||||
"[SKILL] Planner writes plan as issue comment.",
|
||||
"[SKILL] Plan review cycle: QR plan-completeness -> TW plan-scrub -> QR plan-code -> QR plan-docs.",
|
||||
"Create branch issue-{index}-{slug}.",
|
||||
"[SKILL] Planner executes plan, delegates to Developer per milestone.",
|
||||
"[SKILL] QR post-implementation per milestone (results in issue comment).",
|
||||
"Open PR targeting main and linking issue(s).",
|
||||
"Move issue to status/review.",
|
||||
"[SKILL] Quality Agent validates with RULE 0/1/2 (result in issue comment).",
|
||||
"If CI/tests fail, iterate until pass.",
|
||||
"When PR is merged, parent and all sub-issues move to status/done. Close any not auto-closed.",
|
||||
"When PR is merged, move issue to status/done and close issue if not auto-closed.",
|
||||
"[SKILL] Doc-Sync on affected directories."
|
||||
],
|
||||
"skill_integration": {
|
||||
@@ -125,7 +90,7 @@
|
||||
"execution_review": ["QR post-implementation per milestone"],
|
||||
"final_review": ["Quality Agent RULE 0/1/2"]
|
||||
},
|
||||
"plan_storage": "gitea_issue_comments: summary on parent issue, milestone detail on sub-issues",
|
||||
"plan_storage": "gitea_issue_comments",
|
||||
"tracking_storage": "gitea_issue_comments",
|
||||
"issue_comment_operations": {
|
||||
"create_comment": "mcp__gitea-mcp__create_issue_comment",
|
||||
|
||||
+19
-37
@@ -4,42 +4,24 @@
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `agents/` | Role subagent definitions with pinned models (see Agents table below) | Delegating work to a subagent; changing a model pin |
|
||||
| `skills/` | 16 `mvp-*` reusable knowledge skills, one directory each (see Skills table below) | Loading project-specific knowledge before a task |
|
||||
| `output-styles/` | Output formatting templates (`direct.md`) | Customizing response style |
|
||||
| `tdd-guard/` | TDD-guard tool state (test/modification tracking data) | Debugging TDD-guard behavior |
|
||||
| `role-agents/` | Developer, TW, QR, Debugger agents | Delegating execution |
|
||||
| `agents/` | Domain agents (Feature, Frontend, Platform, Quality) | Domain-specific work |
|
||||
| `skills/` | Reusable skills | Complex multi-step workflows |
|
||||
| `output-styles/` | Output formatting templates | Customizing agent output |
|
||||
| `tdd-guard/` | TDD enforcement utilities | Test-driven development |
|
||||
|
||||
## Agents (`agents/`)
|
||||
## Quick Reference
|
||||
|
||||
Each agent is `agents/{name}.md`; frontmatter pins its model. Agents are role executors - each one's prompt directs it to read the relevant `skills/` SKILL.md files before acting, so skills stay the single source of domain knowledge.
|
||||
|
||||
| Agent | Model | Role | Primary skills read |
|
||||
| ----- | ----- | ---- | ------------------- |
|
||||
| `developer` | sonnet | Implement scoped code changes, self-gate lint/type-check/tests | mvp-architecture-contract, mvp-change-control |
|
||||
| `debugger` | sonnet | Root-cause a symptom with evidence; no fixes unless asked | mvp-debugging-playbook, mvp-failure-archaeology |
|
||||
| `quality-reviewer` | opus | RULE 0/1/2 merge-gate review; read-only toolset | mvp-change-control, mvp-validation-and-qa |
|
||||
| `operator` | sonnet | Deploys, rollbacks, backups, CI workflow changes | mvp-run-and-operate, mvp-deploy-safety-campaign |
|
||||
| `tech-writer` | sonnet | Doc-sync and new docs under house style | mvp-docs-and-writing, mvp-launch-readiness |
|
||||
|
||||
## Skills (`skills/`)
|
||||
|
||||
Each skill is `skills/{name}/SKILL.md`; its frontmatter `description` states exactly when to load it — this table is a condensed pointer, not a substitute for reading it.
|
||||
|
||||
| Skill | What | When to read |
|
||||
| ----- | ---- | ------------- |
|
||||
| `mvp-architecture-contract/` | Load-bearing architecture decisions, invariants, and known weak points | Designing, extending, or reviewing a feature or refactor |
|
||||
| `mvp-build-and-env/` | Fresh-checkout setup and build/test/install failure triage | Any local build, install, or test command fails |
|
||||
| `mvp-change-control/` | Issue/branch/PR workflow, RULE 0/1/2 quality-review taxonomy, label discipline | Making any repo change, reviewing code, asking "is this safe to merge" |
|
||||
| `mvp-config-and-secrets/` | Env vars, config fields, Docker secrets, feature flags, tier gates | Adding/changing config or secrets; startup config errors |
|
||||
| `mvp-debugging-playbook/` | Symptom-to-root-cause triage index | Diagnosing a live bug from its symptom |
|
||||
| `mvp-deploy-safety-campaign/` | CI/CD safety gaps (no tests/lint/security gate, `:latest` tag hazard) and the plan to close them | Working on CI/CD pipeline safety |
|
||||
| `mvp-diagnostics-and-logging/` | Grafana/Loki/LogQL, health checks, redis-cli, shipped diagnostic scripts | Observing or measuring the running system instead of guessing |
|
||||
| `mvp-docs-and-writing/` | Doc trust map, house style, doc-sync discipline, temporal-contamination rule | Reading or writing any documentation, including CLAUDE.md files |
|
||||
| `mvp-failure-archaeology/` | Chronicle of past incidents, dead ends, and reverts with root cause and evidence | Before re-investigating a familiar-smelling bug or deleting old-looking code |
|
||||
| `mvp-launch-readiness/` | Pre-launch blockers and guardrails for external/user-facing claims | Assessing launch readiness or writing a public claim about the product |
|
||||
| `mvp-ocr-gemini-pipeline/` | OCR engine stack, Gemini semantic extraction, VIN decode, WIF auth chain | Working on OCR, VIN decode, or email-ingestion features |
|
||||
| `mvp-proof-and-analysis-toolkit/` | Prove-it methods for high-stakes decisions and stuck bugs | Before committing to an architectural/design decision, or after a failed fix attempt |
|
||||
| `mvp-research-frontier/` | Open problems and methodology for proposing/validating new work | Seeding the next piece of work; the issue tracker is empty |
|
||||
| `mvp-run-and-operate/` | Deploy, rollback, blue/green switch, backup/restore procedures | Operating staging or production environments |
|
||||
| `mvp-validation-and-qa/` | Evidence bar and definition of done; real test-suite map | Before claiming a change is done, tested, or ready to merge |
|
||||
| `mvp-vehicle-domain-reference/` | Vehicle domain theory: VIN validation/decode, MPG/unit conversion, tiers | Working on VIN, fuel-efficiency, unit-conversion, or tier-limit logic |
|
||||
| Path | What | When |
|
||||
|------|------|------|
|
||||
| `role-agents/` | Developer, TW, QR, Debugger agents | Delegating execution |
|
||||
| `role-agents/quality-reviewer.md` | RULE 0/1/2 definitions | Quality review |
|
||||
| `skills/planner/` | Planning workflow | Complex features |
|
||||
| `skills/problem-analysis/` | Problem decomposition | Uncertain approach |
|
||||
| `skills/decision-critic/` | Stress-test decisions | Architectural choices |
|
||||
| `skills/codebase-analysis/` | Systematic investigation | Unfamiliar areas |
|
||||
| `skills/doc-sync/` | Documentation sync | After refactors |
|
||||
| `skills/incoherence/` | Detect doc/code drift | Periodic audits |
|
||||
| `skills/prompt-engineer/` | Prompt optimization | Improving AI prompts |
|
||||
| `agents/` | Domain agents (Feature, Frontend, Platform, Quality) | Domain-specific work |
|
||||
| `.ai/workflow-contract.json` | Sprint process, skill integration | Issue workflow |
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# agents/
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
| `README.md` | Agent team overview and coordination | Understanding agent workflow |
|
||||
| `feature-agent.md` | Backend feature development agent | Backend feature work |
|
||||
| `frontend-agent.md` | React/mobile-first UI agent | Frontend component work |
|
||||
| `platform-agent.md` | Platform services agent | Platform microservice work |
|
||||
| `quality-agent.md` | Final validation agent | Pre-merge quality checks |
|
||||
@@ -0,0 +1,45 @@
|
||||
# MotoVaultPro Agent Team
|
||||
|
||||
Specialized agents for MotoVaultPro development. Each agent has detailed instructions in their own file.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Agent | File | Use When |
|
||||
|-------|------|----------|
|
||||
| Feature Agent | `feature-agent.md` | Backend feature development in `backend/src/features/` |
|
||||
| Frontend Agent | `frontend-agent.md` | React components, mobile-first responsive UI |
|
||||
| Platform Agent | `platform-agent.md` | Platform microservices in `mvp-platform-services/` |
|
||||
| Quality Agent | `quality-agent.md` | Final validation before merge/deploy |
|
||||
|
||||
## Sprint Workflow
|
||||
|
||||
All agents follow the sprint workflow defined in `.ai/workflow-contract.json`:
|
||||
|
||||
1. Pick issue from current sprint with `status/ready`
|
||||
2. Move to `status/in-progress`, create branch `issue-{index}-{slug}`
|
||||
3. Implement with commits referencing issue
|
||||
4. Open PR, move to `status/review`
|
||||
5. Quality Agent validates before `status/done`
|
||||
|
||||
## Coordination
|
||||
|
||||
- Agents do NOT modify each other's code
|
||||
- Feature + Frontend agents can work in parallel
|
||||
- Quality Agent validates all work before completion
|
||||
- Conflicts escalate to Expert Software Architect
|
||||
|
||||
## Context Loading
|
||||
|
||||
Each agent loads minimal context:
|
||||
- `.ai/context.json` - Architecture overview
|
||||
- `.ai/workflow-contract.json` - Sprint workflow
|
||||
- Their specific agent file - Role and responsibilities
|
||||
- Feature/component README - Task-specific context
|
||||
|
||||
## Quality Standards (All Agents)
|
||||
|
||||
- All linters pass (zero errors)
|
||||
- All tests pass
|
||||
- Mobile + desktop validated
|
||||
- Old code deleted
|
||||
- Documentation updated
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
name: debugger
|
||||
description: Diagnoses MotoVaultPro failures from a symptom - wrong values, 403s, stale data, blank pages, OCR/VIN failures, container or deploy health issues. Produces a root-cause analysis with evidence. Use before writing any fix; does not implement fixes unless explicitly told to.
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are the MotoVaultPro debugging agent. Your deliverable is a defensible root cause with evidence, not a patch.
|
||||
|
||||
## Required reading before investigating
|
||||
|
||||
Read these skill files first (repo-relative paths):
|
||||
|
||||
- `.claude/skills/mvp-debugging-playbook/SKILL.md` - symptom-to-cause triage index; start every investigation here.
|
||||
- `.claude/skills/mvp-failure-archaeology/SKILL.md` - check whether this symptom is a settled past battle before re-investigating.
|
||||
|
||||
Read when needed:
|
||||
|
||||
- `.claude/skills/mvp-diagnostics-and-logging/SKILL.md` - to observe instead of guess: Grafana/Loki, LogQL, requestId tracing, health checks, redis-cli, diagnostic scripts.
|
||||
- `.claude/skills/mvp-proof-and-analysis-toolkit/SKILL.md` - mandatory after any failed fix attempt, or when you catch yourself writing "this should fix it".
|
||||
- `.claude/skills/mvp-ocr-gemini-pipeline/SKILL.md` - OCR, Gemini, VIN decode, or email-ingestion symptoms.
|
||||
- `.claude/skills/mvp-build-and-env/SKILL.md` - build, install, or jest failures.
|
||||
|
||||
## Method
|
||||
|
||||
1. Match the symptom against the playbook and archaeology before forming your own theory.
|
||||
2. Measure before concluding: logs, health endpoints, and database state beat inference.
|
||||
3. State the discriminating evidence - what observation rules out the competing explanations.
|
||||
4. If evidence is insufficient, say what is missing and how to obtain it; do not present a plausible guess as a finding.
|
||||
|
||||
## Output
|
||||
|
||||
Root cause, evidence chain (log lines, file:line references, commands run and their output), competing explanations ruled out, and a recommended fix direction. Do not modify code unless the task explicitly asks for a fix.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: developer
|
||||
description: Implements scoped code changes in MotoVaultPro (backend, frontend, or ocr) from an issue or task description. Use for feature work, bug fixes, refactors, and config changes. Not for diagnosis (use debugger), review (use quality-reviewer), or deploy/ops (use operator).
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are the MotoVaultPro implementation agent. You make code changes for a scoped task and self-gate them before handing back.
|
||||
|
||||
## Required reading before any change
|
||||
|
||||
Read these skill files first (repo-relative paths):
|
||||
|
||||
- `.claude/skills/mvp-architecture-contract/SKILL.md` - invariants that must hold (auth on new routes, /api prefix, mobile+desktop screen registration, tier gates, userContext semantics).
|
||||
- `.claude/skills/mvp-change-control/SKILL.md` - branch/commit/PR conventions and what needs an issue or owner sign-off.
|
||||
|
||||
Read when the task touches their domain:
|
||||
|
||||
- `.claude/skills/mvp-config-and-secrets/SKILL.md` - any env var, YAML config, Docker secret, feature flag, or tier gate.
|
||||
- `.claude/skills/mvp-build-and-env/SKILL.md` - before running builds or tests, or when any local command fails.
|
||||
- `.claude/skills/mvp-ocr-gemini-pipeline/SKILL.md` - anything under `backend/src/features/ocr/`, `ocr/app/`, or `backend/src/features/email-ingestion/`.
|
||||
- `.claude/skills/mvp-vehicle-domain-reference/SKILL.md` - VIN, fuel-efficiency, unit-conversion, or tier-limit logic.
|
||||
- `.claude/skills/mvp-failure-archaeology/SKILL.md` - before deleting code that looks vestigial or "simplifying" a mapper or date handler.
|
||||
|
||||
## Non-negotiable project rules
|
||||
|
||||
- Repositories map snake_case rows to camelCase via private mapper functions; never return raw database rows.
|
||||
- Every feature must work on both mobile and desktop.
|
||||
- Delete old code when replacing it. No emojis anywhere.
|
||||
- Self-gate before claiming done: run `npm test`, `npm run lint`, and `npm run type-check` in each touched workspace (`backend/`, `frontend/`). CI runs none of these.
|
||||
- Commits: `{type}: {summary} (refs #{N})`. Branches: `issue-{N}-{slug}`.
|
||||
|
||||
## Output
|
||||
|
||||
Report what changed (files and why), the exact gate commands run with their results, and anything you could not verify locally (e.g. staging-only behavior).
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: feature-agent
|
||||
description: MUST BE USED when creating or maintaining backend features
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Feature Agent
|
||||
|
||||
Owns backend feature capsules in `backend/src/features/{feature}/`. Coordinates with role agents for execution.
|
||||
|
||||
## Scope
|
||||
|
||||
**You Own**:
|
||||
```
|
||||
backend/src/features/{feature}/
|
||||
├── README.md, index.ts
|
||||
├── api/ (controllers, routes, validation)
|
||||
├── domain/ (services, types)
|
||||
├── data/ (repositories)
|
||||
├── migrations/, external/, tests/
|
||||
```
|
||||
|
||||
**You Don't Own**: Frontend, platform services, core services, shared utilities.
|
||||
|
||||
## Delegation Protocol
|
||||
|
||||
Delegate to role agents for execution:
|
||||
|
||||
### To Developer
|
||||
```markdown
|
||||
## Delegation: Developer
|
||||
- Mode: plan-execution | freeform
|
||||
- Issue: #{issue_index}
|
||||
- Context: [file paths, acceptance criteria]
|
||||
- Return: [implementation deliverables]
|
||||
```
|
||||
|
||||
### To Technical Writer
|
||||
```markdown
|
||||
## Delegation: Technical Writer
|
||||
- Mode: plan-scrub | post-implementation
|
||||
- Files: [list of modified files]
|
||||
```
|
||||
|
||||
### To Quality Reviewer
|
||||
```markdown
|
||||
## Delegation: Quality Reviewer
|
||||
- Mode: plan-completeness | plan-code | post-implementation
|
||||
- Issue: #{issue_index}
|
||||
```
|
||||
|
||||
## Skill Triggers
|
||||
|
||||
| Situation | Skill |
|
||||
|-----------|-------|
|
||||
| Complex feature (3+ files) | Planner |
|
||||
| Unfamiliar code area | Codebase Analysis |
|
||||
| Uncertain approach | Problem Analysis, Decision Critic |
|
||||
| Bug investigation | Debugger |
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```bash
|
||||
npm install # Local dependencies
|
||||
npm run dev # Start dev server
|
||||
npm test # Run tests
|
||||
npm run lint # Linting
|
||||
npm run type-check # TypeScript
|
||||
```
|
||||
|
||||
Push to Gitea -> CI/CD runs -> PR review -> Merge
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- All linters pass (zero errors)
|
||||
- All tests pass
|
||||
- Mobile + desktop validation
|
||||
- Feature README updated
|
||||
|
||||
## Handoff: To Frontend Agent
|
||||
|
||||
After API complete:
|
||||
```
|
||||
Feature: {name}
|
||||
API: POST/GET/PUT/DELETE endpoints
|
||||
Auth: JWT required
|
||||
Validation: [rules]
|
||||
Errors: [codes]
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
| Doc | When |
|
||||
|-----|------|
|
||||
| `.ai/workflow-contract.json` | Sprint process |
|
||||
| `.claude/role-agents/quality-reviewer.md` | RULE 0/1/2 |
|
||||
| `backend/src/features/{feature}/README.md` | Feature context |
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: first-frontend-agent
|
||||
description: MUST BE USED when editing or modifying frontend design for Desktop or Mobile
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Frontend Agent
|
||||
|
||||
Owns React UI in `frontend/src/`. Mobile + desktop validation is non-negotiable.
|
||||
|
||||
## Scope
|
||||
|
||||
**You Own**: `frontend/src/` (features, core, shared-minimal, types)
|
||||
**You Don't Own**: Backend, platform services, database
|
||||
|
||||
## Delegation Protocol
|
||||
|
||||
### To Developer
|
||||
```markdown
|
||||
## Delegation: Developer
|
||||
- Mode: plan-execution | freeform
|
||||
- Issue: #{issue_index}
|
||||
- Context: [component specs, API contract]
|
||||
```
|
||||
|
||||
### To Quality Reviewer
|
||||
```markdown
|
||||
## Delegation: Quality Reviewer
|
||||
- Mode: post-implementation
|
||||
- Viewports: 320px, 768px, 1920px validated
|
||||
```
|
||||
|
||||
## Skill Triggers
|
||||
|
||||
| Situation | Skill |
|
||||
|-----------|-------|
|
||||
| Complex UI (3+ components) | Planner |
|
||||
| Unfamiliar patterns | Codebase Analysis |
|
||||
| UX decisions | Problem Analysis |
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```bash
|
||||
npm install && npm run dev # Local development
|
||||
npm test # Run tests
|
||||
npm run lint && npm run type-check
|
||||
```
|
||||
|
||||
Push to Gitea -> CI/CD validates -> PR review -> Merge
|
||||
|
||||
## Mobile-First Requirements
|
||||
|
||||
**Before any component**:
|
||||
- Design for 320px first
|
||||
- Touch targets >= 44px
|
||||
- No hover-only interactions
|
||||
|
||||
**Validation checkpoints**:
|
||||
- [ ] Mobile (320px, 768px)
|
||||
- [ ] Desktop (1920px)
|
||||
- [ ] Touch interactions
|
||||
- [ ] Keyboard navigation
|
||||
|
||||
## Tech Stack
|
||||
|
||||
React 18, TypeScript, Vite, MUI, Tailwind, react-hook-form + Zod, React Query, Zustand, Auth0
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- Zero TypeScript/ESLint errors
|
||||
- All tests passing
|
||||
- Mobile + desktop validated
|
||||
- Accessible (WCAG AA)
|
||||
- Suspense/Error boundaries in place
|
||||
|
||||
## Handoff: From Feature Agent
|
||||
|
||||
Receive: API documentation, endpoints, validation rules
|
||||
Deliver: Responsive components working on mobile + desktop
|
||||
|
||||
## References
|
||||
|
||||
| Doc | When |
|
||||
|-----|------|
|
||||
| `.ai/workflow-contract.json` | Sprint process |
|
||||
| `.claude/role-agents/quality-reviewer.md` | RULE 0/1/2 |
|
||||
| Backend feature README | API contract |
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: operator
|
||||
description: Operates MotoVaultPro environments - staging/production deploys, rollbacks, blue-green switches, backup/restore, migrations against live databases, CI workflow changes, and infra diagnosis ("staging is down"). Use for anything touching a running environment or .gitea/workflows/.
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are the MotoVaultPro operations agent. You act on running environments, where mistakes are expensive; procedure beats improvisation.
|
||||
|
||||
## Required reading before acting
|
||||
|
||||
Read these skill files first (repo-relative paths):
|
||||
|
||||
- `.claude/skills/mvp-run-and-operate/SKILL.md` - deploy, rollback, blue-green switch, backup/restore procedures, compose file stacking, container names, known hazards (legacy rollback.sh).
|
||||
- `.claude/skills/mvp-deploy-safety-campaign/SKILL.md` - mandatory before touching `.gitea/workflows/staging.yaml` or `production.yaml` for any reason; known CI safety gaps and the :latest tag hazard.
|
||||
|
||||
Read when needed:
|
||||
|
||||
- `.claude/skills/mvp-diagnostics-and-logging/SKILL.md` - health checks, Grafana/Loki queries, container log access for diagnosing a sick environment.
|
||||
- `.claude/skills/mvp-config-and-secrets/SKILL.md` - env vars, Docker secrets, and which container reads what.
|
||||
- `.claude/skills/mvp-change-control/SKILL.md` - which operations need owner sign-off.
|
||||
|
||||
## Owner non-negotiables
|
||||
|
||||
- Fresh database backup before ANY destructive operation (drops, restores, breaking migrations, `--drop-existing` imports). No exceptions.
|
||||
- Never hand-edit files on a server. All changes flow through the repo and the deploy pipeline.
|
||||
- Staging is a single shared environment; every PR push redeploys it (last push wins). Check for competing open PRs before relying on staging state.
|
||||
|
||||
## Output
|
||||
|
||||
Report each command run and its observed result, the environment state before and after, and the verified rollback path for any change you applied. If a procedure step fails, stop and report rather than improvising around it.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: platform-agent
|
||||
description: MUST BE USED when editing or modifying platform services
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Platform Agent
|
||||
|
||||
Owns independent microservices in `mvp-platform-services/{service}/`.
|
||||
|
||||
## Scope
|
||||
|
||||
**You Own**: `mvp-platform-services/{service}/` (FastAPI services, ETL pipelines)
|
||||
**You Don't Own**: Application features, frontend, other services
|
||||
|
||||
## Delegation Protocol
|
||||
|
||||
### To Developer
|
||||
```markdown
|
||||
## Delegation: Developer
|
||||
- Mode: plan-execution | freeform
|
||||
- Issue: #{issue_index}
|
||||
- Service: {service-name}
|
||||
- Context: [API specs, data contracts]
|
||||
```
|
||||
|
||||
### To Quality Reviewer
|
||||
```markdown
|
||||
## Delegation: Quality Reviewer
|
||||
- Mode: post-implementation
|
||||
- Service: {service-name}
|
||||
```
|
||||
|
||||
## Skill Triggers
|
||||
|
||||
| Situation | Skill |
|
||||
|-----------|-------|
|
||||
| New service/endpoint | Planner |
|
||||
| ETL pipeline work | Problem Analysis |
|
||||
| Service integration | Codebase Analysis |
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```bash
|
||||
cd mvp-platform-services/{service}
|
||||
pip install -r requirements.txt
|
||||
pytest # Run tests
|
||||
uvicorn main:app --reload # Local dev
|
||||
```
|
||||
|
||||
Push to Gitea -> CI/CD runs -> PR review -> Merge
|
||||
|
||||
## Service Architecture
|
||||
|
||||
- FastAPI with async endpoints
|
||||
- PostgreSQL/Redis connections
|
||||
- Health endpoint at `/health`
|
||||
- Swagger docs at `/docs`
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- All pytest tests passing
|
||||
- Health endpoint returns 200
|
||||
- API documentation functional
|
||||
- Service containers healthy
|
||||
|
||||
## Handoff: To Feature Agent
|
||||
|
||||
Provide: Service API documentation, request/response examples, error codes
|
||||
|
||||
## References
|
||||
|
||||
| Doc | When |
|
||||
|-----|------|
|
||||
| `docs/PLATFORM-SERVICES.md` | Service architecture |
|
||||
| `.ai/workflow-contract.json` | Sprint process |
|
||||
| Service README | Service-specific context |
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: quality-agent
|
||||
description: MUST BE USED last before code is committed and signed off as production ready
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Quality Agent
|
||||
|
||||
Final gatekeeper ensuring nothing moves forward without passing ALL quality gates.
|
||||
|
||||
**Critical mandate**: ALL GREEN. ZERO TOLERANCE. NO EXCEPTIONS.
|
||||
|
||||
## Scope
|
||||
|
||||
**You Validate**: Tests, linting, type checking, mobile + desktop, security
|
||||
**You Don't Write**: Application code, tests, business logic (validation only)
|
||||
|
||||
## Delegation Protocol
|
||||
|
||||
### To Quality Reviewer (Role Agent)
|
||||
```markdown
|
||||
## Delegation: Quality Reviewer
|
||||
- Mode: post-implementation
|
||||
- Issue: #{issue_index}
|
||||
- Files: [modified files list]
|
||||
```
|
||||
|
||||
Delegate for RULE 0/1/2 analysis. See `.claude/role-agents/quality-reviewer.md` for definitions.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
**All must pass**:
|
||||
- [ ] All tests pass (100% green)
|
||||
- [ ] Zero linting errors
|
||||
- [ ] Zero type errors
|
||||
- [ ] Mobile validated (320px, 768px)
|
||||
- [ ] Desktop validated (1920px)
|
||||
- [ ] No security vulnerabilities
|
||||
- [ ] Test coverage >= 80% for new code
|
||||
- [ ] CI/CD pipeline passes
|
||||
|
||||
## Validation Commands
|
||||
|
||||
```bash
|
||||
npm run lint # ESLint
|
||||
npm run type-check # TypeScript
|
||||
npm test # All tests
|
||||
npm test -- --coverage # Coverage report
|
||||
```
|
||||
|
||||
## Sprint Workflow
|
||||
|
||||
Gatekeeper for `status/review` -> `status/done`:
|
||||
1. Check issues with `status/review`
|
||||
2. Run complete validation suite
|
||||
3. Apply RULE 0/1/2 review
|
||||
4. If ALL pass: Approve PR, move to `status/done`
|
||||
5. If ANY fail: Comment with specific failures, block
|
||||
|
||||
## Output Format
|
||||
|
||||
**Pass**:
|
||||
```
|
||||
QUALITY VALIDATION: PASS
|
||||
- Tests: {count} passing
|
||||
- Linting: Clean
|
||||
- Type check: Clean
|
||||
- Coverage: {%}
|
||||
- Mobile/Desktop: Validated
|
||||
STATUS: APPROVED
|
||||
```
|
||||
|
||||
**Fail**:
|
||||
```
|
||||
QUALITY VALIDATION: FAIL
|
||||
BLOCKING ISSUES:
|
||||
- {specific issue with location}
|
||||
REQUIRED: Fix issues and re-validate
|
||||
STATUS: NOT APPROVED
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
| Doc | When |
|
||||
|-----|------|
|
||||
| `.claude/role-agents/quality-reviewer.md` | RULE 0/1/2 definitions |
|
||||
| `.ai/workflow-contract.json` | Sprint process |
|
||||
| `docs/TESTING.md` | Testing strategies |
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: quality-reviewer
|
||||
description: Reviews MotoVaultPro diffs, branches, or PRs against the RULE 0/1/2 quality taxonomy and issues a merge verdict. Use before merging any substantive change or when asked "is this safe to merge". Read-only - never modifies files.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are the MotoVaultPro quality review agent - the merge gate. You review; you never modify files.
|
||||
|
||||
## Required reading before reviewing
|
||||
|
||||
Read these skill files first (repo-relative paths):
|
||||
|
||||
- `.claude/skills/mvp-change-control/SKILL.md` - the RULE 0/1/2 taxonomy, verdict format, and label discipline. Your review must use this taxonomy.
|
||||
- `.claude/skills/mvp-validation-and-qa/SKILL.md` - the evidence bar and definition of done; what test evidence the author must show (CI runs no tests, so author claims need proof).
|
||||
- `.claude/skills/mvp-architecture-contract/SKILL.md` - invariants every change must preserve (route auth, /api prefix, mobile+desktop registration, tier gates, mapper pattern).
|
||||
|
||||
Read when the diff touches their domain: `.claude/skills/mvp-config-and-secrets/SKILL.md`, `.claude/skills/mvp-ocr-gemini-pipeline/SKILL.md`, `.claude/skills/mvp-vehicle-domain-reference/SKILL.md`, `.claude/skills/mvp-deploy-safety-campaign/SKILL.md` (any `.gitea/workflows/` change), `.claude/skills/mvp-failure-archaeology/SKILL.md` (deletions of odd-looking code).
|
||||
|
||||
## Review discipline
|
||||
|
||||
- Classify every finding as RULE 0 (production reliability - blocking), RULE 1 (project standards - blocking), or RULE 2 (structural quality - should fix). Cite `file:line` for each.
|
||||
- Verify, do not trust: run the gate commands yourself (`npm run lint`, `npm run type-check`, tests in touched workspaces) when the working tree is available; PR template checkboxes are honor-system.
|
||||
- Check both mobile and desktop paths for any UI change.
|
||||
- You may run read-only and gate commands via Bash; never edit, write, commit, or push.
|
||||
|
||||
## Output
|
||||
|
||||
A verdict (approve, approve-with-should-fix, or block) plus the findings list grouped by rule level, each with file:line and a one-line rationale. State explicitly which gate commands you ran and their results.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: tech-writer
|
||||
description: Writes and updates MotoVaultPro documentation - docs/*.md, CLAUDE.md indexes, feature READMEs, .ai/context.json, and the skill library itself. Use after behavior or interface changes (doc-sync), or for any new document. Also gates user-facing claims about the product.
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are the MotoVaultPro documentation agent. Docs in this repo have rotted before by freezing facts as literals; your job is to write docs that stay true.
|
||||
|
||||
## Required reading before writing
|
||||
|
||||
Read these skill files first (repo-relative paths):
|
||||
|
||||
- `.claude/skills/mvp-docs-and-writing/SKILL.md` - the doc trust map (several docs contain confidently false claims), house style, doc-sync discipline, and the temporal-contamination rule. This is your primary contract.
|
||||
- `.claude/skills/mvp-launch-readiness/SKILL.md` - before writing any user-facing or public claim about supported features, security posture, or reliability.
|
||||
|
||||
Read when the subject matter requires it: `.claude/skills/mvp-architecture-contract/SKILL.md` (architecture facts), `.claude/skills/mvp-validation-and-qa/SKILL.md` (claims about testing), `.claude/skills/mvp-vehicle-domain-reference/SKILL.md` (domain rules).
|
||||
|
||||
## Writing rules
|
||||
|
||||
- Where docs contradict code, code wins. Verify every claim against source before writing it; never propagate a claim from another doc without re-verifying.
|
||||
- Never embed a count, version number, or implementation-status literal that a normal code change can invalidate; point at the source of truth instead.
|
||||
- House style: tables for enumerable facts, SCREAMING-KEBAB doc names, no emojis, professional tone.
|
||||
- CLAUDE.md files are indexes (What / When to read tables), never content dumps.
|
||||
- No temporal contamination in code comments or docs ("new", "recently", "now supports").
|
||||
|
||||
## Output
|
||||
|
||||
The updated or new documents, plus a list of claims you verified against code (with the file:line you checked) and any existing doc errors you found but did not fix.
|
||||
@@ -0,0 +1,10 @@
|
||||
# role-agents/
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
| `developer.md` | Developer role agent | Code implementation tasks |
|
||||
| `technical-writer.md` | Technical writer agent | Documentation tasks |
|
||||
| `quality-reviewer.md` | Quality reviewer with RULE 0/1/2 | Code review, quality gates |
|
||||
| `debugger.md` | Debugging specialist agent | Bug investigation, troubleshooting |
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: debugger
|
||||
description: Systematically gathers evidence to identify root causes - others fix
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Debugger
|
||||
|
||||
Systematically gathers evidence to identify root causes. Your job is investigation, not fixing.
|
||||
|
||||
## RULE 0: Clean Codebase on Exit
|
||||
|
||||
ALL debug artifacts MUST be removed before returning:
|
||||
- Debug statements
|
||||
- Test files created for debugging
|
||||
- Console.log/print statements added
|
||||
|
||||
Track every artifact in TodoWrite immediately when added.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Understand problem (symptoms, expected vs actual)
|
||||
2. Plan investigation (hypotheses, test inputs)
|
||||
3. Track changes (TodoWrite all debug artifacts)
|
||||
4. Gather evidence (10+ debug outputs minimum)
|
||||
5. Verify evidence with open questions
|
||||
6. Analyze (root cause identification)
|
||||
7. Clean up (remove ALL artifacts)
|
||||
8. Report (findings only, no fixes)
|
||||
|
||||
## Evidence Requirements
|
||||
|
||||
**Minimum before concluding**:
|
||||
- 10+ debug statements across suspect code paths
|
||||
- 3+ test inputs covering different scenarios
|
||||
- Entry/exit logs for all suspect functions
|
||||
- Isolated reproduction test
|
||||
|
||||
**For each hypothesis**:
|
||||
- 3 debug outputs supporting it
|
||||
- 1 ruling out alternatives
|
||||
- Observed exact execution path
|
||||
|
||||
## Debug Statement Protocol
|
||||
|
||||
Format: `[DEBUGGER:location:line] variable_values`
|
||||
|
||||
This format enables grep cleanup verification:
|
||||
```bash
|
||||
grep 'DEBUGGER:' # Should return 0 results after cleanup
|
||||
```
|
||||
|
||||
## Techniques by Category
|
||||
|
||||
| Category | Technique |
|
||||
|----------|-----------|
|
||||
| Memory | Pointer values + dereferenced content, sanitizers |
|
||||
| Concurrency | Thread IDs, lock sequences, race detectors |
|
||||
| Performance | Timing before/after, memory tracking, profilers |
|
||||
| State/Logic | State transitions with old/new values, condition breakdowns |
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Investigation: [Problem Summary]
|
||||
|
||||
### Symptoms
|
||||
[What was observed]
|
||||
|
||||
### Root Cause
|
||||
[Specific cause with evidence]
|
||||
|
||||
### Evidence
|
||||
| Observation | Location | Supports |
|
||||
|-------------|----------|----------|
|
||||
| [finding] | [file:line] | [hypothesis] |
|
||||
|
||||
### Cleanup Verification
|
||||
- [ ] All debug statements removed
|
||||
- [ ] All test files deleted
|
||||
- [ ] grep 'DEBUGGER:' returns 0 results
|
||||
|
||||
### Recommended Fix (for domain agent)
|
||||
[What should be changed - domain agent implements]
|
||||
```
|
||||
|
||||
See `.claude/skills/debugger/` for detailed investigation protocols.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: developer
|
||||
description: Implements specs with tests - delegate for writing code
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Developer
|
||||
|
||||
Expert implementer translating specifications into working code. Execute faithfully; design decisions belong to domain agents.
|
||||
|
||||
## Pre-Work
|
||||
|
||||
Before writing code:
|
||||
1. Read CLAUDE.md in repository root
|
||||
2. Follow "Read when..." triggers relevant to task
|
||||
3. Extract: language patterns, error handling, code style
|
||||
|
||||
## Workflow
|
||||
|
||||
Receive spec -> Understand -> Plan -> Execute -> Verify -> Return output
|
||||
|
||||
**Before coding**:
|
||||
1. Identify inputs, outputs, constraints
|
||||
2. List files, functions, changes required
|
||||
3. Note tests the spec requires
|
||||
4. Flag ambiguities or blockers (escalate if found)
|
||||
|
||||
## Spec Types
|
||||
|
||||
### Detailed Specs
|
||||
Prescribes HOW to implement. Signals: "at line 45", "rename X to Y"
|
||||
- Follow exactly
|
||||
- Add nothing beyond what is specified
|
||||
- Match prescribed structure and naming
|
||||
|
||||
### Freeform Specs
|
||||
Describes WHAT to achieve. Signals: "add logging", "improve error handling"
|
||||
- Use judgment for implementation details
|
||||
- Follow project conventions
|
||||
- Implement smallest change that satisfies intent
|
||||
|
||||
**Scope limitation**: Do what is asked; nothing more, nothing less.
|
||||
|
||||
## Priority Order
|
||||
|
||||
When rules conflict:
|
||||
1. Security constraints (RULE 0) - override everything
|
||||
2. Project documentation (CLAUDE.md) - override spec details
|
||||
3. Detailed spec instructions - follow exactly
|
||||
4. Your judgment - for freeform specs only
|
||||
|
||||
## MotoVaultPro Patterns
|
||||
|
||||
- Feature capsules: `backend/src/features/{feature}/`
|
||||
- Repository pattern with mapRow() for DB->TS case conversion
|
||||
- Snake_case in DB, camelCase in TypeScript
|
||||
- Mobile + desktop validation required
|
||||
|
||||
## Comment Handling
|
||||
|
||||
**Plan-based execution**: Transcribe comments from plan verbatim. Comments explain WHY; plan author has already optimized for future readers.
|
||||
|
||||
**Freeform execution**: Write WHY comments for non-obvious code. Skip comments when code is self-documenting.
|
||||
|
||||
**Exclude from output**: FIXED:, NEW:, NOTE:, location directives, planning annotations.
|
||||
|
||||
## Escalation
|
||||
|
||||
Return to domain agent when:
|
||||
- Missing dependencies block implementation
|
||||
- Spec contradictions require design decisions
|
||||
- Ambiguities that project docs cannot resolve
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
### Files Modified
|
||||
- [file]: [what changed]
|
||||
|
||||
### Tests
|
||||
- [test file]: [coverage]
|
||||
|
||||
### Notes
|
||||
[assumptions made, issues encountered]
|
||||
```
|
||||
|
||||
See `.claude/skills/planner/` for diff format specification.
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: quality-reviewer
|
||||
description: Reviews code and plans for production risks, project conformance, and structural quality
|
||||
model: opus
|
||||
---
|
||||
|
||||
# Quality Reviewer
|
||||
|
||||
Expert reviewer detecting production risks, conformance violations, and structural defects.
|
||||
|
||||
## RULE Hierarchy (CANONICAL DEFINITIONS)
|
||||
|
||||
RULE 0 overrides RULE 1; RULE 1 overrides RULE 2.
|
||||
|
||||
### RULE 0: Production Reliability (CRITICAL/HIGH)
|
||||
- Unhandled errors causing data loss or corruption
|
||||
- Security vulnerabilities (injection, auth bypass)
|
||||
- Resource exhaustion (unbounded loops, leaks)
|
||||
- Race conditions affecting correctness
|
||||
- Silent failures masking problems
|
||||
|
||||
**Verification**: Use OPEN questions ("What happens when X fails?"), not yes/no.
|
||||
**CRITICAL findings**: Require dual-path verification (forward + backward reasoning).
|
||||
|
||||
### RULE 1: Project Conformance (HIGH)
|
||||
MotoVaultPro-specific standards:
|
||||
- Mobile + desktop validation required
|
||||
- Snake_case in DB, camelCase in TypeScript
|
||||
- Feature capsule pattern (`backend/src/features/{feature}/`)
|
||||
- Repository pattern with mapRow() for case conversion
|
||||
- CI/CD pipeline must pass
|
||||
|
||||
**Verification**: Cite specific standard from CLAUDE.md or project docs.
|
||||
|
||||
### RULE 2: Structural Quality (SHOULD_FIX/SUGGESTION)
|
||||
- God objects (>15 methods or >10 dependencies)
|
||||
- God functions (>50 lines or >3 nesting levels)
|
||||
- Duplicate logic (copy-pasted blocks)
|
||||
- Dead code (unused, unreachable)
|
||||
- Inconsistent error handling
|
||||
|
||||
**Verification**: Confirm project docs don't explicitly permit the pattern.
|
||||
|
||||
## Invocation Modes
|
||||
|
||||
| Mode | Focus | Rules Applied |
|
||||
|------|-------|---------------|
|
||||
| `plan-completeness` | Plan document structure | Decision Log, Policy Defaults |
|
||||
| `plan-code` | Proposed code in plan | RULE 0/1/2 + codebase alignment |
|
||||
| `plan-docs` | Post-TW documentation | Temporal contamination, comment quality |
|
||||
| `post-implementation` | Code after implementation | All rules |
|
||||
| `reconciliation` | Check milestone completion | Acceptance criteria only |
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## VERDICT: [PASS | PASS_WITH_CONCERNS | NEEDS_CHANGES | CRITICAL_ISSUES]
|
||||
|
||||
## Findings
|
||||
|
||||
### [RULE] [SEVERITY]: [Title]
|
||||
- **Location**: [file:line]
|
||||
- **Issue**: [What is wrong]
|
||||
- **Failure Mode**: [Why this matters]
|
||||
- **Suggested Fix**: [Concrete action]
|
||||
|
||||
## Considered But Not Flagged
|
||||
[Items examined but not issues, with rationale]
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Before flagging**:
|
||||
1. Read CLAUDE.md/project docs for standards (RULE 1 scope)
|
||||
2. Check Planning Context for Known Risks (skip acknowledged risks)
|
||||
3. Verify finding is actionable with specific fix
|
||||
|
||||
**Severity guide**:
|
||||
- CRITICAL: Data loss, security breach, system failure
|
||||
- HIGH: Production reliability or project standard violation
|
||||
- SHOULD_FIX: Structural quality issue
|
||||
- SUGGESTION: Improvement opportunity
|
||||
|
||||
See `.claude/skills/quality-reviewer/` for detailed review protocols.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: technical-writer
|
||||
description: Creates LLM-optimized documentation - every word earns its tokens
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Technical Writer
|
||||
|
||||
Creates documentation optimized for LLM consumption. Every word earns its tokens.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Input | Output |
|
||||
|------|-------|--------|
|
||||
| `plan-scrub` | Plan with code snippets | Plan with temporal-clean comments |
|
||||
| `post-implementation` | Modified files list | CLAUDE.md indexes, README.md if needed |
|
||||
|
||||
## CLAUDE.md Format (~200 tokens)
|
||||
|
||||
Tabular index only, no prose:
|
||||
|
||||
```markdown
|
||||
| Path | What | When |
|
||||
|------|------|------|
|
||||
| `file.ts` | Description | Task trigger |
|
||||
```
|
||||
|
||||
## README.md (Only When Needed)
|
||||
|
||||
Create README.md only for Invisible Knowledge:
|
||||
- Architecture decisions not apparent from code
|
||||
- Invariants and constraints
|
||||
- Design tradeoffs
|
||||
|
||||
## Temporal Contamination Detection
|
||||
|
||||
Comments must pass the **Timeless Present Rule**: written as if reader has no knowledge of code history.
|
||||
|
||||
**Five detection questions**:
|
||||
1. Describes action taken rather than what exists? (change-relative)
|
||||
2. Compares to something not in code? (baseline reference)
|
||||
3. Describes where to put code? (location directive - DELETE)
|
||||
4. Describes intent rather than behavior? (planning artifact)
|
||||
5. Describes author's choice rather than code behavior? (intent leakage)
|
||||
|
||||
| Contaminated | Timeless Present |
|
||||
|--------------|------------------|
|
||||
| "Added mutex to fix race" | "Mutex serializes concurrent access" |
|
||||
| "Replaced per-tag logging" | "Single summary line; per-tag would produce 1500+ lines" |
|
||||
| "After the SendAsync call" | (delete - location is in diff) |
|
||||
|
||||
**Transformation pattern**: Extract technical justification, discard change narrative.
|
||||
|
||||
## Comment Quality
|
||||
|
||||
- Document WHY, never WHAT
|
||||
- Skip comments for CRUD and standard patterns
|
||||
- For >3 step functions, add explanatory block
|
||||
|
||||
## Forbidden Patterns
|
||||
|
||||
- Marketing language: "elegant", "robust", "powerful"
|
||||
- Hedging: "basically", "simply", "just"
|
||||
- Aspirational: "will support", "planned for"
|
||||
|
||||
See `.claude/skills/doc-sync/` for detailed documentation protocols.
|
||||
@@ -0,0 +1,13 @@
|
||||
# skills/
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `planner/` | Planning workflow with resource sync | Complex features (3+ files) |
|
||||
| `problem-analysis/` | Structured problem decomposition | Uncertain approach, debugging |
|
||||
| `decision-critic/` | Decision stress-testing | Architectural choices, tradeoffs |
|
||||
| `codebase-analysis/` | Systematic codebase investigation | Unfamiliar areas, audits |
|
||||
| `doc-sync/` | CLAUDE.md/README.md synchronization | After refactors, periodic audits |
|
||||
| `incoherence/` | Detect doc/code drift | Documentation inconsistencies |
|
||||
| `prompt-engineer/` | Prompt optimization techniques | Improving AI prompts |
|
||||
@@ -0,0 +1,16 @@
|
||||
# skills/codebase-analysis/
|
||||
|
||||
## Overview
|
||||
|
||||
Systematic codebase analysis skill. IMMEDIATELY invoke the script - do NOT explore first.
|
||||
|
||||
## Index
|
||||
|
||||
| File/Directory | Contents | Read When |
|
||||
| -------------------- | ----------------- | ------------------ |
|
||||
| `SKILL.md` | Invocation | Using this skill |
|
||||
| `scripts/analyze.py` | Complete workflow | Debugging behavior |
|
||||
|
||||
## Key Point
|
||||
|
||||
The script IS the workflow. It handles exploration dispatch, focus selection, investigation, and synthesis. Do NOT explore or analyze before invoking. Run the script and obey its output.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Analyze
|
||||
|
||||
Before you plan anything non-trivial, you need to actually understand the
|
||||
codebase. Not impressions -- evidence. The analyze skill forces systematic
|
||||
investigation with structured phases and explicit evidence requirements.
|
||||
|
||||
| Phase | Actions |
|
||||
| ---------------------- | ------------------------------------------------------------------------------ |
|
||||
| Exploration | Delegate to Explore agent; process structure, tech stack, patterns |
|
||||
| Focus Selection | Classify areas (architecture, performance, security, quality); assign P1/P2/P3 |
|
||||
| Investigation Planning | Commit to specific files and questions; create accountability contract |
|
||||
| Deep Analysis | Progressive investigation; document with file:line + quoted code |
|
||||
| Verification | Audit completeness; ensure all commitments addressed |
|
||||
| Synthesis | Consolidate by severity; provide prioritized recommendations |
|
||||
|
||||
## When to Use
|
||||
|
||||
Four scenarios where this matters:
|
||||
|
||||
- **Unfamiliar codebase** -- You cannot plan what you do not understand. Period.
|
||||
- **Security review** -- Vulnerability assessment requires systematic coverage,
|
||||
not "I looked around and it seems fine."
|
||||
- **Performance analysis** -- Before optimization, know where time actually
|
||||
goes, not where you assume it goes.
|
||||
- **Architecture evaluation** -- Major refactors deserve evidence-backed
|
||||
understanding, not vibes.
|
||||
|
||||
## When to Skip
|
||||
|
||||
Not everything needs this level of rigor:
|
||||
|
||||
- You already understand the codebase well
|
||||
- Simple bug fix with obvious scope
|
||||
- User has provided comprehensive context
|
||||
|
||||
The astute reader will notice all three skip conditions share a trait: you
|
||||
already have the evidence. The skill exists for when you do not.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Use your analyze skill to understand this codebase.
|
||||
Focus on security and architecture before we plan the authentication refactor.
|
||||
```
|
||||
|
||||
The skill outputs findings organized by severity (CRITICAL/HIGH/MEDIUM/LOW),
|
||||
each with file:line references and quoted code. This feeds directly into
|
||||
planning -- you have evidence-backed understanding before proposing changes.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: codebase-analysis
|
||||
description: Invoke IMMEDIATELY via python script when user requests codebase analysis, architecture review, security assessment, or quality evaluation. Do NOT explore first - the script orchestrates exploration.
|
||||
---
|
||||
|
||||
# Codebase Analysis
|
||||
|
||||
When this skill activates, IMMEDIATELY invoke the script. The script IS the workflow.
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
python3 scripts/analyze.py \
|
||||
--step-number 1 \
|
||||
--total-steps 6 \
|
||||
--thoughts "Starting analysis. User request: <describe what user asked to analyze>"
|
||||
```
|
||||
|
||||
| Argument | Required | Description |
|
||||
| --------------- | -------- | ----------------------------------------- |
|
||||
| `--step-number` | Yes | Current step (starts at 1) |
|
||||
| `--total-steps` | Yes | Minimum 6; adjust as script instructs |
|
||||
| `--thoughts` | Yes | Accumulated state from all previous steps |
|
||||
|
||||
Do NOT explore or analyze first. Run the script and follow its output.
|
||||
+661
@@ -0,0 +1,661 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze Skill - Step-by-step codebase analysis with exploration and deep investigation.
|
||||
|
||||
Six-phase workflow:
|
||||
1. EXPLORATION: Process Explore sub-agent results
|
||||
2. FOCUS SELECTION: Classify investigation areas
|
||||
3. INVESTIGATION PLANNING: Commit to specific files and questions
|
||||
4. DEEP ANALYSIS (1-N): Progressive investigation with evidence
|
||||
5. VERIFICATION: Validate completeness before synthesis
|
||||
6. SYNTHESIS: Consolidate verified findings
|
||||
|
||||
Usage:
|
||||
python3 analyze.py --step-number 1 --total-steps 6 --thoughts "Explore found: ..."
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def get_phase_name(step: int, total_steps: int) -> str:
|
||||
"""Return the phase name for a given step number."""
|
||||
if step == 1:
|
||||
return "EXPLORATION"
|
||||
elif step == 2:
|
||||
return "FOCUS SELECTION"
|
||||
elif step == 3:
|
||||
return "INVESTIGATION PLANNING"
|
||||
elif step == total_steps - 1:
|
||||
return "VERIFICATION"
|
||||
elif step == total_steps:
|
||||
return "SYNTHESIS"
|
||||
else:
|
||||
return "DEEP ANALYSIS"
|
||||
|
||||
|
||||
def get_state_requirement(step: int) -> list[str]:
|
||||
"""Return state accumulation requirement for steps 2+."""
|
||||
if step < 2:
|
||||
return []
|
||||
|
||||
return [
|
||||
"",
|
||||
"<state_requirement>",
|
||||
"CRITICAL: Your --thoughts for this step MUST include:",
|
||||
"",
|
||||
"1. FOCUS AREAS: Each area identified and its priority (from step 2)",
|
||||
"2. INVESTIGATION PLAN: Files and questions committed to (from step 3)",
|
||||
"3. FILES EXAMINED: Every file read with key observations",
|
||||
"4. ISSUES BY SEVERITY: All [CRITICAL]/[HIGH]/[MEDIUM]/[LOW] items",
|
||||
"5. PATTERNS: Cross-file patterns identified",
|
||||
"6. HYPOTHESES: Current theories and supporting evidence",
|
||||
"7. REMAINING: What still needs investigation",
|
||||
"",
|
||||
"If ANY section is missing, your accumulated state is incomplete.",
|
||||
"Reconstruct it before proceeding.",
|
||||
"</state_requirement>",
|
||||
]
|
||||
|
||||
|
||||
def get_step_guidance(step: int, total_steps: int) -> dict:
|
||||
"""Return step-specific guidance and actions."""
|
||||
|
||||
next_step = step + 1 if step < total_steps else None
|
||||
phase = get_phase_name(step, total_steps)
|
||||
is_final = step >= total_steps
|
||||
|
||||
# Minimum steps: exploration(1) + focus(2) + planning(3) + analysis(4) + verification(5) + synthesis(6)
|
||||
min_steps = 6
|
||||
|
||||
# PHASE 1: EXPLORATION
|
||||
if step == 1:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Process Exploration Results",
|
||||
"actions": [
|
||||
"STOP. Before proceeding, verify you have Explore agent results.",
|
||||
"",
|
||||
"If your --thoughts do NOT contain Explore agent output, you MUST:",
|
||||
"",
|
||||
"<exploration_delegation>",
|
||||
"Assess the scope and delegate appropriately:",
|
||||
"",
|
||||
"SINGLE CODEBASE, FOCUSED SCOPE:",
|
||||
" - One Explore agent is sufficient",
|
||||
" - Use Task tool with subagent_type='Explore'",
|
||||
" - Prompt: 'Explore this repository. Report directory structure,",
|
||||
" tech stack, entry points, main components, observed patterns.'",
|
||||
"",
|
||||
"LARGE CODEBASE OR BROAD SCOPE:",
|
||||
" - Launch MULTIPLE Explore agents IN PARALLEL (single message, multiple Task calls)",
|
||||
" - Divide by logical boundaries: frontend/backend, services, modules",
|
||||
" - Example prompts:",
|
||||
" Agent 1: 'Explore src/api/ and src/services/. Focus on API structure.'",
|
||||
" Agent 2: 'Explore src/core/ and src/models/. Focus on domain logic.'",
|
||||
" Agent 3: 'Explore tests/ and config/. Focus on test patterns and configuration.'",
|
||||
"",
|
||||
"MULTIPLE CODEBASES:",
|
||||
" - Launch ONE Explore agent PER CODEBASE in parallel",
|
||||
" - Each agent explores its repository independently",
|
||||
" - Example:",
|
||||
" Agent 1: 'Explore /path/to/repo-a. Report structure and patterns.'",
|
||||
" Agent 2: 'Explore /path/to/repo-b. Report structure and patterns.'",
|
||||
"",
|
||||
"WAIT for ALL agents to complete before invoking this step again.",
|
||||
"</exploration_delegation>",
|
||||
"",
|
||||
"Only proceed below if you have concrete Explore output to process.",
|
||||
"",
|
||||
"=" * 60,
|
||||
"",
|
||||
"<exploration_processing>",
|
||||
"From the Explore agent(s) report(s), extract and document:",
|
||||
"",
|
||||
"STRUCTURE:",
|
||||
" - Main directories and their purposes",
|
||||
" - Where core logic lives vs. configuration vs. tests",
|
||||
" - File organization patterns",
|
||||
" - (If multiple agents: note boundaries and overlaps)",
|
||||
"",
|
||||
"TECH STACK:",
|
||||
" - Languages, frameworks, key dependencies",
|
||||
" - Build system, package management",
|
||||
" - External services or APIs",
|
||||
"",
|
||||
"ENTRY POINTS:",
|
||||
" - Main executables, API endpoints, CLI commands",
|
||||
" - Data flow through the system",
|
||||
" - Key interfaces between components",
|
||||
"",
|
||||
"INITIAL OBSERVATIONS:",
|
||||
" - Architectural patterns (MVC, microservices, monolith)?",
|
||||
" - Obvious code smells or areas of concern?",
|
||||
" - Parts that seem well-structured vs. problematic?",
|
||||
"</exploration_processing>",
|
||||
],
|
||||
"next": (
|
||||
f"Invoke step {next_step} with your processed exploration summary. "
|
||||
"Include all structure, tech stack, and initial observations in --thoughts."
|
||||
),
|
||||
}
|
||||
|
||||
# PHASE 2: FOCUS SELECTION
|
||||
if step == 2:
|
||||
actions = [
|
||||
"Based on exploration findings, determine what needs deep investigation.",
|
||||
"",
|
||||
"<focus_classification>",
|
||||
"Evaluate the codebase against each dimension. Mark areas needing investigation:",
|
||||
"",
|
||||
"ARCHITECTURE (structural concerns):",
|
||||
" [ ] Component relationships unclear or tangled?",
|
||||
" [ ] Dependency graph needs mapping?",
|
||||
" [ ] Layering violations or circular dependencies?",
|
||||
" [ ] Missing or unclear module boundaries?",
|
||||
"",
|
||||
"PERFORMANCE (efficiency concerns):",
|
||||
" [ ] Hot paths that may be inefficient?",
|
||||
" [ ] Database queries needing review?",
|
||||
" [ ] Memory allocation patterns?",
|
||||
" [ ] Concurrency or parallelism issues?",
|
||||
"",
|
||||
"SECURITY (vulnerability concerns):",
|
||||
" [ ] Input validation gaps?",
|
||||
" [ ] Authentication/authorization flows?",
|
||||
" [ ] Sensitive data handling?",
|
||||
" [ ] External API integrations?",
|
||||
"",
|
||||
"QUALITY (maintainability concerns):",
|
||||
" [ ] Code duplication patterns?",
|
||||
" [ ] Overly complex functions/classes?",
|
||||
" [ ] Missing error handling?",
|
||||
" [ ] Test coverage gaps?",
|
||||
"</focus_classification>",
|
||||
"",
|
||||
"<priority_assignment>",
|
||||
"Rank your focus areas by priority (P1 = most critical):",
|
||||
"",
|
||||
" P1: [focus area] - [why most critical]",
|
||||
" P2: [focus area] - [why second]",
|
||||
" P3: [focus area] - [if applicable]",
|
||||
"",
|
||||
"Consider: security > correctness > performance > maintainability",
|
||||
"</priority_assignment>",
|
||||
"",
|
||||
"<step_estimation>",
|
||||
"Estimate total steps based on scope:",
|
||||
"",
|
||||
f" Minimum steps: {min_steps} (exploration + focus + planning + 1 analysis + verification + synthesis)",
|
||||
" 1-2 focus areas, small codebase: total_steps = 6-7",
|
||||
" 2-3 focus areas, medium codebase: total_steps = 7-9",
|
||||
" 3+ focus areas, large codebase: total_steps = 9-12",
|
||||
"",
|
||||
"You can adjust this estimate as understanding grows.",
|
||||
"</step_estimation>",
|
||||
]
|
||||
actions.extend(get_state_requirement(step))
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Classify Investigation Areas",
|
||||
"actions": actions,
|
||||
"next": (
|
||||
f"Invoke step {next_step} with your prioritized focus areas and "
|
||||
"updated total_steps estimate. Next: create investigation plan."
|
||||
),
|
||||
}
|
||||
|
||||
# PHASE 3: INVESTIGATION PLANNING
|
||||
if step == 3:
|
||||
actions = [
|
||||
"You have identified focus areas. Now commit to specific investigation targets.",
|
||||
"",
|
||||
"This step creates ACCOUNTABILITY. You will verify against these commitments.",
|
||||
"",
|
||||
"<investigation_commitments>",
|
||||
"For EACH focus area (in priority order), specify:",
|
||||
"",
|
||||
"---",
|
||||
"FOCUS AREA: [name] (Priority: P1/P2/P3)",
|
||||
"",
|
||||
"Files to examine:",
|
||||
" - path/to/file1.py",
|
||||
" Question: [specific question to answer about this file]",
|
||||
" Hypothesis: [what you expect to find]",
|
||||
"",
|
||||
" - path/to/file2.py",
|
||||
" Question: [specific question to answer]",
|
||||
" Hypothesis: [what you expect to find]",
|
||||
"",
|
||||
"Evidence needed to confirm/refute:",
|
||||
" - [what specific code patterns would confirm hypothesis]",
|
||||
" - [what would refute it]",
|
||||
"---",
|
||||
"",
|
||||
"Repeat for each focus area.",
|
||||
"</investigation_commitments>",
|
||||
"",
|
||||
"<commitment_rules>",
|
||||
"This is a CONTRACT. In subsequent steps, you MUST:",
|
||||
"",
|
||||
" 1. Read every file listed (using Read tool)",
|
||||
" 2. Answer every question posed",
|
||||
" 3. Document evidence with file:line references",
|
||||
" 4. Update hypothesis based on actual evidence",
|
||||
"",
|
||||
"If you cannot answer a question, document WHY:",
|
||||
" - File doesn't exist?",
|
||||
" - Question was wrong?",
|
||||
" - Need different files?",
|
||||
"",
|
||||
"Do NOT silently skip commitments.",
|
||||
"</commitment_rules>",
|
||||
]
|
||||
actions.extend(get_state_requirement(step))
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Create Investigation Plan",
|
||||
"actions": actions,
|
||||
"next": (
|
||||
f"Invoke step {next_step} with your complete investigation plan. "
|
||||
"Next: begin executing the plan with the highest priority focus area."
|
||||
),
|
||||
}
|
||||
|
||||
# PHASE 5: VERIFICATION (step N-1)
|
||||
if step == total_steps - 1:
|
||||
actions = [
|
||||
"STOP. Before synthesizing, verify your investigation is complete.",
|
||||
"",
|
||||
"<completeness_audit>",
|
||||
"Review your investigation commitments from Step 3.",
|
||||
"",
|
||||
"For EACH file you committed to examine:",
|
||||
" [ ] File was actually read (not just mentioned)?",
|
||||
" [ ] Specific question was answered with evidence?",
|
||||
" [ ] Finding documented with file:line reference and quoted code?",
|
||||
"",
|
||||
"For EACH hypothesis you formed:",
|
||||
" [ ] Evidence collected (confirming OR refuting)?",
|
||||
" [ ] Hypothesis updated based on evidence?",
|
||||
" [ ] If refuted, what replaced it?",
|
||||
"</completeness_audit>",
|
||||
"",
|
||||
"<gap_detection>",
|
||||
"Identify gaps in your investigation:",
|
||||
"",
|
||||
" - Files committed but not examined?",
|
||||
" - Focus areas declared but not investigated?",
|
||||
" - Issues referenced without file:line evidence?",
|
||||
" - Patterns claimed without cross-file validation?",
|
||||
" - Questions posed but not answered?",
|
||||
"",
|
||||
"List each gap explicitly:",
|
||||
" GAP 1: [description]",
|
||||
" GAP 2: [description]",
|
||||
" ...",
|
||||
"</gap_detection>",
|
||||
"",
|
||||
"<gap_resolution>",
|
||||
"If gaps exist:",
|
||||
" 1. INCREASE total_steps by number of gaps that need investigation",
|
||||
" 2. Return to DEEP ANALYSIS phase to fill gaps",
|
||||
" 3. Re-enter VERIFICATION after gaps are filled",
|
||||
"",
|
||||
"If no gaps (or gaps are acceptable):",
|
||||
" Proceed to SYNTHESIS (next step)",
|
||||
"</gap_resolution>",
|
||||
"",
|
||||
"<evidence_quality_check>",
|
||||
"For each [CRITICAL] or [HIGH] severity finding, verify:",
|
||||
" [ ] Has quoted code (2-5 lines)?",
|
||||
" [ ] Has exact file:line reference?",
|
||||
" [ ] Impact is clearly explained?",
|
||||
" [ ] Recommended fix is actionable?",
|
||||
"",
|
||||
"Findings without evidence are UNVERIFIED. Either:",
|
||||
" - Add evidence now, or",
|
||||
" - Downgrade severity, or",
|
||||
" - Mark as 'needs investigation'",
|
||||
"</evidence_quality_check>",
|
||||
]
|
||||
actions.extend(get_state_requirement(step))
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Verify Investigation Completeness",
|
||||
"actions": actions,
|
||||
"next": (
|
||||
"If gaps found: invoke earlier step to fill gaps, then return here. "
|
||||
f"If complete: invoke step {next_step} for final synthesis."
|
||||
),
|
||||
}
|
||||
|
||||
# PHASE 6: SYNTHESIS (final step)
|
||||
if is_final:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Consolidate and Recommend",
|
||||
"actions": [
|
||||
"Investigation verified. Synthesize all findings into actionable output.",
|
||||
"",
|
||||
"<final_consolidation>",
|
||||
"Organize all VERIFIED findings by severity:",
|
||||
"",
|
||||
"CRITICAL ISSUES (must address immediately):",
|
||||
" For each:",
|
||||
" - file:line reference",
|
||||
" - Quoted code (2-5 lines)",
|
||||
" - Impact description",
|
||||
" - Recommended fix",
|
||||
"",
|
||||
"HIGH ISSUES (should address soon):",
|
||||
" For each: file:line, description, recommended fix",
|
||||
"",
|
||||
"MEDIUM ISSUES (consider addressing):",
|
||||
" For each: description, general guidance",
|
||||
"",
|
||||
"LOW ISSUES (nice to fix):",
|
||||
" Summarize patterns, defer to future work",
|
||||
"</final_consolidation>",
|
||||
"",
|
||||
"<pattern_synthesis>",
|
||||
"Identify systemic patterns:",
|
||||
"",
|
||||
" - Issues appearing across multiple files -> systemic problem",
|
||||
" - Root causes explaining multiple symptoms",
|
||||
" - Architectural changes that would prevent recurrence",
|
||||
"</pattern_synthesis>",
|
||||
"",
|
||||
"<recommendations>",
|
||||
"Provide prioritized action plan:",
|
||||
"",
|
||||
"IMMEDIATE (blocks other work / security risk):",
|
||||
" 1. [action with specific file:line reference]",
|
||||
" 2. [action with specific file:line reference]",
|
||||
"",
|
||||
"SHORT-TERM (address within current sprint):",
|
||||
" 1. [action with scope indication]",
|
||||
" 2. [action with scope indication]",
|
||||
"",
|
||||
"LONG-TERM (strategic improvements):",
|
||||
" 1. [architectural or process recommendation]",
|
||||
" 2. [architectural or process recommendation]",
|
||||
"</recommendations>",
|
||||
"",
|
||||
"<final_quality_check>",
|
||||
"Before presenting to user, verify:",
|
||||
"",
|
||||
" [ ] All CRITICAL/HIGH issues have file:line + quoted code?",
|
||||
" [ ] Recommendations are actionable, not vague?",
|
||||
" [ ] Findings organized by impact, not discovery order?",
|
||||
" [ ] No findings lost from earlier steps?",
|
||||
" [ ] Patterns are supported by multiple examples?",
|
||||
"</final_quality_check>",
|
||||
],
|
||||
"next": None,
|
||||
}
|
||||
|
||||
# PHASE 4: DEEP ANALYSIS (steps 4 to N-2)
|
||||
# Calculate position within deep analysis phase
|
||||
deep_analysis_step = step - 3 # 1st, 2nd, 3rd deep analysis step
|
||||
remaining_before_verification = total_steps - 1 - step # steps until verification
|
||||
|
||||
if deep_analysis_step == 1:
|
||||
step_title = "Initial Investigation"
|
||||
focus_instruction = [
|
||||
"Execute your investigation plan from Step 3.",
|
||||
"",
|
||||
"<first_pass_protocol>",
|
||||
"For each file in your P1 (highest priority) focus area:",
|
||||
"",
|
||||
"1. READ the file using the Read tool",
|
||||
"2. ANSWER the specific question you committed to",
|
||||
"3. DOCUMENT findings with evidence:",
|
||||
"",
|
||||
" EVIDENCE FORMAT (required for each finding):",
|
||||
" ```",
|
||||
" [SEVERITY] Brief description (file.py:line-line)",
|
||||
" > quoted code from file (2-5 lines)",
|
||||
" Explanation: why this is an issue",
|
||||
" ```",
|
||||
"",
|
||||
"4. UPDATE your hypothesis based on what you found",
|
||||
" - Confirmed? Document supporting evidence",
|
||||
" - Refuted? Document what you found instead",
|
||||
" - Inconclusive? Note what else you need to check",
|
||||
"</first_pass_protocol>",
|
||||
"",
|
||||
"Findings without quoted code are UNVERIFIED.",
|
||||
]
|
||||
elif deep_analysis_step == 2:
|
||||
step_title = "Deepen Investigation"
|
||||
focus_instruction = [
|
||||
"Review findings from previous step. Go deeper.",
|
||||
"",
|
||||
"<second_pass_protocol>",
|
||||
"For each issue found in the previous step:",
|
||||
"",
|
||||
"1. TRACE to root cause",
|
||||
" - Why does this issue exist?",
|
||||
" - What allowed it to be introduced?",
|
||||
" - Are there related issues in connected files?",
|
||||
"",
|
||||
"2. EXAMINE related files",
|
||||
" - Callers and callees of problematic code",
|
||||
" - Similar patterns elsewhere in codebase",
|
||||
" - Configuration that affects this code",
|
||||
"",
|
||||
"3. LOOK for patterns",
|
||||
" - Same issue in multiple places? -> Systemic problem",
|
||||
" - One-off issue? -> Localized fix",
|
||||
"",
|
||||
"4. MOVE to P2 focus area if P1 is sufficiently investigated",
|
||||
"</second_pass_protocol>",
|
||||
"",
|
||||
"Continue documenting with file:line + quoted code.",
|
||||
]
|
||||
else:
|
||||
step_title = f"Extended Investigation (Pass {deep_analysis_step})"
|
||||
focus_instruction = [
|
||||
"Focus on remaining gaps and open questions.",
|
||||
"",
|
||||
"<extended_investigation_protocol>",
|
||||
"Review your accumulated state. Address:",
|
||||
"",
|
||||
"1. REMAINING items from your investigation plan",
|
||||
" - Any files not yet examined?",
|
||||
" - Any questions not yet answered?",
|
||||
"",
|
||||
"2. OPEN QUESTIONS from previous steps",
|
||||
" - What needed further investigation?",
|
||||
" - What dependencies weren't clear?",
|
||||
"",
|
||||
"3. PATTERN VALIDATION",
|
||||
" - Cross-file patterns claimed but not verified?",
|
||||
" - Need more examples to confirm systemic issues?",
|
||||
"",
|
||||
"4. EVIDENCE STRENGTHENING",
|
||||
" - Any [CRITICAL]/[HIGH] findings without quoted code?",
|
||||
" - Any claims without file:line references?",
|
||||
"</extended_investigation_protocol>",
|
||||
"",
|
||||
"If investigation is complete, reduce total_steps to reach verification.",
|
||||
]
|
||||
|
||||
actions = focus_instruction + [
|
||||
"",
|
||||
"<scope_check>",
|
||||
"After this step's investigation:",
|
||||
"",
|
||||
f" Remaining steps before verification: {remaining_before_verification}",
|
||||
"",
|
||||
" - Discovered more complexity? -> INCREASE total_steps",
|
||||
" - Remaining scope smaller than expected? -> DECREASE total_steps",
|
||||
" - All focus areas sufficiently covered? -> Set next step = total_steps - 1 (verification)",
|
||||
"</scope_check>",
|
||||
]
|
||||
actions.extend(get_state_requirement(step))
|
||||
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": step_title,
|
||||
"actions": actions,
|
||||
"next": (
|
||||
f"Invoke step {next_step}. "
|
||||
f"{remaining_before_verification} step(s) before verification. "
|
||||
"Include ALL accumulated findings in --thoughts. "
|
||||
"Adjust total_steps if scope changed."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def format_output(step: int, total_steps: int, thoughts: str, guidance: dict) -> str:
|
||||
"""Format the output for display."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("=" * 70)
|
||||
lines.append(f"ANALYZE - Step {step}/{total_steps}: {guidance['step_title']}")
|
||||
lines.append(f"Phase: {guidance['phase']}")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
# Status
|
||||
is_final = step >= total_steps
|
||||
is_verification = step == total_steps - 1
|
||||
if is_final:
|
||||
status = "analysis_complete"
|
||||
elif is_verification:
|
||||
status = "verification_required"
|
||||
else:
|
||||
status = "in_progress"
|
||||
lines.append(f"STATUS: {status}")
|
||||
lines.append("")
|
||||
|
||||
# Current thoughts summary (truncated for display)
|
||||
lines.append("YOUR ACCUMULATED STATE:")
|
||||
if len(thoughts) > 600:
|
||||
lines.append(thoughts[:600] + "...")
|
||||
lines.append("[truncated - full state in --thoughts]")
|
||||
else:
|
||||
lines.append(thoughts)
|
||||
lines.append("")
|
||||
|
||||
# Actions
|
||||
lines.append("REQUIRED ACTIONS:")
|
||||
for action in guidance["actions"]:
|
||||
if action:
|
||||
# Handle the separator line specially
|
||||
if action == "=" * 60:
|
||||
lines.append(" " + action)
|
||||
else:
|
||||
lines.append(f" {action}")
|
||||
else:
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
# Next step or completion
|
||||
if guidance["next"]:
|
||||
lines.append("NEXT:")
|
||||
lines.append(guidance["next"])
|
||||
else:
|
||||
lines.append("WORKFLOW COMPLETE")
|
||||
lines.append("")
|
||||
lines.append("Present your consolidated findings to the user:")
|
||||
lines.append(" - Organized by severity (CRITICAL -> LOW)")
|
||||
lines.append(" - With file:line references and quoted code for serious issues")
|
||||
lines.append(" - With actionable recommendations for each category")
|
||||
|
||||
lines.append("")
|
||||
lines.append("=" * 70)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze Skill - Systematic codebase analysis",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Workflow Phases:
|
||||
Step 1: EXPLORATION - Process Explore agent results
|
||||
Step 2: FOCUS SELECTION - Classify investigation areas
|
||||
Step 3: INVESTIGATION PLAN - Commit to specific files and questions
|
||||
Step 4+: DEEP ANALYSIS - Progressive investigation with evidence
|
||||
Step N-1: VERIFICATION - Validate completeness before synthesis
|
||||
Step N: SYNTHESIS - Consolidate verified findings
|
||||
|
||||
Examples:
|
||||
# Step 1: After Explore agent returns
|
||||
python3 analyze.py --step-number 1 --total-steps 6 \\
|
||||
--thoughts "Explore found: Python web app, Flask, SQLAlchemy..."
|
||||
|
||||
# Step 2: Focus selection
|
||||
python3 analyze.py --step-number 2 --total-steps 7 \\
|
||||
--thoughts "Structure: src/, tests/. Focus: security (P1), quality (P2)..."
|
||||
|
||||
# Step 3: Investigation planning
|
||||
python3 analyze.py --step-number 3 --total-steps 7 \\
|
||||
--thoughts "P1 Security: auth/login.py (Q: input validation?), ..."
|
||||
|
||||
# Step 4: Initial investigation
|
||||
python3 analyze.py --step-number 4 --total-steps 7 \\
|
||||
--thoughts "FILES: auth/login.py read. [CRITICAL] SQL injection at :45..."
|
||||
|
||||
# Step 5: Deepen investigation
|
||||
python3 analyze.py --step-number 5 --total-steps 7 \\
|
||||
--thoughts "[Previous state] + traced to db/queries.py, pattern in 3 files..."
|
||||
|
||||
# Step 6: Verification
|
||||
python3 analyze.py --step-number 6 --total-steps 7 \\
|
||||
--thoughts "[All findings] Checking: all files read, all questions answered..."
|
||||
|
||||
# Step 7: Synthesis
|
||||
python3 analyze.py --step-number 7 --total-steps 7 \\
|
||||
--thoughts "[Verified findings] Ready for consolidation..."
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--step-number",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Current step number (starts at 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-steps",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Estimated total steps (adjust as understanding grows)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--thoughts",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Accumulated findings, evidence, and file references",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate inputs
|
||||
if args.step_number < 1:
|
||||
print("ERROR: step-number must be >= 1", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.total_steps < 6:
|
||||
print("ERROR: total-steps must be >= 6 (minimum workflow)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.total_steps < args.step_number:
|
||||
print("ERROR: total-steps must be >= step-number", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get guidance for current step
|
||||
guidance = get_step_guidance(args.step_number, args.total_steps)
|
||||
|
||||
# Print formatted output
|
||||
print(format_output(args.step_number, args.total_steps, args.thoughts, guidance))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
# skills/decision-critic/
|
||||
|
||||
## Overview
|
||||
|
||||
Decision stress-testing skill. IMMEDIATELY invoke the script - do NOT analyze first.
|
||||
|
||||
## Index
|
||||
|
||||
| File/Directory | Contents | Read When |
|
||||
| ---------------------------- | ----------------- | ------------------ |
|
||||
| `SKILL.md` | Invocation | Using this skill |
|
||||
| `scripts/decision-critic.py` | Complete workflow | Debugging behavior |
|
||||
|
||||
## Key Point
|
||||
|
||||
The script IS the workflow. It handles decomposition, verification, challenge, and synthesis phases. Do NOT analyze or critique before invoking. Run the script and obey its output.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Decision Critic
|
||||
|
||||
Here's the problem: LLMs are sycophants. They agree with you. They validate your
|
||||
reasoning. They tell you your architectural decision is sound and well-reasoned.
|
||||
That's not what you need for important decisions -- you need stress-testing.
|
||||
|
||||
The decision-critic skill forces structured adversarial analysis:
|
||||
|
||||
| Phase | Actions |
|
||||
| ------------- | -------------------------------------------------------------------------- |
|
||||
| Decomposition | Extract claims, assumptions, constraints; assign IDs; classify each |
|
||||
| Verification | Generate questions for verifiable items; answer independently; mark status |
|
||||
| Challenge | Steel-man argument against; explore alternative framings |
|
||||
| Synthesis | Verdict (STAND/REVISE/ESCALATE); summary and recommendation |
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this for decisions where you actually want criticism, not agreement:
|
||||
|
||||
- Architectural choices with long-term consequences
|
||||
- Technology selection (language, framework, database)
|
||||
- Tradeoffs between competing concerns (performance vs. maintainability)
|
||||
- Decisions you're uncertain about and want stress-tested
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
I'm considering using Redis for our session storage instead of PostgreSQL.
|
||||
My reasoning:
|
||||
|
||||
- Redis is faster for key-value lookups
|
||||
- Sessions are ephemeral, don't need ACID guarantees
|
||||
- We already have Redis for caching
|
||||
|
||||
Use your decision critic skill to stress-test this decision.
|
||||
```
|
||||
|
||||
So what happens? The skill:
|
||||
|
||||
1. **Decomposes** the decision into claims (C1: Redis is faster), assumptions
|
||||
(A1: sessions don't need durability), constraints (K1: Redis already
|
||||
deployed)
|
||||
2. **Verifies** each claim -- is Redis actually faster for your access pattern?
|
||||
What's the actual latency difference?
|
||||
3. **Challenges** -- what if sessions DO need durability (shopping carts)?
|
||||
What's the operational cost of Redis failures?
|
||||
4. **Synthesizes** -- verdict with specific failed/uncertain items
|
||||
|
||||
## The Anti-Sycophancy Design
|
||||
|
||||
I grounded this skill in three techniques:
|
||||
|
||||
- **Chain-of-Verification** -- factored verification prevents confirmation bias
|
||||
by answering questions independently
|
||||
- **Self-Consistency** -- multiple reasoning paths reveal disagreement
|
||||
- **Multi-Expert Prompting** -- diverse perspectives catch blind spots
|
||||
|
||||
The structure forces the LLM through adversarial phases rather than allowing it
|
||||
to immediately agree with your reasoning. That's the whole point.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: decision-critic
|
||||
description: Invoke IMMEDIATELY via python script to stress-test decisions and reasoning. Do NOT analyze first - the script orchestrates the critique workflow.
|
||||
---
|
||||
|
||||
# Decision Critic
|
||||
|
||||
When this skill activates, IMMEDIATELY invoke the script. The script IS the workflow.
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
python3 scripts/decision-critic.py \
|
||||
--step-number 1 \
|
||||
--total-steps 7 \
|
||||
--decision "<decision text>" \
|
||||
--context "<constraints and background>" \
|
||||
--thoughts "<your accumulated analysis from all previous steps>"
|
||||
```
|
||||
|
||||
| Argument | Required | Description |
|
||||
| --------------- | -------- | ----------------------------------------------------------- |
|
||||
| `--step-number` | Yes | Current step (1-7) |
|
||||
| `--total-steps` | Yes | Always 7 |
|
||||
| `--decision` | Step 1 | The decision statement being criticized |
|
||||
| `--context` | Step 1 | Constraints, background, system context |
|
||||
| `--thoughts` | Yes | Your analysis including all IDs and status from prior steps |
|
||||
|
||||
Do NOT analyze or critique first. Run the script and follow its output.
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Decision Critic - Step-by-step prompt injection for structured decision criticism.
|
||||
|
||||
Grounded in:
|
||||
- Chain-of-Verification (Dhuliawala et al., 2023)
|
||||
- Self-Consistency (Wang et al., 2023)
|
||||
- Multi-Expert Prompting (Wang et al., 2024)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_phase_name(step: int) -> str:
|
||||
"""Return the phase name for a given step number."""
|
||||
if step <= 2:
|
||||
return "DECOMPOSITION"
|
||||
elif step <= 4:
|
||||
return "VERIFICATION"
|
||||
elif step <= 6:
|
||||
return "CHALLENGE"
|
||||
else:
|
||||
return "SYNTHESIS"
|
||||
|
||||
|
||||
def get_step_guidance(step: int, total_steps: int, decision: Optional[str], context: Optional[str]) -> dict:
|
||||
"""Return step-specific guidance and actions."""
|
||||
|
||||
next_step = step + 1 if step < total_steps else None
|
||||
phase = get_phase_name(step)
|
||||
|
||||
# Common state requirement for steps 2+
|
||||
state_requirement = (
|
||||
"CONTEXT REQUIREMENT: Your --thoughts from this step must include ALL IDs, "
|
||||
"classifications, and status markers from previous steps. This accumulated "
|
||||
"state is essential for workflow continuity."
|
||||
)
|
||||
|
||||
# DECOMPOSITION PHASE
|
||||
if step == 1:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Extract Structure",
|
||||
"actions": [
|
||||
"You are a structured decision critic. Your task is to decompose this "
|
||||
"decision into its constituent parts so each can be independently verified "
|
||||
"or challenged. This analysis is critical to the quality of the entire workflow.",
|
||||
"",
|
||||
"Extract and assign stable IDs that will persist through ALL subsequent steps:",
|
||||
"",
|
||||
"CLAIMS [C1, C2, ...] - Factual assertions (3-7 items)",
|
||||
" What facts does this decision assume to be true?",
|
||||
" What cause-effect relationships does it depend on?",
|
||||
"",
|
||||
"ASSUMPTIONS [A1, A2, ...] - Unstated beliefs (2-5 items)",
|
||||
" What is implied but not explicitly stated?",
|
||||
" What would someone unfamiliar with the context not know?",
|
||||
"",
|
||||
"CONSTRAINTS [K1, K2, ...] - Hard boundaries (1-4 items)",
|
||||
" What technical limitations exist?",
|
||||
" What organizational/timeline constraints apply?",
|
||||
"",
|
||||
"JUDGMENTS [J1, J2, ...] - Subjective tradeoffs (1-3 items)",
|
||||
" Where are values being weighed against each other?",
|
||||
" What 'it depends' decisions were made?",
|
||||
"",
|
||||
"OUTPUT FORMAT:",
|
||||
" C1: <claim text>",
|
||||
" C2: <claim text>",
|
||||
" A1: <assumption text>",
|
||||
" K1: <constraint text>",
|
||||
" J1: <judgment text>",
|
||||
"",
|
||||
"These IDs will be referenced in ALL subsequent steps. Be thorough but focused.",
|
||||
],
|
||||
"next": f"Step {next_step}: Classify each item's verifiability.",
|
||||
"academic_note": None,
|
||||
}
|
||||
|
||||
if step == 2:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Classify Verifiability",
|
||||
"actions": [
|
||||
"You are a structured decision critic continuing your analysis.",
|
||||
"",
|
||||
"Classify each item from Step 1. Retain original IDs and add a verifiability tag.",
|
||||
"",
|
||||
"CLASSIFICATIONS:",
|
||||
"",
|
||||
" [V] VERIFIABLE - Can be checked against evidence or tested",
|
||||
" Examples: \"API supports 1000 RPS\" (testable), \"Library X has feature Y\" (checkable)",
|
||||
"",
|
||||
" [J] JUDGMENT - Subjective tradeoff with no objectively correct answer",
|
||||
" Examples: \"Simplicity is more important than flexibility\", \"Risk is acceptable\"",
|
||||
"",
|
||||
" [C] CONSTRAINT - Given condition, accepted as fixed for this decision",
|
||||
" Examples: \"Budget is $50K\", \"Must launch by Q2\", \"Team has 3 engineers\"",
|
||||
"",
|
||||
"EDGE CASE RULE: When an item could fit multiple categories, prefer [V] over [J] over [C].",
|
||||
"Rationale: Verifiable items can be checked; judgments can be debated; constraints are given.",
|
||||
"",
|
||||
"Example edge case:",
|
||||
" \"The team can deliver in 4 weeks\" - Could be [J] (judgment about capacity) or [V] (checkable",
|
||||
" against past velocity). Choose [V] because it CAN be verified against evidence.",
|
||||
"",
|
||||
"OUTPUT FORMAT (preserve original IDs):",
|
||||
" C1 [V]: <claim text>",
|
||||
" C2 [J]: <claim text>",
|
||||
" A1 [V]: <assumption text>",
|
||||
" K1 [C]: <constraint text>",
|
||||
"",
|
||||
"COUNT: State how many [V] items require verification in the next phase.",
|
||||
"",
|
||||
state_requirement,
|
||||
],
|
||||
"next": f"Step {next_step}: Generate verification questions for [V] items.",
|
||||
"academic_note": None,
|
||||
}
|
||||
|
||||
# VERIFICATION PHASE
|
||||
if step == 3:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Generate Verification Questions",
|
||||
"actions": [
|
||||
"You are a structured decision critic. This step is crucial for catching errors.",
|
||||
"",
|
||||
"For each [V] item from Step 2, generate 1-3 verification questions.",
|
||||
"",
|
||||
"CRITERIA FOR GOOD QUESTIONS:",
|
||||
" - Specific and independently answerable",
|
||||
" - Designed to reveal if the claim is FALSE (falsification focus)",
|
||||
" - Do not assume the claim is true in the question itself",
|
||||
" - Each question should test a different aspect of the claim",
|
||||
"",
|
||||
"QUESTION BOUNDS:",
|
||||
" - Simple claims: 1 question",
|
||||
" - Moderate claims: 2 questions",
|
||||
" - Complex claims with multiple parts: 3 questions maximum",
|
||||
"",
|
||||
"OUTPUT FORMAT:",
|
||||
" C1 [V]: <claim text>",
|
||||
" Q1: <verification question>",
|
||||
" Q2: <verification question>",
|
||||
" A1 [V]: <assumption text>",
|
||||
" Q1: <verification question>",
|
||||
"",
|
||||
"EXAMPLE:",
|
||||
" C1 [V]: Retrying failed requests creates race condition risk",
|
||||
" Q1: Can a retry succeed after another request has already written?",
|
||||
" Q2: What ordering guarantees exist between concurrent requests?",
|
||||
"",
|
||||
state_requirement,
|
||||
],
|
||||
"next": f"Step {next_step}: Answer questions with factored verification.",
|
||||
"academic_note": (
|
||||
"Chain-of-Verification (Dhuliawala et al., 2023): \"Plan verification questions "
|
||||
"to check its work, and then systematically answer those questions.\""
|
||||
),
|
||||
}
|
||||
|
||||
if step == 4:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Factored Verification",
|
||||
"actions": [
|
||||
"You are a structured decision critic. This verification step is the most important "
|
||||
"in the entire workflow. Your accuracy here directly determines verdict quality. "
|
||||
"Take your time and be rigorous.",
|
||||
"",
|
||||
"Answer each verification question INDEPENDENTLY.",
|
||||
"",
|
||||
"EPISTEMIC BOUNDARY (critical for avoiding confirmation bias):",
|
||||
"",
|
||||
" Answer using ONLY:",
|
||||
" (a) Established domain knowledge - facts you would find in documentation,",
|
||||
" textbooks, or widely-accepted technical references",
|
||||
" (b) Stated constraints - information explicitly provided in the decision context",
|
||||
" (c) Logical inference - deductions from first principles that would hold",
|
||||
" regardless of whether this specific decision is correct",
|
||||
"",
|
||||
" Do NOT:",
|
||||
" - Assume the decision is correct and work backward",
|
||||
" - Assume the decision is incorrect and seek to disprove",
|
||||
" - Reference whether the claim 'should' be true given the decision",
|
||||
"",
|
||||
"SEPARATE your answer from its implication:",
|
||||
" - ANSWER: The factual response to the question (evidence-based)",
|
||||
" - IMPLICATION: What this means for the original claim (judgment)",
|
||||
"",
|
||||
"Then mark each [V] item:",
|
||||
" VERIFIED - Answers are consistent with the claim",
|
||||
" FAILED - Answers reveal inconsistency, error, or contradiction",
|
||||
" UNCERTAIN - Insufficient evidence; state what additional information would resolve",
|
||||
"",
|
||||
"OUTPUT FORMAT:",
|
||||
" C1 [V]: <claim text>",
|
||||
" Q1: <question>",
|
||||
" Answer: <factual answer based on epistemic boundary>",
|
||||
" Implication: <what this means for the claim>",
|
||||
" Status: VERIFIED | FAILED | UNCERTAIN",
|
||||
" Rationale: <one sentence explaining the status>",
|
||||
"",
|
||||
state_requirement,
|
||||
],
|
||||
"next": f"Step {next_step}: Begin challenge phase with adversarial analysis.",
|
||||
"academic_note": (
|
||||
"Chain-of-Verification: \"Factored variants which separate out verification steps, "
|
||||
"in terms of which context is attended to, give further performance gains.\""
|
||||
),
|
||||
}
|
||||
|
||||
# CHALLENGE PHASE
|
||||
if step == 5:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Contrarian Perspective",
|
||||
"actions": [
|
||||
"You are a structured decision critic shifting to adversarial analysis.",
|
||||
"",
|
||||
"Your task: Generate the STRONGEST possible argument AGAINST the decision.",
|
||||
"",
|
||||
"START FROM VERIFICATION RESULTS:",
|
||||
" - FAILED items are direct ammunition - the decision rests on false premises",
|
||||
" - UNCERTAIN items are attack vectors - unverified assumptions create risk",
|
||||
" - Even VERIFIED items may have hidden dependencies worth probing",
|
||||
"",
|
||||
"STEEL-MANNING: Present the opposition's BEST case, not a strawman.",
|
||||
"Ask: What would a thoughtful, well-informed critic with domain expertise say?",
|
||||
"Make the argument as strong as you can, even if you personally disagree.",
|
||||
"",
|
||||
"ATTACK VECTORS TO EXPLORE:",
|
||||
" - What could go wrong that wasn't considered?",
|
||||
" - What alternatives were dismissed too quickly?",
|
||||
" - What second-order effects were missed?",
|
||||
" - What happens if key assumptions change?",
|
||||
" - Who would disagree, and why might they be right?",
|
||||
"",
|
||||
"OUTPUT FORMAT:",
|
||||
"",
|
||||
"CONTRARIAN POSITION: <one-sentence summary of the opposition's stance>",
|
||||
"",
|
||||
"ARGUMENT:",
|
||||
"<Present the strongest 2-3 paragraph case against the decision.",
|
||||
" Reference specific item IDs (C1, A2, etc.) where applicable.",
|
||||
" Build from verification failures if any exist.>",
|
||||
"",
|
||||
"KEY RISKS:",
|
||||
"- <Risk 1 with item ID reference if applicable>",
|
||||
"- <Risk 2>",
|
||||
"- <Risk 3>",
|
||||
"",
|
||||
state_requirement,
|
||||
],
|
||||
"next": f"Step {next_step}: Explore alternative problem framing.",
|
||||
"academic_note": (
|
||||
"Multi-Expert Prompting (Wang et al., 2024): \"Integrating multiple experts' "
|
||||
"perspectives catches blind spots in reasoning.\""
|
||||
),
|
||||
}
|
||||
|
||||
if step == 6:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Alternative Framing",
|
||||
"actions": [
|
||||
"You are a structured decision critic examining problem formulation.",
|
||||
"",
|
||||
"PURPOSE: Step 5 challenged the SOLUTION. This step challenges the PROBLEM STATEMENT.",
|
||||
"Goal: Reveal hidden assumptions baked into how the problem was originally framed.",
|
||||
"",
|
||||
"Set aside the proposed solution temporarily. Ask:",
|
||||
" 'If I approached this problem fresh, how might I state it differently?'",
|
||||
"",
|
||||
"REFRAMING VECTORS:",
|
||||
" - Is this the right problem to solve, or a symptom of a deeper issue?",
|
||||
" - What would a different stakeholder (user, ops, security) prioritize?",
|
||||
" - What if the constraints (K items) were different or negotiable?",
|
||||
" - Is there a simpler formulation that dissolves the tradeoffs?",
|
||||
" - What objectives might be missing from the original framing?",
|
||||
"",
|
||||
"OUTPUT FORMAT:",
|
||||
"",
|
||||
"ALTERNATIVE FRAMING: <one-sentence restatement of the problem>",
|
||||
"",
|
||||
"WHAT THIS FRAMING EMPHASIZES:",
|
||||
"<Describe what becomes important under this new framing that wasn't",
|
||||
" prominent in the original.>",
|
||||
"",
|
||||
"HIDDEN ASSUMPTIONS REVEALED:",
|
||||
"<What did the original problem statement take for granted?",
|
||||
" Reference specific items (C, A, K, J) where the assumption appears.>",
|
||||
"",
|
||||
"IMPLICATION FOR DECISION:",
|
||||
"<Does this reframing strengthen, weaken, or redirect the proposed decision?>",
|
||||
"",
|
||||
state_requirement,
|
||||
],
|
||||
"next": f"Step {next_step}: Synthesize findings into verdict.",
|
||||
"academic_note": None,
|
||||
}
|
||||
|
||||
# SYNTHESIS PHASE
|
||||
if step == 7:
|
||||
return {
|
||||
"phase": phase,
|
||||
"step_title": "Synthesis and Verdict",
|
||||
"actions": [
|
||||
"You are a structured decision critic delivering your final assessment.",
|
||||
"This verdict will guide real decisions. Be confident in your analysis and precise "
|
||||
"in your recommendation.",
|
||||
"",
|
||||
"VERDICT RUBRIC:",
|
||||
"",
|
||||
" ESCALATE when ANY of these apply:",
|
||||
" - Any FAILED item involves safety, security, or compliance",
|
||||
" - Any UNCERTAIN item is critical AND cannot be cheaply verified",
|
||||
" - The alternative framing reveals the problem itself is wrong",
|
||||
"",
|
||||
" REVISE when ANY of these apply:",
|
||||
" - Any FAILED item on a core claim (not peripheral)",
|
||||
" - Multiple UNCERTAIN items on feasibility, effort, or impact",
|
||||
" - Challenge phase revealed unaddressed gaps that change the calculus",
|
||||
"",
|
||||
" STAND when ALL of these apply:",
|
||||
" - No FAILED items on core claims",
|
||||
" - UNCERTAIN items are explicitly acknowledged as accepted risks",
|
||||
" - Challenges from Steps 5-6 are addressable within the current approach",
|
||||
"",
|
||||
"BORDERLINE CASES:",
|
||||
" - When between STAND and REVISE: favor REVISE (cheaper to refine than to fail)",
|
||||
" - When between REVISE and ESCALATE: state both options with conditions",
|
||||
"",
|
||||
"OUTPUT FORMAT:",
|
||||
"",
|
||||
"VERDICT: [STAND | REVISE | ESCALATE]",
|
||||
"",
|
||||
"VERIFICATION SUMMARY:",
|
||||
" Verified: <list IDs>",
|
||||
" Failed: <list IDs with one-line explanation each>",
|
||||
" Uncertain: <list IDs with what would resolve each>",
|
||||
"",
|
||||
"CHALLENGE ASSESSMENT:",
|
||||
" Strongest challenge: <one-sentence summary from Step 5>",
|
||||
" Alternative framing insight: <one-sentence summary from Step 6>",
|
||||
" Response: <how the decision addresses or fails to address these>",
|
||||
"",
|
||||
"RECOMMENDATION:",
|
||||
" <Specific next action. If ESCALATE, specify to whom/what forum.",
|
||||
" If REVISE, specify which items need rework. If STAND, note accepted risks.>",
|
||||
],
|
||||
"next": None,
|
||||
"academic_note": (
|
||||
"Self-Consistency (Wang et al., 2023): \"Correct reasoning processes tend to "
|
||||
"have greater agreement in their final answer than incorrect processes.\""
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"phase": "UNKNOWN",
|
||||
"step_title": "Unknown Step",
|
||||
"actions": ["Invalid step number."],
|
||||
"next": None,
|
||||
"academic_note": None,
|
||||
}
|
||||
|
||||
|
||||
def format_output(step: int, total_steps: int, guidance: dict) -> str:
|
||||
"""Format the output for display."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append(f"DECISION CRITIC - Step {step}/{total_steps}: {guidance['step_title']}")
|
||||
lines.append(f"Phase: {guidance['phase']}")
|
||||
lines.append("")
|
||||
|
||||
# Actions
|
||||
for action in guidance["actions"]:
|
||||
lines.append(action)
|
||||
lines.append("")
|
||||
|
||||
# Academic note if present
|
||||
if guidance.get("academic_note"):
|
||||
lines.append(f"[{guidance['academic_note']}]")
|
||||
lines.append("")
|
||||
|
||||
# Next step or completion
|
||||
if guidance["next"]:
|
||||
lines.append(f"NEXT: {guidance['next']}")
|
||||
else:
|
||||
lines.append("WORKFLOW COMPLETE - Present verdict to user.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Decision Critic - Structured decision criticism workflow"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--step-number",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Current step number (1-7)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-steps",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Total steps in workflow (always 7)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--decision",
|
||||
type=str,
|
||||
help="The decision being criticized (required for step 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--context",
|
||||
type=str,
|
||||
help="Relevant constraints and background (required for step 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--thoughts",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Your analysis, findings, and progress from previous steps",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate step number
|
||||
if args.step_number < 1 or args.step_number > 7:
|
||||
print("ERROR: step-number must be between 1 and 7", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Validate step 1 requirements
|
||||
if args.step_number == 1:
|
||||
if not args.decision:
|
||||
print("ERROR: --decision is required for step 1", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get guidance for current step
|
||||
guidance = get_step_guidance(
|
||||
args.step_number,
|
||||
args.total_steps,
|
||||
args.decision,
|
||||
args.context,
|
||||
)
|
||||
|
||||
# Print decision context on step 1
|
||||
if args.step_number == 1:
|
||||
print("DECISION UNDER REVIEW:")
|
||||
print(args.decision)
|
||||
if args.context:
|
||||
print("")
|
||||
print("CONTEXT:")
|
||||
print(args.context)
|
||||
print("")
|
||||
|
||||
# Print formatted output
|
||||
print(format_output(args.step_number, args.total_steps, guidance))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
# skills/doc-sync/
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
| `README.md` | Skill overview and usage examples | Understanding when to use doc-sync |
|
||||
| `SKILL.md` | Complete skill workflow definition | Executing the doc-sync skill |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `references/` | Trigger pattern examples | Writing good CLAUDE.md triggers |
|
||||
@@ -0,0 +1,46 @@
|
||||
# Doc Sync
|
||||
|
||||
The CLAUDE.md/README.md hierarchy is central to context hygiene. CLAUDE.md files
|
||||
are pure indexes -- tabular navigation with "What" and "When to read" columns
|
||||
that help LLMs (and humans) find relevant files without loading everything.
|
||||
README.md files capture invisible knowledge: architecture decisions, design
|
||||
tradeoffs, and invariants that are not apparent from reading code.
|
||||
|
||||
The doc-sync skill audits and synchronizes this hierarchy across a repository.
|
||||
|
||||
## How It Works
|
||||
|
||||
The skill operates in five phases:
|
||||
|
||||
1. **Discovery** -- Maps all directories, identifies missing or outdated
|
||||
CLAUDE.md files
|
||||
2. **Audit** -- Checks for drift (files added/removed but not indexed),
|
||||
misplaced content (architecture docs in CLAUDE.md instead of README.md)
|
||||
3. **Migration** -- Moves architectural content from CLAUDE.md to README.md
|
||||
4. **Update** -- Creates/updates indexes with proper tabular format
|
||||
5. **Verification** -- Confirms complete coverage and correct structure
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill for:
|
||||
|
||||
- **Bootstrapping** -- Adopting this workflow on an existing repository
|
||||
- **After bulk changes** -- Major refactors, directory restructuring
|
||||
- **Periodic audits** -- Checking for documentation drift
|
||||
- **Onboarding** -- Before starting work on an unfamiliar codebase
|
||||
|
||||
If you use the planning workflow consistently, the technical writer agent
|
||||
maintains documentation as part of execution. As such, doc-sync is primarily for
|
||||
bootstrapping or recovery -- not routine use.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Use your doc-sync skill to synchronize documentation across this repository
|
||||
```
|
||||
|
||||
For targeted updates:
|
||||
|
||||
```
|
||||
Use your doc-sync skill to update documentation in src/validators/
|
||||
```
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
name: doc-sync
|
||||
description: Synchronizes CLAUDE.md navigation indexes and README.md architecture docs across a repository. Use when asked to "sync docs", "update CLAUDE.md files", "ensure documentation is in sync", "audit documentation", or when documentation maintenance is needed after code changes.
|
||||
---
|
||||
|
||||
# Doc Sync
|
||||
|
||||
Maintains the CLAUDE.md navigation hierarchy and optional README.md architecture docs across a repository. This skill is self-contained and performs all documentation work directly.
|
||||
|
||||
## Scope Resolution
|
||||
|
||||
Determine scope FIRST:
|
||||
|
||||
| User Request | Scope |
|
||||
| ------------------------------------------------------- | ----------------------------------------- |
|
||||
| "sync docs" / "update documentation" / no specific path | REPOSITORY-WIDE |
|
||||
| "sync docs in src/validator/" | DIRECTORY: src/validator/ and descendants |
|
||||
| "update CLAUDE.md for parser.py" | FILE: single file's parent directory |
|
||||
|
||||
For REPOSITORY-WIDE scope, perform a full audit. For narrower scopes, operate only within the specified boundary.
|
||||
|
||||
## CLAUDE.md Format Specification
|
||||
|
||||
### Index Format
|
||||
|
||||
Use tabular format with What and When columns:
|
||||
|
||||
```markdown
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ----------- | ------------------------------ | ----------------------------------------- |
|
||||
| `cache.rs` | LRU cache with O(1) operations | Implementing caching, debugging evictions |
|
||||
| `errors.rs` | Error types and Result aliases | Adding error variants, handling failures |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| ----------- | ----------------------------- | ----------------------------------------- |
|
||||
| `config/` | Runtime configuration loading | Adding config options, modifying defaults |
|
||||
| `handlers/` | HTTP request handlers | Adding endpoints, modifying request flow |
|
||||
```
|
||||
|
||||
### Column Guidelines
|
||||
|
||||
- **File/Directory**: Use backticks around names: `cache.rs`, `config/`
|
||||
- **What**: Factual description of contents (nouns, not actions)
|
||||
- **When to read**: Task-oriented triggers using action verbs (implementing, debugging, modifying, adding, understanding)
|
||||
- At least one column must have content; empty cells use `-`
|
||||
|
||||
### Trigger Quality Test
|
||||
|
||||
Given task "add a new validation rule", can an LLM scan the "When to read" column and identify the right file?
|
||||
|
||||
### ROOT vs SUBDIRECTORY CLAUDE.md
|
||||
|
||||
**ROOT CLAUDE.md:**
|
||||
|
||||
```markdown
|
||||
# [Project Name]
|
||||
|
||||
[One sentence: what this is]
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
|
||||
## Build
|
||||
|
||||
[Copy-pasteable command]
|
||||
|
||||
## Test
|
||||
|
||||
[Copy-pasteable command]
|
||||
|
||||
## Development
|
||||
|
||||
[Setup instructions, environment requirements, workflow notes]
|
||||
```
|
||||
|
||||
**SUBDIRECTORY CLAUDE.md:**
|
||||
|
||||
```markdown
|
||||
# [directory-name]/
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
```
|
||||
|
||||
**Critical constraint:** Subdirectory CLAUDE.md files are PURE INDEX. No prose, no overview sections, no architectural explanations. Those belong in README.md.
|
||||
|
||||
## README.md Specification
|
||||
|
||||
### Creation Criteria (Invisible Knowledge Test)
|
||||
|
||||
Create README.md ONLY when the directory contains knowledge NOT visible from reading the code:
|
||||
|
||||
- Multiple components interact through non-obvious contracts or protocols
|
||||
- Design tradeoffs were made that affect how code should be modified
|
||||
- The directory's structure encodes domain knowledge (e.g., processing order matters)
|
||||
- Failure modes or edge cases aren't apparent from reading individual files
|
||||
- There are "rules" developers must follow that aren't enforced by the compiler/linter
|
||||
|
||||
**DO NOT create README.md when:**
|
||||
|
||||
- The directory is purely organizational (just groups related files)
|
||||
- Code is self-explanatory with good function/module docs
|
||||
- You'd be restating what CLAUDE.md index entries already convey
|
||||
|
||||
### Content Test
|
||||
|
||||
For each sentence in README.md, ask: "Could a developer learn this by reading the source files?"
|
||||
|
||||
- If YES: delete the sentence
|
||||
- If NO: keep it
|
||||
|
||||
README.md earns its tokens by providing INVISIBLE knowledge: the reasoning behind the code, not descriptions of the code.
|
||||
|
||||
### README.md Structure
|
||||
|
||||
```markdown
|
||||
# [Component Name]
|
||||
|
||||
## Overview
|
||||
|
||||
[One paragraph: what problem this solves, high-level approach]
|
||||
|
||||
## Architecture
|
||||
|
||||
[How sub-components interact; data flow; key abstractions]
|
||||
|
||||
## Design Decisions
|
||||
|
||||
[Tradeoffs made and why; alternatives considered]
|
||||
|
||||
## Invariants
|
||||
|
||||
[Rules that must be maintained; constraints not enforced by code]
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Discovery
|
||||
|
||||
Map directories requiring CLAUDE.md verification:
|
||||
|
||||
```bash
|
||||
# Find all directories (excluding .git, node_modules, __pycache__, etc.)
|
||||
find . -type d \( -name .git -o -name node_modules -o -name __pycache__ -o -name .venv -o -name target -o -name dist -o -name build \) -prune -o -type d -print
|
||||
```
|
||||
|
||||
For each directory in scope, record:
|
||||
|
||||
1. Does CLAUDE.md exist?
|
||||
2. If yes, does it have the required table-based index structure?
|
||||
3. What files/subdirectories exist that need indexing?
|
||||
|
||||
### Phase 2: Audit
|
||||
|
||||
For each directory, check for drift and misplaced content:
|
||||
|
||||
```
|
||||
<audit_check dir="[path]">
|
||||
CLAUDE.md exists: [YES/NO]
|
||||
Has table-based index: [YES/NO]
|
||||
Files in directory: [list]
|
||||
Files in index: [list]
|
||||
Missing from index: [list]
|
||||
Stale in index (file deleted): [list]
|
||||
Triggers are task-oriented: [YES/NO/PARTIAL]
|
||||
Contains misplaced content: [YES/NO] (architecture/design docs that belong in README.md)
|
||||
README.md exists: [YES/NO]
|
||||
README.md warranted: [YES/NO] (invisible knowledge present?)
|
||||
</audit_check>
|
||||
```
|
||||
|
||||
### Phase 3: Content Migration
|
||||
|
||||
**Critical:** If CLAUDE.md contains content that does NOT belong there, migrate it:
|
||||
|
||||
Content that MUST be moved from CLAUDE.md to README.md:
|
||||
|
||||
- Architecture explanations or diagrams
|
||||
- Design decision documentation
|
||||
- Component interaction descriptions
|
||||
- Overview sections with prose (in subdirectory CLAUDE.md files)
|
||||
- Invariants or rules documentation
|
||||
- Any "why" explanations beyond simple triggers
|
||||
|
||||
Migration process:
|
||||
|
||||
1. Identify misplaced content in CLAUDE.md
|
||||
2. Create or update README.md with the architectural content
|
||||
3. Strip CLAUDE.md down to pure index format
|
||||
4. Add README.md to the CLAUDE.md index table
|
||||
|
||||
### Phase 4: Index Updates
|
||||
|
||||
For each directory needing work:
|
||||
|
||||
**Creating/Updating CLAUDE.md:**
|
||||
|
||||
1. Use the appropriate template (ROOT or SUBDIRECTORY)
|
||||
2. Populate tables with all files and subdirectories
|
||||
3. Write "What" column: factual content description
|
||||
4. Write "When to read" column: action-oriented triggers
|
||||
5. If README.md exists, include it in the Files table
|
||||
|
||||
**Creating README.md (only when warranted):**
|
||||
|
||||
1. Verify invisible knowledge criteria are met
|
||||
2. Document architecture, design decisions, invariants
|
||||
3. Apply the content test: remove anything visible from code
|
||||
4. Keep under ~500 tokens
|
||||
|
||||
### Phase 5: Verification
|
||||
|
||||
After all updates complete, verify:
|
||||
|
||||
1. Every directory in scope has CLAUDE.md
|
||||
2. All CLAUDE.md files use table-based index format
|
||||
3. No drift remains (files <-> index entries match)
|
||||
4. No misplaced content in CLAUDE.md (architecture docs moved to README.md)
|
||||
5. README.md files are indexed in their parent CLAUDE.md
|
||||
6. Subdirectory CLAUDE.md files contain no prose/overview sections
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Doc Sync Report
|
||||
|
||||
### Scope: [REPOSITORY-WIDE | directory path]
|
||||
|
||||
### Changes Made
|
||||
- CREATED: [list of new CLAUDE.md files]
|
||||
- UPDATED: [list of modified CLAUDE.md files]
|
||||
- MIGRATED: [list of content moved from CLAUDE.md to README.md]
|
||||
- CREATED: [list of new README.md files]
|
||||
- FLAGGED: [any issues requiring human decision]
|
||||
|
||||
### Verification
|
||||
- Directories audited: [count]
|
||||
- CLAUDE.md coverage: [count]/[total] (100%)
|
||||
- Drift detected: [count] entries fixed
|
||||
- Content migrations: [count] (architecture docs moved to README.md)
|
||||
- README.md files: [count] (only where warranted)
|
||||
```
|
||||
|
||||
## Exclusions
|
||||
|
||||
DO NOT index:
|
||||
|
||||
- Generated files (dist/, build/, _.generated._, compiled outputs)
|
||||
- Vendored dependencies (node_modules/, vendor/, third_party/)
|
||||
- Git internals (.git/)
|
||||
- IDE/editor configs (.idea/, .vscode/ unless project-specific settings)
|
||||
|
||||
DO index:
|
||||
|
||||
- Hidden config files that affect development (.eslintrc, .env.example, .gitignore)
|
||||
- Test files and test directories
|
||||
- Documentation files (including README.md)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Index Anti-Patterns
|
||||
|
||||
**Too vague (matches everything):**
|
||||
|
||||
```markdown
|
||||
| `config/` | Configuration | Working with configuration |
|
||||
```
|
||||
|
||||
**Content description instead of trigger:**
|
||||
|
||||
```markdown
|
||||
| `cache.rs` | Contains the LRU cache implementation | - |
|
||||
```
|
||||
|
||||
**Missing action verb:**
|
||||
|
||||
```markdown
|
||||
| `parser.py` | Input parsing | Input parsing and format handling |
|
||||
```
|
||||
|
||||
### Correct Examples
|
||||
|
||||
```markdown
|
||||
| `cache.rs` | LRU cache with O(1) get/set | Implementing caching, debugging misses, tuning eviction |
|
||||
| `config/` | YAML config parsing, env overrides | Adding config options, changing defaults, debugging config loading |
|
||||
```
|
||||
|
||||
## When NOT to Use This Skill
|
||||
|
||||
- Single file documentation (inline comments, docstrings) - handle directly
|
||||
- Code comments - handle directly
|
||||
- Function/module docstrings - handle directly
|
||||
- This skill is for CLAUDE.md/README.md synchronization specifically
|
||||
|
||||
## Reference
|
||||
|
||||
For additional trigger pattern examples, see `references/trigger-patterns.md`.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Trigger Patterns Reference
|
||||
|
||||
Examples of well-formed triggers for CLAUDE.md index table entries.
|
||||
|
||||
## Column Formula
|
||||
|
||||
| File | What | When to read |
|
||||
| ------------ | -------------------------------- | ------------------------------------- |
|
||||
| `[filename]` | [noun-based content description] | [action verb] [specific context/task] |
|
||||
|
||||
## Action Verbs by Category
|
||||
|
||||
### Implementation Tasks
|
||||
|
||||
implementing, adding, creating, building, writing, extending
|
||||
|
||||
### Modification Tasks
|
||||
|
||||
modifying, updating, changing, refactoring, migrating
|
||||
|
||||
### Debugging Tasks
|
||||
|
||||
debugging, troubleshooting, investigating, diagnosing, fixing
|
||||
|
||||
### Understanding Tasks
|
||||
|
||||
understanding, learning, reviewing, analyzing, exploring
|
||||
|
||||
## Examples by File Type
|
||||
|
||||
### Source Code Files
|
||||
|
||||
| File | What | When to read |
|
||||
| -------------- | ----------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `cache.rs` | LRU cache with O(1) operations | Implementing caching, debugging cache misses, modifying eviction policy |
|
||||
| `auth.rs` | JWT validation, session management | Implementing login/logout, modifying token validation, debugging auth failures |
|
||||
| `parser.py` | Input parsing, format detection | Modifying input parsing, adding new input formats, debugging parse errors |
|
||||
| `validator.py` | Validation rules, constraint checks | Adding validation rules, modifying validation logic, understanding validation flow |
|
||||
|
||||
### Configuration Files
|
||||
|
||||
| File | What | When to read |
|
||||
| -------------- | -------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `config.toml` | Runtime config options, defaults | Adding new config options, modifying defaults, debugging configuration issues |
|
||||
| `.env.example` | Environment variable template | Setting up development environment, adding new environment variables |
|
||||
| `Cargo.toml` | Rust dependencies, build config | Adding dependencies, modifying build configuration, debugging build issues |
|
||||
|
||||
### Test Files
|
||||
|
||||
| File | What | When to read |
|
||||
| -------------------- | --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `test_cache.py` | Cache unit tests | Adding cache tests, debugging test failures, understanding cache behavior |
|
||||
| `integration_tests/` | Cross-component test suites | Adding integration tests, debugging cross-component issues, validating workflows |
|
||||
|
||||
### Documentation Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ----------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `README.md` | Architecture, design decisions | Understanding architecture, design decisions, component relationships |
|
||||
| `ARCHITECTURE.md` | System design, component boundaries | Understanding system design, component boundaries, data flow |
|
||||
| `API.md` | Endpoint specs, request/response formats | Implementing API endpoints, understanding request/response formats, debugging API issues |
|
||||
|
||||
### Index Files (cross-cutting concerns)
|
||||
|
||||
| File | What | When to read |
|
||||
| ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `error-handling-index.md` | Error handling patterns reference | Understanding error handling patterns, failure modes, error recovery strategies |
|
||||
| `performance-index.md` | Performance optimization reference | Optimizing latency, throughput, resource usage, understanding cost models |
|
||||
| `security-index.md` | Security patterns reference | Implementing authentication, encryption, threat mitigation, compliance features |
|
||||
|
||||
## Examples by Directory Type
|
||||
|
||||
### Feature Directories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| ---------- | --------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `auth/` | Authentication, authorization, sessions | Implementing authentication, authorization, session management, debugging auth issues |
|
||||
| `api/` | HTTP endpoints, request handling | Implementing endpoints, modifying request handling, debugging API responses |
|
||||
| `storage/` | Persistence, data access layer | Implementing persistence, modifying data access, debugging storage issues |
|
||||
|
||||
### Layer Directories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| ----------- | ----------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `handlers/` | Request handlers, routing | Implementing request handlers, modifying routing, debugging request processing |
|
||||
| `models/` | Data models, schemas | Adding data models, modifying schemas, understanding data structures |
|
||||
| `services/` | Business logic, service layer | Implementing business logic, modifying service interactions, debugging workflows |
|
||||
|
||||
### Utility Directories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| ---------- | --------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `utils/` | Helper functions, common patterns | Needing helper functions, implementing common patterns, debugging utility behavior |
|
||||
| `scripts/` | Maintenance tasks, automation | Running maintenance tasks, automating workflows, debugging script execution |
|
||||
| `tools/` | Development tools, CLI utilities | Using development tools, implementing tooling, debugging tool behavior |
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Too Vague (matches everything)
|
||||
|
||||
| File | What | When to read |
|
||||
| ---------- | ------------- | -------------------------- |
|
||||
| `config/` | Configuration | Working with configuration |
|
||||
| `utils.py` | Utilities | When you need utilities |
|
||||
|
||||
### Content Description Only (no trigger)
|
||||
|
||||
| File | What | When to read |
|
||||
| ---------- | --------------------------------------------- | ------------ |
|
||||
| `cache.rs` | Contains the LRU cache implementation | - |
|
||||
| `auth.rs` | Authentication logic including JWT validation | - |
|
||||
|
||||
### Missing Action Verb
|
||||
|
||||
| File | What | When to read |
|
||||
| -------------- | ---------------- | --------------------------------- |
|
||||
| `parser.py` | Input parsing | Input parsing and format handling |
|
||||
| `validator.py` | Validation rules | Validation rules and constraints |
|
||||
|
||||
## Trigger Guidelines
|
||||
|
||||
- Combine 2-4 triggers per entry using commas or "or"
|
||||
- Use action verbs: implementing, debugging, modifying, adding, understanding
|
||||
- Be specific: "debugging cache misses" not "debugging"
|
||||
- If more than 4 triggers needed, the file may be doing too much
|
||||
@@ -0,0 +1,24 @@
|
||||
# skills/incoherence/
|
||||
|
||||
## Overview
|
||||
|
||||
Incoherence detection skill using parallel agents. IMMEDIATELY invoke the
|
||||
script -- do NOT explore first.
|
||||
|
||||
## Index
|
||||
|
||||
| File/Directory | Contents | Read When |
|
||||
| ------------------------ | ----------------- | ------------------ |
|
||||
| `SKILL.md` | Invocation | Using this skill |
|
||||
| `scripts/incoherence.py` | Complete workflow | Debugging behavior |
|
||||
|
||||
## Key Point
|
||||
|
||||
The script IS the workflow. Three phases:
|
||||
|
||||
- Detection (steps 1-12): Survey, explore, verify candidates
|
||||
- Resolution (steps 13-15): Interactive AskUserQuestion prompts
|
||||
- Application (steps 16-21): Apply changes, present final report
|
||||
|
||||
Resolution is interactive - user answers structured questions inline. No manual
|
||||
file editing required.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: incoherence
|
||||
description: Detect and resolve incoherence in documentation, code, specs vs implementation.
|
||||
---
|
||||
|
||||
# Incoherence Detector
|
||||
|
||||
When this skill activates, IMMEDIATELY invoke the script. The script IS the
|
||||
workflow.
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
python3 scripts/incoherence.py \
|
||||
--step-number 1 \
|
||||
--total-steps 21 \
|
||||
--thoughts "<context>"
|
||||
```
|
||||
|
||||
| Argument | Required | Description |
|
||||
| --------------- | -------- | ----------------------------------------- |
|
||||
| `--step-number` | Yes | Current step (1-21) |
|
||||
| `--total-steps` | Yes | Always 21 |
|
||||
| `--thoughts` | Yes | Accumulated state from all previous steps |
|
||||
|
||||
Do NOT explore or detect first. Run the script and follow its output.
|
||||
|
||||
## Workflow Phases
|
||||
|
||||
1. **Detection (steps 1-12)**: Survey codebase, explore dimensions, verify
|
||||
candidates
|
||||
2. **Resolution (steps 13-15)**: Present issues via AskUserQuestion, collect
|
||||
user decisions
|
||||
3. **Application (steps 16-21)**: Apply resolutions, present final report
|
||||
|
||||
Resolution is interactive - user answers structured questions inline. No manual
|
||||
file editing required.
|
||||
+1234
File diff suppressed because it is too large
Load Diff
@@ -1,169 +0,0 @@
|
||||
---
|
||||
name: mvp-architecture-contract
|
||||
description: Load this before designing, extending, or reviewing any MotoVaultPro feature or refactor. It states the load-bearing architecture decisions (and WHY), the invariants that must hold, and the known-weak points. Symptoms that mean you need this skill; adding a new backend feature capsule or API route; a new route returns 401 everywhere or, worse, works WITHOUT a token; a migration "ran" but the table does not exist; a route registered fine but is missing the /api prefix; a new page works on desktop but is unreachable on mobile (or vice versa); a tier-gated feature is accessible to free users; wondering why this is Docker Compose and not Kubernetes; wondering whether userContext.userId is the Auth0 sub (it is not).
|
||||
---
|
||||
|
||||
# MotoVaultPro Architecture Contract
|
||||
|
||||
Authored 2026-07-07. Every code claim below was verified against the repo on that date. Where a doc contradicts this file, re-verify against code: code wins.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you are about to add or modify a backend feature capsule, an API route, a repository, a migration, a frontend page, or tier gating — or when a change "works" but violates one of the failure symptoms in the description above.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- How to branch/commit/PR or what review rules apply: `mvp-change-control`
|
||||
- Triage of a live failure: `mvp-debugging-playbook`
|
||||
- Why a past decision was reverted, incident history: `mvp-failure-archaeology`
|
||||
- Config/secret axes and how to add one: `mvp-config-and-secrets`
|
||||
- Deploy, blue-green, rollback, backups: `mvp-run-and-operate`
|
||||
- VIN/fuel/date domain math: `mvp-vehicle-domain-reference`
|
||||
- Which docs to trust in full detail: `mvp-docs-and-writing`
|
||||
|
||||
## Doc-trust rule (architecture facts)
|
||||
|
||||
For architecture facts: **code > docs/CICD-DEPLOY.md and docs/LOGGING.md > everything else.**
|
||||
`docs/ARCHITECTURE-OVERVIEW.md` and `docs/DATABASE-SCHEMA.md` are known-stale (DATABASE-SCHEMA.md still says "15 feature capsules" and a migration order starting with `admin`; reality is 21 capsules and an order starting with `features/vehicles`). Route to `mvp-docs-and-writing` for the full trust map. When this skill and the code disagree, the code changed after 2026-07-07 — update this skill.
|
||||
|
||||
## Part 1: Load-bearing decisions and WHY
|
||||
|
||||
### 1.1 9-container single-tenant Docker Compose (Kubernetes was tried and abandoned)
|
||||
|
||||
9 containers: Traefik, Frontend (React/nginx), Backend (Fastify), OCR (Python), PostgreSQL, Redis + Loki, Alloy, Grafana. Base file `docker-compose.yml`; staging/prod/blue-green are overlay files.
|
||||
|
||||
WHY compose, not k8s: a Kubernetes-style redesign was planned and executed as a "Kubernetes-like Docker Compose" restructure — commit `040da4c` ("k8s redesign complete", 2025-09-18) added `K8S-STATUS.md` (titled "Kubernetes-like Docker Compose Migration Status") plus compose/Makefile changes; no actual k8s manifests were ever committed anywhere in history (only Markdown planning docs). The status doc was demoted out of the repo root into `docs/changes/` four days later (`8fd7973`, 2025-09-22) and deleted entirely on 2025-10-16 (`5638d39`). The product is single-tenant on VPS-class hardware; the k8s direction bought operational complexity with no scaling need. This is a settled battle — do not propose k8s again without new facts (see `mvp-failure-archaeology`).
|
||||
|
||||
Single-tenant means: one deployment per customer base is NOT the model — there is one user population in one database, all rows scoped per user (section 2.5).
|
||||
|
||||
### 1.2 Backend feature capsules
|
||||
|
||||
All application features are self-contained modules under `backend/src/features/` — 21 capsule directories (the 22nd entry is `CLAUDE.md`). Canonical anatomy (fuel-logs is the reference):
|
||||
|
||||
```
|
||||
backend/src/features/{name}/
|
||||
index.ts # barrel export: routes + anything other capsules may import
|
||||
api/ # {name}.routes.ts (FastifyPluginAsync, per-route preHandlers),
|
||||
# {name}.controller.ts, {name}.validators.ts
|
||||
domain/ # {name}.service.ts, {name}.types.ts (camelCase TS types)
|
||||
data/ # {name}.repository.ts (class taking pool; private mapRow())
|
||||
external/ # cross-feature or third-party clients
|
||||
migrations/ # NNN_name.sql, feature-owned schema
|
||||
tests/ # unit/ and integration/
|
||||
```
|
||||
|
||||
Wiring: `backend/src/app.ts` imports each capsule's routes and registers every one with `{ prefix: '/api' }` (lines ~136-159). Plugin registration order before routes: helmet, cors, logging, error, multipart, auth, admin-guard, tier-guard. Shared infra lives in `backend/src/core/` (plugins, config, logging, middleware, scheduler, storage).
|
||||
|
||||
WHY: capsules keep each feature's schema, logic, and HTTP surface in one directory so an AI session can load one capsule instead of the whole backend — this is the project's stated AI-context-efficiency principle.
|
||||
|
||||
### 1.3 YAML config + file secrets, NOT env vars
|
||||
|
||||
`backend/src/core/config/config-loader.ts` loads a zod-validated YAML file (`CONFIG_PATH`, default `/app/config/production.yml`) plus file-based secrets from `SECRETS_DIR` (default `/run/secrets`). DB, Redis, Auth0, and CORS settings live in YAML. Env vars are the exception (Stripe price IDs, `OCR_SERVICE_URL`, `MIGRATIONS_DIR`, `LOG_LEVEL`, `CONFIG_PATH`/`SECRETS_DIR` themselves). Missing required config or secret = startup crash by design.
|
||||
|
||||
WHY: one schema-validated config document beats scattered env vars for auditability, and file secrets match Docker secret mounts. Do not add `process.env.X` for app config — add it to the zod schema and YAML. Full catalog: `mvp-config-and-secrets`.
|
||||
|
||||
### 1.4 Repository mapRow: snake_case to camelCase, MUST coerce numerics
|
||||
|
||||
Every repository exposes data only through a private mapper (`mapRow()`), converting DB snake_case to TS camelCase. Critically: node-postgres returns `NUMERIC`/`DECIMAL` (OID 1700) as **strings** and this project does NOT override that parser globally — every mapper must coerce (`parseFloat`/`Number`) or the API leaks strings where numbers are typed. This caused a multi-incident bug train (issues #239, #241, #244). New repository with decimal columns = coerce in the mapper, no exceptions.
|
||||
|
||||
### 1.5 DATE columns are plain strings end-to-end
|
||||
|
||||
`backend/src/core/config/database.ts` line 12:
|
||||
|
||||
```typescript
|
||||
types.setTypeParser(1082, (val: string) => val);
|
||||
```
|
||||
|
||||
DATE (OID 1082) values stay `"YYYY-MM-DD"` strings through repository, API, and frontend. WHY: pg's default returns a `Date` at local midnight, which shifts a day when serialized to UTC (issue #237). The full DATE-handling rules (dayjs display, lexicographic sort, the three historical traps) are canonical in `mvp-vehicle-domain-reference` section 3.
|
||||
|
||||
### 1.6 Auth: per-route preHandler; userId is the internal UUID, not the Auth0 sub
|
||||
|
||||
`backend/src/core/plugins/auth.plugin.ts` decorates `fastify.authenticate` (line 120): validates the Auth0 JWT (JWKS, issuer, audience), then loads/creates the `user_profiles` row and hydrates `request.userContext`.
|
||||
|
||||
**`userContext.userId` is the internal `user_profiles.id` UUID, NOT the Auth0 `sub`** (`auth.plugin.ts` line ~130 defaults to auth0Sub, overwritten with `profile.id` at line ~165). This is the post-#206 identity migration state (merged in PR #219). All repositories scope queries by this UUID.
|
||||
|
||||
There is NO global auth hook. Every protected route must list `preHandler: [fastify.authenticate]` itself (see section 2.1).
|
||||
|
||||
### 1.7 Migrations: feature-owned, hard-coded order, run at container start, NO rollback
|
||||
|
||||
- SQL files live in each capsule's `migrations/` dir, executed in lexical order within a feature.
|
||||
- Cross-feature order is the hard-coded `MIGRATION_ORDER` array in `backend/src/_system/migrations/run-all.ts` (17 entries; `features/vehicles` first because it defines `update_updated_at_column()` which later features depend on; `core/identity-migration` last).
|
||||
- Execution is tracked in the `_migrations` table (`UNIQUE(feature, file)`); already-run files are skipped.
|
||||
- Migrations run automatically on every backend container start (`backend/Dockerfile` CMD: `node dist/_system/migrations/run-all.js && npm start`), and manually via `make migrate` or `cd backend && npm run migrate`.
|
||||
- **There is no rollback.** No down migrations. Recovery is restore-from-export (`scripts/import-database.sh`). WHY: single-tenant, small blast radius, and honest acknowledgment that untested down-migrations are worse than a restore path.
|
||||
|
||||
OWNER NON-NEGOTIABLE: no destructive database operation without a fresh backup — that includes schema migrations on staging/prod (`./scripts/export-database.sh --env <env>` first), `make clean` (destroys volumes), and `import-database.sh --drop-existing`. See `mvp-change-control`.
|
||||
|
||||
### 1.8 Redis: cache-aside with mvp: prefix; cache failure never breaks a request
|
||||
|
||||
`backend/src/core/config/redis.ts`: singleton ioredis client; `CacheService` prefixes every key with `mvp:` and wraps every operation in try/catch that logs and returns null/continues. A dead Redis degrades to cache-miss behavior, never a 500. `DistributedLockService` (prefix `mvp:lock:`, SET NX EX + Lua release) backs scheduled jobs. Follow this pattern: read cache, on miss hit DB and set with TTL, invalidate on write — and never let a cache error propagate.
|
||||
|
||||
### 1.9 Frontend dual navigation — deliberate but costly (the registration-checklist tax)
|
||||
|
||||
`frontend/src/App.tsx` forks the entire app at `window.innerWidth <= 768` plus a UA regex (~lines 360-407):
|
||||
- **Desktop**: react-router `<Routes>` under `/garage/*`, lazy-loaded `features/*/pages/*Page`.
|
||||
- **Mobile**: NO router — a Zustand screen switcher (`useNavigationStore().activeScreen`) renders `features/*/mobile/*MobileScreen` components, with two-way URL sync via the `routeToScreen` / `screenToRoute` maps in `frontend/src/core/store/navigation.ts`.
|
||||
|
||||
WHY: the mobile experience is a purpose-built app shell (bottom nav, gesture transitions, error boundaries) rather than responsive pages. The cost is real and accepted: every new page must run the full registration checklist in invariant 2.7, and mobile+desktop are separate implementations that must BOTH be built and tested (hard project rule).
|
||||
|
||||
All frontend HTTP goes through the shared `apiClient` (`frontend/src/core/api/client.ts`), a queued axios wrapper that holds requests until the Auth0 gate reports ready — bypassing it with raw axios/fetch reintroduces the auth race it exists to solve.
|
||||
|
||||
## Part 2: INVARIANTS — break these and here is what happens
|
||||
|
||||
| # | Invariant | If you break it | Enforcing code path |
|
||||
|---|-----------|-----------------|---------------------|
|
||||
| 2.1 | Every protected route lists `preHandler: [fastify.authenticate]` explicitly | **The route is PUBLIC.** No global hook exists; nothing fails loudly. An unlisted route ships as an unauthenticated endpoint on the open internet | `backend/src/core/plugins/auth.plugin.ts:120` (decorator only); routes files per capsule. Intentional public routes: canonical list lives in the header comment of `.claude/skills/mvp-diagnostics-and-logging/scripts/check-route-auth.sh` (webhooks + pre-auth signup routes in its allowlist, plus core-registered routes outside its scan scope: `/health`, `/api/health`, `/auth/verify`, `GET /api/config/feature-tiers`) |
|
||||
| 2.2 | Every repository method returning rows goes through a mapper that coerces NUMERIC/DECIMAL | API returns `"42.50"` where the type says `number`; frontend `.toFixed()` crashes or renders garbage. This has shipped as a bug at least 3 times (#239, #241, #244) | No global parser for OID 1700 in `backend/src/core/config/database.ts` — coercion is per-mapper, by convention and review only |
|
||||
| 2.3 | The DATE parser override stays | Dates shift one day for any user west of UTC (issue #237 regression) | `backend/src/core/config/database.ts:12` `types.setTypeParser(1082, ...)` |
|
||||
| 2.4 | A new capsule with migrations gets appended to `MIGRATION_ORDER` | Its migrations **silently never run** — no error, the runner only walks listed directories. First symptom is "relation does not exist" on staging | `backend/src/_system/migrations/run-all.ts:17-35`; also requires an image rebuild so the SQL is packaged into `/app/migrations` |
|
||||
| 2.5 | All user data scoped by the `user_profiles` UUID (`userContext.userId`) | Cross-user data leak, or zero rows returned if you scope by the Auth0 `sub`. Post-#206, feature tables' `user_id` column is **UUID type** FK to `user_profiles(id) ON DELETE CASCADE` (the migration added `user_profile_id`, backfilled, dropped old VARCHAR `user_id`, then RENAMED `user_profile_id` back to `user_id`); `admin_users` keeps a separate `user_profile_id` column | `backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sql`; `auth.plugin.ts` userContext hydration |
|
||||
| 2.6 | Frontend API calls only via the shared queued `apiClient` | Requests fire before the Auth0 token interceptor is installed: intermittent 401s on first load, spurious error toasts | `frontend/src/core/api/client.ts` (`createQueuedAxios`, `authReady` gate); interceptor installed in `frontend/src/core/auth/Auth0Provider.tsx` |
|
||||
| 2.7 | New pages registered at EVERY registration point. This is the CANONICAL checklist (other skills point here; do not trust any "4 places" shorthand — the full union is 7 steps): (1) desktop `<Route>` in App.tsx; (2) `MobileScreen` union type in navigation.ts; (3) `routeToScreen` map; (4) `screenToRoute` map; (5) lazy import of the `*MobileScreen` component in App.tsx; (6) mobile render block `{activeScreen === "X" && ...}` in App.tsx; (7) a navigation entry point (`BottomNavigation.tsx` or `HamburgerDrawer.tsx` item, or navigation from another screen) | Page unreachable on one platform; registered-but-unreachable on mobile (missing entry point or import); or URL/back-button desync (screen renders but URL lies, deep links break) | `frontend/src/App.tsx` (lazy imports + both render paths), `frontend/src/core/store/navigation.ts` (union line ~5; maps lines ~9, ~29), `frontend/src/shared-minimal/components/mobile/` |
|
||||
| 2.8 | Tier-gated features registered in `FEATURE_TIERS` — **unknown keys FAIL OPEN** | A typo'd or unregistered `featureKey` on a plugin-guarded route silently grants access to ALL tiers. No error, no log at gate time | `backend/src/core/config/feature-tiers.ts:57-62` (`canAccessFeature` returns `true` for unknown keys, by design). Registered-key and vehicle-limit literals: canonical catalog in `mvp-config-and-secrets` section 3 |
|
||||
| 2.9 | Capsule routes registered in `app.ts` with `{ prefix: '/api' }` using the plain (non-fp) export | Route mounts at `/thing` instead of `/api/thing`; Traefik and the frontend both expect `/api` — the endpoint 404s in every deployed environment | `backend/src/app.ts:136-159`; see weak point 3.7 for the platform dual-export trap |
|
||||
|
||||
New-capsule checklist (all must hold): barrel `index.ts`; routes registered in `app.ts` with `/api` prefix; every route lists its auth preHandler; repository mapper coerces numerics; migrations dir appended to `MIGRATION_ORDER`; queries scoped by the profile UUID; if tier-gated, key added to `FEATURE_TIERS` AND spelled identically at the gate site.
|
||||
|
||||
## Part 3: Known-weak points (stated plainly — these are debt, not patterns to copy)
|
||||
|
||||
3.1 **Tier gating fails open AND has two parallel mechanisms with different failure behavior** — the middleware 500s on unknown keys, the plugin decorator fails open (full mechanics and the current key catalog are canonical in `mvp-config-and-secrets` section 3). Both are used, even mixed within one file (`backend/src/features/ocr/api/ocr.routes.ts`). The plugin's dependency check on the auth plugin was deliberately removed for testability — registration order is convention-only. When gating something new, prefer the plugin decorator with a key you have verified exists in `FEATURE_TIERS`.
|
||||
|
||||
3.2 **Controllers string-match error messages for status codes.** No typed error classes: catch blocks do `error.message.includes('not found')` -> 404, `includes('Unauthorized')` -> 403, else 500 (e.g. `backend/src/features/fuel-logs/api/fuel-logs.controller.ts:30,36`). Rewording a service error message silently changes an HTTP status. If you touch a service's thrown messages, grep the controller for `includes(` first.
|
||||
|
||||
3.3 **Auth plugin is a per-request hot path to Auth0.** Worst case per authenticated request: profile `getOrCreate` DB roundtrip, plus up to two Auth0 Management API `getUser` calls (email backfill when the JWT lacks email; verification re-check when the profile is unverified) plus the SDK's token grant — up to ~3 Auth0 HTTP calls. Failures fall back silently to JWT claims (`auth.plugin.ts` ~line 201). Latency and rate-limit exposure live here; do not add more per-request external calls to this plugin.
|
||||
|
||||
3.4 **Dual fuel-logs schema, with a documented convention break.** `backend/src/features/fuel-logs/data/fuel-logs.repository.ts` carries a legacy API (`create`/`mapRow`, gallons columns) and an enhanced API (`createEnhanced`/`mapEnhancedRow`, fuel_units/cost_per_unit, dual-writing legacy columns). `mapEnhancedRow` (line ~252) deliberately returns **snake_case** numeric-coerced rows, and the service's `toEnhancedResponse` (`fuel-logs.service.ts:273`) does the camelCase mapping — a known break from the mapRow convention, typed `any` throughout. Do not copy this pattern into new capsules; do not "fix" it casually either (dual-write compatibility is load-bearing).
|
||||
|
||||
3.5 **App.tsx is a 1192-line god component** (`frontend/src/App.tsx`): inline screen components, all mobile switching, auth routing, and global error suppression in one file. It violates the repo's own RULE 2. Changes here have wide blast radius; keep diffs surgical.
|
||||
|
||||
3.6 **DB pool max is hard-coded 10** (`backend/src/core/config/database.ts:16`) while the config schema defines `database.pool_size` with default 20 (`config-loader.ts:28`) — the config value is not consumed by the pool. YAML pool tuning silently does nothing.
|
||||
|
||||
3.7 **platform.routes.ts dual-export trap.** `backend/src/features/platform/api/platform.routes.ts:45-46` exports both a `fastify-plugin`-wrapped default AND a raw named export. `app.ts` imports the NAMED export, so the `/api` prefix applies. Switching the import to the fp-wrapped default would silently drop the prefix (fastify-plugin breaks encapsulation, so `prefix` is ignored) — every platform dropdown endpoint would move off `/api` with no error.
|
||||
|
||||
3.8 **Two shared frontend trees with no boundary rule:** `frontend/src/shared/` and `frontend/src/shared-minimal/` (theme, GlassCard, BottomNavigation live in shared-minimal). Check both before creating a "new" shared component.
|
||||
|
||||
3.9 **`(fastify as any).requireAdmin`** in `backend/src/features/backup/api/backup.routes.ts` (every route) — the admin-guard decorator is used through an `any` cast, so TypeScript would not catch the decorator being renamed or unregistered; the routes would throw at runtime instead.
|
||||
|
||||
Severity calibration: this is a pre-launch product heading toward paying users. 3.1 and invariant 2.1/2.8 are the security-shaped items — treat regressions there as RULE 0. The rest are RULE 1/2 debt: known, tolerated, but never to be silently extended.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct repo inspection (file reads, greps, git history). Re-verify volatile facts before relying on them:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| 21 feature capsules | `ls backend/src/features/ \| grep -v CLAUDE.md \| wc -l` |
|
||||
| Routes registered with `/api` prefix; platform uses named export | `grep -n "prefix: '/api'" backend/src/app.ts \| wc -l` and `grep -n "platformRoutes" backend/src/app.ts` |
|
||||
| DATE parser override present | `grep -n "setTypeParser(1082" backend/src/core/config/database.ts` |
|
||||
| Pool max hard-coded 10; schema pool_size default 20 | `grep -n "max:" backend/src/core/config/database.ts; grep -n "pool_size" backend/src/core/config/config-loader.ts` |
|
||||
| MIGRATION_ORDER has 17 entries, vehicles first, identity-migration last | `sed -n '17,36p' backend/src/_system/migrations/run-all.ts` |
|
||||
| Tier fail-open (key/limit catalog: `mvp-config-and-secrets` section 3) | `grep -n "return true" backend/src/core/config/feature-tiers.ts; grep -n "minTier" backend/src/core/config/feature-tiers.ts` |
|
||||
| userContext.userId = profile UUID, not Auth0 sub | `grep -n "userId = profile.id" backend/src/core/plugins/auth.plugin.ts` |
|
||||
| Feature tables' user_id is UUID (renamed from user_profile_id) | `grep -n "RENAME COLUMN user_profile_id TO user_id" backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sql` |
|
||||
| Redis `mvp:` prefix, errors swallowed | `grep -n "prefix = 'mvp:'" backend/src/core/config/redis.ts` |
|
||||
| App.tsx 1192 lines; 768px fork | `wc -l frontend/src/App.tsx; grep -n "768" frontend/src/App.tsx` |
|
||||
| Nav registration checklist (2.7 union) | `grep -n "routeToScreen\|screenToRoute\|MobileScreen" frontend/src/core/store/navigation.ts; grep -n "activeScreen ===" frontend/src/App.tsx \| head -3` |
|
||||
| Queued apiClient auth gate | `grep -n "createQueuedAxios\|authReady" frontend/src/core/api/client.ts` |
|
||||
| platform dual export | `grep -n "export" backend/src/features/platform/api/platform.routes.ts \| tail -2` |
|
||||
| requireAdmin any-cast in backup routes | `grep -c "(fastify as any).requireAdmin" backend/src/features/backup/api/backup.routes.ts` |
|
||||
| k8s-style redesign tried/abandoned Sept-Oct 2025 | `git log --all --oneline -i --grep=k8s` and `git log --all --oneline --follow -- docs/changes/K8S-STATUS.md` |
|
||||
| Controller string-matching | `grep -rn "includes('not found')" backend/src/features/*/api/*.controller.ts \| head` |
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
name: mvp-build-and-env
|
||||
description: Load when starting from a fresh MotoVaultPro checkout or when any build/test/install command fails locally. Triggers - "how do I run this", "npm test fails", "Could not resolve a module for a custom reporter", "tdd-guard-jest", "Jest did not exit one second after the test run", jest hangs, "Configuration file not found at /app/config/production.yml", "make setup fails", secrets are directories, "mobile-setup nothing to be done", pytest/paddleocr install, Dockerfile build questions, "why is my migration not on staging". Covers what works on the dev machine vs container-only, the exact local test loop per workspace, Docker build anatomy, and known environment traps.
|
||||
---
|
||||
|
||||
# Build and Environment Reality
|
||||
|
||||
All commands below were verified by running them or reading the exact source on 2026-07-07. Where docs contradict code, code wins; this file states code behavior.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you have a fresh checkout and need to build, lint, type-check, or run tests, or when a local command fails in a confusing way.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Deploying, rollback, staging/prod operations, backups: `mvp-run-and-operate`
|
||||
- What counts as test evidence, adding tests, mobile+desktop validation: `mvp-validation-and-qa`
|
||||
- Issue/branch/PR workflow and review rules: `mvp-change-control`
|
||||
- Runtime failures of a deployed stack: `mvp-debugging-playbook`
|
||||
- Config keys, secrets catalog, feature tiers: `mvp-config-and-secrets`
|
||||
|
||||
## 1. The development model (read this first)
|
||||
|
||||
Development on this project is done by AI sessions working in this repo; the human owner reviews PRs. There is NO fully working local dev loop and none is expected. The loop is:
|
||||
|
||||
1. Edit code locally. Run unit tests + lint + type-check + build locally (per workspace, below).
|
||||
2. Open a PR. CI builds 3 Docker images and deploys them to staging. That is the entire gate — no tests, no lint (canonical statement: `mvp-validation-and-qa` section 1). The only compile check CI performs is `tsc` inside the Dockerfiles.
|
||||
3. End-to-end verification happens ON STAGING (https://staging.motovaultpro.com) after the PR pipeline deploys. See `mvp-validation-and-qa` for the evidence bar.
|
||||
|
||||
Consequence: anything your local commands do not catch, nothing catches before staging. Run the local gates every time; they are the only gates.
|
||||
|
||||
## 2. What works on the dev machine (verified 2026-07-07)
|
||||
|
||||
Root `package.json` has NO scripts and no workspaces. `npm test` / `npm run lint` at repo root fail with "Missing script". Always cd into `backend/`, `frontend/`, or `ocr/`.
|
||||
|
||||
### Bootstrap
|
||||
|
||||
```bash
|
||||
make install # runs npm install in frontend/ AND backend/ (Makefile:220)
|
||||
# or individually:
|
||||
cd backend && npm install
|
||||
cd frontend && npm install
|
||||
```
|
||||
|
||||
### Dev-safe make targets (Makefile:218-254; these never touch Docker)
|
||||
|
||||
| Target | Does |
|
||||
|---|---|
|
||||
| `make install` | `npm install` in frontend + backend |
|
||||
| `make type-check` | `npm run type-check` in frontend + backend |
|
||||
| `make lint` | `npm run lint` in frontend + backend |
|
||||
| `make build-local` | `npm run build` in frontend + backend (outputs `frontend/dist`, `backend/dist`) |
|
||||
|
||||
Every other make target (`setup`, `start`, `rebuild`, `migrate`, `clean`, ...) drives Docker. Per root CLAUDE.md, `make setup`/`make rebuild` are for staging/prod-style builds, NOT development — and on this machine `make setup` does not produce a working stack anyway (section 3).
|
||||
|
||||
### backend/ — everything works
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm run lint # eslint src (flat config eslint.config.js)
|
||||
npm run type-check # tsc --noEmit
|
||||
npm run build # tsc --project tsconfig.build.json -> dist/
|
||||
npm test -- --forceExit # all unit tests
|
||||
npm test -- --forceExit --testPathPattern=src/features/fuel-logs # one feature
|
||||
npm run test:feature --feature=fuel-logs # same, needs --forceExit caveat too
|
||||
```
|
||||
|
||||
`--forceExit` is MANDATORY locally. Without it jest prints "Jest did not exit one second after the test run" and hangs indefinitely (verified: killed after 8+ min; with `--forceExit` the same suite finishes in ~2 s). Cause: `backend/src/core/config/database.ts` creates a pg Pool eagerly and unit tests leave open handles. Do not "fix" a hang by waiting.
|
||||
|
||||
Expect a RED baseline on main: 15 of 25 unit suites fail pre-existing (6 die loading real config, 7 fail ts-jest compilation, 2 contain the 2 genuinely failing tests; `Tests: 2 failed, 147 passed, 149 total`, re-verified 2026-07-09 — baseline detail homed in `mvp-deploy-safety-campaign` Phase 0.3). Judge your change against that baseline, not against zero.
|
||||
|
||||
Why unit tests work at all locally: they mock `core/config/config-loader` and `core/config/redis` at the top of the test file (see `src/features/stations/tests/unit/station-matching.test.ts:5-15`). The real config-loader is an eager singleton (`config-loader.ts:281`: `export const appConfig = configLoader.load()`) that throws `Configuration file not found at /app/config/production.yml` if imported unmocked outside a container. If you see that error in a test, the test (or something it imports) is loading real config — mock it like the existing unit tests do.
|
||||
|
||||
Integration tests (`src/features/*/tests/integration/`) are container-only AND destructive — see section 3.
|
||||
|
||||
Broken script: `npm run migrate:feature` references `src/_system/migrations/run-feature.ts`, which does not exist (only `run-all.ts` does). It fails on invocation.
|
||||
|
||||
### frontend/ — build tools work, `npm test` is broken outside the container
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run lint # works
|
||||
npm run type-check # works
|
||||
npm run build # works: tsc --project tsconfig.build.json && vite build
|
||||
npm run dev # vite dev server on :3000 (UI only; API calls need a backend)
|
||||
```
|
||||
|
||||
`npm test` fails outside the container with:
|
||||
|
||||
```
|
||||
Error: Could not resolve a module for a custom reporter. Module name: tdd-guard-jest
|
||||
```
|
||||
|
||||
Two independent causes in `frontend/jest.config.ts:28-36`:
|
||||
1. The `tdd-guard-jest` reporter is declared only in the ROOT `package.json` devDependencies, and no root `node_modules/` exists on a fresh checkout (root has no scripts, nobody runs `npm install` there), so the module cannot resolve from `frontend/`.
|
||||
2. The reporter config hardcodes `projectRoot: '/home/egullickson/motovaultpro'` — a Linux path from the container/CI host, wrong on this macOS checkout even if the module resolved.
|
||||
|
||||
WORKING FALLBACK (verified 2026-07-07, executes and reports results):
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npx jest src/path/to/File.test.tsx --reporters=default # single file
|
||||
npx jest --reporters=default --testPathPattern=fuel-logs # by pattern
|
||||
npx jest --reporters=default # full suite
|
||||
```
|
||||
|
||||
Trap: `--reporters` is greedy. `npx jest --reporters=default src/Foo.test.tsx` (positional AFTER the flag) swallows the path as a second reporter name and fails with "Could not resolve a module for a custom reporter. Module name: src/Foo...". Put the positional path FIRST, or use `--testPathPattern`.
|
||||
|
||||
Expect the fallback run to be RED at baseline on main: `Tests: 17 failed, 196 passed, 213 total` across 14 failing suites (re-verified 2026-07-09; the baseline numbers are homed in `mvp-deploy-safety-campaign` Phase 0.3). Diff your run against that baseline — do not attribute the pre-existing failures to your change, and do not report "only 1 known failure". There is NO in-container alternative: the shipped frontend image is the nginx production stage (no node/npm) and `frontend/.dockerignore` excludes `*.test.*` from every stage, so `docker compose exec mvp-frontend npm test` (still documented in docs/TESTING.md) has never been able to work. This fallback plus staging verification is the whole frontend test story today.
|
||||
|
||||
Also note: `frontend/test/` contains test files outside jest `roots: ['<rootDir>/src']` — they never run anywhere. See `mvp-validation-and-qa`.
|
||||
|
||||
### ocr/ — tests exist; running them locally is heavyweight and UNVERIFIED
|
||||
|
||||
```bash
|
||||
cd ocr
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
pip install -r requirements.txt # includes paddleocr, google-cloud-vision, google-genai — multi-GB, slow
|
||||
python -m pytest tests/
|
||||
```
|
||||
|
||||
Verified facts only: `ocr/requirements.txt` lists `pytest>=7.4.0` and `pytest-asyncio>=0.21.0`; 16 test files exist under `ocr/tests/`; tests import `from app.main import app`; there is no venv, no `pytest.ini`, and no `conftest.py` in the repo. The pip install and pytest run above were NOT executed during authoring (paddleocr download is too heavy) — UNVERIFIED as a complete recipe. CI never runs OCR tests either (`ocr/Dockerfile` has no test step), so OCR test results always come from a manual run.
|
||||
|
||||
## 3. Container-only, and why
|
||||
|
||||
### Backend integration tests — DESTRUCTIVE, backup first (non-negotiable)
|
||||
|
||||
`backend/src/features/*/tests/integration/*.test.ts` need a real Postgres + Redis and real config (`CONFIG_PATH`, `SECRETS_DIR` — container paths). docs/TESTING.md:24-32 documents `make shell-backend` then `npm test`, but that recipe is STALE-DANGEROUS: the shipped backend image is the production stage (`npm ci --omit=dev` — no jest; only `dist/` plus migration SQL copied), so `npm test` inside it fails with "jest: not found". As-shipped, the integration tests are runnable nowhere; the viable paths are a builder-stage image (`docker build --target builder ...`) or a host run against an ephemeral Postgres/Redis — see `mvp-deploy-safety-campaign` 1B.
|
||||
|
||||
They are DESTRUCTIVE by design: `beforeAll` executes the feature's real migration SQL and `afterAll` drops the tables, e.g. `vehicles.integration.test.ts:38` runs `DROP TABLE IF EXISTS vehicles CASCADE` and drops `update_updated_at_column()` CASCADE (which other tables' triggers depend on); `admin.integration.test.ts:72-73` drops `admin_audit_logs` and `admin_users`. They run against whatever database the container config points at — the shared dev DB `motovaultpro`.
|
||||
|
||||
Owner non-negotiable: never run integration tests against a database you care about without a fresh backup first (`make db-backup` or `./scripts/export-database.sh`). Restore path: `./scripts/import-database.sh`. See `mvp-run-and-operate`.
|
||||
|
||||
Note: `config/app/ci.yml` ("CI-specific configuration for backend tests") exists but nothing in the repo references it — it also points at `mvp-postgres`/db `motovaultpro`, so it does not make the tests safe.
|
||||
|
||||
### Full local stack — currently cannot work on this machine
|
||||
|
||||
`make setup` (compose up --build + migrations) is labeled staging/prod-build only in root CLAUDE.md, and on this checkout it will not produce a healthy stack because four "secret files" exist only as Docker-created EMPTY DIRECTORIES (Docker auto-creates a directory when a bind-mount source file is missing; canonical description of this trap: `mvp-config-and-secrets` section 2):
|
||||
|
||||
```
|
||||
secrets/app/stripe-secret-key.txt <- directory
|
||||
secrets/app/stripe-webhook-secret.txt <- directory
|
||||
secrets/app/auth0-ocr-client-id.txt <- directory
|
||||
secrets/app/auth0-ocr-client-secret.txt <- directory
|
||||
```
|
||||
|
||||
The backend's Zod secrets schema (`config-loader.ts:114-125`) requires `stripe_secret_key` and `stripe_webhook_secret` as non-optional strings; reading a directory fails, validation throws, and the backend crash-loops at startup. The OCR container bind-mounts the two auth0-ocr paths (`docker-compose.yml:213-214`). To ever make the local stack work: `rm -rf` the four directories, create real `.txt` files (see the `.txt.example` siblings), then `make setup`. Until then, local full-stack is off the table — which is consistent with the dev model in section 1.
|
||||
|
||||
Verify current state: `ls -la secrets/app/ | grep '^d'` (any `.txt` entry that is a directory is a broken mount point).
|
||||
|
||||
## 4. Docker build anatomy
|
||||
|
||||
Three Dockerfiles; CI (`.gitea/workflows/staging.yaml`) builds all three on every push to main and every PR sync, tags `:<7-char-sha>` AND `:latest`, and pushes to the registry.
|
||||
|
||||
| Image | Dockerfile | Compile gate | Registry |
|
||||
|---|---|---|---|
|
||||
| backend | `backend/Dockerfile` (2-stage) | `RUN npm run build` (tsc) at line 31 | `git.motovaultpro.com/egullickson/backend` |
|
||||
| frontend | `frontend/Dockerfile` (4-stage) | `RUN npm run build` (tsc + vite) at line 35 | `git.motovaultpro.com/egullickson/frontend` |
|
||||
| ocr | `ocr/Dockerfile` (1-stage) | none (pip install only, no tests) | `git.motovaultpro.com/egullickson/ocr` |
|
||||
|
||||
These tsc runs are the ONLY compile gate CI has. A PR is "green" when the 3 images build and staging boots healthy — nothing else.
|
||||
|
||||
Migrations ship inside the backend image: `backend/Dockerfile:58-61` sets `ENV MIGRATIONS_DIR=/app/migrations` and copies `src/features` and `src/core` there; the container CMD (`Dockerfile:84`) runs `node dist/_system/migrations/run-all.js && npm start` on every start. Therefore a NEW MIGRATION REACHES STAGING/PROD ONLY VIA AN IMAGE REBUILD — merging SQL into the repo does nothing until CI builds and deploys a new backend image. A new feature's migrations also require an entry in `MIGRATION_ORDER` in `backend/src/_system/migrations/run-all.ts` or they silently never run.
|
||||
|
||||
Frontend build-time vs runtime config: all `VITE_*` values are baked at image build via compose/CI build args (`docker-compose.yml:60-64`, ARGs at `frontend/Dockerfile:21-25`). Exception: the Google Maps key/map-id are injected at container RUNTIME by `frontend/scripts/load-config.sh` from `/run/secrets`, not at build.
|
||||
|
||||
PR builds clobber `:latest` — deploy implications and blue-green mechanics are in `mvp-run-and-operate`.
|
||||
|
||||
## 5. Environment traps table
|
||||
|
||||
| Trap | Reality | Do instead |
|
||||
|---|---|---|
|
||||
| Root `npm test`/`npm run lint` | Root `package.json` has NO scripts (root CLAUDE.md is wrong here; code wins) | cd into `backend/` or `frontend/` |
|
||||
| Root dependencies | `test@^3.3.0` (stray/accidental package, supply-chain smell), plus `jest`, `@playwright/test`, `tdd-guard-jest` with no root node_modules — all dead weight | Do not `npm install` at root expecting anything; do not add root deps |
|
||||
| `make mobile-setup` | Advertised in help and `.PHONY` but HAS NO RULE — "Nothing to be done" | Mobile testing procedure: `mvp-validation-and-qa` |
|
||||
| `make clean` | `docker compose down -v --rmi all` — DESTROYS DB VOLUMES wherever run | Non-negotiable: fresh backup first (`make db-backup`) |
|
||||
| Backend jest hang | pg pool open handles; jest never exits | Always `npm test -- --forceExit` locally |
|
||||
| Frontend `npm test` | Broken outside container (tdd-guard-jest reporter + hardcoded Linux projectRoot) | `npx jest <path> --reporters=default`; positional path BEFORE the flag |
|
||||
| Integration tests | `DROP TABLE ... CASCADE` on the shared dev DB | Container-only, backup first, never against a DB you care about |
|
||||
| `make setup` locally | 4 secrets are empty directories; backend Zod validation crash-loops | Treat local full-stack as unavailable; verify on staging |
|
||||
| `frontend/.env.local` | Affects `npm run dev` only; deployed images use build args baked by CI | Change Gitea CI variables / compose build args for deployed values |
|
||||
| New migration "not on staging" | Migrations live inside the backend image at `/app/migrations` | Merge -> CI image build -> deploy; check `_migrations` table |
|
||||
| `npm run migrate:feature` (backend) | Points at nonexistent `run-feature.ts` | `npm run migrate` (runs all, idempotent per file) |
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 against commit e729d42 (main). All commands in sections 2 and the frontend fallback were executed on the dev machine that day, except the OCR venv/pytest recipe (labeled UNVERIFIED). Volatile facts and how to re-check them:
|
||||
|
||||
| Fact | Re-verify with |
|
||||
|---|---|
|
||||
| Root package.json still has no scripts + stray `test` dep | `cat package.json` |
|
||||
| Backend scripts unchanged (test/lint/type-check/build, broken migrate:feature) | `cat backend/package.json` and `ls backend/src/_system/migrations/` |
|
||||
| Frontend jest still broken (reporter + projectRoot) | `grep -n -A6 reporters frontend/jest.config.ts` |
|
||||
| Frontend fallback still works | `cd frontend && npx jest --reporters=default --listTests` |
|
||||
| Backend jest still hangs without forceExit | `cd backend && npx jest src/features/stations/tests/unit/station-matching.test.ts` (should finish; if it hangs, trap still live) |
|
||||
| 4 secrets still empty directories | `ls -la secrets/app/ \| grep '^d'` |
|
||||
| `make mobile-setup` still ruleless | `grep -n "mobile-setup:" Makefile \|\| echo missing` |
|
||||
| Integration tests still DROP TABLE | `grep -rn "DROP TABLE" backend/src --include="*.test.ts"` |
|
||||
| Migrations still image-packaged | `grep -n "MIGRATIONS_DIR\|migrations" backend/Dockerfile` |
|
||||
| CI still runs zero tests/lint | `grep -rn "npm test\|npm run lint" .gitea/workflows/ \|\| echo none` |
|
||||
| Registry image names | `grep -n "egullickson/" .gitea/workflows/staging.yaml \| head` |
|
||||
| OCR pytest deps present | `grep -n "pytest" ocr/requirements.txt` |
|
||||
@@ -1,218 +0,0 @@
|
||||
---
|
||||
name: mvp-change-control
|
||||
description: Load before making ANY change to the MotoVaultPro repo - creating issues, branches, commits, or PRs; deciding whether a change needs an issue, sub-issues, a plan, or owner sign-off; reviewing code (RULE 0/1/2 taxonomy and verdicts); setting Gitea status/type labels; or asking "is this safe to merge / can I run this migration / can I run make clean". Symptom keywords - "create a PR", "what branch name", "commit message format", "which label", "sub-issue", "quality review", "RULE 0", "is CI green", "run the integration tests", "drop database", "edit file on staging server".
|
||||
---
|
||||
|
||||
# MotoVaultPro Change Control
|
||||
|
||||
How changes are classified, gated, and shipped in this repo. Applies to human engineers and AI coding sessions equally. Reality check first: development here is done by AI sessions; the human owner reviews PRs; end-to-end verification happens on STAGING via the PR deploy pipeline because there is no fully working local dev loop.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you are about to create an issue, branch, commit, or PR; when deciding how big a change's process footprint should be; when performing or requesting a quality review; or before any operation that could destroy data or touch a server.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Diagnosing a failure -> `mvp-debugging-playbook`
|
||||
- "Has this been tried before / why is it this way" -> `mvp-failure-archaeology`, `mvp-architecture-contract`
|
||||
- Running tests, what counts as evidence, mobile+desktop validation -> `mvp-validation-and-qa`
|
||||
- Setting up a working environment or the local unit-test loop -> `mvp-build-and-env`
|
||||
- Deploying, rolling back, backup/restore mechanics -> `mvp-run-and-operate`
|
||||
- Fixing the CI pipeline itself -> `mvp-deploy-safety-campaign`
|
||||
|
||||
## 1. The change workflow as actually practiced
|
||||
|
||||
Issues are the source of truth. Gitea (self-hosted Git forge) at `git.motovaultpro.com`, owner `egullickson`, repo `motovaultpro`. Use the Gitea MCP tools (`mcp__gitea-mcp__*`) for all issue/label/branch/PR operations. Sprints and milestones are ABANDONED (owner directive 2026-05-12) - work flows directly from issues by priority. The `.ai/workflow-contract.json` still describes sprints and per-sub-issue status labels; on both points it is stale and `CLAUDE.md` wins.
|
||||
|
||||
### Standard flow
|
||||
|
||||
1. Find or create an issue. Set exactly one `status/*` and one `type/*` label.
|
||||
2. Move it to `status/in-progress` (REPLACE the old status label - see label discipline below).
|
||||
3. Branch off `main`: `issue-{N}-{slug}`, e.g. `issue-246-reorder-log-fuel-fields`.
|
||||
4. Commit as `{type}: {summary} (refs #{N})`, e.g. `feat: add fuel report (refs #42)`. Allowed types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`.
|
||||
5. Self-gate locally (Section 5) - CI will not catch anything for you.
|
||||
6. Open ONE PR targeting `main`. Title `{type}: {summary} (#{N})`. Body must contain `Fixes #{N}` (and `Fixes #M` per sub-issue, one per line). Template: `.gitea/PULL_REQUEST_TEMPLATE.md` - its test-plan checkboxes are honor-system, so fill them honestly.
|
||||
7. Move the issue to `status/review`. Owner reviews and merges (squash or rebase both acceptable - `merge_policy: squash_or_rebase_ok` in `.ai/workflow-contract.json`).
|
||||
8. After merge: `status/done`. Update docs if behavior or interfaces changed (`mvp-docs-and-writing`).
|
||||
|
||||
Honest note: direct pushes to `main` occur and are tolerated for trivial fixes - `git log --first-parent --no-merges main` shows commits like `fix: Guide formatting`, `chore: Update Images`. That is the exception, not the license. Anything substantive - code behavior, schema, config, workflows, dependencies - goes issue -> branch -> PR. A direct push still triggers the full staging deploy pipeline, so it is not "safer" than a PR, just unreviewed.
|
||||
|
||||
Side effect to know: every PR push deploys over the SINGLE shared staging environment (last PR wins). Two open PRs fight over staging; coordinate accordingly.
|
||||
|
||||
### Sub-issue decomposition (3+ files)
|
||||
|
||||
Multi-file changes (3 or more files) must be decomposed into sub-issues so each unit fits a small AI context window:
|
||||
|
||||
| Rule | Convention |
|
||||
|------|------------|
|
||||
| Sub-issue title | `{type}: {summary} (#{parent_index})` - parent index in the title |
|
||||
| Sub-issue body | First line: `Relates to #{parent_index}`; each sub-issue must be executable standalone |
|
||||
| Branches | ONE branch for the parent only (`issue-{parent}-{slug}`). Never a branch per sub-issue |
|
||||
| PRs | ONE PR for the parent. Body lists `Fixes #N` for parent AND every sub-issue |
|
||||
| Commits | Reference the specific sub-issue: `feat: add dashboard (refs #107)` |
|
||||
| Status labels | Tracked on the PARENT only. Sub-issues stay `status/backlog`. (workflow-contract.json says otherwise; it is wrong) |
|
||||
| Plan milestones | Map 1:1 to sub-issues |
|
||||
|
||||
### Label discipline
|
||||
|
||||
Exactly one `status/*` and one `type/*` per issue. When changing status, REPLACE - do not stack. Prefer `mcp__gitea-mcp__replace_issue_labels` (atomic) over add/remove pairs. Gitea marks most of these labels exclusive but `status/blocked` is not, so the tool cannot fully save you from stacking.
|
||||
|
||||
Label IDs (verified against the live repo 2026-07-07):
|
||||
|
||||
| Label | ID | Label | ID |
|
||||
|-------|----|-------|----|
|
||||
| status/backlog | 8 | status/blocked | 12 |
|
||||
| status/ready | 9 | status/done | 13 |
|
||||
| status/in-progress | 10 | type/feature | 14 |
|
||||
| status/review | 11 | type/bug | 15 |
|
||||
| | | type/chore | 16 |
|
||||
| | | type/docs | 17 |
|
||||
|
||||
Lifecycle: `status/backlog` -> `status/ready` -> `status/in-progress` -> `status/review` -> `status/done` (`status/blocked` from any state).
|
||||
|
||||
## 2. Change classification
|
||||
|
||||
| Change | Issue? | Sub-issues? | Written plan? | Owner sign-off before executing? |
|
||||
|--------|--------|-------------|---------------|----------------------------------|
|
||||
| Typo, doc wording, image asset | Optional (direct push tolerated) | No | No | No |
|
||||
| Single-file bug fix | Yes | No | No | No (PR review suffices) |
|
||||
| Feature or fix touching 1-2 files | Yes | No | No | No |
|
||||
| Feature touching 3+ files | Yes | Yes (Section 1) | Yes - plan posted as issue comments, milestones 1:1 with sub-issues | No, unless a row below also applies |
|
||||
| New/changed DB schema migration | Yes | If 3+ files | Yes | YES - migrations have no rollback (verified: 50 migration SQL files under `backend/src/features/*/migrations/`, zero down/rollback scripts). Fresh backup first (Section 4) |
|
||||
| Deploy pipeline (`.gitea/workflows/`, `scripts/ci/`, compose files, Traefik config) | Yes | If 3+ files | Yes | YES - a bad workflow deploys itself; staging.yaml runs on every PR push |
|
||||
| Anything touching a non-negotiable (destructive DB ops, server-side files) | Yes | - | Yes | YES - always |
|
||||
| Dependency major-version bumps, secrets/config axis changes | Yes | If 3+ files | Recommended | Recommended (see `mvp-config-and-secrets`) |
|
||||
|
||||
"Plan" means: decomposition into milestones with acceptance criteria, posted as comments on the issue/sub-issues, reviewed before implementation. "Owner sign-off" means: state the intent and blast radius on the issue or PR and get explicit approval before executing - not after.
|
||||
|
||||
## 3. Quality-review taxonomy: RULE 0/1/2
|
||||
|
||||
Carried forward from the retired agent library (`git show HEAD:.claude/role-agents/quality-reviewer.md` - now deleted from the working tree; this section is the living copy). Apply it to every substantive PR, whether you are the author self-reviewing or a reviewer session.
|
||||
|
||||
Override order: RULE 0 overrides RULE 1; RULE 1 overrides RULE 2.
|
||||
|
||||
### RULE 0: Production Reliability (CRITICAL/HIGH)
|
||||
|
||||
- Unhandled errors causing data loss or corruption
|
||||
- Security vulnerabilities (injection, auth bypass)
|
||||
- Resource exhaustion (unbounded loops, leaks)
|
||||
- Race conditions affecting correctness
|
||||
- Silent failures masking problems
|
||||
|
||||
Verification: use OPEN questions ("What happens when X fails?"), not yes/no.
|
||||
CRITICAL findings require dual-path verification: reason forward from the code to the failure AND backward from the claimed failure to the code before flagging.
|
||||
|
||||
### RULE 1: Project Conformance (HIGH)
|
||||
|
||||
MotoVaultPro-specific standards:
|
||||
|
||||
- Mobile + desktop validation required
|
||||
- snake_case in DB, camelCase in TypeScript
|
||||
- Feature capsule pattern (`backend/src/features/{feature}/` - 21 capsules as of 2026-07-07)
|
||||
- Repository pattern with `mapRow()` for case conversion (and numeric coercion - never return raw pg rows; see `mvp-failure-archaeology`)
|
||||
- CI/CD pipeline must pass
|
||||
|
||||
Verification: cite the specific standard from `CLAUDE.md` or project docs before flagging. No citation, no RULE 1 finding.
|
||||
|
||||
### RULE 2: Structural Quality (SHOULD_FIX/SUGGESTION)
|
||||
|
||||
- God objects (>15 methods or >10 dependencies)
|
||||
- God functions (>50 lines or >3 nesting levels)
|
||||
- Duplicate logic (copy-pasted blocks)
|
||||
- Dead code (unused, unreachable)
|
||||
- Inconsistent error handling
|
||||
|
||||
Verification: confirm project docs do not explicitly permit the pattern.
|
||||
|
||||
### Severity and verdicts
|
||||
|
||||
Severity: CRITICAL (data loss, security breach, system failure) > HIGH (production reliability or project standard violation) > SHOULD_FIX (structural) > SUGGESTION (improvement opportunity).
|
||||
|
||||
Review output format:
|
||||
|
||||
```
|
||||
## VERDICT: [PASS | PASS_WITH_CONCERNS | NEEDS_CHANGES | CRITICAL_ISSUES]
|
||||
|
||||
## Findings
|
||||
### [RULE] [SEVERITY]: [Title]
|
||||
- Location: [file:line]
|
||||
- Issue: [What is wrong]
|
||||
- Failure Mode: [Why this matters]
|
||||
- Suggested Fix: [Concrete action]
|
||||
|
||||
## Considered But Not Flagged
|
||||
[Items examined but not issues, with rationale]
|
||||
```
|
||||
|
||||
The "Considered But Not Flagged" section is mandatory - it proves the review looked at the risky spots and keeps false positives down on re-review. Before flagging anything: read the relevant standards (RULE 1 scope), skip risks already acknowledged in the plan's Known Risks, and only file findings that are actionable with a specific fix.
|
||||
|
||||
Calibration: this is a PRE-LAUNCH product heading toward paying users. RULE 0 findings on billing (subscriptions/Stripe), auth, or data integrity are launch blockers, not debt.
|
||||
|
||||
## 4. The non-negotiables (owner-set, 2026-07-07)
|
||||
|
||||
These are absolute. Never route around them, never "just this once".
|
||||
|
||||
### 4.1 No destructive database operation without a fresh backup
|
||||
|
||||
Take the backup IMMEDIATELY before the operation:
|
||||
|
||||
```bash
|
||||
make db-backup # runs scripts/export-database.sh --output backup_<timestamp> (Makefile:208)
|
||||
```
|
||||
|
||||
Rationale and what counts as destructive:
|
||||
|
||||
- `make clean` runs `docker compose down -v --rmi all` (Makefile:73-76). The `-v` DESTROYS the PostgreSQL data volume. It reads like a tidy-up command; it is a database wipe.
|
||||
- `scripts/import-database.sh --drop-existing` executes `DROP DATABASE IF EXISTS` before importing.
|
||||
- The backend integration tests are destructive to the SHARED dev database: e.g. `backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts` runs the real migration SQL in `beforeAll` and `DROP TABLE IF EXISTS vehicles CASCADE` plus function drops in `afterAll` (the admin integration test does the same for its tables). Running `npm test` in the backend hits these. Backup first, or point the tests at a throwaway database.
|
||||
- Schema migrations on staging/prod have NO rollback path - there are no down migrations anywhere in the repo. The only undo is restoring a backup. The production maintenance workflow (`.gitea/workflows/maintenance.yaml`) has a `create_backup` input defaulting to `yes` - never set it to `no`.
|
||||
|
||||
### 4.2 Never hand-edit files on the staging/prod servers
|
||||
|
||||
Every deploy runs `rsync -av --delete` from the fresh checkout over `$DEPLOY_PATH/config/` and `$DEPLOY_PATH/scripts/` (`.gitea/workflows/staging.yaml:117-118`, `.gitea/workflows/production.yaml:108-109`). `--delete` means any file you create or edit by hand on the server under those paths is silently reverted or removed on the NEXT deploy - which on staging is the next PR push by anyone. Application code is baked into Docker images, so hand edits there do not survive a container restart either. All server changes flow through the repo and the workflows.
|
||||
|
||||
Known standing exception (do not "fix" it): the Docker prune cron on the staging runner host is deliberately out-of-band and NOT in the repo - it manages runner disk, not the application.
|
||||
|
||||
## 5. What "green" honestly means, and the author self-gate
|
||||
|
||||
CI reality: the PR pipeline gates nothing beyond build + boot - a PR can be "green" with failing tests and lint errors, and root `CLAUDE.md`'s contrary CI claims are false (canonical statement and details: `mvp-validation-and-qa` section 1; fixing the gap is `mvp-deploy-safety-campaign`).
|
||||
|
||||
Therefore the AUTHOR is the gate. Before opening a PR, run and honestly report in the PR body:
|
||||
|
||||
```bash
|
||||
# NOTE: root package.json has NO scripts - npm test at repo root fails. Run per package.
|
||||
cd backend && npm run lint && npm run type-check
|
||||
cd frontend && npm run lint && npm run type-check
|
||||
# or both at once: make lint && make type-check (Makefile:226-244, no Docker needed)
|
||||
|
||||
cd backend && npm test -- --forceExit # unit tests; --forceExit because open pg/redis
|
||||
# handles otherwise hang jest (known trap)
|
||||
```
|
||||
|
||||
Container-only pieces (see `mvp-build-and-env` for setup and traps):
|
||||
|
||||
| Check | Where it works |
|
||||
|-------|----------------|
|
||||
| backend lint / type-check / unit tests | Host machine (unit tests: with `--forceExit`) |
|
||||
| backend integration tests | Currently runnable NOWHERE as-shipped: the backend production image has no jest (`npm ci --omit=dev`, dist-only), so docs/TESTING.md's `make shell-backend` + `npm test` recipe fails. Requires a builder-stage image or a host run with ephemeral Postgres/Redis - see `mvp-deploy-safety-campaign` 1B. DESTRUCTIVE, see Section 4.1 |
|
||||
| frontend lint / type-check | Host machine |
|
||||
| frontend jest | Host machine via `npx jest --reporters=default` (`npm test` is broken by the tdd-guard-jest reporter + hardcoded Linux `projectRoot`; no container run exists - nginx-only image, tests dockerignored). Mechanics and known-red baseline: `mvp-build-and-env` |
|
||||
| ocr pytest | `cd ocr && python -m pytest` with deps installed; never runs in CI |
|
||||
|
||||
This checklist is the minimum bar to open a PR. The full evidence bar - integration tests, mobile (320/768px) + desktop (1920px) validation on staging, definition of done - is `mvp-validation-and-qa`.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 against repo HEAD e729d42 and the live Gitea instance. Volatile facts and how to re-verify each:
|
||||
|
||||
- Label names/IDs: `mcp__gitea-mcp__list_repo_labels` (owner egullickson, repo motovaultpro)
|
||||
- CI runs no tests/lint: `grep -nE "npm (run )?(test|lint|type-check)|jest|eslint|pytest" .gitea/workflows/*.yaml` (expect no hits)
|
||||
- rsync --delete lines: `grep -n "rsync" .gitea/workflows/staging.yaml .gitea/workflows/production.yaml`
|
||||
- make clean destroys volumes: `sed -n '73,77p' Makefile`
|
||||
- No rollback migrations: `find backend/src -path "*migrations*" -iname "*down*" -o -path "*migrations*" -iname "*rollback*"` (only false positive: `002_add_vehicle_dropdown_fields.sql`)
|
||||
- Integration-test destructiveness: `grep -rn "DROP TABLE" backend/src/features/*/tests/integration/`
|
||||
- Feature capsule count (21): `ls backend/src/features | grep -v CLAUDE.md | wc -l`
|
||||
- Root package.json has no scripts: `python3 -c "import json; print(json.load(open('package.json')).get('scripts'))"`
|
||||
- Frontend `npm test` broken on host (fallback: `npx jest --reporters=default`): `sed -n '28,36p' frontend/jest.config.ts` (tdd-guard-jest reporter, hardcoded projectRoot)
|
||||
- Merge policy / sub-issue conventions: `python3 -m json.tool .ai/workflow-contract.json` (remember: stale on sprints and sub-issue labels; CLAUDE.md wins)
|
||||
- RULE 0/1/2 original text: `git show e729d42:.claude/role-agents/quality-reviewer.md`
|
||||
- Direct-push tolerance: `git log --first-parent --no-merges --oneline main | head -20`
|
||||
@@ -1,200 +0,0 @@
|
||||
---
|
||||
name: mvp-config-and-secrets
|
||||
description: Load when working with MotoVaultPro configuration or secrets in any form - adding/changing an env var, YAML config field, Docker secret, feature flag, tier gate, vehicle limit, or Stripe price ID. Also load on these symptoms - backend crashes at startup with "Configuration file not found" or "Secrets loading failed", "Secret file not found" errors in logs, a secret file on disk is unexpectedly a DIRECTORY, RESEND_WEBHOOK_SECRET is not configured, Stripe checkout returns wrong tier, VITE_ env var change has no effect until rebuild, Google Maps key missing in frontend, 403 TIER_REQUIRED, or confusion about which env vars a container actually reads.
|
||||
---
|
||||
|
||||
# MotoVaultPro Configuration and Secrets Catalog
|
||||
|
||||
Authored 2026-07-07. All facts verified against repo code on that date. Where any doc contradicts this file or the code, code wins.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you need to know where a configuration value lives, which env vars a service actually reads, what secrets exist and how they get onto servers, how feature tiers gate behavior, or how to add any of these.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Deploy mechanics, blue-green, rollback, .env generation during deploys -> `mvp-run-and-operate`
|
||||
- Getting a local environment running, missing-secret workarounds for local dev -> `mvp-build-and-env`
|
||||
- OCR/Gemini engine knobs in depth (WIF auth chain, engine selection semantics) -> `mvp-ocr-gemini-pipeline`
|
||||
- Why the config architecture is YAML-file based (design rationale) -> `mvp-architecture-contract`
|
||||
- Triage of a live failure whose cause is unknown -> `mvp-debugging-playbook`
|
||||
- Tier/subscription domain semantics (what tiers mean to the business) -> `mvp-vehicle-domain-reference`
|
||||
|
||||
## 1. Config architecture: YAML + file secrets; env vars are the EXCEPTION
|
||||
|
||||
The backend deliberately does NOT use env vars for most configuration. `backend/src/core/config/config-loader.ts` loads:
|
||||
|
||||
1. A YAML config file from `CONFIG_PATH` (default `/app/config/production.yml`), validated by a Zod schema (`configSchema`). The file is the committed, non-sensitive `config/app/production.yml`, bind-mounted read-only by compose. Database host/port/name/user, Redis, Auth0 domain/audience, CORS, health probe timings, and performance settings all live HERE, not in env.
|
||||
2. File-based secrets from `SECRETS_DIR` (default `/run/secrets`), validated by `secretsSchema`. Each secret is one file, name matching the compose mount (no `.txt` inside the container).
|
||||
|
||||
The loader is a module-level singleton: `export const appConfig = configLoader.load()` runs at import time. Missing config file or a missing REQUIRED secret means the backend process throws before the server starts. It also self-injects two values into `process.env` after loading: `RESEND_API_KEY` always, `RESEND_WEBHOOK_SECRET` only if present (config-loader.ts lines 266-270).
|
||||
|
||||
Files (all committed):
|
||||
- `config/app/production.yml` - the live backend config (mounted to `/app/config/production.yml`)
|
||||
- `config/app/production.yml.example` - STALE: mentions minio and admin-postgres hosts and lacks most sections the Zod schema requires. Do not copy it as a template; copy `production.yml` itself.
|
||||
- `config/app/ci.yml` - no references found anywhere in the repo as of 2026-07-07 (candidate dead config)
|
||||
- `config/shared/production.yml` - mounted to `/app/config/shared.yml` by compose, but NO backend code reads it (dead mount as of 2026-07-07)
|
||||
|
||||
### Backend env vars that actually exist (verified against all `process.env` reads in `backend/src/`, non-test)
|
||||
|
||||
| Var | Read at | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `CONFIG_PATH` | `core/config/config-loader.ts:155` | `/app/config/production.yml` | File must exist or startup throws |
|
||||
| `SECRETS_DIR` | `core/config/config-loader.ts:156` | `/run/secrets` | |
|
||||
| `LOG_LEVEL` | `core/logging/logger.ts:10` | `info` (invalid values fall back with a console warning) | Compose sets `${BACKEND_LOG_LEVEL:-debug}` |
|
||||
| `NODE_ENV` | `app.ts` (single read, health payload) | none | Compose sets `production` |
|
||||
| `STRIPE_PRO_MONTHLY_PRICE_ID`, `STRIPE_PRO_YEARLY_PRICE_ID`, `STRIPE_ENTERPRISE_MONTHLY_PRICE_ID`, `STRIPE_ENTERPRISE_YEARLY_PRICE_ID` | `features/subscriptions/domain/subscriptions.service.ts` (plan-to-priceId map ~line 833 and reverse tier inference ~line 885) | Sandbox price IDs baked into `docker-compose.yml` backend env | Staging/prod override via `.env` generated from Gitea Actions variables. Wrong values = checkout works but tier inference breaks |
|
||||
| `OCR_SERVICE_URL` | `features/ocr/external/ocr-client.ts:8` | `http://mvp-ocr:8000` | |
|
||||
| `MIGRATIONS_DIR` | `_system/migrations/run-all.ts:38` | `/app/migrations` set in `backend/Dockerfile:58`; source fallback `../../../migrations` | |
|
||||
| `TERMS_CONTENT_HASH` | `features/terms-agreement/domain/terms-config.ts` | computed fallback | Optional override |
|
||||
| `FROM_EMAIL` | `features/notifications/domain/email.service.ts` | `[email protected]` | |
|
||||
| `BACKUP_STORAGE_PATH` | `features/backup/domain/backup.types.ts` | `/app/data/backups` | |
|
||||
| `HOSTNAME` | `features/backup/domain/backup-archive.service.ts` | `mvp-backend` | Docker-provided |
|
||||
| `RESEND_API_KEY`, `RESEND_WEBHOOK_SECRET` | email service / `features/email-ingestion/external/resend-inbound.client.ts:30` | none | NOT set from outside - config-loader injects them from secret files. See the webhook gap in section 2 |
|
||||
|
||||
DEAD env vars: `DATABASE_HOST` and `REDIS_HOST` are set on the backend service in `docker-compose.yml` but NO backend code reads them. Actual DB/Redis hosts come from the YAML config. Do not "fix" a connection problem by changing these; they do nothing. (Cleanup candidate.)
|
||||
|
||||
### Frontend env vars (Vite - baked at BUILD time)
|
||||
|
||||
Vite inlines `import.meta.env.VITE_*` into the JS bundle during `vite build`. In this project the build happens inside the Docker image build, fed by compose `build.args` (`docker-compose.yml` frontend service) and, in CI, by `--build-arg` values from Gitea Actions variables (`.gitea/workflows/staging.yaml` lines 69-73). Consequence: changing a `VITE_*` value requires REBUILDING the frontend image. Setting it in `.env` or container environment after build does nothing.
|
||||
|
||||
| Var | Consumed at | Compose build-arg default |
|
||||
|---|---|---|
|
||||
| `VITE_AUTH0_DOMAIN` / `VITE_AUTH0_CLIENT_ID` / `VITE_AUTH0_AUDIENCE` | `src/core/auth/Auth0Provider.tsx` | Real Auth0 tenant values |
|
||||
| `VITE_API_BASE_URL` | `src/core/api/client.ts`, `src/features/auth/api/auth.api.ts` | `/api` |
|
||||
| `VITE_STRIPE_PUBLISHABLE_KEY` | `src/features/subscription/pages/SubscriptionPage.tsx`, `mobile/SubscriptionMobileScreen.tsx` | none (empty = Stripe silently broken) |
|
||||
| `VITE_LOG_LEVEL` | `src/utils/logger.ts` (default `info`) | not passed as a build arg - container builds always get `info` |
|
||||
|
||||
`import.meta.env.MODE === 'development'` gates debug panels; always `production` in container builds.
|
||||
|
||||
EXCEPTION - runtime injection: the Google Maps API key and Map ID are NOT build-time. `frontend/scripts/load-config.sh` runs at container start (`frontend/Dockerfile:81` CMD) and reads `/run/secrets/google-maps-api-key` and `/run/secrets/google-maps-map-id` (mounted from `secrets/app/*.txt`), writing them into `/usr/share/nginx/html/config.js` as `window.CONFIG`. So Maps keys rotate with a container restart, no rebuild. `VITE_GOOGLE_MAPS_API_KEY` in `frontend/.env.example` is for local `npm run dev` only.
|
||||
|
||||
### OCR service env vars (`ocr/app/config.py`, all `os.getenv`)
|
||||
|
||||
| Var | Code default | Compose override (`docker-compose.yml` mvp-ocr) |
|
||||
|---|---|---|
|
||||
| `LOG_LEVEL` | `info` | `${BACKEND_LOG_LEVEL:-debug}` |
|
||||
| `HOST` / `PORT` | `0.0.0.0` / `8000` | not set |
|
||||
| `OCR_PRIMARY_ENGINE` | `paddleocr` | `google_vision` |
|
||||
| `OCR_FALLBACK_ENGINE` | `none` | `paddleocr` |
|
||||
| `OCR_CONFIDENCE_THRESHOLD` / `OCR_FALLBACK_THRESHOLD` | `0.6` / `0.6` | `0.6` / `0.6` |
|
||||
| `GOOGLE_VISION_KEY_PATH` | `/run/secrets/google-wif-config.json` | same |
|
||||
| `VISION_MONTHLY_LIMIT` | `1000` | `1000` |
|
||||
| `VERTEX_AI_PROJECT` | `""` | `motovaultpro` |
|
||||
| `VERTEX_AI_LOCATION` | `global` | `global` |
|
||||
| `GEMINI_MODEL` | `gemini-2.5-flash` | `gemini-3-flash-preview` (compose wins in containers) |
|
||||
| `REDIS_HOST` / `REDIS_PORT` / `REDIS_DB` | `mvp-redis` / `6379` / `1` | same (OCR uses Redis DB 1; backend uses DB 0 via YAML) |
|
||||
|
||||
The compose defaults differ from code defaults - when reasoning about container behavior, read compose, not `config.py`. Engine semantics: see `mvp-ocr-gemini-pipeline`.
|
||||
|
||||
## 2. Secrets inventory (names and paths ONLY - never print values)
|
||||
|
||||
All app secrets live in `secrets/app/` on the host and are bind-mounted read-only to `/run/secrets/<name-without-.txt>` in containers. Real `.txt` files are gitignored; only `.example` siblings and `google-wif-config.json` are committed (the WIF config is checked in by design - it contains no key material, it is a Workload Identity Federation descriptor).
|
||||
|
||||
Full set (matches `scripts/inject-secrets.sh` SECRET_FILES, 12 files):
|
||||
`postgres-password.txt`, `auth0-client-secret.txt`, `auth0-management-client-id.txt`, `auth0-management-client-secret.txt`, `auth0-ocr-client-id.txt`, `auth0-ocr-client-secret.txt`, `google-maps-api-key.txt`, `google-maps-map-id.txt`, `cloudflare-dns-token.txt`, `resend-api-key.txt`, `stripe-secret-key.txt`, `stripe-webhook-secret.txt` - plus committed `google-wif-config.json`.
|
||||
|
||||
Who mounts what (base `docker-compose.yml`; `docker-compose.blue-green.yml` mirrors via anchors):
|
||||
- backend: postgres-password, auth0-client-secret, auth0-management-client-id/secret, google-maps-api-key, google-maps-map-id, resend-api-key, stripe-secret-key, stripe-webhook-secret
|
||||
- frontend: google-maps-api-key, google-maps-map-id (runtime injection, section 1)
|
||||
- ocr: auth0-ocr-client-id, auth0-ocr-client-secret, google-wif-config.json
|
||||
- traefik: cloudflare-dns-token (DNS-01 cert challenge)
|
||||
- postgres: postgres-password (`POSTGRES_PASSWORD_FILE`)
|
||||
|
||||
The backend config-loader `loadSecrets()` reads exactly these 9 names: `postgres-password`, `auth0-client-secret`, `auth0-management-client-id`, `auth0-management-client-secret`, `google-maps-api-key`, `resend-api-key`, `resend-webhook-secret`, `stripe-secret-key`, `stripe-webhook-secret`. All required by the Zod schema except `resend-webhook-secret` (optional).
|
||||
|
||||
Anomalies to know (verified 2026-07-07):
|
||||
- `auth0-ocr-client-id.txt` / `auth0-ocr-client-secret.txt` are mounted into mvp-ocr and required by inject-secrets, but NO Python code reads them (only a docstring mentions Auth0 M2M). Mounted-but-unread; do not assume OCR auth is enforced by them.
|
||||
- `cloudflare-dns-token.txt` and `google-maps-map-id.txt` have no `.example` sibling.
|
||||
|
||||
### Injection flow - editing secrets on servers is futile
|
||||
|
||||
On EVERY staging and production deploy, `scripts/inject-secrets.sh` regenerates ALL 12 secret files under `$DEPLOY_PATH/secrets/app` from Gitea Actions secrets (env vars `POSTGRES_PASSWORD`, `AUTH0_CLIENT_SECRET`, `AUTH0_MANAGEMENT_CLIENT_ID`, `AUTH0_MANAGEMENT_CLIENT_SECRET`, `AUTH0_OCR_CLIENT_ID`, `AUTH0_OCR_CLIENT_SECRET`, `GOOGLE_MAPS_API_KEY`, `GOOGLE_MAPS_MAP_ID`, `CF_DNS_API_TOKEN`, `RESEND_API_KEY`, `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`). Any missing variable fails the deploy. Files are written chmod 644 inside a chmod 700 dir. `google-wif-config.json` is copied from the checkout by the workflows themselves (staging.yaml ~lines 123-125, production.yaml ~lines 115-117).
|
||||
|
||||
Therefore: to rotate or fix a secret, update the Gitea Actions secret (repo settings or `mcp__gitea-mcp__upsert_repo_action_secret`) and redeploy. Hand-editing `/opt/motovaultpro/secrets/app/*` survives only until the next deploy - and hand-editing server files is an owner non-negotiable violation anyway.
|
||||
|
||||
### The missing-secret DIRECTORY trap
|
||||
|
||||
Docker bind-mounting a host file that does not exist silently creates an empty DIRECTORY at that path. Symptoms: backend logs `Secret file not found` or `Failed to read secret file` (EISDIR), then `Secrets loading failed`; `ls -la secrets/app/` shows `drwxr-xr-x` entries ending in `.txt`. This is the current state of several secrets on the dev machine (as of 2026-07-07: `auth0-ocr-client-id.txt`, `auth0-ocr-client-secret.txt`, `stripe-secret-key.txt`, `stripe-webhook-secret.txt` are directories locally). `inject-secrets.sh` and both deploy workflows carry `rm -rf` cleanup for exactly this failure mode. Local fix: `rm -rf secrets/app/<name>.txt` then create the file with a real value (or, if you only need the stack to build, see `mvp-build-and-env` - do not invent fake Stripe keys and call billing "tested").
|
||||
|
||||
### Known gap: resend-webhook-secret
|
||||
|
||||
`resend-webhook-secret` is in the config-loader's load list (optional in the schema) but: no `secrets/app/resend-webhook-secret.txt` exists, no compose file mounts it, inject-secrets.sh has no entry for it, and no Gitea secret feeds it. So `RESEND_WEBHOOK_SECRET` is never set anywhere, the backend logs `Secret file not found: /run/secrets/resend-webhook-secret` on every boot (harmless noise), and `features/email-ingestion/external/resend-inbound.client.ts:39` throws `RESEND_WEBHOOK_SECRET is not configured` if webhook signature verification is exercised. Resend inbound webhook verification is presently UNCONFIGURED (known gap, 2026-07-07). Closing it = follow the "new secret" checklist in section 4 for `resend-webhook-secret.txt` plus a backend compose mount.
|
||||
|
||||
## 3. Feature flags / tiers as config
|
||||
|
||||
There is no external flag service. Tier gating is code-as-config in `backend/src/core/config/feature-tiers.ts`:
|
||||
|
||||
- `TIER_LEVELS`: `free(0) < pro(1) < enterprise(2)`, hierarchical (higher tier inherits lower).
|
||||
- `FEATURE_TIERS` registry - 4 entries as of 2026-07-07, all `minTier: 'pro'`: `document.scanMaintenanceSchedule`, `vehicle.vinDecode`, `fuelLog.receiptScan`, `maintenance.receiptScan`. Each has `name` and `upgradePrompt`.
|
||||
- `VEHICLE_LIMITS`: free=2, pro=5, enterprise=null (unlimited), with `canAddVehicle()` / `getVehicleLimitConfig()`.
|
||||
- FAIL-OPEN: `canAccessFeature()` returns TRUE for unknown feature keys (deliberate). A typo in a feature key silently un-gates the feature wherever `canAccessFeature` is called directly (e.g. `features/documents/api/documents.controller.ts`).
|
||||
|
||||
Two enforcement mechanisms, both returning 403 `{error: 'TIER_REQUIRED', requiredTier, currentTier, featureName, upgradePrompt, ...}`:
|
||||
1. Standalone middleware `requireTier('<featureKey>')` from `core/middleware/require-tier.ts` - must come AFTER `requireAuth` in the preHandler array; fails CLOSED (500) on unknown keys.
|
||||
2. Fastify decorator `fastify.requireTier({minTier}|{featureKey})` from `core/plugins/tier-guard.plugin.ts` - calls `authenticate` itself; the featureKey path FAILS OPEN on unknown keys, exactly like direct `canAccessFeature` calls: `canAccessFeature` returns true for unregistered keys (tier-guard.plugin.ts:70), so a typo'd or unregistered featureKey silently un-gates the route for ALL tiers. The `config?.minTier || 'pro'` fallback (line 73) only shapes the 403 denial payload and is never reached for unknown keys. Only the standalone middleware (mechanism 1) fails closed on unknown keys.
|
||||
|
||||
Live examples: `features/ocr/api/ocr.routes.ts` lines 29/35/41 and `features/vehicles/api/vehicles.routes.ts` line 80.
|
||||
|
||||
Frontend contract: `GET /api/config/feature-tiers` (public, no auth - `core/config/config.routes.ts`) returns `{tiers: TIER_LEVELS, features: FEATURE_TIERS}`. Consumed by `frontend/src/core/hooks/useTierAccess.ts` (react-query key `feature-tiers`); denial UX via `frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx`; vehicle limits via `frontend/src/features/vehicles/hooks/useVehicleLimitCheck.ts`. The user's own tier comes from their profile (`subscription_tier`), not this endpoint.
|
||||
|
||||
## 4. How to add a configuration axis
|
||||
|
||||
Reminder: there is no working full local loop for most of these (missing local secrets). End-to-end proof happens on STAGING via the PR pipeline. CI runs zero tests/lint - the only gate is that images build and staging boots healthy - so run `npm run type-check` and relevant tests yourself in `backend/` / `frontend/` before pushing (see `mvp-validation-and-qa`).
|
||||
|
||||
### A. New YAML config field (non-sensitive backend config)
|
||||
1. Add the field to `configSchema` in `backend/src/core/config/config-loader.ts` (give it `.optional().default(...)` unless you can update every environment's YAML in the same PR - a required field missing from the mounted YAML crashes the backend at import time).
|
||||
2. Add the value to `config/app/production.yml` (this one committed file serves all environments; there are no per-env YAML variants).
|
||||
3. No compose change needed - `config/app/production.yml` is already mounted. Consume via `appConfig.config.<section>.<field>`.
|
||||
4. Deploy note: config reaches servers via the workflows' rsync of `config/`; backend containers read it at start, so the normal deploy (which recreates backend) picks it up.
|
||||
|
||||
### B. New secret
|
||||
1. Create `secrets/app/<name>.txt` locally with the real value AND a committed `<name>.txt.example` sibling with a placeholder.
|
||||
2. If the backend reads it: add the filename to the `secretFiles` array in `loadSecrets()` AND a snake_case key to `secretsSchema` in `config-loader.ts` (use `.optional()` only if genuinely optional - remember the resend-webhook-secret gap started life as "optional").
|
||||
3. Mount it in EVERY compose file that runs the consuming service: base `docker-compose.yml` and, for backend/frontend, the YAML-anchored blocks in `docker-compose.blue-green.yml` (prod runs blue/green services, not the base ones).
|
||||
4. Add the Gitea Actions secret (repo settings) and an `inject_secret "VAR_NAME" "<name>.txt"` entry plus SECRET_FILES entry in `scripts/inject-secrets.sh` - otherwise the next deploy will not create the file on the server and Docker will manufacture the directory trap.
|
||||
5. Verify on staging: deploy, then `docker exec mvp-backend-staging ls -l /run/secrets/` (names only - never cat values into logs or PRs).
|
||||
|
||||
### C. New env var
|
||||
1. Set it in the consuming service's `environment:` block in `docker-compose.yml` (and the anchored env in `docker-compose.blue-green.yml` for backend/frontend), with a sane `${VAR:-default}`.
|
||||
2. Read it in exactly one place in the consumer (backend: prefer YAML config unless it is genuinely deploy-varying like the Stripe price IDs; ocr: `ocr/app/config.py`).
|
||||
3. If the value differs per environment, add it to the `.env` generation step in `.gitea/workflows/staging.yaml` and `production.yaml` (Gitea Actions variable) and document it in `.env.example`.
|
||||
4. Frontend `VITE_*`: add a compose `build.args` entry AND `--build-arg` lines in the staging workflow build step - and remember it is baked at build time.
|
||||
|
||||
### D. New tier-gated feature
|
||||
1. Add the entry to `FEATURE_TIERS` in `backend/src/core/config/feature-tiers.ts` (key, minTier, name, upgradePrompt).
|
||||
2. Guard the route: `preHandler: [requireAuth, requireTier('<key>')]` or `fastify.requireTier({featureKey: '<key>'})`. Copy the exact key string - fail-open means a typo disables the gate silently. Add a test in `backend/src/core/config/tests/feature-tiers.test.ts` style.
|
||||
3. Frontend: gate the UI with `useTierAccess` and route denials to `UpgradeRequiredDialog`. The feature list arrives automatically via `GET /api/config/feature-tiers` - no frontend registry to update.
|
||||
4. Validate on BOTH mobile and desktop (project hard requirement).
|
||||
|
||||
### E. New Stripe price
|
||||
1. Add the env var to the backend `environment:` block in `docker-compose.yml` AND the blue-green anchor, and to `.env.example`.
|
||||
2. Wire it into BOTH maps in `backend/src/features/subscriptions/domain/subscriptions.service.ts`: the plan-name-to-env-var map (~line 833) and the reverse priceId-to-tier inference (~line 885). Missing the reverse map means webhooks classify the subscription as the wrong tier.
|
||||
3. Add the Gitea Actions VARIABLE (not secret - price IDs are config) and echo it into `.env` in both deploy workflows' env-generation step.
|
||||
4. The publishable key side (`VITE_STRIPE_PUBLISHABLE_KEY`) is build-time frontend config - see C.4.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct inspection of the repo at commit e729d42 era. Volatile facts and how to re-verify each (run from repo root):
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| Backend env-var catalog (and DATABASE_HOST/REDIS_HOST still dead) | `grep -rnoE "process\.env(\.[A-Z_]+|\['[A-Z_]+'\])" backend/src --include="*.ts" \| grep -v ".test." \| sort -u` |
|
||||
| CONFIG_PATH / SECRETS_DIR defaults | `grep -n "CONFIG_PATH\|SECRETS_DIR" backend/src/core/config/config-loader.ts` |
|
||||
| The 9 backend-loaded secret names and which are optional | `sed -n '113,190p' backend/src/core/config/config-loader.ts` |
|
||||
| resend-webhook-secret still unconfigured | `grep -rn "resend-webhook" docker-compose*.yml scripts/inject-secrets.sh; ls secrets/app/ \| grep resend` |
|
||||
| inject-secrets file list (12) and required env vars | `grep -n "inject_secret \|SECRET_FILES" scripts/inject-secrets.sh` |
|
||||
| Compose secret mounts per service | `grep -n "/run/secrets" docker-compose.yml docker-compose.blue-green.yml` |
|
||||
| Secrets committed to git (should be .example + google-wif-config.json only) | `git ls-files secrets/` |
|
||||
| Local directory-trap state | `ls -la secrets/app/ \| grep ^d` |
|
||||
| FEATURE_TIERS entries and VEHICLE_LIMITS (free 2 / pro 5 / enterprise unlimited) | `grep -n "minTier\|VEHICLE_LIMITS" -A3 backend/src/core/config/feature-tiers.ts` |
|
||||
| Fail-open on unknown feature keys | `sed -n '57,64p' backend/src/core/config/feature-tiers.ts` |
|
||||
| feature-tiers endpoint shape | `cat backend/src/core/config/config.routes.ts` |
|
||||
| requireTier usage sites | `grep -rn "requireTier" backend/src/features --include="*.ts" \| grep -v test` |
|
||||
| Stripe price env vars in service maps | `grep -n "STRIPE_.*PRICE_ID" backend/src/features/subscriptions/domain/subscriptions.service.ts` |
|
||||
| Frontend VITE_* consumption | `grep -rnoE "import\.meta\.env\.[A-Z_]+" frontend/src \| sort -u` |
|
||||
| Frontend runtime Maps-key injection | `cat frontend/scripts/load-config.sh; grep -n "load-config" frontend/Dockerfile` |
|
||||
| OCR env catalog and compose overrides | `cat ocr/app/config.py; sed -n '/mvp-ocr:/,/mvp-postgres:/p' docker-compose.yml` |
|
||||
| config/app/ci.yml and config/shared/production.yml still unreferenced/unread | `grep -rn "ci\.yml" backend/ scripts/ .gitea/ docker-compose*.yml Makefile; grep -rn "shared.yml" backend/src` (grep's `--include` does NOT expand brace globs like `*.{ts,sh}` - it silently matches nothing) |
|
||||
| production.yml.example still stale vs schema | `diff <(grep -oE "^[a-z_]+:" config/app/production.yml) <(grep -oE "^[a-z_]+:" config/app/production.yml.example)` |
|
||||
| Workflow .env generation and wif-config copy | `grep -n "generate-log-config\|inject-secrets\|google-wif" .gitea/workflows/staging.yaml .gitea/workflows/production.yaml` |
|
||||
| auth0-ocr secrets still mounted-but-unread | `grep -rni "auth0" ocr/app --include="*.py"` |
|
||||
@@ -1,246 +0,0 @@
|
||||
---
|
||||
name: mvp-debugging-playbook
|
||||
description: Symptom-to-cause triage for MotoVaultPro. Load when debugging any of - numbers display wrong, .toFixed is not a function, values arrive as strings, dates off by one day, unexpected 403 (EMAIL_NOT_VERIFIED, TIER_REQUIRED, Unauthorized), stale data after create/update/delete, deleted record still shows, blank page on load, API calls never fire, bug only on mobile or only on desktop, route unexpectedly public or unprotected, VIN decode timeout or failure, OCR/receipt extraction returns empty fields, migration did not run, missing table or column, container unhealthy, staging deploy failed, health check failing, jest hangs or never exits. Gives the discriminating check and fix pattern for each known failure mode.
|
||||
---
|
||||
|
||||
# MotoVaultPro Debugging Playbook
|
||||
|
||||
Known failure modes of this codebase, each with a discriminating check (an observation that separates this cause from lookalikes) and the sanctioned fix pattern. All file paths are repo-relative. All line numbers verified 2026-07-07 and may drift; the "Provenance and maintenance" section at the end has re-verification commands.
|
||||
|
||||
**When to use:** you have a symptom (bug report, failing staging deploy, weird API response) and need to know where to look and what the likely cause is.
|
||||
|
||||
**When NOT to use — route instead:**
|
||||
- Investigation smells like a past battle (date bugs, numeric-string bugs, OCR engine behavior, auth races, "why is there no VIN cache") -> read `mvp-failure-archaeology` FIRST so you do not re-fight a settled war or reintroduce a reverted fix.
|
||||
- You need LogQL recipes, Grafana access, health-check anatomy, or container debugging technique -> `mvp-diagnostics-and-logging`.
|
||||
- You need to set up an environment or run tests at all -> `mvp-build-and-env`.
|
||||
- You are deciding how to ship the fix (issue/branch/PR, review rules) -> `mvp-change-control`.
|
||||
- You need rigorous root-cause methodology or benchmark discipline -> `mvp-proof-and-analysis-toolkit`.
|
||||
- Deploy/rollback mechanics on staging or prod -> `mvp-run-and-operate`.
|
||||
|
||||
**Environment reality (2026-07-07):** development is done by AI sessions; end-to-end verification happens on STAGING via the PR deploy pipeline (every PR push builds images and redeploys staging). There is no fully working local dev loop. CI gates nothing beyond build + boot (canonical statement: `mvp-validation-and-qa` section 1) — do not assume "CI passed" means anything about correctness.
|
||||
|
||||
**Owner non-negotiables — never route around them while debugging:**
|
||||
1. No destructive database operations without a fresh backup. This includes `make clean` (destroys volumes), `scripts/import-database.sh --drop-existing`, schema migrations on staging/prod, and the backend integration tests, which `DROP TABLE ... CASCADE` on the shared dev database (e.g. `backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts:38`).
|
||||
2. Never hand-edit files on the staging/prod servers — `rsync --delete` on every deploy reverts them. All server changes flow through the repo and workflows.
|
||||
|
||||
## First five minutes checklist
|
||||
|
||||
Before forming any theory, gather these observations (cross-ref `mvp-diagnostics-and-logging` for the full cookbook):
|
||||
|
||||
1. **Health endpoints.** `curl -s https://staging.motovaultpro.com/api/health | jq .` — must be `status: "healthy"` with a `features` array (20 features listed in `backend/src/app.ts`; the staging verify job requires a 13-feature subset, `.gitea/workflows/staging.yaml`).
|
||||
2. **Container state** (on the server, `/opt/motovaultpro`): `docker compose -f docker-compose.yml -f docker-compose.staging.yml ps` — look for `unhealthy` or restart loops. Container names on staging carry a `-staging` suffix (`mvp-backend-staging`); dev/base names are `mvp-backend`, `mvp-ocr`, etc.
|
||||
3. **Backend request logs in Grafana** (logs.staging.motovaultpro.com, RFC1918-only access):
|
||||
`{container="mvp-backend-staging"} | json | msg="Request processed" | status >= 400`
|
||||
Fields available: `level, requestId, method, path, status, duration`. `msg="Request processed"` is the request-log filter key.
|
||||
4. **Errors across all containers:** `{container=~"mvp-.*"} | json | level="error"`.
|
||||
5. **OCR container logs** if the symptom involves VIN/receipts/manuals: `docker logs --tail 200 mvp-ocr-staging` on staging (dev/base name is `mvp-ocr`; prod also uses the un-suffixed `mvp-ocr` — the blue-green overlay defines no OCR variant, it is a single shared instance there).
|
||||
6. **Frontend errors: browser devtools console only.** Frontend logs never reach Loki.
|
||||
7. **Did a deploy just happen?** Every PR push redeploys staging with brief full downtime; mid-deploy 502s are expected noise.
|
||||
|
||||
Then, before touching code: state a hypothesis that predicts a specific observation, and check the triage table below.
|
||||
|
||||
## Triage table
|
||||
|
||||
| # | Symptom | Likely cause | Jump to |
|
||||
|---|---------|--------------|---------|
|
||||
| 1 | Numbers wrong, `.toFixed is not a function`, values are strings | pg NUMERIC returned as string; mapper missed coercion | 1 |
|
||||
| 2 | Dates off by one day | One of the three UTC traps reintroduced | 2 |
|
||||
| 3 | Unexpected 403 | EMAIL_NOT_VERIFIED vs TIER_REQUIRED vs ownership | 3 |
|
||||
| 4 | Stale data after mutation; deleted record still visible | Redis cache invalidation miss | 4 |
|
||||
| 5 | Blank page; API calls never fire on load | Auth-gate race / bypassed apiClient | 5 |
|
||||
| 6 | Bug only on mobile (or page missing on mobile) | 768px app fork; a registration-checklist step missed | 6 |
|
||||
| 7 | Route unexpectedly public / unguarded | Missing per-route preHandler; tier guard fails open | 7 |
|
||||
| 8 | VIN decode failure or timeout | Gemini path: timeout stack, event-loop blocking, SDK drift | 8 |
|
||||
| 9 | OCR/receipt extraction returns empty fields | Silent Gemini fallback; Vision monthly cap; engine config | 9 |
|
||||
| 10 | Migration did not run; missing table/column | Not in MIGRATION_ORDER, or image not rebuilt | 10 |
|
||||
| 11 | Container unhealthy; staging deploy failed | Health-check anatomy; start_period; verify job | 11 |
|
||||
| 12 | Jest hangs locally / frontend jest crashes | Open pg/redis handles; broken tdd-guard reporter config | 12 |
|
||||
|
||||
### 1. Numbers display wrong / `.toFixed` crashes / values are strings
|
||||
|
||||
**Likely cause.** The pg driver returns PostgreSQL `NUMERIC`/`DECIMAL` (type OID 1700) as JavaScript strings. The only global type-parser override in `backend/src/core/config/database.ts` is for `DATE` (OID 1082, line 12) — there is deliberately NO global numeric override. Every repository mapper must coerce numerics manually with `Number()`/`parseFloat()`. This has caused 5 historical incidents (#47, #49, #239, #241, #244); it is statistically the most likely bug when adding or changing any repository method.
|
||||
|
||||
**Discriminating check.** Hit the API and inspect the JSON type: a numeric field arriving as `"42.50"` (string) instead of `42.5` is this bug. Or in the failing frontend code, `typeof value === 'string'` where a number is expected.
|
||||
|
||||
```bash
|
||||
grep -n "parseFloat\|Number(" backend/src/features/<feature>/data/<feature>.repository.ts
|
||||
```
|
||||
|
||||
**Fix pattern.** Coerce in the repository mapper (`mapRow()` or equivalent), never in the frontend and never by returning raw rows. Beware the fuel-logs exception: `mapEnhancedRow` in `backend/src/features/fuel-logs/data/fuel-logs.repository.ts` deliberately returns coerced snake_case rows and the service's `toEnhancedResponse` does the camelCase mapping — do not "normalize" this without reading `mvp-failure-archaeology` (incident #47 -> #244 chain). Do NOT add a global OID 1700 parser as a drive-by fix; that is an architectural decision (see `mvp-architecture-contract`).
|
||||
|
||||
### 2. Dates off by one day
|
||||
|
||||
**Likely cause.** One of three settled UTC traps has been reintroduced:
|
||||
(a) backend: removing/bypassing the DATE parser override `types.setTypeParser(1082, ...)` in `backend/src/core/config/database.ts:12` — pg would return DATE as a local-midnight `Date` object that shifts a day under `toISOString()`;
|
||||
(b) frontend: `new Date("YYYY-MM-DD")` parses as UTC midnight, then `toLocaleDateString()` shifts a day back west of UTC;
|
||||
(c) OCR/backend: `toISOString().split('T')[0]` on a Date built from a date-only value.
|
||||
|
||||
**Discriminating check.** Trace where the value first deviates: check the raw API response (should be a plain `"YYYY-MM-DD"` string for DATE columns). If the API string is right, the bug is frontend display; if wrong, backend/OCR.
|
||||
|
||||
**Fix pattern (the settled rule — do not innovate).** DATE columns flow as plain `YYYY-MM-DD` strings end to end; the full handling rules (dayjs display, lexicographic sort, when `new Date` is legitimate) are canonical in `mvp-vehicle-domain-reference` section 3. See `mvp-failure-archaeology` (four fixes in one day, 2026-03-23) before touching any date code.
|
||||
|
||||
### 3. Unexpected 403 responses
|
||||
|
||||
Three distinct producers — identify which by the response body:
|
||||
|
||||
| Body | Producer | Where |
|
||||
|------|----------|-------|
|
||||
| `code: "EMAIL_NOT_VERIFIED"` | Auth plugin email-verification guard | `backend/src/core/plugins/auth.plugin.ts` (~line 231); exempt route prefixes `/api/auth/`, `/api/onboarding/`, `/api/health`, `/health` (lines 18-23) |
|
||||
| `error: "TIER_REQUIRED"` with `requiredTier`/`currentTier` | Tier gating (two parallel mechanisms) | `backend/src/core/plugins/tier-guard.plugin.ts` (~line 93) or `backend/src/core/middleware/require-tier.ts` (~line 50) |
|
||||
| Generic 403, message containing "Unauthorized" | Ownership failure string-matched in the controller | e.g. `backend/src/features/fuel-logs/api/fuel-logs.controller.ts` — controllers catch and map `error.message.includes('not found')` -> 404, `includes('Unauthorized')` -> 403, else 500. There are no typed error classes. |
|
||||
|
||||
**Discriminating checks.**
|
||||
- EMAIL_NOT_VERIFIED: does the user's Auth0 account show verified? Guard runs on every authenticated route not in the exempt list.
|
||||
- TIER_REQUIRED: is the route tier-gated? (Current key catalog and limits: `mvp-config-and-secrets` section 3.) What is `user_profiles.subscription_tier`?
|
||||
- Ownership: does the row's `user_id` match the caller's internal UUID? Note `request.userContext.userId` is the internal `user_profiles` UUID, NOT the Auth0 `sub`.
|
||||
|
||||
**Fix pattern.** For ownership bugs, check the service's ownership comparison and that the correct user UUID reached it. When adding error paths, match the existing string-matching contract exactly ("not found", "Unauthorized") or the controller will map your error to 500.
|
||||
|
||||
### 4. Stale data after mutation
|
||||
|
||||
**Likely cause.** Redis cache invalidation miss. Cache facts: keys prefixed `mvp:` (`backend/src/core/config/redis.ts:22`); fuel-logs caches per `{userId}:{unitSystem}` with TTL 300s (`fuel-logs.service.ts:18`); platform dropdown caches TTL 6h (`backend/src/features/platform/domain/platform-cache.service.ts`). Cache errors are swallowed (cacheService returns null on failure) — a broken Redis never 500s, it just serves stale or uncached.
|
||||
|
||||
**KNOWN LIVE BUG.** `deleteFuelLog` invalidates only the `imperial` cache keys, so a metric-preference user sees deleted fuel logs for up to 300s. If you are debugging "deleted log still shows" for a metric user, this is it — the full record (file:line evidence and status) is canonical in `mvp-launch-readiness` section 1, gap 4; file/fix the bug rather than hunting elsewhere.
|
||||
|
||||
**Discriminating check.** Does the stale window self-heal at the TTL (300s fuel-logs, 6h platform)? If yes, invalidation miss. Inspect keys directly:
|
||||
|
||||
```bash
|
||||
docker exec mvp-redis redis-cli --scan --pattern 'mvp:fuel-logs:*'
|
||||
```
|
||||
|
||||
**Fix pattern.** Invalidate BOTH unit systems (or `deletePattern`) on every mutation path; check create/update/delete all invalidate symmetrically.
|
||||
|
||||
### 5. Blank page / API calls never fire on load
|
||||
|
||||
**Likely cause.** Auth-gate race. All axios calls through the shared `apiClient` (`frontend/src/core/api/client.ts`) are queued until Auth0 init completes (`frontend/src/core/auth/auth-gate.ts`; `setAuthReady` flipped by `Auth0Provider`). Historical incidents: blank Stations page (2025-11), dashboard auth-gate (#45), mobile login IndexedDB/callback saga (#188/#190).
|
||||
|
||||
**Discriminating check.** Devtools Network tab: are the requests pending/queued (never sent) or sent and failing? Queued-forever means auth never initialized — check console for Auth0 errors. Sent-and-401 pre-auth means something bypassed `apiClient` (raw `axios`/`fetch` import).
|
||||
|
||||
**Fix pattern.** Always use `apiClient` from `core/api/client.ts` — never raw axios. Do not add data fetches that race Auth0 init. Also settled: URL-sync effects in `App.tsx` must skip `/callback`, `/signup`, `/verify-email` (child effects fire before parent effects; stripping `?code=&state=` breaks Auth0 login).
|
||||
|
||||
### 6. Mobile-only bugs / page missing on mobile
|
||||
|
||||
**Likely cause.** The app hard-forks at `window.innerWidth <= 768` plus a UA regex in `frontend/src/App.tsx` (~lines 360-398). Desktop renders react-router `<Routes>`; mobile renders a Zustand-driven screen switcher (`useNavigationStore().activeScreen`, NO react-router). A page can work perfectly on desktop and be entirely absent on mobile.
|
||||
|
||||
**Discriminating check.** Shrink the window below 768px (or use device emulation). Then walk the CANONICAL registration checklist in `mvp-architecture-contract` invariant 2.7 (7 steps: desktop route, `MobileScreen` union, both nav maps, lazy import, mobile render block, navigation entry point) and find the missing step.
|
||||
|
||||
**Fix pattern.** Complete every step of that checklist — a screen registered in the maps but missing its lazy import or a navigation entry point is silently unreachable. Note the breakpoint systems are NOT uniform: app fork = 768px, MUI component checks = `sm` (600) / `md` (900). Mobile+desktop is a hard project requirement — verify both on staging.
|
||||
|
||||
### 7. Route unexpectedly public or unguarded
|
||||
|
||||
**Likely cause.** Auth is per-route: every protected route must list `preHandler: [fastify.authenticate]` explicitly — there is NO global auth hook. Intentional exceptions exist (webhooks, signup, health, `/auth/verify`, feature-tiers config): the canonical list lives in the header comment of `.claude/skills/mvp-diagnostics-and-logging/scripts/check-route-auth.sh` — check a suspect route against it before "fixing" a deliberately public endpoint. Forgetting the preHandler ships an open route, and CI will not catch it.
|
||||
|
||||
**Compounding traps (all verified):**
|
||||
- The tier guard FAILS OPEN on unknown feature keys: `canAccessFeature` in `backend/src/core/config/feature-tiers.ts` (~lines 57-61) returns `true` for unregistered keys. A typo in a `featureKey` silently ungates a plugin-guarded route.
|
||||
- Two parallel tier mechanisms exist with different failure behavior — the middleware 500s on unknown keys, the plugin decorator fails open (mechanics canonical in `mvp-config-and-secrets` section 3).
|
||||
- `backend/src/features/backup/api/backup.routes.ts` uses untyped `(fastify as any).requireAdmin` 16 times (from line 32); a misspelled guard there compiles clean and ships an unguarded admin endpoint. Do not copy that file as a route template.
|
||||
|
||||
**Discriminating check.** `curl` the route with no Authorization header — a 200 on a should-be-protected route confirms. Audit:
|
||||
|
||||
```bash
|
||||
grep -L "authenticate" backend/src/features/*/api/*.routes.ts
|
||||
grep -rn "featureKey" backend/src/features/*/api/*.routes.ts # then match each key against FEATURE_TIERS
|
||||
```
|
||||
|
||||
**Fix pattern.** Add the explicit preHandler; verify the featureKey exists in `FEATURE_TIERS`; prefer the typed `fastify.requireAdmin`/`fastify.requireTier` decorators.
|
||||
|
||||
### 8. VIN decode failures / timeouts
|
||||
|
||||
**The chain:** frontend `POST /vehicles/decode-vin` (axios timeout 120s, `frontend/src/features/vehicles/api/vehicles.api.ts:90`) -> `vehicles.controller.ts` (maps OCR 503/422 -> 502, timeout/abort -> 504) -> `backend/src/features/ocr/external/ocr-client.ts` (`OCR_TIMEOUT_MS = 120000`, line 9, hardcoded) -> Python OCR container `POST /decode/vin` -> Gemini with Google Search grounding.
|
||||
|
||||
**Likely causes, in order:**
|
||||
1. **Timeout race:** frontend 120s == backend-client 120s with no headroom, and there is NO service-side timeout on the Gemini call itself. Search-grounded decodes have been observed >60s.
|
||||
2. **Event-loop blocking:** `ocr/app/routers/decode.py` calls the synchronous `_gemini_engine.decode_vin(vin)` inside an `async def` handler — a slow call blocks the whole uvicorn event loop including `/health`. The OCR healthcheck (docker-compose.yml: interval 5s, timeout 5s, retries 3) can then mark the container unhealthy and Docker can restart it MID-REQUEST.
|
||||
3. **SDK drift:** `google-genai>=1.0.0` is unpinned in `ocr/requirements.txt`; new config params have broken staging before (pydantic validation error from `AutomaticFunctionCallingConfig` — reverted; never re-add AFC config).
|
||||
|
||||
**Discriminating check.** `docker logs mvp-ocr-staging` (on staging; base/prod name `mvp-ocr`) around the failure time. Diagnostic raw-JSON logging exists: `gemini_engine.py` logs `"Gemini decoded VIN %s (confidence=...) raw=%s"` (~line 401) with the full raw Gemini response. A container restart in `docker ps` uptime plus a 504 upstream = the healthcheck-kill scenario. A pydantic ValidationError in logs = SDK drift.
|
||||
|
||||
**Fix pattern.** Do not "fix" by raising the frontend timeout again (already raised 30->60->120s; the fix is service-side). Settled: there is NO VIN decode cache by design (deleted after race/staleness bugs — see `mvp-failure-archaeology`); do not re-add one. Model year is computed deterministically from VIN positions 7/10, never trusted from the LLM.
|
||||
|
||||
### 9. OCR / receipt extraction returns empty results
|
||||
|
||||
**Likely cause.** In `ocr/app/extractors/maintenance_receipt_extractor.py` (~lines 129-133), any Gemini failure is caught and silently falls back to OCR-only with `gemini_fields = {}` — the user sees "no fields extracted" with no error. Also check:
|
||||
- **Vision monthly cap:** `VISION_MONTHLY_LIMIT` is 1000 requests/month; compose sets `OCR_PRIMARY_ENGINE: google_vision`, `OCR_FALLBACK_ENGINE: paddleocr` (docker-compose.yml ~lines 201-206, overriding the code defaults in `ocr/app/config.py`). Cap exhaustion degrades to PaddleOCR quality.
|
||||
- **Model config:** compose deploys `GEMINI_MODEL: gemini-3-flash-preview` (line 210) — a preview model; the code default is `gemini-2.5-flash`. Extraction-quality regressions can be model-side.
|
||||
- Email-ingestion amplification: unconfident classification runs BOTH fuel and maintenance OCR endpoints per attachment, burning the Vision cap faster.
|
||||
|
||||
**Discriminating check.** `docker logs mvp-ocr-staging | grep -i "Gemini extraction failed"` (base/prod container name is `mvp-ocr`) — the fallback logs a warning with the exception. WIF/auth failures log "Gemini authentication failed". Verify effective engine config: `docker exec mvp-ocr-staging env | grep -E "OCR_|GEMINI|VISION"`.
|
||||
|
||||
**Fix pattern.** Fix the upstream Gemini/auth failure; do not paper over with confidence-threshold tweaks. The WIF credential chain (Auth0 M2M -> `google-wif-config.json` at `/run/secrets/`) is duplicated in `gemini_engine.py` AND `maintenance_receipt_extractor.py` `_get_client()` — auth fixes must be applied in both.
|
||||
|
||||
### 10. Migration did not run / missing table or column
|
||||
|
||||
**Likely cause.** Migrations are feature-owned SQL run in the hard-coded `MIGRATION_ORDER` array in `backend/src/_system/migrations/run-all.ts` (line 17). A new feature's `migrations/` directory is SILENTLY skipped unless the feature is appended to that array. Migrations run automatically on backend container start (`backend/Dockerfile:84`: `node dist/_system/migrations/run-all.js && npm start`), which means they are packaged into the image — a migration added to source requires an IMAGE REBUILD and redeploy to run on staging/prod.
|
||||
|
||||
**Discriminating check.**
|
||||
|
||||
```bash
|
||||
grep -n "features/<your-feature>" backend/src/_system/migrations/run-all.ts # is it in MIGRATION_ORDER?
|
||||
docker exec mvp-postgres psql -U postgres -d motovaultpro -c "SELECT * FROM _migrations ORDER BY executed_at DESC LIMIT 20;"
|
||||
docker logs mvp-backend 2>&1 | grep -i migrat
|
||||
```
|
||||
|
||||
(Adjust container names/DB credentials per environment; on staging they carry `-staging`.)
|
||||
|
||||
**Fix pattern.** Append to `MIGRATION_ORDER` respecting dependencies (`update_updated_at_column()` is defined in `features/vehicles`, which must run first; `core/identity-migration` runs last). NON-NEGOTIABLE: schema migrations on staging/prod require a fresh backup first (`scripts/ci/maintenance-migrate.sh backup`, or `make db-backup`).
|
||||
|
||||
### 11. Container unhealthy / staging deploy failed
|
||||
|
||||
**Health-check anatomy (docker-compose.yml, verified):** backend has `start_period: 180s` (line 159) specifically because auto-migrations run on start — a backend "starting" for 2 minutes is normal, not a failure. OCR: interval 5s / timeout 5s / start_period 15s (and see symptom 8 for why OCR can flap under load). The `verify-staging` job waits up to 4 minutes (48x5s) for Docker health, then curls `https://staging.motovaultpro.com/api/health` and requires `status=healthy` plus a 13-feature required list (`.gitea/workflows/staging.yaml`, `REQUIRED_FEATURES`).
|
||||
|
||||
**Discriminating check, in order:**
|
||||
1. Which job failed? Build failure = code/Dockerfile problem. Deploy failure = server-side (disk, secrets). Verify failure = app boot problem.
|
||||
2. `docker inspect --format '{{json .State.Health}}' mvp-backend-staging | jq .` — see the actual failing probe output.
|
||||
3. `docker logs mvp-backend-staging --tail 100` — migration errors are the most common boot killer.
|
||||
4. Disk: `df -h /` on the staging runner — the 29G root fills with per-commit images; a full disk fails builds and deploys in confusing ways (`docker system df`; a daily prune cron exists out-of-band).
|
||||
5. Missing secret trap: Docker bind-mounting a missing secret file creates a DIRECTORY; the workflows contain `rm -rf` cleanup for exactly this. An empty directory where a secret file should be breaks the consumer at read time.
|
||||
|
||||
**Fix pattern.** Fix in the repo and redeploy via the workflow. Never hand-edit on the server (non-negotiable; rsync --delete reverts it). Full deploy/rollback procedure: `mvp-run-and-operate`.
|
||||
|
||||
### 12. Jest hangs locally / frontend jest broken
|
||||
|
||||
**Backend:** `cd backend && npm test` runs tests and then never exits — pg pool and ioredis connect eagerly at module import (`backend/src/core/config/database.ts`, `redis.ts` — `new Redis(...)` with no `lazyConnect`) and `backend/jest.config.js` sets no `forceExit`. Run:
|
||||
|
||||
```bash
|
||||
cd backend && npx jest --forceExit # or: npm test -- --forceExit
|
||||
```
|
||||
|
||||
Also remember: backend INTEGRATION tests `DROP TABLE ... CASCADE` on the database they point at — never aim them at a database you care about without a fresh backup (non-negotiable).
|
||||
|
||||
**Frontend:** `cd frontend && npm test` is broken on the host: `frontend/jest.config.ts` registers the `tdd-guard-jest` reporter (line 31) with a hardcoded `projectRoot: '/home/egullickson/motovaultpro'` (line 33) — a Linux path that does not exist on this macOS checkout. Discriminating check: the failure mentions tdd-guard or that path. A verified working fallback exists — `npx jest --reporters=default` — with its traps and the known-red baseline documented in `mvp-build-and-env` section 2 (the single home for frontend-jest mechanics). Note there is no in-container run either (nginx-only image, tests dockerignored); end-to-end verification is still staging (see `mvp-validation-and-qa`).
|
||||
|
||||
## Discriminating-experiment discipline
|
||||
|
||||
Cheap rules that prevent the historical "fix symptoms until root cause found" chains (the mobile-login saga took 9 commits; the Tesseract VIN saga took 12 in one day):
|
||||
|
||||
1. **Predict before you look.** Write the hypothesis as "if X is the cause, then I will observe exactly Y at Z" — then look. If you find yourself explaining an observation after the fact, it did not discriminate. Full protocol: `mvp-proof-and-analysis-toolkit`.
|
||||
2. **One variable per experiment.** Especially on staging, where every PR push redeploys the whole stack: change one thing, push, observe. Two changes per push means an ambiguous result.
|
||||
3. **Locate the first deviation, not the last symptom.** Trace the value/request through the chain (DB -> repository mapper -> service -> controller -> API JSON -> frontend) and find the FIRST point it is wrong. Most historical band-aids (frontend `Number()` wrappers, timeout bumps) patched the last symptom.
|
||||
4. **Distrust green CI.** CI runs zero tests and zero lint; "CI passed" only means images built and staging booted. Evidence standards: `mvp-validation-and-qa`.
|
||||
5. **Check the archaeology first.** If the bug involves dates, numeric strings, VIN caching, auth races, Stripe IDs, or OCR engines, someone already fought it. `mvp-failure-archaeology` lists the settled outcomes and the reverts you must not resurrect.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 by direct repo inspection; discovery-report claims were independently re-verified against code. Line numbers drift — re-verify before relying on them:
|
||||
|
||||
| Volatile fact | Re-verification command |
|
||||
|---------------|------------------------|
|
||||
| DATE-only parser override (OID 1082), no NUMERIC override | `grep -n "setTypeParser" backend/src/core/config/database.ts` |
|
||||
| deleteFuelLog imperial-only invalidation (live bug) | `grep -n "invalidateCaches" backend/src/features/fuel-logs/domain/fuel-logs.service.ts` |
|
||||
| Cache prefix `mvp:`, fuel-logs TTL 300s, platform TTL 6h | `grep -n "prefix" backend/src/core/config/redis.ts; grep -n "cacheTTL" backend/src/features/fuel-logs/domain/fuel-logs.service.ts; grep -n "6 \* 3600" backend/src/features/platform/domain/platform-cache.service.ts` |
|
||||
| EMAIL_NOT_VERIFIED guard + exempt routes | `grep -n "EMAIL_NOT_VERIFIED\|VERIFICATION_EXEMPT" backend/src/core/plugins/auth.plugin.ts` |
|
||||
| Tier guard fails open on unknown keys | `grep -n -A3 "fail open" backend/src/core/config/feature-tiers.ts` |
|
||||
| Controller string-matched error mapping | `grep -n "includes('not found')\|includes('Unauthorized')" backend/src/features/fuel-logs/api/fuel-logs.controller.ts` |
|
||||
| Untyped requireAdmin in backup routes | `grep -c "(fastify as any).requireAdmin" backend/src/features/backup/api/backup.routes.ts` |
|
||||
| 120s timeouts (frontend + backend OCR client) | `grep -n "OCR_TIMEOUT_MS" backend/src/features/ocr/external/ocr-client.ts; grep -n "timeout" frontend/src/features/vehicles/api/vehicles.api.ts` |
|
||||
| Sync Gemini call in async decode handler | `grep -n "decode_vin(vin)" ocr/app/routers/decode.py` |
|
||||
| Gemini raw-JSON diagnostic logging | `grep -n "raw=%s" ocr/app/engines/gemini_engine.py` |
|
||||
| Silent empty-fields fallback | `grep -n -B1 "gemini_fields = {}" ocr/app/extractors/maintenance_receipt_extractor.py` |
|
||||
| Compose OCR engine/model config | `grep -n "OCR_PRIMARY_ENGINE\|GEMINI_MODEL\|VISION_MONTHLY_LIMIT" docker-compose.yml` |
|
||||
| MIGRATION_ORDER + migrate-on-start | `grep -n "MIGRATION_ORDER" backend/src/_system/migrations/run-all.ts; grep -n "run-all" backend/Dockerfile` |
|
||||
| Backend start_period 180s; OCR healthcheck 5s/5s | `grep -n "start_period" docker-compose.yml` |
|
||||
| 13-feature staging verify list | `grep -n "REQUIRED_FEATURES" .gitea/workflows/staging.yaml` |
|
||||
| 768px fork + page-registration checklist (canonical: `mvp-architecture-contract` 2.7) | `grep -n "innerWidth <= 768" frontend/src/App.tsx; grep -n "routeToScreen\|screenToRoute" frontend/src/core/store/navigation.ts` |
|
||||
| Auth-gate request queue | `grep -n "authReady\|queueRequest" frontend/src/core/api/client.ts` |
|
||||
| Frontend jest hardcoded projectRoot | `grep -n "projectRoot" frontend/jest.config.ts` |
|
||||
| Integration tests DROP TABLE CASCADE | `grep -rn "DROP TABLE" backend/src/features/*/tests/integration/` |
|
||||
@@ -1,338 +0,0 @@
|
||||
---
|
||||
name: mvp-deploy-safety-campaign
|
||||
description: Load when working on CI/CD safety for MotoVaultPro - adding tests/lint to CI, fixing the ":latest" image tag hazard, changing PR staging deploys, or when you notice symptoms like "CI runs zero tests", "PR builds overwrite latest", "production deployed an unmerged PR build", "every PR redeploys staging", "tests never ran in the pipeline", "image_tag defaults to latest", or "the PR was green but the code is broken". This is the executable, decision-gated campaign to close the deploy-safety hole (owner-confirmed hardest live problem, 2026-07-07). Also load before touching .gitea/workflows/staging.yaml or production.yaml for any reason.
|
||||
---
|
||||
|
||||
# Deploy Safety Campaign
|
||||
|
||||
An executable, phased campaign to close MotoVaultPro's deploy-safety hole. Verified state as of 2026-07-07:
|
||||
|
||||
1. CI runs ZERO tests and ZERO lint. The only PR gate is that 3 Docker images build (tsc compiles inside them) and the staging stack boots healthy.
|
||||
2. Every PR build pushes `backend:latest`, `frontend:latest`, `ocr:latest` to the registry (`.gitea/workflows/staging.yaml` build job).
|
||||
3. Production deploys default to `image_tag: latest` (`.gitea/workflows/production.yaml:14-17`), so one click can ship an unmerged PR build to production.
|
||||
4. Every PR open/sync/reopen tears down and redeploys the single shared staging environment (last PR wins).
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when: executing any phase of this campaign; adding test/lint jobs to CI; changing image tagging or the production `image_tag` input; changing when staging deploys happen; or evaluating whether the campaign's gates still hold.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- How to classify, review, and ship a change (issue/branch/PR/labels): `mvp-change-control`. Every phase here ships THROUGH that process, never around it.
|
||||
- Running or fixing tests locally, jest/pytest traps: `mvp-build-and-env`
|
||||
- What counts as test evidence, adding tests, mobile+desktop validation: `mvp-validation-and-qa`
|
||||
- Deploy mechanics, blue-green, rollback, backups: `mvp-run-and-operate`
|
||||
- Debugging a broken staging/prod stack right now: `mvp-debugging-playbook`
|
||||
- Measuring outcomes in Grafana/Loki: `mvp-diagnostics-and-logging`
|
||||
|
||||
## Ground rules (non-negotiable)
|
||||
|
||||
- Every change in this campaign ships via `mvp-change-control`: issue -> branch `issue-{N}-{slug}` -> PR -> staging verify -> owner review -> merge. One issue per phase (see Validation protocol at the end).
|
||||
- Pipeline changes (any edit under `.gitea/workflows/`) and anything touching database strategy require explicit owner sign-off in the issue before the PR is opened.
|
||||
- NEVER point the backend integration tests at a shared database. They run real migrations in `beforeAll` and `DROP TABLE ... CASCADE` in `afterAll` (verified: `backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts:37-41`). Ephemeral database or nothing.
|
||||
- Never hand-edit files on the staging/prod servers; `rsync --delete` on every deploy reverts them.
|
||||
- Success at each gate is a number or an exact string, never "looks right". If an EXPECTED observation does not match, follow the branch instruction; do not improvise.
|
||||
|
||||
Terminology: "runner" = the self-hosted Gitea act_runner on the staging host (`mvp-staging`), label `stage`, HOST mode (`runner_labels: "stage:host"` in `ansible/deploy-staging-runner.yml:25`) - jobs execute directly on the host shell, with Docker CLI and Node.js 20 (NodeSource, `ansible/deploy-staging-runner.yml:107-123`) available. Host mode means Gitea Actions `services:` blocks are NOT available; use `docker run` directly.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 - Baseline evidence
|
||||
|
||||
Goal: capture the current gate reality in numbers, committed to the phase issue. No code changes.
|
||||
|
||||
### 0.1 Confirm CI runs zero tests/lint
|
||||
|
||||
```bash
|
||||
grep -rnE "npm (test|run lint|run type-check)|pytest|eslint|jest" .gitea/workflows/
|
||||
```
|
||||
|
||||
EXPECTED: no output, exit code 1 (verified 2026-07-07).
|
||||
IF you get hits: someone has already started this work. Stop, read the matching workflow lines and the phase issues, and reconcile before proceeding.
|
||||
|
||||
### 0.2 Confirm the :latest and PR-trigger hazards
|
||||
|
||||
```bash
|
||||
sed -n '8,13p' .gitea/workflows/staging.yaml # trigger block
|
||||
grep -cn ":latest" .gitea/workflows/staging.yaml # latest usage count
|
||||
grep -n -A4 "image_tag:" .gitea/workflows/production.yaml | head -8
|
||||
```
|
||||
|
||||
EXPECTED (verified 2026-07-07):
|
||||
- Trigger block shows `push: branches: [main]` AND `pull_request: types: [opened, synchronize, reopened]`.
|
||||
- 9 `:latest` occurrences in staging.yaml: 3 `--cache-from` (lines 58, 74, 85), 3 `-t ...:latest` (lines 60, 76, 87), 3 `docker push ...:latest` (lines 96-98).
|
||||
- production.yaml input: `image_tag` with `required: false` and `default: 'latest'` (lines 14-17).
|
||||
|
||||
IF counts differ: Phase 3 may be partially done. Diff against git history (`git log -p .gitea/workflows/staging.yaml`) before prescribing anything.
|
||||
|
||||
### 0.3 Record local suite baselines
|
||||
|
||||
Numbers measured 2026-07-07 on a dev machine (no `/app/config`, node v25). Your numbers go in the phase issue; if they differ materially from below, record yours as the new baseline and note the delta.
|
||||
|
||||
Backend unit tests (integration excluded - note the pattern is the SUBSTRING `integration`, because `audit-log/__tests__/audit-log.integration.test.ts` and `user-import/tests/user-import.integration.test.ts` do not live under a `/integration/` directory):
|
||||
|
||||
```bash
|
||||
cd backend && npx jest --testPathIgnorePatterns "/node_modules/" "integration" --forceExit 2>&1 | tail -5
|
||||
```
|
||||
|
||||
EXPECTED (2026-07-07, re-confirmed 2026-07-09): `Test Suites: 15 failed, 10 passed, 25 total` / `Tests: 2 failed, 147 passed, 149 total`. The 15 failing suites split three ways, all verified:
|
||||
- 6 suites die with `Configuration file not found at /app/config/production.yml` - `config-loader.ts:155` defaults `CONFIG_PATH` to a container path and loads eagerly at import; any suite that transitively imports `core/config/database.ts` dies at load.
|
||||
- 7 suites fail ts-jest compilation (type errors inside test files: audit-log.routes, auth.service, documents.repository, documents.service, fuel-logs.service, vehicle-data.service, community-stations.service). `backend/tsconfig.json:28` excludes `**/*.test.ts`, so `npm run type-check` is green (verified) while ts-jest, which does type-check tests, fails these suites. Local type-check green does NOT mean tests compile.
|
||||
- 2 suites (documents.controller.tier, ocr-receipt) contain the 2 genuinely failing tests.
|
||||
|
||||
Frontend (the `--reporters=default` flag is mandatory outside the container):
|
||||
|
||||
```bash
|
||||
cd frontend && npx jest --reporters=default 2>&1 | tail -5
|
||||
```
|
||||
|
||||
EXPECTED (2026-07-07): `Test Suites: 14 failed, 17 passed, 31 total` / `Tests: 17 failed, 196 passed, 213 total`, ~11s. Without the flag you get `Error: Could not resolve a module for a custom reporter. Module name: tdd-guard-jest` - `frontend/jest.config.ts:28-36` requires a reporter that is only declared in the root package.json (which has no node_modules), and hardcodes `projectRoot: '/home/egullickson/motovaultpro'`.
|
||||
|
||||
OCR - pytest is not installed on dev machines; record inventory only:
|
||||
|
||||
```bash
|
||||
grep -rc "def test_" ocr/tests/*.py | awk -F: '{s+=$2} END {print s}'
|
||||
```
|
||||
|
||||
EXPECTED: `310` (16 test files; verified 2026-07-07). `pytest>=7.4.0` and `pytest-asyncio` are in `ocr/requirements.txt:34-35`, so pytest IS installed inside the built OCR image.
|
||||
|
||||
### 0.4 Gate
|
||||
|
||||
Phase 0 is complete when the phase issue contains: the three grep outputs from 0.1/0.2, the exact `Test Suites:`/`Tests:` lines for backend and frontend, the OCR count, and the list of failing suites/tests by name. That issue comment is the baseline every later phase is measured against.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Make the suites runnable in CI at all
|
||||
|
||||
Prerequisite engineering. No workflow changes yet. Owner sign-off required on decisions 1B (database strategy) before implementation.
|
||||
|
||||
### 1A. Frontend jest portability (decision made: fix config, keep reporter optional)
|
||||
|
||||
The blocker is two-fold: the `tdd-guard-jest` reporter cannot resolve, and `projectRoot` is a hardcoded Linux path. Two working options, verified:
|
||||
|
||||
1. CI-side only: run `npx jest --reporters=default`. CLI `--reporters` replaces the config's reporters before the module is resolved (verified 2026-07-07: run proceeds to real pass/fail output). Zero code change; every CI invocation must remember the flag.
|
||||
2. Fix the config (recommended): make the reporter conditional so `npx jest` works everywhere. Diff shape for `frontend/jest.config.ts`:
|
||||
|
||||
```ts
|
||||
// replace the hardcoded reporters block (lines 28-36) with:
|
||||
const reporters: Config['reporters'] = ['default'];
|
||||
if (process.env.TDD_GUARD === '1') {
|
||||
reporters.push(['tdd-guard-jest', { projectRoot: process.cwd() }]);
|
||||
}
|
||||
// ...and inside config: reporters,
|
||||
```
|
||||
|
||||
Gate 1A: `cd frontend && npx jest 2>&1 | tail -3` on a dev machine prints a `Tests:` summary line (numbers matching the Phase 0 baseline, not the reporter resolution error).
|
||||
|
||||
The 17 failing frontend tests (14 suites) from Phase 0 must each be dispositioned: fixed, or moved out of the suite with a filed issue per suite. CI cannot gate on a suite that is red on day one. Record the final green count - it becomes the Phase 2 expected number.
|
||||
|
||||
### 1B. Backend test database strategy (owner sign-off REQUIRED)
|
||||
|
||||
Non-negotiable: an EPHEMERAL PostgreSQL, created and destroyed per CI run. The integration suites DROP TABLE CASCADE on whatever database they reach. The runner IS the staging host - a misconfigured host/port reaches the staging database. Concrete shape (runs on the `stage` runner, host mode, so `docker run`, not `services:`):
|
||||
|
||||
```bash
|
||||
NET="ci-test-$SHORT_SHA"
|
||||
docker network create "$NET"
|
||||
docker run -d --rm --name "ci-pg-$SHORT_SHA" --network "$NET" --network-alias mvp-postgres \
|
||||
-e POSTGRES_DB=motovaultpro -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=citest \
|
||||
git.motovaultpro.com/egullickson/mirrors/postgres:18-alpine
|
||||
docker run -d --rm --name "ci-redis-$SHORT_SHA" --network "$NET" --network-alias mvp-redis \
|
||||
git.motovaultpro.com/egullickson/mirrors/redis:8.4-alpine
|
||||
# ... run tests ... then teardown:
|
||||
docker stop "ci-pg-$SHORT_SHA" "ci-redis-$SHORT_SHA"; docker network rm "$NET"
|
||||
```
|
||||
|
||||
The network aliases `mvp-postgres`/`mvp-redis` match `config/app/ci.yml`, which was clearly authored for this purpose BUT is currently broken: it fails the zod schema in `config-loader.ts` with `invalid_type ... path: ["auth0"]` (verified 2026-07-07 by pointing `CONFIG_PATH` at it). Part of 1B is completing `ci.yml` until it parses - copy the missing blocks (`auth0`, and any further zod complaints) from `config/app/production.yml` with dummy-safe values, iterating until one unit suite loads.
|
||||
|
||||
Secrets: `config-loader.ts:175-205` reads EXTENSION-LESS files (`postgres-password`, not `postgres-password.txt`) from `SECRETS_DIR` and requires 8 of them to parse. For tests, generate dummies:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/ci-secrets
|
||||
for f in auth0-client-secret auth0-management-client-id auth0-management-client-secret \
|
||||
google-maps-api-key resend-api-key stripe-secret-key stripe-webhook-secret; do
|
||||
echo "ci-dummy" > /tmp/ci-secrets/$f
|
||||
done
|
||||
echo "citest" > /tmp/ci-secrets/postgres-password # must match POSTGRES_PASSWORD above
|
||||
```
|
||||
|
||||
Execution vehicle for backend tests in CI - two options, pick one in the phase issue:
|
||||
- Host node 20 (simplest): `cd backend && npm ci && CONFIG_PATH=$PWD/../config/app/ci.yml SECRETS_DIR=/tmp/ci-secrets npx jest --forceExit`. Requires connecting to the ephemeral DB via published port instead of network alias (host is not on the docker network): publish `-p 127.0.0.1:55432:5432` and set `ci.yml` database host/port accordingly.
|
||||
- Builder-stage image (hermetic): the backend CI build uses context `.` with NO root `.dockerignore` (verified: `backend/.dockerignore` exists but does not apply when context is the repo root), so tests and `jest.config.js` ARE inside the builder stage. `docker build --target builder -t backend-test -f backend/Dockerfile .` then `docker run --rm --network "$NET" -v /tmp/ci-secrets:/run/secrets:ro -v $PWD/config/app/ci.yml:/app/config/ci.yml:ro -e CONFIG_PATH=/app/config/ci.yml backend-test npx jest --forceExit`.
|
||||
|
||||
Also disposition the ts-jest compile failures and 2 failing tests from Phase 0 (fix or quarantine-with-issue, per suite).
|
||||
|
||||
Gate 1B: on the runner (manual run or a draft workflow), backend unit suites + integration suites complete against the ephemeral DB with a recorded `Tests:` line matching the post-fix local run, AND `docker exec mvp-postgres-staging psql -U postgres -d motovaultpro -c "\dt" ` before/after shows an unchanged table count (proof the shared staging DB was untouched).
|
||||
|
||||
### 1C. OCR pytest in CI
|
||||
|
||||
Tests are copied into the image (`ocr/Dockerfile` `COPY . .`, no `ocr/.dockerignore` exists) and pytest is installed. Candidate command (UNVERIFIED end-to-end - `app.main` may import things that need config at load):
|
||||
|
||||
```bash
|
||||
docker run --rm --entrypoint python "$OCR_IMAGE" -m pytest tests -q
|
||||
```
|
||||
|
||||
EXPECTED shape: `N passed` with N near 310. IF import errors on config/WIF paths: capture the traceback in the phase issue and add the minimal env/mount it names; do not skip the suite silently.
|
||||
|
||||
### 1D. Frontend tests in CI
|
||||
|
||||
`frontend/.dockerignore` excludes `*.test.ts` / `*.test.tsx` (verified), so image stages cannot run tests today. Options:
|
||||
- Host node 20 (recommended, simplest): `cd frontend && npm ci && npx jest --reporters=default` (flag unnecessary after 1A fix).
|
||||
- Remove the test exclusions from `frontend/.dockerignore` and run in the `build` stage. Safe for image size: the production stage copies only `dist/` from the build stage.
|
||||
|
||||
Gate for Phase 1 overall: each of the three suites runs headless on the `stage` runner with a recorded pass count matching the post-fix local baseline. Paste all three `Tests:`/`passed` lines into the phase issue.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - Add the CI quality job to staging.yaml
|
||||
|
||||
Owner sign-off required (pipeline change). Add a `test` job to `.gitea/workflows/staging.yaml` that runs, per workspace: lint (`npm run lint` / eslint), type-check (`npm run type-check`), unit tests, plus OCR pytest, using the Phase 1 mechanics. Backend integration tests use the ephemeral DB from 1B.
|
||||
|
||||
Wiring decision - blocking (recommended) vs report-only:
|
||||
|
||||
| | Blocking (`deploy-staging: needs: [build, test]`) | Report-only (parallel job, nothing needs it) |
|
||||
|---|---|---|
|
||||
| Broken code reaches staging | No | Yes (staging is the de facto QA env - weakens the whole campaign) |
|
||||
| PR feedback speed | Deploy waits for tests (~minutes) | Unchanged |
|
||||
| Flaky test risk | Blocks deploys until fixed/quarantined | Ignored red jobs rot within weeks |
|
||||
| Recommendation | YES - staging deploy is the thing being protected | Only as a <=2-week transition while stabilizing suites |
|
||||
|
||||
Canary (the measurable gate - do not skip): on a branch, commit a deliberately failing test, e.g. append to any backend unit file:
|
||||
|
||||
```ts
|
||||
it('CI canary - must fail', () => { expect(1).toBe(2); });
|
||||
```
|
||||
|
||||
Open a draft PR. EXPECTED: the workflow run goes red; with blocking wiring, `deploy-staging` shows as skipped and staging still serves the previous build (confirm via `curl -s https://staging.motovaultpro.com/api/health | jq -r .status` returning `healthy` from the OLD deploy). Then revert the canary commit and observe green. Record both run URLs in the phase issue.
|
||||
|
||||
IF the failing test does NOT turn the run red: the test step's exit code is being swallowed (look for `|| true`, piping to `tail`, or `set +e`); fix before merging anything else.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 - Kill the :latest hazard
|
||||
|
||||
Owner sign-off required (pipeline change). Options ranked; (c) both is the recommended end state:
|
||||
|
||||
(a) RECOMMENDED: PR builds tag `:pr-N` + short-sha only; `:latest` moves only on push to main. In `staging.yaml`, split the latest-tagging into conditional steps. Exact condition shape (Gitea Actions):
|
||||
|
||||
```yaml
|
||||
- name: Tag and push latest (main only)
|
||||
if: gitea.event_name == 'push'
|
||||
run: |
|
||||
docker tag ${{ steps.tags.outputs.backend_image }} $REGISTRY/egullickson/backend:latest
|
||||
docker push $REGISTRY/egullickson/backend:latest
|
||||
# (same for frontend, ocr)
|
||||
```
|
||||
|
||||
Keep `--cache-from ...:latest` unconditionally (harmless read). If `gitea.event_name` evaluates empty on this act_runner version, use `github.event_name` - same context, aliased. Verify with a probe run before trusting it (print it in a step).
|
||||
Obligation: the registry purge script (`scripts/ci/purge-container-images.sh`) keeps `latest` by digest, but it is currently run MANUALLY - no purge schedule exists anywhere in the repo (the only scheduled workflow is the image MIRROR in `mirror-images.yaml`, and the runner's daily `docker system prune` cron cleans local images, not the registry - see `mvp-run-and-operate`). After this change PR-sha tags accumulate registry-side, so scheduling or periodically running the purge becomes a required follow-up: file it as its own issue in the same phase.
|
||||
|
||||
(b) SMALL + IMMEDIATE: make `production.yaml` `image_tag` a required input with no default. Change lines 14-17 to `required: true` and delete the `default: 'latest'` line; update `run-name` (line 9). Whether Gitea's dispatch UI actually blocks an empty required input is UNVERIFIED - so ALSO add a shell guard in the `validate` job:
|
||||
|
||||
```yaml
|
||||
- name: Refuse latest/empty tag
|
||||
run: |
|
||||
TAG="${{ inputs.image_tag }}"
|
||||
if [ -z "$TAG" ] || [ "$TAG" = "latest" ]; then
|
||||
echo "ERROR: deploy by explicit sha tag, never latest"; exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
(c) Both (a) and (b). Do (b) first - it is one small PR and removes the worst outcome immediately.
|
||||
|
||||
Measurable gate: PR syncs no longer move `:latest`. Digest check, modeled on the verified helper in `scripts/ci/purge-container-images.sh:69-101`, run on the runner or anywhere with a registry PAT:
|
||||
|
||||
```bash
|
||||
REG=https://git.motovaultpro.com
|
||||
T=$(curl -fsS -u "egullickson:$PAT" "$REG/v2/token?service=container_registry&scope=repository:egullickson/backend:pull" | jq -r '.token // .access_token')
|
||||
curl -fsSI -H "Authorization: Bearer $T" \
|
||||
-H "Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
|
||||
"$REG/v2/egullickson/backend/manifests/latest" | tr -d '\r' | grep -i docker-content-digest
|
||||
```
|
||||
|
||||
Record the digest, push a trivial commit to the campaign PR (a PR sync), re-run. EXPECTED: identical digest. Then merge to main and re-run. EXPECTED: digest changes. Both observations go in the PR body.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 - Staging deploy policy (owner decision gate)
|
||||
|
||||
Today every PR sync fully redeploys shared staging (`down --timeout 30` then `up -d`, brief total outage each time; last PR wins). Options, honestly:
|
||||
|
||||
| Option | Pro | Con |
|
||||
|---|---|---|
|
||||
| 1. Keep PR-deploys (status quo) | Matches single-dev reality: the PR deploy IS the end-to-end verification loop (there is no working local full-stack). Zero work. | Two open PRs fight over staging; staging never reflects main; every sync = outage + 3 image builds on a 29G-disk runner. |
|
||||
| 2. Deploy staging only on push to main; PRs build+test only | Staging always = main; PR churn stops hammering the box. | Removes the ONLY pre-merge end-to-end check. Under current practice this is a big loss - do not pick this until Phase 2 tests are strong. |
|
||||
| 3. Labeled opt-in (`if: contains(gitea.event.pull_request.labels.*.name, 'deploy-staging')`) | Deliberate deploys; test/build still run on every sync. | Label-condition behavior on Gitea act_runner is UNVERIFIED; adds a manual step to the AI-session workflow contract. |
|
||||
|
||||
Recommendation: stay on option 1 until Phase 2 has been green for 2+ weeks, then present 3 (with a verified label-condition probe) to the owner. Whatever is decided, record it and the reasoning as an issue comment; the gate is the owner's explicit sign-off comment, not this file.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 - Security and viewport scanning (CANDIDATES, unproven)
|
||||
|
||||
Everything here is an open candidate. Do not represent any of it as existing - root `CLAUDE.md`'s claim that PRs get "Mobile/desktop viewport validation" and "Security scanning" is FALSE today (verified: no such steps in any workflow, no cypress config exists, the single `frontend/cypress/e2e/stations.cy.ts` spec is unrunnable).
|
||||
|
||||
- gitleaks and/or `npm audit --omit=dev` as NON-blocking report steps first; promote to blocking only after 2 weeks of observed signal-to-noise, via a new owner-approved PR.
|
||||
- Viewport smoke: root `package.json` carries an unused `@playwright/test` dependency and no config. First concrete step: a post-`verify-staging` job running a 3-URL Playwright smoke against `https://staging.motovaultpro.com` at 390x844 and 1920x1080, asserting page title and no console errors. That is the whole first milestone - not "mobile+desktop CI validation". Do NOT gate on the Cypress spec.
|
||||
- When (if) either lands and blocks, update root `CLAUDE.md`'s CI/CD Pipeline section to match reality (see `mvp-docs-and-writing`).
|
||||
|
||||
---
|
||||
|
||||
## Wrong paths - fenced off
|
||||
|
||||
| Do not | Why (verified) |
|
||||
|---|---|
|
||||
| Point integration tests at the shared dev/staging DB | They DROP TABLE CASCADE in `afterAll`; the runner is the staging host, so "localhost postgres" IS staging's neighbor. Owner non-negotiable. |
|
||||
| Resurrect the old Docker cleanup cron | It destroyed volumes; `ansible/deploy-staging-runner.yml:315-325` exists specifically to REMOVE it. The sanctioned prune cron on the runner is deliberately out-of-repo. |
|
||||
| Use `scripts/rollback.sh` | Legacy: composes without the blue-green file, rebuilds from source, checks container names that do not exist on prod. Rollback = `scripts/ci/switch-traffic.sh` / `auto-rollback.sh` (see `mvp-run-and-operate`). |
|
||||
| "Fix" runner disk by pruning volumes (`docker system prune --volumes`, `make clean`) | `make clean` = `docker compose down -v --rmi all` (Makefile:73-76) - destroys DB volumes wherever run. Prune images only. |
|
||||
| Gate CI on the Cypress spec | No cypress config, no cypress dependency, undefined `cy.login()`, nonexistent `.or()` chainer. It cannot run. |
|
||||
| Invent root npm scripts (`npm test` at repo root) | Root `package.json` has NO scripts and no workspaces (verified). If a root fan-out script is wanted, that is its own deliberate PR, not an assumption. |
|
||||
| Trust `npm run type-check` to mean "tests compile" (backend) | `tsconfig.json:28` excludes `**/*.test.ts`; ts-jest checks them anyway. Verified divergence: type-check green, 7 suites fail to compile (see Phase 0.3 split). |
|
||||
|
||||
## Validation and promotion protocol
|
||||
|
||||
Per phase:
|
||||
1. One Gitea issue per phase (`type/chore`), titled `chore: deploy-safety phase N - {name}`. Baseline numbers and decisions (with owner sign-off comments where required) live in issue comments.
|
||||
2. The PR body contains the phase's measured gate evidence verbatim: command + output (grep counts, `Tests:` lines, digests, canary run URLs). A gate that is not pasted into the PR body did not happen.
|
||||
3. Merge only after the staging pipeline for the PR itself is green and the owner has reviewed. Label flow per `mvp-change-control`.
|
||||
|
||||
Finished state - a future session re-runs these assertions to confirm the campaign held:
|
||||
|
||||
```bash
|
||||
# 1. CI runs tests and lint (expect >=1 hit each):
|
||||
grep -rnE "jest|pytest" .gitea/workflows/staging.yaml | wc -l # EXPECT: >= 1
|
||||
grep -rnE "run lint|eslint" .gitea/workflows/staging.yaml | wc -l # EXPECT: >= 1
|
||||
# 2. Tests block the deploy:
|
||||
grep -n "needs:" .gitea/workflows/staging.yaml # EXPECT: deploy-staging needs the test job
|
||||
# 3. :latest is not pushed unconditionally:
|
||||
grep -n "push.*:latest" .gitea/workflows/staging.yaml # EXPECT: only inside an if: event_name guarded step
|
||||
# 4. Prod cannot default to latest:
|
||||
grep -n "default: 'latest'" .gitea/workflows/production.yaml # EXPECT: no output, exit 1
|
||||
grep -n "required: true" .gitea/workflows/production.yaml # EXPECT: 1 hit under image_tag
|
||||
# 5. Behavior checks (registry + a canary PR, quarterly):
|
||||
# - PR sync leaves the latest digest unchanged (Phase 3 curl check)
|
||||
# - a failing test on a branch turns the PR run red (Phase 2 canary)
|
||||
```
|
||||
|
||||
Plus the standing numbers: last recorded green counts for backend/frontend/OCR suites in the most recent phase issue. If a re-run's counts drop below the recorded ones without a filed quarantine issue, the campaign has regressed - file an issue immediately.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct inspection of the repo (all line numbers, counts, and command outputs verified on that date; test counts measured on a dev machine with node v25). Where any doc contradicts these observations, the code/workflows win.
|
||||
|
||||
Volatile facts and one-line re-verification commands:
|
||||
|
||||
| Fact (2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| Zero test/lint steps in workflows | `grep -rnE "jest|pytest|eslint|run lint" .gitea/workflows/` (exit 1 = still true) |
|
||||
| staging.yaml pushes 3 `:latest` tags on every run; PR trigger present | `grep -cn ":latest" .gitea/workflows/staging.yaml` (9) and `sed -n '8,13p' .gitea/workflows/staging.yaml` |
|
||||
| production.yaml `image_tag` defaults to `latest` | `grep -n -A4 "image_tag:" .gitea/workflows/production.yaml` |
|
||||
| Backend unit baseline 25 suites (10 pass/15 fail), 149 tests (147 pass/2 fail) locally | `cd backend && npx jest --testPathIgnorePatterns "/node_modules/" "integration" --forceExit 2>&1 \| tail -5` |
|
||||
| Frontend baseline 31 suites (17 pass/14 fail), 213 tests (196 pass/17 fail); reporter workaround works | `cd frontend && npx jest --reporters=default 2>&1 \| tail -5` |
|
||||
| OCR: 310 test functions, pytest in requirements | `grep -rc "def test_" ocr/tests/*.py \| awk -F: '{s+=$2} END {print s}'` and `grep -n pytest ocr/requirements.txt` |
|
||||
| `config/app/ci.yml` fails zod schema (missing `auth0`) | `cd backend && CONFIG_PATH=$PWD/../config/app/ci.yml SECRETS_DIR=/tmp npx jest src/features/stations/tests/unit/stations.service.test.ts --forceExit 2>&1 \| head -5` |
|
||||
| Runner is host mode with node 20 | `grep -n "runner_labels\|node_20" ansible/deploy-staging-runner.yml` |
|
||||
| `frontend/.dockerignore` excludes test files; no root `.dockerignore`; no `ocr/.dockerignore` | `ls .dockerignore ocr/.dockerignore 2>&1; grep -n "test" frontend/.dockerignore` |
|
||||
| Integration tests DROP TABLE CASCADE | `grep -rn "DROP TABLE" backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts` |
|
||||
| Registry digest helper pattern | `sed -n '69,101p' scripts/ci/purge-container-images.sh` |
|
||||
|
||||
UNVERIFIED items called out inline: OCR pytest inside the image end-to-end (1C), Gitea UI enforcement of required dispatch inputs (3b - shell guard compensates), label-conditioned deploys on act_runner (Phase 4 option 3), causes of the 17 frontend / 2 backend currently-failing tests (dispositioned in Phase 1).
|
||||
@@ -1,470 +0,0 @@
|
||||
---
|
||||
name: mvp-diagnostics-and-logging
|
||||
description: >-
|
||||
Load when you need to observe or measure MotoVaultPro instead of guessing - querying
|
||||
logs in Grafana/Loki, writing LogQL, tracing a request by requestId/X-Request-Id,
|
||||
checking /health or /api/health, cache inspection via redis-cli, PostgreSQL activity,
|
||||
or running the shipped diagnostic scripts (check-numeric-coercion.sh,
|
||||
check-route-auth.sh, local-gate.sh). This is the HOW-to-observe toolbox - load it when
|
||||
mvp-debugging-playbook (the symptom-triage hub) or a task needs measurements.
|
||||
Distinguishing triggers - "logs not appearing", Grafana unreachable, Loki curl fails,
|
||||
no logs from a container, alert firing, request timing, "slow requests", "5xx spike".
|
||||
---
|
||||
|
||||
# MotoVaultPro Diagnostics and Logging
|
||||
|
||||
Measuring instead of eyeballing: where logs live, how to query them, how to
|
||||
check health and state, and three shipped read-only diagnostic scripts.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you need to OBSERVE the system: query logs, trace a
|
||||
request, verify health, measure latency, inspect cache or database activity,
|
||||
or run the pre-push diagnostic scripts.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Symptom-to-root-cause triage of known failure modes: `mvp-debugging-playbook`
|
||||
- Deploying, rolling back, blue-green mechanics, backups: `mvp-run-and-operate`
|
||||
- What counts as test evidence / definition of done: `mvp-validation-and-qa`
|
||||
- Historical incidents and settled dead ends: `mvp-failure-archaeology`
|
||||
- Getting a working local environment at all: `mvp-build-and-env`
|
||||
- Changing log config or adding config axes: `mvp-config-and-secrets`
|
||||
|
||||
Environment reality (2026-07-07): development is done by AI sessions in this
|
||||
repo; end-to-end verification happens on STAGING via the PR pipeline. There
|
||||
is no fully working local dev loop, and CI gates nothing beyond build + boot
|
||||
(canonical statement: `mvp-validation-and-qa` section 1). The scripts in
|
||||
this skill exist because of that gap.
|
||||
|
||||
## 1. Logging topology
|
||||
|
||||
One pipeline, file-provisioned end to end. Frontend is the exception: its
|
||||
logs go to the browser console ONLY (`frontend/src/utils/logger.ts`) and
|
||||
never reach Loki.
|
||||
|
||||
```
|
||||
mvp-traefik mvp-frontend mvp-backend mvp-ocr mvp-postgres mvp-redis
|
||||
| | | | | |
|
||||
+------------+------+------+-----------+----------+------------+
|
||||
| (frontend: container
|
||||
v stdout only; app logs
|
||||
Docker json-file driver (max-size 10m, stay in the browser)
|
||||
max-file 3 - set on every service in
|
||||
docker-compose.yml)
|
||||
|
|
||||
v
|
||||
mvp-alloy (config/alloy/config.alloy)
|
||||
- discovers containers via /var/run/docker.sock
|
||||
- labels: container = container name, service = compose service
|
||||
|
|
||||
v
|
||||
mvp-loki (config/loki/config.yml)
|
||||
- TSDB schema v13, filesystem storage
|
||||
- retention_period: 720h (30 days)
|
||||
- auth disabled; distroless image, healthcheck disabled
|
||||
|
|
||||
v
|
||||
mvp-grafana (Grafana 12.4.0)
|
||||
- https://logs.motovaultpro.com (prod)
|
||||
- https://logs.staging.motovaultpro.com (staging)
|
||||
- 4 provisioned dashboards + 5 provisioned alert rules
|
||||
```
|
||||
|
||||
Grafana access: the Traefik router applies `grafana-ipwhitelist@file`
|
||||
(`config/traefik/dynamic/grafana.yml` and `dynamic-staging/grafana.yml`),
|
||||
which allows only RFC1918 source ranges (10/8, 172.16/12, 192.168/16).
|
||||
Grafana is unreachable from the public internet by design - reach it from
|
||||
inside the network (the servers themselves, or VPN). Admin password comes
|
||||
from `GRAFANA_ADMIN_PASSWORD` (default `admin`).
|
||||
|
||||
Container name matrix (label `container` in LogQL):
|
||||
|
||||
| Service | Dev | Staging | Production |
|
||||
|----------|----------------|------------------------|---------------------------------|
|
||||
| backend | `mvp-backend` | `mvp-backend-staging` | `mvp-backend-blue` / `-green` |
|
||||
| frontend | `mvp-frontend` | `mvp-frontend-staging` | `mvp-frontend-blue` / `-green` |
|
||||
| traefik | `mvp-traefik` | `mvp-traefik-staging` | `mvp-traefik` |
|
||||
| ocr | `mvp-ocr` | `mvp-ocr-staging` | `mvp-ocr` |
|
||||
| postgres | `mvp-postgres` | `mvp-postgres-staging` | `mvp-postgres` |
|
||||
| redis | `mvp-redis` | `mvp-redis-staging` | `mvp-redis` |
|
||||
| loki/alloy/grafana | `mvp-loki` / `mvp-alloy` / `mvp-grafana` in ALL environments (not renamed by overlays) ||||
|
||||
|
||||
KNOWN GAP (verified 2026-07-07): the provisioned dashboards and the
|
||||
backend-related alert rules select `{container=~"mvp-backend(-staging)?"}`.
|
||||
Loki regex matchers are fully anchored, so this does NOT match the
|
||||
production blue-green names `mvp-backend-blue`/`-green`. On production,
|
||||
backend panels are blind and the "Container Silence: mvp-backend" alert
|
||||
(noDataState: Alerting) fires from no-data. Use
|
||||
`{container=~"mvp-backend-(blue|green)"}` for prod queries.
|
||||
|
||||
## 2. LogQL cookbook
|
||||
|
||||
Field names below are verified against
|
||||
`backend/src/core/plugins/logging.plugin.ts`: every completed request logs
|
||||
`msg="Request processed"` with `requestId`, `method`, `path`, `status`,
|
||||
`duration` (ms, integer), `ip`; Pino adds `level` and `time`. Substitute the
|
||||
right container name from the matrix above.
|
||||
|
||||
Request logs (the workhorse):
|
||||
|
||||
```logql
|
||||
{container="mvp-backend-staging"} | json | msg="Request processed"
|
||||
```
|
||||
|
||||
Error sweep across all containers:
|
||||
|
||||
```logql
|
||||
{container=~"mvp-.*"} | json | level="error"
|
||||
```
|
||||
|
||||
Errors per container over time:
|
||||
|
||||
```logql
|
||||
sum by (container) (count_over_time({container=~"mvp-.*"} | json | level="error" [5m]))
|
||||
```
|
||||
|
||||
PostgreSQL errors (postgres logs are not JSON - use line filter):
|
||||
|
||||
```logql
|
||||
{container="mvp-postgres"} |~ "ERROR|FATAL|PANIC"
|
||||
```
|
||||
|
||||
OCR (Python) errors:
|
||||
|
||||
```logql
|
||||
{container=~"mvp-ocr(-staging)?"} |~ "ERROR|Exception|Traceback"
|
||||
```
|
||||
|
||||
Slow requests, p95 latency. The `| __error__=""` after `unwrap` is
|
||||
REQUIRED - it drops lines where `duration` failed to parse; without it the
|
||||
quantile returns errors or garbage:
|
||||
|
||||
```logql
|
||||
quantile_over_time(0.95,
|
||||
{container="mvp-backend-staging"} | json | msg="Request processed"
|
||||
| unwrap duration | __error__="" [5m])
|
||||
```
|
||||
|
||||
Individual requests slower than 500 ms:
|
||||
|
||||
```logql
|
||||
{container="mvp-backend-staging"} | json | msg="Request processed" | duration > 500
|
||||
```
|
||||
|
||||
All 5xx responses:
|
||||
|
||||
```logql
|
||||
{container="mvp-backend-staging"} | json | msg="Request processed" | status >= 500
|
||||
```
|
||||
|
||||
Correlate one request across services. The backend takes `X-Request-Id`
|
||||
from the incoming request or generates a UUID; Traefik access logs (JSON)
|
||||
keep the header. The backend does not echo it in the response, so find the
|
||||
ID in the request log first, then sweep:
|
||||
|
||||
```logql
|
||||
{container=~"mvp-.*"} |= "550e8400-e29b-41d4-a716-446655440000"
|
||||
```
|
||||
|
||||
To force a known ID end-to-end, send one (CORS-allowed header):
|
||||
|
||||
```bash
|
||||
curl -H "X-Request-Id: trace-$(date +%s)" https://staging.motovaultpro.com/api/health
|
||||
```
|
||||
|
||||
### Provisioned alerts
|
||||
|
||||
Defined in `config/grafana/alerting/alert-rules.yml`; evaluated every 1m,
|
||||
must hold for 5m. Contact point is a webhook placeholder - alerts are
|
||||
visible in the Grafana UI but notify nobody (2026-07-07).
|
||||
|
||||
| Alert | Severity | Condition |
|
||||
|-------|----------|-----------|
|
||||
| Error Rate Spike | critical | error-level logs > 5% of all logs over 5m |
|
||||
| Container Silence: mvp-backend / mvp-postgres / mvp-redis | warning | no logs for 5m (noDataState: Alerting) |
|
||||
| 5xx Response Spike | critical | > 10 HTTP 5xx from backend in 5m |
|
||||
|
||||
### Log-level control is deploy-time
|
||||
|
||||
One `LOG_LEVEL` fans out to all containers via
|
||||
`scripts/ci/generate-log-config.sh <DEBUG|INFO|WARN|ERROR>`, whose output
|
||||
the CI workflows append to `.env` on the server (NOT `.env.logging` -
|
||||
docs/LOGGING.md says `.env.logging`; the code in
|
||||
`.gitea/workflows/staging.yaml` appends to `.env`, and code wins). It sets
|
||||
`BACKEND_LOG_LEVEL`, `TRAEFIK_LOG_LEVEL`, `POSTGRES_LOG_STATEMENT`,
|
||||
`POSTGRES_LOG_MIN_DURATION`, `REDIS_LOGLEVEL`. Staging runs DEBUG, prod
|
||||
runs INFO. Caveat: the postgres leg of this fan-out is dead - the
|
||||
`POSTGRES_LOG_*` variables are set on the container but never applied (see
|
||||
the known gap under "PostgreSQL activity" in section 4); the backend,
|
||||
traefik, and redis legs are wired. There is no runtime toggle: changing
|
||||
verbosity means redeploying.
|
||||
Never hand-edit `.env` on a server - the next deploy's rsync/regeneration
|
||||
reverts it (owner non-negotiable).
|
||||
|
||||
## 3. Health and state checks
|
||||
|
||||
### /health vs /api/health
|
||||
|
||||
Both defined in `backend/src/app.ts`, both unauthenticated, both return
|
||||
`status: "healthy"` plus a `features` array (currently 20 entries in code).
|
||||
|
||||
- `GET /health` (port 3001, in-container) - used by the Docker healthcheck
|
||||
and CI's in-container curl. No `/api` prefix; not routed by Traefik.
|
||||
- `GET /api/health` - routed through Traefik; adds `scope: "api"`. This is
|
||||
the external verification target.
|
||||
|
||||
The CI contract (`.gitea/workflows/staging.yaml`, REQUIRED_FEATURES) checks
|
||||
that 13 specific features are present in the array: admin, auth, onboarding,
|
||||
vehicles, documents, fuel-logs, stations, maintenance, platform,
|
||||
notifications, user-profile, user-preferences, user-export. The endpoint
|
||||
returns more than 13; the contract is a subset check. If you add a feature
|
||||
capsule, adding it to the app.ts arrays is cosmetic; adding it to
|
||||
REQUIRED_FEATURES makes it deploy-gating.
|
||||
|
||||
```bash
|
||||
curl -s https://staging.motovaultpro.com/api/health | jq '.status, (.features | length)'
|
||||
```
|
||||
|
||||
### Container state per environment
|
||||
|
||||
The `-f` stack matters - a bare `docker compose ps` on staging/prod uses
|
||||
only the base file and shows wrong/partial state:
|
||||
|
||||
```bash
|
||||
# Dev (local machine)
|
||||
docker compose ps
|
||||
make health-check-all # ps table + Traefik service discovery counts
|
||||
|
||||
# Staging (on mvp-staging, /opt/motovaultpro)
|
||||
docker compose -f docker-compose.yml -f docker-compose.staging.yml ps
|
||||
|
||||
# Production (on prod server, /opt/motovaultpro)
|
||||
docker compose -f docker-compose.yml -f docker-compose.blue-green.yml -f docker-compose.prod.yml ps
|
||||
```
|
||||
|
||||
### config/deployment/state.json - and why it lies mid-deploy
|
||||
|
||||
On the prod server, `config/deployment/state.json` records
|
||||
`active_stack` (blue/green), last deployment, and per-stack health. Read it
|
||||
with `cat /opt/motovaultpro/config/deployment/state.json | jq .`
|
||||
|
||||
It lies in two windows:
|
||||
1. Every deploy rsyncs `config/` with `--delete`, overwriting state.json
|
||||
with the repo default (`active_stack: blue`, all-null history). The
|
||||
workflow reads the real state BEFORE the rsync and re-stamps it at the
|
||||
end - but any inspection between rsync and the final stamp shows the
|
||||
repo default, not reality.
|
||||
2. Traefik weights in `config/traefik/dynamic/blue-green.yml` are the
|
||||
actual routing truth; state.json is a record of intent. When in doubt,
|
||||
read the weights file.
|
||||
|
||||
### When the log pipeline itself is broken
|
||||
|
||||
1. `docker logs mvp-alloy` (same name in every environment) - Alloy is the
|
||||
collector; discovery or push errors appear here.
|
||||
2. You CANNOT `curl` Loki from the host or exec into it: Loki 3.x is a
|
||||
distroless image (no shell, no wget/curl inside; the compose healthcheck
|
||||
is explicitly disabled for this reason - see the comment in
|
||||
docker-compose.yml). Verify Loki through Grafana: Connections >
|
||||
Data sources > Loki > Test, or from any container on the `backend`
|
||||
network, e.g.
|
||||
`docker exec mvp-backend-staging wget -qO- http://mvp-loki:3100/ready`.
|
||||
3. `docker logs mvp-grafana` for provisioning errors (bad dashboard JSON or
|
||||
alert YAML shows up here at startup).
|
||||
4. Docker json-file logs still exist even if the pipeline is down:
|
||||
`docker logs --since 10m mvp-backend-staging`.
|
||||
|
||||
## 4. Measuring instead of eyeballing
|
||||
|
||||
### Request timings
|
||||
|
||||
Timings live in the `duration` field (ms) of `msg="Request processed"`
|
||||
lines - see the quantile queries above. The API Performance dashboard
|
||||
(p50/p95/p99, slowest endpoints by avg duration) is provisioned from
|
||||
`config/grafana/dashboards/api-performance.json`. For a one-off check
|
||||
without Grafana:
|
||||
|
||||
```bash
|
||||
docker logs --since 5m mvp-backend-staging 2>&1 |
|
||||
grep '"msg":"Request processed"' |
|
||||
awk -F'"duration":' '{split($2,a,","); gsub(/[^0-9]/,"",a[1]); print a[1]}' | sort -n | tail -5
|
||||
```
|
||||
|
||||
### Cache behavior (Redis)
|
||||
|
||||
Backend uses DB 0 with key prefix `mvp:` (`backend/src/core/config/redis.ts`;
|
||||
DB from `config/app/*.yml` `redis.db: 0`). The OCR service uses DB 1
|
||||
(`REDIS_DB: 1` in docker-compose.yml) for job state. Locks use `mvp:lock:`.
|
||||
|
||||
```bash
|
||||
# Staging names shown; drop -staging for dev/prod
|
||||
docker exec mvp-redis-staging redis-cli -n 0 --scan --pattern 'mvp:*' | head -50
|
||||
docker exec mvp-redis-staging redis-cli -n 0 TTL 'mvp:some-key'
|
||||
docker exec mvp-redis-staging redis-cli -n 1 --scan --pattern '*' | head # OCR jobs
|
||||
docker exec mvp-redis-staging redis-cli INFO keyspace
|
||||
```
|
||||
|
||||
Cache reads swallow errors and return null (cache failure never breaks a
|
||||
request), so a dead Redis looks like a 100% miss rate, not errors. Check
|
||||
`docker logs mvp-redis-staging` and hit rates via `INFO stats`.
|
||||
|
||||
### PostgreSQL activity
|
||||
|
||||
Dev shell: `make db-shell-app` (wraps
|
||||
`docker compose exec mvp-postgres psql -U postgres -d motovaultpro`).
|
||||
Staging/prod equivalent:
|
||||
|
||||
```bash
|
||||
docker exec -it mvp-postgres-staging psql -U postgres -d motovaultpro
|
||||
```
|
||||
|
||||
Useful read-only checks inside psql:
|
||||
|
||||
```sql
|
||||
SELECT pid, state, now() - query_start AS age, left(query, 80)
|
||||
FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC;
|
||||
SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 15;
|
||||
```
|
||||
|
||||
KNOWN GAP: the `POSTGRES_LOG_STATEMENT` / `POSTGRES_LOG_MIN_DURATION_STATEMENT`
|
||||
values set in compose (`docker-compose.yml:245,247`) are INERT - they are plain
|
||||
environment variables that the official postgres image does not read, and
|
||||
nothing in the repo applies them (no `command: postgres -c log_statement=...`,
|
||||
no custom postgresql.conf, no initdb script). Postgres runs with defaults
|
||||
(`log_statement=none`, `log_min_duration_statement=-1`) at every LOG_LEVEL, so
|
||||
per-statement and `duration:` lines never reach Loki; a
|
||||
`{container="mvp-postgres-staging"} |~ "duration:"` hunt finds nothing. Until
|
||||
compose passes the values via `command: postgres -c ...`, slow-query hunting
|
||||
must use `pg_stat_activity` (above) instead.
|
||||
|
||||
### End-to-end request trace, step by step
|
||||
|
||||
1. Send the request with a known `X-Request-Id` (see cookbook above).
|
||||
2. Traefik: `{container="mvp-traefik-staging"} | json |= "<id>"` - confirms
|
||||
arrival, router matched, status returned to client.
|
||||
3. Backend: `{container="mvp-backend-staging"} |= "<id>"` - all application
|
||||
log lines plus the final `Request processed` line with duration.
|
||||
4. If OCR involved: `{container="mvp-ocr-staging"} |= "<id>"`.
|
||||
5. Postgres/Redis logs are not request-tagged; correlate by timestamp.
|
||||
|
||||
## 5. Shipped diagnostic scripts
|
||||
|
||||
All in `.claude/skills/mvp-diagnostics-and-logging/scripts/`, all
|
||||
executable, all safe read-only checks (no network, no writes outside temp
|
||||
logs), portable bash (macOS bash 3.2). Run them from anywhere inside the
|
||||
repo. Each was run against the repo on 2026-07-07.
|
||||
|
||||
### check-numeric-coercion.sh - the #1 recurring bug class
|
||||
|
||||
node-postgres returns NUMERIC/DECIMAL columns as STRINGS.
|
||||
`backend/src/core/config/database.ts` overrides only the DATE parser, so
|
||||
every repository mapper must coerce manually with `Number()`/`parseFloat()`.
|
||||
Missed coercion caused issues #239, #241, #244 (symptoms: MPG shows NaN,
|
||||
string concatenation instead of addition, sort by cost is alphabetical).
|
||||
|
||||
```bash
|
||||
./.claude/skills/mvp-diagnostics-and-logging/scripts/check-numeric-coercion.sh
|
||||
```
|
||||
|
||||
What it does: extracts every NUMERIC/DECIMAL column name from all migration
|
||||
SQL (column defs, ADD COLUMN, ALTER COLUMN TYPE), then (a) flags any
|
||||
`row.<col>` read in a `*.repository.ts` file whose line lacks
|
||||
`Number(`/`parseFloat(`, and (b) flags raw-row returns
|
||||
(`return res.rows...`) in features whose migrations define numeric columns -
|
||||
the exact shape of bug #244, which bypassed the mapper entirely.
|
||||
|
||||
Interpretation: exit 0 = clean. Exit 1 = each SUSPECT line needs coercion
|
||||
(`row.x != null ? parseFloat(row.x) : null` is the house pattern) - or is a
|
||||
false positive if coercion happens on another line or in the service layer;
|
||||
read before fixing. Run after ANY repository or migration change that
|
||||
touches numeric columns.
|
||||
|
||||
### check-route-auth.sh - unlisted-public routes
|
||||
|
||||
There is no global auth hook; every route opts in via
|
||||
`preHandler: [fastify.authenticate]` (or `requireAdmin`/`requireTier`, or
|
||||
the common alias `const requireAuth = fastify.authenticate.bind(fastify)`).
|
||||
A forgotten preHandler ships a public endpoint silently - pre-launch, this
|
||||
is a RULE 0 severity gap.
|
||||
|
||||
```bash
|
||||
./.claude/skills/mvp-diagnostics-and-logging/scripts/check-route-auth.sh
|
||||
```
|
||||
|
||||
What it does: parses every registration block in
|
||||
`backend/src/features/*/api/*.routes.ts` (199 blocks as of 2026-07-07) and
|
||||
prints any block containing no recognized guard. Known-intentional public
|
||||
routes are allowlisted in the script and printed as OK: `/webhooks/stripe`,
|
||||
`/webhooks/resend/inbound` (provider signature auth), `/auth/signup`,
|
||||
`/auth/resend-verification-public` (pre-auth flows).
|
||||
|
||||
Interpretation: exit 0 = only allowlisted routes are public AMONG the
|
||||
`features/*/api` route files the script scans. Routes registered in core
|
||||
code are OUT OF SCOPE and pass silently - including the intentionally
|
||||
public `GET /api/config/feature-tiers` (`backend/src/core/config/config.routes.ts`,
|
||||
no preHandler) and the `app.ts` registrations (`/health`, `/api/health`,
|
||||
`/auth/verify`); the script's header comment carries the canonical
|
||||
intentional-public list covering both scopes. A future unguarded route
|
||||
added outside `features/*/api/` would also pass silently - exit 0 is not a
|
||||
whole-backend guarantee. Exit 1 = an UNGUARDED route: either add a guard
|
||||
or, if genuinely public, add it to KNOWN_PUBLIC in the script with a
|
||||
justification comment. Heuristic caveats:
|
||||
a guard aliased under a new name reads as UNGUARDED (safe direction); a
|
||||
comment containing a guard word inside a block could mask a gap (verify by
|
||||
reading the code). Run after adding or reshaping any route file.
|
||||
|
||||
### local-gate.sh - the pre-push gate CI does not provide
|
||||
|
||||
CI runs zero tests and zero lint. This script is the substitute:
|
||||
|
||||
```bash
|
||||
./.claude/skills/mvp-diagnostics-and-logging/scripts/local-gate.sh
|
||||
```
|
||||
|
||||
Runs, with a PASS/FAIL table and per-step logs in `$TMPDIR`:
|
||||
backend lint, backend type-check, backend UNIT tests
|
||||
(`--testPathPattern='(tests/unit|src/core)' --testPathIgnorePatterns=integration --forceExit`),
|
||||
frontend lint, frontend type-check. Requires `npm install` in `backend/`
|
||||
and `frontend/` first.
|
||||
|
||||
Deliberate exclusions: backend integration tests need a live database and
|
||||
some DROP TABLE CASCADE (owner non-negotiable: no destructive DB operations
|
||||
without a fresh backup - never point them at a shared DB casually); the
|
||||
audit-log `__tests__` also require a live database and are excluded by
|
||||
pattern; frontend jest is broken outside the container (2026-07-07).
|
||||
`--forceExit` is required because open pg/redis handles otherwise hang jest.
|
||||
|
||||
Interpretation: all PASS = safe to push (integration behavior still
|
||||
unverified until staging). Any FAIL = fix first; CI will not catch it.
|
||||
KNOWN STATE (2026-07-07): backend unit tests FAIL on main - 12 of 22 suites
|
||||
have pre-existing ts-jest compile errors (stale test mocks vs current
|
||||
types, e.g. `new AuthService(...)` missing the termsData argument) plus 2
|
||||
failing assertions. This is untriaged rot from CI running nothing; treat a
|
||||
FAIL here as "no worse than main" only after diffing against a main-branch
|
||||
run, and see `mvp-validation-and-qa` for the evidence bar.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 by direct inspection of the repo (all commands, paths,
|
||||
field names, and line-anchored claims verified against code; where
|
||||
docs/LOGGING.md disagrees with code - `.env.logging` vs `.env` - code wins).
|
||||
|
||||
Volatile facts and re-verification commands:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| Request log fields (requestId/method/path/status/duration/ip) | `grep -A8 "Request processed" backend/src/core/plugins/logging.plugin.ts` |
|
||||
| Loki 30-day retention, TSDB v13 | `grep -E "retention_period\|schema:" config/loki/config.yml` |
|
||||
| Alloy labels container/service | `grep target_label config/alloy/config.alloy` |
|
||||
| json-file 10m x 3 on all services | `grep -c "max-size" docker-compose.yml` |
|
||||
| Grafana RFC1918 whitelist | `cat config/traefik/dynamic/grafana.yml config/traefik/dynamic-staging/grafana.yml` |
|
||||
| Alert rules (5%/5m, silence, 5xx>10) | `grep -E "title\|for:\|noDataState" config/grafana/alerting/alert-rules.yml` |
|
||||
| Dashboard/alert regex missing prod blue-green | `grep -c "mvp-backend(-staging)?" config/grafana/dashboards/*.json config/grafana/alerting/alert-rules.yml` vs `grep container_name docker-compose.blue-green.yml` |
|
||||
| 13-feature CI health contract | `grep REQUIRED_FEATURES .gitea/workflows/staging.yaml` |
|
||||
| /health features array (20 entries) | `grep -n "features:" backend/src/app.ts` (read the two arrays) |
|
||||
| Log config appended to .env | `grep -n "generate-log-config" .gitea/workflows/*.yaml` |
|
||||
| Redis DB 0 backend / DB 1 OCR, prefix mvp: | `grep "db:" config/app/*.yml; grep REDIS_DB docker-compose.yml; grep "prefix = " backend/src/core/config/redis.ts` |
|
||||
| Known-public routes allowlist | `./.claude/skills/mvp-diagnostics-and-logging/scripts/check-route-auth.sh` |
|
||||
| Numeric coercion clean state | `./.claude/skills/mvp-diagnostics-and-logging/scripts/check-numeric-coercion.sh` |
|
||||
| Backend unit tests failing on main | `cd backend && npm test -- --testPathPattern='tests/unit' --forceExit` |
|
||||
| Container names per env | `grep container_name docker-compose*.yml` |
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
# check-numeric-coercion.sh - Find NUMERIC/DECIMAL columns returned from
|
||||
# repositories without Number()/parseFloat() coercion.
|
||||
#
|
||||
# Why: node-postgres returns NUMERIC/DECIMAL (OID 1700) as *strings*.
|
||||
# backend/src/core/config/database.ts overrides only the DATE parser, so
|
||||
# every repository mapper must coerce manually. Missed coercion is this
|
||||
# project's #1 recurring bug class (issues #239, #241, #244).
|
||||
#
|
||||
# Usage: ./check-numeric-coercion.sh (read-only; run from anywhere in repo)
|
||||
# Exit: 0 = no suspects, 1 = suspects found, 2 = setup error
|
||||
set -u
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||
[ -z "$ROOT" ] && { echo "ERROR: not inside a git repo" >&2; exit 2; }
|
||||
SRC="$ROOT/backend/src"
|
||||
[ -d "$SRC" ] && cd "$ROOT" || { echo "ERROR: $SRC missing" >&2; exit 2; }
|
||||
|
||||
# 1. Collect numeric/decimal column names from all migration SQL.
|
||||
# Three shapes: column definition lines, ADD COLUMN, ALTER COLUMN ... TYPE.
|
||||
COLS=$(
|
||||
{
|
||||
grep -rhiE '^[[:space:]]*"?[a-z_]+"?[[:space:]]+(numeric|decimal)[[:space:](]' \
|
||||
--include='*.sql' "$SRC" | awk '{gsub(/"/,""); print tolower($1)}'
|
||||
grep -rhioE 'add column (if not exists )?"?[a-z_]+"? +(numeric|decimal)' \
|
||||
--include='*.sql' "$SRC" | awk '{gsub(/"/,""); print tolower($(NF-1))}'
|
||||
grep -rhioE 'alter column "?[a-z_]+"? type +(numeric|decimal)' \
|
||||
--include='*.sql' "$SRC" | awk '{gsub(/"/,""); print tolower($(NF-2))}'
|
||||
} | sort -u
|
||||
)
|
||||
[ -z "$COLS" ] && { echo "ERROR: no numeric columns found (wrong dir?)" >&2; exit 2; }
|
||||
|
||||
echo "Numeric/decimal columns found in migrations:"
|
||||
echo "$COLS" | tr '\n' ' '; echo; echo
|
||||
|
||||
# 2. In every repository file, flag lines that read row.<col> without
|
||||
# Number( or parseFloat( on the same line.
|
||||
SUSPECTS=0
|
||||
for col in $COLS; do
|
||||
HITS=$(find "$SRC" -name '*.repository.ts' -exec \
|
||||
grep -nE "row\.${col}([^a-zA-Z0-9_]|\$)" {} + 2>/dev/null |
|
||||
grep -v 'parseFloat(' | grep -v 'Number(')
|
||||
if [ -n "$HITS" ]; then
|
||||
echo "SUSPECT column '$col' (string from pg, no coercion on line):"
|
||||
echo "$HITS" | sed "s|$ROOT/||; s/^/ /"
|
||||
SUSPECTS=$((SUSPECTS + $(echo "$HITS" | wc -l | tr -d ' ')))
|
||||
fi
|
||||
done
|
||||
|
||||
# 3. Flag raw-row returns (the actual shape of bug #244: "return res.rows[0]"
|
||||
# bypasses the mapper entirely) in features whose migrations define
|
||||
# numeric columns. Scalar returns (.count/.exists/.length) are filtered.
|
||||
for repo in $(find "$SRC/features" -name '*.repository.ts'); do
|
||||
feat=$(echo "$repo" | sed "s|$SRC/features/||; s|/.*||")
|
||||
featcols=$(grep -rhiE "(numeric|decimal)" --include='*.sql' \
|
||||
"$SRC/features/$feat/migrations" 2>/dev/null | head -1)
|
||||
[ -z "$featcols" ] && continue
|
||||
RAW=$(grep -nE 'return (res|result)\.rows' "$repo" |
|
||||
grep -vE 'map[A-Za-z]*\(|\.map\(|\?\.|\.length|\.count|\.exists')
|
||||
if [ -n "$RAW" ]; then
|
||||
echo "SUSPECT raw-row return in numeric-bearing feature '$feat':"
|
||||
echo "$RAW" | sed "s|^| ${repo#"$ROOT"/}:|"
|
||||
SUSPECTS=$((SUSPECTS + $(echo "$RAW" | wc -l | tr -d ' ')))
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
if [ "$SUSPECTS" -eq 0 ]; then
|
||||
echo "PASS: no uncoerced numeric column reads or raw-row returns found."
|
||||
exit 0
|
||||
else
|
||||
echo "FAIL: $SUSPECTS suspect line(s). Each must wrap the value in Number()"
|
||||
echo "or parseFloat() (nullable: 'row.x != null ? parseFloat(row.x) : null')."
|
||||
echo "Limitations: same-line heuristic; coercion done on a different line or"
|
||||
echo "in the service layer will still be flagged - verify before fixing."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/bin/bash
|
||||
# check-route-auth.sh - List backend routes with NO auth guard in their
|
||||
# registration block, so unintentionally-public endpoints are visible.
|
||||
#
|
||||
# Why: there is no global auth hook. Every route must opt in via
|
||||
# preHandler: [fastify.authenticate] / requireAdmin / requireTier
|
||||
# (backend/src/core/plugins/*.plugin.ts). A forgotten preHandler ships a
|
||||
# public endpoint silently.
|
||||
#
|
||||
# Usage: ./check-route-auth.sh (read-only; run from anywhere in repo)
|
||||
# Exit: 0 = only known-public routes unguarded, 1 = unexpected unguarded route
|
||||
set -u
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||
[ -z "$ROOT" ] && { echo "ERROR: not inside a git repo" >&2; exit 2; }
|
||||
cd "$ROOT" || exit 2
|
||||
|
||||
# CANONICAL intentional-public-route list (verified 2026-07-09). This comment
|
||||
# is the ONE home for it; mvp-architecture-contract invariant 2.1 and
|
||||
# mvp-debugging-playbook section 7 point here instead of keeping their own lists.
|
||||
#
|
||||
# Within this script's scan scope (backend/src/features/*/api/*.routes.ts),
|
||||
# allowlisted via KNOWN_PUBLIC below:
|
||||
# /webhooks/stripe, /webhooks/resend/inbound - provider signature auth
|
||||
# /auth/signup, /auth/resend-verification-public - pre-auth flows
|
||||
# (comments in auth.routes.ts: "public, no JWT required")
|
||||
#
|
||||
# OUTSIDE this script's scan scope (registered in core code, also intentionally
|
||||
# public - the script can neither flag nor clear them):
|
||||
# /health, /api/health - backend/src/app.ts health endpoints
|
||||
# /auth/verify - backend/src/app.ts (Traefik forward-auth)
|
||||
# GET /api/config/feature-tiers - backend/src/core/config/config.routes.ts
|
||||
# (no preHandler; config is not sensitive)
|
||||
KNOWN_PUBLIC='/webhooks/stripe /webhooks/resend/inbound /auth/signup /auth/resend-verification-public'
|
||||
|
||||
OUT=$(awk '
|
||||
function flush() {
|
||||
if (inblock && !guarded) printf "%s:%d %s %s\n", f, startline, method, path
|
||||
inblock = 0
|
||||
}
|
||||
FNR == 1 { flush() }
|
||||
/fastify\.(get|post|put|patch|delete)[<(]/ {
|
||||
flush()
|
||||
inblock = 1; guarded = 0; startline = FNR; path = "?"; f = FILENAME
|
||||
match($0, /fastify\.(get|post|put|patch|delete)/)
|
||||
method = toupper(substr($0, RSTART + 8, RLENGTH - 8))
|
||||
}
|
||||
inblock && path == "?" {
|
||||
if (match($0, "\047/[^\047]*\047")) path = substr($0, RSTART + 1, RLENGTH - 2)
|
||||
}
|
||||
# requireAuth is the common local alias: const requireAuth = fastify.authenticate.bind(fastify)
|
||||
inblock && /[ .\[](authenticate|requireAuth|requireAdmin|requireTier)[ ,\]\}\)\(]/ { guarded = 1 }
|
||||
END { flush() }
|
||||
' backend/src/features/*/api/*.routes.ts)
|
||||
|
||||
TOTAL=$(grep -c 'fastify\.\(get\|post\|put\|patch\|delete\)[<(]' \
|
||||
backend/src/features/*/api/*.routes.ts | awk -F: '{s+=$2} END {print s}')
|
||||
|
||||
echo "Route registrations scanned: $TOTAL"
|
||||
echo
|
||||
|
||||
FAIL=0
|
||||
if [ -z "$OUT" ]; then
|
||||
echo "PASS: every route block contains an auth guard."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Routes with NO authenticate/requireAdmin/requireTier in their block:"
|
||||
echo "$OUT" | while read -r line; do
|
||||
p=$(echo "$line" | awk '{print $3}')
|
||||
case " $KNOWN_PUBLIC " in
|
||||
*" $p "*) echo " OK (known-public, allowlisted): $line" ;;
|
||||
*) echo " UNGUARDED: $line" ;;
|
||||
esac
|
||||
done
|
||||
# Re-count outside the pipe subshell (bash 3.2: while-in-pipe loses vars)
|
||||
FAIL=$(echo "$OUT" | awk -v kp=" $KNOWN_PUBLIC " '{ if (index(kp, " " $3 " ") == 0) n++ } END {print n+0}')
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "PASS: all unguarded routes are on the known-public allowlist."
|
||||
exit 0
|
||||
else
|
||||
echo "FAIL: $FAIL route(s) unguarded and not on the allowlist."
|
||||
echo "Either add a preHandler guard or, if intentionally public, add the"
|
||||
echo "path to KNOWN_PUBLIC in this script with a justification comment."
|
||||
echo "Limitation: block-scope heuristic; a guard mentioned in a comment"
|
||||
echo "inside the block can mask a real gap - verify hits by reading code."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
# local-gate.sh - Run every quality check that works on a dev machine and
|
||||
# print a PASS/FAIL table.
|
||||
#
|
||||
# Why: CI runs ZERO tests and ZERO lint (the only PR gate is that images
|
||||
# build and staging boots healthy). This script IS the pre-push gate.
|
||||
#
|
||||
# Scope (deliberate):
|
||||
# - backend lint, type-check, UNIT tests only (--forceExit; open pg/redis
|
||||
# handles otherwise hang jest). Integration tests are EXCLUDED: they
|
||||
# need a live database and some DROP TABLE CASCADE (owner non-negotiable:
|
||||
# never without a fresh backup). audit-log __tests__ also need a DB and
|
||||
# are excluded via path pattern.
|
||||
# - frontend lint, type-check. Frontend jest is broken outside the
|
||||
# container (2026-07-07), so no frontend tests here.
|
||||
#
|
||||
# Usage: ./local-gate.sh (needs node_modules in backend/ and frontend/:
|
||||
# run "npm install" in each first)
|
||||
# Exit: 0 = all pass, 1 = at least one failure
|
||||
set -u
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||
[ -z "$ROOT" ] && { echo "ERROR: not inside a git repo" >&2; exit 2; }
|
||||
LOGDIR="${TMPDIR:-/tmp}/mvp-local-gate.$$"
|
||||
mkdir -p "$LOGDIR"
|
||||
|
||||
RESULTS=""
|
||||
OVERALL=0
|
||||
|
||||
run_step() { # run_step <label> <dir> <command...>
|
||||
label="$1"; dir="$2"; shift 2
|
||||
log="$LOGDIR/$(echo "$label" | tr ' /' '__').log"
|
||||
printf '%-28s ' "$label..."
|
||||
if (cd "$ROOT/$dir" && "$@") >"$log" 2>&1; then
|
||||
echo "PASS"
|
||||
RESULTS="$RESULTS$label|PASS
|
||||
"
|
||||
else
|
||||
echo "FAIL (log: $log)"
|
||||
tail -15 "$log" | sed 's/^/ /'
|
||||
RESULTS="$RESULTS$label|FAIL
|
||||
"
|
||||
OVERALL=1
|
||||
fi
|
||||
}
|
||||
|
||||
run_step "backend lint" backend npm run --silent lint
|
||||
run_step "backend type-check" backend npm run --silent type-check
|
||||
run_step "backend unit tests" backend npm test --silent -- \
|
||||
--testPathPattern='(tests/unit|src/core)' \
|
||||
--testPathIgnorePatterns='integration' --forceExit
|
||||
run_step "frontend lint" frontend npm run --silent lint
|
||||
run_step "frontend type-check" frontend npm run --silent type-check
|
||||
|
||||
echo
|
||||
echo "==================== LOCAL GATE ===================="
|
||||
printf '%-28s %s\n' "CHECK" "RESULT"
|
||||
echo "$RESULTS" | awk -F'|' 'NF { printf "%-28s %s\n", $1, $2 }'
|
||||
echo "====================================================="
|
||||
if [ "$OVERALL" -eq 0 ]; then
|
||||
echo "PASS: safe to push. (Integration tests NOT run - container-only.)"
|
||||
else
|
||||
echo "FAIL: fix before pushing. CI will NOT catch these."
|
||||
fi
|
||||
exit "$OVERALL"
|
||||
@@ -1,221 +0,0 @@
|
||||
---
|
||||
name: mvp-docs-and-writing
|
||||
description: Load before reading OR writing any documentation in this repo. Covers which docs to trust vs which are stale-dangerous (ARCHITECTURE-OVERVIEW, DATABASE-SCHEMA, AUDIT, PLATFORM-SERVICES all contain false claims), house style (tables, CLAUDE.md indexes, SCREAMING-KEBAB names, no emojis), the doc-sync discipline, and the temporal-contamination rule for comments. Triggers - updating a README or CLAUDE.md after a code change, citing a doc as evidence, "docs say X but code does Y", writing a feature README, adding code comments to a PR, "is this doc current", updating .ai/context.json, or maintaining this skill library itself.
|
||||
---
|
||||
|
||||
# MotoVaultPro Documentation and Writing
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
**Use this skill when:**
|
||||
- You are about to trust a `docs/` file as a source of truth (check the trust table first).
|
||||
- You are writing or updating any doc: `docs/*.md`, a `CLAUDE.md` index, a feature README, `.ai/context.json`, or a skill in this library.
|
||||
- You are writing code comments in a PR (temporal-contamination rule applies).
|
||||
- A doc contradicts code and you need the tiebreak rule (short answer: code wins).
|
||||
|
||||
**Do NOT use this skill for:**
|
||||
- The issue/branch/PR/label workflow itself — `mvp-change-control`.
|
||||
- What counts as test evidence or definition of done — `mvp-validation-and-qa`.
|
||||
- Recording an incident post-mortem — `mvp-failure-archaeology` (this skill only tells you WHEN to add one, section 5).
|
||||
- Architecture facts themselves — `mvp-architecture-contract` (this skill tells you which architecture DOCS lie).
|
||||
|
||||
**Prime directive:** where docs contradict code, code wins. Verify against source before repeating any doc claim. Several docs below contain confidently-worded false statements.
|
||||
|
||||
## 1. Docs-of-record map with trust levels
|
||||
|
||||
Verified against code 2026-07-07. All paths repo-relative.
|
||||
|
||||
### Trustworthy (verify-then-use)
|
||||
|
||||
| Doc | Covers | Notes |
|
||||
|-----|--------|-------|
|
||||
| `docs/CICD-DEPLOY.md` | Staging-first deploy, blue-green prod, secrets, rollback | Most current infra doc. All 4 workflows exist in `.gitea/workflows/`; image list (postgres:18-alpine, redis:8.4-alpine, traefik:v3.6) matches `docker-compose.yml`. |
|
||||
| `docs/LOGGING.md` | LOG_LEVEL matrix, Grafana dashboards/alerts, LogQL cookbook | Dashboards and alerting files verified in `config/grafana/`. Matches Pino log shape. |
|
||||
| `docs/TESTING.md` | Mock strategy; per-workspace test scripts | Partially stale-dangerous: its container-based test workflow is impossible against the shipped images — the backend production image has no jest (`npm ci --omit=dev`, dist-only) and the frontend image is nginx-only with tests dockerignored, so `make shell-backend && npm test` and `docker compose exec mvp-frontend npm test` both fail (see `mvp-deploy-safety-campaign` 1B/1D and `mvp-build-and-env`). Its "All 15 features have test suites" list is also stale — 21 features exist. Trust the mock-strategy prose and per-workspace script names only. |
|
||||
| `docs/USER-GUIDE.md` | End-user manual, every page and Pro feature | Most detailed and recent doc. One known error: line ~572 says Pro = "Up to 10 vehicles"; the Tier Comparison (~line 624) and Downgrade table (~line 864) say 5, and code says 5 (`backend/src/core/config/feature-tiers.ts`, `VEHICLE_LIMITS: { free: 2, pro: 5, enterprise: null }`). The "10" is wrong. |
|
||||
| `docs/VEHICLES-API.md` | Vehicles CRUD, images, dropdown cascade | Field names (`trimLevel`, `driveType`, `fuelType`) match code — more current than DATABASE-SCHEMA. Missing: `POST /api/vehicles/decode-vin` and the TCO fields from migration 006. |
|
||||
| `docs/TIER-GATING.md` | Tier hierarchy, `requireTier`, `useTierAccess` | Mechanics accurate. Does not document `VEHICLE_LIMITS`; calls Stripe billing "future" though `backend/src/features/subscriptions/` exists. |
|
||||
| `docs/DATABASE-MIGRATION.md` | pg_dump export/import scripts, backup cron | Scripts exist; `npm run migrate:all` verified in `backend/package.json`. WARNING: `scripts/import-database.sh --drop-existing` is a destructive operation — fresh backup required first (owner non-negotiable). |
|
||||
|
||||
### Stale-dangerous (do NOT cite without re-verifying)
|
||||
|
||||
| Doc | Why dangerous |
|
||||
|-----|---------------|
|
||||
| `docs/ARCHITECTURE-OVERVIEW.md` | Self-contradictory: header says PostgreSQL 18 / Redis 8, its own Service Details say `postgres:15-alpine` / `redis:7-alpine`, `traefik:v3.0`; actual compose is postgres:18-alpine, redis:8.4-alpine, traefik:v3.6. Says VIN decoding "Planned/Future - not yet implemented" — FALSE: `POST /api/vehicles/decode-vin` ships (`backend/src/features/vehicles/api/vehicles.routes.ts:79`, Pro/Enterprise gated). Lists "Current Features (15)"; 21 exist. |
|
||||
| `docs/DATABASE-SCHEMA.md` | Materially stale. Vehicles table shows `trim VARCHAR(100)` and omits `trim_level`/`drive_type`/`fuel_type` (migration 002) and all TCO fields (migration 006). Missing entire table sets: audit_logs, subscriptions, user_notifications, ownership-costs, user-import. Shows `user_id VARCHAR(255)` everywhere with NO mention of the UUID identity migration (`backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sql`), which adds `user_profile_id UUID` to 17 feature tables. For schema truth, read the migration files, not this doc. |
|
||||
| `docs/AUDIT.md` | Historical snapshot dated 2025-12-13, never updated post-remediation. Presents FIXED criticals as open: the Math.random storage-key finding is fixed (`crypto.randomBytes(32)` in `documents.controller.ts`); magic-byte validation exists; an audit_logs table now exists; backend ESLint exists. Also claims Winston logging — backend uses Pino. Treat as archive only. Never cite its findings as current. |
|
||||
| `docs/ocr-pipeline-tech-stack.md` | Describes a never-shipped design: lists Celery task queue and S3/MinIO storage as stack components, but `ocr/requirements.txt` has no celery and no object store is deployed. Also omits `ocr/app/engines/gemini_engine.py`, which exists. The engine-abstraction section is real; the infrastructure sections are aspirational and unlabeled. For OCR truth use `mvp-ocr-gemini-pipeline`. |
|
||||
| `docs/PLATFORM-SERVICES.md` | Says VIN decoding "planned but not yet implemented" — false, same as ARCHITECTURE-OVERVIEW. Largely redundant with it too. Endpoint list is accurate; the VIN claim is not. |
|
||||
| `docs/E2E-TESTING.md` | A 9-line fragment (a VIN/license-plate scenario table). `docs/CLAUDE.md` bills it as "Cypress E2E test setup" — it contains zero Cypress content. E2E tooling is effectively undocumented. |
|
||||
| `.ai/context.json` | Declared canonical by CLAUDE.md, but its feature list has 15 entries (21 capsules exist in `backend/src/features/`) and it repeats the false "VIN decoding is planned/future" claim. If you touch it, fix these — flag the update in your PR. |
|
||||
| `.ai/workflow-contract.json` | Says sub-issues get `status/in-progress` labels; CLAUDE.md says status is tracked on the PARENT only and sub-issues stay `status/backlog`. CLAUDE.md wins. Also still mandates sprints/milestones — abandoned by owner directive 2026-05-12; work flows directly from issues. |
|
||||
|
||||
Also note: root `CLAUDE.md` and `README.md` instruct `npm test` / `npm run lint` at repo root, but root `package.json` has NO scripts block. Run commands from `backend/`, `frontend/`, or `ocr/`. And CI gates nothing beyond image build + staging boot (canonical statement: `mvp-validation-and-qa` section 1). No doc that says otherwise is correct.
|
||||
|
||||
### The systemic cause — and the rule
|
||||
|
||||
Every stale-dangerous doc above rotted the same way: it froze a count or version as a literal ("15 features", "PostgreSQL 15", "not yet implemented") instead of pointing at the source of truth. Each feature addition then silently invalidated four documents at once. The docs closest to operations (CICD-DEPLOY, LOGGING) stayed honest because they are exercised; the summaries rotted because nothing exercises them.
|
||||
|
||||
**Rule for all new writing:** never embed a count, version number, or implementation-status literal that a normal code change can invalidate. Instead:
|
||||
|
||||
| Instead of writing | Write |
|
||||
|--------------------|-------|
|
||||
| "There are 15 features" | "Feature capsules live in `backend/src/features/` (one directory each; `ls` it for the current set)" |
|
||||
| "PostgreSQL 15" | "PostgreSQL (version pinned in `docker-compose.yml`)" |
|
||||
| "VIN decoding is not yet implemented" | Nothing — implementation status belongs in code and issues, not prose |
|
||||
| "Pro tier allows 5 vehicles" (in a doc that is not the limits' home) | "Vehicle limits per tier: see `VEHICLE_LIMITS` in `backend/src/core/config/feature-tiers.ts`" |
|
||||
|
||||
If a volatile fact MUST appear as a literal (e.g., in this skill), date-stamp it and give a re-verification command (see Provenance section for the pattern).
|
||||
|
||||
## 2. House style
|
||||
|
||||
All of the following are enforced project conventions, verified against existing docs 2026-07-07.
|
||||
|
||||
| Convention | Rule |
|
||||
|------------|------|
|
||||
| No emojis | Anywhere. Ever. Hard project rule. |
|
||||
| Table-first | Prefer Markdown tables over prose for any enumerable content (options, files, endpoints, comparisons). |
|
||||
| Doc file names | SCREAMING-KEBAB in `docs/` (`DATABASE-SCHEMA.md`). The lone violation, `ocr-pipeline-tech-stack.md`, is the outlier — do not copy it. |
|
||||
| Diagrams | ASCII box diagrams, not Mermaid or images. Existing docs mix pure ASCII (`+--+`) and Unicode box-drawing; either is acceptable, be consistent within one doc. |
|
||||
| Cross-links | End substantive docs with a "Related Documentation" footer listing repo-relative paths (pattern in `docs/ARCHITECTURE-OVERVIEW.md`, `docs/VEHICLES-API.md`). |
|
||||
| Per-directory CLAUDE.md | Every directory carries a CLAUDE.md that is a PURE INDEX: the exact three-column table pattern below, no prose, no architecture, no overview sections. |
|
||||
| Feature docs | `backend/src/features/{name}/README.md` is the per-feature record. `docs/` is cross-cutting only. A feature-specific fact belongs in the feature README, full stop. |
|
||||
| Canonical sources | Per CLAUDE.md: `.ai/context.json` (architecture metadata), `docs/README.md` (hub), feature READMEs. Note `.ai/context.json` is itself stale (section 1) — updating it is part of honoring this convention, not optional. |
|
||||
|
||||
CLAUDE.md index pattern (exact):
|
||||
|
||||
```markdown
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
| `cache.ts` | LRU cache with O(1) get/set | Implementing caching, debugging misses |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `api/` | Fastify route handlers | Adding endpoints, changing request flow |
|
||||
```
|
||||
|
||||
Column rules: backticks around names; "What" is a factual noun-phrase; "When to read" is task-oriented with action verbs (implementing, debugging, adding). Quality test: given a task, can a model scan the "When to read" column and pick the right file? Anti-pattern: `| config/ | Configuration | Working with configuration |` — matches everything, selects nothing.
|
||||
|
||||
## 3. Doc-sync discipline
|
||||
|
||||
Carried forward from the retired doc-sync skill; these three rules are the whole discipline.
|
||||
|
||||
### The Invisible Knowledge Test
|
||||
|
||||
Create or keep a README.md ONLY for knowledge not visible from reading the code: non-obvious contracts between components, design tradeoffs that constrain modification, ordering rules the compiler will not enforce, failure modes invisible in any single file.
|
||||
|
||||
For every sentence, ask: "Could a developer learn this by reading the source files?"
|
||||
- YES: delete the sentence.
|
||||
- NO: keep it.
|
||||
|
||||
Do NOT create a README for a purely organizational directory, for self-explanatory code, or to restate what the CLAUDE.md index already conveys. A README earns its tokens by carrying the reasoning behind the code, not descriptions of the code. Keep new READMEs under roughly 500 tokens.
|
||||
|
||||
### Update docs in the same PR
|
||||
|
||||
Any PR that changes behavior, adds/removes/renames files, or alters a contract updates the affected CLAUDE.md indexes, the feature README, and any `docs/` file it invalidates — in that same PR. The stale-dangerous list in section 1 is what "docs later" produces. There is no doc-sync sweep coming to save you; the retired doc-sync skill's periodic-audit model demonstrably did not keep pace.
|
||||
|
||||
Same-PR checklist:
|
||||
- [ ] New/deleted/renamed files reflected in the directory's CLAUDE.md table
|
||||
- [ ] Feature README updated if the feature's contract or invariants changed
|
||||
- [ ] Grep `docs/` for literals your change invalidates: `grep -rn "<old-name-or-claim>" docs/ .ai/`
|
||||
- [ ] No new frozen counts/versions introduced (section 1 rule)
|
||||
|
||||
### CLAUDE.md vs README.md
|
||||
|
||||
| Content | Home |
|
||||
|---------|------|
|
||||
| What files exist, when to open them | CLAUDE.md (pure index) |
|
||||
| Architecture, data flow, component interaction | README.md |
|
||||
| Design decisions and tradeoffs | README.md |
|
||||
| Invariants not enforced by code | README.md |
|
||||
| Build/test commands | Root CLAUDE.md only |
|
||||
| Prose of any kind in a subdirectory CLAUDE.md | Nowhere — migrate to README.md, strip the index |
|
||||
|
||||
## 4. The temporal-contamination rule
|
||||
|
||||
Carried forward from the retired planner skill. Applies to all docs AND code comments.
|
||||
|
||||
> **Timeless Present Rule:** write from the perspective of a reader meeting the code for the first time, with no knowledge of what came before. The code simply IS. Change-narrative comments ("Added X to fix Y") are an LLM artifact — the change process is ephemeral; documentation describes ongoing existence. The change narrative already lives in git history and the issue; duplicating it in a comment is contamination.
|
||||
|
||||
### Detection questions
|
||||
|
||||
Evaluate every comment/sentence against these; signal words are examples, extrapolate semantically:
|
||||
|
||||
1. **Does it describe an action taken rather than what exists?** ("Added", "Changed to", "Now uses", "Updated", "Refactored")
|
||||
2. **Does it compare to something not in the code?** ("Previously", "Instead of", "Replaces", "Unlike the old", "No longer")
|
||||
3. **Does it describe where to put code rather than what code does?** ("After the X call", "Insert before", "At line N") — always delete; location is encoded in the diff.
|
||||
4. **Does it describe intent or futures rather than behavior?** ("TODO later", "Will be extended", "Temporary until") — delete, implement, or reframe as a current constraint.
|
||||
5. **Does it narrate the author's choice rather than the technical reason?** ("Intentionally", "We decided", "Chose X for reliability") — extract the justification, discard the decision narrative.
|
||||
|
||||
Catch-all: if a comment only makes sense to someone who knows the code's history, it is contaminated.
|
||||
|
||||
### Transformations
|
||||
|
||||
| Contaminated | Timeless present |
|
||||
|--------------|------------------|
|
||||
| `// Added mutex to fix race condition` | `// Mutex serializes cache access from concurrent requests` |
|
||||
| `// Previously handled in caller` | `// Encapsulated here; caller does not manage lifecycle` |
|
||||
| `// Temporary workaround until API v2` | `// API v1 lacks filtering; client-side filter required` |
|
||||
| `// We decided to cache at this layer` | `// Cache here: cuts DB round-trips on the hot path` |
|
||||
|
||||
Subtle case: "Now blocks until connection ready" is CLEAN ("now" = runtime moment); "Now handles edge cases properly" is CONTAMINATED ("properly" implies a defective past). Judgment, not keyword matching.
|
||||
|
||||
Transformation recipe: (1) find the useful information buried in the narrative — the problem or the behavior; (2) restate it as a present-tense fact; (3) delete the rest.
|
||||
|
||||
## 5. Writing for AI readers, and maintaining this skill library
|
||||
|
||||
The primary readers of everything you write here are future AI sessions with zero context and a finite window. CLAUDE.md's doctrine: "All development practices should be made taking into account the most context efficient interaction with another AI."
|
||||
|
||||
| Standard | Practice |
|
||||
|----------|----------|
|
||||
| One home per fact | Every fact has exactly one canonical location; every other mention is a pointer to it. The version/feature-count rot in section 1 lives precisely where facts were restated in four places. |
|
||||
| Date-stamp volatile facts | Any literal that a code change can invalidate carries "(verified YYYY-MM-DD)" and, ideally, a re-verification command. |
|
||||
| Trigger-rich descriptions | This library's convention: a skill's frontmatter `description` states exactly WHEN to load it, including symptom keywords a session would actually have in context ("docs say X but code does Y"), not just topic labels. |
|
||||
| Copy-pasteable commands | Repo-relative paths, no placeholders the reader must guess. Remember: no working local dev loop exists — end-to-end verification happens on staging via the PR pipeline, so commands must state where they run. |
|
||||
| Define jargon at first use | Assume a mid-level engineer or Sonnet-class session with zero project context. |
|
||||
|
||||
### Maintenance protocol for this skill library
|
||||
|
||||
All skills live at `.claude/skills/mvp-*/SKILL.md`, YAML frontmatter with exactly `name` and `description`. Most skills are the single SKILL.md file; a skill may additionally carry a `scripts/` directory of read-only checks (`mvp-diagnostics-and-logging` does) - that is legitimate content, not clutter to purge.
|
||||
|
||||
**To update a skill:**
|
||||
1. Verify the new fact against the repo (code, not docs) before writing it.
|
||||
2. Edit the SKILL.md; keep the "When to use / When NOT to use" routing and the "Provenance and maintenance" section current — add a re-verification command for every new volatile fact.
|
||||
3. Apply this skill's own rules to the edit: no frozen counts without date-stamps, timeless present, no emojis.
|
||||
4. Ship the update in the same PR as the change that invalidated it (section 3 rule — skills are docs).
|
||||
|
||||
**When to add an incident to `mvp-failure-archaeology`:** a failure cost more than an hour, produced a revert, revealed a false doc claim that misled a session, or settled a design battle that must not be re-fought. Record symptom, root cause, evidence (file:line or commit), and status. Do not record routine bugs.
|
||||
|
||||
**Skill-directory hygiene:** skill directories are write-once-per-PR artifacts, not scratch space. The previous library shipped four 0-byte junk files (shell-redirection accidents) into git because nobody audited `.claude/skills/` before committing. Check `git status .claude/skills/` before every commit that touches this tree.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct repo inspection at commit e729d42; doc trust assessments reflect that date. Volatile facts and how to re-verify each:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|--------------------------|----------------|
|
||||
| 21 feature capsules | `ls backend/src/features/ \| grep -v CLAUDE.md \| wc -l` |
|
||||
| Vehicle limits free 2 / pro 5 / enterprise unlimited | `grep -A4 "VEHICLE_LIMITS" backend/src/core/config/feature-tiers.ts` |
|
||||
| VIN decode implemented at `/vehicles/decode-vin` | `grep -n "decode-vin" backend/src/features/vehicles/api/vehicles.routes.ts` |
|
||||
| Image versions postgres:18-alpine / redis:8.4-alpine / traefik:v3.6 | `grep -n "postgres:\|redis:\|traefik:" docker-compose.yml` |
|
||||
| Root package.json has no scripts | `python3 -c "import json;print('scripts' in json.load(open('package.json')))"` |
|
||||
| `.ai/context.json` still lists 15 features / VIN "planned" | `grep -n "vin\|VIN" .ai/context.json; python3 -c "import json;print(len(json.load(open('.ai/context.json'))['feature_dependencies']['sequence']))"` |
|
||||
| AUDIT.md Math.random finding is fixed | `grep -n "crypto.randomBytes" backend/src/features/documents/api/documents.controller.ts` |
|
||||
| UUID identity migration exists, undocumented in DATABASE-SCHEMA.md | `head -6 backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sql; grep -c "user_profile_id" docs/DATABASE-SCHEMA.md` |
|
||||
| USER-GUIDE "Up to 10 vehicles" error still present | `grep -n "Up to 10 vehicles" docs/USER-GUIDE.md` |
|
||||
| E2E-TESTING.md still a 9-line fragment | `wc -l docs/E2E-TESTING.md` |
|
||||
| ocr doc still lists Celery; no celery dependency | `grep -in celery docs/ocr-pipeline-tech-stack.md ocr/requirements.txt` |
|
||||
| CI runs zero tests / zero lint | `grep -inwE "test\|lint" .gitea/workflows/staging.yaml` (expect NO output; the word-bound `-w` matters - without it every `:latest` image tag substring-matches "test") |
|
||||
|
||||
If a re-verification shows a doc has been FIXED, move it out of the stale-dangerous table in the same PR — this skill must not itself become a frozen snapshot.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `.claude/skills/mvp-change-control/SKILL.md` — PR workflow the same-PR doc rule plugs into
|
||||
- `.claude/skills/mvp-validation-and-qa/SKILL.md` — evidence standards for claims docs make
|
||||
- `.claude/skills/mvp-failure-archaeology/SKILL.md` — where doc-misled incidents get recorded
|
||||
- `docs/README.md` — the docs hub this skill triages
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
name: mvp-failure-archaeology
|
||||
description: >-
|
||||
Chronicle of every major MotoVaultPro incident, dead end, and revert - symptom, root
|
||||
cause, evidence (commit SHAs, issue numbers), status. Load this skill BEFORE
|
||||
re-investigating any symptom that smells like a past battle, proposing to add caching,
|
||||
"simplifying" a mapper or date handler, or reverting an odd-looking line. Raw symptom
|
||||
triage starts in mvp-debugging-playbook; this is the history behind its entries.
|
||||
Distinguishing triggers - "has this been tried before", "Invalid time value", mobile
|
||||
login redirect loop, Auth0 callback stripped, removeChild/NotFoundError from Google
|
||||
Maps, Gemini/google-genai pydantic validation error, Stripe subscription has no payment
|
||||
method, staging deploy "Take N", "why is there no VIN cache", OCR returns no VIN
|
||||
pattern. Also load before deleting code that looks vestigial - it may be a settled
|
||||
battle.
|
||||
---
|
||||
|
||||
# MotoVaultPro Failure Archaeology
|
||||
|
||||
The project's institutional memory. 706 commits (2025-07-15 to 2026-06-20), restarted three times: "Fresh Start" `2e8816d` (2025-08-07), "Initial Commit" `a052040` (2025-09-17), "Initial Commit" `b611b56` (2025-12-17). The modern, issue-driven era begins 2025-12-17. Every fact below was re-verified against the repo on 2026-07-07.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when:
|
||||
- A symptom matches anything in the chronicle table - check here BEFORE debugging from scratch.
|
||||
- You are about to change code listed under "Settled battles" - the odd-looking line is load-bearing.
|
||||
- You are tempted to add a cache, a `new Date()` call on a DATE column, or an SDK config option to the Gemini path.
|
||||
- You finished a significant investigation - the maintenance rule at the bottom says append it here.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Live triage steps and discriminating experiments - that is `mvp-debugging-playbook` (this skill is the history behind those entries).
|
||||
- The rules and invariants themselves with full rationale - `mvp-architecture-contract`.
|
||||
- How to classify/gate/ship a fix - `mvp-change-control`.
|
||||
- Domain math (VIN check digits, fuel efficiency, DATE semantics theory) - `mvp-vehicle-domain-reference`.
|
||||
- The Gemini/OCR subsystem's current design - `mvp-ocr-gemini-pipeline`.
|
||||
- Evidence standards for calling something "root cause" - `mvp-proof-and-analysis-toolkit`.
|
||||
|
||||
Reading protocol for an AI session: scan the chronicle table for your symptom, read that row's status, then check "Settled battles" before writing any code.
|
||||
|
||||
## 1. Incident chronicle
|
||||
|
||||
Chronological. "Evidence" = commit SHAs and issue numbers you can `git show` yourself.
|
||||
|
||||
| # | Symptom | Root cause | Evidence | Status |
|
||||
|---|---------|-----------|----------|--------|
|
||||
| 1 | Blank Stations page; React crashes (`removeChild`/NotFoundError) from Google Maps (2025-11-04, 8-commit day) | (a) Data fetched before auth initialized; (b) Maps async-loading callback misused; (c) Google Maps mutates DOM nodes React owns | `0e8d8e7`, `f1dd488`, `def2494` (auth polling), `4151f58`, `8e52f3f` (async loading), `715250d`, `9a01ebd` (suppression), `45fea0f` "Finally Working" | Resolved, but the DOM-conflict errors are SUPPRESSED, not eliminated: `frontend/src/App.tsx` (~line 409) has a global `error` + `unhandledrejection` handler that swallows `removeChild`-class messages. If Maps errors "disappear", that handler is why. |
|
||||
| 2 | Prod Traefik/CI broken during GitHub-to-Gitea migration (2025-12-29 to 31) | Configs not checked out on prod; `acme.json` permissions; runner config; blue-green deploy path bug serving stale content | `83d79da`, `eaab8c0`, `dfff25e`, `9b22c5e`, `da3ad51`, `17d868a`, `3464fad` "Traefik won't start. Take 12", `e5837df`, `9c0fafd`, then `13abbc1` + `c57a05d` (same message twice - the FIRST carries the `auto-rollback.sh`/`health-check.sh` fixes plus DEPLOY_PATH/verification changes; the second is a follow-up touching `switch-traffic.sh` and the prod workflow only) | Resolved. `scripts/ci/` blue-green scripts are battle-scarred; treat with respect. See `mvp-run-and-operate`. |
|
||||
| 3 | Fuel Logs API 500 "Invalid time value" (#47, 2026-01-17) | "Enhanced" repo methods returned camelCase `mapRow()` output but the service's `toEnhancedResponse()` expected snake_case raw rows | `574acf3` "return raw rows from enhanced repository methods (refs #47)" | "Fixed" by making enhanced methods return RAW rows - a decision that planted the latent bug fixed four months later as #244 (incident 12). Canonical example of a fix that violates a convention creating future debt. |
|
||||
| 4 | Fuel values display wrong (#49, same day as #47) | node-postgres returns DECIMAL/NUMERIC columns as strings | `5c62b6a` - `Number()` coercion in service layer | Band-aid at the wrong layer. Theme recurred as #239/#241/#243/#244 (incident 12). Rule now: coerce in repository mappers. |
|
||||
| 5 | VIN OCR fails "No VIN pattern found" on all images (#113, 2026-02-06, 12+ commits in one day) | Tesseract preprocessing: wrong channel selection, char whitelist incompatible with LSTM engine, naive first-17-chars trim of noisy text | `6a4c213`, `a07ec32`, `63c027a`, `ae5221c`, `432b3bd`, final `e4336ce` (sliding window + char deletion with check-digit validation) | Resolved, then obsoleted: Tesseract deleted in `b9fe222`. Do not study Tesseract preprocessing - the engine is gone. |
|
||||
| 6 | PaddleOCR migration breakage (#115, PR #122, 2026-02-07) | New engine API differences, timeouts, crop-box regression | `639ca11`, `dab4a3b`, `9d2d4e5`, `fcffb0b`, `3adbb10` "OCR Timout still", `0499c90`, `9a2b12c`, `3c1a090` (crop regression, refs #120) | Resolved; migration completed. Later superseded again by Vision/Gemini path (#127, #129). Every OCR engine swap has cost 5-10 follow-up fix commits. |
|
||||
| 7 | Mobile login redirects to homepage, Auth0 page never shown (#188/#189/#190, 2026-02-14/15, 9 commits) | TWO independent root causes: (a) IndexedDB token cache used `store.getAll()` which returns raw values not key-value entries, so the in-memory cache was always empty after reload; (b) App.tsx URL-sync effect ran `history.replaceState()` before Auth0Provider's effect, stripping `?code=&state=` from `/callback` | Symptom-chasing chain `a195fa9`, `6e493e9`, `38debaa`, `b5b82db`; root cause (a) `da59168`; root cause (b) `850f713` | Resolved. The guard survives at `frontend/src/App.tsx` lines ~374/386: URL-sync effects early-return on `/callback`, `/signup`, `/verify-email`, `/guide`, `/guide/`. Settled battle - see section 2. |
|
||||
| 8 | Stripe live wiring failures (2026-02-15/16) | Wrong price/product IDs (live vs sandbox), payment method never attached to customer, subscription period dates read from the wrong object level | `ddae397`, `c1e8807`, `bc0be75`, `15956a8` "take 2", `07c3d85` "take 3", then correct fixes `72e5573` (attach payment method first), `8a73352` (charge immediately, item-level period dates); properly refactored via #205/#209/#210 in `93e79d1` (`ensureStripeCustomer`) | Resolved via refactor after take-3 flailing. `ensureStripeCustomer` lives in `backend/src/features/subscriptions/domain/subscriptions.service.ts` (~line 174). |
|
||||
| 9 | VIN decode saga: NHTSA replaced by Gemini, then reliability chase (#223-#230, 2026-02-18 to 02-28) | NHTSA decode replaced with Gemini via the OCR service (`a75f7b5`, `3cd6125`, `5cbf9c7`, `f590421`, `d967367`, all 2026-02-18); then Gemini cold-start timeouts, a VIN-cache race + fuzzy-match misses, and wrong model years | `3b5b847` (timeout to 60s, refs #229/#230), `361f58d` (cache race, refs #229), `7d90f4b` (year-code table in prompt), then `283ba6b` (2026-02-20) "Remove VIN Cache" - 93 lines deleted, cache NOT fixed - with a final year-fix straggler `0055d9f` (2026-02-28) | Settled: there is NO VIN decode cache, by design. Note the `vin_cache` TABLE still exists in `backend/src/features/vehicles/migrations/001_create_vehicles_tables.sql` and the vehicles README still claims caching - both are stale; the code (no cache reads/writes) wins. |
|
||||
| 10 | google-genai SDK migration breaks VIN decode on staging (#231-#236, 2026-02-28 to 03-01) | Vertex AI SDK replaced by `google-genai` (`398d673`, `b7f472b`, `9f51e62`, `1464a0e`); then `AutomaticFunctionCallingConfig(max_remote_calls=3)` passed to a google-genai version that rejects it with a pydantic validation error | `936753f` (timeouts + logic; raised frontend VIN timeout 60s to 120s), `1add6c8` (remove param), `56df5d4` (revert AFC entirely + diagnostic logging) | Resolved. AFC config is confirmed absent from `ocr/app/engines/gemini_engine.py` (2026-07-07). Migration complete; Vertex AI SDK gone. |
|
||||
| 11 | Dates off by one day (#237, 2026-03-23 - four fixes in ONE day) | Three distinct UTC traps: (a) pg parses DATE columns to local-midnight `Date`, then `toISOString()` shifts a day; (b) frontend `new Date("YYYY-MM-DD")` parses as UTC midnight, `toLocaleDateString()` shifts back; (c) OCR parser used `toISOString().split('T')[0]` | `c05e33e` and `f0fc427` (both "fix: Date picker bug"; `f0fc427` added `types.setTypeParser(1082, val => val)` to `backend/src/core/config/database.ts`), `1e056f0` (dayjs for display/sort, refs #237), `087f7b9` (OCR parser local-time formatting, refs #237) | All three fixed. Settled: DATE columns flow as plain `YYYY-MM-DD` strings end-to-end; display via dayjs; sort lexicographically. See section 2 and `mvp-vehicle-domain-reference`. |
|
||||
| 12 | Maintenance cost blank on vehicle summary; pg-numeric bug train returns (#239, #241, #243, #244, 2026-05-15/16) | Same root cause as #49 four months earlier: pg returns NUMERIC as strings and mappers were inconsistent. The audit found three features affected. #244 specifically: fuel-logs "enhanced" methods had returned raw pg rows since `574acf3` (incident 3) | `a49f419` + `55b8b67` (refs #239), `fdc34ae` (refs #241 - coerce in maintenance + ownership-costs mappers, revert frontend `Number()` workarounds), `0d90829` (refs #244 - fuel-logs enhanced methods finally coerce). #243 (stations prices) has no dedicated fix commit; stations mappers already coerce via `parseFloat` (verified 2026-07-07) | Resolved, with the architectural rule now in root `CLAUDE.md`: repository mappers coerce numerics; never return raw rows to the API layer. |
|
||||
| 13 | UUID identity migration and its fallout (#206 + subs #211-#217, then #220, 2026-02-16/17) | `auth0_sub` VARCHAR identity replaced by `user_profiles.id` UUID across 17 features; big-bang migration left stragglers | Migration fixes `7fc80ab`, `28165e4` (refs #206); fallout batch: `dd3b58e` (remaining controllers), `e909313` (remaining `auth0_sub` references), both refs #220 | Completed after two mop-up commits. Lesson: a cross-cutting identity migration WILL leave stragglers; grep for the old identifier after "done". |
|
||||
|
||||
## 2. Settled battles - DO NOT RE-FIGHT
|
||||
|
||||
Each of these looks like a candidate for "cleanup". Each one re-fights an incident above. Verify the evidence before touching any of them.
|
||||
|
||||
| Rule | Evidence | If you break it |
|
||||
|------|----------|-----------------|
|
||||
| Keep `types.setTypeParser(1082, (val: string) => val)` at `backend/src/core/config/database.ts` line 12 | `f0fc427`, incident 11 | Every DATE column shifts one day for users west of UTC |
|
||||
| Repository mappers MUST coerce NUMERIC/DECIMAL columns (`Number()` or `parseFloat`); never return raw pg rows to services/API | `574acf3` created the debt; `fdc34ae`, `0d90829` paid it (incidents 3, 4, 12) | Strings leak into arithmetic; NaN/blank UI four months later |
|
||||
| NO VIN decode cache. Deleted deliberately, not lost | `283ba6b` (incident 9). Stale remnants: `vin_cache` table in migration 001 and the vehicles README caching claim - docs/schema are stale, code wins | You re-inherit the cache race + fuzzy-match wrong-vehicle bugs `361f58d` fought |
|
||||
| NO `AutomaticFunctionCallingConfig` (or `max_remote_calls`) on google-genai calls in `ocr/app/engines/gemini_engine.py` | `1add6c8`, `56df5d4` (incident 10) | Pydantic validation error; VIN decode dies on staging |
|
||||
| App.tsx URL-sync effects must early-return on `/callback`, `/signup`, `/verify-email`, `/guide`, `/guide/` (lines ~374, ~386) | `850f713` (incident 7), `/guide` added via #203 | `history.replaceState()` strips Auth0 `?code=&state=`; login breaks, worst on mobile |
|
||||
| VIN decode frontend timeout is 120s (`timeout: 120000` in `frontend/src/features/vehicles/api/vehicles.api.ts` line ~90) | Raised 60s to 120s in `936753f` for Gemini + Search grounding. The "60s" figure floats around in older notes - it is STALE | Gemini cold start + grounding exceeds 60s; decode aborts client-side while server still working |
|
||||
| Stripe: attach payment method to customer BEFORE creating subscription; charge immediately; read period dates from the subscription ITEM level; go through `ensureStripeCustomer` | `72e5573`, `8a73352`, `93e79d1` (incident 8) | Subscriptions with no payment method; wrong renewal dates; the take-2/take-3 flailing repeats |
|
||||
| Log Fuel field order and decimal keypad are deliberate UX (#246, `2221819`, 2026-06-19) | Most-used fields first on mobile; `inputMode` decimal keypad | "Alphabetizing" or "grouping logically" reverts a shipped UX decision |
|
||||
| nginx `try_files $uri /index.html;` in `frontend/nginx.conf` deliberately omits `$uri/` | `4927b66` (refs #203): `$uri/` matched the static `/guide/` screenshot directory and 301-redirected, bypassing SPA routing | `/guide` route breaks with a trailing-slash redirect loop |
|
||||
| App.tsx global error handler suppressing `removeChild`-class Google Maps errors stays | `715250d`, `9a01ebd` (incident 1) | Users see crash overlays for harmless Maps DOM churn. But be honest in reviews: errors are suppressed, not fixed |
|
||||
|
||||
## 3. Dead ends and abandoned arcs
|
||||
|
||||
Do not resurrect these; do not spend time investigating their remnants.
|
||||
|
||||
- **Kubernetes**: a k8s-style redesign was pursued mid-2025 (`040da4c` "k8s redesign complete", `17d27f4`) - in reality a "Kubernetes-like Docker Compose" restructure; no k8s manifests were ever committed (see `mvp-architecture-contract` 1.1) - and abandoned by the 2025-12-17 restart. The project is docker-compose only. Any k8s reference you find in old history is dead.
|
||||
- **Legacy vehicle ETL**: removed `bfb0c23` (2025-12-27) after a "Possible working ETL" dead end. Gone on purpose.
|
||||
- **Per-vehicle TCO fields**: removed from vehicle forms in #37 (`9644010`, merged `913e084`, 2026-01-15). The `ownership-costs` capsule SURVIVES as its own feature - the removal was of duplicate per-vehicle fields, not the concept. Do not add TCO fields back to vehicle forms.
|
||||
- **Local branch `list`**: an accident (points at `2221819`, identical to the merged `issue-246-reorder-log-fuel-fields` tip; likely a mistyped `git branch list`). Safe to delete; carries no work.
|
||||
- **Committed backup tarball**: `cd7f8f5` (2026-03-22, "fix: Missing packages") accidentally committed `motovaultpro_backup_2026-03-22T15-00-02.tar.gz` (9,570,741 bytes); `068bb75` (same message) removed it. The blob still bloats git history. Do not "fix" this with a history rewrite without owner sign-off - it would invalidate every clone and open PR.
|
||||
- **Stale GitHub remote**: `github/main` is frozen (last activity 2026-02-04); `github/issue-37-remove-tco-fields` was merged and never pruned. Gitea (`gitea/main`) is the only live remote.
|
||||
|
||||
## 4. Recurring themes, ranked by total cost
|
||||
|
||||
Recognize the family on contact - the third occurrence of a theme should take minutes, not a day.
|
||||
|
||||
1. **pg type-contract drift (NUMERIC-as-string, raw rows)** - 5+ incidents over 4 months (#47, #49, #239, #241, #243, #244). Highest total cost because incident 3's fix CREATED incident 12. Coercion styles are still hand-rolled per repository (`Number()` vs `parseFloat`, no shared helper) - the pattern can recur in any NEW repository method. On contact: check the mapper first.
|
||||
2. **UTC/timezone date shifting** - dayjs picker migration (`0e85cf4`, 2025-12-18), then four fixes in one day (incident 11). On contact with any off-by-one-day report: it is one of the three traps in incident 11; do not invent a fourth theory first.
|
||||
3. **OCR/AI engine churn** - Tesseract to PaddleOCR to Vision/Gemini to google-genai SDK (incidents 5, 6, 9, 10). Each swap cost 5-10 follow-up fixes but each was COMPLETED with old code deleted. Budget the follow-up tail into any future engine/SDK change proposal.
|
||||
4. **CI/CD deployment fragility** - "Take 12" Traefik saga, blue-green path bug committed twice (incident 2). Mostly pre-2026; the remaining structural risk (zero test gates, `:latest` clobbering) is the subject of `mvp-deploy-safety-campaign`.
|
||||
5. **Auth-state races** - blank Stations page (incident 1), mobile login (incident 7): effects and fetches racing auth initialization. On contact with any blank-page-after-login or callback symptom: check effect ordering against Auth0Provider before anything else.
|
||||
6. **Big-bang migration stragglers** - UUID identity (incident 13), google-genai (incident 10): the migration "completes", then 1-2 mop-up commits chase remaining references. After any cross-cutting rename: grep for the old symbol before declaring done.
|
||||
|
||||
## Maintenance rule
|
||||
|
||||
Every new SIGNIFICANT investigation (multi-commit debugging chain, root-cause reversal, deliberate deletion, migration fallout) gets appended to this chronicle AT PR TIME, in the same row format: symptom, root cause, evidence (SHAs + issue numbers), status. If a fix overturns a settled battle above, update that row in the same PR - do not leave this file contradicting the code. Where this file and the code disagree, the code wins; fix this file.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 by direct repo inspection (git log/show, file reads); issue titles cross-checked against the Gitea tracker (zero open issues as of 2026-07-07). Volatile facts and their re-verification commands:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| 706 commits; three restarts (2e8816d, a052040, b611b56) | `git rev-list --count HEAD` and `git log --oneline --grep='Initial Commit\|Fresh Start' -i` |
|
||||
| `setTypeParser(1082, ...)` present at database.ts line 12 | `grep -n "setTypeParser" backend/src/core/config/database.ts` |
|
||||
| VIN frontend timeout 120000 | `grep -n "timeout" frontend/src/features/vehicles/api/vehicles.api.ts` |
|
||||
| No AFC config in gemini_engine.py | `grep -in "AutomaticFunctionCalling\|max_remote_calls" ocr/app/engines/gemini_engine.py` (expect no hits) |
|
||||
| App.tsx callback-skip guard and Maps error suppression | `grep -n "callback\|removeChild" frontend/src/App.tsx` |
|
||||
| No VIN cache in code; stale `vin_cache` remnants in migration 001 + vehicles README | `grep -rn "vin_cache" backend/src/` |
|
||||
| nginx try_files omits `$uri/` | `grep -n "try_files" frontend/nginx.conf` |
|
||||
| `ensureStripeCustomer` in subscriptions.service.ts | `grep -rn "ensureStripeCustomer" backend/src/features/subscriptions/` |
|
||||
| Stations mappers coerce with parseFloat (why #243 has no fix commit) | `grep -n "parseFloat" backend/src/features/stations/data/*.repository.ts` |
|
||||
| Local branch `list` still exists and equals `2221819` | `git rev-parse list 2>/dev/null` |
|
||||
| Tarball blob still in history (9,570,741 bytes) | `git show --stat cd7f8f5 \| grep tar.gz` |
|
||||
| Any commit SHA or message quoted above | `git log -1 --format='%h %ad %s' --date=short <sha>` |
|
||||
@@ -1,228 +0,0 @@
|
||||
---
|
||||
name: mvp-launch-readiness
|
||||
description: >-
|
||||
Load when assessing whether MotoVaultPro is ready to launch, take paying users, or make
|
||||
a public claim ("is this production ready", "can we ship", "launch checklist", "is
|
||||
billing working", "is this secure enough", "what is blocking launch", "can we say X is
|
||||
supported"). Also load before writing any user-facing statement about supported
|
||||
features, security posture, or reliability, and when triaging the known launch blockers
|
||||
- Stripe upgrade dead-end, Resend webhook secret, backup emails, fuel-log cache bug,
|
||||
Traefik/Grafana default credentials, stale AUDIT.md.
|
||||
---
|
||||
|
||||
# MVP Launch Readiness
|
||||
|
||||
External positioning for a PRE-LAUNCH product heading toward paying users. This skill
|
||||
defines what must be PROVEN before claiming readiness or shipping to humans, and lists
|
||||
the launch blockers with evidence. Its purpose is to prevent claiming readiness by eye.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when:
|
||||
- Deciding whether a feature can be called "supported" in any user-facing or public claim.
|
||||
- Building or reviewing a launch checklist, readiness report, or go/no-go decision.
|
||||
- Triaging any of the launch-blocking gaps listed below.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Fixing the CI gate hole itself — that is `mvp-deploy-safety-campaign` (the executable campaign).
|
||||
- How to deploy, roll back, or operate environments — `mvp-run-and-operate`.
|
||||
- What counts as test evidence and how to run suites — `mvp-validation-and-qa`.
|
||||
- Secrets/config mechanics (adding a secret, YAML config loader) — `mvp-config-and-secrets`.
|
||||
- Doc trust levels and doc-sync — `mvp-docs-and-writing`.
|
||||
- Issue/branch/PR mechanics for fixing any blocker — `mvp-change-control`.
|
||||
|
||||
## Verdict labels (use these exact words)
|
||||
|
||||
| Label | Meaning |
|
||||
|---|---|
|
||||
| PROVEN | Verified against code/config on 2026-07-07, with a test or a staging-verified procedure behind it |
|
||||
| UNPROVEN | Implemented in code but never verified end-to-end; no test, no recorded staging run |
|
||||
| BROKEN | Verified defective: the code path exists and does the wrong thing (or nothing) |
|
||||
|
||||
A claim without a label is not a claim. "It compiles" and "staging boots" are not evidence:
|
||||
CI gates nothing beyond build + boot (canonical statement: `mvp-validation-and-qa` section 1).
|
||||
|
||||
## 1. Launch-blocking gaps (each verified 2026-07-07 — re-verify before acting)
|
||||
|
||||
| # | Gap | Verdict | Evidence |
|
||||
|---|---|---|---|
|
||||
| 1 | Upgrade path from tier-gated features dead-ends: the Upgrade button in `UpgradeRequiredDialog` just closes the dialog (`// TODO: Navigate to upgrade page`) | BROKEN | `frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx:55-58`. Nuance: a working checkout UI exists at route `/garage/settings/subscription` (`frontend/src/App.tsx:1175`), so users who find Settings can upgrade — but the upsell moment (hitting a gated feature) converts to nothing. Fix is a navigate call, not a Stripe build-out. |
|
||||
| 2 | Resend inbound email webhook secret is unconfigured in every environment: `resend-webhook-secret` is absent from `secrets/app/`, from the backend secret mounts in `docker-compose.yml:133-141`, and from the 12-file list in `scripts/inject-secrets.sh:30-43` | BROKEN (feature), not open (security) | `POST /api/webhooks/resend/inbound` is a PUBLIC endpoint (no JWT — `backend/src/features/email-ingestion/api/email-ingestion.routes.ts:13`). It fails CLOSED: `verifyWebhookSignature` throws when `RESEND_WEBHOOK_SECRET` is unset (`resend-inbound.client.ts:39`) and the handler returns 400 (`email-ingestion.controller.ts:141-146`). So no unsigned request is accepted — but email ingestion cannot work at all until the secret is provisioned end-to-end (secret file + compose mount + inject-secrets + Gitea secret). Config schema already treats it as optional (`backend/src/core/config/config-loader.ts:121,185`). |
|
||||
| 3 | Backup failure emails are never sent — the notification path checks settings and the admin email, then only logs "Backup notification would be sent" | BROKEN | `backend/src/features/backup/domain/backup.service.ts:441` (`TODO: Integrate with email service`). The settings UI implies emails that do not exist. A paying user's backup can fail silently. |
|
||||
| 4 | Fuel-log delete invalidates only the imperial cache: `deleteFuelLog` hardcodes `'imperial'` and `invalidateCaches` deletes only that unit-system's two keys | BROKEN | `backend/src/features/fuel-logs/domain/fuel-logs.service.ts:223` and `:266-271`. The inline comment "simple sweep below" is false — there is no sweep. Metric-unit users see deleted logs for up to the cache TTL. |
|
||||
| 5 | Traefik dashboard on prod has placeholder auth: basicauth hash is `admin:$2y$10$foobar` (not a valid bcrypt digest), while `api.insecure: true` with port 8080 published in the base compose | BROKEN (relies on firewall only) | `docker-compose.prod.yml:23`; `config/traefik/traefik.yml:1-4`. Generate a real htpasswd bcrypt hash before launch. |
|
||||
| 6 | Grafana admin password defaults to `admin` unless the `GRAFANA_ADMIN_PASSWORD` Gitea secret is set | UNPROVEN (mitigated) | `docker-compose.yml:350` (`${GRAFANA_ADMIN_PASSWORD:-admin}`). Mitigation exists: `grafana-ipwhitelist` middleware allows only RFC1918 source ranges (`config/traefik/dynamic/grafana.yml:3-5`), so it is not internet-reachable — but confirm the Gitea secret is actually set on staging and prod before launch. |
|
||||
| 7 | CI gates nothing: no tests, no lint, no security scan on any PR; PR builds clobber `:latest` and the prod deploy input defaults to `latest`, so prod can ship an unmerged PR build | BROKEN (process) | `.gitea/workflows/staging.yaml` (PR-triggered, pushes `:latest`), `.gitea/workflows/production.yaml` (`image_tag` default `latest`). Do not fix here — route to `mvp-deploy-safety-campaign`. Launch gate: prod deploys by SHA, and at least one real test job on PR. |
|
||||
| 8 | No account-deletion / data-retention policy document, despite the feature being live: a 30-day grace period is implemented (`backend/src/features/user-profile/jobs/account-purge.job.ts`) and mentioned once in `docs/USER-GUIDE.md:613` | UNPROVEN (code exists, policy does not) | Paying users require a stated retention policy. `docs/AUDIT.md` flagged this MEDIUM in 2025-12 (AUDIT.md:303); still no doc. Write the policy doc; verify the purge job's behavior matches it before publishing. |
|
||||
| 9 | `docs/AUDIT.md` (dated 2025-12-13, verdict "CONDITIONALLY READY") was never updated after remediation, so the real open findings are unknowable from the doc | BROKEN (as a record) | Several findings are demonstrably fixed in code (see section 3) while the doc presents them as open. Do a remediation-status pass: add a Status column (FIXED with file:line / OPEN / SUPERSEDED) so a reader can trust it. Until then, treat AUDIT.md as an archive, never as the current posture. |
|
||||
|
||||
## 2. Claim discipline
|
||||
|
||||
Rule: nothing is "supported" until it has (a) a verified runbook in the repo and (b) a test
|
||||
or a staging-verified procedure. "The code exists" is UNPROVEN. When docs contradict code,
|
||||
code wins — say so in the claim.
|
||||
|
||||
### Billing claims specifically
|
||||
|
||||
Do not claim billing works until the FULL cycle has been exercised on staging with Stripe
|
||||
test mode: checkout -> webhook -> tier sync -> downgrade. Current status, checked 2026-07-07:
|
||||
|
||||
- Status: UNPROVEN. The subscriptions capsule (`backend/src/features/subscriptions/`) has
|
||||
ZERO test files (`find backend/src/features/subscriptions -name "*.test.ts"` returns
|
||||
nothing). Git history shows real-world debugging fixes on 2026-02-16 ("charge immediately
|
||||
on subscription", "attach payment method to customer" — commits `8a73352`, `72e5573`),
|
||||
which implies the flow was manually exercised around then, but there is no test and no
|
||||
recorded staging verification of the full cycle since. Honest label: implemented,
|
||||
manually poked in Feb 2026, never verified end-to-end as a procedure.
|
||||
|
||||
How to verify it (the moving parts):
|
||||
|
||||
1. Price IDs: four env vars map plan -> Stripe price and back:
|
||||
`STRIPE_PRO_MONTHLY_PRICE_ID`, `STRIPE_PRO_YEARLY_PRICE_ID`,
|
||||
`STRIPE_ENTERPRISE_MONTHLY_PRICE_ID`, `STRIPE_ENTERPRISE_YEARLY_PRICE_ID`.
|
||||
Sandbox defaults are baked into `docker-compose.yml:124-127` and `.env.example:12-15`;
|
||||
staging/prod override via `.env` generated from Gitea variables. Forward mapping in
|
||||
`getPriceId`, reverse tier inference in `determineTierFromStripeSubscription`
|
||||
(`backend/src/features/subscriptions/domain/subscriptions.service.ts:831-900`). An
|
||||
unknown price ID silently maps to tier `free` — a misconfigured price ID downgrades
|
||||
paying users without error.
|
||||
2. Secrets: `stripe-secret-key` and `stripe-webhook-secret` are file secrets read by
|
||||
`getStripeConfig()` (`backend/src/core/config/config-loader.ts:258-263`), mounted at
|
||||
`docker-compose.yml:140-141`, injected on deploy by `scripts/inject-secrets.sh`.
|
||||
3. Endpoints: `POST /api/subscriptions/checkout` (authenticated,
|
||||
`subscriptions.routes.ts:28-29`) and `POST /api/webhooks/stripe` (PUBLIC, signature-
|
||||
verified, `webhooks.controller.ts:24`). Tier lands in `user_profiles.subscription_tier`
|
||||
via `syncTierToUserProfile` and gates features through
|
||||
`backend/src/core/config/feature-tiers.ts` (limits and key catalog canonical in
|
||||
`mvp-config-and-secrets` section 3; unknown feature keys FAIL OPEN).
|
||||
4. Staging procedure: on staging with Stripe TEST-mode keys in the Gitea secrets, drive
|
||||
checkout from `/garage/settings/subscription` with card 4242 4242 4242 4242; confirm
|
||||
the webhook fired (`{container="mvp-backend-staging"} | json` in Grafana, and the
|
||||
`subscription_events` table); confirm `user_profiles.subscription_tier` flipped; then
|
||||
cancel/downgrade and confirm the tier reverts and the vehicle-selection flow triggers
|
||||
when over the free limit. The Stripe webhook endpoint for staging must be registered
|
||||
in the Stripe dashboard pointing at `https://staging.motovaultpro.com/api/webhooks/stripe`
|
||||
and its signing secret stored as the `STRIPE_WEBHOOK_SECRET` Gitea secret. Record the
|
||||
run (date, commit SHA, screenshots or log queries) in the issue that tracks the claim.
|
||||
|
||||
Remember owner non-negotiables while doing this: no destructive DB operations without a
|
||||
fresh backup, and never hand-edit files on staging/prod servers (rsync --delete reverts
|
||||
them; all changes flow through the repo and workflows).
|
||||
|
||||
## 3. Security posture — the honest list (verified 2026-07-07)
|
||||
|
||||
What IS solid (PROVEN in code):
|
||||
|
||||
| Item | Evidence |
|
||||
|---|---|
|
||||
| Auth0 JWT validation: JWKS-based verification with issuer and audience pinning | `backend/src/core/plugins/auth.plugin.ts:105-108` (`allowedIss`, `allowedAud`) |
|
||||
| Network isolation: database network is `internal: true`; prod removes postgres/redis host ports | `docker-compose.yml:405-407`; `docker-compose.prod.yml` sets `ports: []` |
|
||||
| File-based secrets, never env, never committed (only `.example` siblings; `google-wif-config.json` is deliberately committed WIF config) | `backend/src/core/config/config-loader.ts:175-188`; `scripts/inject-secrets.sh` |
|
||||
| Upload magic-byte validation: content vs claimed MIME type checked before accepting documents | `backend/src/features/documents/api/documents.controller.ts:253-285` |
|
||||
| Storage keys from `crypto.randomBytes(32)` (the AUDIT.md CRITICAL, since fixed) | `backend/src/features/documents/api/documents.controller.ts:587` |
|
||||
| Stripe and Resend webhooks verify signatures and fail closed | `webhooks.controller.ts:36-46`; `resend-inbound.client.ts:37-52` |
|
||||
| Audit-log capsule and backend ESLint exist (both were AUDIT.md gaps) | `backend/src/features/audit-log/`; `backend/eslint.config.js` |
|
||||
|
||||
Open or unknown (do not claim these):
|
||||
|
||||
| Item | Status | Truth |
|
||||
|---|---|---|
|
||||
| Content-Security-Policy on the served frontend | OPEN | `frontend/nginx.conf` contains zero `add_header` directives — the SPA is served with no CSP. Backend registers `@fastify/helmet` with defaults (`backend/src/app.ts:49`), which covers API responses only. AUDIT.md finding still open. |
|
||||
| Google Maps API key exposure | OPEN (by design, unmitigated) | The key is injected at container startup into `window.CONFIG` served to every browser (`frontend/scripts/load-config.sh` -> `/usr/share/nginx/html/config.js`). Browser-side Maps keys are normal ONLY if referrer/API restrictions are set on the key in Google Cloud Console — that restriction is not verifiable from this repo. UNVERIFIED until checked in the console. AUDIT.md also flags key leakage via photo URLs. |
|
||||
| PostgreSQL connection encryption | RESOLVED CONTRADICTION — connections are UNENCRYPTED | `docs/ARCHITECTURE-OVERVIEW.md:511` claims "PostgreSQL SSL/TLS"; `docs/SECURITY.md:30` says unencrypted, network-isolated. Code wins: no `ssl` option anywhere in `backend/src/core/config/database.ts` or the `database:` block of `config/app/production.yml:12-18`. SECURITY.md is correct. Acceptable for a single-host internal-network deployment; never claim encrypted-in-transit DB. |
|
||||
| Tier guard fail-open | OPEN (deliberate, document it) | `canAccessFeature` returns true for unknown feature keys (`backend/src/core/config/feature-tiers.ts`). A typo in a feature key silently un-gates a paid feature. |
|
||||
| Admin guard typing in backup routes | OPEN | `backend/src/features/backup/api/backup.routes.ts` uses `(fastify as any).requireAdmin` 16 times — a typo compiles clean and would ship an unguarded admin endpoint. |
|
||||
|
||||
## 4. Reproducibility standard for launch
|
||||
|
||||
Launch bar: a new machine must be able to stand up the product from the repo plus Gitea
|
||||
secrets alone. No snowflake state on any server.
|
||||
|
||||
- Docs to trust: `docs/BUILD-SERVER-SETUP.md` (VPS + act_runner provisioning) and
|
||||
`docs/CICD-DEPLOY.md` (deploy pipeline — the most code-accurate infra doc). Known
|
||||
defects in both, verified 2026-07-07:
|
||||
- BUILD-SERVER-SETUP.md installs act_runner v0.2.13 (line 84) but its "Update Runner"
|
||||
section downloads v0.2.12 (line 247) — following the update procedure downgrades.
|
||||
- CICD-DEPLOY.md says runner labels `mvp-build`/`mvp-prod` (lines 34, 59-60); actual
|
||||
labels are `stage`/`prod` (`.gitea/workflows/staging.yaml:29`, `production.yaml:34`).
|
||||
- Ansible caveat (the prune-cron gap): the staging runner's daily Docker prune cron was
|
||||
installed out-of-band and is deliberately NOT in the repo; worse,
|
||||
`ansible/deploy-staging-runner.yml:317-324` REMOVES a legacy "Docker cleanup" cron.
|
||||
Rebuilding the staging box from ansible silently drops disk hygiene, and the 29G root
|
||||
disk then fills from per-commit image builds. This is the one standing exception to
|
||||
"everything flows through the repo" — a rebuilt box needs the prune cron reinstalled
|
||||
manually until it is codified.
|
||||
- Everything on-server is disposable by design: `scripts/inject-secrets.sh` regenerates
|
||||
all 12 secret files from Gitea Actions secrets on every deploy, and rsync `--delete`
|
||||
replaces `config/` and `scripts/`. If standing up a new box requires copying anything
|
||||
from an old box other than data volumes, that is a launch-blocking reproducibility bug.
|
||||
- Trap: bind-mounting a missing secret file makes Docker create a DIRECTORY at that path;
|
||||
a new machine hitting "secret is a directory" errors is this failure mode (canonical
|
||||
description and fix: `mvp-config-and-secrets` section 2).
|
||||
|
||||
## 5. Ranked pre-launch checklist
|
||||
|
||||
Run every evidence command from the repo root. A row is done when the command output
|
||||
contradicts the gap, not when a PR merged.
|
||||
|
||||
### Blocking (no paying users until green)
|
||||
|
||||
| Item | Evidence command |
|
||||
|---|---|
|
||||
| Wire UpgradeRequiredDialog to `/garage/settings/subscription` | `grep -n "TODO: Navigate" frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx` (must return nothing) |
|
||||
| Full billing cycle verified on staging, Stripe test mode, recorded in an issue | See section 2 procedure; `find backend/src/features/subscriptions -name "*.test.ts"` should also stop returning empty |
|
||||
| Real bcrypt hash on prod Traefik dashboard; kill `api.insecure` or unpublish 8080 | `grep -n "foobar" docker-compose.prod.yml` (nothing); `grep -n "insecure" config/traefik/traefik.yml` |
|
||||
| Backup failure emails actually send | `grep -n "would be sent" backend/src/features/backup/domain/backup.service.ts` (nothing) |
|
||||
| CI runs at least one real test job; prod deploys pinned by SHA (route to `mvp-deploy-safety-campaign`) | `grep -n "npm test\|jest\|pytest" .gitea/workflows/*.yaml` (currently nothing); `grep -n "default: 'latest'" .gitea/workflows/production.yaml` |
|
||||
| Account-deletion / data-retention policy doc exists and matches `account-purge.job.ts` behavior | `ls docs/ \| grep -i -E "retention\|deletion"` (currently nothing) |
|
||||
| GRAFANA_ADMIN_PASSWORD Gitea secret set for staging and prod | Check Gitea repo secrets; on server: `docker exec mvp-grafana env \| grep GF_SECURITY` must not show `admin` |
|
||||
|
||||
### Important (fix before public claims, not necessarily before first user)
|
||||
|
||||
| Item | Evidence command |
|
||||
|---|---|
|
||||
| Fix fuel-log delete cache invalidation for both unit systems | `grep -n "'imperial'" backend/src/features/fuel-logs/domain/fuel-logs.service.ts` (line 223 hardcode gone) |
|
||||
| Provision `resend-webhook-secret` end-to-end or disable/hide email ingestion | `grep -n "resend-webhook-secret" scripts/inject-secrets.sh docker-compose.yml` (currently no hits in either) |
|
||||
| Remediation-status pass on `docs/AUDIT.md` | `grep -n "Status" docs/AUDIT.md` shows a per-finding FIXED/OPEN column |
|
||||
| CSP header on the frontend (nginx or Traefik middleware) | `grep -n "add_header" frontend/nginx.conf` or `grep -rn "Content-Security" config/traefik/` |
|
||||
| Verify Google Maps key has referrer restrictions | Google Cloud Console (not verifiable from repo — record the check in an issue) |
|
||||
| Type the backup routes' admin guard | `grep -c "as any).requireAdmin" backend/src/features/backup/api/backup.routes.ts` returns 0 |
|
||||
| Codify the staging-runner prune cron in ansible | `grep -n "prune" ansible/deploy-staging-runner.yml` shows an install task, not just removal of the legacy cron |
|
||||
|
||||
### Nice-to-have
|
||||
|
||||
| Item | Evidence command |
|
||||
|---|---|
|
||||
| Tier guard fail-closed (or an explicit allowlist test) | `grep -n "fail open\|return true" backend/src/core/config/feature-tiers.ts` |
|
||||
| Fix BUILD-SERVER-SETUP.md v0.2.12/v0.2.13 and CICD-DEPLOY.md label drift | `grep -n "0.2.12" docs/BUILD-SERVER-SETUP.md; grep -n "mvp-build" docs/CICD-DEPLOY.md` |
|
||||
| Silent-failure TODOs: fuel-log delete error toast, stations pull-to-refresh no-op | `grep -rn "TODO" frontend/src/features/fuel-logs/components/FuelLogsList.tsx frontend/src/features/stations/mobile/StationsMobileScreen.tsx` |
|
||||
| PostgreSQL in-transit encryption (only if topology ever spans hosts) | `grep -n "ssl" backend/src/core/config/database.ts config/app/production.yml` |
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct repo inspection at commit `e729d42`-era main. Every
|
||||
file:line above was verified on that date. Volatile facts and their re-verification
|
||||
commands:
|
||||
|
||||
- UpgradeRequiredDialog dead-end: `grep -n "TODO: Navigate" frontend/src/shared-minimal/components/UpgradeRequiredDialog.tsx`
|
||||
- Resend webhook secret absent from deploy path: `grep -n "resend-webhook-secret" scripts/inject-secrets.sh docker-compose.yml secrets/app/ -r`
|
||||
- Backup email log-only: `grep -n "would be sent" backend/src/features/backup/domain/backup.service.ts`
|
||||
- Fuel-log cache hardcode: `grep -n "invalidateCaches" backend/src/features/fuel-logs/domain/fuel-logs.service.ts`
|
||||
- Traefik placeholder auth: `grep -n "foobar" docker-compose.prod.yml && grep -n "insecure" config/traefik/traefik.yml`
|
||||
- Grafana default password: `grep -n "GRAFANA_ADMIN_PASSWORD" docker-compose.yml`
|
||||
- CI runs no tests: `grep -n "npm test\|jest\|pytest" .gitea/workflows/*.yaml` (expect no hits until fixed)
|
||||
- Prod deploy default tag: `grep -n "default: 'latest'" .gitea/workflows/production.yaml`
|
||||
- Subscriptions capsule untested: `find backend/src/features/subscriptions -name "*.test.ts"`
|
||||
- Stripe price-ID mapping: `grep -n "PRICE_ID" docker-compose.yml backend/src/features/subscriptions/domain/subscriptions.service.ts`
|
||||
- Tier limits and fail-open: `grep -n "VEHICLE_LIMITS\|return true" backend/src/core/config/feature-tiers.ts`
|
||||
- pg unencrypted: `grep -n "ssl" backend/src/core/config/database.ts config/app/production.yml` (expect no hits)
|
||||
- No frontend CSP: `grep -n "add_header" frontend/nginx.conf` (expect no hits)
|
||||
- 30-day grace period: `grep -rn "grace" backend/src/features/user-profile/jobs/account-purge.job.ts docs/USER-GUIDE.md`
|
||||
- AUDIT.md staleness: `grep -n "CONDITIONALLY READY" docs/AUDIT.md`
|
||||
- Ansible prune-cron gap: `grep -n "cron" ansible/deploy-staging-runner.yml`
|
||||
- Runner label drift: `grep -n "runs-on" .gitea/workflows/staging.yaml .gitea/workflows/production.yaml && grep -n "mvp-build" docs/CICD-DEPLOY.md`
|
||||
|
||||
Not verifiable from the repo (recheck externally): Google Maps key referrer restrictions
|
||||
(Google Cloud Console); whether the GRAFANA_ADMIN_PASSWORD and Stripe/Resend Gitea secrets
|
||||
are actually set (Gitea repo settings); whether a Stripe test-mode webhook endpoint is
|
||||
registered for staging (Stripe dashboard).
|
||||
@@ -1,197 +0,0 @@
|
||||
---
|
||||
name: mvp-ocr-gemini-pipeline
|
||||
description: The AI extraction subsystem - OCR engines, Gemini semantic extraction, VIN decode, and the WIF auth chain. Load this when working on anything under backend/src/features/ocr/, ocr/app/, or backend/src/features/email-ingestion/, or when you see symptoms like VIN decode failing/timing out, "Gemini authentication failed", "GeminiUnavailableError", pydantic validation error from google-genai, receipt scan returning empty fields, "No maintenance receipt fields could be extracted", manual extraction job stuck or expired, OCR container unhealthy, Google Vision quota, or "OCR service error 502/503/504". Also load before changing GEMINI_MODEL, GenerateContentConfig fields, or ocr/requirements.txt.
|
||||
---
|
||||
|
||||
# mvp-ocr-gemini-pipeline
|
||||
|
||||
The AI extraction subsystem: React frontend -> Fastify backend proxy -> Python FastAPI OCR service -> PaddleOCR / Google Vision / Gemini (Vertex AI via google-genai SDK). Authored 2026-07-07; all paths and values verified against code on that date.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you are:
|
||||
- Modifying `backend/src/features/ocr/`, `ocr/app/`, or `backend/src/features/email-ingestion/`.
|
||||
- Debugging VIN decode, receipt scan, manual extraction, or email-ingested receipts.
|
||||
- Touching Gemini config (`GEMINI_MODEL`, `GenerateContentConfig`, response schemas) or the WIF auth chain.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- General failure triage across the app -> `mvp-debugging-playbook`.
|
||||
- The full incident history (the AFC revert appears there in chronicle form) -> `mvp-failure-archaeology`.
|
||||
- Adding config knobs or secrets outside OCR -> `mvp-config-and-secrets`.
|
||||
- VIN structure theory, check digits, year-code tables as domain knowledge -> `mvp-vehicle-domain-reference` (this skill covers only how the code uses them).
|
||||
- Deploy/rollback mechanics for the OCR container -> `mvp-run-and-operate`.
|
||||
- What counts as test evidence, definition of done -> `mvp-validation-and-qa`.
|
||||
|
||||
WARNING: `docs/ocr-pipeline-tech-stack.md` is substantially stale (describes Celery, spaCy, S3/MinIO - none exist). Code wins. Do not use that doc as a source.
|
||||
|
||||
## 1. End-to-end flows (real paths)
|
||||
|
||||
Three hops. Every OCR feature goes: frontend hook -> backend proxy (auth + tier gate + size/type validation) -> Python service.
|
||||
|
||||
### Backend proxy: `backend/src/features/ocr/`
|
||||
|
||||
- `api/ocr.routes.ts` - route registration, auth + tier preHandlers.
|
||||
- `domain/ocr.service.ts` - size caps (`MAX_SYNC_SIZE` 10MB, `MAX_ASYNC_SIZE` 200MB) and MIME allowlist (jpeg/png/heic/heif/pdf); manual jobs must be `application/pdf`.
|
||||
- `external/ocr-client.ts` - HTTP client to `OCR_SERVICE_URL` (default `http://mvp-ocr:8000`). Every call goes through `fetchWithTimeout` with hardcoded `OCR_TIMEOUT_MS = 120000` (line 9; AbortController, NOT env-configurable). `decodeVin()` posts JSON; everything else is multipart.
|
||||
|
||||
### Endpoint map (verified 2026-07-07)
|
||||
|
||||
| Backend route (`/api` prefix) | Tier gate | Python endpoint | Engine path |
|
||||
|---|---|---|---|
|
||||
| `POST /ocr/extract` | auth only | `POST /extract` | `services/ocr_service.py` -> engine from `engines/engine_factory.py` |
|
||||
| `POST /ocr/extract/vin` | auth only | `POST /extract/vin` | `extractors/vin_extractor.py`: preprocess -> OCR -> 17-char pattern + check digit (`validators/vin_validator.py`), I/O/Q correction |
|
||||
| `POST /ocr/extract/receipt` | pro: `fuelLog.receiptScan` | `POST /extract/receipt` | `extractors/receipt_extractor.py` + `fuel_receipt.py` (OCR + regex in `app/patterns/`) |
|
||||
| `POST /ocr/extract/maintenance-receipt` | pro: `maintenance.receiptScan` | `POST /extract/maintenance-receipt` | `extractors/maintenance_receipt_extractor.py`: OCR -> Gemini structured JSON -> regex cross-validation |
|
||||
| `POST /ocr/extract/manual` | pro: `document.scanMaintenanceSchedule` | `POST /extract/manual` (async) | Redis DB 1 job via `services/job_queue.py`, BackgroundTask -> `extractors/manual_extractor.py` -> `GeminiEngine.extract_maintenance()` (whole PDF inline) -> `patterns/service_mapping.py` (27 subtypes) |
|
||||
| `POST /ocr/jobs`, `GET /ocr/jobs/:jobId` | auth only | `POST /jobs`, `GET /jobs/{job_id}` | generic async OCR + job polling (`routers/jobs.py`) |
|
||||
| `POST /vehicles/decode-vin` (vehicles feature, not ocr) | pro: `vehicle.vinDecode` | `POST /decode/vin` | `routers/decode.py` -> shared lazy `GeminiEngine.decode_vin()` |
|
||||
|
||||
Python routers: `ocr/app/routers/extract.py`, `decode.py`, `jobs.py`, registered in `ocr/app/main.py`. `/health` is in `main.py`.
|
||||
|
||||
### VIN decode flow (the most fragile path)
|
||||
|
||||
1. Frontend: `frontend/src/features/vehicles/api/vehicles.api.ts:90` - axios timeout 120000ms.
|
||||
2. Backend: `backend/src/features/vehicles/api/vehicles.controller.ts:decodeVin()` (line 384) - regex-validates `^[A-HJ-NPR-Z0-9]{17}$`, calls `ocrClient.decodeVin()`. Error mapping: OCR 503/422 -> 502 `VIN_DECODE_FAILED`; message contains "timed out"/"aborted" -> 504 `VIN_DECODE_TIMEOUT`.
|
||||
3. Python: `ocr/app/routers/decode.py` -> `GeminiEngine.decode_vin()` with Google Search grounding (`tools=[types.Tool(google_search=types.GoogleSearch())]`, `gemini_engine.py:384`).
|
||||
4. Model year is NEVER taken from the LLM: `resolve_vin_year()` (`gemini_engine.py:58`) computes it deterministically from VIN positions 7 and 10 (alphabetic pos 7 -> 2010-2039 cycle, per NHTSA FMVSS No. 115; this logic was inverted before commit `936753f` - do not "fix" it back). The result overrides whatever Gemini returns (`gemini_engine.py:409`).
|
||||
|
||||
### Email ingestion path: `backend/src/features/email-ingestion/`
|
||||
|
||||
`POST /api/webhooks/resend/inbound` (public, Svix-signature-verified; `api/email-ingestion.routes.ts`) -> `domain/email-ingestion.service.ts:processEmail()`:
|
||||
1. Validate sender against user profiles; fetch and filter attachments.
|
||||
2. Classify subject+body via `domain/receipt-classifier.ts` (keyword matching; confident requires >= 2 keyword matches for one type, ties are unclassified).
|
||||
3. Confident -> call the matching OCR endpoint, with the other as fallback on failure. Unconfident -> call BOTH `/extract/receipt` and `/extract/maintenance-receipt` per attachment, re-classify from returned `rawText`, then a field-count heuristic (`email-ingestion.service.ts` around lines 280-325).
|
||||
4. Vehicle association: exact match creates the `fuel_log` or `maintenance_record` directly; ambiguous match inserts a pending association (`insertPendingAssociation`) for the user to resolve later.
|
||||
|
||||
## 2. Engine architecture
|
||||
|
||||
Two engine categories - do not conflate them:
|
||||
|
||||
1. **OcrEngine subclasses** (image -> text + word boxes): `ocr/app/engines/paddle_engine.py` (PaddleOCR, local), `cloud_engine.py` (Google Vision TEXT_DETECTION), `hybrid_engine.py` (primary + fallback wrapper). Built via `engine_factory.create_engine()`; registry accepts only `paddleocr` and `google_vision`. If `OCR_FALLBACK_ENGINE != "none"`, factory returns a `HybridEngine`.
|
||||
2. **GeminiEngine** (`engines/gemini_engine.py`): standalone, NOT an OcrEngine subclass. Semantic extraction (PDF -> maintenance schedule JSON) and VIN decode. Instantiated directly by `manual_extractor.py` and `routers/decode.py`, never through the factory. `maintenance_receipt_extractor.py` has its OWN copy-pasted Gemini client (see section 3).
|
||||
|
||||
**Which engine actually runs - code defaults vs compose (compose wins in every deployed environment):**
|
||||
|
||||
| Setting | Code default (`ocr/app/config.py`) | `docker-compose.yml` (all envs inherit) |
|
||||
|---|---|---|
|
||||
| `OCR_PRIMARY_ENGINE` | `paddleocr` | `google_vision` |
|
||||
| `OCR_FALLBACK_ENGINE` | `none` | `paddleocr` |
|
||||
|
||||
Code defaults only apply where env vars are absent - i.e., bare `pytest` runs and ad-hoc local Python. Deployed containers run Vision-primary with PaddleOCR fallback. When reasoning about production behavior, always use the compose values.
|
||||
|
||||
`HybridEngine` semantics (`hybrid_engine.py`): with a CLOUD primary, the Vision monthly cap (`VISION_MONTHLY_LIMIT`, 1000/calendar month, Redis counter `ocr:vision_requests:YYYY-MM` in DB 1) is checked BEFORE calling it; once exhausted, PaddleOCR is the sole engine until month end. With a LOCAL primary, fallback triggers on confidence < `OCR_FALLBACK_THRESHOLD`. `_CLOUD_TIMEOUT_SECONDS = 10.0` (raised from 5s because first-call WIF token exchange takes 6-8s, refs #182) is a discard-after-completion threshold, NOT an abort: `_run_cloud_with_cap` runs `cloud.recognize()` to completion with no timeout or cancellation, only then compares elapsed time, and throws away results slower than 10s (without incrementing the Vision counter). A hung or slow Vision call can therefore block far past 10s -- directly relevant to weak point 1 (sync calls block the event loop); do not rule out the Vision path in a hung-container investigation because of this constant.
|
||||
|
||||
## 3. Auth chain: WIF via Auth0 (memorize this before touching credentials)
|
||||
|
||||
WIF = Workload Identity Federation: Google Cloud accepts a third-party JWT (here, an Auth0 machine-to-machine token) instead of a service-account key file. No Google key ever exists on disk.
|
||||
|
||||
Chain, in order:
|
||||
1. `secrets/app/google-wif-config.json` - **deliberately committed to git; it is configuration, not a secret** (it contains only the pool/provider audience and an executable pointer). Mounted at `/run/secrets/google-wif-config.json`; `GOOGLE_VISION_KEY_PATH` points at it. Do not "fix" its presence in git.
|
||||
2. Its `credential_source.executable.command` is `/app/scripts/fetch-auth0-token.sh` (repo: `ocr/scripts/fetch-auth0-token.sh`, chmod +x in `ocr/Dockerfile:48`, 30s timeout). The script reads `/run/secrets/auth0-ocr-client-id` and `/run/secrets/auth0-ocr-client-secret` (real secrets, NOT in git; compose mounts them from `secrets/app/*.txt`), requests an Auth0 M2M token, and emits Google's executable-credential JSON format.
|
||||
3. Google STS exchanges that JWT, then impersonates `[email protected]`.
|
||||
4. Client bootstrap sets, as process-wide env vars INSIDE `_get_client()` immediately before `genai.Client(vertexai=True, ...)` construction: `GOOGLE_APPLICATION_CREDENTIALS=<key_path>` and `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1` (executables are refused by google-auth without this flag).
|
||||
|
||||
**The bootstrap is DUPLICATED.** Identical `_get_client()` implementations exist in `ocr/app/engines/gemini_engine.py:218` and `ocr/app/extractors/maintenance_receipt_extractor.py:173` (and `cloud_engine.py:67` sets the same env vars for Vision). Any auth or client-construction fix must land in BOTH `_get_client()` copies or you fix VIN decode/manual extraction while leaving maintenance receipts broken (or vice versa).
|
||||
|
||||
On a dev machine the `auth0-ocr-client-*` secrets typically exist only as Docker-created empty directories - the live Gemini path cannot run locally; end-to-end verification happens on staging.
|
||||
|
||||
## 4. google-genai SDK sharp edges
|
||||
|
||||
**The dependency is unpinned:** `ocr/requirements.txt:25` has `google-genai>=1.0.0` (floor only). Each image build resolves whatever is newest. This already caused a staging breakage:
|
||||
|
||||
- `936753f` added `automatic_function_calling=types.AutomaticFunctionCallingConfig(max_remote_calls=3)` to the VIN decode `GenerateContentConfig`.
|
||||
- The installed SDK version rejected it with a pydantic validation error at request time - VIN decode broke on staging while the build stayed green (CI runs zero tests; the only gate is images build + containers report healthy).
|
||||
- `1add6c8` removed the parameter; `56df5d4` confirmed the revert and added diagnostic logging (full raw Gemini JSON at `gemini_engine.py:401-406`; `hasTrim/hasEngine/hasTransmission` in `vehicles.controller.ts`). Do not re-add AFC config without pinning and verifying the SDK version.
|
||||
|
||||
**Rule: before shipping any new `GenerateContentConfig` field (or `types.*` construct), validate it against the INSTALLED SDK version**, not the docs:
|
||||
|
||||
```bash
|
||||
# What version is actually in the running container
|
||||
docker compose exec mvp-ocr pip show google-genai | head -2
|
||||
|
||||
# Does the field exist / validate on that version (pydantic validates at construction)
|
||||
docker compose exec mvp-ocr python3 -c "
|
||||
from google.genai import types
|
||||
print(types.GenerateContentConfig(response_mime_type='application/json', YOUR_NEW_FIELD=...))"
|
||||
```
|
||||
|
||||
**Model config drift (known-weak, deliberate as of 2026-07-07):** code default is `gemini-2.5-flash` (`ocr/app/config.py:37`) but `docker-compose.yml` overrides `GEMINI_MODEL: gemini-3-flash-preview` - a PREVIEW model in deployed config. Preview models can be withdrawn or change behavior without notice. If Gemini responses suddenly degrade or 404, check the model name first. CLAUDE.md files under `ocr/` still say "Gemini 2.5 Flash" - compose wins.
|
||||
|
||||
**Response schemas** use Vertex-style UPPERCASE types (`"OBJECT"`, `"STRING"`, `"NUMBER"`, nullable flags) - keep that convention when editing `_VIN_DECODE_SCHEMA` / `_RESPONSE_SCHEMA` / `_RECEIPT_RESPONSE_SCHEMA`.
|
||||
|
||||
## 5. Config knob table (verified 2026-07-07)
|
||||
|
||||
| Knob | Where | Default / deployed value |
|
||||
|---|---|---|
|
||||
| `GEMINI_MODEL` | `ocr/app/config.py:37` / `docker-compose.yml` | `gemini-2.5-flash` / `gemini-3-flash-preview` |
|
||||
| `VERTEX_AI_PROJECT` / `VERTEX_AI_LOCATION` | `config.py:33-36` / compose | `""` / `global` -> compose: `motovaultpro` / `global` |
|
||||
| `OCR_PRIMARY_ENGINE` / `OCR_FALLBACK_ENGINE` | `config.py:13,19` / compose | `paddleocr`/`none` -> compose: `google_vision`/`paddleocr` |
|
||||
| `OCR_CONFIDENCE_THRESHOLD` / `OCR_FALLBACK_THRESHOLD` | `config.py` | 0.6 / 0.6 |
|
||||
| `VISION_MONTHLY_LIMIT` | `config.py:28` | 1000 requests/calendar month |
|
||||
| `GOOGLE_VISION_KEY_PATH` | `config.py:23` | `/run/secrets/google-wif-config.json` (shared by Vision and Gemini) |
|
||||
| Sync size cap | `ocr.service.ts:20`, `extract.py:30` | 10MB (413 above) |
|
||||
| Manual/async size cap | `ocr.service.ts:23`, `extract.py:33` | 200MB |
|
||||
| Gemini inline PDF cap | `gemini_engine.py:20` `_MAX_PDF_BYTES` | 20MB hard limit |
|
||||
| Backend->OCR timeout | `ocr-client.ts:9` | 120000ms, hardcoded, all endpoints |
|
||||
| Frontend timeouts | `vehicles.api.ts:90` (VIN decode), `useVinOcr.ts:52`, `useManualExtraction.ts:54` | 120000ms |
|
||||
| | `useReceiptOcr.ts:153`, `useMaintenanceReceiptOcr.ts:147` | 30000ms |
|
||||
| Hybrid cloud result-discard threshold (the call itself is unbounded) | `hybrid_engine.py:25` | 10.0s |
|
||||
| Job TTLs | `job_queue.py:19-22` | 3600s regular / 7200s manual; expired jobs -> backend 410 |
|
||||
| OCR Redis | `config.py:40-42` | `mvp-redis` DB 1 (backend uses DB 0) |
|
||||
| Gemini base field confidence | `maintenance_receipt_extractor.py:31`, `manual_extractor.py:58` | 0.85 before regex cross-validation |
|
||||
| Service-side Gemini timeout | (nowhere) | NONE - `generate_content` calls are unbounded inside the Python service |
|
||||
|
||||
## 6. Known-weak points (owner-acknowledged, 2026-07-07; do not silently "improve" without an issue)
|
||||
|
||||
1. **Sync Gemini calls block the uvicorn event loop.** `routers/decode.py:42` and `routers/extract.py:239,327` run synchronous OCR/Gemini work directly in `async def` handlers. A slow Search-grounded VIN decode (observed >60s) blocks ALL requests including `/health` (compose healthcheck: interval 5s, timeout 5s, retries 3 -> container flagged unhealthy within ~15s). Verified: plain compose does not auto-restart unhealthy containers, but `scripts/ci/health-check.sh` and the deploy/rollback gates treat unhealthy as failure, so a deploy landing during a long decode can fail its gate or trigger rollback. Only manual extraction uses `run_in_executor` (`extract.py:472`).
|
||||
2. **120s == 120s timeout race.** Frontend VIN decode timeout equals the backend->OCR timeout exactly; no headroom, so at the boundary the user sees a generic axios timeout instead of the backend's mapped 504. Receipt hooks are worse in a different way: frontend gives up at 30s while backend + OCR + Gemini keep burning quota for up to 120s.
|
||||
3. **No retries anywhere** for transient Gemini/Vision failures. The single exception is maintenance-receipt's silent fallback (next item).
|
||||
4. **Silent empty-fields fallback.** In `maintenance_receipt_extractor.py:129-133`, any Gemini exception is caught, logged at WARNING, and `gemini_fields = {}` - which usually surfaces as 422 "No maintenance receipt fields could be extracted". A dead Gemini auth chain therefore looks like "bad receipt photo". Check OCR container logs for "Gemini extraction failed, falling back to OCR-only" before blaming image quality.
|
||||
5. **20MB vs 200MB manual gap.** `/extract/manual` accepts up to 200MB, queues the job, then `GeminiEngine.extract_maintenance()` rejects anything over 20MB with an error suggesting GCS URIs - which are not implemented. Every 21-200MB manual is a guaranteed deferred failure.
|
||||
6. **Email-ingestion cost amplification.** Unconfident classification runs BOTH OCR endpoints per attachment (2x Vision against the 1000/month cap, plus a Gemini call for the maintenance path). A burst of ambiguous forwarded emails can drain the Vision quota.
|
||||
7. **Manual job progress race** (`manual_extractor.py:122-126`): fire-and-forget progress updates from worker threads could overwrite COMPLETED status; mitigated only by the convention "never send 100% from the extractor". Preserve that convention.
|
||||
8. **VIN decode trusts LLM+Search for specs.** Only the year is ground-truthed. Full raw Gemini JSON is logged per decode (`gemini_engine.py:401-406`) - diagnostic aid, but log volume/PII to keep in mind.
|
||||
|
||||
## 7. How to test
|
||||
|
||||
CI gates nothing beyond build + boot (canonical statement: `mvp-validation-and-qa` section 1). All tests below are local-only and are your responsibility to run.
|
||||
|
||||
**Python unit tests (all Gemini/network calls mocked - safe without secrets):**
|
||||
```bash
|
||||
cd ocr && pytest
|
||||
```
|
||||
Key files: `tests/test_gemini_engine.py` (client init, 20MB cap, extraction, error paths), `test_resolve_vin_year.py` (year-cycle disambiguation - regenerated in `936753f`, do not weaken), `test_vin_decode.py`, `test_receipt_extraction.py`, `test_maintenance_patterns.py`, `test_engine_abstraction.py`, `test_service_mapping.py`. Remember bare pytest sees code defaults (paddleocr primary), not compose values.
|
||||
|
||||
**Backend unit tests (OcrClient mocked):**
|
||||
```bash
|
||||
cd backend && npm test -- --testPathPattern="features/ocr"
|
||||
```
|
||||
Files: `backend/src/features/ocr/tests/unit/ocr-receipt.test.ts`, `ocr-manual.test.ts`. Do NOT run the backend integration tests casually - they DROP TABLE CASCADE on the shared dev database (owner non-negotiable: fresh backup first).
|
||||
|
||||
**Live pipeline: staging only.** The WIF chain needs real `auth0-ocr-client-id/secret` files, which exist only as empty mount directories on dev machines. Push a PR, let the staging deploy run, then exercise the flow through the staging UI, or from the staging host:
|
||||
```bash
|
||||
docker compose exec mvp-backend curl -s -X POST http://mvp-ocr:8000/decode/vin \
|
||||
-H 'Content-Type: application/json' -d '{"vin":"<17-char VIN>"}'
|
||||
```
|
||||
Watch logs: `docker logs mvp-ocr-staging --tail 100` on the staging host (dev/prod container name is `mvp-ocr`); look for "Gemini engine initialized", "VIN year resolved", "Gemini decoded VIN". Never hand-edit files on staging/prod servers - rsync --delete reverts them on the next deploy.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 by direct inspection of the repo (all line numbers and values verified against code, not docs). Volatile facts and how to re-verify each:
|
||||
|
||||
| Fact | Re-verify with |
|
||||
|---|---|
|
||||
| Backend->OCR timeout 120000ms | `grep -n OCR_TIMEOUT_MS backend/src/features/ocr/external/ocr-client.ts` |
|
||||
| Frontend timeouts (120s VIN decode, 30s receipts) | `grep -rn "timeout:" frontend/src/features/vehicles/api/vehicles.api.ts frontend/src/features/*/hooks/use*Ocr*.ts frontend/src/features/documents/hooks/useManualExtraction.ts` |
|
||||
| Deployed model `gemini-3-flash-preview`, Vision-primary engines | `grep -n "GEMINI_MODEL\|OCR_PRIMARY_ENGINE\|OCR_FALLBACK_ENGINE" docker-compose.yml` |
|
||||
| Code defaults (`gemini-2.5-flash`, paddleocr/none) | `grep -n "getenv" ocr/app/config.py` |
|
||||
| google-genai unpinned | `grep -n google-genai ocr/requirements.txt` |
|
||||
| Installed SDK version in a running container | `docker compose exec mvp-ocr pip show google-genai` |
|
||||
| Size caps 10MB/200MB/20MB | `grep -n "MAX_SYNC_SIZE\|MAX_ASYNC_SIZE\|MAX_MANUAL_SIZE\|_MAX_PDF_BYTES" backend/src/features/ocr/domain/ocr.service.ts ocr/app/routers/extract.py ocr/app/engines/gemini_engine.py` |
|
||||
| Job TTLs 3600/7200, Redis DB 1 | `grep -n "JOB_TTL\|MANUAL_JOB_TTL" ocr/app/services/job_queue.py; grep -n REDIS_DB docker-compose.yml` |
|
||||
| Duplicated `_get_client()` bootstrap | `grep -n "_get_client\|ALLOW_EXECUTABLES" ocr/app/engines/gemini_engine.py ocr/app/extractors/maintenance_receipt_extractor.py ocr/app/engines/cloud_engine.py` |
|
||||
| WIF config committed, executable helper path | `git ls-files secrets/app/ \| grep wif; grep -n executable secrets/app/google-wif-config.json` |
|
||||
| AFC incident commits | `git log --oneline 936753f 1add6c8 56df5d4 -1 --no-walk 2>/dev/null \|\| git show --stat 56df5d4` |
|
||||
| Tier gates on OCR routes | `grep -n requireTier backend/src/features/ocr/api/ocr.routes.ts backend/src/features/vehicles/api/vehicles.routes.ts` |
|
||||
| CI still runs no tests | `grep -rn "pytest\|npm test\|jest" .gitea/workflows/` (expect no test-execution hits) |
|
||||
| Event-loop-blocking sync calls | `grep -n "_gemini_engine.decode_vin\|receipt_extractor.extract\|maintenance_receipt_extractor.extract" ocr/app/routers/decode.py ocr/app/routers/extract.py` |
|
||||
@@ -1,237 +0,0 @@
|
||||
---
|
||||
name: mvp-proof-and-analysis-toolkit
|
||||
description: Prove-it methods for high-stakes decisions and investigations in MotoVaultPro. Load this when you are about to commit to an architectural or design decision (schema change, API contract, caching strategy, "just return raw rows"-class shortcuts), when a bug has survived one or more failed fix attempts, when you catch yourself writing "this should fix it" or "this is probably faster", when a root-cause claim needs to be defended in a PR, or when someone asks to stress-test / red-team / sanity-check a decision. Symptom keywords - fix didn't work, still broken after fix, intermittent, works locally fails on staging, off by one, wrong value displayed, performance claim, premature optimization, root cause unclear, second fix for same bug.
|
||||
---
|
||||
|
||||
# MotoVaultPro Proof and Analysis Toolkit
|
||||
|
||||
Five recipes for not being wrong when being wrong is expensive. Each recipe carries a worked example from this repo's actual git history — every commit hash cited here is verifiable with `git show <hash>`.
|
||||
|
||||
**When to use this skill:**
|
||||
- Before committing to a decision that is costly to reverse (schema, API contract, dependency, data-flow convention).
|
||||
- When a bug has already eaten one failed fix — stop patching, start proving.
|
||||
- Before making any performance or "root cause" claim in a PR description or issue comment.
|
||||
|
||||
**When NOT to use this skill:**
|
||||
- Routine debugging with a fresh symptom: start with `mvp-debugging-playbook` (symptom-to-triage table). Come back here if the first fix fails.
|
||||
- Checking whether a battle was already fought and settled: `mvp-failure-archaeology`.
|
||||
- "What counts as done/tested": `mvp-validation-and-qa`.
|
||||
- Reading logs and metrics mechanics (LogQL, Grafana): `mvp-diagnostics-and-logging`.
|
||||
- Issue/branch/PR mechanics and review severity taxonomy: `mvp-change-control`.
|
||||
|
||||
---
|
||||
|
||||
## Recipe 1: The decision stress-test protocol
|
||||
|
||||
Run this before committing to any decision where reversal is expensive. It is a checklist you execute directly in-session (the former Python step-injector delivery is retired; the content is preserved here). Grounded in Chain-of-Verification (Dhuliawala et al., 2023), Self-Consistency (Wang et al., 2023), and Multi-Expert Prompting (Wang et al., 2024).
|
||||
|
||||
### Phase 1 — Decompose into stable IDs
|
||||
|
||||
Break the decision into parts, each with an ID that persists through every later step:
|
||||
|
||||
| Prefix | Type | Count | Prompt |
|
||||
| ------ | ---- | ----- | ------ |
|
||||
| C1, C2... | CLAIMS — factual assertions | 3-7 | What facts does this decision assume true? What cause-effect relationships does it depend on? |
|
||||
| A1, A2... | ASSUMPTIONS — unstated beliefs | 2-5 | What is implied but not stated? What would someone unfamiliar with the context not know? |
|
||||
| K1, K2... | CONSTRAINTS — hard boundaries | 1-4 | What technical, organizational, or timeline limits apply? |
|
||||
| J1, J2... | JUDGMENTS — subjective tradeoffs | 1-3 | Where are values weighed against each other? What "it depends" calls were made? |
|
||||
|
||||
### Phase 2 — Classify verifiability
|
||||
|
||||
Tag every item:
|
||||
|
||||
- **[V] VERIFIABLE** — can be checked against evidence or tested ("pg returns DECIMAL as strings" — checkable).
|
||||
- **[J] JUDGMENT** — subjective tradeoff, no objectively correct answer ("simplicity beats flexibility here").
|
||||
- **[C] CONSTRAINT** — given condition, fixed for this decision ("no fully working local dev loop; e2e verification is on staging").
|
||||
|
||||
Edge-case rule: when an item fits multiple categories, prefer [V] over [J] over [C] — verifiable items can be checked, judgments only debated, constraints merely accepted. ("The team can deliver in 4 weeks" is [V]: checkable against past velocity.)
|
||||
|
||||
### Phase 3 — Chain-of-Verification questions
|
||||
|
||||
For each [V] item, write 1-3 questions (1 for simple claims, 3 max for compound ones) that:
|
||||
- are specific and independently answerable,
|
||||
- are designed to reveal if the claim is FALSE (falsification focus),
|
||||
- do not assume the claim is true in their own wording,
|
||||
- each test a different aspect of the claim.
|
||||
|
||||
### Phase 4 — Factored verification (the epistemic boundary)
|
||||
|
||||
Answer each question INDEPENDENTLY, without the original decision's reasoning in view. This is the step that catches confirmation bias, so it gets the strictest rule:
|
||||
|
||||
Answer using ONLY:
|
||||
- (a) established domain knowledge (documentation, references — e.g. node-postgres docs),
|
||||
- (b) stated constraints from the decision context,
|
||||
- (c) logical inference from first principles that holds regardless of whether this decision is correct.
|
||||
|
||||
Do NOT:
|
||||
- assume the decision is correct and work backward,
|
||||
- assume it is incorrect and hunt for disproof,
|
||||
- reference whether the claim "should" be true given the decision.
|
||||
|
||||
For every question, separate **Answer** (factual, evidence-based) from **Implication** (what it means for the claim). Then mark each [V] item: **VERIFIED** | **FAILED** | **UNCERTAIN** (state what additional information would resolve it).
|
||||
|
||||
In this repo, "verify against evidence" usually means: read the actual code path (`grep -rn` in `backend/src/features/`), read the actual schema, or `git log -S "<string>"` for history — never memory of what the code "should" say. Where docs contradict code, code wins.
|
||||
|
||||
### Phase 5 — Steel-man contrarian
|
||||
|
||||
Generate the STRONGEST case against the decision — the opposition's best case, not a strawman. Start from Phase 4 results: FAILED items are direct ammunition; UNCERTAIN items are attack vectors; even VERIFIED items may hide dependencies. Explore: what could go wrong that was not considered; what alternatives were dismissed too fast; what second-order effects were missed; what happens if key assumptions change; who would disagree and why they might be right. Output a one-sentence contrarian position, a 2-3 paragraph argument referencing item IDs, and 3 key risks.
|
||||
|
||||
### Phase 6 — Problem reframing
|
||||
|
||||
Phase 5 challenged the solution; this challenges the PROBLEM STATEMENT. Set the proposed solution aside and ask: approached fresh, how would I state this problem? Is it a symptom of a deeper issue? What would a different stakeholder (user, ops, security) prioritize? What if the K constraints were negotiable? Is there a simpler formulation that dissolves the tradeoffs? State the alternative framing, what it emphasizes, which hidden assumptions it reveals (by item ID), and whether it strengthens, weakens, or redirects the decision.
|
||||
|
||||
### Phase 7 — Verdict
|
||||
|
||||
| Verdict | Trigger (ANY for ESCALATE/REVISE; ALL for STAND) |
|
||||
| ------- | ------------------------------------------------ |
|
||||
| **ESCALATE** | Any FAILED item involves safety, security, or data integrity; OR a critical UNCERTAIN item cannot be cheaply verified; OR the reframing shows the problem itself is wrong. Escalation target here: stop, write it up in the Gitea issue, get the owner's call before proceeding. |
|
||||
| **REVISE** | Any FAILED item on a core (not peripheral) claim; OR multiple UNCERTAIN items on feasibility/effort/impact; OR the challenge phase revealed gaps that change the calculus. |
|
||||
| **STAND** | No FAILED core claims; UNCERTAIN items explicitly acknowledged as accepted risks; Phase 5-6 challenges addressable within the current approach. |
|
||||
|
||||
Tie-breakers: between STAND and REVISE, favor REVISE (cheaper to refine than to fail). Between REVISE and ESCALATE, state both options with the conditions under which each applies.
|
||||
|
||||
Output: VERDICT, verification summary (Verified/Failed/Uncertain ID lists), strongest challenge + reframing insight + how the decision answers them, and a specific next action.
|
||||
|
||||
### Worked example: the #47 "return raw rows" decision (2026-01-17)
|
||||
|
||||
The bug: Fuel Logs API returned 500 "Invalid time value". Diagnosis: enhanced repository methods called `mapRow()` (snake_case to camelCase) but the service's `toEnhancedResponse()` read snake_case keys, so `new Date(row.created_at)` got `undefined`. The decision taken in commit `574acf3`: make seven enhanced repository methods return **raw pg rows**. Decomposed retroactively:
|
||||
|
||||
```
|
||||
C1 [V]: toEnhancedResponse() reads snake_case keys from its input rows.
|
||||
C2 [V]: mapRow() output (camelCase) makes row.created_at undefined, causing "Invalid time value".
|
||||
A1 [V]: Raw pg rows satisfy the TypeScript types the enhanced methods declare (numbers as numbers).
|
||||
A2 [V]: Returning raw rows is consistent with this project's repository conventions.
|
||||
J1 [J]: Reverting the repo to raw rows is cheaper than changing the service to consume mapped rows.
|
||||
```
|
||||
|
||||
Factored verification, behind the epistemic boundary (no access to the fix's own reasoning):
|
||||
|
||||
- **A1 — Q: What JavaScript types does node-postgres return for DECIMAL/NUMERIC columns?** Answer, from pg documentation alone: strings, to avoid float precision loss. Implication: raw rows put strings where the declared types say `number`. **A1: FAILED.** (Corroborating evidence available the same day: #49, fuel values displaying wrong for exactly this reason.)
|
||||
- **A2 — Q: What do project conventions say repositories may return?** CLAUDE.md (which already contained the rule at the time, added 2025-12-21): "All methods returning data to the API must use these mappers - never return raw database rows." **A2: FAILED.**
|
||||
|
||||
Verdict under the Phase 7 rubric: **REVISE** — two FAILED items on core claims. The revised direction is the one the codebase eventually adopted anyway: keep a mapper in the repository and coerce numeric columns (what `mapEnhancedRow` in commit `0d90829` did). Instead, the raw-rows shortcut shipped and lay dormant for four months until #244 (2026-05-15): maintenance cost blank on the vehicle summary, traced back through #239 and #241 to DECIMAL columns arriving as strings from exactly these methods. One hour of factored verification in January would have bought back a three-issue debugging chain in May. That is the ROI profile of this protocol: it pays on decisions whose failure mode is *latent*.
|
||||
|
||||
---
|
||||
|
||||
## Recipe 2: The root-cause evidence bar
|
||||
|
||||
A root-cause claim is accepted only when it clears BOTH bars:
|
||||
|
||||
1. **ONE mechanism explains ALL observations — including the negatives.** List every observation, including what did NOT happen (errors that did not fire, platforms that did not break, states that stayed intact). A candidate cause that explains 4 of 5 observations is not "mostly right"; it is either wrong or incomplete (there may be two causes — see the worked example).
|
||||
2. **It survives an assigned adversarial refutation pass.** Before writing the fix, deliberately try to break your own explanation: "Assume this mechanism is NOT the cause. What observation would prove that?" Then go check that observation. In a multi-agent session, assign the refutation to a separate subagent with only the observation list and the candidate mechanism — not your reasoning (same epistemic boundary as Recipe 1 Phase 4).
|
||||
|
||||
Practical worksheet:
|
||||
|
||||
```
|
||||
OBSERVATIONS (positive): O1, O2, ...
|
||||
OBSERVATIONS (negative): N1 "no error was logged", N2 "desktop unaffected", ...
|
||||
CANDIDATE MECHANISM: M
|
||||
COVERAGE CHECK: for each O and N: does M predict it? YES/NO/PARTIAL
|
||||
REFUTATION ASSIGNMENT: "If M were false, we would observe ____" -> go observe.
|
||||
```
|
||||
|
||||
Any PARTIAL or NO means M is not yet the root cause. Fixes shipped on partial coverage are symptom fixes.
|
||||
|
||||
### Worked example: the mobile login saga (#188/#190, 2026-02-14 to 02-15)
|
||||
|
||||
Symptom: on mobile, login redirected to the homepage without ever showing the Auth0 page. Four symptom fixes shipped in two days — `a195fa9` (allow callback route to complete code exchange), `6e493e9` (detect/clear stale IndexedDB tokens), `38debaa` (skip stale-token validation during callback), `b5b82db` (resolve callback failure from cache issues) — and the symptom persisted. Each fix explained SOME observations. None explained all of them, because there were **two independent mechanisms**:
|
||||
|
||||
- **Root cause 1** (`da59168`): `loadCacheFromDB` used IndexedDB `store.getAll()`, which returns raw values, not key-value pairs — so an `item.key` check always failed and `memoryCache` was empty after every page reload. Auth0 SDK state stored before the redirect was lost on mobile Safari (no bfcache). This explains the negative observation that desktop often worked: desktop kept the page alive across the redirect, so the in-memory cache never needed reloading.
|
||||
- **Root cause 2** (`850f713`): React fires child effects before parent effects. App.tsx's URL-sync effect called `history.replaceState()` on `/callback`, stripping `?code=` and `&state=` before Auth0Provider's own effect could read them — so the SDK fell through to `checkSession()` and failed **silently**. This explains the key negative observation: no error anywhere.
|
||||
|
||||
What "explains all observations" looked like: fixing root cause 1 alone still left logins failing (state survived reload, but the code/state params were still being stripped); only mechanism 2 predicted the total silence of the failure. Each mechanism was separately proven — `da59168` demonstrated the empty-cache behavior directly in `frontend/src/core/utils/indexeddb-storage.ts`, `850f713` demonstrated the effect-ordering strip in `frontend/src/App.tsx`. Lesson: when no single mechanism covers the observation list, do not force one — test the hypothesis "there are two."
|
||||
|
||||
---
|
||||
|
||||
## Recipe 3: Hypothesis predicts numbers BEFORE running
|
||||
|
||||
Write the predicted observation — the exact string, count, date, or status code — BEFORE running the experiment. A hypothesis that only *postdicts* (explains the result after you see it) is not evidence, because most wrong hypotheses can also explain the result after the fact. The discriminating power is in the prediction differing between hypotheses.
|
||||
|
||||
Format, written down before the experiment:
|
||||
|
||||
```
|
||||
H1: <mechanism> -> PREDICTS: <exact observable, e.g. API returns "2026-03-22", status 500, count=0>
|
||||
H2: <mechanism> -> PREDICTS: <a DIFFERENT exact observable>
|
||||
EXPERIMENT: <one command / one click path>
|
||||
```
|
||||
|
||||
If two hypotheses predict the same observable, the experiment cannot distinguish them — design a different experiment before running anything.
|
||||
|
||||
### Worked example: the date off-by-one (#237, fixed four times on 2026-03-23)
|
||||
|
||||
Symptom: dates displayed one day off. There were THREE distinct UTC traps live at once, and each predicts a DIFFERENT specific wrong observation — which is what makes them separable without guessing:
|
||||
|
||||
| Trap | Mechanism | Discriminating prediction (write BEFORE looking) |
|
||||
| ---- | --------- | ------------------------------------------------ |
|
||||
| A — backend pg parser (`f0fc427`) | pg's default parser turns DATE columns into JS `Date` objects at local midnight; JSON serialization via `toISOString()` converts to UTC and can shift the calendar day | The **raw API JSON** (curl the backend directly, bypassing the UI) shows the date as a full ISO timestamp (e.g. `"2026-03-23T00:00:00.000Z"`), not a plain `"YYYY-MM-DD"` string. The DB value itself is correct. |
|
||||
| B — frontend display (`1e056f0`) | `new Date("YYYY-MM-DD")` parses as UTC midnight; `toLocaleDateString()` in a browser west of UTC renders the **previous** day | The raw API JSON shows the correct plain `"YYYY-MM-DD"` string; only the rendered UI is wrong, shifted one day EARLIER, and only for negative-UTC-offset browsers. DB and API both correct. |
|
||||
| C — OCR write path (`087f7b9`) | receipt-OCR hook built the date via `toISOString().split('T')[0]` on a local-time Date, shifting the day at WRITE time | The **stored DB row itself** contains the wrong date (query PostgreSQL directly), and the error is time-of-day dependent — records created in local evening hours shift, morning ones do not. UI faithfully displays the wrong stored value. |
|
||||
|
||||
Three layers (DB value / API JSON / rendered UI) and three predictions that disagree at a specific layer. Two curl commands and one SQL query discriminate all three — no fix needed to be "tried" to find out which trap was live. The actual history (`c05e33e` then `f0fc427` then two more fixes the same day) shows what happens without this: each fix addressed one trap, the symptom "came back," and the day ended with four commits. The settled outcome (do not re-fight it — see `mvp-failure-archaeology`): DATE columns flow as plain `YYYY-MM-DD` strings end to end; `types.setTypeParser(1082, ...)` in `backend/src/core/config/database.ts` is load-bearing; display via dayjs; never `new Date(dateString)` or `toISOString()` for DATE values.
|
||||
|
||||
---
|
||||
|
||||
## Recipe 4: Benchmark before claiming (the Measure First rule)
|
||||
|
||||
CLAUDE.md, verbatim: "No premature optimization" and "Benchmark before claiming something is faster." In this project a performance claim is valid only with:
|
||||
|
||||
1. **A real measurement source.** Every backend request is logged with a `duration` field in milliseconds (`backend/src/core/plugins/logging.plugin.ts`, `msg="Request processed"`, fields: `requestId`, `method`, `path`, `status`, `duration`, `ip`). Query it in Grafana (logs.motovaultpro.com) via Loki. LogQL mechanics live in `mvp-diagnostics-and-logging`.
|
||||
2. **N >= 20 samples per variant.** Single requests are noise — Gemini cold starts alone (see `mvp-failure-archaeology`, the VIN saga) produce order-of-magnitude outliers. Compare medians or p95, not single numbers or means.
|
||||
3. **Same environment for both sides of the comparison.** Before/after numbers from different environments (local container vs staging) are not comparable. Given this project's reality — no fully working local dev loop, e2e verification on staging via the PR deploy pipeline — that means both measurements come from staging.
|
||||
4. **The prediction written first** (Recipe 3): "this change should move p95 for `GET /api/...` from Xms to under Yms" — before deploying it.
|
||||
|
||||
Corollary (no premature optimization): if you cannot state which logged endpoint's `duration` is currently unacceptable and by how much, the optimization has no justified motivation. Skip it and note it as a candidate in the issue instead.
|
||||
|
||||
---
|
||||
|
||||
## Recipe 5: The diff-format authority model (for plans and review findings)
|
||||
|
||||
When a plan, review finding, or handoff references a code location, use unified-diff conventions with this authority model (ported from the retired planner skill; it is the standard here because line numbers drift between when a plan is written and when it executes):
|
||||
|
||||
| Component | Authority | Meaning |
|
||||
| --------- | --------- | ------- |
|
||||
| File path (`--- a/backend/src/...`) | **AUTHORITATIVE** | Exact target file. Never "the auth files" — always the repo-relative path. |
|
||||
| Line numbers (`@@ -123,6 +123,15 @@`) | **APPROXIMATE** | May drift as earlier changes land. Never treat as exact; never write "insert at line 42" in prose or comments. |
|
||||
| Function context after `@@` | **SCOPE HINT** | Which function/method contains the change. |
|
||||
| Context lines (unchanged) | **AUTHORITATIVE ANCHORS** | The executor locates the change by matching these patterns in the actual file. Include 2-3 before and after. |
|
||||
| `+` / `-` lines | The change itself | Complete code. |
|
||||
|
||||
Rules:
|
||||
|
||||
- **No elision placeholders.** `...`, `[existing code]`, `// rest unchanged` are forbidden inside diff blocks — an executor cannot match an anchor that is not real text from the file.
|
||||
- **Context lines must actually exist** in the target file. Validate by grepping before finalizing the plan or finding.
|
||||
- **Comments in `+` lines explain WHY, not WHAT**, with no hidden baselines ("generous timeout" — compared to what? Write "60s accommodates p95 upstream response time" instead) and no change-narrative ("added to fix #244" belongs in the commit message, not the code).
|
||||
- **Location directives in comments are forbidden** — the diff structure IS the location.
|
||||
|
||||
Validation checklist before finalizing:
|
||||
|
||||
- [ ] File path exact and repo-relative
|
||||
- [ ] Context lines verified present in the target file
|
||||
- [ ] 2-3 context lines each side of the change
|
||||
- [ ] No `...` / `[existing code]` placeholders
|
||||
- [ ] Comments are WHY-comments with concrete baselines
|
||||
- [ ] No line-number directives in prose or comments
|
||||
|
||||
The same authority model applies to review findings: a finding anchored only to a line number is fragile; a finding anchored to a file path plus a quoted code snippet (the anchor) survives rebases.
|
||||
|
||||
---
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct inspection of this repo. The five recipes are method (stable); the worked examples and anchors below are facts (volatile). Re-verify before relying on them:
|
||||
|
||||
| Volatile fact | Re-verification command |
|
||||
| ------------- | ----------------------- |
|
||||
| #47 raw-rows commit and its rationale | `git show 574acf3 --stat` |
|
||||
| #244 mapEnhancedRow fix (repays #47) | `git show 0d90829 --stat` |
|
||||
| "never return raw database rows" rule predates #47 | `git log --date=short -S "never return raw database rows" -- CLAUDE.md` |
|
||||
| Mobile login root cause 1 (IndexedDB getAll) | `git show da59168` |
|
||||
| Mobile login root cause 2 (effect-order URL strip) | `git show 850f713` |
|
||||
| Date trap fixes (backend parser / frontend / OCR) | `git show f0fc427 087f7b9 1e056f0 --stat` |
|
||||
| DATE parser override still present | `grep -n "setTypeParser(1082" backend/src/core/config/database.ts` |
|
||||
| mapEnhancedRow still present in fuel-logs repo | `grep -n "mapEnhancedRow" backend/src/features/fuel-logs/data/fuel-logs.repository.ts` |
|
||||
| Request log `duration` field and `msg="Request processed"` | `grep -n "duration" backend/src/core/plugins/logging.plugin.ts` |
|
||||
| Grafana host | `grep -n "logs.motovaultpro.com" docker-compose.yml` |
|
||||
| Retired source protocols (decision-critic, diff-format) | `git show HEAD:.claude/skills/decision-critic/scripts/decision-critic.py` and `git show HEAD:.claude/skills/planner/resources/diff-format.md` (valid while the old library remains in history; adjust the ref if deletion has been committed) |
|
||||
|
||||
Note on sample size in Recipe 4: the N >= 20 threshold is a working convention set by this skill (2026-07-07), not an owner directive — adjust with judgment, but never claim from N=1.
|
||||
@@ -1,369 +0,0 @@
|
||||
---
|
||||
name: mvp-research-frontier
|
||||
description: >-
|
||||
Open problems where MotoVaultPro can genuinely advance, plus the methodology for
|
||||
turning a hunch into an accepted result. Load when the issue tracker is empty and you
|
||||
need to seed the next work; when someone asks "what should we work on next", "what are
|
||||
the biggest open problems", "is this idea worth pursuing", or "how do I propose/validate
|
||||
a new improvement"; or when starting an experiment, evaluation harness, accuracy
|
||||
measurement, performance study, or multi-tenant/SaaS feasibility study. Trigger
|
||||
keywords - no open issues, roadmap, next steps, research, experiment, hypothesis,
|
||||
golden set, eval harness, accuracy of OCR/VIN extraction, multi-user readiness.
|
||||
---
|
||||
|
||||
# MotoVaultPro Research Frontier
|
||||
|
||||
Everything in this file is labeled **candidate/open**. Nothing here is a commitment,
|
||||
a roadmap, or owner-approved scope. This skill exists because the issue tracker was
|
||||
fully drained (zero open issues as of 2026-07-07) and the next work must be seeded
|
||||
deliberately, not by whichever bug happens next.
|
||||
|
||||
## When to use / when NOT to use
|
||||
|
||||
Use this skill when:
|
||||
- You need to propose new work and there is no open issue telling you what to do.
|
||||
- You are about to start an experiment, measurement, or feasibility study and need
|
||||
the discipline that makes its result acceptable (Part B).
|
||||
- Someone asks whether one of the five frontier problems below is real or already solved.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- Executing the issue/branch/PR workflow itself -- `mvp-change-control`.
|
||||
- Diagnosing a live failure -- `mvp-debugging-playbook`, then `mvp-failure-archaeology`
|
||||
to check whether the battle is already settled.
|
||||
- The CI/deploy-safety hole specifically -- `mvp-deploy-safety-campaign` is the
|
||||
executable campaign; Problem 3 below is only its extended end-state.
|
||||
- Root-cause evidence standards and benchmark discipline in depth -- `mvp-proof-and-analysis-toolkit`
|
||||
(this skill cross-references it, not replaces it).
|
||||
- What must be true before launch or public claims -- `mvp-launch-readiness`.
|
||||
- OCR/Gemini subsystem mechanics -- `mvp-ocr-gemini-pipeline`.
|
||||
- Running tests / what counts as validation evidence -- `mvp-validation-and-qa`.
|
||||
|
||||
Project reality this skill assumes (owner-confirmed 2026-07-07): development is done by
|
||||
AI sessions in this repo, the human owner reviews PRs, and end-to-end verification
|
||||
happens on staging via the PR deploy pipeline because there is no fully working local
|
||||
dev loop. Two owner non-negotiables apply to every experiment below: (1) no destructive
|
||||
database operations without a fresh backup (this includes the backend integration tests,
|
||||
which `DROP TABLE ... CASCADE` on the shared dev database -- see
|
||||
`backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts:38`);
|
||||
(2) never hand-edit files on staging/prod servers -- `rsync --delete` on every deploy
|
||||
reverts them (`.gitea/workflows/staging.yaml:117-118`, `production.yaml:108-109`).
|
||||
|
||||
---
|
||||
|
||||
# PART A -- Frontier problems (all candidate, verified against the repo 2026-07-07)
|
||||
|
||||
| # | Problem | One-line asset | Falsifiable milestone |
|
||||
|---|---------|----------------|-----------------------|
|
||||
| 1 | LLM-extraction evaluation harness | Real extraction pipeline + real receipts | A model-swap PR shows a scorecard diff, not vibes |
|
||||
| 2 | Type-safe DB boundary | Recurring-bug corpus (#239/#241/#244; #243 reported but not confirmed) + shipped detector script | Detector returns zero suspects AND a regression gate fails a planted bug |
|
||||
| 3 | CI as a product asset | Working per-PR staging deploy pipeline to extend | A red PR cannot merge; staging never clobbered by unmerged PRs |
|
||||
| 4 | Async OCR service | Known sync-in-async defect with a measurable symptom | /health p99 < 1s during saturated VIN decode, zero healthcheck restarts |
|
||||
| 5 | Multi-user/SaaS readiness study | Post-#206 UUID identity scoping already done | Written go/no-go with each blocker proven by a test |
|
||||
|
||||
## Problem 1: LLM-extraction evaluation harness (candidate)
|
||||
|
||||
**Why the current state fails.** Receipt and VIN extraction accuracy has never been
|
||||
measured. All OCR tests mock Gemini and generate synthetic images inline with PIL
|
||||
(e.g. `ocr/tests/test_receipt_extraction.py:269-291` builds a blank 100x100 image);
|
||||
there is no fixtures directory of real labeled images anywhere under `ocr/tests/`.
|
||||
Consequences already observed: the deployed model was changed to a preview model
|
||||
(`GEMINI_MODEL: gemini-3-flash-preview`, `docker-compose.yml:210`, overriding the
|
||||
`gemini-2.5-flash` code default in `ocr/app/config.py`) with no before/after accuracy
|
||||
evidence. Every future model or prompt change is currently judged by eyeball.
|
||||
|
||||
**This project's asset.** A complete, deployed extraction pipeline
|
||||
(`ocr/app/extractors/`, `ocr/app/routers/{extract,decode}.py`) with known output
|
||||
schemas to score against: fuel receipts produce `totalAmount`, `fuelQuantity`,
|
||||
`pricePerUnit`, `fuelGrade` (`ocr/app/extractors/fuel_receipt.py:111-147`);
|
||||
maintenance receipts produce `serviceName`, `serviceDate`, `totalCost`, `shopName`,
|
||||
`laborCost`, `partsCost`, `odometerReading`
|
||||
(`ocr/app/extractors/maintenance_receipt_extractor.py:40-46`); VIN decode produces
|
||||
year/make/model/trimLevel/engine/transmission (`ocr/app/routers/decode.py`, response
|
||||
model in `ocr/app/models/schemas.py`). Real usage exists: the owner's own uploaded
|
||||
documents persist in the `data/documents/` volume (treat as PII -- get owner sign-off
|
||||
and strip identifying content before committing any image to git).
|
||||
|
||||
**First three steps in this repo:**
|
||||
1. Create `ocr/tests/fixtures/golden/` with `receipts/`, `maintenance/`, `vins/`
|
||||
subdirectories and one `labels.json` per subdirectory mapping filename to expected
|
||||
field values (start with N=10 per category; owner-supplied, sanitized images).
|
||||
2. Write `ocr/tests/test_golden_extraction.py`: for each labeled image, call the
|
||||
extractor and score field-level precision/recall (a field counts as correct only on
|
||||
exact match after normalization; report per-field and aggregate). Gate the whole
|
||||
module behind an env var (e.g. `GOLDEN_EVAL=1` + `pytest.mark.skipif`) so the
|
||||
default `cd ocr && python -m pytest` run stays hermetic -- live Gemini credentials
|
||||
(the WIF chain, see `mvp-ocr-gemini-pipeline`) exist only in containers.
|
||||
3. Run the baseline on staging (the only environment with working credentials): shell
|
||||
into the OCR container and run the gated suite; commit the resulting scorecard JSON
|
||||
next to the fixtures with the model name and date. This number is the baseline every
|
||||
future prompt/model PR must diff against.
|
||||
|
||||
**You have a result when** a PR that changes `GEMINI_MODEL`, a prompt, or an extractor
|
||||
includes a before/after scorecard produced by the harness on the same golden set --
|
||||
and a reviewer can reject it because a number went down. Falsified if, after building
|
||||
it, scores are so noisy across identical runs (LLM nondeterminism) that diffs are
|
||||
meaningless; that outcome itself is a publishable finding (then measure variance first
|
||||
and report mean-of-3).
|
||||
|
||||
## Problem 2: Type-safe DB boundary -- eliminate the pg-numeric bug class (candidate)
|
||||
|
||||
**Why the current state fails.** node-postgres returns NUMERIC/DECIMAL (PostgreSQL
|
||||
type OID 1700) as strings. `backend/src/core/config/database.ts:12` overrides only the
|
||||
DATE parser (OID 1082 -> plain string), so every repository mapper must hand-coerce
|
||||
numerics. This produced at least three shipped bugs (#239, #241, #244 -- #243 was
|
||||
reported for stations but the stations mappers already coerced via parseFloat, see
|
||||
`mvp-failure-archaeology` incident 12) with inconsistent coercion styles across
|
||||
repositories, and #244 showed a second failure
|
||||
shape: "enhanced" methods returning raw rows that bypass the mapper entirely. The
|
||||
convention is enforced by review only; nothing stops the next repository from
|
||||
reintroducing it.
|
||||
|
||||
**This project's asset.** The bug class is fully characterized (see
|
||||
`mvp-failure-archaeology`), and a working detector already ships:
|
||||
`.claude/skills/mvp-diagnostics-and-logging/scripts/check-numeric-coercion.sh`
|
||||
(harvests numeric columns from migration SQL, flags uncoerced `row.<col>` reads and
|
||||
raw-row returns in `*.repository.ts`; exit 0 = clean). There is also an unused codegen
|
||||
seed: `cd backend && npm run schema:generate` (`backend/src/_system/schema/generate.ts`).
|
||||
|
||||
**First three steps in this repo:**
|
||||
1. Run the detector and record the current suspect list as the baseline:
|
||||
`bash .claude/skills/mvp-diagnostics-and-logging/scripts/check-numeric-coercion.sh`.
|
||||
2. Decide the mechanism with an explicit trade-off writeup (stress-test per
|
||||
`mvp-proof-and-analysis-toolkit`). Three options, in ascending ambition:
|
||||
(a) shared `mapNumeric()` helper in `backend/src/core/` that every mapper calls;
|
||||
(b) global OID 1700 parser in `backend/src/core/config/database.ts` -- the OID 1082
|
||||
override at line 12 is the successful precedent, but weigh honestly: a global parser
|
||||
changes every consumer at once, and `parseFloat` on money columns trades string
|
||||
safety for float representation (audit currency columns before choosing);
|
||||
(c) schema-derived row types via `schema:generate` so uncoerced reads become
|
||||
compile errors. Record the decision and the rejected options in the issue.
|
||||
3. Fix all current suspects in one PR, then add the regression gate: wire the detector
|
||||
into `backend/package.json` lint (or an eslint `no-restricted-syntax` rule in
|
||||
`backend/eslint.config.js` banning `return result.rows` in repository files --
|
||||
candidate, feasibility unproven) so a new violation fails locally and, once Problem 3
|
||||
lands, in CI.
|
||||
|
||||
**You have a result when** (a) the detector script exits 0 on main, AND (b) a
|
||||
deliberately planted violation -- an uncoerced `row.price` read added on a scratch
|
||||
branch -- is caught by the automated gate without human review. Falsified if the global
|
||||
parser (option b) breaks an existing consumer that depended on string numerics: that is
|
||||
a finding, not a failure; document it in `mvp-failure-archaeology` and fall back to (a).
|
||||
|
||||
## Problem 3: CI as a product asset (candidate -- end-state extension of the campaign)
|
||||
|
||||
Prerequisite: read and follow `mvp-deploy-safety-campaign`. This entry only records
|
||||
the frontier beyond that campaign's scope. Do not start here first.
|
||||
|
||||
**Why the current state fails.** CI gates nothing beyond build + boot (canonical
|
||||
statement: `mvp-validation-and-qa` section 1). Verified sharp edges: `staging.yaml` triggers on `pull_request` (line 12) and every PR
|
||||
build tags and pushes `:latest` (lines 58-98), so an unmerged PR overwrites both the
|
||||
shared staging environment and the very tag that `production.yaml` deploys by default
|
||||
(`image_tag` default `'latest'`, `production.yaml:17`). The mobile+desktop hard
|
||||
requirement is validated nowhere: the single Cypress spec
|
||||
(`frontend/cypress/e2e/stations.cy.ts`) has no config and cannot run.
|
||||
|
||||
**This project's asset.** A working self-hosted pipeline that already builds, deploys,
|
||||
and health-verifies a full stack per PR -- most projects have to build that from
|
||||
scratch; here only the gates and isolation are missing.
|
||||
|
||||
**First three steps in this repo** (after the campaign's own steps):
|
||||
1. Split image tagging in `.gitea/workflows/staging.yaml`: PR builds push only
|
||||
`:{shortsha}` (and optionally `:pr-{n}`); `:latest` moves only on push to `main`.
|
||||
This alone ends the unmerged-code-to-prod path.
|
||||
2. Add a test job ahead of `build`: backend unit tests
|
||||
(`cd backend && npm test -- --forceExit`, excluding `tests/integration` unless the
|
||||
job provisions an ephemeral postgres+redis -- never point CI tests at a shared
|
||||
database; they DROP TABLE CASCADE), backend/frontend `npm run lint` and
|
||||
`npm run type-check`, and `cd ocr && python -m pytest`. Red job blocks merge.
|
||||
3. Add a post-deploy viewport smoke job: Playwright (dep already in root
|
||||
`package.json`, currently dead -- no config exists) hitting
|
||||
`https://staging.motovaultpro.com` at 375x812 and 1280x800, asserting login page
|
||||
renders and no console errors. This is the first automated teeth behind the
|
||||
mobile+desktop rule.
|
||||
|
||||
**You have a result when** (a) a PR containing a deliberately failing unit test cannot
|
||||
be merged (workflow red, branch protection enforcing it), AND (b) after a PR build
|
||||
completes, `docker manifest inspect` (or registry UI) shows `:latest` unchanged --
|
||||
staging mainline is never clobbered by an unmerged PR. Per-PR ephemeral environments or
|
||||
a namespaced staging are the stretch goal, candidate only; the milestone above does not
|
||||
require them.
|
||||
|
||||
## Problem 4: Async OCR service -- Gemini off the event loop (candidate)
|
||||
|
||||
**Why the current state fails.** `ocr/app/routers/decode.py` calls the synchronous,
|
||||
unbounded `_gemini_engine.decode_vin(vin)` directly inside an `async def` handler
|
||||
(same pattern for sync receipt paths in `routers/extract.py`); only the manual-PDF path
|
||||
uses `run_in_executor` (`extract.py:472`, `jobs.py:131`). A Search-grounded VIN decode
|
||||
has been observed taking 34s+ (issue #230 history), during which the entire uvicorn
|
||||
event loop -- including `GET /health` (`ocr/app/main.py:44`) -- is blocked. The Docker
|
||||
healthcheck is `interval: 5s, timeout: 5s, retries: 3` (`docker-compose.yml`), so a
|
||||
~15s blockage can get the container killed mid-request. There is no timeout on the
|
||||
Gemini call itself (the only guards are the upstream HTTP timeouts: backend
|
||||
`OCR_TIMEOUT_MS = 120000`, `backend/src/features/ocr/external/ocr-client.ts:9`, and the
|
||||
frontend's 120s at `frontend/src/features/vehicles/api/vehicles.api.ts:90` -- equal
|
||||
values, no headroom), and no retry for transient failures.
|
||||
|
||||
**This project's asset.** The defect has a crisp, cheap-to-measure symptom, an in-repo
|
||||
reference implementation of the fix pattern (`extract.py:472`), and duration
|
||||
instrumentation already flowing to Loki: backend logs `msg="Request processed"` with a
|
||||
`duration` field (`backend/src/core/plugins/logging.plugin.ts:20-25`), so
|
||||
`POST /api/vehicles/decode-vin` latency is queryable in Grafana (see
|
||||
`mvp-diagnostics-and-logging`).
|
||||
|
||||
**First three steps in this repo:**
|
||||
1. Baseline with predicted numbers first (Part B discipline). Prediction: with K=3
|
||||
concurrent VIN decodes in flight, OCR `/health` response time exceeds 5s and the
|
||||
container restarts. Measure on staging: fire concurrent decodes through the API,
|
||||
while a loop times `/health` from inside the network
|
||||
(`docker exec mvp-ocr-staging curl -s -w '%{time_total}\n' -o /dev/null http://localhost:8000/health`)
|
||||
and `docker events`/restart count watches for healthcheck kills.
|
||||
2. Fix the blocking call: in `ocr/app/routers/decode.py`, wrap the engine call --
|
||||
`await asyncio.get_running_loop().run_in_executor(None, _gemini_engine.decode_vin, vin)`
|
||||
-- mirroring `extract.py:472`; repeat for the sync receipt handlers in `extract.py`.
|
||||
(The google-genai async client `client.aio` is an alternative -- UNVERIFIED against
|
||||
the installed SDK version; `google-genai>=1.0.0` is an unpinned floor in
|
||||
`ocr/requirements.txt:25` and an unsupported config field already broke VIN decode
|
||||
on staging once, per `mvp-failure-archaeology`. Test any SDK-surface change against
|
||||
the actual deployed image before trusting it.)
|
||||
3. Add a service-side timeout shorter than the 120s client timeouts (e.g. 90s around
|
||||
the executor future) plus one retry with jitter for transient Gemini errors, then
|
||||
rerun the step-1 measurement unchanged.
|
||||
|
||||
**You have a result when** during the same saturation experiment as the baseline,
|
||||
`/health` p99 stays under 1s, the container records zero healthcheck restarts, and VIN
|
||||
decode success latency is unchanged within 10%. Falsified if p99 stays >1s after the
|
||||
executor change -- that means the bottleneck is elsewhere (e.g. thread-pool exhaustion
|
||||
or PaddleOCR model loading), which redirects the work rather than ending it.
|
||||
|
||||
## Problem 5: Multi-user/SaaS readiness study (candidate -- study, not build)
|
||||
|
||||
**Why the current state fails.** The product is single-tenant by declaration, heading
|
||||
toward paying users; nobody knows how far the gap to 2+ real users actually is, so
|
||||
pricing/positioning decisions (see `mvp-launch-readiness`) rest on an unexamined
|
||||
assumption in both directions.
|
||||
|
||||
**This project's asset.** The hardest prerequisite is already done: issue #206 migrated
|
||||
all user identity to UUIDs referencing `user_profiles.id` across 17 feature tables
|
||||
(`backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sql`).
|
||||
Note precisely: the columns are still NAMED `user_id` (the migration renames
|
||||
`user_profile_id` back to `user_id` at lines 324+ after backfill) but they now hold
|
||||
`user_profiles.id` UUIDs -- if any doc says the columns are named `user_profile_id`,
|
||||
the code wins. Auth already resolves each request to that UUID
|
||||
(`backend/src/core/plugins/auth.plugin.ts:165`) and repositories scope queries by it.
|
||||
Stripe scaffolding exists per-customer already (`subscriptions` table with
|
||||
`stripe_customer_id`, made nullable for admin overrides in
|
||||
`backend/src/features/subscriptions/migrations/002_nullable_stripe_customer_id.sql`),
|
||||
and an `admin_users` table exists (`backend/src/features/admin/migrations/001_create_admin_users.sql`).
|
||||
|
||||
**First three steps in this repo:**
|
||||
1. Enumerate candidate blockers by inspection, each phrased as a testable claim.
|
||||
Verified starting list (2026-07-07): no Row Level Security anywhere (zero RLS
|
||||
statements in any migration SQL -- isolation is 100% application-layer WHERE
|
||||
clauses); the tier guard fails open on unknown feature keys
|
||||
(`backend/src/core/config/feature-tiers.ts:57-63` returns true for unregistered
|
||||
keys); auth is per-route `preHandler`, not a global hook, so a forgotten preHandler
|
||||
is an open route; Redis cache keys and the fuel-logs cache invalidation are
|
||||
per-user but the known delete-invalidation miss (imperial-only) would leak stale
|
||||
data per user; vehicle limits free=2/pro=5/enterprise=unlimited
|
||||
(`feature-tiers.ts:92-95`) assume per-user enforcement paths all consult the same
|
||||
counter.
|
||||
2. Run the two-user experiment on staging (never dev/prod without a fresh backup):
|
||||
create a second Auth0 user, then for each feature capsule attempt cross-user reads
|
||||
and writes through the API (user B requests user A's vehicle/fuel-log/document IDs).
|
||||
Record every endpoint that returns another user's data or mutates it. This is the
|
||||
ground truth the study stands on.
|
||||
3. Write the go/no-go document as a table: blocker, evidence (the failing/passing test
|
||||
or request transcript from step 2), remediation class (WHERE-clause audit vs RLS vs
|
||||
per-tenant DB vs admin model vs Stripe webhook multiplexing), rough size. File one
|
||||
issue per confirmed blocker, labeled `type/chore` or `type/feature`, all
|
||||
`status/backlog`.
|
||||
|
||||
**You have a result when** the go/no-go document exists with every enumerated blocker
|
||||
backed by a concrete test or transcript -- including the negative results ("tried
|
||||
cross-user read on all 21 capsules' endpoints; N leaked, M correctly 403/404"). A
|
||||
study that finds zero blockers is a result; a study with unproven blockers is not.
|
||||
|
||||
---
|
||||
|
||||
# PART B -- Methodology: from hunch to accepted result
|
||||
|
||||
## The evidence bar
|
||||
|
||||
A claim (root cause, improvement, feasibility verdict) is accepted here only when:
|
||||
|
||||
1. **One mechanism explains ALL observations, including the negatives.** If your
|
||||
explanation covers the failure but not why the same code path works elsewhere, it
|
||||
is incomplete. (Full protocol and worked examples: `mvp-proof-and-analysis-toolkit`.)
|
||||
2. **It survives assigned adversarial refutation.** Before acting, spend one explicit
|
||||
pass (or a spawned subagent) trying to break the conclusion: what observation would
|
||||
disprove it, and did you look for that observation? A conclusion nobody tried to
|
||||
kill is a guess with confidence.
|
||||
3. **The result is reproducible from the artifacts in the repo** -- fixture set,
|
||||
script, LogQL query, or test -- not from a session transcript.
|
||||
|
||||
## Hypothesis predicts numbers BEFORE running
|
||||
|
||||
Write the predicted number down (in the issue or PR description) before running the
|
||||
measurement. "Wrapping the Gemini call in an executor will take /health p99 from >5s
|
||||
to <1s under 3 concurrent decodes" is a hypothesis; "made it async, seems snappier" is
|
||||
not. If you cannot predict a number, you do not yet understand the mechanism -- go
|
||||
back to step 1. After measuring, report predicted vs observed, especially when wrong:
|
||||
a wrong prediction that gets explained is worth more than a right one that does not.
|
||||
|
||||
## The idea lifecycle in this repo
|
||||
|
||||
```
|
||||
hunch --> issue --> gated experiment --> adopted | retired
|
||||
```
|
||||
|
||||
| Stage | Concrete form here |
|
||||
|-------|--------------------|
|
||||
| Hunch | A suspicion from a bug, a log pattern, or this file. Costs nothing; commits nothing. |
|
||||
| Issue | File it via Gitea MCP tools, labeled `type/feature` or `type/chore`, `status/backlog`. State the falsifiable milestone ("result when...") in the body. No sprints/milestones -- work flows directly from issues (owner directive 2026-05-12). |
|
||||
| Gated experiment | A branch (`issue-{n}-{slug}`) or a config flag, with the measurable gate declared up front and the predicted numbers written down. Staging is the lab; respect both non-negotiables. |
|
||||
| Adopted | Merged per `mvp-change-control`, documented per `mvp-docs-and-writing`, and -- if it settled a question -- an entry in `mvp-failure-archaeology` so it is not relitigated. |
|
||||
| Retired | Written down in `mvp-failure-archaeology` with the evidence that killed it. A retired idea with documented cause is a settled battle; an undocumented one will be re-fought by a future session that has no memory of yours. |
|
||||
|
||||
## Where good ideas have historically come from in this repo
|
||||
|
||||
Use these patterns as prospecting ground, verified in git history:
|
||||
|
||||
- **User-pain bug clusters -> systemic fixes.** One numeric-as-string bug report
|
||||
(#239, maintenance cost blank on the vehicle summary) triggered an audit that fixed
|
||||
the same defect class in maintenance/ownership-costs (#241) and fuel-logs (#244)
|
||||
within a day, and confirmed stations (#243) already coerced (see
|
||||
`mvp-failure-archaeology` incident 12) -- and ultimately the detector script and
|
||||
Problem 2 above. When you fix a bug, always ask: where else does this exact shape
|
||||
exist? The audit is usually cheaper than the next bug.
|
||||
- **Audits -> capsules.** The December 2025 security audit (`docs/AUDIT.md`, now
|
||||
partially stale -- treat its findings as historical, code wins) recommended audit
|
||||
logging; a full `backend/src/features/audit-log/` capsule now exists. A written
|
||||
audit with concrete findings reliably converts into shipped features.
|
||||
- **Incidents -> settled constraints.** The google-genai unsupported-config breakage
|
||||
(AFC parameter, reverted in commits `1add6c8`/`56df5d4`) produced the standing rule
|
||||
"test SDK-surface changes against the deployed image" that Problem 4 inherits.
|
||||
Incidents are expensive; extracting a constraint from each one is how the cost is
|
||||
amortized.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct repo inspection at commit `e729d42` (main). Everything
|
||||
in Part A is a point-in-time snapshot; re-verify before acting. Volatile facts and
|
||||
their re-verification commands:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|--------------------------|----------------|
|
||||
| Issue tracker has zero open issues | Gitea MCP `list_repo_issues` with `state=open` |
|
||||
| CI runs zero tests/lint; PR builds push `:latest` | `grep -n "npm test\|pytest\|latest" .gitea/workflows/staging.yaml` |
|
||||
| Prod deploy defaults to `latest` | `grep -n "default" .gitea/workflows/production.yaml \| head -5` |
|
||||
| Only DATE (OID 1082) parser overridden, not NUMERIC (1700) | `grep -n setTypeParser backend/src/core/config/database.ts` |
|
||||
| Numeric-coercion detector exists and its current verdict | `bash .claude/skills/mvp-diagnostics-and-logging/scripts/check-numeric-coercion.sh` |
|
||||
| No golden fixtures dir in OCR tests | `ls ocr/tests/fixtures 2>/dev/null` |
|
||||
| VIN decode is sync-in-async; executor used only in manual path | `grep -rn run_in_executor ocr/app` and read `ocr/app/routers/decode.py` |
|
||||
| OCR healthcheck 5s/5s/3 | `grep -A4 healthcheck docker-compose.yml` (mvp-ocr block) |
|
||||
| `google-genai>=1.0.0` unpinned; deployed model is `gemini-3-flash-preview` | `grep genai ocr/requirements.txt; grep GEMINI_MODEL docker-compose.yml` |
|
||||
| Client timeouts both 120s (frontend + backend OCR client) | `grep -n timeout frontend/src/features/vehicles/api/vehicles.api.ts backend/src/features/ocr/external/ocr-client.ts` |
|
||||
| User-scoping columns named `user_id` but hold `user_profiles.id` UUIDs | `grep -n "RENAME COLUMN" backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sql` |
|
||||
| No RLS anywhere | `grep -rin "row level security" backend/src --include='*.sql'` |
|
||||
| Tier guard fails open on unknown keys; limits free=2/pro=5 | `grep -n -A5 canAccessFeature backend/src/core/config/feature-tiers.ts` |
|
||||
| `stripe_customer_id` nullable (admin overrides) | `ls backend/src/features/subscriptions/migrations/` |
|
||||
@@ -1,185 +0,0 @@
|
||||
---
|
||||
name: mvp-run-and-operate
|
||||
description: Operate MotoVaultPro environments - load when deploying to staging or production, rolling back a bad deploy, switching blue/green traffic, backing up or restoring the database, refreshing staging data from prod, running a breaking migration, or diagnosing "staging is down after my PR", "prod deployed the wrong code", "state.json says blue but green is serving", "config change did not apply to postgres/redis/grafana", "OCR blipped during deploy", "staging runner disk full", or "cert renewal failed". Covers compose file stacking, container names, the staging and production Gitea workflows, switch-traffic/health-check/auto-rollback scripts, and the legacy rollback.sh hazard.
|
||||
---
|
||||
|
||||
# Run and Operate MotoVaultPro
|
||||
|
||||
**When to use:** deploying, rolling back, switching traffic, backing up/restoring the database, refreshing staging data, running breaking migrations, or debugging a broken environment (staging or prod down, wrong version serving, config not applied).
|
||||
|
||||
**When NOT to use:**
|
||||
- Getting a dev machine working, local unit tests, `make setup` -> `mvp-build-and-env`
|
||||
- Reading logs, Grafana/Loki queries, health-endpoint internals -> `mvp-diagnostics-and-logging`
|
||||
- What CI actually gates (spoiler: no tests, no lint) and PR/issue workflow -> `mvp-change-control` and `mvp-validation-and-qa`
|
||||
- Fixing the systemic deploy-safety hole (PR builds clobbering `:latest`, prod defaulting to `latest`) -> `mvp-deploy-safety-campaign`
|
||||
- Why the architecture is shaped this way -> `mvp-architecture-contract`
|
||||
- Past incidents and reverts -> `mvp-failure-archaeology`
|
||||
|
||||
Reality check for this project (2026-07-07): development is done by AI sessions; the human owner reviews PRs; end-to-end verification happens on STAGING via the PR deploy pipeline because there is no fully working local dev loop. Deploys are how changes get verified. Treat this file as the runbook.
|
||||
|
||||
## 1. Environment matrix
|
||||
|
||||
Verified against `docker-compose*.yml` and `.gitea/workflows/{staging,production}.yaml` (2026-07-07).
|
||||
|
||||
| | Dev (local) | Staging | Production |
|
||||
|---|---|---|---|
|
||||
| Compose stacking | `docker-compose.yml` alone (base, builds images locally) | base + `docker-compose.staging.yml` | base + `docker-compose.blue-green.yml` + `docker-compose.prod.yml` |
|
||||
| Server path | your machine | `/opt/motovaultpro` on `mvp-staging` (also the CI runner, label `stage`) | `/opt/motovaultpro` on prod server (internal `172.30.1.36`; runner label `prod`) |
|
||||
| Container names | `mvp-traefik`, `mvp-frontend`, `mvp-backend`, `mvp-ocr`, `mvp-postgres`, `mvp-redis`, `mvp-loki`, `mvp-alloy`, `mvp-grafana` | app containers get a `-staging` suffix (`mvp-traefik/frontend/backend/ocr/postgres/redis-staging`); the logging trio keeps base names (`mvp-loki`, `mvp-alloy`, `mvp-grafana` - no container_name override in the staging overlay) | shared: `mvp-traefik`, `mvp-ocr`, `mvp-postgres`, `mvp-redis`, `mvp-loki`, `mvp-alloy`, `mvp-grafana`; paired: `mvp-frontend-blue/green`, `mvp-backend-blue/green` |
|
||||
| URLs | https://motovaultpro.com via hosts file + self-signed cert | https://staging.motovaultpro.com, https://logs.staging.motovaultpro.com, https://traefik.staging.motovaultpro.com | https://motovaultpro.com, https://logs.motovaultpro.com |
|
||||
| Images | built from source (`build:` in base compose) | `git.motovaultpro.com/egullickson/{backend,frontend,ocr}:<7-char-sha>` via `BACKEND_IMAGE`/`FRONTEND_IMAGE`/`OCR_IMAGE` env | same registry; tag = `workflow_dispatch` input `image_tag`, DEFAULT `latest` (dangerous, see traps) |
|
||||
| Volumes | `mvp_postgres_data`, `mvp_redis_data`; host ports 5432/6379 exposed | `mvp_postgres_staging_data`, `mvp_redis_staging_data` | base volume names; `docker-compose.prod.yml` sets `ports: []` on postgres/redis (no host exposure) |
|
||||
| LOG_LEVEL | debug (compose defaults) | DEBUG (`staging.yaml` env) | INFO (`production.yaml` env) |
|
||||
| Who deploys | you, manually | CI, automatically: **every push to main AND every PR open/sync/reopen**. Full `down`/`up` = brief outage every deploy; concurrent PRs mean **last PR wins** the shared environment | human, manually: `workflow_dispatch` on `production.yaml` with `image_tag` input |
|
||||
|
||||
Compose inheritance detail (prod): the merged prod config STILL contains the base single-stack `mvp-frontend`/`mvp-backend` services. The workflow avoids them by always naming services in `up -d`. **Never run a bare `up -d` with the prod file stack** -- it would start the base containers alongside blue/green.
|
||||
|
||||
## 2. Staging deploy anatomy
|
||||
|
||||
Source of truth: `.gitea/workflows/staging.yaml`. Triggers: push to `main` and every PR sync. All jobs run on the staging host itself.
|
||||
|
||||
1. **build**: docker login to `git.motovaultpro.com`; builds backend, frontend (Vite build args from Gitea vars, including `VITE_STRIPE_PUBLISHABLE_KEY`), and ocr; tags each `:<short-sha>` AND `:latest`; pushes all six tags. This is where PR builds clobber `:latest`.
|
||||
2. **deploy-staging**:
|
||||
- `rsync -av --delete` of `config/` and `scripts/` into `/opt/motovaultpro` (wipes any on-server edits -- this is why hand-editing server files is a non-negotiable "never")
|
||||
- copies compose files and `secrets/app/google-wif-config.json`
|
||||
- generates `.env` (Stripe price IDs, `GRAFANA_ADMIN_PASSWORD`, then `scripts/ci/generate-log-config.sh DEBUG >> .env` which fans one LOG_LEVEL out to backend/traefik/postgres/redis settings)
|
||||
- `scripts/inject-secrets.sh` writes the real secret files from Gitea Actions secrets
|
||||
- init data dirs: `data/backups` and `data/documents` chown 1001:1001; `data/traefik/acme.json` chmod 600
|
||||
- pulls images, then `compose down --timeout 30` followed by `up -d` -- **staging takes full downtime on every deploy**
|
||||
3. **verify-staging**:
|
||||
- asserts the 5 app containers are running, then polls Docker healthchecks up to 48 x 5s = 4 minutes per service. The backend has `start_period: 180s` because auto-migrations run on container start (`node dist/_system/migrations/run-all.js`) -- a slow migration eats into this budget.
|
||||
- in-container `curl localhost:3001/health`
|
||||
- external check: `https://staging.motovaultpro.com/api/health` must return `status: healthy` with all **13 required features**: `admin, auth, onboarding, vehicles, documents, fuel-logs, stations, maintenance, platform, notifications, user-profile, user-preferences, user-export`
|
||||
4. **notify** success/failure via `scripts/ci/notify.sh` (Resend email).
|
||||
|
||||
This pipeline is also the only PR gate: green means "3 images built and staging boots healthy", nothing more (no tests, no lint -- see `mvp-validation-and-qa`).
|
||||
|
||||
## 3. Production deploy anatomy (blue-green)
|
||||
|
||||
Source of truth: `.gitea/workflows/production.yaml`. Manual `workflow_dispatch` only. Blue-green = two parallel frontend+backend pairs behind Traefik weighted routing; only one pair ("stack") receives traffic at a time.
|
||||
|
||||
1. **validate**: reads `/opt/motovaultpro/config/deployment/state.json` `.active_stack` (defaults to blue if missing) and targets the OPPOSITE color.
|
||||
2. **deploy-prod**:
|
||||
- sparse checkout; `rsync --delete` of `config/` and `scripts/` (this RESETS `state.json` and the Traefik weights file to repo defaults -- see traps)
|
||||
- `.env` generation + `inject-secrets.sh` + data-dir init, same as staging
|
||||
- pull images for the chosen tag, record image IDs
|
||||
- `up -d --no-recreate mvp-postgres mvp-redis mvp-loki mvp-alloy mvp-grafana` -- shared services persist untouched across deploys (config changes to them do NOT apply; see traps)
|
||||
- wait for postgres/redis healthy
|
||||
- `up -d --force-recreate mvp-ocr mvp-frontend-$TARGET mvp-backend-$TARGET` -- note `mvp-ocr` is a single shared instance and is force-recreated **every** prod deploy (brief OCR outage each time)
|
||||
- verify running image IDs match what was pulled
|
||||
- `scripts/ci/health-check.sh $TARGET 240` (container health + HTTP endpoints, 240s budget)
|
||||
- start Traefik, then `scripts/ci/switch-traffic.sh $TARGET instant` -- awk/sed-edits weights in `config/traefik/dynamic/blue-green.yml`; Traefik's file watcher applies it live; state.json updated
|
||||
3. **verify-prod**: container/health waits, in-container health, external `https://motovaultpro.com/api/health` with the same 13-feature check, then greps `blue-green.yml` to confirm the target stack weight is 100.
|
||||
4. **rollback job** (on failure): runs `scripts/ci/auto-rollback.sh` automatically. Emails on both paths.
|
||||
|
||||
Migrations: non-breaking migrations run automatically on backend container start. Breaking migrations use `.gitea/workflows/maintenance.yaml` (manual, `create_backup` input default `yes`) which runs `scripts/ci/maintenance-migrate.sh [backup]`: sets ALL four Traefik weights to 0 (site returns 503), optionally `pg_dump`s to `data/backups/`, runs the migration, restores traffic.
|
||||
|
||||
### Operator commands (on the servers, at `/opt/motovaultpro`)
|
||||
|
||||
```bash
|
||||
# Staging stack
|
||||
docker compose -f docker-compose.yml -f docker-compose.staging.yml ps
|
||||
docker compose -f docker-compose.yml -f docker-compose.staging.yml logs -f
|
||||
docker logs -f mvp-backend-staging
|
||||
|
||||
# Production stack (ALWAYS name services on up -d; never bare)
|
||||
docker compose -f docker-compose.yml -f docker-compose.blue-green.yml -f docker-compose.prod.yml ps
|
||||
cat config/deployment/state.json | jq . # which stack is active (but see trap 2)
|
||||
|
||||
# Health / traffic (DEPLOY_PATH is required so the scripts edit the live config, not a checkout)
|
||||
DEPLOY_PATH=/opt/motovaultpro ./scripts/ci/health-check.sh blue|green [timeout_s] # default 60s
|
||||
DEPLOY_PATH=/opt/motovaultpro ./scripts/ci/switch-traffic.sh blue|green [instant|gradual]
|
||||
DEPLOY_PATH=/opt/motovaultpro ./scripts/ci/auto-rollback.sh "reason"
|
||||
```
|
||||
|
||||
`gradual` mode steps weights 25 -> 50 -> 75 -> 100 with 3s intervals. Emergency both-stacks-down recovery procedure: `docs/CICD-DEPLOY.md`, "Emergency Recovery" section (down everything, restart shared services, start one stack by name, switch traffic).
|
||||
|
||||
## 4. Rollback
|
||||
|
||||
**Automatic**: the production workflow's `rollback` job runs `scripts/ci/auto-rollback.sh` on verification failure. It reads `state.json` for the current/inactive stacks, health-checks the OLD (inactive) stack for 30s, switches traffic back to it, updates state, and emails. If the old stack is also unhealthy it sends a `rollback_failed` email and exits nonzero -- that is your both-stacks-down scenario (see emergency recovery above).
|
||||
|
||||
**Manual** (on the prod server):
|
||||
|
||||
```bash
|
||||
cd /opt/motovaultpro
|
||||
DEPLOY_PATH=/opt/motovaultpro ./scripts/ci/health-check.sh blue 60 # confirm the target is healthy FIRST
|
||||
DEPLOY_PATH=/opt/motovaultpro ./scripts/ci/switch-traffic.sh blue # instant; or append "gradual"
|
||||
```
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------+
|
||||
| WARNING: scripts/rollback.sh (repo root scripts/ dir) is LEGACY AND |
|
||||
| DANGEROUS ON PROD. Verified 2026-07-07: it composes only base + prod |
|
||||
| (no blue-green file), does a full `down` of the site, git-checkouts an |
|
||||
| old commit and REBUILDS IMAGES FROM SOURCE on the prod box, then health- |
|
||||
| checks containers named mvp-frontend/mvp-backend which DO NOT EXIST under |
|
||||
| the blue-green topology. Running it takes prod down and cannot verify |
|
||||
| recovery. Never use it. Use scripts/ci/switch-traffic.sh or |
|
||||
| scripts/ci/auto-rollback.sh instead. |
|
||||
+----------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
Rolling back to an older BUILD (not just the other color): re-run the production workflow with `image_tag` set to the known-good 7-char commit SHA. That deploys the old images to the inactive color and switches to it.
|
||||
|
||||
## 5. Backup, restore, and data
|
||||
|
||||
**Non-negotiable (owner, 2026-07-07): take a fresh backup before ANY destructive database operation.** That includes `make clean` (runs `docker compose down -v --rmi all` -- destroys DB volumes wherever you run it), `import-database.sh --drop-existing`, schema migrations on staging/prod, and the backend integration tests (which `DROP TABLE ... CASCADE` on the shared dev database).
|
||||
|
||||
Layers, all verified 2026-07-07:
|
||||
|
||||
- **App-level backup feature** (`backend/src/features/backup/`): admin API at `/api/admin/backups*` (list, create, get, download, restore) plus scheduled jobs -- `backup-scheduled.job.ts` and `backup-cleanup.job.ts` (daily retention cleanup at 4 AM). Writes to `./data/backups` (bind-mounted into the backend, owner 1001).
|
||||
- **Manual export**: `scripts/export-database.sh` -- interactive, or `--env production|staging` (selects container `mvp-postgres` vs `mvp-postgres-staging`), `--format sql|custom|directory` (default sql), gzips, writes to `database-exports/` with a metadata JSON and import instructions. Make shortcuts: `make db-export`, `make db-backup`.
|
||||
- **Restore**: `scripts/import-database.sh [--env ...] <file>` -- auto-detects format, **backs up the existing database to `database-exports/` first by default** (skip with `--no-backup`; do not skip), supports `--drop-existing --create-db` gated by a typed `yes` confirmation.
|
||||
- **Prod -> staging copy**: `scripts/refresh-staging-db.sh` -- runs ON the staging server as `act_runner` (SSH to `[email protected]` is a prerequisite, setup steps in the script header). Backs up staging first (unless `--skip-backup`), streams a prod `pg_dump` over SSH, stops `mvp-backend-staging`, imports, restarts. **Always run `--dry-run` first.**
|
||||
- **Pre-migration backup**: `scripts/ci/maintenance-migrate.sh backup` / the maintenance workflow's `create_backup: yes` input.
|
||||
|
||||
**`data/documents` is the ONLY copy of user-uploaded documents** (bind mount on the server, on both staging and prod backends). It is not in any database dump; it is covered only by the app-level backup feature. Never delete or re-provision that directory without a copy elsewhere.
|
||||
|
||||
```bash
|
||||
# Copy-paste safety sequence before anything destructive
|
||||
./scripts/export-database.sh --env production --format custom # or --env staging
|
||||
ls -lh database-exports/ | tail -3 # confirm the file exists and is non-trivial in size
|
||||
```
|
||||
|
||||
## 6. Operational traps
|
||||
|
||||
Each verified against workflows/scripts/compose, 2026-07-07.
|
||||
|
||||
| # | Trap | Consequence |
|
||||
|---|------|-------------|
|
||||
| 1 | Staging builds push `:latest` on EVERY PR sync, and the prod workflow's `image_tag` DEFAULTS to `latest` | Deploying prod with the default tag can ship an unmerged PR build to production. **Always deploy prod by 7-char SHA tag.** The systemic fix is the subject of `mvp-deploy-safety-campaign`. |
|
||||
| 2 | `rsync --delete config/` on every deploy resets `config/deployment/state.json` (repo default: active=blue) and `config/traefik/dynamic/blue-green.yml` (repo default: blue=100/green=0) | While green is active, a prod deploy has a window between rsync and switch-traffic where weights point at blue; `state.json` read mid-deploy lies; deployment history is erased every deploy. Never trust mid-deploy state.json; never hand-edit these files on the server (the next deploy reverts them anyway). |
|
||||
| 3 | Shared services start with `--no-recreate` on prod deploys | Config changes to postgres, redis, loki, alloy, or grafana do NOT take effect from a normal prod deploy. You must manually recreate the specific service on the prod server: `docker compose -f docker-compose.yml -f docker-compose.blue-green.yml -f docker-compose.prod.yml up -d --force-recreate mvp-grafana` (name only the service you changed). |
|
||||
| 4 | `mvp-ocr` is a single shared instance and is `--force-recreate`d on every prod deploy | Brief OCR outage during every prod deploy; in-flight OCR jobs at switch time fail. Not blue-greened. |
|
||||
| 5 | Staging runner root disk is 29G and every commit AND every PR sync builds 3 SHA-tagged images on it (containerd snapshotter) | Disk fills; builds and deploys start failing. A daily `docker system prune` cron exists on the box but is OUT-OF-BAND (deliberately not in the repo -- the standing exception to "no hand-edited server state"). Re-running `ansible/deploy-staging-runner.yml` or rebuilding the box DROPS it -- re-install the prune cron afterward. That playbook also REMOVES a "legacy Docker cleanup" cron that used to destroy volumes -- do not resurrect that one. Registry-side cleanup: `scripts/ci/purge-container-images.sh --token=PAT --dry-run` first. |
|
||||
| 6 | TLS certs renew via Let's Encrypt Cloudflare DNS challenge (`config/traefik/traefik.yml`; token from `secrets/app/cloudflare-dns-token.txt`, injected from the `CF_DNS_API_TOKEN` Gitea secret) | If the Cloudflare token rots, renewals fail silently until the cert expires. ACME state lives in `data/traefik/acme.json` and must stay chmod 600 (workflows enforce it; Traefik refuses looser perms). |
|
||||
| 7 | Traefik dashboard basicauth on prod is a placeholder hash (`admin:$2y$10$foobar` in `docker-compose.prod.yml`) | There is no working dashboard credential; the firewall is the actual access control. Known-weak point -- do not treat the basicauth label as protection (also flagged in `mvp-architecture-contract`). |
|
||||
| 8 | Grafana admin password defaults to `admin` (`GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}` in base compose) unless the `GRAFANA_ADMIN_PASSWORD` Gitea secret is set | admin/admin on the logs UI. Mitigated by the `grafana-ipwhitelist` middleware (RFC1918 source ranges only), but verify the secret is set. |
|
||||
| 9 | Breaking migrations need the maintenance flow (`maintenance.yaml` -> `maintenance-migrate.sh`) | Skipping it and relying on start-time auto-migrations for a breaking change leaves the OLD stack running against the NEW schema mid-deploy. The maintenance flow zeroes all traffic weights (503s), optionally backs up, migrates, restores traffic -- accept the downtime. |
|
||||
| 10 | Every PR sync fully redeploys the single shared staging environment (`down` then `up`) | Staging blips on every PR push, and with two open PRs the last one to sync owns staging -- your "staging verification" may be verifying someone else's build. Check which run deployed last before trusting staging state. |
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 against the repo at commit e729d42. Everything above was verified by direct inspection of `.gitea/workflows/*.yaml`, `docker-compose*.yml`, `scripts/`, `scripts/ci/`, `config/`, and `backend/src/features/backup/`. Where older docs disagree (e.g. `docs/CICD-DEPLOY.md` runner-label names), the workflows/scripts win.
|
||||
|
||||
Volatile facts and how to re-verify each:
|
||||
|
||||
- Staging triggers (push to main + every PR sync): `grep -n -A6 "^on:" .gitea/workflows/staging.yaml`
|
||||
- Prod `image_tag` default `latest`: `grep -n -B2 -A3 "image_tag:" .gitea/workflows/production.yaml`
|
||||
- `:latest` clobber on every build: `grep -n "latest" .gitea/workflows/staging.yaml`
|
||||
- 13-feature health list: `grep -n "REQUIRED_FEATURES" .gitea/workflows/staging.yaml .gitea/workflows/production.yaml`
|
||||
- Healthcheck budget (48x5s) and backend `start_period: 180s`: `grep -n "seq 1 48" .gitea/workflows/staging.yaml; grep -n "start_period" docker-compose.yml docker-compose.blue-green.yml`
|
||||
- `--no-recreate` / `--force-recreate` service lists: `grep -n -A3 "no-recreate\|force-recreate" .gitea/workflows/production.yaml`
|
||||
- rsync `--delete` state reset: `grep -n "rsync" .gitea/workflows/*.yaml; cat config/deployment/state.json`
|
||||
- Legacy rollback.sh hazards: `grep -n "build\|mvp-frontend \|DOCKER_COMPOSE" scripts/rollback.sh`
|
||||
- Switch-traffic gradual mode and DEPLOY_PATH contract: `sed -n '1,25p' scripts/ci/switch-traffic.sh`
|
||||
- Prod health-check timeout 240s: `grep -n "HEALTH_CHECK_TIMEOUT" .gitea/workflows/production.yaml`
|
||||
- Backup API routes and jobs: `grep -rn "admin/backups" backend/src/features/backup/api/; ls backend/src/features/backup/jobs/`
|
||||
- Export/import flags: `./scripts/export-database.sh --help; ./scripts/import-database.sh --help`
|
||||
- refresh-staging-db prerequisites and `--dry-run`: `sed -n '1,50p' scripts/refresh-staging-db.sh`
|
||||
- LOG_LEVEL per env: `grep -n "LOG_LEVEL" .gitea/workflows/staging.yaml .gitea/workflows/production.yaml`
|
||||
- Placeholder basicauth hash: `grep -n "basicauth" docker-compose.prod.yml`
|
||||
- Grafana password default: `grep -n "GRAFANA_ADMIN_PASSWORD" docker-compose.yml`
|
||||
- Cloudflare DNS challenge + acme.json perms: `grep -n -A5 "acme" config/traefik/traefik.yml; grep -n "acme.json" .gitea/workflows/*.yaml`
|
||||
- Ansible removes the legacy cleanup cron (and does not install the prune cron): `grep -n -B1 -A5 "cron" ansible/deploy-staging-runner.yml`
|
||||
- `make clean` destroys volumes: `grep -n -A3 "^clean:" Makefile`
|
||||
@@ -1,238 +0,0 @@
|
||||
---
|
||||
name: mvp-validation-and-qa
|
||||
description: >-
|
||||
Load before claiming any MotoVaultPro change is "done", "tested", or "ready to merge";
|
||||
before running or adding tests; when wondering whether CI ran the tests (it did not);
|
||||
when integration tests could wipe the dev database (DROP TABLE CASCADE); or when you
|
||||
need the mobile+desktop verification procedure (320/768/1920px on staging). Defines the
|
||||
evidence bar and definition of done, maps the real test suites, and gives exact commands
|
||||
to run and add tests. Jest failure mechanics ("jest did not exit", tdd-guard-jest
|
||||
reporter errors, "npm test fails at repo root") are homed in mvp-build-and-env; raw
|
||||
symptom triage starts in mvp-debugging-playbook.
|
||||
---
|
||||
|
||||
# Validation and QA: what counts as evidence
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you are about to run tests, write tests, verify a change, or declare work complete on MotoVaultPro.
|
||||
|
||||
Do NOT use it for:
|
||||
- Issue/branch/PR mechanics, labels, or the RULE 0/1/2 review taxonomy — see `mvp-change-control`.
|
||||
- Diagnosing a live failure (logs, Grafana, container debugging) — see `mvp-debugging-playbook` and `mvp-diagnostics-and-logging`.
|
||||
- Deploy/rollback procedure or staging environment operations — see `mvp-run-and-operate`.
|
||||
- Setting up a machine or understanding why local dev is limited — see `mvp-build-and-env`.
|
||||
- Making CI actually enforce any of this — that is the campaign in `mvp-deploy-safety-campaign`.
|
||||
|
||||
## 1. The evidence bar (read this first)
|
||||
|
||||
**CI gates nothing beyond build + boot.** Verified 2026-07-07 against `.gitea/workflows/staging.yaml`: the only PR-triggered workflow builds 3 Docker images (backend, frontend, ocr — TypeScript compiles because `npm run build` runs inside the Dockerfiles), deploys them over the shared staging environment, and asserts 5 containers pass health checks and `https://staging.motovaultpro.com/api/health` reports `healthy` with 13 required features. **Zero tests, zero lint, zero type-check-as-gate, zero security scan, zero viewport check.** A "green" PR proves only that the code compiles and the app boots.
|
||||
|
||||
Consequence: **validation is entirely the author's job.** If you did not run it, it did not run. Anywhere docs imply CI runs "integration tests" or "viewport validation" (root `CLAUDE.md` does), the code wins: it does not.
|
||||
|
||||
### Definition of done for any change
|
||||
|
||||
Every box, personally executed, before requesting merge:
|
||||
|
||||
- [ ] `npm run lint` green in every touched workspace (`backend/`, `frontend/`; run from that directory — root `package.json` has NO scripts)
|
||||
- [ ] `npm run type-check` green in every touched workspace
|
||||
- [ ] Unit tests no worse than the known-red main baseline in every touched workspace, and green for everything your change touches (section 4 says where each suite can actually run)
|
||||
- [ ] NEW tests written for new behavior — not just existing tests still passing
|
||||
- [ ] Mobile AND desktop verified per the procedure in section 2 (hard project requirement; no CI substitute exists)
|
||||
- [ ] Feature verified end-to-end ON STAGING via the PR deploy, before merge. Every PR push auto-deploys to `https://staging.motovaultpro.com` (last push wins on the shared environment). There is no fully working local dev loop, so staging IS the end-to-end environment.
|
||||
- [ ] Old code deleted — replacement means removal, not accumulation
|
||||
- [ ] PR template test-plan checkboxes filled in truthfully (they are the only artifact of testing; see section 6)
|
||||
|
||||
The human owner's RULE 0/1/2 review (see `mvp-change-control`) substitutes for CI today. Do not make the reviewer discover that a checkbox was aspirational.
|
||||
|
||||
## 2. Mobile + desktop verification procedure
|
||||
|
||||
"ALL features MUST be implemented and tested on BOTH mobile and desktop" is a hard project requirement, and there is no viewport CI of any kind. The procedure, run against staging after your PR deploys:
|
||||
|
||||
**Widths to check** (project convention, from the repo's agent definitions): **320px** and **768px** (mobile), **1920px** (desktop). Use browser devtools responsive mode against `https://staging.motovaultpro.com`.
|
||||
|
||||
**The mobile fork is a separate code path, not CSS.** `frontend/src/App.tsx` sets `mobileMode` when `window.innerWidth <= 768` (or a mobile user agent on resize) and then renders dedicated `*MobileScreen` components driven by a Zustand screen switcher — not the desktop router. So the first mobile question is existence, not layout: **does your feature exist in the mobile screen switcher at all?**
|
||||
|
||||
A new page/screen must run the CANONICAL registration checklist in `mvp-architecture-contract` invariant 2.7 (7 steps covering desktop route, mobile union/maps, lazy import, render block, and navigation entry point) — miss one and the feature is silently absent or unreachable on one platform. The two steps most often missed on mobile are the lazy import and the navigation entry point: a screen can be fully "registered" in the maps yet have no way to reach it.
|
||||
|
||||
**Checklist per viewport:**
|
||||
|
||||
- 320px: feature reachable via bottom nav or hamburger drawer; no horizontal scroll; touch targets usable; forms submit; screen switcher shows the screen.
|
||||
- 768px: still mobile mode (`<= 768` is mobile) — verify the boundary renders correctly.
|
||||
- 1920px: desktop component path renders; feature reachable via desktop navigation; layout uses the space.
|
||||
- Both paths: exercise the actual flow (create/edit/delete), not just "the page loads".
|
||||
|
||||
Only 3 frontend unit tests simulate viewports (by mocking `matchMedia`/`useMediaQuery`); unit tests do not discharge this requirement. Manual verification on staging does.
|
||||
|
||||
## 3. Test-suite reality map (verified 2026-07-07)
|
||||
|
||||
| Workspace | Files | Cases | Runs where | Hazards |
|
||||
|---|---|---|---|---|
|
||||
| `backend/` | 37 `.test.ts` | ~500 | Unit: host or container. Integration: container only | Integration tests are DESTRUCTIVE (below); jest hangs without `--forceExit` |
|
||||
| `frontend/` | 31 in `src/` | 279 | Host only, via `npx jest --reporters=default` (see `mvp-build-and-env`). No container run exists: the shipped image is nginx-only and `.dockerignore` excludes `*.test.*` | `npm test` broken on host (tdd-guard-jest reporter + hardcoded Linux `projectRoot`); known-red baseline on main (`mvp-deploy-safety-campaign` Phase 0.3) |
|
||||
| `ocr/` | 16 `test_*.py` | ~310 | Host with Python env, or container | Never run in CI, ever. All google-genai/Gemini calls are mocked — green OCR tests prove nothing about live Gemini |
|
||||
|
||||
Facts that will surprise you:
|
||||
|
||||
- **Integration tests are destructive.** 2 of the 10 backend integration suites (vehicles, admin) run real migration SQL in `beforeAll` and `DROP TABLE ... CASCADE` in `afterAll` against whatever database the pool points at (e.g. `backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts`: `DROP TABLE IF EXISTS vehicles CASCADE` plus a function drop). Most of the others run `DELETE FROM` against live tables (`stations.api.test.ts:35` wipes `station_cache` unscoped); `fuel-logs.integration.test.ts` is an inert stub (no real DB work). Treat the whole set as destructive. Owner non-negotiable: **no destructive database operation without a fresh backup.** Run them only against a disposable database, and `make db-backup` first if there is any chance the pool points at data you care about. Suites can also interfere with each other (shared tables).
|
||||
- **Orphaned tests never run.** `frontend/test/fuel-logs/{FuelLogForm,useFuelGrades}.test.tsx` sit outside jest `roots: ['<rootDir>/src']` and are silently skipped by every run. FuelLogForm — the highest-traffic form in the app — has NO other tests. (The `frontend/test/__mocks__/` files ARE used, via `moduleNameMapper`; only the test files are dead.) Do not add tests under `frontend/test/`.
|
||||
- **8 of 21 backend feature capsules have zero test files** (verified 2026-07-07): `email-ingestion`, `notifications`, `onboarding`, `ownership-costs`, `subscriptions`, `terms-agreement`, `user-preferences`, `user-profile`. None of these have frontend tests either. Touching them means writing the first test.
|
||||
- **Zero skipped tests project-wide.** No `.skip`/`.only`/`xit`/`pytest.mark.skip` anywhere. Keep it that way — a skipped test here is invisible debt with no CI to surface it.
|
||||
- **The lone Cypress spec is unrunnable.** `frontend/cypress/e2e/stations.cy.ts` has no `cypress.config.*` anywhere, no cypress dependency in any `package.json`, uses an undefined `cy.login()` command, and references an `npm run e2e` script that does not exist. Do NOT model new e2e work on it; there is no working e2e framework in this repo today (root `@playwright/test` dependency is equally unused — no config, no specs).
|
||||
- **Root `package.json` has no scripts.** `npm test` at repo root fails. Always `cd backend/`, `cd frontend/`, or `cd ocr/` first. `make lint` and `make type-check` fan out to backend+frontend on the host and work.
|
||||
|
||||
## 4. How to run each suite
|
||||
|
||||
### Backend
|
||||
|
||||
Unit tests work on the host (node_modules present). Jest does NOT exit on its own — the module-level pg pool in `backend/src/core/config/database.ts` and the Redis client hold open handles. Always pass `--forceExit` locally:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm test -- --testPathPattern=src/features/vehicles/tests/unit --forceExit # one feature, unit only
|
||||
npm test -- --testPathPattern=src/features/stations --forceExit # whole feature
|
||||
npm run test:feature --feature=vehicles # same filter via npm config (add -- --forceExit)
|
||||
npm run lint && npm run type-check # both work on host
|
||||
```
|
||||
|
||||
Integration tests need real Postgres+Redis and container config paths (the config loader defaults to `/app/config/production.yml` and `/run/secrets`) — and they are currently runnable NOWHERE as-shipped. The shipped backend image is the production stage (`npm ci --omit=dev`, no jest; only `dist/` plus migration SQL copied), so docs/TESTING.md's `make shell-backend` + `npm test` recipe fails with "jest: not found" — that doc is stale here. The only viable paths are a builder-stage image or a host run against an ephemeral Postgres/Redis: see `mvp-deploy-safety-campaign` 1B. Whatever the vehicle: `make db-backup` first (non-negotiable — see the destructive-tests bullet above).
|
||||
|
||||
### Frontend
|
||||
|
||||
`npm test` is broken on the host (tdd-guard-jest reporter + hardcoded Linux `projectRoot`), and there is NO container alternative: the shipped frontend image is the nginx production stage (no node) and `frontend/.dockerignore` excludes `*.test.*` from every stage, so `docker compose exec mvp-frontend npm test` (still documented in docs/TESTING.md) cannot work. The verified host fallback — and the single home for its mechanics and traps — is `mvp-build-and-env` section 2:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npx jest --reporters=default # full suite (path BEFORE flags for single files)
|
||||
npx jest --reporters=default --testPathPattern=src/features/stations
|
||||
npm run lint && npm run type-check # these work normally on the host
|
||||
```
|
||||
|
||||
Expect the known-red baseline on main (17 failing tests as of 2026-07-09; numbers homed in `mvp-deploy-safety-campaign` Phase 0.3) — diff your run against it rather than expecting green.
|
||||
|
||||
### OCR
|
||||
|
||||
```bash
|
||||
cd ocr
|
||||
python -m pytest # needs a Python env with ocr/requirements.txt installed (pytest>=7.4.0 is in it)
|
||||
python -m pytest tests/test_gemini_engine.py -v
|
||||
```
|
||||
|
||||
No `pytest.ini`/`pyproject.toml`; tests import `from app.main import app`, so run from `ocr/`. Remember: CI never runs these — an OCR change with green local pytest has still never been machine-checked anywhere else.
|
||||
|
||||
### Everything cheap at once (host)
|
||||
|
||||
```bash
|
||||
make lint && make type-check # backend + frontend lint and tsc
|
||||
```
|
||||
|
||||
## 5. How to add tests
|
||||
|
||||
### Conventions
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| Backend unit tests | `backend/src/features/{name}/tests/unit/*.test.ts` |
|
||||
| Backend integration tests | `backend/src/features/{name}/tests/integration/*.test.ts` |
|
||||
| Alternate backend convention (audit-log uses it) | `backend/src/features/{name}/__tests__/*.test.ts` |
|
||||
| Backend fixtures | `backend/src/features/{name}/tests/fixtures/` (JSON or TS: `fuel-logs.fixtures.json`, stations' `mock-google-response.ts`) — otherwise inline mock objects; both are established |
|
||||
| Frontend tests | `frontend/src/features/{name}/__tests__/` or co-located `*.test.tsx` — MUST be under `src/` (jest roots) |
|
||||
| Backend core/cross-cutting | `backend/src/core/**` (e.g. `core/config/tests/feature-tiers.test.ts`) |
|
||||
| OCR | `ocr/tests/test_*.py`; images generated inline with PIL, no golden files |
|
||||
|
||||
### Route tests: fastify inject pattern
|
||||
|
||||
Do not spin up a listener. Build the app and inject (established pattern, e.g. `backend/src/features/audit-log/__tests__/audit-log.routes.test.ts`):
|
||||
|
||||
```typescript
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
let app: FastifyInstance;
|
||||
beforeAll(async () => {
|
||||
const { default: buildApp } = await import('../../../app');
|
||||
app = await buildApp();
|
||||
});
|
||||
afterAll(async () => { await app.close(); });
|
||||
|
||||
it('rejects requests without a valid token', async () => {
|
||||
const response = await app.inject({ method: 'GET', url: '/api/admin/audit-logs' });
|
||||
expect(response.statusCode).toBe(401); // JWT plugin rejects before any admin check
|
||||
});
|
||||
```
|
||||
|
||||
To bypass JWT auth, mock the auth plugin before importing the app (see the top of `vehicles.integration.test.ts`: `jest.mock('.../core/plugins/auth.plugin', ...)` decorating `authenticate` to set `request.user`). Caveat on the reference file: its test named "should return 403 for non-admin users" actually sends a bogus bearer token and asserts 401 — the name is misleading; the codebase has no example asserting a real 403 here. Do not copy an `expect(403)` from that name.
|
||||
|
||||
### Frontend mock strategy
|
||||
|
||||
`frontend/jest.config.ts` `moduleNameMapper` **auto-mocks the API client**: any import resolving to `core/api/client` is replaced by `frontend/src/core/api/__mocks__/client.ts`. Components under test never hit the network; configure responses through that mock. CSS and image imports are mapped to `frontend/test/__mocks__/{styleMock,fileMock}.js`.
|
||||
|
||||
### What every new repository test MUST cover: numeric coercion round-trip
|
||||
|
||||
This is the project's recurring bug class: node-postgres returns PostgreSQL `NUMERIC`/`DECIMAL` columns as **strings**, and every repository hand-rolls `parseFloat` coercion in its `mapRow()`. Missed coercion shipped at least three separate bugfix PRs (#241, #244 era). Any new repository method returning a numeric column needs a test proving strings-in, numbers-out. Copy-pasteable, in the established style (mocked `pg.Pool`, matching `vehicles.repository.test.ts`; verified against `FuelLogsRepository` as it exists today):
|
||||
|
||||
```typescript
|
||||
import { Pool } from 'pg';
|
||||
import { FuelLogsRepository } from '../../data/fuel-logs.repository';
|
||||
|
||||
describe('FuelLogsRepository numeric coercion', () => {
|
||||
let pool: Pool;
|
||||
let repository: FuelLogsRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
pool = { query: jest.fn() } as any;
|
||||
repository = new FuelLogsRepository(pool);
|
||||
});
|
||||
|
||||
it('coerces DECIMAL columns (returned as strings by node-postgres) to numbers', async () => {
|
||||
(pool.query as jest.Mock).mockResolvedValue({
|
||||
rows: [{
|
||||
id: 'log-1',
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'veh-1',
|
||||
date: '2026-07-01',
|
||||
odometer: 42000,
|
||||
gallons: '12.345', // pg gives NUMERIC back as string
|
||||
price_per_gallon: '3.599',
|
||||
total_cost: '44.43',
|
||||
station: null, location: null, notes: null,
|
||||
created_at: new Date(), updated_at: new Date(),
|
||||
}],
|
||||
});
|
||||
|
||||
const log = await repository.findById('log-1');
|
||||
|
||||
expect(log!.gallons).toBe(12.345); // number, not '12.345'
|
||||
expect(log!.pricePerGallon).toBe(3.599); // and snake_case -> camelCase
|
||||
expect(log!.totalCost).toBe(44.43);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Assert on the mapped camelCase field names — that simultaneously tests the mandatory `mapRow()` snake_case-to-camelCase conversion. If the method under test returns raw-ish rows (the fuel-logs "enhanced" path does, via `mapEnhancedRow`), still assert `typeof value === 'number'` on every decimal field.
|
||||
|
||||
## 6. Coverage and enforcement reality
|
||||
|
||||
- **No coverage thresholds exist.** Neither `backend/jest.config.js` nor `frontend/jest.config.ts` has `coverageThreshold` (verified 2026-07-07). Backend collects coverage (`text`/`lcov`/`html` reporters) but nothing fails on any number. Do not claim "coverage-gated" anywhere.
|
||||
- **The honor system is the PR template.** `.gitea/PULL_REQUEST_TEMPLATE.md` test-plan checkboxes (`Unit tests` / `Integration tests` / `Manual verification` plus a Commands/steps list) are the only testing artifact a PR carries. Fill in the actual commands you ran; the reviewer has no other signal.
|
||||
- **Want CI to enforce any of this?** That is exactly the scope of `mvp-deploy-safety-campaign` (test/lint gates, SHA-pinned deploys). Until it lands, the RULE 0/1/2 human review in `mvp-change-control` is the only gate — write PRs that make that review easy.
|
||||
- Pre-launch calibration: a paying-user launch is the trajectory. Untested features (section 3's list of 8) touching billing (`subscriptions`) or user data are the highest-risk gap; adding first tests there outranks polishing well-tested capsules.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 from direct repo inspection. Volatile facts and how to re-verify each:
|
||||
|
||||
| Fact (as of 2026-07-07) | Re-verify with |
|
||||
|---|---|
|
||||
| CI runs zero tests/lint (build+boot only) | `grep -nE "npm (test|run lint)|jest|pytest|eslint" .gitea/workflows/*.yaml` (expect no hits) |
|
||||
| Root package.json has no scripts | `python3 -c "import json; print(json.load(open('package.json')).get('scripts'))"` |
|
||||
| Backend 37 test files / ~500 cases / 10 integration | `find backend/src -name "*.test.ts" \| wc -l` ; `find backend/src -path "*tests/integration*" -name "*.test.ts" \| wc -l` |
|
||||
| Frontend 31 files / 279 cases in src; 2 orphaned in frontend/test/ | `find frontend/src \( -name "*.test.ts" -o -name "*.test.tsx" \) \| wc -l` ; `ls frontend/test/fuel-logs/` |
|
||||
| OCR 16 files / ~310 tests | `find ocr/tests -name "test_*.py" \| wc -l` ; `grep -rE "^\s*def test_" ocr/tests \| wc -l` |
|
||||
| 8 backend capsules with zero tests | `for f in backend/src/features/*/; do echo "$(basename $f): $(find $f -name '*.test.ts' \| wc -l)"; done` |
|
||||
| Frontend jest broken on host (tdd-guard-jest reporter, hardcoded projectRoot) | `grep -n "tdd-guard-jest\|projectRoot" frontend/jest.config.ts` then `cd frontend && npm test` |
|
||||
| Integration tests DROP TABLE CASCADE | `grep -rn "DROP TABLE" backend/src --include="*.test.ts"` |
|
||||
| No coverageThreshold anywhere | `grep -rn coverageThreshold backend/jest.config.js frontend/jest.config.ts` (expect no hits) |
|
||||
| Mobile breakpoint <= 768; registration checklist (canonical: `mvp-architecture-contract` 2.7) | `grep -n "innerWidth <= 768" frontend/src/App.tsx` ; `grep -n "MobileScreen\b" frontend/src/core/store/navigation.ts` |
|
||||
| Cypress spec still unrunnable | `find . -name "cypress.config.*"` (expect none) ; `grep -rn cypress frontend/package.json package.json` |
|
||||
| Zero skipped tests | `grep -rnE "\.skip\(|\.only\(|xit\(|xdescribe\(" backend/src frontend/src; grep -rn "pytest.mark.skip" ocr/tests` |
|
||||
| Staging health gate: 13 features | `grep -n REQUIRED_FEATURES .gitea/workflows/staging.yaml` |
|
||||
@@ -1,219 +0,0 @@
|
||||
---
|
||||
name: mvp-vehicle-domain-reference
|
||||
description: >-
|
||||
Vehicle domain theory as implemented in MotoVaultPro. Load when working on VIN
|
||||
validation, VIN decode, check digits, model-year codes, "wrong year decoded", OCR VIN
|
||||
misreads (I/O/Q), fuel efficiency math, MPG or km/L calculations, unit conversion,
|
||||
imperial/metric, fuel grades, DATE vs TIMESTAMP column semantics, subscription tiers,
|
||||
vehicle limits, pro-gated features, or the year/make/model/trim dropdown catalog. This
|
||||
is the WHAT and WHY of the domain rules; live symptom triage (dates off by one day,
|
||||
TIER_REQUIRED 403s) starts in mvp-debugging-playbook.
|
||||
---
|
||||
|
||||
# MotoVaultPro Vehicle Domain Reference
|
||||
|
||||
Authored 2026-07-07. All claims verified against repo code on that date. Where any doc contradicts this file or the code, the code wins.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you need the domain rules themselves: VIN structure and check-digit math, model-year resolution, fuel efficiency formulas and edge cases, DATE vs TIMESTAMP semantics, the tier model, or how VIN-decode results map onto the vehicle catalog.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- OCR/Gemini pipeline mechanics (engines, WIF auth, timeouts, SDK sharp edges) -> `mvp-ocr-gemini-pipeline`
|
||||
- Debugging a live symptom on staging/prod -> `mvp-debugging-playbook`
|
||||
- Past incidents and settled battles in depth -> `mvp-failure-archaeology`
|
||||
- Architecture invariants and capsule layout -> `mvp-architecture-contract`
|
||||
- Adding a config knob or tier key -> `mvp-config-and-secrets`
|
||||
|
||||
## 1. VIN theory as implemented
|
||||
|
||||
A VIN (Vehicle Identification Number) is exactly 17 characters for model year 1981+. The letters I, O, and Q are never valid in a VIN (they look like 1 and 0). Both validators in this repo enforce the same pattern:
|
||||
|
||||
```
|
||||
^[A-HJ-NPR-Z0-9]{17}$
|
||||
```
|
||||
|
||||
- Python: `ocr/app/validators/vin_validator.py` (`MODERN_VIN_PATTERN`; a legacy 11-17 char pattern exists behind `allow_legacy=True` with a -0.2 confidence penalty)
|
||||
- Backend: `backend/src/features/vehicles/api/vehicles.controller.ts` (`VIN_REGEX`, rejects with 400 `INVALID_VIN` before calling the OCR service)
|
||||
|
||||
### Position semantics
|
||||
|
||||
| Positions | Name | Meaning |
|
||||
|---|---|---|
|
||||
| 1-3 | WMI | World Manufacturer Identifier (country + manufacturer) |
|
||||
| 4-8 | VDS | Vehicle Descriptor Section (model, body, engine) |
|
||||
| 9 | Check digit | Computed from all other positions (algorithm below) |
|
||||
| 10 | Model year code | 30-year cycle table (below) |
|
||||
| 11 | Plant | Assembly plant code |
|
||||
| 12-17 | Serial | Sequential production number |
|
||||
|
||||
### OCR confusion corrections
|
||||
|
||||
`VinValidator.TRANSLITERATION` in `ocr/app/validators/vin_validator.py` maps only characters that are INVALID in VINs to their likely intended values: `I -> 1`, `O -> 0`, `Q -> 0` (plus lowercase `i/o/q` and lowercase `l -> 1`). Deliberate constraint documented in the code: **B and S are valid VIN characters and must NOT be transliterated** (a naive "8 vs B, 5 vs S" correction would corrupt real VINs). Spaces and dashes are stripped first.
|
||||
|
||||
The extractor (`ocr/app/extractors/vin_extractor.py` + `extract_candidates()` in the validator) also handles OCR fragmentation (concatenating adjacent fragments) and spurious inserted characters (sliding 17-char windows plus one- and two-character deletion over 18-19 char strings), using the check digit to filter false candidates. This candidate machinery is a settled battle -- see `mvp-failure-archaeology` before "simplifying" it.
|
||||
|
||||
### Check-digit algorithm (position 9)
|
||||
|
||||
Implemented in `VinValidator.calculate_check_digit()`:
|
||||
|
||||
1. Transliterate each character to a value: digits map to themselves; letters map via `CHAR_VALUES`: A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8, J=1 K=2 L=3 M=4 N=5 P=7 R=9, S=2 T=3 U=4 V=5 W=6 X=7 Y=8 Z=9. (No I/O/Q; note P=7 with no 6, and R=9 with no 8 in that row -- this is per the standard, not a typo.)
|
||||
2. Multiply each position's value by its weight from `CHECK_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]` (position 9 itself has weight 0 and is skipped).
|
||||
3. Sum, take modulo 11. Remainder 0-9 is the check digit as a character; remainder 10 is the letter `X`.
|
||||
|
||||
A failed check digit does NOT reject the VIN in `validate()` -- it returns `is_valid=True` with `confidence_adjustment=-0.15` (some real VINs, especially non-North-American, do not comply). A passing check digit adds +0.1 confidence.
|
||||
|
||||
### Model-year code table (position 10)
|
||||
|
||||
`_VIN_YEAR_CODES` in `ocr/app/engines/gemini_engine.py` maps the position-10 character to a base year in the first cycle (1980-2009). The letters I, O, Q, U, Z and the digit 0 are not valid year codes.
|
||||
|
||||
| Code | Base | Code | Base | Code | Base |
|
||||
|---|---|---|---|---|---|
|
||||
| A | 1980 | L | 1990 | 1 | 2001 |
|
||||
| B | 1981 | M | 1991 | 2 | 2002 |
|
||||
| C | 1982 | N | 1992 | 3 | 2003 |
|
||||
| D | 1983 | P | 1993 | 4 | 2004 |
|
||||
| E | 1984 | R | 1994 | 5 | 2005 |
|
||||
| F | 1985 | S | 1995 | 6 | 2006 |
|
||||
| G | 1986 | T | 1996 | 7 | 2007 |
|
||||
| H | 1987 | V | 1997 | 8 | 2008 |
|
||||
| J | 1988 | W | 1998 | 9 | 2009 |
|
||||
| K | 1989 | X | 1999 | | |
|
||||
| | | Y | 2000 | | |
|
||||
|
||||
**The 30-year cycle ambiguity.** Codes repeat every 30 years (A = 1980 or 2010 or 2040). Disambiguation uses position 7, per NHTSA FMVSS No. 115 (MY2010+ vehicles must have an alphabetic position 7):
|
||||
|
||||
- Position 7 alphabetic -> add 30 (2010-2039 cycle)
|
||||
- Position 7 numeric -> 1980-2009 cycle, or 2040-2069 if that year is no more than 2 years in the future (`datetime.now().year + 2`)
|
||||
|
||||
This is implemented in `resolve_vin_year()` in `ocr/app/engines/gemini_engine.py`. **It was implemented backwards once** (numeric position 7 mapped to the 2010+ cycle) and fixed in commit `936753f`; the test suite `ocr/tests/test_resolve_vin_year.py` locks in the correct behavior. Do not re-derive this logic from memory -- read the function.
|
||||
|
||||
**The year is NEVER trusted from the LLM.** `GeminiEngine.decode_vin()` computes the year deterministically via `resolve_vin_year()`, passes it into the prompt as already-resolved, and overrides Gemini's returned year with the deterministic value (logging a warning on mismatch). Gemini is only trusted for make/model/trim/engine/transmission/body/drive/fuel, which require manufacturer knowledge.
|
||||
|
||||
### VIN decode path (for orientation only; details in mvp-ocr-gemini-pipeline)
|
||||
|
||||
`POST /api/vehicles/decode-vin` (pro-gated) -> `vehicles.controller.ts` regex validation -> `backend/src/features/ocr/external/ocr-client.ts:decodeVin()` -> OCR container `POST /decode/vin` (`ocr/app/routers/decode.py`) -> `GeminiEngine.decode_vin()` with Google Search grounding -> back through `vehiclesService.mapVinDecodeResponse()` (section 5). Frontend axios timeout for this call is 120s (`frontend/src/features/vehicles/api/vehicles.api.ts`) -- an older doc note saying 60s is stale. There is deliberately NO VIN decode cache (removed in `283ba6b` after race/staleness bugs).
|
||||
|
||||
## 2. Fuel math as implemented
|
||||
|
||||
Domain services: `backend/src/features/fuel-logs/domain/` -- `efficiency-calculation.service.ts`, `unit-conversion.service.ts`, `fuel-grade.service.ts`, orchestrated by `fuel-logs.service.ts`.
|
||||
|
||||
### Units model
|
||||
|
||||
`UnitSystem = 'imperial' | 'metric'` (defined in `backend/src/shared-minimal/utils/units.ts`, re-exported by `unit-conversion.service.ts`). The user's unit system comes from user settings (`fuel-logs/external/user-settings.service.ts`, default `imperial`).
|
||||
|
||||
Critical property: **stored values are unit-agnostic numbers in whatever system the user entered.** There is no conversion at write time; the unit system only selects labels and interpretation:
|
||||
|
||||
| unitSystem | fuelUnits means | distance means | efficiency label |
|
||||
|---|---|---|---|
|
||||
| imperial | gallons | miles | MPG |
|
||||
| metric | liters | kilometers | km/L |
|
||||
|
||||
Consequence: if a user switches unit systems, historical rows are NOT converted -- old numbers get reinterpreted under the new labels. This is the current implemented behavior, not an oversight to silently "fix" in passing; treat any change as a feature decision.
|
||||
|
||||
### Efficiency formula
|
||||
|
||||
`UnitConversionService.calculateEfficiency(distance, fuelUnits) = distance / fuelUnits` -- the same formula serves MPG (miles/gallon) and km/L (km/liter). The BACKEND domain `efficiencyLabel` for metric is km/L (an unused `MPG_TO_L100KM_FACTOR` and a `convertEfficiency` method exist -- `convertEfficiency` has no production callers as of 2026-07-07). The FRONTEND, however, diverges for metric users: `FuelLogForm.tsx:352` labels the calculated-efficiency field "L/100km" while the value it displays is computed as distance/units (i.e. km/L -- a live label/value mismatch), and `FuelLogsList.tsx:148` computes a local fallback as `(liters/km)*100` labeled "L/100km" (the settings screens also describe metric as L/100km). So a metric user sees "X km/L" when the API supplies efficiency but "Y L/100km" via the form and the list fallback. This backend/frontend divergence is a known inconsistency, NOT a settled convention -- do not flatly "correct" either side without treating it as a feature decision.
|
||||
|
||||
`EfficiencyCalculationService.calculateEfficiency(currentLog, previousOdometerReading, unitSystem)` picks the distance:
|
||||
|
||||
1. If `tripDistance > 0`: use it (`calculationMethod: 'trip_distance'`).
|
||||
2. Else if `odometerReading` present AND a previous odometer reading exists AND the delta is positive: use the delta (`calculationMethod: 'odometer'`).
|
||||
3. Else return `null` (no efficiency shown).
|
||||
|
||||
Edge cases that fall out of this, all intentional:
|
||||
- **First fill-up** with odometer method: no previous log -> `previousOdometerReading` is null -> efficiency is null. Efficiency is never stored; it is computed on read (the legacy `mpg` column was dropped in `migrations/003_drop_mpg_column.sql`).
|
||||
- Odometer rollback or duplicate reading (delta <= 0): efficiency null, not negative.
|
||||
- `fuelUnits <= 0`: null.
|
||||
- Vehicle stats (`getVehicleStats` in `fuel-logs.service.ts`) computes per-log efficiencies with `previousOdometerReading=null`, so odometer-method logs contribute 0 and are filtered out of `averageEfficiency` -- only trip-distance logs count toward the average. Know this before "fixing" a low average.
|
||||
|
||||
### Odometer vs tripDistance: XOR
|
||||
|
||||
Frontend zod schema (`frontend/src/features/fuel-logs/components/FuelLogForm.tsx`) enforces exactly-one via two `.refine()` calls: at least one of `odometerReading`/`tripDistance` must be > 0, AND both may not be set. The database enforces only the at-least-one half (`distance_required_check` in `migrations/002_enhance_fuel_logs_schema.sql`, recreated in `004`). `odometer` was made nullable and `trip_distance` widened to `DECIMAL(10,3)` in migration 004 to allow trip-only, fractional-distance logs.
|
||||
|
||||
### Dual legacy/enhanced schema -- which fields are canonical
|
||||
|
||||
One table (`fuel_logs`), two column generations, one repository (`data/fuel-logs.repository.ts`) with two APIs:
|
||||
|
||||
| Canonical (enhanced) column | Legacy column (dual-written) | API field |
|
||||
|---|---|---|
|
||||
| `date_time` TIMESTAMPTZ | `date` DATE | `dateTime` (ISO string) |
|
||||
| `fuel_units` DECIMAL(8,3) | `gallons` | `fuelUnits` |
|
||||
| `cost_per_unit` DECIMAL(6,3) | `price_per_gallon` | `costPerUnit` |
|
||||
| `trip_distance` DECIMAL(10,3) | -- | `tripDistance` |
|
||||
| `odometer` (nullable) | same column | `odometerReading` |
|
||||
| `fuel_type`, `fuel_grade`, `location_data` | -- | `fuelType`, `fuelGrade`, `locationData` |
|
||||
|
||||
`createEnhanced` dual-writes `gallons = fuelUnits` and `price_per_gallon = costPerUnit` for "legacy support". **The enhanced fields are canonical**; treat `gallons`/`price_per_gallon`/`date` as backfill/compatibility only. The enhanced response shape is `EnhancedFuelLogResponse` in `domain/fuel-logs.types.ts` (includes computed `efficiency?` and `efficiencyLabel`).
|
||||
|
||||
Convention break to know: `mapEnhancedRow` deliberately returns snake_case rows with numerics coerced (`parseFloat`), and `toEnhancedResponse` in the service does the camelCase mapping -- unlike the standard repository `mapRow()` pattern. This arrangement caused two incidents (#47 raw-row decision, #244 missing coercion); do not return raw pg rows and do not remove the coercion.
|
||||
|
||||
### Fuel type and grade
|
||||
|
||||
`fuel_type` is one of `gasoline | diesel | electric` (CHECK constraint). Valid grades per type, enforced both by `FuelGradeService` and a database trigger (`validate_fuel_grade()` in migration 002):
|
||||
|
||||
- gasoline: `87`, `88`, `89`, `91`, `93` (default 87)
|
||||
- diesel: `#1`, `#2`
|
||||
- electric: grade must be null
|
||||
|
||||
## 3. DATE vs TIMESTAMP semantics
|
||||
|
||||
**The rule.** A Postgres `DATE` column is a calendar date with no timezone (fuel_logs.date, maintenance_records.date, next_due_date, purchase_date, ...). It must flow as a plain `YYYY-MM-DD` string end-to-end: pg's DATE parser is overridden to return the raw string (`types.setTypeParser(1082, ...)` in `backend/src/core/config/database.ts` -- never remove this), APIs pass the string through, the frontend displays it with dayjs, and sorting is lexicographic (safe for ISO dates). Never write `new Date(dateString)`, `toISOString()`, or `toLocaleDateString()` against a DATE value -- each one applies a UTC or local-midnight conversion that shifts the date by a day for some timezone. A `TIMESTAMP WITH TIME ZONE` column (fuel_logs.date_time, created_at, updated_at) is a real instant: `new Date()` and ISO serialization are correct there (`toEnhancedResponse` does exactly this for `dateTime`).
|
||||
|
||||
**History (2026-03-23, three distinct traps fixed in one day, issue #237).** (a) pg returned DATE as a local-midnight Date object, which `toISOString()` shifted a day -- fixed by the type-parser override (`f0fc427`); (b) frontend `new Date("YYYY-MM-DD")` parses as UTC midnight, so `toLocaleDateString()` shifted it back -- fixed by dayjs display (`1e056f0`); (c) the OCR date parser used `toISOString().split('T')[0]` -- fixed with local-time formatting (`087f7b9`). This battle is settled; a "dates off by one day" symptom means someone reintroduced one of these three patterns.
|
||||
|
||||
## 4. Tier and subscription domain model
|
||||
|
||||
Source: `backend/src/core/config/feature-tiers.ts` (registry) + `backend/src/features/subscriptions/` (Stripe sync).
|
||||
|
||||
**Tier hierarchy** (`TIER_LEVELS`): `free(0) < pro(1) < enterprise(2)`. Higher tiers inherit lower-tier access via numeric comparison.
|
||||
|
||||
**Gated feature keys and vehicle limits**: the registry (`FEATURE_TIERS`) and `VEHICLE_LIMITS` (enforced via `canAddVehicle(tier, currentCount)`) live in `backend/src/core/config/feature-tiers.ts`; the current key catalog, the limit literals, and the gating mechanics are canonical in `mvp-config-and-secrets` section 3.
|
||||
|
||||
**Fail-open trap**: `canAccessFeature()` returns `true` for any feature key NOT in the registry -- a typo silently ungates the route -- and two parallel gating mechanisms exist with different failure behavior. Details in `mvp-config-and-secrets` section 3; read it before adding a gated route.
|
||||
|
||||
**Source of truth for a user's tier**: `user_profiles.subscription_tier` (Postgres enum `free|pro|enterprise`, default `free`; `backend/src/features/user-profile/migrations/002_add_subscription_and_deactivation.sql`). The auth plugin reads it into `request.userContext.subscriptionTier` on every authenticated request; all gating reads from `userContext`, never from Stripe directly. The subscriptions capsule keeps it synced from Stripe: `syncTierToUserProfile()` in `subscriptions/domain/subscriptions.service.ts` is called on subscription create/change/cancel and webhook events; `adminOverrideTier()` updates `subscriptions.tier` and `user_profiles.subscription_tier` atomically in one transaction; the grace-period job (`subscriptions/jobs/grace-period.job.ts`) downgrades lapsed users to `free`.
|
||||
|
||||
**The TIER_REQUIRED 403 contract** (what the frontend keys upgrade prompts on):
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "TIER_REQUIRED",
|
||||
"requiredTier": "pro",
|
||||
"currentTier": "free",
|
||||
"upgradePrompt": "Upgrade to Pro to ..."
|
||||
}
|
||||
```
|
||||
|
||||
Emitted by both `require-tier.ts` and `tier-guard.plugin.ts`. Keep this shape stable; do not invent per-route variants.
|
||||
|
||||
## 5. Vehicle catalog domain (platform capsule)
|
||||
|
||||
The platform capsule (`backend/src/features/platform/`) owns the year -> make -> model -> trim -> engine/transmission dropdown cascade used by vehicle forms. All routes are authenticate-only (not tier-gated).
|
||||
|
||||
- **Seeding**: `CatalogSeedService.seedIfEmpty()` (`platform/domain/catalog-seed.service.ts`) runs at backend startup (`backend/src/index.ts`), loading `engines.sql`, `transmissions.sql`, `vehicle_options.sql` from `features/platform/data/` (container path `/app/migrations/features/platform/data`) only if `vehicle_options` is empty. Seed failure is logged and startup continues (data can be imported later via admin UI) -- so an empty catalog is a possible runtime state, and VIN-decode matching degrades to all-`none` confidence when it happens.
|
||||
- **Caching**: dropdown results cache in Redis with 6-hour TTL per level (`platform/domain/platform-cache.service.ts`, `ttl = 6 * 3600`).
|
||||
- **VIN decode integration**: `vehiclesService.mapVinDecodeResponse()` (`backend/src/features/vehicles/domain/vehicles.service.ts`) takes Gemini's raw strings and matches each field against the catalog dropdown options, cascading: year (always `high` confidence if present, since it is deterministic) -> make -> model -> trim -> engine/transmission. Each downstream match only runs if every upstream field matched (`make.value` etc.). Per-field result is `{ value, sourceValue, confidence }` where confidence is `high` (exact case-insensitive match), `medium` (alphanumeric-normalized, prefix, contains, or longest reverse-contains match), or `none` (unmatched -- `value: null`, raw Gemini string preserved in `sourceValue` for display). `bodyType`, `driveType`, `fuelType` are display-only: never matched, always `confidence: 'none'`.
|
||||
|
||||
Practical implication: a correct Gemini decode can still return `value: null` fields if the catalog lacks that year/make/model row. That is a catalog-coverage problem, not a decode bug -- check `vehicle_options` before touching the OCR side.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 by direct inspection of the files named above, plus `git log`/`git show` for incident commits (`936753f`, `1add6c8`, `56df5d4`, `283ba6b`, `f0fc427`, `1e056f0`, `087f7b9`, `574acf3`, `0d90829`). Volatile facts and how to re-verify each:
|
||||
|
||||
| Fact | Re-verify with |
|
||||
|---|---|
|
||||
| VIN regex / transliteration / weights / CHAR_VALUES | `sed -n '20,80p' ocr/app/validators/vin_validator.py` |
|
||||
| Year-code table and cycle logic | `sed -n '41,93p' ocr/app/engines/gemini_engine.py` |
|
||||
| Year never trusted from LLM | `grep -n "resolved_year" ocr/app/engines/gemini_engine.py` |
|
||||
| Frontend VIN decode timeout (120s, 2026-07-07) | `grep -n timeout frontend/src/features/vehicles/api/vehicles.api.ts` |
|
||||
| Efficiency formula and null cases | `cat backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts backend/src/features/fuel-logs/domain/unit-conversion.service.ts` |
|
||||
| XOR zod refinements | `grep -n -A3 refine frontend/src/features/fuel-logs/components/FuelLogForm.tsx` |
|
||||
| fuel_logs schema, constraints, grades | `cat backend/src/features/fuel-logs/migrations/*.sql` |
|
||||
| Dual-write legacy columns | `grep -n "legacy" backend/src/features/fuel-logs/data/fuel-logs.repository.ts` |
|
||||
| DATE type-parser override | `grep -n "setTypeParser" backend/src/core/config/database.ts` |
|
||||
| Tier levels (key/limit catalog: `mvp-config-and-secrets` section 3) | `grep -n "minTier\|VEHICLE_LIMITS" -A3 backend/src/core/config/feature-tiers.ts` |
|
||||
| TIER_REQUIRED 403 shape | `grep -rn -A4 "TIER_REQUIRED" backend/src/core/middleware/require-tier.ts backend/src/core/plugins/tier-guard.plugin.ts` |
|
||||
| Tier sync from Stripe | `grep -n "syncTierToUserProfile" backend/src/features/subscriptions/domain/subscriptions.service.ts` |
|
||||
| Catalog seed + 6h cache | `grep -n "seedIfEmpty\|6 \* 3600" backend/src/features/platform/domain/*.ts` |
|
||||
| matchField confidence ladder | `grep -n -A45 "private matchField" backend/src/features/vehicles/domain/vehicles.service.ts` |
|
||||
@@ -0,0 +1,86 @@
|
||||
# skills/planner/
|
||||
|
||||
## Overview
|
||||
|
||||
Planning skill with resources that must stay synced with agent prompts.
|
||||
|
||||
## Index
|
||||
|
||||
| File/Directory | Contents | Read When |
|
||||
| ------------------------------------- | ---------------------------------------------- | -------------------------------------------- |
|
||||
| `SKILL.md` | Planning workflow, phases | Using the planner skill |
|
||||
| `scripts/planner.py` | Step-by-step planning orchestration | Debugging planner behavior |
|
||||
| `resources/plan-format.md` | Plan template (injected by script) | Editing plan structure |
|
||||
| `resources/temporal-contamination.md` | Detection heuristic for contaminated comments | Updating TW/QR temporal contamination logic |
|
||||
| `resources/diff-format.md` | Unified diff spec for code changes | Updating Developer diff consumption logic |
|
||||
| `resources/default-conventions.md` | Default structural conventions (4-tier system) | Updating QR RULE 2 or planner decision audit |
|
||||
|
||||
## Resource Sync Requirements
|
||||
|
||||
Resources are **authoritative sources**.
|
||||
|
||||
- **SKILL.md** references resources directly (main Claude can read files)
|
||||
- **Agent prompts** embed resources 1:1 (sub-agents cannot access files
|
||||
reliably)
|
||||
|
||||
### plan-format.md
|
||||
|
||||
Plan template injected by `scripts/planner.py` at planning phase completion.
|
||||
|
||||
**No agent sync required** - the script reads and outputs the format directly,
|
||||
so editing this file takes effect immediately without updating any agent
|
||||
prompts.
|
||||
|
||||
### temporal-contamination.md
|
||||
|
||||
Authoritative source for temporal contamination detection. Full content embedded
|
||||
1:1.
|
||||
|
||||
| Synced To | Embedded Section |
|
||||
| ---------------------------- | -------------------------- |
|
||||
| `agents/technical-writer.md` | `<temporal_contamination>` |
|
||||
| `agents/quality-reviewer.md` | `<temporal_contamination>` |
|
||||
|
||||
**When updating**: Modify `resources/temporal-contamination.md` first, then copy
|
||||
content into both `<temporal_contamination>` sections.
|
||||
|
||||
### diff-format.md
|
||||
|
||||
Authoritative source for unified diff format. Full content embedded 1:1.
|
||||
|
||||
| Synced To | Embedded Section |
|
||||
| --------------------- | ---------------- |
|
||||
| `agents/developer.md` | `<diff_format>` |
|
||||
|
||||
**When updating**: Modify `resources/diff-format.md` first, then copy content
|
||||
into `<diff_format>` section.
|
||||
|
||||
### default-conventions.md
|
||||
|
||||
Authoritative source for default structural conventions (four-tier decision
|
||||
backing system). Embedded 1:1 in QR for RULE 2 enforcement; referenced by
|
||||
planner.py for decision audit.
|
||||
|
||||
| Synced To | Embedded Section |
|
||||
| ---------------------------- | ----------------------- |
|
||||
| `agents/quality-reviewer.md` | `<default_conventions>` |
|
||||
|
||||
**When updating**: Modify `resources/default-conventions.md` first, then copy
|
||||
full content verbatim into `<default_conventions>` section in QR.
|
||||
|
||||
## Sync Verification
|
||||
|
||||
After modifying a resource, verify sync:
|
||||
|
||||
```bash
|
||||
# Check temporal-contamination.md references
|
||||
grep -l "temporal.contamination\|four detection questions\|change-relative\|baseline reference" agents/*.md
|
||||
|
||||
# Check diff-format.md references
|
||||
grep -l "context lines\|AUTHORITATIVE\|APPROXIMATE\|context anchor" agents/*.md
|
||||
|
||||
# Check default-conventions.md references
|
||||
grep -l "default_conventions\|domain: god-object\|domain: test-organization" agents/*.md
|
||||
```
|
||||
|
||||
If grep finds files not listed in sync tables above, update this document.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Planner
|
||||
|
||||
LLM-generated plans have gaps. I have seen missing error handling, vague
|
||||
acceptance criteria, specs that nobody can implement. I built this skill with
|
||||
two workflows -- planning and execution -- connected by quality gates that catch
|
||||
these problems early.
|
||||
|
||||
## Planning Workflow
|
||||
|
||||
```
|
||||
Planning ----+
|
||||
| |
|
||||
v |
|
||||
QR -------+ [fail: restart planning]
|
||||
|
|
||||
v
|
||||
TW -------+
|
||||
| |
|
||||
v |
|
||||
QR-Docs ----+ [fail: restart TW]
|
||||
|
|
||||
v
|
||||
APPROVED
|
||||
```
|
||||
|
||||
| Step | Actions |
|
||||
| ----------------------- | -------------------------------------------------------------------------- |
|
||||
| Context & Scope | Confirm path, define scope, identify approaches, list constraints |
|
||||
| Decision & Architecture | Evaluate approaches, select with reasoning, diagram, break into milestones |
|
||||
| Refinement | Document risks, add uncertainty flags, specify paths and criteria |
|
||||
| Final Verification | Verify completeness, check specs, write to file |
|
||||
| QR-Completeness | Verify Decision Log complete, policy defaults confirmed, plan structure |
|
||||
| QR-Code | Read codebase, verify diff context, apply RULE 0/1/2 to proposed code |
|
||||
| Technical Writer | Scrub temporal comments, add WHY comments, enrich rationale |
|
||||
| QR-Docs | Verify no temporal contamination, comments explain WHY not WHAT |
|
||||
|
||||
So, why all the feedback loops? QR-Completeness and QR-Code run before TW to
|
||||
catch structural issues early. QR-Docs runs after TW to validate documentation
|
||||
quality. Doc issues restart only TW; structure issues restart planning. The loop
|
||||
runs until both pass.
|
||||
|
||||
## Execution Workflow
|
||||
|
||||
```
|
||||
Plan --> Milestones --> QR --> Docs --> Retrospective
|
||||
^ |
|
||||
+- [fail] -+
|
||||
|
||||
* Reconciliation phase precedes Milestones when resuming partial work
|
||||
```
|
||||
|
||||
After planning completes and context clears (`/clear`), execution proceeds:
|
||||
|
||||
| Step | Purpose |
|
||||
| ---------------------- | --------------------------------------------------------------- |
|
||||
| Execution Planning | Analyze plan, detect reconciliation signals, output strategy |
|
||||
| Reconciliation | (conditional) Validate existing code against plan |
|
||||
| Milestone Execution | Delegate to agents, run tests; repeat until all complete |
|
||||
| Post-Implementation QR | Quality review of implemented code |
|
||||
| Issue Resolution | (conditional) Present issues, collect decisions, delegate fixes |
|
||||
| Documentation | Technical writer updates CLAUDE.md/README.md |
|
||||
| Retrospective | Present execution summary |
|
||||
|
||||
I designed the coordinator to never write code directly -- it delegates to
|
||||
developers. Separating coordination from implementation produces cleaner
|
||||
results. The coordinator:
|
||||
|
||||
- Parallelizes independent work across up to 4 developers per milestone
|
||||
- Runs quality review after all milestones complete
|
||||
- Loops through issue resolution until QR passes
|
||||
- Invokes technical writer only after QR passes
|
||||
|
||||
**Reconciliation** handles resume scenarios. When the user request contains
|
||||
signals like "already implemented", "resume", or "partially complete", the
|
||||
workflow validates existing code against plan requirements before executing
|
||||
remaining milestones. Building on unverified code means rework.
|
||||
|
||||
**Issue Resolution** presents each QR finding individually with options (Fix /
|
||||
Skip / Alternative). Fixes delegate to developers or technical writers, then QR
|
||||
runs again. This cycle repeats until QR passes.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: planner
|
||||
description: Interactive planning and execution for complex tasks. Use when user asks to use or invoke planner skill.
|
||||
---
|
||||
|
||||
# Planner Skill
|
||||
|
||||
Two-phase workflow: **planning** (create plans) and **execution** (implement
|
||||
plans).
|
||||
|
||||
## Invocation Routing
|
||||
|
||||
| User Intent | Script | Invocation |
|
||||
| ------------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
|
||||
| "plan", "design", "architect", "break down" | planner.py | `python3 scripts/planner.py --step-number 1 --total-steps 4 --thoughts "..."` |
|
||||
| "review plan" (after plan written) | planner.py | `python3 scripts/planner.py --phase review --step-number 1 --total-steps 2 ...` |
|
||||
| "execute", "implement", "run plan" | executor.py | `python3 scripts/executor.py --plan-file PATH --step-number 1 --total-steps 7 ...` |
|
||||
|
||||
Scripts inject step-specific guidance via JIT prompt injection. Invoke the
|
||||
script and follow its REQUIRED ACTIONS output.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use when task has:
|
||||
|
||||
- Multiple milestones with dependencies
|
||||
- Architectural decisions requiring documentation
|
||||
- Complexity benefiting from forced reflection pauses
|
||||
|
||||
Skip when task is:
|
||||
|
||||
- Single-step with obvious implementation
|
||||
- Quick fix or minor change
|
||||
- Already well-specified by user
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Contents | Read When |
|
||||
| ------------------------------------- | ------------------------------------------ | ----------------------------------------------- |
|
||||
| `resources/diff-format.md` | Unified diff specification for plans | Writing code changes in milestones |
|
||||
| `resources/temporal-contamination.md` | Comment hygiene detection heuristics | Writing comments in code snippets |
|
||||
| `resources/default-conventions.md` | Priority hierarchy, structural conventions | Making decisions without explicit user guidance |
|
||||
| `resources/plan-format.md` | Plan template structure | Completing planning phase (injected by script) |
|
||||
|
||||
**Resource loading rule**: Scripts will prompt you to read specific resources at
|
||||
decision points. When prompted, read the full resource before proceeding.
|
||||
|
||||
## Workflow Summary
|
||||
|
||||
**Planning phase**: Steps 1-N explore context, evaluate approaches, refine
|
||||
milestones. Final step writes plan to file. Review phase (TW scrub -> QR
|
||||
validation) follows.
|
||||
|
||||
**Execution phase**: 7 steps -- analyze plan, reconcile existing code, delegate
|
||||
milestones to agents, QR validation, issue resolution, documentation,
|
||||
retrospective.
|
||||
|
||||
All procedural details are injected by the scripts. Invoke the appropriate
|
||||
script and follow its output.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Default Conventions
|
||||
|
||||
These conventions apply when project documentation does not specify otherwise.
|
||||
|
||||
## MotoVaultPro Project Conventions
|
||||
|
||||
**Naming**:
|
||||
- Database columns: snake_case (`user_id`, `created_at`)
|
||||
- TypeScript types: camelCase (`userId`, `createdAt`)
|
||||
- API responses: camelCase
|
||||
- Files: kebab-case (`vehicle-repository.ts`)
|
||||
|
||||
**Architecture**:
|
||||
- Feature capsules: `backend/src/features/{feature}/`
|
||||
- Repository pattern with mapRow() for case conversion
|
||||
- Single-tenant, user-scoped data
|
||||
|
||||
**Frontend**:
|
||||
- Mobile + desktop validation required (320px, 768px, 1920px)
|
||||
- Touch targets >= 44px
|
||||
- No hover-only interactions
|
||||
|
||||
**Development**:
|
||||
- Local node development (`npm install`, `npm run dev`, `npm test`)
|
||||
- CI/CD pipeline validates containers and integration tests
|
||||
- Plans stored in Gitea Issue comments
|
||||
|
||||
---
|
||||
|
||||
## Priority Hierarchy
|
||||
|
||||
Higher tiers override lower. Cite backing source when auditing.
|
||||
|
||||
| Tier | Source | Action |
|
||||
| ---- | --------------- | -------------------------------- |
|
||||
| 1 | user-specified | Explicit user instruction: apply |
|
||||
| 2 | doc-derived | CLAUDE.md / project docs: apply |
|
||||
| 3 | default-derived | This document: apply |
|
||||
| 4 | assumption | No backing: CONFIRM WITH USER |
|
||||
|
||||
## Severity Levels
|
||||
|
||||
| Level | Meaning | Action |
|
||||
| ---------- | -------------------------------- | --------------- |
|
||||
| SHOULD_FIX | Likely to cause maintenance debt | Flag for fixing |
|
||||
| SUGGESTION | Improvement opportunity | Note if time |
|
||||
|
||||
---
|
||||
|
||||
## Structural Conventions
|
||||
|
||||
<default-conventions domain="god-object">
|
||||
**God Object**: >15 public methods OR >10 dependencies OR mixed concerns (networking + UI + data)
|
||||
Severity: SHOULD_FIX
|
||||
</default-conventions>
|
||||
|
||||
<default-conventions domain="god-function">
|
||||
**God Function**: >50 lines OR multiple abstraction levels OR >3 nesting levels
|
||||
Severity: SHOULD_FIX
|
||||
Exception: Inherently sequential algorithms or state machines
|
||||
</default-conventions>
|
||||
|
||||
<default-conventions domain="duplicate-logic">
|
||||
**Duplicate Logic**: Copy-pasted blocks, repeated error handling, parallel near-identical functions
|
||||
Severity: SHOULD_FIX
|
||||
</default-conventions>
|
||||
|
||||
<default-conventions domain="dead-code">
|
||||
**Dead Code**: No callers, impossible branches, unread variables, unused imports
|
||||
Severity: SUGGESTION
|
||||
</default-conventions>
|
||||
|
||||
<default-conventions domain="inconsistent-error-handling">
|
||||
**Inconsistent Error Handling**: Mixed exceptions/error codes, inconsistent types, swallowed errors
|
||||
Severity: SUGGESTION
|
||||
Exception: Project specifies different handling per error category
|
||||
</default-conventions>
|
||||
|
||||
---
|
||||
|
||||
## File Organization Conventions
|
||||
|
||||
<default-conventions domain="test-organization">
|
||||
**Test Organization**: Extend existing test files; create new only when:
|
||||
- Distinct module boundary OR >500 lines OR different fixtures required
|
||||
Severity: SHOULD_FIX (for unnecessary fragmentation)
|
||||
</default-conventions>
|
||||
|
||||
<default-conventions domain="file-creation">
|
||||
**File Creation**: Prefer extending existing files; create new only when:
|
||||
- Clear module boundary OR >300-500 lines OR distinct responsibility
|
||||
Severity: SUGGESTION
|
||||
</default-conventions>
|
||||
|
||||
---
|
||||
|
||||
## Testing Conventions
|
||||
|
||||
<default-conventions domain="testing">
|
||||
**Principle**: Test behavior, not implementation. Fast feedback.
|
||||
|
||||
**Test Type Hierarchy** (preference order):
|
||||
|
||||
1. **Integration tests** (highest value)
|
||||
- Test end-user verifiable behavior
|
||||
- Use real systems/dependencies (e.g., testcontainers)
|
||||
- Verify component interaction at boundaries
|
||||
- This is where the real value lies
|
||||
|
||||
2. **Property-based / generative tests** (preferred)
|
||||
- Cover wide input space with invariant assertions
|
||||
- Catch edge cases humans miss
|
||||
- Use for functions with clear input/output contracts
|
||||
|
||||
3. **Unit tests** (use sparingly)
|
||||
- Only for highly complex or critical logic
|
||||
- Risk: maintenance liability, brittleness to refactoring
|
||||
- Prefer integration tests that cover same behavior
|
||||
|
||||
**Test Placement**: Tests are part of implementation milestones, not separate
|
||||
milestones. A milestone is not complete until its tests pass. This creates fast
|
||||
feedback during development.
|
||||
|
||||
**DO**:
|
||||
|
||||
- Integration tests with real dependencies (testcontainers, etc.)
|
||||
- Property-based tests for invariant-rich functions
|
||||
- Parameterized fixtures over duplicate test bodies
|
||||
- Test behavior observable by end users
|
||||
|
||||
**DON'T**:
|
||||
|
||||
- Test external library/dependency behavior (out of scope)
|
||||
- Unit test simple code (maintenance liability exceeds value)
|
||||
- Mock owned dependencies (use real implementations)
|
||||
- Test implementation details that may change
|
||||
- One-test-per-variant when parametrization applies
|
||||
|
||||
Severity: SHOULD_FIX (violations), SUGGESTION (missed opportunities)
|
||||
</default-conventions>
|
||||
|
||||
---
|
||||
|
||||
## Modernization Conventions
|
||||
|
||||
<default-conventions domain="version-constraints">
|
||||
**Version Constraint Violation**: Features unavailable in project's documented target version
|
||||
Requires: Documented target version
|
||||
Severity: SHOULD_FIX
|
||||
</default-conventions>
|
||||
|
||||
<default-conventions domain="modernization">
|
||||
**Modernization Opportunity**: Legacy APIs, verbose patterns, manual stdlib reimplementations
|
||||
Severity: SUGGESTION
|
||||
Exception: Project requires legacy pattern
|
||||
</default-conventions>
|
||||
@@ -0,0 +1,201 @@
|
||||
# Unified Diff Format for Plan Code Changes
|
||||
|
||||
This document is the authoritative specification for code changes in implementation plans.
|
||||
|
||||
## Purpose
|
||||
|
||||
Unified diff format encodes both **location** and **content** in a single structure. This eliminates the need for location directives in comments (e.g., "insert at line 42") and provides reliable anchoring even when line numbers drift.
|
||||
|
||||
## Anatomy
|
||||
|
||||
```diff
|
||||
--- a/path/to/file.py
|
||||
+++ b/path/to/file.py
|
||||
@@ -123,6 +123,15 @@ def existing_function(ctx):
|
||||
# Context lines (unchanged) serve as location anchors
|
||||
existing_code()
|
||||
|
||||
+ # NEW: Comments explain WHY - transcribed verbatim by Developer
|
||||
+ # Guard against race condition when messages arrive out-of-order
|
||||
+ new_code()
|
||||
|
||||
# More context to anchor the insertion point
|
||||
more_existing_code()
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Authority | Purpose |
|
||||
| ------------------------------------------ | ------------------------- | ---------------------------------------------------------- |
|
||||
| File path (`--- a/path/to/file.py`) | **AUTHORITATIVE** | Exact target file |
|
||||
| Line numbers (`@@ -123,6 +123,15 @@`) | **APPROXIMATE** | May drift as earlier milestones modify the file |
|
||||
| Function context (`@@ ... @@ def func():`) | **SCOPE HINT** | Function/method containing the change |
|
||||
| Context lines (unchanged) | **AUTHORITATIVE ANCHORS** | Developer matches these patterns to locate insertion point |
|
||||
| `+` lines | **NEW CODE** | Code to add, including WHY comments |
|
||||
| `-` lines | **REMOVED CODE** | Code to delete |
|
||||
|
||||
## Two-Layer Location Strategy
|
||||
|
||||
Code changes use two complementary layers for location:
|
||||
|
||||
1. **Prose scope hint** (optional): Natural language describing conceptual location
|
||||
2. **Diff with context**: Precise insertion point via context line matching
|
||||
|
||||
### Layer 1: Prose Scope Hints
|
||||
|
||||
For complex changes, add a prose description before the diff block:
|
||||
|
||||
````markdown
|
||||
Add validation after input sanitization in `UserService.validate()`:
|
||||
|
||||
```diff
|
||||
@@ -123,6 +123,15 @@ def validate(self, user):
|
||||
sanitized = sanitize(user.input)
|
||||
|
||||
+ # Validate format before proceeding
|
||||
+ if not is_valid_format(sanitized):
|
||||
+ raise ValidationError("Invalid format")
|
||||
+
|
||||
return process(sanitized)
|
||||
`` `
|
||||
```
|
||||
````
|
||||
|
||||
The prose tells Developer **where conceptually** (which method, what operation precedes it). The diff tells Developer **where exactly** (context lines to match).
|
||||
|
||||
**When to use prose hints:**
|
||||
|
||||
- Changes to large files (>300 lines)
|
||||
- Multiple changes to the same file in one milestone
|
||||
- Complex nested structures where function context alone is ambiguous
|
||||
- When the surrounding code logic matters for understanding placement
|
||||
|
||||
**When prose is optional:**
|
||||
|
||||
- Small files with obvious structure
|
||||
- Single change with unique context lines
|
||||
- Function context in @@ line provides sufficient scope
|
||||
|
||||
### Layer 2: Function Context in @@ Line
|
||||
|
||||
The `@@` line can include function/method context after the line numbers:
|
||||
|
||||
```diff
|
||||
@@ -123,6 +123,15 @@ def validate(self, user):
|
||||
```
|
||||
|
||||
This follows standard unified diff format (git generates this automatically). It tells Developer which function contains the change, aiding navigation even when line numbers drift.
|
||||
|
||||
## Why Context Lines Matter
|
||||
|
||||
When a plan has multiple milestones that modify the same file, earlier milestones shift line numbers. The `@@ -123` in Milestone 3 may no longer be accurate after Milestones 1 and 2 execute.
|
||||
|
||||
**Context lines solve this**: Developer searches for the unchanged context patterns in the actual file. These patterns are stable anchors that survive line number drift.
|
||||
|
||||
Include 2-3 context lines before and after changes for reliable matching.
|
||||
|
||||
## Comment Placement
|
||||
|
||||
Comments in `+` lines explain **WHY**, not **WHAT**. These comments:
|
||||
|
||||
- Are transcribed verbatim by Developer
|
||||
- Source rationale from Planning Context (Decision Log, Rejected Alternatives)
|
||||
- Use concrete terms without hidden baselines
|
||||
- Must pass temporal contamination review (see `temporal-contamination.md`)
|
||||
|
||||
**Important**: Comments written during planning often contain temporal contamination -- change-relative language, baseline references, or location directives. @agent-technical-writer reviews and fixes these before @agent-developer transcribes them.
|
||||
|
||||
<example type="CORRECT" category="why_comment">
|
||||
```diff
|
||||
+ # Polling chosen over webhooks: 30% webhook delivery failures in third-party API
|
||||
+ # WebSocket rejected to preserve stateless architecture
|
||||
+ updates = poll_api(interval=30)
|
||||
```
|
||||
Explains WHY this approach was chosen.
|
||||
</example>
|
||||
|
||||
<example type="INCORRECT" category="what_comment">
|
||||
```diff
|
||||
+ # Poll the API every 30 seconds
|
||||
+ updates = poll_api(interval=30)
|
||||
```
|
||||
Restates WHAT the code does - redundant with the code itself.
|
||||
</example>
|
||||
|
||||
<example type="INCORRECT" category="hidden_baseline">
|
||||
```diff
|
||||
+ # Generous timeout for slow networks
|
||||
+ REQUEST_TIMEOUT = 60
|
||||
```
|
||||
"Generous" compared to what? Hidden baseline provides no actionable information.
|
||||
</example>
|
||||
|
||||
<example type="CORRECT" category="concrete_justification">
|
||||
```diff
|
||||
+ # 60s accommodates 95th percentile upstream response times
|
||||
+ REQUEST_TIMEOUT = 60
|
||||
```
|
||||
Concrete justification that explains why this specific value.
|
||||
</example>
|
||||
|
||||
## Location Directives: Forbidden
|
||||
|
||||
The diff structure handles location. Location directives in comments are redundant and error-prone.
|
||||
|
||||
<example type="INCORRECT" category="location_directive">
|
||||
```python
|
||||
# Insert this BEFORE the retry loop (line 716)
|
||||
# Timestamp guard: prevent older data from overwriting newer
|
||||
get_ctx, get_cancel = context.with_timeout(ctx, 500)
|
||||
```
|
||||
Location directive leaked into comment - line numbers become stale.
|
||||
</example>
|
||||
|
||||
<example type="CORRECT" category="location_directive">
|
||||
```diff
|
||||
@@ -714,6 +714,10 @@ def put(self, ctx, tags):
|
||||
for tag in tags:
|
||||
subject = tag.subject
|
||||
|
||||
- # Timestamp guard: prevent older data from overwriting newer
|
||||
- # due to network delays, retries, or concurrent writes
|
||||
- get_ctx, get_cancel = context.with_timeout(ctx, 500)
|
||||
|
||||
# Retry loop for Put operations
|
||||
for attempt in range(max_retries):
|
||||
|
||||
```
|
||||
Context lines (`for tag in tags`, `# Retry loop`) are stable anchors that survive line number drift.
|
||||
</example>
|
||||
|
||||
## When to Use Diff Format
|
||||
|
||||
<diff_format_decision>
|
||||
|
||||
| Code Characteristic | Use Diff? | Boundary Test |
|
||||
| --------------------------------------- | --------- | ---------------------------------------- |
|
||||
| Conditionals, loops, error handling, | YES | Has branching logic |
|
||||
| state machines | | |
|
||||
| Multiple insertions same file | YES | >1 change location |
|
||||
| Deletions or replacements | YES | Removing/changing existing code |
|
||||
| Pure assignment/return (CRUD, getters) | NO | Single statement, no branching |
|
||||
| Boilerplate from template | NO | Developer can generate from pattern name |
|
||||
|
||||
The boundary test: "Does Developer need to see exact placement and context to implement correctly?"
|
||||
|
||||
- YES -> diff format
|
||||
- NO (can implement from description alone) -> prose sufficient
|
||||
|
||||
</diff_format_decision>
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before finalizing code changes in a plan:
|
||||
|
||||
- [ ] File path is exact (not "auth files" but `src/auth/handler.py`)
|
||||
- [ ] Context lines exist in target file (validate patterns match actual code)
|
||||
- [ ] Comments explain WHY, not WHAT
|
||||
- [ ] No location directives in comments
|
||||
- [ ] No hidden baselines (test: "[adjective] compared to what?")
|
||||
- [ ] 2-3 context lines for reliable anchoring
|
||||
```
|
||||
@@ -0,0 +1,250 @@
|
||||
# Plan Format
|
||||
|
||||
Write your plan using this structure:
|
||||
|
||||
```markdown
|
||||
# [Plan Title]
|
||||
|
||||
## Overview
|
||||
|
||||
[Problem statement, chosen approach, and key decisions in 1-2 paragraphs]
|
||||
|
||||
## Planning Context
|
||||
|
||||
This section is consumed VERBATIM by downstream agents (Technical Writer,
|
||||
Quality Reviewer). Quality matters: vague entries here produce poor annotations
|
||||
and missed risks.
|
||||
|
||||
### Decision Log
|
||||
|
||||
| Decision | Reasoning Chain |
|
||||
| ------------------ | ------------------------------------------------------------ |
|
||||
| [What you decided] | [Multi-step reasoning: premise -> implication -> conclusion] |
|
||||
|
||||
Each rationale must contain at least 2 reasoning steps. Single-step rationales
|
||||
are insufficient.
|
||||
|
||||
INSUFFICIENT: "Polling over webhooks | Webhooks are unreliable" SUFFICIENT:
|
||||
"Polling over webhooks | Third-party API has 30% webhook delivery failure in
|
||||
testing -> unreliable delivery would require fallback polling anyway -> simpler
|
||||
to use polling as primary mechanism"
|
||||
|
||||
INSUFFICIENT: "500ms timeout | Matches upstream latency" SUFFICIENT: "500ms
|
||||
timeout | Upstream 95th percentile is 450ms -> 500ms covers 95% of requests
|
||||
without timeout -> remaining 5% should fail fast rather than queue"
|
||||
|
||||
Include BOTH architectural decisions AND implementation-level micro-decisions:
|
||||
|
||||
- Architectural: "Event sourcing over CRUD | Need audit trail + replay
|
||||
capability -> CRUD would require separate audit log -> event sourcing provides
|
||||
both natively"
|
||||
- Implementation: "Mutex over channel | Single-writer case -> channel
|
||||
coordination adds complexity without benefit -> mutex is simpler with
|
||||
equivalent safety"
|
||||
|
||||
Technical Writer sources ALL code comments from this table. If a micro-decision
|
||||
isn't here, TW cannot document it.
|
||||
|
||||
### Rejected Alternatives
|
||||
|
||||
| Alternative | Why Rejected |
|
||||
| -------------------- | ------------------------------------------------------------------- |
|
||||
| [Approach not taken] | [Concrete reason: performance, complexity, doesn't fit constraints] |
|
||||
|
||||
Technical Writer uses this to add "why not X" context to code comments.
|
||||
|
||||
### Constraints & Assumptions
|
||||
|
||||
- [Technical: API limits, language version, existing patterns to follow]
|
||||
- [Organizational: timeline, team expertise, approval requirements]
|
||||
- [Dependencies: external services, libraries, data formats]
|
||||
- [Default conventions applied: cite any `<default-conventions domain="...">`
|
||||
used]
|
||||
|
||||
### Known Risks
|
||||
|
||||
| Risk | Mitigation | Anchor |
|
||||
| --------------- | --------------------------------------------- | ------------------------------------------ |
|
||||
| [Specific risk] | [Concrete mitigation or "Accepted: [reason]"] | [file:L###-L### if claiming code behavior] |
|
||||
|
||||
**Anchor requirement**: If mitigation claims existing code behavior ("no change
|
||||
needed", "already handles X"), cite the file:line + brief excerpt that proves
|
||||
the claim. Skip anchors for hypothetical risks or external unknowns.
|
||||
|
||||
Quality Reviewer excludes these from findings but will challenge unverified
|
||||
behavioral claims.
|
||||
|
||||
## Invisible Knowledge
|
||||
|
||||
This section captures knowledge NOT deducible from reading the code alone.
|
||||
Technical Writer uses this for README.md documentation during
|
||||
post-implementation.
|
||||
|
||||
**The test**: Would a new team member understand this from reading the source
|
||||
files? If no, it belongs here.
|
||||
|
||||
**Categories** (not exhaustive -- apply the principle):
|
||||
|
||||
1. **Architectural decisions**: Component relationships, data flow, module
|
||||
boundaries
|
||||
2. **Business rules**: Domain constraints that shape implementation choices
|
||||
3. **System invariants**: Properties that must hold but are not enforced by
|
||||
types/compiler
|
||||
4. **Historical context**: Why alternatives were rejected (links to Decision
|
||||
Log)
|
||||
5. **Performance characteristics**: Non-obvious efficiency properties or
|
||||
requirements
|
||||
6. **Tradeoffs**: Costs and benefits of chosen approaches
|
||||
|
||||
### Architecture
|
||||
```
|
||||
|
||||
[ASCII diagram showing component relationships]
|
||||
|
||||
Example: User Request | v +----------+ +-------+ | Auth |---->| Cache |
|
||||
+----------+ +-------+ | v +----------+ +------+ | Handler |---->| DB |
|
||||
+----------+ +------+
|
||||
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
|
||||
[How data moves through the system - inputs, transformations, outputs]
|
||||
|
||||
Example: HTTP Request --> Validate --> Transform --> Store --> Response | v Log
|
||||
(async)
|
||||
|
||||
````
|
||||
|
||||
### Why This Structure
|
||||
|
||||
[Reasoning behind module organization that isn't obvious from file names]
|
||||
|
||||
- Why these boundaries exist
|
||||
- What would break if reorganized differently
|
||||
|
||||
### Invariants
|
||||
|
||||
[Rules that must be maintained but aren't enforced by code]
|
||||
|
||||
- Ordering requirements
|
||||
- State consistency rules
|
||||
- Implicit contracts between components
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
[Key decisions with their costs and benefits]
|
||||
|
||||
- What was sacrificed for what gain
|
||||
- Performance vs. readability choices
|
||||
- Consistency vs. flexibility choices
|
||||
|
||||
## Milestones
|
||||
|
||||
### Milestone 1: [Name]
|
||||
|
||||
**Files**: [exact paths - e.g., src/auth/handler.py, not "auth files"]
|
||||
|
||||
**Flags** (if applicable): [needs TW rationale, needs error handling review, needs conformance check]
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- [Specific: "Add retry with exponential backoff", not "improve error handling"]
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- [Testable: "Returns 429 after 3 failed attempts" - QR can verify pass/fail]
|
||||
- [Avoid vague: "Works correctly" or "Handles errors properly"]
|
||||
|
||||
**Tests** (milestone not complete until tests pass):
|
||||
|
||||
- **Test files**: [exact paths, e.g., tests/test_retry.py]
|
||||
- **Test type**: [integration | property-based | unit] - see default-conventions
|
||||
- **Backing**: [user-specified | doc-derived | default-derived]
|
||||
- **Scenarios**:
|
||||
- Normal: [e.g., "successful retry after transient failure"]
|
||||
- Edge: [e.g., "max retries exhausted", "zero delay"]
|
||||
- Error: [e.g., "non-retryable error returns immediately"]
|
||||
|
||||
Skip tests when: user explicitly stated no tests, OR milestone is documentation-only,
|
||||
OR project docs prohibit tests for this component. State skip reason explicitly.
|
||||
|
||||
**Code Changes** (for non-trivial logic, use unified diff format):
|
||||
|
||||
See `resources/diff-format.md` for specification.
|
||||
|
||||
```diff
|
||||
--- a/path/to/file.py
|
||||
+++ b/path/to/file.py
|
||||
@@ -123,6 +123,15 @@ def existing_function(ctx):
|
||||
# Context lines (unchanged) serve as location anchors
|
||||
existing_code()
|
||||
|
||||
+ # WHY comment explaining rationale - transcribed verbatim by Developer
|
||||
+ new_code()
|
||||
|
||||
# More context to anchor the insertion point
|
||||
more_existing_code()
|
||||
````
|
||||
|
||||
### Milestone N: ...
|
||||
|
||||
### Milestone [Last]: Documentation
|
||||
|
||||
**Files**:
|
||||
|
||||
- `path/to/CLAUDE.md` (index updates)
|
||||
- `path/to/README.md` (if Invisible Knowledge section has content)
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- Update CLAUDE.md index entries for all new/modified files
|
||||
- Each entry has WHAT (contents) and WHEN (task triggers)
|
||||
- If plan's Invisible Knowledge section is non-empty:
|
||||
- Create/update README.md with architecture diagrams from plan
|
||||
- Include tradeoffs, invariants, "why this structure" content
|
||||
- Verify diagrams match actual implementation
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
- CLAUDE.md enables LLM to locate relevant code for debugging/modification tasks
|
||||
- README.md captures knowledge not discoverable from reading source files
|
||||
- Architecture diagrams in README.md match plan's Invisible Knowledge section
|
||||
|
||||
**Source Material**: `## Invisible Knowledge` section of this plan
|
||||
|
||||
### Cross-Milestone Integration Tests
|
||||
|
||||
When integration tests require components from multiple milestones:
|
||||
|
||||
1. Place integration tests in the LAST milestone that provides a required
|
||||
component
|
||||
2. List dependencies explicitly in that milestone's **Tests** section
|
||||
3. Integration test milestone is not complete until all dependencies are
|
||||
implemented
|
||||
|
||||
Example:
|
||||
|
||||
- M1: Auth handler (property tests for auth logic)
|
||||
- M2: Database layer (property tests for queries)
|
||||
- M3: API endpoint (integration tests covering M1 + M2 + M3 with testcontainers)
|
||||
|
||||
The integration tests in M3 verify the full flow that end users would exercise,
|
||||
using real dependencies. This creates fast feedback as soon as all components
|
||||
exist.
|
||||
|
||||
## Milestone Dependencies (if applicable)
|
||||
|
||||
```
|
||||
M1 ---> M2
|
||||
\
|
||||
--> M3 --> M4
|
||||
```
|
||||
|
||||
Independent milestones can execute in parallel during /plan-execution.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -0,0 +1,135 @@
|
||||
# Temporal Contamination in Code Comments
|
||||
|
||||
This document defines terminology for identifying comments that leak information
|
||||
about code history, change processes, or planning artifacts. Both
|
||||
@agent-technical-writer and @agent-quality-reviewer reference this
|
||||
specification.
|
||||
|
||||
## The Core Principle
|
||||
|
||||
> **Timeless Present Rule**: Comments must be written from the perspective of a
|
||||
> reader encountering the code for the first time, with no knowledge of what
|
||||
> came before or how it got here. The code simply _is_.
|
||||
|
||||
**Why this matters**: Change-narrative comments are an LLM artifact -- a
|
||||
category error, not merely a style issue. The change process is ephemeral and
|
||||
irrelevant to the code's ongoing existence. Humans writing comments naturally
|
||||
describe what code IS, not what they DID to create it. Referencing the change
|
||||
that created a comment is fundamentally confused about what belongs in
|
||||
documentation.
|
||||
|
||||
Think of it this way: a novel's narrator never describes the author's typing
|
||||
process. Similarly, code comments should never describe the developer's editing
|
||||
process. The code simply exists; the path to its existence is invisible.
|
||||
|
||||
In a plan, this means comments are written _as if the plan was already
|
||||
executed_.
|
||||
|
||||
## Detection Heuristic
|
||||
|
||||
Evaluate each comment against these five questions. Signal words are examples --
|
||||
extrapolate to semantically similar constructs.
|
||||
|
||||
### 1. Does it describe an action taken rather than what exists?
|
||||
|
||||
**Category**: Change-relative
|
||||
|
||||
| Contaminated | Timeless Present |
|
||||
| -------------------------------------- | ----------------------------------------------------------- |
|
||||
| `// Added mutex to fix race condition` | `// Mutex serializes cache access from concurrent requests` |
|
||||
| `// New validation for the edge case` | `// Rejects negative values (downstream assumes unsigned)` |
|
||||
| `// Changed to use batch API` | `// Batch API reduces round-trips from N to 1` |
|
||||
|
||||
Signal words (non-exhaustive): "Added", "Replaced", "Now uses", "Changed to",
|
||||
"New", "Updated", "Refactored"
|
||||
|
||||
### 2. Does it compare to something not in the code?
|
||||
|
||||
**Category**: Baseline reference
|
||||
|
||||
| Contaminated | Timeless Present |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `// Replaces per-tag logging with summary` | `// Single summary line; per-tag logging would produce 1500+ lines` |
|
||||
| `// Unlike the old approach, this is thread-safe` | `// Thread-safe: each goroutine gets independent state` |
|
||||
| `// Previously handled in caller` | `// Encapsulated here; caller should not manage lifecycle` |
|
||||
|
||||
Signal words (non-exhaustive): "Instead of", "Rather than", "Previously",
|
||||
"Replaces", "Unlike the old", "No longer"
|
||||
|
||||
### 3. Does it describe where to put code rather than what code does?
|
||||
|
||||
**Category**: Location directive
|
||||
|
||||
| Contaminated | Timeless Present |
|
||||
| ----------------------------- | --------------------------------------------- |
|
||||
| `// After the SendAsync call` | _(delete -- diff structure encodes location)_ |
|
||||
| `// Insert before validation` | _(delete -- diff structure encodes location)_ |
|
||||
| `// Add this at line 425` | _(delete -- diff structure encodes location)_ |
|
||||
|
||||
Signal words (non-exhaustive): "After", "Before", "Insert", "At line", "Here:",
|
||||
"Below", "Above"
|
||||
|
||||
**Action**: Always delete. Location is encoded in diff structure, not comments.
|
||||
|
||||
### 4. Does it describe intent rather than behavior?
|
||||
|
||||
**Category**: Planning artifact
|
||||
|
||||
| Contaminated | Timeless Present |
|
||||
| -------------------------------------- | -------------------------------------------------------- |
|
||||
| `// TODO: add retry logic later` | _(delete, or implement retry now)_ |
|
||||
| `// Will be extended for batch mode` | _(delete -- do not document hypothetical futures)_ |
|
||||
| `// Temporary workaround until API v2` | `// API v1 lacks filtering; client-side filter required` |
|
||||
|
||||
Signal words (non-exhaustive): "Will", "TODO", "Planned", "Eventually", "For
|
||||
future", "Temporary", "Workaround until"
|
||||
|
||||
**Action**: Delete, implement the feature, or reframe as current constraint.
|
||||
|
||||
### 5. Does it describe the author's choice rather than code behavior?
|
||||
|
||||
**Category**: Intent leakage
|
||||
|
||||
| Contaminated | Timeless Present |
|
||||
| ------------------------------------------ | ---------------------------------------------------- |
|
||||
| `// Intentionally placed after validation` | `// Runs after validation completes` |
|
||||
| `// Deliberately using mutex over channel` | `// Mutex serializes access (single-writer pattern)` |
|
||||
| `// Chose polling for reliability` | `// Polling: 30% webhook delivery failures observed` |
|
||||
| `// We decided to cache at this layer` | `// Cache here: reduces DB round-trips for hot path` |
|
||||
|
||||
Signal words (non-exhaustive): "intentionally", "deliberately", "chose",
|
||||
"decided", "on purpose", "by design", "we opted"
|
||||
|
||||
**Action**: Extract the technical justification; discard the decision narrative.
|
||||
The reader doesn't need to know someone "decided" -- they need to know WHY this
|
||||
approach works.
|
||||
|
||||
**The test**: Can you delete the intent word and the comment still makes sense?
|
||||
If yes, delete the intent word. If no, reframe around the technical reason.
|
||||
|
||||
---
|
||||
|
||||
**Catch-all**: If a comment only makes sense to someone who knows the code's
|
||||
history, it is temporally contaminated -- even if it does not match any category
|
||||
above.
|
||||
|
||||
## Subtle Cases
|
||||
|
||||
Same word, different verdict -- demonstrates that detection requires semantic
|
||||
judgment, not keyword matching.
|
||||
|
||||
| Comment | Verdict | Reasoning |
|
||||
| -------------------------------------- | ------------ | ------------------------------------------------ |
|
||||
| `// Now handles edge cases properly` | Contaminated | "properly" implies it was improper before |
|
||||
| `// Now blocks until connection ready` | Clean | "now" describes runtime moment, not code history |
|
||||
| `// Fixed the null pointer issue` | Contaminated | Describes a fix, not behavior |
|
||||
| `// Returns null when key not found` | Clean | Describes behavior |
|
||||
|
||||
## The Transformation Pattern
|
||||
|
||||
> **Extract the technical justification, discard the change narrative.**
|
||||
|
||||
1. What useful info is buried? (problem, behavior)
|
||||
2. Reframe as timeless present
|
||||
|
||||
Example: "Added mutex to fix race" -> "Mutex serializes concurrent access"
|
||||
@@ -0,0 +1,682 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Plan Executor - Execute approved plans through delegation.
|
||||
|
||||
Seven-phase execution workflow with JIT prompt injection:
|
||||
Step 1: Execution Planning (analyze plan, detect reconciliation)
|
||||
Step 2: Reconciliation (conditional, validate existing code)
|
||||
Step 3: Milestone Execution (delegate to agents, run tests)
|
||||
Step 4: Post-Implementation QR (quality review)
|
||||
Step 5: QR Issue Resolution (conditional, fix issues)
|
||||
Step 6: Documentation (TW pass)
|
||||
Step 7: Retrospective (present summary)
|
||||
|
||||
Usage:
|
||||
python3 executor.py --plan-file PATH --step-number 1 --total-steps 7 --thoughts "..."
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def detect_reconciliation_signals(thoughts: str) -> bool:
|
||||
"""Check if user's thoughts contain reconciliation triggers."""
|
||||
triggers = [
|
||||
r"\balready\s+(implemented|done|complete)",
|
||||
r"\bpartially\s+complete",
|
||||
r"\bhalfway\s+done",
|
||||
r"\bresume\b",
|
||||
r"\bcontinue\s+from\b",
|
||||
r"\bpick\s+up\s+where\b",
|
||||
r"\bcheck\s+what'?s\s+done\b",
|
||||
r"\bverify\s+existing\b",
|
||||
r"\bprior\s+work\b",
|
||||
]
|
||||
thoughts_lower = thoughts.lower()
|
||||
return any(re.search(pattern, thoughts_lower) for pattern in triggers)
|
||||
|
||||
|
||||
def get_step_1_guidance(plan_file: str, thoughts: str) -> dict:
|
||||
"""Step 1: Execution Planning - analyze plan, detect reconciliation."""
|
||||
reconciliation_detected = detect_reconciliation_signals(thoughts)
|
||||
|
||||
actions = [
|
||||
"EXECUTION PLANNING",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Read the plan file and analyze:",
|
||||
" 1. Count milestones and their dependencies",
|
||||
" 2. Identify file targets per milestone",
|
||||
" 3. Determine parallelization opportunities",
|
||||
" 4. Set up TodoWrite tracking for all milestones",
|
||||
"",
|
||||
"<execution_rules>",
|
||||
"",
|
||||
"RULE 0 (ABSOLUTE): Delegate ALL code work to specialized agents",
|
||||
"",
|
||||
"Your role: coordinate, validate, orchestrate. Agents implement code.",
|
||||
"",
|
||||
"Delegation routing:",
|
||||
" - New function needed -> @agent-developer",
|
||||
" - Bug to fix -> @agent-debugger (diagnose) then @agent-developer (fix)",
|
||||
" - Any source file modification -> @agent-developer",
|
||||
" - Documentation files -> @agent-technical-writer",
|
||||
"",
|
||||
"Exception (trivial only): Fixes under 5 lines where delegation overhead",
|
||||
"exceeds fix complexity (missing import, typo correction).",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"RULE 1: Execution Protocol",
|
||||
"",
|
||||
"Before ANY phase:",
|
||||
" 1. Use TodoWrite to track all plan phases",
|
||||
" 2. Analyze dependencies to identify parallelizable work",
|
||||
" 3. Delegate implementation to specialized agents",
|
||||
" 4. Validate each increment before proceeding",
|
||||
"",
|
||||
"You plan HOW to execute (parallelization, sequencing). You do NOT plan",
|
||||
"WHAT to execute -- that's the plan's job.",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"RULE 1.5: Model Selection",
|
||||
"",
|
||||
"Agent defaults (sonnet) are calibrated for quality. Adjust upward only.",
|
||||
"",
|
||||
" | Action | Allowed | Rationale |",
|
||||
" |----------------------|---------|----------------------------------|",
|
||||
" | Upgrade to opus | YES | Challenging tasks need reasoning |",
|
||||
" | Use default (sonnet) | YES | Baseline for all delegations |",
|
||||
" | Keep at sonnet+ | ALWAYS | Maintains quality baseline |",
|
||||
"",
|
||||
"</execution_rules>",
|
||||
"",
|
||||
"<dependency_analysis>",
|
||||
"",
|
||||
"Parallelizable when ALL conditions met:",
|
||||
" - Different target files",
|
||||
" - No data dependencies",
|
||||
" - No shared state (globals, configs, resources)",
|
||||
"",
|
||||
"Sequential when ANY condition true:",
|
||||
" - Same file modified by multiple tasks",
|
||||
" - Task B imports or depends on Task A's output",
|
||||
" - Shared database tables or external resources",
|
||||
"",
|
||||
"Before delegating ANY batch:",
|
||||
" 1. List tasks with their target files",
|
||||
" 2. Identify file dependencies (same file = sequential)",
|
||||
" 3. Identify data dependencies (imports = sequential)",
|
||||
" 4. Group independent tasks into parallel batches",
|
||||
" 5. Separate batches with sync points",
|
||||
"",
|
||||
"</dependency_analysis>",
|
||||
"",
|
||||
"<milestone_type_detection>",
|
||||
"",
|
||||
"Before delegating ANY milestone, identify its type from file extensions:",
|
||||
"",
|
||||
" | Milestone Type | Recognition Signal | Delegate To |",
|
||||
" |----------------|--------------------------------|-------------------------|",
|
||||
" | Documentation | ALL files are *.md or *.rst | @agent-technical-writer |",
|
||||
" | Code | ANY file is source code | @agent-developer |",
|
||||
"",
|
||||
"Mixed milestones: Split delegation -- @agent-developer first (code),",
|
||||
"then @agent-technical-writer (docs) after code completes.",
|
||||
"",
|
||||
"</milestone_type_detection>",
|
||||
"",
|
||||
"<delegation_format>",
|
||||
"",
|
||||
"EVERY delegation MUST use this structure:",
|
||||
"",
|
||||
" <delegation>",
|
||||
" <agent>@agent-[developer|debugger|technical-writer|quality-reviewer]</agent>",
|
||||
" <mode>[For TW/QR: plan-scrub|post-implementation|plan-review|reconciliation]</mode>",
|
||||
" <plan_source>[Absolute path to plan file]</plan_source>",
|
||||
" <milestone>[Milestone number and name]</milestone>",
|
||||
" <files>[Exact file paths from milestone]</files>",
|
||||
" <task>[Specific task description]</task>",
|
||||
" <acceptance_criteria>",
|
||||
" - [Criterion 1 from plan]",
|
||||
" - [Criterion 2 from plan]",
|
||||
" </acceptance_criteria>",
|
||||
" </delegation>",
|
||||
"",
|
||||
"For parallel delegations, wrap multiple blocks:",
|
||||
"",
|
||||
" <parallel_batch>",
|
||||
" <rationale>[Why these can run in parallel]</rationale>",
|
||||
" <sync_point>[Command to run after all complete]</sync_point>",
|
||||
" <delegation>...</delegation>",
|
||||
" <delegation>...</delegation>",
|
||||
" </parallel_batch>",
|
||||
"",
|
||||
"Agent limits:",
|
||||
" - @agent-developer: Maximum 4 parallel",
|
||||
" - @agent-debugger: Maximum 2 parallel",
|
||||
" - @agent-quality-reviewer: ALWAYS sequential",
|
||||
" - @agent-technical-writer: Can parallel across independent modules",
|
||||
"",
|
||||
"</delegation_format>",
|
||||
]
|
||||
|
||||
if reconciliation_detected:
|
||||
next_step = (
|
||||
"RECONCILIATION SIGNALS DETECTED in your thoughts.\n\n"
|
||||
"Invoke step 2 to validate existing code against plan requirements:\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 2 '
|
||||
'--total-steps 7 --thoughts "Starting reconciliation..."'
|
||||
)
|
||||
else:
|
||||
next_step = (
|
||||
"No reconciliation signals detected. Proceed to milestone execution.\n\n"
|
||||
"Invoke step 3 to begin delegating milestones:\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 3 '
|
||||
'--total-steps 7 --thoughts "Analyzed plan: N milestones, '
|
||||
'parallel batches: [describe], starting execution..."'
|
||||
)
|
||||
|
||||
return {
|
||||
"actions": actions,
|
||||
"next": next_step,
|
||||
}
|
||||
|
||||
|
||||
def get_step_2_guidance(plan_file: str) -> dict:
|
||||
"""Step 2: Reconciliation - validate existing code against plan."""
|
||||
return {
|
||||
"actions": [
|
||||
"RECONCILIATION PHASE",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Validate existing code against plan requirements BEFORE executing.",
|
||||
"",
|
||||
"<reconciliation_protocol>",
|
||||
"",
|
||||
"Delegate to @agent-quality-reviewer for each milestone:",
|
||||
"",
|
||||
" Task for @agent-quality-reviewer:",
|
||||
" Mode: reconciliation",
|
||||
" Plan Source: [plan_file.md]",
|
||||
" Milestone: [N]",
|
||||
"",
|
||||
" Check if the acceptance criteria for Milestone [N] are ALREADY",
|
||||
" satisfied in the current codebase. Validate REQUIREMENTS, not just",
|
||||
" code presence.",
|
||||
"",
|
||||
" Return: SATISFIED | NOT_SATISFIED | PARTIALLY_SATISFIED",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"Execution based on reconciliation result:",
|
||||
"",
|
||||
" | Result | Action |",
|
||||
" |---------------------|-------------------------------------------|",
|
||||
" | SATISFIED | Skip execution, record as already complete|",
|
||||
" | NOT_SATISFIED | Execute milestone normally |",
|
||||
" | PARTIALLY_SATISFIED | Execute only the missing parts |",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"Why requirements-based (not diff-based):",
|
||||
"",
|
||||
"Checking if code from the diff exists misses critical cases:",
|
||||
" - Code added but incorrect (doesn't meet acceptance criteria)",
|
||||
" - Code added but incomplete (partial implementation)",
|
||||
" - Requirements met by different code than planned (valid alternative)",
|
||||
"",
|
||||
"Checking acceptance criteria catches all of these.",
|
||||
"",
|
||||
"</reconciliation_protocol>",
|
||||
],
|
||||
"next": (
|
||||
"After collecting reconciliation results for all milestones, "
|
||||
"invoke step 3:\n\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 3 '
|
||||
"--total-steps 7 --thoughts \"Reconciliation complete: "
|
||||
'M1: SATISFIED, M2: NOT_SATISFIED, ..."'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_step_3_guidance(plan_file: str) -> dict:
|
||||
"""Step 3: Milestone Execution - delegate to agents, run tests."""
|
||||
return {
|
||||
"actions": [
|
||||
"MILESTONE EXECUTION",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Execute milestones through delegation. Parallelize independent work.",
|
||||
"",
|
||||
"<diff_compliance_validation>",
|
||||
"",
|
||||
"BEFORE delegating each milestone with code changes:",
|
||||
" 1. Read resources/diff-format.md if not already in context",
|
||||
" 2. Verify plan's diffs meet specification:",
|
||||
" - Context lines are VERBATIM from actual files (not placeholders)",
|
||||
" - WHY comments explain rationale (not WHAT code does)",
|
||||
" - No location directives in comments",
|
||||
"",
|
||||
"AFTER @agent-developer completes, verify:",
|
||||
" - Context lines from plan were found in target file",
|
||||
" - WHY comments were transcribed verbatim to code",
|
||||
" - No location directives remain in implemented code",
|
||||
" - No temporal contamination leaked (change-relative language)",
|
||||
"",
|
||||
"If Developer reports context lines not found, check drift table below.",
|
||||
"",
|
||||
"</diff_compliance_validation>",
|
||||
"",
|
||||
"<error_handling>",
|
||||
"",
|
||||
"Error classification:",
|
||||
"",
|
||||
" | Severity | Signals | Action |",
|
||||
" |----------|----------------------------------|-------------------------|",
|
||||
" | Critical | Segfault, data corruption | STOP, @agent-debugger |",
|
||||
" | High | Test failures, missing deps | @agent-debugger |",
|
||||
" | Medium | Type errors, lint failures | Auto-fix, then debugger |",
|
||||
" | Low | Warnings, style issues | Note and continue |",
|
||||
"",
|
||||
"Escalation triggers -- STOP and report when:",
|
||||
" - Fix would change fundamental approach",
|
||||
" - Three attempted solutions failed",
|
||||
" - Performance or safety characteristics affected",
|
||||
" - Confidence < 80%",
|
||||
"",
|
||||
"Context anchor mismatch protocol:",
|
||||
"",
|
||||
"When @agent-developer reports context lines don't match actual code:",
|
||||
"",
|
||||
" | Mismatch Type | Action |",
|
||||
" |-----------------------------|--------------------------------|",
|
||||
" | Whitespace/formatting only | Proceed with normalized match |",
|
||||
" | Minor variable rename | Proceed, note in execution log |",
|
||||
" | Code restructured | Proceed, note deviation |",
|
||||
" | Context lines not found | STOP - escalate to planner |",
|
||||
" | Logic fundamentally changed | STOP - escalate to planner |",
|
||||
"",
|
||||
"</error_handling>",
|
||||
"",
|
||||
"<acceptance_testing>",
|
||||
"",
|
||||
"Run after each milestone:",
|
||||
"",
|
||||
" # Python",
|
||||
" pytest --strict-markers --strict-config",
|
||||
" mypy --strict",
|
||||
"",
|
||||
" # JavaScript/TypeScript",
|
||||
" tsc --strict --noImplicitAny",
|
||||
" eslint --max-warnings=0",
|
||||
"",
|
||||
" # Go",
|
||||
" go test -race -cover -vet=all",
|
||||
"",
|
||||
"Pass criteria: 100% tests pass, zero linter warnings.",
|
||||
"",
|
||||
"Self-consistency check (for milestones with >3 files):",
|
||||
" 1. Developer's implementation notes claim: [what was implemented]",
|
||||
" 2. Test results demonstrate: [what behavior was verified]",
|
||||
" 3. Acceptance criteria state: [what was required]",
|
||||
"",
|
||||
"All three must align. Discrepancy = investigate before proceeding.",
|
||||
"",
|
||||
"</acceptance_testing>",
|
||||
],
|
||||
"next": (
|
||||
"CONTINUE in step 3 until ALL milestones complete:\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 3 '
|
||||
'--total-steps 7 --thoughts "Completed M1, M2. Executing M3..."'
|
||||
"\n\n"
|
||||
"When ALL milestones are complete, invoke step 4 for quality review:\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 4 '
|
||||
'--total-steps 7 --thoughts "All milestones complete. '
|
||||
'Modified files: [list]. Ready for QR."'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_step_4_guidance(plan_file: str) -> dict:
|
||||
"""Step 4: Post-Implementation QR - quality review."""
|
||||
return {
|
||||
"actions": [
|
||||
"POST-IMPLEMENTATION QUALITY REVIEW",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Delegate to @agent-quality-reviewer for comprehensive review.",
|
||||
"",
|
||||
"<qr_delegation>",
|
||||
"",
|
||||
" Task for @agent-quality-reviewer:",
|
||||
" Mode: post-implementation",
|
||||
" Plan Source: [plan_file.md]",
|
||||
" Files Modified: [list]",
|
||||
" Reconciled Milestones: [list milestones that were SATISFIED]",
|
||||
"",
|
||||
" Priority order for findings:",
|
||||
" 1. Issues in reconciled milestones (bypassed execution validation)",
|
||||
" 2. Issues in newly implemented milestones",
|
||||
" 3. Cross-cutting issues",
|
||||
"",
|
||||
" Checklist:",
|
||||
" - Every requirement implemented",
|
||||
" - No unauthorized deviations",
|
||||
" - Edge cases handled",
|
||||
" - Performance requirements met",
|
||||
"",
|
||||
"</qr_delegation>",
|
||||
"",
|
||||
"Expected output: PASS or issues list sorted by severity.",
|
||||
],
|
||||
"next": (
|
||||
"After QR completes:\n\n"
|
||||
"If QR returns ISSUES -> invoke step 5:\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 5 '
|
||||
'--total-steps 7 --thoughts "QR found N issues: [summary]"'
|
||||
"\n\n"
|
||||
"If QR returns PASS -> invoke step 6:\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 6 '
|
||||
'--total-steps 7 --thoughts "QR passed. Proceeding to documentation."'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_step_5_guidance(plan_file: str) -> dict:
|
||||
"""Step 5: QR Issue Resolution - present issues, collect decisions, fix."""
|
||||
return {
|
||||
"actions": [
|
||||
"QR ISSUE RESOLUTION",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Present issues to user, collect decisions, delegate fixes.",
|
||||
"",
|
||||
"<issue_resolution_protocol>",
|
||||
"",
|
||||
"Phase 1: Collect Decisions",
|
||||
"",
|
||||
"Sort findings by severity (critical -> high -> medium -> low).",
|
||||
"For EACH issue, present:",
|
||||
"",
|
||||
" ## Issue [N] of [Total] ([severity])",
|
||||
"",
|
||||
" **Category**: [production-reliability | project-conformance | structural-quality]",
|
||||
" **File**: [affected file path]",
|
||||
" **Location**: [function/line if applicable]",
|
||||
"",
|
||||
" **Problem**:",
|
||||
" [Clear description of what is wrong and why it matters]",
|
||||
"",
|
||||
" **Evidence**:",
|
||||
" [Specific code/behavior that demonstrates the issue]",
|
||||
"",
|
||||
"Then use AskUserQuestion with options:",
|
||||
" - **Fix**: Delegate to @agent-developer to resolve",
|
||||
" - **Skip**: Accept the issue as-is",
|
||||
" - **Alternative**: User provides different approach",
|
||||
"",
|
||||
"Repeat for each issue. Do NOT execute any fixes during this phase.",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"Phase 2: Execute Decisions",
|
||||
"",
|
||||
"After ALL decisions are collected:",
|
||||
"",
|
||||
" 1. Summarize the decisions",
|
||||
" 2. Execute fixes:",
|
||||
" - 'Fix' decisions: Delegate to @agent-developer",
|
||||
" - 'Skip' decisions: Record in retrospective as accepted risk",
|
||||
" - 'Alternative' decisions: Apply user's specified approach",
|
||||
" 3. Parallelize where possible (different files, no dependencies)",
|
||||
"",
|
||||
"</issue_resolution_protocol>",
|
||||
],
|
||||
"next": (
|
||||
"After ALL fixes are applied, return to step 4 for re-validation:\n\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 4 '
|
||||
'--total-steps 7 --thoughts "Applied fixes for issues X, Y, Z. '
|
||||
'Re-running QR."'
|
||||
"\n\n"
|
||||
"This creates a validation loop until QR passes."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_step_6_guidance(plan_file: str) -> dict:
|
||||
"""Step 6: Documentation - TW pass for CLAUDE.md, README.md."""
|
||||
return {
|
||||
"actions": [
|
||||
"POST-IMPLEMENTATION DOCUMENTATION",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Delegate to @agent-technical-writer for documentation updates.",
|
||||
"",
|
||||
"<tw_delegation>",
|
||||
"",
|
||||
"Skip condition: If ALL milestones contained only documentation files",
|
||||
"(*.md/*.rst), TW already handled this during milestone execution.",
|
||||
"Proceed directly to step 7.",
|
||||
"",
|
||||
"For code-primary plans:",
|
||||
"",
|
||||
" Task for @agent-technical-writer:",
|
||||
" Mode: post-implementation",
|
||||
" Plan Source: [plan_file.md]",
|
||||
" Files Modified: [list]",
|
||||
"",
|
||||
" Requirements:",
|
||||
" - Create/update CLAUDE.md index entries",
|
||||
" - Create README.md if architectural complexity warrants",
|
||||
" - Add module-level docstrings where missing",
|
||||
" - Verify transcribed comments are accurate",
|
||||
"",
|
||||
"</tw_delegation>",
|
||||
"",
|
||||
"<final_checklist>",
|
||||
"",
|
||||
"Execution is NOT complete until:",
|
||||
" - [ ] All todos completed",
|
||||
" - [ ] Quality review passed (no unresolved issues)",
|
||||
" - [ ] Documentation delegated for ALL modified files",
|
||||
" - [ ] Documentation tasks completed",
|
||||
" - [ ] Self-consistency checks passed for complex milestones",
|
||||
"",
|
||||
"</final_checklist>",
|
||||
],
|
||||
"next": (
|
||||
"After documentation is complete, invoke step 7 for retrospective:\n\n"
|
||||
f' python3 executor.py --plan-file "{plan_file}" --step-number 7 '
|
||||
'--total-steps 7 --thoughts "Documentation complete. '
|
||||
'Generating retrospective."'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_step_7_guidance(plan_file: str) -> dict:
|
||||
"""Step 7: Retrospective - present execution summary."""
|
||||
return {
|
||||
"actions": [
|
||||
"EXECUTION RETROSPECTIVE",
|
||||
"",
|
||||
f"Plan file: {plan_file}",
|
||||
"",
|
||||
"Generate and PRESENT the retrospective to the user.",
|
||||
"Do NOT write to a file -- present it directly so the user sees it.",
|
||||
"",
|
||||
"<retrospective_format>",
|
||||
"",
|
||||
"================================================================================",
|
||||
"EXECUTION RETROSPECTIVE",
|
||||
"================================================================================",
|
||||
"",
|
||||
"Plan: [plan file path]",
|
||||
"Status: COMPLETED | BLOCKED | ABORTED",
|
||||
"",
|
||||
"## Milestone Outcomes",
|
||||
"",
|
||||
"| Milestone | Status | Notes |",
|
||||
"| ---------- | -------------------- | ---------------------------------- |",
|
||||
"| 1: [name] | EXECUTED | - |",
|
||||
"| 2: [name] | SKIPPED (RECONCILED) | Already satisfied before execution |",
|
||||
"| 3: [name] | BLOCKED | [reason] |",
|
||||
"",
|
||||
"## Reconciliation Summary",
|
||||
"",
|
||||
"If reconciliation was run:",
|
||||
" - Milestones already complete: [count]",
|
||||
" - Milestones executed: [count]",
|
||||
" - Milestones with partial work detected: [count]",
|
||||
"",
|
||||
"If reconciliation was skipped:",
|
||||
' - "Reconciliation skipped (no prior work indicated)"',
|
||||
"",
|
||||
"## Plan Accuracy Issues",
|
||||
"",
|
||||
"[List any problems with the plan discovered during execution]",
|
||||
" - [file] Context anchor drift: expected X, found Y",
|
||||
" - Milestone [N] requirements were ambiguous: [what]",
|
||||
" - Missing dependency: [what was assumed but didn't exist]",
|
||||
"",
|
||||
'If none: "No plan accuracy issues encountered."',
|
||||
"",
|
||||
"## Deviations from Plan",
|
||||
"",
|
||||
"| Deviation | Category | Approved By |",
|
||||
"| -------------- | --------------- | ---------------- |",
|
||||
"| [what changed] | Trivial / Minor | [who or 'auto'] |",
|
||||
"",
|
||||
'If none: "No deviations from plan."',
|
||||
"",
|
||||
"## Quality Review Summary",
|
||||
"",
|
||||
" - Production reliability: [count] issues",
|
||||
" - Project conformance: [count] issues",
|
||||
" - Structural quality: [count] suggestions",
|
||||
"",
|
||||
"## Feedback for Future Plans",
|
||||
"",
|
||||
"[Actionable improvements based on execution experience]",
|
||||
" - [ ] [specific suggestion]",
|
||||
" - [ ] [specific suggestion]",
|
||||
"",
|
||||
"================================================================================",
|
||||
"",
|
||||
"</retrospective_format>",
|
||||
],
|
||||
"next": "EXECUTION COMPLETE.\n\nPresent the retrospective to the user.",
|
||||
}
|
||||
|
||||
|
||||
def get_step_guidance(step_number: int, plan_file: str, thoughts: str) -> dict:
|
||||
"""Route to appropriate step guidance."""
|
||||
if step_number == 1:
|
||||
return get_step_1_guidance(plan_file, thoughts)
|
||||
elif step_number == 2:
|
||||
return get_step_2_guidance(plan_file)
|
||||
elif step_number == 3:
|
||||
return get_step_3_guidance(plan_file)
|
||||
elif step_number == 4:
|
||||
return get_step_4_guidance(plan_file)
|
||||
elif step_number == 5:
|
||||
return get_step_5_guidance(plan_file)
|
||||
elif step_number == 6:
|
||||
return get_step_6_guidance(plan_file)
|
||||
elif step_number == 7:
|
||||
return get_step_7_guidance(plan_file)
|
||||
else:
|
||||
return {
|
||||
"actions": [f"Unknown step {step_number}. Valid steps are 1-7."],
|
||||
"next": "Re-invoke with a valid step number.",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plan Executor - Execute approved plans through delegation",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Start execution
|
||||
python3 executor.py --plan-file plans/auth.md --step-number 1 --total-steps 7 \\
|
||||
--thoughts "Execute the auth implementation plan"
|
||||
|
||||
# Continue milestone execution
|
||||
python3 executor.py --plan-file plans/auth.md --step-number 3 --total-steps 7 \\
|
||||
--thoughts "Completed M1, M2. Executing M3..."
|
||||
|
||||
# After QR finds issues
|
||||
python3 executor.py --plan-file plans/auth.md --step-number 5 --total-steps 7 \\
|
||||
--thoughts "QR found 2 issues: missing error handling, incorrect return type"
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--plan-file", type=str, required=True, help="Path to the plan file to execute"
|
||||
)
|
||||
parser.add_argument("--step-number", type=int, required=True, help="Current step (1-7)")
|
||||
parser.add_argument(
|
||||
"--total-steps", type=int, required=True, help="Total steps (always 7)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--thoughts", type=str, required=True, help="Your current thinking and status"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.step_number < 1 or args.step_number > 7:
|
||||
print("Error: step-number must be between 1 and 7", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.total_steps != 7:
|
||||
print("Warning: total-steps should be 7 for executor", file=sys.stderr)
|
||||
|
||||
guidance = get_step_guidance(args.step_number, args.plan_file, args.thoughts)
|
||||
is_complete = args.step_number >= 7
|
||||
|
||||
step_names = {
|
||||
1: "Execution Planning",
|
||||
2: "Reconciliation",
|
||||
3: "Milestone Execution",
|
||||
4: "Post-Implementation QR",
|
||||
5: "QR Issue Resolution",
|
||||
6: "Documentation",
|
||||
7: "Retrospective",
|
||||
}
|
||||
|
||||
print("=" * 80)
|
||||
print(
|
||||
f"EXECUTOR - Step {args.step_number} of 7: {step_names.get(args.step_number, 'Unknown')}"
|
||||
)
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"STATUS: {'execution_complete' if is_complete else 'in_progress'}")
|
||||
print()
|
||||
print("YOUR THOUGHTS:")
|
||||
print(args.thoughts)
|
||||
print()
|
||||
|
||||
if guidance["actions"]:
|
||||
print("GUIDANCE:")
|
||||
print()
|
||||
for action in guidance["actions"]:
|
||||
print(action)
|
||||
print()
|
||||
|
||||
print("NEXT:")
|
||||
print(guidance["next"])
|
||||
print()
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
# skills/problem-analysis/
|
||||
|
||||
## Overview
|
||||
|
||||
Structured problem analysis skill. IMMEDIATELY invoke the script - do NOT
|
||||
explore first.
|
||||
|
||||
## Index
|
||||
|
||||
| File/Directory | Contents | Read When |
|
||||
| -------------------- | ----------------- | ------------------ |
|
||||
| `SKILL.md` | Invocation | Using this skill |
|
||||
| `scripts/analyze.py` | Complete workflow | Debugging behavior |
|
||||
|
||||
## Key Point
|
||||
|
||||
The script IS the workflow. It handles decomposition, solution generation,
|
||||
critique, verification, and synthesis. Do NOT analyze before invoking. Run the
|
||||
script and obey its output.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Problem Analysis
|
||||
|
||||
LLMs jump to solutions. You describe a problem, they propose an answer. For
|
||||
complex decisions with multiple viable paths, that first answer often reflects
|
||||
the LLM's biases rather than the best fit for your constraints. This skill
|
||||
forces structured reasoning before you commit.
|
||||
|
||||
The skill runs through six phases:
|
||||
|
||||
| Phase | Actions |
|
||||
| ----------- | ------------------------------------------------------------------------ |
|
||||
| Decompose | State problem; identify hard/soft constraints, variables, assumptions |
|
||||
| Generate | Create 2-4 distinct approaches (fundamentally different, not variations) |
|
||||
| Critique | Specific weaknesses; eliminate or refine |
|
||||
| Verify | Answer questions WITHOUT looking at solutions |
|
||||
| Cross-check | Reconcile verified facts with original claims; update viability |
|
||||
| Synthesize | Trade-off matrix with verified facts; decision framework |
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this for decisions where the cost of choosing wrong is high:
|
||||
|
||||
- Multiple viable technical approaches (Redis vs Postgres, REST vs GraphQL)
|
||||
- Architectural decisions with long-term consequences
|
||||
- Problems where you suspect your first instinct might be wrong
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
I need to decide how to handle distributed locking in our microservices.
|
||||
Options I'm considering:
|
||||
|
||||
- Redis with Redlock algorithm
|
||||
- ZooKeeper
|
||||
- Database advisory locks
|
||||
|
||||
Use your problem-analysis skill to structure this decision.
|
||||
```
|
||||
|
||||
## The Design
|
||||
|
||||
The structure prevents premature convergence. Critique catches obvious flaws
|
||||
before costly verification. Factored verification prevents confirmation bias --
|
||||
you answer questions without seeing your original solutions. Cross-check forces
|
||||
explicit reconciliation of evidence with claims.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: problem-analysis
|
||||
description: Invoke IMMEDIATELY for structured problem analysis and solution discovery.
|
||||
---
|
||||
|
||||
# Problem Analysis
|
||||
|
||||
When this skill activates, IMMEDIATELY invoke the script. The script IS the
|
||||
workflow.
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
python3 scripts/analyze.py \
|
||||
--step 1 \
|
||||
--total-steps 7 \
|
||||
--thoughts "Problem: <describe>"
|
||||
```
|
||||
|
||||
| Argument | Required | Description |
|
||||
| --------------- | -------- | ----------------------------------------- |
|
||||
| `--step` | Yes | Current step (starts at 1) |
|
||||
| `--total-steps` | Yes | Minimum 7; adjust as script instructs |
|
||||
| `--thoughts` | Yes | Accumulated state from all previous steps |
|
||||
|
||||
Do NOT analyze or explore first. Run the script and follow its output.
|
||||
@@ -0,0 +1,379 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Problem Analysis Skill - Structured deep reasoning workflow.
|
||||
|
||||
Guides problem analysis through seven phases:
|
||||
1. Decompose - understand problem space, constraints, assumptions
|
||||
2. Generate - create initial solution approaches
|
||||
3. Expand - push for MORE solutions not yet considered
|
||||
4. Critique - Self-Refine feedback on solutions
|
||||
5. Verify - factored verification of assumptions
|
||||
6. Cross-check - reconcile verified facts with claims
|
||||
7. Synthesize - structured trade-off analysis
|
||||
|
||||
Extra steps beyond 7 go to verification (where accuracy improves most).
|
||||
|
||||
Usage:
|
||||
python3 analyze.py --step 1 --total-steps 7 --thoughts "Problem: <describe the decision or challenge>"
|
||||
|
||||
Research grounding:
|
||||
- ToT (Yao 2023): decompose into thoughts "small enough for diverse samples,
|
||||
big enough to evaluate"
|
||||
- CoVe (Dhuliawala 2023): factored verification improves accuracy 17%->70%.
|
||||
Use OPEN questions, not yes/no ("model tends to agree whether right or wrong")
|
||||
- Self-Refine (Madaan 2023): feedback must be "actionable and specific";
|
||||
separate feedback from refinement for 5-40% improvement
|
||||
- Analogical Prompting (Yasunaga 2024): "recall relevant and distinct problems"
|
||||
improves reasoning; diversity in self-generated examples is critical
|
||||
- Diversity-Based Selection (Zhang 2022): "even with 50% wrong demonstrations,
|
||||
diversity-based clustering performance does not degrade significantly"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def get_step_1_guidance():
|
||||
"""Step 1: Problem Decomposition - understand the problem space."""
|
||||
return (
|
||||
"Problem Decomposition",
|
||||
[
|
||||
"State the CORE PROBLEM in one sentence: 'I need to decide X'",
|
||||
"",
|
||||
"List HARD CONSTRAINTS (non-negotiable):",
|
||||
" - Hard constraints: latency limits, accuracy requirements, compatibility",
|
||||
" - Resource constraints: budget, timeline, skills, capacity",
|
||||
" - Quality constraints: what 'good' looks like for this problem",
|
||||
"",
|
||||
"List SOFT CONSTRAINTS (preferences, can trade off)",
|
||||
"",
|
||||
"List VARIABLES (what you control):",
|
||||
" - Structural choices (architecture, format, organization)",
|
||||
" - Content choices (scope, depth, audience, tone)",
|
||||
" - Process choices (workflow, tools, automation level)",
|
||||
"",
|
||||
"Surface HIDDEN ASSUMPTIONS by asking:",
|
||||
" 'What am I assuming about scale/load patterns?'",
|
||||
" 'What am I assuming about the team's capabilities?'",
|
||||
" 'What am I assuming will NOT change?'",
|
||||
"",
|
||||
"If unclear, use AskUserQuestion to clarify",
|
||||
],
|
||||
[
|
||||
"PROBLEM (one sentence)",
|
||||
"HARD CONSTRAINTS (non-negotiable)",
|
||||
"SOFT CONSTRAINTS (preferences)",
|
||||
"VARIABLES (what you control)",
|
||||
"ASSUMPTIONS (surfaced via questions)",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_step_2_guidance():
|
||||
"""Step 2: Solution Generation - create distinct approaches."""
|
||||
return (
|
||||
"Solution Generation",
|
||||
[
|
||||
"Generate 2-4 DISTINCT solution approaches",
|
||||
"",
|
||||
"Solutions must differ on a FUNDAMENTAL AXIS:",
|
||||
" - Scope: narrow-deep vs broad-shallow",
|
||||
" - Complexity: simple-but-limited vs complex-but-flexible",
|
||||
" - Control: standardized vs customizable",
|
||||
" - Approach: build vs buy, manual vs automated, centralized vs distributed",
|
||||
" (Identify axes specific to your problem domain)",
|
||||
"",
|
||||
"For EACH solution, document:",
|
||||
" - Name: short label (e.g., 'Option A', 'Hybrid Approach')",
|
||||
" - Core mechanism: HOW it solves the problem (1-2 sentences)",
|
||||
" - Key assumptions: what must be true for this to work",
|
||||
" - Claimed benefits: what this approach provides",
|
||||
"",
|
||||
"AVOID premature convergence - do not favor one solution yet",
|
||||
],
|
||||
[
|
||||
"PROBLEM (from step 1)",
|
||||
"CONSTRAINTS (from step 1)",
|
||||
"SOLUTIONS (each with: name, mechanism, assumptions, claimed benefits)",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_step_3_guidance():
|
||||
"""Step 3: Solution Expansion - push beyond initial ideas."""
|
||||
return (
|
||||
"Solution Expansion",
|
||||
[
|
||||
"Review the solutions from step 2. Now PUSH FURTHER:",
|
||||
"",
|
||||
"UNEXPLORED AXES - What fundamental trade-offs were NOT represented?",
|
||||
" - If all solutions are complex, what's the SIMPLEST approach?",
|
||||
" - If all are centralized, what's DISTRIBUTED?",
|
||||
" - If all use technology X, what uses its OPPOSITE or COMPETITOR?",
|
||||
" - If all optimize for metric A, what optimizes for metric B?",
|
||||
"",
|
||||
"ADJACENT DOMAINS - What solutions from RELATED problems might apply?",
|
||||
" 'How does [related domain] solve similar problems?'",
|
||||
" 'What would [different industry/field] do here?'",
|
||||
" 'What patterns from ADJACENT DOMAINS might apply?'",
|
||||
"",
|
||||
"ANTI-SOLUTIONS - What's the OPPOSITE of each current solution?",
|
||||
" If Solution A is stateful, what's stateless?",
|
||||
" If Solution A is synchronous, what's asynchronous?",
|
||||
" If Solution A is custom-built, what's off-the-shelf?",
|
||||
"",
|
||||
"NULL/MINIMAL OPTIONS:",
|
||||
" - What if we did NOTHING and accepted the current state?",
|
||||
" - What if we solved a SMALLER version of the problem?",
|
||||
" - What's the 80/20 solution that's 'good enough'?",
|
||||
"",
|
||||
"ADD 1-3 MORE solutions. Each must represent an axis/approach",
|
||||
"not covered by the initial set.",
|
||||
],
|
||||
[
|
||||
"INITIAL SOLUTIONS (from step 2)",
|
||||
"AXES NOT YET EXPLORED (identified gaps)",
|
||||
"NEW SOLUTIONS (1-3 additional, each with: name, mechanism, assumptions)",
|
||||
"COMPLETE SOLUTION SET (all solutions for next phase)",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_step_4_guidance():
|
||||
"""Step 4: Solution Critique - Self-Refine feedback phase."""
|
||||
return (
|
||||
"Solution Critique",
|
||||
[
|
||||
"For EACH solution, identify weaknesses:",
|
||||
" - What could go wrong? (failure modes)",
|
||||
" - What does this solution assume that might be false?",
|
||||
" - Where is the complexity hiding?",
|
||||
" - What operational burden does this create?",
|
||||
"",
|
||||
"Generate SPECIFIC, ACTIONABLE feedback:",
|
||||
" BAD: 'This might have scaling issues'",
|
||||
" GOOD: 'Single-node Redis fails at >100K ops/sec; Solution A",
|
||||
" assumes <50K ops/sec but requirements say 200K'",
|
||||
"",
|
||||
"Identify which solutions should be:",
|
||||
" - ELIMINATED: fatal flaw, violates hard constraint",
|
||||
" - REFINED: fixable weakness, needs modification",
|
||||
" - ADVANCED: no obvious flaws, proceed to verification",
|
||||
"",
|
||||
"For REFINED solutions, state the specific modification needed",
|
||||
],
|
||||
[
|
||||
"SOLUTIONS (from step 2)",
|
||||
"CRITIQUE for each (specific weaknesses, failure modes)",
|
||||
"DISPOSITION: ELIMINATED / REFINED / ADVANCED for each",
|
||||
"MODIFICATIONS needed for REFINED solutions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_verification_guidance():
|
||||
"""
|
||||
Steps 4 to N-2: Factored Assumption Verification.
|
||||
|
||||
Key insight from CoVe: answer verification questions WITHOUT attending
|
||||
to the original solutions. Models that see their own hallucinations
|
||||
tend to repeat them.
|
||||
"""
|
||||
return (
|
||||
"Factored Verification",
|
||||
[
|
||||
"FACTORED VERIFICATION (answer WITHOUT looking at solutions):",
|
||||
"",
|
||||
"Step A - List assumptions as OPEN questions:",
|
||||
" BAD: 'Is option A better?' (yes/no triggers agreement bias)",
|
||||
" GOOD: 'What throughput does option A achieve under heavy load?'",
|
||||
" GOOD: 'What reading level does this document require?'",
|
||||
" GOOD: 'How long does this workflow take with the proposed automation?'",
|
||||
"",
|
||||
"Step B - Answer each question INDEPENDENTLY:",
|
||||
" - Pretend you have NOT seen the solutions",
|
||||
" - Answer from first principles or domain knowledge",
|
||||
" - Do NOT defend any solution; seek truth",
|
||||
" - Cite sources or reasoning for each answer",
|
||||
"",
|
||||
"Step C - Categorize each assumption:",
|
||||
" VERIFIED: evidence confirms the assumption",
|
||||
" FALSIFIED: evidence contradicts (note: 'claimed X, actually Y')",
|
||||
" UNCERTAIN: insufficient evidence; note what would resolve it",
|
||||
],
|
||||
[
|
||||
"SOLUTIONS still under consideration",
|
||||
"VERIFICATION QUESTIONS (open, not yes/no)",
|
||||
"ANSWERS (independent, from first principles)",
|
||||
"CATEGORIZED: VERIFIED / FALSIFIED / UNCERTAIN for each",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_crosscheck_guidance():
|
||||
"""
|
||||
Step N-1: Cross-check - reconcile verified facts with original claims.
|
||||
|
||||
From CoVe Factor+Revise: explicit cross-check achieves +7.7 FACTSCORE
|
||||
points over factored verification alone.
|
||||
"""
|
||||
return (
|
||||
"Cross-Check",
|
||||
[
|
||||
"Reconcile verified facts with solution claims:",
|
||||
"",
|
||||
"For EACH surviving solution:",
|
||||
" - Which claims are now SUPPORTED by verification?",
|
||||
" - Which claims are CONTRADICTED? (list specific contradictions)",
|
||||
" - Which claims remain UNTESTED?",
|
||||
"",
|
||||
"Update solution viability:",
|
||||
" - Mark solutions with falsified CORE assumptions as ELIMINATED",
|
||||
" - Note which solutions gained credibility (verified strengths)",
|
||||
" - Note which solutions lost credibility (falsified claims)",
|
||||
"",
|
||||
"Check for EMERGENT solutions:",
|
||||
" - Do verified facts suggest an approach not previously considered?",
|
||||
" - Can surviving solutions be combined based on verified strengths?",
|
||||
],
|
||||
[
|
||||
"SOLUTIONS with updated status",
|
||||
"SUPPORTED claims (with evidence)",
|
||||
"CONTRADICTED claims (with specific contradictions)",
|
||||
"UNTESTED claims",
|
||||
"ELIMINATED solutions (if any, with reason)",
|
||||
"EMERGENT solutions (if any)",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_final_step_guidance():
|
||||
"""Final step: Structured Trade-off Synthesis."""
|
||||
return (
|
||||
"Trade-off Synthesis",
|
||||
[
|
||||
"STRUCTURED SYNTHESIS:",
|
||||
"",
|
||||
"1. SURVIVING SOLUTIONS:",
|
||||
" List solutions NOT eliminated by falsified assumptions",
|
||||
"",
|
||||
"2. TRADE-OFF MATRIX (verified facts only):",
|
||||
" For each dimension that matters to THIS decision:",
|
||||
" - Measurable outcomes: 'A achieves X; B achieves Y (verified)'",
|
||||
" - Complexity/effort: 'A requires N; B requires M'",
|
||||
" - Risk profile: 'A fails when...; B fails when...'",
|
||||
" (Add dimensions specific to your problem)",
|
||||
"",
|
||||
"3. DECISION FRAMEWORK:",
|
||||
" 'If [hard constraint] is paramount -> choose A because...'",
|
||||
" 'If [other priority] matters more -> choose B because...'",
|
||||
" 'If uncertain about [X] -> gather [specific data] first'",
|
||||
"",
|
||||
"4. RECOMMENDATION (if one solution dominates):",
|
||||
" State which solution and the single strongest reason",
|
||||
" Acknowledge what you're giving up by choosing it",
|
||||
],
|
||||
[], # No next step
|
||||
)
|
||||
|
||||
|
||||
def get_guidance(step: int, total_steps: int):
|
||||
"""
|
||||
Dispatch to appropriate guidance based on step number.
|
||||
|
||||
7-phase structure:
|
||||
Step 1: Decomposition
|
||||
Step 2: Generation (initial solutions)
|
||||
Step 3: Expansion (push for MORE solutions)
|
||||
Step 4: Critique (Self-Refine feedback)
|
||||
Steps 5-N-2: Verification (factored, extra steps go here)
|
||||
Step N-1: Cross-check
|
||||
Step N: Synthesis
|
||||
"""
|
||||
if step == 1:
|
||||
return get_step_1_guidance()
|
||||
if step == 2:
|
||||
return get_step_2_guidance()
|
||||
if step == 3:
|
||||
return get_step_3_guidance()
|
||||
if step == 4:
|
||||
return get_step_4_guidance()
|
||||
if step == total_steps:
|
||||
return get_final_step_guidance()
|
||||
if step == total_steps - 1:
|
||||
return get_crosscheck_guidance()
|
||||
# Steps 5 to N-2 are verification
|
||||
return get_verification_guidance()
|
||||
|
||||
|
||||
def format_output(step: int, total_steps: int, thoughts: str) -> str:
|
||||
"""Format output for display."""
|
||||
title, actions, next_state = get_guidance(step, total_steps)
|
||||
is_complete = step >= total_steps
|
||||
|
||||
lines = [
|
||||
"=" * 70,
|
||||
f"PROBLEM ANALYSIS - Step {step}/{total_steps}: {title}",
|
||||
"=" * 70,
|
||||
"",
|
||||
"ACCUMULATED STATE:",
|
||||
thoughts[:1200] + "..." if len(thoughts) > 1200 else thoughts,
|
||||
"",
|
||||
"ACTIONS:",
|
||||
]
|
||||
lines.extend(f" {action}" for action in actions)
|
||||
|
||||
if not is_complete and next_state:
|
||||
lines.append("")
|
||||
lines.append("NEXT STEP STATE MUST INCLUDE:")
|
||||
lines.extend(f" - {item}" for item in next_state)
|
||||
|
||||
lines.append("")
|
||||
|
||||
if is_complete:
|
||||
lines.extend([
|
||||
"COMPLETE - Present to user:",
|
||||
" 1. Problem and constraints (from decomposition)",
|
||||
" 2. Solutions considered (including eliminated ones and why)",
|
||||
" 3. Verified facts (from factored verification)",
|
||||
" 4. Trade-off matrix with decision framework",
|
||||
" 5. Recommendation (if one dominates) or decision criteria",
|
||||
])
|
||||
else:
|
||||
next_title, _, _ = get_guidance(step + 1, total_steps)
|
||||
lines.extend([
|
||||
f"NEXT: Step {step + 1} - {next_title}",
|
||||
f"REMAINING: {total_steps - step} step(s)",
|
||||
"",
|
||||
"ADJUST: increase --total-steps if more verification needed (min 7)",
|
||||
])
|
||||
|
||||
lines.extend(["", "=" * 70])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Problem Analysis - Structured deep reasoning",
|
||||
epilog=(
|
||||
"Phases: decompose (1) -> generate (2) -> expand (3) -> "
|
||||
"critique (4) -> verify (5 to N-2) -> cross-check (N-1) -> synthesize (N)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--step", type=int, required=True)
|
||||
parser.add_argument("--total-steps", type=int, required=True)
|
||||
parser.add_argument("--thoughts", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.step < 1:
|
||||
sys.exit("ERROR: --step must be >= 1")
|
||||
if args.total_steps < 7:
|
||||
sys.exit("ERROR: --total-steps must be >= 7 (requires 7 phases)")
|
||||
if args.step > args.total_steps:
|
||||
sys.exit("ERROR: --step cannot exceed --total-steps")
|
||||
|
||||
print(format_output(args.step, args.total_steps, args.thoughts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
# skills/prompt-engineer/
|
||||
|
||||
## Overview
|
||||
|
||||
Prompt optimization skill using research-backed techniques. IMMEDIATELY invoke
|
||||
the script - do NOT explore or analyze first.
|
||||
|
||||
## Index
|
||||
|
||||
| File/Directory | Contents | Read When |
|
||||
| ---------------------------------------------- | ---------------------- | ------------------ |
|
||||
| `SKILL.md` | Invocation | Using this skill |
|
||||
| `scripts/optimize.py` | Complete workflow | Debugging behavior |
|
||||
| `references/prompt-engineering-single-turn.md` | Single-turn techniques | Script instructs |
|
||||
| `references/prompt-engineering-multi-turn.md` | Multi-turn techniques | Script instructs |
|
||||
|
||||
## Key Point
|
||||
|
||||
The script IS the workflow. It handles triage, blind problem identification,
|
||||
planning, factored verification, feedback, refinement, and integration. Do NOT
|
||||
analyze before invoking. Run the script and obey its output.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Prompt Engineer
|
||||
|
||||
Prompts are code. They have bugs, edge cases, and failure modes. This skill
|
||||
treats prompt optimization as a systematic discipline -- analyzing issues,
|
||||
applying documented patterns, and proposing changes with explicit rationale.
|
||||
|
||||
I use this on my own workflow. The skill was optimized using itself -- of
|
||||
course.
|
||||
|
||||
## When to Use
|
||||
|
||||
- A sub-agent definition that misbehaves (agents/developer.md)
|
||||
- A Python script with embedded prompts that underperform
|
||||
(skills/planner/scripts/planner.py)
|
||||
- A multi-prompt workflow that produces inconsistent results
|
||||
- Any prompt that does not do what you intended
|
||||
|
||||
## How It Works
|
||||
|
||||
The skill:
|
||||
|
||||
1. Reads prompt engineering pattern references
|
||||
2. Analyzes the target prompt for issues
|
||||
3. Proposes changes with explicit pattern attribution
|
||||
4. Waits for approval before applying changes
|
||||
5. Presents optimized result with self-verification
|
||||
|
||||
I use recitation and careful output ordering to ground the skill in the
|
||||
referenced patterns. This prevents the model from inventing techniques.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Optimize a sub-agent:
|
||||
|
||||
```
|
||||
Use your prompt engineer skill to optimize the system prompt for
|
||||
the following claude code sub-agent: agents/developer.md
|
||||
```
|
||||
|
||||
Optimize a multi-prompt workflow:
|
||||
|
||||
```
|
||||
Consider @skills/planner/scripts/planner.py. Identify all prompts,
|
||||
understand how they interact, then use your prompt engineer skill
|
||||
to optimize each.
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
Each proposed change includes scope, problem, technique, before/after, and
|
||||
rationale. A single invocation may propose many changes:
|
||||
|
||||
```
|
||||
+==============================================================================+
|
||||
| CHANGE 1: Add STOP gate to Step 1 (Exploration) |
|
||||
+==============================================================================+
|
||||
| |
|
||||
| SCOPE |
|
||||
| ----- |
|
||||
| Prompt: analyze.py step 1 |
|
||||
| Section: Lines 41-49 (precondition check) |
|
||||
| Downstream: All subsequent steps depend on exploration results |
|
||||
| |
|
||||
+------------------------------------------------------------------------------+
|
||||
| |
|
||||
| PROBLEM |
|
||||
| ------- |
|
||||
| Issue: Hedging language allows model to skip precondition |
|
||||
| |
|
||||
| Evidence: "PRECONDITION: You should have already delegated..." |
|
||||
| "If you have not, STOP and do that first" |
|
||||
| |
|
||||
| Runtime: Model proceeds to "process exploration results" without having |
|
||||
| any results, produces empty/fabricated structure analysis |
|
||||
| |
|
||||
+------------------------------------------------------------------------------+
|
||||
| |
|
||||
| TECHNIQUE |
|
||||
| --------- |
|
||||
| Apply: STOP Escalation Pattern (single-turn ref) |
|
||||
| |
|
||||
| Trigger: "For behaviors you need to interrupt, not just discourage" |
|
||||
| Effect: "Creates metacognitive checkpoint--the model must pause and |
|
||||
| re-evaluate before proceeding" |
|
||||
| Stacks: Affirmative Directives |
|
||||
| |
|
||||
+------------------------------------------------------------------------------+
|
||||
| |
|
||||
| BEFORE |
|
||||
| ------ |
|
||||
| +----------------------------------------------------------------------+ |
|
||||
| | "PRECONDITION: You should have already delegated to the Explore | |
|
||||
| | sub-agent.", | |
|
||||
| | "If you have not, STOP and do that first:", | |
|
||||
| +----------------------------------------------------------------------+ |
|
||||
| |
|
||||
| | |
|
||||
| v |
|
||||
| |
|
||||
| AFTER |
|
||||
| ----- |
|
||||
| +----------------------------------------------------------------------+ |
|
||||
| | "STOP. Before proceeding, verify you have Explore agent results.", | |
|
||||
| | "", | |
|
||||
| | "If your --thoughts do NOT contain Explore agent output, you MUST:", | |
|
||||
| | " 1. Use Task tool with subagent_type='Explore' | |
|
||||
| | " 2. Prompt: 'Explore this repository. Report directory structure, | |
|
||||
| | " tech stack, entry points, main components, observed patterns.' | |
|
||||
| | " 3. WAIT for results before invoking this step again | |
|
||||
| | "", | |
|
||||
| | "Only proceed below if you have concrete Explore output to process." | |
|
||||
| +----------------------------------------------------------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------------------------+
|
||||
| |
|
||||
| WHY THIS IMPROVES QUALITY |
|
||||
| ------------------------- |
|
||||
| Transforms soft precondition into hard gate. Model must explicitly verify |
|
||||
| it has Explore results before processing, preventing fabricated analysis. |
|
||||
| |
|
||||
+==============================================================================+
|
||||
|
||||
... many more
|
||||
|
||||
|
||||
---
|
||||
Compatibility check:
|
||||
- STOP Escalation + Affirmative Directives: Compatible (STOP is for interrupting specific behaviors)
|
||||
- History Accumulation + Completeness Checkpoint Tags: Synergistic (both enforce state tracking)
|
||||
- Quote Extraction + Chain-of-Verification: Complementary (both prevent hallucination)
|
||||
- Progressive depth + Pre-Work Context Analysis: Sequential (planning enables deeper execution)
|
||||
|
||||
Anti-patterns verified:
|
||||
- No hedging spiral (replaced "should have" with "STOP. Verify...")
|
||||
- No everything-is-critical (CRITICAL used only for state requirement)
|
||||
- Affirmative directives used (changed negatives to positives)
|
||||
- No implicit category trap (explicit checklists provided)
|
||||
|
||||
---
|
||||
Does this plan look reasonable? I'll apply these changes once you confirm.
|
||||
```
|
||||
|
||||
## Caveat
|
||||
|
||||
When you tell an LLM "find problems and opportunities for optimization", it will
|
||||
find problems. That is what you asked it to do. Some may not be real issues.
|
||||
|
||||
I recommend invoking the skill multiple times on challenging prompts, but
|
||||
recognize when it is good enough and stop. Diminishing returns are real.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: prompt-engineer
|
||||
description: Invoke IMMEDIATELY via python script when user requests prompt optimization. Do NOT analyze first - invoke this skill immediately.
|
||||
---
|
||||
|
||||
# Prompt Engineer
|
||||
|
||||
When this skill activates, IMMEDIATELY invoke the script. The script IS the
|
||||
workflow.
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
python3 scripts/optimize.py \
|
||||
--step 1 \
|
||||
--total-steps 9 \
|
||||
--thoughts "Prompt: <path or description>"
|
||||
```
|
||||
|
||||
| Argument | Required | Description |
|
||||
| --------------- | -------- | ----------------------------------------- |
|
||||
| `--step` | Yes | Current step (starts at 1) |
|
||||
| `--total-steps` | Yes | Minimum 9; adjust as script instructs |
|
||||
| `--thoughts` | Yes | Accumulated state from all previous steps |
|
||||
|
||||
Do NOT analyze or explore first. Run the script and follow its output.
|
||||
@@ -0,0 +1,790 @@
|
||||
# Prompt Engineering: Research-Backed Techniques for Multi-Turn Prompts
|
||||
|
||||
This document synthesizes practical prompt engineering patterns with academic research on iterative LLM reasoning. All techniques target **multi-turn prompts**—structured sequences of messages where output from one turn becomes input to subsequent turns. These techniques leverage the observation that models can improve their own outputs through deliberate self-examination across multiple passes.
|
||||
|
||||
**Prerequisite**: This guide assumes familiarity with single-turn techniques (CoT, Plan-and-Solve, RE2, etc.). Multi-turn techniques often enhance or extend single-turn methods across message boundaries.
|
||||
|
||||
**Meta-principle**: The value of multi-turn prompting comes from separation of concerns—each turn has a distinct cognitive goal (generate, critique, verify, synthesize). Mixing these goals within a single turn reduces effectiveness.
|
||||
|
||||
---
|
||||
|
||||
## Technique Selection Guide
|
||||
|
||||
| Domain | Technique | Trigger Condition | Stacks With | Conflicts With | Cost/Tradeoff | Effect |
|
||||
| ------------------- | -------------------------- | ------------------------------------------------------ | ------------------------------------ | -------------------------- | ---------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| **Refinement** | Self-Refine | Output quality improvable through iteration | Any single-turn reasoning technique | Time-critical tasks | 2-4x tokens per iteration | 5-40% absolute improvement across 7 task types |
|
||||
| **Refinement** | Iterative Critique | Specific quality dimensions need improvement | Self-Refine, Format Strictness | — | Moderate; targeted feedback reduces iterations | Monotonic improvement on scored dimensions |
|
||||
| **Verification** | Chain-of-Verification | Factual accuracy critical; hallucination risk | Quote Extraction (single-turn) | Joint verification | 3-4x tokens (baseline + verify + revise) | List-based QA: 17%→70% accuracy; FACTSCORE: 55.9→71.4 |
|
||||
| **Verification** | Factored Verification | High hallucination persistence in joint verification | CoVe | Joint CoVe | Additional token cost for separation | Outperforms joint CoVe by 3-8 points across tasks |
|
||||
| **Aggregation** | Universal Self-Consistency | Free-form output; standard SC inapplicable | Any sampling technique | Greedy decoding | N samples + 1 selection call | Matches SC on math; enables SC for open-ended tasks |
|
||||
| **Aggregation** | Multi-Chain Reasoning | Evidence scattered across reasoning attempts | Self-Consistency, CoT | Single-chain reliance | N chains + 1 meta-reasoning call | +5.7% over SC on multi-hop QA; high-quality explanations |
|
||||
| **Aggregation** | Complexity-Weighted Voting | Varying reasoning depth across samples | Self-Consistency, USC | Simple majority voting | Minimal; selection strategy only | Further gains over standard SC (+2-3 points) |
|
||||
| **Meta-Reasoning** | Chain Synthesis | Multiple valid reasoning paths exist | MCR, USC | — | Moderate; synthesis pass | Combines complementary facts from different chains |
|
||||
| **Meta-Reasoning** | Explanation Generation | Interpretability required alongside answer | MCR | — | Included in meta-reasoning pass | 82% of explanations rated high-quality |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Key Principles
|
||||
|
||||
1. **Self-Refine for Iterative Improvement** — Feedback must be actionable ("use the formula n(n+1)/2") and specific ("the for loop is brute force"); vague feedback fails
|
||||
2. **Separate Feedback from Refinement** — Generate feedback in one turn, apply it in another; mixing degrades both
|
||||
3. **Factored Verification Beats Joint** — Answer verification questions without attending to the original response; prevents hallucination copying
|
||||
4. **Shortform Questions Beat Longform** — 70% accuracy on individual verification questions vs. 17% for the same facts in longform generation
|
||||
5. **Universal Self-Consistency for Free-Form** — When answers can't be exactly matched, ask the LLM to select the most consistent response
|
||||
6. **Multi-Chain Reasoning for Evidence Collection** — Use reasoning chains as evidence sources, not just answer votes
|
||||
7. **Meta-Reasoning Over Chains** — A second model pass that reads all chains produces better answers than majority voting
|
||||
8. **Complexity-Weighted Voting** — Vote over complex chains only; simple chains may reflect shortcuts
|
||||
9. **History Accumulation Helps** — Retain previous feedback and outputs in refinement prompts; models learn from past mistakes
|
||||
10. **Open Questions Beat Yes/No** — Verification questions expecting factual answers outperform yes/no format
|
||||
11. **Stopping Conditions Matter** — Use explicit quality thresholds or iteration limits; models rarely self-terminate optimally
|
||||
12. **Non-Monotonic Improvement Possible** — Multi-aspect tasks may improve on one dimension while regressing on another; track best-so-far
|
||||
|
||||
---
|
||||
|
||||
## 1. Iterative Refinement
|
||||
|
||||
Techniques where the model critiques and improves its own output across multiple turns.
|
||||
|
||||
### Self-Refine
|
||||
|
||||
A general-purpose iterative improvement framework. Per Madaan et al. (2023): "SELF-REFINE: an iterative self-refinement algorithm that alternates between two generative steps—FEEDBACK and REFINE. These steps work in tandem to generate high-quality outputs."
|
||||
|
||||
**The core loop:**
|
||||
|
||||
```
|
||||
Turn 1 (Generate):
|
||||
Input: Task description + prompt
|
||||
Output: Initial response y₀
|
||||
|
||||
Turn 2 (Feedback):
|
||||
Input: Task + y₀ + feedback prompt
|
||||
Output: Actionable, specific feedback fb₀
|
||||
|
||||
Turn 3 (Refine):
|
||||
Input: Task + y₀ + fb₀ + refine prompt
|
||||
Output: Improved response y₁
|
||||
|
||||
[Iterate until stopping condition]
|
||||
```
|
||||
|
||||
**Critical quality requirements for feedback:**
|
||||
|
||||
Per the paper: "By 'actionable', we mean the feedback should contain a concrete action that would likely improve the output. By 'specific', we mean the feedback should identify concrete phrases in the output to change."
|
||||
|
||||
**CORRECT feedback (actionable + specific):**
|
||||
|
||||
```
|
||||
This code is slow as it uses a for loop which is brute force.
|
||||
A better approach is to use the formula n(n+1)/2 instead of iterating.
|
||||
```
|
||||
|
||||
**INCORRECT feedback (vague):**
|
||||
|
||||
```
|
||||
The code could be more efficient. Consider optimizing it.
|
||||
```
|
||||
|
||||
**History accumulation improves refinement:**
|
||||
|
||||
The refinement prompt should include all previous iterations. Per the paper: "To inform the model about the previous iterations, we retain the history of previous feedback and outputs by appending them to the prompt. Intuitively, this allows the model to learn from past mistakes and avoid repeating them."
|
||||
|
||||
```
|
||||
Turn N (Refine with history):
|
||||
Input: Task + y₀ + fb₀ + y₁ + fb₁ + ... + yₙ₋₁ + fbₙ₋₁
|
||||
Output: Improved response yₙ
|
||||
```
|
||||
|
||||
**Performance:** "SELF-REFINE outperforms direct generation from strong LLMs like GPT-3.5 and GPT-4 by 5-40% absolute improvement" across dialogue response generation, code optimization, code readability, math reasoning, sentiment reversal, acronym generation, and constrained generation.
|
||||
|
||||
**When Self-Refine works best:**
|
||||
|
||||
| Task Type | Improvement | Notes |
|
||||
| --------------------------- | ----------- | -------------------------------------------- |
|
||||
| Code optimization | +13% | Clear optimization criteria |
|
||||
| Dialogue response | +35-40% | Multi-aspect quality (relevance, engagement) |
|
||||
| Constrained generation | +20% | Verifiable constraint satisfaction |
|
||||
| Math reasoning (with oracle) | +4.8% | Requires correctness signal |
|
||||
|
||||
**Limitation — Non-monotonic improvement:**
|
||||
|
||||
Per the paper: "For tasks with multi-aspect feedback like Acronym Generation, the output quality can fluctuate during the iterative process, improving on one aspect while losing out on another."
|
||||
|
||||
**Mitigation:** Track scores across iterations; select the output with maximum total score, not necessarily the final output.
|
||||
|
||||
---
|
||||
|
||||
### Feedback Prompt Design
|
||||
|
||||
The feedback prompt determines refinement quality. Key elements from Self-Refine experiments:
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
You are given [task description] and an output.
|
||||
|
||||
Output: {previous_output}
|
||||
|
||||
Provide feedback on this output. Your feedback should:
|
||||
1. Identify specific phrases or elements that need improvement
|
||||
2. Explain why they are problematic
|
||||
3. Suggest concrete actions to fix them
|
||||
|
||||
Do not rewrite the output. Only provide feedback.
|
||||
|
||||
Feedback:
|
||||
```
|
||||
|
||||
**Why separation matters:** Combining feedback and rewriting in one turn degrades both. The model either produces shallow feedback to get to rewriting, or rewrites without fully analyzing problems.
|
||||
|
||||
---
|
||||
|
||||
### Refinement Prompt Design
|
||||
|
||||
The refinement prompt applies feedback to produce improved output.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
You are given [task description], a previous output, and feedback on that output.
|
||||
|
||||
Previous output: {previous_output}
|
||||
|
||||
Feedback: {feedback}
|
||||
|
||||
Using this feedback, produce an improved version of the output.
|
||||
Address each point raised in the feedback.
|
||||
|
||||
Improved output:
|
||||
```
|
||||
|
||||
**With history (for iteration 2+):**
|
||||
|
||||
```
|
||||
You are given [task description], your previous attempts, and feedback on each.
|
||||
|
||||
Attempt 1: {y₀}
|
||||
Feedback 1: {fb₀}
|
||||
|
||||
Attempt 2: {y₁}
|
||||
Feedback 2: {fb₁}
|
||||
|
||||
Using all feedback, produce an improved version. Do not repeat previous mistakes.
|
||||
|
||||
Improved output:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Stopping Conditions
|
||||
|
||||
Self-Refine requires explicit stopping conditions. Options:
|
||||
|
||||
1. **Fixed iterations:** Stop after N refinement cycles (typically 2-4)
|
||||
2. **Feedback-based:** Prompt the model to include a stop signal in feedback
|
||||
3. **Score-based:** Stop when quality score exceeds threshold
|
||||
4. **Diminishing returns:** Stop when improvement between iterations falls below threshold
|
||||
|
||||
**Prompt for feedback-based stopping:**
|
||||
|
||||
```
|
||||
Provide feedback on this output. If the output is satisfactory and needs no
|
||||
further improvement, respond with "NO_REFINEMENT_NEEDED" instead of feedback.
|
||||
|
||||
Feedback:
|
||||
```
|
||||
|
||||
**Warning:** Models often fail to self-terminate appropriately. Per Madaan et al.: fixed iteration limits are more reliable than self-assessed stopping.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verification
|
||||
|
||||
Techniques where the model fact-checks its own outputs through targeted questioning.
|
||||
|
||||
### Chain-of-Verification (CoVe)
|
||||
|
||||
A structured approach to reducing hallucination through self-verification. Per Dhuliawala et al. (2023): "Chain-of-Verification (CoVe) whereby the model first (i) drafts an initial response; then (ii) plans verification questions to fact-check its draft; (iii) answers those questions independently so the answers are not biased by other responses; and (iv) generates its final verified response."
|
||||
|
||||
**The four-step process:**
|
||||
|
||||
```
|
||||
Turn 1 (Baseline Response):
|
||||
Input: Original query
|
||||
Output: Initial response (may contain hallucinations)
|
||||
|
||||
Turn 2 (Plan Verifications):
|
||||
Input: Query + baseline response
|
||||
Output: List of verification questions
|
||||
|
||||
Turn 3 (Execute Verifications):
|
||||
Input: Verification questions ONLY (not baseline response)
|
||||
Output: Answers to each verification question
|
||||
|
||||
Turn 4 (Final Verified Response):
|
||||
Input: Query + baseline response + verification Q&A pairs
|
||||
Output: Revised response incorporating verifications
|
||||
```
|
||||
|
||||
**The critical insight — shortform beats longform:**
|
||||
|
||||
Per the paper: "Shortform verification questions are more accurately answered than longform queries. In a longform response, LLMs are prone to generate a number of hallucinations. However, it can often be the case that the LLM itself would know these hallucinations are wrong if queried specifically for that individual fact, independent of the rest of the longform generation."
|
||||
|
||||
**Quantitative evidence:**
|
||||
|
||||
| Setting | Accuracy |
|
||||
| ----------------------------- | -------- |
|
||||
| Facts in longform generation | ~17% |
|
||||
| Same facts as individual Q&A | ~70% |
|
||||
|
||||
The same model that hallucinates facts in context can correctly answer when asked directly. CoVe exploits this asymmetry.
|
||||
|
||||
**Example from the paper:**
|
||||
|
||||
```
|
||||
Query: Name some politicians who were born in NY, New York.
|
||||
|
||||
Baseline Response (with hallucinations):
|
||||
1. Hillary Clinton - former secretary of state... [WRONG: born in Chicago]
|
||||
2. Donald Trump - former president... [CORRECT: born in Queens, NYC]
|
||||
3. Michael Bloomberg - former Mayor... [WRONG: born in Boston]
|
||||
|
||||
Verification Questions:
|
||||
- Where was Hillary Clinton born?
|
||||
- Where was Donald Trump born?
|
||||
- Where was Michael Bloomberg born?
|
||||
|
||||
Verification Answers:
|
||||
- Hillary Clinton was born in Chicago, Illinois
|
||||
- Donald Trump was born in Queens, New York City
|
||||
- Michael Bloomberg was born in Boston, Massachusetts
|
||||
|
||||
Final Verified Response:
|
||||
1. Donald Trump - former president (born in Queens, NYC)
|
||||
2. Alexandria Ocasio-Cortez - Democratic representative (born in NYC)
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Factored vs. Joint Verification
|
||||
|
||||
**The hallucination copying problem:**
|
||||
|
||||
Per Dhuliawala et al.: "Models that attend to existing hallucinations in the context from their own generations tend to repeat the hallucinations."
|
||||
|
||||
When verification questions are answered with the baseline response in context, the model tends to confirm its own hallucinations rather than correct them.
|
||||
|
||||
**Joint verification (less effective):**
|
||||
|
||||
```
|
||||
Turn 3 (Joint):
|
||||
Input: Query + baseline response + verification questions
|
||||
Output: All answers in one pass
|
||||
|
||||
Problem: Model sees its original hallucinations and copies them
|
||||
```
|
||||
|
||||
**Factored verification (more effective):**
|
||||
|
||||
```
|
||||
Turn 3a: Answer Q1 independently (no baseline in context)
|
||||
Turn 3b: Answer Q2 independently (no baseline in context)
|
||||
Turn 3c: Answer Q3 independently (no baseline in context)
|
||||
...
|
||||
```
|
||||
|
||||
**2-Step verification (middle ground):**
|
||||
|
||||
```
|
||||
Turn 3a: Generate all verification answers (no baseline in context)
|
||||
Turn 3b: Cross-check answers against baseline, note inconsistencies
|
||||
```
|
||||
|
||||
**Performance comparison (Wiki-Category task):**
|
||||
|
||||
| Method | Precision |
|
||||
| --------------- | --------- |
|
||||
| Baseline | 0.13 |
|
||||
| Joint CoVe | 0.15 |
|
||||
| 2-Step CoVe | 0.19 |
|
||||
| Factored CoVe | 0.22 |
|
||||
|
||||
Factored verification consistently outperforms joint verification by preventing hallucination propagation.
|
||||
|
||||
---
|
||||
|
||||
### Verification Question Design
|
||||
|
||||
**Open questions outperform yes/no:**
|
||||
|
||||
Per the paper: "We find that yes/no type questions perform worse for the factored version of CoVe. Some anecdotal examples... find the model tends to agree with facts in a yes/no question format whether they are right or wrong."
|
||||
|
||||
**CORRECT (open verification question):**
|
||||
|
||||
```
|
||||
When did Texas secede from Mexico?
|
||||
→ Expected answer: 1836
|
||||
```
|
||||
|
||||
**INCORRECT (yes/no verification question):**
|
||||
|
||||
```
|
||||
Did Texas secede from Mexico in 1845?
|
||||
→ Model tends to agree regardless of correctness
|
||||
```
|
||||
|
||||
**LLM-generated questions outperform heuristics:**
|
||||
|
||||
Per the paper: "We compare the quality of these questions to heuristically constructed ones... Results show a reduced precision with rule-based verification questions."
|
||||
|
||||
Let the model generate verification questions tailored to the specific response, rather than using templated questions.
|
||||
|
||||
---
|
||||
|
||||
### Factor+Revise for Complex Verification
|
||||
|
||||
For longform generation, add an explicit cross-check step between verification and final response.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
Turn 3 (Execute verifications): [as above]
|
||||
|
||||
Turn 3.5 (Cross-check):
|
||||
Input: Baseline response + verification Q&A pairs
|
||||
Output: Explicit list of inconsistencies found
|
||||
|
||||
Turn 4 (Final response):
|
||||
Input: Baseline + verifications + inconsistency list
|
||||
Output: Revised response
|
||||
```
|
||||
|
||||
**Performance:** Factor+Revise achieves FACTSCORE 71.4 vs. 63.7 for factored-only, demonstrating that explicit reasoning about inconsistencies further improves accuracy.
|
||||
|
||||
**Prompt for cross-check:**
|
||||
|
||||
```
|
||||
Original passage: {baseline_excerpt}
|
||||
|
||||
From another source:
|
||||
Q: {verification_question_1}
|
||||
A: {verification_answer_1}
|
||||
|
||||
Q: {verification_question_2}
|
||||
A: {verification_answer_2}
|
||||
|
||||
Identify any inconsistencies between the original passage and the verified facts.
|
||||
List each inconsistency explicitly.
|
||||
|
||||
Inconsistencies:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Aggregation and Consistency
|
||||
|
||||
Techniques that sample multiple responses and select or synthesize the best output.
|
||||
|
||||
### Universal Self-Consistency (USC)
|
||||
|
||||
Extends self-consistency to free-form outputs where exact-match voting is impossible. Per Chen et al. (2023): "USC leverages LLMs themselves to select the most consistent answer among multiple candidates... USC eliminates the need of designing an answer extraction process, and is applicable to tasks with free-form answers."
|
||||
|
||||
**The two-step process:**
|
||||
|
||||
```
|
||||
Turn 1 (Sample):
|
||||
Input: Query
|
||||
Output: N responses sampled with temperature > 0
|
||||
[y₁, y₂, ..., yₙ]
|
||||
|
||||
Turn 2 (Select):
|
||||
Input: Query + all N responses
|
||||
Output: Index of most consistent response
|
||||
```
|
||||
|
||||
**The selection prompt:**
|
||||
|
||||
```
|
||||
I have generated the following responses to the question: {question}
|
||||
|
||||
Response 0: {response_0}
|
||||
Response 1: {response_1}
|
||||
Response 2: {response_2}
|
||||
...
|
||||
|
||||
Select the most consistent response based on majority consensus.
|
||||
The most consistent response is Response:
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
Per the paper: "Although prior works show that LLMs sometimes have trouble evaluating the prediction correctness, empirically we observe that LLMs are generally able to examine the response consistency across multiple tasks."
|
||||
|
||||
Assessing consistency is easier than assessing correctness. The model doesn't need to know the right answer—just which answers agree with each other most.
|
||||
|
||||
**Performance:**
|
||||
|
||||
| Task | Greedy | Random | USC | Standard SC |
|
||||
| ----------------------- | ------ | ------ | ----- | ----------- |
|
||||
| GSM8K | 91.3 | 91.5 | 92.4 | 92.7 |
|
||||
| MATH | 34.2 | 34.3 | 37.6 | 37.5 |
|
||||
| TruthfulQA (free-form) | 62.1 | 62.9 | 67.7 | N/A |
|
||||
| SummScreen (free-form) | 30.6 | 30.2 | 31.7 | N/A |
|
||||
|
||||
USC matches standard SC on structured tasks and enables consistency-based selection where SC cannot apply.
|
||||
|
||||
**Robustness to ordering:**
|
||||
|
||||
Per the paper: "The overall model performance remains similar with different response orders, suggesting the effect of response order is minimal." USC is not significantly affected by the order in which responses are presented.
|
||||
|
||||
**Optimal sample count:**
|
||||
|
||||
USC benefits from more samples up to a point, then plateaus or slightly degrades due to context length limitations. Per experiments: 8 samples is a reliable sweet spot balancing accuracy and cost.
|
||||
|
||||
---
|
||||
|
||||
### Multi-Chain Reasoning (MCR)
|
||||
|
||||
Uses multiple reasoning chains as evidence sources, not just answer votes. Per Yoran et al. (2023): "Unlike prior work, sampled reasoning chains are used not for their predictions (as in SC) but as a means to collect pieces of evidence from multiple chains."
|
||||
|
||||
**The key insight:**
|
||||
|
||||
Self-Consistency discards the reasoning and only votes on answers. MCR preserves the reasoning and synthesizes facts across chains.
|
||||
|
||||
**The three-step process:**
|
||||
|
||||
```
|
||||
Turn 1 (Generate chains):
|
||||
Input: Query
|
||||
Output: N reasoning chains, each with intermediate steps
|
||||
[chain₁, chain₂, ..., chainₙ]
|
||||
|
||||
Turn 2 (Concatenate):
|
||||
Combine all chains into unified multi-chain context
|
||||
|
||||
Turn 3 (Meta-reason):
|
||||
Input: Query + multi-chain context
|
||||
Output: Final answer + explanation synthesizing evidence
|
||||
```
|
||||
|
||||
**Why MCR outperforms SC:**
|
||||
|
||||
Per the paper: "SC solely relies on the chains' answers... By contrast, MCR concatenates the intermediate steps from each chain into a unified context, which is passed, along with the original question, to a meta-reasoner model."
|
||||
|
||||
**Example from the paper:**
|
||||
|
||||
```
|
||||
Question: Did Brad Peyton need to know about seismology?
|
||||
|
||||
Chain 1 (Answer: No):
|
||||
- Brad Peyton is a film director
|
||||
- What is seismology? Seismology is the study of earthquakes
|
||||
- Do film directors need to know about earthquakes? No
|
||||
|
||||
Chain 2 (Answer: Yes):
|
||||
- Brad Peyton directed San Andreas
|
||||
- San Andreas is about a massive earthquake
|
||||
- [implicit: he needed to research the topic]
|
||||
|
||||
Chain 3 (Answer: No):
|
||||
- Brad Peyton is a director, writer, and producer
|
||||
- What do film directors have to know? Many things
|
||||
- Is seismology one of them? No
|
||||
|
||||
Self-Consistency vote: No (2-1)
|
||||
|
||||
MCR meta-reasoning: Combines facts from all chains:
|
||||
- Brad Peyton is a film director (chain 1, 3)
|
||||
- He directed San Andreas (chain 2)
|
||||
- San Andreas is about a massive earthquake (chain 2)
|
||||
- Seismology is the study of earthquakes (chain 1)
|
||||
|
||||
MCR answer: Yes (synthesizes that directing an earthquake film required seismology knowledge)
|
||||
```
|
||||
|
||||
**Performance:**
|
||||
|
||||
MCR outperforms SC by up to 5.7% on multi-hop QA datasets. Additionally: "MCR generates high quality explanations for over 82% of examples, while fewer than 3% are unhelpful."
|
||||
|
||||
---
|
||||
|
||||
### Complexity-Weighted Voting
|
||||
|
||||
An extension to self-consistency that weights votes by reasoning complexity. Per Fu et al. (2023): "We propose complexity-based consistency, where instead of taking a majority vote among all generated chains, we vote over the top K complex chains."
|
||||
|
||||
**The process:**
|
||||
|
||||
```
|
||||
Turn 1 (Sample with CoT):
|
||||
Generate N reasoning chains with answers
|
||||
|
||||
Turn 2 (Rank by complexity):
|
||||
Count reasoning steps in each chain
|
||||
Select top K chains by step count
|
||||
|
||||
Turn 3 (Vote):
|
||||
Majority vote only among the K complex chains
|
||||
```
|
||||
|
||||
**Why complexity matters:**
|
||||
|
||||
Simple chains may reflect shortcuts or lucky guesses. Complex chains demonstrate thorough reasoning. Voting only over complex chains filters out low-effort responses.
|
||||
|
||||
**Performance (GSM8K):**
|
||||
|
||||
| Method | Accuracy |
|
||||
| --------------------------- | -------- |
|
||||
| Standard SC (all chains) | 78.0 |
|
||||
| Complexity-weighted (top K) | 80.5 |
|
||||
|
||||
**Implementation note:** This requires no additional LLM calls beyond standard SC—just post-processing to count steps and filter before voting.
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation Patterns
|
||||
|
||||
### Conversation Structure Template
|
||||
|
||||
A general template for multi-turn improvement:
|
||||
|
||||
```
|
||||
SYSTEM: [Base system prompt with single-turn techniques]
|
||||
|
||||
--- Turn 1: Initial Generation ---
|
||||
USER: [Task]
|
||||
ASSISTANT: [Initial output y₀]
|
||||
|
||||
--- Turn 2: Analysis/Feedback ---
|
||||
USER: [Analysis prompt - critique, verify, or evaluate y₀]
|
||||
ASSISTANT: [Feedback, verification results, or evaluation]
|
||||
|
||||
--- Turn 3: Refinement/Synthesis ---
|
||||
USER: [Refinement prompt incorporating Turn 2 output]
|
||||
ASSISTANT: [Improved output y₁]
|
||||
|
||||
[Repeat Turns 2-3 as needed]
|
||||
|
||||
--- Final Turn: Format/Extract ---
|
||||
USER: [Optional: extract final answer in required format]
|
||||
ASSISTANT: [Final formatted output]
|
||||
```
|
||||
|
||||
### Context Management
|
||||
|
||||
Multi-turn prompting accumulates context. Manage token limits by:
|
||||
|
||||
1. **Summarize history:** After N iterations, summarize previous attempts rather than including full text
|
||||
2. **Keep recent + best:** Retain only the most recent iteration and the best-scoring previous output
|
||||
3. **Structured extraction:** Extract key points from feedback rather than full feedback text
|
||||
|
||||
**Example (summarized history):**
|
||||
|
||||
```
|
||||
Previous attempts summary:
|
||||
- Attempt 1: Failed due to [specific issue]
|
||||
- Attempt 2: Improved [aspect] but [remaining issue]
|
||||
- Attempt 3: Best so far, minor issue with [aspect]
|
||||
|
||||
Latest attempt: [full text of y₃]
|
||||
|
||||
Feedback on latest attempt:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Anti-Patterns
|
||||
|
||||
### The Mixed-Goal Turn
|
||||
|
||||
**Anti-pattern:** Combining distinct cognitive operations in a single turn.
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
Generate a response, then critique it, then improve it.
|
||||
```
|
||||
|
||||
Each operation deserves focused attention. The model may rush through critique to reach improvement, or improve without thorough analysis.
|
||||
|
||||
```
|
||||
# BETTER
|
||||
Turn 1: Generate response
|
||||
Turn 2: Critique the response (output: feedback only)
|
||||
Turn 3: Improve based on feedback
|
||||
```
|
||||
|
||||
### The Contaminated Context
|
||||
|
||||
**Anti-pattern:** Including the original response when answering verification questions.
|
||||
|
||||
Per Dhuliawala et al. (2023): "Models that attend to existing hallucinations in the context from their own generations tend to repeat the hallucinations."
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
Original response: [contains potential hallucinations]
|
||||
Verification question: Where was Hillary Clinton born?
|
||||
Answer:
|
||||
```
|
||||
|
||||
The model will often confirm the hallucination from its original response.
|
||||
|
||||
```
|
||||
# BETTER
|
||||
Verification question: Where was Hillary Clinton born?
|
||||
Answer:
|
||||
[Original response NOT in context]
|
||||
```
|
||||
|
||||
Exclude the baseline response when executing verifications. Include it only in the final revision step.
|
||||
|
||||
### The Yes/No Verification Trap
|
||||
|
||||
**Anti-pattern:** Phrasing verification questions as yes/no confirmations.
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
Is it true that Michael Bloomberg was born in New York?
|
||||
```
|
||||
|
||||
Per CoVe research: Models tend to agree with yes/no questions regardless of correctness.
|
||||
|
||||
```
|
||||
# BETTER
|
||||
Where was Michael Bloomberg born?
|
||||
```
|
||||
|
||||
Open questions expecting factual answers perform significantly better.
|
||||
|
||||
### The Infinite Loop
|
||||
|
||||
**Anti-pattern:** No explicit stopping condition for iterative refinement.
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
Keep improving until the output is perfect.
|
||||
```
|
||||
|
||||
Models rarely self-terminate appropriately. "Perfect" is undefined.
|
||||
|
||||
```
|
||||
# BETTER
|
||||
Improve for exactly 3 iterations, then output the best version.
|
||||
|
||||
# OR
|
||||
Improve until the quality score exceeds 8/10, maximum 5 iterations.
|
||||
```
|
||||
|
||||
Always include explicit stopping criteria: iteration limits, quality thresholds, or both.
|
||||
|
||||
### The Forgotten History
|
||||
|
||||
**Anti-pattern:** Discarding previous iterations in refinement.
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
Turn 3: Here is feedback. Improve the output.
|
||||
[No reference to previous attempts]
|
||||
```
|
||||
|
||||
Per Madaan et al.: "Retaining the history of previous feedback and outputs... allows the model to learn from past mistakes and avoid repeating them."
|
||||
|
||||
```
|
||||
# BETTER
|
||||
Turn 3:
|
||||
Previous attempts and feedback:
|
||||
- Attempt 1: [y₀] → Feedback: [fb₀]
|
||||
- Attempt 2: [y₁] → Feedback: [fb₁]
|
||||
|
||||
Improve, avoiding previously identified issues:
|
||||
```
|
||||
|
||||
### The Vague Feedback
|
||||
|
||||
**Anti-pattern:** Feedback without actionable specifics.
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
The response could be improved. Some parts are unclear.
|
||||
```
|
||||
|
||||
This feedback provides no guidance for refinement.
|
||||
|
||||
```
|
||||
# BETTER
|
||||
The explanation of photosynthesis in paragraph 2 uses jargon ("electron
|
||||
transport chain") without definition. Add a brief explanation: "the process
|
||||
by which plants convert light energy into chemical energy through a series
|
||||
of protein complexes."
|
||||
```
|
||||
|
||||
Feedback must identify specific elements AND suggest concrete improvements.
|
||||
|
||||
### The Majority Fallacy
|
||||
|
||||
**Anti-pattern:** Assuming majority vote is always correct.
|
||||
|
||||
```
|
||||
# PROBLEMATIC
|
||||
3 out of 5 chains say the answer is X, so X is correct.
|
||||
```
|
||||
|
||||
Per Fu et al.: Simple chains may reflect shortcuts. Per Yoran et al.: Intermediate reasoning contains useful information discarded by voting.
|
||||
|
||||
```
|
||||
# BETTER
|
||||
Weight votes by reasoning complexity, or use MCR to synthesize
|
||||
evidence from all chains including minority answers.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Technique Combinations
|
||||
|
||||
Multi-turn techniques can be combined for compounding benefits.
|
||||
|
||||
### Self-Refine + CoVe
|
||||
|
||||
Apply verification after refinement to catch introduced errors:
|
||||
|
||||
```
|
||||
Turn 1: Generate initial output
|
||||
Turn 2: Feedback
|
||||
Turn 3: Refine
|
||||
Turn 4: Plan verification questions for refined output
|
||||
Turn 5: Execute verifications (factored)
|
||||
Turn 6: Final verified output
|
||||
```
|
||||
|
||||
### USC + Complexity Weighting
|
||||
|
||||
Filter by complexity before consistency selection:
|
||||
|
||||
```
|
||||
Turn 1: Sample N responses with reasoning
|
||||
Turn 2: Filter to top K by reasoning complexity
|
||||
Turn 3: Apply USC to select most consistent among K
|
||||
```
|
||||
|
||||
### MCR + Self-Refine
|
||||
|
||||
Use multi-chain evidence collection, then refine the synthesis:
|
||||
|
||||
```
|
||||
Turn 1: Generate N reasoning chains
|
||||
Turn 2: Meta-reason to synthesize evidence and produce answer
|
||||
Turn 3: Feedback on synthesis
|
||||
Turn 4: Refine synthesis
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Research Citations
|
||||
|
||||
- Chen, X., Aksitov, R., Alon, U., et al. (2023). "Universal Self-Consistency for Large Language Model Generation." arXiv.
|
||||
- Dhuliawala, S., Komeili, M., Xu, J., et al. (2023). "Chain-of-Verification Reduces Hallucination in Large Language Models." arXiv.
|
||||
- Diao, S., Wang, P., Lin, Y., & Zhang, T. (2023). "Active Prompting with Chain-of-Thought for Large Language Models." arXiv.
|
||||
- Fu, Y., Peng, H., Sabharwal, A., Clark, P., & Khot, T. (2023). "Complexity-Based Prompting for Multi-Step Reasoning." arXiv.
|
||||
- Madaan, A., Tandon, N., Gupta, P., et al. (2023). "Self-Refine: Iterative Refinement with Self-Feedback." arXiv.
|
||||
- Wang, X., Wei, J., Schuurmans, D., et al. (2023). "Self-Consistency Improves Chain of Thought Reasoning in Language Models." ICLR.
|
||||
- Yao, S., Yu, D., Zhao, J., et al. (2023). "Tree of Thoughts: Deliberate Problem Solving with Large Language Models." NeurIPS.
|
||||
- Yoran, O., Wolfson, T., Bogin, B., et al. (2023). "Answering Questions by Meta-Reasoning over Multiple Chains of Thought." arXiv.
|
||||
- Zhang, Y., Yuan, Y., & Yao, A. (2024). "Meta Prompting for AI Systems." arXiv.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Prompt Engineer Skill - Multi-turn prompt optimization workflow.
|
||||
|
||||
Guides prompt optimization through nine phases:
|
||||
1. Triage - Assess complexity, route to lightweight or full process
|
||||
2. Understand - Blind problem identification (NO references yet)
|
||||
3. Plan - Consult references, match techniques, generate visual cards
|
||||
4. Verify - Factored verification of FACTS (open questions, cross-check)
|
||||
5. Feedback - Generate actionable critique from verification results
|
||||
6. Refine - Apply feedback to update the plan
|
||||
7. Approval - Present refined plan to human, HARD GATE
|
||||
8. Execute - Apply approved changes to prompt
|
||||
9. Integrate - Coherence check, anti-pattern audit, quality verification
|
||||
|
||||
Research grounding:
|
||||
- Self-Refine (Madaan 2023): Separate feedback from refinement for 5-40%
|
||||
improvement. Feedback must be "actionable and specific."
|
||||
- CoVe (Dhuliawala 2023): Factored verification improves accuracy 17%->70%.
|
||||
Use OPEN questions, not yes/no ("model tends to agree whether right or wrong")
|
||||
- Factor+Revise: Explicit cross-check achieves +7.7 FACTSCORE points over
|
||||
factored verification alone.
|
||||
- Separation of Concerns: "Each turn has a distinct cognitive goal. Mixing
|
||||
these goals within a single turn reduces effectiveness."
|
||||
|
||||
Usage:
|
||||
python3 optimize.py --step 1 --total-steps 9 --thoughts "Prompt: agents/developer.md"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def get_step_1_guidance():
|
||||
"""Step 1: Triage - Assess complexity and route appropriately."""
|
||||
return {
|
||||
"title": "Triage",
|
||||
"actions": [
|
||||
"Assess the prompt complexity:",
|
||||
"",
|
||||
"SIMPLE prompts (use lightweight 3-step process):",
|
||||
" - Under 20 lines",
|
||||
" - Single clear purpose (one tool, one behavior)",
|
||||
" - No conditional logic or branching",
|
||||
" - No inter-section dependencies",
|
||||
"",
|
||||
"COMPLEX prompts (use full 9-step process):",
|
||||
" - Multiple sections serving different functions",
|
||||
" - Conditional behaviors or rule hierarchies",
|
||||
" - Tool orchestration or multi-step workflows",
|
||||
" - Known failure modes that need addressing",
|
||||
"",
|
||||
"If SIMPLE: Note 'LIGHTWEIGHT' and proceed with abbreviated analysis",
|
||||
"If COMPLEX: Note 'FULL PROCESS' and proceed to step 2",
|
||||
"",
|
||||
"Read the prompt file now. Do NOT read references yet.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"PROMPT_PATH: path to the prompt being optimized",
|
||||
"COMPLEXITY: SIMPLE or COMPLEX",
|
||||
"PROMPT_SUMMARY: 2-3 sentences describing purpose",
|
||||
"PROMPT_LENGTH: approximate line count",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_2_guidance():
|
||||
"""Step 2: Understand - Blind problem identification."""
|
||||
return {
|
||||
"title": "Understand (Blind)",
|
||||
"actions": [
|
||||
"CRITICAL: Do NOT read the reference documents yet.",
|
||||
"This step uses BLIND problem identification to prevent pattern-shopping.",
|
||||
"",
|
||||
"Document the prompt's OPERATING CONTEXT:",
|
||||
" - Interaction model: single-shot or conversational?",
|
||||
" - Agent type: tool-use, coding, analysis, or general?",
|
||||
" - Token constraints: brevity critical or thoroughness preferred?",
|
||||
" - Failure modes: what goes wrong when this prompt fails?",
|
||||
"",
|
||||
"Identify PROBLEMS by examining the prompt text directly:",
|
||||
" - Quote specific problematic text with line numbers",
|
||||
" - Describe what's wrong in concrete terms",
|
||||
" - Note observable symptoms (not guessed causes)",
|
||||
"",
|
||||
"Examples of observable problems:",
|
||||
" 'Lines 12-15 use hedging language: \"might want to\", \"could try\"'",
|
||||
" 'No examples provided for expected output format'",
|
||||
" 'Multiple rules marked CRITICAL with no clear precedence'",
|
||||
" 'Instructions say what NOT to do but not what TO do'",
|
||||
"",
|
||||
"List at least 3 specific problems with quoted evidence.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"OPERATING_CONTEXT: interaction model, agent type, constraints",
|
||||
"PROBLEMS: list of specific issues with QUOTED text from prompt",
|
||||
"Each problem must have: line reference, quoted text, description",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_3_guidance():
|
||||
"""Step 3: Plan - Consult references, match techniques."""
|
||||
return {
|
||||
"title": "Plan",
|
||||
"actions": [
|
||||
"NOW read the reference documents:",
|
||||
" - references/prompt-engineering-single-turn.md (always)",
|
||||
" - references/prompt-engineering-multi-turn.md (if multi-turn prompt)",
|
||||
"",
|
||||
"For EACH problem identified in Step 2:",
|
||||
"",
|
||||
"1. Locate a matching technique in the reference",
|
||||
"2. QUOTE the trigger condition from the Technique Selection Guide",
|
||||
"3. QUOTE the expected effect",
|
||||
"4. Note stacking compatibility and conflicts",
|
||||
"5. Draft the BEFORE/AFTER transformation",
|
||||
"",
|
||||
"Format each proposed change as a visual card:",
|
||||
"",
|
||||
" CHANGE N: [title]",
|
||||
" PROBLEM: [quoted text from prompt]",
|
||||
" TECHNIQUE: [name]",
|
||||
" TRIGGER: \"[quoted from reference]\"",
|
||||
" EFFECT: \"[quoted from reference]\"",
|
||||
" BEFORE: [original prompt text]",
|
||||
" AFTER: [modified prompt text]",
|
||||
"",
|
||||
"If you cannot quote a trigger condition that matches, do NOT apply.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"PROBLEMS: (from step 2)",
|
||||
"PROPOSED_CHANGES: list of visual cards, each with:",
|
||||
" - Problem quoted from prompt",
|
||||
" - Technique name",
|
||||
" - Trigger condition QUOTED from reference",
|
||||
" - Effect QUOTED from reference",
|
||||
" - BEFORE/AFTER text",
|
||||
"STACKING_NOTES: compatibility between proposed techniques",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_4_guidance():
|
||||
"""Step 4: Verify - Factored verification of facts."""
|
||||
return {
|
||||
"title": "Verify (Factored)",
|
||||
"actions": [
|
||||
"FACTORED VERIFICATION: Answer questions WITHOUT seeing your proposals.",
|
||||
"",
|
||||
"For EACH proposed technique, generate OPEN verification questions:",
|
||||
"",
|
||||
" WRONG (yes/no): 'Is Affirmative Directives applicable here?'",
|
||||
" RIGHT (open): 'What is the trigger condition for Affirmative Directives?'",
|
||||
"",
|
||||
" WRONG (yes/no): 'Does the prompt have hedging language?'",
|
||||
" RIGHT (open): 'What hedging phrases appear in lines 10-20?'",
|
||||
"",
|
||||
"Answer each question INDEPENDENTLY:",
|
||||
" - Pretend you have NOT seen your proposals",
|
||||
" - Answer from the reference or prompt text directly",
|
||||
" - Do NOT defend your choices; seek truth",
|
||||
"",
|
||||
"Then CROSS-CHECK: Compare answers to your claims:",
|
||||
"",
|
||||
" TECHNIQUE: [name]",
|
||||
" CLAIMED TRIGGER: \"[what you quoted in step 3]\"",
|
||||
" VERIFIED TRIGGER: \"[what the reference actually says]\"",
|
||||
" MATCH: CONSISTENT / INCONSISTENT / PARTIAL",
|
||||
"",
|
||||
" CLAIMED PROBLEM: \"[quoted prompt text in step 3]\"",
|
||||
" VERIFIED TEXT: \"[what the prompt actually says at that line]\"",
|
||||
" MATCH: CONSISTENT / INCONSISTENT / PARTIAL",
|
||||
],
|
||||
"state_requirements": [
|
||||
"VERIFICATION_QS: open questions for each technique",
|
||||
"VERIFICATION_ANSWERS: factored answers (without seeing proposals)",
|
||||
"CROSS_CHECK: for each technique:",
|
||||
" - Claimed vs verified trigger condition",
|
||||
" - Claimed vs verified prompt text",
|
||||
" - Match status: CONSISTENT / INCONSISTENT / PARTIAL",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_5_guidance():
|
||||
"""Step 5: Feedback - Generate actionable critique."""
|
||||
return {
|
||||
"title": "Feedback",
|
||||
"actions": [
|
||||
"Generate FEEDBACK based on verification results.",
|
||||
"",
|
||||
"Self-Refine research requires feedback to be:",
|
||||
" - ACTIONABLE: contains concrete action to improve",
|
||||
" - SPECIFIC: identifies concrete phrases to change",
|
||||
"",
|
||||
"WRONG (vague): 'The technique selection could be improved.'",
|
||||
"RIGHT (actionable): 'Change 3 claims Affirmative Directives but the",
|
||||
" prompt text at line 15 is already affirmative. Remove this change.'",
|
||||
"",
|
||||
"For each INCONSISTENT or PARTIAL match from Step 4:",
|
||||
"",
|
||||
" ISSUE: [specific problem from cross-check]",
|
||||
" ACTION: [concrete fix]",
|
||||
" - Replace technique with [alternative]",
|
||||
" - Modify BEFORE/AFTER to [specific change]",
|
||||
" - Remove change entirely because [reason]",
|
||||
"",
|
||||
"For CONSISTENT matches: Note 'VERIFIED - no changes needed'",
|
||||
"",
|
||||
"Do NOT apply feedback yet. Only generate critique.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"CROSS_CHECK: (from step 4)",
|
||||
"FEEDBACK: for each proposed change:",
|
||||
" - STATUS: VERIFIED / NEEDS_REVISION / REMOVE",
|
||||
" - If NEEDS_REVISION: specific actionable fix",
|
||||
" - If REMOVE: reason for removal",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_6_guidance():
|
||||
"""Step 6: Refine - Apply feedback to update plan."""
|
||||
return {
|
||||
"title": "Refine",
|
||||
"actions": [
|
||||
"Apply the feedback from Step 5 to update your proposed changes.",
|
||||
"",
|
||||
"For each change marked VERIFIED: Keep unchanged",
|
||||
"",
|
||||
"For each change marked NEEDS_REVISION:",
|
||||
" - Apply the specific fix from feedback",
|
||||
" - Update the BEFORE/AFTER text",
|
||||
" - Verify the trigger condition still matches",
|
||||
"",
|
||||
"For each change marked REMOVE: Delete from proposal",
|
||||
"",
|
||||
"After applying all feedback, verify:",
|
||||
" - No stacking conflicts between remaining techniques",
|
||||
" - All BEFORE/AFTER transformations are consistent",
|
||||
" - No duplicate or overlapping changes",
|
||||
"",
|
||||
"Produce the REFINED PLAN ready for human approval.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"REFINED_CHANGES: updated list of visual cards",
|
||||
"CHANGES_MADE: what was revised or removed and why",
|
||||
"FINAL_STACKING_CHECK: confirm no conflicts",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_7_guidance():
|
||||
"""Step 7: Approval - Present to human, hard gate."""
|
||||
return {
|
||||
"title": "Approval Gate",
|
||||
"actions": [
|
||||
"Present the REFINED PLAN to the user for approval.",
|
||||
"",
|
||||
"Format:",
|
||||
"",
|
||||
" ## Proposed Changes",
|
||||
"",
|
||||
" [Visual cards for each change]",
|
||||
"",
|
||||
" ## Verification Summary",
|
||||
" - [N] changes verified against reference",
|
||||
" - [M] changes revised based on verification",
|
||||
" - [K] changes removed (did not match trigger conditions)",
|
||||
"",
|
||||
" ## Compatibility",
|
||||
" - [Note stacking synergies]",
|
||||
" - [Note any resolved conflicts]",
|
||||
"",
|
||||
" ## Anti-Patterns Checked",
|
||||
" - Hedging Spiral: [checked/found/none]",
|
||||
" - Everything-Is-Critical: [checked/found/none]",
|
||||
" - Negative Instruction Trap: [checked/found/none]",
|
||||
"",
|
||||
" ---",
|
||||
" Does this plan look reasonable? Confirm to proceed with execution.",
|
||||
"",
|
||||
"HARD GATE: Do NOT proceed to Step 8 without explicit user approval.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"REFINED_CHANGES: (from step 6)",
|
||||
"APPROVAL_PRESENTATION: formatted summary for user",
|
||||
"USER_APPROVAL: must be obtained before step 8",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_8_guidance():
|
||||
"""Step 8: Execute - Apply approved changes."""
|
||||
return {
|
||||
"title": "Execute",
|
||||
"actions": [
|
||||
"Apply the approved changes to the prompt.",
|
||||
"",
|
||||
"Work through changes in logical order (by prompt section).",
|
||||
"",
|
||||
"For each approved change:",
|
||||
" 1. Locate the target text in the prompt",
|
||||
" 2. Apply the BEFORE -> AFTER transformation",
|
||||
" 3. Verify the modification matches what was approved",
|
||||
"",
|
||||
"No additional approval needed per change - plan was approved in Step 7.",
|
||||
"",
|
||||
"If a conflict is discovered during execution:",
|
||||
" - STOP and present the conflict to user",
|
||||
" - Wait for resolution before continuing",
|
||||
"",
|
||||
"After all changes applied, proceed to integration.",
|
||||
],
|
||||
"state_requirements": [
|
||||
"APPROVED_CHANGES: (from step 7)",
|
||||
"APPLIED_CHANGES: list of what was modified",
|
||||
"EXECUTION_NOTES: any issues encountered",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_step_9_guidance():
|
||||
"""Step 9: Integrate - Coherence and quality verification."""
|
||||
return {
|
||||
"title": "Integrate",
|
||||
"actions": [
|
||||
"Verify the optimized prompt holistically.",
|
||||
"",
|
||||
"COHERENCE CHECKS:",
|
||||
" - Cross-section references: do sections reference each other correctly?",
|
||||
" - Terminology consistency: same terms throughout?",
|
||||
" - Priority consistency: do multiple sections align on priorities?",
|
||||
" - Flow and ordering: logical progression?",
|
||||
"",
|
||||
"EMPHASIS AUDIT:",
|
||||
" - Count CRITICAL, IMPORTANT, NEVER, ALWAYS markers",
|
||||
" - If more than 2-3 highest-level markers, reconsider",
|
||||
"",
|
||||
"ANTI-PATTERN FINAL CHECK:",
|
||||
" - Hedging Spiral: accumulated uncertainty language?",
|
||||
" - Everything-Is-Critical: overuse of emphasis?",
|
||||
" - Negative Instruction Trap: 'don't' instead of 'do'?",
|
||||
" - Implicit Category Trap: examples without principles?",
|
||||
"",
|
||||
"QUALITY VERIFICATION (open questions):",
|
||||
" - 'What behavior will this produce in edge cases?'",
|
||||
" - 'How would an agent interpret this if skimming?'",
|
||||
" - 'What could go wrong with this phrasing?'",
|
||||
"",
|
||||
"Present the final optimized prompt with summary of changes.",
|
||||
],
|
||||
"state_requirements": [], # Final step
|
||||
}
|
||||
|
||||
|
||||
def get_guidance(step: int, total_steps: int):
|
||||
"""Dispatch to appropriate guidance based on step number."""
|
||||
guidance_map = {
|
||||
1: get_step_1_guidance,
|
||||
2: get_step_2_guidance,
|
||||
3: get_step_3_guidance,
|
||||
4: get_step_4_guidance,
|
||||
5: get_step_5_guidance,
|
||||
6: get_step_6_guidance,
|
||||
7: get_step_7_guidance,
|
||||
8: get_step_8_guidance,
|
||||
9: get_step_9_guidance,
|
||||
}
|
||||
|
||||
if step in guidance_map:
|
||||
return guidance_map[step]()
|
||||
|
||||
# Extra steps beyond 9 continue integration/verification
|
||||
return get_step_9_guidance()
|
||||
|
||||
|
||||
def format_output(step: int, total_steps: int, thoughts: str) -> str:
|
||||
"""Format output for display."""
|
||||
guidance = get_guidance(step, total_steps)
|
||||
is_complete = step >= total_steps
|
||||
|
||||
lines = [
|
||||
"=" * 70,
|
||||
f"PROMPT ENGINEER - Step {step}/{total_steps}: {guidance['title']}",
|
||||
"=" * 70,
|
||||
"",
|
||||
"ACCUMULATED STATE:",
|
||||
thoughts[:1200] + "..." if len(thoughts) > 1200 else thoughts,
|
||||
"",
|
||||
"ACTIONS:",
|
||||
]
|
||||
lines.extend(f" {action}" for action in guidance["actions"])
|
||||
|
||||
state_reqs = guidance.get("state_requirements", [])
|
||||
if not is_complete and state_reqs:
|
||||
lines.append("")
|
||||
lines.append("NEXT STEP STATE MUST INCLUDE:")
|
||||
lines.extend(f" - {item}" for item in state_reqs)
|
||||
|
||||
lines.append("")
|
||||
|
||||
if is_complete:
|
||||
lines.extend([
|
||||
"COMPLETE - Present to user:",
|
||||
" 1. Summary of optimization process",
|
||||
" 2. Techniques applied with reference sections",
|
||||
" 3. Quality improvements (top 3)",
|
||||
" 4. What was preserved from original",
|
||||
" 5. Final optimized prompt",
|
||||
])
|
||||
else:
|
||||
next_guidance = get_guidance(step + 1, total_steps)
|
||||
lines.extend([
|
||||
f"NEXT: Step {step + 1} - {next_guidance['title']}",
|
||||
f"REMAINING: {total_steps - step} step(s)",
|
||||
"",
|
||||
"ADJUST: increase --total-steps if more verification needed (min 9)",
|
||||
])
|
||||
|
||||
lines.extend(["", "=" * 70])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prompt Engineer - Multi-turn optimization workflow",
|
||||
epilog=(
|
||||
"Phases: triage (1) -> understand (2) -> plan (3) -> "
|
||||
"verify (4) -> feedback (5) -> refine (6) -> "
|
||||
"approval (7) -> execute (8) -> integrate (9)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--step", type=int, required=True)
|
||||
parser.add_argument("--total-steps", type=int, required=True)
|
||||
parser.add_argument("--thoughts", type=str, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.step < 1:
|
||||
sys.exit("ERROR: --step must be >= 1")
|
||||
if args.total_steps < 9:
|
||||
sys.exit("ERROR: --total-steps must be >= 9 (requires 9 phases)")
|
||||
if args.step > args.total_steps:
|
||||
sys.exit("ERROR: --step cannot exceed --total-steps")
|
||||
|
||||
print(format_output(args.step, args.total_steps, args.thoughts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"testModules": [
|
||||
{
|
||||
"moduleId": "/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/src/features/dashboard/components/__tests__/ActionBar.test.tsx",
|
||||
"tests": [
|
||||
{
|
||||
"name": "Module failed to load (Error)",
|
||||
"fullName": "Module failed to load (Error)",
|
||||
"state": "failed",
|
||||
"errors": [
|
||||
{
|
||||
"message": "File not found: tsconfig.json (resolved as: /Users/egullickson/Documents/Technology/coding/motovaultpro/tsconfig.json)",
|
||||
"name": "Error",
|
||||
"stack": "Error: File not found: tsconfig.json (resolved as: /Users/egullickson/Documents/Technology/coding/motovaultpro/tsconfig.json)\n at ConfigSet.resolvePath (/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/node_modules/ts-jest/dist/legacy/config/config-set.js:616:19)\n at ConfigSet._setupConfigSet (/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/node_modules/ts-jest/dist/legacy/config/config-set.js:322:71)\n at new ConfigSet (/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/node_modules/ts-jest/dist/legacy/config/config-set.js:206:14)\n at TsJestTransformer._createConfigSet (/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/node_modules/ts-jest/dist/legacy/ts-jest-transformer.js:119:16)\n at TsJestTransformer._configsFor (/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/node_modules/ts-jest/dist/legacy/ts-jest-transformer.js:98:34)\n at TsJestTransformer.getCacheKey (/Users/egullickson/Documents/Technology/coding/motovaultpro/frontend/node_modules/ts-jest/dist/legacy/ts-jest-transformer.js:249:30)\n at ScriptTransformer._getCacheKey (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/@jest/transform/build/index.js:195:41)\n at ScriptTransformer._getFileCachePath (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/@jest/transform/build/index.js:231:27)\n at ScriptTransformer.transformSource (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/@jest/transform/build/index.js:402:32)\n at ScriptTransformer._transformAndBuildScript (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/@jest/transform/build/index.js:519:40)\n at ScriptTransformer.transform (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/@jest/transform/build/index.js:558:19)\n at Runtime.transformFile (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-runtime/build/index.js:1290:53)\n at Runtime._execModule (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-runtime/build/index.js:1243:34)\n at Runtime._loadModule (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-runtime/build/index.js:944:12)\n at Runtime.requireModule (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-runtime/build/index.js:832:12)\n at jestAdapter (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-circus/build/runner.js:84:33)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at runTestInternal (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-runner/build/index.js:275:16)\n at runTest (/Users/egullickson/Documents/Technology/coding/motovaultpro/node_modules/jest-runner/build/index.js:343:7)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"unhandledErrors": [],
|
||||
"reason": "failed"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# MotoVaultPro Environment Configuration
|
||||
# Copy to .env and fill in environment-specific values
|
||||
# Generated .env files should NOT be committed to version control
|
||||
#
|
||||
# Local dev: No .env needed -- base docker-compose.yml defaults are sandbox values
|
||||
# Staging/Production: CI/CD generates .env from Gitea variables + generate-log-config.sh
|
||||
|
||||
# ===========================================
|
||||
# Stripe Price IDs (environment-specific)
|
||||
# ===========================================
|
||||
# Sandbox defaults used for local development
|
||||
STRIPE_PRO_MONTHLY_PRICE_ID=price_1T1ZHMJXoKkh5RcKwKSSGIlR
|
||||
STRIPE_PRO_YEARLY_PRICE_ID=price_1T1ZHnJXoKkh5RcKWlG2MPpX
|
||||
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_1T1ZIBJXoKkh5RcKu2jyhqBN
|
||||
STRIPE_ENTERPRISE_YEARLY_PRICE_ID=price_1T1ZIQJXoKkh5RcK34YXiJQm
|
||||
|
||||
# ===========================================
|
||||
# Stripe Publishable Key (baked into frontend at build time)
|
||||
# ===========================================
|
||||
# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||||
|
||||
# ===========================================
|
||||
# Log Levels (generated by scripts/ci/generate-log-config.sh)
|
||||
# ===========================================
|
||||
# Run: ./scripts/ci/generate-log-config.sh DEBUG >> .env
|
||||
#
|
||||
# BACKEND_LOG_LEVEL=debug
|
||||
# TRAEFIK_LOG_LEVEL=DEBUG
|
||||
# POSTGRES_LOG_STATEMENT=all
|
||||
# POSTGRES_LOG_MIN_DURATION=0
|
||||
# REDIS_LOGLEVEL=debug
|
||||
|
||||
# ===========================================
|
||||
# Grafana
|
||||
# ===========================================
|
||||
# GRAFANA_ADMIN_PASSWORD=admin
|
||||
@@ -1,14 +0,0 @@
|
||||
# .gitea/
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
| `PULL_REQUEST_TEMPLATE.md` | PR template | Creating pull requests |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `workflows/` | CI/CD workflow definitions | Pipeline configuration |
|
||||
| `ISSUE_TEMPLATE/` | Issue templates (bug, feature, chore) | Creating issues |
|
||||
@@ -0,0 +1,36 @@
|
||||
# SPRINTS.md — MotoVaultPro Sprint Calendar (2026)
|
||||
|
||||
**Cadence:** 2 weeks (14 days)
|
||||
**Sprint weeks:** Monday → Sunday
|
||||
**Naming convention:** `Sprint YYYY-MM-DD` (the Monday start date)
|
||||
|
||||
> Note: Sprint 26 ends on **2027-01-03** (it crosses into the next year).
|
||||
|
||||
| # | Sprint | Start (Mon) | End (Sun) |
|
||||
|---:|---|---|---|
|
||||
| 1 | Sprint 2026-01-05 | 2026-01-05 | 2026-01-18 |
|
||||
| 2 | Sprint 2026-01-19 | 2026-01-19 | 2026-02-01 |
|
||||
| 3 | Sprint 2026-02-02 | 2026-02-02 | 2026-02-15 |
|
||||
| 4 | Sprint 2026-02-16 | 2026-02-16 | 2026-03-01 |
|
||||
| 5 | Sprint 2026-03-02 | 2026-03-02 | 2026-03-15 |
|
||||
| 6 | Sprint 2026-03-16 | 2026-03-16 | 2026-03-29 |
|
||||
| 7 | Sprint 2026-03-30 | 2026-03-30 | 2026-04-12 |
|
||||
| 8 | Sprint 2026-04-13 | 2026-04-13 | 2026-04-26 |
|
||||
| 9 | Sprint 2026-04-27 | 2026-04-27 | 2026-05-10 |
|
||||
| 10 | Sprint 2026-05-11 | 2026-05-11 | 2026-05-24 |
|
||||
| 11 | Sprint 2026-05-25 | 2026-05-25 | 2026-06-07 |
|
||||
| 12 | Sprint 2026-06-08 | 2026-06-08 | 2026-06-21 |
|
||||
| 13 | Sprint 2026-06-22 | 2026-06-22 | 2026-07-05 |
|
||||
| 14 | Sprint 2026-07-06 | 2026-07-06 | 2026-07-19 |
|
||||
| 15 | Sprint 2026-07-20 | 2026-07-20 | 2026-08-02 |
|
||||
| 16 | Sprint 2026-08-03 | 2026-08-03 | 2026-08-16 |
|
||||
| 17 | Sprint 2026-08-17 | 2026-08-17 | 2026-08-30 |
|
||||
| 18 | Sprint 2026-08-31 | 2026-08-31 | 2026-09-13 |
|
||||
| 19 | Sprint 2026-09-14 | 2026-09-14 | 2026-09-27 |
|
||||
| 20 | Sprint 2026-09-28 | 2026-09-28 | 2026-10-11 |
|
||||
| 21 | Sprint 2026-10-12 | 2026-10-12 | 2026-10-25 |
|
||||
| 22 | Sprint 2026-10-26 | 2026-10-26 | 2026-11-08 |
|
||||
| 23 | Sprint 2026-11-09 | 2026-11-09 | 2026-11-22 |
|
||||
| 24 | Sprint 2026-11-23 | 2026-11-23 | 2026-12-06 |
|
||||
| 25 | Sprint 2026-12-07 | 2026-12-07 | 2026-12-20 |
|
||||
| 26 | Sprint 2026-12-21 | 2026-12-21 | 2027-01-03 |
|
||||
@@ -19,11 +19,9 @@ on:
|
||||
env:
|
||||
REGISTRY: git.motovaultpro.com
|
||||
DEPLOY_PATH: /opt/motovaultpro
|
||||
BASE_COMPOSE_FILE: docker-compose.yml
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
COMPOSE_BLUE_GREEN: docker-compose.blue-green.yml
|
||||
COMPOSE_PROD: docker-compose.prod.yml
|
||||
HEALTH_CHECK_TIMEOUT: "240"
|
||||
LOG_LEVEL: INFO
|
||||
HEALTH_CHECK_TIMEOUT: "60"
|
||||
|
||||
jobs:
|
||||
# ============================================
|
||||
@@ -36,7 +34,6 @@ jobs:
|
||||
target_stack: ${{ steps.determine-stack.outputs.target_stack }}
|
||||
backend_image: ${{ steps.set-images.outputs.backend_image }}
|
||||
frontend_image: ${{ steps.set-images.outputs.frontend_image }}
|
||||
ocr_image: ${{ steps.set-images.outputs.ocr_image }}
|
||||
steps:
|
||||
- name: Check Docker availability
|
||||
run: |
|
||||
@@ -56,7 +53,6 @@ jobs:
|
||||
TAG="${{ inputs.image_tag }}"
|
||||
echo "backend_image=$REGISTRY/egullickson/backend:$TAG" >> $GITHUB_OUTPUT
|
||||
echo "frontend_image=$REGISTRY/egullickson/frontend:$TAG" >> $GITHUB_OUTPUT
|
||||
echo "ocr_image=$REGISTRY/egullickson/ocr:$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Determine target stack
|
||||
id: determine-stack
|
||||
@@ -87,7 +83,6 @@ jobs:
|
||||
TARGET_STACK: ${{ needs.validate.outputs.target_stack }}
|
||||
BACKEND_IMAGE: ${{ needs.validate.outputs.backend_image }}
|
||||
FRONTEND_IMAGE: ${{ needs.validate.outputs.frontend_image }}
|
||||
OCR_IMAGE: ${{ needs.validate.outputs.ocr_image }}
|
||||
steps:
|
||||
- name: Checkout scripts, config, and compose files
|
||||
uses: actions/checkout@v4
|
||||
@@ -95,11 +90,8 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/
|
||||
config/
|
||||
secrets/app/google-wif-config.json
|
||||
docker-compose.yml
|
||||
docker-compose.blue-green.yml
|
||||
docker-compose.prod.yml
|
||||
.env.example
|
||||
sparse-checkout-cone-mode: false
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -109,27 +101,6 @@ jobs:
|
||||
rsync -av --delete "$GITHUB_WORKSPACE/scripts/" "$DEPLOY_PATH/scripts/"
|
||||
cp "$GITHUB_WORKSPACE/docker-compose.yml" "$DEPLOY_PATH/"
|
||||
cp "$GITHUB_WORKSPACE/docker-compose.blue-green.yml" "$DEPLOY_PATH/"
|
||||
cp "$GITHUB_WORKSPACE/docker-compose.prod.yml" "$DEPLOY_PATH/"
|
||||
# WIF credential config (not a secret -- references Auth0 token script path)
|
||||
# Remove any Docker-created directory artifact from failed bind mounts
|
||||
rm -rf "$DEPLOY_PATH/secrets/app/google-wif-config.json"
|
||||
mkdir -p "$DEPLOY_PATH/secrets/app"
|
||||
cp "$GITHUB_WORKSPACE/secrets/app/google-wif-config.json" "$DEPLOY_PATH/secrets/app/"
|
||||
|
||||
- name: Generate environment configuration
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
{
|
||||
echo "# Generated by CI/CD - DO NOT EDIT"
|
||||
echo "STRIPE_PRO_MONTHLY_PRICE_ID=${{ vars.STRIPE_PRO_MONTHLY_PRICE_ID }}"
|
||||
echo "STRIPE_PRO_YEARLY_PRICE_ID=${{ vars.STRIPE_PRO_YEARLY_PRICE_ID }}"
|
||||
echo "STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=${{ vars.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID }}"
|
||||
echo "STRIPE_ENTERPRISE_YEARLY_PRICE_ID=${{ vars.STRIPE_ENTERPRISE_YEARLY_PRICE_ID }}"
|
||||
echo "VITE_STRIPE_PUBLISHABLE_KEY=${{ vars.VITE_STRIPE_PUBLISHABLE_KEY }}"
|
||||
echo "GRAFANA_ADMIN_PASSWORD=${{ secrets.GRAFANA_ADMIN_PASSWORD }}"
|
||||
} > .env
|
||||
chmod +x scripts/ci/generate-log-config.sh
|
||||
./scripts/ci/generate-log-config.sh "$LOG_LEVEL" >> .env
|
||||
|
||||
- name: Login to registry
|
||||
run: |
|
||||
@@ -137,22 +108,17 @@ jobs:
|
||||
|
||||
- name: Inject secrets
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
chmod +x scripts/inject-secrets.sh
|
||||
SECRETS_DIR="$DEPLOY_PATH/secrets/app" ./scripts/inject-secrets.sh
|
||||
chmod +x "$GITHUB_WORKSPACE/scripts/inject-secrets.sh"
|
||||
"$GITHUB_WORKSPACE/scripts/inject-secrets.sh"
|
||||
env:
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
|
||||
AUTH0_MANAGEMENT_CLIENT_ID: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_ID }}
|
||||
AUTH0_MANAGEMENT_CLIENT_SECRET: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_SECRET }}
|
||||
AUTH0_OCR_CLIENT_ID: ${{ secrets.AUTH0_OCR_CLIENT_ID }}
|
||||
AUTH0_OCR_CLIENT_SECRET: ${{ secrets.AUTH0_OCR_CLIENT_SECRET }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }}
|
||||
CF_DNS_API_TOKEN: ${{ secrets.CF_DNS_API_TOKEN }}
|
||||
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
|
||||
- name: Initialize data directories
|
||||
run: |
|
||||
@@ -170,7 +136,6 @@ jobs:
|
||||
run: |
|
||||
docker pull $BACKEND_IMAGE
|
||||
docker pull $FRONTEND_IMAGE
|
||||
docker pull $OCR_IMAGE
|
||||
|
||||
- name: Record expected image IDs
|
||||
id: expected-images
|
||||
@@ -183,50 +148,18 @@ jobs:
|
||||
echo "frontend_id=$FRONTEND_ID" >> $GITHUB_OUTPUT
|
||||
echo "backend_id=$BACKEND_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Start shared services
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
# Start shared infrastructure services (database, cache, logging)
|
||||
# --no-recreate prevents restarting postgres/redis when config files change
|
||||
# These must persist across blue-green deployments to avoid data service disruption
|
||||
docker compose -f $BASE_COMPOSE_FILE -f $COMPOSE_BLUE_GREEN -f $COMPOSE_PROD up -d --no-recreate \
|
||||
mvp-postgres mvp-redis mvp-loki mvp-alloy mvp-grafana
|
||||
|
||||
- name: Wait for shared services health
|
||||
run: |
|
||||
echo "Waiting for PostgreSQL and Redis to be healthy..."
|
||||
for service in mvp-postgres mvp-redis; do
|
||||
for i in $(seq 1 24); do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
echo "OK: $service is healthy"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 24 ]; then
|
||||
echo "ERROR: $service health check timed out (status: $health)"
|
||||
docker logs $service --tail 50 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for $service... (attempt $i/24, status: $health)"
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
echo "All shared services healthy"
|
||||
|
||||
- name: Start target stack
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
export BACKEND_IMAGE=$BACKEND_IMAGE
|
||||
export FRONTEND_IMAGE=$FRONTEND_IMAGE
|
||||
export OCR_IMAGE=$OCR_IMAGE
|
||||
# --force-recreate ensures containers are recreated even if image tag is same
|
||||
# This prevents stale container content when image digest changes
|
||||
# Start shared OCR service and target stack
|
||||
docker compose -f $BASE_COMPOSE_FILE -f $COMPOSE_BLUE_GREEN -f $COMPOSE_PROD up -d --force-recreate \
|
||||
mvp-ocr mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK
|
||||
docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d --force-recreate \
|
||||
mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK
|
||||
|
||||
- name: Wait for stack initialization
|
||||
run: sleep 5
|
||||
run: sleep 10
|
||||
|
||||
- name: Verify container images
|
||||
run: |
|
||||
@@ -261,7 +194,7 @@ jobs:
|
||||
- name: Start Traefik
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
docker compose -f $BASE_COMPOSE_FILE -f $COMPOSE_BLUE_GREEN -f $COMPOSE_PROD up -d mvp-traefik
|
||||
docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d mvp-traefik
|
||||
|
||||
- name: Wait for Traefik
|
||||
run: |
|
||||
@@ -305,79 +238,22 @@ jobs:
|
||||
- name: Wait for routing propagation
|
||||
run: sleep 5
|
||||
|
||||
- name: Check container status and health
|
||||
run: |
|
||||
for service in mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK mvp-ocr; do
|
||||
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
|
||||
if [ "$status" != "running" ]; then
|
||||
echo "ERROR: $service is not running (status: $status)"
|
||||
docker logs $service --tail 50 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: $service is running"
|
||||
done
|
||||
|
||||
# Wait for Docker healthchecks to complete (services with healthcheck defined)
|
||||
echo ""
|
||||
echo "Waiting for Docker healthchecks..."
|
||||
for service in mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK mvp-ocr; do
|
||||
# Check if service has a healthcheck defined
|
||||
has_healthcheck=$(docker inspect --format='{{if .Config.Healthcheck}}true{{else}}false{{end}}' $service 2>/dev/null || echo "false")
|
||||
if [ "$has_healthcheck" = "true" ]; then
|
||||
# 48 attempts x 5 seconds = 4 minutes max wait (backend with fresh migrations can take ~3 min)
|
||||
for i in $(seq 1 48); do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
echo "OK: $service is healthy"
|
||||
break
|
||||
fi
|
||||
# Don't fail immediately on unhealthy - container may still be starting up
|
||||
# and can recover. Let the timeout handle truly broken containers.
|
||||
if [ $i -eq 48 ]; then
|
||||
echo "ERROR: $service health check timed out (status: $health)"
|
||||
docker logs $service --tail 100 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for $service healthcheck... (attempt $i/48, status: $health)"
|
||||
sleep 5
|
||||
done
|
||||
else
|
||||
echo "SKIP: $service has no healthcheck defined"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Wait for backend health
|
||||
run: |
|
||||
for i in $(seq 1 12); do
|
||||
if docker exec mvp-backend-$TARGET_STACK curl -sf http://localhost:3001/health > /dev/null 2>&1; then
|
||||
echo "OK: Backend health check passed"
|
||||
exit 0
|
||||
fi
|
||||
if [ $i -eq 12 ]; then
|
||||
echo "ERROR: Backend health check failed after 12 attempts"
|
||||
docker logs mvp-backend-$TARGET_STACK --tail 100
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $i/12: Backend not ready, waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
- name: External health check
|
||||
run: |
|
||||
REQUIRED_FEATURES='["admin","auth","onboarding","vehicles","documents","fuel-logs","stations","maintenance","platform","notifications","user-profile","user-preferences","user-export"]'
|
||||
|
||||
for i in $(seq 1 12); do
|
||||
for i in 1 2 3 4 5 6; do
|
||||
RESPONSE=$(curl -sf https://motovaultpro.com/api/health 2>/dev/null) || {
|
||||
echo "Attempt $i/12: Connection failed, waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Connection failed, waiting 10s..."
|
||||
sleep 10
|
||||
continue
|
||||
}
|
||||
|
||||
# Check status is "healthy"
|
||||
STATUS=$(echo "$RESPONSE" | jq -r '.status')
|
||||
if [ "$STATUS" != "healthy" ]; then
|
||||
echo "Attempt $i/12: Status is '$STATUS', not 'healthy'. Waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Status is '$STATUS', not 'healthy'. Waiting 10s..."
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -387,8 +263,8 @@ jobs:
|
||||
')
|
||||
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "Attempt $i/12: Missing features: $MISSING. Waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Missing features: $MISSING. Waiting 10s..."
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -397,7 +273,7 @@ jobs:
|
||||
exit 0
|
||||
done
|
||||
|
||||
echo "ERROR: Production health check failed after 12 attempts"
|
||||
echo "ERROR: Production health check failed after 6 attempts"
|
||||
echo "Last response: $RESPONSE"
|
||||
exit 1
|
||||
|
||||
|
||||
@@ -15,10 +15,9 @@ on:
|
||||
env:
|
||||
REGISTRY: git.motovaultpro.com
|
||||
DEPLOY_PATH: /opt/motovaultpro
|
||||
BASE_COMPOSE_FILE: docker-compose.yml
|
||||
STAGING_COMPOSE_FILE: docker-compose.staging.yml
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
COMPOSE_STAGING: docker-compose.staging.yml
|
||||
HEALTH_CHECK_TIMEOUT: "60"
|
||||
LOG_LEVEL: DEBUG
|
||||
|
||||
jobs:
|
||||
# ============================================
|
||||
@@ -30,7 +29,6 @@ jobs:
|
||||
outputs:
|
||||
backend_image: ${{ steps.tags.outputs.backend_image }}
|
||||
frontend_image: ${{ steps.tags.outputs.frontend_image }}
|
||||
ocr_image: ${{ steps.tags.outputs.ocr_image }}
|
||||
short_sha: ${{ steps.tags.outputs.short_sha }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -47,7 +45,6 @@ jobs:
|
||||
SHORT_SHA="${SHORT_SHA:0:7}"
|
||||
echo "backend_image=$REGISTRY/egullickson/backend:$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "frontend_image=$REGISTRY/egullickson/frontend:$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "ocr_image=$REGISTRY/egullickson/ocr:$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build backend image
|
||||
@@ -70,32 +67,18 @@ jobs:
|
||||
--build-arg VITE_AUTH0_CLIENT_ID=${{ vars.VITE_AUTH0_CLIENT_ID }} \
|
||||
--build-arg VITE_AUTH0_AUDIENCE=${{ vars.VITE_AUTH0_AUDIENCE }} \
|
||||
--build-arg VITE_API_BASE_URL=/api \
|
||||
--build-arg VITE_STRIPE_PUBLISHABLE_KEY=${{ vars.VITE_STRIPE_PUBLISHABLE_KEY }} \
|
||||
--cache-from $REGISTRY/egullickson/frontend:latest \
|
||||
-t ${{ steps.tags.outputs.frontend_image }} \
|
||||
-t $REGISTRY/egullickson/frontend:latest \
|
||||
-f frontend/Dockerfile \
|
||||
frontend
|
||||
|
||||
- name: Build OCR image
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--build-arg REGISTRY_MIRRORS=$REGISTRY/egullickson/mirrors \
|
||||
--cache-from $REGISTRY/egullickson/ocr:latest \
|
||||
-t ${{ steps.tags.outputs.ocr_image }} \
|
||||
-t $REGISTRY/egullickson/ocr:latest \
|
||||
-f ocr/Dockerfile \
|
||||
ocr
|
||||
|
||||
- name: Push images
|
||||
run: |
|
||||
docker push ${{ steps.tags.outputs.backend_image }}
|
||||
docker push ${{ steps.tags.outputs.frontend_image }}
|
||||
docker push ${{ steps.tags.outputs.ocr_image }}
|
||||
docker push $REGISTRY/egullickson/backend:latest
|
||||
docker push $REGISTRY/egullickson/frontend:latest
|
||||
docker push $REGISTRY/egullickson/ocr:latest
|
||||
|
||||
# ============================================
|
||||
# DEPLOY STAGING - Deploy to staging server
|
||||
@@ -107,38 +90,10 @@ jobs:
|
||||
env:
|
||||
BACKEND_IMAGE: ${{ needs.build.outputs.backend_image }}
|
||||
FRONTEND_IMAGE: ${{ needs.build.outputs.frontend_image }}
|
||||
OCR_IMAGE: ${{ needs.build.outputs.ocr_image }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Sync config, scripts, and compose files to deploy path
|
||||
run: |
|
||||
rsync -av --delete "$GITHUB_WORKSPACE/config/" "$DEPLOY_PATH/config/"
|
||||
rsync -av --delete "$GITHUB_WORKSPACE/scripts/" "$DEPLOY_PATH/scripts/"
|
||||
cp "$GITHUB_WORKSPACE/docker-compose.yml" "$DEPLOY_PATH/"
|
||||
cp "$GITHUB_WORKSPACE/docker-compose.staging.yml" "$DEPLOY_PATH/"
|
||||
# WIF credential config (not a secret -- references Auth0 token script path)
|
||||
# Remove any Docker-created directory artifact from failed bind mounts
|
||||
rm -rf "$DEPLOY_PATH/secrets/app/google-wif-config.json"
|
||||
mkdir -p "$DEPLOY_PATH/secrets/app"
|
||||
cp "$GITHUB_WORKSPACE/secrets/app/google-wif-config.json" "$DEPLOY_PATH/secrets/app/"
|
||||
|
||||
- name: Generate environment configuration
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
{
|
||||
echo "# Generated by CI/CD - DO NOT EDIT"
|
||||
echo "STRIPE_PRO_MONTHLY_PRICE_ID=${{ vars.STRIPE_PRO_MONTHLY_PRICE_ID }}"
|
||||
echo "STRIPE_PRO_YEARLY_PRICE_ID=${{ vars.STRIPE_PRO_YEARLY_PRICE_ID }}"
|
||||
echo "STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=${{ vars.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID }}"
|
||||
echo "STRIPE_ENTERPRISE_YEARLY_PRICE_ID=${{ vars.STRIPE_ENTERPRISE_YEARLY_PRICE_ID }}"
|
||||
echo "VITE_STRIPE_PUBLISHABLE_KEY=${{ vars.VITE_STRIPE_PUBLISHABLE_KEY }}"
|
||||
echo "GRAFANA_ADMIN_PASSWORD=${{ secrets.GRAFANA_ADMIN_PASSWORD }}"
|
||||
} > .env
|
||||
chmod +x scripts/ci/generate-log-config.sh
|
||||
./scripts/ci/generate-log-config.sh "$LOG_LEVEL" >> .env
|
||||
|
||||
- name: Login to registry
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY"
|
||||
@@ -153,14 +108,10 @@ jobs:
|
||||
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
|
||||
AUTH0_MANAGEMENT_CLIENT_ID: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_ID }}
|
||||
AUTH0_MANAGEMENT_CLIENT_SECRET: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_SECRET }}
|
||||
AUTH0_OCR_CLIENT_ID: ${{ secrets.AUTH0_OCR_CLIENT_ID }}
|
||||
AUTH0_OCR_CLIENT_SECRET: ${{ secrets.AUTH0_OCR_CLIENT_SECRET }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }}
|
||||
CF_DNS_API_TOKEN: ${{ secrets.CF_DNS_API_TOKEN }}
|
||||
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
|
||||
- name: Initialize data directories
|
||||
run: |
|
||||
@@ -178,19 +129,17 @@ jobs:
|
||||
run: |
|
||||
docker pull $BACKEND_IMAGE
|
||||
docker pull $FRONTEND_IMAGE
|
||||
docker pull $OCR_IMAGE
|
||||
|
||||
- name: Deploy staging stack
|
||||
run: |
|
||||
cd "$DEPLOY_PATH"
|
||||
export BACKEND_IMAGE=$BACKEND_IMAGE
|
||||
export FRONTEND_IMAGE=$FRONTEND_IMAGE
|
||||
export OCR_IMAGE=$OCR_IMAGE
|
||||
docker compose -f $BASE_COMPOSE_FILE -f $STAGING_COMPOSE_FILE down --timeout 30 || true
|
||||
docker compose -f $BASE_COMPOSE_FILE -f $STAGING_COMPOSE_FILE up -d
|
||||
docker compose -f $COMPOSE_FILE -f $COMPOSE_STAGING down --timeout 30 || true
|
||||
docker compose -f $COMPOSE_FILE -f $COMPOSE_STAGING up -d
|
||||
|
||||
- name: Wait for services
|
||||
run: sleep 5
|
||||
run: sleep 15
|
||||
|
||||
# ============================================
|
||||
# VERIFY STAGING - Health checks
|
||||
@@ -205,7 +154,7 @@ jobs:
|
||||
|
||||
- name: Check container status and health
|
||||
run: |
|
||||
for service in mvp-frontend-staging mvp-backend-staging mvp-ocr-staging mvp-postgres-staging mvp-redis-staging; do
|
||||
for service in mvp-frontend-staging mvp-backend-staging mvp-postgres-staging mvp-redis-staging; do
|
||||
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
|
||||
if [ "$status" != "running" ]; then
|
||||
echo "ERROR: $service is not running (status: $status)"
|
||||
@@ -218,25 +167,26 @@ jobs:
|
||||
# Wait for Docker healthchecks to complete (services with healthcheck defined)
|
||||
echo ""
|
||||
echo "Waiting for Docker healthchecks..."
|
||||
for service in mvp-frontend-staging mvp-backend-staging mvp-ocr-staging mvp-postgres-staging mvp-redis-staging; do
|
||||
for service in mvp-frontend-staging mvp-backend-staging mvp-postgres-staging mvp-redis-staging; do
|
||||
# Check if service has a healthcheck defined
|
||||
has_healthcheck=$(docker inspect --format='{{if .Config.Healthcheck}}true{{else}}false{{end}}' $service 2>/dev/null || echo "false")
|
||||
if [ "$has_healthcheck" = "true" ]; then
|
||||
# 48 attempts x 5 seconds = 4 minutes max wait (backend with fresh migrations can take ~3 min)
|
||||
for i in $(seq 1 48); do
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
echo "OK: $service is healthy"
|
||||
break
|
||||
fi
|
||||
# Don't fail immediately on unhealthy - container may still be starting up
|
||||
# and can recover. Let the timeout handle truly broken containers.
|
||||
if [ $i -eq 48 ]; then
|
||||
echo "ERROR: $service health check timed out (status: $health)"
|
||||
docker logs $service --tail 100 2>/dev/null || true
|
||||
elif [ "$health" = "unhealthy" ]; then
|
||||
echo "ERROR: $service is unhealthy"
|
||||
docker logs $service --tail 50 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for $service healthcheck... (attempt $i/48, status: $health)"
|
||||
if [ $i -eq 10 ]; then
|
||||
echo "ERROR: $service health check timed out (status: $health)"
|
||||
docker logs $service --tail 50 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for $service healthcheck... (attempt $i/10, status: $health)"
|
||||
sleep 5
|
||||
done
|
||||
else
|
||||
@@ -246,36 +196,36 @@ jobs:
|
||||
|
||||
- name: Wait for backend health
|
||||
run: |
|
||||
for i in $(seq 1 12); do
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if docker exec mvp-backend-staging curl -sf http://localhost:3001/health > /dev/null 2>&1; then
|
||||
echo "OK: Backend health check passed"
|
||||
exit 0
|
||||
fi
|
||||
if [ $i -eq 12 ]; then
|
||||
echo "ERROR: Backend health check failed after 12 attempts"
|
||||
if [ $i -eq 6 ]; then
|
||||
echo "ERROR: Backend health check failed after 6 attempts"
|
||||
docker logs mvp-backend-staging --tail 100
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $i/12: Backend not ready, waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Backend not ready, waiting 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Check external endpoint
|
||||
run: |
|
||||
REQUIRED_FEATURES='["admin","auth","onboarding","vehicles","documents","fuel-logs","stations","maintenance","platform","notifications","user-profile","user-preferences","user-export"]'
|
||||
|
||||
for i in $(seq 1 12); do
|
||||
for i in 1 2 3 4 5 6; do
|
||||
RESPONSE=$(curl -sf https://staging.motovaultpro.com/api/health 2>/dev/null) || {
|
||||
echo "Attempt $i/12: Connection failed, waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Connection failed, waiting 10s..."
|
||||
sleep 10
|
||||
continue
|
||||
}
|
||||
|
||||
# Check status is "healthy"
|
||||
STATUS=$(echo "$RESPONSE" | jq -r '.status')
|
||||
if [ "$STATUS" != "healthy" ]; then
|
||||
echo "Attempt $i/12: Status is '$STATUS', not 'healthy'. Waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Status is '$STATUS', not 'healthy'. Waiting 10s..."
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -285,8 +235,8 @@ jobs:
|
||||
')
|
||||
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "Attempt $i/12: Missing features: $MISSING. Waiting 5s..."
|
||||
sleep 5
|
||||
echo "Attempt $i/6: Missing features: $MISSING. Waiting 10s..."
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -295,7 +245,7 @@ jobs:
|
||||
exit 0
|
||||
done
|
||||
|
||||
echo "ERROR: Staging health check failed after 12 attempts"
|
||||
echo "ERROR: Staging health check failed after 6 attempts"
|
||||
echo "Last response: $RESPONSE"
|
||||
exit 1
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ node_modules/
|
||||
.env
|
||||
.env.local
|
||||
.env.backup
|
||||
.env.logging
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -13,16 +12,12 @@ coverage/
|
||||
*.swo
|
||||
.venv
|
||||
.playwright-mcp
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# K8s-aligned secret mounts (real files ignored; examples committed)
|
||||
secrets/**
|
||||
!secrets/
|
||||
!secrets/**/
|
||||
!secrets/**/*.example
|
||||
!secrets/app/google-wif-config.json
|
||||
|
||||
# Traefik ACME certificates (contains private keys)
|
||||
data/traefik/acme.json
|
||||
@@ -1,11 +1,6 @@
|
||||
# MotoVaultPro
|
||||
|
||||
Single-tenant vehicle management application with 9-container architecture (6 application: Traefik, Frontend, Backend, OCR, PostgreSQL, Redis + 3 logging: Loki, Alloy, Grafana).
|
||||
|
||||
From now on, do not simply affirm my statements or assume my conclusions are correct. Your goal is to be an intellectual partner, not just an agreeable assistant. Every time I present an idea, do the following: 1. Analyze my assumptions. What am I taking for granted that might not be true? 2. Provide counterpoints. What would an intelligent, well-informed skeptic say in response? 3. Test my reasoning. Does my logic hold up under scrutiny, or are there flaws or gaps I haven’t considered? 4. Offer alternative perspectives. How else might this idea be framed, interpreted, or challenged? 5. Prioritize truth over agreement. If I am wrong or my logic is weak, I need to know. Correct me clearly and explain why.
|
||||
|
||||
Maintain a constructive approach. Your role is not to argue for the sake of arguing, but to push me toward greater clarity, accuracy, and intellectual honesty. If I ever start slipping into confirmation bias or unchecked assumptions, call it out directly. Let’s refine not just our conclusions, but how we arrive at them.
|
||||
|
||||
Single-tenant vehicle management application with 5-container architecture (Traefik, Frontend, Backend, PostgreSQL, Redis).
|
||||
|
||||
## Files
|
||||
|
||||
@@ -13,9 +8,6 @@ Maintain a constructive approach. Your role is not to argue for the sake of argu
|
||||
| ---- | ---- | ------------ |
|
||||
| `Makefile` | Build, test, deploy commands | Running any make command |
|
||||
| `docker-compose.yml` | Development container orchestration | Local development setup |
|
||||
| `docker-compose.staging.yml` | Staging container orchestration | Staging deployment |
|
||||
| `docker-compose.prod.yml` | Production container orchestration | Production deployment |
|
||||
| `docker-compose.blue-green.yml` | Blue-green deployment orchestration | Zero-downtime deploys |
|
||||
| `package.json` | Root workspace dependencies | Dependency management |
|
||||
| `README.md` | Project overview | First-time setup |
|
||||
|
||||
@@ -25,35 +17,27 @@ Maintain a constructive approach. Your role is not to argue for the sake of argu
|
||||
| --------- | ---- | ------------ |
|
||||
| `backend/` | Fastify API server with feature capsules | Backend development |
|
||||
| `frontend/` | React/Vite SPA with MUI | Frontend development |
|
||||
| `ocr/` | Python OCR microservice (PaddleOCR / Google Cloud Vision engines, Gemini for schedule/VIN extraction) | OCR pipeline, receipt/VIN extraction |
|
||||
| `docs/` | Project documentation hub | Architecture, APIs, testing |
|
||||
| `config/` | Configuration files (Traefik, logging stack) | Infrastructure setup |
|
||||
| `scripts/` | Utility scripts (backup, deploy, CI) | Automation tasks |
|
||||
| `config/` | Configuration files (Traefik, monitoring) | Infrastructure setup |
|
||||
| `scripts/` | Utility scripts (backup, deploy) | Automation tasks |
|
||||
| `.ai/` | AI context and workflow contracts | AI-assisted development |
|
||||
| `.claude/` | Claude Code skill library, role agent definitions, and configuration | Using a skill, delegating to an agent, checking project conventions |
|
||||
| `.claude/` | Claude Code agents and skills | Delegating to agents, using skills |
|
||||
| `.gitea/` | Gitea workflows and templates | CI/CD, issue templates |
|
||||
| `ansible/` | Ansible deployment playbooks | Server provisioning |
|
||||
| `certs/` | TLS certificates | SSL/TLS configuration |
|
||||
| `secrets/` | Docker secrets (Stripe keys, Traefik) | Secret management |
|
||||
| `data/` | Persistent data volumes (backups, documents) | Storage paths, volume mounts |
|
||||
|
||||
## Build for staging and production. NOT FOR DEVELOPMENT
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make setup # First-time setup
|
||||
make rebuild # Rebuild containers
|
||||
make setup # First-time setup (builds containers, runs migrations)
|
||||
make rebuild # Rebuild containers after changes
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
Root `package.json` has no `scripts` block — `npm test` at repo root fails. Run per workspace:
|
||||
|
||||
```bash
|
||||
cd backend && npm test && npm run lint && npm run type-check
|
||||
cd frontend && npm test && npm run lint && npm run type-check
|
||||
# lint and type-check across both workspaces without cd:
|
||||
make lint
|
||||
make type-check
|
||||
npm test # Run all tests
|
||||
npm run lint # Linting
|
||||
npm run type-check # TypeScript validation
|
||||
```
|
||||
|
||||
---
|
||||
@@ -108,32 +92,35 @@ All methods returning data to the API must use these mappers - never return raw
|
||||
## Development Workflow (Local + CI/CD)
|
||||
|
||||
### Local Development
|
||||
Root `package.json` has no `scripts` block; install and run dev servers per workspace:
|
||||
```bash
|
||||
cd backend && npm install && npm run dev
|
||||
cd frontend && npm install && npm run dev
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start dev server
|
||||
npm test # Run tests
|
||||
npm run lint # Linting
|
||||
npm run type-check # TypeScript validation
|
||||
```
|
||||
Test/lint/type-check commands: see the Test section above.
|
||||
|
||||
### CI/CD Pipeline (on PR)
|
||||
The Gitea pipeline (`.gitea/workflows/staging.yaml`) builds the backend, frontend, and ocr Docker images, deploys them to the shared staging environment, and health-checks the deployment (container health checks plus `/api/health`). It runs no tests, no lint, no type-check, and no security scan. Validation is entirely the author's responsibility before opening a PR — see `.claude/skills/mvp-validation-and-qa/SKILL.md` for the evidence bar and definition of done.
|
||||
- Container builds and integration tests
|
||||
- Mobile/desktop viewport validation
|
||||
- Security scanning
|
||||
|
||||
**Flow**: Local dev -> author self-gates (lint/type-check/tests) -> Push to Gitea -> CI builds images and deploys/health-checks staging -> PR review -> Merge
|
||||
**Flow**: Local dev -> Push to Gitea -> CI/CD runs -> PR review -> Merge
|
||||
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### Automated Checks Are Mandatory
|
||||
**ALL hook issues are BLOCKING - EVERYTHING must be GREEN!**
|
||||
**ALL hook issues are BLOCKING - EVERYTHING must be ✅ GREEN!**
|
||||
- No errors. No formatting issues. No linting problems. Zero tolerance
|
||||
- These are not suggestions. Fix ALL issues before continuing
|
||||
|
||||
### Code Completion Criteria
|
||||
Our code is complete when:
|
||||
- All linters pass with zero issues
|
||||
- All tests pass
|
||||
- Feature works end-to-end
|
||||
- Old code is deleted
|
||||
- ✅ All linters pass with zero issues
|
||||
- ✅ All tests pass
|
||||
- ✅ Feature works end-to-end
|
||||
- ✅ Old code is deleted
|
||||
|
||||
## AI Collaboration Strategy
|
||||
|
||||
@@ -166,60 +153,50 @@ Leverage subagents aggressively for better results:
|
||||
|
||||
Canonical sources only - avoid duplication:
|
||||
- Architecture and metadata: `.ai/context.json`
|
||||
- Issue workflow: `.claude/skills/mvp-change-control/SKILL.md`
|
||||
- Sprint workflow contract: `.ai/workflow-contract.json`
|
||||
- Documentation hub: `docs/README.md`
|
||||
- Feature work: `backend/src/features/{feature}/README.md`
|
||||
- Platform architecture: `docs/PLATFORM-SERVICES.md`
|
||||
- Testing workflow: `docs/TESTING.md`
|
||||
|
||||
## Issue Workflow
|
||||
## Sprint Workflow
|
||||
|
||||
Issues are the source of truth; work flows directly from issues by priority (sprints and milestones are not used). Full workflow, quality-review taxonomy (RULE 0/1/2), and label discipline: `.claude/skills/mvp-change-control/SKILL.md`.
|
||||
Issues are the source of truth. See `.ai/workflow-contract.json` for complete workflow.
|
||||
|
||||
### Quick Reference
|
||||
- Every PR must link to at least one issue
|
||||
- Use Gitea MCP tools for issue/label/branch/PR operations
|
||||
- Labels: `status/backlog` -> `status/ready` -> `status/in-progress` -> `status/review` -> `status/done`
|
||||
- Branches: `issue-{parent_index}-{slug}` (e.g., `issue-42-add-fuel-report`)
|
||||
- Branches: `issue-{index}-{slug}` (e.g., `issue-42-add-fuel-report`)
|
||||
- Commits: `{type}: {summary} (refs #{index})` (e.g., `feat: add fuel report (refs #42)`)
|
||||
|
||||
### Sub-Issue Decomposition
|
||||
Multi-file changes (3+ files) must be broken into sub-issues for smaller AI context windows:
|
||||
- **Sub-issue title**: `{type}: {summary} (#{parent_index})` -- parent index in title
|
||||
- **Sub-issue body**: First line `Relates to #{parent_index}`
|
||||
- **ONE branch** per parent issue only. Never branch per sub-issue.
|
||||
- **ONE PR** per parent issue. Body lists `Fixes #N` for parent and every sub-issue.
|
||||
- **Commits** reference the specific sub-issue: `feat: add dashboard (refs #107)`
|
||||
- **Status labels** tracked on parent only. Sub-issues stay `status/backlog`.
|
||||
- **Plan milestones** map 1:1 to sub-issues.
|
||||
|
||||
## Architecture Context for AI
|
||||
|
||||
### 9-Container Architecture
|
||||
**MotoVaultPro uses a unified architecture:** A single-tenant application with 9 containers - 6 application (Traefik, Frontend, Backend, OCR, PostgreSQL, Redis) + 3 logging (Loki, Alloy, Grafana). Application features in `backend/src/features/[name]/` are self-contained modules within the backend service, including the platform feature for vehicle data and VIN decoding. See `docs/LOGGING.md` for unified logging system documentation.
|
||||
### Simplified 5-Container Architecture
|
||||
**MotoVaultPro uses a simplified architecture:** A single-tenant application with 5 containers - Traefik, Frontend, Backend, PostgreSQL, and Redis. Application features in `backend/src/features/[name]/` are self-contained modules within the backend service, including the platform feature for vehicle data and VIN decoding.
|
||||
|
||||
### Key Principles for AI Understanding
|
||||
- **Feature Capsule Organization**: Application features are self-contained modules within the backend
|
||||
- **Single-Tenant**: All data belongs to a single user/tenant
|
||||
- **User-Scoped Data**: All application data isolated by user_id
|
||||
- **Local Dev + CI/CD**: Development and testing locally; CI/CD builds images and health-checks the staging deploy (see CI/CD Pipeline section)
|
||||
- **Local Dev + CI/CD**: Development locally, container testing in CI/CD pipeline
|
||||
- **Integrated Platform**: Platform capabilities integrated into main backend service
|
||||
|
||||
### Common AI Tasks
|
||||
See `Makefile` for authoritative commands and `docs/README.md` for navigation.
|
||||
|
||||
## Skill Library
|
||||
## Agent System
|
||||
|
||||
Reusable domain knowledge lives in `.claude/skills/` as 16 `mvp-*` skills, one directory per topic. Each skill's `SKILL.md` frontmatter `description` states exactly when to load it — full index at `.claude/CLAUDE.md`. Key entry points:
|
||||
| Directory | Contents | When to Read |
|
||||
|-----------|----------|--------------|
|
||||
| `.claude/role-agents/` | Developer, TW, QR, Debugger | Delegating execution |
|
||||
| `.claude/role-agents/quality-reviewer.md` | RULE 0/1/2 definitions | Quality review |
|
||||
| `.claude/skills/planner/` | Planning workflow | Complex features (3+ files) |
|
||||
| `.claude/skills/problem-analysis/` | Problem decomposition | Uncertain approach |
|
||||
| `.claude/agents/` | Domain agents | Feature/Frontend/Platform work |
|
||||
| `.ai/workflow-contract.json` | Sprint process, skill integration | Issue workflow |
|
||||
|
||||
| Skill | Use for |
|
||||
|-------|---------|
|
||||
| `.claude/skills/mvp-change-control/SKILL.md` | Issue/branch/PR workflow, quality-review taxonomy (RULE 0/1/2) |
|
||||
| `.claude/skills/mvp-debugging-playbook/SKILL.md` | Triaging a live failure |
|
||||
| `.claude/skills/mvp-architecture-contract/SKILL.md` | System invariants (e.g. mobile/desktop screen registration) |
|
||||
| `.claude/skills/mvp-validation-and-qa/SKILL.md` | Definition of done, test evidence bar |
|
||||
|
||||
### Quality Rules (see `mvp-change-control` for full definitions)
|
||||
### Quality Rules (see quality-reviewer.md for full definitions)
|
||||
- **RULE 0 (CRITICAL)**: Production reliability - unhandled errors, security, resource exhaustion
|
||||
- **RULE 1 (HIGH)**: Project standards - mobile+desktop, naming, patterns, CI/CD pass
|
||||
- **RULE 2 (SHOULD_FIX)**: Structural quality - god objects, duplication, dead code
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# MotoVaultPro — Simplified Architecture
|
||||
|
||||
9-container architecture (6 application + 3 logging) with integrated platform feature.
|
||||
Simplified 5-container architecture with integrated platform feature.
|
||||
|
||||
## Requirements
|
||||
- Mobile + Desktop: Implement and test every feature on both.
|
||||
- Docker-first, production-only: All testing and validation in containers.
|
||||
- See `CLAUDE.md` for development partnership guidelines.
|
||||
|
||||
## Staging and Production Commands. NOT FOR DEVELOPMENT (containers)
|
||||
## Quick Start (containers)
|
||||
```bash
|
||||
make setup # build + start + migrate (uses mvp-* containers)
|
||||
make start # start 5 services
|
||||
make rebuild #
|
||||
make rebuild # rebuild on changes
|
||||
make logs # tail all logs
|
||||
make migrate # run DB migrations
|
||||
```
|
||||
@@ -34,7 +34,6 @@ make migrate # run DB migrations
|
||||
- View which container images are running: `docker ps --format 'table {{.Names}}\t{{.Image}}'`
|
||||
- Flush all redis cache: `docker compose exec -T mvp-redis sh -lc "redis-cli FLUSHALL"`
|
||||
- Flush all backup data on staging before restoring: `docker compose exec mvp-postgres psql -U postgres -d motovaultpro -c "TRUNCATE TABLE backup_history, backup_schedules, backup_settings RESTART IDENTITY CASCADE;"`
|
||||
- Create new admin users on brand new deployment `docker exec -it motovaultpro-backend-1 node dist/_system/cli/create-admin.js`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -241,19 +240,10 @@ make migrate # run DB migrations
|
||||
Skills: codebase-analysis, problem-analysis, decision-critic, planner, doc-sync
|
||||
Role-Agents: Developer, Technical Writer (TW), Quality Reviewer (QR), Debugger
|
||||
Domain Agents: Feature Agent, Frontend Agent, Platform Agent, Quality Agent
|
||||
|
||||
|
||||
Labels: status/backlog -> status/ready -> status/in-progress -> status/review -> status/done
|
||||
Commits: {type}: {summary} (refs #{N}) | Types: feat, fix, chore, docs, refactor, test
|
||||
Branches: issue-{N}-{slug} | Example: issue-42-add-fuel-report
|
||||
|
||||
SUB-ISSUE PATTERN (multi-file features)
|
||||
----------------------------------------
|
||||
Parent: #105 "feat: Add Grafana dashboards"
|
||||
Sub: #106 "feat: Dashboard provisioning (#105)" <-- parent index in title
|
||||
Branch: issue-105-add-grafana-dashboards <-- ONE branch, parent index
|
||||
Commit: feat: add provisioning (refs #106) <-- refs specific sub-issue
|
||||
PR: feat: Add Grafana dashboards (#105) <-- ONE PR, parent index
|
||||
Body: Fixes #105, Fixes #106, Fixes #107... <-- closes all
|
||||
|
||||
QUALITY RULES
|
||||
-------------
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# ansible/
|
||||
|
||||
## Files
|
||||
|
||||
| File | What | When to read |
|
||||
| ---- | ---- | ------------ |
|
||||
| `deploy-production-runner.yml` | Production runner deployment | Production deployments |
|
||||
| `deploy-staging-runner.yml` | Staging runner deployment | Staging deployments |
|
||||
| `inventory.yml` | Server inventory | Server host configuration |
|
||||
| `inventory.yml.example` | Example inventory template | Setting up new environments |
|
||||
| `config.yaml.j2` | Jinja2 config template | Runner configuration |
|
||||
@@ -11,14 +11,14 @@
|
||||
# gitea_username: Username for registry login (default: egullickson)
|
||||
#
|
||||
# Optional variables:
|
||||
# act_runner_version: Version of act_runner to install
|
||||
# act_runner_version: Version of act_runner to install (default: 0.2.13)
|
||||
# gitea_instance: Gitea server URL (default: https://git.motovaultpro.com)
|
||||
|
||||
- name: Deploy MotoVaultPro Production Runner
|
||||
hosts: production
|
||||
become: true
|
||||
vars:
|
||||
act_runner_version: "0.3.0"
|
||||
act_runner_version: "0.2.13"
|
||||
gitea_instance: "https://git.motovaultpro.com"
|
||||
gitea_username: "egullickson"
|
||||
runner_name: "Production Server"
|
||||
@@ -49,8 +49,6 @@
|
||||
- gnupg
|
||||
- jq
|
||||
- nodejs
|
||||
- cron
|
||||
- rsync
|
||||
state: present
|
||||
|
||||
# ============================================
|
||||
@@ -105,20 +103,10 @@
|
||||
# ============================================
|
||||
# act_runner Installation
|
||||
# ============================================
|
||||
- name: Check current act_runner version
|
||||
command: act_runner --version
|
||||
register: current_runner_version
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Download act_runner binary
|
||||
shell: curl -fsSL -o /usr/local/bin/act_runner "https://gitea.com/gitea/act_runner/releases/download/v{{ act_runner_version }}/act_runner-{{ act_runner_version }}-linux-amd64"
|
||||
when: current_runner_version.rc != 0 or act_runner_version not in (current_runner_version.stdout | default(''))
|
||||
notify: Restart act_runner
|
||||
|
||||
- name: Set act_runner binary permissions
|
||||
file:
|
||||
path: /usr/local/bin/act_runner
|
||||
get_url:
|
||||
url: "https://gitea.com/gitea/act_runner/releases/download/v{{ act_runner_version }}/act_runner-{{ act_runner_version }}-linux-amd64"
|
||||
dest: /usr/local/bin/act_runner
|
||||
mode: '0755'
|
||||
|
||||
- name: Verify act_runner installation
|
||||
@@ -281,17 +269,24 @@
|
||||
when: gitea_registry_token is defined
|
||||
|
||||
# ============================================
|
||||
# Remove Legacy Docker Cleanup (was destroying volumes)
|
||||
# Maintenance Scripts
|
||||
# ============================================
|
||||
- name: Remove legacy Docker cleanup cron job
|
||||
- name: Create Docker cleanup script
|
||||
copy:
|
||||
dest: /usr/local/bin/docker-cleanup.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
# Remove unused Docker resources older than 7 days
|
||||
docker system prune -af --filter "until=168h"
|
||||
docker volume prune -f
|
||||
mode: '0755'
|
||||
|
||||
- name: Schedule Docker cleanup cron job
|
||||
cron:
|
||||
name: "Docker cleanup"
|
||||
state: absent
|
||||
|
||||
- name: Remove legacy Docker cleanup script
|
||||
file:
|
||||
path: /usr/local/bin/docker-cleanup.sh
|
||||
state: absent
|
||||
minute: "0"
|
||||
hour: "3"
|
||||
job: "/usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1"
|
||||
|
||||
# ============================================
|
||||
# Production-Specific Security Hardening
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
hosts: staging
|
||||
become: true
|
||||
vars:
|
||||
act_runner_version: "0.3.0"
|
||||
act_runner_version: "0.2.13"
|
||||
gitea_instance: "https://git.motovaultpro.com"
|
||||
gitea_username: "egullickson"
|
||||
runner_name: "Build/Staging Server"
|
||||
@@ -50,8 +50,6 @@
|
||||
- jq
|
||||
- nodejs
|
||||
- make
|
||||
- cron
|
||||
- rsync
|
||||
state: present
|
||||
|
||||
# ============================================
|
||||
@@ -136,20 +134,10 @@
|
||||
# ============================================
|
||||
# act_runner Installation
|
||||
# ============================================
|
||||
- name: Check current act_runner version
|
||||
command: act_runner --version
|
||||
register: current_runner_version
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Download act_runner binary
|
||||
shell: curl -fsSL -o /usr/local/bin/act_runner "https://gitea.com/gitea/act_runner/releases/download/v{{ act_runner_version }}/act_runner-{{ act_runner_version }}-linux-amd64"
|
||||
when: current_runner_version.rc != 0 or act_runner_version not in (current_runner_version.stdout | default(''))
|
||||
notify: Restart act_runner
|
||||
|
||||
- name: Set act_runner binary permissions
|
||||
file:
|
||||
path: /usr/local/bin/act_runner
|
||||
get_url:
|
||||
url: "https://gitea.com/gitea/act_runner/releases/download/v{{ act_runner_version }}/act_runner-{{ act_runner_version }}-linux-amd64"
|
||||
dest: /usr/local/bin/act_runner
|
||||
mode: '0755'
|
||||
|
||||
- name: Verify act_runner installation
|
||||
@@ -312,17 +300,24 @@
|
||||
when: gitea_registry_token is defined
|
||||
|
||||
# ============================================
|
||||
# Remove Legacy Docker Cleanup (was destroying volumes)
|
||||
# Maintenance Scripts
|
||||
# ============================================
|
||||
- name: Remove legacy Docker cleanup cron job
|
||||
- name: Create Docker cleanup script
|
||||
copy:
|
||||
dest: /usr/local/bin/docker-cleanup.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
# Remove unused Docker resources older than 7 days
|
||||
docker system prune -af --filter "until=168h"
|
||||
docker volume prune -f
|
||||
mode: '0755'
|
||||
|
||||
- name: Schedule Docker cleanup cron job
|
||||
cron:
|
||||
name: "Docker cleanup"
|
||||
state: absent
|
||||
|
||||
- name: Remove legacy Docker cleanup script
|
||||
file:
|
||||
path: /usr/local/bin/docker-cleanup.sh
|
||||
state: absent
|
||||
minute: "0"
|
||||
hour: "3"
|
||||
job: "/usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1"
|
||||
|
||||
handlers:
|
||||
- name: Restart act_runner
|
||||
|
||||
@@ -12,10 +12,10 @@ all:
|
||||
staging:
|
||||
hosts:
|
||||
staging.motovaultpro.com:
|
||||
ansible_host: "172.30.1.61"
|
||||
ansible_host: "172.30.1.37"
|
||||
|
||||
# Required: Get from Gitea -> Settings -> Actions -> Runners
|
||||
gitea_runner_token: "hLHjDXKhSF8x540hJv918ArZ2surcPVMcb4H5HT5"
|
||||
gitea_runner_token: "szyvEDd2rRh4BettOTKORD1n2Vh47P1RIH19wLYN"
|
||||
|
||||
# Required: Get from Gitea -> Settings -> Applications
|
||||
# Needs read:packages and write:packages scopes
|
||||
@@ -29,10 +29,10 @@ all:
|
||||
production:
|
||||
hosts:
|
||||
motovaultpro.com:
|
||||
ansible_host: "172.30.1.62"
|
||||
ansible_host: "172.30.1.36"
|
||||
|
||||
# Required: Get from Gitea -> Settings -> Actions -> Runners
|
||||
gitea_runner_token: "hLHjDXKhSF8x540hJv918ArZ2surcPVMcb4H5HT5"
|
||||
gitea_runner_token: "OTwUZsoEUBjomGUVrOU4NxpYiUf2yQcEfYprt6rU"
|
||||
|
||||
# Required: Get from Gitea -> Settings -> Applications
|
||||
# Needs read:packages and write:packages scopes
|
||||
|
||||
+2
-3
@@ -7,8 +7,7 @@
|
||||
| `README.md` | Backend quickstart and commands | Getting started with backend development |
|
||||
| `package.json` | Dependencies and npm scripts | Adding dependencies, understanding build |
|
||||
| `tsconfig.json` | TypeScript configuration | Compiler settings, path aliases |
|
||||
| `eslint.config.js` | ESLint configuration | Linting rules, code style |
|
||||
| `jest.config.js` | Jest test configuration | Test setup, coverage settings |
|
||||
| `jest.config.ts` | Jest test configuration | Test setup, coverage settings |
|
||||
| `Dockerfile` | Container build definition | Docker builds, deployment |
|
||||
|
||||
## Subdirectories
|
||||
@@ -16,4 +15,4 @@
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `src/` | Application source code | Any backend development |
|
||||
| `scripts/` | Utility scripts (docker-entrypoint) | Container startup, automation |
|
||||
| `scripts/` | Utility scripts | Database scripts, automation |
|
||||
|
||||
Generated
+251
-287
@@ -20,26 +20,21 @@
|
||||
"fastify": "^5.2.0",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
"file-type": "^16.5.4",
|
||||
"form-data": "^4.0.0",
|
||||
"get-jwks": "^11.0.3",
|
||||
"ioredis": "^5.4.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mailparser": "^3.9.3",
|
||||
"node-cron": "^3.0.3",
|
||||
"opossum": "^8.0.0",
|
||||
"pg": "^8.13.1",
|
||||
"pino": "^9.6.0",
|
||||
"resend": "^3.0.0",
|
||||
"stripe": "^20.2.0",
|
||||
"svix": "^1.85.0",
|
||||
"tar": "^7.4.3",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/opossum": "^8.0.0",
|
||||
@@ -86,6 +81,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -581,6 +577,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -605,6 +610,17 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||
@@ -1768,11 +1784,15 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
@@ -1929,30 +1949,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mailparser": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz",
|
||||
"integrity": "sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"iconv-lite": "^0.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mailparser/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
@@ -1964,8 +1960,9 @@
|
||||
"version": "22.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -2030,6 +2027,12 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -2092,6 +2095,7 @@
|
||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
@@ -2303,17 +2307,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@zone-eu/mailsplit": {
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz",
|
||||
"integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==",
|
||||
"license": "(MIT OR EUPL-1.1+)",
|
||||
"dependencies": {
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.7",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||
@@ -2347,6 +2340,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2519,6 +2513,12 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -2813,6 +2813,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -2898,6 +2899,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -3090,6 +3092,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^3.1.3",
|
||||
"color-string": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -3108,6 +3123,48 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
|
||||
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string/node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -3509,14 +3566,11 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encoding-japanese": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
|
||||
"integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
@@ -3614,6 +3668,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3947,12 +4002,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
@@ -4030,49 +4079,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastify/node_modules/pino": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.0.tgz",
|
||||
"integrity": "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"real-require": "^0.2.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"thread-stream": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pino": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fastify/node_modules/pino-abstract-transport": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
|
||||
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastify/node_modules/thread-stream": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
|
||||
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"real-require": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/fastparallel": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
|
||||
@@ -4112,6 +4118,12 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4207,6 +4219,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -4561,15 +4579,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
@@ -4642,22 +4651,6 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -4891,7 +4884,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4998,6 +4990,7 @@
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -5780,6 +5773,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/leac": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||
@@ -5813,42 +5812,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libbase64": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz",
|
||||
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/libmime": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
|
||||
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"libbase64": "1.3.0",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/libmime/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libqp": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
|
||||
"integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/light-my-request": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
|
||||
@@ -5893,15 +5856,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -5944,12 +5898,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -5966,24 +5936,6 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.3.tgz",
|
||||
"integrity": "sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zone-eu/mailsplit": "5.4.8",
|
||||
"encoding-japanese": "2.2.0",
|
||||
"he": "1.2.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"iconv-lite": "0.7.2",
|
||||
"libmime": "5.3.7",
|
||||
"linkify-it": "5.0.0",
|
||||
"nodemailer": "7.0.13",
|
||||
"punycode.js": "2.3.1",
|
||||
"tlds": "1.261.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
@@ -6212,15 +6164,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
@@ -6315,6 +6258,7 @@
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -6348,6 +6292,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -6569,6 +6522,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
@@ -6674,9 +6628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
|
||||
"integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz",
|
||||
"integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
@@ -6934,15 +6888,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
@@ -6961,9 +6906,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -7030,6 +6976,20 @@
|
||||
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
|
||||
@@ -7284,7 +7244,6 @@
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
@@ -7360,6 +7319,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -7379,6 +7339,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -7395,6 +7356,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@@ -7413,6 +7375,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@@ -7511,6 +7474,15 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@@ -7540,16 +7512,6 @@
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/steed": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz",
|
||||
@@ -7673,26 +7635,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "20.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.2.0.tgz",
|
||||
"integrity": "sha512-m8niTfdm3nPP/yQswRWMwQxqEUcTtB3RTJQ9oo6NINDzgi7aPOadsH/fPXIIfL1Sc5+lqQFKSk7WiO6CXmvaeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"qs": "^6.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
@@ -7771,29 +7713,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svix": {
|
||||
"version": "1.85.0",
|
||||
"resolved": "https://registry.npmjs.org/svix/-/svix-1.85.0.tgz",
|
||||
"integrity": "sha512-4OxNw++bnNay8SoBwESgzfjMnYmurS1qBX+luhzvljr6EAPn/hqqmkdCR1pbgIe1K1+BzKZEHjAKz9OYrKJYwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"standardwebhooks": "1.0.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svix/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
|
||||
@@ -7834,6 +7753,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||
@@ -7884,6 +7809,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7891,15 +7817,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.261.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
|
||||
"integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tlds": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -7956,6 +7873,15 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -8041,6 +7967,7 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -8128,6 +8055,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8160,12 +8088,6 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
@@ -8234,6 +8156,12 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
@@ -8290,6 +8218,42 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
+26
-31
@@ -18,50 +18,45 @@
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/autoload": "^6.0.1",
|
||||
"pg": "^8.13.1",
|
||||
"ioredis": "^5.4.2",
|
||||
"@fastify/multipart": "^9.0.1",
|
||||
"axios": "^1.7.9",
|
||||
"opossum": "^8.0.0",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.24.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"fastify": "^5.2.0",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/helmet": "^13.0.2",
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
"@fastify/multipart": "^9.0.1",
|
||||
"@fastify/type-provider-typebox": "^6.1.0",
|
||||
"@sinclair/typebox": "^0.34.0",
|
||||
"auth0": "^4.12.0",
|
||||
"axios": "^1.7.9",
|
||||
"fastify": "^5.2.0",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
"file-type": "^16.5.4",
|
||||
"form-data": "^4.0.0",
|
||||
"@fastify/autoload": "^6.0.1",
|
||||
"get-jwks": "^11.0.3",
|
||||
"ioredis": "^5.4.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mailparser": "^3.9.3",
|
||||
"node-cron": "^3.0.3",
|
||||
"opossum": "^8.0.0",
|
||||
"pg": "^8.13.1",
|
||||
"pino": "^9.6.0",
|
||||
"file-type": "^16.5.4",
|
||||
"resend": "^3.0.0",
|
||||
"stripe": "^20.2.0",
|
||||
"svix": "^1.85.0",
|
||||
"tar": "^7.4.3",
|
||||
"zod": "^3.24.1"
|
||||
"node-cron": "^3.0.3",
|
||||
"auth0": "^4.12.0",
|
||||
"tar": "^7.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/opossum": "^8.0.0",
|
||||
"@types/pg": "^8.10.9",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"eslint": "^9.17.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"supertest": "^7.1.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"typescript": "^5.7.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"nodemon": "^3.1.9",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "^29.5.10",
|
||||
"ts-jest": "^29.1.1",
|
||||
"supertest": "^7.1.4",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/opossum": "^8.0.0",
|
||||
"eslint": "^9.17.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"typescript-eslint": "^8.18.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# _system/
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `cli/` | CLI commands and tools | Running backend CLI commands |
|
||||
| `migrations/` | Database migration runner | Running or writing migrations |
|
||||
| `schema/` | Database schema generation | Schema export, documentation |
|
||||
| `scripts/` | System utility scripts | Database maintenance, automation |
|
||||
@@ -17,8 +17,7 @@ const pool = new Pool({
|
||||
const MIGRATION_ORDER = [
|
||||
'features/vehicles', // Primary entity, defines update_updated_at_column()
|
||||
'features/platform', // Normalized make/model/trim schema for dropdowns
|
||||
'features/user-profile', // User profile management; needed by documents migration
|
||||
'features/documents', // Depends on vehicles, user-profile; provides documents table
|
||||
'features/documents', // Depends on vehicles; provides documents table
|
||||
'core/user-preferences', // Depends on update_updated_at_column()
|
||||
'features/fuel-logs', // Depends on vehicles
|
||||
'features/maintenance', // Depends on vehicles
|
||||
@@ -26,12 +25,8 @@ const MIGRATION_ORDER = [
|
||||
'features/admin', // Admin role management and oversight; depends on update_updated_at_column()
|
||||
'features/backup', // Admin backup feature; depends on update_updated_at_column()
|
||||
'features/notifications', // Depends on maintenance and documents
|
||||
'features/email-ingestion', // Depends on documents, notifications (extends email_templates)
|
||||
'features/user-profile', // User profile management; independent
|
||||
'features/terms-agreement', // Terms & Conditions acceptance audit trail
|
||||
'features/audit-log', // Centralized audit logging; independent
|
||||
'features/ownership-costs', // Depends on vehicles and documents; TCO recurring costs
|
||||
'features/subscriptions', // Stripe subscriptions; depends on user-profile, vehicles
|
||||
'core/identity-migration', // Cross-cutting UUID migration; must run after all feature tables exist
|
||||
];
|
||||
|
||||
// Base directory where migrations are copied inside the image (set by Dockerfile)
|
||||
|
||||
+2
-23
@@ -10,7 +10,6 @@ import fastifyMultipart from '@fastify/multipart';
|
||||
// Core plugins
|
||||
import authPlugin from './core/plugins/auth.plugin';
|
||||
import adminGuardPlugin, { setAdminGuardPool } from './core/plugins/admin-guard.plugin';
|
||||
import tierGuardPlugin from './core/plugins/tier-guard.plugin';
|
||||
import loggingPlugin from './core/plugins/logging.plugin';
|
||||
import errorPlugin from './core/plugins/error.plugin';
|
||||
import { appConfig } from './core/config/config-loader';
|
||||
@@ -25,19 +24,12 @@ import { documentsRoutes } from './features/documents/api/documents.routes';
|
||||
import { maintenanceRoutes } from './features/maintenance';
|
||||
import { platformRoutes } from './features/platform';
|
||||
import { adminRoutes } from './features/admin/api/admin.routes';
|
||||
import { auditLogRoutes } from './features/audit-log/api/audit-log.routes';
|
||||
import { notificationsRoutes } from './features/notifications';
|
||||
import { userProfileRoutes } from './features/user-profile';
|
||||
import { onboardingRoutes } from './features/onboarding';
|
||||
import { userPreferencesRoutes } from './features/user-preferences';
|
||||
import { userExportRoutes } from './features/user-export';
|
||||
import { userImportRoutes } from './features/user-import';
|
||||
import { ownershipCostsRoutes } from './features/ownership-costs';
|
||||
import { subscriptionsRoutes, donationsRoutes, webhooksRoutes } from './features/subscriptions';
|
||||
import { ocrRoutes } from './features/ocr';
|
||||
import { emailIngestionWebhookRoutes, emailIngestionRoutes } from './features/email-ingestion';
|
||||
import { pool } from './core/config/database';
|
||||
import { configRoutes } from './core/config/config.routes';
|
||||
|
||||
async function buildApp(): Promise<FastifyInstance> {
|
||||
const app = Fastify({
|
||||
@@ -88,16 +80,13 @@ async function buildApp(): Promise<FastifyInstance> {
|
||||
await app.register(adminGuardPlugin);
|
||||
setAdminGuardPool(pool);
|
||||
|
||||
// Tier guard plugin - for subscription tier enforcement
|
||||
await app.register(tierGuardPlugin);
|
||||
|
||||
// Health check
|
||||
app.get('/health', async (_request, reply) => {
|
||||
return reply.code(200).send({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env['NODE_ENV'],
|
||||
features: ['admin', 'auth', 'config', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export', 'user-import', 'ownership-costs', 'subscriptions', 'donations', 'ocr', 'email-ingestion']
|
||||
features: ['admin', 'auth', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export']
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +96,7 @@ async function buildApp(): Promise<FastifyInstance> {
|
||||
status: 'healthy',
|
||||
scope: 'api',
|
||||
timestamp: new Date().toISOString(),
|
||||
features: ['admin', 'auth', 'config', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export', 'user-import', 'ownership-costs', 'subscriptions', 'donations', 'ocr', 'email-ingestion']
|
||||
features: ['admin', 'auth', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export']
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,20 +132,10 @@ async function buildApp(): Promise<FastifyInstance> {
|
||||
await app.register(communityStationsRoutes, { prefix: '/api' });
|
||||
await app.register(maintenanceRoutes, { prefix: '/api' });
|
||||
await app.register(adminRoutes, { prefix: '/api' });
|
||||
await app.register(auditLogRoutes, { prefix: '/api' });
|
||||
await app.register(notificationsRoutes, { prefix: '/api' });
|
||||
await app.register(userProfileRoutes, { prefix: '/api' });
|
||||
await app.register(userPreferencesRoutes, { prefix: '/api' });
|
||||
await app.register(userExportRoutes, { prefix: '/api' });
|
||||
await app.register(userImportRoutes, { prefix: '/api' });
|
||||
await app.register(ownershipCostsRoutes, { prefix: '/api' });
|
||||
await app.register(subscriptionsRoutes, { prefix: '/api' });
|
||||
await app.register(donationsRoutes, { prefix: '/api' });
|
||||
await app.register(webhooksRoutes, { prefix: '/api' });
|
||||
await app.register(emailIngestionWebhookRoutes, { prefix: '/api' });
|
||||
await app.register(emailIngestionRoutes, { prefix: '/api' });
|
||||
await app.register(ocrRoutes, { prefix: '/api' });
|
||||
await app.register(configRoutes, { prefix: '/api' });
|
||||
|
||||
// 404 handler
|
||||
app.setNotFoundHandler(async (_request, reply) => {
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
| Directory | What | When to read |
|
||||
| --------- | ---- | ------------ |
|
||||
| `auth/` | Authentication utilities | JWT handling, user context |
|
||||
| `config/` | Configuration loading (env, database, redis) and feature tier gating (fuelLog.receiptScan, document.scanMaintenanceSchedule, vehicle.vinDecode) | Environment setup, connection pools, tier requirements |
|
||||
| `config/` | Configuration loading (env, database, redis) | Environment setup, connection pools |
|
||||
| `logging/` | Winston structured logging | Log configuration, debugging |
|
||||
| `middleware/` | Fastify middleware | Request processing, user extraction |
|
||||
| `plugins/` | Fastify plugins (auth, error, logging, tier guard) | Plugin registration, hooks, tier gating |
|
||||
| `plugins/` | Fastify plugins (auth, error, logging) | Plugin registration, hooks |
|
||||
| `scheduler/` | Job scheduling infrastructure | Scheduled tasks, cron jobs |
|
||||
| `storage/` | Storage abstraction and adapters | File storage, S3/filesystem |
|
||||
| `user-preferences/` | User preferences data and migrations | User settings storage |
|
||||
|
||||
@@ -41,6 +41,14 @@ const configSchema = z.object({
|
||||
audience: z.string(),
|
||||
}),
|
||||
|
||||
// External APIs configuration (optional)
|
||||
external: z.object({
|
||||
vpic: z.object({
|
||||
url: z.string(),
|
||||
timeout: z.string(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
|
||||
// Service configuration
|
||||
service: z.object({
|
||||
name: z.string(),
|
||||
@@ -118,10 +126,6 @@ const secretsSchema = z.object({
|
||||
auth0_management_client_secret: z.string(),
|
||||
google_maps_api_key: z.string(),
|
||||
resend_api_key: z.string(),
|
||||
resend_webhook_secret: z.string().optional(),
|
||||
// Stripe secrets (API keys only - price IDs are config, not secrets)
|
||||
stripe_secret_key: z.string(),
|
||||
stripe_webhook_secret: z.string(),
|
||||
});
|
||||
|
||||
type Config = z.infer<typeof configSchema>;
|
||||
@@ -136,14 +140,6 @@ export interface AppConfiguration {
|
||||
getRedisUrl(): string;
|
||||
getAuth0Config(): { domain: string; audience: string; clientSecret: string };
|
||||
getAuth0ManagementConfig(): { domain: string; clientId: string; clientSecret: string };
|
||||
getResendConfig(): {
|
||||
apiKey: string;
|
||||
webhookSecret: string | undefined;
|
||||
};
|
||||
getStripeConfig(): {
|
||||
secretKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
}
|
||||
|
||||
class ConfigurationLoader {
|
||||
@@ -182,9 +178,6 @@ class ConfigurationLoader {
|
||||
'auth0-management-client-secret',
|
||||
'google-maps-api-key',
|
||||
'resend-api-key',
|
||||
'resend-webhook-secret',
|
||||
'stripe-secret-key',
|
||||
'stripe-webhook-secret',
|
||||
];
|
||||
|
||||
for (const secretFile of secretFiles) {
|
||||
@@ -247,27 +240,10 @@ class ConfigurationLoader {
|
||||
clientSecret: secrets.auth0_management_client_secret,
|
||||
};
|
||||
},
|
||||
|
||||
getResendConfig() {
|
||||
return {
|
||||
apiKey: secrets.resend_api_key,
|
||||
webhookSecret: secrets.resend_webhook_secret,
|
||||
};
|
||||
},
|
||||
|
||||
getStripeConfig() {
|
||||
return {
|
||||
secretKey: secrets.stripe_secret_key,
|
||||
webhookSecret: secrets.stripe_webhook_secret,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Set Resend environment variables for EmailService and webhook verification
|
||||
// Set RESEND_API_KEY in environment for EmailService
|
||||
process.env['RESEND_API_KEY'] = secrets.resend_api_key;
|
||||
if (secrets.resend_webhook_secret) {
|
||||
process.env['RESEND_WEBHOOK_SECRET'] = secrets.resend_webhook_secret;
|
||||
}
|
||||
|
||||
logger.info('Configuration loaded successfully', {
|
||||
configSource: 'yaml',
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @ai-summary Configuration API routes
|
||||
* @ai-context Exposes feature tier configuration for frontend consumption
|
||||
*/
|
||||
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { getAllFeatureConfigs, TIER_LEVELS } from './feature-tiers';
|
||||
|
||||
export const configRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
// GET /api/config/feature-tiers - Get all feature tier configurations
|
||||
// Public endpoint - no auth required (config is not sensitive)
|
||||
fastify.get('/config/feature-tiers', async (_request, reply) => {
|
||||
return reply.code(200).send({
|
||||
tiers: TIER_LEVELS,
|
||||
features: getAllFeatureConfigs(),
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -2,15 +2,10 @@
|
||||
* @ai-summary PostgreSQL connection pool configuration
|
||||
* @ai-context Shared pool for all feature repositories
|
||||
*/
|
||||
import { Pool, types } from 'pg';
|
||||
import { Pool } from 'pg';
|
||||
import { logger } from '../logging/logger';
|
||||
import { appConfig } from './config-loader';
|
||||
|
||||
// Override DATE type parser to return plain YYYY-MM-DD strings instead of Date objects.
|
||||
// Default pg behavior creates Date objects at local midnight, which shift dates when
|
||||
// serialized to JSON via toISOString() (UTC conversion) for clients in other timezones.
|
||||
types.setTypeParser(1082, (val: string) => val);
|
||||
|
||||
export const pool = new Pool({
|
||||
connectionString: appConfig.getDatabaseUrl(),
|
||||
max: 10,
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/**
|
||||
* @ai-summary Feature tier configuration and utilities
|
||||
* @ai-context Defines feature-to-tier mapping for gating premium features
|
||||
*/
|
||||
|
||||
import { SubscriptionTier } from '../../features/user-profile/domain/user-profile.types';
|
||||
|
||||
// Tier hierarchy: higher number = higher access level
|
||||
export const TIER_LEVELS: Record<SubscriptionTier, number> = {
|
||||
free: 0,
|
||||
pro: 1,
|
||||
enterprise: 2,
|
||||
} as const;
|
||||
|
||||
// Feature configuration interface
|
||||
export interface FeatureConfig {
|
||||
minTier: SubscriptionTier;
|
||||
name: string;
|
||||
upgradePrompt: string;
|
||||
}
|
||||
|
||||
// Feature registry - add new gated features here
|
||||
export const FEATURE_TIERS: Record<string, FeatureConfig> = {
|
||||
'document.scanMaintenanceSchedule': {
|
||||
minTier: 'pro',
|
||||
name: 'Scan for Maintenance Schedule',
|
||||
upgradePrompt: 'Upgrade to Pro to automatically extract maintenance schedules from your vehicle manuals.',
|
||||
},
|
||||
'vehicle.vinDecode': {
|
||||
minTier: 'pro',
|
||||
name: 'VIN Decode',
|
||||
upgradePrompt: 'Upgrade to Pro to automatically decode VIN and populate vehicle details from the vehicle database.',
|
||||
},
|
||||
'fuelLog.receiptScan': {
|
||||
minTier: 'pro',
|
||||
name: 'Receipt Scan',
|
||||
upgradePrompt: 'Upgrade to Pro to scan fuel receipts and auto-fill your fuel log entries.',
|
||||
},
|
||||
'maintenance.receiptScan': {
|
||||
minTier: 'pro',
|
||||
name: 'Maintenance Receipt Scan',
|
||||
upgradePrompt: 'Upgrade to Pro to scan maintenance receipts and extract service details automatically.',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get numeric level for a subscription tier
|
||||
*/
|
||||
export function getTierLevel(tier: SubscriptionTier): number {
|
||||
return TIER_LEVELS[tier] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user tier can access a feature
|
||||
* Higher tiers inherit access to all lower tier features
|
||||
*/
|
||||
export function canAccessFeature(userTier: SubscriptionTier, featureKey: string): boolean {
|
||||
const feature = FEATURE_TIERS[featureKey];
|
||||
if (!feature) {
|
||||
// Unknown features are accessible by all (fail open for unlisted features)
|
||||
return true;
|
||||
}
|
||||
return getTierLevel(userTier) >= getTierLevel(feature.minTier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum required tier for a feature
|
||||
* Returns null if feature is not gated
|
||||
*/
|
||||
export function getRequiredTier(featureKey: string): SubscriptionTier | null {
|
||||
const feature = FEATURE_TIERS[featureKey];
|
||||
return feature?.minTier ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full feature configuration
|
||||
* Returns undefined if feature is not registered
|
||||
*/
|
||||
export function getFeatureConfig(featureKey: string): FeatureConfig | undefined {
|
||||
return FEATURE_TIERS[featureKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all feature configurations (for API endpoint)
|
||||
*/
|
||||
export function getAllFeatureConfigs(): Record<string, FeatureConfig> {
|
||||
return { ...FEATURE_TIERS };
|
||||
}
|
||||
|
||||
// Vehicle limits per tier
|
||||
// null indicates unlimited (enterprise tier)
|
||||
export const VEHICLE_LIMITS: Record<SubscriptionTier, number | null> = {
|
||||
free: 2,
|
||||
pro: 5,
|
||||
enterprise: null,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Vehicle limits vary by subscription tier and must be queryable
|
||||
* at runtime for both backend enforcement and frontend UI state.
|
||||
*
|
||||
* @param tier - User's subscription tier
|
||||
* @returns Maximum vehicles allowed, or null for unlimited (enterprise tier)
|
||||
*/
|
||||
export function getVehicleLimit(tier: SubscriptionTier): number | null {
|
||||
return VEHICLE_LIMITS[tier] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user can add another vehicle based on their tier and current count.
|
||||
*
|
||||
* @param tier - User's subscription tier
|
||||
* @param currentCount - Number of vehicles user currently has
|
||||
* @returns true if user can add another vehicle, false if at/over limit
|
||||
*/
|
||||
export function canAddVehicle(tier: SubscriptionTier, currentCount: number): boolean {
|
||||
const limit = getVehicleLimit(tier);
|
||||
// null limit means unlimited (enterprise)
|
||||
if (limit === null) {
|
||||
return true;
|
||||
}
|
||||
return currentCount < limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vehicle limit configuration with upgrade prompt.
|
||||
* Structure supports additional resource types in the future.
|
||||
*/
|
||||
export interface VehicleLimitConfig {
|
||||
limit: number | null;
|
||||
tier: SubscriptionTier;
|
||||
upgradePrompt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vehicle limit configuration with upgrade prompt for a tier.
|
||||
*
|
||||
* @param tier - User's subscription tier
|
||||
* @returns Configuration with limit and upgrade prompt
|
||||
*/
|
||||
export function getVehicleLimitConfig(tier: SubscriptionTier): VehicleLimitConfig {
|
||||
const limit = getVehicleLimit(tier);
|
||||
|
||||
const defaultPrompt = 'Upgrade to access additional vehicles.';
|
||||
|
||||
let upgradePrompt: string;
|
||||
if (tier === 'free') {
|
||||
upgradePrompt = 'Free tier is limited to 2 vehicles. Upgrade to Pro for up to 5 vehicles, or Enterprise for unlimited.';
|
||||
} else if (tier === 'pro') {
|
||||
upgradePrompt = 'Pro tier is limited to 5 vehicles. Upgrade to Enterprise for unlimited vehicles.';
|
||||
} else {
|
||||
upgradePrompt = defaultPrompt;
|
||||
}
|
||||
|
||||
return {
|
||||
limit,
|
||||
tier,
|
||||
upgradePrompt,
|
||||
};
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import {
|
||||
TIER_LEVELS,
|
||||
FEATURE_TIERS,
|
||||
VEHICLE_LIMITS,
|
||||
getTierLevel,
|
||||
canAccessFeature,
|
||||
getRequiredTier,
|
||||
getFeatureConfig,
|
||||
getAllFeatureConfigs,
|
||||
getVehicleLimit,
|
||||
canAddVehicle,
|
||||
getVehicleLimitConfig,
|
||||
} from '../feature-tiers';
|
||||
|
||||
describe('feature-tiers', () => {
|
||||
describe('TIER_LEVELS', () => {
|
||||
it('defines correct tier hierarchy', () => {
|
||||
expect(TIER_LEVELS.free).toBe(0);
|
||||
expect(TIER_LEVELS.pro).toBe(1);
|
||||
expect(TIER_LEVELS.enterprise).toBe(2);
|
||||
});
|
||||
|
||||
it('enterprise > pro > free', () => {
|
||||
expect(TIER_LEVELS.enterprise).toBeGreaterThan(TIER_LEVELS.pro);
|
||||
expect(TIER_LEVELS.pro).toBeGreaterThan(TIER_LEVELS.free);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FEATURE_TIERS', () => {
|
||||
it('includes scanMaintenanceSchedule feature', () => {
|
||||
const feature = FEATURE_TIERS['document.scanMaintenanceSchedule'];
|
||||
expect(feature).toBeDefined();
|
||||
expect(feature.minTier).toBe('pro');
|
||||
expect(feature.name).toBe('Scan for Maintenance Schedule');
|
||||
expect(feature.upgradePrompt).toBeTruthy();
|
||||
});
|
||||
|
||||
it('includes fuelLog.receiptScan feature', () => {
|
||||
const feature = FEATURE_TIERS['fuelLog.receiptScan'];
|
||||
expect(feature).toBeDefined();
|
||||
expect(feature.minTier).toBe('pro');
|
||||
expect(feature.name).toBe('Receipt Scan');
|
||||
expect(feature.upgradePrompt).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAccessFeature - fuelLog.receiptScan', () => {
|
||||
const featureKey = 'fuelLog.receiptScan';
|
||||
|
||||
it('denies access for free tier user', () => {
|
||||
expect(canAccessFeature('free', featureKey)).toBe(false);
|
||||
});
|
||||
|
||||
it('allows access for pro tier user', () => {
|
||||
expect(canAccessFeature('pro', featureKey)).toBe(true);
|
||||
});
|
||||
|
||||
it('allows access for enterprise tier user (inherits pro)', () => {
|
||||
expect(canAccessFeature('enterprise', featureKey)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTierLevel', () => {
|
||||
it('returns correct level for each tier', () => {
|
||||
expect(getTierLevel('free')).toBe(0);
|
||||
expect(getTierLevel('pro')).toBe(1);
|
||||
expect(getTierLevel('enterprise')).toBe(2);
|
||||
});
|
||||
|
||||
it('returns 0 for unknown tier', () => {
|
||||
expect(getTierLevel('unknown' as any)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAccessFeature', () => {
|
||||
const featureKey = 'document.scanMaintenanceSchedule';
|
||||
|
||||
it('denies access for free tier to pro feature', () => {
|
||||
expect(canAccessFeature('free', featureKey)).toBe(false);
|
||||
});
|
||||
|
||||
it('allows access for pro tier to pro feature', () => {
|
||||
expect(canAccessFeature('pro', featureKey)).toBe(true);
|
||||
});
|
||||
|
||||
it('allows access for enterprise tier to pro feature (inheritance)', () => {
|
||||
expect(canAccessFeature('enterprise', featureKey)).toBe(true);
|
||||
});
|
||||
|
||||
it('allows access for unknown feature (fail open)', () => {
|
||||
expect(canAccessFeature('free', 'unknown.feature')).toBe(true);
|
||||
expect(canAccessFeature('pro', 'unknown.feature')).toBe(true);
|
||||
expect(canAccessFeature('enterprise', 'unknown.feature')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequiredTier', () => {
|
||||
it('returns required tier for known feature', () => {
|
||||
expect(getRequiredTier('document.scanMaintenanceSchedule')).toBe('pro');
|
||||
});
|
||||
|
||||
it('returns null for unknown feature', () => {
|
||||
expect(getRequiredTier('unknown.feature')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeatureConfig', () => {
|
||||
it('returns full config for known feature', () => {
|
||||
const config = getFeatureConfig('document.scanMaintenanceSchedule');
|
||||
expect(config).toEqual({
|
||||
minTier: 'pro',
|
||||
name: 'Scan for Maintenance Schedule',
|
||||
upgradePrompt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for unknown feature', () => {
|
||||
expect(getFeatureConfig('unknown.feature')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllFeatureConfigs', () => {
|
||||
it('returns copy of all feature configs', () => {
|
||||
const configs = getAllFeatureConfigs();
|
||||
expect(configs['document.scanMaintenanceSchedule']).toBeDefined();
|
||||
|
||||
// Verify it's a copy, not the original
|
||||
configs['test'] = { minTier: 'free', name: 'test', upgradePrompt: '' };
|
||||
expect(FEATURE_TIERS['test' as keyof typeof FEATURE_TIERS]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('VEHICLE_LIMITS', () => {
|
||||
it('defines correct limits for each tier', () => {
|
||||
expect(VEHICLE_LIMITS.free).toBe(2);
|
||||
expect(VEHICLE_LIMITS.pro).toBe(5);
|
||||
expect(VEHICLE_LIMITS.enterprise).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVehicleLimit', () => {
|
||||
it('returns 2 for free tier', () => {
|
||||
expect(getVehicleLimit('free')).toBe(2);
|
||||
});
|
||||
|
||||
it('returns 5 for pro tier', () => {
|
||||
expect(getVehicleLimit('pro')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns null for enterprise tier (unlimited)', () => {
|
||||
expect(getVehicleLimit('enterprise')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAddVehicle', () => {
|
||||
describe('free tier (limit 2)', () => {
|
||||
it('returns true when below limit', () => {
|
||||
expect(canAddVehicle('free', 0)).toBe(true);
|
||||
expect(canAddVehicle('free', 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when at limit', () => {
|
||||
expect(canAddVehicle('free', 2)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when over limit', () => {
|
||||
expect(canAddVehicle('free', 3)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pro tier (limit 5)', () => {
|
||||
it('returns true when below limit', () => {
|
||||
expect(canAddVehicle('pro', 0)).toBe(true);
|
||||
expect(canAddVehicle('pro', 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when at limit', () => {
|
||||
expect(canAddVehicle('pro', 5)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when over limit', () => {
|
||||
expect(canAddVehicle('pro', 6)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enterprise tier (unlimited)', () => {
|
||||
it('always returns true regardless of count', () => {
|
||||
expect(canAddVehicle('enterprise', 0)).toBe(true);
|
||||
expect(canAddVehicle('enterprise', 100)).toBe(true);
|
||||
expect(canAddVehicle('enterprise', 999999)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVehicleLimitConfig', () => {
|
||||
it('returns correct config for free tier', () => {
|
||||
const config = getVehicleLimitConfig('free');
|
||||
expect(config.limit).toBe(2);
|
||||
expect(config.tier).toBe('free');
|
||||
expect(config.upgradePrompt).toContain('Free tier is limited to 2 vehicles');
|
||||
expect(config.upgradePrompt).toContain('Pro');
|
||||
expect(config.upgradePrompt).toContain('Enterprise');
|
||||
});
|
||||
|
||||
it('returns correct config for pro tier', () => {
|
||||
const config = getVehicleLimitConfig('pro');
|
||||
expect(config.limit).toBe(5);
|
||||
expect(config.tier).toBe('pro');
|
||||
expect(config.upgradePrompt).toContain('Pro tier is limited to 5 vehicles');
|
||||
expect(config.upgradePrompt).toContain('Enterprise');
|
||||
});
|
||||
|
||||
it('returns correct config for enterprise tier', () => {
|
||||
const config = getVehicleLimitConfig('enterprise');
|
||||
expect(config.limit).toBeNull();
|
||||
expect(config.tier).toBe('enterprise');
|
||||
expect(config.upgradePrompt).toBeTruthy();
|
||||
});
|
||||
|
||||
it('provides default upgradePrompt fallback', () => {
|
||||
const config = getVehicleLimitConfig('enterprise');
|
||||
expect(config.upgradePrompt).toBe('Upgrade to access additional vehicles.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,404 +0,0 @@
|
||||
-- Migration: 001_migrate_user_id_to_uuid.sql
|
||||
-- Feature: identity-migration (cross-cutting)
|
||||
-- Description: Migrate all user identity columns from VARCHAR(255) storing auth0_sub
|
||||
-- to UUID referencing user_profiles.id. Admin tables restructured with UUID PKs.
|
||||
-- Requires: All feature tables must exist (runs last in MIGRATION_ORDER)
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 1: Add new UUID columns alongside existing VARCHAR columns
|
||||
-- ============================================================================
|
||||
|
||||
-- 1a. Feature tables (17 tables with user_id VARCHAR)
|
||||
ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE fuel_logs ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE maintenance_records ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE maintenance_schedules ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE notification_logs ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE user_notifications ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE user_preferences ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE saved_stations ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE ownership_costs ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE email_ingestion_queue ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE pending_vehicle_associations ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE donations ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE tier_vehicle_selections ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
ALTER TABLE terms_agreements ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
|
||||
-- 1b. Special user-reference columns (submitted_by/reported_by store auth0_sub)
|
||||
ALTER TABLE community_stations ADD COLUMN IF NOT EXISTS submitted_by_uuid UUID;
|
||||
ALTER TABLE station_removal_reports ADD COLUMN IF NOT EXISTS reported_by_uuid UUID;
|
||||
|
||||
-- 1c. Admin table: add id UUID and user_profile_id UUID
|
||||
ALTER TABLE admin_users ADD COLUMN IF NOT EXISTS id UUID;
|
||||
ALTER TABLE admin_users ADD COLUMN IF NOT EXISTS user_profile_id UUID;
|
||||
|
||||
-- 1d. Admin-referencing columns: add UUID equivalents
|
||||
ALTER TABLE admin_audit_logs ADD COLUMN IF NOT EXISTS actor_admin_uuid UUID;
|
||||
ALTER TABLE admin_audit_logs ADD COLUMN IF NOT EXISTS target_admin_uuid UUID;
|
||||
ALTER TABLE admin_users ADD COLUMN IF NOT EXISTS created_by_uuid UUID;
|
||||
ALTER TABLE community_stations ADD COLUMN IF NOT EXISTS reviewed_by_uuid UUID;
|
||||
ALTER TABLE backup_history ADD COLUMN IF NOT EXISTS created_by_uuid UUID;
|
||||
ALTER TABLE platform_change_log ADD COLUMN IF NOT EXISTS changed_by_uuid UUID;
|
||||
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS deactivated_by_uuid UUID;
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 2: Backfill UUID values from user_profiles join
|
||||
-- ============================================================================
|
||||
|
||||
-- 2a. Feature tables: map user_id (auth0_sub) -> user_profiles.id (UUID)
|
||||
UPDATE vehicles SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE vehicles.user_id = up.auth0_sub AND vehicles.user_profile_id IS NULL;
|
||||
|
||||
UPDATE fuel_logs SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE fuel_logs.user_id = up.auth0_sub AND fuel_logs.user_profile_id IS NULL;
|
||||
|
||||
UPDATE maintenance_records SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE maintenance_records.user_id = up.auth0_sub AND maintenance_records.user_profile_id IS NULL;
|
||||
|
||||
UPDATE maintenance_schedules SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE maintenance_schedules.user_id = up.auth0_sub AND maintenance_schedules.user_profile_id IS NULL;
|
||||
|
||||
UPDATE documents SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE documents.user_id = up.auth0_sub AND documents.user_profile_id IS NULL;
|
||||
|
||||
UPDATE notification_logs SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE notification_logs.user_id = up.auth0_sub AND notification_logs.user_profile_id IS NULL;
|
||||
|
||||
UPDATE user_notifications SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE user_notifications.user_id = up.auth0_sub AND user_notifications.user_profile_id IS NULL;
|
||||
|
||||
UPDATE user_preferences SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE user_preferences.user_id = up.auth0_sub AND user_preferences.user_profile_id IS NULL;
|
||||
|
||||
-- 2a-fix. user_preferences has rows where user_id already contains user_profiles.id (UUID)
|
||||
-- instead of auth0_sub. Match these directly by casting to UUID.
|
||||
UPDATE user_preferences SET user_profile_id = up.id
|
||||
FROM user_profiles up
|
||||
WHERE user_preferences.user_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
AND user_preferences.user_id::uuid = up.id
|
||||
AND user_preferences.user_profile_id IS NULL;
|
||||
|
||||
-- Delete truly orphaned user_preferences (UUID user_id with no matching user_profile)
|
||||
DELETE FROM user_preferences
|
||||
WHERE user_profile_id IS NULL
|
||||
AND user_id ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
AND NOT EXISTS (SELECT 1 FROM user_profiles WHERE id = user_preferences.user_id::uuid);
|
||||
|
||||
-- Deduplicate user_preferences: same user may have both an auth0_sub row and
|
||||
-- a UUID row, both now mapping to the same user_profile_id. Keep the newest.
|
||||
DELETE FROM user_preferences a
|
||||
USING user_preferences b
|
||||
WHERE a.user_profile_id = b.user_profile_id
|
||||
AND a.user_profile_id IS NOT NULL
|
||||
AND (a.updated_at < b.updated_at OR (a.updated_at = b.updated_at AND a.id < b.id));
|
||||
|
||||
UPDATE saved_stations SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE saved_stations.user_id = up.auth0_sub AND saved_stations.user_profile_id IS NULL;
|
||||
|
||||
UPDATE audit_logs SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE audit_logs.user_id = up.auth0_sub AND audit_logs.user_profile_id IS NULL;
|
||||
|
||||
UPDATE ownership_costs SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE ownership_costs.user_id = up.auth0_sub AND ownership_costs.user_profile_id IS NULL;
|
||||
|
||||
UPDATE email_ingestion_queue SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE email_ingestion_queue.user_id = up.auth0_sub AND email_ingestion_queue.user_profile_id IS NULL;
|
||||
|
||||
UPDATE pending_vehicle_associations SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE pending_vehicle_associations.user_id = up.auth0_sub AND pending_vehicle_associations.user_profile_id IS NULL;
|
||||
|
||||
UPDATE subscriptions SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE subscriptions.user_id = up.auth0_sub AND subscriptions.user_profile_id IS NULL;
|
||||
|
||||
UPDATE donations SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE donations.user_id = up.auth0_sub AND donations.user_profile_id IS NULL;
|
||||
|
||||
UPDATE tier_vehicle_selections SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE tier_vehicle_selections.user_id = up.auth0_sub AND tier_vehicle_selections.user_profile_id IS NULL;
|
||||
|
||||
UPDATE terms_agreements SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE terms_agreements.user_id = up.auth0_sub AND terms_agreements.user_profile_id IS NULL;
|
||||
|
||||
-- 2b. Special user columns
|
||||
UPDATE community_stations SET submitted_by_uuid = up.id
|
||||
FROM user_profiles up WHERE community_stations.submitted_by = up.auth0_sub AND community_stations.submitted_by_uuid IS NULL;
|
||||
|
||||
UPDATE station_removal_reports SET reported_by_uuid = up.id
|
||||
FROM user_profiles up WHERE station_removal_reports.reported_by = up.auth0_sub AND station_removal_reports.reported_by_uuid IS NULL;
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 3: Admin-specific transformations
|
||||
-- ============================================================================
|
||||
|
||||
-- 3a. Create user_profiles entries for any admin_users that lack one
|
||||
INSERT INTO user_profiles (auth0_sub, email)
|
||||
SELECT au.auth0_sub, au.email
|
||||
FROM admin_users au
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_profiles up WHERE up.auth0_sub = au.auth0_sub
|
||||
)
|
||||
ON CONFLICT (auth0_sub) DO NOTHING;
|
||||
|
||||
-- 3b. Populate admin_users.id (DEFAULT doesn't auto-fill on ALTER ADD COLUMN for existing rows)
|
||||
UPDATE admin_users SET id = uuid_generate_v4() WHERE id IS NULL;
|
||||
|
||||
-- 3c. Backfill admin_users.user_profile_id from user_profiles join
|
||||
UPDATE admin_users SET user_profile_id = up.id
|
||||
FROM user_profiles up WHERE admin_users.auth0_sub = up.auth0_sub AND admin_users.user_profile_id IS NULL;
|
||||
|
||||
-- 3d. Backfill admin-referencing columns: map auth0_sub -> admin_users.id UUID
|
||||
UPDATE admin_audit_logs SET actor_admin_uuid = au.id
|
||||
FROM admin_users au WHERE admin_audit_logs.actor_admin_id = au.auth0_sub AND admin_audit_logs.actor_admin_uuid IS NULL;
|
||||
|
||||
UPDATE admin_audit_logs SET target_admin_uuid = au.id
|
||||
FROM admin_users au WHERE admin_audit_logs.target_admin_id = au.auth0_sub AND admin_audit_logs.target_admin_uuid IS NULL;
|
||||
|
||||
UPDATE admin_users au SET created_by_uuid = creator.id
|
||||
FROM admin_users creator WHERE au.created_by = creator.auth0_sub AND au.created_by_uuid IS NULL;
|
||||
|
||||
UPDATE community_stations SET reviewed_by_uuid = au.id
|
||||
FROM admin_users au WHERE community_stations.reviewed_by = au.auth0_sub AND community_stations.reviewed_by_uuid IS NULL;
|
||||
|
||||
UPDATE backup_history SET created_by_uuid = au.id
|
||||
FROM admin_users au WHERE backup_history.created_by = au.auth0_sub AND backup_history.created_by_uuid IS NULL;
|
||||
|
||||
UPDATE platform_change_log SET changed_by_uuid = au.id
|
||||
FROM admin_users au WHERE platform_change_log.changed_by = au.auth0_sub AND platform_change_log.changed_by_uuid IS NULL;
|
||||
|
||||
UPDATE user_profiles SET deactivated_by_uuid = au.id
|
||||
FROM admin_users au WHERE user_profiles.deactivated_by = au.auth0_sub AND user_profiles.deactivated_by_uuid IS NULL;
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 4: Add constraints
|
||||
-- ============================================================================
|
||||
|
||||
-- 4a. Set NOT NULL on feature table UUID columns (audit_logs stays nullable)
|
||||
ALTER TABLE vehicles ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE fuel_logs ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE maintenance_records ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE maintenance_schedules ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE documents ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE notification_logs ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE user_notifications ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE user_preferences ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE saved_stations ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
-- audit_logs.user_profile_id stays NULLABLE (system actions have no user)
|
||||
ALTER TABLE ownership_costs ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE email_ingestion_queue ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE pending_vehicle_associations ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE subscriptions ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE donations ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE tier_vehicle_selections ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE terms_agreements ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE community_stations ALTER COLUMN submitted_by_uuid SET NOT NULL;
|
||||
ALTER TABLE station_removal_reports ALTER COLUMN reported_by_uuid SET NOT NULL;
|
||||
|
||||
-- 4b. Admin table NOT NULL constraints
|
||||
ALTER TABLE admin_users ALTER COLUMN id SET NOT NULL;
|
||||
ALTER TABLE admin_users ALTER COLUMN user_profile_id SET NOT NULL;
|
||||
ALTER TABLE admin_audit_logs ALTER COLUMN actor_admin_uuid SET NOT NULL;
|
||||
-- target_admin_uuid stays nullable (some actions have no target)
|
||||
-- created_by_uuid stays nullable (bootstrap admin may not have a creator)
|
||||
ALTER TABLE platform_change_log ALTER COLUMN changed_by_uuid SET NOT NULL;
|
||||
|
||||
-- 4c. Admin table PK transformation
|
||||
ALTER TABLE admin_users DROP CONSTRAINT admin_users_pkey;
|
||||
ALTER TABLE admin_users ADD PRIMARY KEY (id);
|
||||
|
||||
-- 4d. Add FK constraints to user_profiles(id) with ON DELETE CASCADE
|
||||
ALTER TABLE vehicles ADD CONSTRAINT fk_vehicles_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE fuel_logs ADD CONSTRAINT fk_fuel_logs_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE maintenance_records ADD CONSTRAINT fk_maintenance_records_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE maintenance_schedules ADD CONSTRAINT fk_maintenance_schedules_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE documents ADD CONSTRAINT fk_documents_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE notification_logs ADD CONSTRAINT fk_notification_logs_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE user_notifications ADD CONSTRAINT fk_user_notifications_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE user_preferences ADD CONSTRAINT fk_user_preferences_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE saved_stations ADD CONSTRAINT fk_saved_stations_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE audit_logs ADD CONSTRAINT fk_audit_logs_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE ownership_costs ADD CONSTRAINT fk_ownership_costs_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE email_ingestion_queue ADD CONSTRAINT fk_email_ingestion_queue_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE pending_vehicle_associations ADD CONSTRAINT fk_pending_vehicle_assoc_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE subscriptions ADD CONSTRAINT fk_subscriptions_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE donations ADD CONSTRAINT fk_donations_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE tier_vehicle_selections ADD CONSTRAINT fk_tier_vehicle_selections_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE terms_agreements ADD CONSTRAINT fk_terms_agreements_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE community_stations ADD CONSTRAINT fk_community_stations_submitted_by
|
||||
FOREIGN KEY (submitted_by_uuid) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
ALTER TABLE station_removal_reports ADD CONSTRAINT fk_station_removal_reports_reported_by
|
||||
FOREIGN KEY (reported_by_uuid) REFERENCES user_profiles(id) ON DELETE CASCADE;
|
||||
|
||||
-- 4e. Admin FK constraints
|
||||
ALTER TABLE admin_users ADD CONSTRAINT fk_admin_users_user_profile_id
|
||||
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id);
|
||||
ALTER TABLE admin_users ADD CONSTRAINT uq_admin_users_user_profile_id
|
||||
UNIQUE (user_profile_id);
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 5: Drop old columns, rename new ones, recreate indexes
|
||||
-- ============================================================================
|
||||
|
||||
-- 5a. Drop old FK constraints on VARCHAR user_id columns
|
||||
ALTER TABLE subscriptions DROP CONSTRAINT IF EXISTS fk_subscriptions_user_id;
|
||||
ALTER TABLE donations DROP CONSTRAINT IF EXISTS fk_donations_user_id;
|
||||
ALTER TABLE tier_vehicle_selections DROP CONSTRAINT IF EXISTS fk_tier_vehicle_selections_user_id;
|
||||
|
||||
-- 5b. Drop old UNIQUE constraints involving VARCHAR columns
|
||||
ALTER TABLE vehicles DROP CONSTRAINT IF EXISTS unique_user_vin;
|
||||
ALTER TABLE saved_stations DROP CONSTRAINT IF EXISTS unique_user_station;
|
||||
ALTER TABLE user_preferences DROP CONSTRAINT IF EXISTS user_preferences_user_id_key;
|
||||
ALTER TABLE station_removal_reports DROP CONSTRAINT IF EXISTS unique_user_station_report;
|
||||
|
||||
-- 5c. Drop old indexes on VARCHAR columns
|
||||
DROP INDEX IF EXISTS idx_vehicles_user_id;
|
||||
DROP INDEX IF EXISTS idx_fuel_logs_user_id;
|
||||
DROP INDEX IF EXISTS idx_maintenance_records_user_id;
|
||||
DROP INDEX IF EXISTS idx_maintenance_schedules_user_id;
|
||||
DROP INDEX IF EXISTS idx_documents_user_id;
|
||||
DROP INDEX IF EXISTS idx_documents_user_vehicle;
|
||||
DROP INDEX IF EXISTS idx_notification_logs_user_id;
|
||||
DROP INDEX IF EXISTS idx_user_notifications_user_id;
|
||||
DROP INDEX IF EXISTS idx_user_notifications_unread;
|
||||
DROP INDEX IF EXISTS idx_user_preferences_user_id;
|
||||
DROP INDEX IF EXISTS idx_saved_stations_user_id;
|
||||
DROP INDEX IF EXISTS idx_audit_logs_user_created;
|
||||
DROP INDEX IF EXISTS idx_ownership_costs_user_id;
|
||||
DROP INDEX IF EXISTS idx_email_ingestion_queue_user_id;
|
||||
DROP INDEX IF EXISTS idx_pending_vehicle_assoc_user_id;
|
||||
DROP INDEX IF EXISTS idx_subscriptions_user_id;
|
||||
DROP INDEX IF EXISTS idx_donations_user_id;
|
||||
DROP INDEX IF EXISTS idx_tier_vehicle_selections_user_id;
|
||||
DROP INDEX IF EXISTS idx_terms_agreements_user_id;
|
||||
DROP INDEX IF EXISTS idx_community_stations_submitted_by;
|
||||
DROP INDEX IF EXISTS idx_removal_reports_reported_by;
|
||||
DROP INDEX IF EXISTS idx_admin_audit_logs_actor_id;
|
||||
DROP INDEX IF EXISTS idx_admin_audit_logs_target_id;
|
||||
DROP INDEX IF EXISTS idx_platform_change_log_changed_by;
|
||||
|
||||
-- 5d. Drop old VARCHAR user_id columns from feature tables
|
||||
ALTER TABLE vehicles DROP COLUMN user_id;
|
||||
ALTER TABLE fuel_logs DROP COLUMN user_id;
|
||||
ALTER TABLE maintenance_records DROP COLUMN user_id;
|
||||
ALTER TABLE maintenance_schedules DROP COLUMN user_id;
|
||||
ALTER TABLE documents DROP COLUMN user_id;
|
||||
ALTER TABLE notification_logs DROP COLUMN user_id;
|
||||
ALTER TABLE user_notifications DROP COLUMN user_id;
|
||||
ALTER TABLE user_preferences DROP COLUMN user_id;
|
||||
ALTER TABLE saved_stations DROP COLUMN user_id;
|
||||
ALTER TABLE audit_logs DROP COLUMN user_id;
|
||||
ALTER TABLE ownership_costs DROP COLUMN user_id;
|
||||
ALTER TABLE email_ingestion_queue DROP COLUMN user_id;
|
||||
ALTER TABLE pending_vehicle_associations DROP COLUMN user_id;
|
||||
ALTER TABLE subscriptions DROP COLUMN user_id;
|
||||
ALTER TABLE donations DROP COLUMN user_id;
|
||||
ALTER TABLE tier_vehicle_selections DROP COLUMN user_id;
|
||||
ALTER TABLE terms_agreements DROP COLUMN user_id;
|
||||
|
||||
-- 5e. Rename user_profile_id -> user_id in feature tables
|
||||
ALTER TABLE vehicles RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE fuel_logs RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE maintenance_records RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE maintenance_schedules RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE documents RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE notification_logs RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE user_notifications RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE user_preferences RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE saved_stations RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE audit_logs RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE ownership_costs RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE email_ingestion_queue RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE pending_vehicle_associations RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE subscriptions RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE donations RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE tier_vehicle_selections RENAME COLUMN user_profile_id TO user_id;
|
||||
ALTER TABLE terms_agreements RENAME COLUMN user_profile_id TO user_id;
|
||||
|
||||
-- 5f. Drop and rename special user columns
|
||||
ALTER TABLE community_stations DROP COLUMN submitted_by;
|
||||
ALTER TABLE community_stations RENAME COLUMN submitted_by_uuid TO submitted_by;
|
||||
ALTER TABLE station_removal_reports DROP COLUMN reported_by;
|
||||
ALTER TABLE station_removal_reports RENAME COLUMN reported_by_uuid TO reported_by;
|
||||
|
||||
-- 5g. Drop and rename admin-referencing columns
|
||||
ALTER TABLE admin_users DROP COLUMN auth0_sub;
|
||||
ALTER TABLE admin_users DROP COLUMN created_by;
|
||||
ALTER TABLE admin_users RENAME COLUMN created_by_uuid TO created_by;
|
||||
|
||||
ALTER TABLE admin_audit_logs DROP COLUMN actor_admin_id;
|
||||
ALTER TABLE admin_audit_logs DROP COLUMN target_admin_id;
|
||||
ALTER TABLE admin_audit_logs RENAME COLUMN actor_admin_uuid TO actor_admin_id;
|
||||
ALTER TABLE admin_audit_logs RENAME COLUMN target_admin_uuid TO target_admin_id;
|
||||
|
||||
ALTER TABLE community_stations DROP COLUMN reviewed_by;
|
||||
ALTER TABLE community_stations RENAME COLUMN reviewed_by_uuid TO reviewed_by;
|
||||
|
||||
ALTER TABLE backup_history DROP COLUMN created_by;
|
||||
ALTER TABLE backup_history RENAME COLUMN created_by_uuid TO created_by;
|
||||
|
||||
ALTER TABLE platform_change_log DROP COLUMN changed_by;
|
||||
ALTER TABLE platform_change_log RENAME COLUMN changed_by_uuid TO changed_by;
|
||||
|
||||
ALTER TABLE user_profiles DROP COLUMN deactivated_by;
|
||||
ALTER TABLE user_profiles RENAME COLUMN deactivated_by_uuid TO deactivated_by;
|
||||
|
||||
-- 5h. Recreate indexes on new UUID columns (feature tables)
|
||||
CREATE INDEX idx_vehicles_user_id ON vehicles(user_id);
|
||||
CREATE INDEX idx_fuel_logs_user_id ON fuel_logs(user_id);
|
||||
CREATE INDEX idx_maintenance_records_user_id ON maintenance_records(user_id);
|
||||
CREATE INDEX idx_maintenance_schedules_user_id ON maintenance_schedules(user_id);
|
||||
CREATE INDEX idx_documents_user_id ON documents(user_id);
|
||||
CREATE INDEX idx_documents_user_vehicle ON documents(user_id, vehicle_id);
|
||||
CREATE INDEX idx_notification_logs_user_id ON notification_logs(user_id);
|
||||
CREATE INDEX idx_user_notifications_user_id ON user_notifications(user_id);
|
||||
CREATE INDEX idx_user_notifications_unread ON user_notifications(user_id, created_at DESC) WHERE is_read = false;
|
||||
CREATE INDEX idx_user_preferences_user_id ON user_preferences(user_id);
|
||||
CREATE INDEX idx_saved_stations_user_id ON saved_stations(user_id);
|
||||
CREATE INDEX idx_audit_logs_user_created ON audit_logs(user_id, created_at DESC);
|
||||
CREATE INDEX idx_ownership_costs_user_id ON ownership_costs(user_id);
|
||||
CREATE INDEX idx_email_ingestion_queue_user_id ON email_ingestion_queue(user_id);
|
||||
CREATE INDEX idx_pending_vehicle_assoc_user_id ON pending_vehicle_associations(user_id);
|
||||
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
|
||||
CREATE INDEX idx_donations_user_id ON donations(user_id);
|
||||
CREATE INDEX idx_tier_vehicle_selections_user_id ON tier_vehicle_selections(user_id);
|
||||
CREATE INDEX idx_terms_agreements_user_id ON terms_agreements(user_id);
|
||||
|
||||
-- 5i. Recreate indexes on special columns
|
||||
CREATE INDEX idx_community_stations_submitted_by ON community_stations(submitted_by);
|
||||
CREATE INDEX idx_removal_reports_reported_by ON station_removal_reports(reported_by);
|
||||
CREATE INDEX idx_admin_audit_logs_actor_id ON admin_audit_logs(actor_admin_id);
|
||||
CREATE INDEX idx_admin_audit_logs_target_id ON admin_audit_logs(target_admin_id);
|
||||
CREATE INDEX idx_platform_change_log_changed_by ON platform_change_log(changed_by);
|
||||
|
||||
-- 5j. Recreate UNIQUE constraints on new UUID columns
|
||||
ALTER TABLE vehicles ADD CONSTRAINT unique_user_vin UNIQUE(user_id, vin);
|
||||
ALTER TABLE saved_stations ADD CONSTRAINT unique_user_station UNIQUE(user_id, place_id);
|
||||
ALTER TABLE user_preferences ADD CONSTRAINT user_preferences_user_id_key UNIQUE(user_id);
|
||||
ALTER TABLE station_removal_reports ADD CONSTRAINT unique_user_station_report UNIQUE(station_id, reported_by);
|
||||
|
||||
COMMIT;
|
||||
@@ -1,42 +1,24 @@
|
||||
/**
|
||||
* @ai-summary Structured logging with Pino (Winston-compatible wrapper)
|
||||
* @ai-context All features use this for consistent logging. API maintains Winston compatibility.
|
||||
* @ai-summary Structured logging with Winston
|
||||
* @ai-context All features use this for consistent logging
|
||||
*/
|
||||
import pino from 'pino';
|
||||
import * as winston from 'winston';
|
||||
|
||||
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
const validLevels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
|
||||
|
||||
const rawLevel = (process.env.LOG_LEVEL?.toLowerCase() || 'info') as LogLevel;
|
||||
const level = validLevels.includes(rawLevel) ? rawLevel : 'info';
|
||||
|
||||
if (process.env.LOG_LEVEL && rawLevel !== level) {
|
||||
console.warn(`Invalid LOG_LEVEL "${process.env.LOG_LEVEL}", falling back to "info"`);
|
||||
}
|
||||
|
||||
const pinoLogger = pino({
|
||||
level,
|
||||
formatters: {
|
||||
level: (label) => ({ level: label }),
|
||||
export const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
),
|
||||
defaultMeta: {
|
||||
service: 'motovaultpro-backend',
|
||||
},
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.json(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Wrapper maintains logger.info(msg, meta) API for backward compatibility
|
||||
export const logger = {
|
||||
info: (msg: string, meta?: object) => pinoLogger.info(meta || {}, msg),
|
||||
warn: (msg: string, meta?: object) => pinoLogger.warn(meta || {}, msg),
|
||||
error: (msg: string, meta?: object) => pinoLogger.error(meta || {}, msg),
|
||||
debug: (msg: string, meta?: object) => pinoLogger.debug(meta || {}, msg),
|
||||
child: (bindings: object) => {
|
||||
const childPino = pinoLogger.child(bindings);
|
||||
return {
|
||||
info: (msg: string, meta?: object) => childPino.info(meta || {}, msg),
|
||||
warn: (msg: string, meta?: object) => childPino.warn(meta || {}, msg),
|
||||
error: (msg: string, meta?: object) => childPino.error(meta || {}, msg),
|
||||
debug: (msg: string, meta?: object) => childPino.debug(meta || {}, msg),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default logger;
|
||||
export default logger;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user