From 9192c02e1032d7e0b92c15fff7a746b419b51d38 Mon Sep 17 00:00:00 2001 From: hampton Date: Tue, 28 Jul 2026 18:38:08 +0000 Subject: [PATCH 1/3] Add reusable development seed dataset Amp-Thread-ID: https://ampcode.com/threads/T-019fa9fa-a0a9-7607-b551-df57797d3054 Co-authored-by: Amp --- db/seeds.rb | 142 +--------- db/seeds/development.rb | 421 ++++++++++++++++++++++++++++++ spec/lib/development_seed_spec.rb | 55 ++++ 3 files changed, 478 insertions(+), 140 deletions(-) create mode 100644 db/seeds/development.rb create mode 100644 spec/lib/development_seed_spec.rb diff --git a/db/seeds.rb b/db/seeds.rb index 4dd3442f..29db1bb3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,141 +1,3 @@ -puts "Seeding users..." -hampton = CoPlan::User.find_or_create_by!(email: "hampton@squareup.com") do |u| - u.external_id = "hampton@squareup.com" - u.name = "Hampton Lintorn-Catlin" - u.username = "hampton" - u.admin = true - u.avatar_url = "https://avatars.githubusercontent.com/u/111?s=80" - u.title = "Staff Engineer" - u.team = "Developer Tools" -end -hampton.update!(avatar_url: "https://avatars.githubusercontent.com/u/111?s=80") if hampton.avatar_url.blank? +require_relative "seeds/development" -puts "Seeding plans..." -if CoPlan::Plan.count == 0 - plan = CoPlan::Plans::Create.call( - title: "Q3 Product Roadmap", - content: "# Q3 Product Roadmap\n\n## Goals\n\n- Launch new dashboard\n- Improve API performance\n- Add team collaboration features\n\n## Timeline\n\n### Month 1\n- Design reviews\n- Technical planning\n\n### Month 2\n- Core implementation\n- Testing\n\n### Month 3\n- Beta launch\n- Feedback collection\n", - user: hampton - ) - plan.update!(status: "considering") -end - -puts "Seeding comments..." -if CoPlan::CommentThread.count == 0 - plan = CoPlan::Plan.first - if plan&.current_plan_version - reviewer = CoPlan::User.find_or_create_by!(email: "reviewer@squareup.com") do |u| - u.external_id = "reviewer@squareup.com" - u.name = "Plan Reviewer" - u.title = "Senior Engineer" - u.team = "Platform" - end - - thread = CoPlan::CommentThread.create!( - plan: plan, - plan_version: plan.current_plan_version, - start_line: 5, - end_line: 8, - created_by_user: reviewer - ) - thread.comments.create!( - author_type: "human", - author_id: reviewer.id, - body_markdown: "I think the timeline for Month 1 is too aggressive. Can we break this into smaller milestones?" - ) - - general_thread = CoPlan::CommentThread.create!( - plan: plan, - plan_version: plan.current_plan_version, - created_by_user: hampton - ) - general_thread.comments.create!( - author_type: "human", - author_id: hampton.id, - body_markdown: "Overall this is looking good. Let's move forward with the **beta launch** plan." - ) - end -end - -puts "Seeding API tokens..." -if CoPlan::ApiToken.count == 0 - raw_token = "dev-api-token-#{SecureRandom.hex(8)}" - CoPlan::ApiToken.create!( - user: hampton, - name: "Development Agent", - token_digest: Digest::SHA256.hexdigest(raw_token), - token_prefix: raw_token[0, 8] - ) - puts " Created API token: #{raw_token}" - puts " (Save this — it won't be shown again)" -end - -puts "Seeding plan types..." -# Icons come from the built-in named set (CoPlan::PlansHelper::PLAN_TYPE_ICONS) -# so each install can brand its document types. -general = CoPlan::PlanType.find_or_create_by!(name: "General") do |pt| - pt.description = "General-purpose plan" -end -general.update!(icon: "file-text") if general.icon.blank? -CoPlan::PlanType.find_or_create_by!(name: "RFC") do |pt| - pt.description = "Request for Comments — propose a significant change for team review" - pt.default_tags = ["rfc"] -end.tap { |pt| pt.update!(icon: "scroll") if pt.icon.blank? } -CoPlan::PlanType.find_or_create_by!(name: "Design Doc") do |pt| - pt.description = "Technical design document for a new system or feature" - pt.default_tags = ["design"] -end.tap { |pt| pt.update!(icon: "compass") if pt.icon.blank? } -CoPlan::PlanType.find_or_create_by!(name: "ADR") do |pt| - pt.description = "Architecture Decision Record — document a key technical decision" - pt.default_tags = ["adr"] -end.tap { |pt| pt.update!(icon: "scale") if pt.icon.blank? } - -# Backfill any plans without a plan type -CoPlan::Plan.where(plan_type_id: nil).update_all(plan_type_id: general.id) - -puts "Seeding tags on plans..." -CoPlan::Plan.includes(:tags, :plan_type).find_each do |p| - if p.tags.empty? && p.plan_type&.default_tags&.any? - p.tag_names = p.plan_type.default_tags - end -end - -# Add some demo plans with tags for local development -if CoPlan::Plan.count < 3 - rfc_type = CoPlan::PlanType.find_by(name: "RFC") - design_type = CoPlan::PlanType.find_by(name: "Design Doc") - - api_plan = CoPlan::Plans::Create.call( - title: "API Rate Limiting Strategy", - content: "# API Rate Limiting Strategy\n\n## Problem\n\nOur API endpoints have no rate limiting, leading to occasional abuse.\n\n## Proposal\n\nImplement token-bucket rate limiting at the gateway level.\n\n## Alternatives Considered\n\n- Per-IP limiting\n- API key quotas\n", - user: hampton, - plan_type_id: rfc_type&.id - ) - api_plan.update!(status: "considering") - api_plan.tag_names = ["api", "infrastructure", "security"] - - auth_plan = CoPlan::Plans::Create.call( - title: "Authentication System Redesign", - content: "# Authentication System Redesign\n\n## Goals\n\n- Migrate from session-based to token-based auth\n- Support SSO providers\n- Improve security posture\n\n## Architecture\n\nOIDC-based flow with JWT access tokens.\n", - user: hampton, - plan_type_id: design_type&.id - ) - auth_plan.update!(status: "developing") - auth_plan.tag_names = ["security", "infrastructure", "design"] -end - -# Tag the original Q3 roadmap if it has no tags -q3 = CoPlan::Plan.find_by(title: "Q3 Product Roadmap") -q3.tag_names = ["roadmap", "product"] if q3 && q3.tags.empty? - -puts "Seeding folders..." -if CoPlan::Folder.count == 0 - product = CoPlan::Folder.find_or_create_by_path!("Product/Q3", created_by_user: hampton) - infra = CoPlan::Folder.find_or_create_by_path!("Infrastructure", created_by_user: hampton) - - q3&.update!(folder: product) if q3&.folder_id.nil? - api_seed = CoPlan::Plan.find_by(title: "API Rate Limiting Strategy") - api_seed&.update!(folder: infra) if api_seed&.folder_id.nil? -end - -puts "Done! #{CoPlan::User.count} users, #{CoPlan::Plan.count} plans, #{CoPlan::Folder.count} folders, #{CoPlan::CommentThread.count} threads, #{CoPlan::Comment.count} comments, #{CoPlan::ApiToken.count} API tokens." +CoPlan::DevelopmentSeed.call diff --git a/db/seeds/development.rb b/db/seeds/development.rb new file mode 100644 index 00000000..1b59331c --- /dev/null +++ b/db/seeds/development.rb @@ -0,0 +1,421 @@ +module CoPlan + # A deterministic, idempotent dataset for exercising CoPlan's development UI. + # Existing seed documents keep local content edits; missing records are restored + # on the next run and seed-owned metadata remains consistent. + module DevelopmentSeed + module_function + + USERS = [ + { key: "alex", name: "Alex Morgan", username: "alex", email: "alex@example.com", title: "Staff Engineer", team: "Platform", admin: true }, + { key: "priya", name: "Priya Shah", username: "priya", email: "priya@example.com", title: "Product Manager", team: "Growth" }, + { key: "mateo", name: "Mateo García", username: "mateo", email: "mateo@example.com", title: "Senior Designer", team: "Mobile" }, + { key: "aiko", name: "佐藤 愛子", username: "aiko", email: "aiko@example.com", title: "Engineering Manager", team: "Reliability" }, + { key: "noura", name: "نورا الخطيب", username: "noura", email: "noura@example.com", title: "Security Engineer", team: "Trust" }, + { key: "sam", name: "Sam Okafor", username: "sam", email: "sam@example.com", title: "Data Scientist", team: "Insights" } + ].freeze + + PLAN_TYPES = [ + { name: "General", icon: "file-text", description: "A flexible document for notes and proposals", default_tags: [] }, + { name: "RFC", icon: "scroll", description: "A request for comments on a significant change", default_tags: [ "rfc" ] }, + { name: "Design Doc", icon: "compass", description: "Technical design for a system or feature", default_tags: [ "design" ] }, + { name: "ADR", icon: "scale", description: "A durable architecture decision record", default_tags: [ "architecture" ] }, + { name: "Product Brief", icon: "lightbulb", description: "Product context, goals, and measures of success", default_tags: [ "product" ] }, + { name: "Runbook", icon: "wrench", description: "Operational diagnosis and recovery steps", default_tags: [ "operations" ] }, + { name: "Research Note", icon: "flask", description: "Findings, evidence, and open questions", default_tags: [ "research" ] }, + { name: "Roadmap", icon: "map", description: "Sequenced outcomes and milestones", default_tags: [ "roadmap" ] } + ].freeze + + DOCUMENTS = [ + { + key: "one-line-decision", author: "alex", type: "ADR", title: "Use UUIDv7 identifiers", + tags: %w[architecture database], visibility: "published", folder: "Engineering/Architecture decisions", + content: <<~MARKDOWN + # Use UUIDv7 identifiers + + We will use time-sortable UUIDv7 identifiers for new application records. + MARKDOWN + }, + { + key: "api-gateway", author: "alex", type: "RFC", title: "RFC: Consolidating edge authentication in the API gateway", + tags: %w[api security infrastructure], visibility: "published", folder: "Engineering/Active projects", + content: <<~MARKDOWN + # Consolidating edge authentication in the API gateway + + ## Context + + Three public services currently validate credentials independently. Their behavior has drifted, and clients receive inconsistent errors. + + ## Proposal + + Move credential validation to the gateway while leaving resource authorization in each service. + + ```mermaid + flowchart LR + Client --> Gateway + Gateway --> Identity[Identity service] + Gateway --> Catalog[Catalog API] + Gateway --> Billing[Billing API] + Catalog --> DB[(Catalog database)] + Billing --> Ledger[(Ledger)] + ``` + + ## Rollout + + 1. Mirror validation decisions without enforcement. + 2. Compare decisions and resolve mismatches. + 3. Enable enforcement for internal clients, then external clients. + + ## Open questions + + - Where should rate-limit identity be derived? + - How long should revoked credentials remain cached? + MARKDOWN + }, + { + key: "mobile-checkout", author: "mateo", type: "Design Doc", title: "Mobile checkout: resilient state transitions when the network disappears", + tags: %w[mobile reliability payments], visibility: "published", folder: "Product/Mobile", + content: <<~MARKDOWN + # Mobile checkout under unreliable networks + + ## Goals + + Preserve the customer's intent, prevent duplicate charges, and make recovery understandable. + + ## State model + + ```mermaid + stateDiagram-v2 + [*] --> Editing + Editing --> Submitting: Pay + Submitting --> Confirmed: accepted + Submitting --> Retryable: timeout + Retryable --> Submitting: retry + Retryable --> Cancelled: cancel + Confirmed --> [*] + Cancelled --> [*] + ``` + + ## Offline behavior + + The client stores an idempotency key and a redacted checkout snapshot before sending the request. It never reports success without server confirmation. + + ## Accessibility + + Status changes are announced without stealing focus. Recovery actions use explicit labels rather than color alone. + MARKDOWN + }, + { + key: "spanish-brief", author: "mateo", type: "Product Brief", title: "Mejoras para la experiencia de incorporación", + tags: %w[onboarding product localization], visibility: "published", folder: "Product/Discovery", + content: <<~MARKDOWN + # Mejoras para la experiencia de incorporación + + ## Problema + + Las personas nuevas no saben qué paso completar después de crear su cuenta. + + ## Resultado esperado + + Una lista breve y personalizada muestra el siguiente paso, explica su valor y permite omitir tareas no relevantes. + + ## Métricas + + - Tiempo hasta completar la primera tarea + - Porcentaje de cuentas activas después de siete días + - Tasa de abandono por paso + MARKDOWN + }, + { + key: "japanese-roadmap", author: "aiko", type: "Roadmap", title: "信頼性向上ロードマップ — 2027年前半", + tags: %w[reliability roadmap], visibility: "published", folder: "Operations/Reliability", + content: <<~MARKDOWN + # 信頼性向上ロードマップ + + ## 目標 + + 障害の影響を小さくし、復旧までの時間を短縮します。 + + ## 第1四半期 + + - 重要なユーザーフローのSLOを定義する + - アラートの重複を減らす + - 復旧手順を自動で検証する + + ## 第2四半期 + + - 地域フェイルオーバー演習 + - キャパシティ予測の導入 + - インシデントレビューの改善 + MARKDOWN + }, + { + key: "arabic-research", author: "noura", type: "Research Note", title: "بحث: تقليل مخاطر سرقة الجلسات", + tags: %w[security research authentication], visibility: "published", folder: "Research/Security", + content: <<~MARKDOWN + # تقليل مخاطر سرقة الجلسات + + ## الملخص + + تقارن هذه المذكرة بين الجلسات قصيرة العمر، وتدوير الرموز، وربط الجلسة بالجهاز. + + ## النتائج الأولية + + - تقليل مدة الجلسة يحد من نافذة الهجوم. + - تدوير الرموز يحتاج إلى كشف موثوق لإعادة الاستخدام. + - ربط الجلسة بخصائص متغيرة قد يمنع مستخدمين شرعيين. + + ## الخطوة التالية + + تشغيل تجربة محكومة تقيس الأمان ومعدل طلبات تسجيل الدخول الجديدة. + MARKDOWN + }, + { + key: "incident-runbook", author: "aiko", type: "Runbook", title: "Payments API latency incident runbook", + tags: %w[operations payments on-call], visibility: "published", folder: "Operations/Runbooks", + content: <<~MARKDOWN + # Payments API latency incident runbook + + ## Trigger + + Use this runbook when p95 latency exceeds 800 ms for ten minutes. + + ## Triage + + 1. Confirm whether errors and saturation increased with latency. + 2. Compare regions and client versions. + 3. Check the latest deploy and dependency health. + + ## Mitigation + + Roll back a correlated deploy or shed optional enrichment calls. Do not increase timeouts before identifying the constrained resource. + + ## Escalation + + Page the database owner when connection utilization exceeds 85%. Page the ledger owner when authorization latency is isolated downstream. + MARKDOWN + }, + { + key: "experiment-results", author: "sam", type: "Research Note", title: "Search ranking experiment #42", + tags: %w[search experimentation data], visibility: "published", folder: "Research/Experiments", + content: <<~MARKDOWN + # Search ranking experiment #42 + + ## Hypothesis + + Boosting recently edited documents will improve successful search sessions without reducing result diversity. + + ## Result + + The treatment increased successful sessions by **2.8%** with a 95% confidence interval of **1.1–4.5%**. + + | Metric | Control | Treatment | + | --- | ---: | ---: | + | Successful sessions | 61.2% | 62.9% | + | Reformulation rate | 18.4% | 17.7% | + | Unique results opened | 2.3 | 2.3 | + + ## Decision + + Ship the boost at 50%, monitor for two weeks, then expand. + MARKDOWN + }, + { + key: "draft-notes", author: "priya", type: "General", title: "Untitled thoughts on activation", + tags: %w[product draft], visibility: "draft", folder: "Personal notes", + content: <<~MARKDOWN + # Activation notes + + What if the first-run experience began with a real task instead of a product tour? + + ## Questions + + - Which task has the shortest time to value? + - Can we infer intent without another form? + MARKDOWN + }, + { + key: "archived-proposal", author: "alex", type: "RFC", title: "Retired proposal: weekly XML exports", + tags: %w[archive integrations], visibility: "published", archived: true, folder: "Engineering/Archive", + content: <<~MARKDOWN + # Weekly XML exports + + This proposal was retired after customer interviews showed a preference for incremental webhooks and CSV downloads. + MARKDOWN + }, + { + key: "emoji-title", author: "priya", type: "Product Brief", title: "Faster feedback loops ⚡", + tags: %w[collaboration product], visibility: "published", folder: "Product/Discovery", + content: <<~MARKDOWN + # Faster feedback loops ⚡ + + ## Idea + + Let reviewers react to a specific proposal before composing detailed feedback. + + ## Guardrail + + Reactions supplement comments; they never replace a required approval or accessibility label. + MARKDOWN + }, + { + key: "long-title", author: "noura", type: "Design Doc", + title: "Designing a privacy-preserving audit trail for delegated administrative actions across regional data boundaries", + tags: %w[security privacy compliance], visibility: "published", folder: "Engineering/Architecture decisions", + content: <<~MARKDOWN + # Privacy-preserving audit trail + + ## Requirements + + Every delegated action is attributable and tamper-evident while sensitive payload fields remain in their region of origin. + + ## Design + + Regional writers store the complete event and publish a signed, redacted envelope to the global index. Investigators request privileged fields through the existing approval workflow. + + ```mermaid + sequenceDiagram + participant A as Administrator + participant R as Regional writer + participant G as Global index + A->>R: Delegated action + R->>R: Store complete event + R->>G: Signed redacted envelope + G-->>A: Receipt ID + ``` + + ## Retention + + Envelopes remain globally searchable for one year. Region-specific policy controls complete event retention. + MARKDOWN + }, + { + key: "long-mobile-toc", author: "priya", type: "Product Brief", title: "The complete guide to launching shared workspaces across web, iOS, and Android", + tags: %w[collaboration mobile launch], visibility: "published", folder: "Product/Launches/Shared workspace", + content: nil + } + ].freeze + + LONG_DOCUMENT_SECTIONS = [ + [ "Executive summary", "Shared workspaces let a group collect plans, decisions, and operational knowledge without changing who owns the source documents." ], + [ "Customer problem", "Teams currently reproduce links in chat, bookmarks, and spreadsheets. The copies drift and new teammates cannot tell which collection is authoritative." ], + [ "Audience", "The first release serves teams of 5–50 people coordinating a product launch or an operational program." ], + [ "Principles", "Preserve document ownership, make access legible, and keep common organization tasks reversible." ], + [ "Goals", "Create a workspace, invite collaborators, organize documents, and understand recent activity from every supported client." ], + [ "Non-goals", "This release does not replace project management, synchronize external drives, or introduce custom permission roles." ], + [ "Jobs to be done", "A lead can assemble the current context; a reviewer can find decisions; a new teammate can orient themselves without asking for a link dump." ], + [ "Information architecture", "Workspace navigation separates stable collections from recency-driven activity. Search spans both without moving documents." ], + [ "Web navigation", "The web client uses a persistent sidebar, keyboard navigation, and direct URLs for every folder." ], + [ "iOS navigation", "The iOS client uses a compact root list and preserves the last open folder when switching tabs." ], + [ "Android navigation", "Android follows the same hierarchy while using platform-native back behavior and predictive back previews." ], + [ "Small-screen table of contents", "The document outline is independently scrollable so every section remains reachable without pushing the article off screen." ], + [ "Workspace creation", "Creation asks only for a name. Defaults are useful immediately and can be refined after the first document is added." ], + [ "Invitations", "Invitations identify the workspace, inviter, granted access, and expiration before the recipient accepts." ], + [ "Roles and permissions", "Owners manage access; editors organize content; viewers browse and comment. Source-document permissions remain authoritative." ], + [ "Document placement", "Adding a document creates a placement, not a copy. The same document may appear in several workspaces." ], + [ "Folder behavior", "Folders support three levels, clear breadcrumbs, spring-loaded drag targets, and an explicit move action." ], + [ "Search", "Results explain whether a match came from title, content, author, or tags and retain the active workspace scope." ], + [ "Activity", "The activity feed groups noisy operations and highlights decisions, comments, access changes, and newly added documents." ], + [ "Notifications", "Notifications are opt-in by workspace and bundled to avoid sending one message for every organizational change." ], + [ "Empty states", "Each empty state explains the value of the surface and offers one primary next action." ], + [ "Loading and offline states", "Clients preserve the visible hierarchy during refresh and distinguish cached content from confirmed server state." ], + [ "Accessibility", "All organization flows support keyboard and screen readers, visible focus, reduced motion, and 200% text scaling." ], + [ "Localization", "Layouts tolerate long labels and mixed writing directions. Dates, counts, and sorting follow the viewer's locale." ], + [ "Analytics", "Measure workspace activation, successful document discovery, invitation acceptance, and seven-day returning collaboration." ], + [ "Privacy", "Private drafts never surface through workspace discovery. Audit events record access changes without copying document contents." ], + [ "Performance budgets", "Cached navigation responds within 100 ms; server-backed folder changes acknowledge within one second at p95." ], + [ "Migration", "Existing personal folders remain personal. Users choose which collections to promote into shared workspaces." ], + [ "Rollout", "Begin with internal teams, then a small customer cohort, then regional availability after reliability gates pass." ], + [ "Support readiness", "Support receives troubleshooting guides, permission diagrams, and a safe impersonation-free diagnostic view." ], + [ "Risks", "Permission confusion, notification fatigue, and deep hierarchies are the primary adoption risks." ], + [ "Open questions", "Should viewers be able to suggest folder moves? Which events deserve email? How should inactive workspaces be presented?" ], + [ "Decision log", "Record changes to permissions, hierarchy limits, rollout gates, and client parity expectations here." ], + [ "Appendix: terminology", "A workspace is a shared collection; a placement points to a document; a folder organizes placements." ] + ].freeze + + def call + puts "Seeding reusable development data..." + users = seed_users + plan_types = seed_plan_types + plans = seed_documents(users, plan_types) + seed_shared_library_examples(users, plans) + + puts "Done! #{User.count} users, #{Plan.count} documents, #{Folder.count} folders, #{Tag.count} tags, and #{PlanType.count} document types." + end + + def seed_users + USERS.index_with do |attributes| + user = User.find_or_initialize_by(external_id: "seed:#{attributes.fetch(:key)}") + user.assign_attributes(attributes.except(:key).merge(metadata: user.metadata.to_h.merge("development_seed" => true))) + user.save! + user.library + user + end.transform_keys { |attributes| attributes.fetch(:key) } + end + + def seed_plan_types + PLAN_TYPES.index_with do |attributes| + PlanType.find_or_initialize_by(name: attributes.fetch(:name)).tap do |plan_type| + plan_type.assign_attributes(attributes) + plan_type.save! + end + end.transform_keys { |attributes| attributes.fetch(:name) } + end + + def seed_documents(users, plan_types) + DOCUMENTS.index_with do |definition| + author = users.fetch(definition.fetch(:author)) + plan = find_seed_document(definition.fetch(:key), author) + + unless plan + content = definition[:content] || long_document_content + plan = Plans::Create.call( + title: definition.fetch(:title), + content: content, + user: author, + plan_type_id: plan_types.fetch(definition.fetch(:type)).id, + visibility: definition.fetch(:visibility) + ) + plan.update!( + archived_at: definition[:archived] ? Time.current : nil, + metadata: plan.metadata.to_h.merge("development_seed_key" => definition.fetch(:key)) + ) + end + + plan.tag_names = definition.fetch(:tags) + place(plan, definition.fetch(:folder), author) + plan + end.transform_keys { |definition| definition.fetch(:key) } + end + + def find_seed_document(key, author) + author.created_plans.detect { |plan| plan.metadata.to_h["development_seed_key"] == key } + end + + def place(plan, path, user) + folder = Folder.find_or_create_by_path!(path, library: user.library, created_by_user: user) + result = Plans::Place.call(plan: plan, folder: folder, actor: user) + raise result.error unless result.success? + end + + def seed_shared_library_examples(users, plans) + # Demonstrate that a published document can sit on someone else's shelf + # without changing the author's organization. + place(plans.fetch("api-gateway"), "Reading list/Security", users.fetch("noura")) + place(plans.fetch("experiment-results"), "Research to discuss", users.fetch("alex")) + end + + def long_document_content + sections = LONG_DOCUMENT_SECTIONS.map do |heading, body| + "## #{heading}\n\n#{body}" + end.join("\n\n") + + <<~MARKDOWN + # The complete guide to launching shared workspaces + + This deliberately long seed document exercises the desktop and mobile table of contents, deep document scrolling, and headings with varied lengths. + + #{sections} + MARKDOWN + end + end +end diff --git a/spec/lib/development_seed_spec.rb b/spec/lib/development_seed_spec.rb new file mode 100644 index 00000000..68428361 --- /dev/null +++ b/spec/lib/development_seed_spec.rb @@ -0,0 +1,55 @@ +require "rails_helper" +require Rails.root.join("db/seeds/development") + +RSpec.describe CoPlan::DevelopmentSeed do + describe ".call" do + it "creates a varied, idempotent development dataset" do + described_class.call + + counts = [ + CoPlan::User.count, + CoPlan::Plan.count, + CoPlan::Folder.count, + CoPlan::Tag.count, + CoPlan::PlanType.count, + CoPlan::PlanPlacement.count + ] + + described_class.call + + expect([ + CoPlan::User.count, + CoPlan::Plan.count, + CoPlan::Folder.count, + CoPlan::Tag.count, + CoPlan::PlanType.count, + CoPlan::PlanPlacement.count + ]).to eq(counts) + + seeded_plans = CoPlan::Plan.select { |plan| plan.metadata.to_h.key?("development_seed_key") } + expect(seeded_plans.size).to be >= 12 + expect(seeded_plans.map(&:visibility)).to include("draft", "published") + expect(seeded_plans).to include(be_archived) + expect(seeded_plans.map { |plan| plan.plan_type.name }.uniq.size).to be >= 7 + expect(seeded_plans.flat_map(&:tag_names).uniq.size).to be >= 12 + expect(seeded_plans.map(&:title)).to include(a_string_matching(/信頼性/), a_string_matching(/تقليل/)) + + long_document = seeded_plans.find { |plan| plan.metadata["development_seed_key"] == "long-mobile-toc" } + expect(long_document.current_content.scan(/^## /).size).to be >= 30 + expect(seeded_plans.count { |plan| plan.current_content.include?("```mermaid") }).to be >= 3 + expect(CoPlan::Folder.where.not(parent_id: nil)).to exist + end + + it "preserves edits to existing seed document content and titles" do + described_class.call + plan = CoPlan::Plan.detect { |candidate| candidate.metadata.to_h["development_seed_key"] == "api-gateway" } + plan.update!(title: "Locally edited title") + plan.current_plan_version.update!(content_markdown: "# Locally edited content", content_sha256: nil) + + described_class.call + + expect(plan.reload.title).to eq("Locally edited title") + expect(plan.current_content).to eq("# Locally edited content") + end + end +end From 09588a153a2f41e093df26b57e2769891d90af6a Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Tue, 28 Jul 2026 15:28:36 -0400 Subject: [PATCH 2/3] Use Faker for development seeds Amp-Thread-ID: https://ampcode.com/threads/T-019faa2f-4e22-737a-ac6c-bab5fb9c5602 Co-authored-by: Amp --- Gemfile | 3 + Gemfile.lock | 4 ++ db/seeds.rb | 9 ++- db/seeds/development.rb | 94 +++++++++++++++---------------- spec/lib/development_seed_spec.rb | 4 +- 5 files changed, 63 insertions(+), 51 deletions(-) diff --git a/Gemfile b/Gemfile index 054f8860..5a0ec0ed 100644 --- a/Gemfile +++ b/Gemfile @@ -61,6 +61,9 @@ group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + # Generate realistic, reproducible local seed data + gem "faker" + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) gem "bundler-audit", require: false diff --git a/Gemfile.lock b/Gemfile.lock index dc1b40d9..6cd855cf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -165,6 +165,8 @@ GEM factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) + faker (3.8.0) + i18n (>= 1.8.11, < 2) faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) json @@ -537,6 +539,7 @@ DEPENDENCIES debug diffy factory_bot_rails + faker image_processing (~> 1.2) importmap-rails jbuilder @@ -612,6 +615,7 @@ CHECKSUMS event_stream_parser (1.0.0) sha256=a2683bab70126286f8184dc88f7968ffc4028f813161fb073ec90d171f7de3c8 factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077 factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 + faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c faraday-mashify (1.0.2) sha256=ebdc93b3f807af9a4b18c3d967fc7bcda01e297aed9cc2015ffe8db6e7add2e9 faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 diff --git a/db/seeds.rb b/db/seeds.rb index 29db1bb3..f133f618 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,3 +1,6 @@ -require_relative "seeds/development" - -CoPlan::DevelopmentSeed.call +if Rails.env.local? + require_relative "seeds/development" + CoPlan::DevelopmentSeed.call +else + warn "Development seed data is only available in development and test environments." +end diff --git a/db/seeds/development.rb b/db/seeds/development.rb index 1b59331c..8cca72a9 100644 --- a/db/seeds/development.rb +++ b/db/seeds/development.rb @@ -1,3 +1,5 @@ +require "faker" + module CoPlan # A deterministic, idempotent dataset for exercising CoPlan's development UI. # Existing seed documents keep local content edits; missing records are restored @@ -6,12 +8,12 @@ module DevelopmentSeed module_function USERS = [ - { key: "alex", name: "Alex Morgan", username: "alex", email: "alex@example.com", title: "Staff Engineer", team: "Platform", admin: true }, - { key: "priya", name: "Priya Shah", username: "priya", email: "priya@example.com", title: "Product Manager", team: "Growth" }, - { key: "mateo", name: "Mateo García", username: "mateo", email: "mateo@example.com", title: "Senior Designer", team: "Mobile" }, - { key: "aiko", name: "佐藤 愛子", username: "aiko", email: "aiko@example.com", title: "Engineering Manager", team: "Reliability" }, - { key: "noura", name: "نورا الخطيب", username: "noura", email: "noura@example.com", title: "Security Engineer", team: "Trust" }, - { key: "sam", name: "Sam Okafor", username: "sam", email: "sam@example.com", title: "Data Scientist", team: "Insights" } + { key: "alex", locale: :en, admin: true }, + { key: "priya", locale: :en }, + { key: "mateo", locale: :es }, + { key: "aiko", locale: :ja }, + { key: "noura", locale: :ar }, + { key: "sam", locale: :en } ].freeze PLAN_TYPES = [ @@ -296,48 +298,22 @@ module DevelopmentSeed ].freeze LONG_DOCUMENT_SECTIONS = [ - [ "Executive summary", "Shared workspaces let a group collect plans, decisions, and operational knowledge without changing who owns the source documents." ], - [ "Customer problem", "Teams currently reproduce links in chat, bookmarks, and spreadsheets. The copies drift and new teammates cannot tell which collection is authoritative." ], - [ "Audience", "The first release serves teams of 5–50 people coordinating a product launch or an operational program." ], - [ "Principles", "Preserve document ownership, make access legible, and keep common organization tasks reversible." ], - [ "Goals", "Create a workspace, invite collaborators, organize documents, and understand recent activity from every supported client." ], - [ "Non-goals", "This release does not replace project management, synchronize external drives, or introduce custom permission roles." ], - [ "Jobs to be done", "A lead can assemble the current context; a reviewer can find decisions; a new teammate can orient themselves without asking for a link dump." ], - [ "Information architecture", "Workspace navigation separates stable collections from recency-driven activity. Search spans both without moving documents." ], - [ "Web navigation", "The web client uses a persistent sidebar, keyboard navigation, and direct URLs for every folder." ], - [ "iOS navigation", "The iOS client uses a compact root list and preserves the last open folder when switching tabs." ], - [ "Android navigation", "Android follows the same hierarchy while using platform-native back behavior and predictive back previews." ], - [ "Small-screen table of contents", "The document outline is independently scrollable so every section remains reachable without pushing the article off screen." ], - [ "Workspace creation", "Creation asks only for a name. Defaults are useful immediately and can be refined after the first document is added." ], - [ "Invitations", "Invitations identify the workspace, inviter, granted access, and expiration before the recipient accepts." ], - [ "Roles and permissions", "Owners manage access; editors organize content; viewers browse and comment. Source-document permissions remain authoritative." ], - [ "Document placement", "Adding a document creates a placement, not a copy. The same document may appear in several workspaces." ], - [ "Folder behavior", "Folders support three levels, clear breadcrumbs, spring-loaded drag targets, and an explicit move action." ], - [ "Search", "Results explain whether a match came from title, content, author, or tags and retain the active workspace scope." ], - [ "Activity", "The activity feed groups noisy operations and highlights decisions, comments, access changes, and newly added documents." ], - [ "Notifications", "Notifications are opt-in by workspace and bundled to avoid sending one message for every organizational change." ], - [ "Empty states", "Each empty state explains the value of the surface and offers one primary next action." ], - [ "Loading and offline states", "Clients preserve the visible hierarchy during refresh and distinguish cached content from confirmed server state." ], - [ "Accessibility", "All organization flows support keyboard and screen readers, visible focus, reduced motion, and 200% text scaling." ], - [ "Localization", "Layouts tolerate long labels and mixed writing directions. Dates, counts, and sorting follow the viewer's locale." ], - [ "Analytics", "Measure workspace activation, successful document discovery, invitation acceptance, and seven-day returning collaboration." ], - [ "Privacy", "Private drafts never surface through workspace discovery. Audit events record access changes without copying document contents." ], - [ "Performance budgets", "Cached navigation responds within 100 ms; server-backed folder changes acknowledge within one second at p95." ], - [ "Migration", "Existing personal folders remain personal. Users choose which collections to promote into shared workspaces." ], - [ "Rollout", "Begin with internal teams, then a small customer cohort, then regional availability after reliability gates pass." ], - [ "Support readiness", "Support receives troubleshooting guides, permission diagrams, and a safe impersonation-free diagnostic view." ], - [ "Risks", "Permission confusion, notification fatigue, and deep hierarchies are the primary adoption risks." ], - [ "Open questions", "Should viewers be able to suggest folder moves? Which events deserve email? How should inactive workspaces be presented?" ], - [ "Decision log", "Record changes to permissions, hierarchy limits, rollout gates, and client parity expectations here." ], - [ "Appendix: terminology", "A workspace is a shared collection; a placement points to a document; a folder organizes placements." ] + "Executive summary", "Customer problem", "Audience", "Principles", "Goals", "Non-goals", + "Jobs to be done", "Information architecture", "Web navigation", "iOS navigation", "Android navigation", + "Small-screen table of contents", "Workspace creation", "Invitations", "Roles and permissions", + "Document placement", "Folder behavior", "Search", "Activity", "Notifications", "Empty states", + "Loading and offline states", "Accessibility", "Localization", "Analytics", "Privacy", "Performance budgets", + "Migration", "Rollout", "Support readiness", "Risks", "Open questions", "Decision log", "Appendix: terminology" ].freeze def call puts "Seeding reusable development data..." - users = seed_users - plan_types = seed_plan_types - plans = seed_documents(users, plan_types) - seed_shared_library_examples(users, plans) + with_reproducible_faker do + users = seed_users + plan_types = seed_plan_types + plans = seed_documents(users, plan_types) + seed_shared_library_examples(users, plans) + end puts "Done! #{User.count} users, #{Plan.count} documents, #{Folder.count} folders, #{Tag.count} tags, and #{PlanType.count} document types." end @@ -345,7 +321,18 @@ def call def seed_users USERS.index_with do |attributes| user = User.find_or_initialize_by(external_id: "seed:#{attributes.fetch(:key)}") - user.assign_attributes(attributes.except(:key).merge(metadata: user.metadata.to_h.merge("development_seed" => true))) + Faker::Config.locale = attributes.fetch(:locale) + name = Faker::Name.name + Faker::Config.locale = :en + user.assign_attributes( + name: name, + username: attributes.fetch(:key), + email: Faker::Internet.unique.email(name: name), + title: Faker::Job.title, + team: Faker::Company.industry, + admin: attributes.fetch(:admin, false), + metadata: user.metadata.to_h.merge("development_seed" => true) + ) user.save! user.library user @@ -405,8 +392,8 @@ def seed_shared_library_examples(users, plans) end def long_document_content - sections = LONG_DOCUMENT_SECTIONS.map do |heading, body| - "## #{heading}\n\n#{body}" + sections = LONG_DOCUMENT_SECTIONS.map do |heading| + "## #{heading}\n\n#{Faker::Lorem.paragraph(sentence_count: 3)}" end.join("\n\n") <<~MARKDOWN @@ -417,5 +404,18 @@ def long_document_content #{sections} MARKDOWN end + + def with_reproducible_faker + previous_random = Faker::Config.random + previous_locale = Faker::Config.locale + Faker::Config.random = Random.new(12_345) + Faker::Config.locale = :en + Faker::UniqueGenerator.clear + yield + ensure + Faker::Config.random = previous_random + Faker::Config.locale = previous_locale + Faker::UniqueGenerator.clear + end end end diff --git a/spec/lib/development_seed_spec.rb b/spec/lib/development_seed_spec.rb index 68428361..593e8ba6 100644 --- a/spec/lib/development_seed_spec.rb +++ b/spec/lib/development_seed_spec.rb @@ -14,6 +14,7 @@ CoPlan::PlanType.count, CoPlan::PlanPlacement.count ] + generated_users = CoPlan::User.order(:external_id).pluck(:name, :email, :title, :team) described_class.call @@ -25,6 +26,7 @@ CoPlan::PlanType.count, CoPlan::PlanPlacement.count ]).to eq(counts) + expect(CoPlan::User.order(:external_id).pluck(:name, :email, :title, :team)).to eq(generated_users) seeded_plans = CoPlan::Plan.select { |plan| plan.metadata.to_h.key?("development_seed_key") } expect(seeded_plans.size).to be >= 12 @@ -42,7 +44,7 @@ it "preserves edits to existing seed document content and titles" do described_class.call - plan = CoPlan::Plan.detect { |candidate| candidate.metadata.to_h["development_seed_key"] == "api-gateway" } + plan = CoPlan::Plan.find { |candidate| candidate.metadata.to_h["development_seed_key"] == "api-gateway" } plan.update!(title: "Locally edited title") plan.current_plan_version.update!(content_markdown: "# Locally edited content", content_sha256: nil) From 9a6927dc7a094a57a99377ccebf98854f89e7425 Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Tue, 28 Jul 2026 15:31:16 -0400 Subject: [PATCH 3/3] Generate seed prose with Faker Amp-Thread-ID: https://ampcode.com/threads/T-019faa2f-4e22-737a-ac6c-bab5fb9c5602 Co-authored-by: Amp --- db/seeds/development.rb | 303 ++++++++++------------------------------ 1 file changed, 73 insertions(+), 230 deletions(-) diff --git a/db/seeds/development.rb b/db/seeds/development.rb index 8cca72a9..b14d7642 100644 --- a/db/seeds/development.rb +++ b/db/seeds/development.rb @@ -30,273 +30,97 @@ module DevelopmentSeed DOCUMENTS = [ { key: "one-line-decision", author: "alex", type: "ADR", title: "Use UUIDv7 identifiers", - tags: %w[architecture database], visibility: "published", folder: "Engineering/Architecture decisions", - content: <<~MARKDOWN - # Use UUIDv7 identifiers - - We will use time-sortable UUIDv7 identifiers for new application records. - MARKDOWN + tags: %w[architecture database], visibility: "published", folder: "Engineering/Architecture decisions", sections: 0 }, { key: "api-gateway", author: "alex", type: "RFC", title: "RFC: Consolidating edge authentication in the API gateway", - tags: %w[api security infrastructure], visibility: "published", folder: "Engineering/Active projects", - content: <<~MARKDOWN - # Consolidating edge authentication in the API gateway - - ## Context - - Three public services currently validate credentials independently. Their behavior has drifted, and clients receive inconsistent errors. - - ## Proposal - - Move credential validation to the gateway while leaving resource authorization in each service. - - ```mermaid - flowchart LR - Client --> Gateway - Gateway --> Identity[Identity service] - Gateway --> Catalog[Catalog API] - Gateway --> Billing[Billing API] - Catalog --> DB[(Catalog database)] - Billing --> Ledger[(Ledger)] - ``` - - ## Rollout - - 1. Mirror validation decisions without enforcement. - 2. Compare decisions and resolve mismatches. - 3. Enable enforcement for internal clients, then external clients. - - ## Open questions - - - Where should rate-limit identity be derived? - - How long should revoked credentials remain cached? - MARKDOWN + tags: %w[api security infrastructure], visibility: "published", folder: "Engineering/Active projects", fixture: :flowchart }, { key: "mobile-checkout", author: "mateo", type: "Design Doc", title: "Mobile checkout: resilient state transitions when the network disappears", - tags: %w[mobile reliability payments], visibility: "published", folder: "Product/Mobile", - content: <<~MARKDOWN - # Mobile checkout under unreliable networks - - ## Goals - - Preserve the customer's intent, prevent duplicate charges, and make recovery understandable. - - ## State model - - ```mermaid - stateDiagram-v2 - [*] --> Editing - Editing --> Submitting: Pay - Submitting --> Confirmed: accepted - Submitting --> Retryable: timeout - Retryable --> Submitting: retry - Retryable --> Cancelled: cancel - Confirmed --> [*] - Cancelled --> [*] - ``` - - ## Offline behavior - - The client stores an idempotency key and a redacted checkout snapshot before sending the request. It never reports success without server confirmation. - - ## Accessibility - - Status changes are announced without stealing focus. Recovery actions use explicit labels rather than color alone. - MARKDOWN + tags: %w[mobile reliability payments], visibility: "published", folder: "Product/Mobile", fixture: :state_diagram }, { key: "spanish-brief", author: "mateo", type: "Product Brief", title: "Mejoras para la experiencia de incorporación", - tags: %w[onboarding product localization], visibility: "published", folder: "Product/Discovery", - content: <<~MARKDOWN - # Mejoras para la experiencia de incorporación - - ## Problema - - Las personas nuevas no saben qué paso completar después de crear su cuenta. - - ## Resultado esperado - - Una lista breve y personalizada muestra el siguiente paso, explica su valor y permite omitir tareas no relevantes. - - ## Métricas - - - Tiempo hasta completar la primera tarea - - Porcentaje de cuentas activas después de siete días - - Tasa de abandono por paso - MARKDOWN + tags: %w[onboarding product localization], visibility: "published", folder: "Product/Discovery", fixture: :spanish }, { key: "japanese-roadmap", author: "aiko", type: "Roadmap", title: "信頼性向上ロードマップ — 2027年前半", - tags: %w[reliability roadmap], visibility: "published", folder: "Operations/Reliability", - content: <<~MARKDOWN - # 信頼性向上ロードマップ - - ## 目標 - - 障害の影響を小さくし、復旧までの時間を短縮します。 - - ## 第1四半期 - - - 重要なユーザーフローのSLOを定義する - - アラートの重複を減らす - - 復旧手順を自動で検証する - - ## 第2四半期 - - - 地域フェイルオーバー演習 - - キャパシティ予測の導入 - - インシデントレビューの改善 - MARKDOWN + tags: %w[reliability roadmap], visibility: "published", folder: "Operations/Reliability", fixture: :japanese }, { key: "arabic-research", author: "noura", type: "Research Note", title: "بحث: تقليل مخاطر سرقة الجلسات", - tags: %w[security research authentication], visibility: "published", folder: "Research/Security", - content: <<~MARKDOWN - # تقليل مخاطر سرقة الجلسات - - ## الملخص - - تقارن هذه المذكرة بين الجلسات قصيرة العمر، وتدوير الرموز، وربط الجلسة بالجهاز. - - ## النتائج الأولية - - - تقليل مدة الجلسة يحد من نافذة الهجوم. - - تدوير الرموز يحتاج إلى كشف موثوق لإعادة الاستخدام. - - ربط الجلسة بخصائص متغيرة قد يمنع مستخدمين شرعيين. - - ## الخطوة التالية - - تشغيل تجربة محكومة تقيس الأمان ومعدل طلبات تسجيل الدخول الجديدة. - MARKDOWN + tags: %w[security research authentication], visibility: "published", folder: "Research/Security", fixture: :arabic }, { key: "incident-runbook", author: "aiko", type: "Runbook", title: "Payments API latency incident runbook", - tags: %w[operations payments on-call], visibility: "published", folder: "Operations/Runbooks", - content: <<~MARKDOWN - # Payments API latency incident runbook - - ## Trigger - - Use this runbook when p95 latency exceeds 800 ms for ten minutes. - - ## Triage - - 1. Confirm whether errors and saturation increased with latency. - 2. Compare regions and client versions. - 3. Check the latest deploy and dependency health. - - ## Mitigation - - Roll back a correlated deploy or shed optional enrichment calls. Do not increase timeouts before identifying the constrained resource. - - ## Escalation - - Page the database owner when connection utilization exceeds 85%. Page the ledger owner when authorization latency is isolated downstream. - MARKDOWN + tags: %w[operations payments on-call], visibility: "published", folder: "Operations/Runbooks" }, { key: "experiment-results", author: "sam", type: "Research Note", title: "Search ranking experiment #42", - tags: %w[search experimentation data], visibility: "published", folder: "Research/Experiments", - content: <<~MARKDOWN - # Search ranking experiment #42 - - ## Hypothesis - - Boosting recently edited documents will improve successful search sessions without reducing result diversity. - - ## Result - - The treatment increased successful sessions by **2.8%** with a 95% confidence interval of **1.1–4.5%**. - - | Metric | Control | Treatment | - | --- | ---: | ---: | - | Successful sessions | 61.2% | 62.9% | - | Reformulation rate | 18.4% | 17.7% | - | Unique results opened | 2.3 | 2.3 | - - ## Decision - - Ship the boost at 50%, monitor for two weeks, then expand. - MARKDOWN + tags: %w[search experimentation data], visibility: "published", folder: "Research/Experiments", fixture: :table }, { key: "draft-notes", author: "priya", type: "General", title: "Untitled thoughts on activation", - tags: %w[product draft], visibility: "draft", folder: "Personal notes", - content: <<~MARKDOWN - # Activation notes - - What if the first-run experience began with a real task instead of a product tour? - - ## Questions - - - Which task has the shortest time to value? - - Can we infer intent without another form? - MARKDOWN + tags: %w[product draft], visibility: "draft", folder: "Personal notes", sections: 1 }, { key: "archived-proposal", author: "alex", type: "RFC", title: "Retired proposal: weekly XML exports", - tags: %w[archive integrations], visibility: "published", archived: true, folder: "Engineering/Archive", - content: <<~MARKDOWN - # Weekly XML exports - - This proposal was retired after customer interviews showed a preference for incremental webhooks and CSV downloads. - MARKDOWN + tags: %w[archive integrations], visibility: "published", archived: true, folder: "Engineering/Archive", sections: 1 }, { key: "emoji-title", author: "priya", type: "Product Brief", title: "Faster feedback loops ⚡", - tags: %w[collaboration product], visibility: "published", folder: "Product/Discovery", - content: <<~MARKDOWN - # Faster feedback loops ⚡ - - ## Idea - - Let reviewers react to a specific proposal before composing detailed feedback. - - ## Guardrail - - Reactions supplement comments; they never replace a required approval or accessibility label. - MARKDOWN + tags: %w[collaboration product], visibility: "published", folder: "Product/Discovery" }, { key: "long-title", author: "noura", type: "Design Doc", title: "Designing a privacy-preserving audit trail for delegated administrative actions across regional data boundaries", - tags: %w[security privacy compliance], visibility: "published", folder: "Engineering/Architecture decisions", - content: <<~MARKDOWN - # Privacy-preserving audit trail - - ## Requirements - - Every delegated action is attributable and tamper-evident while sensitive payload fields remain in their region of origin. - - ## Design - - Regional writers store the complete event and publish a signed, redacted envelope to the global index. Investigators request privileged fields through the existing approval workflow. - - ```mermaid - sequenceDiagram - participant A as Administrator - participant R as Regional writer - participant G as Global index - A->>R: Delegated action - R->>R: Store complete event - R->>G: Signed redacted envelope - G-->>A: Receipt ID - ``` - - ## Retention - - Envelopes remain globally searchable for one year. Region-specific policy controls complete event retention. - MARKDOWN + tags: %w[security privacy compliance], visibility: "published", folder: "Engineering/Architecture decisions", fixture: :sequence_diagram }, { key: "long-mobile-toc", author: "priya", type: "Product Brief", title: "The complete guide to launching shared workspaces across web, iOS, and Android", - tags: %w[collaboration mobile launch], visibility: "published", folder: "Product/Launches/Shared workspace", - content: nil + tags: %w[collaboration mobile launch], visibility: "published", folder: "Product/Launches/Shared workspace", long: true } ].freeze + CONTENT_FIXTURES = { + flowchart: <<~MARKDOWN, + ```mermaid + flowchart LR + Client --> Gateway + Gateway --> API + API --> DB[(Database)] + ``` + MARKDOWN + state_diagram: <<~MARKDOWN, + ```mermaid + stateDiagram-v2 + [*] --> Editing + Editing --> Submitting + Submitting --> Confirmed + Confirmed --> [*] + ``` + MARKDOWN + sequence_diagram: <<~MARKDOWN, + ```mermaid + sequenceDiagram + participant A as Administrator + participant S as Service + A->>S: Delegated action + S-->>A: Receipt ID + ``` + MARKDOWN + table: <<~MARKDOWN, + | Metric | Control | Treatment | + | --- | ---: | ---: | + | Success | 61.2% | 62.9% | + | Errors | 18.4% | 17.7% | + MARKDOWN + spanish: "## Problema\n\nLas personas nuevas necesitan saber qué paso completar.\n\n## Resultado\n\nUna lista breve muestra el siguiente paso.", + japanese: "## 目標\n\n障害の影響を小さくし、復旧までの時間を短縮します。\n\n## 次のステップ\n\n復旧手順を自動で検証します。", + arabic: "## الملخص\n\nتقارن هذه المذكرة بين الجلسات قصيرة العمر وتدوير الرموز.\n\n## الخطوة التالية\n\nتشغيل تجربة محكومة لقياس الأمان." + }.freeze + LONG_DOCUMENT_SECTIONS = [ "Executive summary", "Customer problem", "Audience", "Principles", "Goals", "Non-goals", "Jobs to be done", "Information architecture", "Web navigation", "iOS navigation", "Android navigation", @@ -354,10 +178,9 @@ def seed_documents(users, plan_types) plan = find_seed_document(definition.fetch(:key), author) unless plan - content = definition[:content] || long_document_content plan = Plans::Create.call( title: definition.fetch(:title), - content: content, + content: document_content(definition), user: author, plan_type_id: plan_types.fetch(definition.fetch(:type)).id, visibility: definition.fetch(:visibility) @@ -391,6 +214,26 @@ def seed_shared_library_examples(users, plans) place(plans.fetch("experiment-results"), "Research to discuss", users.fetch("alex")) end + def document_content(definition) + return long_document_content if definition[:long] + + fixture = CONTENT_FIXTURES[definition[:fixture]] + parts = [ "# #{definition.fetch(:title)}" ] + + if %i[spanish japanese arabic].include?(definition[:fixture]) + parts << fixture + return parts.join("\n\n") + end + + parts << Faker::Lorem.paragraph(sentence_count: 3) + definition.fetch(:sections, 3).times do + heading = Faker::Lorem.words(number: 3).join(" ").capitalize + parts << "## #{heading}\n\n#{Faker::Lorem.paragraph(sentence_count: 3)}" + end + parts << fixture if fixture + parts.join("\n\n") + end + def long_document_content sections = LONG_DOCUMENT_SECTIONS.map do |heading| "## #{heading}\n\n#{Faker::Lorem.paragraph(sentence_count: 3)}" @@ -399,7 +242,7 @@ def long_document_content <<~MARKDOWN # The complete guide to launching shared workspaces - This deliberately long seed document exercises the desktop and mobile table of contents, deep document scrolling, and headings with varied lengths. + #{Faker::Lorem.paragraph(sentence_count: 3)} #{sections} MARKDOWN