bmad-module-skill-forge 1.4.1 → 1.5.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 +0 -8
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/STABILITY.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/examples.md +1 -1
- package/docs/workflows.md +5 -5
- package/package.json +2 -2
- package/src/README.md +1 -1
- package/src/knowledge/ccc-bridge.md +12 -12
- package/src/knowledge/qmd-registry.md +5 -5
- package/src/shared/health-check.md +5 -5
- package/src/shared/references/description-guard-protocol.md +100 -0
- package/src/shared/references/output-contract-schema.md +1 -1
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
- package/src/shared/scripts/skf-build-change-manifest.py +420 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
- package/src/shared/scripts/skf-description-guard.py +359 -0
- package/src/shared/scripts/skf-detect-language.py +2 -2
- package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
- package/src/shared/scripts/skf-detect-tools.py +97 -8
- package/src/shared/scripts/skf-detect-workspaces.py +1 -1
- package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
- package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
- package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
- package/src/shared/scripts/skf-hash-content.py +284 -0
- package/src/shared/scripts/skf-load-provenance.py +295 -0
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
- package/src/shared/scripts/skf-pair-intersect.py +250 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
- package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
- package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
- package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
- package/src/shared/scripts/skf-scan-manifests.py +738 -0
- package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
- package/src/shared/scripts/skf-update-active-symlink.py +315 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
- package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
- package/src/shared/scripts/skf-write-skill-brief.py +61 -5
- package/src/skf-analyze-source/SKILL.md +69 -16
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/customize.toml +55 -0
- package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
- package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
- package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
- package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +44 -14
- package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +27 -6
- package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
- package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
- package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +12 -10
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
- package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
- package/src/skf-audit-skill/SKILL.md +66 -15
- package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
- package/src/skf-audit-skill/customize.toml +49 -0
- package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
- package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +57 -19
- package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
- package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
- package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
- package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
- package/src/skf-audit-skill/references/severity-rules.md +2 -0
- package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +29 -17
- package/src/skf-brief-skill/SKILL.md +48 -18
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
- package/src/skf-brief-skill/customize.toml +44 -0
- package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +5 -5
- package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
- package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
- package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +24 -24
- package/src/skf-brief-skill/references/headless-args.md +9 -9
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
- package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
- package/src/skf-brief-skill/references/qmd-collection-registration.md +1 -1
- package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +19 -16
- package/src/skf-brief-skill/references/version-resolution.md +2 -2
- package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +11 -10
- package/src/skf-create-skill/SKILL.md +25 -19
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
- package/src/skf-create-skill/assets/skill-sections.md +5 -5
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
- package/src/skf-create-skill/customize.toml +35 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
- package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
- package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
- package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
- package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
- package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
- package/src/skf-create-skill/references/extraction-patterns.md +8 -8
- package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
- package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
- package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
- package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
- package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
- package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
- package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
- package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
- package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +46 -26
- package/src/skf-create-stack-skill/SKILL.md +76 -17
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +46 -0
- package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
- package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +32 -11
- package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +28 -28
- package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
- package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
- package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
- package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
- package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +40 -17
- package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
- package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
- package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
- package/src/skf-drop-skill/SKILL.md +84 -12
- package/src/skf-drop-skill/customize.toml +57 -0
- package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
- package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
- package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
- package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
- package/src/skf-export-skill/SKILL.md +85 -16
- package/src/skf-export-skill/assets/managed-section-format.md +3 -3
- package/src/skf-export-skill/assets/snippet-format.md +2 -2
- package/src/skf-export-skill/customize.toml +49 -0
- package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
- package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
- package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
- package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
- package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
- package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
- package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
- package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +74 -0
- package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +20 -8
- package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
- package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
- package/src/skf-quick-skill/SKILL.md +63 -123
- package/src/skf-quick-skill/customize.toml +44 -0
- package/src/skf-quick-skill/references/batch-mode.md +102 -0
- package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
- package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
- package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
- package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
- package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
- package/src/skf-quick-skill/references/registry-resolution.md +2 -0
- package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
- package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
- package/src/skf-refine-architecture/SKILL.md +86 -16
- package/src/skf-refine-architecture/customize.toml +49 -0
- package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
- package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
- package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
- package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
- package/src/skf-refine-architecture/references/init.md +144 -0
- package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
- package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
- package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
- package/src/skf-rename-skill/SKILL.md +82 -12
- package/src/skf-rename-skill/customize.toml +52 -0
- package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
- package/src/skf-rename-skill/references/health-check.md +30 -0
- package/src/skf-rename-skill/references/rebuild-context.md +110 -0
- package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
- package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
- package/src/skf-setup/SKILL.md +35 -35
- package/src/skf-setup/customize.toml +33 -0
- package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
- package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
- package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
- package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
- package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
- package/src/skf-setup/references/tier-rules.md +1 -1
- package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
- package/src/skf-test-skill/SKILL.md +77 -15
- package/src/skf-test-skill/customize.toml +54 -0
- package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +28 -71
- package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
- package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
- package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
- package/src/skf-test-skill/references/health-check.md +14 -0
- package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
- package/src/skf-test-skill/references/migration-section-rules.md +4 -2
- package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
- package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
- package/src/skf-test-skill/references/scoring-rules.md +8 -6
- package/src/skf-test-skill/references/source-access-protocol.md +7 -5
- package/src/skf-test-skill/scripts/compute-score.py +88 -17
- package/src/skf-test-skill/templates/test-report-template.md +13 -13
- package/src/skf-update-skill/SKILL.md +23 -14
- package/src/skf-update-skill/customize.toml +44 -0
- package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
- package/src/skf-update-skill/references/health-check.md +32 -0
- package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
- package/src/skf-update-skill/references/manual-section-rules.md +4 -0
- package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
- package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
- package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
- package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
- package/src/skf-update-skill/references/report.md +214 -0
- package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
- package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
- package/src/skf-verify-stack/SKILL.md +89 -17
- package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
- package/src/skf-verify-stack/customize.toml +50 -0
- package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
- package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +7 -7
- package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
- package/src/skf-verify-stack/references/init.md +166 -0
- package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
- package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +12 -12
- package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +22 -41
- package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +7 -7
- package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +8 -8
- package/tools/validate-docs-drift.js +1 -1
- package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
- package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
- package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
- package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
- package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
- package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Resolve Authoritative Files — §2a deterministic pre-prompt pipeline.
|
|
6
|
+
|
|
7
|
+
`skf-create-skill/references/extract.md §2a "Discovered Authoritative Files
|
|
8
|
+
Protocol"` performs five deterministic phases before the LLM can prompt the
|
|
9
|
+
user on each candidate:
|
|
10
|
+
|
|
11
|
+
1. **Heuristic scan** — walk the source tree, find files whose case-
|
|
12
|
+
insensitive basename matches the auth-doc heuristic list (`llms.txt`,
|
|
13
|
+
`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, etc.).
|
|
14
|
+
2. **Scope diff** — for each match, decide whether it is already in
|
|
15
|
+
§2's filtered list (matched by `scope.include` AND not by
|
|
16
|
+
`scope.exclude`) or excluded.
|
|
17
|
+
3. **Amendment reconciliation** — consult `brief.scope.amendments[]`
|
|
18
|
+
for a prior decision on this path.
|
|
19
|
+
4. **Preview load** — for unresolved candidates, read the first 20
|
|
20
|
+
lines and capture line_count + size_bytes for the prompt.
|
|
21
|
+
5. **Content hash** — SHA-256 of the file's bytes (needed for any
|
|
22
|
+
candidate that will populate `promoted_docs[]`).
|
|
23
|
+
|
|
24
|
+
The prose previously asked the LLM to chain all five per run, with the
|
|
25
|
+
SHA-256 hashing and the scope-filter glob matching being the most
|
|
26
|
+
drift-prone parts. The helper consolidates them into one `resolve` call.
|
|
27
|
+
|
|
28
|
+
Subcommand:
|
|
29
|
+
resolve --source-root <path> --brief <brief.yaml-path>
|
|
30
|
+
[--preview-lines 20]
|
|
31
|
+
|
|
32
|
+
Output JSON (stdout):
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
"status": "no-candidates" | "candidates-found",
|
|
36
|
+
"summary": {
|
|
37
|
+
"candidates_total": N,
|
|
38
|
+
"already_in_scope_count": N,
|
|
39
|
+
"pre_decided_count": N,
|
|
40
|
+
"unresolved_count": N
|
|
41
|
+
},
|
|
42
|
+
"already_in_scope": [
|
|
43
|
+
{"path": "...", "heuristic": "llms.txt", "size_bytes": N,
|
|
44
|
+
"line_count": N, "content_hash": "sha256:..."}
|
|
45
|
+
],
|
|
46
|
+
"pre_decided": [
|
|
47
|
+
{"path": "...", "heuristic": "...", "prior_action": "promoted"|"skipped",
|
|
48
|
+
"should_add_to_promoted_docs": <bool>,
|
|
49
|
+
"size_bytes": N|null, "line_count": N|null,
|
|
50
|
+
"content_hash": "sha256:..." | null}
|
|
51
|
+
],
|
|
52
|
+
"unresolved": [
|
|
53
|
+
{"path": "...", "heuristic": "...", "size_bytes": N,
|
|
54
|
+
"line_count": N, "content_hash": "sha256:...",
|
|
55
|
+
"preview": "<first N lines as a string>",
|
|
56
|
+
"excluded_by_pattern": "<glob>" | "not matched by any scope.include"}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Paths are emitted relative to `source-root`, forward-slash form
|
|
61
|
+
(cross-platform JSON convention — same as skf-detect-scripts-assets.py).
|
|
62
|
+
|
|
63
|
+
`pre_decided` semantics:
|
|
64
|
+
- `prior_action="promoted"` AND `should_add_to_promoted_docs=true` →
|
|
65
|
+
the path is in scope but the caller still needs to add it to
|
|
66
|
+
`promoted_docs[]` so step 5 §6 writes the `file_entries[]` row.
|
|
67
|
+
`content_hash` / `size_bytes` / `line_count` are populated.
|
|
68
|
+
- `prior_action="skipped"` → the user previously declined; caller
|
|
69
|
+
does nothing. Hash/size/lines are null (no need to read the file).
|
|
70
|
+
|
|
71
|
+
Exit codes:
|
|
72
|
+
0 — operation succeeded (any status)
|
|
73
|
+
1 — user error (paths invalid, brief unparseable)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import argparse
|
|
79
|
+
import hashlib
|
|
80
|
+
import json
|
|
81
|
+
import re
|
|
82
|
+
import sys
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
from typing import Iterable
|
|
85
|
+
|
|
86
|
+
import yaml
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
# Heuristic list — case-insensitive basename match
|
|
91
|
+
# --------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
AUTH_DOC_BASENAMES = {
|
|
95
|
+
"llms.txt",
|
|
96
|
+
"llms-full.txt",
|
|
97
|
+
"agents.md",
|
|
98
|
+
"claude.md",
|
|
99
|
+
"gemini.md",
|
|
100
|
+
"copilot.md",
|
|
101
|
+
".cursorrules",
|
|
102
|
+
".windsurfrules",
|
|
103
|
+
".clinerules",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Path-segment names that mark generated/vendored output trees — same
|
|
108
|
+
# exclusion set as skf-detect-scripts-assets.py.
|
|
109
|
+
EXCLUDED_DIR_NAMES = {
|
|
110
|
+
"node_modules", "__pycache__", "dist", "build", ".webpack",
|
|
111
|
+
"target", ".next", ".nuxt", "out", "coverage", ".git",
|
|
112
|
+
".venv", "venv", ".tox", ".mypy_cache", ".pytest_cache",
|
|
113
|
+
".ruff_cache", ".gradle", ".idea", ".vscode",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --------------------------------------------------------------------------
|
|
118
|
+
# Source-tree walk
|
|
119
|
+
# --------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def iter_auth_doc_files(source_root: Path) -> Iterable[tuple[Path, str]]:
|
|
123
|
+
"""Yield (file_path, matched_heuristic_basename) pairs for every file
|
|
124
|
+
in the source tree whose basename matches an auth-doc heuristic.
|
|
125
|
+
|
|
126
|
+
Heuristic matching is case-insensitive on the basename, depth-agnostic.
|
|
127
|
+
Pruned: EXCLUDED_DIR_NAMES at any depth.
|
|
128
|
+
"""
|
|
129
|
+
stack: list[Path] = [source_root]
|
|
130
|
+
while stack:
|
|
131
|
+
current = stack.pop()
|
|
132
|
+
try:
|
|
133
|
+
entries = list(current.iterdir())
|
|
134
|
+
except (PermissionError, FileNotFoundError):
|
|
135
|
+
continue
|
|
136
|
+
for entry in entries:
|
|
137
|
+
if entry.is_symlink():
|
|
138
|
+
continue
|
|
139
|
+
if entry.is_dir():
|
|
140
|
+
if entry.name in EXCLUDED_DIR_NAMES:
|
|
141
|
+
continue
|
|
142
|
+
stack.append(entry)
|
|
143
|
+
elif entry.is_file():
|
|
144
|
+
heuristic = entry.name.lower()
|
|
145
|
+
if heuristic in AUTH_DOC_BASENAMES:
|
|
146
|
+
yield entry, heuristic
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------------------------------
|
|
150
|
+
# Scope filtering — supports `**` recursive globs
|
|
151
|
+
# --------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
_GLOB_CACHE: dict[str, re.Pattern[str]] = {}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _glob_to_regex(pattern: str) -> re.Pattern[str]:
|
|
158
|
+
"""Translate a glob pattern with `**` support into a compiled regex.
|
|
159
|
+
|
|
160
|
+
Semantics (gitignore-compatible enough for brief.scope.include/exclude):
|
|
161
|
+
- `**` matches zero-or-more path segments (across separators)
|
|
162
|
+
- `*` matches anything but the path separator
|
|
163
|
+
- `?` matches any single character but the path separator
|
|
164
|
+
- other chars are escaped literally
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
`**/llms.txt` → matches `llms.txt`, `a/llms.txt`, `a/b/llms.txt`
|
|
168
|
+
`src/**` → matches `src/anything`, `src/a/b/c`, but NOT bare `src`
|
|
169
|
+
`**/test_*` → matches `test_x`, `a/test_x`, `a/b/test_x`
|
|
170
|
+
"""
|
|
171
|
+
cached = _GLOB_CACHE.get(pattern)
|
|
172
|
+
if cached is not None:
|
|
173
|
+
return cached
|
|
174
|
+
|
|
175
|
+
parts = pattern.split("/")
|
|
176
|
+
regex_parts: list[str] = []
|
|
177
|
+
i = 0
|
|
178
|
+
while i < len(parts):
|
|
179
|
+
part = parts[i]
|
|
180
|
+
if part == "**":
|
|
181
|
+
# `a/**/b` should match `a/b` (zero segments between) AND
|
|
182
|
+
# `a/x/y/b` (any number). The trick: consume the trailing `/`
|
|
183
|
+
# before `**` if there's one, then emit `(?:.*/)?` for the
|
|
184
|
+
# `**/` case or `.*` at end of pattern.
|
|
185
|
+
if i + 1 < len(parts):
|
|
186
|
+
# `**` followed by more — must allow zero-or-more segments
|
|
187
|
+
regex_parts.append("(?:.*/)?")
|
|
188
|
+
i += 1
|
|
189
|
+
continue
|
|
190
|
+
# `**` at end — match anything (including nothing)
|
|
191
|
+
regex_parts.append(".*")
|
|
192
|
+
i += 1
|
|
193
|
+
continue
|
|
194
|
+
# Translate a single segment (no `**` inside)
|
|
195
|
+
segment = []
|
|
196
|
+
for ch in part:
|
|
197
|
+
if ch == "*":
|
|
198
|
+
segment.append("[^/]*")
|
|
199
|
+
elif ch == "?":
|
|
200
|
+
segment.append("[^/]")
|
|
201
|
+
else:
|
|
202
|
+
segment.append(re.escape(ch))
|
|
203
|
+
regex_parts.append("".join(segment))
|
|
204
|
+
if i + 1 < len(parts):
|
|
205
|
+
regex_parts.append("/")
|
|
206
|
+
i += 1
|
|
207
|
+
|
|
208
|
+
regex_str = "^" + "".join(regex_parts) + "$"
|
|
209
|
+
compiled = re.compile(regex_str)
|
|
210
|
+
_GLOB_CACHE[pattern] = compiled
|
|
211
|
+
return compiled
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def glob_match(rel_path: str, pattern: str) -> bool:
|
|
215
|
+
return bool(_glob_to_regex(pattern).match(rel_path))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def scope_match(
|
|
219
|
+
rel_path: str, includes: list[str], excludes: list[str]
|
|
220
|
+
) -> tuple[bool, str | None]:
|
|
221
|
+
"""Return (is_in_scope, excluded_by_pattern).
|
|
222
|
+
|
|
223
|
+
is_in_scope is True iff at least one include matches AND no exclude
|
|
224
|
+
matches. excluded_by_pattern is set when the path is OUT of scope:
|
|
225
|
+
- the matching exclude pattern if an exclude rule fires
|
|
226
|
+
- "not matched by any scope.include" if no include matches
|
|
227
|
+
- None when the path IS in scope
|
|
228
|
+
"""
|
|
229
|
+
matched_exclude = next(
|
|
230
|
+
(p for p in excludes if glob_match(rel_path, p)), None
|
|
231
|
+
)
|
|
232
|
+
matched_include = any(glob_match(rel_path, p) for p in includes)
|
|
233
|
+
|
|
234
|
+
if matched_exclude is not None:
|
|
235
|
+
return False, matched_exclude
|
|
236
|
+
if not matched_include:
|
|
237
|
+
return False, "not matched by any scope.include"
|
|
238
|
+
return True, None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --------------------------------------------------------------------------
|
|
242
|
+
# Brief load
|
|
243
|
+
# --------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def load_brief(brief_path: Path) -> dict:
|
|
247
|
+
try:
|
|
248
|
+
text = brief_path.read_text(encoding="utf-8")
|
|
249
|
+
except OSError as exc:
|
|
250
|
+
raise ValueError(f"cannot read brief at {brief_path}: {exc}") from exc
|
|
251
|
+
try:
|
|
252
|
+
brief = yaml.safe_load(text)
|
|
253
|
+
except yaml.YAMLError as exc:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"brief at {brief_path} is not valid YAML: {exc}"
|
|
256
|
+
) from exc
|
|
257
|
+
if not isinstance(brief, dict):
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"brief at {brief_path} must be a YAML mapping; got "
|
|
260
|
+
f"{type(brief).__name__}"
|
|
261
|
+
)
|
|
262
|
+
return brief
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def extract_scope(brief: dict) -> tuple[list[str], list[str], dict[str, list[str]]]:
|
|
266
|
+
"""Extract (includes, excludes, amendments_by_path) from the brief.
|
|
267
|
+
|
|
268
|
+
amendments_by_path maps each path to a list of actions seen, in the
|
|
269
|
+
order they appear in the brief (so reconcile() can pick most-recent
|
|
270
|
+
by iterating in reverse).
|
|
271
|
+
"""
|
|
272
|
+
scope = brief.get("scope") if isinstance(brief.get("scope"), dict) else {}
|
|
273
|
+
includes = scope.get("include") if isinstance(scope.get("include"), list) else []
|
|
274
|
+
excludes = scope.get("exclude") if isinstance(scope.get("exclude"), list) else []
|
|
275
|
+
amendments = scope.get("amendments") if isinstance(scope.get("amendments"), list) else []
|
|
276
|
+
|
|
277
|
+
by_path: dict[str, list[str]] = {}
|
|
278
|
+
for amend in amendments:
|
|
279
|
+
if not isinstance(amend, dict):
|
|
280
|
+
continue
|
|
281
|
+
path = amend.get("path")
|
|
282
|
+
action = amend.get("action")
|
|
283
|
+
if isinstance(path, str) and isinstance(action, str):
|
|
284
|
+
by_path.setdefault(path, []).append(action)
|
|
285
|
+
|
|
286
|
+
# filter to strings only — schema requires strings but defensively guard
|
|
287
|
+
includes = [p for p in includes if isinstance(p, str)]
|
|
288
|
+
excludes = [p for p in excludes if isinstance(p, str)]
|
|
289
|
+
return includes, excludes, by_path
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# --------------------------------------------------------------------------
|
|
293
|
+
# File metadata: hash + size + lines + preview
|
|
294
|
+
# --------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def sha256_of_file(path: Path) -> str:
|
|
298
|
+
h = hashlib.sha256()
|
|
299
|
+
with path.open("rb") as fh:
|
|
300
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
301
|
+
h.update(chunk)
|
|
302
|
+
return "sha256:" + h.hexdigest()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def count_lines(path: Path) -> int:
|
|
306
|
+
n = 0
|
|
307
|
+
try:
|
|
308
|
+
with path.open("rb") as fh:
|
|
309
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
310
|
+
n += chunk.count(b"\n")
|
|
311
|
+
except OSError:
|
|
312
|
+
return 0
|
|
313
|
+
return n
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def load_preview(path: Path, *, max_lines: int) -> str:
|
|
317
|
+
"""Read up to `max_lines` lines from `path` and return them as a single
|
|
318
|
+
string (newline-joined, no trailing newline). Best-effort UTF-8 decode;
|
|
319
|
+
binary content returns "".
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
with path.open("r", encoding="utf-8", errors="replace") as fh:
|
|
323
|
+
lines: list[str] = []
|
|
324
|
+
for _ in range(max_lines):
|
|
325
|
+
line = fh.readline()
|
|
326
|
+
if not line:
|
|
327
|
+
break
|
|
328
|
+
lines.append(line.rstrip("\n"))
|
|
329
|
+
return "\n".join(lines)
|
|
330
|
+
except OSError:
|
|
331
|
+
return ""
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# --------------------------------------------------------------------------
|
|
335
|
+
# Resolve
|
|
336
|
+
# --------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
_PROMOTED = "promoted"
|
|
340
|
+
_SKIPPED = "skipped"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _latest_action(actions: list[str]) -> str | None:
|
|
344
|
+
"""Most-recent matching amendment wins. Returns 'promoted', 'skipped',
|
|
345
|
+
or None if no recognized action was seen."""
|
|
346
|
+
for action in reversed(actions):
|
|
347
|
+
if action in (_PROMOTED, _SKIPPED):
|
|
348
|
+
return action
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def resolve(
|
|
353
|
+
source_root: Path,
|
|
354
|
+
brief_path: Path,
|
|
355
|
+
*,
|
|
356
|
+
preview_lines: int = 20,
|
|
357
|
+
) -> dict:
|
|
358
|
+
"""Run the §2a deterministic pipeline. See module docstring for output shape."""
|
|
359
|
+
brief = load_brief(brief_path)
|
|
360
|
+
includes, excludes, amendments_by_path = extract_scope(brief)
|
|
361
|
+
|
|
362
|
+
already_in_scope: list[dict] = []
|
|
363
|
+
pre_decided: list[dict] = []
|
|
364
|
+
unresolved: list[dict] = []
|
|
365
|
+
|
|
366
|
+
for file_path, heuristic in iter_auth_doc_files(source_root):
|
|
367
|
+
rel = file_path.relative_to(source_root).as_posix()
|
|
368
|
+
in_scope, excluded_by = scope_match(rel, includes, excludes)
|
|
369
|
+
prior_action = _latest_action(amendments_by_path.get(rel, []))
|
|
370
|
+
|
|
371
|
+
if in_scope and prior_action != _SKIPPED:
|
|
372
|
+
# Already in scope — remove from §2 filtered list and add to
|
|
373
|
+
# promoted_docs[]. Includes the "user manually added to
|
|
374
|
+
# scope.include" case AND the "amendments has promoted" case
|
|
375
|
+
# (which is the deterministic replay path).
|
|
376
|
+
already_in_scope.append({
|
|
377
|
+
"path": rel,
|
|
378
|
+
"heuristic": heuristic,
|
|
379
|
+
"size_bytes": file_path.stat().st_size,
|
|
380
|
+
"line_count": count_lines(file_path),
|
|
381
|
+
"content_hash": sha256_of_file(file_path),
|
|
382
|
+
})
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
if prior_action == _PROMOTED:
|
|
386
|
+
# Amendment says promoted but scope.include doesn't currently
|
|
387
|
+
# match — unusual (brief was edited?) — still populate
|
|
388
|
+
# promoted_docs[] per the deterministic replay rule.
|
|
389
|
+
pre_decided.append({
|
|
390
|
+
"path": rel,
|
|
391
|
+
"heuristic": heuristic,
|
|
392
|
+
"prior_action": _PROMOTED,
|
|
393
|
+
"should_add_to_promoted_docs": True,
|
|
394
|
+
"size_bytes": file_path.stat().st_size,
|
|
395
|
+
"line_count": count_lines(file_path),
|
|
396
|
+
"content_hash": sha256_of_file(file_path),
|
|
397
|
+
})
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
if prior_action == _SKIPPED:
|
|
401
|
+
pre_decided.append({
|
|
402
|
+
"path": rel,
|
|
403
|
+
"heuristic": heuristic,
|
|
404
|
+
"prior_action": _SKIPPED,
|
|
405
|
+
"should_add_to_promoted_docs": False,
|
|
406
|
+
"size_bytes": None,
|
|
407
|
+
"line_count": None,
|
|
408
|
+
"content_hash": None,
|
|
409
|
+
})
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
# No prior amendment and out of scope → unresolved
|
|
413
|
+
unresolved.append({
|
|
414
|
+
"path": rel,
|
|
415
|
+
"heuristic": heuristic,
|
|
416
|
+
"size_bytes": file_path.stat().st_size,
|
|
417
|
+
"line_count": count_lines(file_path),
|
|
418
|
+
"content_hash": sha256_of_file(file_path),
|
|
419
|
+
"preview": load_preview(file_path, max_lines=preview_lines),
|
|
420
|
+
"excluded_by_pattern": excluded_by,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
# Deterministic ordering for stable diffs / cache keys
|
|
424
|
+
already_in_scope.sort(key=lambda r: r["path"])
|
|
425
|
+
pre_decided.sort(key=lambda r: r["path"])
|
|
426
|
+
unresolved.sort(key=lambda r: r["path"])
|
|
427
|
+
|
|
428
|
+
candidates_total = (
|
|
429
|
+
len(already_in_scope) + len(pre_decided) + len(unresolved)
|
|
430
|
+
)
|
|
431
|
+
return {
|
|
432
|
+
"status": "candidates-found" if candidates_total > 0 else "no-candidates",
|
|
433
|
+
"summary": {
|
|
434
|
+
"candidates_total": candidates_total,
|
|
435
|
+
"already_in_scope_count": len(already_in_scope),
|
|
436
|
+
"pre_decided_count": len(pre_decided),
|
|
437
|
+
"unresolved_count": len(unresolved),
|
|
438
|
+
},
|
|
439
|
+
"already_in_scope": already_in_scope,
|
|
440
|
+
"pre_decided": pre_decided,
|
|
441
|
+
"unresolved": unresolved,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# --------------------------------------------------------------------------
|
|
446
|
+
# CLI
|
|
447
|
+
# --------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _cmd_resolve(args: argparse.Namespace) -> int:
|
|
451
|
+
source_root = Path(args.source_root)
|
|
452
|
+
if not source_root.is_dir():
|
|
453
|
+
print(
|
|
454
|
+
f"error: source-root not a directory: {source_root}", file=sys.stderr
|
|
455
|
+
)
|
|
456
|
+
return 1
|
|
457
|
+
brief_path = Path(args.brief)
|
|
458
|
+
if not brief_path.is_file():
|
|
459
|
+
print(f"error: brief not found: {brief_path}", file=sys.stderr)
|
|
460
|
+
return 1
|
|
461
|
+
if args.preview_lines < 1:
|
|
462
|
+
print("error: --preview-lines must be >= 1", file=sys.stderr)
|
|
463
|
+
return 1
|
|
464
|
+
try:
|
|
465
|
+
result = resolve(source_root, brief_path, preview_lines=args.preview_lines)
|
|
466
|
+
except ValueError as exc:
|
|
467
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
468
|
+
return 1
|
|
469
|
+
json.dump(result, sys.stdout, indent=2)
|
|
470
|
+
sys.stdout.write("\n")
|
|
471
|
+
return 0
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
475
|
+
parser = argparse.ArgumentParser(
|
|
476
|
+
prog="skf-resolve-authoritative-files",
|
|
477
|
+
description=(
|
|
478
|
+
"Scan a source tree for authoritative AI documentation files, "
|
|
479
|
+
"classify each against scope filters + amendments, emit "
|
|
480
|
+
"already-in-scope / pre-decided / unresolved buckets."
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
484
|
+
p = sub.add_parser("resolve", help="run the §2a pipeline")
|
|
485
|
+
p.add_argument("--source-root", required=True, help="path to the source tree")
|
|
486
|
+
p.add_argument("--brief", required=True, help="path to skill-brief.yaml")
|
|
487
|
+
p.add_argument(
|
|
488
|
+
"--preview-lines",
|
|
489
|
+
type=int,
|
|
490
|
+
default=20,
|
|
491
|
+
help="number of lines to capture for the prompt preview (default: 20)",
|
|
492
|
+
)
|
|
493
|
+
p.set_defaults(func=_cmd_resolve)
|
|
494
|
+
return parser
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def main(argv: list[str] | None = None) -> int:
|
|
498
|
+
parser = _build_parser()
|
|
499
|
+
args = parser.parse_args(argv)
|
|
500
|
+
return args.func(args)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__":
|
|
504
|
+
raise SystemExit(main())
|