bmad-module-skill-forge 3.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/architecture.md +2 -2
- package/docs/concepts.md +4 -4
- package/docs/examples.md +33 -26
- package/docs/getting-started.md +3 -3
- package/docs/how-it-works.md +2 -2
- package/docs/index.md +1 -1
- package/docs/skill-model.md +41 -39
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +46 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +5 -5
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +12 -4
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +1437 -178
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
- package/src/shared/scripts/skf-extract-public-api.py +1673 -189
- package/src/shared/scripts/skf-extraction-inventory.py +584 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-parse-gaps.py +135 -26
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-resolve-customization.py +197 -0
- package/src/shared/scripts/skf-shard-body.py +239 -41
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +1439 -79
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +2 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +3 -3
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +4 -11
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-campaign/SKILL.md +2 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +7 -7
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +23 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +10 -11
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +21 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +2 -2
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-export-skill/SKILL.md +2 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -2
- package/src/skf-refine-architecture/SKILL.md +6 -8
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-setup/SKILL.md +2 -2
- package/src/skf-test-skill/SKILL.md +2 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +5 -4
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +69 -111
- package/src/skf-test-skill/references/init.md +5 -6
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +11 -9
- package/src/skf-test-skill/references/scoring-rules.md +8 -6
- package/src/skf-test-skill/references/source-access-protocol.md +15 -18
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +580 -80
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +646 -107
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +450 -23
- package/src/skf-test-skill/scripts/score-signatures.py +356 -61
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +2 -2
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +37 -22
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +17 -12
- package/src/skf-update-skill/references/re-extract.md +4 -2
- package/src/skf-update-skill/references/report.md +7 -4
- package/src/skf-update-skill/references/write.md +77 -36
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
- package/src/skf-verify-stack/SKILL.md +2 -2
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# /// script
|
|
2
2
|
# requires-python = ">=3.11"
|
|
3
|
-
# dependencies = []
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Extraction Inventory: write, extend and read create-skill's extraction inventory on disk.
|
|
6
6
|
|
|
@@ -12,8 +12,9 @@ runner's JSON and the script/asset detector's JSON, so the model never
|
|
|
12
12
|
types a record those tools already wrote; the model then sends only what it
|
|
13
13
|
produced itself (`patch`, `add`, `set`). Step 3c adds the T3 items it
|
|
14
14
|
extracts from fetched documentation, step 4 the T2 annotations it finds in
|
|
15
|
-
QMD,
|
|
16
|
-
|
|
15
|
+
QMD, compile writes the provenance map (`provenance`) and metadata.json
|
|
16
|
+
(`metadata`) from it, and step 7 writes `extraction-rules.yaml` from it
|
|
17
|
+
(`rules`). Every write goes through a temporary file and one rename.
|
|
17
18
|
|
|
18
19
|
Subcommands:
|
|
19
20
|
|
|
@@ -32,6 +33,33 @@ Subcommands:
|
|
|
32
33
|
file_issues[] file, each files_without_recipes extension, each
|
|
33
34
|
warnings[] entry). From --detected (skf-detect-scripts-assets.py
|
|
34
35
|
detect): scripts_inventory and assets_inventory.
|
|
36
|
+
Public surface. When the runner's scope.type is public-api (in
|
|
37
|
+
any case) and its entry-point diff can be read, each export of a
|
|
38
|
+
language family whose entry_points.by_language status is
|
|
39
|
+
`barrel` is marked `public: true` when its name and file are a
|
|
40
|
+
pair of the diff's `public` or `other_definitions` (each other
|
|
41
|
+
in-scope definition of a public name, #830; its name, or the
|
|
42
|
+
name `local` gives it in that file) or `extraction_gaps`, or
|
|
43
|
+
when a `public` item of its name and family has no file;
|
|
44
|
+
`public: false` otherwise. A
|
|
45
|
+
`public` item with `via` namespace and a `members` list (what
|
|
46
|
+
the submodule passes on, which users reach as `pkg.sub.name`)
|
|
47
|
+
gives its members' pairs (each member's name, or its `local`
|
|
48
|
+
one, and its file; a member with no file its name) in place of
|
|
49
|
+
its own; one whose `members` is null (a module the runner could
|
|
50
|
+
not read), or that has no `members` key (an older runner's),
|
|
51
|
+
gives its own pair, as any item does. It is the rule
|
|
52
|
+
skf-build-change-manifest.py's `records` marks an update's
|
|
53
|
+
exports by (that script loads it from here). The rule's inputs
|
|
54
|
+
are kept as `public_surface`, so `add` marks an export read by
|
|
55
|
+
eye the same way. Any other scope type, and a family with no
|
|
56
|
+
barrel, mark nothing. A public-api run with no entry-point diff,
|
|
57
|
+
an incomplete one, one with errors or one whose
|
|
58
|
+
entry_points.unresolved is not empty marks nothing either
|
|
59
|
+
(`public_surface` null), with one warning that says why: every
|
|
60
|
+
export is then documented. One warning names the exports marked
|
|
61
|
+
`public: false`, each as `name (file)` (the first ten, then how
|
|
62
|
+
many more).
|
|
35
63
|
|
|
36
64
|
add --inventory <file> --field exports|t2_annotations|t3_items|warnings
|
|
37
65
|
Read a JSON list on stdin (objects; strings for warnings) and
|
|
@@ -40,7 +68,16 @@ Subcommands:
|
|
|
40
68
|
added twice and counts in `duplicates`. An exports entry needs an
|
|
41
69
|
export_name; a missing confidence, extraction_method,
|
|
42
70
|
ast_node_type or ast_recipe defaults to T1-low, source-read,
|
|
43
|
-
null and null (an export read by eye).
|
|
71
|
+
null and null (an export read by eye). Its source_file is
|
|
72
|
+
relative to the source root, written and compared with `/`
|
|
73
|
+
separators and no leading `./`, as the runner writes its files
|
|
74
|
+
(`./pkg/api.py` and `pkg\\api.py` are `pkg/api.py`); an absolute
|
|
75
|
+
one (a leading `/` or a drive letter) is refused, and the call
|
|
76
|
+
adds nothing. An exports entry is
|
|
77
|
+
marked `public` by the inventory's `public_surface`, as `init`
|
|
78
|
+
marks the runner's (a `public` key the payload sends is not
|
|
79
|
+
kept), and the warning naming the exports marked `public: false`
|
|
80
|
+
is written again, in its place. A t3_items entry whose
|
|
44
81
|
export_name names an export the inventory already holds is
|
|
45
82
|
dropped, not added: T3 content never overrides a T1, T1-low or
|
|
46
83
|
T2 entry for the same export.
|
|
@@ -54,7 +91,8 @@ Subcommands:
|
|
|
54
91
|
in `unmatched`, one that matches several in `ambiguous`; neither
|
|
55
92
|
changes anything. params lists each parameter as the runner
|
|
56
93
|
records one, {name, type, default, optional}, or as a typed
|
|
57
|
-
string ('userId: string').
|
|
94
|
+
string ('userId: string'). The source_file is compared as `add`
|
|
95
|
+
compares it: `./pkg/api.py` matches the export at `pkg/api.py`.
|
|
58
96
|
|
|
59
97
|
set --inventory <file>
|
|
60
98
|
Read a JSON object on stdin and set each of its keys: top_exports,
|
|
@@ -64,7 +102,9 @@ Subcommands:
|
|
|
64
102
|
and intent_mapping. intent_mapping maps `scripts` and `assets`
|
|
65
103
|
to {"intent": "<free text>", "kept": [source_file, ...]}: the
|
|
66
104
|
inventory keeps only the kept entries of that list and records
|
|
67
|
-
the others in the mapping's `left_out`.
|
|
105
|
+
the others in the mapping's `left_out`. A top_exports name every
|
|
106
|
+
export of which is marked `public: false` is not kept and is
|
|
107
|
+
listed in `dropped`.
|
|
68
108
|
|
|
69
109
|
provenance --inventory <file> --target <provenance-map.json> [--add-entries]
|
|
70
110
|
Write the provenance map to --target, so compile never types an
|
|
@@ -74,7 +114,9 @@ Subcommands:
|
|
|
74
114
|
"2.0"), skill_name (default the inventory's), skill_type
|
|
75
115
|
(default "single") and generated_at (default now, ISO-8601
|
|
76
116
|
UTC); any other key is refused. entries[] holds the entry of
|
|
77
|
-
every export of the inventory, in its order
|
|
117
|
+
every export of the inventory, in its order, but one marked
|
|
118
|
+
`public: false` (off a public-api skill's public surface, which
|
|
119
|
+
neither entries[] nor `unsigned` lists): export_name,
|
|
78
120
|
export_type, params, return_type, source_file, source_line,
|
|
79
121
|
confidence, extraction_method, ast_node_type and
|
|
80
122
|
signature_source, labeled by the tool that found it. An export
|
|
@@ -97,7 +139,52 @@ Subcommands:
|
|
|
97
139
|
T3) and the file entries. An entry whose export_name and
|
|
98
140
|
source_file the map already holds, or a file entry whose
|
|
99
141
|
file_name it holds, is not added and counts in `duplicates`, so
|
|
100
|
-
an inventory export's entry is never replaced.
|
|
142
|
+
an inventory export's entry is never replaced. An entry whose
|
|
143
|
+
export_name and source_file are an inventory export's marked
|
|
144
|
+
`public: false` is not added either and is listed in
|
|
145
|
+
`not_public`, so the map never holds an export off the surface.
|
|
146
|
+
|
|
147
|
+
metadata --inventory <file> --brief <skill-brief.yaml> --stats <file>
|
|
148
|
+
--skf-root <dir> --target <metadata.json>
|
|
149
|
+
Write the skill's metadata.json to --target, so compile types
|
|
150
|
+
only the fields it decides. stdin holds them, a JSON object:
|
|
151
|
+
version and language (non-empty strings), source_root,
|
|
152
|
+
source_commit, source_ref and qmd_version (strings or null),
|
|
153
|
+
each required, and ast_grep_version (a string or null, default
|
|
154
|
+
null) and stats_extra (an object, default {}); any other key is
|
|
155
|
+
refused. The rest comes from the files:
|
|
156
|
+
name, scope_type (the brief's scope.type, verbatim), source_repo
|
|
157
|
+
and source_authority (default "community") from --brief;
|
|
158
|
+
description from the frontmatter of the SKILL.md beside
|
|
159
|
+
--target (compile's staged one); confidence_tier (the
|
|
160
|
+
inventory's tier); exports[], the names of the inventory's
|
|
161
|
+
exports but one marked `internal: true` or `public: false`,
|
|
162
|
+
each once, in its order (a docs-only inventory's t3_items
|
|
163
|
+
names; none for a reference-app brief); ast_node_count and
|
|
164
|
+
tool_versions.ast_grep, only when ast-grep ran: when the
|
|
165
|
+
runner ran it (extraction_rules.ast_grep_version is set), the
|
|
166
|
+
runner's aggregates.exports and that version; otherwise, when
|
|
167
|
+
exports carry extraction_method ast-grep (the recipes run
|
|
168
|
+
without the runner), their number and stdin's
|
|
169
|
+
ast_grep_version, else "unknown" with a warning (stdin's
|
|
170
|
+
ast_grep_version is read in that case only);
|
|
171
|
+
tool_versions.ast_grep is null when ast-grep did not run, and
|
|
172
|
+
ast_node_count left out; scripts[] and assets[], {file:
|
|
173
|
+
scripts/<name> or assets/<name>, purpose, source_file,
|
|
174
|
+
confidence} per scripts_inventory or assets_inventory entry,
|
|
175
|
+
left out when it is empty; and stats and
|
|
176
|
+
confidence_distribution verbatim from --stats (the JSON
|
|
177
|
+
skf-render-metadata-stats.py wrote), with stats_extra's keys
|
|
178
|
+
added: a component-library brief's own stats, or a
|
|
179
|
+
reference-app brief's notes alone (another scope type takes
|
|
180
|
+
none, and a key the stats helper wrote is refused). It stamps
|
|
181
|
+
generation_date (UTC, to the second), reads tool_versions.skf
|
|
182
|
+
under --skf-root (the installed module) as
|
|
183
|
+
skf-render-quick-metadata.py does (package.json's version, else
|
|
184
|
+
the VERSION file's first line, else "unknown", with a warning),
|
|
185
|
+
and writes skill_type "single", spec_version "1.3",
|
|
186
|
+
generated_by "create-skill", dependencies [] and compatibility
|
|
187
|
+
"". The inventory is not changed.
|
|
101
188
|
|
|
102
189
|
rules --inventory <file> --language <language> --target <file>
|
|
103
190
|
Write extraction-rules.yaml (the skill, its language, tier and
|
|
@@ -105,7 +192,9 @@ Subcommands:
|
|
|
105
192
|
--target. The inventory is not changed.
|
|
106
193
|
|
|
107
194
|
summary --inventory <file>
|
|
108
|
-
Print the inventory's counts without changing it
|
|
195
|
+
Print the inventory's counts without changing it, and in
|
|
196
|
+
`not_public` each export marked `public: false` ({name, file}),
|
|
197
|
+
which the evidence report lists.
|
|
109
198
|
|
|
110
199
|
Inventory shape (the keys this helper reads or writes):
|
|
111
200
|
|
|
@@ -120,49 +209,69 @@ Inventory shape (the keys this helper reads or writes):
|
|
|
120
209
|
"co_imports": [...], "promoted_docs": [...],
|
|
121
210
|
"authoritative_files_scan": {...} | null,
|
|
122
211
|
"extraction_rules": {"recipe_set", "recipes", "scope", "ast_grep_version"},
|
|
212
|
+
"public_surface": {"by_language": {...}, "public": [...],
|
|
213
|
+
"other_definitions": [...],
|
|
214
|
+
"extraction_gaps": [...]} | null,
|
|
123
215
|
"t2_annotations": [...], "t3_items": [...], "warnings": ["..."]
|
|
124
216
|
}
|
|
125
217
|
|
|
126
|
-
|
|
218
|
+
An export may carry `internal: true` (the runner's entry_point_diff.internal
|
|
219
|
+
lists it) and `public: true` or `public: false` (see init's Public surface).
|
|
220
|
+
|
|
221
|
+
Output (stdout, exit 0), for every subcommand but rules, provenance and
|
|
222
|
+
metadata:
|
|
127
223
|
|
|
128
224
|
{
|
|
129
225
|
"status": "ok",
|
|
130
226
|
"inventory": "<path, / separators>",
|
|
131
227
|
"extraction_mode": "...",
|
|
132
|
-
"counts": {"files_scanned": N | null, "exports": N, "
|
|
133
|
-
"t1_low": N, "by_type": {"<export_type>": N, ...},
|
|
228
|
+
"counts": {"files_scanned": N | null, "exports": N, "not_public": N,
|
|
229
|
+
"t1": N, "t1_low": N, "by_type": {"<export_type>": N, ...},
|
|
134
230
|
"top_exports": N, "co_imports": N, "scripts": N,
|
|
135
231
|
"assets": N, "t2_annotations": N, "t2_past": N,
|
|
136
232
|
"t2_future": N, "functions_enriched": N, "t3_items": N,
|
|
137
233
|
"items": N},
|
|
138
234
|
"added": N, "duplicates": N, "dropped": ["name"], # add
|
|
139
235
|
"patched": N, "unmatched": [...], "ambiguous": [...], # patch
|
|
140
|
-
"set": ["key", ...]
|
|
236
|
+
"set": ["key", ...], "dropped": ["name"], # set
|
|
237
|
+
"not_public": [{"name", "file"}, ...] # summary
|
|
141
238
|
}
|
|
142
239
|
|
|
143
|
-
|
|
144
|
-
|
|
240
|
+
not_public counts the exports marked `public: false`, which the provenance
|
|
241
|
+
map leaves out: when it equals `exports`, the map holds no export. `items`
|
|
242
|
+
is exports plus t3_items, less not_public: what compile can document. t1
|
|
243
|
+
and t1_low count the exports by their `confidence`, by_type by their
|
|
244
|
+
`export_type`.
|
|
145
245
|
t2_past and t2_future count the T2 annotations by their `temporal`, and
|
|
146
|
-
functions_enriched the distinct export_name values among them
|
|
246
|
+
functions_enriched the distinct export_name values among them; an
|
|
247
|
+
annotation of a name every export of which is marked `public: false`
|
|
248
|
+
counts in none of t2_annotations, t2_past, t2_future and
|
|
249
|
+
functions_enriched. rules
|
|
147
250
|
prints {"status": "ok", "target": "<path>", "bytes": N}, provenance
|
|
148
251
|
{"status": "ok", "target": "<path>", "entries": N, "unsigned": ["name
|
|
149
252
|
(file)", ...]}, and provenance --add-entries {"status": "ok", "target":
|
|
150
|
-
"<path>", "entries": N, "file_entries": N, "added": N, "duplicates": N
|
|
151
|
-
the counts of the map after the
|
|
253
|
+
"<path>", "entries": N, "file_entries": N, "added": N, "duplicates": N,
|
|
254
|
+
"not_public": [{"name", "file"}, ...]}, the counts of the map after the
|
|
255
|
+
merge, and metadata {"status": "ok", "target": "<path>", "exports": N,
|
|
256
|
+
"commit_short": "<source_commit's first 8 characters, or unknown>",
|
|
257
|
+
"tool_versions": {...}, "warnings": [...]}.
|
|
152
258
|
|
|
153
259
|
Exit codes:
|
|
154
260
|
0 success
|
|
155
261
|
1 input error, nothing changed: an inventory that is missing, not JSON
|
|
156
|
-
or has no exports list, an --extraction
|
|
157
|
-
cannot be read, a provenance map at --target that
|
|
158
|
-
cannot read, a
|
|
159
|
-
|
|
262
|
+
or has no exports list, an --extraction, --detected, --brief or
|
|
263
|
+
--stats file that cannot be read, a provenance map at --target that
|
|
264
|
+
--add-entries cannot read, a staged SKILL.md with no frontmatter
|
|
265
|
+
description, a stdin payload of the wrong shape, an exports entry
|
|
266
|
+
whose source_file is absolute, an unknown key or a kept file the
|
|
267
|
+
inventory does not list, or a write that failed
|
|
160
268
|
2 usage error (argparse)
|
|
161
269
|
"""
|
|
162
270
|
|
|
163
271
|
from __future__ import annotations
|
|
164
272
|
|
|
165
273
|
import argparse
|
|
274
|
+
import importlib.util
|
|
166
275
|
import json
|
|
167
276
|
import os
|
|
168
277
|
import re
|
|
@@ -219,9 +328,27 @@ def _read_json(path: Path, label: str):
|
|
|
219
328
|
raise InventoryError(f"cannot read the {label} {path.as_posix()}: {exc}") from exc
|
|
220
329
|
|
|
221
330
|
|
|
331
|
+
def _off_surface_names(data: dict) -> set[str]:
|
|
332
|
+
"""The export names every inventory export of which is marked `public: false`."""
|
|
333
|
+
marks: dict[str, bool] = {}
|
|
334
|
+
for export in data.get("exports") or []:
|
|
335
|
+
if isinstance(export, dict) and isinstance(export.get("export_name"), str):
|
|
336
|
+
name = export["export_name"]
|
|
337
|
+
marks[name] = marks.get(name, True) and export.get("public") is False
|
|
338
|
+
return {name for name, off in marks.items() if off}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _not_public(data: dict) -> list[dict]:
|
|
342
|
+
"""{name, file} of each export marked `public: false`, in the inventory's order."""
|
|
343
|
+
return [{"name": e.get("export_name"), "file": e.get("source_file")} for e in data.get("exports") or []
|
|
344
|
+
if isinstance(e, dict) and e.get("public") is False]
|
|
345
|
+
|
|
346
|
+
|
|
222
347
|
def counts(data: dict) -> dict:
|
|
223
348
|
exports = [e for e in data["exports"] if isinstance(e, dict)]
|
|
224
|
-
|
|
349
|
+
off = _off_surface_names(data)
|
|
350
|
+
raw_notes = [n for n in data.get("t2_annotations") or [] if isinstance(n, dict)]
|
|
351
|
+
notes = [n for n in raw_notes if not (isinstance(n.get("export_name"), str) and n["export_name"] in off)]
|
|
225
352
|
|
|
226
353
|
def _length(key: str) -> int:
|
|
227
354
|
value = data.get(key)
|
|
@@ -233,9 +360,11 @@ def counts(data: dict) -> dict:
|
|
|
233
360
|
if isinstance(kind, str) and kind:
|
|
234
361
|
by_type[kind] = by_type.get(kind, 0) + 1
|
|
235
362
|
files = data.get("files_scanned")
|
|
363
|
+
not_public = sum(1 for e in exports if e.get("public") is False)
|
|
236
364
|
return {
|
|
237
365
|
"files_scanned": files if isinstance(files, int) else None,
|
|
238
366
|
"exports": len(data["exports"]),
|
|
367
|
+
"not_public": not_public,
|
|
239
368
|
"t1": sum(1 for e in exports if e.get("confidence") == "T1"),
|
|
240
369
|
"t1_low": sum(1 for e in exports if e.get("confidence") == "T1-low"),
|
|
241
370
|
"by_type": dict(sorted(by_type.items())),
|
|
@@ -243,12 +372,12 @@ def counts(data: dict) -> dict:
|
|
|
243
372
|
"co_imports": _length("co_imports"),
|
|
244
373
|
"scripts": _length("scripts_inventory"),
|
|
245
374
|
"assets": _length("assets_inventory"),
|
|
246
|
-
"t2_annotations": _length("t2_annotations"),
|
|
375
|
+
"t2_annotations": _length("t2_annotations") - (len(raw_notes) - len(notes)),
|
|
247
376
|
"t2_past": sum(1 for n in notes if n.get("temporal") == "T2-past"),
|
|
248
377
|
"t2_future": sum(1 for n in notes if n.get("temporal") == "T2-future"),
|
|
249
378
|
"functions_enriched": len({n["export_name"] for n in notes if isinstance(n.get("export_name"), str)}),
|
|
250
379
|
"t3_items": _length("t3_items"),
|
|
251
|
-
"items": len(data["exports"]) + _length("t3_items"),
|
|
380
|
+
"items": len(data["exports"]) - not_public + _length("t3_items"),
|
|
252
381
|
}
|
|
253
382
|
|
|
254
383
|
|
|
@@ -264,7 +393,7 @@ def runner_warnings(extraction: dict) -> list[str]:
|
|
|
264
393
|
for error in extraction.get("errors") or []:
|
|
265
394
|
if isinstance(error, dict):
|
|
266
395
|
lines.append(f"ast-grep did not finish on {error.get('files')} files from `{error.get('first_file')}` "
|
|
267
|
-
f"({error.get('detail')}): exports
|
|
396
|
+
f"({error.get('detail')}): their exports are read by eye")
|
|
268
397
|
for issue in extraction.get("file_issues") or []:
|
|
269
398
|
if isinstance(issue, dict):
|
|
270
399
|
lines.append(f"`{issue.get('file')}` ({issue.get('issue')}): its exports are read by eye")
|
|
@@ -276,13 +405,180 @@ def runner_warnings(extraction: dict) -> list[str]:
|
|
|
276
405
|
return lines
|
|
277
406
|
|
|
278
407
|
|
|
408
|
+
# --------------------------------------------------------------------------
|
|
409
|
+
# The public surface of a public-api skill (see init's "Public surface"):
|
|
410
|
+
# one rule, which skf-build-change-manifest.py loads from here for update-skill
|
|
411
|
+
# --------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _norm_path(path: object) -> str | None:
|
|
415
|
+
"""A file path compared as structural-diff compares it: `/` separators, no leading `./`."""
|
|
416
|
+
if not isinstance(path, str) or not path.strip():
|
|
417
|
+
return None
|
|
418
|
+
text = path.strip().replace("\\", "/")
|
|
419
|
+
while text.startswith("./"):
|
|
420
|
+
text = text[2:]
|
|
421
|
+
return text
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _relative_source(entry: dict) -> bool:
|
|
425
|
+
"""Write an exports entry's source_file in _norm_path's form, the runner's; False for an absolute one."""
|
|
426
|
+
path = _norm_path(entry.get("source_file"))
|
|
427
|
+
if path is None:
|
|
428
|
+
return True
|
|
429
|
+
if path.startswith("/") or re.match(r"[A-Za-z]:/", path):
|
|
430
|
+
return False
|
|
431
|
+
entry["source_file"] = path
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
PUBLIC_API = "public-api"
|
|
436
|
+
# A record's language family, the one whose entry points declare its names: skf-extract-public-api.py's FAMILY_OF,
|
|
437
|
+
# by the record's `language`, else by its file's extension (that script's EXTENSION_LANGUAGES).
|
|
438
|
+
_FAMILY_OF_LANGUAGE = {"python": "python", "typescript": "javascript", "tsx": "javascript",
|
|
439
|
+
"javascript": "javascript", "vue": "javascript", "rust": "rust", "go": "go"}
|
|
440
|
+
_FAMILY_OF_EXTENSION = {".py": "python", ".pyi": "python", ".rs": "rust", ".go": "go",
|
|
441
|
+
**dict.fromkeys((".ts", ".mts", ".cts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue"),
|
|
442
|
+
"javascript")}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class _PublicSurface:
|
|
446
|
+
"""The public surface of a public-api skill's extraction (see init's "Public surface" in the module
|
|
447
|
+
docstring)."""
|
|
448
|
+
|
|
449
|
+
def __init__(self, diff: dict, statuses: dict):
|
|
450
|
+
self.barrels = {family for family, status in statuses.items() if status == "barrel"}
|
|
451
|
+
self.pairs: set[tuple[str, str]] = set()
|
|
452
|
+
self.unfiled: set[tuple[object, str]] = set() # (family, name) of a public name no file defines
|
|
453
|
+
for key in ("public", "other_definitions", "extraction_gaps"):
|
|
454
|
+
for item in diff.get(key) or []:
|
|
455
|
+
if not isinstance(item, dict) or not isinstance(item.get("name"), str) or not item["name"]:
|
|
456
|
+
continue
|
|
457
|
+
family = item.get("language")
|
|
458
|
+
members = item.get("members") if key == "public" and item.get("via") == "namespace" else None
|
|
459
|
+
if isinstance(members, list):
|
|
460
|
+
# a namespace whose module the runner read: its members, in place of the module itself
|
|
461
|
+
for member in members:
|
|
462
|
+
if isinstance(member, dict) and isinstance(member.get("name"), str) and member["name"]:
|
|
463
|
+
self._add(family, member["name"], member.get("local"), member.get("file"), True)
|
|
464
|
+
continue
|
|
465
|
+
self._add(family, item["name"], item.get("local") if key != "extraction_gaps" else None,
|
|
466
|
+
item.get("file"), key == "public")
|
|
467
|
+
|
|
468
|
+
def _add(self, family: object, name: str, local: object, path: object, listed_public: bool) -> None:
|
|
469
|
+
"""The pairs one public name (or gap) gives: (name, file) and (local, file), or, with no file, its
|
|
470
|
+
(family, name) when `public` lists it."""
|
|
471
|
+
file = _norm_path(path)
|
|
472
|
+
if not file:
|
|
473
|
+
if listed_public:
|
|
474
|
+
self.unfiled.add((family, name))
|
|
475
|
+
return
|
|
476
|
+
self.pairs.update((n, file) for n in (name, local) if isinstance(n, str) and n)
|
|
477
|
+
|
|
478
|
+
def public(self, name: str, path: str, language: object = None) -> bool | None:
|
|
479
|
+
"""Whether the export is on the public surface; None (no mark) when its family's entry points are no
|
|
480
|
+
barrel."""
|
|
481
|
+
family = _FAMILY_OF_LANGUAGE.get(language) if isinstance(language, str) else None
|
|
482
|
+
if family is None:
|
|
483
|
+
family = _FAMILY_OF_EXTENSION.get(os.path.splitext(path)[1].lower())
|
|
484
|
+
if family not in self.barrels:
|
|
485
|
+
return None
|
|
486
|
+
return (name, path) in self.pairs or (family, name) in self.unfiled
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def public_surface(extraction: object) -> tuple[_PublicSurface | None, str | None]:
|
|
490
|
+
"""(the public surface, None) of the recipe runner's output when its scope type is public-api; (None, why the
|
|
491
|
+
surface cannot be read from it) for a public-api run with no entry-point diff, an incomplete one, one with
|
|
492
|
+
errors or one whose entry points name a module the trace could not follow; (None, None) for any other scope
|
|
493
|
+
type. With no surface every export is kept, as before."""
|
|
494
|
+
if not isinstance(extraction, dict):
|
|
495
|
+
return None, None
|
|
496
|
+
scope, diff, points = extraction.get("scope"), extraction.get("entry_point_diff"), extraction.get("entry_points")
|
|
497
|
+
kind = scope.get("type") if isinstance(scope, dict) else None
|
|
498
|
+
if not (isinstance(kind, str) and kind.strip().lower() == PUBLIC_API):
|
|
499
|
+
return None, None
|
|
500
|
+
statuses = points.get("by_language") if isinstance(points, dict) else None
|
|
501
|
+
if not isinstance(diff, dict) or not isinstance(statuses, dict):
|
|
502
|
+
return None, "the recipe runner gave no entry-point diff (Quick tier, or a run that could not read the tree)"
|
|
503
|
+
if extraction.get("status") == "incomplete" or extraction.get("errors"):
|
|
504
|
+
return None, "the recipe runner's run is incomplete"
|
|
505
|
+
if points.get("unresolved"):
|
|
506
|
+
return None, "an entry point names a module the runner could not trace (entry_points.unresolved)"
|
|
507
|
+
return _PublicSurface(diff, statuses), None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# The fields of an entry_point_diff item the rule reads, which the inventory's public_surface keeps for `add`, and
|
|
511
|
+
# those of each member of a namespace item.
|
|
512
|
+
SURFACE_FIELDS = {"public": ("name", "language", "file", "local", "via", "members"),
|
|
513
|
+
"other_definitions": ("name", "language", "file", "local"),
|
|
514
|
+
"extraction_gaps": ("name", "language", "file")}
|
|
515
|
+
MEMBER_FIELDS = ("name", "local", "file")
|
|
516
|
+
NOT_PUBLIC_NAMED = 10 # the names the not-public warning spells out, as update-skill's apply does
|
|
517
|
+
NOT_PUBLIC_LEAD = "Off the public surface: "
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _surface_state(extraction: dict) -> dict:
|
|
521
|
+
"""The parts of the runner's JSON the rule reads (public_surface found a surface in it)."""
|
|
522
|
+
diff = extraction["entry_point_diff"]
|
|
523
|
+
state: dict = {"by_language": dict(extraction["entry_points"]["by_language"])}
|
|
524
|
+
for key, fields in SURFACE_FIELDS.items():
|
|
525
|
+
state[key] = [{field: item[field] for field in fields if field in item}
|
|
526
|
+
for item in diff.get(key) or [] if isinstance(item, dict)]
|
|
527
|
+
for item in state["public"]:
|
|
528
|
+
if isinstance(item.get("members"), list):
|
|
529
|
+
item["members"] = [{field: m[field] for field in MEMBER_FIELDS if field in m}
|
|
530
|
+
for m in item["members"] if isinstance(m, dict)]
|
|
531
|
+
return state
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _stored_surface(data: dict) -> _PublicSurface | None:
|
|
535
|
+
"""The public surface the inventory's public_surface keeps, or None when init kept none."""
|
|
536
|
+
state = data.get("public_surface")
|
|
537
|
+
if not isinstance(state, dict) or not isinstance(state.get("by_language"), dict):
|
|
538
|
+
return None
|
|
539
|
+
return _PublicSurface(state, state["by_language"])
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _mark(surface: _PublicSurface | None, export: dict) -> None:
|
|
543
|
+
"""Set the export's `public` mark by the rule; no other `public` value is kept."""
|
|
544
|
+
export.pop("public", None)
|
|
545
|
+
name, path = export.get("export_name"), _norm_path(export.get("source_file"))
|
|
546
|
+
if surface is None or not isinstance(name, str) or not name or path is None:
|
|
547
|
+
return
|
|
548
|
+
mark = surface.public(name, path, export.get("language"))
|
|
549
|
+
if mark is not None:
|
|
550
|
+
export["public"] = mark
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _not_public_warning(names: list[str]) -> str:
|
|
554
|
+
"""The one warning naming the exports marked `public: false` (`name (file)`), for Gate 2 and the evidence
|
|
555
|
+
report."""
|
|
556
|
+
shown = ", ".join(names[:NOT_PUBLIC_NAMED])
|
|
557
|
+
more = f" and {len(names) - NOT_PUBLIC_NAMED} more" if len(names) > NOT_PUBLIC_NAMED else ""
|
|
558
|
+
return (f"{NOT_PUBLIC_LEAD}{len(names)} export(s) of this public-api skill are left out of the provenance map, "
|
|
559
|
+
f"SKILL.md, references/, the context snippet and metadata.json's exports[] (each is marked "
|
|
560
|
+
f"`public: false` in the inventory, and `summary` lists them all): {shown}{more}")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _write_not_public_warning(data: dict) -> None:
|
|
564
|
+
"""Write the not-public warning in place of the one an earlier call wrote (else at the end), or drop it when
|
|
565
|
+
no export is marked `public: false`."""
|
|
566
|
+
names = [f"{item['name']} ({item['file']})" for item in _not_public(data)]
|
|
567
|
+
warnings = data.setdefault("warnings", [])
|
|
568
|
+
at = next((i for i, w in enumerate(warnings) if isinstance(w, str) and w.startswith(NOT_PUBLIC_LEAD)), None)
|
|
569
|
+
if at is not None:
|
|
570
|
+
del warnings[at]
|
|
571
|
+
if names:
|
|
572
|
+
warnings.insert(len(warnings) if at is None else at, _not_public_warning(names))
|
|
573
|
+
|
|
574
|
+
|
|
279
575
|
def init(skill: str, mode: str, tier: str, extraction: dict | None = None, detected: dict | None = None) -> dict:
|
|
280
576
|
"""A new inventory, seeded from the runner's JSON and the detector's JSON."""
|
|
281
577
|
data: dict = {
|
|
282
578
|
"skill_name": skill, "extraction_mode": mode, "tier": tier, "files_scanned": None,
|
|
283
579
|
"exports": [], "top_exports": [], "aggregates": {}, "counts": {}, "arms": {},
|
|
284
580
|
"scripts_inventory": [], "assets_inventory": [], "intent_mapping": {}, "co_imports": [],
|
|
285
|
-
"promoted_docs": [], "authoritative_files_scan": None, "extraction_rules": {},
|
|
581
|
+
"promoted_docs": [], "authoritative_files_scan": None, "extraction_rules": {}, "public_surface": None,
|
|
286
582
|
"t2_annotations": [], "t3_items": [], "warnings": [],
|
|
287
583
|
}
|
|
288
584
|
if extraction is not None:
|
|
@@ -291,6 +587,7 @@ def init(skill: str, mode: str, tier: str, extraction: dict | None = None, detec
|
|
|
291
587
|
diff = extraction.get("entry_point_diff")
|
|
292
588
|
internal = {(i.get("name"), i.get("source_file"))
|
|
293
589
|
for i in (diff.get("internal") or [] if isinstance(diff, dict) else []) if isinstance(i, dict)}
|
|
590
|
+
surface, problem = public_surface(extraction)
|
|
294
591
|
for record in extraction.get("exports") or []:
|
|
295
592
|
if not isinstance(record, dict):
|
|
296
593
|
continue
|
|
@@ -299,6 +596,7 @@ def init(skill: str, mode: str, tier: str, extraction: dict | None = None, detec
|
|
|
299
596
|
export.setdefault("extraction_method", "ast-grep")
|
|
300
597
|
if (export.get("export_name"), export.get("source_file")) in internal:
|
|
301
598
|
export["internal"] = True
|
|
599
|
+
_mark(surface, export)
|
|
302
600
|
data["exports"].append(export)
|
|
303
601
|
files = extraction.get("files_in_scope")
|
|
304
602
|
data["files_scanned"] = files if isinstance(files, int) else None
|
|
@@ -313,6 +611,12 @@ def init(skill: str, mode: str, tier: str, extraction: dict | None = None, detec
|
|
|
313
611
|
"ast_grep_version": ast_grep.get("version") if isinstance(ast_grep, dict) else None,
|
|
314
612
|
}
|
|
315
613
|
data["warnings"] = runner_warnings(extraction)
|
|
614
|
+
if problem:
|
|
615
|
+
data["warnings"].append(f"The public surface of this public-api skill could not be applied ({problem}): "
|
|
616
|
+
"no export is marked `public: false`, so every export found is documented")
|
|
617
|
+
if surface is not None:
|
|
618
|
+
data["public_surface"] = _surface_state(extraction)
|
|
619
|
+
_write_not_public_warning(data)
|
|
316
620
|
if detected is not None:
|
|
317
621
|
if not isinstance(detected, dict):
|
|
318
622
|
raise InventoryError("the --detected file is not a JSON object")
|
|
@@ -335,10 +639,20 @@ def add(data: dict, field: str, entries: list) -> tuple[int, int, list[str]]:
|
|
|
335
639
|
kind = str if field == "warnings" else dict
|
|
336
640
|
if not isinstance(entries, list) or not all(isinstance(e, kind) for e in entries):
|
|
337
641
|
raise InventoryError(f"stdin must hold a JSON list of {'strings' if kind is str else 'objects'}")
|
|
642
|
+
surface = _stored_surface(data) if field == "exports" else None
|
|
338
643
|
if field == "exports":
|
|
339
644
|
if not all(isinstance(e.get("export_name"), str) and e["export_name"] for e in entries):
|
|
340
645
|
raise InventoryError("every exports entry needs an export_name")
|
|
341
646
|
entries = [{**BY_EYE_DEFAULTS, **e} for e in entries]
|
|
647
|
+
absolute = [e for e in entries if not _relative_source(e)]
|
|
648
|
+
if absolute:
|
|
649
|
+
sent = ", ".join(f"{e['export_name']}: source_file {e['source_file']}" for e in absolute[:10])
|
|
650
|
+
sent += f" and {len(absolute) - 10} more" if len(absolute) > 10 else ""
|
|
651
|
+
verb, them = ("is", "it") if len(absolute) == 1 else ("are", "them")
|
|
652
|
+
raise InventoryError(f"{sent} {verb} absolute; send {them} relative to the source root, with / "
|
|
653
|
+
"separators, as the runner writes its files")
|
|
654
|
+
for entry in entries:
|
|
655
|
+
_mark(surface, entry)
|
|
342
656
|
target = data.setdefault(field, [])
|
|
343
657
|
known = {e.get("export_name") for e in data["exports"] if isinstance(e, dict)} - {None}
|
|
344
658
|
added = duplicates = 0
|
|
@@ -348,8 +662,8 @@ def add(data: dict, field: str, entries: list) -> tuple[int, int, list[str]]:
|
|
|
348
662
|
dropped.append(entry["export_name"])
|
|
349
663
|
continue
|
|
350
664
|
if field == "exports":
|
|
351
|
-
key = (entry["export_name"], entry.get("source_file"))
|
|
352
|
-
repeat = any(isinstance(e, dict) and (e.get("export_name"), e.get("source_file")) == key
|
|
665
|
+
key = (entry["export_name"], _norm_path(entry.get("source_file")))
|
|
666
|
+
repeat = any(isinstance(e, dict) and (e.get("export_name"), _norm_path(e.get("source_file"))) == key
|
|
353
667
|
for e in target)
|
|
354
668
|
else:
|
|
355
669
|
repeat = any(_same(entry, e) for e in target)
|
|
@@ -358,6 +672,8 @@ def add(data: dict, field: str, entries: list) -> tuple[int, int, list[str]]:
|
|
|
358
672
|
continue
|
|
359
673
|
target.append(entry)
|
|
360
674
|
added += 1
|
|
675
|
+
if surface is not None:
|
|
676
|
+
_write_not_public_warning(data)
|
|
361
677
|
return added, duplicates, dropped
|
|
362
678
|
|
|
363
679
|
|
|
@@ -378,7 +694,7 @@ def patch(data: dict, entries: list) -> tuple[int, list[str], list[str]]:
|
|
|
378
694
|
for entry in entries:
|
|
379
695
|
name, source = entry["export_name"], entry.get("source_file")
|
|
380
696
|
found = [e for e in data["exports"] if isinstance(e, dict) and e.get("export_name") == name
|
|
381
|
-
and (source is None or e.get("source_file") == source)]
|
|
697
|
+
and (source is None or _norm_path(e.get("source_file")) == _norm_path(source))]
|
|
382
698
|
label = name if source is None else f"{name} ({source})"
|
|
383
699
|
if not found:
|
|
384
700
|
unmatched.append(label)
|
|
@@ -390,8 +706,9 @@ def patch(data: dict, entries: list) -> tuple[int, list[str], list[str]]:
|
|
|
390
706
|
return patched, unmatched, ambiguous
|
|
391
707
|
|
|
392
708
|
|
|
393
|
-
def set_fields(data: dict, values: dict) -> list[str]:
|
|
394
|
-
"""Set the keys of `values` on the inventory (see the module docstring); return the keys set
|
|
709
|
+
def set_fields(data: dict, values: dict) -> tuple[list[str], list[str]]:
|
|
710
|
+
"""Set the keys of `values` on the inventory (see the module docstring); return (the keys set, the top_exports
|
|
711
|
+
names dropped as off the public surface)."""
|
|
395
712
|
if not isinstance(values, dict):
|
|
396
713
|
raise InventoryError("stdin must hold a JSON object")
|
|
397
714
|
unknown = sorted(set(values) - set(SETTABLE))
|
|
@@ -403,6 +720,11 @@ def set_fields(data: dict, values: dict) -> list[str]:
|
|
|
403
720
|
names = " or ".join("null" if t is type(None) else t.__name__ for t in types)
|
|
404
721
|
raise InventoryError(f"{key} must be {names}")
|
|
405
722
|
mapping = _intent_mapping(data, values["intent_mapping"]) if "intent_mapping" in values else {}
|
|
723
|
+
dropped: list[str] = []
|
|
724
|
+
if "top_exports" in values:
|
|
725
|
+
off = _off_surface_names(data)
|
|
726
|
+
dropped = [n for n in values["top_exports"] if isinstance(n, str) and n in off]
|
|
727
|
+
values = {**values, "top_exports": [n for n in values["top_exports"] if not (isinstance(n, str) and n in off)]}
|
|
406
728
|
for key, value in values.items():
|
|
407
729
|
if key == "intent_mapping":
|
|
408
730
|
data["intent_mapping"] = {**(data.get("intent_mapping") or {}), **mapping}
|
|
@@ -410,7 +732,7 @@ def set_fields(data: dict, values: dict) -> list[str]:
|
|
|
410
732
|
data[key] = {**(data.get(key) if isinstance(data.get(key), dict) else {}), **value}
|
|
411
733
|
else:
|
|
412
734
|
data[key] = value
|
|
413
|
-
return list(values)
|
|
735
|
+
return list(values), dropped
|
|
414
736
|
|
|
415
737
|
|
|
416
738
|
def _intent_mapping(data: dict, mapping: dict) -> dict:
|
|
@@ -488,11 +810,12 @@ def _labels(export: dict) -> dict:
|
|
|
488
810
|
|
|
489
811
|
|
|
490
812
|
def provenance_entries(data: dict) -> tuple[list[dict], list[str]]:
|
|
491
|
-
"""(the provenance map entry of every export
|
|
813
|
+
"""(the provenance map entry of every export but one marked `public: false`, the functions among them with no
|
|
814
|
+
params)."""
|
|
492
815
|
entries: list[dict] = []
|
|
493
816
|
unsigned: list[str] = []
|
|
494
817
|
for export in data.get("exports") or []:
|
|
495
|
-
if not isinstance(export, dict):
|
|
818
|
+
if not isinstance(export, dict) or export.get("public") is False:
|
|
496
819
|
continue
|
|
497
820
|
raw = export.get("params")
|
|
498
821
|
params = None
|
|
@@ -512,6 +835,11 @@ def provenance_entries(data: dict) -> tuple[list[dict], list[str]]:
|
|
|
512
835
|
return entries, unsigned
|
|
513
836
|
|
|
514
837
|
|
|
838
|
+
def _utc_now() -> str:
|
|
839
|
+
"""Now, in UTC, to the second (ISO-8601)."""
|
|
840
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
841
|
+
|
|
842
|
+
|
|
515
843
|
def map_header(data: dict, given) -> dict:
|
|
516
844
|
"""The provenance map's header: stdin's keys over the defaults."""
|
|
517
845
|
if not isinstance(given, dict):
|
|
@@ -522,8 +850,7 @@ def map_header(data: dict, given) -> dict:
|
|
|
522
850
|
missing = [key for key, default in MAP_HEADER.items() if default is REQUIRED and key not in given]
|
|
523
851
|
if missing:
|
|
524
852
|
raise InventoryError(f"the map's header needs {', '.join(missing)} (null when unknown)")
|
|
525
|
-
defaults = {"skill_name": data.get("skill_name"),
|
|
526
|
-
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
|
853
|
+
defaults = {"skill_name": data.get("skill_name"), "generated_at": _utc_now()}
|
|
527
854
|
return {key: given[key] if key in given else defaults.get(key, default) for key, default in MAP_HEADER.items()}
|
|
528
855
|
|
|
529
856
|
|
|
@@ -533,12 +860,15 @@ def provenance_map(data: dict, header) -> tuple[dict, list[str]]:
|
|
|
533
860
|
return {**map_header(data, header), "entries": entries}, unsigned
|
|
534
861
|
|
|
535
862
|
|
|
536
|
-
def add_map_entries(provenance: dict, payload) -> tuple[int, int]:
|
|
537
|
-
"""Append stdin's `entries` and `file_entries` to the map; return (added, duplicates
|
|
863
|
+
def add_map_entries(provenance: dict, payload, off: frozenset = frozenset()) -> tuple[int, int, list[dict]]:
|
|
864
|
+
"""Append stdin's `entries` and `file_entries` to the map; return (added, duplicates, {name, file} of each entry
|
|
865
|
+
not added because `off`, the (export_name, source_file) of the inventory's exports marked `public: false`,
|
|
866
|
+
holds it)."""
|
|
538
867
|
if not isinstance(payload, dict) or not payload or set(payload) - {"entries", "file_entries"}:
|
|
539
868
|
raise InventoryError('stdin must hold {"entries": [...], "file_entries": [...]} (either list, or both)')
|
|
540
869
|
keys = {"entries": ("export_name", "source_file"), "file_entries": ("file_name",)}
|
|
541
870
|
added = duplicates = 0
|
|
871
|
+
not_public: list[dict] = []
|
|
542
872
|
for field, items in payload.items():
|
|
543
873
|
if not isinstance(items, list) or not all(isinstance(e, dict) for e in items):
|
|
544
874
|
raise InventoryError(f"{field} must be a list of objects")
|
|
@@ -551,13 +881,197 @@ def add_map_entries(provenance: dict, payload) -> tuple[int, int]:
|
|
|
551
881
|
held = {tuple(e.get(k) for k in keys[field]) for e in target if isinstance(e, dict)}
|
|
552
882
|
for item in items:
|
|
553
883
|
key = tuple(item.get(k) for k in keys[field])
|
|
884
|
+
if field == "entries" and (item.get("export_name"), _norm_path(item.get("source_file"))) in off:
|
|
885
|
+
not_public.append({"name": item.get("export_name"), "file": item.get("source_file")})
|
|
886
|
+
continue
|
|
554
887
|
if key in held:
|
|
555
888
|
duplicates += 1
|
|
556
889
|
continue
|
|
557
890
|
held.add(key)
|
|
558
891
|
target.append(item)
|
|
559
892
|
added += 1
|
|
560
|
-
return added, duplicates
|
|
893
|
+
return added, duplicates, not_public
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
_SIBLINGS: dict = {}
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _sibling(filename: str):
|
|
900
|
+
"""A helper installed beside this script, loaded once as a module: skf-resolve-authoritative-files.py (the
|
|
901
|
+
brief's YAML), skf-render-quick-metadata.py (the installed SKF's version) and skf-description-guard.py (the
|
|
902
|
+
staged SKILL.md's description)."""
|
|
903
|
+
if filename not in _SIBLINGS:
|
|
904
|
+
path = Path(__file__).resolve().parent / filename
|
|
905
|
+
spec = importlib.util.spec_from_file_location("skf_" + filename[4:-3].replace("-", "_"), path)
|
|
906
|
+
if spec is None or spec.loader is None:
|
|
907
|
+
raise InventoryError(f"cannot load {filename} beside {Path(__file__).name}")
|
|
908
|
+
module = importlib.util.module_from_spec(spec)
|
|
909
|
+
try:
|
|
910
|
+
spec.loader.exec_module(module)
|
|
911
|
+
except ImportError as exc:
|
|
912
|
+
raise InventoryError(f"cannot load {filename} ({exc}): run this helper with `uv run`") from exc
|
|
913
|
+
except (OSError, SyntaxError) as exc:
|
|
914
|
+
raise InventoryError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
|
|
915
|
+
_SIBLINGS[filename] = module
|
|
916
|
+
return _SIBLINGS[filename]
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def read_brief(path: Path) -> dict:
|
|
920
|
+
"""The skill brief, a YAML mapping."""
|
|
921
|
+
module = _sibling("skf-resolve-authoritative-files.py")
|
|
922
|
+
try:
|
|
923
|
+
brief = module.parse_brief_yaml(path.read_text(encoding="utf-8"))
|
|
924
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
925
|
+
raise InventoryError(f"cannot read the brief {path.as_posix()}: {exc}") from exc
|
|
926
|
+
except module.yaml.YAMLError as exc:
|
|
927
|
+
raise InventoryError(f"the brief {path.as_posix()} is not valid YAML: {exc}") from exc
|
|
928
|
+
if not isinstance(brief, dict):
|
|
929
|
+
raise InventoryError(f"the brief {path.as_posix()} is not a YAML mapping")
|
|
930
|
+
return brief
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# metadata: the stdin keys, each with the JSON types it takes; METADATA_REQUIRED have no default.
|
|
934
|
+
METADATA_INPUT = {
|
|
935
|
+
"version": (str,), "language": (str,),
|
|
936
|
+
"source_root": (str, type(None)), "source_commit": (str, type(None)), "source_ref": (str, type(None)),
|
|
937
|
+
"qmd_version": (str, type(None)), "ast_grep_version": (str, type(None)), "stats_extra": (dict,),
|
|
938
|
+
}
|
|
939
|
+
METADATA_REQUIRED = ("version", "language", "source_root", "source_commit", "source_ref", "qmd_version")
|
|
940
|
+
NON_EMPTY = ("version", "language")
|
|
941
|
+
REFERENCE_APP = "reference-app"
|
|
942
|
+
COMPONENT_LIBRARY = "component-library"
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _metadata_input(given) -> dict:
|
|
946
|
+
"""stdin's fields, checked, with the defaults of the optional ones."""
|
|
947
|
+
if not isinstance(given, dict):
|
|
948
|
+
raise InventoryError("stdin must hold a JSON object with " + ", ".join(METADATA_REQUIRED))
|
|
949
|
+
unknown = sorted(set(given) - set(METADATA_INPUT))
|
|
950
|
+
if unknown:
|
|
951
|
+
raise InventoryError(f"metadata takes only {', '.join(METADATA_INPUT)}; not {', '.join(unknown)}")
|
|
952
|
+
missing = [key for key in METADATA_REQUIRED if key not in given]
|
|
953
|
+
if missing:
|
|
954
|
+
raise InventoryError(f"metadata needs {', '.join(missing)} (null when unknown, but for "
|
|
955
|
+
f"{', '.join(NON_EMPTY)})")
|
|
956
|
+
for key, value in given.items():
|
|
957
|
+
if not isinstance(value, METADATA_INPUT[key]) or (key in NON_EMPTY and not value.strip()):
|
|
958
|
+
names = " or ".join("null" if t is type(None) else t.__name__ for t in METADATA_INPUT[key])
|
|
959
|
+
raise InventoryError(f"{key} must be {'a non-empty ' if key in NON_EMPTY else ''}{names}")
|
|
960
|
+
return {"ast_grep_version": None, "stats_extra": {}, **given}
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def read_description(skill_md: Path) -> str:
|
|
964
|
+
"""The `description` of the staged SKILL.md's frontmatter, which compile §2a wrote."""
|
|
965
|
+
module = _sibling("skf-description-guard.py")
|
|
966
|
+
try:
|
|
967
|
+
description, _hash = module.read_description(skill_md)
|
|
968
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
969
|
+
raise InventoryError(f"cannot read the description of the staged SKILL.md: {exc} (write SKILL.md, "
|
|
970
|
+
"compile §2, before metadata.json)") from exc
|
|
971
|
+
if not description.strip():
|
|
972
|
+
raise InventoryError(f"the description of {skill_md.as_posix()} is empty")
|
|
973
|
+
return description
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def metadata_exports(data: dict, scope_type: object) -> list[str]:
|
|
977
|
+
"""metadata.json's exports[]: the names of the inventory's exports but one marked internal or not public, each
|
|
978
|
+
once (a docs-only inventory's t3_items names; none for a reference app)."""
|
|
979
|
+
if scope_type == REFERENCE_APP:
|
|
980
|
+
return []
|
|
981
|
+
if data.get("extraction_mode") == "docs-only":
|
|
982
|
+
items = [i for i in data.get("t3_items") or [] if isinstance(i, dict)]
|
|
983
|
+
else:
|
|
984
|
+
items = [e for e in data["exports"] if isinstance(e, dict) and e.get("internal") is not True
|
|
985
|
+
and e.get("public") is not False]
|
|
986
|
+
names = (i.get("export_name") for i in items)
|
|
987
|
+
return list(dict.fromkeys(n for n in names if isinstance(n, str) and n))
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _file_rows(entries: object, folder: str) -> list[dict]:
|
|
991
|
+
"""The scripts[] or assets[] rows of a scripts_inventory or assets_inventory."""
|
|
992
|
+
rows = []
|
|
993
|
+
for entry in entries if isinstance(entries, list) else []:
|
|
994
|
+
if not isinstance(entry, dict):
|
|
995
|
+
continue
|
|
996
|
+
source = entry.get("source_file")
|
|
997
|
+
name = entry.get("name") or (Path(str(source)).name if source else None)
|
|
998
|
+
rows.append({"file": f"{folder}/{name}", "purpose": entry.get("purpose") or "", "source_file": source,
|
|
999
|
+
"confidence": entry.get("confidence") or "T1-low"})
|
|
1000
|
+
return rows
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _ast_grep_run(data: dict, given_version: str | None) -> tuple[str | None, int | None, list[str]]:
|
|
1004
|
+
"""(tool_versions.ast_grep, ast_node_count, warnings): the runner's version and match count when it ran
|
|
1005
|
+
ast-grep, else the number of exports labeled ast-grep (the recipes run without the runner) and the version
|
|
1006
|
+
stdin gave, else (None, None) when ast-grep did not run."""
|
|
1007
|
+
rules = data.get("extraction_rules") if isinstance(data.get("extraction_rules"), dict) else {}
|
|
1008
|
+
version = rules.get("ast_grep_version")
|
|
1009
|
+
if isinstance(version, str) and version:
|
|
1010
|
+
aggregates = data.get("aggregates") if isinstance(data.get("aggregates"), dict) else {}
|
|
1011
|
+
matched = aggregates.get("exports")
|
|
1012
|
+
return version, matched if isinstance(matched, int) and not isinstance(matched, bool) else None, []
|
|
1013
|
+
matched = sum(1 for e in data["exports"] if isinstance(e, dict) and e.get("extraction_method") == "ast-grep")
|
|
1014
|
+
if not matched:
|
|
1015
|
+
return None, None, []
|
|
1016
|
+
if given_version and given_version.strip():
|
|
1017
|
+
return given_version.strip(), matched, []
|
|
1018
|
+
return "unknown", matched, [f"tool_versions.ast_grep is unknown: ast-grep matched {matched} exports without "
|
|
1019
|
+
"the recipe runner, and stdin gave no ast_grep_version"]
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def build_metadata(data: dict, brief: dict, stats: object, given, skf_version: str,
|
|
1023
|
+
description: str) -> tuple[dict, list[str]]:
|
|
1024
|
+
"""(the skill's metadata.json, its warnings) (see `metadata` in the module docstring)."""
|
|
1025
|
+
fields = _metadata_input(given)
|
|
1026
|
+
if not isinstance(stats, dict) or not isinstance(stats.get("stats"), dict) \
|
|
1027
|
+
or not isinstance(stats.get("confidence_distribution"), dict):
|
|
1028
|
+
raise InventoryError("the --stats file must hold the stats helper's JSON: a stats and a "
|
|
1029
|
+
"confidence_distribution object")
|
|
1030
|
+
clash = sorted(set(fields["stats_extra"]) & set(stats["stats"]))
|
|
1031
|
+
if clash:
|
|
1032
|
+
raise InventoryError(f"stats_extra may not set {', '.join(clash)}: the stats helper writes it")
|
|
1033
|
+
scope = brief.get("scope") if isinstance(brief.get("scope"), dict) else {}
|
|
1034
|
+
scope_type = scope.get("type")
|
|
1035
|
+
if not isinstance(scope_type, str) or not scope_type:
|
|
1036
|
+
raise InventoryError("the brief has no scope.type, which metadata.json records as scope_type")
|
|
1037
|
+
extra = sorted(fields["stats_extra"])
|
|
1038
|
+
if scope_type == REFERENCE_APP and set(extra) - {"notes"}:
|
|
1039
|
+
raise InventoryError(f"a reference app's stats_extra holds only notes, not {', '.join(extra)}: the stats "
|
|
1040
|
+
"helper writes pattern_surfaces_documented")
|
|
1041
|
+
if extra and scope_type not in (REFERENCE_APP, COMPONENT_LIBRARY):
|
|
1042
|
+
raise InventoryError(f"a {scope_type} skill takes no stats_extra (only a component library or a "
|
|
1043
|
+
"reference app adds stats)")
|
|
1044
|
+
ast_grep, node_count, warnings = _ast_grep_run(data, fields["ast_grep_version"])
|
|
1045
|
+
metadata = {
|
|
1046
|
+
"name": brief.get("name") or data.get("skill_name"),
|
|
1047
|
+
"version": fields["version"],
|
|
1048
|
+
"skill_type": "single",
|
|
1049
|
+
"scope_type": scope_type,
|
|
1050
|
+
"source_authority": brief.get("source_authority") or "community",
|
|
1051
|
+
"source_repo": brief.get("source_repo"),
|
|
1052
|
+
"source_root": fields["source_root"],
|
|
1053
|
+
"source_commit": fields["source_commit"],
|
|
1054
|
+
"source_ref": fields["source_ref"],
|
|
1055
|
+
"confidence_tier": data.get("tier"),
|
|
1056
|
+
"spec_version": "1.3",
|
|
1057
|
+
"generation_date": _utc_now(),
|
|
1058
|
+
"description": description,
|
|
1059
|
+
"language": fields["language"],
|
|
1060
|
+
}
|
|
1061
|
+
if node_count is not None:
|
|
1062
|
+
metadata["ast_node_count"] = node_count
|
|
1063
|
+
metadata["exports"] = metadata_exports(data, scope_type)
|
|
1064
|
+
metadata["tool_versions"] = {"ast_grep": ast_grep, "qmd": fields["qmd_version"], "skf": skf_version}
|
|
1065
|
+
metadata["confidence_distribution"] = stats["confidence_distribution"]
|
|
1066
|
+
metadata["stats"] = {**stats["stats"], **fields["stats_extra"]}
|
|
1067
|
+
for key, folder in (("scripts_inventory", "scripts"), ("assets_inventory", "assets")):
|
|
1068
|
+
rows = _file_rows(data.get(key), folder)
|
|
1069
|
+
if rows:
|
|
1070
|
+
metadata[folder] = rows
|
|
1071
|
+
metadata["generated_by"] = "create-skill"
|
|
1072
|
+
metadata["dependencies"] = []
|
|
1073
|
+
metadata["compatibility"] = ""
|
|
1074
|
+
return metadata, warnings
|
|
561
1075
|
|
|
562
1076
|
|
|
563
1077
|
def rules_yaml(data: dict, language: str) -> str:
|
|
@@ -669,6 +1183,16 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
669
1183
|
p_prov.add_argument("--target", required=True, type=Path, help="the provenance-map.json to write")
|
|
670
1184
|
p_prov.add_argument("--add-entries", action="store_true",
|
|
671
1185
|
help="append stdin's entries and file_entries to the map at --target instead")
|
|
1186
|
+
p_meta = sub.add_parser("metadata", help="write metadata.json from the inventory, the brief and the stats, "
|
|
1187
|
+
"the fields compile decides on stdin")
|
|
1188
|
+
p_meta.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
1189
|
+
p_meta.add_argument("--brief", required=True, type=Path, help="the skill-brief.yaml")
|
|
1190
|
+
p_meta.add_argument("--stats", required=True, type=Path,
|
|
1191
|
+
help="the JSON skf-render-metadata-stats.py wrote (its -o file)")
|
|
1192
|
+
p_meta.add_argument("--skf-root", required=True, type=Path,
|
|
1193
|
+
help="the installed SKF module folder ({project-root}/_bmad/skf), for tool_versions.skf")
|
|
1194
|
+
p_meta.add_argument("--target", required=True, type=Path,
|
|
1195
|
+
help="the metadata.json to write, beside the staged SKILL.md it reads the description of")
|
|
672
1196
|
p_summary = sub.add_parser("summary", help="print the inventory's counts")
|
|
673
1197
|
p_summary.add_argument("--inventory", required=True, type=Path, help=inventory_help)
|
|
674
1198
|
return parser
|
|
@@ -685,21 +1209,35 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
685
1209
|
write(path, data)
|
|
686
1210
|
result = _result(path, data)
|
|
687
1211
|
elif args.command == "provenance" and args.add_entries:
|
|
688
|
-
load(path)
|
|
1212
|
+
data = load(path)
|
|
689
1213
|
provenance = _read_json(args.target, "provenance map")
|
|
690
1214
|
if not isinstance(provenance, dict) or not isinstance(provenance.get("entries"), list):
|
|
691
1215
|
raise InventoryError(f"the provenance map {args.target.as_posix()} has no entries list: "
|
|
692
1216
|
"run provenance without --add-entries first")
|
|
693
|
-
|
|
1217
|
+
off = frozenset((item["name"], _norm_path(item["file"])) for item in _not_public(data))
|
|
1218
|
+
added, duplicates, not_public = add_map_entries(provenance, _stdin_json(), off)
|
|
694
1219
|
write(args.target, provenance)
|
|
695
1220
|
result = {"status": "ok", "target": args.target.as_posix(), "entries": len(provenance["entries"]),
|
|
696
1221
|
"file_entries": len(provenance.get("file_entries") or []), "added": added,
|
|
697
|
-
"duplicates": duplicates}
|
|
1222
|
+
"duplicates": duplicates, "not_public": not_public}
|
|
698
1223
|
elif args.command == "provenance":
|
|
699
1224
|
provenance, unsigned = provenance_map(load(path), _stdin_json())
|
|
700
1225
|
write(args.target, provenance)
|
|
701
1226
|
result = {"status": "ok", "target": args.target.as_posix(), "entries": len(provenance["entries"]),
|
|
702
1227
|
"unsigned": unsigned}
|
|
1228
|
+
elif args.command == "metadata":
|
|
1229
|
+
skf_version = _sibling("skf-render-quick-metadata.py").probe_skf_version(args.skf_root)
|
|
1230
|
+
metadata, warnings = build_metadata(load(path), read_brief(args.brief),
|
|
1231
|
+
_read_json(args.stats, "stats JSON"), _stdin_json(), skf_version,
|
|
1232
|
+
read_description(args.target.parent / "SKILL.md"))
|
|
1233
|
+
write(args.target, metadata)
|
|
1234
|
+
commit = metadata["source_commit"]
|
|
1235
|
+
if skf_version == "unknown":
|
|
1236
|
+
warnings.append(f"tool_versions.skf is unknown: {args.skf_root.as_posix()} holds no package.json "
|
|
1237
|
+
"or VERSION file with a version")
|
|
1238
|
+
result = {"status": "ok", "target": args.target.as_posix(), "exports": len(metadata["exports"]),
|
|
1239
|
+
"commit_short": commit[:8] if commit else "unknown",
|
|
1240
|
+
"tool_versions": metadata["tool_versions"], "warnings": warnings}
|
|
703
1241
|
elif args.command == "rules":
|
|
704
1242
|
text = rules_yaml(load(path), args.language)
|
|
705
1243
|
write(args.target, text)
|
|
@@ -707,7 +1245,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
707
1245
|
else:
|
|
708
1246
|
data = load(path)
|
|
709
1247
|
if args.command == "summary":
|
|
710
|
-
result = _result(path, data)
|
|
1248
|
+
result = _result(path, data, not_public=_not_public(data))
|
|
711
1249
|
elif args.command == "add":
|
|
712
1250
|
payload = _stdin_json()
|
|
713
1251
|
added, duplicates, dropped = add(data, args.field, [] if payload is None else payload)
|
|
@@ -720,9 +1258,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
720
1258
|
result = _result(path, data, patched=patched, unmatched=unmatched, ambiguous=ambiguous)
|
|
721
1259
|
else:
|
|
722
1260
|
payload = _stdin_json()
|
|
723
|
-
keys = set_fields(data, {} if payload is None else payload)
|
|
1261
|
+
keys, dropped = set_fields(data, {} if payload is None else payload)
|
|
724
1262
|
write(path, data)
|
|
725
|
-
result = _result(path, data, set=keys)
|
|
1263
|
+
result = _result(path, data, set=keys, dropped=dropped)
|
|
726
1264
|
except InventoryError as exc:
|
|
727
1265
|
print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
|
|
728
1266
|
return 1
|