bmad-module-skill-forge 2.1.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 +51 -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 +14 -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 +117 -9
- package/src/shared/scripts/skf-detect-docs.py +892 -4
- 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 +215 -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 +17 -6
- 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 +55 -18
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
- 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 -89
- package/src/skf-setup/references/detect-and-tier.md +58 -66
- 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 -645
- 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
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF CCC Git Hygiene — keep ccc's index folders and SKF's workspace lock
|
|
6
|
+
out of git outside the project root.
|
|
7
|
+
|
|
8
|
+
ccc adds `/.cocoindex_code/` to `.gitignore` only when it creates a project
|
|
9
|
+
at the top of a git checkout whose `.git` is a folder. That leaves two gaps,
|
|
10
|
+
one per subcommand:
|
|
11
|
+
|
|
12
|
+
- In an SKF workspace clone the `.gitignore` ccc edits is usually
|
|
13
|
+
tracked, so the edit is a local change that makes git refuse a later
|
|
14
|
+
checkout of another ref, and SKF's `.skf-workspace.lock` shows as
|
|
15
|
+
untracked.
|
|
16
|
+
- A ccc project in a subfolder of a checkout, in a linked worktree or in
|
|
17
|
+
a submodule gets no entry at all, so git lists its index database as
|
|
18
|
+
untracked and a `git add -A` would commit it.
|
|
19
|
+
|
|
20
|
+
CLI:
|
|
21
|
+
|
|
22
|
+
uv run skf-ccc-git-hygiene.py workspace --repo <clone path>
|
|
23
|
+
uv run skf-ccc-git-hygiene.py nested --dir <source folder> --project-root <project root>
|
|
24
|
+
|
|
25
|
+
`~` is expanded in --repo, --dir, --project-root and SKF_WORKSPACE.
|
|
26
|
+
|
|
27
|
+
workspace, in order:
|
|
28
|
+
|
|
29
|
+
1. Guard: act only when --repo lies under
|
|
30
|
+
`{SKF_WORKSPACE or ~/.skf/workspace}/repos/` and is the top level of its
|
|
31
|
+
git work tree (`git rev-parse --show-toplevel`). Anything else is
|
|
32
|
+
skipped without a write.
|
|
33
|
+
2. Exclude: append `.cocoindex_code/` (unanchored, so an index in a
|
|
34
|
+
subfolder is covered too) and `/.skf-workspace.lock`, under an SKF
|
|
35
|
+
header, to the file `git rev-parse --git-path info/exclude` names (the
|
|
36
|
+
common one for a linked worktree). An entry already there as an exact
|
|
37
|
+
line is not added again. This runs before any `.gitignore` change, so
|
|
38
|
+
the index folder never shows between the two steps; when it fails,
|
|
39
|
+
`.gitignore` is left alone.
|
|
40
|
+
3. `.gitignore` repair: when the working-tree `.gitignore` is exactly what
|
|
41
|
+
ccc's append makes of the committed one (line endings normalized on
|
|
42
|
+
both sides; ccc writes nothing when its entry is already a line),
|
|
43
|
+
restore it with `git checkout -- .gitignore`, which reapplies autocrlf
|
|
44
|
+
and eol attributes. When `.gitignore` is untracked and holds exactly
|
|
45
|
+
ccc's two lines, delete it. Any other change, a link, conflict stages,
|
|
46
|
+
an unusual mode or a deleted tracked file is kept.
|
|
47
|
+
|
|
48
|
+
nested:
|
|
49
|
+
|
|
50
|
+
Skips a missing folder, the project root, a `.cocoindex_code` link and a
|
|
51
|
+
folder without `.cocoindex_code/settings.yml`. It then runs
|
|
52
|
+
`git check-ignore -q --no-index` for `settings.yml`, `target_sqlite.db`
|
|
53
|
+
and `cocoindex.db/mdb/data.mdb` under `.cocoindex_code/`, one call per
|
|
54
|
+
path (`-q` refuses several). When any of them is not ignored, it creates
|
|
55
|
+
`.cocoindex_code/.gitignore` holding `*` so the folder ignores itself.
|
|
56
|
+
The write is create-only: an existing file is never overwritten, and a
|
|
57
|
+
write that fails removes the partial file it created. When git
|
|
58
|
+
already tracks index files there, the notice names the
|
|
59
|
+
`git rm -r --cached` remedy. It never reads or writes the project's
|
|
60
|
+
`.gitignore` or any `info/exclude`.
|
|
61
|
+
|
|
62
|
+
Output: one JSON object on one stdout line (ASCII).
|
|
63
|
+
|
|
64
|
+
workspace:
|
|
65
|
+
mode "workspace"
|
|
66
|
+
status "ok" | "skipped"
|
|
67
|
+
skip_reason null | "missing" | "not-a-workspace-clone" |
|
|
68
|
+
"git-unavailable" | "git-failed" | "not-a-clone-root"
|
|
69
|
+
repo --repo as given
|
|
70
|
+
exclude_path absolute path of the exclude file, or null
|
|
71
|
+
exclude_added the entries appended by this run, in order
|
|
72
|
+
gitignore_action null when skipped | "none" | "restored" | "deleted" |
|
|
73
|
+
"kept"
|
|
74
|
+
warnings one entry per failed sub-step
|
|
75
|
+
|
|
76
|
+
nested:
|
|
77
|
+
mode "nested"
|
|
78
|
+
status "ok" | "skipped"
|
|
79
|
+
skip_reason null | "missing" | "project-root" |
|
|
80
|
+
"index-dir-is-link" | "no-ccc-project" |
|
|
81
|
+
"git-unavailable" | "not-a-work-tree" | "git-failed"
|
|
82
|
+
index_dir --dir as given plus "/.cocoindex_code"
|
|
83
|
+
action null when skipped | "none" | "wrote" | "exists" |
|
|
84
|
+
"failed"
|
|
85
|
+
tracked_db_files tracked paths under the folder other than
|
|
86
|
+
settings.yml and .gitignore
|
|
87
|
+
notice one line for the user, or null
|
|
88
|
+
warnings one entry per failed sub-step
|
|
89
|
+
|
|
90
|
+
Exit codes:
|
|
91
|
+
0 every "ok" and "skipped" result, including partial failures reported
|
|
92
|
+
in `warnings`
|
|
93
|
+
1 unexpected error: {"status": "error", "message": ...} on stderr,
|
|
94
|
+
nothing on stdout
|
|
95
|
+
2 usage error (argparse)
|
|
96
|
+
|
|
97
|
+
git runs with the git location variables (GIT_DIR, GIT_INDEX_FILE, ...)
|
|
98
|
+
removed from its environment. A git hook or `git rebase --exec` exports
|
|
99
|
+
them (GIT_DIR too when run in a linked worktree); inherited by
|
|
100
|
+
`git -C <clone>`, they would make the helper touch another repository.
|
|
101
|
+
|
|
102
|
+
It never edits a project's own `.gitignore`: in a workspace clone it only
|
|
103
|
+
restores the committed file or deletes the one ccc created, and elsewhere
|
|
104
|
+
it writes only inside a ccc index folder.
|
|
105
|
+
|
|
106
|
+
It never gates: callers treat any non-zero exit or missing JSON as "did not
|
|
107
|
+
run" and continue.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
from __future__ import annotations
|
|
111
|
+
|
|
112
|
+
import argparse
|
|
113
|
+
import contextlib
|
|
114
|
+
import json
|
|
115
|
+
import os
|
|
116
|
+
import shutil
|
|
117
|
+
import subprocess
|
|
118
|
+
import sys
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
|
|
121
|
+
CCC_DIR = ".cocoindex_code"
|
|
122
|
+
CCC_ENTRY = "/.cocoindex_code/"
|
|
123
|
+
CCC_BLOCK = b"# CocoIndex Code (ccc)\n/.cocoindex_code/\n" # what ccc init appends
|
|
124
|
+
EXCLUDE_HEADER = "# SKF: ccc index folders and the workspace lock stay out of git"
|
|
125
|
+
EXCLUDE_ENTRIES = (".cocoindex_code/", "/.skf-workspace.lock")
|
|
126
|
+
INDEX_PROBES = (
|
|
127
|
+
".cocoindex_code/settings.yml",
|
|
128
|
+
".cocoindex_code/target_sqlite.db",
|
|
129
|
+
".cocoindex_code/cocoindex.db/mdb/data.mdb",
|
|
130
|
+
)
|
|
131
|
+
SELF_IGNORE = b"# Created by SKF so git ignores this ccc index folder.\n*\n"
|
|
132
|
+
NOT_DB_FILES = {".cocoindex_code/settings.yml", ".cocoindex_code/.gitignore"}
|
|
133
|
+
GIT_TIMEOUT_SEC = 30
|
|
134
|
+
# Keep identical to GIT_LOCATION_VARS in skf-check-workspace-drift.py.
|
|
135
|
+
GIT_LOCATION_VARS = (
|
|
136
|
+
"GIT_DIR",
|
|
137
|
+
"GIT_WORK_TREE",
|
|
138
|
+
"GIT_INDEX_FILE",
|
|
139
|
+
"GIT_PREFIX",
|
|
140
|
+
"GIT_COMMON_DIR",
|
|
141
|
+
"GIT_OBJECT_DIRECTORY",
|
|
142
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
143
|
+
"GIT_NAMESPACE",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Keep identical to _is_link_or_junction in skf-atomic-write.py
|
|
148
|
+
# (test/test-skf-ccc-git-hygiene.py pins the copy).
|
|
149
|
+
def _is_link_or_junction(p: Path) -> bool:
|
|
150
|
+
"""True for POSIX symlinks AND Windows junctions/symlinks.
|
|
151
|
+
|
|
152
|
+
`Path.is_symlink()` is False for Windows junctions; os.readlink succeeds
|
|
153
|
+
for both symlinks and junctions (since CPython 3.8 on Windows). A regular
|
|
154
|
+
directory raises OSError on readlink, which is the signal we want to
|
|
155
|
+
refuse replacement. On Windows, any other reparse point (a cloud-sync
|
|
156
|
+
placeholder, a deduplicated file, an app execution alias) raises
|
|
157
|
+
ValueError: it does not redirect to another path, so it is not a link.
|
|
158
|
+
"""
|
|
159
|
+
if p.is_symlink():
|
|
160
|
+
return True
|
|
161
|
+
if not p.exists() and not p.is_symlink():
|
|
162
|
+
return False
|
|
163
|
+
try:
|
|
164
|
+
os.readlink(p)
|
|
165
|
+
return True
|
|
166
|
+
except (OSError, ValueError):
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _resolve_outside_cwd(command: str) -> str | None:
|
|
171
|
+
"""shutil.which with a CWD-shim guard. Returns the resolved path or None.
|
|
172
|
+
|
|
173
|
+
shutil.which on Windows searches the current directory ahead of PATH,
|
|
174
|
+
and CWD here may be a repository SKF does not control — a bare-name
|
|
175
|
+
lookup resolving into CWD would execute a planted shim (e.g. git.exe).
|
|
176
|
+
Such a resolution is treated as not-found. Explicit paths supplied by
|
|
177
|
+
callers (containing a separator) are honored as-is. Keep the code
|
|
178
|
+
identical to the sibling guards in skf-merge-ccc-exclusions.py,
|
|
179
|
+
skf-detect-tools.py, skf-qmd-classify-collections.py,
|
|
180
|
+
skf-source-tree.py, skf-tessl-review.py and
|
|
181
|
+
skf-verify-provenance-completeness.py
|
|
182
|
+
(test/test-skf-ccc-git-hygiene.py pins it against
|
|
183
|
+
skf-merge-ccc-exclusions.py).
|
|
184
|
+
"""
|
|
185
|
+
resolved = shutil.which(command)
|
|
186
|
+
if resolved is None:
|
|
187
|
+
return None
|
|
188
|
+
if os.sep in command or (os.altsep and os.altsep in command):
|
|
189
|
+
return resolved
|
|
190
|
+
resolved_dir = os.path.dirname(resolved)
|
|
191
|
+
if resolved_dir:
|
|
192
|
+
cwd = os.path.normcase(os.path.abspath(os.getcwd()))
|
|
193
|
+
if os.path.normcase(os.path.abspath(resolved_dir)) == cwd:
|
|
194
|
+
return None
|
|
195
|
+
return resolved
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _git(root: Path, *args: str) -> tuple[int, bytes, str] | None:
|
|
199
|
+
"""Run `git -C <root> <args>` with the git location variables removed.
|
|
200
|
+
|
|
201
|
+
Returns None when git cannot be found (or only as a shim in CWD) or
|
|
202
|
+
cannot start, (-1, b"", "timeout") on a timeout, and otherwise
|
|
203
|
+
(returncode, stdout bytes, stderr text). LC_ALL=C keeps git's messages
|
|
204
|
+
in one locale.
|
|
205
|
+
"""
|
|
206
|
+
exe = _resolve_outside_cwd("git")
|
|
207
|
+
if exe is None:
|
|
208
|
+
return None
|
|
209
|
+
env = {k: v for k, v in os.environ.items() if k not in GIT_LOCATION_VARS}
|
|
210
|
+
env["LC_ALL"] = "C"
|
|
211
|
+
try:
|
|
212
|
+
result = subprocess.run(
|
|
213
|
+
[exe, "-C", str(root), *args],
|
|
214
|
+
stdin=subprocess.DEVNULL,
|
|
215
|
+
capture_output=True,
|
|
216
|
+
env=env,
|
|
217
|
+
timeout=GIT_TIMEOUT_SEC,
|
|
218
|
+
)
|
|
219
|
+
except subprocess.TimeoutExpired:
|
|
220
|
+
return -1, b"", "timeout"
|
|
221
|
+
except (OSError, ValueError):
|
|
222
|
+
return None
|
|
223
|
+
return result.returncode, result.stdout, result.stderr.decode("utf-8", errors="replace")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _same_path(a: str | Path, b: str | Path) -> bool:
|
|
227
|
+
"""Path identity that covers Windows case and 8.3 names, forward-slash
|
|
228
|
+
git output and macOS /private."""
|
|
229
|
+
return os.path.normcase(os.path.realpath(a)) == os.path.normcase(os.path.realpath(b))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _norm(b: bytes) -> bytes:
|
|
233
|
+
return b.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def is_ccc_edit(committed: bytes | None, worktree: bytes) -> bool:
|
|
237
|
+
"""True when `worktree` is exactly what ccc's .gitignore append makes of `committed`
|
|
238
|
+
(None = no tracked .gitignore). Line endings are normalized on both sides, so ccc's
|
|
239
|
+
text-mode rewrite (LF on POSIX, CRLF throughout on Windows) matches."""
|
|
240
|
+
w = _norm(worktree)
|
|
241
|
+
if committed is None:
|
|
242
|
+
return w == CCC_BLOCK
|
|
243
|
+
base = _norm(committed)
|
|
244
|
+
if CCC_ENTRY in base.decode("utf-8", "surrogateescape").splitlines():
|
|
245
|
+
return False # ccc writes nothing when its entry is already a line
|
|
246
|
+
sep = b"\n" if base and not base.endswith(b"\n") else b""
|
|
247
|
+
return w == base + sep + CCC_BLOCK
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _ensure_excludes(path: Path, warnings: list[str]) -> list[str] | None:
|
|
251
|
+
"""Append the EXCLUDE_ENTRIES missing from `path` as exact lines.
|
|
252
|
+
|
|
253
|
+
Returns the entries added ([] when all were there), or None after a
|
|
254
|
+
read or write failure, which is recorded in `warnings`.
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
try:
|
|
258
|
+
cur = path.read_bytes()
|
|
259
|
+
except FileNotFoundError:
|
|
260
|
+
cur = b""
|
|
261
|
+
text = _norm(cur).decode("utf-8", "surrogateescape")
|
|
262
|
+
present = {line.rstrip(" \t") for line in text.split("\n")}
|
|
263
|
+
missing = [entry for entry in EXCLUDE_ENTRIES if entry not in present]
|
|
264
|
+
if not missing:
|
|
265
|
+
return []
|
|
266
|
+
lead = "\n" if cur and not cur.endswith((b"\n", b"\r")) else ""
|
|
267
|
+
block = lead + EXCLUDE_HEADER + "\n" + "".join(entry + "\n" for entry in missing)
|
|
268
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
# O_BINARY (Windows only; 0 elsewhere) keeps the LF line endings.
|
|
270
|
+
fd = os.open(
|
|
271
|
+
path,
|
|
272
|
+
os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_BINARY", 0),
|
|
273
|
+
0o644,
|
|
274
|
+
)
|
|
275
|
+
try:
|
|
276
|
+
os.write(fd, block.encode("utf-8"))
|
|
277
|
+
finally:
|
|
278
|
+
os.close(fd)
|
|
279
|
+
except OSError as e:
|
|
280
|
+
warnings.append(f"could not update {path}: {e.strerror or e}")
|
|
281
|
+
return None
|
|
282
|
+
return missing
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _repair_gitignore(repo: Path, warnings: list[str]) -> str:
|
|
286
|
+
"""Undo ccc's edit to the clone's top-level .gitignore; see the module docstring."""
|
|
287
|
+
gi = repo / ".gitignore"
|
|
288
|
+
if _is_link_or_junction(gi):
|
|
289
|
+
return "kept"
|
|
290
|
+
res = _git(repo, "ls-files", "-z", "--stage", "--", ".gitignore")
|
|
291
|
+
if res is None or res[0] != 0:
|
|
292
|
+
warnings.append("git ls-files failed for .gitignore; left it alone")
|
|
293
|
+
return "kept"
|
|
294
|
+
entries = [e for e in res[1].split(b"\0") if e]
|
|
295
|
+
try:
|
|
296
|
+
if not entries: # untracked
|
|
297
|
+
if not gi.exists():
|
|
298
|
+
return "none"
|
|
299
|
+
if not gi.is_file():
|
|
300
|
+
return "kept"
|
|
301
|
+
if not is_ccc_edit(None, gi.read_bytes()):
|
|
302
|
+
return "kept"
|
|
303
|
+
gi.unlink()
|
|
304
|
+
return "deleted"
|
|
305
|
+
if len(entries) != 1: # conflict stages
|
|
306
|
+
return "kept"
|
|
307
|
+
meta = entries[0].split(b"\t", 1)[0].split(b" ")
|
|
308
|
+
if len(meta) != 3:
|
|
309
|
+
return "kept"
|
|
310
|
+
mode, sha, stage = meta
|
|
311
|
+
if stage != b"0" or mode not in (b"100644", b"100755"):
|
|
312
|
+
return "kept"
|
|
313
|
+
if not gi.is_file(): # a deleted tracked file is someone else's change
|
|
314
|
+
return "kept"
|
|
315
|
+
blob = _git(repo, "cat-file", "blob", sha.decode("ascii"))
|
|
316
|
+
if blob is None or blob[0] != 0:
|
|
317
|
+
warnings.append("git cat-file failed for .gitignore; left it alone")
|
|
318
|
+
return "kept"
|
|
319
|
+
worktree = gi.read_bytes()
|
|
320
|
+
except OSError as e:
|
|
321
|
+
warnings.append(f"could not read or delete {gi}: {e.strerror or e}")
|
|
322
|
+
return "kept"
|
|
323
|
+
if _norm(worktree) == _norm(blob[1]):
|
|
324
|
+
return "none" # unchanged, or line endings only: left alone
|
|
325
|
+
if not is_ccc_edit(blob[1], worktree):
|
|
326
|
+
return "kept"
|
|
327
|
+
res = _git(repo, "checkout", "-q", "--", ".gitignore")
|
|
328
|
+
if res is None or res[0] != 0:
|
|
329
|
+
warnings.append("git checkout -- .gitignore failed; left it alone")
|
|
330
|
+
return "kept"
|
|
331
|
+
return "restored"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def workspace(repo_arg: str) -> dict:
|
|
335
|
+
"""The `workspace` subcommand; returns the output object."""
|
|
336
|
+
out = {
|
|
337
|
+
"mode": "workspace",
|
|
338
|
+
"status": "skipped",
|
|
339
|
+
"skip_reason": None,
|
|
340
|
+
"repo": repo_arg,
|
|
341
|
+
"exclude_path": None,
|
|
342
|
+
"exclude_added": [],
|
|
343
|
+
"gitignore_action": None,
|
|
344
|
+
"warnings": [],
|
|
345
|
+
}
|
|
346
|
+
repo = Path(os.path.expanduser(repo_arg))
|
|
347
|
+
if not repo.is_dir():
|
|
348
|
+
out["skip_reason"] = "missing"
|
|
349
|
+
return out
|
|
350
|
+
ws = os.environ.get("SKF_WORKSPACE") or "~/.skf/workspace"
|
|
351
|
+
repos_root = os.path.normcase(os.path.realpath(os.path.join(os.path.expanduser(ws), "repos")))
|
|
352
|
+
real = os.path.normcase(os.path.realpath(repo))
|
|
353
|
+
try:
|
|
354
|
+
inside = os.path.commonpath([real, repos_root]) == repos_root and real != repos_root
|
|
355
|
+
except ValueError: # different drives on Windows
|
|
356
|
+
inside = False
|
|
357
|
+
if not inside:
|
|
358
|
+
out["skip_reason"] = "not-a-workspace-clone"
|
|
359
|
+
return out
|
|
360
|
+
top = _git(repo, "rev-parse", "--show-toplevel")
|
|
361
|
+
if top is None:
|
|
362
|
+
out["skip_reason"] = "git-unavailable"
|
|
363
|
+
return out
|
|
364
|
+
if top[0] == -1:
|
|
365
|
+
out["skip_reason"] = "git-failed"
|
|
366
|
+
return out
|
|
367
|
+
if top[0] != 0 or os.path.normcase(os.path.realpath(os.fsdecode(top[1].strip()))) != real:
|
|
368
|
+
out["skip_reason"] = "not-a-clone-root"
|
|
369
|
+
return out
|
|
370
|
+
out["status"] = "ok"
|
|
371
|
+
added = None
|
|
372
|
+
gp = _git(repo, "rev-parse", "--git-path", "info/exclude")
|
|
373
|
+
if gp is None or gp[0] != 0:
|
|
374
|
+
out["warnings"].append("git rev-parse --git-path info/exclude failed")
|
|
375
|
+
else:
|
|
376
|
+
path = Path(os.fsdecode(gp[1].strip()))
|
|
377
|
+
if not path.is_absolute():
|
|
378
|
+
path = repo / path
|
|
379
|
+
out["exclude_path"] = os.path.abspath(path)
|
|
380
|
+
added = _ensure_excludes(path, out["warnings"])
|
|
381
|
+
if added is None:
|
|
382
|
+
out["gitignore_action"] = "kept"
|
|
383
|
+
out["warnings"].append(
|
|
384
|
+
".gitignore left alone: restoring it without the exclude would show the index folder"
|
|
385
|
+
)
|
|
386
|
+
return out
|
|
387
|
+
out["exclude_added"] = added
|
|
388
|
+
out["gitignore_action"] = _repair_gitignore(repo, out["warnings"])
|
|
389
|
+
return out
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _nested_notice(action: str, index_dir: str, tracked: int, reason: str = "") -> str | None:
|
|
393
|
+
if action == "wrote" and tracked == 0:
|
|
394
|
+
return (
|
|
395
|
+
f"Wrote {index_dir}/.gitignore so git ignores the ccc index in that folder; "
|
|
396
|
+
f"your own .gitignore files are unchanged."
|
|
397
|
+
)
|
|
398
|
+
if action == "wrote":
|
|
399
|
+
return (
|
|
400
|
+
f"Wrote {index_dir}/.gitignore so git ignores new ccc index files in that folder, "
|
|
401
|
+
f"but git already tracks {tracked} of its files; to stop tracking them, run: "
|
|
402
|
+
f"git rm -r --cached -- \"{index_dir}\""
|
|
403
|
+
)
|
|
404
|
+
if action == "exists":
|
|
405
|
+
return (
|
|
406
|
+
f"git lists files in {index_dir} as untracked, and the .gitignore already in that "
|
|
407
|
+
f"folder does not ignore them; add a line holding only * to {index_dir}/.gitignore."
|
|
408
|
+
)
|
|
409
|
+
if action == "failed":
|
|
410
|
+
return (
|
|
411
|
+
f"Could not write {index_dir}/.gitignore ({reason}); git lists the ccc index in "
|
|
412
|
+
f"that folder as untracked. Create that file holding the line * to keep it out of git."
|
|
413
|
+
)
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def nested(dir_arg: str, project_root: str) -> dict:
|
|
418
|
+
"""The `nested` subcommand; returns the output object."""
|
|
419
|
+
index_dir = dir_arg.rstrip("/\\") + "/" + CCC_DIR
|
|
420
|
+
out = {
|
|
421
|
+
"mode": "nested",
|
|
422
|
+
"status": "skipped",
|
|
423
|
+
"skip_reason": None,
|
|
424
|
+
"index_dir": index_dir,
|
|
425
|
+
"action": None,
|
|
426
|
+
"tracked_db_files": 0,
|
|
427
|
+
"notice": None,
|
|
428
|
+
"warnings": [],
|
|
429
|
+
}
|
|
430
|
+
d = Path(os.path.expanduser(dir_arg))
|
|
431
|
+
if not d.is_dir():
|
|
432
|
+
out["skip_reason"] = "missing"
|
|
433
|
+
return out
|
|
434
|
+
if _same_path(d, os.path.expanduser(project_root)):
|
|
435
|
+
out["skip_reason"] = "project-root"
|
|
436
|
+
return out
|
|
437
|
+
idx = d / CCC_DIR
|
|
438
|
+
if _is_link_or_junction(idx):
|
|
439
|
+
out["skip_reason"] = "index-dir-is-link"
|
|
440
|
+
return out
|
|
441
|
+
if not (idx / "settings.yml").is_file():
|
|
442
|
+
out["skip_reason"] = "no-ccc-project"
|
|
443
|
+
return out
|
|
444
|
+
listed = False
|
|
445
|
+
for probe in INDEX_PROBES: # one call per path: -q refuses several
|
|
446
|
+
res = _git(d, "check-ignore", "-q", "--no-index", "--", probe)
|
|
447
|
+
if res is None:
|
|
448
|
+
out["skip_reason"] = "git-unavailable"
|
|
449
|
+
return out
|
|
450
|
+
if res[0] == 1:
|
|
451
|
+
listed = True
|
|
452
|
+
elif res[0] == 128:
|
|
453
|
+
out["skip_reason"] = "not-a-work-tree"
|
|
454
|
+
return out
|
|
455
|
+
elif res[0] != 0:
|
|
456
|
+
out["skip_reason"] = "git-failed"
|
|
457
|
+
return out
|
|
458
|
+
out["status"] = "ok"
|
|
459
|
+
if not listed:
|
|
460
|
+
out["action"] = "none"
|
|
461
|
+
return out
|
|
462
|
+
tracked = _git(d, "ls-files", "-z", "--", CCC_DIR) # paths relative to d
|
|
463
|
+
if tracked is not None and tracked[0] == 0:
|
|
464
|
+
files = [os.fsdecode(f) for f in tracked[1].split(b"\0") if f]
|
|
465
|
+
out["tracked_db_files"] = sum(1 for f in files if f not in NOT_DB_FILES)
|
|
466
|
+
target = idx / ".gitignore"
|
|
467
|
+
try:
|
|
468
|
+
fd = os.open(
|
|
469
|
+
target,
|
|
470
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
|
|
471
|
+
0o644,
|
|
472
|
+
)
|
|
473
|
+
except FileExistsError:
|
|
474
|
+
out["action"] = "exists"
|
|
475
|
+
out["notice"] = _nested_notice("exists", index_dir, out["tracked_db_files"])
|
|
476
|
+
return out
|
|
477
|
+
except OSError as e:
|
|
478
|
+
return _nested_failed(out, e.strerror or str(e))
|
|
479
|
+
reason = None
|
|
480
|
+
try:
|
|
481
|
+
try:
|
|
482
|
+
if os.write(fd, SELF_IGNORE) != len(SELF_IGNORE):
|
|
483
|
+
reason = "short write"
|
|
484
|
+
finally:
|
|
485
|
+
os.close(fd)
|
|
486
|
+
except OSError as e:
|
|
487
|
+
reason = e.strerror or str(e)
|
|
488
|
+
if reason is not None:
|
|
489
|
+
# Remove the partial file this run created, so the next run writes it
|
|
490
|
+
# again instead of reporting a foreign .gitignore.
|
|
491
|
+
with contextlib.suppress(OSError):
|
|
492
|
+
os.unlink(target)
|
|
493
|
+
return _nested_failed(out, reason)
|
|
494
|
+
out["action"] = "wrote"
|
|
495
|
+
out["notice"] = _nested_notice("wrote", index_dir, out["tracked_db_files"])
|
|
496
|
+
return out
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _nested_failed(out: dict, reason: str) -> dict:
|
|
500
|
+
"""Record a failed create or write of the self-ignoring file in `out`."""
|
|
501
|
+
index_dir = out["index_dir"]
|
|
502
|
+
out["action"] = "failed"
|
|
503
|
+
out["warnings"].append(f"could not create {index_dir}/.gitignore: {reason}")
|
|
504
|
+
out["notice"] = _nested_notice("failed", index_dir, 0, reason)
|
|
505
|
+
return out
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def main(argv: list[str] | None = None) -> int:
|
|
509
|
+
parser = argparse.ArgumentParser(
|
|
510
|
+
description="Keep ccc's index folders and SKF's workspace lock out of git.",
|
|
511
|
+
)
|
|
512
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
513
|
+
ws = sub.add_parser("workspace", help="repair and exclude in an SKF workspace clone")
|
|
514
|
+
ws.add_argument("--repo", required=True, help="the workspace clone's top-level folder")
|
|
515
|
+
ne = sub.add_parser("nested", help="make a nested ccc index folder ignore itself")
|
|
516
|
+
ne.add_argument("--dir", required=True, help="the local source folder ccc indexed")
|
|
517
|
+
ne.add_argument("--project-root", required=True, help="the SKF project root")
|
|
518
|
+
args = parser.parse_args(argv)
|
|
519
|
+
try:
|
|
520
|
+
if args.command == "workspace":
|
|
521
|
+
result = workspace(args.repo)
|
|
522
|
+
else:
|
|
523
|
+
result = nested(args.dir, args.project_root)
|
|
524
|
+
except Exception as e: # noqa: BLE001 — any failure means "did not run"
|
|
525
|
+
print(json.dumps({"status": "error", "message": f"{type(e).__name__}: {e}"}), file=sys.stderr)
|
|
526
|
+
return 1
|
|
527
|
+
print(json.dumps(result))
|
|
528
|
+
return 0
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
sys.exit(main())
|