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,576 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Disqualify Candidates — deterministic filter for skillable-unit boundaries.
|
|
6
|
+
|
|
7
|
+
`identify-units.md` step 2 §D asks the LLM to evaluate each detected boundary
|
|
8
|
+
against a fixed disqualification list (too-few-files, too-low-loc,
|
|
9
|
+
generated-code, auto-generated-tag, etc.). The boundary checks are fully
|
|
10
|
+
deterministic — file counts, line counts, path-substring matches against a
|
|
11
|
+
known generated-code roster, header sentinel scans — and prior runs have
|
|
12
|
+
shown LLMs miscount files, miss generated-code paths inside nested
|
|
13
|
+
`node_modules/`, and forget to scan for `@generated`. This script collapses
|
|
14
|
+
that work into one call.
|
|
15
|
+
|
|
16
|
+
Canonical disqualification rules (kept in sync with
|
|
17
|
+
`src/skf-analyze-source/references/unit-detection-heuristics.md`):
|
|
18
|
+
|
|
19
|
+
- too-few-files : files_count < {MIN_FILES} (default 3)
|
|
20
|
+
- too-low-loc : loc_total < {MIN_LOC} (default 100)
|
|
21
|
+
- generated-code : any file path contains a known generated/vendor
|
|
22
|
+
directory segment (node_modules/, vendor/, dist/,
|
|
23
|
+
build/, __pycache__/, .next/, target/, .gradle/)
|
|
24
|
+
- auto-generated-tag: any file's first 10 lines contain a known
|
|
25
|
+
"auto-generated" sentinel string (`@generated`,
|
|
26
|
+
`// GENERATED CODE`, `# AUTOGENERATED`,
|
|
27
|
+
`<!-- generated -->`); case-insensitive matching
|
|
28
|
+
on the full sentinel phrases.
|
|
29
|
+
|
|
30
|
+
Rules NOT enforced by this script (still LLM-judged in step 2 §D):
|
|
31
|
+
- pure-configuration
|
|
32
|
+
- test-only
|
|
33
|
+
- already-skilled (requires existing_skills cross-reference)
|
|
34
|
+
|
|
35
|
+
LoC counting: per file, count lines with NON-empty content. A "blank line"
|
|
36
|
+
is one whose stripped form is empty (whitespace-only lines count as blank).
|
|
37
|
+
This matches the spec phrasing "lines with content (strip empty lines)".
|
|
38
|
+
|
|
39
|
+
The check order matters because heuristics are reported as a single
|
|
40
|
+
`reason`; the first match wins. Order is:
|
|
41
|
+
1. generated-code (path-based, cheapest, often the dominant signal)
|
|
42
|
+
2. auto-generated-tag (header scan)
|
|
43
|
+
3. too-few-files (file count after the above)
|
|
44
|
+
4. too-low-loc (loc after the above)
|
|
45
|
+
|
|
46
|
+
Subcommand:
|
|
47
|
+
filter --boundaries <json-file-or--for-stdin> [--source-root <path>]
|
|
48
|
+
[--min-files N] [--min-loc N]
|
|
49
|
+
|
|
50
|
+
Read a boundaries JSON of the shape:
|
|
51
|
+
[
|
|
52
|
+
{"name": "<unit-name>",
|
|
53
|
+
"path": "<rel-from-source-root>",
|
|
54
|
+
"files": ["<rel-path>", ...]},
|
|
55
|
+
...
|
|
56
|
+
]
|
|
57
|
+
and emit:
|
|
58
|
+
{
|
|
59
|
+
"kept": [{"name": "...", "path": "...",
|
|
60
|
+
"files_count": N, "loc_total": L}, ...],
|
|
61
|
+
"dropped": [{"name": "...",
|
|
62
|
+
"reason": "<too-few-files|too-low-loc|generated-code|"
|
|
63
|
+
"auto-generated-tag>",
|
|
64
|
+
"context": {"files_count": N, "loc_total": L,
|
|
65
|
+
"first_match": "..."}}, ...],
|
|
66
|
+
"stats": {"kept": N, "dropped": N,
|
|
67
|
+
"by_reason": {"<reason>": N, ...}}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
--source-root defaults to the current working directory. File paths in
|
|
71
|
+
the `files[]` array are resolved against `<source-root>/<boundary-path>`
|
|
72
|
+
first (to match `path` from the earlier scan-project step), and falling
|
|
73
|
+
back to `<source-root>/<file-rel>` if that doesn't exist. Missing files
|
|
74
|
+
are treated as 0-line zero-byte files; the boundary is still subject to
|
|
75
|
+
count/loc thresholds.
|
|
76
|
+
|
|
77
|
+
CLI examples:
|
|
78
|
+
uv run skf-disqualify-candidates.py filter --boundaries boundaries.json
|
|
79
|
+
cat boundaries.json | uv run skf-disqualify-candidates.py filter \\
|
|
80
|
+
--boundaries - --source-root /path/to/project
|
|
81
|
+
|
|
82
|
+
Exit codes:
|
|
83
|
+
0 — operation succeeded (including: empty input → empty kept/dropped)
|
|
84
|
+
1 — user error (bad JSON, missing required keys, malformed entry)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
from __future__ import annotations
|
|
88
|
+
|
|
89
|
+
import argparse
|
|
90
|
+
import json
|
|
91
|
+
import sys
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --------------------------------------------------------------------------
|
|
96
|
+
# Constants — disqualification rules (single source of truth)
|
|
97
|
+
# --------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
MIN_FILES = 3
|
|
101
|
+
MIN_LOC = 100
|
|
102
|
+
|
|
103
|
+
# Path segments that mark generated / vendored / build-output trees.
|
|
104
|
+
# Trailing slash is significant — we match the segment as a directory.
|
|
105
|
+
GENERATED_PATH_SEGMENTS = (
|
|
106
|
+
"node_modules/",
|
|
107
|
+
"vendor/",
|
|
108
|
+
"dist/",
|
|
109
|
+
"build/",
|
|
110
|
+
"__pycache__/",
|
|
111
|
+
".next/",
|
|
112
|
+
"target/",
|
|
113
|
+
".gradle/",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Header-sentinel scan: case-insensitive substring match on each of the
|
|
117
|
+
# first AUTO_GEN_HEADER_LINES of a file. The strings are spec-canonical;
|
|
118
|
+
# we apply case-insensitive matching (compare to a lowercased line).
|
|
119
|
+
AUTO_GEN_SENTINELS = (
|
|
120
|
+
"@generated",
|
|
121
|
+
"// generated code",
|
|
122
|
+
"# autogenerated",
|
|
123
|
+
"<!-- generated -->",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
AUTO_GEN_HEADER_LINES = 10
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Reason codes — exported as module-level constants so callers (and tests)
|
|
130
|
+
# can reference them without string typos.
|
|
131
|
+
REASON_TOO_FEW_FILES = "too-few-files"
|
|
132
|
+
REASON_TOO_LOW_LOC = "too-low-loc"
|
|
133
|
+
REASON_GENERATED_CODE = "generated-code"
|
|
134
|
+
REASON_AUTO_GENERATED_TAG = "auto-generated-tag"
|
|
135
|
+
|
|
136
|
+
ALL_REASONS = (
|
|
137
|
+
REASON_GENERATED_CODE,
|
|
138
|
+
REASON_AUTO_GENERATED_TAG,
|
|
139
|
+
REASON_TOO_FEW_FILES,
|
|
140
|
+
REASON_TOO_LOW_LOC,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# --------------------------------------------------------------------------
|
|
145
|
+
# Boundary validation
|
|
146
|
+
# --------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def validate_boundaries(boundaries: object) -> list[dict]:
|
|
150
|
+
"""Validate the boundaries input is a list of {name, path, files} records.
|
|
151
|
+
|
|
152
|
+
Returns the list with each entry's `files` normalized to forward-slash
|
|
153
|
+
strings. Raises ValueError on any structural defect.
|
|
154
|
+
"""
|
|
155
|
+
if not isinstance(boundaries, list):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"boundaries input must be a JSON array; got {type(boundaries).__name__}"
|
|
158
|
+
)
|
|
159
|
+
normalized: list[dict] = []
|
|
160
|
+
for idx, entry in enumerate(boundaries):
|
|
161
|
+
if not isinstance(entry, dict):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"boundary entry at index {idx} is not an object: {entry!r}"
|
|
164
|
+
)
|
|
165
|
+
name = entry.get("name")
|
|
166
|
+
if not isinstance(name, str) or not name:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"boundary entry at index {idx} missing required `name` field"
|
|
169
|
+
)
|
|
170
|
+
path = entry.get("path")
|
|
171
|
+
if not isinstance(path, str):
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"boundary entry {name!r} missing required `path` field"
|
|
174
|
+
)
|
|
175
|
+
files = entry.get("files")
|
|
176
|
+
if not isinstance(files, list):
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"boundary entry {name!r} missing required `files` array"
|
|
179
|
+
)
|
|
180
|
+
norm_files: list[str] = []
|
|
181
|
+
for f_idx, f in enumerate(files):
|
|
182
|
+
if not isinstance(f, str):
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"boundary {name!r} files[{f_idx}] is not a string: {f!r}"
|
|
185
|
+
)
|
|
186
|
+
norm_files.append(f.replace("\\", "/"))
|
|
187
|
+
normalized.append({
|
|
188
|
+
"name": name,
|
|
189
|
+
"path": path.replace("\\", "/"),
|
|
190
|
+
"files": norm_files,
|
|
191
|
+
})
|
|
192
|
+
return normalized
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --------------------------------------------------------------------------
|
|
196
|
+
# Path-based detection (generated-code)
|
|
197
|
+
# --------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def find_generated_segment(file_path: str) -> str | None:
|
|
201
|
+
"""Return the first generated-path segment that appears in `file_path`,
|
|
202
|
+
or None if none match.
|
|
203
|
+
|
|
204
|
+
The match is on the raw string with forward slashes. We compare against
|
|
205
|
+
`<segment>` at any position (start or interior); a `node_modules/` at
|
|
206
|
+
the root counts the same as one nested deeper.
|
|
207
|
+
"""
|
|
208
|
+
norm = file_path.replace("\\", "/")
|
|
209
|
+
# accept matches at the very start (no leading slash needed) too —
|
|
210
|
+
# so `node_modules/foo.js` and `pkg/node_modules/foo.js` both hit.
|
|
211
|
+
bracketed = "/" + norm # so segments can match at the head consistently
|
|
212
|
+
for seg in GENERATED_PATH_SEGMENTS:
|
|
213
|
+
if "/" + seg in bracketed:
|
|
214
|
+
return seg
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# --------------------------------------------------------------------------
|
|
219
|
+
# Header-sentinel scan (auto-generated-tag)
|
|
220
|
+
# --------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_AUTO_GEN_SENTINELS_LC = tuple(s.lower() for s in AUTO_GEN_SENTINELS)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def has_auto_generated_marker(file_abs: Path) -> str | None:
|
|
227
|
+
"""Scan the first AUTO_GEN_HEADER_LINES lines of `file_abs` for any of
|
|
228
|
+
the AUTO_GEN_SENTINELS (case-insensitive substring match).
|
|
229
|
+
|
|
230
|
+
Returns the matched sentinel (lowercased) on first hit, or None.
|
|
231
|
+
Unreadable / non-UTF-8 files return None — header-sentinel detection
|
|
232
|
+
is opportunistic; binary blobs will simply not match.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
with file_abs.open("r", encoding="utf-8", errors="replace") as fh:
|
|
236
|
+
for i, raw_line in enumerate(fh):
|
|
237
|
+
if i >= AUTO_GEN_HEADER_LINES:
|
|
238
|
+
break
|
|
239
|
+
lc = raw_line.lower()
|
|
240
|
+
for sentinel in _AUTO_GEN_SENTINELS_LC:
|
|
241
|
+
if sentinel in lc:
|
|
242
|
+
return sentinel
|
|
243
|
+
except OSError:
|
|
244
|
+
return None
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# --------------------------------------------------------------------------
|
|
249
|
+
# LoC counting
|
|
250
|
+
# --------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def count_nonblank_lines(file_abs: Path) -> int:
|
|
254
|
+
"""Count lines whose stripped form is non-empty. Whitespace-only lines
|
|
255
|
+
count as blank. Returns 0 for unreadable files.
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
with file_abs.open("r", encoding="utf-8", errors="replace") as fh:
|
|
259
|
+
n = 0
|
|
260
|
+
for line in fh:
|
|
261
|
+
if line.strip():
|
|
262
|
+
n += 1
|
|
263
|
+
return n
|
|
264
|
+
except OSError:
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# --------------------------------------------------------------------------
|
|
269
|
+
# Per-boundary evaluation
|
|
270
|
+
# --------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _resolve_file(source_root: Path, boundary_path: str, file_rel: str) -> Path:
|
|
274
|
+
"""Resolve a boundary's `files[]` entry to an absolute path.
|
|
275
|
+
|
|
276
|
+
Tries in order:
|
|
277
|
+
1. <source_root>/<file_rel> (file_rel already source-root-relative)
|
|
278
|
+
2. <source_root>/<boundary_path>/<file_rel> (file_rel boundary-relative)
|
|
279
|
+
|
|
280
|
+
The first existing path wins; otherwise returns the source-root option
|
|
281
|
+
so downstream code can decide what "missing" means.
|
|
282
|
+
"""
|
|
283
|
+
candidate_a = source_root / file_rel
|
|
284
|
+
if candidate_a.is_file():
|
|
285
|
+
return candidate_a
|
|
286
|
+
candidate_b = source_root / boundary_path / file_rel
|
|
287
|
+
if candidate_b.is_file():
|
|
288
|
+
return candidate_b
|
|
289
|
+
# neither exists — return the source-root form for missing-file accounting
|
|
290
|
+
return candidate_a
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def evaluate_boundary(
|
|
294
|
+
boundary: dict,
|
|
295
|
+
*,
|
|
296
|
+
source_root: Path,
|
|
297
|
+
min_files: int = MIN_FILES,
|
|
298
|
+
min_loc: int = MIN_LOC,
|
|
299
|
+
) -> dict:
|
|
300
|
+
"""Evaluate a single boundary against all disqualification rules.
|
|
301
|
+
|
|
302
|
+
Returns a record:
|
|
303
|
+
- kept: {"name", "path", "files_count", "loc_total"}
|
|
304
|
+
- dropped: {"name", "reason", "context": {...}}
|
|
305
|
+
|
|
306
|
+
Check order:
|
|
307
|
+
1. generated-code (path segment match)
|
|
308
|
+
2. auto-generated-tag (header sentinel scan)
|
|
309
|
+
3. too-few-files (count < min_files)
|
|
310
|
+
4. too-low-loc (sum < min_loc)
|
|
311
|
+
"""
|
|
312
|
+
name = boundary["name"]
|
|
313
|
+
path = boundary["path"]
|
|
314
|
+
files = boundary["files"]
|
|
315
|
+
files_count = len(files)
|
|
316
|
+
|
|
317
|
+
# 1. generated-code — path-substring match (cheapest signal)
|
|
318
|
+
for f in files:
|
|
319
|
+
seg = find_generated_segment(f)
|
|
320
|
+
if seg:
|
|
321
|
+
return {
|
|
322
|
+
"kind": "dropped",
|
|
323
|
+
"record": {
|
|
324
|
+
"name": name,
|
|
325
|
+
"reason": REASON_GENERATED_CODE,
|
|
326
|
+
"context": {
|
|
327
|
+
"files_count": files_count,
|
|
328
|
+
"first_match": f,
|
|
329
|
+
"matched_segment": seg,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# 2. auto-generated-tag — header sentinel scan
|
|
335
|
+
for f in files:
|
|
336
|
+
abs_path = _resolve_file(source_root, path, f)
|
|
337
|
+
sentinel = has_auto_generated_marker(abs_path)
|
|
338
|
+
if sentinel:
|
|
339
|
+
return {
|
|
340
|
+
"kind": "dropped",
|
|
341
|
+
"record": {
|
|
342
|
+
"name": name,
|
|
343
|
+
"reason": REASON_AUTO_GENERATED_TAG,
|
|
344
|
+
"context": {
|
|
345
|
+
"files_count": files_count,
|
|
346
|
+
"first_match": f,
|
|
347
|
+
"matched_sentinel": sentinel,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Compute loc_total for the remaining checks (and for kept records).
|
|
353
|
+
loc_total = 0
|
|
354
|
+
for f in files:
|
|
355
|
+
abs_path = _resolve_file(source_root, path, f)
|
|
356
|
+
loc_total += count_nonblank_lines(abs_path)
|
|
357
|
+
|
|
358
|
+
# 3. too-few-files
|
|
359
|
+
if files_count < min_files:
|
|
360
|
+
return {
|
|
361
|
+
"kind": "dropped",
|
|
362
|
+
"record": {
|
|
363
|
+
"name": name,
|
|
364
|
+
"reason": REASON_TOO_FEW_FILES,
|
|
365
|
+
"context": {
|
|
366
|
+
"files_count": files_count,
|
|
367
|
+
"loc_total": loc_total,
|
|
368
|
+
"threshold": min_files,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# 4. too-low-loc
|
|
374
|
+
if loc_total < min_loc:
|
|
375
|
+
return {
|
|
376
|
+
"kind": "dropped",
|
|
377
|
+
"record": {
|
|
378
|
+
"name": name,
|
|
379
|
+
"reason": REASON_TOO_LOW_LOC,
|
|
380
|
+
"context": {
|
|
381
|
+
"files_count": files_count,
|
|
382
|
+
"loc_total": loc_total,
|
|
383
|
+
"threshold": min_loc,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"kind": "kept",
|
|
390
|
+
"record": {
|
|
391
|
+
"name": name,
|
|
392
|
+
"path": path,
|
|
393
|
+
"files_count": files_count,
|
|
394
|
+
"loc_total": loc_total,
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --------------------------------------------------------------------------
|
|
400
|
+
# Top-level filter
|
|
401
|
+
# --------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def filter_boundaries(
|
|
405
|
+
boundaries: list[dict],
|
|
406
|
+
*,
|
|
407
|
+
source_root: Path,
|
|
408
|
+
min_files: int = MIN_FILES,
|
|
409
|
+
min_loc: int = MIN_LOC,
|
|
410
|
+
) -> dict:
|
|
411
|
+
"""Run evaluate_boundary on each entry; aggregate kept/dropped/stats."""
|
|
412
|
+
kept: list[dict] = []
|
|
413
|
+
dropped: list[dict] = []
|
|
414
|
+
by_reason: dict[str, int] = {r: 0 for r in ALL_REASONS}
|
|
415
|
+
|
|
416
|
+
for b in boundaries:
|
|
417
|
+
result = evaluate_boundary(
|
|
418
|
+
b,
|
|
419
|
+
source_root=source_root,
|
|
420
|
+
min_files=min_files,
|
|
421
|
+
min_loc=min_loc,
|
|
422
|
+
)
|
|
423
|
+
if result["kind"] == "kept":
|
|
424
|
+
kept.append(result["record"])
|
|
425
|
+
else:
|
|
426
|
+
dropped.append(result["record"])
|
|
427
|
+
by_reason[result["record"]["reason"]] += 1
|
|
428
|
+
|
|
429
|
+
# prune zero-count reasons from by_reason — keeps the report tight
|
|
430
|
+
by_reason_compact = {r: n for r, n in by_reason.items() if n > 0}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
"kept": kept,
|
|
434
|
+
"dropped": dropped,
|
|
435
|
+
"stats": {
|
|
436
|
+
"kept": len(kept),
|
|
437
|
+
"dropped": len(dropped),
|
|
438
|
+
"by_reason": by_reason_compact,
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# --------------------------------------------------------------------------
|
|
444
|
+
# CLI
|
|
445
|
+
# --------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _read_boundaries_source(source: str) -> object:
|
|
449
|
+
"""Read boundaries JSON from a file path or stdin (if source == '-')."""
|
|
450
|
+
if source == "-":
|
|
451
|
+
try:
|
|
452
|
+
text = sys.stdin.read()
|
|
453
|
+
except OSError as exc:
|
|
454
|
+
raise ValueError(
|
|
455
|
+
f"failed to read boundaries JSON from stdin: {exc}"
|
|
456
|
+
) from exc
|
|
457
|
+
else:
|
|
458
|
+
path = Path(source)
|
|
459
|
+
if not path.is_file():
|
|
460
|
+
raise ValueError(f"boundaries file not found: {path}")
|
|
461
|
+
try:
|
|
462
|
+
text = path.read_text(encoding="utf-8")
|
|
463
|
+
except OSError as exc:
|
|
464
|
+
raise ValueError(
|
|
465
|
+
f"failed to read boundaries file {path}: {exc}"
|
|
466
|
+
) from exc
|
|
467
|
+
try:
|
|
468
|
+
return json.loads(text)
|
|
469
|
+
except json.JSONDecodeError as exc:
|
|
470
|
+
raise ValueError(f"malformed JSON in boundaries input: {exc}") from exc
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _cmd_filter(args: argparse.Namespace) -> int:
|
|
474
|
+
try:
|
|
475
|
+
raw = _read_boundaries_source(args.boundaries)
|
|
476
|
+
boundaries = validate_boundaries(raw)
|
|
477
|
+
except ValueError as exc:
|
|
478
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
479
|
+
return 1
|
|
480
|
+
|
|
481
|
+
source_root = Path(args.source_root or ".").resolve()
|
|
482
|
+
if not source_root.is_dir():
|
|
483
|
+
print(
|
|
484
|
+
f"error: source-root not a directory: {source_root}", file=sys.stderr
|
|
485
|
+
)
|
|
486
|
+
return 1
|
|
487
|
+
|
|
488
|
+
if args.min_files < 0:
|
|
489
|
+
print(
|
|
490
|
+
f"error: --min-files must be >= 0; got {args.min_files}",
|
|
491
|
+
file=sys.stderr,
|
|
492
|
+
)
|
|
493
|
+
return 1
|
|
494
|
+
if args.min_loc < 0:
|
|
495
|
+
print(
|
|
496
|
+
f"error: --min-loc must be >= 0; got {args.min_loc}",
|
|
497
|
+
file=sys.stderr,
|
|
498
|
+
)
|
|
499
|
+
return 1
|
|
500
|
+
|
|
501
|
+
result = filter_boundaries(
|
|
502
|
+
boundaries,
|
|
503
|
+
source_root=source_root,
|
|
504
|
+
min_files=args.min_files,
|
|
505
|
+
min_loc=args.min_loc,
|
|
506
|
+
)
|
|
507
|
+
json.dump(result, sys.stdout, indent=2)
|
|
508
|
+
sys.stdout.write("\n")
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
513
|
+
parser = argparse.ArgumentParser(
|
|
514
|
+
prog="skf-disqualify-candidates",
|
|
515
|
+
description=(
|
|
516
|
+
"Filter detected boundaries against the unit-detection "
|
|
517
|
+
"disqualification list (too-few-files, too-low-loc, "
|
|
518
|
+
"generated-code, auto-generated-tag)."
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
522
|
+
|
|
523
|
+
p_filter = sub.add_parser(
|
|
524
|
+
"filter",
|
|
525
|
+
help="emit kept/dropped/stats partition as JSON",
|
|
526
|
+
)
|
|
527
|
+
p_filter.add_argument(
|
|
528
|
+
"--boundaries",
|
|
529
|
+
required=True,
|
|
530
|
+
help=(
|
|
531
|
+
"path to boundaries JSON, or '-' to read from stdin. "
|
|
532
|
+
"Shape: [{\"name\": \"<unit>\", \"path\": \"<rel>\", "
|
|
533
|
+
"\"files\": [\"<rel>\", ...]}, ...]"
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
p_filter.add_argument(
|
|
537
|
+
"--source-root",
|
|
538
|
+
default=None,
|
|
539
|
+
help=(
|
|
540
|
+
"project root for resolving file paths in `files[]` "
|
|
541
|
+
"(default: current working directory). Each file is resolved as "
|
|
542
|
+
"<source-root>/<file-rel> first, falling back to "
|
|
543
|
+
"<source-root>/<boundary-path>/<file-rel>."
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
p_filter.add_argument(
|
|
547
|
+
"--min-files",
|
|
548
|
+
type=int,
|
|
549
|
+
default=MIN_FILES,
|
|
550
|
+
help=(
|
|
551
|
+
f"minimum files for too-few-files (default {MIN_FILES}; "
|
|
552
|
+
"matches unit-detection-heuristics.md)"
|
|
553
|
+
),
|
|
554
|
+
)
|
|
555
|
+
p_filter.add_argument(
|
|
556
|
+
"--min-loc",
|
|
557
|
+
type=int,
|
|
558
|
+
default=MIN_LOC,
|
|
559
|
+
help=(
|
|
560
|
+
f"minimum non-blank LoC for too-low-loc (default {MIN_LOC}; "
|
|
561
|
+
"matches unit-detection-heuristics.md)"
|
|
562
|
+
),
|
|
563
|
+
)
|
|
564
|
+
p_filter.set_defaults(func=_cmd_filter)
|
|
565
|
+
|
|
566
|
+
return parser
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def main(argv: list[str] | None = None) -> int:
|
|
570
|
+
parser = _build_parser()
|
|
571
|
+
args = parser.parse_args(argv)
|
|
572
|
+
return args.func(args)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
if __name__ == "__main__":
|
|
576
|
+
raise SystemExit(main())
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Emit Brief Result Envelope — Schema-locked headless output for skf-brief-skill.
|
|
6
6
|
|
|
7
|
-
Replaces the prose-driven envelope assembly in `src/skf-brief-skill/
|
|
8
|
-
|
|
7
|
+
Replaces the prose-driven envelope assembly in `src/skf-brief-skill/references/
|
|
8
|
+
write-brief.md` §4b with one Python invocation. The envelope contract
|
|
9
9
|
(SKF_BRIEF_RESULT_JSON) is documented in src/skf-brief-skill/SKILL.md Result
|
|
10
10
|
Contract section.
|
|
11
11
|
|
|
@@ -239,7 +239,7 @@ def main() -> int:
|
|
|
239
239
|
"--target",
|
|
240
240
|
choices=["stdout", "stderr"],
|
|
241
241
|
default="stdout",
|
|
242
|
-
help="Output stream for the prefixed envelope line. step
|
|
242
|
+
help="Output stream for the prefixed envelope line. step 5 §4b uses stdout on success and stderr on HARD HALT.",
|
|
243
243
|
)
|
|
244
244
|
|
|
245
245
|
sub.add_parser("validate", help="Read envelope JSON on stdin, exit 0 if schema-valid")
|