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
|
@@ -31,7 +31,7 @@ answer per input and judges no meaning. For each export name it lists:
|
|
|
31
31
|
as data or output (`json`, `yaml`, `text`, `console` ...) holds none.
|
|
32
32
|
|
|
33
33
|
A call is `awaited` when `await` stands before it (through its receiver:
|
|
34
|
-
`await client.fetchData(...)`), `.await` follows it (Rust), an asynchronous
|
|
34
|
+
`await client.fetchData(...)`), `.await` or `?.await` follows it (Rust), an asynchronous
|
|
35
35
|
loop or context manager consumes it (`async for row in stream(...)`, `async
|
|
36
36
|
with session(...)`, `for await (const row of stream(...))`), or it sits in
|
|
37
37
|
the arguments of a call that is itself awaited or is an event-loop runner
|
|
@@ -40,6 +40,26 @@ crossing into a function body. `chainedMember` names the member read or
|
|
|
40
40
|
called on its result right after it (`then`, `catch`, `join`, `get`), else
|
|
41
41
|
null: whether that waits for the result is the caller's call.
|
|
42
42
|
|
|
43
|
+
`awaitedVia` says how an awaited call is awaited, null when it is not:
|
|
44
|
+
|
|
45
|
+
- "direct": the await applies to the call itself (`directlyAwaited`):
|
|
46
|
+
`.await` follows it, or `await` stands before it with nothing applied to
|
|
47
|
+
its result (`await client.fetchData(...)`) but a member that waits for an
|
|
48
|
+
awaitable (`then`, `catch`, `finally`, `ConfigureAwait`), or it alone
|
|
49
|
+
fills the groups that `await` stands before (`await ((fetchData(x)))`,
|
|
50
|
+
`await (fetchData(x) as T)`) and nothing follows them but the end of the
|
|
51
|
+
statement, of a list or of a bracket.
|
|
52
|
+
- "chain": the await applies to an expression the call starts or sits in:
|
|
53
|
+
a member, subscript or call applied to its result
|
|
54
|
+
(`await use_context(e).embed(t)`, `await fetchData(x)[0]`) or a group
|
|
55
|
+
around it (`await (fetchData(x) or other())`).
|
|
56
|
+
- "consumer": an asynchronous loop or context manager consumes it.
|
|
57
|
+
- "argument" or "runner": it sits in the arguments of an awaited call, or of
|
|
58
|
+
an event-loop runner; `enclosingCallee` names that callee, whitespace
|
|
59
|
+
removed (`asyncio.gather`, `Promise.all`, `asyncio.run`), else null.
|
|
60
|
+
|
|
61
|
+
A backslash continuation counts as whitespace throughout.
|
|
62
|
+
|
|
43
63
|
Name match: the name as a fixed string, case-sensitive, never inside a
|
|
44
64
|
longer identifier (the Export match rule of skf-scan-skill-md-structure.py
|
|
45
65
|
usage-scope). A qualified name (`client.fetch`, `Store::load`) matches only
|
|
@@ -74,9 +94,11 @@ Output (stdout, one object):
|
|
|
74
94
|
"name": "fetchData",
|
|
75
95
|
"descriptionCount": 3, # all found; descriptions[] holds the first 12
|
|
76
96
|
"descriptions": [{"file", "line", "endLine", "kind", "section", "header", "text", "truncated"}],
|
|
77
|
-
"calls": [{"file", "line", "fence", "language", "awaited", "
|
|
97
|
+
"calls": [{"file", "line", "fence", "language", "awaited", "directlyAwaited", "awaitedVia",
|
|
98
|
+
"enclosingCallee", "chainedMember", "text"}],
|
|
78
99
|
"callCount": 2,
|
|
79
|
-
"awaitedCount": 1
|
|
100
|
+
"awaitedCount": 1, # calls whose `awaited` is true
|
|
101
|
+
"directlyAwaitedCount": 1 # calls whose `directlyAwaited` is true
|
|
80
102
|
}
|
|
81
103
|
],
|
|
82
104
|
"notCalled": ["parseConfig"], # names no example calls
|
|
@@ -126,9 +148,20 @@ LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d{1,9}[.)])(?:[ \t]+|$)")
|
|
|
126
148
|
TABLE_ROW_RE = re.compile(r"^\s*\|")
|
|
127
149
|
TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$")
|
|
128
150
|
GENERIC_OR_PATH = r"(?:[ \t]*::)?(?:[ \t]*<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?"
|
|
129
|
-
|
|
151
|
+
# Whitespace, a Python backslash continuation included; LINE_SPACE stays on its line.
|
|
152
|
+
SPACE = r"(?:\s|\\\n)*"
|
|
153
|
+
LINE_SPACE = r"(?:[ \t]|\\\n)*"
|
|
154
|
+
AFTER_CALL_RE = re.compile(f"{SPACE}[?!]?\\.{SPACE}((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)")
|
|
130
155
|
AWAIT_BEFORE_RE = re.compile(f"(?<![{IDENT_CHARS}])await$")
|
|
131
|
-
AWAIT_AFTER_RE = re.compile(f"
|
|
156
|
+
AWAIT_AFTER_RE = re.compile(f"{SPACE}\\??\\.{SPACE}await(?![{IDENT_CHARS}])")
|
|
157
|
+
# A subscript or call applied to a call's result: `f(x)[0]`, `f(x)(y)`, `f(x)?.[0]`, `f(x)!(y)`.
|
|
158
|
+
APPLIED_RE = re.compile(f"{LINE_SPACE}(?:\\?\\.|!)?[\\[(]")
|
|
159
|
+
# What may close a group that holds only the call (`(f(x) as T)`, `(f(x)!)`), and what may follow
|
|
160
|
+
# the outermost group: the end of the statement, of a list or of a bracket.
|
|
161
|
+
GROUP_TAIL_RE = re.compile(f"{SPACE}(?:!|as(?![{IDENT_CHARS}])[^()]*)?{SPACE}")
|
|
162
|
+
GROUP_END_RE = re.compile(f"{LINE_SPACE}(?:[;,)\\]}}]|\\Z|\\n\\s*(?:[^\\s.?\\[(]|\\Z))")
|
|
163
|
+
# Members whose call waits for an awaitable: under `await`, the export returns one.
|
|
164
|
+
WAIT_MEMBERS = frozenset({"then", "catch", "finally", "ConfigureAwait"})
|
|
132
165
|
# An expression an asynchronous loop or context manager consumes: `async for
|
|
133
166
|
# x in NAME(...)`, `async with NAME(...)`, `for await (const x of NAME(...))`,
|
|
134
167
|
# `await foreach (var x in NAME(...))`. Matched against its line up to it.
|
|
@@ -515,7 +548,8 @@ def _matching_open(text, close_idx):
|
|
|
515
548
|
|
|
516
549
|
|
|
517
550
|
def _skip_space_back(text, i):
|
|
518
|
-
|
|
551
|
+
"""Back over whitespace, a Python backslash continuation included."""
|
|
552
|
+
while i > 0 and (text[i - 1] in " \t\n" or (text[i - 1] == "\\" and text.startswith("\n", i))):
|
|
519
553
|
i -= 1
|
|
520
554
|
return i
|
|
521
555
|
|
|
@@ -543,14 +577,37 @@ def _chain_start(text, pos):
|
|
|
543
577
|
i = start
|
|
544
578
|
|
|
545
579
|
|
|
580
|
+
def _await_before(text, start):
|
|
581
|
+
"""True when `await` stands right before the expression that starts at `start`."""
|
|
582
|
+
return bool(AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)))
|
|
583
|
+
|
|
584
|
+
|
|
546
585
|
def _awaited_at(text, start):
|
|
547
586
|
"""True when `await` stands right before the expression that starts at `start`, or an
|
|
548
587
|
asynchronous loop or context manager consumes it."""
|
|
549
|
-
if
|
|
588
|
+
if _await_before(text, start):
|
|
550
589
|
return True
|
|
551
590
|
return bool(ASYNC_CONSUMER_RE.search(text[text.rfind("\n", 0, start) + 1:start]))
|
|
552
591
|
|
|
553
592
|
|
|
593
|
+
def _grouped_await(text, chain, close):
|
|
594
|
+
"""True for `await (call(...))` and `await ((call(...) as T))`: the call alone fills each group
|
|
595
|
+
around it (a trailing `as T` or `!` allowed), `await` stands before the outermost one, and
|
|
596
|
+
nothing but the end of the statement, of a list or of a bracket follows it."""
|
|
597
|
+
start, end = chain, close
|
|
598
|
+
while end is not None:
|
|
599
|
+
group = _skip_space_back(text, start) - 1
|
|
600
|
+
if group < 0 or text[group] != "(":
|
|
601
|
+
return False
|
|
602
|
+
end_group = _matching_close(text, group)
|
|
603
|
+
if end_group is None or not GROUP_TAIL_RE.fullmatch(text, end + 1, end_group):
|
|
604
|
+
return False
|
|
605
|
+
if _await_before(text, group):
|
|
606
|
+
return bool(GROUP_END_RE.match(text, end_group + 1))
|
|
607
|
+
start, end = group, end_group
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
|
|
554
611
|
def _is_runner(callee):
|
|
555
612
|
"""True for `asyncio.run`, `loop.run_until_complete`, `rt.block_on` and the like."""
|
|
556
613
|
parts = [p for p in re.split(r"\.|::", callee) if p]
|
|
@@ -568,7 +625,12 @@ def _opens_body(text, brace_idx):
|
|
|
568
625
|
|
|
569
626
|
|
|
570
627
|
def _enclosing_awaits(text, start):
|
|
571
|
-
"""
|
|
628
|
+
"""How a call or group around the expression at `start` awaits it, within one function body.
|
|
629
|
+
|
|
630
|
+
("runner", callee) for a runner it is an argument of, ("argument", callee) for an awaited or
|
|
631
|
+
consumed call it is an argument of, ("chain", None) for an awaited group around it and
|
|
632
|
+
("consumer", None) for a consumed one; None when nothing around it awaits it.
|
|
633
|
+
"""
|
|
572
634
|
i, depth = start, 0
|
|
573
635
|
while i > 0:
|
|
574
636
|
i -= 1
|
|
@@ -583,17 +645,39 @@ def _enclosing_awaits(text, start):
|
|
|
583
645
|
elif c == "(":
|
|
584
646
|
k = _skip_space_back(text, i)
|
|
585
647
|
word = WORD_BEFORE_RE.search(text, 0, k)
|
|
648
|
+
callee = None
|
|
586
649
|
if word and word.group(1) not in EXPRESSION_WORDS:
|
|
587
650
|
expr = _chain_start(text, word.start())
|
|
588
|
-
|
|
589
|
-
|
|
651
|
+
callee = re.sub(r"(?:\s|\\\n)+", "", text[expr:k])
|
|
652
|
+
if _is_runner(callee):
|
|
653
|
+
return "runner", callee
|
|
590
654
|
else:
|
|
591
655
|
expr = i # a grouping `(`: `await (fetch())`
|
|
592
656
|
close = _matching_close(text, i)
|
|
593
|
-
|
|
594
|
-
|
|
657
|
+
awaits = _await_before(text, expr) or (close is not None and AWAIT_AFTER_RE.match(text, close + 1))
|
|
658
|
+
if awaits or _awaited_at(text, expr):
|
|
659
|
+
return ("argument", callee) if callee else ("chain" if awaits else "consumer", None)
|
|
595
660
|
i = expr
|
|
596
|
-
return
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _how_awaited(text, chain, close):
|
|
665
|
+
"""(awaitedVia, enclosingCallee, chainedMember) of the call whose expression starts at `chain`
|
|
666
|
+
and whose argument list closes at `close`."""
|
|
667
|
+
before = _await_before(text, chain)
|
|
668
|
+
member = AFTER_CALL_RE.match(text, close + 1) if close is not None else None
|
|
669
|
+
chained = member.group(1) if member else None
|
|
670
|
+
if close is not None and AWAIT_AFTER_RE.match(text, close + 1):
|
|
671
|
+
return "direct", None, None
|
|
672
|
+
if member or (close is not None and APPLIED_RE.match(text, close + 1)):
|
|
673
|
+
if before:
|
|
674
|
+
return ("direct" if chained in WAIT_MEMBERS else "chain"), None, chained
|
|
675
|
+
elif before or _grouped_await(text, chain, close):
|
|
676
|
+
return "direct", None, None
|
|
677
|
+
if _awaited_at(text, chain):
|
|
678
|
+
return "consumer", None, chained
|
|
679
|
+
via, callee = _enclosing_awaits(text, chain) or (None, None)
|
|
680
|
+
return via, callee, chained
|
|
597
681
|
|
|
598
682
|
|
|
599
683
|
def _closes_generic(tail):
|
|
@@ -670,15 +754,11 @@ def scan_block(rel, lines, opening, closing, info, patterns):
|
|
|
670
754
|
continue
|
|
671
755
|
if regex is not call_re:
|
|
672
756
|
continue # the last part of a qualified name counts only as a declaration
|
|
673
|
-
|
|
674
|
-
if close is not None:
|
|
675
|
-
if AWAIT_AFTER_RE.match(text, close + 1):
|
|
676
|
-
awaited = True
|
|
677
|
-
elif member := AFTER_CALL_RE.match(text, close + 1):
|
|
678
|
-
chained = member.group(1)
|
|
757
|
+
via, callee, chained = _how_awaited(text, chain, close)
|
|
679
758
|
found[name]["calls"].append({
|
|
680
759
|
"file": rel, "line": opening + row + 2, "fence": opening + 1, "language": language or None,
|
|
681
|
-
"awaited":
|
|
760
|
+
"awaited": via is not None, "directlyAwaited": via == "direct", "awaitedVia": via,
|
|
761
|
+
"enclosingCallee": callee, "chainedMember": chained,
|
|
682
762
|
"text": _cap(body[row].strip(), LINE_TEXT_CAP)[0],
|
|
683
763
|
})
|
|
684
764
|
return found
|
|
@@ -734,6 +814,7 @@ def locate(inp):
|
|
|
734
814
|
"calls": calls[name],
|
|
735
815
|
"callCount": len(calls[name]),
|
|
736
816
|
"awaitedCount": sum(1 for c in calls[name] if c["awaited"]),
|
|
817
|
+
"directlyAwaitedCount": sum(1 for c in calls[name] if c["directlyAwaited"]),
|
|
737
818
|
} for name in names if calls[name]]
|
|
738
819
|
not_called = [name for name in names if not calls[name]]
|
|
739
820
|
return {
|