diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 498b42b..e54c52b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,6 +37,19 @@ jobs: path: .stack-cli persist-credentials: false + - name: Read pinned example corpus contract + id: corpus + run: | + node --input-type=module -e 'import { exampleCorpusSource } from "./scripts/example-corpus.config.mjs"; console.log(`revision=${exampleCorpusSource.revision}`)' >> "$GITHUB_OUTPUT" + + - name: Check out pinned Stack example corpus + uses: actions/checkout@v7 + with: + repository: stack-sh/specification + ref: ${{ steps.corpus.outputs.revision }} + path: .stack-specification + persist-credentials: false + - name: Install minimum supported Rust run: rustup toolchain install 1.85.0 --profile minimal @@ -55,6 +68,11 @@ jobs: - name: Run tests run: npm test + - name: Verify pinned example snapshots + env: + STACK_SPECIFICATION_ROOT: ${{ github.workspace }}/.stack-specification + run: npm run examples:check:source + - name: Smoke test documentation CLI examples env: STACK_CLI_BIN: ${{ github.workspace }}/.stack-cli/target/debug/stack diff --git a/.gitignore b/.gitignore index 617ed6e..b1dc5c4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ docs/.vitepress/cache .wrangler .dev.vars* .stack-cli +.stack-specification # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 674eafa..932617d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ npm run dev npm run docs:dev npm run docs:check npm run docs:test +npm run examples:check npm run format npm run format:check npm run lint @@ -46,6 +47,8 @@ Formatting and linting use Oxfmt and Oxlint. The production build compiles the R CI additionally builds the Stack CLI revision pinned in [`scripts/docs-validation.config.mjs`](./scripts/docs-validation.config.mjs) with its minimum supported Rust version, then runs `npm run docs:smoke` through `STACK_CLI_BIN`. This checks the documented version, help surface, and safe executable examples without adding Rust to normal Web builds. Intentional locale or execution differences must be declared in the same configuration with a non-empty reason; the validator rejects stale exceptions. +The example gallery is generated from the public specification commit pinned in [`scripts/example-corpus.config.mjs`](./scripts/example-corpus.config.mjs). [`example-corpus`](./example-corpus) is a hermetic snapshot for the Playground and documentation build; CI checks every catalog, schema, and `.stack` source byte against the pinned provider commit. `npm run examples:check` checks and renders all examples, resolves every namespaced icon against the published provider catalog, and detects stale SVG thumbnails. To intentionally advance the corpus, check out the new pinned specification revision, run `STACK_SPECIFICATION_ROOT=/path/to/specification npm run examples:sync`, and then run `npm run examples:generate`. + ## Brand mark [`public/favicon.svg`](./public/favicon.svg) is the canonical Stack logo mark for Web surfaces. The Playground uses it as its favicon and header mark; VitePress uses the identical [`docs/public/favicon.svg`](./docs/public/favicon.svg) copy for its favicon, navigation, and localized home pages. The documentation validator rejects the build if the two public-root copies differ. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 76a9a18..b6b57a9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -69,6 +69,7 @@ type Labels = { guide: string introduction: string gettingStarted: string + examples: string playground: string providerIcons: string language: string @@ -107,6 +108,7 @@ function localeTheme(prefix: string, labels: Labels): DefaultTheme.Config { return { nav: [ { text: labels.guide, link: `${prefix}/guide/getting-started` }, + { text: labels.examples, link: `${prefix}/examples/` }, { text: labels.reference, link: `${prefix}/reference/diagnostics-and-limits` }, { text: "Playground", link: "https://stack-diagram.com/", target: "_self" }, ], @@ -131,6 +133,7 @@ const en: Labels = { guide: "Guide", introduction: "What is Stack?", gettingStarted: "Getting started", + examples: "Examples", playground: "Using the Playground", providerIcons: "Provider icons", language: "Language", @@ -159,6 +162,7 @@ const ja: Labels = { guide: "ガイド", introduction: "Stackとは", gettingStarted: "はじめる", + examples: "Example gallery", playground: "Playgroundの使い方", providerIcons: "Provider icon", language: "言語", @@ -187,6 +191,7 @@ const zh: Labels = { guide: "指南", introduction: "什么是 Stack?", gettingStarted: "快速开始", + examples: "示例库", playground: "使用 Playground", providerIcons: "云服务商图标", language: "语言", @@ -215,6 +220,7 @@ const ko: Labels = { guide: "가이드", introduction: "Stack이란?", gettingStarted: "시작하기", + examples: "예제 갤러리", playground: "Playground 사용법", providerIcons: "클라우드 제공자 아이콘", language: "언어", diff --git a/docs/.vitepress/theme/components/ExampleGallery.vue b/docs/.vitepress/theme/components/ExampleGallery.vue new file mode 100644 index 0000000..3e169e3 --- /dev/null +++ b/docs/.vitepress/theme/components/ExampleGallery.vue @@ -0,0 +1,136 @@ + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index fcb1bd7..6d0560e 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -2,6 +2,7 @@ import DefaultTheme from "vitepress/theme" import "@fontsource-variable/ibm-plex-sans" import "@fontsource/ibm-plex-mono/400.css" +import ExampleGallery from "./components/ExampleGallery.vue" import IconCatalog from "./components/IconCatalog.vue" import ProviderCatalog from "./components/ProviderCatalog.vue" import "./style.css" @@ -9,6 +10,7 @@ import "./style.css" export default { extends: DefaultTheme, enhanceApp({ app }) { + app.component("ExampleGallery", ExampleGallery) app.component("IconCatalog", IconCatalog) app.component("ProviderCatalog", ProviderCatalog) }, diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index adddb8b..d0a7b4c 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -588,6 +588,192 @@ body { border-color: var(--vp-c-brand-1); } +.stack-example-gallery { + margin-top: 1.5rem; +} + +.stack-example-gallery__contract { + display: flex; + align-items: baseline; + gap: 0.5rem; + padding: 0.75rem 1rem; + color: var(--vp-c-text-2); + font-size: 0.8125rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 0.375rem; +} + +.vp-doc .stack-example-gallery__contract code { + color: var(--vp-c-text-1); +} + +.stack-example-stage > h2 { + margin-bottom: 1rem; +} + +.stack-example-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.stack-example-card { + display: grid; + min-width: 0; + overflow: hidden; + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-divider); + border-radius: 0.375rem; + grid-template-rows: auto 1fr; +} + +.stack-example-card__preview { + display: grid; + min-height: 12rem; + padding: 1rem; + background: var(--vp-c-bg-soft); + border-bottom: 1px solid var(--vp-c-divider); + place-items: center; +} + +.stack-example-card__preview img { + display: block; + width: 100%; + height: 11rem; + margin: 0; + object-fit: contain; +} + +.stack-example-card__body { + display: flex; + min-width: 0; + padding: 1rem; + flex-direction: column; +} + +.stack-example-card__heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; +} + +.vp-doc .stack-example-card__heading h3 { + margin: 0; + font-size: 1rem; + line-height: 1.4; +} + +.stack-example-card__stage { + color: var(--vp-c-text-2); + font-family: var(--vp-font-family-mono); + font-size: 0.6875rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.vp-doc .stack-example-card__body > p { + margin: 0.625rem 0 0; + color: var(--vp-c-text-2); + font-size: 0.875rem; + line-height: 1.55; +} + +.stack-example-card__meta { + display: grid; + gap: 0.625rem; + padding-top: 0.875rem; + margin-top: 0.875rem; + border-top: 1px solid var(--vp-c-divider); +} + +.stack-example-card__meta > div { + display: grid; + gap: 0.125rem; +} + +.stack-example-card__meta strong, +.stack-example-card__meta span { + font-size: 0.75rem; + line-height: 1.45; +} + +.stack-example-card__meta strong { + color: var(--vp-c-text-3); + font-weight: 500; +} + +.stack-example-card__counts { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.5rem; + margin: 0.875rem 0 0; +} + +.stack-example-card__counts > div { + padding: 0.5rem; + text-align: center; + background: var(--vp-c-bg-soft); + border-radius: 0.25rem; +} + +.stack-example-card__counts dt { + color: var(--vp-c-text-2); + font-size: 0.6875rem; +} + +.stack-example-card__counts dd { + margin: 0.1rem 0 0; + color: var(--vp-c-text-1); + font-family: var(--vp-font-family-mono); + font-size: 0.875rem; + font-weight: 600; +} + +.stack-example-card__features { + margin-top: 0.875rem; + color: var(--vp-c-text-2); + font-size: 0.75rem; +} + +.stack-example-card__features summary { + width: fit-content; + cursor: pointer; + font-weight: 600; +} + +.stack-example-card__features ul { + display: flex; + padding: 0; + margin: 0.625rem 0 0; + flex-wrap: wrap; + gap: 0.375rem; + list-style: none; +} + +.stack-example-card__features li { + padding: 0.125rem 0.375rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 0.25rem; + line-height: 1.35; +} + +.stack-example-card__source { + width: fit-content; + margin-top: auto; + padding-top: 1rem; + font-size: 0.8125rem; + font-weight: 600; +} + +.stack-example-gallery a:focus-visible, +.stack-example-card__features summary:focus-visible { + outline: 2px solid var(--vp-c-brand-1); + outline-offset: 3px; +} + @media (max-width: 639px) { .vp-doc table { display: block; @@ -601,6 +787,10 @@ body { align-items: flex-start; flex-direction: column; } + + .stack-example-list { + grid-template-columns: 1fr; + } } @media (prefers-reduced-motion: reduce) { diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..e0dd853 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,7 @@ +# Example gallery + +Start with a small diagram, then move through groups, layout constraints, provider icons, and production-like systems. Every thumbnail below is generated from the canonical source pinned to the public Stack specification. + +Provider examples keep artwork caller-owned: install the listed icon packs when you want branded icons. Without a pack, the same source remains valid and renders with Stack's deterministic missing-icon fallback. + + diff --git a/docs/ja/examples/index.md b/docs/ja/examples/index.md new file mode 100644 index 0000000..690ea1d --- /dev/null +++ b/docs/ja/examples/index.md @@ -0,0 +1,7 @@ +# Example gallery + +小さなdiagramから始め、group、layout constraint、provider icon、production-likeなsystemへ段階的に進めます。下のthumbnailはすべて、公開Stack specificationへpinしたcanonical sourceから生成しています。 + +Provider exampleのartworkはcaller-ownedです。Brand iconを使う場合は表示されたicon packを導入してください。Packがなくても同じsourceはvalidで、Stackのdeterministicなmissing-icon fallbackを使ってrenderされます。 + + diff --git a/docs/ko/examples/index.md b/docs/ko/examples/index.md new file mode 100644 index 0000000..e9908e6 --- /dev/null +++ b/docs/ko/examples/index.md @@ -0,0 +1,7 @@ +# 예제 갤러리 + +작은 다이어그램에서 시작해 그룹, 레이아웃 제약, 클라우드 제공자 아이콘, 프로덕션 수준의 시스템으로 단계적으로 확장합니다. 아래의 모든 썸네일은 공개 Stack 명세에 고정된 canonical source에서 생성됩니다. + +Provider 예제의 아트워크는 caller-owned입니다. 브랜드 아이콘이 필요하면 표시된 아이콘 팩을 설치하세요. 팩이 없어도 같은 소스는 유효하며 Stack의 deterministic missing-icon fallback으로 렌더링됩니다. + + diff --git a/docs/public/examples/application-and-data.svg b/docs/public/examples/application-and-data.svg new file mode 100644 index 0000000..2700645 --- /dev/null +++ b/docs/public/examples/application-and-data.svg @@ -0,0 +1,47 @@ + + + Application and datastore + Architecture diagram with 2 nodes, 0 groups, and 1 relationship. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + Application and datastore + + + + + Application flows to Primary database: SQL + + + + + + Application: Business logic + + + Application + Business logic + + + Primary database: PostgreSQL + + + + Primary database + PostgreSQL + + + + diff --git a/docs/public/examples/aws-serverless-checkout.svg b/docs/public/examples/aws-serverless-checkout.svg new file mode 100644 index 0000000..c736fce --- /dev/null +++ b/docs/public/examples/aws-serverless-checkout.svg @@ -0,0 +1,142 @@ + + + AWS serverless checkout + Architecture diagram with 8 nodes, 3 groups, and 7 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + AWS serverless checkout + + + Edge + + Edge + + + Serverless compute + + Serverless compute + + + Data + + Data + + + + + Customer flows to CloudFront: HTTPS + + + + CloudFront flows to API Gateway: API request + + + + API Gateway flows to Checkout: Invoke + + + + Checkout flows to Orders: Write order + + + + Checkout flows to Order events: OrderPlaced + + + + Order events flows to Fulfillment: Dispatch + + + + Fulfillment flows to Receipts: Archive + + + + + + Customer + + + Customer + + + CloudFront + + + CloudFront + + + API Gateway + + + API Gateway + + + Checkout + + + Checkout + + + Order events + + + Order events + + + Fulfillment + + + Fulfillment + + + Orders + + + + Orders + + + Receipts + + + Receipts + + + + diff --git a/docs/public/examples/azure-event-platform.svg b/docs/public/examples/azure-event-platform.svg new file mode 100644 index 0000000..e65dd92 --- /dev/null +++ b/docs/public/examples/azure-event-platform.svg @@ -0,0 +1,142 @@ + + + Azure event platform + Architecture diagram with 8 nodes, 3 groups, and 7 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + Azure event platform + + + Delivery + + Delivery + + + Event processing + + Event processing + + + Data + + Data + + + + + Customer flows to Front Door: HTTPS + + + + Front Door flows to App Service: Route + + + + App Service flows to Ingest function: Submit + + + + Ingest function flows to Service Bus: Work item + + + + Service Bus flows to Background function: Dispatch + + + + Background function flows to Cosmos DB: Persist + + + + Background function flows to Storage account: Archive + + + + + + Customer + + + Customer + + + Front Door + + + Front Door + + + App Service + + + App Service + + + Ingest function + + + Ingest function + + + Service Bus + + + Service Bus + + + Background function + + + Background function + + + Cosmos DB + + + + Cosmos DB + + + Storage account + + + Storage account + + + + diff --git a/docs/public/examples/commerce-platform.svg b/docs/public/examples/commerce-platform.svg new file mode 100644 index 0000000..a26b025 --- /dev/null +++ b/docs/public/examples/commerce-platform.svg @@ -0,0 +1,239 @@ + + + Commerce platform + Architecture diagram with 13 nodes, 5 groups, and 12 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + Commerce platform + + + Storefront + + Storefront + + + Commerce services + + Commerce services + + + Asynchronous processing + + Asynchronous processing + + + Data + + Data + + + External systems + + External systems + + + + + Customer flows to Web storefront: Browse and buy + + + + Web storefront flows to Edge gateway: HTTPS + + + + Edge gateway flows to Catalog API: Catalog requests + + + + Edge gateway flows to Checkout API: Checkout requests + + + + Catalog API flows to Product database: SQL + + + + Catalog API flows to Product media: Media URLs + + + + Checkout API flows to Order database: Transactions + + + + Checkout API flows to Payment provider: Payment API + + + + Checkout API flows to Event bus: OrderPlaced + + + + Event bus flows to Fulfillment worker: OrderPlaced + + + + Event bus flows to Notification worker: OrderPlaced + + + + Notification worker flows to Email provider: Send receipt + + + + + + Customer + + + Customer + + + Web storefront: Next.js + + + Web storefront + Next.js + + + Edge gateway + + + Edge gateway + + + Catalog API: Products and pricing + + + Catalog API + Products and pricing + + + Checkout API: Order orchestration + + + Checkout API + Order orchestration + + + Event bus + + + Event bus + + + Fulfillment worker + + + Fulfillment worker + + + Notification worker + + + Notification worker + + + Product database + + + + Product database + + + Order database + + + + Order database + + + Product media + + + Product media + + + Payment provider + + + Payment provider + + + Email provider + + + Email provider + + + + diff --git a/docs/public/examples/gcp-data-service.svg b/docs/public/examples/gcp-data-service.svg new file mode 100644 index 0000000..edbc5fc --- /dev/null +++ b/docs/public/examples/gcp-data-service.svg @@ -0,0 +1,111 @@ + + + GCP data service + Architecture diagram with 6 nodes, 2 groups, and 5 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + GCP data service + + + Application + + Application + + + Analytics + + Analytics + + + + + Web client flows to Cloud Run API: HTTPS + + + + Cloud Run API flows to Cloud SQL: SQL + + + + Cloud Run API flows to Cloud Storage: Uploads + + + + Cloud Run API flows to BigQuery: Events + + + + Analyst flows to BigQuery: Queries + + + + + + Web client + + + Web client + + + Cloud Run API + + + Cloud Run API + + + Cloud SQL + + + + Cloud SQL + + + Cloud Storage + + + Cloud Storage + + + BigQuery + + + + BigQuery + + + Analyst + + + Analyst + + + + diff --git a/docs/public/examples/github-delivery-workflow.svg b/docs/public/examples/github-delivery-workflow.svg new file mode 100644 index 0000000..5e33d21 --- /dev/null +++ b/docs/public/examples/github-delivery-workflow.svg @@ -0,0 +1,94 @@ + + + GitHub delivery workflow + Architecture diagram with 5 nodes, 2 groups, and 4 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + GitHub delivery workflow + + + Source + + Source + + + Delivery + + Delivery + + + + + Developer flows to GitHub repository: Push + + + + GitHub repository flows to GitHub Actions: Workflow + + + + GitHub Actions flows to Vercel: Deploy + + + + Vercel flows to Sentry: Report errors + + + + + + Developer + + + Developer + + + GitHub repository + + + GitHub repository + + + GitHub Actions + + + GitHub Actions + + + Vercel + + + Vercel + + + Sentry + + + Sentry + + + + diff --git a/docs/public/examples/groups-and-layout.svg b/docs/public/examples/groups-and-layout.svg new file mode 100644 index 0000000..69f4588 --- /dev/null +++ b/docs/public/examples/groups-and-layout.svg @@ -0,0 +1,100 @@ + + + Public application + Architecture diagram with 5 nodes, 2 groups, and 4 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + Public application + + + Clients + + Clients + + + Platform + + Platform + + + + + Browser flows to Edge gateway: HTTPS + + + + Mobile app flows to Edge gateway: HTTPS + + + + Edge gateway flows to Application API: HTTPS + + + + Application API flows to Primary database: SQL + + + + + + Browser + + + Browser + + + Mobile app + + + Mobile app + + + Edge gateway + + + Edge gateway + + + Application API + + + Application API + + + Primary database + + + + Primary database + + + + diff --git a/docs/public/examples/hello-stack.svg b/docs/public/examples/hello-stack.svg new file mode 100644 index 0000000..9fd7b83 --- /dev/null +++ b/docs/public/examples/hello-stack.svg @@ -0,0 +1,41 @@ + + + Hello Stack + Architecture diagram with 2 nodes, 0 groups, and 1 relationship. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + Hello Stack + + + + + Web app flows to API + + + + + + Web app + + + Web app + + + API + + + API + + + + diff --git a/docs/public/examples/mixed-provider-platform.svg b/docs/public/examples/mixed-provider-platform.svg new file mode 100644 index 0000000..83c765a --- /dev/null +++ b/docs/public/examples/mixed-provider-platform.svg @@ -0,0 +1,133 @@ + + + Mixed-provider platform + Architecture diagram with 7 nodes, 4 groups, and 6 relationships. + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 + + + + + + + Mixed-provider platform + + + Platform + + Platform + + + Experience + + Experience + + + Processing + + Processing + + + Operations + + Operations + + + + + Customer flows to Web application: HTTPS + + + + Web application is connected bidirectionally with Public API: Live checkout + + + + Public API flows to Checkout: Invoke + + + + Checkout flows to Order events: OrderPlaced + + + + Checkout flows to Orders: Transaction + + + + Public API is associated with Error monitoring: Telemetry + + + + + + Customer + + + Customer + + + Web application + + + Web application + + + Public API + + + Public API + + + Checkout + + + Checkout + + + Order events + + + Order events + + + Orders + + + + Orders + + + Error monitoring + + + Error monitoring + + + + diff --git a/docs/public/llms.txt b/docs/public/llms.txt index d445829..28661b2 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -8,6 +8,7 @@ These Markdown pages are the public user-facing reference. Examples marked as `s - [What is Stack?](https://stack-diagram.com/docs/guide/what-is-stack.md): Goals, conceptual model, processing pipeline, and deliberate non-goals. - [Getting started](https://stack-diagram.com/docs/guide/getting-started.md): Package installation, first diagram, engine operations, and browser Playground workflow. +- [Example gallery](https://stack-diagram.com/docs/examples/index.md): Curated starter-to-production corpus with canonical source, generated thumbnails, provider requirements, and expected structure. - [Using the Playground](https://stack-diagram.com/docs/guide/playground.md): Editor, formatter, diagnostics, preview, color mode, and SVG download behavior. - [Provider icons](https://stack-diagram.com/docs/guide/provider-icons.md): Searchable AWS, Google Cloud, Azure, and tool catalogs; shared icon-store import, Playground loading, configuration, and terms. diff --git a/docs/zh/examples/index.md b/docs/zh/examples/index.md new file mode 100644 index 0000000..0dd253c --- /dev/null +++ b/docs/zh/examples/index.md @@ -0,0 +1,7 @@ +# 示例库 + +从小型图开始,逐步学习分组、布局约束、云服务商图标和接近生产环境的系统。下面的每个缩略图都由固定到公开 Stack 规范的规范源代码生成。 + +云服务商示例中的图标资源由调用方管理。需要品牌图标时,请安装页面标出的图标包。即使没有图标包,同一份源代码仍然有效,并会使用 Stack 的确定性缺失图标回退进行渲染。 + + diff --git a/example-corpus/catalog.json b/example-corpus/catalog.json new file mode 100644 index 0000000..095e3bc --- /dev/null +++ b/example-corpus/catalog.json @@ -0,0 +1,245 @@ +{ + "$schema": "../schemas/example-catalog.schema.json", + "schemaVersion": "1.0", + "languageVersion": "1.0", + "examples": [ + { + "id": "hello-stack", + "title": "Hello Stack", + "summary": "Start with two nodes and one directed edge.", + "learningStage": "starter", + "providers": [], + "features": ["directed-edges"], + "source": "01-minimal.stack", + "expected": { + "description": "A web app points to an API in one compact row.", + "nodes": 2, + "groups": 0, + "edges": 1 + }, + "thumbnail": { + "alt": "A minimal diagram with Web app connected to API" + } + }, + { + "id": "application-and-data", + "title": "Application and data", + "summary": "Add semantic node kinds, icons, details, an edge label, and a light theme.", + "learningStage": "starter", + "providers": [], + "features": [ + "themes", + "node-kinds", + "node-details", + "edge-labels", + "edge-kinds", + "directed-edges" + ], + "source": "02-node-semantics.stack", + "expected": { + "description": "An application sends SQL data to a PostgreSQL database.", + "nodes": 2, + "groups": 0, + "edges": 1 + }, + "thumbnail": { + "alt": "Application and Primary database nodes connected by a SQL edge" + } + }, + { + "id": "groups-and-layout", + "title": "Groups and layout", + "summary": "Organize clients and platform services with explicit direction, rank, and order hints.", + "learningStage": "intermediate", + "providers": [], + "features": [ + "node-kinds", + "groups", + "layout-direction", + "rank-constraints", + "order-constraints", + "edge-labels", + "edge-kinds", + "directed-edges" + ], + "source": "03-groups-and-layout.stack", + "expected": { + "description": "Browser and mobile clients converge on a gateway before the API and database.", + "nodes": 5, + "groups": 2, + "edges": 4 + }, + "thumbnail": { + "alt": "Client and Platform groups connected through an Edge gateway" + } + }, + { + "id": "commerce-platform", + "title": "Commerce platform", + "summary": "Model a production-like commerce system with nested responsibilities and asynchronous processing.", + "learningStage": "advanced", + "providers": [], + "features": [ + "node-kinds", + "node-details", + "groups", + "layout-direction", + "rank-constraints", + "order-constraints", + "edge-labels", + "edge-kinds", + "directed-edges" + ], + "source": "04-commerce-platform.stack", + "expected": { + "description": "Storefront, commerce, processing, data, and partner groups show a full order flow.", + "nodes": 13, + "groups": 5, + "edges": 12 + }, + "thumbnail": { + "alt": "A production commerce architecture from customer through storefront, services, data, and partners" + } + }, + { + "id": "aws-serverless-checkout", + "title": "AWS serverless checkout", + "summary": "Follow a checkout request through AWS edge, serverless compute, events, and storage services.", + "learningStage": "intermediate", + "providers": ["aws"], + "features": [ + "themes", + "node-kinds", + "groups", + "layout-direction", + "edge-labels", + "edge-kinds", + "directed-edges", + "provider-icons" + ], + "source": "05-aws-serverless.stack", + "expected": { + "description": "AWS edge and Lambda services emit an order event before persisting receipts.", + "nodes": 8, + "groups": 3, + "edges": 7 + }, + "thumbnail": { + "alt": "AWS checkout architecture with Edge, Serverless compute, and Data groups" + } + }, + { + "id": "gcp-data-service", + "title": "GCP data service", + "summary": "Connect Cloud Run to transactional, object, and analytical data stores.", + "learningStage": "intermediate", + "providers": ["gcp"], + "features": [ + "themes", + "node-kinds", + "groups", + "layout-direction", + "edge-labels", + "edge-kinds", + "directed-edges", + "provider-icons" + ], + "source": "06-gcp-data-service.stack", + "expected": { + "description": "A Cloud Run API serves a web client and feeds Cloud SQL, Cloud Storage, and BigQuery.", + "nodes": 6, + "groups": 2, + "edges": 5 + }, + "thumbnail": { + "alt": "GCP application and analytics groups connected to a web client" + } + }, + { + "id": "azure-event-platform", + "title": "Azure event platform", + "summary": "Trace asynchronous work from Azure delivery services through functions, Service Bus, and data stores.", + "learningStage": "intermediate", + "providers": ["azure"], + "features": [ + "themes", + "node-kinds", + "groups", + "layout-direction", + "edge-labels", + "edge-kinds", + "directed-edges", + "provider-icons" + ], + "source": "07-azure-event-platform.stack", + "expected": { + "description": "Front Door and App Service hand work to functions and Service Bus before storage.", + "nodes": 8, + "groups": 3, + "edges": 7 + }, + "thumbnail": { + "alt": "Azure delivery, event processing, and data groups in a left-to-right flow" + } + }, + { + "id": "github-delivery-workflow", + "title": "GitHub delivery workflow", + "summary": "Describe a SaaS delivery path from source control and CI to hosting and error monitoring.", + "learningStage": "intermediate", + "providers": ["simple-icons"], + "features": [ + "themes", + "node-kinds", + "groups", + "layout-direction", + "edge-labels", + "edge-kinds", + "directed-edges", + "provider-icons" + ], + "source": "08-github-delivery.stack", + "expected": { + "description": "A developer push triggers GitHub Actions, deployment, and error reporting.", + "nodes": 5, + "groups": 2, + "edges": 4 + }, + "thumbnail": { + "alt": "GitHub repository and Actions connected to Vercel and Sentry" + } + }, + { + "id": "mixed-provider-platform", + "title": "Mixed-provider platform", + "summary": "Combine AWS, GCP, Azure, and SaaS services while preserving semantic kinds and portable flow.", + "learningStage": "advanced", + "providers": ["aws", "gcp", "azure", "simple-icons"], + "features": [ + "themes", + "node-kinds", + "groups", + "nested-groups", + "layout-direction", + "rank-constraints", + "order-constraints", + "edge-labels", + "edge-kinds", + "directed-edges", + "bidirectional-edges", + "association-edges", + "provider-icons" + ], + "source": "09-mixed-provider-platform.stack", + "expected": { + "description": "A web client crosses provider boundaries for API, processing, events, data, and monitoring.", + "nodes": 7, + "groups": 4, + "edges": 6 + }, + "thumbnail": { + "alt": "A mixed-provider platform spanning Vercel, GCP, AWS, Azure, PostgreSQL, and Sentry" + } + } + ] +} diff --git a/example-corpus/schema.json b/example-corpus/schema.json new file mode 100644 index 0000000..a553814 --- /dev/null +++ b/example-corpus/schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Stack Example Catalog", + "type": "object", + "additionalProperties": false, + "required": ["$schema", "schemaVersion", "languageVersion", "examples"], + "properties": { + "$schema": { + "const": "../schemas/example-catalog.schema.json" + }, + "schemaVersion": { + "const": "1.0" + }, + "languageVersion": { + "const": "1.0" + }, + "examples": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/example" + } + } + }, + "$defs": { + "example": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "title", + "summary", + "learningStage", + "providers", + "features", + "source", + "expected", + "thumbnail" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]{0,63}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "learningStage": { + "enum": ["starter", "intermediate", "advanced"] + }, + "providers": { + "type": "array", + "items": { + "enum": ["aws", "gcp", "azure", "simple-icons"] + }, + "uniqueItems": true + }, + "features": { + "type": "array", + "minItems": 1, + "items": { + "enum": [ + "themes", + "node-kinds", + "node-details", + "groups", + "nested-groups", + "layout-direction", + "rank-constraints", + "order-constraints", + "edge-labels", + "edge-kinds", + "directed-edges", + "bidirectional-edges", + "association-edges", + "provider-icons" + ] + }, + "uniqueItems": true + }, + "source": { + "type": "string", + "pattern": "^[0-9]{2}-[a-z0-9-]+\\.stack$" + }, + "expected": { + "type": "object", + "additionalProperties": false, + "required": ["description", "nodes", "groups", "edges"], + "properties": { + "description": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "nodes": { + "type": "integer", + "minimum": 1, + "maximum": 40 + }, + "groups": { + "type": "integer", + "minimum": 0, + "maximum": 12 + }, + "edges": { + "type": "integer", + "minimum": 0, + "maximum": 80 + } + } + }, + "thumbnail": { + "type": "object", + "additionalProperties": false, + "required": ["alt"], + "properties": { + "alt": { + "type": "string", + "minLength": 1, + "maxLength": 180 + } + } + } + } + } + } +} diff --git a/example-corpus/sources/01-minimal.stack b/example-corpus/sources/01-minimal.stack new file mode 100644 index 0000000..6a1b623 --- /dev/null +++ b/example-corpus/sources/01-minimal.stack @@ -0,0 +1,7 @@ +stack 1.0 + +diagram "Hello Stack" { + node web "Web app" + node api "API" + edge web -> api +} diff --git a/example-corpus/sources/02-node-semantics.stack b/example-corpus/sources/02-node-semantics.stack new file mode 100644 index 0000000..27b521b --- /dev/null +++ b/example-corpus/sources/02-node-semantics.stack @@ -0,0 +1,21 @@ +stack 1.0 + +diagram "Application and datastore" { + theme light + + node app "Application" { + kind service + icon "server" + detail "Business logic" + } + + node db "Primary database" { + kind database + icon "kind-database" + detail "PostgreSQL" + } + + edge app -> db "SQL" { + kind data + } +} diff --git a/example-corpus/sources/03-groups-and-layout.stack b/example-corpus/sources/03-groups-and-layout.stack new file mode 100644 index 0000000..6e8c2f6 --- /dev/null +++ b/example-corpus/sources/03-groups-and-layout.stack @@ -0,0 +1,52 @@ +stack 1.0 + +diagram "Public application" { + layout { + direction right + } + + group clients "Clients" { + layout { + direction down + rank same [browser, mobile] + order [browser, mobile] + } + + node browser "Browser" { + kind client + icon "web" + } + + node mobile "Mobile app" { + kind client + icon "mobile" + } + } + + node gateway "Edge gateway" { + icon "gateway" + } + + group platform "Platform" { + node api "Application API" + node db "Primary database" { + kind database + } + } + + edge browser -> gateway "HTTPS" { + kind request + } + + edge mobile -> gateway "HTTPS" { + kind request + } + + edge gateway -> api "HTTPS" { + kind request + } + + edge api -> db "SQL" { + kind data + } +} diff --git a/example-corpus/sources/04-commerce-platform.stack b/example-corpus/sources/04-commerce-platform.stack new file mode 100644 index 0000000..61b303a --- /dev/null +++ b/example-corpus/sources/04-commerce-platform.stack @@ -0,0 +1,127 @@ +stack 1.0 + +diagram "Commerce platform" { + layout { + direction right + } + + node customer "Customer" { + kind actor + } + + group storefront "Storefront" { + node web "Web storefront" { + kind client + icon "web" + detail "Next.js" + } + + node gateway "Edge gateway" { + icon "gateway" + } + } + + group commerce "Commerce services" { + layout { + direction down + rank same [catalog, checkout] + order [catalog, checkout] + } + + node catalog "Catalog API" { + detail "Products and pricing" + } + + node checkout "Checkout API" { + detail "Order orchestration" + } + } + + group asynchronous "Asynchronous processing" { + node events "Event bus" { + kind queue + } + + node fulfillment "Fulfillment worker" { + kind worker + } + + node notifications "Notification worker" { + kind worker + } + } + + group data "Data" { + node products "Product database" { + kind database + icon "kind-database" + } + + node orders "Order database" { + kind database + icon "kind-database" + } + + node assets "Product media" { + kind storage + } + } + + group partners "External systems" { + node payment "Payment provider" { + kind external + } + + node email "Email provider" { + kind external + } + } + + edge customer -> web "Browse and buy" { + kind request + } + + edge web -> gateway "HTTPS" { + kind request + } + + edge gateway -> catalog "Catalog requests" { + kind request + } + + edge gateway -> checkout "Checkout requests" { + kind request + } + + edge catalog -> products "SQL" { + kind data + } + + edge catalog -> assets "Media URLs" { + kind data + } + + edge checkout -> orders "Transactions" { + kind data + } + + edge checkout -> payment "Payment API" { + kind request + } + + edge checkout -> events "OrderPlaced" { + kind event + } + + edge events -> fulfillment "OrderPlaced" { + kind event + } + + edge events -> notifications "OrderPlaced" { + kind event + } + + edge notifications -> email "Send receipt" { + kind request + } +} diff --git a/example-corpus/sources/05-aws-serverless.stack b/example-corpus/sources/05-aws-serverless.stack new file mode 100644 index 0000000..81e8371 --- /dev/null +++ b/example-corpus/sources/05-aws-serverless.stack @@ -0,0 +1,76 @@ +stack 1.0 + +diagram "AWS serverless checkout" { + theme light + + layout { + direction right + } + + node customer "Customer" { + kind actor + } + + group edge "Edge" { + node cdn "CloudFront" { + kind service + icon "aws:amazon-cloudfront" + } + + node gateway "API Gateway" { + kind service + icon "aws:amazon-api-gateway" + } + } + + group compute "Serverless compute" { + node checkout "Checkout" { + kind function + icon "aws:lambda" + } + + node events "Order events" { + kind queue + icon "aws:amazon-eventbridge" + } + + node fulfillment "Fulfillment" { + kind worker + icon "aws:lambda" + } + } + + group data "Data" { + node orders "Orders" { + kind database + icon "aws:dynamodb" + } + + node receipts "Receipts" { + kind storage + icon "aws:s3" + } + } + + edge customer -> cdn "HTTPS" { + kind request + } + edge cdn -> gateway "API request" { + kind request + } + edge gateway -> checkout "Invoke" { + kind request + } + edge checkout -> orders "Write order" { + kind data + } + edge checkout -> events "OrderPlaced" { + kind event + } + edge events -> fulfillment "Dispatch" { + kind event + } + edge fulfillment -> receipts "Archive" { + kind data + } +} diff --git a/example-corpus/sources/06-gcp-data-service.stack b/example-corpus/sources/06-gcp-data-service.stack new file mode 100644 index 0000000..cac7939 --- /dev/null +++ b/example-corpus/sources/06-gcp-data-service.stack @@ -0,0 +1,58 @@ +stack 1.0 + +diagram "GCP data service" { + theme light + + layout { + direction right + } + + node client "Web client" { + kind client + icon "web" + } + + group application "Application" { + node api "Cloud Run API" { + kind service + icon "gcp:cloud-run" + } + + node database "Cloud SQL" { + kind database + icon "gcp:cloud-sql" + } + + node objects "Cloud Storage" { + kind storage + icon "gcp:cloud-storage" + } + } + + group analytics "Analytics" { + node warehouse "BigQuery" { + kind database + icon "gcp:bigquery" + } + + node analyst "Analyst" { + kind actor + } + } + + edge client -> api "HTTPS" { + kind request + } + edge api -> database "SQL" { + kind data + } + edge api -> objects "Uploads" { + kind data + } + edge api -> warehouse "Events" { + kind event + } + edge analyst -> warehouse "Queries" { + kind request + } +} diff --git a/example-corpus/sources/07-azure-event-platform.stack b/example-corpus/sources/07-azure-event-platform.stack new file mode 100644 index 0000000..87b5941 --- /dev/null +++ b/example-corpus/sources/07-azure-event-platform.stack @@ -0,0 +1,76 @@ +stack 1.0 + +diagram "Azure event platform" { + theme light + + layout { + direction right + } + + node customer "Customer" { + kind actor + } + + group delivery "Delivery" { + node frontdoor "Front Door" { + kind service + icon "azure:front-door-and-cdn-profiles" + } + + node app "App Service" { + kind service + icon "azure:app-service" + } + } + + group processing "Event processing" { + node ingest "Ingest function" { + kind function + icon "azure:function-apps" + } + + node bus "Service Bus" { + kind queue + icon "azure:azure-service-bus" + } + + node worker "Background function" { + kind worker + icon "azure:function-apps" + } + } + + group data "Data" { + node database "Cosmos DB" { + kind database + icon "azure:azure-cosmos-db" + } + + node archive "Storage account" { + kind storage + icon "azure:storage-accounts" + } + } + + edge customer -> frontdoor "HTTPS" { + kind request + } + edge frontdoor -> app "Route" { + kind request + } + edge app -> ingest "Submit" { + kind request + } + edge ingest -> bus "Work item" { + kind event + } + edge bus -> worker "Dispatch" { + kind event + } + edge worker -> database "Persist" { + kind data + } + edge worker -> archive "Archive" { + kind data + } +} diff --git a/example-corpus/sources/08-github-delivery.stack b/example-corpus/sources/08-github-delivery.stack new file mode 100644 index 0000000..f24bbd9 --- /dev/null +++ b/example-corpus/sources/08-github-delivery.stack @@ -0,0 +1,50 @@ +stack 1.0 + +diagram "GitHub delivery workflow" { + theme dark + + layout { + direction right + } + + node developer "Developer" { + kind actor + } + + group source "Source" { + node github "GitHub repository" { + kind service + icon "simple-icons:github" + } + + node actions "GitHub Actions" { + kind worker + icon "simple-icons:githubactions" + } + } + + group delivery "Delivery" { + node web "Vercel" { + kind external + icon "simple-icons:vercel" + } + + node errors "Sentry" { + kind external + icon "simple-icons:sentry" + } + } + + edge developer -> github "Push" { + kind data + } + edge github -> actions "Workflow" { + kind event + } + edge actions -> web "Deploy" { + kind dependency + } + edge web -> errors "Report errors" { + kind event + } +} diff --git a/example-corpus/sources/09-mixed-provider-platform.stack b/example-corpus/sources/09-mixed-provider-platform.stack new file mode 100644 index 0000000..6f464c9 --- /dev/null +++ b/example-corpus/sources/09-mixed-provider-platform.stack @@ -0,0 +1,80 @@ +stack 1.0 + +diagram "Mixed-provider platform" { + theme light + + layout { + direction right + } + + node customer "Customer" { + kind actor + } + + group platform "Platform" { + layout { + direction down + } + + group experience "Experience" { + node web "Web application" { + kind client + icon "simple-icons:vercel" + } + + node api "Public API" { + kind service + icon "gcp:cloud-run" + } + } + + group processing "Processing" { + layout { + direction down + rank same [checkout, events] + order [checkout, events] + } + + node checkout "Checkout" { + kind function + icon "aws:lambda" + } + + node events "Order events" { + kind queue + icon "azure:azure-service-bus" + } + } + + group operations "Operations" { + node database "Orders" { + kind database + icon "simple-icons:postgresql" + } + + node monitoring "Error monitoring" { + kind external + icon "simple-icons:sentry" + } + } + } + + edge customer -> web "HTTPS" { + kind request + } + edge web <-> api "Live checkout" { + kind flow + } + edge api -> checkout "Invoke" { + kind dependency + } + edge checkout -> events "OrderPlaced" { + kind event + } + edge checkout -> database "Transaction" { + kind data + } + edge api -- monitoring "Telemetry" { + kind data + } +} diff --git a/package.json b/package.json index 5a453e3..649724a 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,15 @@ "deploy": "npm run build && wrangler deploy", "dev": "vite", "docs:build": "npm run docs:check && vitepress build docs && node scripts/validate-docs-output.mjs", - "docs:check": "node scripts/validate-docs.mjs && node scripts/validate-docs-examples.mjs", + "docs:check": "node scripts/validate-docs.mjs && node scripts/validate-docs-examples.mjs && npm run examples:check", "docs:smoke": "node scripts/validate-docs-cli.mjs", "docs:test": "node --test scripts/*.test.mjs", "docs:dev": "vitepress dev docs", "docs:preview": "vitepress preview docs", + "examples:check": "node scripts/validate-example-corpus.mjs", + "examples:check:source": "node scripts/sync-example-corpus.mjs --check", + "examples:generate": "node scripts/validate-example-corpus.mjs --write-thumbnails", + "examples:sync": "node scripts/sync-example-corpus.mjs", "format": "oxfmt --write .", "format:check": "oxfmt --check .", "lint": "oxlint --deny-warnings .", diff --git a/public/llms.txt b/public/llms.txt index 7e216a3..eabea89 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -9,6 +9,7 @@ Stack source describes components, boundaries, relationships, themes, and layout - [Stack documentation](https://stack-diagram.com/docs/llms.txt): Curated guides, complete language reference, diagnostics, limits, versioning, and safety information. - [Complete English documentation](https://stack-diagram.com/llms-full.txt): All English documentation in one Markdown document for agents that prefer a single context. - [Getting started](https://stack-diagram.com/docs/guide/getting-started.md): Install the browser package, write a first `.stack` document, and use the Playground. +- [Example gallery](https://stack-diagram.com/docs/examples/index.md): Reusable canonical examples from small diagrams through provider and mixed-cloud systems. - [Language syntax](https://stack-diagram.com/docs/language/syntax.md): Document structure, identifiers, strings, comments, and validation rules. - [Themes and icons](https://stack-diagram.com/docs/language/themes-and-icons.md): Theme selection, icon references, fallbacks, and catalog ownership. - [Diagnostics and limits](https://stack-diagram.com/docs/reference/diagnostics-and-limits.md): Diagnostic codes, source ranges, warnings, and safety limits. diff --git a/scripts/docs-validation.config.mjs b/scripts/docs-validation.config.mjs index 1fe1046..803fcc3 100644 --- a/scripts/docs-validation.config.mjs +++ b/scripts/docs-validation.config.mjs @@ -7,6 +7,7 @@ export const documentationContract = { items: [ { label: "introduction", page: "guide/what-is-stack.md" }, { label: "gettingStarted", page: "guide/getting-started.md" }, + { label: "examples", page: "examples/index.md" }, { label: "playground", page: "guide/playground.md" }, { label: "providerIcons", page: "guide/provider-icons.md" }, ], diff --git a/scripts/example-corpus.config.mjs b/scripts/example-corpus.config.mjs new file mode 100644 index 0000000..1cee343 --- /dev/null +++ b/scripts/example-corpus.config.mjs @@ -0,0 +1,6 @@ +export const exampleCorpusSource = { + repository: "stack-sh/specification", + revision: "8e5477c67389afbbf53b7e41bc017bdb4d682f66", + catalogPath: "examples/catalog.json", + schemaPath: "schemas/example-catalog.schema.json", +} diff --git a/scripts/sync-example-corpus.mjs b/scripts/sync-example-corpus.mjs new file mode 100644 index 0000000..7cb4361 --- /dev/null +++ b/scripts/sync-example-corpus.mjs @@ -0,0 +1,73 @@ +import assert from "node:assert/strict" +import { execFile } from "node:child_process" +import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises" +import path from "node:path" +import { promisify } from "node:util" + +import { exampleCorpusSource } from "./example-corpus.config.mjs" + +const execute = promisify(execFile) +const sourceRootValue = process.env.STACK_SPECIFICATION_ROOT +if (!sourceRootValue) { + throw new Error("STACK_SPECIFICATION_ROOT must point to the pinned specification checkout") +} + +const checkOnly = process.argv.includes("--check") +const sourceRoot = path.resolve(sourceRootValue) +const destinationRoot = path.resolve("example-corpus") +const destinationSources = path.join(destinationRoot, "sources") +const { stdout: revisionOutput } = await execute("git", ["rev-parse", "HEAD"], { + cwd: sourceRoot, + encoding: "utf8", +}) +assert.equal( + revisionOutput.trim(), + exampleCorpusSource.revision, + "Specification checkout does not match the pinned example corpus revision", +) + +const sourceCatalog = await readFile(path.join(sourceRoot, exampleCorpusSource.catalogPath), "utf8") +const sourceSchema = await readFile(path.join(sourceRoot, exampleCorpusSource.schemaPath), "utf8") +const catalog = JSON.parse(sourceCatalog) +const expectedSources = catalog.examples.map((example) => example.source).sort() +const snapshots = new Map([ + [path.join(destinationRoot, "catalog.json"), sourceCatalog], + [path.join(destinationRoot, "schema.json"), sourceSchema], +]) +for (const source of expectedSources) { + snapshots.set( + path.join(destinationSources, source), + await readFile(path.join(sourceRoot, "examples", source), "utf8"), + ) +} + +if (checkOnly) { + const actualSources = (await readdir(destinationSources)) + .filter((entry) => entry.endsWith(".stack")) + .sort() + assert.deepEqual(actualSources, expectedSources, "Vendored example source inventory has drifted") + for (const [destination, expected] of snapshots) { + assert.equal( + await readFile(destination, "utf8"), + expected, + `${path.relative(process.cwd(), destination)} has drifted from the pinned specification`, + ) + } + console.log( + `Verified ${expectedSources.length} example snapshots against ${exampleCorpusSource.repository}@${exampleCorpusSource.revision}.`, + ) +} else { + await mkdir(destinationSources, { recursive: true }) + for (const entry of await readdir(destinationSources)) { + if (entry.endsWith(".stack") && !expectedSources.includes(entry)) { + await rm(path.join(destinationSources, entry)) + } + } + for (const [destination, contents] of snapshots) { + await mkdir(path.dirname(destination), { recursive: true }) + await writeFile(destination, contents) + } + console.log( + `Synchronized ${expectedSources.length} examples from ${exampleCorpusSource.repository}@${exampleCorpusSource.revision}.`, + ) +} diff --git a/scripts/validate-docs-output.mjs b/scripts/validate-docs-output.mjs index 9427885..21e9b6f 100644 --- a/scripts/validate-docs-output.mjs +++ b/scripts/validate-docs-output.mjs @@ -1,8 +1,12 @@ import { readFile, readdir } from "node:fs/promises" import path from "node:path" +import { validateDocumentationContract } from "./docs-contract.mjs" +import { documentationContract } from "./docs-validation.config.mjs" + const outputRoot = path.resolve("dist/docs") const siteOutputRoot = path.resolve("dist") +const docsRoot = path.resolve("docs") const localePages = [ ["index.html", "en-US"], ["ja/index.html", "ja-JP"], @@ -86,6 +90,18 @@ for (const locale of ["", "ja/", "zh/", "ko/"]) { if (cards !== 30) throw new Error(`${page} contains ${cards} icon cards instead of 30`) } +for (const locale of ["", "ja/", "zh/", "ko/"]) { + const page = `${locale}examples/index.html` + const html = await readFile(path.join(outputRoot, page), "utf8") + const cards = html.match(/class="stack-example-card"/g)?.length ?? 0 + const thumbnails = html.match(/class="stack-example-card__preview"/g)?.length ?? 0 + + if (cards !== 9) throw new Error(`${page} contains ${cards} example cards instead of 9`) + if (thumbnails !== 9) { + throw new Error(`${page} contains ${thumbnails} example thumbnails instead of 9`) + } +} + const documentationAssets = await readdir(path.join(outputRoot, "assets")) if (!documentationAssets.some((asset) => /^stack_engine_bg\..+\.wasm$/.test(asset))) { @@ -94,9 +110,20 @@ if (!documentationAssets.some((asset) => /^stack_engine_bg\..+\.wasm$/.test(asse const sitemap = await readFile(path.join(outputRoot, "sitemap.xml"), "utf8") const locations = [...sitemap.matchAll(/(.*?)<\/loc>/g)].map((match) => match[1]) - -if (locations.length !== 48) - throw new Error(`Expected 48 sitemap locations, found ${locations.length}`) +const { pages } = await validateDocumentationContract({ + docsRoot, + locales: documentationContract.locales, + allowedFenceLanguages: documentationContract.allowedFenceLanguages, + navigationPages: documentationContract.navigation.flatMap((section) => + section.items.map((item) => item.page), + ), + exceptions: documentationContract.exceptions, +}) +const expectedLocations = pages.length * (documentationContract.locales.length + 1) + +if (locations.length !== expectedLocations) { + throw new Error(`Expected ${expectedLocations} sitemap locations, found ${locations.length}`) +} for (const location of locations) { if (!location.startsWith("https://stack-diagram.com/docs/")) @@ -135,5 +162,5 @@ for (const requiredContent of [ } console.log( - "Validated site metadata, agent discovery files, four locale entry points, and all 48 sitemap locations.", + `Validated site metadata, agent discovery files, four locale entry points, and all ${expectedLocations} sitemap locations.`, ) diff --git a/scripts/validate-example-corpus.mjs b/scripts/validate-example-corpus.mjs new file mode 100644 index 0000000..2939f15 --- /dev/null +++ b/scripts/validate-example-corpus.mjs @@ -0,0 +1,100 @@ +import assert from "node:assert/strict" +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises" +import path from "node:path" + +import init, { check, render } from "@stack-sh/engine" + +const writeThumbnails = process.argv.includes("--write-thumbnails") +const corpusRoot = path.resolve("example-corpus") +const sourceRoot = path.join(corpusRoot, "sources") +const thumbnailRoot = path.resolve("docs/public/examples") +const catalog = JSON.parse(await readFile(path.join(corpusRoot, "catalog.json"), "utf8")) +const providerCatalog = JSON.parse( + await readFile(path.resolve("docs/.vitepress/theme/data/provider-catalogs.json"), "utf8"), +) +const wasm = await readFile(path.resolve("node_modules/@stack-sh/engine/dist/stack_engine_bg.wasm")) +await init({ module_or_path: wasm }) + +const availableProviderIcons = new Set( + providerCatalog.providers.flatMap((provider) => provider.icons.map((icon) => icon.id)), +) +const expectedSources = catalog.examples.map((example) => example.source).sort() +const actualSources = (await readdir(sourceRoot)).filter((entry) => entry.endsWith(".stack")).sort() +assert.deepEqual(actualSources, expectedSources, "Example source inventory does not match catalog") + +function declarations(source, declaration) { + return source.match(new RegExp(`^\\s*${declaration}\\s+`, "gm"))?.length ?? 0 +} + +const expectedThumbnails = [] +for (const example of catalog.examples) { + const source = await readFile(path.join(sourceRoot, example.source), "utf8") + for (const field of ["nodes", "groups", "edges"]) { + const declaration = field.slice(0, -1) + assert.equal( + declarations(source, declaration), + example.expected[field], + `${example.id} ${field} metadata does not match its source`, + ) + } + + const providerIcons = [...source.matchAll(/icon "([a-z][a-z0-9-]+:[a-z0-9-]+)"/g)].map( + (match) => match[1], + ) + const providers = [...new Set(providerIcons.map((icon) => icon.split(":", 1)[0]))] + assert.deepEqual(providers.sort(), [...example.providers].sort(), `${example.id} provider drift`) + for (const icon of providerIcons) { + assert.ok(availableProviderIcons.has(icon), `${example.id} uses unknown provider icon ${icon}`) + } + + const checked = check(source) + const rendered = render(source) + for (const [operation, result] of [ + ["check", checked], + ["render", rendered], + ]) { + const unexpected = result.diagnostics.filter((diagnostic) => diagnostic.code !== "STK5001") + assert.deepEqual( + unexpected, + [], + `${example.id} ${operation} returned unexpected diagnostics: ${unexpected.map((diagnostic) => diagnostic.code).join(", ")}`, + ) + assert.equal( + result.diagnostics.length, + providerIcons.length, + `${example.id} ${operation} provider fallback count does not match its source`, + ) + assert.equal(result.metadata.engineVersion, "0.6.0") + assert.deepEqual(result.metadata.languageVersion, { major: 1, minor: 0 }) + } + + assert.ok(rendered.svg, `${example.id} did not produce an SVG`) + assert.match(rendered.svg, /]*\bviewBox="[^"]+"/) + assert.doesNotMatch(rendered.svg, / entry.endsWith(".svg")) + .sort() +assert.deepEqual( + actualThumbnails, + expectedThumbnails.sort(), + "Example thumbnail inventory has drifted", +) +console.log( + `${writeThumbnails ? "Generated" : "Validated"} ${catalog.examples.length} checked, rendered, and provider-resolved example thumbnails with @stack-sh/engine 0.6.0.`, +) diff --git a/src/lib/example.ts b/src/lib/example.ts index 6b37488..067006c 100644 --- a/src/lib/example.ts +++ b/src/lib/example.ts @@ -1,109 +1,3 @@ -export const EXAMPLE_SOURCE = `// Declare the Stack language version. -stack 1.0 +import commercePlatformSource from "../../example-corpus/sources/04-commerce-platform.stack?raw" -diagram "Commerce platform" { - // The diagram theme controls the generated SVG, independently of the Web UI. - theme light - - // Layout hints describe reading direction, alignment, and relative order. - layout { - direction right - rank same [shopper, platform] - order [shopper, platform] - } - - // Nodes use semantic kinds, with optional icons and visible details. - node shopper "Shopper" { - kind actor - detail "Places an order" - } - - group platform "Platform" { - layout { - direction down - rank same [storefront, api] - order [storefront, api] - } - - node storefront "Storefront" { - kind client - icon "web" - detail "Web application" - } - - node api "Commerce API" { - kind service - icon "gateway" - detail "Orders and inventory" - } - - group compute "Compute" { - node checkout "Checkout" { - kind function - detail "Validates payment" - } - - node fulfillment "Fulfillment worker" { - kind worker - detail "Processes orders" - } - } - - group state "State" { - node database "Orders" { - kind database - detail "Transactional records" - } - - node cache "Product cache" { - kind cache - } - - node queue "Order events" { - kind queue - } - - node storage "Receipts" { - kind storage - } - } - - node payments "Payment provider" { - kind external - } - } - - // Edge operators express directed, bidirectional, and undirected relationships. - edge shopper -> storefront "HTTPS" { - kind request - } - - edge storefront <-> api "Live cart" { - kind flow - } - - edge api -> checkout "Invoke" { - kind dependency - } - - edge checkout -> database "SQL" { - kind data - } - - edge checkout -> queue "OrderPlaced" { - kind event - } - - edge fulfillment -- storage "Archive" { - kind data - } - - edge api -> cache "Read through" { - kind data - } - - edge checkout -> payments "Authorize" { - kind request - } -} -` +export const EXAMPLE_SOURCE = commercePlatformSource