forgetrail 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTINUATION_PROMPT.md +42 -0
- package/INITIAL_PROMPT.md +214 -0
- package/LICENSE +201 -0
- package/README.md +198 -0
- package/TRACKING_SCHEMA.md +162 -0
- package/TRY_FORGETRAIL.md +78 -0
- package/WORKFLOW.md +921 -0
- package/content/AGENT_INTEGRATION_claude.md +38 -0
- package/content/AGENT_INTEGRATION_cursor.md +53 -0
- package/content/AGENT_INTEGRATION_generic.md +50 -0
- package/content/AGENT_INTEGRATION_grok.md +82 -0
- package/content/DEV_AUTOMATION_SCRIPTS.md +103 -0
- package/content/FORGETRAIL_LITE.md +1202 -0
- package/content/FORGETRAIL_LITE_UPDATES.md +13 -0
- package/content/FORGETRAIL_PROGRESS.md +26 -0
- package/content/GENESIS_SPEC_PROMPT.md +70 -0
- package/content/GENESIS_STUB.md +18 -0
- package/content/GREENFIELD_INTAKE.md +76 -0
- package/content/KICKOFF_WITHOUT_MCP.md +16 -0
- package/content/LITE_WORKFLOW_TRACKING.json +42 -0
- package/content/NEW_PROJECT_BOOTSTRAP.md +180 -0
- package/content/ONE_CLICK_DEV_SETUP.md +142 -0
- package/content/PLAN_MODE_PATTERNS.md +43 -0
- package/content/POCKETBASE_SCHEMA_SCRIPT.md +52 -0
- package/content/POST_BOOTSTRAP_USER_MESSAGE.md +51 -0
- package/content/SCAFFOLD_INSTALL.json +110 -0
- package/content/SESSION_RESUME_MCP.md +35 -0
- package/content/SYSTEM_HEALTH_CHECKS.md +131 -0
- package/content/USER_REPLY_FORMAT.md +41 -0
- package/content/cursor-rules/forgetrail-lessons-gate.mdc +30 -0
- package/content/cursor-rules/forgetrail-lessons-mcp.mdc +24 -0
- package/content/cursor-rules/forgetrail-no-trailer.mdc +49 -0
- package/content/cursor-rules/forgetrail-phase-status.mdc +61 -0
- package/content/cursor-rules/forgetrail-updates-log.mdc +30 -0
- package/content/cursor-rules/spec-completion.mdc +33 -0
- package/content/cursor-rules/specs-and-todo.mdc +26 -0
- package/content/cursor-rules/url-host-matching.mdc +31 -0
- package/content/cursor-rules/us-english.mdc +9 -0
- package/content/cursor-rules/user-facing-content.mdc +31 -0
- package/content/examples/GENESIS_SAMPLE_mars-habitat-roster.md +154 -0
- package/content/forgetrail-workspace-README.md +43 -0
- package/content/scripts/forgetrail-dev-launcher.mjs +208 -0
- package/content/scripts/forgetrail-env.mjs +51 -0
- package/content/scripts/setup-ollama.mjs +207 -0
- package/content/scripts/setup-pocketbase.mjs +133 -0
- package/content/scripts/test-ollama.mjs +71 -0
- package/content/scripts/test-pocketbase.mjs +110 -0
- package/content/skills/forgetrail/SKILL.md +92 -0
- package/docs/AUTOMATED_TESTING.md +269 -0
- package/docs/BLACK_HAT_REPORT.md +143 -0
- package/docs/BRAND_AND_PRODUCT.md +381 -0
- package/docs/BUGS.md +62 -0
- package/docs/BUSINESS_PLAN.md +256 -0
- package/docs/CODE_QUALITY.md +170 -0
- package/docs/CONTEXT_PROMPT.md +357 -0
- package/docs/DEPLOYMENT.md +181 -0
- package/docs/DESIGN_SYSTEM.md +414 -0
- package/docs/DEV_ESTIMATE.md +127 -0
- package/docs/FORGETRAIL_RENAME.md +193 -0
- package/docs/IDEAS.md +19 -0
- package/docs/MARKETING_GROWTH.md +301 -0
- package/docs/NAMING_EXPLORATION.md +406 -0
- package/docs/NPM.md +98 -0
- package/docs/PHASE_1_BRIEF.md +161 -0
- package/docs/README.md +75 -0
- package/docs/SPEC_FEATURE_TEMPLATE.md +155 -0
- package/docs/SPEC_UI_CHROME_NAV_TEMPLATE.md +48 -0
- package/docs/TECHNICAL_REFERENCE.md +836 -0
- package/docs/TEST_PLAN.md +467 -0
- package/docs/TODO.md +125 -0
- package/package.json +68 -0
- package/prompts/black-hat-audit.md +235 -0
- package/prompts/brand-copy-edit-pass.md +55 -0
- package/prompts/cialdini-marketing-audit.md +141 -0
- package/prompts/competitor-deep-dive.md +201 -0
- package/prompts/docs-alignment-audit.md +108 -0
- package/prompts/engineering-skill-library.md +75 -0
- package/prompts/landing-page-rewrite.md +63 -0
- package/prompts/microcopy-centralization.md +128 -0
- package/prompts/panel-usability-audit.md +197 -0
- package/prompts/personal-beta-outreach.md +195 -0
- package/prompts/pre-launch-audit.md +148 -0
- package/prompts/product-feedback-to-spec.md +76 -0
- package/prompts/propagate-to-forgetrail.md +315 -0
- package/prompts/user-facing-content-sync-audit.md +200 -0
- package/prompts/ux-cohesion-audit.md +165 -0
- package/scripts/ensure-lease.mjs +23 -0
- package/scripts/forgetrail-cli.mjs +85 -0
- package/scripts/install-forgetrail-lite.mjs +11 -0
- package/scripts/install-forgetrail.mjs +11 -0
- package/scripts/install-lib.mjs +115 -0
- package/scripts/install.mjs +163 -0
- package/scripts/link-global.mjs +100 -0
- package/scripts/mcp-lib.mjs +177 -0
- package/scripts/mcp-status.mjs +155 -0
- package/scripts/publish-gate.mjs +110 -0
- package/scripts/rename-to-forgetrail.mjs +273 -0
- package/workflow_tracking.json +124 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
# [App Name] - Technical Reference
|
|
2
|
+
|
|
3
|
+
_Comprehensive documentation of how each feature works, data models, API routes, and configuration. This is the "how does it actually work" document._
|
|
4
|
+
|
|
5
|
+
_For brand positioning and product copy, see [BRAND_AND_PRODUCT.md](BRAND_AND_PRODUCT.md). For business model, see [BUSINESS_PLAN.md](BUSINESS_PLAN.md)._
|
|
6
|
+
|
|
7
|
+
_Instructions: Start populating this during Phase 4 (Feature Iteration) and keep it updated as features are built. Organize by functional area, not by file. Each section should answer: "What does this feature do, how does it work technically, and what are the edge cases?"_
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Architecture Overview
|
|
12
|
+
|
|
13
|
+
### System Design
|
|
14
|
+
|
|
15
|
+
[High-level description of how the major components interact. Include a simple diagram if helpful.]
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[User] β [Frontend (SvelteKit)] β [API Routes] β [Backend Services] β [Database]
|
|
19
|
+
β [External APIs (LLM, etc.)]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Design Principles
|
|
23
|
+
|
|
24
|
+
>π§ **Guidance:** These should mirror your BRAND_AND_PRODUCT.md pillars but from a technical perspective.
|
|
25
|
+
>
|
|
26
|
+
> π **Example:**
|
|
27
|
+
> - Human-in-the-loop: Every AI feature prepares, drafts, suggests, then returns control. No auto-submission.
|
|
28
|
+
> - Prep-before-write: Intersections β Selling Points β Materials. This sequence is enforced in the UI flow.
|
|
29
|
+
> - Progressive disclosure: Basic features visible by default, advanced features revealed through user action.
|
|
30
|
+
|
|
31
|
+
- **[Principle]:** [Technical manifestation]
|
|
32
|
+
|
|
33
|
+
## Data Model
|
|
34
|
+
|
|
35
|
+
### Collections/Tables
|
|
36
|
+
|
|
37
|
+
| Collection | Purpose | Key Fields |
|
|
38
|
+
| ---------- | ---------------- | ------------------ |
|
|
39
|
+
| [name] | [what it stores] | [field: type, ...] |
|
|
40
|
+
|
|
41
|
+
### Relationships
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
[Collection A] 1βββ€ [Collection B]
|
|
45
|
+
βββ [field_name]
|
|
46
|
+
|
|
47
|
+
[Collection C] β€βββ€ [Collection D]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Schema Notes
|
|
51
|
+
|
|
52
|
+
> π§ **Guidance:** Document any non-obvious schema decisions:
|
|
53
|
+
> - Field size limits (especially for fields storing LLM output)
|
|
54
|
+
> - Enum values and what they mean
|
|
55
|
+
> - Fields that are computed vs. stored
|
|
56
|
+
> - Migration notes if schema has changed
|
|
57
|
+
|
|
58
|
+
> π‘ **Lesson learned:** **Mapper inputs should reflect what PocketBase returns.** Functions that turn a raw collection row into a typed domain object (`[UserConfig]`, `[Job]`, etc.) should accept the SDK record type (commonly `RecordModel`) at the boundary. Casting with `as RecordModel` (or passing through from `getOne` / `getList` items) preserves intent; `as any` only silences the compiler. Pair with **`catch (e: unknown)`** and narrowers in routes that call mappers so failures donβt devolve into untyped throws.
|
|
59
|
+
|
|
60
|
+
> π‘ **Lesson learned:** **List endpoints that enrich each parent row with related-collection reads must respect the JS SDKβs request lifecycle.** The PocketBase JavaScript client auto-cancels overlapping in-flight requests to the same collection on the same client instance. A handler that runs `Promise.all(parents.map((p) => pb.collection('[child]').getList(..., { filter: β¦ p.id β¦ }) ))` can abort every call but one; catch blocks then return empty related arrays so the API responds 200 but omits nested data after reload. Prefer **`requestKey: null`** (or distinct keys per item) on those `getList` calls, serialize the related fetches, or use separate client instances per concurrent branch. Symptom: rows exist in the database but the list route always shows empty children.
|
|
61
|
+
|
|
62
|
+
### Deletion and data lifecycle
|
|
63
|
+
|
|
64
|
+
Document **what actually happens** when users remove data β not only the happy-path UI copy. Support and product need the same picture engineering uses.
|
|
65
|
+
|
|
66
|
+
> π‘ **Lesson learned:** **Hard delete vs soft delete should be explicit per entity.** Note which collections rely on **cascade delete** (schema + setup script), which use a **`deleted_at` (or equivalent) tombstone**, and which are **hard-deleted only**. If forks/copies can outlive a βparentβ record, say so β children may keep a pointer to a removed id by design; list/detail routes should tolerate missing parents for labels, not 500s.
|
|
67
|
+
|
|
68
|
+
> π‘ **Lesson learned:** **Orphan and stale-pointer risks belong in this doc, not only in a spec.** Call out relations that are *not* cascade-deleted (e.g. person vs connections), optional analytics rows tied to deleted parents, and file blobs β and what you verified in staging. That sets expectations: recovery is often operational (backup/restore), not an in-app βundo,β unless you ship trash/restore.
|
|
69
|
+
|
|
70
|
+
> π‘ **Lesson learned:** **Admin off-boarding: archive vs gated erase.** For **operator/admin** accounts, prefer **archive or deactivate** (revoke sessions, remove from admin lists, retain audit history) over hard-delete unless policy requires erasure. If you offer **hard-delete or PII wipe**, gate it (confirm text, elevated role, or break-glass) and document **cascade** vs **orphan** behavior so support knows what disappears from dashboards, billing hooks, and exports.
|
|
71
|
+
|
|
72
|
+
> π§ **Guidance:** When you add user-visible **trash** or retention later, this section becomes the checklist for filters (`deleted_at` empty vs set), exports, and purge jobs β keep it updated as the source of truth.
|
|
73
|
+
|
|
74
|
+
## User-facing copy (optional β adopt when copy volume grows)
|
|
75
|
+
|
|
76
|
+
> π§ **Guidance:** Centralize product strings so landing, help, tours, and in-app UI stay aligned. ForgeTrail describes the **pattern**; each app implements modules and audit scripts under its own `$lib/content/` (or equivalent).
|
|
77
|
+
|
|
78
|
+
### Three layers
|
|
79
|
+
|
|
80
|
+
| Layer | Typical module | Owns |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| Feature labels | `[productLabels].ts` | Canonical feature names for marketing, help, tours, nav |
|
|
83
|
+
| Cross-cutting UI | `[microcopy].ts` | Save failures, quota errors, shared toasts, connectivity banners |
|
|
84
|
+
| Surface modules | `[panelName]Copy.ts` | Panel intros, modals, wizard steps, section blurbs |
|
|
85
|
+
|
|
86
|
+
**Inline exceptions:** `Cancel`, `Save`, `Close` unless repeated 3+ times. **Legal pages:** one markdown file per page (`terms.md`, `privacy.md`) β not sharded into copy constants.
|
|
87
|
+
|
|
88
|
+
### Export and audits
|
|
89
|
+
|
|
90
|
+
| Script / prompt | Purpose |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `pnpm export:copy` (or app equivalent) | Regenerate **prose** + **terms** catalogs for writers (`docs/internal/USER_FACING_COPY.*.md`) |
|
|
93
|
+
| `pnpm audit:copy` | Duplicate prose inside the export inventory |
|
|
94
|
+
| `pnpm audit:inline-copy` | AST scan of `.svelte`; backlog = UI prose **not** in export |
|
|
95
|
+
| `prompts/user-facing-content-sync-audit.md` | Feature discoverability across landing, help, tours |
|
|
96
|
+
| `prompts/microcopy-centralization.md` | Phased migration checklist and duplication policy |
|
|
97
|
+
|
|
98
|
+
**Pre-release target:** inline audit at **0 UI prose not in export**; sync audit passes for shipped features.
|
|
99
|
+
|
|
100
|
+
**Cursor rules:** `.cursor/rules/user-facing-content.mdc` (no spec paths in UI), `.cursor/rules/us-english.mdc` (US English default).
|
|
101
|
+
|
|
102
|
+
## Feature Documentation
|
|
103
|
+
|
|
104
|
+
_One section per major feature area. Each section covers: what it does, how it works technically, API routes, and edge cases._
|
|
105
|
+
|
|
106
|
+
### [Feature Area 1]
|
|
107
|
+
|
|
108
|
+
**What it does:** [User-facing description]
|
|
109
|
+
|
|
110
|
+
**How it works:**
|
|
111
|
+
[Technical description of the flow. Include which files/services are involved.]
|
|
112
|
+
|
|
113
|
+
**API Routes:**
|
|
114
|
+
|
|
115
|
+
| Method | Route | Purpose | Auth Required |
|
|
116
|
+
| --------------- | ---------- | -------------- | ------------- |
|
|
117
|
+
| [GET/POST/etc.] | [/api/...] | [what it does] | [yes/no] |
|
|
118
|
+
|
|
119
|
+
**Edge Cases:**
|
|
120
|
+
|
|
121
|
+
- [Edge case]: [How it's handled]
|
|
122
|
+
|
|
123
|
+
**Known Limitations:**
|
|
124
|
+
|
|
125
|
+
- [Limitation]: [Why, and potential fix]
|
|
126
|
+
|
|
127
|
+
### [Feature Area 2]
|
|
128
|
+
|
|
129
|
+
_[Same structure as above]_
|
|
130
|
+
|
|
131
|
+
### [Calendar / scheduling exports (optional)]
|
|
132
|
+
|
|
133
|
+
> π§ **Guidance:** If users download **`.ics`** (or similar) for interviews, reminders, or deadlines, document **generation** (server vs client), **timezone** rules (floating vs UTC), **UID** stability across edits, and **routes** or handlers that serve the file. Note re-download behavior (duplicate events in some calendar apps).
|
|
134
|
+
|
|
135
|
+
**API Routes:**
|
|
136
|
+
|
|
137
|
+
| Method | Route | Purpose | Auth Required |
|
|
138
|
+
| ------ | ----- | ------- | ------------- |
|
|
139
|
+
| [GET] | [/api/.../events.ics] | [Build calendar payload] | [yes] |
|
|
140
|
+
|
|
141
|
+
### [User data export & portability (optional)]
|
|
142
|
+
|
|
143
|
+
> π§ **Guidance:** If you offer **self-service export** (ZIP, JSON, or bundled documents of user-owned records), document **scope** (included vs excluded collections), **inline vs async job** flow, **retention / rate limits**, and how exports interact with **deletion and data lifecycle** (see Data Model). PII-heavy bundles may need entitlement gates and audit notes.
|
|
144
|
+
|
|
145
|
+
**API Routes:**
|
|
146
|
+
|
|
147
|
+
| Method | Route | Purpose | Auth Required |
|
|
148
|
+
| ------ | ----- | ------- | ------------- |
|
|
149
|
+
| [POST] | [/api/user/export] | [Queue or return archive] | [yes] |
|
|
150
|
+
|
|
151
|
+
### [Structured eligibility / requirement fit (optional)]
|
|
152
|
+
|
|
153
|
+
> π§ **Guidance:** When postings or profiles carry **structured requirements** the product compares to user-declared status (education level, professional license, security clearance, work authorization, etc.), implement as a **repeatable shape** β not one-off strings per surface.
|
|
154
|
+
|
|
155
|
+
**Taxonomy module (shared client + server):**
|
|
156
|
+
|
|
157
|
+
- Ordered **requirement levels** + **flexibility** qualifiers (e.g. required / preferred / flexible; active / sponsorable).
|
|
158
|
+
- Separate **user status** type when profile values differ from job requirements (e.g. active vs inactive/lapsed).
|
|
159
|
+
- Type guards, `hardMismatch()`, optional `match()` for positive badges, `formatRequirement()` for Logistics rows.
|
|
160
|
+
|
|
161
|
+
**Data model:**
|
|
162
|
+
|
|
163
|
+
- Job/posting fields: `[domain]_requirement`, `[domain]_flexibility` (or JSON sub-object if multiple domains).
|
|
164
|
+
- User profile fields: `[domain]_status`, optional `[domain]_inactive_level`.
|
|
165
|
+
- PocketBase mappers **`pbJobToApp` / `appJobToPB` / user config** β include fields in **PATCH field maps** (easy to forget).
|
|
166
|
+
|
|
167
|
+
**Extraction pipeline:**
|
|
168
|
+
|
|
169
|
+
- Regex/heuristics on plain posting text first (context-gated patterns to avoid false positives).
|
|
170
|
+
- LLM extraction schema second; merge on import/refresh with regex fallback when LLM omits.
|
|
171
|
+
- Propagate on **metadata-only refresh** and **full posting refresh** branches.
|
|
172
|
+
|
|
173
|
+
**Surfaces (keep in sync):**
|
|
174
|
+
|
|
175
|
+
- Detail **Logistics** row; card **match/mismatch** badge; board **filter** predicate shared with `FilterBar` counts.
|
|
176
|
+
- **`buildCandidateProfileForFit`** (or equivalent) + **`build*ContextBlock`** in tailoring/prep/adapt prompts.
|
|
177
|
+
- Onboarding/profile select + optional **resume inference**; help topic.
|
|
178
|
+
|
|
179
|
+
**Edge cases:**
|
|
180
|
+
|
|
181
|
+
- `preferred` / `must_be_clearable` flexibilities should not trigger hard mismatch badges.
|
|
182
|
+
- Do not backfill legacy rows; populate on next import/refresh unless you run an explicit migration.
|
|
183
|
+
|
|
184
|
+
**API Routes:** document import, refresh, user config PATCH, and any prerequisite errors (`so we suggest X` β assertive voice).
|
|
185
|
+
|
|
186
|
+
## AI/LLM Integration
|
|
187
|
+
|
|
188
|
+
_If your app uses AI, document the integration patterns here. Record the Phase 1 **content-generation pattern** (runtime API, build-time seed, or BYO-LLM paste) and **provider** β including **local Ollama** when used (`OLLAMA_BASE_URL`, `OLLAMA_MODEL`; setup/test via ForgeTrail **`setup-ollama`** / **`test-ollama`**). Default local models: **Granite 4.1** or **Gemma 3** instruct β not reasoning/thinking models unless product requirements say otherwise._
|
|
189
|
+
|
|
190
|
+
### LLM Functions
|
|
191
|
+
|
|
192
|
+
| Function | Purpose | Model | Avg Tokens | Cost/Call |
|
|
193
|
+
| -------- | ------------------- | ------- | ---------- | --------- |
|
|
194
|
+
| [name] | [what it generates] | [model] | [~N] | [$X] |
|
|
195
|
+
|
|
196
|
+
### Prompt Architecture
|
|
197
|
+
|
|
198
|
+
[How are prompts structured? What context is injected? What rules are enforced?]
|
|
199
|
+
|
|
200
|
+
> π‘ **Lesson learned:** Style rules (no em dashes, Oxford comma, active voice, etc.) are injected into ALL LLM prompts, not applied as post-processing. This produces better results than trying to fix output after generation.
|
|
201
|
+
>
|
|
202
|
+
> π‘ **Also:** When LLM output needs to fit into structured formats (DOCX, database fields), have the code own the structure and use the LLM only for content. Don't ask the LLM to generate find/replace pairs or structural markup.
|
|
203
|
+
|
|
204
|
+
> π‘ **Lesson learned:** **Fence untrusted content inside prompts.** Any text the app did not author β user input, scraped pages, uploaded documents, third-party API responses β is a **prompt-injection vector** ("ignore previous instructions andβ¦"). Wrap every such value in a labeled delimiter block and prepend a standing guard instruction that says content inside the block is **data to analyze, never instructions to follow.** Keep the wrapping helper (and a shared `INJECTION_GUARD`-style constant) **internal to the one LLM module** so new prompt functions inherit the defense by construction rather than each author remembering it. **Why:** a model that obeys embedded directives can leak the system prompt or emit fabricated content that the user then submits under their own name β the highest-trust failure class for any AI product.
|
|
205
|
+
|
|
206
|
+
### Model selection and the provider choke point
|
|
207
|
+
|
|
208
|
+
[Document your model tiers, how a model is chosen per call, and the single module all model calls pass through.]
|
|
209
|
+
|
|
210
|
+
> π‘ **Lesson learned:** **Route every model call through one function and pick the tier by the value of the output.** A single `callLLM()`-style entry point is where provider switching (hosted vs local Ollama), usage logging, and response normalization live β features should never construct a provider client directly. Choose the model by what the output is worth: the **most capable/expensive** tier for extraction and anything the user submits as their own; a **mid** tier as the default workhorse; a **cheap/fast** tier for classification, retries, and recovery. Keep model ids in **config/constants**, never in function or route names (`callBigModel()` rots the day you change providers β see `.cursor/rules/api-naming` guidance). For user-facing actions, a small **fallback chain** that retries down the tiers on provider overload (429/503) beats hard-failing.
|
|
211
|
+
>
|
|
212
|
+
> π **Example:** Read the **entire** response, not the first content block β providers can return reasoning/tool blocks before text, so `content[0]` intermittently yields "empty" output; concatenate all text blocks in the wrapper. Disable **extended/adaptive thinking** for JSON tasks (reasoning tokens can eat the output budget and return empty text). Treat a **blank model-name env var** as unset so `MODEL_X=` doesn't get passed into the API and rejected.
|
|
213
|
+
|
|
214
|
+
### Output Validation
|
|
215
|
+
|
|
216
|
+
[How do you validate LLM responses? What happens when the LLM returns malformed output?]
|
|
217
|
+
|
|
218
|
+
> π‘ **Lesson learned:** LLM JSON parsing with no runtime validation means missing fields propagate as incomplete objects. Always validate the shape of LLM responses before storing or displaying them.
|
|
219
|
+
|
|
220
|
+
> π‘ **Lesson learned:** **Verbatim-copy JSON** (resume/cover-letter upload, large structured transforms) fails when the model puts **literal newlines or tabs inside quoted strings** β `JSON.parse` throws `Bad control character in string literal` even when the payload is otherwise correct. **Defense in depth:** (1) prompt rule β escape `\\n` / `\\t` / `\\r` inside every string value; (2) shared **`parseJsonFromLlmOutput`** that tries strict parse first, then **`sanitizeJsonControlChars`** (string-aware escape of U+0000βU+001F only inside `"β¦"`) before retry; (3) route **all** verbatim-text mapping parsers through that helper, not raw `JSON.parse(stripCodeFence(β¦))`. Preserves candidate text instead of failing onboarding.
|
|
221
|
+
|
|
222
|
+
> π§ **Guidance:** Implement `sanitizeJsonControlChars` + `parseJsonFromLlmOutput` in a shared module (e.g. `lib/format.ts`). Unit-test a minimal broken payload: `{"bullets":["line one\nline two"]}`. Log parse failures with `server_failure_llm_parse` and return a **Support ID** on the upload route.
|
|
223
|
+
|
|
224
|
+
> π‘ **Lesson learned:** When an LLM assigns a numeric score or rank, always ask for a short textual justification in the same response. A bare number (e.g. "fit rank: 3/5") is opaque to users and useless for debugging prompt quality. A 1-2 sentence explanation ("Direct enterprise SaaS leadership maps to this VP role, but the ML research requirement is a gap") turns the score into actionable insight. Store the justification as a separate field so the UI can show it conditionally (gracefully hidden for older records that predate the field), and include concrete prompt guidance like "reference specific alignment or gaps" so the LLM produces useful text, not generic filler.
|
|
225
|
+
>
|
|
226
|
+
> π **Example:** A job-fit scoring prompt originally asked for `fitRank: 1-5`. Adding `fitJustification: 1-2 sentence explanation referencing specific alignment or gaps` required a full-stack field addition (type definition β database mapping β schema migration β API route β UI display) but immediately made the feature comprehensible to users.
|
|
227
|
+
|
|
228
|
+
> π‘ **Lesson learned:** **Treat HTTP responses from long-running generation routes as untyped until validated.** Clients polling or awaiting dossiers, multi-section briefs, or chained LLM pipelines must handle **`application/json` failures**, **`text/html` error pages**, and empty bodies from gateways β parse safely (`Content-Type` check or read-as-text then `JSON.parse` inside try/catch) before assigning to UI models. Prefer **`Accept: application/json`** on clients and return structured `{ message, code?, correlationId? }` from your routes when upstream failures occur so UX stays consistent with progressive-import reconcile paths.
|
|
229
|
+
|
|
230
|
+
### AI-generated section lifecycle (multi-surface records)
|
|
231
|
+
|
|
232
|
+
> π§ **Guidance:** When one primary entity has **several distinct LLM outputs** (e.g. alignment brief, company dossier, personalized resonance, document transformation), document:
|
|
233
|
+
>
|
|
234
|
+
> - **Routes and persistence** per output type (separate fields or documents β do not overwrite Lens A when regenerating Lens B).
|
|
235
|
+
> - **Phase UI** shared across surfaces: empty CTA, generating (button spinner and/or staged progress), ready, stale-with-refresh.
|
|
236
|
+
> - **Staleness detection:** server-side input hash (or explicit version) compared at read time; client shows a non-blocking stale banner β **no auto-regen** on input change.
|
|
237
|
+
> - **Vocabulary module** for shared refresh labels and stale message structure; per-surface generate-verbs only where voice differs.
|
|
238
|
+
>
|
|
239
|
+
> See **`DESIGN_SYSTEM.md` β AI-generated section lifecycle** and **`TEST_PLAN.md` Β§7.4f** for manual checks.
|
|
240
|
+
|
|
241
|
+
### LLM Usage Tracking
|
|
242
|
+
|
|
243
|
+
[How do you track and audit LLM usage? Two concerns: cost visibility (admin) and limit enforcement (user).]
|
|
244
|
+
|
|
245
|
+
> π‘ **Lesson learned:** Two-layer LLM tracking architecture:
|
|
246
|
+
>
|
|
247
|
+
> Layer 1 β Token-level logging (cost visibility):
|
|
248
|
+
> - Log every LLM API call to a `llm_usage` collection with: input_tokens, output_tokens, model, cost_estimate, duration_ms, operation_name, user_id.
|
|
249
|
+
> - The centralized `callLLM()` function reads a global tracking context (set by each API route via `setLLMTrackingContext()`) and logs automatically after each response.
|
|
250
|
+
> - Surfaces on an admin dashboard for cost analysis.
|
|
251
|
+
>
|
|
252
|
+
> Layer 2 β Feature-level counters (limit enforcement):
|
|
253
|
+
> - Integer fields on the user record (e.g., `ai_tailoring_count`, `ai_prep_count`) incremented by `incrementCounter()`.
|
|
254
|
+
> - Gated by `checkLimit()` which compares against `PLAN_LIMITS[plan][counter]`.
|
|
255
|
+
> - Reset on a 30-day rolling window.
|
|
256
|
+
> - Uses PocketBase atomic increment syntax (`{ 'counter+': 1 }`) to prevent race conditions.
|
|
257
|
+
>
|
|
258
|
+
> Bridge: The API route calls `setLLMTrackingContext()` at the top, then `checkLimit()` for entitlement enforcement. After a successful LLM call, it calls `incrementCounter()`. The `callLLM()` function handles Layer 1 automatically.
|
|
259
|
+
>
|
|
260
|
+
> Audit methodology: Periodically inventory ALL routes that call `callLLM()` and check:
|
|
261
|
+
> 1. Does the route set tracking context? (If not, token usage is silently lost)
|
|
262
|
+
> 2. Does the route call `checkLimit()`? (If not, the operation is unmetered)
|
|
263
|
+
> 3. Does the route call `incrementCounter()` after success? (If not, usage isn't counted)
|
|
264
|
+
> Create a table of all routes with their tracking status. Routes with token logging but no feature counter are the most dangerous β they cost money but don't count against limits.
|
|
265
|
+
|
|
266
|
+
### Content Refresh from External Sources
|
|
267
|
+
|
|
268
|
+
[If your app imports content from external URLs, how does it handle stale/changed/deleted content?]
|
|
269
|
+
|
|
270
|
+
> π‘ **Lesson learned:** Re-fetch pattern for imported content:
|
|
271
|
+
>
|
|
272
|
+
> 1. Eligible items: Only items in early pipeline statuses (e.g., "backlog", "draft"). Items in later stages (submitted, active) have already been acted on β refreshing the source is less actionable.
|
|
273
|
+
> 2. Re-scrape using the same pipeline as initial import (fallback chain: fetch β headless browser β third-party extraction API).
|
|
274
|
+
> 3. Detect closed/removed status via: HTTP 404/410, redirect to generic page, regex patterns for known "closed" phrases across major platforms, content below minimum length threshold.
|
|
275
|
+
> 4. Compare new content to stored content to detect changes. Surface a diff summary (LLM-generated or structural).
|
|
276
|
+
> 5. Return a typed result: `{ status: 'open' | 'closed' | 'changed' | 'unchanged' | 'unreachable', ...details }`.
|
|
277
|
+
> 6. UI: Button per item (not batch). Show result inline. If closed, offer to update status. If changed, offer to re-process.
|
|
278
|
+
|
|
279
|
+
### URL import: paywalls and bot interstitials
|
|
280
|
+
|
|
281
|
+
[If your app creates records from external URLs β job postings, listings, articles β document how you handle unusable responses.]
|
|
282
|
+
|
|
283
|
+
> π‘ **Lesson learned:** The same URL pipeline used for **re-scrape** applies to **first-time import**: many sites return HTML shells (sign-in required, apply-only view, rate limit, or bot challenge) to automated clients. Heuristics should include marker strings, minimum extracted text length, and structural checks (e.g. absence of job-title/description blocks). On failure, respond with a **typed outcome** the UI can explain β not a generic 500 β and never write placeholder HTML into the user's record as if it were the real content. Optional: track a "blocked / paywall suspected" flag for support and product analytics.
|
|
284
|
+
|
|
285
|
+
### URL import: deterministic extractors vs markup drift (and optional LLM recover)
|
|
286
|
+
|
|
287
|
+
[When **Cheerio**, **domain-specific parsers**, or **structured-data** extraction pull **title/metadata** from HTML but the **main body** parses empty because the host **changed DOM** (new ATS layout, renamed containers, alternate subdomain).]
|
|
288
|
+
|
|
289
|
+
> π‘ **Lesson learned:** **Treat βfetch succeeded, parse yielded nothingβ separately from βwrong URL.β** The server may return 200 and a large HTML document while your selectors no longer match the description root β users paste a correct listing link and still see failure. **Why:** Blaming the URL trains mistrust; support burns on false negatives.
|
|
290
|
+
>
|
|
291
|
+
> **Pattern:** Keep a **layered pipeline** (fast HTTP fetch β headless browser if needed β optional third-party or residential fetch for bot-hostile hosts). Prefer **updating selectors** or adding **host-specific branches** when one board pattern is known. When all deterministic stages still produce **below-minimum body text** but you retained a **large raw HTML snapshot**, an optional **single** structured extract via your **small/cheap LLM** on **plain text derived from that HTML** can recover β only as a **last resort before throwing**, with **verbatim-from-source** prompt rules, **JSON validation**, and the same **minimum length** gate. Provide an **env opt-out** so operators can disable the path for cost or policy.
|
|
292
|
+
>
|
|
293
|
+
> **Observability:** Record **per-stage diagnostics** (e.g. fetch vs browser vs recover; approximate description length) for support bundles β without logging full HTML in analytics.
|
|
294
|
+
>
|
|
295
|
+
> π§ **Guidance:** Capture shipped behavior in a **delivery spec** (`specs/partial/` until deferred observability or tests are done); link from backlog (`TODO.md`). Full ForgeTrail methodology also mirrors scrape/import resilience for Lite-only projects in **`content/FORGETRAIL_LITE.md`** Β§7.2.
|
|
296
|
+
|
|
297
|
+
### Multi-record URL import (bulk paste / spreadsheet)
|
|
298
|
+
|
|
299
|
+
[If users can add **many** external URLs or CSV rows in one operation β jobs, listings, bookmarks β document how it differs from single import.]
|
|
300
|
+
|
|
301
|
+
> π‘ **Lesson learned:** **Preview and confirm** before firing N expensive fetches. Parse client- or server-side with a **row cap**; dedupe using the **same URL normalization** as `POST` handlers (tracking params, host casing, trailing slashes) so preview matches server duplicate behavior. Each accepted row should pass the **same** entitlement, rate-limit, and duplicate checks as a one-off import β batch UX must not become a bypass. **Progressive pipeline:** Prefer the same **stub β finalize** (or equivalent) two-phase pattern as single import so the UI shows partial records immediately and enrichment finishes asynchronously. Optional **batch / lite finalize** flag: skip or defer noisy automatic follow-ups (e.g. secondary LLM alignment) while still running trust/extraction steps you rely on for data quality. **Hints from files:** After server extract, merge user-supplied columns (status, tags, labels) with a PATCH or dedicated merge rule so spreadsheet metadata isn't silently discarded. **Observability:** Emit **aggregate** analytics (started/completed counts, errors, skips) β avoid PII in event payloads. **CSV symmetry:** If you export a spreadsheet for backup, align **header order and row cell order**; misaligned exports make re-import mapping look "random" and erode trust.
|
|
302
|
+
|
|
303
|
+
### URL import: extraction hints and required-field merge
|
|
304
|
+
|
|
305
|
+
[If **enrichment** or **finalize** merges LLM output into records that already have stub or scrape metadata β especially when the BaaS marks fields **required**.]
|
|
306
|
+
|
|
307
|
+
> π‘ **Lesson learned:** **Pass structured hints into extraction, and merge defensively.**
|
|
308
|
+
>
|
|
309
|
+
> 1. **Hints:** After the initial fetch/scrape, you often know title, employer/site name, or canonical listing id from the page header, JSON-LD, OpenGraph, or URL path β even when the **body text** omits the employer (common on job boards). Feed those hints into the second-phase extraction prompt (or parallel structured parser) so the model is not forced to invent or leave fields empty.
|
|
310
|
+
> 2. **Required fields:** When applying LLM JSON to an existing row, treat **empty strings** and obvious **placeholder** tokens as βno new information.β Fall back to the stub value, the hint, or a small resolver (e.g. match company by hostname) instead of overwriting. **Why:** A blank model field should not clear a PocketBase (or similar) **required** relation/string that was already satisfied at stub time β users see validation errors or broken lists after a βsuccessfulβ import.
|
|
311
|
+
> 3. **Normalization:** Use the **same** URL normalization for dedupe, fetch, and re-scrape (tracking query params stripped, stable path for platforms that embed ids in the path). See duplicate-detection patterns in **`docs/CONTEXT_PROMPT.md`**.
|
|
312
|
+
|
|
313
|
+
### URL import: client reconciliation after multi-step finalize
|
|
314
|
+
|
|
315
|
+
[When **stub β finalize** (or similar) runs as **more than one HTTP round-trip** from the browser β modal add, discovery batch, onboarding β and the UI must stay consistent if the second request misbehaves.]
|
|
316
|
+
|
|
317
|
+
> π‘ **Lesson learned:** **Do not treat a failed or unreadable finalize response as the source of truth for terminal state.** The server may already have completed enrichment while the client sees a network error, non-JSON body, or timeout. **Why:** Users end up with a half-updated card, stuck progress, or no toast while dedupe still blocks a retry β support cannot see what the user sees.
|
|
318
|
+
>
|
|
319
|
+
> **Pattern:** After finalize returns an error or unusable JSON, **GET the record by id** (or a small βstatusβ endpoint) and merge the **authoritative** fields the UI needs (e.g. import/enrichment flags, title, error state). Strip bulky sidecars from the payload if your read API includes logs or relation blobs not meant for list merge. If the record is **terminal** (success or failed), drive the same completion/failure handlers you would have run on a clean finalize JSON so every surface (main add flow, suggestions, bulk) stays consistent.
|
|
320
|
+
>
|
|
321
|
+
> **Surfaces:** Any code path that calls stub then finalize **separately** needs the same **failure / merge** callback wiring as the primary add flow β not only the happy path. Use **safe JSON parsing** (e.g. `.json().catch(() => ({}))`) on both steps so a bad body does not throw past your error UI after the user has already dismissed a modal.
|
|
322
|
+
|
|
323
|
+
### Stored workflow enums and filter surfaces
|
|
324
|
+
|
|
325
|
+
[When records use **status**, **stage**, or **column** values that power **boards**, **search**, and **deduplication**.]
|
|
326
|
+
|
|
327
|
+
> π‘ **Lesson learned:** **Normalize at the data boundary before UI filters.** Legacy rows, imports, or one-off writes can leave **empty** or **deprecated** enum values that match **no** visible column or filter bucket. The same row may still **dedupe** against new adds β producing βghostβ cards, count mismatches (βshowing N of Mβ), and search that cannot find the item. Map unknown/empty values to a **defined** internal key (or a dedicated βinbox / needs triageβ column) in one mapper used by list, detail, and board views.
|
|
328
|
+
>
|
|
329
|
+
> π§ **Guidance:** Document the canonical set of keys, where normalization runs (BaaS hook, server mapper, or client store), and how migrations or backfills handle legacy values.
|
|
330
|
+
|
|
331
|
+
### Imported record: apply URL vs discovery URL
|
|
332
|
+
|
|
333
|
+
[When listings can carry **two URLs** β where the user **applies** or reads the official posting vs where they **discovered** the lead (board, aggregator, email).]
|
|
334
|
+
|
|
335
|
+
> π‘ **Lesson learned:** **Store both when the product needs them.** Application or ATS links differ from βwhere I found thisβ links; users need one-click access to each for different tasks. **Dedupe and normalization** should define which URL is canonical for identity (often the stable employer/ATS listing) vs which is diagnostic metadata.
|
|
336
|
+
>
|
|
337
|
+
> π§ **Guidance:** Document field names, which URL feeds **re-scrape** / refresh, how exports and **CSV** include both without column drift, and how the UI labels them so buyers are not confused.
|
|
338
|
+
|
|
339
|
+
### Posting body upgrade (additive merge)
|
|
340
|
+
|
|
341
|
+
[When users **paste**, **upload**, or **fetch** a richer job/posting body **after** an initial import β e.g. employer career site vs thin board snippet.]
|
|
342
|
+
|
|
343
|
+
> π‘ **Lesson learned:** **Prefer additive merge with preview/confirm.** Overwriting the whole record from a second pass can destroy user edits, connection notes, or status. Merge new fields **only where the incoming payload is strictly stronger**; keep explicit **fallback extraction** from plain text when structured panels omit salary, location, or work arrangement.
|
|
344
|
+
>
|
|
345
|
+
> π§ **Guidance:** List preview and confirm routes, merge rules, idempotency if the user retries, and how this path shares enrichment with **URL import** pipelines.
|
|
346
|
+
|
|
347
|
+
### AI-assisted tailoring: in-tab critique and structural controls
|
|
348
|
+
|
|
349
|
+
[When tailoring a document to a target (job, role, school) goes beyond **one-shot generate** β **quality review**, **heuristic catalog**, **user-approved actions**, **structural adjustments** (e.g. reorder for narrative flow), **persistence per target**.]
|
|
350
|
+
|
|
351
|
+
> π‘ **Lesson learned:** **Treat critique and structure as first-class persisted state**, not ephemeral chat. Results should survive **tab switches** and reloads; approvals should bind to the **specific artifact + target pair**. If **optional AI providers or keys** are missing, finish in a **terminal state** with clear UX β not an infinite βstill workingβ placeholder.
|
|
352
|
+
>
|
|
353
|
+
> π‘ **Lesson learned:** **Base cover-letter templates** for emailed or attached DOCX should omit legacy mailed-letter headers (`[City, State]`, employer street address blocks unless the product truly mails letters). Those lines are often marked βfixedβ in find/replace tailoring and leak through as untailored placeholders. Audit the stored base doc and any βprotected template spansβ list when users report leftover bracket text.
|
|
354
|
+
>
|
|
355
|
+
> π§ **Guidance:** Document collections/fields for critique output, versioning or timestamps, `/api/` routes, and where the same **quality rules** apply across surfaces (e.g. base doc editor vs role-specific tab) so signals stay aligned.
|
|
356
|
+
|
|
357
|
+
### Factual grounding and programmatic veracity checks on tailored outputs
|
|
358
|
+
|
|
359
|
+
[When generating tailored artifacts (cover letters, statements of interest, custom pitches) that must represent the user truthfully, implementing **factual grounding** (passing the full source document in the prompt) and a **programmatic veracity pass** (a post-generation audit) to catch fabrications, hallucinated metrics, or mis-attributed achievements.]
|
|
360
|
+
|
|
361
|
+
> π‘ **Lesson learned:** **Never tailor in a vacuum.** If the user has a source document of record (e.g. a base resume), always pass its full verbatim text directly in the tailoring prompt as the *only* source of truth for candidate facts. Relying on lossy summaries or profile blocks invites the LLM to invent technologies, metrics, or past roles to match the target description.
|
|
362
|
+
>
|
|
363
|
+
> π‘ **Lesson learned:** **Implement a programmatic veracity pass as an automated safety net.** Run a fast, cheap semantic audit (using a smaller model) immediately after generation. Compare the drafted paragraphs against the source document to flag ungrounded claims. Surfacing these issues in the UI with exact excerpts, clear reasons, and suggested repairs provides a robust, human-in-the-loop safety net before the user treats the output as final.
|
|
364
|
+
>
|
|
365
|
+
> π§ **Guidance:** Document the veracity check schema, how results are persisted on the target record, `/api/` routes for generation and dismissal, and the UI components used to display grounding gaps to the user.
|
|
366
|
+
|
|
367
|
+
### Anti-self-importance and position-of-strength tone controls
|
|
368
|
+
|
|
369
|
+
[When generating outbound documents (cover letters, statements of interest, custom pitches) representing the user, implementing strict tone and posture controls to prevent grandiose, visionary, or striving registers from leaking into the output.]
|
|
370
|
+
|
|
371
|
+
> π‘ **Lesson learned:** **Filter out rhetorical scaffolding and self-narration.** Avoid letting the model tell the reader how to interpret the user's career or label their experiences as "chapters," "milestones," or "my range." Let the facts of what they built carry the weight directly.
|
|
372
|
+
>
|
|
373
|
+
> π‘ **Lesson learned:** **Ban clever, self-authored aphorisms and insider-coded observations.** Do not include philosophical observations about what the work "really requires" or "depends on" (e.g., "reading across teams as much as on the code," "where the margin for architectural guesswork was low," or "at the pace an enterprise sales cycle sets").
|
|
374
|
+
>
|
|
375
|
+
> π‘ **Lesson learned:** **Enforce a "position of strength" posture.** Ensure the generated text does not mirror the job description as proof of fit, deliver maxims/theses as quotable wisdom, brand ordinary habits as named methods, or use striving/grind vocabulary.
|
|
376
|
+
>
|
|
377
|
+
> π§ **Guidance:** Document how tone and posture guidelines are injected into the prompts, the specific anti-patterns banned, and how to validate generated text against these rules (e.g., using a programmatic LLM audit pass during development or as a post-generation safety net).
|
|
378
|
+
|
|
379
|
+
### Persistent contextual assistant (dock, rail, or side panel)
|
|
380
|
+
|
|
381
|
+
[When the product embeds a **long-lived AI or copilot surface** alongside the main workflow β not only one-off modals β scoped to a **selected record** or page context.]
|
|
382
|
+
|
|
383
|
+
> π‘ **Lesson learned:** Treat the assistant as **integrated shell state**: which record (or nothing) it is bound to, how it survives **panel open/close**, **resize**, and **navigation**, and how **streaming** partial tokens interact with layout and focus. **Entitlements and rate limits** should match other LLM entry points; avoid a second-class code path that bypasses metering or observability.
|
|
384
|
+
>
|
|
385
|
+
> π§ **Guidance:** Document routes for turns/messages, compaction or thread limits, archive/history, injection safety boundaries, and **capability-based** API names (see project **api-naming** rules). Optional: feature flags for rollout.
|
|
386
|
+
|
|
387
|
+
### Companion lenses on a primary record (multi-tab analysis)
|
|
388
|
+
|
|
389
|
+
[When one **entity** (deal, ticket, job application, candidate, project) supports **more than one analytic or generative βviewβ** on the same underlying data β e.g. fit vs motivation, summary vs risk checklist β each with **its own persisted output**.]
|
|
390
|
+
|
|
391
|
+
> π‘ **Lesson learned:** **Avoid one blob that tries to do everything.** Separate **lens identity** in the data model (or versioned sub-documents) so regen, export, and assistant prompts do not overwrite unrelated content. **Reuse** shared inputs (base description, scores) but **do not** silently duplicate conflicting LLM narratives in the same UI without clear labels.
|
|
392
|
+
>
|
|
393
|
+
> π§ **Guidance:** List tab or sub-route names, persistence keys, regen actions, inclusion in **ZIP/exports**, and how the in-app assistant references each lens.
|
|
394
|
+
|
|
395
|
+
### User-visible external identity URLs (professional / social profile links)
|
|
396
|
+
|
|
397
|
+
[When you store **URLs** that point at the user or a contact on a **third-party identity surface** β professional network, code host, portfolio β for display, deep links, or prompt context.]
|
|
398
|
+
|
|
399
|
+
> π‘ **Lesson learned:** **Normalize and validate** URLs on write (scheme, host policy if you need one). Prefer **storing canonical form** for display and dedupe; never leak raw tokens in analytics. In **exports**, decide whether these fields are **PII** β document opt-out or redaction for shared bundles.
|
|
400
|
+
>
|
|
401
|
+
> π§ **Guidance:** Fields on which collection, UI entry points (profile tab, settings, modal), and how imports (e.g. paste from address bar) map to the same normalizer as manual edit.
|
|
402
|
+
|
|
403
|
+
### User intent profile (direction, goals) driving discovery and artifacts
|
|
404
|
+
|
|
405
|
+
[If the app stores a **north-star profile** β career direction, priorities, constraints β that powers recommendations, search, or multiple **variants** of a core artifact, document it as its own feature area.]
|
|
406
|
+
|
|
407
|
+
> π‘ **Lesson learned:** **Treat the intent profile as first-class data**, not ephemeral UI state. Downstream surfaces (discovery panels, board filters, tailored exports, βvariantβ records) should **read the same structured fields** with explicit invalidation when the profile changes. If users edit in **multiple modes** (structured sections vs free text), define how those views sync and which is canonical for server routes. Document **which API handlers and jobs consume which fields** so refactors donβt leave half the app on a stale shape.
|
|
408
|
+
|
|
409
|
+
> π§ **Guidance:** List collections/fields, routes that mutate the profile, and how **discovery results** enter the **same create/import pipeline** as manual entry (entitlements, duplicates, telemetry). See **`docs/CONTEXT_PROMPT.md` β Patterns to Follow** for discovery β import parity.
|
|
410
|
+
|
|
411
|
+
### Branded discovery hub and navigation elevation (optional)
|
|
412
|
+
|
|
413
|
+
[When a **named discovery** capability lives primarily inside a multi-tab panel but you also expose it from **top-level navigation**, marketing, or help as a first-class entry.]
|
|
414
|
+
|
|
415
|
+
> π‘ **Lesson learned:** **Burying discovery behind an unrelated parent label** trains users to hunt; **elevating** it often does not require a new route β a nav item or CTA that **deep-opens the existing panel and tab** is enough. The fragile part is **message parity**: if you introduce a **branded name** in marketing, every surface that references the workflow (About, Help, tours, onboarding, tab labels) must use the same name or explicitly say where the feature lives. Otherwise users believe something is "missing."
|
|
416
|
+
|
|
417
|
+
> π§ **Guidance:** Document client actions or routes that open the **same panel + tab** from the main menu, help "open panel" hooks, and tour effects. After IA or renaming passes, run the project's copy of **`user-facing-content-sync-audit.md`** (or equivalent) so landing and in-app story stay aligned.
|
|
418
|
+
|
|
419
|
+
### Optional: LLM vendor browsing tools (web search / fetch)
|
|
420
|
+
|
|
421
|
+
[If you integrate **hosted** search or fetch tools exposed by your LLM provider β distinct from your own scrape or third-party search API β document when they are primary vs fallback, cost/latency tradeoffs, and how results feed **URL import** or research records.]
|
|
422
|
+
|
|
423
|
+
> π§ **Guidance:** List feature flags or env toggles, adapters, rate limits, and merge rules with existing pipelines. Keep **capability-based** names in exported APIs and routes; treat vendor product names as configuration details.
|
|
424
|
+
|
|
425
|
+
> π‘ **Lesson learned:** **Prefer structured fallbacks.** When the vendor tool is unavailable, over budget, or returns thin results, your app should degrade to the existing fetch/scrape path (or a clear "could not retrieve" state) β not a silent empty UX.
|
|
426
|
+
|
|
427
|
+
### Trust or plausibility checks on imported external content
|
|
428
|
+
|
|
429
|
+
[If you run heuristics, rules, or LLM audits on records created from URLs β job postings, listings, articles β document inputs, outputs, and how warnings map to the UI.]
|
|
430
|
+
|
|
431
|
+
> π‘ **Lesson learned:** **When structured signals exist, use them before inferring from loose text.** If the source exposes machine-readable comp (JSON-LD salary, OpenGraph, native pay-range UI, schema fields), parse and **prefer those** for severity. Mixed signals (vague prose but a concrete structured range) should **not** surface βunknown payβ or βtruncatedβ at high severity β users read that as a product bug. **Why:** Trust features erode confidence faster than no feature when they contradict what users see on the source page.
|
|
432
|
+
|
|
433
|
+
### Record-level activity / event timeline (optional)
|
|
434
|
+
|
|
435
|
+
[If a primary entity (record, ticket, listing) accumulates user actions, AI decisions, or external signals over time β document the event model, creation paths, and UI surface.]
|
|
436
|
+
|
|
437
|
+
> π§ **Guidance:** Consider a **per-entity event log** when the primary record accumulates meaningful status changes, communications, AI-generated outputs, or user reflections over days or weeks. The model is typically an append-mostly child collection (parent relation, `type` enum, `timestamp`, `body`, optional `metadata` JSON). Key design decisions to document:
|
|
438
|
+
>
|
|
439
|
+
> - **Creation paths:** Manual log entry, automatic system events on status change, inline "quick log" from reminder or dashboard surfaces, AI-suggested triage notes.
|
|
440
|
+
> - **Event types:** Define an enum or union (`note`, `status_change`, `communication`, `ai_triage`, `milestone`, `reminder_action`) β breadth grows over time; plan for extensibility.
|
|
441
|
+
> - **AI-generated entries:** When an AI agent produces triage notes, action items, or summaries from entity context, store the output as a timeline event (not loose entity fields) so the user can review, edit, or discard it alongside their own notes. Mark AI-generated entries distinctly in the UI.
|
|
442
|
+
> - **Deduplication:** Guard against duplicate event creation from optimistic UI + server hooks firing on the same action (e.g., a status-change hook **and** an explicit client POST).
|
|
443
|
+
> - **Rendering:** Markdown or structured body; open external links in new tabs; consider grouping by date.
|
|
444
|
+
> - **Inline editing:** Quick-log and full-form affordances; forms should be vertically dense (consolidated field rows) to avoid pushing the existing timeline off-screen.
|
|
445
|
+
|
|
446
|
+
### Demo Mode
|
|
447
|
+
|
|
448
|
+
[If your app needs demo capabilities, how do you create demo-safe content?]
|
|
449
|
+
|
|
450
|
+
> π‘ **Lesson learned:** Two components for effective demos:
|
|
451
|
+
>
|
|
452
|
+
> 1. Content anonymization: LLM-powered tool that takes real content (e.g., a resume DOCX) and produces a new version with all identifying information replaced but professional substance preserved. The LLM generates a JSON replacement map (`{find: "real text", replace: "anonymized text"}`), then existing document manipulation code applies the replacements. This preserves formatting exactly β the LLM doesn't rewrite the document, it maps substitutions. Apply replacements longest-string-first to prevent partial matches.
|
|
453
|
+
>
|
|
454
|
+
> 2. Quick-access links: Pre-configured links to external sources (job boards, search results) with dynamic query parameters built from demo-account metadata (tags, role types, locations). One click opens a live, current results page ready for URL-grabbing during demos. Beats hunting for listings mid-demo.
|
|
455
|
+
>
|
|
456
|
+
> Both features live behind admin-only access. The anonymizer is a one-time prep tool, not a runtime feature.
|
|
457
|
+
|
|
458
|
+
## Configuration
|
|
459
|
+
|
|
460
|
+
### Environment Variables
|
|
461
|
+
|
|
462
|
+
| Variable | Required | Default | Description |
|
|
463
|
+
| -------- | -------- | ------------------- | ------------------ |
|
|
464
|
+
| [VAR] | [yes/no] | [default or "none"] | [what it controls] |
|
|
465
|
+
|
|
466
|
+
Optional β **app-owned transactional email** (welcome, security notices, billing receipts) sent from your server via a provider HTTP API (e.g. Resend, Postmark):
|
|
467
|
+
|
|
468
|
+
| Variable | Required | Description |
|
|
469
|
+
| -------- | -------- | ----------- |
|
|
470
|
+
| `[PROVIDER]_API_KEY` | If sending | Server-only API key for the email provider. |
|
|
471
|
+
| `EMAIL_FROM` | With API key | Full From header (e.g. `[Product] <notifications@[domain]>`). |
|
|
472
|
+
| `EMAIL_REPLY_TO` | No | Reply-To when support should not use the From address. |
|
|
473
|
+
|
|
474
|
+
> π‘ **Lesson learned:** **Split βBaaS/auth emailβ from βapp-owned email.β** Many stacks use the database or auth serviceβs **built-in SMTP** only for password reset and verification. Product mail (welcome, payment failed, password changed) should go through **one server module** (templates + shared layout + provider client) so subjects, footers, and failure logging stay consistent. **Webhook- or cron-triggered** sends should accept an **idempotency key** (provider header or internal dedupe) so retries donβt spam users.
|
|
475
|
+
|
|
476
|
+
> π‘ **Lesson learned:** When API key or From is **unset**, prefer a **silent no-op** for non-critical mail (log at debug / structured failure channel) rather than failing the userβs primary action β unless the message is legally required for the step to count as complete.
|
|
477
|
+
|
|
478
|
+
### Startup Validation
|
|
479
|
+
|
|
480
|
+
[What does the app check on startup? What fails gracefully vs. hard-fails?]
|
|
481
|
+
|
|
482
|
+
> π‘ **Lesson learned:** The app works without an LLM API key (graceful degradation). Scraping still works; just no AI-powered enrichment. This pattern is valuable for development and testing.
|
|
483
|
+
|
|
484
|
+
### Service base URLs (HTTPS and ports)
|
|
485
|
+
|
|
486
|
+
When constructing the base URL for a BaaS client, API gateway, or database SDK from environment variables (e.g. separate `HOST` and `PORT` with a default for local dev), **treat `https://` origins as authoritative**: if the configured URL already resolves to an origin on the standard HTTPS port, do not concatenate an extra port segment from a dev-default variable. **Why:** Requests to `[https host]:[wrong port]` fail TLS negotiation; failures often appear as generic fetch or SDK errors rather than βfix your `.env`.β Document which modes append ports (local HTTP) vs. which use the URL as-is (production HTTPS).
|
|
487
|
+
|
|
488
|
+
## File/Document Pipeline
|
|
489
|
+
|
|
490
|
+
_If your app processes files (upload, transform, export), document the pipeline here._
|
|
491
|
+
|
|
492
|
+
### Upload Flow
|
|
493
|
+
|
|
494
|
+
[How files get into the system]
|
|
495
|
+
|
|
496
|
+
### Processing/Transformation
|
|
497
|
+
|
|
498
|
+
[What happens to files after upload]
|
|
499
|
+
|
|
500
|
+
> π‘ **Lesson learned:** **Centralize file text-extraction parsing behind a dedicated server library wrapper (e.g., `pbFiles.ts`).** Avoid importing third-party parsing packages (such as `mammoth`, `pdf-parse`, or other document extractors) directly inside individual route endpoints. Instead, define clean extractor helper functions (such as `extractTextFromPdfBuffer(buffer)`) inside a server utility layer. Let these helper methods encapsulate lazy imports of their underlying packages (to keep server startup light) and contain common edge cases (such as catching `PasswordException` or invalid buffers). This prevents duplicate package initializations and ensures a single point of maintenance for third-party parser integrations.
|
|
501
|
+
|
|
502
|
+
### Output/Export
|
|
503
|
+
|
|
504
|
+
[How files leave the system]
|
|
505
|
+
|
|
506
|
+
> π‘ **Lesson learned:** **Headless office conversion (e.g. LibreOffice `soffice` for DOCXβPDF or similar):** Point the process at a **dedicated writable user/profile directory** per job or worker (via env such as `HOME` or the suiteβs user-installation flag) so concurrent exports donβt trample the same cache. **Install fonts** on the conversion host if users rely on non-core typefaces β otherwise substituted metrics change pagination. Capture **stdout/stderr** with request correlation; failures often only appear under parallelism or cold start.
|
|
507
|
+
>
|
|
508
|
+
> π‘ **Lesson learned:** When writing files that might be open in another application (e.g., Word), detect the lock error and create versioned copies (filename-v2, v3) instead of failing. Simple, but prevents a class of support tickets.
|
|
509
|
+
>
|
|
510
|
+
> π‘ **Lesson learned:** **Programmatic DOCX from markdown-ish strings** (headings, bullets, `**bold**`, `_italic_`, optional line-prefix centering): if you split on `**bold**` *before* resolving outer `_β¦_` (or guarded `*β¦*`) emphasis, lines like `_See **Settings β Profile** in the app._` break β the underscores end up in different segments and show **literally** in Word. **Peel** whole-line underscore wrappers first (inner must not contain stray `_`), then apply bold splits with a shared italic flag; for `*β¦*`, avoid treating `*a* and *b*` as one span (skip peel when the inner slice contains a lone `*`). Same pipeline applies to any OOXML builder that tokenizes markdown-lite inline.
|
|
511
|
+
>
|
|
512
|
+
> π‘ **Lesson learned:** **User-supplied long context for LLM** (pasted job text, extra profile notes, supplementary files): enforce a **documented max length** (characters or tokens) at validation time; show remaining budget in the UI. WHY: Caps cost, reduces truncation surprises, and keeps prompts within model context. Pair with clear empty-state copy when users hit the limit.
|
|
513
|
+
>
|
|
514
|
+
> π‘ **Lesson learned:** **Feature flags** for gradual rollout: store a small JSON object on the user or a global settings record (`{ flagName: true }`), hydrate into session or a reactive store on load, and gate **both** server handlers and client routes. WHY: Ship dark features to internal accounts without branch drift; admin UI optional early on.
|
|
515
|
+
>
|
|
516
|
+
> π‘ **Lesson learned:** **Template / document transformation history** (optional): append-only rows per user for major events β upload, AI transformation, manual save β with timestamps, optional excerpt or diagnosis text, and pointers to stored files. Distinct from raw file storage; supports βwhat changed when?β in support, compliance, and **export packages** without full VCS.
|
|
517
|
+
|
|
518
|
+
## Multi-User / Auth
|
|
519
|
+
|
|
520
|
+
_If applicable._
|
|
521
|
+
|
|
522
|
+
### Auth Flow
|
|
523
|
+
|
|
524
|
+
[How users authenticate. OAuth providers, session management, cookie configuration.]
|
|
525
|
+
|
|
526
|
+
### Authorization Model
|
|
527
|
+
|
|
528
|
+
[Who can see/do what. How permissions are checked.]
|
|
529
|
+
|
|
530
|
+
> π‘ **Lesson learned:** Distinguish between authenticated identity (who you are) and data ownership (whose data you're viewing). Cookie-based delegation allows advisors to manage client data without impersonating them. This distinction is subtle but critical for permission logic.
|
|
531
|
+
>
|
|
532
|
+
> π‘ **Lesson learned:** Multi-tier delegation access control:
|
|
533
|
+
>
|
|
534
|
+
> 1. Define access levels as an enum or union type: 'full' | 'collaborator' | 'viewer' (or similar).
|
|
535
|
+
> - full: Can do everything the owner can, including managing other delegates.
|
|
536
|
+
> - collaborator: Can create, edit, and delete records. Cannot manage team or billing.
|
|
537
|
+
> - viewer: Read-only access. Can view all data but not modify anything.
|
|
538
|
+
>
|
|
539
|
+
> 2. Store delegation records in a dedicated collection: { owner, delegate, accessLevel, grantedAt }.
|
|
540
|
+
> Each record represents "user X can access user Y's data at level Z."
|
|
541
|
+
>
|
|
542
|
+
> 3. Create a server-side helper: getActiveUserId(request) that checks:
|
|
543
|
+
> a. Is there a delegation cookie? If so, return the delegated user's ID (not the logged-in user's ID).
|
|
544
|
+
> b. Otherwise, return the logged-in user's ID.
|
|
545
|
+
> All data-fetching routes use this β they operate on the "active" user, not necessarily the authenticated user.
|
|
546
|
+
>
|
|
547
|
+
> 4. Create a permission check helper: canPerformAction(request, requiredLevel) that:
|
|
548
|
+
> a. If no delegation is active, the user is the owner β allow everything.
|
|
549
|
+
> b. If delegation is active, look up the delegate's access level and compare against requiredLevel.
|
|
550
|
+
> c. Use a numeric hierarchy (viewer=1, collaborator=2, full=3) so checks are simple comparisons.
|
|
551
|
+
>
|
|
552
|
+
> 5. Protect sensitive routes explicitly:
|
|
553
|
+
> - Billing/subscription routes: owner only (no delegation).
|
|
554
|
+
> - Team management routes: full access only.
|
|
555
|
+
> - Data modification routes: collaborator or above.
|
|
556
|
+
> - Read routes: any access level.
|
|
557
|
+
>
|
|
558
|
+
> 6. Anti-pattern: Don't check delegation in every route individually. Centralize it in getActiveUserId() and canPerformAction(), then call those helpers. Scattered inline checks lead to inconsistency and missed routes.
|
|
559
|
+
|
|
560
|
+
### Gated Server-Side Proxies for Private Edge Services (Admin-Only)
|
|
561
|
+
|
|
562
|
+
[If the system integrates private edge/node services (e.g. specialized crawlers, residential proxies, internal queues) that are protected behind firewall boundaries (such as a mesh VPN/Tailscale) or require unauthenticated endpoints to be proxied to administrative dashboards, document the security proxy pattern here.]
|
|
563
|
+
|
|
564
|
+
> π‘ **Lesson learned:** **Never expose raw edge service credentials or unauthenticated health URLs to the client browser.** Instead, route admin-only dashboard requests through a gated server-side API proxy (e.g., `GET /api/admin/[service]-health`). On this server route:
|
|
565
|
+
> 1. Enforce strict admin session verification first (e.g., `requireAdmin(locals)`).
|
|
566
|
+
> 2. Fetch from the private edge node (using a short timeout like 10s to prevent hanging).
|
|
567
|
+
> 3. Inject sensitive tokens (such as `[SERVICE]_TOKEN`) entirely on the server-side, returning only sanitized status, version, and queue depths to the client.
|
|
568
|
+
> 4. Resolve connection hints (e.g., stripping passwords/hosts from connection strings) to keep internal network topography hidden from the browser.
|
|
569
|
+
|
|
570
|
+
## Billing / Entitlements
|
|
571
|
+
|
|
572
|
+
_If applicable. Fill in during Phase 7._
|
|
573
|
+
|
|
574
|
+
### Tier Enforcement
|
|
575
|
+
|
|
576
|
+
[How are tier limits checked? Which routes enforce limits?]
|
|
577
|
+
|
|
578
|
+
> π‘ **Lesson learned:** Entitlement enforcement architecture:
|
|
579
|
+
>
|
|
580
|
+
> 1. Create a shared entitlements module ($lib/server/entitlements.ts) with a PLAN_LIMITS constant:
|
|
581
|
+
> - Keys: plan names (e.g., 'starter', 'pro', 'hold', 'expired')
|
|
582
|
+
> - Values: objects mapping counter names to numeric limits (0 = blocked, 999 = unlimited)
|
|
583
|
+
> - Export helpers: getEffectivePlan(user), checkLimit(user, counter), incrementCounter(user, counter)
|
|
584
|
+
>
|
|
585
|
+
> 2. checkLimit() does TWO things atomically: verifies the user hasn't exceeded their limit AND returns the current count.
|
|
586
|
+
> - Use 30-day rolling windows for usage counters (not calendar month). Calculate: count records where created >= 30 days ago.
|
|
587
|
+
> - Return a structured response: { allowed: boolean, current: number, limit: number, plan: string }
|
|
588
|
+
> - When blocked, return a 402 with a limitReachedBody() that includes the plan name and a message tailored to the plan state (expired vs. hold vs. standard limit-reached).
|
|
589
|
+
>
|
|
590
|
+
> 3. incrementCounter() creates a usage record AFTER the action succeeds (not before). If the action fails, the counter shouldn't increment.
|
|
591
|
+
>
|
|
592
|
+
> 4. Import and call checkLimit() at the TOP of every paid/gated API route handler. Don't scatter limit checks inside business logic.
|
|
593
|
+
>
|
|
594
|
+
> 5. Client-side entitlements: Expose plan + limits via the root layout server load. Create a reactive entitlements store that derives isExpired, isHold, isPaid, and per-counter remaining counts. Components read from this store to show inline usage badges and gate UI elements β but client-side checks are UX only, never security.
|
|
595
|
+
>
|
|
596
|
+
> π‘ **Lesson learned:** Naming plan-tier numerics and shared policy limits:
|
|
597
|
+
>
|
|
598
|
+
> 1. Define per-tier limits as **named constants** in a dedicated module, then assemble `PLAN_LIMITS` (or equivalent) from those names β readable diffs and easy grep when pricing changes.
|
|
599
|
+
> 2. Keep trial length, usage-period length, trial-specific download/export caps, and HTTP rate limits in **separate small modules**; import from one source in entitlements, hooks, and gated API routes.
|
|
600
|
+
> 3. Use a single **milliseconds-per-day** (or your stackβs shared helper) for all βN days from nowβ math, including usage-period rollover and trial remaining days.
|
|
601
|
+
> 4. Plain Node setup scripts that cannot import your TS modules may **duplicate the integers** with a one-line comment: must match the canonical `[path]` β so changes are intentional, not forgotten.
|
|
602
|
+
>
|
|
603
|
+
> π‘ **Lesson learned:** Implementing a "Hold" or "Pause" tier alongside standard paid tiers:
|
|
604
|
+
>
|
|
605
|
+
> 1. Add the hold tier to your PLAN_LIMITS with 0 for all action counters but a HIGH maxItems value (e.g., 999). The high item limit preserves the user's existing data; the zero action limits block all expensive operations.
|
|
606
|
+
> 2. Add a dedicated `isHoldPlan(user)` helper. Use it for creation-blocking checks (e.g., "can add new item?") because the limit-based check alone won't catch it β maxItems is intentionally high.
|
|
607
|
+
> 3. The hold tier uses the same Stripe product/price/webhook plumbing as every other tier. No special Stripe logic needed β just another price ID mapped in `priceIdToPlan()`.
|
|
608
|
+
> 4. For `limitReachedBody()` (the 402 error response), check the plan and return a hold-specific message ("Your account is paused. Resume to use this feature.") instead of the generic limit-reached wording. The client can then render contextual upgrade UI based on the plan field.
|
|
609
|
+
> 5. Counter resets still run for hold users (harmless). When they resume to a paid plan via Stripe Checkout, the checkout webhook sets the new plan and resets counters normally.
|
|
610
|
+
>
|
|
611
|
+
> π‘ **Lesson learned:** Replacing "free tier" with "expired" state:
|
|
612
|
+
>
|
|
613
|
+
> 1. After trial ends, `getEffectivePlan()` returns `'expired'` (not `'free'`). The expired tier has ALL limits at 0 β no AI features, no new item creation.
|
|
614
|
+
> 2. `limitReachedBody()` needs 3-way messaging: hold ("Account paused"), expired ("Trial ended β subscribe or hold to preserve data"), standard limit-reached ("Upgrade for more").
|
|
615
|
+
> 3. Webhook `subscription.deleted` sets plan to `'expired'`, not `'free'`. Same for `priceIdToPlan()` fallback.
|
|
616
|
+
> 4. Client-side: add `isExpired` getter alongside `isHold`, `isPaid`. BillingSection shows red banner for expired, amber for hold.
|
|
617
|
+
> 5. Data retention window (e.g., 30 days) after expiration before archival. Hold ($10/mo) is the only path to indefinite data preservation without a full subscription.
|
|
618
|
+
>
|
|
619
|
+
> π‘ **Lesson learned:** Inline usage visibility (UsageBadge pattern):
|
|
620
|
+
>
|
|
621
|
+
> 1. Create a reusable component that shows "X / Y" usage counts inline near each feature's trigger (button, section header).
|
|
622
|
+
> 2. Component takes a counter name, derives current usage and max limit from the reactive entitlements store.
|
|
623
|
+
> 3. Color coding: muted (normal), amber (β₯70%), red (at limit or blocked). Hidden when unlimited (β₯999).
|
|
624
|
+
> 4. At max=0 (expired/hold): show "Subscribe to unlock" link to billing. At limit: show "X/Y β Upgrade" link. Normal: plain "X/Y".
|
|
625
|
+
> 5. Place badges next to EVERY feature gated by plan limits β users should never be surprised by a 402 error.
|
|
626
|
+
|
|
627
|
+
### Stripe Integration
|
|
628
|
+
|
|
629
|
+
[Products, prices, webhooks, customer portal. How the payment flow works end to end.]
|
|
630
|
+
|
|
631
|
+
### Dynamic Pricing
|
|
632
|
+
|
|
633
|
+
> π‘ **Lesson learned:** Display prices should come from Stripe, not hardcoded strings. Pattern:
|
|
634
|
+
> 1. Server-side module calls stripe.prices.retrieve() for each price ID, caches results (1 hour TTL)
|
|
635
|
+
> 2. Root layout server load includes pricing in returned data
|
|
636
|
+
> 3. Components read prices from page data ($page.data.pricing or data.pricing)
|
|
637
|
+
> 4. Fallback values in components handle Stripe-unreachable gracefully
|
|
638
|
+
>
|
|
639
|
+
> This means changing prices in Stripe Dashboard propagates to the app within the cache TTL, no redeploy needed.
|
|
640
|
+
|
|
641
|
+
[How are display prices fetched and cached? Where do components get price data?]
|
|
642
|
+
|
|
643
|
+
## Promo Code System
|
|
644
|
+
|
|
645
|
+
_If your app supports promotional discounts. Fill in during Phase 7._
|
|
646
|
+
|
|
647
|
+
### Promo Code Architecture
|
|
648
|
+
|
|
649
|
+
[How promo codes are stored, validated, and applied.]
|
|
650
|
+
|
|
651
|
+
> π‘ **Lesson learned:** Promo code system implementation pattern:
|
|
652
|
+
>
|
|
653
|
+
> 1. Data model: Create a promo_codes collection with fields:
|
|
654
|
+
> - code (text, unique, uppercase-normalized)
|
|
655
|
+
> - discountType: 'percent' | 'fixed' | 'trial_extension'
|
|
656
|
+
> - discountValue: number (percentage 0-100, or fixed amount in cents, or trial days)
|
|
657
|
+
> - maxRedemptions: number (0 = unlimited)
|
|
658
|
+
> - currentRedemptions: number (incremented atomically on use)
|
|
659
|
+
> - expiresAt: datetime (null = never)
|
|
660
|
+
> - active: boolean
|
|
661
|
+
> - stripePromotionId: text (optional β maps to a Stripe Promotion Code for Checkout integration)
|
|
662
|
+
> - Create a separate promo_redemptions collection: { user, promo_code, redeemedAt } to track per-user usage and prevent reuse.
|
|
663
|
+
>
|
|
664
|
+
> 2. Validation flow (server-side):
|
|
665
|
+
> a. Normalize code to uppercase, trim whitespace.
|
|
666
|
+
> b. Look up code in promo_codes collection. If not found β 404.
|
|
667
|
+
> c. Check active === true. If not β 410 "Code is no longer active."
|
|
668
|
+
> d. Check expiresAt (if set) > now. If expired β 410 "Code has expired."
|
|
669
|
+
> e. Check currentRedemptions < maxRedemptions (if maxRedemptions > 0). If exhausted β 410 "Code has reached its maximum uses."
|
|
670
|
+
> f. Check promo_redemptions for existing record with this user + code. If found β 409 "You've already used this code."
|
|
671
|
+
> g. Return the validated promo details to the client.
|
|
672
|
+
>
|
|
673
|
+
> 3. Stripe integration:
|
|
674
|
+
> - For percent/fixed discounts: Create a Stripe Promotion Code (via Stripe Dashboard or API) and store the ID in stripePromotionId.
|
|
675
|
+
> - When creating a Checkout Session, pass the promotion code via `discounts: [{ promotion_code: promo.stripePromotionId }]`.
|
|
676
|
+
> - For trial extensions: Don't use Stripe promotion codes. Instead, pass `subscription_data: { trial_period_days: promo.discountValue }` to the Checkout Session.
|
|
677
|
+
>
|
|
678
|
+
> 4. Redemption (atomic):
|
|
679
|
+
> - AFTER successful Stripe checkout (in the webhook handler), create the promo_redemptions record AND increment currentRedemptions.
|
|
680
|
+
> - Do NOT redeem on validation β only on successful payment. Users who validate but don't complete checkout shouldn't consume redemption slots.
|
|
681
|
+
>
|
|
682
|
+
> 5. Client-side UX:
|
|
683
|
+
> - Add a "Have a promo code?" toggle on the billing/upgrade UI. Don't show the input field by default (it anchors users on finding a discount).
|
|
684
|
+
> - On validation success, show the discount details and auto-apply to the Checkout flow.
|
|
685
|
+
> - Store the validated promo code ID in a brief cookie or session state so it survives the redirect to Stripe Checkout and back.
|
|
686
|
+
>
|
|
687
|
+
> 6. Anti-pattern: Don't validate and redeem promo codes in the same step. Validation is "can this code be used?" β redemption is "this code WAS used." They happen at different points in the checkout flow.
|
|
688
|
+
|
|
689
|
+
### API Routes
|
|
690
|
+
|
|
691
|
+
| Method | Route | Purpose |
|
|
692
|
+
| ------ | --------------------- | ------------------------------------------ |
|
|
693
|
+
| [POST] | [/api/promo/validate] | [Validate a code, return discount details] |
|
|
694
|
+
|
|
695
|
+
## Input Validation
|
|
696
|
+
|
|
697
|
+
_Centralized validation strategy for all user input._
|
|
698
|
+
|
|
699
|
+
### Validation Architecture
|
|
700
|
+
|
|
701
|
+
[How is input validated? Where do schemas live? What library is used?]
|
|
702
|
+
|
|
703
|
+
> π‘ **Lesson learned:** Centralized Zod validation pattern:
|
|
704
|
+
>
|
|
705
|
+
> 1. Create a single validation module ($lib/server/validation.ts) with:
|
|
706
|
+
> - All Zod schemas for every API route's expected input
|
|
707
|
+
> - A parseBody(request, schema) helper that: reads request.json(), runs schema.safeParse(), returns { success, data, error }
|
|
708
|
+
> - On validation failure, return a 400 with the Zod error formatted as a human-readable message
|
|
709
|
+
>
|
|
710
|
+
> 2. Every API route's first action: const { data, error } = await parseBody(request, mySchema). If error, return 400 immediately. No business logic runs on unvalidated input.
|
|
711
|
+
>
|
|
712
|
+
> 3. Schema design:
|
|
713
|
+
> - Use z.string().trim() for all text fields (prevents whitespace-only submissions)
|
|
714
|
+
> - Use z.string().max(N) for fields that map to database columns with size limits
|
|
715
|
+
> - Use z.enum([...]) for status fields, plan names, and other fixed-value fields
|
|
716
|
+
> - Use z.coerce.number() for numeric inputs that arrive as strings (common in form data)
|
|
717
|
+
> - Export schemas so they can be shared with client-side validation if needed
|
|
718
|
+
>
|
|
719
|
+
> 4. Benefits over ad-hoc validation:
|
|
720
|
+
> - One place to audit all input contracts
|
|
721
|
+
> - Automatic TypeScript type inference (z.infer<typeof schema>)
|
|
722
|
+
> - Consistent error format across all routes
|
|
723
|
+
> - No "forgot to validate" bugs β the pattern is obvious and repeatable
|
|
724
|
+
>
|
|
725
|
+
> 5. Anti-pattern: Don't validate inside business logic functions. Validate at the API boundary (the route handler), then pass clean, typed data to business logic. Business logic should trust its inputs.
|
|
726
|
+
|
|
727
|
+
## Product Analytics
|
|
728
|
+
|
|
729
|
+
_Instrument user behavior to validate the product works (SM0) and track growth metrics._
|
|
730
|
+
|
|
731
|
+
> π‘ **Lesson learned:** PostHog Cloud is the recommended default for product analytics.
|
|
732
|
+
>
|
|
733
|
+
> Architecture:
|
|
734
|
+
> - Client module: $lib/posthog.ts β thin wrapper. Exports initPostHog(), identifyUser(), resetUser(), trackEvent(), trackPageView().
|
|
735
|
+
> - Initialization: Root +layout.svelte calls initPostHog() in onMount, then identifies the user from server layout data.
|
|
736
|
+
> - SPA page views: Tracked via afterNavigate (disable PostHog's capture_pageview to avoid double-counting).
|
|
737
|
+
> - CSP: Add PostHog domains to connect-src and script-src in hooks.server.ts.
|
|
738
|
+
> - Env vars: PUBLIC_POSTHOG_KEY and PUBLIC_POSTHOG_HOST. If key is empty, all tracking silently no-ops.
|
|
739
|
+
>
|
|
740
|
+
> Key events to track (adapt to your app):
|
|
741
|
+
> - user_signed_up, user_logged_in
|
|
742
|
+
> - [core_action_completed] (the "aha moment")
|
|
743
|
+
> - subscription_started
|
|
744
|
+
> - [milestone_reached] (the success/celebration moment)
|
|
745
|
+
>
|
|
746
|
+
> Define your SM0 funnel: the sequence of events that proves a new user got value from the product.
|
|
747
|
+
### First-party / reverse-proxy ingest (optional)
|
|
748
|
+
|
|
749
|
+
> π§ **Guidance:** When analytics HTTP is routed through **your own origin** (reverse proxy) for CSP tightness, privacy posture, or ad-block resilience, document the **proxy path**, **upstream host env**, and how **`PUBLIC_*`** client config matches the path usersβ browsers hit. Verify in staging that events arrive; misaligned host/base URLs often fail **silently**.
|
|
750
|
+
|
|
751
|
+
### Digest, labs, or experimental analytics panels (optional)
|
|
752
|
+
|
|
753
|
+
[When you ship a **digest**, **labs**, or **experiments** area that runs **secondary analysis** β clustering, grouping, novelty summaries β on data the user already owns, distinct from core CRUD.]
|
|
754
|
+
|
|
755
|
+
> π§ **Guidance:** Document **routes**, **entitlements** (plan gates vs internal-only), and whether results are **persisted** or **computed on demand**. Expose APIs with **capability-based** names; keep model/provider choice in **configuration**, not in path segments. If sub-views are shareable, sync **URL query state** for tabs or filters. Reuse your **central failure / observability** helpers for errors; sample or strip PII in analytics payloads.
|
|
756
|
+
|
|
757
|
+
> π‘ **Lesson learned:** Treat outputs as **suggestions** unless the product explicitly auto-writes records. A **lightweight** secondary model pass is often enough for exploratory grouping when cost must stay bounded; surface uncertainty in the UI when decisions are high-stakes.
|
|
758
|
+
|
|
759
|
+
### Server-Side Failure Observability
|
|
760
|
+
|
|
761
|
+
> π‘ **Lesson learned:** Most server-side failures return 4xx JSON and only `console.error`. They never reach your analytics dashboard unless they throw an uncaught 500. This creates a blind spot: LLM parse failures, billing webhook mismatches, auth errors, and file-processing problems are invisible in PostHog while being the failures users actually encounter.
|
|
762
|
+
>
|
|
763
|
+
> **Pattern: centralized failure logging**
|
|
764
|
+
>
|
|
765
|
+
> 1. Create a **low-level server capture module** (`posthogServer.ts`): a single `captureServerEvent(distinctId, event, properties)` function that POSTs to PostHog's `/capture/` endpoint. Fire-and-forget (never block the response). All server PostHog events flow through this one function.
|
|
766
|
+
>
|
|
767
|
+
> 2. Create a **failure logging module** (`failureLog.ts`) that wraps it:
|
|
768
|
+
> - `logServerFailure({ userId, category, operation, error, path, httpStatus, extra })` β generates a UUID `debugId`, writes tagged `console.error` with JSON payload, fires PostHog event, returns the `debugId`.
|
|
769
|
+
> - The `debugId` can be included in the API response for support ticket correlation.
|
|
770
|
+
> - Error messages are clipped (2000 chars), extra values clipped (500 chars). Never send full HTML, request bodies, or credentials.
|
|
771
|
+
>
|
|
772
|
+
> 3. **Failure categories map to PostHog event names:**
|
|
773
|
+
> - `llm_parse` β `server_failure_llm_parse` (LLM returned unparseable output)
|
|
774
|
+
> - `llm_call` β `server_failure_llm_call` (LLM API itself failed)
|
|
775
|
+
> - `auth` β `server_failure_auth`
|
|
776
|
+
> - `billing` β `server_failure_billing`
|
|
777
|
+
> - `file_operation` β `server_failure_file`
|
|
778
|
+
> - `data_persistence` β `server_failure_data`
|
|
779
|
+
> - `external_api` β `server_failure_external`
|
|
780
|
+
> - `general` β `server_failure`
|
|
781
|
+
>
|
|
782
|
+
> 4. **Convenience wrappers** for high-volume categories. Example: `logLLMParseFailure()` reads the LLM tracking context (AsyncLocalStorage) for `userId` and `operation`, so call sites only pass the error and a raw output preview.
|
|
783
|
+
>
|
|
784
|
+
> 5. **Domain-specific failure modules** when the category needs extra diagnostics. Example: scrape failures warrant response-fingerprinting (Cloudflare detection, captcha heuristics, content-length checks) that don't belong in the general module. Keep the specialized module (`scrapeFailureLog.ts`) and the general one (`failureLog.ts`) separate.
|
|
785
|
+
>
|
|
786
|
+
> 6. **Refactor `handleError` hooks** to use the shared `captureServerEvent` β don't duplicate the PostHog HTTP POST inline.
|
|
787
|
+
>
|
|
788
|
+
> 7. **What NOT to track:** Expected 400s the user can self-correct (bad email format, missing required field). Track failures that indicate bugs, infrastructure problems, or degraded service.
|
|
789
|
+
>
|
|
790
|
+
> 8. **Instrumentation priority:**
|
|
791
|
+
> - Tier 1 (ship immediately): LLM parse failures, billing/webhook failures, auth failures, scrape failures.
|
|
792
|
+
> - Tier 2 (instrument when touching the area): file uploads/processing, tailoring route failures, external API errors.
|
|
793
|
+
> - Tier 3 (low priority): admin routes, internal tooling.
|
|
794
|
+
>
|
|
795
|
+
> π **Example:** In a `catch` block that already has `console.error`:
|
|
796
|
+
> ```typescript
|
|
797
|
+
> } catch (e) {
|
|
798
|
+
> console.error('[myRoute] Failed:', e);
|
|
799
|
+
> logServerFailure({ userId: user.id, category: 'data_persistence', operation: 'saveNote', error: e, path: '/api/notes' });
|
|
800
|
+
> return json({ message: 'Failed' }, { status: 500 });
|
|
801
|
+
> }
|
|
802
|
+
> ```
|
|
803
|
+
|
|
804
|
+
### Setup
|
|
805
|
+
|
|
806
|
+
[PostHog Cloud or self-hosted? Env var names? CSP additions?]
|
|
807
|
+
|
|
808
|
+
### Tracked Events
|
|
809
|
+
|
|
810
|
+
| Event | Location | Properties |
|
|
811
|
+
| ------------ | ---------------- | ----------------- |
|
|
812
|
+
| [event_name] | [file/component] | [key: value, ...] |
|
|
813
|
+
|
|
814
|
+
### SM0 Funnel
|
|
815
|
+
|
|
816
|
+
[The primary funnel that validates whether the product works. e.g., signup β core_action β value_delivered]
|
|
817
|
+
|
|
818
|
+
### NPS Survey (SM1)
|
|
819
|
+
|
|
820
|
+
[In-app NPS survey implementation. Fill in once you build it.]
|
|
821
|
+
|
|
822
|
+
> π‘ **Lesson learned:** NPS implementation pattern:
|
|
823
|
+
>
|
|
824
|
+
> Architecture:
|
|
825
|
+
> - Component: A fixed-position toast (bottom-right) with 3 phases: score β qualitative reason β thank-you auto-dismiss.
|
|
826
|
+
> - API: GET /api/nps?trigger= checks if user already responded. POST /api/nps submits score + reason. Prevents duplicates (409).
|
|
827
|
+
> - Data: PocketBase collection `nps_responses` (score: number 0-10, reason: text, trigger: select, user: relation). Immutable records.
|
|
828
|
+
> - Validation: Zod schemas for both check (trigger enum) and submit (score 0-10, reason optional, trigger enum).
|
|
829
|
+
> - Triggers: Feature components dispatch window CustomEvents (e.g., 'app:feature-done'). Main page listens, checks eligibility, shows survey.
|
|
830
|
+
> - PostHog: Track nps_submitted (score, trigger) and nps_dismissed (trigger) for response rate analysis.
|
|
831
|
+
>
|
|
832
|
+
> Key decisions:
|
|
833
|
+
> - In-app toast, not email β catches users in context, much higher response rate.
|
|
834
|
+
> - One response per trigger per user β server enforces via DB query before showing.
|
|
835
|
+
> - Immutable records β no update/delete API rules. Users can't change scores.
|
|
836
|
+
> - Decoupled via window events β NPS logic lives in the page, not inside feature components.
|