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,613 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Detect Scripts & Assets — file-level artifact detection for skill compilation.
|
|
6
|
+
|
|
7
|
+
Scans a source tree and identifies script and asset files per the detection
|
|
8
|
+
heuristics documented in
|
|
9
|
+
`src/skf-create-skill/references/extraction-patterns-tracing.md` (Script/Asset
|
|
10
|
+
Extraction Patterns section). The output feeds `scripts_inventory[]` and
|
|
11
|
+
`assets_inventory[]` in the create-skill workflow's extraction inventory
|
|
12
|
+
(see step 3 §4c).
|
|
13
|
+
|
|
14
|
+
This script replaces ~13 lines of prose plus implicit "compute SHA-256, read
|
|
15
|
+
shebang, find header comments" operations that would otherwise be re-derived
|
|
16
|
+
by the LLM on every run.
|
|
17
|
+
|
|
18
|
+
Subcommand: a single `detect` subcommand keeps room for future extensions
|
|
19
|
+
(e.g., `detect-entry-points` that only parses package.json/Cargo.toml).
|
|
20
|
+
|
|
21
|
+
CLI:
|
|
22
|
+
uv run skf-detect-scripts-assets.py detect <source-root> \\
|
|
23
|
+
[--scripts-intent detect|none] \\
|
|
24
|
+
[--assets-intent detect|none] \\
|
|
25
|
+
[--scope-include "pattern1,pattern2,..."] \\
|
|
26
|
+
[--max-lines 500]
|
|
27
|
+
|
|
28
|
+
Detection rules — scripts:
|
|
29
|
+
Directory convention: scripts/, bin/, tools/, cli/
|
|
30
|
+
Shebang signals: #!/bin/bash, #!/usr/bin/env python|node|bash|sh, etc.
|
|
31
|
+
Entry point declarations: package.json `bin`, pyproject.toml [project.scripts]
|
|
32
|
+
CLI argument-parser imports are detected for the LLM-judgment tier only —
|
|
33
|
+
this script flags the directory/shebang/entry-point cases that are fully
|
|
34
|
+
deterministic.
|
|
35
|
+
|
|
36
|
+
Detection rules — assets:
|
|
37
|
+
Directory convention: assets/, templates/, schemas/, configs/, examples/
|
|
38
|
+
Pattern matches: *.schema.json (contains "$schema"), *.example,
|
|
39
|
+
*.template.*, *.sample, openapi.json, *.graphql, swagger.yaml
|
|
40
|
+
|
|
41
|
+
Exclusions:
|
|
42
|
+
Binary extensions: .so, .dll, .jar, .wasm, .exe, .dylib, .a, .o, .pyc,
|
|
43
|
+
.class, .png, .jpg, .jpeg, .gif, .ico, .pdf, .zip, .tar, .gz, .tgz
|
|
44
|
+
Generated paths: dist/, build/, .webpack/, node_modules/, __pycache__/,
|
|
45
|
+
target/ (Rust), .next/, .nuxt/, out/, coverage/
|
|
46
|
+
|
|
47
|
+
Size flag:
|
|
48
|
+
Files >max-lines (default 500) get size_flag="oversized" but are still
|
|
49
|
+
included — the caller decides whether to bundle them.
|
|
50
|
+
|
|
51
|
+
Output JSON (stdout):
|
|
52
|
+
{
|
|
53
|
+
"scripts_inventory": [ {name, source_file, purpose, language,
|
|
54
|
+
content_hash, confidence, lines, size_flag}, ... ],
|
|
55
|
+
"assets_inventory": [ {name, source_file, purpose, type, content_hash,
|
|
56
|
+
confidence, lines, size_flag}, ... ],
|
|
57
|
+
"scripts_skipped": false,
|
|
58
|
+
"assets_skipped": false,
|
|
59
|
+
"stats": { "scripts_found": N, "assets_found": M, "files_scanned": K }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Confidence is always "T1-low" — file existence verified, content not
|
|
63
|
+
AST-analyzed. Matches the spec at extraction-patterns-tracing.md §Provenance.
|
|
64
|
+
|
|
65
|
+
Exit codes:
|
|
66
|
+
0 — detection succeeded (including --scripts-intent=none --assets-intent=none)
|
|
67
|
+
1 — user error (bad source root, malformed args)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
from __future__ import annotations
|
|
71
|
+
|
|
72
|
+
import argparse
|
|
73
|
+
import fnmatch
|
|
74
|
+
import hashlib
|
|
75
|
+
import json
|
|
76
|
+
import re
|
|
77
|
+
import sys
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
from typing import Iterable
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------------------
|
|
83
|
+
# Constants — detection rules (single source of truth)
|
|
84
|
+
# --------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
SCRIPT_DIRS = {"scripts", "bin", "tools", "cli"}
|
|
88
|
+
ASSET_DIRS = {"assets", "templates", "schemas", "configs", "examples"}
|
|
89
|
+
|
|
90
|
+
SHEBANG_LANGS = {
|
|
91
|
+
"bash": "bash",
|
|
92
|
+
"sh": "shell",
|
|
93
|
+
"zsh": "zsh",
|
|
94
|
+
"python": "python",
|
|
95
|
+
"python2": "python",
|
|
96
|
+
"python3": "python",
|
|
97
|
+
"node": "javascript",
|
|
98
|
+
"deno": "typescript",
|
|
99
|
+
"ruby": "ruby",
|
|
100
|
+
"perl": "perl",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
EXTENSION_LANGS = {
|
|
104
|
+
".sh": "shell",
|
|
105
|
+
".bash": "bash",
|
|
106
|
+
".zsh": "zsh",
|
|
107
|
+
".py": "python",
|
|
108
|
+
".js": "javascript",
|
|
109
|
+
".mjs": "javascript",
|
|
110
|
+
".cjs": "javascript",
|
|
111
|
+
".ts": "typescript",
|
|
112
|
+
".rb": "ruby",
|
|
113
|
+
".pl": "perl",
|
|
114
|
+
".go": "go",
|
|
115
|
+
".rs": "rust",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
BINARY_EXTS = {
|
|
119
|
+
".so", ".dll", ".jar", ".wasm", ".exe", ".dylib", ".a", ".o",
|
|
120
|
+
".pyc", ".class", ".png", ".jpg", ".jpeg", ".gif", ".ico",
|
|
121
|
+
".pdf", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z",
|
|
122
|
+
".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Path-segment names that mark generated/vendored output trees.
|
|
126
|
+
EXCLUDED_DIR_NAMES = {
|
|
127
|
+
"node_modules", "__pycache__", "dist", "build", ".webpack",
|
|
128
|
+
"target", ".next", ".nuxt", "out", "coverage", ".git",
|
|
129
|
+
".venv", "venv", ".tox", ".mypy_cache", ".pytest_cache",
|
|
130
|
+
".ruff_cache", ".gradle", ".idea", ".vscode",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ASSET_TYPE_SCHEMA = "schema"
|
|
134
|
+
ASSET_TYPE_TEMPLATE = "template"
|
|
135
|
+
ASSET_TYPE_CONFIG = "config"
|
|
136
|
+
ASSET_TYPE_EXAMPLE = "example"
|
|
137
|
+
|
|
138
|
+
CONFIDENCE = "T1-low"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# --------------------------------------------------------------------------
|
|
142
|
+
# Walk + filter
|
|
143
|
+
# --------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def iter_files(source_root: Path) -> Iterable[Path]:
|
|
147
|
+
"""Yield every file under source_root, skipping excluded directory trees.
|
|
148
|
+
|
|
149
|
+
Done as a manual walk so we can prune EXCLUDED_DIR_NAMES at any depth
|
|
150
|
+
without scanning their contents (Path.rglob can't prune efficiently).
|
|
151
|
+
"""
|
|
152
|
+
stack: list[Path] = [source_root]
|
|
153
|
+
while stack:
|
|
154
|
+
current = stack.pop()
|
|
155
|
+
try:
|
|
156
|
+
entries = list(current.iterdir())
|
|
157
|
+
except (PermissionError, FileNotFoundError):
|
|
158
|
+
continue
|
|
159
|
+
for entry in entries:
|
|
160
|
+
if entry.is_symlink():
|
|
161
|
+
continue
|
|
162
|
+
if entry.is_dir():
|
|
163
|
+
if entry.name in EXCLUDED_DIR_NAMES:
|
|
164
|
+
continue
|
|
165
|
+
stack.append(entry)
|
|
166
|
+
elif entry.is_file():
|
|
167
|
+
yield entry
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def is_binary_path(path: Path) -> bool:
|
|
171
|
+
return path.suffix.lower() in BINARY_EXTS
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def relative_segments(path: Path, root: Path) -> tuple[str, ...]:
|
|
175
|
+
"""Return path parts relative to root, dropping the filename."""
|
|
176
|
+
rel = path.relative_to(root)
|
|
177
|
+
return rel.parts[:-1]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def in_directory(path: Path, root: Path, dir_names: set[str]) -> bool:
|
|
181
|
+
"""True if any segment of path's relative dir is in dir_names."""
|
|
182
|
+
return any(seg in dir_names for seg in relative_segments(path, root))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def matches_scope(path: Path, root: Path, scope_patterns: list[str]) -> bool:
|
|
186
|
+
"""Match `path` against scope-include glob patterns relative to root.
|
|
187
|
+
|
|
188
|
+
Forward-slash form so scope patterns like "scripts/*" match on Windows
|
|
189
|
+
too — fnmatch is byte-exact and won't normalize separators for us.
|
|
190
|
+
"""
|
|
191
|
+
if not scope_patterns:
|
|
192
|
+
return True
|
|
193
|
+
rel = path.relative_to(root).as_posix()
|
|
194
|
+
return any(fnmatch.fnmatch(rel, pattern) for pattern in scope_patterns)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# --------------------------------------------------------------------------
|
|
198
|
+
# Read helpers
|
|
199
|
+
# --------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def read_text_safe(path: Path, *, max_bytes: int | None = None) -> str | None:
|
|
203
|
+
"""Read text from `path` as UTF-8, returning None on decode failure
|
|
204
|
+
(treated as binary). Caller can pass max_bytes to bound reads of large
|
|
205
|
+
files."""
|
|
206
|
+
try:
|
|
207
|
+
if max_bytes is None:
|
|
208
|
+
return path.read_text(encoding="utf-8")
|
|
209
|
+
with path.open("rb") as fh:
|
|
210
|
+
blob = fh.read(max_bytes)
|
|
211
|
+
return blob.decode("utf-8")
|
|
212
|
+
except (UnicodeDecodeError, OSError):
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def sha256_of_file(path: Path) -> str:
|
|
217
|
+
"""Compute SHA-256 of file content; emits with `sha256:` prefix."""
|
|
218
|
+
h = hashlib.sha256()
|
|
219
|
+
with path.open("rb") as fh:
|
|
220
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
221
|
+
h.update(chunk)
|
|
222
|
+
return "sha256:" + h.hexdigest()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def count_lines(path: Path) -> int:
|
|
226
|
+
"""Count newlines in the file; returns 0 if file is unreadable."""
|
|
227
|
+
n = 0
|
|
228
|
+
try:
|
|
229
|
+
with path.open("rb") as fh:
|
|
230
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
231
|
+
n += chunk.count(b"\n")
|
|
232
|
+
except OSError:
|
|
233
|
+
return 0
|
|
234
|
+
return n
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# --------------------------------------------------------------------------
|
|
238
|
+
# Script detection
|
|
239
|
+
# --------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
_SHEBANG_RE = re.compile(rb"^#!.*?(\b(?:" + "|".join(SHEBANG_LANGS).encode() + rb")\b)")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def detect_shebang_language(path: Path) -> str | None:
|
|
246
|
+
"""Return language name if the file starts with a recognized shebang."""
|
|
247
|
+
try:
|
|
248
|
+
with path.open("rb") as fh:
|
|
249
|
+
first = fh.readline(256)
|
|
250
|
+
except OSError:
|
|
251
|
+
return None
|
|
252
|
+
if not first.startswith(b"#!"):
|
|
253
|
+
return None
|
|
254
|
+
match = _SHEBANG_RE.match(first)
|
|
255
|
+
if not match:
|
|
256
|
+
return None
|
|
257
|
+
interp = match.group(1).decode()
|
|
258
|
+
return SHEBANG_LANGS.get(interp)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def detect_language(path: Path, *, shebang_lang: str | None) -> str:
|
|
262
|
+
"""Determine the script's language for the inventory record."""
|
|
263
|
+
if shebang_lang:
|
|
264
|
+
return shebang_lang
|
|
265
|
+
ext_lang = EXTENSION_LANGS.get(path.suffix.lower())
|
|
266
|
+
if ext_lang:
|
|
267
|
+
return ext_lang
|
|
268
|
+
return "unknown"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def entry_point_script_paths(source_root: Path) -> set[Path]:
|
|
272
|
+
"""Parse package.json `bin` and pyproject.toml [project.scripts] to find
|
|
273
|
+
explicit entry-point scripts. Returns absolute paths that exist on disk.
|
|
274
|
+
|
|
275
|
+
Other manifest formats (Cargo.toml [[bin]]) are handled by directory
|
|
276
|
+
convention (`src/bin/` etc.) rather than parsing — adding a Cargo parser
|
|
277
|
+
is future work and not in this script's scope per the deterministic-only
|
|
278
|
+
extraction rule.
|
|
279
|
+
"""
|
|
280
|
+
found: set[Path] = set()
|
|
281
|
+
found.update(_parse_package_json_bin(source_root))
|
|
282
|
+
found.update(_parse_pyproject_scripts(source_root))
|
|
283
|
+
return found
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _parse_package_json_bin(source_root: Path) -> Iterable[Path]:
|
|
287
|
+
pkg = source_root / "package.json"
|
|
288
|
+
if not pkg.is_file():
|
|
289
|
+
return ()
|
|
290
|
+
text = read_text_safe(pkg, max_bytes=512 * 1024)
|
|
291
|
+
if text is None:
|
|
292
|
+
return ()
|
|
293
|
+
try:
|
|
294
|
+
data = json.loads(text)
|
|
295
|
+
except json.JSONDecodeError:
|
|
296
|
+
return ()
|
|
297
|
+
bin_field = data.get("bin")
|
|
298
|
+
if isinstance(bin_field, str):
|
|
299
|
+
candidates = [bin_field]
|
|
300
|
+
elif isinstance(bin_field, dict):
|
|
301
|
+
candidates = [v for v in bin_field.values() if isinstance(v, str)]
|
|
302
|
+
else:
|
|
303
|
+
return ()
|
|
304
|
+
out: list[Path] = []
|
|
305
|
+
for cand in candidates:
|
|
306
|
+
# bin paths are relative to package.json
|
|
307
|
+
p = (source_root / cand).resolve()
|
|
308
|
+
if p.is_file():
|
|
309
|
+
out.append(p)
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _parse_pyproject_scripts(source_root: Path) -> Iterable[Path]:
|
|
314
|
+
"""Pyproject [project.scripts] points at a dotted module path, not a file
|
|
315
|
+
path — we can't deterministically map `mypkg.cli:main` to a script file
|
|
316
|
+
without invoking the package's installer. We instead surface the existence
|
|
317
|
+
of any [project.scripts] entry as a hint by treating any file under
|
|
318
|
+
`<source_root>/<top-level>/__main__.py` or files matching the cli module
|
|
319
|
+
in standard layouts. To avoid speculative mapping, this function returns
|
|
320
|
+
an empty set today; the directory-convention path picks up most cases
|
|
321
|
+
(script-like files in scripts/ or bin/). Future work: parse setuptools
|
|
322
|
+
package layout to resolve.
|
|
323
|
+
"""
|
|
324
|
+
_ = source_root
|
|
325
|
+
return ()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def is_script(path: Path, source_root: Path, *, entry_points: set[Path]) -> tuple[bool, str | None]:
|
|
329
|
+
"""Decide whether `path` is a script. Returns (is_script, shebang_lang)."""
|
|
330
|
+
if path.resolve() in entry_points:
|
|
331
|
+
return True, detect_shebang_language(path)
|
|
332
|
+
if in_directory(path, source_root, SCRIPT_DIRS):
|
|
333
|
+
return True, detect_shebang_language(path)
|
|
334
|
+
shebang_lang = detect_shebang_language(path)
|
|
335
|
+
if shebang_lang is not None:
|
|
336
|
+
return True, shebang_lang
|
|
337
|
+
return False, None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# --------------------------------------------------------------------------
|
|
341
|
+
# Asset detection
|
|
342
|
+
# --------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
_ASSET_FILENAME_TYPE: list[tuple[re.Pattern[str], str]] = [
|
|
346
|
+
(re.compile(r".*\.schema\.json$", re.IGNORECASE), ASSET_TYPE_SCHEMA),
|
|
347
|
+
(re.compile(r"^openapi\.(json|ya?ml)$", re.IGNORECASE), ASSET_TYPE_SCHEMA),
|
|
348
|
+
(re.compile(r"^swagger\.(json|ya?ml)$", re.IGNORECASE), ASSET_TYPE_SCHEMA),
|
|
349
|
+
(re.compile(r".*\.graphql$", re.IGNORECASE), ASSET_TYPE_SCHEMA),
|
|
350
|
+
(re.compile(r".*\.template\..+$", re.IGNORECASE), ASSET_TYPE_TEMPLATE),
|
|
351
|
+
(re.compile(r".*\.(example|sample)(\..+)?$", re.IGNORECASE), ASSET_TYPE_EXAMPLE),
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def asset_type(path: Path, source_root: Path) -> str | None:
|
|
356
|
+
"""Return the asset type for `path` or None if it doesn't match asset rules."""
|
|
357
|
+
# Pattern-based detection (works regardless of containing directory)
|
|
358
|
+
for pattern, atype in _ASSET_FILENAME_TYPE:
|
|
359
|
+
if pattern.match(path.name):
|
|
360
|
+
return atype
|
|
361
|
+
|
|
362
|
+
if in_directory(path, source_root, ASSET_DIRS):
|
|
363
|
+
segments = set(relative_segments(path, source_root))
|
|
364
|
+
if "templates" in segments:
|
|
365
|
+
return ASSET_TYPE_TEMPLATE
|
|
366
|
+
if "schemas" in segments:
|
|
367
|
+
# JSON schema content check
|
|
368
|
+
if path.suffix.lower() == ".json":
|
|
369
|
+
text = read_text_safe(path, max_bytes=64 * 1024)
|
|
370
|
+
if text and '"$schema"' in text:
|
|
371
|
+
return ASSET_TYPE_SCHEMA
|
|
372
|
+
return ASSET_TYPE_SCHEMA
|
|
373
|
+
if "configs" in segments:
|
|
374
|
+
return ASSET_TYPE_CONFIG
|
|
375
|
+
if "examples" in segments:
|
|
376
|
+
return ASSET_TYPE_EXAMPLE
|
|
377
|
+
# assets/ — type unknown, default to config (safer than template)
|
|
378
|
+
return ASSET_TYPE_CONFIG
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# --------------------------------------------------------------------------
|
|
383
|
+
# Purpose extraction
|
|
384
|
+
# --------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
_HEADER_COMMENT_RE = re.compile(
|
|
388
|
+
r"^\s*(?:#|//|/\*|\*)\s*(.{3,})$"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def extract_purpose(path: Path, *, asset_type_hint: str | None) -> str:
|
|
393
|
+
"""Pull a one-line purpose summary from the file's leading content.
|
|
394
|
+
|
|
395
|
+
Heuristics (first match wins):
|
|
396
|
+
- JSON schema: read top-level `title` or `description` field
|
|
397
|
+
- Files with header comments: first comment line after any shebang
|
|
398
|
+
- Otherwise: fall back to the filename (per spec)
|
|
399
|
+
"""
|
|
400
|
+
if asset_type_hint == ASSET_TYPE_SCHEMA and path.suffix.lower() == ".json":
|
|
401
|
+
text = read_text_safe(path, max_bytes=64 * 1024)
|
|
402
|
+
if text:
|
|
403
|
+
try:
|
|
404
|
+
data = json.loads(text)
|
|
405
|
+
for key in ("title", "description"):
|
|
406
|
+
val = data.get(key) if isinstance(data, dict) else None
|
|
407
|
+
if isinstance(val, str) and val.strip():
|
|
408
|
+
return val.strip()
|
|
409
|
+
except json.JSONDecodeError:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
text = read_text_safe(path, max_bytes=4 * 1024)
|
|
413
|
+
if text:
|
|
414
|
+
for i, line in enumerate(text.splitlines()[:10]):
|
|
415
|
+
if i == 0 and line.startswith("#!"):
|
|
416
|
+
continue
|
|
417
|
+
if not line.strip():
|
|
418
|
+
continue
|
|
419
|
+
match = _HEADER_COMMENT_RE.match(line)
|
|
420
|
+
if match:
|
|
421
|
+
purpose = match.group(1).strip().rstrip("*/").strip()
|
|
422
|
+
if purpose:
|
|
423
|
+
return purpose
|
|
424
|
+
|
|
425
|
+
return path.name
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# --------------------------------------------------------------------------
|
|
429
|
+
# Build records
|
|
430
|
+
# --------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def build_script_record(
|
|
434
|
+
path: Path, source_root: Path, *, shebang_lang: str | None, max_lines: int
|
|
435
|
+
) -> dict:
|
|
436
|
+
rel = path.relative_to(source_root).as_posix()
|
|
437
|
+
lines = count_lines(path)
|
|
438
|
+
return {
|
|
439
|
+
"name": path.name,
|
|
440
|
+
"source_file": rel,
|
|
441
|
+
"purpose": extract_purpose(path, asset_type_hint=None),
|
|
442
|
+
"language": detect_language(path, shebang_lang=shebang_lang),
|
|
443
|
+
"content_hash": sha256_of_file(path),
|
|
444
|
+
"confidence": CONFIDENCE,
|
|
445
|
+
"lines": lines,
|
|
446
|
+
"size_flag": "oversized" if lines > max_lines else None,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def build_asset_record(
|
|
451
|
+
path: Path, source_root: Path, *, atype: str, max_lines: int
|
|
452
|
+
) -> dict:
|
|
453
|
+
rel = path.relative_to(source_root).as_posix()
|
|
454
|
+
lines = count_lines(path)
|
|
455
|
+
return {
|
|
456
|
+
"name": path.name,
|
|
457
|
+
"source_file": rel,
|
|
458
|
+
"purpose": extract_purpose(path, asset_type_hint=atype),
|
|
459
|
+
"type": atype,
|
|
460
|
+
"content_hash": sha256_of_file(path),
|
|
461
|
+
"confidence": CONFIDENCE,
|
|
462
|
+
"lines": lines,
|
|
463
|
+
"size_flag": "oversized" if lines > max_lines else None,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# --------------------------------------------------------------------------
|
|
468
|
+
# Main detection
|
|
469
|
+
# --------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def detect(
|
|
473
|
+
source_root: Path,
|
|
474
|
+
*,
|
|
475
|
+
scripts_intent: str = "detect",
|
|
476
|
+
assets_intent: str = "detect",
|
|
477
|
+
scope_patterns: list[str] | None = None,
|
|
478
|
+
max_lines: int = 500,
|
|
479
|
+
) -> dict:
|
|
480
|
+
scope_patterns = scope_patterns or []
|
|
481
|
+
scripts_skipped = scripts_intent == "none"
|
|
482
|
+
assets_skipped = assets_intent == "none"
|
|
483
|
+
|
|
484
|
+
scripts_inventory: list[dict] = []
|
|
485
|
+
assets_inventory: list[dict] = []
|
|
486
|
+
files_scanned = 0
|
|
487
|
+
|
|
488
|
+
entry_points = (
|
|
489
|
+
entry_point_script_paths(source_root) if not scripts_skipped else set()
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if not scripts_skipped or not assets_skipped:
|
|
493
|
+
for path in iter_files(source_root):
|
|
494
|
+
files_scanned += 1
|
|
495
|
+
if is_binary_path(path):
|
|
496
|
+
continue
|
|
497
|
+
if not matches_scope(path, source_root, scope_patterns):
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if not scripts_skipped:
|
|
501
|
+
is_scr, shebang_lang = is_script(
|
|
502
|
+
path, source_root, entry_points=entry_points
|
|
503
|
+
)
|
|
504
|
+
if is_scr:
|
|
505
|
+
scripts_inventory.append(
|
|
506
|
+
build_script_record(
|
|
507
|
+
path, source_root,
|
|
508
|
+
shebang_lang=shebang_lang, max_lines=max_lines,
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
continue # a script can't also be an asset
|
|
512
|
+
|
|
513
|
+
if not assets_skipped:
|
|
514
|
+
atype = asset_type(path, source_root)
|
|
515
|
+
if atype is not None:
|
|
516
|
+
assets_inventory.append(
|
|
517
|
+
build_asset_record(
|
|
518
|
+
path, source_root, atype=atype, max_lines=max_lines,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Deterministic ordering for stable diffs / cacheability
|
|
523
|
+
scripts_inventory.sort(key=lambda r: r["source_file"])
|
|
524
|
+
assets_inventory.sort(key=lambda r: r["source_file"])
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
"scripts_inventory": scripts_inventory,
|
|
528
|
+
"assets_inventory": assets_inventory,
|
|
529
|
+
"scripts_skipped": scripts_skipped,
|
|
530
|
+
"assets_skipped": assets_skipped,
|
|
531
|
+
"stats": {
|
|
532
|
+
"scripts_found": len(scripts_inventory),
|
|
533
|
+
"assets_found": len(assets_inventory),
|
|
534
|
+
"files_scanned": files_scanned,
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# --------------------------------------------------------------------------
|
|
540
|
+
# CLI
|
|
541
|
+
# --------------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _cmd_detect(args: argparse.Namespace) -> int:
|
|
545
|
+
source_root = Path(args.source_root).resolve()
|
|
546
|
+
if not source_root.is_dir():
|
|
547
|
+
print(f"error: source root not a directory: {source_root}", file=sys.stderr)
|
|
548
|
+
return 1
|
|
549
|
+
scope_patterns = (
|
|
550
|
+
[s.strip() for s in args.scope_include.split(",") if s.strip()]
|
|
551
|
+
if args.scope_include
|
|
552
|
+
else []
|
|
553
|
+
)
|
|
554
|
+
if args.scripts_intent not in ("detect", "none"):
|
|
555
|
+
print(f"error: --scripts-intent must be detect|none", file=sys.stderr)
|
|
556
|
+
return 1
|
|
557
|
+
if args.assets_intent not in ("detect", "none"):
|
|
558
|
+
print(f"error: --assets-intent must be detect|none", file=sys.stderr)
|
|
559
|
+
return 1
|
|
560
|
+
|
|
561
|
+
result = detect(
|
|
562
|
+
source_root,
|
|
563
|
+
scripts_intent=args.scripts_intent,
|
|
564
|
+
assets_intent=args.assets_intent,
|
|
565
|
+
scope_patterns=scope_patterns,
|
|
566
|
+
max_lines=args.max_lines,
|
|
567
|
+
)
|
|
568
|
+
json.dump(result, sys.stdout, indent=2)
|
|
569
|
+
sys.stdout.write("\n")
|
|
570
|
+
return 0
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
574
|
+
parser = argparse.ArgumentParser(
|
|
575
|
+
prog="skf-detect-scripts-assets",
|
|
576
|
+
description="Detect script and asset files in a source tree per SKF extraction rules.",
|
|
577
|
+
)
|
|
578
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
579
|
+
p = sub.add_parser("detect", help="scan a source tree and emit inventory JSON")
|
|
580
|
+
p.add_argument("source_root", help="path to the source tree root")
|
|
581
|
+
p.add_argument(
|
|
582
|
+
"--scripts-intent",
|
|
583
|
+
default="detect",
|
|
584
|
+
help="detect|none (default: detect)",
|
|
585
|
+
)
|
|
586
|
+
p.add_argument(
|
|
587
|
+
"--assets-intent",
|
|
588
|
+
default="detect",
|
|
589
|
+
help="detect|none (default: detect)",
|
|
590
|
+
)
|
|
591
|
+
p.add_argument(
|
|
592
|
+
"--scope-include",
|
|
593
|
+
default=None,
|
|
594
|
+
help="comma-separated glob patterns to limit detection (relative to source-root)",
|
|
595
|
+
)
|
|
596
|
+
p.add_argument(
|
|
597
|
+
"--max-lines",
|
|
598
|
+
type=int,
|
|
599
|
+
default=500,
|
|
600
|
+
help="files above this line count get size_flag='oversized' (default: 500)",
|
|
601
|
+
)
|
|
602
|
+
p.set_defaults(func=_cmd_detect)
|
|
603
|
+
return parser
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def main(argv: list[str] | None = None) -> int:
|
|
607
|
+
parser = _build_parser()
|
|
608
|
+
args = parser.parse_args(argv)
|
|
609
|
+
return args.func(args)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
if __name__ == "__main__":
|
|
613
|
+
raise SystemExit(main())
|