@design-ai/cli 4.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +164 -0
- package/AGENTS.ko.md +126 -0
- package/AGENTS.md +182 -0
- package/CHANGELOG.md +6883 -0
- package/CLAUDE.md +39 -0
- package/LICENSE +21 -0
- package/README.ko.md +245 -0
- package/README.md +277 -0
- package/agents/README.md +14 -0
- package/agents/a11y-reviewer.md +121 -0
- package/agents/component-architect.md +57 -0
- package/agents/design-critic.md +46 -0
- package/agents/token-extractor.md +63 -0
- package/cli/bin/design-ai-mcp.mjs +6 -0
- package/cli/bin/design-ai.mjs +40 -0
- package/cli/commands/audit.mjs +111 -0
- package/cli/commands/check.mjs +226 -0
- package/cli/commands/doctor.mjs +141 -0
- package/cli/commands/examples.mjs +58 -0
- package/cli/commands/help.mjs +271 -0
- package/cli/commands/install.mjs +161 -0
- package/cli/commands/learn.mjs +1871 -0
- package/cli/commands/list.mjs +143 -0
- package/cli/commands/mcp.mjs +27 -0
- package/cli/commands/pack.mjs +211 -0
- package/cli/commands/prompt.mjs +199 -0
- package/cli/commands/route.mjs +247 -0
- package/cli/commands/search.mjs +60 -0
- package/cli/commands/show.mjs +52 -0
- package/cli/commands/site.mjs +558 -0
- package/cli/commands/status.mjs +161 -0
- package/cli/commands/uninstall.mjs +119 -0
- package/cli/commands/update.mjs +184 -0
- package/cli/commands/version.mjs +94 -0
- package/cli/commands/workspace.mjs +176 -0
- package/cli/lib/brief.mjs +52 -0
- package/cli/lib/check.mjs +642 -0
- package/cli/lib/dispatch.mjs +109 -0
- package/cli/lib/doctor.mjs +505 -0
- package/cli/lib/examples.mjs +258 -0
- package/cli/lib/exec.mjs +63 -0
- package/cli/lib/help-flags.mjs +5 -0
- package/cli/lib/learn.mjs +3694 -0
- package/cli/lib/log.mjs +47 -0
- package/cli/lib/mcp-server.mjs +426 -0
- package/cli/lib/output.mjs +101 -0
- package/cli/lib/pack.mjs +599 -0
- package/cli/lib/paths.mjs +63 -0
- package/cli/lib/prompt.mjs +603 -0
- package/cli/lib/route.mjs +740 -0
- package/cli/lib/search.mjs +158 -0
- package/cli/lib/show.mjs +161 -0
- package/cli/lib/signals.mjs +2337 -0
- package/cli/lib/site.mjs +6617 -0
- package/cli/lib/skill-proposals.mjs +2046 -0
- package/cli/lib/suggest.mjs +66 -0
- package/cli/lib/workspace.mjs +1056 -0
- package/commands/README.md +25 -0
- package/commands/component-spec.md +34 -0
- package/commands/conversational.md +44 -0
- package/commands/design-from-brief.md +141 -0
- package/commands/design-review.md +62 -0
- package/commands/document-from-brief.md +47 -0
- package/commands/extract-tokens.md +38 -0
- package/commands/game-ui.md +40 -0
- package/commands/illustration.md +37 -0
- package/commands/iterate.md +167 -0
- package/commands/motion-design.md +54 -0
- package/commands/palette-from-brand.md +50 -0
- package/commands/print.md +49 -0
- package/commands/slide-deck.md +46 -0
- package/commands/spatial.md +47 -0
- package/commands/stability-review.md +74 -0
- package/commands/video.md +42 -0
- package/commands/website-improvement.md +77 -0
- package/docs/AGENT-DEVELOPMENT.md +658 -0
- package/docs/AI-LEARNING.md +429 -0
- package/docs/AIDER-INTEGRATION.md +224 -0
- package/docs/ARCHITECTURE.ko.md +183 -0
- package/docs/ARCHITECTURE.md +105 -0
- package/docs/CODEX-INTEGRATION.md +311 -0
- package/docs/COMPANY-WEBSITE-DOGFOOD.ko.md +144 -0
- package/docs/COMPANY-WEBSITE-DOGFOOD.md +150 -0
- package/docs/COMPANY-WEBSITE-INTAKE-TEMPLATE.ko.md +153 -0
- package/docs/COMPANY-WEBSITE-INTAKE-TEMPLATE.md +154 -0
- package/docs/CONTRIBUTING.ko.md +203 -0
- package/docs/CONTRIBUTING.md +242 -0
- package/docs/CURSOR-INTEGRATION.md +220 -0
- package/docs/DISTRIBUTION.ko.md +248 -0
- package/docs/DISTRIBUTION.md +253 -0
- package/docs/DOGFOOD-FINDINGS.md +180 -0
- package/docs/DOGFOOD-V4-FINDINGS.md +139 -0
- package/docs/DOGFOOD-V4-MKDOCS-FINDINGS.md +233 -0
- package/docs/DOGFOOD-V4-NPM-FINDINGS.md +157 -0
- package/docs/DOGFOOD-V4-VSCODE-FINDINGS.md +135 -0
- package/docs/FIGMA-INTEGRATION.md +222 -0
- package/docs/MCP-INTEGRATION.md +243 -0
- package/docs/MIGRATION-v4.md +121 -0
- package/docs/PLUGIN-PACKAGING.md +240 -0
- package/docs/PRODUCT-READINESS.md +66 -0
- package/docs/QUICKSTART.ko.md +178 -0
- package/docs/QUICKSTART.md +179 -0
- package/docs/RELEASE-CHECKLIST.md +292 -0
- package/docs/ROADMAP.md +19495 -0
- package/docs/SESSION-LOG.md +940 -0
- package/docs/TOKEN-SYNC.md +237 -0
- package/docs/USING.ko.md +206 -0
- package/docs/USING.md +218 -0
- package/docs/WEBSITE-IMPROVEMENT.ko.md +75 -0
- package/docs/WEBSITE-IMPROVEMENT.md +89 -0
- package/docs/announcements/README.md +56 -0
- package/docs/announcements/dev-to-korea.md +206 -0
- package/docs/announcements/hashnode-post.ko.md +232 -0
- package/docs/announcements/okky-post.ko.md +158 -0
- package/docs/announcements/press-kit.md +101 -0
- package/docs/announcements/reddit-r-korea.md +196 -0
- package/docs/announcements/show-hn.md +114 -0
- package/docs/announcements/twitter-thread.md +255 -0
- package/docs/external-status.md +34 -0
- package/docs/integrations/aider-walkthrough.ko.md +275 -0
- package/docs/integrations/aider-walkthrough.md +274 -0
- package/docs/integrations/codex-walkthrough.ko.md +297 -0
- package/docs/integrations/codex-walkthrough.md +297 -0
- package/docs/integrations/cursor-walkthrough.ko.md +281 -0
- package/docs/integrations/cursor-walkthrough.md +284 -0
- package/docs/integrations/design-ai-mcp-server.md +169 -0
- package/docs/integrations/figma-mcp.md +149 -0
- package/docs/integrations/github-mcp.md +178 -0
- package/docs/integrations/linear-mcp.md +215 -0
- package/docs/integrations/notion-mcp.md +174 -0
- package/docs/integrations/sdk-walkthrough.ko.md +375 -0
- package/docs/integrations/sdk-walkthrough.md +375 -0
- package/docs/integrations/slack-mcp.md +208 -0
- package/docs/integrations/vscode-walkthrough.ko.md +202 -0
- package/docs/integrations/vscode-walkthrough.md +205 -0
- package/docs/requirements.txt +16 -0
- package/docs/site-overrides/extra.css +83 -0
- package/docs/site-overrides/favicon.svg +4 -0
- package/docs/site-overrides/logo.svg +10 -0
- package/docs/site-overrides/main.html +18 -0
- package/docs/website-console/app.js +2540 -0
- package/docs/website-console/index.html +24 -0
- package/docs/website-console/styles.css +975 -0
- package/examples/README.md +252 -0
- package/examples/cases/dogfood-v4-korean-hr-onboarding.md +356 -0
- package/examples/component-accordion-actions.md +153 -0
- package/examples/component-accordion-details.md +147 -0
- package/examples/component-accordion-summary.md +180 -0
- package/examples/component-accordion.md +229 -0
- package/examples/component-account-card.md +284 -0
- package/examples/component-address-input.md +260 -0
- package/examples/component-affix.md +190 -0
- package/examples/component-alert-dialog.md +210 -0
- package/examples/component-alert-title.md +77 -0
- package/examples/component-alert.md +192 -0
- package/examples/component-amount-input.md +278 -0
- package/examples/component-anchor.md +198 -0
- package/examples/component-app-bar.md +249 -0
- package/examples/component-aspect-ratio.md +156 -0
- package/examples/component-auto-complete.md +199 -0
- package/examples/component-avatar-group.md +170 -0
- package/examples/component-avatar.md +191 -0
- package/examples/component-back-top.md +123 -0
- package/examples/component-backdrop.md +148 -0
- package/examples/component-badge.md +181 -0
- package/examples/component-banner.md +237 -0
- package/examples/component-biometric-gate.md +281 -0
- package/examples/component-blockquote.md +153 -0
- package/examples/component-border-beam.md +207 -0
- package/examples/component-bottom-navigation.md +226 -0
- package/examples/component-box.md +112 -0
- package/examples/component-breadcrumb.md +222 -0
- package/examples/component-button-base.md +190 -0
- package/examples/component-button-group.md +117 -0
- package/examples/component-button.md +213 -0
- package/examples/component-calendar.md +280 -0
- package/examples/component-callout.md +131 -0
- package/examples/component-card-actions.md +101 -0
- package/examples/component-card-content.md +90 -0
- package/examples/component-card-header.md +122 -0
- package/examples/component-card-media.md +79 -0
- package/examples/component-card.md +233 -0
- package/examples/component-carousel.md +266 -0
- package/examples/component-cascader.md +135 -0
- package/examples/component-category-picker.md +271 -0
- package/examples/component-chart.md +237 -0
- package/examples/component-chat-interface.md +399 -0
- package/examples/component-checkbox.md +149 -0
- package/examples/component-click-away-listener.md +87 -0
- package/examples/component-code.md +213 -0
- package/examples/component-collapsible.md +219 -0
- package/examples/component-color-picker.md +110 -0
- package/examples/component-combobox.md +267 -0
- package/examples/component-command.md +299 -0
- package/examples/component-config-provider.md +190 -0
- package/examples/component-context-menu.md +179 -0
- package/examples/component-css-baseline.md +148 -0
- package/examples/component-date-picker.md +249 -0
- package/examples/component-descriptions.md +242 -0
- package/examples/component-dialog-actions.md +111 -0
- package/examples/component-dialog-content-text.md +76 -0
- package/examples/component-dialog-content.md +105 -0
- package/examples/component-dialog-title.md +104 -0
- package/examples/component-dialog.md +176 -0
- package/examples/component-divider.md +182 -0
- package/examples/component-doc-page.md +217 -0
- package/examples/component-drawer.md +257 -0
- package/examples/component-dropdown.md +220 -0
- package/examples/component-email-layout.md +253 -0
- package/examples/component-empty-state.md +269 -0
- package/examples/component-empty.md +172 -0
- package/examples/component-fade.md +87 -0
- package/examples/component-feature-grid.md +201 -0
- package/examples/component-field.md +266 -0
- package/examples/component-filled-input.md +114 -0
- package/examples/component-flex.md +117 -0
- package/examples/component-float-button.md +214 -0
- package/examples/component-form-control-label.md +134 -0
- package/examples/component-form-control.md +113 -0
- package/examples/component-form-controls.md +251 -0
- package/examples/component-form-group.md +86 -0
- package/examples/component-form-helper-text.md +95 -0
- package/examples/component-form-label.md +98 -0
- package/examples/component-form.md +291 -0
- package/examples/component-game-hud.md +362 -0
- package/examples/component-game-menu.md +327 -0
- package/examples/component-grid.md +140 -0
- package/examples/component-grow.md +79 -0
- package/examples/component-hero-block.md +252 -0
- package/examples/component-hover-card.md +211 -0
- package/examples/component-icon-button.md +142 -0
- package/examples/component-icon.md +102 -0
- package/examples/component-illustration.md +280 -0
- package/examples/component-image-list.md +107 -0
- package/examples/component-image.md +282 -0
- package/examples/component-input-adornment.md +101 -0
- package/examples/component-input-base.md +134 -0
- package/examples/component-input-number.md +141 -0
- package/examples/component-input-otp.md +271 -0
- package/examples/component-input.md +213 -0
- package/examples/component-item.md +243 -0
- package/examples/component-kbd.md +197 -0
- package/examples/component-krw-amount.md +219 -0
- package/examples/component-label.md +118 -0
- package/examples/component-layout.md +217 -0
- package/examples/component-link.md +223 -0
- package/examples/component-list-item-avatar.md +72 -0
- package/examples/component-list-item-button.md +139 -0
- package/examples/component-list-item-icon.md +82 -0
- package/examples/component-list-item-text.md +98 -0
- package/examples/component-list-item.md +116 -0
- package/examples/component-list-subheader.md +70 -0
- package/examples/component-list.md +198 -0
- package/examples/component-loading-button.md +125 -0
- package/examples/component-loading-sequence.md +254 -0
- package/examples/component-lottie-player.md +279 -0
- package/examples/component-masonry.md +191 -0
- package/examples/component-mentions.md +189 -0
- package/examples/component-menu-item.md +102 -0
- package/examples/component-menu-list.md +98 -0
- package/examples/component-menu.md +139 -0
- package/examples/component-menubar.md +259 -0
- package/examples/component-message.md +162 -0
- package/examples/component-mobile-stepper.md +155 -0
- package/examples/component-modal.md +220 -0
- package/examples/component-navigation-menu.md +295 -0
- package/examples/component-notification.md +166 -0
- package/examples/component-otp-countdown.md +245 -0
- package/examples/component-outlined-input.md +145 -0
- package/examples/component-page-transition.md +281 -0
- package/examples/component-pagination.md +226 -0
- package/examples/component-paper.md +199 -0
- package/examples/component-pass-auth.md +265 -0
- package/examples/component-payment-brand-button.md +253 -0
- package/examples/component-payment-method-selector.md +272 -0
- package/examples/component-payment-receipt.md +277 -0
- package/examples/component-popconfirm.md +144 -0
- package/examples/component-popover.md +239 -0
- package/examples/component-popper.md +129 -0
- package/examples/component-pricing-cards.md +247 -0
- package/examples/component-progress.md +195 -0
- package/examples/component-qr-code.md +201 -0
- package/examples/component-radio.md +167 -0
- package/examples/component-rate.md +202 -0
- package/examples/component-resizable.md +163 -0
- package/examples/component-result.md +270 -0
- package/examples/component-scroll-area.md +199 -0
- package/examples/component-scroll-reveal.md +320 -0
- package/examples/component-segmented.md +178 -0
- package/examples/component-select.md +258 -0
- package/examples/component-separator.md +177 -0
- package/examples/component-sheet.md +277 -0
- package/examples/component-sidebar.md +257 -0
- package/examples/component-skeleton.md +178 -0
- package/examples/component-slide.md +89 -0
- package/examples/component-slider.md +208 -0
- package/examples/component-snackbar-content.md +92 -0
- package/examples/component-snackbar.md +121 -0
- package/examples/component-sonner.md +165 -0
- package/examples/component-space.md +125 -0
- package/examples/component-spatial-locomotion.md +373 -0
- package/examples/component-spatial-panel.md +330 -0
- package/examples/component-speed-dial-action.md +78 -0
- package/examples/component-speed-dial.md +177 -0
- package/examples/component-spin.md +173 -0
- package/examples/component-spinner.md +204 -0
- package/examples/component-splitter.md +210 -0
- package/examples/component-stack.md +130 -0
- package/examples/component-statistic.md +282 -0
- package/examples/component-step-button.md +157 -0
- package/examples/component-step-connector.md +157 -0
- package/examples/component-step-content.md +121 -0
- package/examples/component-step-icon.md +115 -0
- package/examples/component-step-label.md +101 -0
- package/examples/component-step.md +84 -0
- package/examples/component-steps.md +264 -0
- package/examples/component-stock-chart.md +251 -0
- package/examples/component-swipeable-drawer.md +149 -0
- package/examples/component-switch.md +99 -0
- package/examples/component-tab-scroll-button.md +180 -0
- package/examples/component-tab.md +120 -0
- package/examples/component-table-body.md +82 -0
- package/examples/component-table-cell.md +118 -0
- package/examples/component-table-container.md +75 -0
- package/examples/component-table-footer.md +72 -0
- package/examples/component-table-head.md +78 -0
- package/examples/component-table-pagination.md +137 -0
- package/examples/component-table-row.md +113 -0
- package/examples/component-table-sort-label.md +109 -0
- package/examples/component-table.md +202 -0
- package/examples/component-tabs.md +184 -0
- package/examples/component-tag-badge.md +309 -0
- package/examples/component-tag.md +102 -0
- package/examples/component-testimonial-carousel.md +218 -0
- package/examples/component-textarea-autosize.md +114 -0
- package/examples/component-textarea.md +173 -0
- package/examples/component-time-picker.md +205 -0
- package/examples/component-timeline.md +170 -0
- package/examples/component-toast.md +224 -0
- package/examples/component-toggle-button.md +129 -0
- package/examples/component-toggle.md +220 -0
- package/examples/component-toolbar.md +104 -0
- package/examples/component-tooltip.md +178 -0
- package/examples/component-tour.md +176 -0
- package/examples/component-transaction-list-item.md +243 -0
- package/examples/component-transfer.md +156 -0
- package/examples/component-tree-select.md +202 -0
- package/examples/component-tree.md +297 -0
- package/examples/component-typography.md +215 -0
- package/examples/component-upload.md +295 -0
- package/examples/component-video-hero.md +409 -0
- package/examples/component-video-player.md +340 -0
- package/examples/component-voice-input.md +408 -0
- package/examples/component-watermark.md +217 -0
- package/examples/component-zoom.md +92 -0
- package/examples/doc-explanation-example.md +196 -0
- package/examples/doc-how-to-example.md +205 -0
- package/examples/doc-tutorial-example.md +162 -0
- package/examples/dogfood-korean-fintech-system.md +433 -0
- package/examples/email-transactional-example.md +259 -0
- package/examples/palette-saas-violet.md +235 -0
- package/examples/print-business-card-spec.md +205 -0
- package/examples/print-packaging-spec.md +285 -0
- package/examples/report-example.md +228 -0
- package/examples/slide-deck-example.md +237 -0
- package/examples/website-improvement-report.md +128 -0
- package/examples/website-improvement-workspace.json +126 -0
- package/install.sh +189 -0
- package/knowledge/COVERAGE.md +740 -0
- package/knowledge/PRINCIPLES.md +108 -0
- package/knowledge/a11y/contrast.md +70 -0
- package/knowledge/a11y/keyboard-and-focus.md +108 -0
- package/knowledge/colors/color-theory.md +90 -0
- package/knowledge/colors/mui-palette-structure.md +105 -0
- package/knowledge/colors/palettes-by-product-type.md +876 -0
- package/knowledge/components/INDEX.md +231 -0
- package/knowledge/components/index.json +1470 -0
- package/knowledge/components/shadcn-registry.md +159 -0
- package/knowledge/conversational/ai-chat-interfaces.md +323 -0
- package/knowledge/conversational/chatbot-design.md +312 -0
- package/knowledge/conversational/conversational-ui-fundamentals.md +299 -0
- package/knowledge/conversational/korean-voice-conventions.md +281 -0
- package/knowledge/conversational/voice-ui-patterns.md +297 -0
- package/knowledge/design-tokens/ant-design.md +124 -0
- package/knowledge/design-tokens/material-3.md +235 -0
- package/knowledge/design-tokens/polaris-and-carbon.md +231 -0
- package/knowledge/design-tokens/tailwind-v4.md +284 -0
- package/knowledge/game-ui/game-accessibility.md +317 -0
- package/knowledge/game-ui/game-ui-fundamentals.md +281 -0
- package/knowledge/game-ui/hud-design.md +324 -0
- package/knowledge/game-ui/korean-gaming-conventions.md +312 -0
- package/knowledge/game-ui/menu-systems.md +388 -0
- package/knowledge/i18n/korean-app-store-visual.md +223 -0
- package/knowledge/i18n/korean-document-style.md +301 -0
- package/knowledge/i18n/korean-payments.md +211 -0
- package/knowledge/i18n/korean-product-conventions.md +96 -0
- package/knowledge/i18n/korean-publishing.md +139 -0
- package/knowledge/i18n/korean-typography.md +123 -0
- package/knowledge/icons/curated-sets.md +225 -0
- package/knowledge/illustration/hero-illustrations.md +234 -0
- package/knowledge/illustration/illustration-systems.md +221 -0
- package/knowledge/illustration/mascot-design.md +208 -0
- package/knowledge/illustration/spot-illustrations.md +246 -0
- package/knowledge/illustration/svg-optimization.md +308 -0
- package/knowledge/layout/spacing-and-grid.md +119 -0
- package/knowledge/motion/app-loading-sequences.md +277 -0
- package/knowledge/motion/choreography-depth.md +297 -0
- package/knowledge/motion/marketing-motion.md +223 -0
- package/knowledge/motion/micro-interactions.md +266 -0
- package/knowledge/motion/motion-tools.md +281 -0
- package/knowledge/motion/principles.md +126 -0
- package/knowledge/patterns/auth-flow-design.md +316 -0
- package/knowledge/patterns/b2b-onboarding-flows.md +182 -0
- package/knowledge/patterns/brand-identity.md +238 -0
- package/knowledge/patterns/brand-references.md +97 -0
- package/knowledge/patterns/chart-color-encoding.md +269 -0
- package/knowledge/patterns/chart-types.md +344 -0
- package/knowledge/patterns/dashboard-composition.md +299 -0
- package/knowledge/patterns/design-system-qa.md +356 -0
- package/knowledge/patterns/document-typography.md +248 -0
- package/knowledge/patterns/email-design.md +338 -0
- package/knowledge/patterns/empty-states.md +263 -0
- package/knowledge/patterns/error-states.md +318 -0
- package/knowledge/patterns/form-design.md +237 -0
- package/knowledge/patterns/information-architecture.md +341 -0
- package/knowledge/patterns/landing-hero-design.md +283 -0
- package/knowledge/patterns/landing-page-patterns.md +681 -0
- package/knowledge/patterns/list-and-feed.md +371 -0
- package/knowledge/patterns/mobile-navigation.md +278 -0
- package/knowledge/patterns/money-and-amount.md +301 -0
- package/knowledge/patterns/onboarding.md +300 -0
- package/knowledge/patterns/pricing-page-design.md +261 -0
- package/knowledge/patterns/realtime-data.md +280 -0
- package/knowledge/patterns/report-design.md +330 -0
- package/knowledge/patterns/search-ux.md +373 -0
- package/knowledge/patterns/settings-page.md +325 -0
- package/knowledge/patterns/slide-deck-design.md +316 -0
- package/knowledge/patterns/styles-catalog.md +2599 -0
- package/knowledge/patterns/technical-writing.md +306 -0
- package/knowledge/patterns/ui-reasoning.md +1853 -0
- package/knowledge/patterns/ux-guidelines.md +1454 -0
- package/knowledge/platforms/react-native.md +395 -0
- package/knowledge/print/brochures-and-flyers.md +276 -0
- package/knowledge/print/korean-print-conventions.md +284 -0
- package/knowledge/print/packaging.md +321 -0
- package/knowledge/print/print-fundamentals.md +238 -0
- package/knowledge/print/signage-and-posters.md +235 -0
- package/knowledge/print/stationery.md +262 -0
- package/knowledge/spatial/ar-patterns.md +253 -0
- package/knowledge/spatial/comfort-and-accessibility.md +245 -0
- package/knowledge/spatial/spatial-design-fundamentals.md +346 -0
- package/knowledge/spatial/spatial-ui-elements.md +343 -0
- package/knowledge/spatial/vr-patterns.md +302 -0
- package/knowledge/typography/font-pairings.md +332 -0
- package/knowledge/typography/mui-type-scale.md +57 -0
- package/knowledge/typography/type-scale-fundamentals.md +105 -0
- package/knowledge/video/in-product-video.md +277 -0
- package/knowledge/video/korean-video-conventions.md +247 -0
- package/knowledge/video/marketing-video.md +258 -0
- package/knowledge/video/social-and-short-form.md +257 -0
- package/knowledge/video/video-fundamentals.md +296 -0
- package/package.json +106 -0
- package/skills/README.md +39 -0
- package/skills/color-palette/PLAYBOOK.md +175 -0
- package/skills/color-palette/SKILL.md +6 -0
- package/skills/color-palette/TEMPLATE.md +103 -0
- package/skills/component-spec-writer/PLAYBOOK.md +169 -0
- package/skills/component-spec-writer/SKILL.md +6 -0
- package/skills/component-spec-writer/TEMPLATE.md +115 -0
- package/skills/conversational-ui-designer/PLAYBOOK.md +231 -0
- package/skills/conversational-ui-designer/SKILL.md +6 -0
- package/skills/design-broadcast/PLAYBOOK.md +146 -0
- package/skills/design-broadcast/SKILL.md +6 -0
- package/skills/design-critique/PLAYBOOK.md +137 -0
- package/skills/design-critique/SKILL.md +6 -0
- package/skills/design-pr-review/PLAYBOOK.md +171 -0
- package/skills/design-pr-review/SKILL.md +6 -0
- package/skills/design-system-builder/PLAYBOOK.md +218 -0
- package/skills/design-system-builder/SKILL.md +6 -0
- package/skills/design-system-qa/PLAYBOOK.md +163 -0
- package/skills/design-system-qa/SKILL.md +6 -0
- package/skills/document-author/PLAYBOOK.md +167 -0
- package/skills/document-author/SKILL.md +6 -0
- package/skills/figma-token-sync/PLAYBOOK.md +149 -0
- package/skills/figma-token-sync/SKILL.md +6 -0
- package/skills/game-ui-designer/PLAYBOOK.md +209 -0
- package/skills/game-ui-designer/SKILL.md +6 -0
- package/skills/handoff-spec/PLAYBOOK.md +170 -0
- package/skills/handoff-spec/SKILL.md +6 -0
- package/skills/illustration-designer/PLAYBOOK.md +166 -0
- package/skills/illustration-designer/SKILL.md +6 -0
- package/skills/motion-designer/PLAYBOOK.md +223 -0
- package/skills/motion-designer/SKILL.md +6 -0
- package/skills/print-designer/PLAYBOOK.md +209 -0
- package/skills/print-designer/SKILL.md +6 -0
- package/skills/slide-deck-author/PLAYBOOK.md +198 -0
- package/skills/slide-deck-author/SKILL.md +6 -0
- package/skills/spatial-designer/PLAYBOOK.md +238 -0
- package/skills/spatial-designer/SKILL.md +6 -0
- package/skills/ux-audit/PLAYBOOK.md +142 -0
- package/skills/ux-audit/SKILL.md +6 -0
- package/skills/video-designer/PLAYBOOK.md +240 -0
- package/skills/video-designer/SKILL.md +6 -0
- package/skills/website-improvement/PLAYBOOK.md +118 -0
- package/skills/website-improvement/SKILL.md +6 -0
- package/skills/website-improvement/TEMPLATE.md +103 -0
- package/tools/audit/changelog-generate.py +176 -0
- package/tools/audit/check-coverage.py +483 -0
- package/tools/audit/doctor_assertions.py +256 -0
- package/tools/audit/example-qa.py +108 -0
- package/tools/audit/frontmatter-check.py +199 -0
- package/tools/audit/integration-check.py +95 -0
- package/tools/audit/korean-copy-check.py +161 -0
- package/tools/audit/link-check.py +146 -0
- package/tools/audit/local-ci.py +558 -0
- package/tools/audit/package-contents.py +723 -0
- package/tools/audit/package-smoke.py +22443 -0
- package/tools/audit/raw-hex-check.py +252 -0
- package/tools/audit/registry-smoke.py +8188 -0
- package/tools/audit/release-metadata.py +9832 -0
- package/tools/audit/run-all.py +351 -0
- package/tools/audit/smoke_assertions.py +12742 -0
- package/tools/audit/stability-review.py +332 -0
- package/tools/audit/stale-check.py +156 -0
- package/tools/migrations/add-version-frontmatter.py +132 -0
- package/tools/migrations/bump-last-updated.py +131 -0
- package/tools/migrations/promote-stability.py +172 -0
- package/tools/preview/README.md +58 -0
- package/tools/preview/render-tokens.py +454 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Quarterly stability review report.
|
|
3
|
+
|
|
4
|
+
Generates a markdown report listing knowledge files grouped by stability
|
|
5
|
+
level + freshness. Surfaces:
|
|
6
|
+
- experimental files that held up >= 6 months → promote to stable.
|
|
7
|
+
- beta files >= 3 months old without recent edits → promote to stable.
|
|
8
|
+
- stable files > 12 months old → review + bump or revise.
|
|
9
|
+
- deprecated files with no removal plan → flag for next major.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 tools/audit/stability-review.py
|
|
13
|
+
python3 tools/audit/stability-review.py --output docs/STABILITY-REVIEW.md
|
|
14
|
+
python3 tools/audit/stability-review.py --warn-months 3 --promote-after 6
|
|
15
|
+
|
|
16
|
+
Pairs with:
|
|
17
|
+
- tools/migrations/promote-stability.py (bulk promote/demote)
|
|
18
|
+
- tools/migrations/bump-last-updated.py (bulk freshness bump)
|
|
19
|
+
|
|
20
|
+
Read docs/CONTRIBUTING.md "Quarterly stability review" for the full ritual.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from collections import defaultdict
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from datetime import date
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
33
|
+
KNOWLEDGE = ROOT / "knowledge"
|
|
34
|
+
|
|
35
|
+
DATE_RE = re.compile(r"^(\d{4})-(\d{2})(?:-(\d{2}))?$")
|
|
36
|
+
STABILITY_VALUES = {"stable", "beta", "experimental", "deprecated"}
|
|
37
|
+
|
|
38
|
+
# Generated artifacts that are auto-regenerated and don't need manual review.
|
|
39
|
+
# These are excluded from the "files without stability metadata" section.
|
|
40
|
+
# (Surfaced as a v4.7 dogfood fix — COVERAGE.md was a false positive.)
|
|
41
|
+
GENERATED_ARTIFACTS = {
|
|
42
|
+
"knowledge/COVERAGE.md",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class FileInfo:
|
|
48
|
+
path: Path
|
|
49
|
+
rel: Path
|
|
50
|
+
stability: str | None
|
|
51
|
+
last_updated: str | None
|
|
52
|
+
last_updated_date: date | None
|
|
53
|
+
age_months: int | None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def find_frontmatter_bounds(text: str) -> tuple[int, int] | None:
|
|
57
|
+
lines = text.splitlines()
|
|
58
|
+
cursor = 0
|
|
59
|
+
while cursor < len(lines):
|
|
60
|
+
s = lines[cursor].strip()
|
|
61
|
+
if s.startswith("<!--") and s.endswith("-->"):
|
|
62
|
+
cursor += 1
|
|
63
|
+
continue
|
|
64
|
+
if s == "":
|
|
65
|
+
cursor += 1
|
|
66
|
+
continue
|
|
67
|
+
break
|
|
68
|
+
if cursor >= len(lines) or lines[cursor].strip() != "---":
|
|
69
|
+
return None
|
|
70
|
+
start = cursor + 1
|
|
71
|
+
for i in range(start, len(lines)):
|
|
72
|
+
if lines[i].strip() == "---":
|
|
73
|
+
return (start, i)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def extract_frontmatter_value(text: str, key: str) -> str | None:
|
|
78
|
+
bounds = find_frontmatter_bounds(text)
|
|
79
|
+
if bounds is None:
|
|
80
|
+
return None
|
|
81
|
+
start, end = bounds
|
|
82
|
+
for line in text.splitlines()[start:end]:
|
|
83
|
+
if line.lstrip().startswith(f"{key}:"):
|
|
84
|
+
return line.split(":", 1)[1].strip().strip("'\"")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_date(value: str | None) -> date | None:
|
|
89
|
+
if value is None:
|
|
90
|
+
return None
|
|
91
|
+
m = DATE_RE.match(value)
|
|
92
|
+
if not m:
|
|
93
|
+
return None
|
|
94
|
+
year, month, day = int(m.group(1)), int(m.group(2)), m.group(3)
|
|
95
|
+
if day is None:
|
|
96
|
+
return date(year, month, 28)
|
|
97
|
+
return date(year, month, int(day))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def months_diff(target: date, today: date) -> int:
|
|
101
|
+
return (today.year - target.year) * 12 + (today.month - target.month)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def collect(today: date) -> list[FileInfo]:
|
|
105
|
+
out: list[FileInfo] = []
|
|
106
|
+
for path in sorted(KNOWLEDGE.rglob("*.md")):
|
|
107
|
+
rel = path.relative_to(ROOT)
|
|
108
|
+
# Skip generated artifacts — they don't carry stability by design.
|
|
109
|
+
if str(rel).replace("\\", "/") in GENERATED_ARTIFACTS:
|
|
110
|
+
continue
|
|
111
|
+
text = path.read_text(encoding="utf-8")
|
|
112
|
+
stability = extract_frontmatter_value(text, "stability")
|
|
113
|
+
last_updated = extract_frontmatter_value(text, "last_updated")
|
|
114
|
+
d = parse_date(last_updated)
|
|
115
|
+
age = months_diff(d, today) if d else None
|
|
116
|
+
out.append(
|
|
117
|
+
FileInfo(
|
|
118
|
+
path=path,
|
|
119
|
+
rel=rel,
|
|
120
|
+
stability=stability,
|
|
121
|
+
last_updated=last_updated,
|
|
122
|
+
last_updated_date=d,
|
|
123
|
+
age_months=age,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def render_promotion_candidates(files: list[FileInfo], from_level: str, min_age: int) -> list[FileInfo]:
|
|
130
|
+
"""Files at `from_level` whose last_updated is at least `min_age` months ago."""
|
|
131
|
+
return [
|
|
132
|
+
f for f in files
|
|
133
|
+
if f.stability == from_level and f.age_months is not None and f.age_months >= min_age
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def render_stale_within_level(files: list[FileInfo], level: str, threshold: int) -> list[FileInfo]:
|
|
138
|
+
return [
|
|
139
|
+
f for f in files
|
|
140
|
+
if f.stability == level and f.age_months is not None and f.age_months >= threshold
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def render_report(files: list[FileInfo], today: date, *, warn_months: int, promote_after: int, stale_months: int) -> str:
|
|
145
|
+
lines: list[str] = []
|
|
146
|
+
lines.append(f"# Stability review — {today.isoformat()}")
|
|
147
|
+
lines.append("")
|
|
148
|
+
lines.append(
|
|
149
|
+
f"Generated by `tools/audit/stability-review.py`. "
|
|
150
|
+
f"Pairs with `tools/migrations/promote-stability.py` (bulk promote/demote) "
|
|
151
|
+
f"and `tools/migrations/bump-last-updated.py` (bulk freshness bump)."
|
|
152
|
+
)
|
|
153
|
+
lines.append("")
|
|
154
|
+
|
|
155
|
+
# Summary table
|
|
156
|
+
by_level: dict[str, list[FileInfo]] = defaultdict(list)
|
|
157
|
+
no_stability: list[FileInfo] = []
|
|
158
|
+
for f in files:
|
|
159
|
+
if f.stability and f.stability in STABILITY_VALUES:
|
|
160
|
+
by_level[f.stability].append(f)
|
|
161
|
+
else:
|
|
162
|
+
no_stability.append(f)
|
|
163
|
+
|
|
164
|
+
lines.append("## Summary")
|
|
165
|
+
lines.append("")
|
|
166
|
+
lines.append("| Level | Count | Oldest (months) |")
|
|
167
|
+
lines.append("| --- | --- | --- |")
|
|
168
|
+
for level in ("stable", "beta", "experimental", "deprecated"):
|
|
169
|
+
items = by_level.get(level, [])
|
|
170
|
+
oldest = max((f.age_months for f in items if f.age_months is not None), default=None)
|
|
171
|
+
oldest_str = f"{oldest}" if oldest is not None else "—"
|
|
172
|
+
lines.append(f"| {level} | {len(items)} | {oldest_str} |")
|
|
173
|
+
if no_stability:
|
|
174
|
+
lines.append(f"| (no stability key) | {len(no_stability)} | — |")
|
|
175
|
+
lines.append("")
|
|
176
|
+
|
|
177
|
+
# Section 1: experimental → stable promotion candidates
|
|
178
|
+
promote_exp = render_promotion_candidates(files, "experimental", promote_after)
|
|
179
|
+
lines.append(f"## Promotion candidates: experimental → stable (held up ≥ {promote_after} months)")
|
|
180
|
+
lines.append("")
|
|
181
|
+
if promote_exp:
|
|
182
|
+
lines.append("Run after review:")
|
|
183
|
+
lines.append("")
|
|
184
|
+
lines.append("```bash")
|
|
185
|
+
lines.append("python3 tools/migrations/promote-stability.py --from experimental --to stable \\")
|
|
186
|
+
for f in promote_exp:
|
|
187
|
+
lines.append(f" {f.rel} \\")
|
|
188
|
+
if lines[-1].endswith(" \\"):
|
|
189
|
+
lines[-1] = lines[-1].rstrip(" \\")
|
|
190
|
+
lines.append("```")
|
|
191
|
+
lines.append("")
|
|
192
|
+
else:
|
|
193
|
+
lines.append("(none)")
|
|
194
|
+
lines.append("")
|
|
195
|
+
|
|
196
|
+
# Section 2: beta → stable promotion candidates
|
|
197
|
+
promote_beta = render_promotion_candidates(files, "beta", warn_months)
|
|
198
|
+
lines.append(f"## Promotion candidates: beta → stable (held up ≥ {warn_months} months)")
|
|
199
|
+
lines.append("")
|
|
200
|
+
if promote_beta:
|
|
201
|
+
lines.append("Run after review:")
|
|
202
|
+
lines.append("")
|
|
203
|
+
lines.append("```bash")
|
|
204
|
+
lines.append("python3 tools/migrations/promote-stability.py --from beta --to stable \\")
|
|
205
|
+
for f in promote_beta:
|
|
206
|
+
lines.append(f" {f.rel} \\")
|
|
207
|
+
if lines[-1].endswith(" \\"):
|
|
208
|
+
lines[-1] = lines[-1].rstrip(" \\")
|
|
209
|
+
lines.append("```")
|
|
210
|
+
lines.append("")
|
|
211
|
+
else:
|
|
212
|
+
lines.append("(none)")
|
|
213
|
+
lines.append("")
|
|
214
|
+
|
|
215
|
+
# Section 3: stable files getting stale
|
|
216
|
+
stale_stable = render_stale_within_level(files, "stable", stale_months)
|
|
217
|
+
lines.append(f"## Stable files due for re-review (last_updated ≥ {stale_months} months ago)")
|
|
218
|
+
lines.append("")
|
|
219
|
+
if stale_stable:
|
|
220
|
+
lines.append(
|
|
221
|
+
f"Either bump `last_updated` (still accurate, no edits needed) or revise + bump."
|
|
222
|
+
)
|
|
223
|
+
lines.append("")
|
|
224
|
+
lines.append("Bulk-bump after review:")
|
|
225
|
+
lines.append("")
|
|
226
|
+
lines.append("```bash")
|
|
227
|
+
lines.append("python3 tools/migrations/bump-last-updated.py \\")
|
|
228
|
+
for f in stale_stable[:20]:
|
|
229
|
+
lines.append(f" {f.rel} \\")
|
|
230
|
+
if len(stale_stable) > 20:
|
|
231
|
+
lines.append(f" # ... and {len(stale_stable) - 20} more")
|
|
232
|
+
if lines[-1].endswith(" \\"):
|
|
233
|
+
lines[-1] = lines[-1].rstrip(" \\")
|
|
234
|
+
lines.append("```")
|
|
235
|
+
lines.append("")
|
|
236
|
+
lines.append("Files:")
|
|
237
|
+
lines.append("")
|
|
238
|
+
for f in stale_stable:
|
|
239
|
+
lines.append(f"- `{f.rel}` — {f.age_months} months old")
|
|
240
|
+
lines.append("")
|
|
241
|
+
else:
|
|
242
|
+
lines.append("(none)")
|
|
243
|
+
lines.append("")
|
|
244
|
+
|
|
245
|
+
# Section 4: deprecated files
|
|
246
|
+
deprecated = by_level.get("deprecated", [])
|
|
247
|
+
lines.append("## Deprecated files (review for next major removal)")
|
|
248
|
+
lines.append("")
|
|
249
|
+
if deprecated:
|
|
250
|
+
lines.append("These should be removed in the next major version. Confirm removal plan exists in CHANGELOG.")
|
|
251
|
+
lines.append("")
|
|
252
|
+
for f in deprecated:
|
|
253
|
+
age_str = f"{f.age_months} months" if f.age_months is not None else "no last_updated"
|
|
254
|
+
lines.append(f"- `{f.rel}` — {age_str}")
|
|
255
|
+
lines.append("")
|
|
256
|
+
else:
|
|
257
|
+
lines.append("(none)")
|
|
258
|
+
lines.append("")
|
|
259
|
+
|
|
260
|
+
# Section 5: files without stability metadata
|
|
261
|
+
if no_stability:
|
|
262
|
+
lines.append("## Files without `stability` metadata")
|
|
263
|
+
lines.append("")
|
|
264
|
+
lines.append("Add `stability:` to frontmatter (one of stable / beta / experimental / deprecated).")
|
|
265
|
+
lines.append("")
|
|
266
|
+
for f in no_stability:
|
|
267
|
+
lines.append(f"- `{f.rel}`")
|
|
268
|
+
lines.append("")
|
|
269
|
+
|
|
270
|
+
# Closing
|
|
271
|
+
lines.append("---")
|
|
272
|
+
lines.append("")
|
|
273
|
+
lines.append("## Ritual checklist")
|
|
274
|
+
lines.append("")
|
|
275
|
+
lines.append("Walk through each section above. For each candidate:")
|
|
276
|
+
lines.append("")
|
|
277
|
+
lines.append("- [ ] Read the file. Is the content still accurate?")
|
|
278
|
+
lines.append("- [ ] If yes: bump `last_updated` (use `tools/migrations/bump-last-updated.py`).")
|
|
279
|
+
lines.append("- [ ] If revisions needed: edit + bump.")
|
|
280
|
+
lines.append("- [ ] If obsolete: mark `stability: deprecated` and add deprecation note in body.")
|
|
281
|
+
lines.append("- [ ] For promotion candidates: confirm content stable, then promote.")
|
|
282
|
+
lines.append("")
|
|
283
|
+
lines.append("After the ritual, document outcome in `CHANGELOG.md` under the next minor release.")
|
|
284
|
+
lines.append("")
|
|
285
|
+
|
|
286
|
+
return "\n".join(lines)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main() -> None:
|
|
290
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"--output",
|
|
293
|
+
type=Path,
|
|
294
|
+
help="Write report to this path (otherwise stdout)",
|
|
295
|
+
)
|
|
296
|
+
parser.add_argument("--warn-months", type=int, default=3, help="Beta promotion threshold (default: 3)")
|
|
297
|
+
parser.add_argument(
|
|
298
|
+
"--promote-after",
|
|
299
|
+
type=int,
|
|
300
|
+
default=6,
|
|
301
|
+
help="Experimental → stable threshold (default: 6)",
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--stale-months",
|
|
305
|
+
type=int,
|
|
306
|
+
default=12,
|
|
307
|
+
help="Stable file re-review threshold (default: 12)",
|
|
308
|
+
)
|
|
309
|
+
parser.add_argument("--today", help="Override today (YYYY-MM-DD); for testing")
|
|
310
|
+
args = parser.parse_args()
|
|
311
|
+
|
|
312
|
+
today = date.fromisoformat(args.today) if args.today else date.today()
|
|
313
|
+
|
|
314
|
+
files = collect(today)
|
|
315
|
+
report = render_report(
|
|
316
|
+
files,
|
|
317
|
+
today,
|
|
318
|
+
warn_months=args.warn_months,
|
|
319
|
+
promote_after=args.promote_after,
|
|
320
|
+
stale_months=args.stale_months,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if args.output:
|
|
324
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
args.output.write_text(report, encoding="utf-8")
|
|
326
|
+
print(f"Wrote: {args.output}")
|
|
327
|
+
else:
|
|
328
|
+
sys.stdout.write(report)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Flag knowledge files whose `last_updated` is too old.
|
|
3
|
+
|
|
4
|
+
Operationalizes the versioned frontmatter (added in v3.11). Default:
|
|
5
|
+
- Warn: last_updated > 6 months ago.
|
|
6
|
+
- Error: last_updated > 12 months ago (with --strict).
|
|
7
|
+
|
|
8
|
+
Files without `last_updated` are skipped (backward-compatible).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 tools/audit/stale-check.py
|
|
12
|
+
python3 tools/audit/stale-check.py --strict
|
|
13
|
+
python3 tools/audit/stale-check.py --warn-months 3 --error-months 6
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import date
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
24
|
+
KNOWLEDGE = ROOT / "knowledge"
|
|
25
|
+
|
|
26
|
+
DATE_RE = re.compile(r"^(\d{4})-(\d{2})(?:-(\d{2}))?$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_frontmatter_bounds(text: str) -> tuple[int, int] | None:
|
|
30
|
+
lines = text.splitlines()
|
|
31
|
+
cursor = 0
|
|
32
|
+
while cursor < len(lines):
|
|
33
|
+
s = lines[cursor].strip()
|
|
34
|
+
if s.startswith("<!--") and s.endswith("-->"):
|
|
35
|
+
cursor += 1
|
|
36
|
+
continue
|
|
37
|
+
if s == "":
|
|
38
|
+
cursor += 1
|
|
39
|
+
continue
|
|
40
|
+
break
|
|
41
|
+
if cursor >= len(lines) or lines[cursor].strip() != "---":
|
|
42
|
+
return None
|
|
43
|
+
start = cursor + 1
|
|
44
|
+
for i in range(start, len(lines)):
|
|
45
|
+
if lines[i].strip() == "---":
|
|
46
|
+
return (start, i)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_last_updated(text: str) -> str | None:
|
|
51
|
+
bounds = find_frontmatter_bounds(text)
|
|
52
|
+
if bounds is None:
|
|
53
|
+
return None
|
|
54
|
+
start, end = bounds
|
|
55
|
+
for line in text.splitlines()[start:end]:
|
|
56
|
+
if line.lstrip().startswith("last_updated:"):
|
|
57
|
+
value = line.split(":", 1)[1].strip().strip("'\"")
|
|
58
|
+
return value
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_date(value: str) -> date | None:
|
|
63
|
+
"""Parse YYYY-MM or YYYY-MM-DD; return last day of month if no day given."""
|
|
64
|
+
m = DATE_RE.match(value)
|
|
65
|
+
if not m:
|
|
66
|
+
return None
|
|
67
|
+
year, month, day = int(m.group(1)), int(m.group(2)), m.group(3)
|
|
68
|
+
if day is None:
|
|
69
|
+
# Treat YYYY-MM as the LAST day of that month (most generous reading).
|
|
70
|
+
# Pick day 28 — safe for all months — then advance to month-end.
|
|
71
|
+
d = date(year, month, 28)
|
|
72
|
+
else:
|
|
73
|
+
d = date(year, month, int(day))
|
|
74
|
+
return d
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def months_diff(target: date, today: date) -> int:
|
|
78
|
+
"""Approximate month difference (today − target). Negative if target is in the future."""
|
|
79
|
+
return (today.year - target.year) * 12 + (today.month - target.month)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> None:
|
|
83
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
84
|
+
parser.add_argument("--strict", action="store_true", help="Exit 1 if any file exceeds the error threshold")
|
|
85
|
+
parser.add_argument("--warn-months", type=int, default=6, help="Months before warning (default: 6)")
|
|
86
|
+
parser.add_argument("--error-months", type=int, default=12, help="Months before error (default: 12)")
|
|
87
|
+
parser.add_argument("--today", default=None, help="Override today's date (YYYY-MM-DD) — for testing")
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
|
|
90
|
+
today = date.fromisoformat(args.today) if args.today else date.today()
|
|
91
|
+
|
|
92
|
+
warns: list[str] = []
|
|
93
|
+
errors: list[str] = []
|
|
94
|
+
skipped = 0
|
|
95
|
+
invalid = 0
|
|
96
|
+
fresh = 0
|
|
97
|
+
|
|
98
|
+
for path in sorted(KNOWLEDGE.rglob("*.md")):
|
|
99
|
+
text = path.read_text(encoding="utf-8")
|
|
100
|
+
last_updated = extract_last_updated(text)
|
|
101
|
+
rel = path.relative_to(ROOT)
|
|
102
|
+
|
|
103
|
+
if last_updated is None:
|
|
104
|
+
skipped += 1
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
d = parse_date(last_updated)
|
|
108
|
+
if d is None:
|
|
109
|
+
invalid += 1
|
|
110
|
+
errors.append(f"{rel}: invalid last_updated format '{last_updated}'")
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
diff = months_diff(d, today)
|
|
114
|
+
if diff >= args.error_months:
|
|
115
|
+
errors.append(f"{rel}: {diff} months stale (last_updated={last_updated})")
|
|
116
|
+
elif diff >= args.warn_months:
|
|
117
|
+
warns.append(f"{rel}: {diff} months old (last_updated={last_updated})")
|
|
118
|
+
else:
|
|
119
|
+
fresh += 1
|
|
120
|
+
|
|
121
|
+
total = fresh + len(warns) + len(errors) + skipped + invalid
|
|
122
|
+
print()
|
|
123
|
+
print(f" Fresh (≤ {args.warn_months} months): {fresh}")
|
|
124
|
+
print(f" Warning (>{args.warn_months} months): {len(warns)}")
|
|
125
|
+
print(f" Stale (>{args.error_months} months): {len(errors)}")
|
|
126
|
+
print(f" Skipped (no last_updated): {skipped}")
|
|
127
|
+
print(f" Invalid format: {invalid}")
|
|
128
|
+
print(f" Total knowledge files: {total}")
|
|
129
|
+
print()
|
|
130
|
+
|
|
131
|
+
if warns and not args.strict:
|
|
132
|
+
print("⚠ Warnings:")
|
|
133
|
+
for w in warns[:10]:
|
|
134
|
+
print(f" - {w}")
|
|
135
|
+
if len(warns) > 10:
|
|
136
|
+
print(f" ... and {len(warns) - 10} more")
|
|
137
|
+
|
|
138
|
+
if errors:
|
|
139
|
+
print("✗ Stale files:")
|
|
140
|
+
for e in errors[:20]:
|
|
141
|
+
print(f" - {e}")
|
|
142
|
+
if len(errors) > 20:
|
|
143
|
+
print(f" ... and {len(errors) - 20} more")
|
|
144
|
+
|
|
145
|
+
if errors:
|
|
146
|
+
if args.strict:
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
else:
|
|
149
|
+
print()
|
|
150
|
+
print("Use --strict to fail CI on stale files.")
|
|
151
|
+
else:
|
|
152
|
+
print("All knowledge files within freshness window ✓")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
main()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-shot migration: add `version`, `last_updated`, `stability` keys to
|
|
3
|
+
the YAML frontmatter of every knowledge file.
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
- Locates every .md file in knowledge/ that already has a frontmatter
|
|
7
|
+
block (between `---` markers, optionally preceded by an HTML comment
|
|
8
|
+
like `<!-- hand-written -->`).
|
|
9
|
+
- If `version` is already present, the file is skipped (idempotent).
|
|
10
|
+
- Otherwise inserts `version: 1.0.0`, `last_updated: 2026-05`, and
|
|
11
|
+
`stability: stable` BEFORE the closing `---` line. Existing keys
|
|
12
|
+
are preserved as-is.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 tools/migrations/add-version-frontmatter.py # dry-run
|
|
16
|
+
python3 tools/migrations/add-version-frontmatter.py --write # apply
|
|
17
|
+
|
|
18
|
+
After running, verify with:
|
|
19
|
+
python3 tools/audit/frontmatter-check.py
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
27
|
+
KNOWLEDGE = ROOT / "knowledge"
|
|
28
|
+
|
|
29
|
+
DEFAULT_VERSION = "1.0.0"
|
|
30
|
+
DEFAULT_LAST_UPDATED = "2026-05"
|
|
31
|
+
DEFAULT_STABILITY = "stable"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_frontmatter_bounds(text: str) -> tuple[int, int] | None:
|
|
35
|
+
"""Return (start_line_idx, end_line_idx) of frontmatter, exclusive of
|
|
36
|
+
the closing `---` line marker (so insertions go ABOVE end_line_idx).
|
|
37
|
+
|
|
38
|
+
Skips leading HTML comments and blank lines before the opening `---`.
|
|
39
|
+
Returns None if no frontmatter block found.
|
|
40
|
+
"""
|
|
41
|
+
lines = text.splitlines()
|
|
42
|
+
cursor = 0
|
|
43
|
+
|
|
44
|
+
# Skip leading HTML comments + blank lines
|
|
45
|
+
while cursor < len(lines):
|
|
46
|
+
stripped = lines[cursor].strip()
|
|
47
|
+
if stripped.startswith("<!--") and stripped.endswith("-->"):
|
|
48
|
+
cursor += 1
|
|
49
|
+
continue
|
|
50
|
+
if stripped == "":
|
|
51
|
+
cursor += 1
|
|
52
|
+
continue
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
# Now expect opening `---`
|
|
56
|
+
if cursor >= len(lines) or lines[cursor].strip() != "---":
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
start = cursor + 1
|
|
60
|
+
# Find closing `---`
|
|
61
|
+
for i in range(start, len(lines)):
|
|
62
|
+
if lines[i].strip() == "---":
|
|
63
|
+
return (start, i)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def has_version_key(text: str, start: int, end: int) -> bool:
|
|
68
|
+
lines = text.splitlines()
|
|
69
|
+
for line in lines[start:end]:
|
|
70
|
+
if line.lstrip().startswith("version:"):
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def insert_version_keys(text: str, start: int, end: int) -> str:
|
|
76
|
+
lines = text.splitlines(keepends=True)
|
|
77
|
+
insertion = (
|
|
78
|
+
f"version: {DEFAULT_VERSION}\n"
|
|
79
|
+
f"last_updated: {DEFAULT_LAST_UPDATED}\n"
|
|
80
|
+
f"stability: {DEFAULT_STABILITY}\n"
|
|
81
|
+
)
|
|
82
|
+
# Insert before the closing `---` line (line index `end`)
|
|
83
|
+
lines.insert(end, insertion)
|
|
84
|
+
return "".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def process_file(path: Path, write: bool) -> str:
|
|
88
|
+
"""Returns one of: 'skip-no-frontmatter', 'skip-already-versioned',
|
|
89
|
+
'updated', 'would-update'."""
|
|
90
|
+
text = path.read_text(encoding="utf-8")
|
|
91
|
+
bounds = find_frontmatter_bounds(text)
|
|
92
|
+
if bounds is None:
|
|
93
|
+
return "skip-no-frontmatter"
|
|
94
|
+
|
|
95
|
+
start, end = bounds
|
|
96
|
+
if has_version_key(text, start, end):
|
|
97
|
+
return "skip-already-versioned"
|
|
98
|
+
|
|
99
|
+
new_text = insert_version_keys(text, start, end)
|
|
100
|
+
if write:
|
|
101
|
+
path.write_text(new_text, encoding="utf-8")
|
|
102
|
+
return "updated"
|
|
103
|
+
return "would-update"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> None:
|
|
107
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
108
|
+
parser.add_argument("--write", action="store_true", help="Apply changes (without this, dry-run)")
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
|
|
111
|
+
files = sorted(KNOWLEDGE.rglob("*.md"))
|
|
112
|
+
counts = {"updated": 0, "would-update": 0, "skip-already-versioned": 0, "skip-no-frontmatter": 0}
|
|
113
|
+
|
|
114
|
+
for path in files:
|
|
115
|
+
result = process_file(path, args.write)
|
|
116
|
+
counts[result] += 1
|
|
117
|
+
|
|
118
|
+
print()
|
|
119
|
+
if args.write:
|
|
120
|
+
print(f" ✓ Updated: {counts['updated']}")
|
|
121
|
+
else:
|
|
122
|
+
print(f" Would update: {counts['would-update']}")
|
|
123
|
+
print(f" Skipped (already versioned): {counts['skip-already-versioned']}")
|
|
124
|
+
print(f" Skipped (no frontmatter): {counts['skip-no-frontmatter']}")
|
|
125
|
+
print(f" Total scanned: {sum(counts.values())}")
|
|
126
|
+
print()
|
|
127
|
+
if not args.write and counts["would-update"] > 0:
|
|
128
|
+
print("Re-run with --write to apply.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|