bmad-module-skill-forge 1.4.1 → 1.5.1
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 +147 -16
- 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-manifest-ops.py +22 -6
- 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} +48 -14
- package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +32 -9
- 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} +16 -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} +63 -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} +35 -17
- package/src/skf-brief-skill/SKILL.md +49 -19
- 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} +27 -15
- 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} +35 -29
- 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 +2 -2
- package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +27 -20
- 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} +29 -16
- 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 +10 -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} +48 -28
- 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} +37 -11
- package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +32 -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} +44 -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 +79 -0
- package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +23 -11
- 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} +37 -73
- 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} +13 -9
- 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 +170 -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} +18 -14
- package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +27 -42
- package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +13 -9
- package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +14 -10
- 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,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/armelhbobdad/bmad-module-skill-forge/src/shared/scripts/schemas/skf-update-result-envelope.v1.json",
|
|
4
|
+
"title": "SKF_UPDATE_RESULT_JSON envelope (v1)",
|
|
5
|
+
"description": "Single-line JSON document emitted by skf-update-skill step 7 (report) when {headless_mode} is true, prefixed with the literal 'SKF_UPDATE_RESULT_JSON: '. Pipelines branch on the envelope's top-level fields without parsing the human-readable report. The `headless_decisions[]` array surfaces every auto-resolved gate so non-interactive runs can be audited post-hoc — previously these were buried in the evidence report as free-form log lines. Schema added in PR #319.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["skf_update"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"skf_update": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": false,
|
|
13
|
+
"required": [
|
|
14
|
+
"status",
|
|
15
|
+
"skill_name",
|
|
16
|
+
"version",
|
|
17
|
+
"previous_version",
|
|
18
|
+
"update_mode",
|
|
19
|
+
"files_written",
|
|
20
|
+
"headless_decisions",
|
|
21
|
+
"warnings",
|
|
22
|
+
"error"
|
|
23
|
+
],
|
|
24
|
+
"properties": {
|
|
25
|
+
"status": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": [
|
|
28
|
+
"success",
|
|
29
|
+
"no-changes",
|
|
30
|
+
"detect-only",
|
|
31
|
+
"dry-run",
|
|
32
|
+
"halted-for-workspace-drift",
|
|
33
|
+
"halted-for-brief-refinement",
|
|
34
|
+
"halted-for-audit",
|
|
35
|
+
"halted-for-remediation-path",
|
|
36
|
+
"halted-for-manual-mismatch",
|
|
37
|
+
"halted-for-write-failure",
|
|
38
|
+
"halted-for-concurrent-run",
|
|
39
|
+
"blocked"
|
|
40
|
+
],
|
|
41
|
+
"description": "Single-field outcome for pipeline branching. 'success' when the run completed and wrote the updated artifacts. 'no-changes' when detect-changes found nothing to update. 'detect-only' when the workflow was invoked with --detect-only and exited after step 2 without modifying any artifact. 'dry-run' when the workflow was invoked with --dry-run and exited after step 3 (re-extract) without merging or writing — the envelope describes what merge+write WOULD have done. Each 'halted-for-*' value corresponds to a documented halt in the stage prose (workspace-drift from re-extract.md §0.a, brief-refinement from detect-changes.md §1c [U] / §2.2 [B], audit from §2.2 [A], remediation-path from re-extract.md §0a, manual-mismatch from write.md §1, write-failure for any §5b/§6 failure, concurrent-run from init.md §1b's PID-file collision check). 'blocked' is the catch-all for halts that don't map to a documented status code. NOTE: detect-only and dry-run are successful exits — pipelines treating non-success as failure must include them in the success set."
|
|
42
|
+
},
|
|
43
|
+
"skill_name": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1,
|
|
46
|
+
"description": "Name of the skill being updated, read from metadata.json at workflow start."
|
|
47
|
+
},
|
|
48
|
+
"version": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"minLength": 1,
|
|
51
|
+
"description": "Version written this run. In gap-driven mode this equals previous_version (gap-driven runs do not bump). In normal mode this is either the detected source version or an incremented patch."
|
|
52
|
+
},
|
|
53
|
+
"previous_version": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"minLength": 1,
|
|
56
|
+
"description": "Version read from metadata.json at workflow start, before any write."
|
|
57
|
+
},
|
|
58
|
+
"update_mode": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"enum": ["normal", "gap-driven", "degraded"],
|
|
61
|
+
"description": "Which detect-changes path ran. 'normal' = provenance map present and source-drift detection ran. 'gap-driven' = --from-test-report path. 'degraded' = no provenance baseline (every export looks modified)."
|
|
62
|
+
},
|
|
63
|
+
"files_written": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"items": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": ["SKILL.md", "metadata.json", "provenance-map.json", "evidence-report.md", "context-snippet.md", "active-symlink"]
|
|
68
|
+
},
|
|
69
|
+
"uniqueItems": true,
|
|
70
|
+
"description": "Artifacts actually written or updated this run. Empty array when a halt fired before any write completed."
|
|
71
|
+
},
|
|
72
|
+
"headless_decisions": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": ["gate", "default_action", "taken_action", "reason"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"gate": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"enum": [
|
|
82
|
+
"init.update-confirmation",
|
|
83
|
+
"detect-changes.promoted-doc-prompt",
|
|
84
|
+
"detect-changes.scope-expansion",
|
|
85
|
+
"detect-changes.deletion-ratio",
|
|
86
|
+
"merge.clean-merge-gate"
|
|
87
|
+
],
|
|
88
|
+
"description": "Identifier for the gate. Matches the labels in stage prose so post-hoc audits can grep both directions."
|
|
89
|
+
},
|
|
90
|
+
"default_action": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1,
|
|
93
|
+
"description": "The action documented as the default for this gate (e.g. 'C' for [C] Continue, 'S' for [S] Skip)."
|
|
94
|
+
},
|
|
95
|
+
"taken_action": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"minLength": 1,
|
|
98
|
+
"description": "The action actually taken. In headless mode this almost always equals default_action — the field exists so future user-overrides via flags (e.g. --orphan-action style) can differ from defaults."
|
|
99
|
+
},
|
|
100
|
+
"reason": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"minLength": 1,
|
|
103
|
+
"description": "Human-readable rationale (e.g. 'headless: no user to prompt')."
|
|
104
|
+
},
|
|
105
|
+
"evidence": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"additionalProperties": true,
|
|
108
|
+
"description": "Optional gate-specific data — for deletion-ratio, the ratio value and counts; for promoted-doc, the candidate path; etc. Pipelines that want the audit trail richer than reason can read this."
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"description": "One entry per gate that auto-resolved during this run. Empty when no gates fired (no-changes path, or interactive run where the user resolved gates manually). The merge.clean-merge-gate entry never appears with taken_action=continue when the gate halted on conflicts — conflict halts produce a halted-for-* status instead."
|
|
113
|
+
},
|
|
114
|
+
"warnings": {
|
|
115
|
+
"type": "array",
|
|
116
|
+
"items": { "type": "string" },
|
|
117
|
+
"description": "Non-fatal anomalies surfaced during the run. May include workspace_drift_overridden, scope_reconciliation_post entries, qmd indexing failures, etc. Empty when none."
|
|
118
|
+
},
|
|
119
|
+
"error": {
|
|
120
|
+
"oneOf": [
|
|
121
|
+
{ "type": "null" },
|
|
122
|
+
{
|
|
123
|
+
"type": "object",
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"required": ["phase", "reason"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"phase": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"minLength": 1,
|
|
130
|
+
"description": "Step or operation that failed (e.g. 'write:active-symlink', 'init:provenance-load')."
|
|
131
|
+
},
|
|
132
|
+
"path": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "Optional path the failed operation targeted (when applicable)."
|
|
135
|
+
},
|
|
136
|
+
"reason": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"minLength": 1,
|
|
139
|
+
"description": "Human-readable error message."
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"description": "Null on success or no-changes. Object describing the failure when a halt or write error fired. Pipelines branch on error !== null for non-zero exit semantics."
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/armelhbobdad/bmad-module-skill-forge/src/shared/scripts/schemas/skill-brief.v1.json",
|
|
4
4
|
"title": "skill-brief.yaml (v1)",
|
|
5
|
-
"description": "Canonical schema for the skill-brief.yaml artifact written by skf-brief-skill step
|
|
5
|
+
"description": "Canonical schema for the skill-brief.yaml artifact written by skf-brief-skill step 5 and consumed by skf-create-skill, skf-update-skill, skf-audit-skill. Mirrors src/skf-brief-skill/assets/skill-brief-schema.md.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["name", "version", "source_repo", "language", "description", "forge_tier", "created", "created_by", "scope"],
|
|
8
8
|
"properties": {
|
|
@@ -70,7 +70,27 @@
|
|
|
70
70
|
},
|
|
71
71
|
"include": { "type": "array", "items": { "type": "string" } },
|
|
72
72
|
"exclude": { "type": "array", "items": { "type": "string" } },
|
|
73
|
-
"notes": { "type": "string" }
|
|
73
|
+
"notes": { "type": "string" },
|
|
74
|
+
"rationale": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"description": "Optional authoring-time scope-type decision record written once by skf-brief-skill step 03. Absent on legacy briefs.",
|
|
77
|
+
"required": ["recommended", "chosen", "accepted_recommendation", "heuristic", "reason", "recorded"],
|
|
78
|
+
"additionalProperties": false,
|
|
79
|
+
"properties": {
|
|
80
|
+
"recommended": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"enum": ["full-library", "specific-modules", "public-api", "component-library", "reference-app", "docs-only"]
|
|
83
|
+
},
|
|
84
|
+
"chosen": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"enum": ["full-library", "specific-modules", "public-api", "component-library", "reference-app", "docs-only"]
|
|
87
|
+
},
|
|
88
|
+
"accepted_recommendation": { "type": "boolean" },
|
|
89
|
+
"heuristic": { "type": "string", "minLength": 1 },
|
|
90
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
91
|
+
"recorded": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
74
94
|
}
|
|
75
95
|
}
|
|
76
96
|
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Build Change Manifest — aggregate category A/B/C/D detection results.
|
|
6
|
+
|
|
7
|
+
`skf-update-skill/references/detect-changes.md §3` assembles the final
|
|
8
|
+
change manifest from four category result streams the LLM (or its
|
|
9
|
+
subprocess workers) produces upstream:
|
|
10
|
+
|
|
11
|
+
- Category A — file-level changes (modified/added/deleted)
|
|
12
|
+
- Category B — export-level changes (for MODIFIED files)
|
|
13
|
+
- Category C — rename detection (cross-files and cross-exports)
|
|
14
|
+
- Category D — script/asset file changes
|
|
15
|
+
|
|
16
|
+
The aggregation itself is purely deterministic: count rollups, grouping
|
|
17
|
+
exports under their owning file, deduplicating across categories, and
|
|
18
|
+
emitting the canonical manifest shape that downstream stages consume.
|
|
19
|
+
The §3 prose previously asked the LLM to count entries across four
|
|
20
|
+
streams every run — the helper makes it one bash call with stable
|
|
21
|
+
shape.
|
|
22
|
+
|
|
23
|
+
The same script also implements §2.2's Major-Version Scope
|
|
24
|
+
Reconciliation trigger (the deletion-ratio formula) — same input
|
|
25
|
+
schema, plus the provenance map for the denominator.
|
|
26
|
+
|
|
27
|
+
Subcommands:
|
|
28
|
+
|
|
29
|
+
build [--input <file>] [--no-changes-on-empty]
|
|
30
|
+
Reads category JSON from stdin or `--input <file>`, emits the
|
|
31
|
+
unified manifest envelope.
|
|
32
|
+
|
|
33
|
+
deletion-ratio --provenance-map <file> [--input <file>]
|
|
34
|
+
Same category JSON input. Reads provenance entries[] from
|
|
35
|
+
<file>, computes the §2.2 trigger envelope. Auto-skips when
|
|
36
|
+
degraded_mode or update_mode==gap-driven is set in the input.
|
|
37
|
+
|
|
38
|
+
Input JSON shape (object on stdin or in --input file):
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
"category_a": {
|
|
42
|
+
"modified": ["path1", ...],
|
|
43
|
+
"added": ["path2", ...],
|
|
44
|
+
"deleted": ["path3", ...]
|
|
45
|
+
},
|
|
46
|
+
"category_b": {
|
|
47
|
+
"modified_exports": [{name, file, old_line, new_line}, ...],
|
|
48
|
+
"new_exports": [{name, file, line}, ...],
|
|
49
|
+
"deleted_exports": [{name, file, old_line}, ...],
|
|
50
|
+
"moved_exports": [{name, file, old_line, new_line}, ...]
|
|
51
|
+
},
|
|
52
|
+
"category_c": {
|
|
53
|
+
"renamed_files": [{old_path, new_path}, ...],
|
|
54
|
+
"renamed_exports": [{old_name, new_name, file}, ...]
|
|
55
|
+
},
|
|
56
|
+
"category_d": {
|
|
57
|
+
"scripts_modified": ["path", ...], "scripts_added": [...], "scripts_deleted": [...],
|
|
58
|
+
"assets_modified": [...], "assets_added": [...], "assets_deleted": [...]
|
|
59
|
+
},
|
|
60
|
+
"degraded_mode": false,
|
|
61
|
+
"update_mode": "normal" | "gap-driven" // optional; influences deletion-ratio only
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Every category and sub-key is optional — missing keys default to
|
|
65
|
+
empty lists. This lets gap-driven runs (which produce no Category D
|
|
66
|
+
results) and degraded runs (which skip Category B) feed the same
|
|
67
|
+
script without sentinel values.
|
|
68
|
+
|
|
69
|
+
Exit codes:
|
|
70
|
+
0 — operation succeeded
|
|
71
|
+
1 — user error (malformed JSON, bad path, malformed provenance file)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
from __future__ import annotations
|
|
75
|
+
|
|
76
|
+
import argparse
|
|
77
|
+
import json
|
|
78
|
+
import sys
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------------------
|
|
83
|
+
# Input normalization
|
|
84
|
+
# --------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_list(d: dict, *path: str) -> list:
|
|
88
|
+
"""Pluck a list out of a nested dict, defaulting to []."""
|
|
89
|
+
cur = d
|
|
90
|
+
for key in path:
|
|
91
|
+
if not isinstance(cur, dict):
|
|
92
|
+
return []
|
|
93
|
+
cur = cur.get(key)
|
|
94
|
+
if cur is None:
|
|
95
|
+
return []
|
|
96
|
+
return cur if isinstance(cur, list) else []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _load_input(input_path: Path | None) -> dict:
|
|
100
|
+
"""Read JSON either from --input path or from stdin."""
|
|
101
|
+
if input_path is not None:
|
|
102
|
+
text = input_path.read_text(encoding="utf-8")
|
|
103
|
+
else:
|
|
104
|
+
text = sys.stdin.read()
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(text)
|
|
107
|
+
except json.JSONDecodeError as exc:
|
|
108
|
+
raise ValueError(f"input is not valid JSON: {exc}") from exc
|
|
109
|
+
if not isinstance(data, dict):
|
|
110
|
+
raise ValueError(f"input must be a JSON object, got {type(data).__name__}")
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --------------------------------------------------------------------------
|
|
115
|
+
# Build manifest
|
|
116
|
+
# --------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_manifest(payload: dict) -> dict:
|
|
120
|
+
"""Aggregate category A/B/C/D into the unified manifest envelope."""
|
|
121
|
+
cat_a = payload.get("category_a", {}) or {}
|
|
122
|
+
cat_b = payload.get("category_b", {}) or {}
|
|
123
|
+
cat_c = payload.get("category_c", {}) or {}
|
|
124
|
+
cat_d = payload.get("category_d", {}) or {}
|
|
125
|
+
|
|
126
|
+
a_modified = _get_list(cat_a, "modified")
|
|
127
|
+
a_added = _get_list(cat_a, "added")
|
|
128
|
+
a_deleted = _get_list(cat_a, "deleted")
|
|
129
|
+
|
|
130
|
+
b_modified = _get_list(cat_b, "modified_exports")
|
|
131
|
+
b_new = _get_list(cat_b, "new_exports")
|
|
132
|
+
b_deleted = _get_list(cat_b, "deleted_exports")
|
|
133
|
+
b_moved = _get_list(cat_b, "moved_exports")
|
|
134
|
+
|
|
135
|
+
c_renamed_files = _get_list(cat_c, "renamed_files")
|
|
136
|
+
c_renamed_exports = _get_list(cat_c, "renamed_exports")
|
|
137
|
+
|
|
138
|
+
d = {
|
|
139
|
+
"scripts_modified": len(_get_list(cat_d, "scripts_modified")),
|
|
140
|
+
"scripts_added": len(_get_list(cat_d, "scripts_added")),
|
|
141
|
+
"scripts_deleted": len(_get_list(cat_d, "scripts_deleted")),
|
|
142
|
+
"assets_modified": len(_get_list(cat_d, "assets_modified")),
|
|
143
|
+
"assets_added": len(_get_list(cat_d, "assets_added")),
|
|
144
|
+
"assets_deleted": len(_get_list(cat_d, "assets_deleted")),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
counts = {
|
|
148
|
+
"files_changed": len(a_modified),
|
|
149
|
+
"files_added": len(a_added),
|
|
150
|
+
"files_deleted": len(a_deleted),
|
|
151
|
+
"files_moved": len(c_renamed_files),
|
|
152
|
+
"exports_modified": len(b_modified),
|
|
153
|
+
"exports_new": len(b_new),
|
|
154
|
+
"exports_deleted": len(b_deleted),
|
|
155
|
+
"exports_renamed": len(c_renamed_exports),
|
|
156
|
+
"exports_moved": len(b_moved),
|
|
157
|
+
**d,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
per_file = _build_per_file(
|
|
161
|
+
a_modified=a_modified,
|
|
162
|
+
a_added=a_added,
|
|
163
|
+
a_deleted=a_deleted,
|
|
164
|
+
c_renamed_files=c_renamed_files,
|
|
165
|
+
b_modified=b_modified,
|
|
166
|
+
b_new=b_new,
|
|
167
|
+
b_deleted=b_deleted,
|
|
168
|
+
b_moved=b_moved,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
total_export_changes = (
|
|
172
|
+
counts["exports_modified"]
|
|
173
|
+
+ counts["exports_new"]
|
|
174
|
+
+ counts["exports_deleted"]
|
|
175
|
+
+ counts["exports_renamed"]
|
|
176
|
+
+ counts["exports_moved"]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
no_changes = (
|
|
180
|
+
sum(counts.values()) == 0 # every count is zero
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"no_changes": no_changes,
|
|
185
|
+
"degraded_mode": bool(payload.get("degraded_mode")),
|
|
186
|
+
"counts": counts,
|
|
187
|
+
"total_export_changes": total_export_changes,
|
|
188
|
+
"per_file": per_file,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _build_per_file(
|
|
193
|
+
*,
|
|
194
|
+
a_modified: list,
|
|
195
|
+
a_added: list,
|
|
196
|
+
a_deleted: list,
|
|
197
|
+
c_renamed_files: list,
|
|
198
|
+
b_modified: list,
|
|
199
|
+
b_new: list,
|
|
200
|
+
b_deleted: list,
|
|
201
|
+
b_moved: list,
|
|
202
|
+
) -> list[dict]:
|
|
203
|
+
"""Group exports under their owning file path. Preserves stable ordering:
|
|
204
|
+
MODIFIED files first, then ADDED, then DELETED, then MOVED. Files within
|
|
205
|
+
each status sorted alphabetically by path."""
|
|
206
|
+
exports_by_file: dict[str, list[dict]] = {}
|
|
207
|
+
|
|
208
|
+
def _record(entry: dict, change_type: str) -> None:
|
|
209
|
+
path = entry.get("file")
|
|
210
|
+
if not path:
|
|
211
|
+
return
|
|
212
|
+
exports_by_file.setdefault(path, []).append({
|
|
213
|
+
"name": entry.get("name"),
|
|
214
|
+
"change_type": change_type,
|
|
215
|
+
"old_line": entry.get("old_line"),
|
|
216
|
+
"new_line": entry.get("new_line") or entry.get("line"),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
for e in b_modified:
|
|
220
|
+
_record(e, "MODIFIED_EXPORT")
|
|
221
|
+
for e in b_new:
|
|
222
|
+
_record(e, "NEW_EXPORT")
|
|
223
|
+
for e in b_deleted:
|
|
224
|
+
_record(e, "DELETED_EXPORT")
|
|
225
|
+
for e in b_moved:
|
|
226
|
+
_record(e, "MOVED_EXPORT")
|
|
227
|
+
|
|
228
|
+
out: list[dict] = []
|
|
229
|
+
|
|
230
|
+
for path in sorted(a_modified):
|
|
231
|
+
out.append({
|
|
232
|
+
"file_path": path,
|
|
233
|
+
"status": "MODIFIED",
|
|
234
|
+
"exports_affected": exports_by_file.get(path, []),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
for path in sorted(a_added):
|
|
238
|
+
out.append({
|
|
239
|
+
"file_path": path,
|
|
240
|
+
"status": "ADDED",
|
|
241
|
+
"exports_affected": exports_by_file.get(path, []),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
for path in sorted(a_deleted):
|
|
245
|
+
out.append({
|
|
246
|
+
"file_path": path,
|
|
247
|
+
"status": "DELETED",
|
|
248
|
+
"exports_affected": exports_by_file.get(path, []),
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# MOVED entries from Category C — emit one record per move with the new path
|
|
252
|
+
for move in sorted(c_renamed_files, key=lambda m: m.get("new_path") or ""):
|
|
253
|
+
new_path = move.get("new_path")
|
|
254
|
+
if new_path is None:
|
|
255
|
+
continue
|
|
256
|
+
out.append({
|
|
257
|
+
"file_path": new_path,
|
|
258
|
+
"status": "MOVED",
|
|
259
|
+
"old_path": move.get("old_path"),
|
|
260
|
+
"exports_affected": exports_by_file.get(new_path, []),
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# --------------------------------------------------------------------------
|
|
267
|
+
# Deletion ratio (§2.2)
|
|
268
|
+
# --------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def compute_deletion_ratio(payload: dict, provenance: dict) -> dict:
|
|
272
|
+
"""Compute the §2.2 deletion-ratio trigger envelope.
|
|
273
|
+
|
|
274
|
+
Skip conditions (per the §2.2 prose):
|
|
275
|
+
- update_mode == "gap-driven"
|
|
276
|
+
- degraded_mode is True
|
|
277
|
+
- provenance has zero entries (denominator would be zero)
|
|
278
|
+
"""
|
|
279
|
+
if payload.get("update_mode") == "gap-driven":
|
|
280
|
+
return _ratio_skip(skip_reason="gap-driven")
|
|
281
|
+
if payload.get("degraded_mode"):
|
|
282
|
+
return _ratio_skip(skip_reason="degraded-mode")
|
|
283
|
+
|
|
284
|
+
entries = provenance.get("entries", [])
|
|
285
|
+
if not isinstance(entries, list):
|
|
286
|
+
raise ValueError("provenance `entries` must be an array")
|
|
287
|
+
total = len(entries)
|
|
288
|
+
if total == 0:
|
|
289
|
+
return _ratio_skip(skip_reason="zero-provenance-exports")
|
|
290
|
+
|
|
291
|
+
# Count exports under category A deleted files (via provenance lookup)
|
|
292
|
+
by_source: dict[str, int] = {}
|
|
293
|
+
for entry in entries:
|
|
294
|
+
sf = entry.get("source_file")
|
|
295
|
+
if isinstance(sf, str):
|
|
296
|
+
by_source[sf] = by_source.get(sf, 0) + 1
|
|
297
|
+
|
|
298
|
+
cat_a = payload.get("category_a", {}) or {}
|
|
299
|
+
deleted_files = _get_list(cat_a, "deleted")
|
|
300
|
+
deleted_in_files = sum(by_source.get(p, 0) for p in deleted_files)
|
|
301
|
+
|
|
302
|
+
cat_b = payload.get("category_b", {}) or {}
|
|
303
|
+
deleted_exports = len(_get_list(cat_b, "deleted_exports"))
|
|
304
|
+
|
|
305
|
+
deleted_export_count = deleted_in_files + deleted_exports
|
|
306
|
+
ratio = deleted_export_count / total
|
|
307
|
+
|
|
308
|
+
cat_c = payload.get("category_c", {}) or {}
|
|
309
|
+
renamed_or_moved = (
|
|
310
|
+
len(_get_list(cat_c, "renamed_files"))
|
|
311
|
+
+ len(_get_list(cat_b, "moved_exports"))
|
|
312
|
+
+ len(_get_list(cat_c, "renamed_exports"))
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"skip_reason": None,
|
|
317
|
+
"deleted_export_count": deleted_export_count,
|
|
318
|
+
"total_provenance_exports": total,
|
|
319
|
+
"deletion_ratio": ratio,
|
|
320
|
+
"deleted_file_count": len(deleted_files),
|
|
321
|
+
"added_in_scope_count": len(_get_list(cat_a, "added")),
|
|
322
|
+
"renamed_or_moved_count": renamed_or_moved,
|
|
323
|
+
"should_trigger": ratio >= 0.50,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _ratio_skip(*, skip_reason: str) -> dict:
|
|
328
|
+
return {
|
|
329
|
+
"skip_reason": skip_reason,
|
|
330
|
+
"deleted_export_count": 0,
|
|
331
|
+
"total_provenance_exports": 0,
|
|
332
|
+
"deletion_ratio": 0.0,
|
|
333
|
+
"deleted_file_count": 0,
|
|
334
|
+
"added_in_scope_count": 0,
|
|
335
|
+
"renamed_or_moved_count": 0,
|
|
336
|
+
"should_trigger": False,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# --------------------------------------------------------------------------
|
|
341
|
+
# CLI
|
|
342
|
+
# --------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _cmd_build(args: argparse.Namespace) -> int:
|
|
346
|
+
try:
|
|
347
|
+
payload = _load_input(Path(args.input) if args.input else None)
|
|
348
|
+
except ValueError as exc:
|
|
349
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
350
|
+
return 1
|
|
351
|
+
manifest = build_manifest(payload)
|
|
352
|
+
json.dump(manifest, sys.stdout, indent=2)
|
|
353
|
+
sys.stdout.write("\n")
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _cmd_deletion_ratio(args: argparse.Namespace) -> int:
|
|
358
|
+
try:
|
|
359
|
+
payload = _load_input(Path(args.input) if args.input else None)
|
|
360
|
+
except ValueError as exc:
|
|
361
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
362
|
+
return 1
|
|
363
|
+
prov_path = Path(args.provenance_map)
|
|
364
|
+
if not prov_path.is_file():
|
|
365
|
+
print(f"error: provenance-map not found: {prov_path}", file=sys.stderr)
|
|
366
|
+
return 1
|
|
367
|
+
try:
|
|
368
|
+
provenance = json.loads(prov_path.read_text(encoding="utf-8"))
|
|
369
|
+
except json.JSONDecodeError as exc:
|
|
370
|
+
print(f"error: provenance-map is not valid JSON: {exc}", file=sys.stderr)
|
|
371
|
+
return 1
|
|
372
|
+
try:
|
|
373
|
+
result = compute_deletion_ratio(payload, provenance)
|
|
374
|
+
except ValueError as exc:
|
|
375
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
376
|
+
return 1
|
|
377
|
+
json.dump(result, sys.stdout, indent=2)
|
|
378
|
+
sys.stdout.write("\n")
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
383
|
+
parser = argparse.ArgumentParser(
|
|
384
|
+
prog="skf-build-change-manifest",
|
|
385
|
+
description=(
|
|
386
|
+
"Aggregate category A/B/C/D detection results into the unified "
|
|
387
|
+
"change manifest, OR compute the §2.2 deletion-ratio trigger."
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
391
|
+
|
|
392
|
+
p_build = sub.add_parser("build", help="aggregate categories into manifest")
|
|
393
|
+
p_build.add_argument(
|
|
394
|
+
"--input", default=None, help="path to JSON input (default: stdin)"
|
|
395
|
+
)
|
|
396
|
+
p_build.set_defaults(func=_cmd_build)
|
|
397
|
+
|
|
398
|
+
p_ratio = sub.add_parser(
|
|
399
|
+
"deletion-ratio",
|
|
400
|
+
help="compute §2.2 trigger; requires --provenance-map",
|
|
401
|
+
)
|
|
402
|
+
p_ratio.add_argument(
|
|
403
|
+
"--input", default=None, help="path to JSON input (default: stdin)"
|
|
404
|
+
)
|
|
405
|
+
p_ratio.add_argument(
|
|
406
|
+
"--provenance-map", required=True, help="path to provenance-map.json"
|
|
407
|
+
)
|
|
408
|
+
p_ratio.set_defaults(func=_cmd_deletion_ratio)
|
|
409
|
+
|
|
410
|
+
return parser
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def main(argv: list[str] | None = None) -> int:
|
|
414
|
+
parser = _build_parser()
|
|
415
|
+
args = parser.parse_args(argv)
|
|
416
|
+
return args.func(args)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
raise SystemExit(main())
|