bmad-module-skill-forge 2.2.0 → 3.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/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,665 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate documentation links, in the source AND in the built site.
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS EXISTS
|
|
5
|
-
*
|
|
6
|
-
* `docs/` is read in two places: rendered on GitHub, and published as the
|
|
7
|
-
* site. A link can be valid in one and dead in the other, and nothing fails
|
|
8
|
-
* on it: an href that resolves nowhere is still valid HTML, so Astro builds
|
|
9
|
-
* clean and markdownlint has no opinion. `validate-doc-links.js` covers the
|
|
10
|
-
* source side (site-absolute targets and anchors) but never sees the built
|
|
11
|
-
* output, so a link that is correct in the Markdown and wrong as a route
|
|
12
|
-
* passes every check and ships.
|
|
13
|
-
*
|
|
14
|
-
* A source-only check is therefore NOT sufficient, and that is the lesson
|
|
15
|
-
* this tool encodes. Rewriting happens between the Markdown and the page:
|
|
16
|
-
* `website/src/rehype-markdown-links.js` turns `.md` targets into routes, so
|
|
17
|
-
* a bug there produces hrefs that look right in source and 404 in a browser.
|
|
18
|
-
* Only resolving the BUILT output against the file tree catches that class.
|
|
19
|
-
*
|
|
20
|
-
* So there are two passes:
|
|
21
|
-
*
|
|
22
|
-
* 1. SOURCE (docs/ *.md) - a relative `.md` target must exist on disk.
|
|
23
|
-
* Cheap, and catches a typo before a build is needed.
|
|
24
|
-
* 2. BUILT (build/site/ *.html) - every internal href must resolve to a
|
|
25
|
-
* real file. This is the pass that catches plugin bugs, base-path
|
|
26
|
-
* bugs, and anything else that only manifests as a route.
|
|
27
|
-
*
|
|
28
|
-
* The built pass is skipped with a warning when no build is present, so the
|
|
29
|
-
* tool stays usable locally without a build step. CI builds the site, so the
|
|
30
|
-
* pass runs there. Use --require-build to make a missing build an error.
|
|
31
|
-
*
|
|
32
|
-
* Usage:
|
|
33
|
-
* node tools/validate-docs-links.js [--strict] [--verbose] [--require-build]
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
const fs = require('node:fs');
|
|
37
|
-
const path = require('node:path');
|
|
38
|
-
|
|
39
|
-
/** Hosts/schemes that are never local links. */
|
|
40
|
-
const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Read a `--flag value` pair out of an argv array.
|
|
44
|
-
*
|
|
45
|
-
* @param {string[]} argv - Arguments to search
|
|
46
|
-
* @param {string} name - Flag name, including leading dashes
|
|
47
|
-
* @returns {string|undefined} The value, or undefined when absent
|
|
48
|
-
*/
|
|
49
|
-
function flagValue(argv, name) {
|
|
50
|
-
const at = argv.indexOf(name);
|
|
51
|
-
return at === -1 ? undefined : argv[at + 1];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Resolve where to look, from argv, defaulting to this repository.
|
|
56
|
-
*
|
|
57
|
-
* The directory overrides exist so the checks can be pointed at a fixture
|
|
58
|
-
* tree. Without them every path derives from `__dirname` and the tool can
|
|
59
|
-
* only ever inspect its own repo, which makes it untestable: the logic that
|
|
60
|
-
* decides whether a merge is blocked would itself be unverified.
|
|
61
|
-
*
|
|
62
|
-
* @param {string[]} [argv] - Defaults to the real process arguments
|
|
63
|
-
* @returns {{projectRoot: string, docsDir: string, buildDir: string, strict: boolean, verbose: boolean, requireBuild: boolean}} Config
|
|
64
|
-
*/
|
|
65
|
-
function resolveConfig(argv = process.argv.slice(2)) {
|
|
66
|
-
const projectRoot = flagValue(argv, '--project-root') ?? path.resolve(__dirname, '..');
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
projectRoot,
|
|
70
|
-
docsDir: flagValue(argv, '--docs-dir') ?? path.join(projectRoot, 'docs'),
|
|
71
|
-
buildDir: flagValue(argv, '--build-dir') ?? path.join(projectRoot, 'build', 'site'),
|
|
72
|
-
strict: argv.includes('--strict'),
|
|
73
|
-
verbose: argv.includes('--verbose'),
|
|
74
|
-
requireBuild: argv.includes('--require-build'),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Recursively collect files with one of the given extensions.
|
|
80
|
-
*
|
|
81
|
-
* @param {string} dir - Directory to walk
|
|
82
|
-
* @param {Set<string>} exts - Extensions to keep, with leading dot
|
|
83
|
-
* @param {string[]} [out] - Accumulator
|
|
84
|
-
* @returns {string[]} Absolute paths
|
|
85
|
-
*/
|
|
86
|
-
function walk(dir, exts, out = []) {
|
|
87
|
-
let entries;
|
|
88
|
-
try {
|
|
89
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
90
|
-
} catch {
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (const entry of entries) {
|
|
95
|
-
const full = path.join(dir, entry.name);
|
|
96
|
-
if (entry.isDirectory()) {
|
|
97
|
-
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
walk(full, exts, out);
|
|
101
|
-
} else if (exts.has(path.extname(entry.name))) {
|
|
102
|
-
out.push(full);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Strip the query and anchor from an href, returning the path portion. */
|
|
110
|
-
function pathPortion(href) {
|
|
111
|
-
const q = href.indexOf('?');
|
|
112
|
-
const h = href.indexOf('#');
|
|
113
|
-
const first = Math.min(q === -1 ? Infinity : q, h === -1 ? Infinity : h);
|
|
114
|
-
return first === Infinity ? href : href.slice(0, first);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Pass 1: source
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
/** Markdown inline links: the target inside `](...)`. */
|
|
122
|
-
const MD_LINK = /\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Blank out code so a link that is merely *shown* is not read as a real one.
|
|
126
|
-
*
|
|
127
|
-
* `MD_LINK` is a regex over raw text, so it cannot tell a link from a picture
|
|
128
|
-
* of a link. Without this, documenting the link convention itself, e.g. a
|
|
129
|
-
* ```markdown block containing [label](./example.md), fails the build as a
|
|
130
|
-
* broken link, and the only way to appease it is to create the placeholder
|
|
131
|
-
* file. (`tools/validate-file-refs.js` does NOT do this, so it carries the
|
|
132
|
-
* same latent false positive; worth fixing there too if it ever fires.)
|
|
133
|
-
*
|
|
134
|
-
* Fences are tracked line by line rather than matched as a pair, so an
|
|
135
|
-
* unterminated fence blanks to end of file instead of silently reverting to
|
|
136
|
-
* prose scanning. Inline spans are only stripped outside a fence, so a stray
|
|
137
|
-
* backtick inside a block cannot pair with one outside it. Line count is
|
|
138
|
-
* preserved, which keeps byte offsets usable if this ever reports positions.
|
|
139
|
-
*
|
|
140
|
-
* @param {string} text - Raw Markdown
|
|
141
|
-
* @returns {string} The same text with fenced blocks and code spans blanked
|
|
142
|
-
*/
|
|
143
|
-
function blankCode(text) {
|
|
144
|
-
let fence = null;
|
|
145
|
-
|
|
146
|
-
return text
|
|
147
|
-
.split('\n')
|
|
148
|
-
.map((line) => {
|
|
149
|
-
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/);
|
|
150
|
-
|
|
151
|
-
if (fence) {
|
|
152
|
-
// A closing fence is the same character, and at least as long.
|
|
153
|
-
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length) {
|
|
154
|
-
fence = null;
|
|
155
|
-
}
|
|
156
|
-
return '';
|
|
157
|
-
}
|
|
158
|
-
if (marker) {
|
|
159
|
-
fence = marker[1];
|
|
160
|
-
return '';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Inline spans: a run of N backticks closes on a run of N.
|
|
164
|
-
return line.replaceAll(/(`+)[^`]*\1/g, ' ');
|
|
165
|
-
})
|
|
166
|
-
.join('\n');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Check that every relative `.md` target in docs/ exists on disk.
|
|
171
|
-
*
|
|
172
|
-
* Deliberately does NOT enforce a `./` prefix. The plugin accepts bare links,
|
|
173
|
-
* and GitHub renders them correctly, so requiring the prefix would be style,
|
|
174
|
-
* not correctness. What matters is that the target exists.
|
|
175
|
-
*/
|
|
176
|
-
function checkSource({ projectRoot, docsDir }, report) {
|
|
177
|
-
const files = walk(docsDir, new Set(['.md']));
|
|
178
|
-
|
|
179
|
-
for (const file of files) {
|
|
180
|
-
const rel = path.relative(projectRoot, file);
|
|
181
|
-
const dir = path.dirname(file);
|
|
182
|
-
const text = blankCode(fs.readFileSync(file, 'utf8'));
|
|
183
|
-
|
|
184
|
-
for (const match of text.matchAll(MD_LINK)) {
|
|
185
|
-
const target = pathPortion(match[1]);
|
|
186
|
-
|
|
187
|
-
if (!target || target.startsWith('#') || EXTERNAL.test(target)) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (!target.endsWith('.md')) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
// Root-absolute targets mean different things on the two surfaces, and
|
|
194
|
-
// this pass owns the GitHub one. GitHub resolves `/x.md` against the
|
|
195
|
-
// REPOSITORY root (it rewrites the href to /<owner>/<repo>/blob/<ref>/x.md),
|
|
196
|
-
// whereas the rewriter treats docs/ as the site root, so `/architecture.md`
|
|
197
|
-
// becomes the valid route /architecture/ while pointing at a repo-root
|
|
198
|
-
// file that does not exist. The built pass only ever sees the route, so
|
|
199
|
-
// it cannot notice. Resolve against the repo root, which is the surface
|
|
200
|
-
// this pass is responsible for.
|
|
201
|
-
//
|
|
202
|
-
// Note `/docs/architecture.md` is CORRECT on both: repo-root
|
|
203
|
-
// docs/architecture.md exists, and the rewriter strips the `/docs`
|
|
204
|
-
// prefix. Existence is the test, not the prefix.
|
|
205
|
-
const resolved = target.startsWith('/') ? path.join(projectRoot, target) : path.resolve(dir, target);
|
|
206
|
-
|
|
207
|
-
if (!fs.existsSync(resolved)) {
|
|
208
|
-
report(
|
|
209
|
-
rel,
|
|
210
|
-
target.startsWith('/')
|
|
211
|
-
? `root-absolute link target does not exist at the repository root, so it is dead on GitHub: ${match[1]}`
|
|
212
|
-
: `link target does not exist: ${match[1]}`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return files.length;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
// Pass 2: built output
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
|
|
225
|
-
/** Normalize a URL pathname into a base with exactly one trailing slash. */
|
|
226
|
-
function asBase(pathname) {
|
|
227
|
-
if (!pathname || pathname === '/') {
|
|
228
|
-
return '/';
|
|
229
|
-
}
|
|
230
|
-
return pathname.endsWith('/') ? pathname : `${pathname}/`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Resolve the base path the build on disk was actually produced with.
|
|
235
|
-
*
|
|
236
|
-
* Read from the ARTIFACT, not the environment. The environment can disagree
|
|
237
|
-
* with the build (a site built with SITE_URL set, then validated without it),
|
|
238
|
-
* and that disagreement makes every absolute href look broken: a loud, and
|
|
239
|
-
* entirely false, failure. The root page's canonical URL is written by the
|
|
240
|
-
* build itself, so it cannot drift from the output it describes.
|
|
241
|
-
*
|
|
242
|
-
* Falls back to the environment, then to '/', when no canonical is present.
|
|
243
|
-
*
|
|
244
|
-
* @returns {Promise<string>} The base path, e.g. '/repo/' or '/'
|
|
245
|
-
*/
|
|
246
|
-
async function resolveBase({ projectRoot, buildDir }) {
|
|
247
|
-
const rootIndex = path.join(buildDir, 'index.html');
|
|
248
|
-
|
|
249
|
-
if (fs.existsSync(rootIndex)) {
|
|
250
|
-
const html = fs.readFileSync(rootIndex, 'utf8');
|
|
251
|
-
const canonical =
|
|
252
|
-
html.match(/<link[^>]+rel="canonical"[^>]+href="([^"]+)"/i) || html.match(/<meta[^>]+property="og:url"[^>]+content="([^"]+)"/i);
|
|
253
|
-
|
|
254
|
-
if (canonical) {
|
|
255
|
-
try {
|
|
256
|
-
return asBase(new URL(canonical[1]).pathname);
|
|
257
|
-
} catch {
|
|
258
|
-
// Not an absolute URL; fall through to the environment.
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const mod = await import(`file://${path.join(projectRoot, 'website', 'src', 'lib', 'site-url.js')}`);
|
|
265
|
-
return asBase(new URL(mod.getSiteUrl()).pathname);
|
|
266
|
-
} catch {
|
|
267
|
-
return '/';
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Every URL a page asks the browser to fetch: navigations AND subresources.
|
|
273
|
-
*
|
|
274
|
-
* `href` alone is not the whole surface. A missing `src` is the same defect
|
|
275
|
-
* with a worse symptom: the browser fetches it without the user clicking, and
|
|
276
|
-
* a 404 script or image degrades the page silently. This matters here because
|
|
277
|
-
* assets under `website/public/` are copied verbatim and referenced by
|
|
278
|
-
* hand-written string paths (see `mermaid-lightbox.js` in astro.config.mjs),
|
|
279
|
-
* so renaming one leaves a green build and a dead reference on every page.
|
|
280
|
-
*
|
|
281
|
-
* `srcset` is a comma-separated candidate list where each entry is a URL
|
|
282
|
-
* optionally followed by a width/density descriptor, so it is split apart.
|
|
283
|
-
* The build emits no `srcset` today, only SVGs, but adding one raster image
|
|
284
|
-
* to the docs would change that without touching this file.
|
|
285
|
-
*
|
|
286
|
-
* @param {string} html - Full page source
|
|
287
|
-
* @returns {Array<{attr: string, url: string}>} References, in document order
|
|
288
|
-
*/
|
|
289
|
-
function assetUrls(html) {
|
|
290
|
-
const refs = [];
|
|
291
|
-
|
|
292
|
-
// `src` here never matches `srcset=`, which continues with `set=`.
|
|
293
|
-
for (const match of html.matchAll(/\s(href|src)="([^"]*)"/g)) {
|
|
294
|
-
refs.push({ attr: match[1], url: match[2] });
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
for (const match of html.matchAll(/\ssrcset="([^"]*)"/g)) {
|
|
298
|
-
for (const candidate of match[1].split(',')) {
|
|
299
|
-
const url = candidate.trim().split(/\s+/)[0];
|
|
300
|
-
if (url) {
|
|
301
|
-
refs.push({ attr: 'srcset', url });
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return refs;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Verify every internal reference in the built site resolves to a real file.
|
|
311
|
-
*
|
|
312
|
-
* Covers navigations (`href`) and subresources (`src`, `srcset`) alike: both
|
|
313
|
-
* are URLs the page promises the browser it can fetch.
|
|
314
|
-
*
|
|
315
|
-
* A route ending in `/` must have an `index.html`; anything else must exist
|
|
316
|
-
* as a file. Relative URLs are resolved against the page's own directory,
|
|
317
|
-
* which is precisely how a browser resolves them, and precisely the step that
|
|
318
|
-
* the earlier page-relative bug failed.
|
|
319
|
-
*
|
|
320
|
-
* @param {string} base - Deployment base path
|
|
321
|
-
* @returns {number} Number of HTML pages checked
|
|
322
|
-
*/
|
|
323
|
-
function checkBuilt({ projectRoot, buildDir }, base, report) {
|
|
324
|
-
const pages = walk(buildDir, new Set(['.html']));
|
|
325
|
-
|
|
326
|
-
for (const page of pages) {
|
|
327
|
-
const rel = path.relative(projectRoot, page);
|
|
328
|
-
const html = fs.readFileSync(page, 'utf8');
|
|
329
|
-
// The page's URL directory, e.g. build/site/troubleshooting/index.html
|
|
330
|
-
// is served at /troubleshooting/, so relative links resolve from there.
|
|
331
|
-
const pageDir = path.dirname(page);
|
|
332
|
-
|
|
333
|
-
const seen = new Set();
|
|
334
|
-
|
|
335
|
-
for (const { attr, url: href } of assetUrls(html)) {
|
|
336
|
-
if (!href || href.startsWith('#') || EXTERNAL.test(href) || seen.has(href)) {
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
seen.add(href);
|
|
340
|
-
|
|
341
|
-
const target = pathPortion(href);
|
|
342
|
-
if (!target) {
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
let resolved;
|
|
347
|
-
if (target.startsWith('/')) {
|
|
348
|
-
// Root-absolute: strip the base, then read from the build root.
|
|
349
|
-
let withoutBase = target;
|
|
350
|
-
if (base !== '/' && target.startsWith(base)) {
|
|
351
|
-
withoutBase = `/${target.slice(base.length)}`;
|
|
352
|
-
} else if (base !== '/') {
|
|
353
|
-
report(rel, `absolute ${attr} is missing the base path ${base}: ${href}`);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
resolved = path.join(buildDir, withoutBase);
|
|
357
|
-
} else {
|
|
358
|
-
resolved = path.resolve(pageDir, target);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Never let a link escape the build root.
|
|
362
|
-
if (!resolved.startsWith(buildDir + path.sep)) {
|
|
363
|
-
report(rel, `${attr} escapes the site root: ${href}`);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const candidate = target.endsWith('/') || !path.extname(resolved) ? path.join(resolved, 'index.html') : resolved;
|
|
368
|
-
|
|
369
|
-
if (!fs.existsSync(candidate)) {
|
|
370
|
-
report(rel, `${attr} resolves to nothing: ${href} (looked for ${path.relative(buildDir, candidate)})`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return pages.length;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
|
|
380
|
-
const BUILD_INFRA_INPUTS = [
|
|
381
|
-
'website/src/rehype-markdown-links.js',
|
|
382
|
-
'website/src/rehype-base-paths.js',
|
|
383
|
-
'website/src/lib/site-url.js',
|
|
384
|
-
'website/astro.config.mjs',
|
|
385
|
-
'tools/build-docs.js',
|
|
386
|
-
];
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* One sha1 per build input: the flat docs/*.md set plus BUILD_INFRA_INPUTS,
|
|
390
|
-
* the route-shaping infrastructure (BOTH rehype rewriters, the base/site
|
|
391
|
-
* resolver, the Astro config, and the builder itself). Canonical for both the
|
|
392
|
-
* manifest writer in tools/build-docs.js and the freshness check below - the
|
|
393
|
-
* first cut duplicated the list in both tools and both copies missed the
|
|
394
|
-
* second rehype rewriter, which is exactly the drift a single owner prevents.
|
|
395
|
-
*/
|
|
396
|
-
function collectBuildInputs(projectRoot) {
|
|
397
|
-
const crypto = require('node:crypto');
|
|
398
|
-
const sha1 = (file) => crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex');
|
|
399
|
-
const inputs = {};
|
|
400
|
-
const docsDir = path.join(projectRoot, 'docs');
|
|
401
|
-
if (fs.existsSync(docsDir)) {
|
|
402
|
-
for (const name of fs.readdirSync(docsDir).sort()) {
|
|
403
|
-
if (name.endsWith('.md')) {
|
|
404
|
-
inputs[`docs/${name}`] = sha1(path.join(docsDir, name));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
for (const rel of BUILD_INFRA_INPUTS) {
|
|
409
|
-
const file = path.join(projectRoot, rel);
|
|
410
|
-
if (fs.existsSync(file)) {
|
|
411
|
-
inputs[rel] = sha1(file);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return inputs;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Compare the build-inputs manifest against the tree as it is NOW.
|
|
419
|
-
*
|
|
420
|
-
* Returns {state, changed}: state is 'missing' (no manifest - an older build,
|
|
421
|
-
* nothing to compare), 'match', or 'stale' with the changed inputs listed.
|
|
422
|
-
* A 'match' is deliberately NOT a freshness certificate: Astro's content cache
|
|
423
|
-
* can replay a stale render against unchanged inputs, so a matching manifest
|
|
424
|
-
* proves nothing - only a MISmatch proves staleness. That asymmetry is why the
|
|
425
|
-
* stale branch speaks definitely and the match branch stays hedged.
|
|
426
|
-
*/
|
|
427
|
-
function buildInputsStatus({ projectRoot, buildDir }) {
|
|
428
|
-
const manifestPath = path.join(path.dirname(buildDir), '.build-inputs.json');
|
|
429
|
-
let manifest;
|
|
430
|
-
try {
|
|
431
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
432
|
-
} catch {
|
|
433
|
-
return { state: 'missing', changed: [] };
|
|
434
|
-
}
|
|
435
|
-
if (!manifest || typeof manifest.inputs !== 'object' || manifest.inputs === null) {
|
|
436
|
-
return { state: 'missing', changed: [] };
|
|
437
|
-
}
|
|
438
|
-
const current = collectBuildInputs(projectRoot);
|
|
439
|
-
const changed = [];
|
|
440
|
-
for (const key of new Set([...Object.keys(manifest.inputs), ...Object.keys(current)])) {
|
|
441
|
-
if (manifest.inputs[key] !== current[key]) {
|
|
442
|
-
changed.push(key);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
changed.sort();
|
|
446
|
-
return { state: changed.length > 0 ? 'stale' : 'match', changed };
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Resolve every site-internal link in the built llms.txt against the built
|
|
451
|
-
* tree. This link class is invisible to the HTML pass (llms.txt is not HTML),
|
|
452
|
-
* which is exactly how a deleted page's index entry survived every check.
|
|
453
|
-
*
|
|
454
|
-
* The site root is read off the file's own `Documentation: <url>` header line
|
|
455
|
-
* rather than off the build base, so the check is independent of what base the
|
|
456
|
-
* local render happened to use. No header, or no file: nothing to check -
|
|
457
|
-
* reported as a count of 0, never as a pass over content that was not read.
|
|
458
|
-
*/
|
|
459
|
-
function checkLlmsIndex({ buildDir }, report) {
|
|
460
|
-
const llmsPath = path.join(buildDir, 'llms.txt');
|
|
461
|
-
let text;
|
|
462
|
-
try {
|
|
463
|
-
text = fs.readFileSync(llmsPath, 'utf-8');
|
|
464
|
-
} catch {
|
|
465
|
-
return 0;
|
|
466
|
-
}
|
|
467
|
-
const rootMatch = text.match(/^Documentation:\s+(\S+)$/m);
|
|
468
|
-
if (!rootMatch) {
|
|
469
|
-
return 0;
|
|
470
|
-
}
|
|
471
|
-
let rootPath;
|
|
472
|
-
try {
|
|
473
|
-
rootPath = new URL(rootMatch[1]).pathname;
|
|
474
|
-
} catch {
|
|
475
|
-
return 0;
|
|
476
|
-
}
|
|
477
|
-
if (!rootPath.endsWith('/')) {
|
|
478
|
-
rootPath += '/';
|
|
479
|
-
}
|
|
480
|
-
let checked = 0;
|
|
481
|
-
for (const match of text.matchAll(/\]\((\S+?)\)/g)) {
|
|
482
|
-
const target = match[1];
|
|
483
|
-
let url;
|
|
484
|
-
try {
|
|
485
|
-
url = new URL(target);
|
|
486
|
-
} catch {
|
|
487
|
-
continue; // relative or malformed: llms.txt only carries absolute links
|
|
488
|
-
}
|
|
489
|
-
if (!url.pathname.startsWith(rootPath)) {
|
|
490
|
-
continue; // external link, not a site route
|
|
491
|
-
}
|
|
492
|
-
checked += 1;
|
|
493
|
-
const route = url.pathname.slice(rootPath.length);
|
|
494
|
-
const candidates =
|
|
495
|
-
route === ''
|
|
496
|
-
? [path.join(buildDir, 'index.html')]
|
|
497
|
-
: [path.join(buildDir, route.replace(/\/$/, ''), 'index.html'), path.join(buildDir, route)];
|
|
498
|
-
if (!candidates.some((candidate) => fs.existsSync(candidate))) {
|
|
499
|
-
report(
|
|
500
|
-
'build/site/llms.txt',
|
|
501
|
-
`links a route that resolves to nothing: ${target} (looked for ${candidates
|
|
502
|
-
.map((candidate) => path.relative(buildDir, candidate))
|
|
503
|
-
.join(' or ')})`,
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return checked;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Run both passes and return what happened, without touching the process.
|
|
512
|
-
*
|
|
513
|
-
* Separated from the CLI so tests can drive it against a fixture tree and
|
|
514
|
-
* assert on the result rather than on stdout or an exit code.
|
|
515
|
-
*
|
|
516
|
-
* @param {object} cfg - From resolveConfig()
|
|
517
|
-
* @param {(msg: string) => void} [log] - Output sink; defaults to console.log
|
|
518
|
-
* @returns {Promise<{issues: Array<{file: string, detail: string}>, sourceCount: number, builtCount: number, haveBuild: boolean, missingRequiredBuild: boolean, ok: boolean}>} Outcome
|
|
519
|
-
*/
|
|
520
|
-
async function run(cfg, log = console.log) {
|
|
521
|
-
const issues = [];
|
|
522
|
-
const report = (file, detail) => issues.push({ file, detail });
|
|
523
|
-
|
|
524
|
-
log(`\nValidating documentation links in: ${cfg.projectRoot}`);
|
|
525
|
-
log(`Mode: ${cfg.strict ? 'STRICT (exit 1 on issues)' : 'ADVISORY'}`);
|
|
526
|
-
log('');
|
|
527
|
-
|
|
528
|
-
if (!fs.existsSync(cfg.docsDir)) {
|
|
529
|
-
throw new Error(`docs/ not found at ${cfg.docsDir}`);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const sourceCount = checkSource(cfg, report);
|
|
533
|
-
log(`Source pass: ${sourceCount} markdown file(s) in docs/`);
|
|
534
|
-
|
|
535
|
-
let builtCount = 0;
|
|
536
|
-
const haveBuild = fs.existsSync(cfg.buildDir);
|
|
537
|
-
let missingRequiredBuild = false;
|
|
538
|
-
|
|
539
|
-
if (haveBuild) {
|
|
540
|
-
const base = await resolveBase(cfg);
|
|
541
|
-
builtCount = checkBuilt(cfg, base, report);
|
|
542
|
-
log(`Built pass: ${builtCount} page(s) in build/site (base ${base})`);
|
|
543
|
-
|
|
544
|
-
const llmsCount = checkLlmsIndex(cfg, report);
|
|
545
|
-
log(`llms.txt: ${llmsCount} site link(s) checked`);
|
|
546
|
-
|
|
547
|
-
// This pass is only as honest as the build it reads. The manifest turns
|
|
548
|
-
// "may be stale" into a definite STALE where inputs provably changed; a
|
|
549
|
-
// matching manifest is still no freshness certificate, because Astro
|
|
550
|
-
// caches rendered content in website/node_modules/.astro and that cache
|
|
551
|
-
// survives both `rm -rf website/.astro` and a rebuild - a stale render can
|
|
552
|
-
// report a clean pass against unchanged inputs (this bit the author while
|
|
553
|
-
// writing this tool). CI is immune because `npm ci` starts cold.
|
|
554
|
-
// Only the hedged branch is suppressed under --require-build (the CI
|
|
555
|
-
// path); a provable STALE always speaks, since a cold CI build that
|
|
556
|
-
// still mismatches its inputs is worth saying out loud.
|
|
557
|
-
const freshness = buildInputsStatus(cfg);
|
|
558
|
-
if (freshness.state === 'stale') {
|
|
559
|
-
log('');
|
|
560
|
-
log(` STALE BUILD: ${freshness.changed.length} input(s) changed since this render:`);
|
|
561
|
-
for (const name of freshness.changed.slice(0, 5)) {
|
|
562
|
-
log(` - ${name}`);
|
|
563
|
-
}
|
|
564
|
-
if (freshness.changed.length > 5) {
|
|
565
|
-
log(` - (and ${freshness.changed.length - 5} more)`);
|
|
566
|
-
}
|
|
567
|
-
log(' This pass validated the OLD render. Rebuild before trusting it:');
|
|
568
|
-
log(' npm run docs:build');
|
|
569
|
-
} else if (!cfg.requireBuild) {
|
|
570
|
-
log('');
|
|
571
|
-
log(' Reading an existing build. If results look impossible, the render');
|
|
572
|
-
log(' may be cached. Clear it and rebuild:');
|
|
573
|
-
log(' rm -rf website/node_modules/.astro website/node_modules/.vite website/.astro build/site');
|
|
574
|
-
log(' npm run docs:build');
|
|
575
|
-
}
|
|
576
|
-
} else if (cfg.requireBuild) {
|
|
577
|
-
missingRequiredBuild = true;
|
|
578
|
-
report('build/site', 'no build found, and --require-build was given. Run `npm run docs:build` first.');
|
|
579
|
-
} else {
|
|
580
|
-
log('Built pass: SKIPPED (no build/site). This pass is what catches route bugs.');
|
|
581
|
-
log(' Run `npm run docs:build` first, or pass --require-build to enforce it.');
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
log('');
|
|
585
|
-
log('\u2500'.repeat(60));
|
|
586
|
-
log('');
|
|
587
|
-
|
|
588
|
-
if (issues.length === 0) {
|
|
589
|
-
log('Summary:');
|
|
590
|
-
log(` Markdown files: ${sourceCount}`);
|
|
591
|
-
log(` Built pages: ${builtCount}${haveBuild ? '' : ' (skipped)'}`);
|
|
592
|
-
log('');
|
|
593
|
-
|
|
594
|
-
// Never report an unqualified pass when the load-bearing pass did not run.
|
|
595
|
-
// A green line that means "half the checks were skipped" is the exact
|
|
596
|
-
// failure mode this tool exists to prevent, and reading one as a full pass
|
|
597
|
-
// is how the broken links reached production in the first place.
|
|
598
|
-
if (haveBuild) {
|
|
599
|
-
log(' All documentation links valid!');
|
|
600
|
-
} else {
|
|
601
|
-
log(' Source links valid. THE BUILT-OUTPUT PASS DID NOT RUN, so');
|
|
602
|
-
log(' route bugs (the class that shipped 404s) are unchecked here.');
|
|
603
|
-
log(' Run `npm run docs:build` first for the full check. CI always does.');
|
|
604
|
-
}
|
|
605
|
-
log('');
|
|
606
|
-
} else {
|
|
607
|
-
const byFile = new Map();
|
|
608
|
-
for (const issue of issues) {
|
|
609
|
-
if (!byFile.has(issue.file)) {
|
|
610
|
-
byFile.set(issue.file, []);
|
|
611
|
-
}
|
|
612
|
-
byFile.get(issue.file).push(issue.detail);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
log(`Broken documentation links: ${issues.length}\n`);
|
|
616
|
-
for (const [file, details] of byFile) {
|
|
617
|
-
log(` ${file}`);
|
|
618
|
-
for (const detail of details) {
|
|
619
|
-
log(` - ${detail}`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
log('');
|
|
623
|
-
|
|
624
|
-
if (cfg.verbose) {
|
|
625
|
-
log('A route that resolves to nothing is still valid HTML, so neither');
|
|
626
|
-
log('Astro nor markdownlint will fail on it. That is why this check exists.\n');
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// --require-build is a fail-closed contract in its own right: the flag exists
|
|
631
|
-
// to turn the skipped built pass into an error, so honouring it must not
|
|
632
|
-
// depend on --strict also being passed. Everything else stays advisory.
|
|
633
|
-
const ok = !(issues.length > 0 && (cfg.strict || missingRequiredBuild));
|
|
634
|
-
|
|
635
|
-
return { issues, sourceCount, builtCount, haveBuild, missingRequiredBuild, ok };
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
module.exports = {
|
|
639
|
-
resolveConfig,
|
|
640
|
-
run,
|
|
641
|
-
checkSource,
|
|
642
|
-
checkBuilt,
|
|
643
|
-
resolveBase,
|
|
644
|
-
blankCode,
|
|
645
|
-
assetUrls,
|
|
646
|
-
walk,
|
|
647
|
-
buildInputsStatus,
|
|
648
|
-
collectBuildInputs,
|
|
649
|
-
checkLlmsIndex,
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
/* c8 ignore start -- CLI entry point */
|
|
653
|
-
if (require.main === module) {
|
|
654
|
-
run(resolveConfig())
|
|
655
|
-
.then((result) => {
|
|
656
|
-
if (!result.ok) {
|
|
657
|
-
process.exit(1);
|
|
658
|
-
}
|
|
659
|
-
})
|
|
660
|
-
.catch((error) => {
|
|
661
|
-
console.error(error.message);
|
|
662
|
-
process.exit(1);
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
/* c8 ignore stop */
|