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.
Files changed (141) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +3 -3
  3. package/docs/_data/pinned.yaml +19 -13
  4. package/docs/architecture.md +2 -2
  5. package/docs/concepts.md +4 -4
  6. package/docs/examples.md +33 -26
  7. package/docs/getting-started.md +3 -3
  8. package/docs/how-it-works.md +2 -2
  9. package/docs/index.md +1 -1
  10. package/docs/skill-model.md +41 -39
  11. package/docs/troubleshooting.md +8 -0
  12. package/docs/verifying-a-skill.md +46 -21
  13. package/docs/why-skf.md +1 -1
  14. package/docs/workflows.md +5 -5
  15. package/package.json +1 -1
  16. package/src/knowledge/qmd-registry.md +1 -1
  17. package/src/knowledge/version-paths.md +1 -1
  18. package/src/shared/health-check.md +2 -50
  19. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
  20. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +12 -4
  21. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  22. package/src/shared/scripts/skf-atomic-write.py +212 -59
  23. package/src/shared/scripts/skf-build-change-manifest.py +1437 -178
  24. package/src/shared/scripts/skf-classify-changed-files.py +78 -0
  25. package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
  26. package/src/shared/scripts/skf-count-tokens.py +54 -10
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
  28. package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
  29. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  30. package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
  31. package/src/shared/scripts/skf-extract-public-api.py +1673 -189
  32. package/src/shared/scripts/skf-extraction-inventory.py +584 -46
  33. package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
  34. package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
  35. package/src/shared/scripts/skf-github-probe.py +47 -8
  36. package/src/shared/scripts/skf-hash-content.py +14 -8
  37. package/src/shared/scripts/skf-load-provenance.py +3 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
  39. package/src/shared/scripts/skf-parse-gaps.py +135 -26
  40. package/src/shared/scripts/skf-preflight.py +15 -1
  41. package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
  42. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  43. package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
  44. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  45. package/src/shared/scripts/skf-shard-body.py +239 -41
  46. package/src/shared/scripts/skf-structural-diff.py +133 -18
  47. package/src/shared/scripts/skf-update-active-symlink.py +115 -62
  48. package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
  49. package/src/shared/scripts/skf-validate-output.py +12 -3
  50. package/src/shared/scripts/skf-verify-provenance-completeness.py +1439 -79
  51. package/src/shared/scripts/skf-write-skill-brief.py +41 -8
  52. package/src/shared/tool-requirements.yaml +8 -2
  53. package/src/skf-analyze-source/SKILL.md +2 -2
  54. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  55. package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
  56. package/src/skf-audit-skill/SKILL.md +2 -2
  57. package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
  58. package/src/skf-audit-skill/references/headless-contract.md +1 -1
  59. package/src/skf-audit-skill/references/health-check.md +2 -0
  60. package/src/skf-audit-skill/references/init.md +2 -1
  61. package/src/skf-audit-skill/references/re-index.md +14 -9
  62. package/src/skf-audit-skill/references/report.md +2 -2
  63. package/src/skf-audit-skill/references/semantic-diff.md +15 -15
  64. package/src/skf-audit-skill/references/structural-diff.md +10 -5
  65. package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
  66. package/src/skf-brief-skill/SKILL.md +3 -3
  67. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  68. package/src/skf-brief-skill/assets/skill-brief-schema.md +4 -11
  69. package/src/skf-brief-skill/references/analyze-target.md +6 -6
  70. package/src/skf-brief-skill/references/confirm-brief.md +1 -1
  71. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  72. package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
  73. package/src/skf-brief-skill/references/version-resolution.md +5 -9
  74. package/src/skf-campaign/SKILL.md +2 -2
  75. package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
  76. package/src/skf-create-skill/SKILL.md +2 -2
  77. package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
  78. package/src/skf-create-skill/assets/skill-sections.md +7 -7
  79. package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
  80. package/src/skf-create-skill/references/compile.md +31 -60
  81. package/src/skf-create-skill/references/component-extraction.md +3 -3
  82. package/src/skf-create-skill/references/enrich.md +2 -2
  83. package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
  84. package/src/skf-create-skill/references/extract.md +23 -19
  85. package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
  86. package/src/skf-create-skill/references/extraction-patterns.md +10 -11
  87. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  88. package/src/skf-create-skill/references/report.md +21 -5
  89. package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
  90. package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
  91. package/src/skf-create-stack-skill/SKILL.md +2 -2
  92. package/src/skf-create-stack-skill/references/validate.md +1 -1
  93. package/src/skf-drop-skill/SKILL.md +2 -2
  94. package/src/skf-drop-skill/references/execute.md +5 -1
  95. package/src/skf-drop-skill/references/health-check.md +2 -0
  96. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  97. package/src/skf-drop-skill/references/report.md +2 -2
  98. package/src/skf-drop-skill/references/select.md +7 -3
  99. package/src/skf-export-skill/SKILL.md +2 -2
  100. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  101. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  102. package/src/skf-export-skill/references/load-skill.md +1 -1
  103. package/src/skf-export-skill/references/token-report.md +29 -9
  104. package/src/skf-forger/SKILL.md +2 -2
  105. package/src/skf-quick-skill/SKILL.md +2 -2
  106. package/src/skf-refine-architecture/SKILL.md +6 -8
  107. package/src/skf-rename-skill/SKILL.md +2 -2
  108. package/src/skf-setup/SKILL.md +2 -2
  109. package/src/skf-test-skill/SKILL.md +2 -2
  110. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  111. package/src/skf-test-skill/references/coherence-check.md +5 -4
  112. package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
  113. package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
  114. package/src/skf-test-skill/references/coverage-check.md +69 -111
  115. package/src/skf-test-skill/references/init.md +5 -6
  116. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  117. package/src/skf-test-skill/references/report.md +11 -9
  118. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  119. package/src/skf-test-skill/references/source-access-protocol.md +15 -18
  120. package/src/skf-test-skill/scripts/build-result-context.py +114 -21
  121. package/src/skf-test-skill/scripts/gap-ledger.py +580 -80
  122. package/src/skf-test-skill/scripts/load-coverage-inputs.py +646 -107
  123. package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
  124. package/src/skf-test-skill/scripts/reconcile-coverage.py +450 -23
  125. package/src/skf-test-skill/scripts/score-signatures.py +356 -61
  126. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  127. package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
  128. package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
  129. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  130. package/src/skf-update-skill/SKILL.md +2 -2
  131. package/src/skf-update-skill/references/detect-changes.md +32 -13
  132. package/src/skf-update-skill/references/gap-driven.md +37 -22
  133. package/src/skf-update-skill/references/init.md +6 -2
  134. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  135. package/src/skf-update-skill/references/merge.md +17 -12
  136. package/src/skf-update-skill/references/re-extract.md +4 -2
  137. package/src/skf-update-skill/references/report.md +7 -4
  138. package/src/skf-update-skill/references/write.md +77 -36
  139. package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
  140. package/src/skf-verify-stack/SKILL.md +2 -2
  141. 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", "chainedMember", "text"}],
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
- AFTER_CALL_RE = re.compile(f"\\s*\\??\\.\\s*((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)")
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"\\s*\\.\\s*await(?![{IDENT_CHARS}])")
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
- while i > 0 and text[i - 1] in " \t\n":
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 AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)):
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
- """True when a call around the expression at `start` is awaited or runs it, within one function body."""
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
- if _is_runner(re.sub(r"\s+", "", text[expr:k])):
589
- return True
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
- if _awaited_at(text, expr) or (close is not None and AWAIT_AFTER_RE.match(text, close + 1)):
594
- return True
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 False
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
- awaited, chained = _awaited_at(text, chain), None
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": awaited or _enclosing_awaits(text, chain), "chainedMember": chained,
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 {