@growth-labs/cms 0.5.8 → 0.5.10

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.
Files changed (48) hide show
  1. package/README.md +26 -0
  2. package/dist/engine/content-insights.d.ts +11 -4
  3. package/dist/engine/content-insights.d.ts.map +1 -1
  4. package/dist/engine/content-insights.js +22 -7
  5. package/dist/engine/content-insights.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/routes/content-insights.d.ts +1 -1
  11. package/dist/routes/content-insights.d.ts.map +1 -1
  12. package/dist/routes/content-insights.js +20 -5
  13. package/dist/routes/content-insights.js.map +1 -1
  14. package/dist/schema/index.d.ts +1 -0
  15. package/dist/schema/index.d.ts.map +1 -1
  16. package/dist/schema/index.js +1 -0
  17. package/dist/schema/index.js.map +1 -1
  18. package/dist/schema/insights-ingest.d.ts +2386 -0
  19. package/dist/schema/insights-ingest.d.ts.map +1 -1
  20. package/dist/schema/insights-ingest.js +78 -0
  21. package/dist/schema/insights-ingest.js.map +1 -1
  22. package/dist/schema/migrations.d.ts.map +1 -1
  23. package/dist/schema/migrations.js +9 -0
  24. package/dist/schema/migrations.js.map +1 -1
  25. package/dist/schema/types.d.ts +67 -0
  26. package/dist/schema/types.d.ts.map +1 -1
  27. package/dist/schema/types.js +10 -0
  28. package/dist/schema/types.js.map +1 -1
  29. package/dist/surveys/schema.d.ts +48 -48
  30. package/dist/ui/screens/ContentInsightsScreen.d.ts +28 -0
  31. package/dist/ui/screens/ContentInsightsScreen.d.ts.map +1 -1
  32. package/dist/ui/screens/ContentInsightsScreen.js +111 -36
  33. package/dist/ui/screens/ContentInsightsScreen.js.map +1 -1
  34. package/dist/ui/screens/content-insights-data.d.ts +13 -1
  35. package/dist/ui/screens/content-insights-data.d.ts.map +1 -1
  36. package/dist/ui/screens/content-insights-data.js +29 -1
  37. package/dist/ui/screens/content-insights-data.js.map +1 -1
  38. package/migrations/0022_content_insight_dismissal_reasons.sql +8 -0
  39. package/package.json +1 -1
  40. package/src/engine/content-insights.ts +32 -7
  41. package/src/index.ts +2 -0
  42. package/src/routes/content-insights.ts +31 -5
  43. package/src/schema/index.ts +1 -0
  44. package/src/schema/insights-ingest.ts +94 -0
  45. package/src/schema/migrations.ts +9 -0
  46. package/src/schema/types.ts +92 -0
  47. package/src/ui/screens/ContentInsightsScreen.tsx +416 -195
  48. package/src/ui/screens/content-insights-data.ts +40 -2
@@ -13,6 +13,69 @@ import { z } from 'zod'
13
13
  // replay — run_attempt is the monotonic guard.
14
14
 
15
15
  // Reusable evidence/brief JSON shapes (stored as TEXT NOT NULL, zod-validated).
