opencode-skills-collection 3.0.34 → 3.0.36
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/bundled-skills/.antigravity-install-manifest.json +16 -1
- package/bundled-skills/accesslint-audit/SKILL.md +115 -0
- package/bundled-skills/accesslint-diff/SKILL.md +81 -0
- package/bundled-skills/accesslint-scan/SKILL.md +47 -0
- package/bundled-skills/composition-patterns/SKILL.md +87 -0
- package/bundled-skills/composition-patterns/rules/_sections.md +29 -0
- package/bundled-skills/composition-patterns/rules/_template.md +24 -0
- package/bundled-skills/composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/bundled-skills/composition-patterns/rules/architecture-compound-components.md +112 -0
- package/bundled-skills/composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/bundled-skills/composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/bundled-skills/composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/bundled-skills/composition-patterns/rules/state-context-interface.md +191 -0
- package/bundled-skills/composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/bundled-skills/composition-patterns/rules/state-lift-state.md +125 -0
- package/bundled-skills/debugging-toolkit/SKILL.md +35 -0
- package/bundled-skills/deploy-to-vercel/SKILL.md +304 -0
- package/bundled-skills/deploy-to-vercel/resources/deploy-codex.sh +301 -0
- package/bundled-skills/deploy-to-vercel/resources/deploy.sh +301 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/backups/README-2026-06-02.md +687 -0
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +4 -4
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +245 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +3 -3
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/plugins.md +21 -13
- package/bundled-skills/docs/users/specialized-plugin-roadmap.md +95 -0
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/mmx-cli/SKILL.md +5 -2
- package/bundled-skills/nextjs-seo-indexing/SKILL.md +3 -3
- package/bundled-skills/polis-protocol/SKILL.md +93 -0
- package/bundled-skills/python-development/SKILL.md +35 -0
- package/bundled-skills/radix-ui-design-system/SKILL.md +2 -2
- package/bundled-skills/react-native-skills/SKILL.md +120 -0
- package/bundled-skills/react-native-skills/rules/_sections.md +86 -0
- package/bundled-skills/react-native-skills/rules/_template.md +28 -0
- package/bundled-skills/react-native-skills/rules/animation-derived-value.md +53 -0
- package/bundled-skills/react-native-skills/rules/animation-gesture-detector-press.md +95 -0
- package/bundled-skills/react-native-skills/rules/animation-gpu-properties.md +65 -0
- package/bundled-skills/react-native-skills/rules/design-system-compound-components.md +66 -0
- package/bundled-skills/react-native-skills/rules/fonts-config-plugin.md +71 -0
- package/bundled-skills/react-native-skills/rules/imports-design-system-folder.md +68 -0
- package/bundled-skills/react-native-skills/rules/js-hoist-intl.md +61 -0
- package/bundled-skills/react-native-skills/rules/list-performance-callbacks.md +44 -0
- package/bundled-skills/react-native-skills/rules/list-performance-function-references.md +132 -0
- package/bundled-skills/react-native-skills/rules/list-performance-images.md +53 -0
- package/bundled-skills/react-native-skills/rules/list-performance-inline-objects.md +97 -0
- package/bundled-skills/react-native-skills/rules/list-performance-item-expensive.md +94 -0
- package/bundled-skills/react-native-skills/rules/list-performance-item-memo.md +82 -0
- package/bundled-skills/react-native-skills/rules/list-performance-item-types.md +104 -0
- package/bundled-skills/react-native-skills/rules/list-performance-virtualize.md +67 -0
- package/bundled-skills/react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
- package/bundled-skills/react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
- package/bundled-skills/react-native-skills/rules/navigation-native-navigators.md +188 -0
- package/bundled-skills/react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
- package/bundled-skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
- package/bundled-skills/react-native-skills/rules/react-state-dispatcher.md +91 -0
- package/bundled-skills/react-native-skills/rules/react-state-fallback.md +56 -0
- package/bundled-skills/react-native-skills/rules/react-state-minimize.md +65 -0
- package/bundled-skills/react-native-skills/rules/rendering-no-falsy-and.md +74 -0
- package/bundled-skills/react-native-skills/rules/rendering-text-in-text-component.md +36 -0
- package/bundled-skills/react-native-skills/rules/scroll-position-no-state.md +82 -0
- package/bundled-skills/react-native-skills/rules/state-ground-truth.md +80 -0
- package/bundled-skills/react-native-skills/rules/ui-expo-image.md +66 -0
- package/bundled-skills/react-native-skills/rules/ui-image-gallery.md +104 -0
- package/bundled-skills/react-native-skills/rules/ui-measure-views.md +78 -0
- package/bundled-skills/react-native-skills/rules/ui-menus.md +174 -0
- package/bundled-skills/react-native-skills/rules/ui-native-modals.md +77 -0
- package/bundled-skills/react-native-skills/rules/ui-pressable.md +61 -0
- package/bundled-skills/react-native-skills/rules/ui-safe-area-scroll.md +65 -0
- package/bundled-skills/react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
- package/bundled-skills/react-native-skills/rules/ui-styling.md +87 -0
- package/bundled-skills/schema-markup-generator/SKILL.md +1 -1
- package/bundled-skills/skill-issue/SKILL.md +73 -0
- package/bundled-skills/social-metadata-hardening/SKILL.md +4 -3
- package/bundled-skills/social-post-writer-seo/SKILL.md +19 -0
- package/bundled-skills/tdd-workflows/SKILL.md +35 -0
- package/bundled-skills/user-thoughts/SKILL.md +236 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/README.ai.md +13 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/define.ini +3 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/README.ai.md +25 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/backlog.md +19 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/details/dev-stack.md +7 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/details/general.md +7 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/details/plans.md +7 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/details/rules.md +7 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/details/ui/details.md +7 -0
- package/bundled-skills/user-thoughts/assets/Runtime-Template/mdbase/details/ui/outline.md +7 -0
- package/bundled-skills/user-thoughts/references/commands.md +54 -0
- package/bundled-skills/user-thoughts/references/edge-cases.md +84 -0
- package/bundled-skills/user-thoughts/references/safety.md +65 -0
- package/bundled-skills/user-thoughts/references/sortin.md +76 -0
- package/bundled-skills/user-thoughts/scripts/common.py +62 -0
- package/bundled-skills/user-thoughts/scripts/ignore_ops.py +125 -0
- package/bundled-skills/user-thoughts/scripts/init.py +63 -0
- package/bundled-skills/user-thoughts/scripts/show_mdbase.py +93 -0
- package/bundled-skills/user-thoughts/scripts/show_raw.py +42 -0
- package/bundled-skills/user-thoughts/scripts/sortin.py +211 -0
- package/bundled-skills/user-thoughts/scripts/status.py +56 -0
- package/bundled-skills/user-thoughts/scripts/toggle.py +68 -0
- package/bundled-skills/user-thoughts/scripts/write_raw.py +106 -0
- package/bundled-skills/vercel-cli-with-tokens/SKILL.md +361 -0
- package/bundled-skills/vercel-optimize/CONTRIBUTING.md +41 -0
- package/bundled-skills/vercel-optimize/SKILL.md +331 -0
- package/bundled-skills/vercel-optimize/lib/auth-route.mjs +23 -0
- package/bundled-skills/vercel-optimize/lib/budget-summary.mjs +182 -0
- package/bundled-skills/vercel-optimize/lib/citations.mjs +139 -0
- package/bundled-skills/vercel-optimize/lib/cost-coverage.mjs +143 -0
- package/bundled-skills/vercel-optimize/lib/dedup-recs.mjs +325 -0
- package/bundled-skills/vercel-optimize/lib/deep-dive.mjs +350 -0
- package/bundled-skills/vercel-optimize/lib/display-labels.mjs +185 -0
- package/bundled-skills/vercel-optimize/lib/extract-claims.mjs +550 -0
- package/bundled-skills/vercel-optimize/lib/framework-support.mjs +67 -0
- package/bundled-skills/vercel-optimize/lib/gates/build-minutes-fanout.mjs +69 -0
- package/bundled-skills/vercel-optimize/lib/gates/cold-start.mjs +66 -0
- package/bundled-skills/vercel-optimize/lib/gates/contract.mjs +79 -0
- package/bundled-skills/vercel-optimize/lib/gates/cwv-poor.mjs +87 -0
- package/bundled-skills/vercel-optimize/lib/gates/external-api-slow.mjs +55 -0
- package/bundled-skills/vercel-optimize/lib/gates/hard-gates.mjs +73 -0
- package/bundled-skills/vercel-optimize/lib/gates/index.mjs +45 -0
- package/bundled-skills/vercel-optimize/lib/gates/isr-overrevalidation.mjs +62 -0
- package/bundled-skills/vercel-optimize/lib/gates/middleware-heavy.mjs +51 -0
- package/bundled-skills/vercel-optimize/lib/gates/observability-events-attribution.mjs +56 -0
- package/bundled-skills/vercel-optimize/lib/gates/platform-bot-protection.mjs +115 -0
- package/bundled-skills/vercel-optimize/lib/gates/platform-fluid-compute.mjs +83 -0
- package/bundled-skills/vercel-optimize/lib/gates/region-misconfig.mjs +64 -0
- package/bundled-skills/vercel-optimize/lib/gates/route-errors.mjs +80 -0
- package/bundled-skills/vercel-optimize/lib/gates/scanner-driven.mjs +122 -0
- package/bundled-skills/vercel-optimize/lib/gates/select-candidates.mjs +134 -0
- package/bundled-skills/vercel-optimize/lib/gates/slow-route.mjs +88 -0
- package/bundled-skills/vercel-optimize/lib/gates/types.d.ts +38 -0
- package/bundled-skills/vercel-optimize/lib/gates/uncached-route.mjs +93 -0
- package/bundled-skills/vercel-optimize/lib/gates/usage-spike-triage.mjs +121 -0
- package/bundled-skills/vercel-optimize/lib/grade-recommendation.mjs +155 -0
- package/bundled-skills/vercel-optimize/lib/impact-label.mjs +126 -0
- package/bundled-skills/vercel-optimize/lib/impact-magnitude.mjs +60 -0
- package/bundled-skills/vercel-optimize/lib/investigation-brief.mjs +610 -0
- package/bundled-skills/vercel-optimize/lib/observation-safety.mjs +174 -0
- package/bundled-skills/vercel-optimize/lib/project-facts.mjs +99 -0
- package/bundled-skills/vercel-optimize/lib/queries.mjs +315 -0
- package/bundled-skills/vercel-optimize/lib/reconcile-candidates.mjs +372 -0
- package/bundled-skills/vercel-optimize/lib/render-report.mjs +955 -0
- package/bundled-skills/vercel-optimize/lib/repo-root.mjs +86 -0
- package/bundled-skills/vercel-optimize/lib/route-normalize.mjs +220 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/bot-protection-certainty.mjs +38 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/cache-tag-invalidation-certainty.mjs +30 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/count-correct.mjs +52 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/function-duration-invocations.mjs +38 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/index.mjs +79 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/middleware-conflict.mjs +36 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/missing-citation.mjs +16 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/pre-release.mjs +74 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/rate-limit.mjs +67 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/rendering-mode-mislabel.mjs +38 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/undeclared-dep.mjs +78 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/vercel-directive-strip.mjs +37 -0
- package/bundled-skills/vercel-optimize/lib/sanitizers/window-units.mjs +32 -0
- package/bundled-skills/vercel-optimize/lib/scanners/cache-components-suspense-dedupe.mjs +109 -0
- package/bundled-skills/vercel-optimize/lib/scanners/edge-heavy-import.mjs +94 -0
- package/bundled-skills/vercel-optimize/lib/scanners/force-dynamic.mjs +42 -0
- package/bundled-skills/vercel-optimize/lib/scanners/headers-in-page.mjs +44 -0
- package/bundled-skills/vercel-optimize/lib/scanners/index.mjs +35 -0
- package/bundled-skills/vercel-optimize/lib/scanners/large-static-asset.mjs +92 -0
- package/bundled-skills/vercel-optimize/lib/scanners/max-age-without-s-maxage.mjs +42 -0
- package/bundled-skills/vercel-optimize/lib/scanners/middleware-broad-matcher.mjs +55 -0
- package/bundled-skills/vercel-optimize/lib/scanners/missing-cache-headers.mjs +90 -0
- package/bundled-skills/vercel-optimize/lib/scanners/prisma-include-tree.mjs +42 -0
- package/bundled-skills/vercel-optimize/lib/scanners/region-pin-in-config.mjs +88 -0
- package/bundled-skills/vercel-optimize/lib/scanners/source-maps-production.mjs +36 -0
- package/bundled-skills/vercel-optimize/lib/scanners/sveltekit-prerender-missing.mjs +43 -0
- package/bundled-skills/vercel-optimize/lib/scanners/turbo-force-bypass.mjs +129 -0
- package/bundled-skills/vercel-optimize/lib/scanners/unoptimized-image.mjs +113 -0
- package/bundled-skills/vercel-optimize/lib/scanners/use-cache-date-stamp.mjs +106 -0
- package/bundled-skills/vercel-optimize/lib/support-topics.mjs +355 -0
- package/bundled-skills/vercel-optimize/lib/throttle.mjs +273 -0
- package/bundled-skills/vercel-optimize/lib/util.mjs +17 -0
- package/bundled-skills/vercel-optimize/lib/vercel.mjs +784 -0
- package/bundled-skills/vercel-optimize/lib/verify-claim.mjs +1296 -0
- package/bundled-skills/vercel-optimize/lib/workspace-resolver.mjs +521 -0
- package/bundled-skills/vercel-optimize/references/candidates.md +176 -0
- package/bundled-skills/vercel-optimize/references/data-collection.md +218 -0
- package/bundled-skills/vercel-optimize/references/docs-library.json +683 -0
- package/bundled-skills/vercel-optimize/references/doctrine.md +105 -0
- package/bundled-skills/vercel-optimize/references/observability-plus.md +108 -0
- package/bundled-skills/vercel-optimize/references/playbooks/README.md +53 -0
- package/bundled-skills/vercel-optimize/references/playbooks/ai-application.md +32 -0
- package/bundled-skills/vercel-optimize/references/playbooks/api-service.md +30 -0
- package/bundled-skills/vercel-optimize/references/playbooks/content-site.md +30 -0
- package/bundled-skills/vercel-optimize/references/playbooks/ecommerce.md +30 -0
- package/bundled-skills/vercel-optimize/references/playbooks/marketing.md +30 -0
- package/bundled-skills/vercel-optimize/references/playbooks/saas.md +31 -0
- package/bundled-skills/vercel-optimize/references/playbooks/sveltekit.md +75 -0
- package/bundled-skills/vercel-optimize/references/recommendations.md +203 -0
- package/bundled-skills/vercel-optimize/references/scanner-patterns.md +251 -0
- package/bundled-skills/vercel-optimize/references/scoring.md +205 -0
- package/bundled-skills/vercel-optimize/references/support-topics/README.md +46 -0
- package/bundled-skills/vercel-optimize/references/support-topics/astro-edge-middleware-scope.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/astro-output-mode-and-isr.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/auth-preserving-parallelization.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/bot-protection-product-guardrails.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/build-minutes-monorepo-fanout.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/cache-components-static-shell-boundaries.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/cache-components-suspense-dedupe-pitfall.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/cdn-cache-auth-safety.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/cold-start-initialization-bundle.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/core-web-vitals-client-bottlenecks.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/database-egress-pooling-region.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/dynamic-rendering-traps.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/external-api-critical-path-platform.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/external-api-critical-path.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/fast-data-transfer-payloads.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/fluid-compute-caveats.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/function-duration-io-and-after.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/function-invocation-reduction.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/function-region-misconfiguration-ttfb.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/image-optimization-cost-control.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/isr-revalidation-static-generation.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/middleware-proxy-edge-cost.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/next-fetch-revalidate-floor.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/next-font-cls-self-hosting.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/next-heavy-ui-lazy-load-boundaries.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/next-image-lcp-preload-sizes.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/next-route-handler-get-cache-defaults.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/next-script-third-party-strategy.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/nextjs-version-cache-semantics.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/not-found-catchall-request-waste.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/nuxt-route-rules-cache-isr.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/observability-events-cost-attribution.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/post-response-work-waituntil.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/route-error-durable-offload.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/route-error-runtime-limits.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/runtime-cache-reusable-data.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/sveltekit-isr-prerender-safety.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/sveltekit-split-cold-start-tradeoff.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/usage-spike-triage.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/use-cache-date-stamp-isr-write-amplifier.md +23 -0
- package/bundled-skills/vercel-optimize/references/support-topics/use-cache-remote-shared-origin-data.md +22 -0
- package/bundled-skills/vercel-optimize/references/support-topics/workflow-resumable-stream-routes.md +23 -0
- package/bundled-skills/vercel-optimize/references/verification.md +102 -0
- package/bundled-skills/vercel-optimize/references/voice.md +76 -0
- package/bundled-skills/vercel-optimize/scripts/budget-summary.mjs +56 -0
- package/bundled-skills/vercel-optimize/scripts/build-docs.mjs +74 -0
- package/bundled-skills/vercel-optimize/scripts/check-citations.mjs +81 -0
- package/bundled-skills/vercel-optimize/scripts/check-docs-fresh.mjs +93 -0
- package/bundled-skills/vercel-optimize/scripts/collect-signals.mjs +576 -0
- package/bundled-skills/vercel-optimize/scripts/collect-sub-agent-outputs.mjs +296 -0
- package/bundled-skills/vercel-optimize/scripts/deep-dive.mjs +319 -0
- package/bundled-skills/vercel-optimize/scripts/gate-investigations.mjs +166 -0
- package/bundled-skills/vercel-optimize/scripts/merge-signals.mjs +192 -0
- package/bundled-skills/vercel-optimize/scripts/prepare-investigation-brief.mjs +231 -0
- package/bundled-skills/vercel-optimize/scripts/reconcile-candidates.mjs +62 -0
- package/bundled-skills/vercel-optimize/scripts/render-report.mjs +437 -0
- package/bundled-skills/vercel-optimize/scripts/scan-codebase.mjs +313 -0
- package/bundled-skills/vercel-optimize/scripts/verify-and-regen.mjs +346 -0
- package/bundled-skills/vercel-optimize/scripts/verify-finding.mjs +19 -0
- package/bundled-skills/vercel-react-view-transitions/SKILL.md +327 -0
- package/bundled-skills/vercel-react-view-transitions/references/css-recipes.md +242 -0
- package/bundled-skills/vercel-react-view-transitions/references/implementation.md +182 -0
- package/bundled-skills/vercel-react-view-transitions/references/nextjs.md +176 -0
- package/bundled-skills/vercel-react-view-transitions/references/patterns.md +262 -0
- package/bundled-skills/vibe-code-cleanup/SKILL.md +4 -4
- package/bundled-skills/vibecode-production-qa-validator/SKILL.md +3 -2
- package/package.json +1 -1
- package/skills_index.json +338 -4
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Sortin and Resort Algorithms
|
|
2
|
+
|
|
3
|
+
This document describes how raw thoughts become organized mdbase records.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | Behavior |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `/ustht sortin` | Soft maintenance: append new raw entries into mdbase without restructuring existing content. |
|
|
10
|
+
| `/ustht resort` | Hard maintenance: review all mdbase content, deduplicate, reclassify, merge, and update indexes. |
|
|
11
|
+
| `--dry` | Preview intended changes without writing. |
|
|
12
|
+
|
|
13
|
+
## Raw Format
|
|
14
|
+
|
|
15
|
+
Before processing:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
- [14:30] Make buttons use 8px radius | suggested-dim:ui/details
|
|
19
|
+
- [14:45] Login should use a dark theme | suggested-dim:ui/outline
|
|
20
|
+
- [15:10] Use REST APIs, not GraphQL | suggested-dim:dev-stack
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After processing, the first line of the file becomes:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
<!-- processed -->
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Soft Append Format
|
|
30
|
+
|
|
31
|
+
A raw entry is appended under a date heading in the selected dimension file:
|
|
32
|
+
|
|
33
|
+
```markdown
|
|
34
|
+
## 2026-06-01
|
|
35
|
+
|
|
36
|
+
- Make buttons use 8px radius
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
|
|
41
|
+
- Preserve original wording.
|
|
42
|
+
- Remove only the timestamp and `suggested-dim` suffix.
|
|
43
|
+
- Group entries by raw-file date.
|
|
44
|
+
- Append to an existing date section when present.
|
|
45
|
+
- Create a new date section when needed.
|
|
46
|
+
|
|
47
|
+
## Dimension Management
|
|
48
|
+
|
|
49
|
+
Create a new dimension only when the thought does not fit an existing dimension. Dimension names must be kebab-case path segments and must pass safety validation.
|
|
50
|
+
|
|
51
|
+
When `resort` finds overlapping dimensions, merge them into the clearest target and preserve provenance. When a dimension is no longer useful, mark it with `<!-- deprecated -->` instead of deleting it.
|
|
52
|
+
|
|
53
|
+
## Classification Priority
|
|
54
|
+
|
|
55
|
+
1. User-specified dimension.
|
|
56
|
+
2. Exact existing dimension match.
|
|
57
|
+
3. Closest semantic existing dimension, with a note if the fit is weak.
|
|
58
|
+
4. `general.md` fallback.
|
|
59
|
+
|
|
60
|
+
## Import Algorithm
|
|
61
|
+
|
|
62
|
+
`/ustht import <path>` scans markdown files under a safe project-local path and extracts project-relevant user decisions, constraints, and requirements. It should not modify source files. Imported entries should include source provenance such as `[source:docs/design.md]`.
|
|
63
|
+
|
|
64
|
+
Skip ordinary technical docs, generated docs, API reference text, and code comments unless they clearly encode a user decision.
|
|
65
|
+
|
|
66
|
+
## Summary Output
|
|
67
|
+
|
|
68
|
+
After `sortin`, report the number of processed entries and destination dimensions, for example:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
Soft maintenance complete. Processed 3 thoughts:
|
|
72
|
+
-> ui/details.md: +1
|
|
73
|
+
-> ui/outline.md: +1
|
|
74
|
+
-> dev-stack.md: +1
|
|
75
|
+
LAST_SORTIN updated to 2026-06-01 15:30
|
|
76
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared helpers for user-thoughts scripts."""
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_ustht() -> Path | None:
|
|
7
|
+
"""Find .ustht/ in the current directory or one of its parents."""
|
|
8
|
+
cwd = Path.cwd()
|
|
9
|
+
for d in [cwd, *cwd.parents]:
|
|
10
|
+
ustht = d / ".ustht"
|
|
11
|
+
if ustht.is_dir():
|
|
12
|
+
return ustht
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def find_skill_dir() -> Path | None:
|
|
17
|
+
"""Find the installed user-thoughts skill directory."""
|
|
18
|
+
script_dir = Path(__file__).resolve().parent
|
|
19
|
+
skill_dir = script_dir.parent
|
|
20
|
+
if (skill_dir / "SKILL.md").exists():
|
|
21
|
+
return skill_dir
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_define_ini(ustht: Path) -> dict:
|
|
26
|
+
"""Read define.ini and return key/value pairs."""
|
|
27
|
+
ini = ustht / "define.ini"
|
|
28
|
+
if not ini.exists():
|
|
29
|
+
return {}
|
|
30
|
+
result = {}
|
|
31
|
+
for line in ini.read_text(encoding="utf-8").splitlines():
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if "=" in line and not line.startswith("#"):
|
|
34
|
+
k, v = line.split("=", 1)
|
|
35
|
+
result[k.strip()] = v.strip()
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def write_define_ini(ustht: Path, cfg: dict):
|
|
40
|
+
"""Replace define.ini with the provided key/value pairs."""
|
|
41
|
+
ini = ustht / "define.ini"
|
|
42
|
+
lines = [f"{k}={v}" for k, v in cfg.items()]
|
|
43
|
+
ini.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_processed(filepath: Path) -> bool:
|
|
47
|
+
"""Return true when the first raw-file line is the processed marker."""
|
|
48
|
+
first_line = filepath.read_text(encoding="utf-8").split("\n", 1)[0].strip()
|
|
49
|
+
return first_line == "<!-- processed -->"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_dim_name(dim: str) -> bool:
|
|
53
|
+
"""Validate a dimension path made of safe kebab-case segments."""
|
|
54
|
+
reserved = {"raw", "ignored", "export", "define", "readme-ai"}
|
|
55
|
+
if not dim or len(dim) > 64 or ".." in dim or "\\" in dim or " " in dim:
|
|
56
|
+
return False
|
|
57
|
+
for part in dim.split("/"):
|
|
58
|
+
if part in reserved:
|
|
59
|
+
return False
|
|
60
|
+
if not part or not re.match(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", part):
|
|
61
|
+
return False
|
|
62
|
+
return True
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Manage ignored user-thought entries."""
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from common import find_ustht
|
|
7
|
+
|
|
8
|
+
HELP = """Usage: python ignore_ops.py show|remove_last|add_suffix "text" [--help]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
show List entries under #ignored/
|
|
12
|
+
remove_last Remove the latest raw entry and move it to #ignored/
|
|
13
|
+
add_suffix "text" Add a suffix-ignored entry to #ignored/
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_last_raw_entry(raw_dir: Path):
|
|
18
|
+
"""Return (file path, line index, entry text) for the latest raw entry."""
|
|
19
|
+
files = sorted(raw_dir.glob("*.md"), reverse=True)
|
|
20
|
+
for f in files:
|
|
21
|
+
lines = f.read_text(encoding="utf-8").splitlines()
|
|
22
|
+
if lines and lines[0].strip() == "<!-- processed -->":
|
|
23
|
+
continue
|
|
24
|
+
for idx in range(len(lines) - 1, -1, -1):
|
|
25
|
+
if lines[idx].strip().startswith("- ["):
|
|
26
|
+
return f, idx, lines[idx]
|
|
27
|
+
return None, None, None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def remove_line(filepath: Path, idx: int):
|
|
31
|
+
"""Remove one line from a file."""
|
|
32
|
+
lines = filepath.read_text(encoding="utf-8").splitlines()
|
|
33
|
+
del lines[idx]
|
|
34
|
+
filepath.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def append_to_ignored(ignored_dir: Path, text: str, reason: str):
|
|
38
|
+
"""Append one ignored entry to today's ignored file."""
|
|
39
|
+
ignored_dir.mkdir(exist_ok=True)
|
|
40
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
41
|
+
now = datetime.now().strftime("%H:%M")
|
|
42
|
+
f = ignored_dir / f"{today}.md"
|
|
43
|
+
clean = text.strip()
|
|
44
|
+
if " | suggested-dim:" in clean:
|
|
45
|
+
clean = clean.rsplit(" | suggested-dim:", 1)[0]
|
|
46
|
+
entry = f"- [{now}] {clean} ({reason})"
|
|
47
|
+
if f.exists():
|
|
48
|
+
content = f.read_text(encoding="utf-8").rstrip()
|
|
49
|
+
f.write_text(f"{content}\n{entry}\n", encoding="utf-8")
|
|
50
|
+
else:
|
|
51
|
+
f.write_text(f"{entry}\n", encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def show_ignored(ignored_dir: Path):
|
|
55
|
+
"""Print all ignored entries."""
|
|
56
|
+
if not ignored_dir.exists():
|
|
57
|
+
print("No ignored entries.")
|
|
58
|
+
return
|
|
59
|
+
files = sorted(ignored_dir.glob("*.md"), reverse=True)
|
|
60
|
+
if not files:
|
|
61
|
+
print("No ignored entries.")
|
|
62
|
+
return
|
|
63
|
+
for f in files:
|
|
64
|
+
entries = [line for line in f.read_text(encoding="utf-8").splitlines() if line.strip().startswith("- [")]
|
|
65
|
+
if entries:
|
|
66
|
+
print(f"#{f.name} ({len(entries)} entries):")
|
|
67
|
+
for entry in entries:
|
|
68
|
+
print(entry)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def remove_last(ustht: Path):
|
|
72
|
+
raw_dir = ustht / "raw"
|
|
73
|
+
if not raw_dir.exists():
|
|
74
|
+
print("No previous thought to ignore.")
|
|
75
|
+
return
|
|
76
|
+
filepath, idx, entry = find_last_raw_entry(raw_dir)
|
|
77
|
+
if filepath is None:
|
|
78
|
+
print("No previous thought to ignore.")
|
|
79
|
+
return
|
|
80
|
+
remove_line(filepath, idx)
|
|
81
|
+
append_to_ignored(ustht / "ignored", entry, "ignored with --last")
|
|
82
|
+
display = entry
|
|
83
|
+
if "] " in display:
|
|
84
|
+
display = display.split("] ", 1)[1]
|
|
85
|
+
if " | suggested-dim:" in display:
|
|
86
|
+
display = display.rsplit(" | suggested-dim:", 1)[0]
|
|
87
|
+
print(f"Ignored previous thought: {display}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def add_suffix(ustht: Path, text: str):
|
|
91
|
+
append_to_ignored(ustht / "ignored", text, "ignored by suffix")
|
|
92
|
+
print("Ignored current message.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
97
|
+
print(HELP)
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
ustht = find_ustht()
|
|
101
|
+
if ustht is None:
|
|
102
|
+
print("Error: .ustht/ was not found. Run /ustht init first.")
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
if len(sys.argv) < 2:
|
|
106
|
+
print(f"Usage: {sys.argv[0]} show|remove_last|add_suffix \"text\"")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
cmd = sys.argv[1]
|
|
110
|
+
if cmd == "show":
|
|
111
|
+
show_ignored(ustht / "ignored")
|
|
112
|
+
elif cmd == "remove_last":
|
|
113
|
+
remove_last(ustht)
|
|
114
|
+
elif cmd == "add_suffix":
|
|
115
|
+
if len(sys.argv) < 3:
|
|
116
|
+
print("Error: add_suffix requires text.")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
add_suffix(ustht, sys.argv[2])
|
|
119
|
+
else:
|
|
120
|
+
print(f"Unknown command: {cmd}. Available: show, remove_last, add_suffix")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Initialize the .ustht/ runtime directory from templates."""
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from common import find_skill_dir
|
|
7
|
+
|
|
8
|
+
HELP = """Usage: python init.py [--help]
|
|
9
|
+
|
|
10
|
+
Create .ustht/ in the current working directory, copy the runtime templates,
|
|
11
|
+
and create raw/, ignored/, and export/ directories. Existing .ustht/ content is
|
|
12
|
+
not overwritten.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def copy_template(src: Path, dst: Path):
|
|
17
|
+
"""Copy template files while skipping symlinks."""
|
|
18
|
+
for item in src.rglob("*"):
|
|
19
|
+
rel = item.relative_to(src)
|
|
20
|
+
target = dst / rel
|
|
21
|
+
if item.is_symlink():
|
|
22
|
+
continue
|
|
23
|
+
if item.is_dir():
|
|
24
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
else:
|
|
26
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
shutil.copy2(item, target)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
32
|
+
print(HELP)
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
target = Path.cwd() / ".ustht"
|
|
36
|
+
if target.exists():
|
|
37
|
+
print("Already initialized; .ustht/ exists, skipping creation.")
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
|
|
40
|
+
skill_dir = find_skill_dir()
|
|
41
|
+
if skill_dir is None:
|
|
42
|
+
print("Error: SKILL.md was not found. Ensure this script is inside user-thoughts/scripts/.")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
template = skill_dir / "assets" / "Runtime-Template"
|
|
46
|
+
if not template.exists():
|
|
47
|
+
print(f"Error: template directory does not exist: {template}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
target.mkdir()
|
|
51
|
+
copy_template(template, target)
|
|
52
|
+
for name in ["raw", "ignored", "export"]:
|
|
53
|
+
(target / name).mkdir(exist_ok=True)
|
|
54
|
+
|
|
55
|
+
define = target / "define.ini"
|
|
56
|
+
if not define.exists():
|
|
57
|
+
define.write_text("SKILL_STATUS=on\nINSTANT_STATUS=off\nLAST_SORTIN=\n", encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
print("Initialized .ustht/.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Show the mdbase index or dimension content."""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from common import find_ustht, validate_dim_name
|
|
6
|
+
|
|
7
|
+
HELP = """Usage: python show_mdbase.py show [--all|--dimension] [--help]
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
show Show README.ai.md index
|
|
11
|
+
show --all List all dimensions and entry counts
|
|
12
|
+
show <dimension> Show one dimension file
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def show_index(mdbase: Path):
|
|
17
|
+
index = mdbase / "README.ai.md"
|
|
18
|
+
if not index.exists():
|
|
19
|
+
print("mdbase/README.ai.md does not exist.")
|
|
20
|
+
return
|
|
21
|
+
print(index.read_text(encoding="utf-8"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def list_dims(mdbase: Path):
|
|
25
|
+
details = mdbase / "details"
|
|
26
|
+
if not details.exists():
|
|
27
|
+
return []
|
|
28
|
+
return sorted(p.relative_to(details).with_suffix("").as_posix() for p in details.rglob("*.md"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def show_dim(mdbase: Path, dim: str):
|
|
32
|
+
if not validate_dim_name(dim):
|
|
33
|
+
print(f"Invalid dimension name: {dim}. Use lowercase letters, digits, hyphens, and optional / subdirectories.")
|
|
34
|
+
return
|
|
35
|
+
if dim == "backlog":
|
|
36
|
+
path = mdbase / "backlog.md"
|
|
37
|
+
else:
|
|
38
|
+
path = mdbase / "details" / f"{dim}.md"
|
|
39
|
+
if not path.exists():
|
|
40
|
+
print(f"mdbase/details/{dim}.md does not exist yet.")
|
|
41
|
+
return
|
|
42
|
+
print(path.read_text(encoding="utf-8"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def show_all(mdbase: Path):
|
|
46
|
+
details = mdbase / "details"
|
|
47
|
+
if not details.exists():
|
|
48
|
+
print("mdbase/details/ does not exist.")
|
|
49
|
+
return
|
|
50
|
+
dims = list_dims(mdbase)
|
|
51
|
+
if not dims:
|
|
52
|
+
print("mdbase has no dimension files.")
|
|
53
|
+
return
|
|
54
|
+
print(f"mdbase has {len(dims)} dimensions:")
|
|
55
|
+
for dim in dims:
|
|
56
|
+
path = details / f"{dim}.md"
|
|
57
|
+
lines = [line for line in path.read_text(encoding="utf-8").splitlines() if line.strip().startswith("- ")]
|
|
58
|
+
print(f" {dim}.md: {len(lines)} entries")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
63
|
+
print(HELP)
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
ustht = find_ustht()
|
|
67
|
+
if ustht is None:
|
|
68
|
+
print("Error: .ustht/ was not found. Run /ustht init first.")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
mdbase = ustht / "mdbase"
|
|
72
|
+
if not mdbase.exists():
|
|
73
|
+
print("mdbase is not initialized. Run /ustht init first.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
args = sys.argv[1:]
|
|
77
|
+
if not args or args[0] != "show":
|
|
78
|
+
print(f"Usage: {sys.argv[0]} show [--all|--dimension]")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
rest = args[1:]
|
|
82
|
+
if not rest:
|
|
83
|
+
show_index(mdbase)
|
|
84
|
+
elif rest[0] == "--all":
|
|
85
|
+
show_all(mdbase)
|
|
86
|
+
elif rest[0].startswith("--"):
|
|
87
|
+
show_dim(mdbase, rest[0][2:])
|
|
88
|
+
else:
|
|
89
|
+
show_dim(mdbase, rest[0])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Show unprocessed raw files."""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from common import find_ustht, is_processed
|
|
6
|
+
|
|
7
|
+
HELP = """Usage: python show_raw.py [--help]
|
|
8
|
+
|
|
9
|
+
Show unprocessed #raw/ files, including filenames, entry counts, and content.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
15
|
+
print(HELP)
|
|
16
|
+
sys.exit(0)
|
|
17
|
+
|
|
18
|
+
ustht = find_ustht()
|
|
19
|
+
if ustht is None:
|
|
20
|
+
print("Error: .ustht/ was not found. Run /ustht init first.")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
raw_dir = ustht / "raw"
|
|
24
|
+
if not raw_dir.exists():
|
|
25
|
+
print("No unprocessed records.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
files = [f for f in sorted(raw_dir.glob("*.md"), reverse=True) if not is_processed(f)]
|
|
29
|
+
if not files:
|
|
30
|
+
print("No unprocessed records. All raw files are marked processed.")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
for f in files:
|
|
34
|
+
content = f.read_text(encoding="utf-8").strip()
|
|
35
|
+
entry_count = sum(1 for line in content.splitlines() if line.strip().startswith("- ["))
|
|
36
|
+
print(f"#{f.name} ({entry_count} unprocessed entries):")
|
|
37
|
+
print(content)
|
|
38
|
+
print()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Soft-maintain raw user-thought entries into mdbase."""
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from common import find_ustht, read_define_ini, write_define_ini, is_processed, validate_dim_name
|
|
9
|
+
|
|
10
|
+
HELP = """Usage: python sortin.py [--dry] [--help]
|
|
11
|
+
|
|
12
|
+
Soft maintenance: parse unprocessed #raw/*.md files, append entries to matching
|
|
13
|
+
mdbase dimensions, mark raw files as processed, and update LAST_SORTIN.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--dry Preview changes without writing
|
|
17
|
+
--help Show this help text
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_raw_file(filepath: Path):
|
|
22
|
+
"""Parse raw entries from one file."""
|
|
23
|
+
entries = []
|
|
24
|
+
date = filepath.stem.split("-", 3)
|
|
25
|
+
if len(date) >= 3:
|
|
26
|
+
date = "-".join(date[:3])
|
|
27
|
+
else:
|
|
28
|
+
date = datetime.now().strftime("%Y-%m-%d")
|
|
29
|
+
|
|
30
|
+
for line in filepath.read_text(encoding="utf-8").splitlines():
|
|
31
|
+
line = line.strip()
|
|
32
|
+
match = re.match(r"^- \[(\d{2}:\d{2})\] (.*)$", line)
|
|
33
|
+
if not match:
|
|
34
|
+
continue
|
|
35
|
+
time, content = match.groups()
|
|
36
|
+
dim = "general"
|
|
37
|
+
text = content
|
|
38
|
+
if " | suggested-dim:" in content:
|
|
39
|
+
text, dim = content.rsplit(" | suggested-dim:", 1)
|
|
40
|
+
dim = dim.strip()
|
|
41
|
+
if not validate_dim_name(dim):
|
|
42
|
+
dim = "general"
|
|
43
|
+
entries.append({"time": time, "text": text.strip(), "dimension": dim, "date": date})
|
|
44
|
+
return entries
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def dim_path(mdbase: Path, dim: str) -> Path:
|
|
48
|
+
"""Return the target file path for a dimension."""
|
|
49
|
+
if dim == "backlog":
|
|
50
|
+
return mdbase / "backlog.md"
|
|
51
|
+
return mdbase / "details" / f"{dim}.md"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def count_entries(path: Path) -> int:
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return 0
|
|
57
|
+
return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if line.strip().startswith("- "))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def append_entries(path: Path, entries):
|
|
61
|
+
"""Append entries grouped by date to one dimension file."""
|
|
62
|
+
by_date = defaultdict(list)
|
|
63
|
+
for entry in entries:
|
|
64
|
+
by_date[entry["date"]].append(entry)
|
|
65
|
+
|
|
66
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
if not path.exists():
|
|
68
|
+
title = path.stem.replace("-", " ").title()
|
|
69
|
+
path.write_text(f"# {title}\n\n> Project memory for `{path.stem}`.\n\n", encoding="utf-8")
|
|
70
|
+
|
|
71
|
+
content = path.read_text(encoding="utf-8").rstrip()
|
|
72
|
+
for date, date_entries in sorted(by_date.items()):
|
|
73
|
+
lines = [f"- {entry['text']}" for entry in date_entries]
|
|
74
|
+
block = "\n".join(lines)
|
|
75
|
+
heading = f"## {date}"
|
|
76
|
+
if heading in content:
|
|
77
|
+
content_lines = content.splitlines()
|
|
78
|
+
heading_idx = next(i for i, line in enumerate(content_lines) if line.strip() == heading)
|
|
79
|
+
insert_idx = len(content_lines)
|
|
80
|
+
for i in range(heading_idx + 1, len(content_lines)):
|
|
81
|
+
if content_lines[i].startswith("## "):
|
|
82
|
+
insert_idx = i
|
|
83
|
+
break
|
|
84
|
+
before = content_lines[:insert_idx]
|
|
85
|
+
after = content_lines[insert_idx:]
|
|
86
|
+
if before and before[-1].strip():
|
|
87
|
+
before.append("")
|
|
88
|
+
before.extend(lines)
|
|
89
|
+
if after:
|
|
90
|
+
before.append("")
|
|
91
|
+
before.extend(after)
|
|
92
|
+
content = "\n".join(before).rstrip()
|
|
93
|
+
else:
|
|
94
|
+
content = f"{content}\n\n{heading}\n\n{block}".rstrip()
|
|
95
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mark_processed(filepath: Path):
|
|
99
|
+
"""Insert the processed marker at the top of a raw file."""
|
|
100
|
+
content = filepath.read_text(encoding="utf-8")
|
|
101
|
+
if content.split("\n", 1)[0].strip() != "<!-- processed -->":
|
|
102
|
+
filepath.write_text("<!-- processed -->\n" + content, encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def update_index(mdbase: Path):
|
|
106
|
+
"""Rebuild mdbase/README.ai.md with dimension counts."""
|
|
107
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
108
|
+
details = mdbase / "details"
|
|
109
|
+
dims = []
|
|
110
|
+
if details.exists():
|
|
111
|
+
dims = sorted(p.relative_to(details).with_suffix("").as_posix() for p in details.rglob("*.md"))
|
|
112
|
+
|
|
113
|
+
rows = ["| File | Dimension | Entries |", "|------|-----------|---------|"]
|
|
114
|
+
backlog = mdbase / "backlog.md"
|
|
115
|
+
if backlog.exists():
|
|
116
|
+
rows.append(f"| [backlog.md](backlog.md) | backlog | {count_entries(backlog)} |")
|
|
117
|
+
for dim in dims:
|
|
118
|
+
path = details / f"{dim}.md"
|
|
119
|
+
rows.append(f"| [details/{dim}.md](details/{dim}.md) | {dim} | {count_entries(path)} |")
|
|
120
|
+
|
|
121
|
+
content = "\n".join([
|
|
122
|
+
"# user-thoughts mdbase Index",
|
|
123
|
+
"",
|
|
124
|
+
"This directory stores user-provided project decisions, constraints, preferences, and plans.",
|
|
125
|
+
"",
|
|
126
|
+
f"Last updated: {now}",
|
|
127
|
+
"",
|
|
128
|
+
"## Maintenance Rules",
|
|
129
|
+
"",
|
|
130
|
+
"- Preserve user wording and constraints.",
|
|
131
|
+
"- Append entries by date under `## yyyy-mm-dd` headings.",
|
|
132
|
+
"- Prefer existing dimensions before creating new ones.",
|
|
133
|
+
"- Mark deprecated content instead of silently deleting history.",
|
|
134
|
+
"",
|
|
135
|
+
"## Document Index",
|
|
136
|
+
"",
|
|
137
|
+
*rows,
|
|
138
|
+
"",
|
|
139
|
+
])
|
|
140
|
+
(mdbase / "README.ai.md").write_text(content, encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
145
|
+
print(HELP)
|
|
146
|
+
sys.exit(0)
|
|
147
|
+
|
|
148
|
+
dry = "--dry" in sys.argv
|
|
149
|
+
ustht = find_ustht()
|
|
150
|
+
if ustht is None:
|
|
151
|
+
print("Error: .ustht/ was not found. Run /ustht init first.")
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
cfg = read_define_ini(ustht)
|
|
155
|
+
if cfg.get("SKILL_STATUS") == "off":
|
|
156
|
+
print("SKILL is off; write ignored. Run /ustht skill on to enable it.")
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
raw_dir = ustht / "raw"
|
|
160
|
+
if not raw_dir.exists():
|
|
161
|
+
print("No unprocessed records.")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
raw_files = [f for f in sorted(raw_dir.glob("*.md")) if not is_processed(f)]
|
|
165
|
+
if not raw_files:
|
|
166
|
+
print("No unprocessed records. All raw files are marked processed.")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
all_entries = []
|
|
170
|
+
entries_by_file = {}
|
|
171
|
+
for f in raw_files:
|
|
172
|
+
entries = parse_raw_file(f)
|
|
173
|
+
entries_by_file[f] = entries
|
|
174
|
+
all_entries.extend(entries)
|
|
175
|
+
|
|
176
|
+
if not all_entries:
|
|
177
|
+
print("No valid entries found in raw files.")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
grouped = defaultdict(list)
|
|
181
|
+
for entry in all_entries:
|
|
182
|
+
grouped[entry["dimension"]].append(entry)
|
|
183
|
+
|
|
184
|
+
print("Preview mode:" if dry else f"Soft maintenance complete. Processed {len(all_entries)} thoughts:")
|
|
185
|
+
mdbase = ustht / "mdbase"
|
|
186
|
+
for dim, entries in sorted(grouped.items()):
|
|
187
|
+
target = dim_path(mdbase, dim)
|
|
188
|
+
label = f"{dim}.md" if target.exists() else f"{dim}.md [new dimension]"
|
|
189
|
+
sample = entries[0]["text"][:60]
|
|
190
|
+
print(f" -> {label}: +{len(entries)} ({sample})")
|
|
191
|
+
|
|
192
|
+
if dry:
|
|
193
|
+
print(f" {len(all_entries)} total entries; no files were changed.")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
for dim, entries in grouped.items():
|
|
197
|
+
append_entries(dim_path(mdbase, dim), entries)
|
|
198
|
+
|
|
199
|
+
for f in raw_files:
|
|
200
|
+
if entries_by_file.get(f):
|
|
201
|
+
mark_processed(f)
|
|
202
|
+
|
|
203
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
204
|
+
cfg["LAST_SORTIN"] = now
|
|
205
|
+
write_define_ini(ustht, cfg)
|
|
206
|
+
update_index(mdbase)
|
|
207
|
+
print(f" LAST_SORTIN updated to {now}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
main()
|