@topy-ai/maggie 0.7.33 → 0.7.34

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 (138) hide show
  1. package/README-zh-TW.md +2 -2
  2. package/README.md +16 -3
  3. package/bundled-skills/maggie-blog-bootstrap/SKILL.md +8 -0
  4. package/bundled-skills/maggie-clone/SKILL.md +7 -1
  5. package/bundled-templates/astro-blog/README.md +217 -0
  6. package/bundled-templates/astro-blog/examples/blog.config.ts +30 -0
  7. package/bundled-templates/astro-blog/examples/content.config.ts +28 -0
  8. package/bundled-templates/astro-blog/examples/metadata.ts +37 -0
  9. package/bundled-templates/astro-blog/examples/ops-api.ts +31 -0
  10. package/bundled-templates/astro-blog/examples/route-manifest.ts +21 -0
  11. package/bundled-templates/astro-blog/starter/.env.example +36 -0
  12. package/bundled-templates/astro-blog/starter/README.md +168 -0
  13. package/bundled-templates/astro-blog/starter/astro.config.mjs +19 -0
  14. package/bundled-templates/astro-blog/starter/db/schema.sql +508 -0
  15. package/bundled-templates/astro-blog/starter/package-lock.json +8039 -0
  16. package/bundled-templates/astro-blog/starter/package.json +41 -0
  17. package/bundled-templates/astro-blog/starter/scripts/contract-check.mjs +16 -0
  18. package/bundled-templates/astro-blog/starter/scripts/fixture-seed.mjs +11 -0
  19. package/bundled-templates/astro-blog/starter/scripts/schema-check.mjs +13 -0
  20. package/bundled-templates/astro-blog/starter/scripts/seed-db.mjs +20 -0
  21. package/bundled-templates/astro-blog/starter/src/data/demo.ts +52 -0
  22. package/bundled-templates/astro-blog/starter/src/layouts/BaseLayout.astro +53 -0
  23. package/bundled-templates/astro-blog/starter/src/layouts/OpsLayout.astro +73 -0
  24. package/bundled-templates/astro-blog/starter/src/lib/ai-cmo.ts +14 -0
  25. package/bundled-templates/astro-blog/starter/src/lib/config.ts +15 -0
  26. package/bundled-templates/astro-blog/starter/src/lib/db.ts +95 -0
  27. package/bundled-templates/astro-blog/starter/src/lib/google.ts +28 -0
  28. package/bundled-templates/astro-blog/starter/src/lib/media.ts +12 -0
  29. package/bundled-templates/astro-blog/starter/src/lib/migration-validation.ts +11 -0
  30. package/bundled-templates/astro-blog/starter/src/lib/migration.ts +21 -0
  31. package/bundled-templates/astro-blog/starter/src/lib/ops-auth.ts +21 -0
  32. package/bundled-templates/astro-blog/starter/src/lib/providers/email.ts +45 -0
  33. package/bundled-templates/astro-blog/starter/src/lib/providers/media-storage.ts +37 -0
  34. package/bundled-templates/astro-blog/starter/src/lib/report-delivery.ts +26 -0
  35. package/bundled-templates/astro-blog/starter/src/lib/report-pdf.ts +49 -0
  36. package/bundled-templates/astro-blog/starter/src/lib/repository.ts +72 -0
  37. package/bundled-templates/astro-blog/starter/src/lib/seo-audit.ts +72 -0
  38. package/bundled-templates/astro-blog/starter/src/lib/seo-insights.ts +14 -0
  39. package/bundled-templates/astro-blog/starter/src/lib/seo.ts +33 -0
  40. package/bundled-templates/astro-blog/starter/src/lib/types.ts +58 -0
  41. package/bundled-templates/astro-blog/starter/src/lib/wordpress.ts +847 -0
  42. package/bundled-templates/astro-blog/starter/src/middleware.ts +7 -0
  43. package/bundled-templates/astro-blog/starter/src/pages/404.astro +11 -0
  44. package/bundled-templates/astro-blog/starter/src/pages/about.astro +15 -0
  45. package/bundled-templates/astro-blog/starter/src/pages/api/approval/[token].ts +25 -0
  46. package/bundled-templates/astro-blog/starter/src/pages/api/consent.ts +8 -0
  47. package/bundled-templates/astro-blog/starter/src/pages/api/ops/agency.ts +47 -0
  48. package/bundled-templates/astro-blog/starter/src/pages/api/ops/authors.ts +11 -0
  49. package/bundled-templates/astro-blog/starter/src/pages/api/ops/bulk.ts +58 -0
  50. package/bundled-templates/astro-blog/starter/src/pages/api/ops/calendar/publish-due.ts +12 -0
  51. package/bundled-templates/astro-blog/starter/src/pages/api/ops/config/validate.ts +4 -0
  52. package/bundled-templates/astro-blog/starter/src/pages/api/ops/content-tracking/report-state/batch.ts +19 -0
  53. package/bundled-templates/astro-blog/starter/src/pages/api/ops/content-tracking/report-state.ts +15 -0
  54. package/bundled-templates/astro-blog/starter/src/pages/api/ops/ctas.ts +5 -0
  55. package/bundled-templates/astro-blog/starter/src/pages/api/ops/entities.ts +23 -0
  56. package/bundled-templates/astro-blog/starter/src/pages/api/ops/media/optimize.ts +5 -0
  57. package/bundled-templates/astro-blog/starter/src/pages/api/ops/media/upload.ts +30 -0
  58. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id]/resume.ts +28 -0
  59. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id]/validate.ts +6 -0
  60. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/[id].ts +7 -0
  61. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/import.ts +458 -0
  62. package/bundled-templates/astro-blog/starter/src/pages/api/ops/migrations/media.ts +5 -0
  63. package/bundled-templates/astro-blog/starter/src/pages/api/ops/operations.ts +189 -0
  64. package/bundled-templates/astro-blog/starter/src/pages/api/ops/playbooks/[id]/run.ts +55 -0
  65. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/approve.ts +13 -0
  66. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/preview.ts +9 -0
  67. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id]/publish.ts +13 -0
  68. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/[id].ts +18 -0
  69. package/bundled-templates/astro-blog/starter/src/pages/api/ops/posts/index.ts +28 -0
  70. package/bundled-templates/astro-blog/starter/src/pages/api/ops/providers.ts +6 -0
  71. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/history.ts +3 -0
  72. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/latest.ts +3 -0
  73. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/posts.ts +3 -0
  74. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/project-context.ts +4 -0
  75. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/sync-updates.ts +43 -0
  76. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/sync.ts +172 -0
  77. package/bundled-templates/astro-blog/starter/src/pages/api/ops/pull/updates.ts +3 -0
  78. package/bundled-templates/astro-blog/starter/src/pages/api/ops/redirects/check.ts +5 -0
  79. package/bundled-templates/astro-blog/starter/src/pages/api/ops/redirects.ts +32 -0
  80. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/deliver-due.ts +12 -0
  81. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/deliver.ts +6 -0
  82. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports/export.ts +17 -0
  83. package/bundled-templates/astro-blog/starter/src/pages/api/ops/reports.ts +31 -0
  84. package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/history/[contentId].ts +3 -0
  85. package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/policy.ts +4 -0
  86. package/bundled-templates/astro-blog/starter/src/pages/api/ops/rewrite/queue.ts +4 -0
  87. package/bundled-templates/astro-blog/starter/src/pages/api/ops/seo/audit.ts +7 -0
  88. package/bundled-templates/astro-blog/starter/src/pages/api/ops/seo/insights.ts +5 -0
  89. package/bundled-templates/astro-blog/starter/src/pages/api/ops/settings/site.ts +7 -0
  90. package/bundled-templates/astro-blog/starter/src/pages/api/ops/settings.ts +19 -0
  91. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/auto-detect.ts +13 -0
  92. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/diff.ts +11 -0
  93. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/match.ts +16 -0
  94. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/matching-history.ts +4 -0
  95. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap/runs.ts +4 -0
  96. package/bundled-templates/astro-blog/starter/src/pages/api/ops/sitemap.ts +11 -0
  97. package/bundled-templates/astro-blog/starter/src/pages/api/ops/summary.ts +12 -0
  98. package/bundled-templates/astro-blog/starter/src/pages/api/ops/topics/[id]/faq.ts +12 -0
  99. package/bundled-templates/astro-blog/starter/src/pages/api/ops/topics.ts +11 -0
  100. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/capabilities.ts +5 -0
  101. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migration-plan.ts +21 -0
  102. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id]/resume.ts +5 -0
  103. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id]/validate.ts +6 -0
  104. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations/[id].ts +13 -0
  105. package/bundled-templates/astro-blog/starter/src/pages/api/ops/wordpress/migrations.ts +147 -0
  106. package/bundled-templates/astro-blog/starter/src/pages/authors/[slug].astro +12 -0
  107. package/bundled-templates/astro-blog/starter/src/pages/blog/[slug].astro +25 -0
  108. package/bundled-templates/astro-blog/starter/src/pages/blog/index.astro +32 -0
  109. package/bundled-templates/astro-blog/starter/src/pages/blog/page/[page].astro +21 -0
  110. package/bundled-templates/astro-blog/starter/src/pages/ops/index.astro +20 -0
  111. package/bundled-templates/astro-blog/starter/src/pages/ops/operations.astro +33 -0
  112. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/[id]/edit.astro +17 -0
  113. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/[id]/preview.astro +11 -0
  114. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/index.astro +15 -0
  115. package/bundled-templates/astro-blog/starter/src/pages/ops/posts/new.astro +12 -0
  116. package/bundled-templates/astro-blog/starter/src/pages/ops/reports.astro +12 -0
  117. package/bundled-templates/astro-blog/starter/src/pages/ops/rewrite.astro +13 -0
  118. package/bundled-templates/astro-blog/starter/src/pages/ops/settings/integrations.astro +13 -0
  119. package/bundled-templates/astro-blog/starter/src/pages/ops/settings/site.astro +11 -0
  120. package/bundled-templates/astro-blog/starter/src/pages/ops/sitemap.astro +15 -0
  121. package/bundled-templates/astro-blog/starter/src/pages/ops/topics.astro +13 -0
  122. package/bundled-templates/astro-blog/starter/src/pages/ops/wordpress.astro +34 -0
  123. package/bundled-templates/astro-blog/starter/src/pages/review/[token].astro +13 -0
  124. package/bundled-templates/astro-blog/starter/src/pages/robots.txt.ts +4 -0
  125. package/bundled-templates/astro-blog/starter/src/pages/sitemap.xml.ts +12 -0
  126. package/bundled-templates/astro-blog/starter/src/pages/topics/[slug]/page/[page].astro +10 -0
  127. package/bundled-templates/astro-blog/starter/src/pages/topics/[slug].astro +19 -0
  128. package/bundled-templates/astro-blog/starter/src/pages/topics/index.astro +10 -0
  129. package/bundled-templates/astro-blog/starter/src/styles/global.css +4 -0
  130. package/bundled-templates/astro-blog/starter/tsconfig.json +9 -0
  131. package/bundled-templates/astro-blog/starter/wrangler.jsonc +52 -0
  132. package/bundled-tools/clis/maggie_clone.py +42 -15
  133. package/bundled-tools/clis/maggie_dash.py +41 -25
  134. package/bundled-tools/clis/maggie_design.py +2 -0
  135. package/bundled-tools/clis/maggie_design_system.py +7 -1
  136. package/bundled-tools/clis/site_audit.py +25 -2
  137. package/bundled-tools/runtime/maggie_memory.py +6 -1
  138. package/package.json +1 -1