16
+ export const InsightLaneSchema = z.enum([
17
+ 'commission',
18
+ 'refresh',
19
+ 'optimize-ctr',
20
+ 'consolidate',
21
+ 'monitor',
22
+ ])
23
+
24
+ export const InsightScoreBreakdownSchema = z.object({
25
+ actualCtr: z.number().min(0).max(1),
26
+ expectedCtr: z.number().min(0).max(1),
27
+ expectedClicksGain: z.number().nonnegative(),
28
+ laneWeight: z.number().min(0).max(1),
29
+ score: z.number().nonnegative(),
30
+ targetPosition: z.number().nonnegative(),
31
+ topicalFit: z.number().min(0).max(1),
32
+ winnabilityPrior: z.number().min(0).max(1),
33
+ })
34
+
35
+ export const InsightJudgeReasonCodeSchema = z.enum([
36
+ 'no_coherent_intent',
37
+ 'navigational_or_entity_locked',
38
+ 'off_topic',
39
+ 'already_covered',
40
+ 'unwinnable',
41
+ 'unsustainable_format',
42
+ 'too_thin',
43
+ 'accept',
44
+ ])
45
+
46
+ export const InsightJudgeVerdictSchema = z.enum(['accept', 'reject', 'abstain'])
47
+
48
+ export const InsightWinnabilityEvidenceSchema = z.object({
49
+ verdict: InsightJudgeVerdictSchema,
50
+ confidence: z.number().min(0).max(1),
51
+ serp_summary: z.string().min(1),
52
+ })
53
+
54
+ export const InsightJudgeEvidenceSchema = z.object({
55
+ verdict: InsightJudgeVerdictSchema,
56
+ reason_code: InsightJudgeReasonCodeSchema,
57
+ confidence: z.number().min(0).max(1),
58
+ intent_statement: z.string().min(1),
59
+ winnability: InsightWinnabilityEvidenceSchema.optional(),
60
+ })
61
+
62
+ export const InsightSemanticEvidenceSchema = z.object({
63
+ topicalFit: z.number().min(0).max(1),
64
+ disposition: z.enum(['reject', 'judge_only', 'advance']),
65
+ coverageSimilarity: z.number().min(-1).max(1),
66
+ coverageAligned: z.boolean(),
67
+ sharedConcepts: z.number().int().nonnegative(),
68
+ nearest: z
69
+ .array(
70
+ z.object({
71
+ title: z.string().min(1),
72
+ slug: z.string().min(1),
73
+ similarity: z.number().min(-1).max(1),
74
+ }),
75
+ )
76
+ .max(3),
77
+ })
78
+
16
79
  export const InsightEvidenceDemandSchema = z.object({
17
80
  query: z.string().min(1),
18
81
  impressions: z.number().int().nonnegative(),
@@ -20,11 +83,21 @@ export const InsightEvidenceDemandSchema = z.object({
20
83
  ctr: z.number().min(0).max(1),
21
84
  position: z.number().nonnegative(),
22
85
  trend: z.number(),
86
+ lane: InsightLaneSchema.optional(),
87
+ coveredBy: z.string().url().nullable().optional(),
88
+ coverageSource: z.enum(['page_join', 'semantic', 'lexical']).nullable().optional(),
89
+ scoreBreakdown: InsightScoreBreakdownSchema.optional(),
90
+ judge: InsightJudgeEvidenceSchema.optional(),
91
+ semantic: InsightSemanticEvidenceSchema.optional(),
23
92
  })
24
93
 
25
94
  export const InsightEvidenceOpportunitySchema = z.object({
26
95
  basis: z.string().min(1),
27
96
  source_count: z.number().int().nonnegative(),
97
+ lane: InsightLaneSchema.optional(),
98
+ scoreBreakdown: InsightScoreBreakdownSchema.optional(),
99
+ judge: InsightJudgeEvidenceSchema.optional(),
100
+ semantic: InsightSemanticEvidenceSchema.optional(),
28
101
  })
29
102
 
30
103
  export const InsightEvidenceSchema = z.union([
@@ -55,6 +128,22 @@ export const InsightBriefSchema = z.object({
55
128
  url: z.string().url(),
56
129
  }),
57
130
  ),
131
+ format: z.enum(['cornerstone', 'pillar', 'standalone']).optional(),
132
+ content_outline: z
133
+ .array(
134
+ z.object({
135
+ question: z.string().min(1),
136
+ demand: z
137
+ .object({
138
+ query: z.string().min(1),
139
+ impressions: z.number().int().nonnegative(),
140
+ position: z.number().nonnegative(),
141
+ trend: z.number(),
142
+ })
143
+ .optional(),
144
+ }),
145
+ )
146
+ .optional(),
58
147
  /**
59
148
  * Which open-web discovery STREAM produced this opportunity row (the theater
60
149
  * split). REQUIRED here: Zod strips unknown brief keys, so without this field the
@@ -124,3 +213,8 @@ export const InsightsIngestPayloadSchema = z
124
213
  export type InsightsIngestPayload = z.infer<typeof InsightsIngestPayloadSchema>
125
214
  export type InsightsIngestMeta = z.infer<typeof InsightsIngestMetaSchema>
126
215
  export type InsightSuggestionRowPayload = z.infer<typeof InsightSuggestionRowSchema>
216
+ export type InsightLane = z.infer<typeof InsightLaneSchema>
217
+ export type InsightScoreBreakdown = z.infer<typeof InsightScoreBreakdownSchema>
218
+ export type InsightJudgeReasonCode = z.infer<typeof InsightJudgeReasonCodeSchema>
219
+ export type InsightJudgeVerdict = z.infer<typeof InsightJudgeVerdictSchema>
220
+ export type InsightJudgeEvidence = z.infer<typeof InsightJudgeEvidenceSchema>
@@ -802,6 +802,15 @@ WHERE operation = 'publish' AND target_published_at IS NULL;
802
802
  sql: `
803
803
  CREATE INDEX IF NOT EXISTS idx_content_revisions_published_slug
804
804
  ON content_revisions(json_extract(payload_json, '$.item.slug'));
805
+ `,
806
+ },
807
+ {
808
+ id: '0022_content_insight_dismissal_reasons',
809
+ sql: `
810
+ ALTER TABLE content_insight_dismissals
811
+ ADD COLUMN reason_code TEXT CHECK (reason_code IS NULL OR reason_code IN ('off_topic', 'already_covered', 'cant_win', 'wrong_format', 'not_now', 'other'));
812
+ ALTER TABLE content_insight_dismissals
813
+ ADD COLUMN note TEXT CHECK (note IS NULL OR length(note) <= 500);
805
814
  `,
806
815
  },
807
816
  ]
@@ -9,6 +9,8 @@
9
9
  // Nullability: a column declared NOT NULL maps to a required, non-nullable
10
10
  // field; a nullable column maps to `T | null` (D1 returns SQL NULL as JS null).
11
11
 
12
+ import { z } from 'zod'
13
+
12
14
  /** content_items.type CHECK domain. */
13
15
  export type ContentType = 'article' | 'video' | 'podcast' | 'newsletter' | 'page'
14
16
 
@@ -417,6 +419,60 @@ export type InsightRunStatus = 'running' | 'completed' | 'partial' | 'failed'
417
419
  /** Beat facet — secondary topical facet (geographic theater lives in desk). */
418
420
  export type InsightBeat = 'defense' | 'intelligence' | 'diplomacy' | 'analysis'
419
421
 
422
+ /** Insight Engine v2 editorial action lane. */
423
+ export type InsightLane = 'commission' | 'refresh' | 'optimize-ctr' | 'consolidate' | 'monitor'
424
+
425
+ /** Bounded scoring inputs carried with Insight Engine v2 evidence. */
426
+ export interface InsightScoreBreakdown {
427
+ actualCtr: number
428
+ expectedCtr: number
429
+ expectedClicksGain: number
430
+ laneWeight: number
431
+ score: number
432
+ targetPosition: number
433
+ topicalFit: number
434
+ winnabilityPrior: number
435
+ }
436
+
437
+ export type InsightJudgeReasonCode =
438
+ | 'no_coherent_intent'
439
+ | 'navigational_or_entity_locked'
440
+ | 'off_topic'
441
+ | 'already_covered'
442
+ | 'unwinnable'
443
+ | 'unsustainable_format'
444
+ | 'too_thin'
445
+ | 'accept'
446
+
447
+ export type InsightJudgeVerdict = 'accept' | 'reject' | 'abstain'
448
+
449
+ export interface InsightWinnabilityEvidence {
450
+ verdict: InsightJudgeVerdict
451
+ confidence: number
452
+ serp_summary: string
453
+ }
454
+
455
+ export interface InsightJudgeEvidence {
456
+ verdict: InsightJudgeVerdict
457
+ reason_code: InsightJudgeReasonCode
458
+ confidence: number
459
+ intent_statement: string
460
+ winnability?: InsightWinnabilityEvidence
461
+ }
462
+
463
+ export interface InsightSemanticEvidence {
464
+ topicalFit: number
465
+ disposition: 'reject' | 'judge_only' | 'advance'
466
+ coverageSimilarity: number
467
+ coverageAligned: boolean
468
+ sharedConcepts: number
469
+ nearest: Array<{
470
+ title: string
471
+ slug: string
472
+ similarity: number
473
+ }>
474
+ }
475
+
420
476
  /** Demand evidence chip (signal_type='demand'): the real GSC metric. */
421
477
  export interface InsightEvidenceDemand {
422
478
  query: string
@@ -428,6 +484,12 @@ export interface InsightEvidenceDemand {
428
484
  position: number
429
485
  /** Impression delta vs the prior window (signed). */
430
486
  trend: number
487
+ lane?: InsightLane
488
+ coveredBy?: string | null
489
+ coverageSource?: 'page_join' | 'semantic' | 'lexical' | null
490
+ scoreBreakdown?: InsightScoreBreakdown
491
+ judge?: InsightJudgeEvidence
492
+ semantic?: InsightSemanticEvidence
431
493
  }
432
494
 
433
495
  /** Opportunity evidence chip (signal_type='opportunity'): validated-research basis. */
@@ -436,6 +498,10 @@ export interface InsightEvidenceOpportunity {
436
498
  basis: string
437
499
  /** Count of cite-or-cut validated sources backing the idea. */
438
500
  source_count: number
501
+ lane?: InsightLane
502
+ scoreBreakdown?: InsightScoreBreakdown
503
+ judge?: InsightJudgeEvidence
504
+ semantic?: InsightSemanticEvidence
439
505
  }
440
506
 
441
507
  /** content_insights.evidence — JSON stored as TEXT NOT NULL, zod-validated. */
@@ -463,6 +529,16 @@ export interface InsightBrief {
463
529
  spokes: string[]
464
530
  }
465
531
  sources: Array<{ title: string; url: string }>
532
+ format?: 'cornerstone' | 'pillar' | 'standalone'
533
+ content_outline?: Array<{
534
+ question: string
535
+ demand?: {
536
+ query: string
537
+ impressions: number
538
+ position: number
539
+ trend: number
540
+ }
541
+ }>
466
542
  /**
467
543
  * Which open-web discovery STREAM produced this opportunity row (the theater
468
544
  * split): 'competitor' (RSS competitor-watch) vs 'audience_question' (Google
@@ -532,12 +608,28 @@ export interface ContentInsightRunRow {
532
608
  error: string | null
533
609
  }
534
610
 
611
+ /** Editor-owned dismissal taxonomy (migration 0022). */
612
+ export const InsightDismissalReasonCodeSchema = z.enum([
613
+ 'off_topic',
614
+ 'already_covered',
615
+ 'cant_win',
616
+ 'wrong_format',
617
+ 'not_now',
618
+ 'other',
619
+ ])
620
+
621
+ export type InsightDismissalReasonCode = z.infer<typeof InsightDismissalReasonCodeSchema>
622
+
535
623
  /** A content_insight_dismissals row (PK (site_id, intent_key)). */
536
624
  export interface ContentInsightDismissalRow {
537
625
  site_id: string
538
626
  intent_key: string
539
627
  dismissed_at: number
540
628
  dismissed_by: string | null
629
+ /** NULL is a pre-0022 legacy dismissal. */
630
+ reason_code: InsightDismissalReasonCode | null
631
+ /** NULL is either legacy or an editor dismissal without a note. */
632
+ note: string | null
541
633
  }
542
634
 
543
635
  export type ContentPublicationOperation = 'publish' | 'rollback'