@erclx/canon 4.0.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/LICENSE +21 -0
- package/README.md +129 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
- package/claude/skills/bash-cli-script/SKILL.md +48 -0
- package/claude/skills/bash-cli-script/references/template.md +43 -0
- package/claude/skills/bash-script/REQUIREMENT.md +36 -0
- package/claude/skills/bash-script/SKILL.md +100 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
- package/claude/skills/canon-cli/SKILL.md +103 -0
- package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-file/SKILL.md +80 -0
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
- package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
- package/claude/skills/canon-operator/SKILL.md +108 -0
- package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
- package/claude/skills/canon-rollout/SKILL.md +147 -0
- package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/canon-screencast/SKILL.md +167 -0
- package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/canon-slides-draft/SKILL.md +62 -0
- package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
- package/claude/skills/ci-workflow/SKILL.md +65 -0
- package/claude/skills/ci-workflow/references/workflows.md +98 -0
- package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
- package/claude/skills/claude-address-review/SKILL.md +212 -0
- package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
- package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
- package/claude/skills/claude-autoship/SKILL.md +207 -0
- package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
- package/claude/skills/claude-design-extract/SKILL.md +102 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-diagram/SKILL.md +177 -0
- package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
- package/claude/skills/claude-docs/SKILL.md +287 -0
- package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
- package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
- package/claude/skills/claude-feature/SKILL.md +115 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
- package/claude/skills/claude-groundwork/SKILL.md +142 -0
- package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
- package/claude/skills/claude-intake/SKILL.md +114 -0
- package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
- package/claude/skills/claude-intake-answer/SKILL.md +90 -0
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
- package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
- package/claude/skills/claude-markdown-propose/references/format.md +107 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-capture/SKILL.md +101 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-review/SKILL.md +210 -0
- package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
- package/claude/skills/claude-orchestrate/SKILL.md +241 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
- package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
- package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
- package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
- package/claude/skills/claude-pr-review/SKILL.md +295 -0
- package/claude/skills/claude-review/REQUIREMENT.md +39 -0
- package/claude/skills/claude-review/SKILL.md +142 -0
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
- package/claude/skills/claude-seed-sync/SKILL.md +156 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +99 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-tasks/SKILL.md +161 -0
- package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
- package/claude/skills/claude-teach/SKILL.md +196 -0
- package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
- package/claude/skills/claude-teach/references/pedagogy.md +67 -0
- package/claude/skills/claude-teach/references/promotion.md +54 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ui-test/SKILL.md +77 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/SKILL.md +79 -0
- package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
- package/claude/skills/claude-ux-measure/SKILL.md +122 -0
- package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
- package/claude/skills/claude-worker/SKILL.md +96 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
- package/claude/skills/claude-worktree/SKILL.md +136 -0
- package/claude/skills/create-rule/REQUIREMENT.md +45 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +38 -0
- package/claude/skills/create-skill/SKILL.md +32 -0
- package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
- package/claude/skills/create-snippet/SKILL.md +30 -0
- package/claude/skills/create-standard/REQUIREMENT.md +35 -0
- package/claude/skills/create-standard/SKILL.md +29 -0
- package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
- package/claude/skills/decision-escalate/SKILL.md +79 -0
- package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
- package/claude/skills/docs-sync/SKILL.md +95 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-commit/REQUIREMENT.md +36 -0
- package/claude/skills/git-commit/SKILL.md +49 -0
- package/claude/skills/git-followup/REQUIREMENT.md +43 -0
- package/claude/skills/git-followup/SKILL.md +48 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +65 -0
- package/claude/skills/git-pr/REQUIREMENT.md +50 -0
- package/claude/skills/git-pr/SKILL.md +164 -0
- package/claude/skills/git-pr/references/labels.md +95 -0
- package/claude/skills/git-ship/REQUIREMENT.md +42 -0
- package/claude/skills/git-ship/SKILL.md +55 -0
- package/claude/skills/git-split/REQUIREMENT.md +39 -0
- package/claude/skills/git-split/SKILL.md +162 -0
- package/claude/skills/git-stage/REQUIREMENT.md +39 -0
- package/claude/skills/git-stage/SKILL.md +73 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
- package/claude/skills/git-worktree/SKILL.md +130 -0
- package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
- package/claude/skills/migration-claude-md/SKILL.md +76 -0
- package/claude/skills/migration-context/REQUIREMENT.md +36 -0
- package/claude/skills/migration-context/SKILL.md +95 -0
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
- package/claude/skills/migration-standards-drop/SKILL.md +113 -0
- package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
- package/claude/skills/migration-superseded/SKILL.md +115 -0
- package/claude/skills/project-commands/REQUIREMENT.md +42 -0
- package/claude/skills/project-commands/SKILL.md +85 -0
- package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
- package/claude/skills/restate-plainly/SKILL.md +39 -0
- package/claude/skills/session-map/REQUIREMENT.md +57 -0
- package/claude/skills/session-map/SKILL.md +70 -0
- package/claude/skills/session-resume/REQUIREMENT.md +49 -0
- package/claude/skills/session-resume/SKILL.md +51 -0
- package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
- package/claude/skills/setup-gov/SKILL.md +77 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
- package/claude/skills/setup-indexes/SKILL.md +153 -0
- package/claude/skills/setup-init/REQUIREMENT.md +45 -0
- package/claude/skills/setup-init/SKILL.md +127 -0
- package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
- package/claude/skills/setup-plugins/SKILL.md +81 -0
- package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
- package/claude/skills/setup-verify/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/write-human/REQUIREMENT.md +46 -0
- package/claude/skills/write-human/SKILL.md +68 -0
- package/claude/skills/write-human/references/density.md +38 -0
- package/claude/skills/write-human/references/machine-tells.md +107 -0
- package/claude/skills/write-human/references/source-material.md +37 -0
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents/audits.md +98 -0
- package/docs/agents/capture.md +37 -0
- package/docs/agents/census.md +23 -0
- package/docs/agents/commands.md +146 -0
- package/docs/agents/comments.md +34 -0
- package/docs/agents/context-audit-checks.md +120 -0
- package/docs/agents/context-audit.md +83 -0
- package/docs/agents/counts.md +76 -0
- package/docs/agents/demo.md +86 -0
- package/docs/agents/docs.md +17 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +47 -0
- package/docs/agents/indexes.md +35 -0
- package/docs/agents/install-and-sync.md +385 -0
- package/docs/agents/intake.md +81 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/label-coverage.md +73 -0
- package/docs/agents/markdown-audit.md +197 -0
- package/docs/agents/output-shape.md +70 -0
- package/docs/agents/overview.md +26 -0
- package/docs/agents/records.md +170 -0
- package/docs/agents/restated.md +81 -0
- package/docs/agents/review-classification.md +77 -0
- package/docs/agents/routing.md +61 -0
- package/docs/agents/rule-citations.md +98 -0
- package/docs/agents/sandbox.md +71 -0
- package/docs/agents/scripting.md +149 -0
- package/docs/agents/sessions.md +120 -0
- package/docs/agents/skills-audit.md +94 -0
- package/docs/agents/skills-reach.md +64 -0
- package/docs/agents/standards-audit.md +38 -0
- package/docs/agents/state-scoped-risk.md +105 -0
- package/docs/agents/superseded.md +85 -0
- package/docs/agents/targets.md +83 -0
- package/docs/agents/tasks.md +200 -0
- package/docs/agents/teach.md +158 -0
- package/docs/agents/test-order.md +56 -0
- package/docs/agents/worktrees.md +62 -0
- package/docs/ai-workflow.md +317 -0
- package/docs/index.md +23 -0
- package/docs/operating-model.md +223 -0
- package/docs/target-projects.md +258 -0
- package/docs/visual-design-workflow.md +151 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/ci/700-ci-workflow.md +44 -0
- package/governance/rules/claude/500-prose.md +15 -0
- package/governance/rules/claude/501-markdown.md +14 -0
- package/governance/rules/claude/510-context.md +28 -0
- package/governance/rules/claude/511-indexes.md +14 -0
- package/governance/rules/claude/520-wireframes.md +12 -0
- package/governance/rules/claude/530-requirements.md +11 -0
- package/governance/rules/claude/540-architecture.md +11 -0
- package/governance/rules/claude/550-design.md +11 -0
- package/governance/rules/claude/555-tasks.md +12 -0
- package/governance/rules/claude/556-groundwork.md +11 -0
- package/governance/rules/claude/557-intake.md +11 -0
- package/governance/rules/claude/558-plan.md +22 -0
- package/governance/rules/claude/559-memory.md +11 -0
- package/governance/rules/claude/560-diagrams.md +18 -0
- package/governance/rules/claude/561-teach.md +13 -0
- package/governance/rules/claude/562-session.md +15 -0
- package/governance/rules/claude/570-skill.md +24 -0
- package/governance/rules/claude/575-hooks.md +17 -0
- package/governance/rules/claude/576-settings.md +14 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/claude/591-standard-authoring.md +12 -0
- package/governance/rules/claude/592-claude-md.md +19 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/005-behavior.md +27 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/015-output.md +20 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/025-indexes.md +9 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/035-tasks.md +13 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/045-memory.md +12 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/055-scratch.md +9 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/065-spelling.md +19 -0
- package/governance/rules/core/070-planning.md +18 -0
- package/governance/rules/core/075-dependencies.md +25 -0
- package/governance/rules/core/080-config-comments.md +22 -0
- package/governance/rules/core/085-worktrees.md +17 -0
- package/governance/rules/core/087-git.md +11 -0
- package/governance/rules/core/090-code-comments.md +39 -0
- package/governance/rules/framework/200-react.md +51 -0
- package/governance/rules/framework/210-astro.md +41 -0
- package/governance/rules/framework/220-fastapi.md +43 -0
- package/governance/rules/framework/230-nextjs.md +48 -0
- package/governance/rules/framework/250-tailwind.md +32 -0
- package/governance/rules/framework/260-shadcn.md +34 -0
- package/governance/rules/lang/100-typescript.md +40 -0
- package/governance/rules/lang/110-python.md +42 -0
- package/governance/rules/lang/120-bash.md +19 -0
- package/governance/rules/lib/300-testing-ts.md +39 -0
- package/governance/rules/lib/305-e2e-reliability.md +34 -0
- package/governance/rules/lib/306-test-scope.md +25 -0
- package/governance/rules/lib/310-zod.md +25 -0
- package/governance/rules/lib/320-tanstack-query.md +32 -0
- package/governance/rules/lib/330-testing-py.md +44 -0
- package/governance/rules/lib/340-pydantic.md +38 -0
- package/governance/rules/lib/350-security-web.md +32 -0
- package/governance/rules/lib/360-security-server.md +39 -0
- package/governance/rules/lib/370-database.md +35 -0
- package/governance/rules/snippets/505-at-references.md +9 -0
- package/governance/rules/ui/400-ui.md +36 -0
- package/governance/rules/ui/410-a11y.md +48 -0
- package/governance/rules/ui/420-forms.md +36 -0
- package/governance/rules/ui/430-ux-completeness.md +65 -0
- package/governance/rules/ui/440-surface-capture.md +34 -0
- package/governance/rules/ui/450-link-behavior.md +19 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.toml +8 -0
- package/governance/stacks/node-server.toml +2 -0
- package/governance/stacks/node.toml +2 -0
- package/governance/stacks/python-fastapi.toml +2 -0
- package/governance/stacks/python.toml +2 -0
- package/governance/stacks/react.toml +2 -0
- package/package.json +69 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-color-source.sh +41 -0
- package/scripts/core/check-ignore-parity.sh +162 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-seed-independence.sh +59 -0
- package/scripts/core/check-skill-paths.sh +24 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +101 -0
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/regen-claude-copies.sh +10 -0
- package/scripts/core/regen-hero.sh +217 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-tooling-paths.sh +61 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/snapshot.sh +134 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/docs/list.sh +165 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +191 -0
- package/scripts/lib/sandbox-git.sh +125 -0
- package/scripts/lib/sandbox-path.sh +206 -0
- package/scripts/lib/tooling.sh +35 -0
- package/scripts/lib/ui.sh +266 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +466 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +115 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/align.md +12 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +24 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/decision-help.md +6 -0
- package/snippets/meta-prompt.md +14 -0
- package/snippets/research-prompt.md +7 -0
- package/snippets/session-notes.md +11 -0
- package/snippets/snippets.toml +5 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/audits/baseline.ts +201 -0
- package/src/audits/catalog.ts +876 -0
- package/src/audits/run.ts +204 -0
- package/src/autoship/classify.ts +75 -0
- package/src/autoship/paths.ts +51 -0
- package/src/binary.ts +16 -0
- package/src/browser/engine.ts +40 -0
- package/src/census/count.ts +113 -0
- package/src/claude/cases/all.ts +24 -0
- package/src/claude/cases/authoring.ts +53 -0
- package/src/claude/cases/claude-workflow.ts +158 -0
- package/src/claude/cases/git.ts +44 -0
- package/src/claude/cases/misc.ts +27 -0
- package/src/claude/cases/setup.ts +94 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/routing.ts +283 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +150 -0
- package/src/claude/settings.ts +151 -0
- package/src/claude/skills-audit.ts +228 -0
- package/src/claude/skills-drift.ts +156 -0
- package/src/claude/skills-list.ts +99 -0
- package/src/claude/skills-rank.ts +320 -0
- package/src/claude/skills-reach.ts +227 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +200 -0
- package/src/commands/audits.ts +350 -0
- package/src/commands/autoship.ts +129 -0
- package/src/commands/capture.ts +133 -0
- package/src/commands/census.ts +105 -0
- package/src/commands/claude.ts +1286 -0
- package/src/commands/comments.ts +240 -0
- package/src/commands/context.ts +857 -0
- package/src/commands/demo.ts +389 -0
- package/src/commands/deps.ts +173 -0
- package/src/commands/design.ts +36 -0
- package/src/commands/docs.ts +60 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +112 -0
- package/src/commands/gate.ts +189 -0
- package/src/commands/gov.ts +1265 -0
- package/src/commands/indexes.ts +184 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/intake.ts +406 -0
- package/src/commands/inventory.ts +256 -0
- package/src/commands/labels.ts +361 -0
- package/src/commands/markdown.ts +544 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/pr.ts +411 -0
- package/src/commands/records.ts +728 -0
- package/src/commands/sandbox.ts +468 -0
- package/src/commands/secrets.ts +132 -0
- package/src/commands/serve.ts +159 -0
- package/src/commands/sessions.ts +408 -0
- package/src/commands/slides.ts +126 -0
- package/src/commands/snippets.ts +84 -0
- package/src/commands/standards.ts +247 -0
- package/src/commands/sync.ts +428 -0
- package/src/commands/targets.ts +319 -0
- package/src/commands/tasks.ts +743 -0
- package/src/commands/teach.ts +786 -0
- package/src/commands/tooling.ts +573 -0
- package/src/commands/transcripts.ts +44 -0
- package/src/commands/upgrade.ts +231 -0
- package/src/commands/wiki.ts +100 -0
- package/src/commands/worktrees.ts +191 -0
- package/src/comments/scan.ts +338 -0
- package/src/comments/trend.ts +207 -0
- package/src/comments/vocabulary.ts +85 -0
- package/src/context/architecture.ts +364 -0
- package/src/context/audit.ts +790 -0
- package/src/context/citations.ts +196 -0
- package/src/context/folders.ts +186 -0
- package/src/context/gate.ts +57 -0
- package/src/context/index-drift.ts +64 -0
- package/src/context/narration.ts +99 -0
- package/src/copy.ts +30 -0
- package/src/counts/catalogs.ts +96 -0
- package/src/counts/numbers.ts +79 -0
- package/src/counts/scan.ts +314 -0
- package/src/demo/beats.ts +135 -0
- package/src/demo/compile.ts +326 -0
- package/src/demo/container.ts +63 -0
- package/src/demo/cursors.ts +55 -0
- package/src/demo/drive.ts +357 -0
- package/src/demo/pointer.ts +178 -0
- package/src/demo/theme.ts +112 -0
- package/src/deps/audit.ts +153 -0
- package/src/design/parse.ts +116 -0
- package/src/design/render.ts +249 -0
- package/src/docs/read.ts +77 -0
- package/src/exec.ts +16 -0
- package/src/exempt-marker.ts +43 -0
- package/src/frontmatter.ts +13 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/git-env.ts +36 -0
- package/src/git-files.ts +105 -0
- package/src/git-ignore.ts +46 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +24 -0
- package/src/gov/adapter.ts +103 -0
- package/src/gov/citations.ts +514 -0
- package/src/gov/consumed.ts +129 -0
- package/src/gov/install.ts +132 -0
- package/src/gov/list.ts +106 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/restated.ts +814 -0
- package/src/gov/stacks.ts +205 -0
- package/src/gov/superseded.ts +415 -0
- package/src/gov/test-order.ts +407 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +201 -0
- package/src/indexes/walk.ts +83 -0
- package/src/init/flags.ts +60 -0
- package/src/init/plan.ts +114 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +77 -0
- package/src/intake/folder.ts +320 -0
- package/src/intake/items.ts +174 -0
- package/src/inventory/config.ts +117 -0
- package/src/inventory/group.ts +76 -0
- package/src/inventory/subjects.ts +114 -0
- package/src/inventory/walk.ts +129 -0
- package/src/labels/audit.ts +82 -0
- package/src/labels/coverage.ts +79 -0
- package/src/labels/map.ts +101 -0
- package/src/labels/phase.ts +95 -0
- package/src/markdown/bans.ts +94 -0
- package/src/markdown/files.ts +102 -0
- package/src/markdown/gate.ts +28 -0
- package/src/markdown/scan.ts +295 -0
- package/src/markdown/structure.ts +730 -0
- package/src/migrate/apply.ts +115 -0
- package/src/migrate/plan.ts +103 -0
- package/src/migrate/rename.ts +183 -0
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/src/process/harness.ts +167 -0
- package/src/project-root.ts +19 -0
- package/src/records/backup.ts +455 -0
- package/src/records/migrate.ts +78 -0
- package/src/records/size.ts +260 -0
- package/src/records/validate.ts +1162 -0
- package/src/sandbox/census.ts +228 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +629 -0
- package/src/sandbox/tree.ts +47 -0
- package/src/secrets/marker.ts +30 -0
- package/src/secrets/patterns.ts +142 -0
- package/src/secrets/scan.ts +123 -0
- package/src/secrets/shipped.ts +111 -0
- package/src/seed-marker.ts +74 -0
- package/src/serve/static.ts +322 -0
- package/src/sessions/claim.ts +85 -0
- package/src/sessions/live.ts +79 -0
- package/src/sessions/registry.ts +137 -0
- package/src/sessions/resolve.ts +333 -0
- package/src/slides/layouts.ts +391 -0
- package/src/slides/open.ts +18 -0
- package/src/slides/parse.ts +84 -0
- package/src/slides/render.ts +88 -0
- package/src/slides/styles.ts +44 -0
- package/src/snippets/categories.ts +66 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/audit.ts +132 -0
- package/src/standards/read.ts +100 -0
- package/src/sync/check.ts +692 -0
- package/src/sync/engine.ts +576 -0
- package/src/sync/git.ts +225 -0
- package/src/sync/history.ts +123 -0
- package/src/sync/layout.ts +140 -0
- package/src/sync/reverse.ts +268 -0
- package/src/sync/seeds-report.ts +126 -0
- package/src/sync/stamp.ts +358 -0
- package/src/sync/target.ts +70 -0
- package/src/sync/workflow.ts +200 -0
- package/src/target.ts +43 -0
- package/src/targets/pulls.ts +250 -0
- package/src/targets/registry.ts +203 -0
- package/src/targets/resolve.ts +145 -0
- package/src/targets/sweep.ts +246 -0
- package/src/tasks/archive.ts +506 -0
- package/src/tasks/record.ts +311 -0
- package/src/tasks/trunk.ts +89 -0
- package/src/tasks/validate.ts +1001 -0
- package/src/teach/lesson.ts +180 -0
- package/src/teach/workspace.ts +842 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +193 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +176 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +147 -0
- package/src/tooling/stamp.ts +44 -0
- package/src/transcripts/fetch.ts +156 -0
- package/src/transcripts/metadata.ts +54 -0
- package/src/transcripts/vtt.ts +114 -0
- package/src/ui.ts +266 -0
- package/src/version/compare.ts +53 -0
- package/src/version/installed.ts +40 -0
- package/src/version/manager.ts +67 -0
- package/src/version/skew.ts +192 -0
- package/src/wiki/init.ts +85 -0
- package/src/worktree.ts +143 -0
- package/src/worktrees/reclaim.ts +306 -0
- package/standards/architecture.md +72 -0
- package/standards/branch.md +59 -0
- package/standards/commit.md +72 -0
- package/standards/context.md +151 -0
- package/standards/design.md +93 -0
- package/standards/diagrams.md +152 -0
- package/standards/glossary.md +75 -0
- package/standards/groundwork.md +211 -0
- package/standards/index.md +36 -0
- package/standards/intake.md +192 -0
- package/standards/issue.md +94 -0
- package/standards/markdown.md +137 -0
- package/standards/memory.md +144 -0
- package/standards/plan.md +172 -0
- package/standards/pr.md +139 -0
- package/standards/publish.md +51 -0
- package/standards/readme.md +208 -0
- package/standards/requirements.md +70 -0
- package/standards/rule.md +118 -0
- package/standards/session.md +109 -0
- package/standards/skill.md +300 -0
- package/standards/slug.md +39 -0
- package/standards/snippets.md +76 -0
- package/standards/standard.md +170 -0
- package/standards/tasks.md +254 -0
- package/standards/teach.md +153 -0
- package/standards/versioning.md +71 -0
- package/standards/wireframes.md +113 -0
- package/tooling/astro/configs/astro.config.mjs +31 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +26 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +34 -0
- package/tooling/astro/reference.md +60 -0
- package/tooling/base/configs/.editorconfig +5 -0
- package/tooling/base/configs/.github/pull_request_template.md +18 -0
- package/tooling/base/configs/.github/workflows/verify.yml +35 -0
- package/tooling/base/configs/.husky/commit-msg +1 -0
- package/tooling/base/configs/.husky/post-merge +61 -0
- package/tooling/base/configs/.husky/post-rewrite +21 -0
- package/tooling/base/configs/.husky/pre-commit +1 -0
- package/tooling/base/configs/.husky/pre-push +1 -0
- package/tooling/base/configs/.prettierrc +12 -0
- package/tooling/base/configs/.shellcheckrc +1 -0
- package/tooling/base/configs/.vscode/extensions.json +9 -0
- package/tooling/base/configs/.vscode/settings.json +3 -0
- package/tooling/base/configs/commitlint.config.js +11 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +30 -0
- package/tooling/base/reference.md +98 -0
- package/tooling/base/seeds/.claude/context/ci.md +33 -0
- package/tooling/base/seeds/.claude/context/development.md +37 -0
- package/tooling/base/seeds/.claude/context/index.md +11 -0
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/base/seeds/.lintstagedrc +8 -0
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/base/seeds/cspell.json +20 -0
- package/tooling/claude/manifest.toml +14 -0
- package/tooling/claude/reference.md +79 -0
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
- package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
- package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
- package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +47 -0
- package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
- package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
- package/tooling/claude/seeds/CLAUDE.md +29 -0
- package/tooling/claude/user/settings.template.json +10 -0
- package/tooling/claude/user/statusline-command.sh +53 -0
- package/tooling/python/configs/.coveragerc +14 -0
- package/tooling/python/configs/.python-version +1 -0
- package/tooling/python/configs/mypy.ini +6 -0
- package/tooling/python/configs/pytest.ini +4 -0
- package/tooling/python/configs/ruff.toml +15 -0
- package/tooling/python/configs/scripts/verify.sh +77 -0
- package/tooling/python/manifest.toml +16 -0
- package/tooling/python/reference.md +66 -0
- package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/python/seeds/tests/test_smoke.py +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +26 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +24 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +23 -0
- package/tooling/vite-react/reference.md +55 -0
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +134 -0
- package/tooling/web/configs/.vscode/extensions.json +13 -0
- package/tooling/web/configs/.vscode/settings.json +10 -0
- package/tooling/web/configs/e2e/home.spec.ts +6 -0
- package/tooling/web/configs/e2e/screenshot.ts +53 -0
- package/tooling/web/configs/eslint.config.js +82 -0
- package/tooling/web/configs/scripts/screenshot.sh +28 -0
- package/tooling/web/configs/scripts/verify.sh +80 -0
- package/tooling/web/configs/scripts/worktree-port.sh +74 -0
- package/tooling/web/configs/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +58 -0
- package/tooling/web/reference.md +115 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Spells the package root in a report where every other root spells a
|
|
7
|
+
* project-relative path. The `source` field promises a path inside the project
|
|
8
|
+
* and a package copy is the one source that promise cannot cover, so the
|
|
9
|
+
* spelling has to be one nothing will join to a project root.
|
|
10
|
+
*/
|
|
11
|
+
const PACKAGE_LABEL = '<canon>'
|
|
12
|
+
|
|
13
|
+
export interface ReferenceRoot {
|
|
14
|
+
/** Absolute `tooling/` directory to search. */
|
|
15
|
+
readonly dir: string
|
|
16
|
+
/** How a report spells a copy found under this root. */
|
|
17
|
+
readonly label: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResolvedReference {
|
|
21
|
+
readonly path: string
|
|
22
|
+
/** The label of the root that won, joined to the stack and filename. */
|
|
23
|
+
readonly source: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toolingSourceDir(root: string): string {
|
|
27
|
+
return join(root, 'tooling')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The roots a stack reference resolves against, mirroring `standardRoots` in
|
|
32
|
+
* `src/standards/read.ts`. No stack reference installs into a project any
|
|
33
|
+
* more, so the package corpus is the only root that answers there. The
|
|
34
|
+
* authoring root stays ahead of it for the toolkit's own repository, which
|
|
35
|
+
* authors the corpus this reads, and for a project authoring stacks of its
|
|
36
|
+
* own under the same layout.
|
|
37
|
+
*/
|
|
38
|
+
export function referenceRoots(root: string): ReferenceRoot[] {
|
|
39
|
+
return [
|
|
40
|
+
{ dir: toolingSourceDir(root), label: 'tooling' },
|
|
41
|
+
{
|
|
42
|
+
dir: toolingSourceDir(PROJECT_ROOT),
|
|
43
|
+
label: join(PACKAGE_LABEL, 'tooling'),
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolves a stack's reference doc, working root first. */
|
|
49
|
+
export function resolveReference(
|
|
50
|
+
root: string,
|
|
51
|
+
stack: string,
|
|
52
|
+
): ResolvedReference | undefined {
|
|
53
|
+
for (const { dir, label } of referenceRoots(root)) {
|
|
54
|
+
const path = join(dir, stack, 'reference.md')
|
|
55
|
+
if (existsSync(path)) {
|
|
56
|
+
return { path, source: join(label, stack, 'reference.md') }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readReference(reference: ResolvedReference): string {
|
|
64
|
+
return readFileSync(reference.path, 'utf8')
|
|
65
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { mergeSections } from '@/tooling/gitignore'
|
|
4
|
+
import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
|
|
5
|
+
import {
|
|
6
|
+
collectDeps,
|
|
7
|
+
collectScripts,
|
|
8
|
+
type DepState,
|
|
9
|
+
readPackage,
|
|
10
|
+
type ScriptState,
|
|
11
|
+
} from '@/tooling/package'
|
|
12
|
+
|
|
13
|
+
export interface ConfigState {
|
|
14
|
+
readonly rel: string
|
|
15
|
+
readonly stack: string
|
|
16
|
+
readonly state: 'new' | 'drifted' | 'matching'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SeedState {
|
|
20
|
+
readonly rel: string
|
|
21
|
+
readonly state: 'missing' | 'present'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EntryState {
|
|
25
|
+
readonly entry: string
|
|
26
|
+
readonly state: 'missing' | 'present'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ScanResult {
|
|
30
|
+
readonly configs: readonly ConfigState[]
|
|
31
|
+
readonly seeds: readonly SeedState[]
|
|
32
|
+
readonly scripts: readonly ScriptState[]
|
|
33
|
+
readonly deps: readonly DepState[]
|
|
34
|
+
readonly gitignore: readonly EntryState[]
|
|
35
|
+
readonly hasPackageJson: boolean
|
|
36
|
+
readonly totalChanges: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isIdentical(a: string, b: string): boolean {
|
|
40
|
+
if (!existsSync(b)) return false
|
|
41
|
+
return readFileSync(a).equals(readFileSync(b))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compares every stack in the chain against the target and reports what would
|
|
46
|
+
* change. Nothing is written. Which stack wins a duplicate differs per
|
|
47
|
+
* category and mirrors the bash: configs, seeds, and scripts resolve nearest
|
|
48
|
+
* stack first, while dependencies and gitignore entries resolve from the
|
|
49
|
+
* furthest ancestor inward.
|
|
50
|
+
*/
|
|
51
|
+
export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
52
|
+
const configs: ConfigState[] = []
|
|
53
|
+
const seenConfigs = new Set<string>()
|
|
54
|
+
|
|
55
|
+
for (const manifest of chain) {
|
|
56
|
+
for (const rel of listFiles(manifest.configsDir)) {
|
|
57
|
+
if (seenConfigs.has(rel)) continue
|
|
58
|
+
seenConfigs.add(rel)
|
|
59
|
+
|
|
60
|
+
const source = join(manifest.configsDir, rel)
|
|
61
|
+
const dest = join(target, rel)
|
|
62
|
+
const state = !existsSync(dest)
|
|
63
|
+
? 'new'
|
|
64
|
+
: isIdentical(source, dest)
|
|
65
|
+
? 'matching'
|
|
66
|
+
: 'drifted'
|
|
67
|
+
|
|
68
|
+
configs.push({ rel, stack: manifest.name, state })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const seeds: SeedState[] = []
|
|
73
|
+
const seenSeeds = new Set<string>()
|
|
74
|
+
|
|
75
|
+
for (const manifest of chain) {
|
|
76
|
+
for (const rel of listFiles(manifest.seedsDir)) {
|
|
77
|
+
if (seenSeeds.has(rel)) continue
|
|
78
|
+
seenSeeds.add(rel)
|
|
79
|
+
|
|
80
|
+
seeds.push({
|
|
81
|
+
rel,
|
|
82
|
+
state: existsSync(join(target, rel)) ? 'present' : 'missing',
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const packagePath = join(target, 'package.json')
|
|
88
|
+
const pkg = readPackage(packagePath)
|
|
89
|
+
const scripts = pkg ? collectScripts(chain, pkg) : []
|
|
90
|
+
const deps = pkg ? collectDeps(chain, pkg) : []
|
|
91
|
+
|
|
92
|
+
const gitignorePath = join(target, '.gitignore')
|
|
93
|
+
const gitignoreContent = existsSync(gitignorePath)
|
|
94
|
+
? readFileSync(gitignorePath, 'utf8')
|
|
95
|
+
: ''
|
|
96
|
+
const gitignore = scanGitignore(chain, gitignoreContent)
|
|
97
|
+
|
|
98
|
+
const totalChanges =
|
|
99
|
+
configs.filter((entry) => entry.state !== 'matching').length +
|
|
100
|
+
seeds.filter((entry) => entry.state === 'missing').length +
|
|
101
|
+
scripts.filter((entry) => entry.state !== 'matching').length +
|
|
102
|
+
deps.filter((entry) => entry.state === 'missing').length +
|
|
103
|
+
gitignore.filter((entry) => entry.state === 'missing').length
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
configs,
|
|
107
|
+
seeds,
|
|
108
|
+
scripts,
|
|
109
|
+
deps,
|
|
110
|
+
gitignore,
|
|
111
|
+
hasPackageJson: pkg !== undefined,
|
|
112
|
+
totalChanges,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Replays the merge against a copy of the file so an entry that an earlier
|
|
118
|
+
* section or stack would add is not also reported as missing.
|
|
119
|
+
*/
|
|
120
|
+
function scanGitignore(
|
|
121
|
+
chain: readonly Manifest[],
|
|
122
|
+
content: string,
|
|
123
|
+
): EntryState[] {
|
|
124
|
+
const states: EntryState[] = []
|
|
125
|
+
const seen = new Set<string>()
|
|
126
|
+
let projected = content
|
|
127
|
+
|
|
128
|
+
for (const manifest of ancestorsFirst(chain)) {
|
|
129
|
+
for (const section of manifest.gitignore) {
|
|
130
|
+
for (const entry of section.entries) {
|
|
131
|
+
if (seen.has(entry)) continue
|
|
132
|
+
seen.add(entry)
|
|
133
|
+
|
|
134
|
+
const result = mergeSections(projected, [
|
|
135
|
+
{ header: section.header, entries: [entry] },
|
|
136
|
+
])
|
|
137
|
+
states.push({
|
|
138
|
+
entry,
|
|
139
|
+
state: result.added.length > 0 ? 'missing' : 'present',
|
|
140
|
+
})
|
|
141
|
+
projected = result.content
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return states
|
|
147
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { writeChainStamp } from '@/sync/stamp'
|
|
4
|
+
import type { Manifest } from '@/tooling/manifest'
|
|
5
|
+
import { readPackage } from '@/tooling/package'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether the target is a workspace root, where every package resolves its own
|
|
9
|
+
* stack. `package.json` covers bun, npm, and yarn, and pnpm declares the same
|
|
10
|
+
* fact in its own file instead.
|
|
11
|
+
*/
|
|
12
|
+
export function isWorkspaceRoot(target: string): boolean {
|
|
13
|
+
if (existsSync(join(target, 'pnpm-workspace.yaml'))) return true
|
|
14
|
+
|
|
15
|
+
const workspaces = readPackage(join(target, 'package.json'))?.workspaces
|
|
16
|
+
return Array.isArray(workspaces) || isRecord(workspaces)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Records the chain an install resolved, and returns whether it wrote one. A
|
|
21
|
+
* workspace root is the single refusal: one chain recorded there would be a
|
|
22
|
+
* guess at what its packages hold, and the report reading tooling as unmeasured
|
|
23
|
+
* is the true answer rather than a clean one.
|
|
24
|
+
*/
|
|
25
|
+
export async function recordToolingChain(
|
|
26
|
+
toolkitRoot: string,
|
|
27
|
+
target: string,
|
|
28
|
+
chain: readonly Manifest[],
|
|
29
|
+
now: Date,
|
|
30
|
+
): Promise<boolean> {
|
|
31
|
+
if (isWorkspaceRoot(target)) return false
|
|
32
|
+
|
|
33
|
+
await writeChainStamp(
|
|
34
|
+
target,
|
|
35
|
+
{ domain: 'tooling', toolkitRoot },
|
|
36
|
+
chain.map((manifest) => manifest.name),
|
|
37
|
+
now,
|
|
38
|
+
)
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
44
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'node:fs'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { execa } from 'execa'
|
|
12
|
+
import {
|
|
13
|
+
type VideoMetadata,
|
|
14
|
+
formatUploadDate,
|
|
15
|
+
renderFrontmatter,
|
|
16
|
+
slugify,
|
|
17
|
+
} from '@/transcripts/metadata'
|
|
18
|
+
import { dedupeRollingCues, parseVtt, renderBody } from '@/transcripts/vtt'
|
|
19
|
+
|
|
20
|
+
const YT_DLP_TIMEOUT_MS = 120_000
|
|
21
|
+
|
|
22
|
+
export function ensureYtDlp(): void {
|
|
23
|
+
if (Bun.which('yt-dlp') === null) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'yt-dlp not found on PATH. Install it from https://github.com/yt-dlp/yt-dlp.',
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FetchResult {
|
|
31
|
+
readonly metadata: VideoMetadata
|
|
32
|
+
readonly subPath: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runYtDlp(url: string, workDir: string): Promise<FetchResult> {
|
|
36
|
+
const outputTemplate = join(workDir, '%(id)s.%(ext)s')
|
|
37
|
+
let stdout: string
|
|
38
|
+
try {
|
|
39
|
+
const result = await execa(
|
|
40
|
+
'yt-dlp',
|
|
41
|
+
[
|
|
42
|
+
'--dump-json',
|
|
43
|
+
'--no-simulate',
|
|
44
|
+
'--write-sub',
|
|
45
|
+
'--write-auto-sub',
|
|
46
|
+
'--sub-langs',
|
|
47
|
+
'en,en-orig',
|
|
48
|
+
'--sub-format',
|
|
49
|
+
'vtt',
|
|
50
|
+
'--skip-download',
|
|
51
|
+
'-o',
|
|
52
|
+
outputTemplate,
|
|
53
|
+
url,
|
|
54
|
+
],
|
|
55
|
+
{ timeout: YT_DLP_TIMEOUT_MS },
|
|
56
|
+
)
|
|
57
|
+
stdout = result.stdout
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`yt-dlp failed for ${url}: ${lastLine(error)}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const info = JSON.parse(stdout) as Record<string, unknown>
|
|
63
|
+
const videoId = String(info['id'] ?? '').trim()
|
|
64
|
+
if (!videoId) throw new Error('yt-dlp returned no video id')
|
|
65
|
+
|
|
66
|
+
const metadata: VideoMetadata = {
|
|
67
|
+
videoId,
|
|
68
|
+
title: String(info['title'] || 'untitled'),
|
|
69
|
+
channel: String(info['channel'] || info['uploader'] || 'unknown'),
|
|
70
|
+
durationSeconds: Number(info['duration'] || 0),
|
|
71
|
+
published: formatUploadDate(asString(info['upload_date'])),
|
|
72
|
+
url: String(info['webpage_url'] || url),
|
|
73
|
+
}
|
|
74
|
+
const subPath = findSubtitle(workDir, videoId)
|
|
75
|
+
return { metadata, subPath }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface WriteOptions {
|
|
79
|
+
readonly outDir: string
|
|
80
|
+
readonly keepTimestamps: boolean
|
|
81
|
+
readonly fetchedAt: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function writeTranscript(
|
|
85
|
+
metadata: VideoMetadata,
|
|
86
|
+
subPath: string | null,
|
|
87
|
+
{ outDir, keepTimestamps, fetchedAt }: WriteOptions,
|
|
88
|
+
): string {
|
|
89
|
+
mkdirSync(outDir, { recursive: true })
|
|
90
|
+
const slug = `${slugify(metadata.title)}--${metadata.videoId}`
|
|
91
|
+
const target = join(outDir, `${slug}.md`)
|
|
92
|
+
|
|
93
|
+
const hasTranscript = subPath !== null
|
|
94
|
+
let body = ''
|
|
95
|
+
if (subPath !== null) {
|
|
96
|
+
const cues = parseVtt(readFileSync(subPath, 'utf8'))
|
|
97
|
+
body = renderBody(dedupeRollingCues(cues), { keepTimestamps })
|
|
98
|
+
} else {
|
|
99
|
+
body =
|
|
100
|
+
'Transcript unavailable. Auto-captions are not enabled for this video.\n'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const frontmatter = renderFrontmatter(metadata, { hasTranscript, fetchedAt })
|
|
104
|
+
const heading = `# ${metadata.title}\n\n`
|
|
105
|
+
writeFileSync(target, frontmatter + heading + body)
|
|
106
|
+
return target
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface FetchOptions {
|
|
110
|
+
readonly outDir: string
|
|
111
|
+
readonly keepTimestamps: boolean
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function fetchOne(
|
|
115
|
+
url: string,
|
|
116
|
+
{ outDir, keepTimestamps }: FetchOptions,
|
|
117
|
+
): Promise<string> {
|
|
118
|
+
const workDir = mkdtempSync(join(tmpdir(), 'yt-transcripts-'))
|
|
119
|
+
try {
|
|
120
|
+
const { metadata, subPath } = await runYtDlp(url, workDir)
|
|
121
|
+
return writeTranscript(metadata, subPath, {
|
|
122
|
+
outDir,
|
|
123
|
+
keepTimestamps,
|
|
124
|
+
fetchedAt: todayLocal(),
|
|
125
|
+
})
|
|
126
|
+
} finally {
|
|
127
|
+
rmSync(workDir, { recursive: true, force: true })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findSubtitle(workDir: string, videoId: string): string | null {
|
|
132
|
+
const matches = readdirSync(workDir)
|
|
133
|
+
.filter((name) => name.startsWith(videoId) && name.endsWith('.vtt'))
|
|
134
|
+
.sort()
|
|
135
|
+
return matches.length ? join(workDir, matches[0]) : null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function todayLocal(): string {
|
|
139
|
+
const now = new Date()
|
|
140
|
+
const year = now.getFullYear()
|
|
141
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
142
|
+
const day = String(now.getDate()).padStart(2, '0')
|
|
143
|
+
return `${year}-${month}-${day}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function asString(value: unknown): string | undefined {
|
|
147
|
+
return typeof value === 'string' ? value : undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function lastLine(error: unknown): string {
|
|
151
|
+
const stderr =
|
|
152
|
+
error instanceof Error && 'stderr' in error ? String(error.stderr) : ''
|
|
153
|
+
const lines = stderr.trim().split('\n').filter(Boolean)
|
|
154
|
+
if (lines.length) return lines[lines.length - 1]
|
|
155
|
+
return error instanceof Error ? error.message : String(error)
|
|
156
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface VideoMetadata {
|
|
2
|
+
readonly videoId: string
|
|
3
|
+
readonly title: string
|
|
4
|
+
readonly channel: string
|
|
5
|
+
readonly durationSeconds: number
|
|
6
|
+
readonly published: string
|
|
7
|
+
readonly url: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SLUG_PATTERN = /[^a-z0-9]+/g
|
|
11
|
+
|
|
12
|
+
export function slugify(text: string, maxLength = 50): string {
|
|
13
|
+
const cleaned = text
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(SLUG_PATTERN, '-')
|
|
16
|
+
.replace(/(^-|-$)/g, '')
|
|
17
|
+
if (!cleaned) return 'untitled'
|
|
18
|
+
return cleaned.slice(0, maxLength).replace(/-$/, '') || 'untitled'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatUploadDate(yyyymmdd: string | undefined): string {
|
|
22
|
+
if (!yyyymmdd || yyyymmdd.length !== 8 || !/^\d{8}$/.test(yyyymmdd)) return ''
|
|
23
|
+
return `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6)}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function yamlEscape(value: string): string {
|
|
27
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FrontmatterOptions {
|
|
31
|
+
readonly hasTranscript: boolean
|
|
32
|
+
readonly fetchedAt: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderFrontmatter(
|
|
36
|
+
video: VideoMetadata,
|
|
37
|
+
{ hasTranscript, fetchedAt }: FrontmatterOptions,
|
|
38
|
+
): string {
|
|
39
|
+
const lines = [
|
|
40
|
+
'---',
|
|
41
|
+
`title: ${yamlEscape(video.title)}`,
|
|
42
|
+
`channel: ${yamlEscape(video.channel)}`,
|
|
43
|
+
`video_id: ${video.videoId}`,
|
|
44
|
+
`url: ${video.url}`,
|
|
45
|
+
`duration: ${video.durationSeconds}`,
|
|
46
|
+
video.published ? `published: ${video.published}` : 'published:',
|
|
47
|
+
`fetched_at: ${fetchedAt}`,
|
|
48
|
+
`has_transcript: ${hasTranscript ? 'true' : 'false'}`,
|
|
49
|
+
'---',
|
|
50
|
+
'',
|
|
51
|
+
'',
|
|
52
|
+
]
|
|
53
|
+
return lines.join('\n')
|
|
54
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export interface Cue {
|
|
2
|
+
readonly timestamp: string
|
|
3
|
+
readonly text: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const VTT_TIMESTAMP = /^(\d\d:)?\d\d:\d\d\.\d{3} --> .*$/
|
|
7
|
+
const VTT_INLINE_TAG = /<[^>]+>/g
|
|
8
|
+
const VTT_NUMERIC_LINE = /^\d+$/
|
|
9
|
+
|
|
10
|
+
export function parseVtt(raw: string): Cue[] {
|
|
11
|
+
const cues: Cue[] = []
|
|
12
|
+
let currentTimestamp: string | null = null
|
|
13
|
+
let currentLines: string[] = []
|
|
14
|
+
let inHeader = true
|
|
15
|
+
|
|
16
|
+
const flush = (): void => {
|
|
17
|
+
if (currentTimestamp !== null && currentLines.length) {
|
|
18
|
+
const text = currentLines.join(' ').trim()
|
|
19
|
+
if (text) cues.push({ timestamp: currentTimestamp, text })
|
|
20
|
+
}
|
|
21
|
+
currentTimestamp = null
|
|
22
|
+
currentLines = []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
26
|
+
const line = rawLine.replace(/\s+$/, '')
|
|
27
|
+
if (inHeader) {
|
|
28
|
+
if (VTT_TIMESTAMP.test(line)) inHeader = false
|
|
29
|
+
else continue
|
|
30
|
+
}
|
|
31
|
+
if (VTT_TIMESTAMP.test(line)) {
|
|
32
|
+
flush()
|
|
33
|
+
currentTimestamp = line.split(' --> ', 1)[0].trim()
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (line.trim() === '') {
|
|
37
|
+
flush()
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (VTT_NUMERIC_LINE.test(line.trim())) continue
|
|
41
|
+
const cleaned = line.replace(VTT_INLINE_TAG, '').trim()
|
|
42
|
+
if (cleaned) currentLines.push(cleaned)
|
|
43
|
+
}
|
|
44
|
+
flush()
|
|
45
|
+
return cues
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function dedupeRollingCues(cues: Cue[]): Cue[] {
|
|
49
|
+
const deduped: Cue[] = []
|
|
50
|
+
let running = ''
|
|
51
|
+
for (const { timestamp, text } of cues) {
|
|
52
|
+
const maxOverlap = Math.min(running.length, text.length)
|
|
53
|
+
let overlap = 0
|
|
54
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
55
|
+
if (running.endsWith(text.slice(0, size))) {
|
|
56
|
+
overlap = size
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const addition = text.slice(overlap).trim()
|
|
61
|
+
if (!addition) continue
|
|
62
|
+
running = `${running} ${addition}`.trim()
|
|
63
|
+
deduped.push({ timestamp, text: addition })
|
|
64
|
+
}
|
|
65
|
+
return deduped
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function vttTimestampToSeconds(stamp: string): number {
|
|
69
|
+
const parts = stamp.split(':')
|
|
70
|
+
let hours = '0'
|
|
71
|
+
let minutes: string
|
|
72
|
+
let rest: string
|
|
73
|
+
if (parts.length === 3) [hours, minutes, rest] = parts
|
|
74
|
+
else if (parts.length === 2) [minutes, rest] = parts
|
|
75
|
+
else return 0
|
|
76
|
+
const seconds = rest.split('.', 1)[0]
|
|
77
|
+
return Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatSeconds(seconds: number): string {
|
|
81
|
+
const minutes = Math.floor(seconds / 60)
|
|
82
|
+
const secs = seconds % 60
|
|
83
|
+
return `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface RenderOptions {
|
|
87
|
+
readonly keepTimestamps: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function renderBody(
|
|
91
|
+
cues: Cue[],
|
|
92
|
+
{ keepTimestamps }: RenderOptions,
|
|
93
|
+
): string {
|
|
94
|
+
if (!cues.length) return ''
|
|
95
|
+
if (keepTimestamps) {
|
|
96
|
+
const lines = cues.map(
|
|
97
|
+
({ timestamp, text }) =>
|
|
98
|
+
`[${formatSeconds(vttTimestampToSeconds(timestamp))}] ${text}`,
|
|
99
|
+
)
|
|
100
|
+
return `${lines.join('\n')}\n`
|
|
101
|
+
}
|
|
102
|
+
const joined = cues.map(({ text }) => text).join(' ')
|
|
103
|
+
return `${paragraphWrap(joined)}\n`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function paragraphWrap(text: string, wordsPerParagraph = 60): string {
|
|
107
|
+
const words = text.split(/\s+/).filter(Boolean)
|
|
108
|
+
if (!words.length) return ''
|
|
109
|
+
const paragraphs: string[] = []
|
|
110
|
+
for (let start = 0; start < words.length; start += wordsPerParagraph) {
|
|
111
|
+
paragraphs.push(words.slice(start, start + wordsPerParagraph).join(' '))
|
|
112
|
+
}
|
|
113
|
+
return paragraphs.join('\n\n')
|
|
114
|
+
}
|