@@ -0,0 +1,168 @@
1
+ # Maggie Astro Blog Starter
2
+
3
+ This directory is a runnable reference scaffold for the [Astro Blog Template
4
+ Contract](../README.md). It provides public routes, a stable SQLite schema and
5
+ a server-only Maggie Ops API. The UI can be redesigned without changing the
6
+ content, publishing or SEO contracts.
7
+
8
+ ## Run it
9
+
10
+ ```bash
11
+ cp .env.example .env
12
+ # Set PUBLIC_SITE_URL to the real absolute origin before building.
13
+ npm install
14
+ npm run db:seed
15
+ npm run dev
16
+ npm run build
17
+ ```
18
+
19
+ The default example uses SQLite. The schema is created on first server request at
20
+ `BLOG_DB_PATH`. Demo posts are development-only; production with no configured
21
+ adapter renders an empty, non-indexable content surface rather than publishing
22
+ fixture posts. For local-only Ops testing set `OPS_AUTH_PROVIDER=local` and a
23
+ long random `OPS_LOCAL_TOKEN`. Production deployments must replace the local
24
+ token with a server-side OIDC/session adapter.
25
+
26
+ The blog index defaults to 9 posts per page in a 3-column (3×3) grid. Change
27
+ `PUBLIC_POSTS_PER_PAGE` or `PUBLIC_GRID_COLUMNS` only through reviewed site
28
+ configuration so pagination and SEO checks continue to use the same values.
29
+
30
+ `PUBLIC_SITE_URL` is required and must be an absolute URL; the starter does not
31
+ fall back to `example.com`. Provider reports use `GSC_SITE_URL` + `GSC_ACCESS_TOKEN` for visibility and
32
+ `GA4_PROPERTY_ID` + `GA4_ACCESS_TOKEN` for analytics. Tokens are never returned
33
+ by the settings API. Technical SEO and content-quality reports can run locally;
34
+ provider reports persist an explicit error when credentials are unavailable.
35
+
36
+ To load the deterministic example records into SQLite, run `npm run db:seed`.
37
+ For a browser session in local mode, set the `maggie_ops_token` cookie to the
38
+ same value as `OPS_LOCAL_TOKEN`; API clients may instead send a Bearer token.
39
+
40
+ ## Routes
41
+
42
+ ```text
43
+ /blog
44
+ /blog/[slug]
45
+ /blog/page/[page]
46
+ /about
47
+ /topics
48
+ /topics/[slug]
49
+ /authors/[slug]
50
+ /sitemap.xml
51
+ /robots.txt
52
+ /ops/* authenticated Maggie Ops screens
53
+ /api/ops/* authenticated content, sitemap, report and settings API
54
+ /api/approval/[token] expiring client review link API
55
+ ```
56
+
57
+ The public pages use the same publication predicate, URL helper, metadata
58
+ builder, and JSON-LD shape that a production adapter must preserve. The
59
+ server-only Ops API currently exposes summary, author/post CRUD, guarded
60
+ publish, topics/FAQs/CTAs, sitemap sources/runs/matching/history, reports,
61
+ settings, API Pull project-context/posts/updates/history/latest sync, rewrite
62
+ queue/history/policy, content-tracking state reporting, and server-side AI
63
+ CMO/GSC/GA4 provider seams.
64
+
65
+ `/ops/wordpress` provides a capability, mapping-plan, preview/apply/resume migration flow for the
66
+ WordPress REST resources `/wp/v2/posts`, `/pages`, `/categories`, `/tags`, and
67
+ `/media`. Selected post types, category/topic mappings, author mappings, SEO
68
+ normalization, source modified timestamps, and conflict policy are persisted in
69
+ the migration options.
70
+ It preserves WordPress source IDs, stores categories as draft topics, stores
71
+ tags as migration terms, registers media URLs, and records redirects when a
72
+ source URL differs from the destination slug. Configure WordPress API access
73
+ with `WORDPRESS_API_TOKEN` or a server-only application-password pair.
74
+
75
+ ## Replace points
76
+
77
+ | File | Replace with |
78
+ |---|---|
79
+ | `src/data/demo.ts` | demo-only fallback content |
80
+ | `src/lib/repository.ts` | canonical publication queries and adapter mapping |
81
+ | `src/lib/seo.ts` | validated metadata/JSON-LD builder if fields expand |
82
+ | `db/schema.sql` | selected database migration, preserving constraints |
83
+ | `src/middleware.ts` | real session and role authorization |
84
+ | `src/lib/db.ts` | Postgres adapter or migration runner when SQLite is not selected |
85
+ | `src/pages/ops/*` | authenticated Maggie Ops screens calling the API |
86
+ | `src/pages/sitemap.xml.ts` | repository-backed published URL query |
87
+
88
+ The starter includes a Tailwind v4 Vite entry point for projects that choose
89
+ Tailwind during bootstrap. It remains compatible with the existing contract
90
+ surface and does not require replacing the host's design system.
91
+
92
+ `npm run maggie:*` uses the installed `maggie` binary. It is portable after the
93
+ starter is copied out of this monorepo; it no longer depends on repository-
94
+ relative Python paths.
95
+
96
+ Do not expose `AI_CMO_API_KEY`, GSC verification tokens, GA4 credentials, or
97
+ database credentials to browser code. API Pull sync, sitemap matching,
98
+ rewrite queueing and report-state remain server/CLI operations; connect them to
99
+ the authenticated Ops actions rather than accepting secrets from the browser.
100
+
101
+ ## Operations contract
102
+
103
+ `/api/ops/operations` exposes durable calendar, revision, bulk preview/apply,
104
+ redirect, task, media and agency workspace operations. Approved posts can be
105
+ published by schedule with `POST /api/ops/calendar/publish-due`. Client review
106
+ links are created by the authenticated API and expose only a hashed token.
107
+
108
+ Agency clients should use `/api/ops/agency?workspaceId=...` for scoped reads and
109
+ `POST /api/ops/agency` for client, site, project, template, and member changes.
110
+ Cross-workspace relations are rejected and provider credentials are accepted only
111
+ as server-side references.
112
+
113
+ For bulk changes, use `POST /api/ops/bulk`: first send `filter`, `changes`,
114
+ and preview mode, then apply the returned `previewId`. Apply rejects stale
115
+ previews, records a revision for every affected post, and supports taxonomy,
116
+ metadata, publish/archive, regeneration, and rewrite-queue changes.
117
+
118
+ Generic migration is available at `POST /api/ops/migrations/import` with
119
+ `format` set to `json`, `csv`, `wxr`, `sitemap`, or `media-archive`, plus
120
+ `mode: preview|apply`. `syncMode` supports `all`, `new`, and `modified`, while
121
+ `detectDeleted` archives records missing from a full source snapshot. Applied
122
+ records use source identity and SHA-256 checksums for safe reruns; conflicts
123
+ support source-wins, target-wins, or manual review.
124
+
125
+ After explicit bootstrap confirmation, the CLI writes `.maggie/project.json`,
126
+ `decisions.json`, `schema.json`, `routes.json`, `integrations.json`, and
127
+ `migration.json`. Use `maggie doctor --strict --require-bootstrap`,
128
+ `maggie generate fixture`, `npm run schema:check`, and `npm run fixture:seed`
129
+ as local or CI gates. `npm run maggie:ci` is the strict CI entry point after
130
+ bootstrap has been completed.
131
+
132
+ API Pull lifecycle routes are server-only: use `/api/ops/pull/project-context`
133
+ for sanitized project and CTA context, `/api/ops/pull/sync` for idempotent
134
+ local storage, `/api/ops/pull/sync-updates` for rewrite/update delivery,
135
+ `/api/ops/sitemap/matching-history` for readback, and
136
+ `/api/ops/content-tracking/report-state` (or its bounded batch variant) only
137
+ after local publication state is known. API keys are never accepted in request
138
+ bodies.
139
+
140
+ Run `POST /api/ops/seo/audit` to persist findings for canonical conflicts,
141
+ redirect chains, orphan pages, pagination requirements, duplicate titles, thin
142
+ content, missing authors/alt text, invalid JSON-LD, image performance, hreflang,
143
+ FAQ quality, answer-first structure, entity completeness, metadata and
144
+ publication-date integrity. Playbooks and runs are stored in
145
+ `playbooks` and `playbook_runs` so agencies can attach repeatable migration,
146
+ launch, refresh and audit workflows to a site.
147
+ Run and advance a playbook through `/api/ops/playbooks/:id/run`; steps are
148
+ confirmation-gated and completion requires declared validation gates.
149
+
150
+ `/api/ops/seo/insights` exposes internal-link suggestions, topic coverage,
151
+ FAQ quality, answer-first checks, refresh recommendations, entity profiles,
152
+ entity graph relations, and crawler policy.
153
+ `/api/ops/sitemap/diff` stores URL snapshots and returns added/removed URLs;
154
+ `/api/ops/redirects?format=csv` exports redirect mappings; POSTing either JSON
155
+ `{"items": [...]}` or `text/csv` imports them. `/api/ops/redirects/check` checks
156
+ known targets.
157
+
158
+ Report deliveries accept `daily`, `weekly`, `monthly`, or an ISO timestamp.
159
+ Configure a platform cron to call `POST /api/ops/reports/deliver-due`; the
160
+ default server-only email adapter is Resend (`EMAIL_PROVIDER=resend`). A
161
+ Webhook adapter remains available for self-hosted mail gateways, and delivery
162
+ state is persisted regardless of provider.
163
+
164
+ Media uploads use the same provider boundary. The default is Cloudinary
165
+ (`MEDIA_STORAGE_PROVIDER=cloudinary`) with signed server-side uploads; set it
166
+ to `local` for development. Custom adapters can be registered in
167
+ `src/lib/providers/` against the stable provider interface. `GET /api/ops/providers`
168
+ returns capability and configuration status without returning credentials.
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from "astro/config";
2
+ import { loadEnv } from "vite";
3
+ import node from "@astrojs/node";
4
+ import cloudflare from "@astrojs/cloudflare";
5
+ import tailwindcss from "@tailwindcss/vite";
6
+
7
+ const env = loadEnv(process.env.NODE_ENV || "development", process.cwd(), "");
8
+ const site = env.PUBLIC_SITE_URL?.trim();
9
+ if (!site) throw new Error("PUBLIC_SITE_URL is required; copy .env.example and configure the canonical public site URL");
10
+ try { new URL(site); } catch { throw new Error("PUBLIC_SITE_URL must be an absolute URL"); }
11
+ const deploymentProvider = env.MAGGIE_DEPLOY_PROVIDER || "node";
12
+
13
+ export default defineConfig({
14
+ output: "server",
15
+ adapter: deploymentProvider === "cloudflare" ? cloudflare() : node({ mode: "standalone" }),
16
+ site,
17
+ vite: { plugins: [tailwindcss()] },
18
+ security: { checkOrigin: true },
19
+ });
@@ -0,0 +1,508 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE TABLE IF NOT EXISTS authors (
4
+ id TEXT PRIMARY KEY,
5
+ slug TEXT NOT NULL UNIQUE,
6
+ name TEXT NOT NULL,
7
+ job_title TEXT,
8
+ organisation TEXT,
9
+ bio TEXT,
10
+ profile_url TEXT,
11
+ credentials_json TEXT NOT NULL DEFAULT '[]',
12
+ created_at TEXT NOT NULL,
13
+ updated_at TEXT NOT NULL
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS posts (
17
+ id TEXT PRIMARY KEY,
18
+ source TEXT NOT NULL CHECK (source IN ('local', 'api-pull', 'cms', 'manual')),
19
+ source_id TEXT,
20
+ slug TEXT NOT NULL UNIQUE,
21
+ title TEXT NOT NULL,
22
+ excerpt TEXT NOT NULL DEFAULT '',
23
+ content_markdown TEXT,
24
+ content_html TEXT,
25
+ canonical_url TEXT NOT NULL UNIQUE,
26
+ cover_image_url TEXT,
27
+ cover_image_alt TEXT NOT NULL DEFAULT '',
28
+ author_id TEXT NOT NULL REFERENCES authors(id),
29
+ language TEXT NOT NULL DEFAULT 'en',
30
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'approved', 'scheduled', 'published', 'archived')),
31
+ published_at TEXT,
32
+ updated_at TEXT,
33
+ created_at TEXT NOT NULL,
34
+ content_hash TEXT,
35
+ seo_title TEXT,
36
+ meta_description TEXT,
37
+ focus_keyword TEXT,
38
+ canonical_override TEXT,
39
+ robots_index TEXT NOT NULL DEFAULT 'index',
40
+ robots_follow TEXT NOT NULL DEFAULT 'follow',
41
+ og_image_url TEXT,
42
+ schema_type TEXT NOT NULL DEFAULT 'Article',
43
+ custom_metadata_json TEXT NOT NULL DEFAULT '{}',
44
+ scheduled_at TEXT,
45
+ checksum TEXT,
46
+ source_modified_at TEXT,
47
+ CHECK (status <> 'published' OR published_at IS NOT NULL)
48
+ );
49
+ CREATE UNIQUE INDEX IF NOT EXISTS posts_source_identity ON posts (source, source_id) WHERE source_id IS NOT NULL;
50
+ CREATE INDEX IF NOT EXISTS posts_public_order ON posts (status, published_at DESC, updated_at DESC);
51
+
52
+ CREATE TABLE IF NOT EXISTS topics (
53
+ id TEXT PRIMARY KEY,
54
+ slug TEXT NOT NULL UNIQUE,
55
+ title TEXT NOT NULL,
56
+ description TEXT NOT NULL,
57
+ seo_title TEXT,
58
+ seo_description TEXT,
59
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
60
+ minimum_published_posts INTEGER NOT NULL DEFAULT 3,
61
+ created_at TEXT NOT NULL,
62
+ updated_at TEXT NOT NULL
63
+ );
64
+ CREATE TABLE IF NOT EXISTS post_topics (
65
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
66
+ topic_id TEXT NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
67
+ PRIMARY KEY (post_id, topic_id)
68
+ );
69
+
70
+ CREATE TABLE IF NOT EXISTS faq_items (
71
+ id TEXT PRIMARY KEY,
72
+ question TEXT NOT NULL,
73
+ answer_markdown TEXT NOT NULL,
74
+ sort_order INTEGER NOT NULL DEFAULT 0,
75
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published')),
76
+ created_at TEXT NOT NULL,
77
+ updated_at TEXT NOT NULL
78
+ );
79
+ CREATE TABLE IF NOT EXISTS post_faqs (
80
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
81
+ faq_id TEXT NOT NULL REFERENCES faq_items(id) ON DELETE CASCADE,
82
+ PRIMARY KEY (post_id, faq_id)
83
+ );
84
+ CREATE TABLE IF NOT EXISTS topic_faqs (
85
+ topic_id TEXT NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
86
+ faq_id TEXT NOT NULL REFERENCES faq_items(id) ON DELETE CASCADE,
87
+ PRIMARY KEY (topic_id, faq_id)
88
+ );
89
+
90
+ CREATE TABLE IF NOT EXISTS ctas (
91
+ id TEXT PRIMARY KEY,
92
+ label TEXT NOT NULL,
93
+ url TEXT NOT NULL,
94
+ placement TEXT NOT NULL,
95
+ tracking_key TEXT,
96
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'archived')),
97
+ created_at TEXT NOT NULL,
98
+ updated_at TEXT NOT NULL
99
+ );
100
+ CREATE TABLE IF NOT EXISTS post_ctas (
101
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
102
+ cta_id TEXT NOT NULL REFERENCES ctas(id) ON DELETE CASCADE,
103
+ PRIMARY KEY (post_id, cta_id)
104
+ );
105
+ CREATE TABLE IF NOT EXISTS topic_ctas (
106
+ topic_id TEXT NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
107
+ cta_id TEXT NOT NULL REFERENCES ctas(id) ON DELETE CASCADE,
108
+ PRIMARY KEY (topic_id, cta_id)
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS content_sync_state (
112
+ source TEXT PRIMARY KEY,
113
+ cursor TEXT,
114
+ etag TEXT,
115
+ last_success_at TEXT,
116
+ last_error TEXT,
117
+ updated_at TEXT NOT NULL
118
+ );
119
+ CREATE TABLE IF NOT EXISTS content_delivery_state (
120
+ post_id TEXT PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE,
121
+ remote_content_id TEXT,
122
+ remote_version TEXT,
123
+ delivery_status TEXT NOT NULL DEFAULT 'not_delivered',
124
+ last_reported_url TEXT,
125
+ last_reported_hash TEXT,
126
+ last_error TEXT,
127
+ updated_at TEXT NOT NULL
128
+ );
129
+ CREATE TABLE IF NOT EXISTS sitemap_sources (
130
+ id TEXT PRIMARY KEY,
131
+ url TEXT NOT NULL UNIQUE,
132
+ source_type TEXT NOT NULL CHECK (source_type IN ('manual', 'auto_detected')),
133
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
134
+ created_at TEXT NOT NULL,
135
+ updated_at TEXT NOT NULL
136
+ );
137
+ CREATE TABLE IF NOT EXISTS sitemap_runs (
138
+ id TEXT PRIMARY KEY,
139
+ source_id TEXT REFERENCES sitemap_sources(id),
140
+ status TEXT NOT NULL CHECK (status IN ('queued', 'running', 'completed', 'failed')),
141
+ total_urls INTEGER NOT NULL DEFAULT 0,
142
+ matched_posts INTEGER NOT NULL DEFAULT 0,
143
+ eligible_posts INTEGER NOT NULL DEFAULT 0,
144
+ unmatched_urls INTEGER NOT NULL DEFAULT 0,
145
+ error TEXT,
146
+ started_at TEXT NOT NULL,
147
+ completed_at TEXT
148
+ );
149
+ CREATE TABLE IF NOT EXISTS blog_reports (
150
+ id TEXT PRIMARY KEY,
151
+ run_type TEXT NOT NULL,
152
+ status TEXT NOT NULL CHECK (status IN ('queued', 'running', 'completed', 'failed')),
153
+ summary_json TEXT NOT NULL DEFAULT '{}',
154
+ findings_json TEXT NOT NULL DEFAULT '[]',
155
+ started_at TEXT,
156
+ completed_at TEXT,
157
+ error TEXT
158
+ );
159
+ CREATE TABLE IF NOT EXISTS blog_settings (
160
+ key TEXT PRIMARY KEY,
161
+ value_json TEXT NOT NULL,
162
+ updated_at TEXT NOT NULL
163
+ );
164
+ CREATE TABLE IF NOT EXISTS audit_events (
165
+ id TEXT PRIMARY KEY,
166
+ actor_id TEXT NOT NULL,
167
+ action TEXT NOT NULL,
168
+ entity_type TEXT NOT NULL,
169
+ entity_id TEXT NOT NULL,
170
+ previous_state TEXT,
171
+ next_state TEXT,
172
+ reason TEXT,
173
+ correlation_id TEXT NOT NULL,
174
+ created_at TEXT NOT NULL
175
+ );
176
+
177
+ CREATE TABLE IF NOT EXISTS wp_migrations (
178
+ id TEXT PRIMARY KEY,
179
+ source_url TEXT NOT NULL,
180
+ mode TEXT NOT NULL CHECK (mode IN ('preview', 'apply')),
181
+ status TEXT NOT NULL CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
182
+ options_json TEXT NOT NULL DEFAULT '{}',
183
+ counts_json TEXT NOT NULL DEFAULT '{}',
184
+ error TEXT,
185
+ started_at TEXT NOT NULL,
186
+ completed_at TEXT,
187
+ checkpoint_json TEXT NOT NULL DEFAULT '{}',
188
+ source_checksum TEXT,
189
+ deleted_count INTEGER NOT NULL DEFAULT 0,
190
+ conflict_count INTEGER NOT NULL DEFAULT 0
191
+ );
192
+ CREATE TABLE IF NOT EXISTS wp_import_items (
193
+ id TEXT PRIMARY KEY,
194
+ migration_id TEXT NOT NULL REFERENCES wp_migrations(id) ON DELETE CASCADE,
195
+ entity_type TEXT NOT NULL CHECK (entity_type IN ('post', 'category', 'tag', 'author', 'media')),
196
+ source_id TEXT NOT NULL,
197
+ target_id TEXT,
198
+ action TEXT NOT NULL CHECK (action IN ('created', 'updated', 'skipped', 'failed')),
199
+ error TEXT,
200
+ created_at TEXT NOT NULL,
201
+ UNIQUE (migration_id, entity_type, source_id)
202
+ );
203
+ CREATE TABLE IF NOT EXISTS wp_terms (
204
+ id TEXT PRIMARY KEY,
205
+ taxonomy TEXT NOT NULL CHECK (taxonomy IN ('category', 'tag')),
206
+ source_id TEXT NOT NULL,
207
+ name TEXT NOT NULL,
208
+ slug TEXT NOT NULL,
209
+ description TEXT NOT NULL DEFAULT '',
210
+ topic_id TEXT REFERENCES topics(id),
211
+ created_at TEXT NOT NULL,
212
+ updated_at TEXT NOT NULL,
213
+ UNIQUE (taxonomy, source_id)
214
+ );
215
+ CREATE TABLE IF NOT EXISTS post_wp_terms (
216
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
217
+ term_id TEXT NOT NULL REFERENCES wp_terms(id) ON DELETE CASCADE,
218
+ PRIMARY KEY (post_id, term_id)
219
+ );
220
+ CREATE TABLE IF NOT EXISTS media_assets (
221
+ id TEXT PRIMARY KEY,
222
+ source TEXT NOT NULL,
223
+ source_id TEXT NOT NULL,
224
+ source_url TEXT NOT NULL,
225
+ local_url TEXT,
226
+ mime_type TEXT,
227
+ alt_text TEXT NOT NULL DEFAULT '',
228
+ caption TEXT NOT NULL DEFAULT '',
229
+ width INTEGER,
230
+ height INTEGER,
231
+ metadata_json TEXT NOT NULL DEFAULT '{}',
232
+ optimized_formats_json TEXT NOT NULL DEFAULT '[]',
233
+ optimization_status TEXT NOT NULL DEFAULT 'pending',
234
+ storage_provider TEXT NOT NULL DEFAULT 'local',
235
+ storage_key TEXT,
236
+ delivery_url TEXT,
237
+ upload_status TEXT NOT NULL DEFAULT 'pending',
238
+ upload_error TEXT,
239
+ created_at TEXT NOT NULL,
240
+ updated_at TEXT NOT NULL,
241
+ UNIQUE (source, source_id)
242
+ );
243
+ CREATE TABLE IF NOT EXISTS post_media (
244
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
245
+ media_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
246
+ role TEXT NOT NULL CHECK (role IN ('featured', 'inline', 'gallery')),
247
+ sort_order INTEGER NOT NULL DEFAULT 0,
248
+ PRIMARY KEY (post_id, media_id, role)
249
+ );
250
+ CREATE TABLE IF NOT EXISTS redirects (
251
+ id TEXT PRIMARY KEY,
252
+ source_path TEXT NOT NULL UNIQUE,
253
+ target_path TEXT NOT NULL,
254
+ status_code INTEGER NOT NULL DEFAULT 301 CHECK (status_code IN (301, 308)),
255
+ reason TEXT NOT NULL,
256
+ created_at TEXT NOT NULL
257
+ );
258
+
259
+ CREATE TABLE IF NOT EXISTS post_revisions (
260
+ id TEXT PRIMARY KEY,
261
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
262
+ version INTEGER NOT NULL,
263
+ snapshot_json TEXT NOT NULL,
264
+ actor_id TEXT NOT NULL,
265
+ reason TEXT,
266
+ created_at TEXT NOT NULL,
267
+ UNIQUE(post_id, version)
268
+ );
269
+ CREATE TABLE IF NOT EXISTS content_calendar (
270
+ id TEXT PRIMARY KEY,
271
+ post_id TEXT NOT NULL UNIQUE REFERENCES posts(id) ON DELETE CASCADE,
272
+ scheduled_at TEXT NOT NULL,
273
+ campaign TEXT,
274
+ notes TEXT,
275
+ reminder_at TEXT,
276
+ created_at TEXT NOT NULL,
277
+ updated_at TEXT NOT NULL
278
+ );
279
+ CREATE TABLE IF NOT EXISTS bulk_operations (
280
+ id TEXT PRIMARY KEY,
281
+ operation TEXT NOT NULL,
282
+ filter_json TEXT NOT NULL,
283
+ changes_json TEXT NOT NULL,
284
+ preview_json TEXT NOT NULL DEFAULT '{}',
285
+ status TEXT NOT NULL CHECK(status IN ('preview','queued','running','completed','failed','cancelled')),
286
+ actor_id TEXT NOT NULL,
287
+ created_at TEXT NOT NULL,
288
+ completed_at TEXT,
289
+ error TEXT
290
+ );
291
+ CREATE TABLE IF NOT EXISTS media_folders (
292
+ id TEXT PRIMARY KEY,
293
+ name TEXT NOT NULL UNIQUE,
294
+ created_at TEXT NOT NULL
295
+ );
296
+ CREATE TABLE IF NOT EXISTS media_folder_items (
297
+ media_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
298
+ folder_id TEXT NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE,
299
+ PRIMARY KEY(media_id, folder_id)
300
+ );
301
+ CREATE TABLE IF NOT EXISTS migration_sources (
302
+ id TEXT PRIMARY KEY,
303
+ kind TEXT NOT NULL CHECK(kind IN ('wordpress-rest','wxr','csv','json','sitemap','media-archive')),
304
+ location TEXT NOT NULL,
305
+ options_json TEXT NOT NULL DEFAULT '{}',
306
+ checksum TEXT,
307
+ created_at TEXT NOT NULL,
308
+ updated_at TEXT NOT NULL
309
+ );
310
+ CREATE TABLE IF NOT EXISTS migration_conflicts (
311
+ id TEXT PRIMARY KEY,
312
+ migration_id TEXT NOT NULL REFERENCES wp_migrations(id) ON DELETE CASCADE,
313
+ source_id TEXT NOT NULL,
314
+ target_id TEXT,
315
+ resolution TEXT NOT NULL DEFAULT 'manual' CHECK(resolution IN ('source-wins','target-wins','manual')),
316
+ details_json TEXT NOT NULL DEFAULT '{}',
317
+ created_at TEXT NOT NULL,
318
+ resolved_at TEXT
319
+ );
320
+ CREATE TABLE IF NOT EXISTS workspaces (
321
+ id TEXT PRIMARY KEY,
322
+ name TEXT NOT NULL,
323
+ slug TEXT NOT NULL UNIQUE,
324
+ logo_url TEXT,
325
+ brand_colors_json TEXT NOT NULL DEFAULT '{}',
326
+ custom_domain TEXT,
327
+ created_at TEXT NOT NULL,
328
+ updated_at TEXT NOT NULL
329
+ );
330
+ CREATE TABLE IF NOT EXISTS clients (
331
+ id TEXT PRIMARY KEY,
332
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
333
+ name TEXT NOT NULL,
334
+ email TEXT,
335
+ created_at TEXT NOT NULL,
336
+ updated_at TEXT NOT NULL
337
+ );
338
+ CREATE TABLE IF NOT EXISTS sites (
339
+ id TEXT PRIMARY KEY,
340
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
341
+ client_id TEXT REFERENCES clients(id) ON DELETE SET NULL,
342
+ name TEXT NOT NULL,
343
+ domain TEXT NOT NULL,
344
+ settings_json TEXT NOT NULL DEFAULT '{}',
345
+ credentials_json TEXT NOT NULL DEFAULT '{}',
346
+ created_at TEXT NOT NULL,
347
+ updated_at TEXT NOT NULL,
348
+ UNIQUE(workspace_id, domain)
349
+ );
350
+ CREATE TABLE IF NOT EXISTS projects (
351
+ id TEXT PRIMARY KEY,
352
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
353
+ site_id TEXT REFERENCES sites(id) ON DELETE SET NULL,
354
+ name TEXT NOT NULL,
355
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','paused','archived')),
356
+ created_at TEXT NOT NULL,
357
+ updated_at TEXT NOT NULL
358
+ );
359
+ CREATE TABLE IF NOT EXISTS workspace_templates (
360
+ id TEXT PRIMARY KEY,
361
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
362
+ name TEXT NOT NULL,
363
+ template_type TEXT NOT NULL,
364
+ content_json TEXT NOT NULL DEFAULT '{}',
365
+ created_at TEXT NOT NULL,
366
+ updated_at TEXT NOT NULL,
367
+ UNIQUE(workspace_id,name)
368
+ );
369
+ CREATE TABLE IF NOT EXISTS workspace_members (
370
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
371
+ user_id TEXT NOT NULL,
372
+ role TEXT NOT NULL CHECK(role IN ('agency-owner','account-manager','seo-strategist','writer','editor','developer','client-reviewer','read-only-analyst')),
373
+ created_at TEXT NOT NULL,
374
+ PRIMARY KEY(workspace_id, user_id)
375
+ );
376
+ CREATE TABLE IF NOT EXISTS approval_links (
377
+ id TEXT PRIMARY KEY,
378
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
379
+ token_hash TEXT NOT NULL UNIQUE,
380
+ expires_at TEXT NOT NULL,
381
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','changes_requested','expired')),
382
+ reviewer_name TEXT,
383
+ reviewer_email TEXT,
384
+ comment TEXT,
385
+ approved_at TEXT,
386
+ created_at TEXT NOT NULL
387
+ );
388
+ CREATE TABLE IF NOT EXISTS tasks (
389
+ id TEXT PRIMARY KEY,
390
+ title TEXT NOT NULL,
391
+ description TEXT NOT NULL DEFAULT '',
392
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','in_progress','blocked','done','cancelled')),
393
+ assignee_id TEXT,
394
+ due_at TEXT,
395
+ linked_type TEXT,
396
+ linked_id TEXT,
397
+ approval_status TEXT,
398
+ created_at TEXT NOT NULL,
399
+ updated_at TEXT NOT NULL
400
+ );
401
+ CREATE TABLE IF NOT EXISTS task_comments (
402
+ id TEXT PRIMARY KEY,
403
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
404
+ author_id TEXT NOT NULL,
405
+ body TEXT NOT NULL,
406
+ created_at TEXT NOT NULL
407
+ );
408
+ CREATE TABLE IF NOT EXISTS playbooks (
409
+ id TEXT PRIMARY KEY,
410
+ name TEXT NOT NULL UNIQUE,
411
+ description TEXT NOT NULL DEFAULT '',
412
+ steps_json TEXT NOT NULL,
413
+ gates_json TEXT NOT NULL DEFAULT '[]',
414
+ report_format TEXT NOT NULL DEFAULT 'json',
415
+ created_at TEXT NOT NULL,
416
+ updated_at TEXT NOT NULL
417
+ );
418
+ CREATE TABLE IF NOT EXISTS playbook_runs (
419
+ id TEXT PRIMARY KEY,
420
+ playbook_id TEXT NOT NULL REFERENCES playbooks(id),
421
+ site_id TEXT REFERENCES sites(id),
422
+ status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','completed','failed')),
423
+ state_json TEXT NOT NULL DEFAULT '{}',
424
+ started_at TEXT,
425
+ completed_at TEXT,
426
+ error TEXT
427
+ );
428
+ CREATE TABLE IF NOT EXISTS seo_findings (
429
+ id TEXT PRIMARY KEY,
430
+ run_id TEXT NOT NULL REFERENCES blog_reports(id) ON DELETE CASCADE,
431
+ post_id TEXT REFERENCES posts(id) ON DELETE CASCADE,
432
+ category TEXT NOT NULL,
433
+ severity TEXT NOT NULL CHECK(severity IN ('info','warning','error')),
434
+ code TEXT NOT NULL,
435
+ message TEXT NOT NULL,
436
+ details_json TEXT NOT NULL DEFAULT '{}',
437
+ created_at TEXT NOT NULL
438
+ );
439
+ CREATE TABLE IF NOT EXISTS fact_review_reminders (
440
+ id TEXT PRIMARY KEY,
441
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
442
+ due_at TEXT NOT NULL,
443
+ reason TEXT NOT NULL,
444
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','completed','snoozed')),
445
+ completed_at TEXT,
446
+ created_at TEXT NOT NULL,
447
+ updated_at TEXT NOT NULL,
448
+ UNIQUE(post_id,status)
449
+ );
450
+ CREATE TABLE IF NOT EXISTS rewrite_queue_items (
451
+ id TEXT PRIMARY KEY,
452
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
453
+ reason TEXT NOT NULL DEFAULT 'manual',
454
+ status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','processing','completed','failed','cancelled')),
455
+ priority INTEGER NOT NULL DEFAULT 0,
456
+ requested_by TEXT NOT NULL,
457
+ created_at TEXT NOT NULL,
458
+ updated_at TEXT NOT NULL,
459
+ UNIQUE(post_id, status)
460
+ );
461
+ CREATE TABLE IF NOT EXISTS not_found_logs (
462
+ path TEXT PRIMARY KEY,
463
+ hits INTEGER NOT NULL DEFAULT 1,
464
+ referrer TEXT,
465
+ first_seen_at TEXT NOT NULL,
466
+ last_seen_at TEXT NOT NULL
467
+ );
468
+ CREATE TABLE IF NOT EXISTS notifications (
469
+ id TEXT PRIMARY KEY,
470
+ user_id TEXT,
471
+ type TEXT NOT NULL,
472
+ entity_type TEXT,
473
+ entity_id TEXT,
474
+ message TEXT NOT NULL,
475
+ read_at TEXT,
476
+ created_at TEXT NOT NULL
477
+ );
478
+ CREATE TABLE IF NOT EXISTS approval_comments (
479
+ id TEXT PRIMARY KEY,
480
+ approval_link_id TEXT NOT NULL REFERENCES approval_links(id) ON DELETE CASCADE,
481
+ paragraph_key TEXT,
482
+ body TEXT NOT NULL,
483
+ author_name TEXT,
484
+ created_at TEXT NOT NULL
485
+ );
486
+ CREATE TABLE IF NOT EXISTS report_deliveries (
487
+ id TEXT PRIMARY KEY,
488
+ workspace_id TEXT REFERENCES workspaces(id) ON DELETE CASCADE,
489
+ report_type TEXT NOT NULL,
490
+ recipient TEXT NOT NULL,
491
+ format TEXT NOT NULL CHECK(format IN ('json','csv','html','pdf')),
492
+ schedule TEXT,
493
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','paused','failed')),
494
+ last_sent_at TEXT,
495
+ last_error TEXT,
496
+ created_at TEXT NOT NULL
497
+ );
498
+ CREATE TABLE IF NOT EXISTS content_citations (id TEXT PRIMARY KEY, post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, source_url TEXT NOT NULL, source_title TEXT, verified_at TEXT, created_at TEXT NOT NULL);
499
+ CREATE TABLE IF NOT EXISTS query_page_map (id TEXT PRIMARY KEY, query TEXT NOT NULL, post_id TEXT REFERENCES posts(id) ON DELETE CASCADE, intent TEXT, priority INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, UNIQUE(query, post_id));
500
+ CREATE TABLE IF NOT EXISTS brand_mentions (id TEXT PRIMARY KEY, query TEXT NOT NULL, source_url TEXT NOT NULL, source_name TEXT, mentioned INTEGER NOT NULL DEFAULT 0, checked_at TEXT NOT NULL);
501
+ CREATE TABLE IF NOT EXISTS entity_profiles (id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, entity_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, same_as_json TEXT NOT NULL DEFAULT '[]', schema_json TEXT NOT NULL DEFAULT '{}', updated_at TEXT NOT NULL);
502
+ CREATE TABLE IF NOT EXISTS entity_relations (subject_key TEXT NOT NULL REFERENCES entity_profiles(entity_key) ON DELETE CASCADE, predicate TEXT NOT NULL, object_key TEXT NOT NULL REFERENCES entity_profiles(entity_key) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY(subject_key,predicate,object_key));
503
+ CREATE TABLE IF NOT EXISTS indexability_checks (id TEXT PRIMARY KEY, url TEXT NOT NULL, status TEXT NOT NULL, robots TEXT, canonical TEXT, checked_at TEXT NOT NULL, details_json TEXT NOT NULL DEFAULT '{}');
504
+ CREATE TABLE IF NOT EXISTS sitemap_snapshots (id TEXT PRIMARY KEY, source_url TEXT NOT NULL, urls_json TEXT NOT NULL, checksum TEXT NOT NULL, created_at TEXT NOT NULL);
505
+ CREATE TABLE IF NOT EXISTS hreflang_links (post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, language TEXT NOT NULL, url TEXT NOT NULL, PRIMARY KEY(post_id, language));
506
+ CREATE TABLE IF NOT EXISTS core_web_vitals (id TEXT PRIMARY KEY, url TEXT NOT NULL, period_start TEXT NOT NULL, lcp REAL, inp REAL, cls REAL, source TEXT NOT NULL, created_at TEXT NOT NULL);
507
+ CREATE INDEX IF NOT EXISTS tasks_due_idx ON tasks(status, due_at);
508
+ CREATE INDEX IF NOT EXISTS post_revisions_idx ON post_revisions(post_id, version DESC);