bmad-module-skill-forge 4.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 +32 -25
- 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/verifying-a-skill.md +41 -20
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +881 -168
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- 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 +439 -107
- package/src/shared/scripts/skf-extraction-inventory.py +10 -6
- package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
- package/src/shared/scripts/skf-parse-gaps.py +135 -26
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- 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-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +470 -38
- 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-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-brief-skill/SKILL.md +2 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- 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/skill-sections.md +1 -1
- package/src/skf-create-skill/references/extract.md +2 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/report.md +6 -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-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 +4 -3
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +45 -69
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +2 -2
- package/src/skf-test-skill/references/scoring-rules.md +5 -4
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/scripts/gap-ledger.py +378 -163
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +171 -70
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +329 -54
- 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/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 +1 -1
- package/src/skf-update-skill/references/gap-driven.md +36 -21
- package/src/skf-update-skill/references/merge.md +15 -10
- package/src/skf-update-skill/references/re-extract.md +3 -1
- package/src/skf-update-skill/references/report.md +5 -4
- package/src/skf-update-skill/references/write.md +39 -18
- package/src/skf-verify-stack/SKILL.md +2 -2
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -350,7 +350,11 @@ scope.type and language; --include, --exclude, --tier-a-include,
|
|
|
350
350
|
Which recipes. --recipe-set picks the recipes whose metadata.sets lists it:
|
|
351
351
|
`component-library` when the scope type is component-library, else
|
|
352
352
|
`standard`. Each recipe runs in the languages its metadata.languages
|
|
353
|
-
lists. A match whose name starts with `_` is never an
|
|
353
|
+
lists. A Python or Rust match whose name starts with `_` is never an
|
|
354
|
+
export, nor public anywhere below: Python's by the Source API Surface
|
|
355
|
+
Definition's rule, Rust's because a `_` item is hidden or unnamed (a
|
|
356
|
+
`#[doc(hidden)] pub mod __private`, which the runner cannot read, or a
|
|
357
|
+
`pub const _`); a TS/JS `_` name is an export like any other. --head-cap
|
|
354
358
|
keeps at most N matches of each recipe, in file and line order (0: no
|
|
355
359
|
cap); without it the cap is 200, or at Forge+ and Deep tiers 500 for a
|
|
356
360
|
full-library scope and 300 for a component-library one. `truncated` is
|
|
@@ -423,40 +427,75 @@ module names.
|
|
|
423
427
|
A chain the trace cannot follow (a module no file here holds, a `#[path]`
|
|
424
428
|
module) is listed in `unresolved`. The diff against the recipe inventory,
|
|
425
429
|
one language family at a time (JavaScript/TypeScript, Python, Rust, Go):
|
|
426
|
-
`public` (the entry points' names
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
`public` (the entry points' names, each at the file and line the first
|
|
431
|
+
entry point naming it gives, in path order, a JS/TS entry point's own and
|
|
432
|
+
named exports all coming before any `export *`, unless that file is out
|
|
433
|
+
of scope, or it gives none, and another entry point gives a file in
|
|
434
|
+
scope, other than through a star or glob its own export of the name
|
|
435
|
+
shadows),
|
|
436
|
+
`other_definitions` (each other definition of a public name in a file in
|
|
437
|
+
scope, one row per file and `local` name: the file and line an entry
|
|
438
|
+
point gives, never a namespace nor one beside a public row that is a
|
|
439
|
+
namespace, nor a star or glob through which an entry point passes on a
|
|
440
|
+
name it already exports (its own export shadows it), while two exports
|
|
441
|
+
of the name by one entry point, such as Rust `pub use` items a `cfg`
|
|
442
|
+
gates, are both kept; in every language family, so a name each
|
|
443
|
+
framework package of a monorepo defines keeps
|
|
444
|
+
each package's file, while a gap and an outside_scope row still go by
|
|
445
|
+
name, from `public` alone),
|
|
446
|
+
`internal` (names the recipes found that no entry point exports or makes
|
|
447
|
+
reachable), `extraction_gaps` (names an entry point exports that no
|
|
448
|
+
recipe found, defined in a file in scope or in none the trace reached:
|
|
449
|
+
read them by eye) and `outside_scope` (names an entry point exports that
|
|
450
|
+
a file out of scope defines, even when a recipe found the name in another
|
|
451
|
+
file, which is another definition: widen scope.include to document
|
|
431
452
|
them). A namespace with `members` is diffed as its members: each is found
|
|
432
453
|
when a recipe found its name, or its `local` one, in its file (one with
|
|
433
454
|
no file by its name alone, as a public name is), and has no row of its
|
|
434
455
|
own when a public name (or another namespace's member) has its name and
|
|
435
456
|
file; one with null `members`, and a member that is such a module, is no
|
|
436
457
|
gap. In a family with no entry point, or only empty ones, every name the
|
|
437
|
-
recipes found is public.
|
|
458
|
+
recipes found is public. A JS/TS module exports, besides the names the
|
|
459
|
+
recipes match, those its column-0 `export` statements declare in a form
|
|
460
|
+
no recipe matches, read from its text: `export let`, `export declare
|
|
461
|
+
const`, `export namespace`, and each name a destructuring pattern that
|
|
462
|
+
opens an `export const`, `let` or `var` statement binds (`export const
|
|
463
|
+
{ a, b: c } = obj` exports a and c), at the line the name is on. Such a
|
|
464
|
+
name that an entry point exports, or passes on through `export *`, is
|
|
465
|
+
public and, no recipe having found it, an `extraction_gaps` row
|
|
466
|
+
(`outside_scope`, in a file out of scope) at that line. A pattern after
|
|
467
|
+
the statement's first declarator (`export const a = 1, { b } = o`), or
|
|
468
|
+
one the reader cannot read (one that does not close, or a default
|
|
469
|
+
holding a template literal over several lines), binds nothing here and
|
|
470
|
+
no warning names it: read it by eye.
|
|
438
471
|
|
|
439
472
|
Counts. `exports_public_api` counts the public names (the package's whole
|
|
440
473
|
public API, `outside_scope` names included), each distinct name and file
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
474
|
+
`public` gives once (`other_definitions` adds none): a namespace with
|
|
475
|
+
`members` counts as its members (none for an empty list, and a member
|
|
476
|
+
that is also a public name, or another namespace's member, once), one
|
|
477
|
+
with null `members` as one. `exports_internal` counts the internal names.
|
|
478
|
+
`effective_denominator` counts those public names with a definition file
|
|
479
|
+
(the `public` row's, or one of its `other_definitions`') that matches
|
|
480
|
+
scope.include (any file when it has no glob), scope.tier_a_include when
|
|
481
|
+
given, and no scope.exclude glob: each name once, however many in-scope
|
|
482
|
+
files define it, so a type counts once whatever its methods, a barrel
|
|
483
|
+
that only re-exports adds nothing of its own, and a name the entry points
|
|
484
|
+
pass on from two files (two packages' entry points that each define it,
|
|
485
|
+
or a namespace member named like another public name, or like another
|
|
486
|
+
namespace's member, at another file) or in two languages counts once,
|
|
487
|
+
when any one of its files matches, as Test Skill's sets count it. A tier
|
|
488
|
+
A list that counts no public name (each glob matches no file, or only
|
|
489
|
+
files scope.exclude drops, that scope.include leaves out or that define
|
|
490
|
+
no public name) narrows nothing on a run that counted a public name and
|
|
491
|
+
read every file (no `errors` item): the count falls back to scope.include
|
|
492
|
+
(else every file), with a warning.
|
|
458
493
|
`arms` gives compile's effective_denominator arms:
|
|
459
|
-
(a) `monorepo` from skf-detect-workspaces.py
|
|
494
|
+
(a) `monorepo` from skf-detect-workspaces.py over the tree the brief
|
|
495
|
+
keeps (its scoped_tree: a member counts only through the files no
|
|
496
|
+
scope.exclude glob matches, so excluding its sources drops it, excluding
|
|
497
|
+
only its manifest does not, root manifests always stay, and a workspace
|
|
498
|
+
with no member left is none), (b) `specific_modules`
|
|
460
499
|
(scope type specific-modules on a repository that is not a monorepo) and
|
|
461
500
|
(c) `multi_subpath_exports` (an in-scope package.json whose `exports` map
|
|
462
501
|
has more than one non-root subpath without a `*`, on a repository that is
|
|
@@ -466,20 +505,22 @@ Store or not. `curated_subset` is true when the files the count's basis
|
|
|
466
505
|
globs list (scope.include's, else every file; a tier A count's, those
|
|
467
506
|
among them a tier A glob matches too; under arm (b), less those a
|
|
468
507
|
scope.exclude glob matches) leave out a file that defines a counted public
|
|
469
|
-
name (an `outside_scope` name's
|
|
470
|
-
|
|
508
|
+
name (an `outside_scope` name's file and an `other_definitions` file
|
|
509
|
+
included) or every source file of a
|
|
510
|
+
workspace skf-detect-workspaces.py finds over the whole tree
|
|
511
|
+
(scope.exclude aside, unlike arm (a): a member the brief drops is still
|
|
512
|
+
part of the repository it chose a subset of); a basis of `all-files` lists
|
|
471
513
|
none. `effective_denominator_decision` says whether create-skill and
|
|
472
514
|
update-skill store `effective_denominator` as stats.effective_denominator,
|
|
473
515
|
the brief's scope.notes aside (documenting the stratification is the one
|
|
474
|
-
judgment left to them): `store` is true only when none of these holds,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
`
|
|
481
|
-
|
|
482
|
-
deflation).
|
|
516
|
+
judgment left to them): `store` is true only when none of these holds, and
|
|
517
|
+
`reason` names the first that does, else `curated-subset`: `reference-app`
|
|
518
|
+
(scope type reference-app: pattern_surfaces_documented is its basis),
|
|
519
|
+
`incomplete-run` (an `errors` item: a file a failed call left unread can
|
|
520
|
+
hide a public name, so the count can be 0 or partial), `no-arm` (no arm is
|
|
521
|
+
true), `full-library` (scope type full-library), `all-files` (the basis),
|
|
522
|
+
`not-curated` (`curated_subset` false) and `zero-count` (a stored 0 is no
|
|
523
|
+
denominator: Test Skill reads it as deflation).
|
|
483
524
|
|
|
484
525
|
Output JSON (stdout, or -o):
|
|
485
526
|
|
|
@@ -530,6 +571,8 @@ Output JSON (stdout, or -o):
|
|
|
530
571
|
"file", "line",
|
|
531
572
|
"members": [{"name", "local", "file", "line"}, ...]
|
|
532
573
|
| null}, ...], # members: via namespace only
|
|
574
|
+
"other_definitions": [{"name", "language", "entry", "via", "from",
|
|
575
|
+
"local", "file", "line"}, ...], # by name, then file
|
|
533
576
|
"internal": [{"name", "language", "source_file", "source_line"}, ...],
|
|
534
577
|
"extraction_gaps": [{"name", "language", "entry", "file", "line"}, ...],
|
|
535
578
|
"outside_scope": [{"name", "language", "entry", "file", "line"}, ...]},
|
|
@@ -618,9 +661,16 @@ the entry points export by name, which counts as the names its module
|
|
|
618
661
|
passes on), each export of a --quick output at its `defined_in` (else its
|
|
619
662
|
`source_file`), and the --extraction runner's `entry_point_diff.public`;
|
|
620
663
|
a payload name replaces a --quick or runner one of its family and name.
|
|
664
|
+
The runner's `other_definitions` count in `effective_denominator` and
|
|
665
|
+
`curated_subset` as full mode counts them, never in `exports_public_api`,
|
|
666
|
+
each only while the record kept for its name names the file of the
|
|
667
|
+
runner's `public` row (a row whose name has no runner row, or one a
|
|
668
|
+
payload or --quick name moves to another file, adds nothing), and none
|
|
669
|
+
beside a namespace with members, as in full mode.
|
|
621
670
|
`exports_internal` counts the names of the payload's `exports` (the
|
|
622
|
-
exports extraction found; `_` names left out
|
|
623
|
-
|
|
671
|
+
exports extraction found; Python, Rust and Dart `_` names left out, those
|
|
672
|
+
of every other language counted) and of the runner's `internal` that no
|
|
673
|
+
public name or member has. `effective_denominator`,
|
|
624
674
|
its basis, `curated_subset` and `effective_denominator_decision` follow
|
|
625
675
|
full mode's rules, over the tree under --source-root, with the scope and
|
|
626
676
|
arms full mode works out there; an --extraction that is `incomplete` is
|
|
@@ -638,10 +688,12 @@ and `arms`.
|
|
|
638
688
|
|
|
639
689
|
`entry_point_diff` replaces a compare by eye, each item {"name",
|
|
640
690
|
"language", "file"}: `public`, the names `exports_public_api` counts (a
|
|
641
|
-
namespace as its `members`); `
|
|
642
|
-
counts
|
|
643
|
-
|
|
644
|
-
|
|
691
|
+
namespace as its `members`); `other_definitions`, the runner's other
|
|
692
|
+
definitions the counts read, by name then file; `internal`, the exports
|
|
693
|
+
`exports_internal` counts; `extraction_gaps`, the public names the
|
|
694
|
+
payload and --quick give that no export found has (the payload's
|
|
695
|
+
`exports`, the --extraction runner's names), to trace (the runner lists
|
|
696
|
+
its own gaps).
|
|
645
697
|
|
|
646
698
|
Output JSON: `mode` "denominator", `source_root`, `scope` (as full mode
|
|
647
699
|
writes it), `files_in_scope`, `files_without_recipes`, `entry_points`,
|
|
@@ -2627,11 +2679,22 @@ def _preferred(at_line: list[dict], forms: dict, order: dict[str, int]) -> dict:
|
|
|
2627
2679
|
return min(left, key=lambda m: (order[m["rule"]], m["language"]))
|
|
2628
2680
|
|
|
2629
2681
|
|
|
2682
|
+
def _private(family: str | None, name: str) -> bool:
|
|
2683
|
+
"""A name full mode and denominator mode never count: one that starts
|
|
2684
|
+
with `_` in Python (the Source API Surface Definition leaves it out),
|
|
2685
|
+
Rust (a `#[doc(hidden)] pub mod __private`, which the runner cannot
|
|
2686
|
+
read, or a `pub const _`) or Dart (private to its library). A TS/JS `_`
|
|
2687
|
+
name is exported like any other, and Go's recipe reads only a
|
|
2688
|
+
capitalized name. Quick mode drops only Python's `_` names."""
|
|
2689
|
+
return family in ("python", "rust", "dart") and name.startswith("_")
|
|
2690
|
+
|
|
2691
|
+
|
|
2630
2692
|
def merge_exports(matches: list[dict], recipes: list[dict], recipe_set: str,
|
|
2631
2693
|
head_cap: int | None) -> tuple[list[dict], list[dict]]:
|
|
2632
2694
|
"""(exports, per-recipe stats) from the matches of one recipe set: at
|
|
2633
2695
|
most `head_cap` matches of each recipe, in file and line order, merged
|
|
2634
|
-
by (file, name) at the first line that names it.
|
|
2696
|
+
by (file, name) at the first line that names it. A Python or Rust `_`
|
|
2697
|
+
name is no export."""
|
|
2635
2698
|
forms = _forms(recipes)
|
|
2636
2699
|
order: dict[str, int] = {}
|
|
2637
2700
|
languages: dict[str, list[str]] = {}
|
|
@@ -2642,7 +2705,8 @@ def merge_exports(matches: list[dict], recipes: list[dict], recipe_set: str,
|
|
|
2642
2705
|
langs.extend(lang for lang in recipe["metadata"]["languages"] if lang not in langs)
|
|
2643
2706
|
per_recipe: dict[str, list[dict]] = {rid: [] for rid in order}
|
|
2644
2707
|
for m in matches:
|
|
2645
|
-
if m["rule"] in per_recipe and m["name"] and not
|
|
2708
|
+
if (m["rule"] in per_recipe and m["name"] and not _private(FAMILY_OF.get(m["language"]), m["name"])
|
|
2709
|
+
and (m["rule"], m["language"]) in forms):
|
|
2646
2710
|
per_recipe[m["rule"]].append(m)
|
|
2647
2711
|
stats, kept = [], []
|
|
2648
2712
|
for rid, found in per_recipe.items():
|
|
@@ -2839,14 +2903,190 @@ _JS_DEFAULT_NAME_RE = re.compile(r"^export\s+default\s+([A-Za-z_$][\w$]*)\s*;?\s
|
|
|
2839
2903
|
_JS_DEFAULT_EXPORT_RE = re.compile(r"export\s+default\b")
|
|
2840
2904
|
|
|
2841
2905
|
|
|
2906
|
+
# Keep identical to _js_pattern_bindings in skf-verify-provenance-completeness.py.
|
|
2907
|
+
def _js_pattern_bindings(
|
|
2908
|
+
text: str, start: int
|
|
2909
|
+
) -> tuple[list[tuple[str, int]], int | None]:
|
|
2910
|
+
"""The names the destructuring pattern whose `{` or `[` is at
|
|
2911
|
+
`text[start]` binds, each with its offset in `text`, and the offset
|
|
2912
|
+
just past the pattern's closing bracket.
|
|
2913
|
+
|
|
2914
|
+
An object pattern's property is `...target`, a shorthand key or
|
|
2915
|
+
`key: target` (a key is a name, a string, a number or a computed
|
|
2916
|
+
`[...]`, and only a shorthand key binds itself); an array pattern's
|
|
2917
|
+
element is `...target`, a target or a hole; a target is a name or a
|
|
2918
|
+
nested pattern. A `= default` is skipped to the next comma at the
|
|
2919
|
+
pattern's depth, and so is a property or element of any other shape,
|
|
2920
|
+
which binds nothing. '...', "..." and `...` strings are skipped
|
|
2921
|
+
whole, so no bracket or comma inside one counts. Whitespace, newlines
|
|
2922
|
+
included, may sit anywhere between the parts.
|
|
2923
|
+
|
|
2924
|
+
Returns ([], None) when `text[start]` opens no pattern, the pattern
|
|
2925
|
+
does not close in `text`, a string in it does not end, or a bracket in
|
|
2926
|
+
it closes one it did not open. A pure function of its arguments (no
|
|
2927
|
+
module state), so another script can carry a copy of it.
|
|
2928
|
+
"""
|
|
2929
|
+
n = len(text)
|
|
2930
|
+
names: list[tuple[str, int]] = []
|
|
2931
|
+
|
|
2932
|
+
def space(i: int) -> int:
|
|
2933
|
+
while i < n and text[i].isspace():
|
|
2934
|
+
i += 1
|
|
2935
|
+
return i
|
|
2936
|
+
|
|
2937
|
+
def name_end(i: int) -> int:
|
|
2938
|
+
"""The end of the identifier at `i`, `i` itself when none starts there."""
|
|
2939
|
+
if i < n and (text[i].isalpha() or text[i] in "_$"):
|
|
2940
|
+
i += 1
|
|
2941
|
+
while i < n and (text[i].isalnum() or text[i] in "_$"):
|
|
2942
|
+
i += 1
|
|
2943
|
+
return i
|
|
2944
|
+
|
|
2945
|
+
def string_end(i: int) -> int | None:
|
|
2946
|
+
"""The offset past the string whose quote is at `i`."""
|
|
2947
|
+
quote = text[i]
|
|
2948
|
+
i += 1
|
|
2949
|
+
while i < n:
|
|
2950
|
+
if text[i] == "\\":
|
|
2951
|
+
i += 2
|
|
2952
|
+
elif text[i] == quote:
|
|
2953
|
+
return i + 1
|
|
2954
|
+
else:
|
|
2955
|
+
i += 1
|
|
2956
|
+
return None
|
|
2957
|
+
|
|
2958
|
+
def skip(i: int, to_comma: bool = True) -> int | None:
|
|
2959
|
+
"""From `i`, the offset of the next comma (unless not `to_comma`)
|
|
2960
|
+
or closing bracket at depth 0; with `to_comma` False, `text[i]`
|
|
2961
|
+
opens a bracket and the offset past the one closing it."""
|
|
2962
|
+
depth = 0
|
|
2963
|
+
while i < n:
|
|
2964
|
+
ch = text[i]
|
|
2965
|
+
if ch in "'\"`":
|
|
2966
|
+
end = string_end(i)
|
|
2967
|
+
if end is None:
|
|
2968
|
+
return None
|
|
2969
|
+
i = end
|
|
2970
|
+
continue
|
|
2971
|
+
if ch in "([{":
|
|
2972
|
+
depth += 1
|
|
2973
|
+
elif ch in ")]}":
|
|
2974
|
+
if depth == 0:
|
|
2975
|
+
return i
|
|
2976
|
+
depth -= 1
|
|
2977
|
+
if depth == 0 and not to_comma:
|
|
2978
|
+
return i + 1
|
|
2979
|
+
elif ch == "," and depth == 0 and to_comma:
|
|
2980
|
+
return i
|
|
2981
|
+
i += 1
|
|
2982
|
+
return None
|
|
2983
|
+
|
|
2984
|
+
def default(i: int) -> int | None:
|
|
2985
|
+
"""After a target: past its `= default`, when it has one."""
|
|
2986
|
+
i = space(i)
|
|
2987
|
+
if i < n and text[i] == "=" and text[i + 1 : i + 2] not in ("=", ">"):
|
|
2988
|
+
return skip(i + 1)
|
|
2989
|
+
return i
|
|
2990
|
+
|
|
2991
|
+
def bind(i: int, end: int, close: str) -> int | None:
|
|
2992
|
+
"""The name `text[i:end]` is bound when its property or element
|
|
2993
|
+
ends after it or takes a default (any other text: another shape)."""
|
|
2994
|
+
after = space(end)
|
|
2995
|
+
if after < n and (
|
|
2996
|
+
text[after] in (",", close)
|
|
2997
|
+
or text[after] == "=" and text[after + 1 : after + 2] not in ("=", ">")
|
|
2998
|
+
):
|
|
2999
|
+
names.append((text[i:end], i))
|
|
3000
|
+
return default(after)
|
|
3001
|
+
return after
|
|
3002
|
+
|
|
3003
|
+
def target(i: int, close: str) -> int | None:
|
|
3004
|
+
if i < n and text[i] in "{[":
|
|
3005
|
+
end = pattern(i)
|
|
3006
|
+
return None if end is None else default(end)
|
|
3007
|
+
end = name_end(i)
|
|
3008
|
+
return i if end == i else bind(i, end, close)
|
|
3009
|
+
|
|
3010
|
+
def element(i: int, close: str) -> int | None:
|
|
3011
|
+
"""One property or element at `i`: the offset after it."""
|
|
3012
|
+
if text.startswith("...", i):
|
|
3013
|
+
return target(space(i + 3), close)
|
|
3014
|
+
if close == "]":
|
|
3015
|
+
return target(i, close)
|
|
3016
|
+
if text[i] in "'\"":
|
|
3017
|
+
end = string_end(i)
|
|
3018
|
+
elif text[i] == "[":
|
|
3019
|
+
end = skip(i, to_comma=False) # a computed key
|
|
3020
|
+
else:
|
|
3021
|
+
end = name_end(i)
|
|
3022
|
+
if end > i:
|
|
3023
|
+
after = space(end)
|
|
3024
|
+
if after < n and text[after] == ":":
|
|
3025
|
+
return target(space(after + 1), close)
|
|
3026
|
+
return bind(i, end, close) # a shorthand key
|
|
3027
|
+
while end < n and (text[end].isalnum() or text[end] in "._"):
|
|
3028
|
+
end += 1 # a number key
|
|
3029
|
+
if end is None:
|
|
3030
|
+
return None
|
|
3031
|
+
after = space(end)
|
|
3032
|
+
if end > i and after < n and text[after] == ":":
|
|
3033
|
+
return target(space(after + 1), close)
|
|
3034
|
+
return after
|
|
3035
|
+
|
|
3036
|
+
def pattern(i: int) -> int | None:
|
|
3037
|
+
"""The pattern opening at `i`: the offset past its closing bracket."""
|
|
3038
|
+
close = "}" if text[i] == "{" else "]"
|
|
3039
|
+
i += 1
|
|
3040
|
+
while True:
|
|
3041
|
+
i = space(i)
|
|
3042
|
+
if i >= n:
|
|
3043
|
+
return None
|
|
3044
|
+
if text[i] == close:
|
|
3045
|
+
return i + 1
|
|
3046
|
+
if text[i] in ")]}":
|
|
3047
|
+
return None
|
|
3048
|
+
if text[i] == ",":
|
|
3049
|
+
i += 1 # a hole, or a stray comma
|
|
3050
|
+
continue
|
|
3051
|
+
end = element(i, close)
|
|
3052
|
+
if end is None:
|
|
3053
|
+
return None
|
|
3054
|
+
end = space(end)
|
|
3055
|
+
if end < n and text[end] not in (",", close):
|
|
3056
|
+
end = skip(end) # any other shape binds nothing
|
|
3057
|
+
if end is None:
|
|
3058
|
+
return None
|
|
3059
|
+
i = end + 1 if end < n and text[end] == "," else end
|
|
3060
|
+
|
|
3061
|
+
if not (0 <= start < n and text[start] in "{["):
|
|
3062
|
+
return [], None
|
|
3063
|
+
end = pattern(start)
|
|
3064
|
+
return (names, end) if end is not None else ([], None)
|
|
3065
|
+
|
|
3066
|
+
|
|
2842
3067
|
def _js_text_names(text: str) -> tuple[list[tuple[str, int, bool]], list[tuple[str, int]]]:
|
|
2843
3068
|
"""The names a JS/TS file's column-0 `export` declarations bind, read
|
|
2844
3069
|
from its text for the forms no recipe matches (`export let`, `export
|
|
2845
|
-
declare const`, `export namespace
|
|
2846
|
-
|
|
3070
|
+
declare const`, `export namespace`, and each name a destructuring
|
|
3071
|
+
pattern that opens an `export const`, `let` or `var` statement binds:
|
|
3072
|
+
`export const { a, b: c, ...d } = obj` binds a, c and d), as (name,
|
|
3073
|
+
line, is the default export), and its `export default <name>;`
|
|
3074
|
+
statements, as (name, line).
|
|
3075
|
+
|
|
3076
|
+
A pattern is read from its `{` or `[` on (`_JS_DESTRUCTURE_RE`) with
|
|
3077
|
+
`_js_pattern_bindings`, so it may span lines, and each name it binds
|
|
3078
|
+
takes the line the name is on. A pattern after the first declarator
|
|
3079
|
+
is not read, and one that does not close, or that nests deeper than
|
|
3080
|
+
the interpreter's recursion limit, binds nothing."""
|
|
2847
3081
|
code = _blank(text, "js")
|
|
2848
3082
|
declared = [(m.group(2), _line_of(code, m.start(2)), bool(m.group(1)))
|
|
2849
3083
|
for rx in (_JS_DECLARATION_RE, _JS_IMPORT_EQUALS_RE) for m in rx.finditer(code)]
|
|
3084
|
+
for m in _JS_DESTRUCTURE_RE.finditer(code):
|
|
3085
|
+
try:
|
|
3086
|
+
names, _ = _js_pattern_bindings(code, m.end() - 1)
|
|
3087
|
+
except RecursionError:
|
|
3088
|
+
continue # patterns nested past the interpreter's limit: none bound
|
|
3089
|
+
declared += [(name, _line_of(code, offset), False) for name, offset in names]
|
|
2850
3090
|
defaults = [(m.group(1), _line_of(code, m.start(1))) for m in _JS_DEFAULT_NAME_RE.finditer(code)]
|
|
2851
3091
|
return declared, defaults
|
|
2852
3092
|
|
|
@@ -3084,12 +3324,13 @@ def _member(name: str, local: str | None, file: str | None, line: int | None) ->
|
|
|
3084
3324
|
return {"name": name, "local": local if local and local != name else None, "file": file, "line": line}
|
|
3085
3325
|
|
|
3086
3326
|
|
|
3087
|
-
def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]]) -> list[dict]:
|
|
3088
|
-
"""The members of the namespace module `start
|
|
3089
|
-
passes on, (name, "member", member) or
|
|
3090
|
-
each nested namespace module's members
|
|
3091
|
-
module once, so a cycle is cut), each
|
|
3092
|
-
that starts with `_`,
|
|
3327
|
+
def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]], family: str) -> list[dict]:
|
|
3328
|
+
"""The members of the namespace module `start`, of language family
|
|
3329
|
+
`family`: what `direct` says it passes on, (name, "member", member) or
|
|
3330
|
+
(name, "module", the module), each nested namespace module's members
|
|
3331
|
+
spliced in its place (each module once, so a cycle is cut), each
|
|
3332
|
+
(name, file) once, and no Python or Rust name that starts with `_`,
|
|
3333
|
+
which is never public."""
|
|
3093
3334
|
out: list[dict] = []
|
|
3094
3335
|
seen: set[tuple[str, str | None]] = set()
|
|
3095
3336
|
visited: set = set()
|
|
@@ -3097,7 +3338,7 @@ def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]]) -
|
|
|
3097
3338
|
def walk(module) -> None:
|
|
3098
3339
|
visited.add(module)
|
|
3099
3340
|
for name, kind, value in direct(module):
|
|
3100
|
-
if not name or name
|
|
3341
|
+
if not name or _private(family, name):
|
|
3101
3342
|
continue
|
|
3102
3343
|
if kind == "module":
|
|
3103
3344
|
if value not in visited:
|
|
@@ -3111,17 +3352,35 @@ def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]]) -
|
|
|
3111
3352
|
|
|
3112
3353
|
|
|
3113
3354
|
class _Surface:
|
|
3114
|
-
"""The public names
|
|
3115
|
-
{name: info}, the first
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3355
|
+
"""The public names the entry points of language family `family`
|
|
3356
|
+
declare: {name: info}, the first `add` of a name wins (the JS/TS trace
|
|
3357
|
+
adds every entry point's own and named exports before any `export *`),
|
|
3358
|
+
unless the file it gives is out of scope (or it gives none) and another
|
|
3359
|
+
entry point gives a file in `in_scope`, which then replaces it (one
|
|
3360
|
+
entry point never replaces its own, and a star or glob that passes on a
|
|
3361
|
+
name its entry point already exports replaces none: an explicit export
|
|
3362
|
+
shadows it; with no `in_scope`, the first always wins); `others`,
|
|
3363
|
+
{name: [info]}, the other definitions of a held name in files in
|
|
3364
|
+
`in_scope` (#830), each an add that does not become the held record,
|
|
3365
|
+
is no such shadowed star or glob, is no namespace, beside a held record
|
|
3366
|
+
that is none, and gives a file in scope whose (file, `local` or name)
|
|
3367
|
+
neither the held record nor an earlier one of `others` gives; the
|
|
3368
|
+
names, and the files, a namespace export (a module) makes reachable,
|
|
3369
|
+
which are not internal; and the re-exports the trace could not follow.
|
|
3370
|
+
A namespace item also carries its `members`, the names its module
|
|
3371
|
+
passes on (None when the trace cannot read that module); `opaque` holds
|
|
3372
|
+
the (name, file) of each member that is itself a module the trace
|
|
3373
|
+
cannot read, which, as such a namespace, is no recipe's target. A
|
|
3374
|
+
Python or Rust `_` name is never added."""
|
|
3375
|
+
|
|
3376
|
+
def __init__(self, family: str, in_scope: set[str] | None = None) -> None:
|
|
3377
|
+
self.family = family
|
|
3378
|
+
self.in_scope = in_scope
|
|
3124
3379
|
self.names: dict[str, dict] = {}
|
|
3380
|
+
self.others: dict[str, list[dict]] = {}
|
|
3381
|
+
self._other_at: dict[str, set[tuple[str, str]]] = {} # name: the (file, local or name) of its others
|
|
3382
|
+
# (entry, name) of each add: a star or glob an entry point passes an exported name on through is shadowed
|
|
3383
|
+
self._added: set[tuple[str, str]] = set()
|
|
3125
3384
|
self.reachable: set[str] = set()
|
|
3126
3385
|
self.reachable_files: set[str] = set()
|
|
3127
3386
|
self.unresolved: list[dict] = []
|
|
@@ -3130,14 +3389,25 @@ class _Surface:
|
|
|
3130
3389
|
def add(self, name: str, entry: str, via: str, file: str | None, line: int | None,
|
|
3131
3390
|
source: str | None = None, local: str | None = None,
|
|
3132
3391
|
members: list[dict] | None = None) -> dict | None:
|
|
3133
|
-
if not name or name
|
|
3392
|
+
if not name or _private(self.family, name):
|
|
3134
3393
|
return None
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3394
|
+
held = self.names.get(name)
|
|
3395
|
+
shadowed = via == "star" and (entry, name) in self._added
|
|
3396
|
+
self._added.add((entry, name))
|
|
3397
|
+
info = {"name": name, "entry": entry, "via": via, "from": source,
|
|
3398
|
+
"local": local if local and local != name else None, "file": file, "line": line}
|
|
3399
|
+
at = (file, info["local"] or name)
|
|
3400
|
+
if held is None or (not shadowed and self.in_scope is not None and file in self.in_scope
|
|
3401
|
+
and entry != held["entry"]
|
|
3402
|
+
and (held["file"] is None or held["file"] not in self.in_scope)):
|
|
3403
|
+
self.names[name] = info
|
|
3139
3404
|
if via == "namespace":
|
|
3140
3405
|
self.names[name]["members"] = [dict(m) for m in members] if members is not None else None
|
|
3406
|
+
elif (not shadowed and via != "namespace" and held["via"] != "namespace" and self.in_scope is not None
|
|
3407
|
+
and file in self.in_scope and at != (held["file"], held["local"] or name)
|
|
3408
|
+
and at not in self._other_at.get(name, ())):
|
|
3409
|
+
self.others.setdefault(name, []).append(info)
|
|
3410
|
+
self._other_at.setdefault(name, set()).add(at)
|
|
3141
3411
|
return self.names[name]
|
|
3142
3412
|
|
|
3143
3413
|
def unresolve(self, entry: str, name: str, source: str | None, file: str) -> None:
|
|
@@ -3341,7 +3611,7 @@ def _js_surface(root: Path, entries: list[dict], tree_set: set[str], forms: dict
|
|
|
3341
3611
|
|
|
3342
3612
|
def members(file: str) -> list[dict]:
|
|
3343
3613
|
if file not in member_lists:
|
|
3344
|
-
member_lists[file] = _flatten(file, direct)
|
|
3614
|
+
member_lists[file] = _flatten(file, direct, "javascript")
|
|
3345
3615
|
return member_lists[file]
|
|
3346
3616
|
|
|
3347
3617
|
def publish(entry: str, name: str, via: str, module: str, source: str | None) -> None:
|
|
@@ -3712,7 +3982,7 @@ def _py_surface(root: Path, entries: list[dict], tree_set: set[str], surface: _S
|
|
|
3712
3982
|
|
|
3713
3983
|
def members(rel: str, top: str) -> list[dict]:
|
|
3714
3984
|
if (rel, top) not in member_lists:
|
|
3715
|
-
member_lists[(rel, top)] = _flatten(rel, lambda module: direct(module, top))
|
|
3985
|
+
member_lists[(rel, top)] = _flatten(rel, lambda module: direct(module, top), "python")
|
|
3716
3986
|
return member_lists[(rel, top)]
|
|
3717
3987
|
|
|
3718
3988
|
for entry in [e["file"] for e in entries]:
|
|
@@ -4030,7 +4300,7 @@ def _rust_surface(root: Path, entries: list[dict], tree_set: set[str], surface:
|
|
|
4030
4300
|
return None
|
|
4031
4301
|
key = (lib, tuple(here))
|
|
4032
4302
|
if key not in member_lists:
|
|
4033
|
-
member_lists[key] = _flatten(tuple(here), lambda path: direct(lib, path))
|
|
4303
|
+
member_lists[key] = _flatten(tuple(here), lambda path: direct(lib, path), "rust")
|
|
4034
4304
|
return member_lists[key]
|
|
4035
4305
|
|
|
4036
4306
|
def publish(lib: str, here: list[str], name: str, via: str, source: str | None,
|
|
@@ -4130,19 +4400,25 @@ def _go_surface(root: Path, files: list[str], surface: _Surface, entries: list[d
|
|
|
4130
4400
|
|
|
4131
4401
|
def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries: list[dict],
|
|
4132
4402
|
in_scope: set[str]) -> tuple[dict[str, str], dict[tuple[str, str], dict], list[dict],
|
|
4133
|
-
list[tuple[str, dict]], list[tuple[str, dict]]
|
|
4403
|
+
list[tuple[str, dict]], list[tuple[str, dict]],
|
|
4404
|
+
list[tuple[str, dict]]]:
|
|
4134
4405
|
"""The entry-point diff, one language family at a time: (status by
|
|
4135
4406
|
family, public names {(family, name): info}, internal export records,
|
|
4136
|
-
and (family, info) of the extraction gaps
|
|
4137
|
-
|
|
4407
|
+
and (family, info) of the extraction gaps, of the names outside scope
|
|
4408
|
+
and of the other in-scope definitions of public names, `_Surface`'s
|
|
4409
|
+
`others`, which no gap or outside_scope row reads: those go by name).
|
|
4410
|
+
A family whose entry points declare names diffs them with its
|
|
4138
4411
|
exports: an export no entry point names, nor traces back to, nor makes
|
|
4139
4412
|
reachable (a name, or any export of a reachable file), is internal. A
|
|
4140
|
-
declared name
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
recipe found
|
|
4145
|
-
|
|
4413
|
+
declared name, at the file `_Surface` kept for it (not always the first
|
|
4414
|
+
entry point's in path order), is outside scope when a file out of scope
|
|
4415
|
+
defines it, even when a recipe found its name in another file (another
|
|
4416
|
+
definition: no recipe reads a file out of scope), and a gap when no
|
|
4417
|
+
recipe found it and a file in scope defines it (or the trace found no
|
|
4418
|
+
file); a module is no recipe's target, so neither: a namespace with
|
|
4419
|
+
`members` has each member diffed in its place, found when a recipe
|
|
4420
|
+
found its name, or its `local` one, in its file, and given no row of
|
|
4421
|
+
its own when a declared name has its name and file (or another
|
|
4146
4422
|
namespace gave it one); one whose members are unknown (None), or a
|
|
4147
4423
|
member that is such a module (`opaque`), is no gap. In a family with
|
|
4148
4424
|
no entry point, or only empty ones, every export is public."""
|
|
@@ -4151,6 +4427,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4151
4427
|
internal: list[dict] = []
|
|
4152
4428
|
gaps: list[tuple[str, dict]] = []
|
|
4153
4429
|
outside: list[tuple[str, dict]] = []
|
|
4430
|
+
others: list[tuple[str, dict]] = []
|
|
4154
4431
|
entry_families = {FAMILY_OF[e["language"]] for e in entries}
|
|
4155
4432
|
families = {FAMILY_OF[e["language"]] for e in exports} | entry_families
|
|
4156
4433
|
for family in sorted(families | {f for f, s in surfaces.items() if s.names}):
|
|
@@ -4168,6 +4445,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4168
4445
|
names = {e["export_name"] for e in records}
|
|
4169
4446
|
traced = {info["local"] for info in declared.values() if info["local"]} | surface.reachable
|
|
4170
4447
|
public.update(((family, name), info) for name, info in declared.items())
|
|
4448
|
+
others.extend((family, dict(info)) for name in sorted(surface.others) for info in surface.others[name])
|
|
4171
4449
|
seen: set[str] = set()
|
|
4172
4450
|
for e in records:
|
|
4173
4451
|
name = e["export_name"]
|
|
@@ -4176,9 +4454,13 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4176
4454
|
seen.add(name)
|
|
4177
4455
|
internal.append(e)
|
|
4178
4456
|
for name, info in declared.items():
|
|
4179
|
-
if
|
|
4457
|
+
if info["via"] == "namespace":
|
|
4180
4458
|
continue
|
|
4181
|
-
|
|
4459
|
+
# A recipe never reads a file out of scope: a name it found elsewhere is another definition.
|
|
4460
|
+
if info["file"] is not None and info["file"] not in in_scope:
|
|
4461
|
+
outside.append((family, info))
|
|
4462
|
+
elif name not in names and (info["local"] or name) not in names:
|
|
4463
|
+
gaps.append((family, info))
|
|
4182
4464
|
# A namespace's members, each by its name (or `local`) and file, or
|
|
4183
4465
|
# by name alone when it has no file, as a declared name is: a name
|
|
4184
4466
|
# at a file a declared name already gives counts once.
|
|
@@ -4195,7 +4477,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4195
4477
|
row = {"name": member["name"], "entry": info["entry"], "file": member["file"],
|
|
4196
4478
|
"line": member["line"]}
|
|
4197
4479
|
(gaps if member["file"] is None or member["file"] in in_scope else outside).append((family, row))
|
|
4198
|
-
return statuses, public, internal, gaps, outside
|
|
4480
|
+
return statuses, public, internal, gaps, outside, others
|
|
4199
4481
|
|
|
4200
4482
|
|
|
4201
4483
|
def _has_members(info: dict) -> bool:
|
|
@@ -4204,9 +4486,9 @@ def _has_members(info: dict) -> bool:
|
|
|
4204
4486
|
|
|
4205
4487
|
|
|
4206
4488
|
def _public_keys(public: dict[tuple[str, str], dict]) -> set[tuple[str, str, str | None]]:
|
|
4207
|
-
"""The (family, name, file)
|
|
4208
|
-
namespace with members, in its place each member, and so a member
|
|
4209
|
-
is also a declared name (or another namespace's member) once."""
|
|
4489
|
+
"""The (family, name, file) exports_public_api counts: each public name
|
|
4490
|
+
but a namespace with members, in its place each member, and so a member
|
|
4491
|
+
that is also a declared name (or another namespace's member) once."""
|
|
4210
4492
|
keys: set[tuple[str, str, str | None]] = set()
|
|
4211
4493
|
for (family, _), info in public.items():
|
|
4212
4494
|
if _has_members(info):
|
|
@@ -4216,6 +4498,16 @@ def _public_keys(public: dict[tuple[str, str], dict]) -> set[tuple[str, str, str
|
|
|
4216
4498
|
return keys
|
|
4217
4499
|
|
|
4218
4500
|
|
|
4501
|
+
def _other_keys(others: list[tuple[str, dict]]) -> set[tuple[str, str, str | None]]:
|
|
4502
|
+
"""The (family, name, file) of the other in-scope definitions of public
|
|
4503
|
+
names (#830), which join the public keys in the set effective_denominator
|
|
4504
|
+
and curated_subset read, never exports_public_api: a name still counts
|
|
4505
|
+
once, when any of its files matches."""
|
|
4506
|
+
return {(family, info["name"], info["file"]) for family, info in others
|
|
4507
|
+
if isinstance(info.get("name"), str) and info["name"] and isinstance(info.get("file"), str)
|
|
4508
|
+
and info["file"]}
|
|
4509
|
+
|
|
4510
|
+
|
|
4219
4511
|
def _head_cap(value: int | None, tier: str | None, scope_type: str | None) -> int | None:
|
|
4220
4512
|
if value is not None:
|
|
4221
4513
|
return value or None
|
|
@@ -4223,7 +4515,10 @@ def _head_cap(value: int | None, tier: str | None, scope_type: str | None) -> in
|
|
|
4223
4515
|
|
|
4224
4516
|
|
|
4225
4517
|
def _workspaces(root: Path, tree: list[str], tree_set: set[str]) -> dict:
|
|
4226
|
-
"""skf-detect-workspaces.py's
|
|
4518
|
+
"""skf-detect-workspaces.py's result over `tree` (its workspaces
|
|
4519
|
+
resolved against those paths), with the content of each root manifest
|
|
4520
|
+
of WORKSPACE_MANIFESTS that `tree_set` lists. No brief scope applies:
|
|
4521
|
+
the monorepo arm reads it through _monorepo."""
|
|
4227
4522
|
manifests = {}
|
|
4228
4523
|
for name in WORKSPACE_MANIFESTS:
|
|
4229
4524
|
if name in tree_set:
|
|
@@ -4233,15 +4528,24 @@ def _workspaces(root: Path, tree: list[str], tree_set: set[str]) -> dict:
|
|
|
4233
4528
|
return _sibling("skf-detect-workspaces.py").detect({"tree": tree, "manifests": manifests})
|
|
4234
4529
|
|
|
4235
4530
|
|
|
4236
|
-
def _monorepo(root: Path, tree: list[str],
|
|
4237
|
-
|
|
4238
|
-
|
|
4531
|
+
def _monorepo(root: Path, tree: list[str], excludes: list[str],
|
|
4532
|
+
glob_match: Callable[[str, str], bool]) -> tuple[bool, str | None]:
|
|
4533
|
+
"""Arm (a): (is the source a monorepo, its manifest kind), from
|
|
4534
|
+
skf-detect-workspaces.py over the tree the brief keeps, its
|
|
4535
|
+
scoped_tree: a workspace member counts only through the files no
|
|
4536
|
+
scope.exclude glob matches, every root manifest stays, and a workspace
|
|
4537
|
+
with no member left is none (#780)."""
|
|
4538
|
+
kept = _sibling("skf-detect-workspaces.py").scoped_tree(tree, excludes, glob_match)
|
|
4539
|
+
result = _workspaces(root, kept, set(kept))
|
|
4239
4540
|
return bool(result.get("is_monorepo")), result.get("manifest_kind")
|
|
4240
4541
|
|
|
4241
4542
|
|
|
4242
4543
|
def _workspace_sources(result: dict, tree: list[str], wanted: set[str]) -> list[set[str]]:
|
|
4243
|
-
"""The source files (of a language --language selects)
|
|
4244
|
-
|
|
4544
|
+
"""The source files (of a language --language selects) under each
|
|
4545
|
+
workspace of `result`, one set per workspace that holds any. _scope_facts
|
|
4546
|
+
passes the workspaces skf-detect-workspaces.py finds over the whole
|
|
4547
|
+
tree, scope.exclude aside (unlike arm (a)): a member the brief drops is
|
|
4548
|
+
still part of the repository the brief chose a subset of."""
|
|
4245
4549
|
sources = []
|
|
4246
4550
|
for workspace in result.get("workspaces") or [] if result.get("is_monorepo") else []:
|
|
4247
4551
|
folder = _norm(workspace.get("path") if isinstance(workspace, dict) else "").strip("/")
|
|
@@ -4288,8 +4592,10 @@ def curated_subset(counted: set[tuple[str, str, str | None]], basis: str, tree:
|
|
|
4288
4592
|
whole workspace (in a single-package repository, the whole package): the
|
|
4289
4593
|
files the basis globs list (a tier A list's, among those scope.include
|
|
4290
4594
|
lists; under arm (b), less those scope.exclude drops) leave out a file
|
|
4291
|
-
that defines a counted public name or
|
|
4292
|
-
|
|
4595
|
+
that defines a counted public name or every source file of one of
|
|
4596
|
+
`workspace_sources` (the workspaces found over the whole tree,
|
|
4597
|
+
scope.exclude aside, so excluding a member the include globs leave out
|
|
4598
|
+
anyway changes nothing). A basis of all-files lists none."""
|
|
4293
4599
|
if basis == "all-files":
|
|
4294
4600
|
return False
|
|
4295
4601
|
files = {f for f in tree if not includes or any(glob_match(f, g) for g in includes)}
|
|
@@ -4413,7 +4719,7 @@ def _scope_facts(args: argparse.Namespace, root: Path) -> dict:
|
|
|
4413
4719
|
|
|
4414
4720
|
# The entry points and the arms need no ast-grep.
|
|
4415
4721
|
in_scope_set = set(in_scope)
|
|
4416
|
-
surfaces = {family: _Surface() for family in FAMILIES}
|
|
4722
|
+
surfaces = {family: _Surface(family, in_scope_set) for family in FAMILIES}
|
|
4417
4723
|
js_entries, maps, unresolved_subpaths = _js_entries(root, in_scope, tree, tree_set, warnings)
|
|
4418
4724
|
py_entries = _py_entries(in_scope, tree_set)
|
|
4419
4725
|
rust_entries = _rust_entries(root, in_scope, tree, tree_set)
|
|
@@ -4428,11 +4734,10 @@ def _scope_facts(args: argparse.Namespace, root: Path) -> dict:
|
|
|
4428
4734
|
f"{entry['package']}) is outside the files in scope: widen scope.include to document "
|
|
4429
4735
|
"the names it exports (entry_point_diff.outside_scope), which effective_denominator "
|
|
4430
4736
|
"leaves out")
|
|
4431
|
-
|
|
4432
|
-
monorepo = bool(workspaces.get("is_monorepo"))
|
|
4737
|
+
monorepo, monorepo_kind = _monorepo(root, tree, excludes, glob_match)
|
|
4433
4738
|
arms = {
|
|
4434
4739
|
"monorepo": monorepo,
|
|
4435
|
-
"monorepo_kind":
|
|
4740
|
+
"monorepo_kind": monorepo_kind,
|
|
4436
4741
|
"specific_modules": scope_type == "specific-modules" and not monorepo,
|
|
4437
4742
|
"multi_subpath_exports": not monorepo and any(m["non_root_subpaths"] > 1 for m in maps),
|
|
4438
4743
|
}
|
|
@@ -4442,7 +4747,8 @@ def _scope_facts(args: argparse.Namespace, root: Path) -> dict:
|
|
|
4442
4747
|
"in_scope": in_scope, "without_recipes": without_recipes, "unmatched_include": unmatched_include,
|
|
4443
4748
|
"unmatched_tier_a": unmatched_tier_a, "surfaces": surfaces, "js_entries": js_entries, "maps": maps,
|
|
4444
4749
|
"unresolved_subpaths": unresolved_subpaths, "py_entries": py_entries, "rust_entries": rust_entries,
|
|
4445
|
-
"entries": entries, "arms": arms,
|
|
4750
|
+
"entries": entries, "arms": arms,
|
|
4751
|
+
"workspace_sources": _workspace_sources(_workspaces(root, tree, tree_set), tree, wanted),
|
|
4446
4752
|
}
|
|
4447
4753
|
|
|
4448
4754
|
|
|
@@ -4586,7 +4892,7 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4586
4892
|
for record in exports:
|
|
4587
4893
|
if record["from"] and record["language"] in JS_FAMILY:
|
|
4588
4894
|
record["from_file"] = resolve_js_module(record["from"], record["source_file"], tree_set)
|
|
4589
|
-
statuses, public, internal, gaps, outside = entry_point_diff(exports, surfaces, entries, in_scope_set)
|
|
4895
|
+
statuses, public, internal, gaps, outside, others = entry_point_diff(exports, surfaces, entries, in_scope_set)
|
|
4590
4896
|
if "barrel" in statuses.values():
|
|
4591
4897
|
status = "barrel"
|
|
4592
4898
|
else:
|
|
@@ -4596,8 +4902,10 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4596
4902
|
# only files an exclude drops, that scope.include leaves out or that
|
|
4597
4903
|
# define no public name) narrows nothing, once the run counted a name
|
|
4598
4904
|
# and read every file (no error).
|
|
4599
|
-
counted
|
|
4600
|
-
|
|
4905
|
+
# Each other in-scope definition of a public name counts its name where any of its files is counted (#830).
|
|
4906
|
+
public_keys = _public_keys(public)
|
|
4907
|
+
counted = public_keys | _other_keys(others)
|
|
4908
|
+
counts = _denominator_counts(facts, counted, len(public_keys), len(internal), bool(errors), warnings)
|
|
4601
4909
|
|
|
4602
4910
|
by_type: Counter = Counter(e["export_type"] for e in exports)
|
|
4603
4911
|
ordered = sorted(public.items(), key=lambda item: (item[0][1], item[0][0]))
|
|
@@ -4611,6 +4919,8 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4611
4919
|
"unresolved": unresolved_subpaths + [u for s in surfaces.values() for u in s.unresolved]},
|
|
4612
4920
|
"entry_point_diff": {
|
|
4613
4921
|
"public": [dict(info, language=family) for (family, _), info in ordered],
|
|
4922
|
+
"other_definitions": [dict(info, language=family) for family, info in
|
|
4923
|
+
sorted(others, key=lambda o: (o[1]["name"], o[1]["file"], o[0]))],
|
|
4614
4924
|
"internal": [{"name": e["export_name"], "language": FAMILY_OF[e["language"]],
|
|
4615
4925
|
"source_file": e["source_file"], "source_line": e["source_line"]} for e in internal],
|
|
4616
4926
|
"extraction_gaps": [{"name": info["name"], "language": family, "entry": info["entry"],
|
|
@@ -4730,6 +5040,8 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4730
5040
|
# The exports found, each key at its file: the payload's and the runner's.
|
|
4731
5041
|
found: dict[tuple[str, str], str | None] = {}
|
|
4732
5042
|
internal: set[tuple[str, str]] = set()
|
|
5043
|
+
# The runner's other in-scope definitions of its public names (#830), counted where the names are.
|
|
5044
|
+
others: list[tuple[str, dict]] = []
|
|
4733
5045
|
incomplete = False
|
|
4734
5046
|
runner_status = None
|
|
4735
5047
|
if args.extraction:
|
|
@@ -4744,6 +5056,8 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4744
5056
|
key = (str(info.get("language") or ""), info["name"])
|
|
4745
5057
|
public[key] = runner_public[key] = info
|
|
4746
5058
|
found[key] = info.get("file")
|
|
5059
|
+
others = [(str(info.get("language") or ""), info) for info in diff.get("other_definitions") or []
|
|
5060
|
+
if isinstance(info, dict)]
|
|
4747
5061
|
for item in diff.get("internal") or []:
|
|
4748
5062
|
if isinstance(item, dict) and isinstance(item.get("name"), str):
|
|
4749
5063
|
internal.add((str(item.get("language") or ""), item["name"]))
|
|
@@ -4767,11 +5081,27 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4767
5081
|
for item in _payload_names(payload.get("exports"), "exports"):
|
|
4768
5082
|
key = (_name_family(item["language"], item["file"]), item["name"])
|
|
4769
5083
|
found.setdefault(key, item["file"])
|
|
4770
|
-
if not item["name"]
|
|
5084
|
+
if not _private(key[0], item["name"]):
|
|
4771
5085
|
internal.add(key)
|
|
4772
5086
|
named = set(public) | {(family, m["name"]) for (family, _), info in public.items() if _has_members(info)
|
|
4773
5087
|
for m in info["members"]}
|
|
4774
|
-
|
|
5088
|
+
public_keys = _public_keys(public)
|
|
5089
|
+
def still_held(key: tuple[str, str]) -> bool:
|
|
5090
|
+
"""Whether the record kept for the runner's public name `key` still names the runner row's file, a
|
|
5091
|
+
namespace with members in neither (full mode keeps no other definition beside one)."""
|
|
5092
|
+
runner_row, kept = runner_public.get(key), public.get(key)
|
|
5093
|
+
if runner_row is None or kept is None or _has_members(runner_row) or _has_members(kept):
|
|
5094
|
+
return False
|
|
5095
|
+
files = [_norm(row["file"]) if isinstance(row.get("file"), str) and row["file"] else None
|
|
5096
|
+
for row in (runner_row, kept)]
|
|
5097
|
+
return files[0] == files[1]
|
|
5098
|
+
|
|
5099
|
+
# A runner row counts only while the record kept for its name names the runner's file: an orphan row, or
|
|
5100
|
+
# one of a name a payload or --quick record moved to another file, adds nothing.
|
|
5101
|
+
others = [(family, info) for family, info in others
|
|
5102
|
+
if isinstance(info.get("name"), str) and still_held((family, info["name"]))]
|
|
5103
|
+
other_keys = _other_keys(others)
|
|
5104
|
+
counted = public_keys | other_keys
|
|
4775
5105
|
|
|
4776
5106
|
if args.source_root:
|
|
4777
5107
|
root = Path(args.source_root)
|
|
@@ -4807,7 +5137,7 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4807
5137
|
outside_tree = sorted((name, file) for _, name, file in counted if file and file not in tree_set)
|
|
4808
5138
|
warnings += [f"public name {name} is defined in {file}, which is not in the tree: effective_denominator "
|
|
4809
5139
|
"and curated_subset leave it out" for name, file in outside_tree]
|
|
4810
|
-
counts = _denominator_counts(facts, counted, len(
|
|
5140
|
+
counts = _denominator_counts(facts, counted, len(public_keys), len(internal - named), incomplete, warnings)
|
|
4811
5141
|
if not args.source_root and counts["effective_denominator_decision"]["reason"] not in ("reference-app",
|
|
4812
5142
|
"incomplete-run"):
|
|
4813
5143
|
counts["effective_denominator_decision"] = {"store": False, "reason": "no-source-tree"}
|
|
@@ -4838,7 +5168,9 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4838
5168
|
"counts": counts,
|
|
4839
5169
|
"entry_point_diff": {
|
|
4840
5170
|
"public": [row((family, name), file)
|
|
4841
|
-
for family, name, file in sorted(
|
|
5171
|
+
for family, name, file in sorted(public_keys, key=lambda k: (k[1], k[0], k[2] or ""))],
|
|
5172
|
+
"other_definitions": [row((family, name), file)
|
|
5173
|
+
for family, name, file in sorted(other_keys, key=lambda k: (k[1], k[2], k[0]))],
|
|
4842
5174
|
"internal": [row(key, found.get(key)) for key in by_name(internal - named)],
|
|
4843
5175
|
"extraction_gaps": [row(key, given[key]) for key in by_name(given) if key not in found],
|
|
4844
5176
|
},
|
|
@@ -5586,7 +5918,7 @@ def _py_defined_in(root: Path, tree: set[str]) -> Callable[[str, str], str | Non
|
|
|
5586
5918
|
|
|
5587
5919
|
def resolve(file: str, name: str) -> str | None:
|
|
5588
5920
|
if file not in traced:
|
|
5589
|
-
surface = _Surface()
|
|
5921
|
+
surface = _Surface("python")
|
|
5590
5922
|
_py_surface(root, [{"file": file}], tree, surface, [])
|
|
5591
5923
|
traced[file] = {n: info["file"] for n, info in surface.names.items()}
|
|
5592
5924
|
return traced[file].get(name)
|