bmad-module-skill-forge 4.0.0 → 6.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/agents.md +1 -1
- package/docs/architecture.md +5 -5
- package/docs/campaign.md +1 -1
- 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/troubleshooting.md +9 -3
- package/docs/verifying-a-skill.md +42 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +6 -6
- package/package.json +1 -1
- package/src/knowledge/tool-resolution.md +6 -6
- package/src/shared/references/headless-gate-convention.md +4 -1
- package/src/shared/references/output-contract-schema.md +3 -3
- package/src/shared/references/pipeline-contracts.md +2 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.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 +1180 -189
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- 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 +64 -20
- package/src/shared/scripts/skf-extract-public-api.py +472 -120
- 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 +136 -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 +574 -45
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +3 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/generate-briefs.md +1 -1
- package/src/skf-analyze-source/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/SKILL.md +4 -3
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +3 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +3 -2
- package/src/skf-campaign/references/campaign-contracts.md +2 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +8 -3
- package/src/skf-campaign/references/step-10-export.md +3 -2
- package/src/skf-create-skill/SKILL.md +4 -3
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/ccc-index-check.md +1 -1
- package/src/skf-create-skill/references/component-extraction.md +1 -1
- package/src/skf-create-skill/references/extract.md +3 -2
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +8 -3
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +3 -2
- package/src/skf-create-stack-skill/references/generate-output.md +1 -1
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
- package/src/skf-create-stack-skill/references/report.md +1 -1
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +3 -2
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +1 -1
- package/src/skf-export-skill/SKILL.md +3 -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/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +3 -2
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +4 -3
- package/src/skf-quick-skill/references/finalize.md +1 -1
- package/src/skf-quick-skill/references/halt-contract.md +2 -2
- package/src/skf-quick-skill/references/write-and-validate.md +1 -1
- package/src/skf-refine-architecture/SKILL.md +8 -9
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +3 -2
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +3 -2
- package/src/skf-setup/references/ccc-index.md +1 -1
- package/src/skf-setup/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/SKILL.md +3 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +11 -8
- package/src/skf-test-skill/references/coverage-check-branches.md +81 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +60 -97
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/inventory-contract.md +27 -0
- package/src/skf-test-skill/references/invocation-contract.md +3 -3
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +5 -5
- package/src/skf-test-skill/references/scoring-rules.md +8 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
- package/src/skf-test-skill/references/source-access-protocol.md +14 -10
- package/src/skf-test-skill/references/step-hard-gate.md +1 -0
- package/src/skf-test-skill/scripts/build-result-context.py +20 -3
- package/src/skf-test-skill/scripts/gap-ledger.py +456 -176
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +516 -79
- package/src/skf-test-skill/scripts/locate-export-segments.py +685 -76
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +1205 -65
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +350 -35
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +3 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +54 -33
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- 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 +6 -5
- package/src/skf-update-skill/references/write.md +42 -20
- package/src/skf-verify-stack/SKILL.md +4 -3
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -29,9 +29,53 @@ answer per input and judges no meaning. For each export name it lists:
|
|
|
29
29
|
declaration. Comments and string literals are blanked first in the
|
|
30
30
|
languages this script knows by the fence's info string; a block tagged
|
|
31
31
|
as data or output (`json`, `yaml`, `text`, `console` ...) holds none.
|
|
32
|
+
`receiver` is the expression before the name and its accessor
|
|
33
|
+
(`client.api` in `client.api.fetchData(`), null when none stands there,
|
|
34
|
+
as the blanked code holds it: whitespace beside an accessor, a bracket or
|
|
35
|
+
a comma dropped, any other run cut to one space, and a non-null `!!` or
|
|
36
|
+
`!` dropped.
|
|
37
|
+
|
|
38
|
+
A method given with its `owner` (the class or object the skill documents
|
|
39
|
+
it on) is called only on that owner. A receiver names an owner when its
|
|
40
|
+
last member, or the callee of its last call, is the owner's last part, in
|
|
41
|
+
any case (`client.messages` names `Messages`), and, when both have several
|
|
42
|
+
parts, the owner's parts are the receiver's last ones (`other_lib.agents`
|
|
43
|
+
does not name `cognee.agents`). A trailing `new` is dropped
|
|
44
|
+
(`UuidGenerator::new()`, Ruby's `UuidGenerator.new`), and so are an
|
|
45
|
+
owner's type parameters (`Repository` for `Repository<T>`). Each match of
|
|
46
|
+
the method's name is classed by its receiver (`receiverMatch`):
|
|
47
|
+
|
|
48
|
+
- "owner": the receiver names the owner (`cognee.agents.get(`,
|
|
49
|
+
`coco.App(...).update(`).
|
|
50
|
+
- "bound": the receiver is one identifier whose last binding on a line
|
|
51
|
+
before the call, in the same fenced block, is to the owner. Only a
|
|
52
|
+
constructor-like form binds: `x = Owner(...)`, `x = await Owner(...)`,
|
|
53
|
+
`x = new Owner(...)`, `x = Owner::new(...)`, `x = Owner.new`,
|
|
54
|
+
`x := Owner{`, an annotation `x: Owner`, `with Owner(...) as x`, a Java-
|
|
55
|
+
or C#-style `Owner x =` and Go's `var x Owner` or `var x *Owner`. Any
|
|
56
|
+
other assignment (`agent = await cognee.agents.create(n)`, the result of
|
|
57
|
+
a member call) binds the identifier to nothing, and an assignment in an
|
|
58
|
+
argument list (`f(gen=UuidGenerator())`) is no binding.
|
|
59
|
+
- "other": the receiver names, or its binding is to, another owner the
|
|
60
|
+
input lists. It is no call of the method, only counted
|
|
61
|
+
(`otherReceiverCount`).
|
|
62
|
+
- "unknown": no receiver (`get(...)`: an import or an implicit `this` may
|
|
63
|
+
tie it, the text does not), or any other one (`settings.get(`,
|
|
64
|
+
`self.gen.next_uuid(`). The call cannot be tied to the owner, so it goes
|
|
65
|
+
to `unverifiedCalls[]`, never to calls[].
|
|
66
|
+
|
|
67
|
+
A method no call is tied to is in `notCalled[]`, written `owner.name`, and
|
|
68
|
+
its unverified calls in `notCalledUnverified[]`. A declaration is an owned
|
|
69
|
+
method's only on its owner or a variable bound to it, inside the body of
|
|
70
|
+
its class (`class UuidGenerator:` ... `def next_uuid(self)`), or on a Go
|
|
71
|
+
receiver of its type (`func (g *UuidGenerator) next_uuid(`): neither
|
|
72
|
+
`cognee.config.get(key: str)` nor a free `def get(` is `agents.get`'s. Its
|
|
73
|
+
descriptions that name the owner come first, before the cap. A name given
|
|
74
|
+
without an owner is matched on any receiver, and its calls carry a null
|
|
75
|
+
`receiverMatch`.
|
|
32
76
|
|
|
33
77
|
A call is `awaited` when `await` stands before it (through its receiver:
|
|
34
|
-
`await client.fetchData(...)`), `.await` follows it (Rust), an asynchronous
|
|
78
|
+
`await client.fetchData(...)`), `.await` or `?.await` follows it (Rust), an asynchronous
|
|
35
79
|
loop or context manager consumes it (`async for row in stream(...)`, `async
|
|
36
80
|
with session(...)`, `for await (const row of stream(...))`), or it sits in
|
|
37
81
|
the arguments of a call that is itself awaited or is an event-loop runner
|
|
@@ -40,12 +84,41 @@ crossing into a function body. `chainedMember` names the member read or
|
|
|
40
84
|
called on its result right after it (`then`, `catch`, `join`, `get`), else
|
|
41
85
|
null: whether that waits for the result is the caller's call.
|
|
42
86
|
|
|
87
|
+
`awaitedVia` says how an awaited call is awaited, null when it is not:
|
|
88
|
+
|
|
89
|
+
- "direct": the await applies to the call itself (`directlyAwaited`):
|
|
90
|
+
`.await` follows it, or `await` stands before it with nothing applied to
|
|
91
|
+
its result (`await client.fetchData(...)`) but a member that waits for an
|
|
92
|
+
awaitable (`then`, `catch`, `finally`, `ConfigureAwait`), or it alone
|
|
93
|
+
fills the groups that `await` stands before (`await ((fetchData(x)))`,
|
|
94
|
+
`await (fetchData(x) as T)`) and nothing follows them but the end of the
|
|
95
|
+
statement, of a list or of a bracket.
|
|
96
|
+
- "chain": the await applies to an expression the call starts or sits in:
|
|
97
|
+
a member, subscript or call applied to its result
|
|
98
|
+
(`await use_context(e).embed(t)`, `await fetchData(x)[0]`) or a group
|
|
99
|
+
around it (`await (fetchData(x) or other())`).
|
|
100
|
+
- "consumer": an asynchronous loop or context manager consumes it.
|
|
101
|
+
- "argument" or "runner": it sits in the arguments of an awaited call, or of
|
|
102
|
+
an event-loop runner; `enclosingCallee` names that callee, whitespace
|
|
103
|
+
removed (`asyncio.gather`, `Promise.all`, `asyncio.run`), else null.
|
|
104
|
+
|
|
105
|
+
A backslash continuation counts as whitespace throughout.
|
|
106
|
+
|
|
43
107
|
Name match: the name as a fixed string, case-sensitive, never inside a
|
|
44
108
|
longer identifier (the Export match rule of skf-scan-skill-md-structure.py
|
|
45
109
|
usage-scope). A qualified name (`client.fetch`, `Store::load`) matches only
|
|
46
110
|
as written, except that a declaration also matches its last part
|
|
47
111
|
(`def fetch(`): a declaration never carries the qualifier.
|
|
48
112
|
|
|
113
|
+
Annotated parameters: in a block tagged Python, the JavaScript family,
|
|
114
|
+
Kotlin or Scala, an argument list that holds an annotated parameter
|
|
115
|
+
(`name: Type`; `name?: Type` in the JavaScript family; `*args: T`,
|
|
116
|
+
`...rest: T`) is a signature, with or without a receiver:
|
|
117
|
+
`get(key: str, reveal_secrets: bool = False)`. A lambda argument (`lambda
|
|
118
|
+
a, b: a + b`), a walrus (`x := 1`), a ternary (`dev ? 'x' : y`), `::`,
|
|
119
|
+
Kotlin's `object : T` and Scala's `xs: _*` and `_: T` stay calls. Swift, C# and Dart write named arguments that way
|
|
120
|
+
(`f(id: 1)`), so their blocks, and untagged ones, keep these calls.
|
|
121
|
+
|
|
49
122
|
Line model: UTF-8 with a leading byte order mark dropped, split on `\\n`
|
|
50
123
|
(a `\\r\\n` pair loses its `\\r`), so each `line` matches `grep -n`. The
|
|
51
124
|
frontmatter is a leading `---` ... `---` block. A fence opens on three or
|
|
@@ -57,13 +130,33 @@ indent); its section runs to the next heading of the same or a higher level.
|
|
|
57
130
|
Input (one JSON object):
|
|
58
131
|
{
|
|
59
132
|
"names": ["fetchData", "Client.fetch", ...], # the §1 inventory's function and method names
|
|
133
|
+
"methods": [{"name": "get", "owner": "agents"}], # optional: each method with its owner
|
|
134
|
+
"owners": ["agents", "config"], # optional: the classes and namespaces the skill documents
|
|
60
135
|
"skillPackagePath": "/path/to/skill" # holds SKILL.md and references/
|
|
61
136
|
}
|
|
62
137
|
|
|
138
|
+
Each method's owner is one of the owners. A `methods` item without a
|
|
139
|
+
non-empty string `name` and `owner` is skipped. An owner applies only to a
|
|
140
|
+
plain name: a qualified one (`agents.get`) is matched as written, as in
|
|
141
|
+
`names`.
|
|
142
|
+
|
|
63
143
|
or the files: --inventory <file> --skill-dir <dir>. --inventory reads the
|
|
64
|
-
validated inventory validate-inventory.py wrote (its --output)
|
|
65
|
-
|
|
66
|
-
|
|
144
|
+
validated inventory validate-inventory.py wrote (its --output), so no name
|
|
145
|
+
is copied by hand:
|
|
146
|
+
|
|
147
|
+
- names: each entry whose kind is `function`, or `method` with no
|
|
148
|
+
non-empty string `owner`.
|
|
149
|
+
- methods: each other `method` entry, with its `owner`, and each string
|
|
150
|
+
item of the `methods` list of a class, interface, struct, trait or type
|
|
151
|
+
entry, owned by that entry (no other kind's list is read). An item names
|
|
152
|
+
the identifier before its first `(` (the last part of a qualified one),
|
|
153
|
+
else its leading identifier, once its leading `.` or `#`, its decorators
|
|
154
|
+
(`@property`, `@lru_cache(maxsize=1)`), a Go receiver
|
|
155
|
+
(`func (c *Client)`) and its modifier words (`async`, `static`, `def`
|
|
156
|
+
...) are dropped. A dunder (`__init__`), an item that starts with
|
|
157
|
+
`await` and one of modifiers only (`async`) name no method.
|
|
158
|
+
- owners: the name of each class, struct, interface, trait, type, enum or
|
|
159
|
+
adapter entry, and each method's owner.
|
|
67
160
|
|
|
68
161
|
Output (stdout, one object):
|
|
69
162
|
{
|
|
@@ -74,13 +167,19 @@ Output (stdout, one object):
|
|
|
74
167
|
"name": "fetchData",
|
|
75
168
|
"descriptionCount": 3, # all found; descriptions[] holds the first 12
|
|
76
169
|
"descriptions": [{"file", "line", "endLine", "kind", "section", "header", "text", "truncated"}],
|
|
77
|
-
"
|
|
170
|
+
"owner": null, # a method's owner, null for a name
|
|
171
|
+
"calls": [{"file", "line", "fence", "language", "receiver", "receiverMatch", "awaited",
|
|
172
|
+
"directlyAwaited", "awaitedVia", "enclosingCallee", "chainedMember", "text"}],
|
|
78
173
|
"callCount": 2,
|
|
79
|
-
"awaitedCount": 1
|
|
174
|
+
"awaitedCount": 1, # calls whose `awaited` is true
|
|
175
|
+
"directlyAwaitedCount": 1, # calls whose `directlyAwaited` is true
|
|
176
|
+
"unverifiedCalls": [], # a method's calls on an "unknown" receiver, as calls[]
|
|
177
|
+
"otherReceiverCount": 0 # a method's matches on an "other" receiver
|
|
80
178
|
}
|
|
81
179
|
],
|
|
82
|
-
"notCalled": ["parseConfig"],
|
|
83
|
-
"
|
|
180
|
+
"notCalled": ["parseConfig", "agents.get"], # names, and owner.method, no example calls
|
|
181
|
+
"notCalledUnverified": [{"name": "get", "owner": "agents", "unverifiedCalls": [...]}], # their unverified calls
|
|
182
|
+
"summary": {"names": N, "called": N, "notCalled": N} # names: the names and methods, deduplicated
|
|
84
183
|
}
|
|
85
184
|
or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
|
|
86
185
|
|
|
@@ -102,6 +201,7 @@ Exit codes:
|
|
|
102
201
|
from __future__ import annotations
|
|
103
202
|
|
|
104
203
|
import argparse
|
|
204
|
+
import bisect
|
|
105
205
|
import json
|
|
106
206
|
import re
|
|
107
207
|
import sys
|
|
@@ -126,9 +226,20 @@ LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d{1,9}[.)])(?:[ \t]+|$)")
|
|
|
126
226
|
TABLE_ROW_RE = re.compile(r"^\s*\|")
|
|
127
227
|
TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$")
|
|
128
228
|
GENERIC_OR_PATH = r"(?:[ \t]*::)?(?:[ \t]*<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?"
|
|
129
|
-
|
|
229
|
+
# Whitespace, a Python backslash continuation included; LINE_SPACE stays on its line.
|
|
230
|
+
SPACE = r"(?:\s|\\\n)*"
|
|
231
|
+
LINE_SPACE = r"(?:[ \t]|\\\n)*"
|
|
232
|
+
AFTER_CALL_RE = re.compile(f"{SPACE}[?!]?\\.{SPACE}((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)")
|
|
130
233
|
AWAIT_BEFORE_RE = re.compile(f"(?<![{IDENT_CHARS}])await$")
|
|
131
|
-
AWAIT_AFTER_RE = re.compile(f"
|
|
234
|
+
AWAIT_AFTER_RE = re.compile(f"{SPACE}\\??\\.{SPACE}await(?![{IDENT_CHARS}])")
|
|
235
|
+
# A subscript or call applied to a call's result: `f(x)[0]`, `f(x)(y)`, `f(x)?.[0]`, `f(x)!(y)`.
|
|
236
|
+
APPLIED_RE = re.compile(f"{LINE_SPACE}(?:\\?\\.|!)?[\\[(]")
|
|
237
|
+
# What may close a group that holds only the call (`(f(x) as T)`, `(f(x)!)`), and what may follow
|
|
238
|
+
# the outermost group: the end of the statement, of a list or of a bracket.
|
|
239
|
+
GROUP_TAIL_RE = re.compile(f"{SPACE}(?:!|as(?![{IDENT_CHARS}])[^()]*)?{SPACE}")
|
|
240
|
+
GROUP_END_RE = re.compile(f"{LINE_SPACE}(?:[;,)\\]}}]|\\Z|\\n\\s*(?:[^\\s.?\\[(]|\\Z))")
|
|
241
|
+
# Members whose call waits for an awaitable: under `await`, the export returns one.
|
|
242
|
+
WAIT_MEMBERS = frozenset({"then", "catch", "finally", "ConfigureAwait"})
|
|
132
243
|
# An expression an asynchronous loop or context manager consumes: `async for
|
|
133
244
|
# x in NAME(...)`, `async with NAME(...)`, `for await (const x of NAME(...))`,
|
|
134
245
|
# `await foreach (var x in NAME(...))`. Matched against its line up to it.
|
|
@@ -169,6 +280,21 @@ TYPED_DECLARATIONS = C_FAMILY | frozenset({"groovy", ""})
|
|
|
169
280
|
# A `{` after a call at the start of a line opens a method body here; in
|
|
170
281
|
# Kotlin, Swift or Ruby it can be a trailing closure of a call.
|
|
171
282
|
BRACE_DECLARATIONS = JS_FAMILY | C_FAMILY
|
|
283
|
+
# Languages where no call's argument is `name: ...`, so an argument list that
|
|
284
|
+
# holds one declares the name. Swift, C# and Dart write named arguments that
|
|
285
|
+
# way, and untagged code can be any of them.
|
|
286
|
+
ANNOTATED_PARAMS = JS_FAMILY | frozenset({"python", "py", "python3", "kotlin", "kt", "kts", "scala"})
|
|
287
|
+
# An annotated parameter: `name: Type`, `*args: T`, `...rest: T`, and `name?: T` (group 2, the `?` right
|
|
288
|
+
# before the `:`) in the JavaScript family, where no `?:` operator exists. Not `::`, nor a walrus `:=`, nor a
|
|
289
|
+
# ternary (`dev ? 'x' : y`).
|
|
290
|
+
ANNOTATED_PARAM_RE = re.compile(
|
|
291
|
+
f"\\s*(?:\\*{{1,2}}|\\.\\.\\.)?((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)(?:(\\?)|\\s*):(?![:=])(.*)", re.S)
|
|
292
|
+
# A Python lambda's parameters, which hold commas: `lambda a, b:`.
|
|
293
|
+
LAMBDA_PARAMS_RE = re.compile(f"(?<![{IDENT_CHARS}])lambda(?![{IDENT_CHARS}])[^:]*:")
|
|
294
|
+
PYTHON = frozenset({"python", "py", "python3"})
|
|
295
|
+
# The accessors between a receiver and the member it holds, longest first.
|
|
296
|
+
ACCESSORS = ("?.", "!.", "::", "->", ".")
|
|
297
|
+
IDENTIFIER_RE = re.compile(f"(?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*")
|
|
172
298
|
|
|
173
299
|
# A word right before the name that declares it.
|
|
174
300
|
DECLARATION_WORDS = frozenset({
|
|
@@ -192,6 +318,36 @@ RUN_OWNERS = frozenset({"asyncio", "trio", "anyio"})
|
|
|
192
318
|
BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
193
319
|
CLOSERS = {v: k for k, v in BRACKETS.items()}
|
|
194
320
|
|
|
321
|
+
# Receivers and the bindings that tie them to an owner.
|
|
322
|
+
IDENT = f"(?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*"
|
|
323
|
+
TYPE_PATH = f"{IDENT}(?:(?:\\.|::){IDENT})*"
|
|
324
|
+
GENERIC = r"(?:[ \t]*<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?"
|
|
325
|
+
SEGMENT_HEAD_RE = re.compile(f"({IDENT}){GENERIC}")
|
|
326
|
+
# `x = ...` or `x := ...`, not `==`, `=>` or `=~`.
|
|
327
|
+
ASSIGN_RE = re.compile(f"(?<![{IDENT_CHARS}.])({IDENT})[ \\t]*(?::=|=(?![=>~]))\\s*")
|
|
328
|
+
RHS_RE = re.compile(f"(?:await\\s+)?(new\\s+)?(?:&[ \\t]*(?:mut\\s+)?)?({TYPE_PATH}){GENERIC}[ \\t]*([({{]?)")
|
|
329
|
+
# A member, subscript or call applied to a new object (`.await` is no member).
|
|
330
|
+
AFTER_CONSTRUCTOR_RE = re.compile(f"[ \\t]*(?:\\?\\.|!\\.|\\.(?!await(?![{IDENT_CHARS}]))|::|->|\\[|\\()")
|
|
331
|
+
ANNOTATION_RE = re.compile(f"(?<![{IDENT_CHARS}.])({IDENT})\\??[ \\t]*:[ \\t]*(?:&[ \\t]*(?:mut[ \\t]+)?|\\*)?"
|
|
332
|
+
f"({TYPE_PATH})(?![{IDENT_CHARS}(.:])")
|
|
333
|
+
TYPED_BINDING_RE = re.compile(f"(?<![{IDENT_CHARS}.])({TYPE_PATH}){GENERIC}(?:\\[\\])*[ \\t]+({IDENT})[ \\t]*"
|
|
334
|
+
f"(?:=(?![=>~])|;)")
|
|
335
|
+
GO_VAR_RE = re.compile(f"(?<![{IDENT_CHARS}.])var[ \\t]+({IDENT})[ \\t]+\\*?({TYPE_PATH})(?![{IDENT_CHARS}(.:])")
|
|
336
|
+
WITH_AS_RE = re.compile(f"\\)[ \\t]*as[ \\t]+({IDENT})(?![{IDENT_CHARS}])")
|
|
337
|
+
CALLEE_END_RE = re.compile(f"({TYPE_PATH}){GENERIC}[ \\t]*$")
|
|
338
|
+
# Words that stand where TYPED_BINDING_RE reads a type: `const x = ...`, `return x;`.
|
|
339
|
+
BINDING_WORDS = DECLARATION_WORDS | EXPRESSION_WORDS | frozenset({
|
|
340
|
+
"var", "let", "const", "val", "auto", "mut", "dim", "my", "local", "global", "nonlocal",
|
|
341
|
+
})
|
|
342
|
+
# A class-like body: `class Agents`, `impl Store`, `impl<T> Trait for Store<T>`, Go's `type Client struct`.
|
|
343
|
+
CLASS_RE = re.compile(
|
|
344
|
+
f"(?<![{IDENT_CHARS}.])(?:(?:class|struct|interface|trait|object|protocol|record|enum|extension)[ \\t]+"
|
|
345
|
+
f"|impl{GENERIC}[ \\t]+(?:{TYPE_PATH}{GENERIC}[ \\t]+for[ \\t]+)?)({TYPE_PATH})"
|
|
346
|
+
f"|(?<![{IDENT_CHARS}.])type[ \\t]+({IDENT})[ \\t]+(?:struct|interface)(?![{IDENT_CHARS}])")
|
|
347
|
+
BRACE_BODY_RE = re.compile(r"[^{};\n]*(?:\n[ \t]*)?\{")
|
|
348
|
+
GO_RECEIVER_TAIL_RE = re.compile(
|
|
349
|
+
f"func[ \\t]*\\([ \\t]*(?:{IDENT}[ \\t]+)?\\*?({TYPE_PATH}){GENERIC}(?:\\[[^\\]]*\\])?[ \\t]*\\)[ \\t]*$")
|
|
350
|
+
|
|
195
351
|
|
|
196
352
|
def make_error(message):
|
|
197
353
|
return {"error": message, "code": "INVALID_INPUT"}
|
|
@@ -515,7 +671,8 @@ def _matching_open(text, close_idx):
|
|
|
515
671
|
|
|
516
672
|
|
|
517
673
|
def _skip_space_back(text, i):
|
|
518
|
-
|
|
674
|
+
"""Back over whitespace, a Python backslash continuation included."""
|
|
675
|
+
while i > 0 and (text[i - 1] in " \t\n" or (text[i - 1] == "\\" and text.startswith("\n", i))):
|
|
519
676
|
i -= 1
|
|
520
677
|
return i
|
|
521
678
|
|
|
@@ -529,6 +686,8 @@ def _chain_start(text, pos):
|
|
|
529
686
|
if accessor is None:
|
|
530
687
|
return i
|
|
531
688
|
k = _skip_space_back(text, k - len(accessor))
|
|
689
|
+
while k > 0 and text[k - 1] == "!":
|
|
690
|
+
k -= 1 # Kotlin's `!!` before the accessor: `gen!!.next_uuid()`
|
|
532
691
|
start = k
|
|
533
692
|
while start > 0 and text[start - 1] in ")]":
|
|
534
693
|
opened = _matching_open(text, start - 1)
|
|
@@ -543,14 +702,253 @@ def _chain_start(text, pos):
|
|
|
543
702
|
i = start
|
|
544
703
|
|
|
545
704
|
|
|
705
|
+
def _receiver(text, chain, name_start):
|
|
706
|
+
"""The expression before the member at `name_start`, without its accessor: `cognee.agents` in
|
|
707
|
+
`cognee.agents.get(`; None when the name has no receiver. Whitespace (a line break in a fluent chain, a
|
|
708
|
+
blanked string) is dropped beside an accessor, a bracket or a comma, and cut to one space elsewhere."""
|
|
709
|
+
k = _skip_space_back(text, name_start)
|
|
710
|
+
accessor = next((a for a in ACCESSORS if text.endswith(a, chain, k)), None)
|
|
711
|
+
if accessor is None:
|
|
712
|
+
return None
|
|
713
|
+
receiver = re.sub(r"(?:\s|\\\n)+", " ", text[chain:k - len(accessor)]).strip()
|
|
714
|
+
receiver = re.sub(r" ?(\?\.|!\.|::|->|[.,()\[\]]) ?", r"\1", receiver)
|
|
715
|
+
receiver = re.sub(r"!+(?=\?\.|\.|::|->|$)", "", receiver) # a non-null assertion: `gen!!`, `client!`
|
|
716
|
+
return receiver or None
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _owner_display(owner):
|
|
720
|
+
"""An owner as the output names it: its type parameters dropped (`Repository` for `Repository<T>`)."""
|
|
721
|
+
owner = owner.strip()
|
|
722
|
+
while (bare := re.sub(r"<[^<>]*>", "", owner)) != owner:
|
|
723
|
+
owner = bare
|
|
724
|
+
return re.sub(r"\[[^\[\]]*\]$", "", owner).strip()
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _type_parts(path):
|
|
728
|
+
"""The parts of a dotted or `::` path, lower case, a trailing `new` dropped (`Owner::new`, Ruby's
|
|
729
|
+
`Owner.new`); None for an empty path."""
|
|
730
|
+
parts = tuple(p.lower() for p in re.split(r"\.|::|#|->", path) if p)
|
|
731
|
+
if len(parts) > 1 and parts[-1] == "new":
|
|
732
|
+
parts = parts[:-1]
|
|
733
|
+
return parts or None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _segment_name(segment):
|
|
737
|
+
"""The identifier of one member of a receiver, plain or called once (`App` in `App(x)`), else None."""
|
|
738
|
+
m = SEGMENT_HEAD_RE.match(segment)
|
|
739
|
+
if not m:
|
|
740
|
+
return None
|
|
741
|
+
rest = segment[m.end():]
|
|
742
|
+
if not rest or (rest.startswith("(") and _matching_close(rest, 0) == len(rest) - 1):
|
|
743
|
+
return m.group(1).lower()
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _receiver_parts(receiver):
|
|
748
|
+
"""The member names of a receiver, lower case, None for a member that is no plain or once-called name:
|
|
749
|
+
("coco", "app") for `coco.App(...)`, (None, "agents") for `get()[0].agents`; a trailing `new` dropped."""
|
|
750
|
+
segments, depth, start, i = [], 0, 0, 0
|
|
751
|
+
while i < len(receiver):
|
|
752
|
+
c = receiver[i]
|
|
753
|
+
if c in BRACKETS:
|
|
754
|
+
depth += 1
|
|
755
|
+
elif c in CLOSERS:
|
|
756
|
+
depth -= 1
|
|
757
|
+
elif depth == 0 and (accessor := next((a for a in ACCESSORS if receiver.startswith(a, i)), None)):
|
|
758
|
+
segments.append(receiver[start:i])
|
|
759
|
+
i += len(accessor)
|
|
760
|
+
start = i
|
|
761
|
+
continue
|
|
762
|
+
i += 1
|
|
763
|
+
segments.append(receiver[start:])
|
|
764
|
+
parts = tuple(_segment_name(s) for s in segments)
|
|
765
|
+
return parts[:-1] if len(parts) > 1 and parts[-1] == "new" else parts
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _ties(parts, owner_parts):
|
|
769
|
+
"""True when a receiver or a bound type names the owner: the last parts agree, case-insensitively, and when
|
|
770
|
+
both have several parts the owner's are the receiver's last ones (`other_lib.agents` is not `cognee.agents`)."""
|
|
771
|
+
if not parts or parts[-1] is None or parts[-1] != owner_parts[-1]:
|
|
772
|
+
return False
|
|
773
|
+
if len(parts) > 1 and len(owner_parts) > 1:
|
|
774
|
+
return parts[-len(owner_parts):] == owner_parts
|
|
775
|
+
return True
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class _Owners:
|
|
779
|
+
"""Every owner of the input, looked up by its last part."""
|
|
780
|
+
|
|
781
|
+
def __init__(self, owners):
|
|
782
|
+
self.parts = {o: p for o in owners if (p := _type_parts(o))}
|
|
783
|
+
self.by_last = {}
|
|
784
|
+
for owner, parts in self.parts.items():
|
|
785
|
+
self.by_last.setdefault(parts[-1], []).append(owner)
|
|
786
|
+
|
|
787
|
+
def tied(self, parts):
|
|
788
|
+
"""The owners `parts` names."""
|
|
789
|
+
if not parts or parts[-1] is None:
|
|
790
|
+
return frozenset()
|
|
791
|
+
return frozenset(o for o in self.by_last.get(parts[-1], ()) if _ties(parts, self.parts[o]))
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _constructed(text, pos):
|
|
795
|
+
"""The parts of the type a constructor-like right-hand side at `pos` builds: `Owner(...)`, `await Owner(...)`,
|
|
796
|
+
`new Owner(...)`, `Owner::new(...)`, `Owner.new`, `Owner{`; None for anything else, a member call's result
|
|
797
|
+
(`cognee.agents.create(n)` builds a `create`, never an `agents`) or a member of the new object included."""
|
|
798
|
+
m = RHS_RE.match(text, pos)
|
|
799
|
+
if not m:
|
|
800
|
+
return None
|
|
801
|
+
new, path, follow = m.group(1), m.group(2), m.group(3)
|
|
802
|
+
if follow == "(":
|
|
803
|
+
close = _matching_close(text, m.end() - 1)
|
|
804
|
+
if close is not None and AFTER_CONSTRUCTOR_RE.match(text, close + 1):
|
|
805
|
+
return None # `Owner().build()`: the object a member returns
|
|
806
|
+
return _type_parts(path)
|
|
807
|
+
if follow == "{" or new:
|
|
808
|
+
return _type_parts(path)
|
|
809
|
+
raw = re.split(r"\.|::", path)
|
|
810
|
+
return _type_parts(path) if len(raw) > 1 and raw[-1] == "new" else None
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _bindings(text):
|
|
814
|
+
"""{identifier: ([row, ...], [type parts or None, ...])} of a block, in text order: each line that binds an
|
|
815
|
+
identifier to a type, and each plain assignment (to None, so a later `x = make()` ends an earlier binding).
|
|
816
|
+
|
|
817
|
+
A binding is `x = Owner(...)` (see _constructed), an annotation `x: Owner`, `with Owner(...) as x`, a
|
|
818
|
+
Java- or C#-style `Owner x =` and Go's `var x Owner` or `var x *Owner`. An assignment inside an argument
|
|
819
|
+
list (`f(gen=UuidGenerator())`) binds nothing.
|
|
820
|
+
"""
|
|
821
|
+
found = {}
|
|
822
|
+
for m in ASSIGN_RE.finditer(text):
|
|
823
|
+
before = _skip_space_back(text, m.start())
|
|
824
|
+
if before > 0 and text[before - 1] in "(,":
|
|
825
|
+
continue # a keyword argument or a parameter default
|
|
826
|
+
found[m.start(1)] = (m.group(1), _constructed(text, m.end()))
|
|
827
|
+
for m in TYPED_BINDING_RE.finditer(text):
|
|
828
|
+
if m.group(1) not in BINDING_WORDS:
|
|
829
|
+
found[m.start(2)] = (m.group(2), _type_parts(m.group(1)))
|
|
830
|
+
for regex in (ANNOTATION_RE, GO_VAR_RE):
|
|
831
|
+
for m in regex.finditer(text):
|
|
832
|
+
found[m.start(1)] = (m.group(1), _type_parts(m.group(2)))
|
|
833
|
+
for m in WITH_AS_RE.finditer(text):
|
|
834
|
+
opened = _matching_open(text, m.start())
|
|
835
|
+
callee = CALLEE_END_RE.search(text, max(0, opened - 300), opened) if opened is not None else None
|
|
836
|
+
if callee:
|
|
837
|
+
found[m.start(1)] = (m.group(1), _type_parts(callee.group(1)))
|
|
838
|
+
starts = [0] + [i + 1 for i, c in enumerate(text) if c == "\n"]
|
|
839
|
+
by_name = {}
|
|
840
|
+
for pos in sorted(found):
|
|
841
|
+
name, parts = found[pos]
|
|
842
|
+
rows, types = by_name.setdefault(name, ([], []))
|
|
843
|
+
rows.append(bisect.bisect_right(starts, pos) - 1)
|
|
844
|
+
types.append(parts)
|
|
845
|
+
return by_name
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _class_spans(text):
|
|
849
|
+
"""(start, end, class parts) of each class-like body in a block: braces, else indentation (Python, Ruby)."""
|
|
850
|
+
spans = []
|
|
851
|
+
for m in CLASS_RE.finditer(text):
|
|
852
|
+
name = m.group(1) or m.group(2)
|
|
853
|
+
brace = BRACE_BODY_RE.match(text, m.end())
|
|
854
|
+
if brace:
|
|
855
|
+
close = _matching_close(text, brace.end() - 1)
|
|
856
|
+
spans.append((brace.end() - 1, len(text) if close is None else close, _type_parts(name)))
|
|
857
|
+
continue
|
|
858
|
+
line_start = text.rfind("\n", 0, m.start()) + 1
|
|
859
|
+
indent = len(text[line_start:m.start()]) - len(text[line_start:m.start()].lstrip(" \t"))
|
|
860
|
+
end, pos = len(text), text.find("\n", m.end())
|
|
861
|
+
while pos != -1:
|
|
862
|
+
nxt = text.find("\n", pos + 1)
|
|
863
|
+
line = text[pos + 1:nxt if nxt != -1 else len(text)]
|
|
864
|
+
if line.strip() and len(line) - len(line.lstrip(" \t")) <= indent:
|
|
865
|
+
end = pos + 1
|
|
866
|
+
break
|
|
867
|
+
pos = nxt
|
|
868
|
+
spans.append((m.end(), end, _type_parts(name)))
|
|
869
|
+
return spans
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
class _Block:
|
|
873
|
+
"""One fenced block's receivers, read once: its bindings and class bodies, built on first use."""
|
|
874
|
+
|
|
875
|
+
def __init__(self, text, owners):
|
|
876
|
+
self.text, self.owners = text, owners
|
|
877
|
+
self._bindings = self._spans = None
|
|
878
|
+
|
|
879
|
+
def ties(self, match):
|
|
880
|
+
"""(via a binding, the owners the receiver of `match` names), cached on the match."""
|
|
881
|
+
if "ties" not in match:
|
|
882
|
+
receiver, ties = match["receiver"], (False, frozenset())
|
|
883
|
+
if receiver is not None:
|
|
884
|
+
rows, types, last = (), (), -1
|
|
885
|
+
if IDENTIFIER_RE.fullmatch(receiver):
|
|
886
|
+
if self._bindings is None:
|
|
887
|
+
self._bindings = _bindings(self.text)
|
|
888
|
+
rows, types = self._bindings.get(receiver, ((), ()))
|
|
889
|
+
last = bisect.bisect_left(rows, match["row"]) - 1 # the last binding on a line before the call
|
|
890
|
+
if last >= 0:
|
|
891
|
+
ties = (True, self.owners.tied(types[last]))
|
|
892
|
+
else:
|
|
893
|
+
ties = (False, self.owners.tied(_receiver_parts(receiver)))
|
|
894
|
+
match["ties"] = ties
|
|
895
|
+
return match["ties"]
|
|
896
|
+
|
|
897
|
+
def receiver_match(self, match, owner):
|
|
898
|
+
""""owner", "bound", "other" or "unknown": how the receiver of `match` ties it to `owner`."""
|
|
899
|
+
if match["receiver"] is None:
|
|
900
|
+
return "unknown" # a bare call: an import or an implicit `this` may tie it, the text does not
|
|
901
|
+
via_binding, tied = self.ties(match)
|
|
902
|
+
if owner in tied:
|
|
903
|
+
return "bound" if via_binding else "owner"
|
|
904
|
+
return "other" if tied else "unknown"
|
|
905
|
+
|
|
906
|
+
def declares(self, match, owner, kind):
|
|
907
|
+
"""True when the declaration at `match` is `owner`'s: on its receiver, in its class body, or on a Go
|
|
908
|
+
receiver of its type (`func (g *UuidGenerator) next_uuid(`)."""
|
|
909
|
+
if match["receiver"] is not None:
|
|
910
|
+
return kind in ("owner", "bound")
|
|
911
|
+
owner_parts = self.owners.parts.get(owner)
|
|
912
|
+
go = GO_RECEIVER_TAIL_RE.search(self.text, match["line_start"], match["chain"])
|
|
913
|
+
if go:
|
|
914
|
+
return _ties(_type_parts(go.group(1)), owner_parts)
|
|
915
|
+
if self._spans is None:
|
|
916
|
+
self._spans = _class_spans(self.text)
|
|
917
|
+
inside = [s for s in self._spans if s[0] <= match["name_start"] < s[1]]
|
|
918
|
+
return bool(inside) and _ties(max(inside, key=lambda s: s[0])[2], owner_parts) # the innermost body
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _await_before(text, start):
|
|
922
|
+
"""True when `await` stands right before the expression that starts at `start`."""
|
|
923
|
+
return bool(AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)))
|
|
924
|
+
|
|
925
|
+
|
|
546
926
|
def _awaited_at(text, start):
|
|
547
927
|
"""True when `await` stands right before the expression that starts at `start`, or an
|
|
548
928
|
asynchronous loop or context manager consumes it."""
|
|
549
|
-
if
|
|
929
|
+
if _await_before(text, start):
|
|
550
930
|
return True
|
|
551
931
|
return bool(ASYNC_CONSUMER_RE.search(text[text.rfind("\n", 0, start) + 1:start]))
|
|
552
932
|
|
|
553
933
|
|
|
934
|
+
def _grouped_await(text, chain, close):
|
|
935
|
+
"""True for `await (call(...))` and `await ((call(...) as T))`: the call alone fills each group
|
|
936
|
+
around it (a trailing `as T` or `!` allowed), `await` stands before the outermost one, and
|
|
937
|
+
nothing but the end of the statement, of a list or of a bracket follows it."""
|
|
938
|
+
start, end = chain, close
|
|
939
|
+
while end is not None:
|
|
940
|
+
group = _skip_space_back(text, start) - 1
|
|
941
|
+
if group < 0 or text[group] != "(":
|
|
942
|
+
return False
|
|
943
|
+
end_group = _matching_close(text, group)
|
|
944
|
+
if end_group is None or not GROUP_TAIL_RE.fullmatch(text, end + 1, end_group):
|
|
945
|
+
return False
|
|
946
|
+
if _await_before(text, group):
|
|
947
|
+
return bool(GROUP_END_RE.match(text, end_group + 1))
|
|
948
|
+
start, end = group, end_group
|
|
949
|
+
return False
|
|
950
|
+
|
|
951
|
+
|
|
554
952
|
def _is_runner(callee):
|
|
555
953
|
"""True for `asyncio.run`, `loop.run_until_complete`, `rt.block_on` and the like."""
|
|
556
954
|
parts = [p for p in re.split(r"\.|::", callee) if p]
|
|
@@ -568,7 +966,12 @@ def _opens_body(text, brace_idx):
|
|
|
568
966
|
|
|
569
967
|
|
|
570
968
|
def _enclosing_awaits(text, start):
|
|
571
|
-
"""
|
|
969
|
+
"""How a call or group around the expression at `start` awaits it, within one function body.
|
|
970
|
+
|
|
971
|
+
("runner", callee) for a runner it is an argument of, ("argument", callee) for an awaited or
|
|
972
|
+
consumed call it is an argument of, ("chain", None) for an awaited group around it and
|
|
973
|
+
("consumer", None) for a consumed one; None when nothing around it awaits it.
|
|
974
|
+
"""
|
|
572
975
|
i, depth = start, 0
|
|
573
976
|
while i > 0:
|
|
574
977
|
i -= 1
|
|
@@ -583,17 +986,39 @@ def _enclosing_awaits(text, start):
|
|
|
583
986
|
elif c == "(":
|
|
584
987
|
k = _skip_space_back(text, i)
|
|
585
988
|
word = WORD_BEFORE_RE.search(text, 0, k)
|
|
989
|
+
callee = None
|
|
586
990
|
if word and word.group(1) not in EXPRESSION_WORDS:
|
|
587
991
|
expr = _chain_start(text, word.start())
|
|
588
|
-
|
|
589
|
-
|
|
992
|
+
callee = re.sub(r"(?:\s|\\\n)+", "", text[expr:k])
|
|
993
|
+
if _is_runner(callee):
|
|
994
|
+
return "runner", callee
|
|
590
995
|
else:
|
|
591
996
|
expr = i # a grouping `(`: `await (fetch())`
|
|
592
997
|
close = _matching_close(text, i)
|
|
593
|
-
|
|
594
|
-
|
|
998
|
+
awaits = _await_before(text, expr) or (close is not None and AWAIT_AFTER_RE.match(text, close + 1))
|
|
999
|
+
if awaits or _awaited_at(text, expr):
|
|
1000
|
+
return ("argument", callee) if callee else ("chain" if awaits else "consumer", None)
|
|
595
1001
|
i = expr
|
|
596
|
-
return
|
|
1002
|
+
return None
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _how_awaited(text, chain, close):
|
|
1006
|
+
"""(awaitedVia, enclosingCallee, chainedMember) of the call whose expression starts at `chain`
|
|
1007
|
+
and whose argument list closes at `close`."""
|
|
1008
|
+
before = _await_before(text, chain)
|
|
1009
|
+
member = AFTER_CALL_RE.match(text, close + 1) if close is not None else None
|
|
1010
|
+
chained = member.group(1) if member else None
|
|
1011
|
+
if close is not None and AWAIT_AFTER_RE.match(text, close + 1):
|
|
1012
|
+
return "direct", None, None
|
|
1013
|
+
if member or (close is not None and APPLIED_RE.match(text, close + 1)):
|
|
1014
|
+
if before:
|
|
1015
|
+
return ("direct" if chained in WAIT_MEMBERS else "chain"), None, chained
|
|
1016
|
+
elif before or _grouped_await(text, chain, close):
|
|
1017
|
+
return "direct", None, None
|
|
1018
|
+
if _awaited_at(text, chain):
|
|
1019
|
+
return "consumer", None, chained
|
|
1020
|
+
via, callee = _enclosing_awaits(text, chain) or (None, None)
|
|
1021
|
+
return via, callee, chained
|
|
597
1022
|
|
|
598
1023
|
|
|
599
1024
|
def _closes_generic(tail):
|
|
@@ -612,8 +1037,41 @@ def _closes_generic(tail):
|
|
|
612
1037
|
return False
|
|
613
1038
|
|
|
614
1039
|
|
|
615
|
-
def
|
|
1040
|
+
def _top_level_args(args):
|
|
1041
|
+
"""The arguments of an argument list's text, split at the commas outside any bracket."""
|
|
1042
|
+
parts, depth, start = [], 0, 0
|
|
1043
|
+
for i, c in enumerate(args):
|
|
1044
|
+
if c in BRACKETS:
|
|
1045
|
+
depth += 1
|
|
1046
|
+
elif c in CLOSERS:
|
|
1047
|
+
depth -= 1
|
|
1048
|
+
elif c == "," and depth == 0:
|
|
1049
|
+
parts.append(args[start:i])
|
|
1050
|
+
start = i + 1
|
|
1051
|
+
parts.append(args[start:])
|
|
1052
|
+
return parts
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _annotated(args, language):
|
|
1056
|
+
"""True when an argument is an annotated parameter (`key: str`): no call's argument is, in `language`."""
|
|
1057
|
+
if language in PYTHON:
|
|
1058
|
+
args = LAMBDA_PARAMS_RE.sub("lambda:", args) # `key=lambda k, v: v` is one argument
|
|
1059
|
+
for arg in _top_level_args(args):
|
|
1060
|
+
m = ANNOTATED_PARAM_RE.match(arg)
|
|
1061
|
+
if not m or m.group(1) == "lambda" or (m.group(2) and language not in JS_FAMILY):
|
|
1062
|
+
continue
|
|
1063
|
+
if language in ("kotlin", "kt", "kts") and m.group(1) == "object":
|
|
1064
|
+
continue # Kotlin's anonymous object argument: `f(object : Listener { ... })`
|
|
1065
|
+
if language == "scala" and (m.group(1) == "_" or m.group(3).strip() == "_*"):
|
|
1066
|
+
continue # Scala's typed placeholder `f(_: Int)`, and `f(xs: _*)` passing a sequence as varargs
|
|
1067
|
+
return True
|
|
1068
|
+
return False
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _is_declaration(text, line_start, chain, name_start, paren, close, language):
|
|
616
1072
|
"""True when the name at name_start is declared there, not called."""
|
|
1073
|
+
if close is not None and language in ANNOTATED_PARAMS and _annotated(text[paren + 1:close], language):
|
|
1074
|
+
return True # a signature listing: `get(key: str, reveal_secrets: bool = False)`
|
|
617
1075
|
tail = text[line_start:chain].rstrip(" \t")
|
|
618
1076
|
if chain == name_start and tail:
|
|
619
1077
|
word = WORD_BEFORE_RE.search(tail)
|
|
@@ -641,46 +1099,81 @@ def _is_declaration(text, line_start, chain, name_start, close, language):
|
|
|
641
1099
|
return False
|
|
642
1100
|
|
|
643
1101
|
|
|
644
|
-
def
|
|
645
|
-
"""
|
|
1102
|
+
def _matches(text, call_re, declaration_res, language):
|
|
1103
|
+
"""Each match of a name in a block, read once: where it stands, its receiver, and whether it declares the
|
|
1104
|
+
name (`declaration`) or is a call (`call`: the last part of a qualified name counts only as a declaration)."""
|
|
1105
|
+
seen, found = set(), []
|
|
1106
|
+
for regex in (call_re, *declaration_res):
|
|
1107
|
+
for m in regex.finditer(text):
|
|
1108
|
+
name_start, paren = m.start(), m.end() - 1
|
|
1109
|
+
if name_start in seen:
|
|
1110
|
+
continue
|
|
1111
|
+
seen.add(name_start)
|
|
1112
|
+
close = _matching_close(text, paren)
|
|
1113
|
+
chain = _chain_start(text, name_start)
|
|
1114
|
+
line_start = text.rfind("\n", 0, name_start) + 1
|
|
1115
|
+
declaration = _is_declaration(text, line_start, chain, name_start, paren, close, language)
|
|
1116
|
+
if declaration or regex is call_re:
|
|
1117
|
+
found.append({"name_start": name_start, "close": close, "chain": chain, "line_start": line_start,
|
|
1118
|
+
"row": text.count("\n", 0, name_start), "receiver": _receiver(text, chain, name_start),
|
|
1119
|
+
"declaration": declaration})
|
|
1120
|
+
return found
|
|
646
1121
|
|
|
647
|
-
|
|
648
|
-
|
|
1122
|
+
|
|
1123
|
+
def scan_block(rel, lines, opening, closing, info, names, keys_by_name, owners):
|
|
1124
|
+
"""{key: {"calls": [...], "unverified": [...], "other": N, "signatures": [...]}} for one fenced block.
|
|
1125
|
+
|
|
1126
|
+
`names` maps each name to (its call regex, the regexes of the other forms
|
|
1127
|
+
a declaration of it may take), `keys_by_name` each name to its keys,
|
|
1128
|
+
(owner or None, name), and `owners` indexes every owner. A key without an
|
|
1129
|
+
owner takes every call and declaration. An owned key takes the calls on
|
|
1130
|
+
its owner or on a variable the block binds to it, lists those on an
|
|
1131
|
+
"unknown" receiver in `unverified`, counts those on an "other" one, and
|
|
1132
|
+
takes a declaration on its owner, in its class body or on a Go receiver
|
|
1133
|
+
of its type.
|
|
649
1134
|
"""
|
|
650
1135
|
language = _language(info)
|
|
651
|
-
found = {
|
|
1136
|
+
found = {key: {"calls": [], "unverified": [], "other": 0, "signatures": []}
|
|
1137
|
+
for keys in keys_by_name.values() for key in keys}
|
|
652
1138
|
if language in DATA_LANGUAGES:
|
|
653
1139
|
return found
|
|
654
1140
|
body = lines[opening + 1:closing]
|
|
655
1141
|
text = mask_code("\n".join(body), language)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1142
|
+
block = _Block(text, owners)
|
|
1143
|
+
|
|
1144
|
+
def call(match, receiver_match):
|
|
1145
|
+
if "await" not in match:
|
|
1146
|
+
match["await"] = _how_awaited(text, match["chain"], match["close"])
|
|
1147
|
+
via, callee, chained = match["await"]
|
|
1148
|
+
row = match["row"]
|
|
1149
|
+
return {
|
|
1150
|
+
"file": rel, "line": opening + row + 2, "fence": opening + 1, "language": language or None,
|
|
1151
|
+
"receiver": match["receiver"], "receiverMatch": receiver_match,
|
|
1152
|
+
"awaited": via is not None, "directlyAwaited": via == "direct", "awaitedVia": via,
|
|
1153
|
+
"enclosingCallee": callee, "chainedMember": chained,
|
|
1154
|
+
"text": _cap(body[row].strip(), LINE_TEXT_CAP)[0],
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
for name, (call_re, declaration_res) in names.items():
|
|
1158
|
+
for match in _matches(text, call_re, declaration_res, language):
|
|
1159
|
+
signature = {"line": opening + match["row"] + 2, "text": body[match["row"]].strip()}
|
|
1160
|
+
for key in keys_by_name[name]:
|
|
1161
|
+
owner = key[0]
|
|
1162
|
+
got = found[key]
|
|
1163
|
+
if owner is None:
|
|
1164
|
+
if match["declaration"]:
|
|
1165
|
+
got["signatures"].append(signature)
|
|
1166
|
+
else:
|
|
1167
|
+
got["calls"].append(call(match, None))
|
|
662
1168
|
continue
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if regex is not call_re:
|
|
672
|
-
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)
|
|
679
|
-
found[name]["calls"].append({
|
|
680
|
-
"file": rel, "line": opening + row + 2, "fence": opening + 1, "language": language or None,
|
|
681
|
-
"awaited": awaited or _enclosing_awaits(text, chain), "chainedMember": chained,
|
|
682
|
-
"text": _cap(body[row].strip(), LINE_TEXT_CAP)[0],
|
|
683
|
-
})
|
|
1169
|
+
kind = block.receiver_match(match, owner)
|
|
1170
|
+
if match["declaration"]:
|
|
1171
|
+
if block.declares(match, owner, kind):
|
|
1172
|
+
got["signatures"].append(signature)
|
|
1173
|
+
elif kind == "other":
|
|
1174
|
+
got["other"] += 1
|
|
1175
|
+
else:
|
|
1176
|
+
got["unverified" if kind == "unknown" else "calls"].append(call(match, kind))
|
|
684
1177
|
return found
|
|
685
1178
|
|
|
686
1179
|
|
|
@@ -694,6 +1187,10 @@ def _validate(inp):
|
|
|
694
1187
|
return "Input must be a JSON object"
|
|
695
1188
|
if not isinstance(inp.get("names"), list):
|
|
696
1189
|
return "names must be a list of export names"
|
|
1190
|
+
if inp.get("methods") is not None and not isinstance(inp["methods"], list):
|
|
1191
|
+
return "methods must be a list of {name, owner} objects when given"
|
|
1192
|
+
if inp.get("owners") is not None and not isinstance(inp["owners"], list):
|
|
1193
|
+
return "owners must be a list of class and namespace names when given"
|
|
697
1194
|
path = inp.get("skillPackagePath")
|
|
698
1195
|
if not isinstance(path, str) or not path:
|
|
699
1196
|
return "skillPackagePath is required: the folder that holds SKILL.md and references/"
|
|
@@ -702,46 +1199,92 @@ def _validate(inp):
|
|
|
702
1199
|
return None
|
|
703
1200
|
|
|
704
1201
|
|
|
1202
|
+
def _keys(inp):
|
|
1203
|
+
"""(owner or None, name) for each name and each method of the input, in input order, deduplicated.
|
|
1204
|
+
|
|
1205
|
+
An owner applies only to a plain name: a qualified one (`agents.get`) is matched as written."""
|
|
1206
|
+
keys = [(None, n) for n in inp["names"] if isinstance(n, str) and n.strip()]
|
|
1207
|
+
for item in inp.get("methods") or []:
|
|
1208
|
+
if not isinstance(item, dict):
|
|
1209
|
+
continue
|
|
1210
|
+
name, owner = item.get("name"), item.get("owner")
|
|
1211
|
+
if not (isinstance(name, str) and name.strip() and isinstance(owner, str) and owner.strip()):
|
|
1212
|
+
continue
|
|
1213
|
+
name, owner = name.strip(), _owner_display(owner)
|
|
1214
|
+
if _last_part(name):
|
|
1215
|
+
keys.append((None, name))
|
|
1216
|
+
elif _type_parts(owner):
|
|
1217
|
+
keys.append((owner, name))
|
|
1218
|
+
return list(dict.fromkeys(keys))
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _owner_first(descriptions, owner):
|
|
1222
|
+
"""An owned method's descriptions, those that name its owner (`agents` for `cognee.agents`, any case) first."""
|
|
1223
|
+
if owner is None:
|
|
1224
|
+
return descriptions
|
|
1225
|
+
names_owner = re.compile(_bounded(_last_part(owner) or owner).pattern, re.I)
|
|
1226
|
+
return sorted(descriptions, key=lambda d: not names_owner.search(d["text"]))
|
|
1227
|
+
|
|
1228
|
+
|
|
705
1229
|
def locate(inp):
|
|
706
1230
|
"""Pure location over SKILL.md and references/**/*.md under skillPackagePath."""
|
|
707
1231
|
err = _validate(inp)
|
|
708
1232
|
if err:
|
|
709
1233
|
return make_error(err)
|
|
710
|
-
|
|
1234
|
+
keys = _keys(inp)
|
|
1235
|
+
owners = _Owners(dict.fromkeys(
|
|
1236
|
+
[d for o in inp.get("owners") or [] if isinstance(o, str) and (d := _owner_display(o))]
|
|
1237
|
+
+ [owner for owner, _ in keys if owner is not None]))
|
|
1238
|
+
names = list(dict.fromkeys(name for _, name in keys))
|
|
711
1239
|
bounded = {name: _bounded(name) for name in names}
|
|
712
1240
|
patterns = {name: (_call_pattern(name), [_call_pattern(p)] if (p := _last_part(name)) else [])
|
|
713
1241
|
for name in names}
|
|
1242
|
+
keys_by_name = {name: [key for key in keys if key[1] == name] for name in names}
|
|
714
1243
|
files = skill_files(inp["skillPackagePath"])
|
|
715
|
-
descriptions = {
|
|
716
|
-
calls = {
|
|
1244
|
+
descriptions = {key: [] for key in keys}
|
|
1245
|
+
calls = {key: [] for key in keys}
|
|
1246
|
+
unverified = {key: [] for key in keys}
|
|
1247
|
+
other = dict.fromkeys(keys, 0)
|
|
717
1248
|
for rel, lines in files:
|
|
718
1249
|
start = _frontmatter_end(lines)
|
|
719
1250
|
fenced, code = _fenced_blocks(lines, start)
|
|
720
1251
|
headings, blocks = _prose(lines, start, code)
|
|
721
|
-
|
|
1252
|
+
prose = {name: describe(rel, lines, code, headings, blocks, bounded[name]) for name in names}
|
|
1253
|
+
per_file = {key: list(prose[key[1]]) for key in keys}
|
|
722
1254
|
for opening, closing, info in fenced:
|
|
723
|
-
for
|
|
724
|
-
calls[
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1255
|
+
for key, got in scan_block(rel, lines, opening, closing, info, patterns, keys_by_name, owners).items():
|
|
1256
|
+
calls[key] += got["calls"]
|
|
1257
|
+
unverified[key] += got["unverified"]
|
|
1258
|
+
other[key] += got["other"]
|
|
1259
|
+
per_file[key] += [_entry(rel, s["line"], s["line"], "signature",
|
|
1260
|
+
_heading_path(headings, s["line"] - 1), None, s["text"], LINE_TEXT_CAP)
|
|
1261
|
+
for s in got["signatures"]]
|
|
1262
|
+
for key in keys:
|
|
1263
|
+
descriptions[key] += sorted(per_file[key], key=lambda e: e["line"])
|
|
730
1264
|
exports = [{
|
|
731
1265
|
"name": name,
|
|
732
|
-
"
|
|
733
|
-
"
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
|
|
738
|
-
|
|
1266
|
+
"owner": owner,
|
|
1267
|
+
"descriptionCount": len(descriptions[(owner, name)]),
|
|
1268
|
+
"descriptions": _owner_first(descriptions[(owner, name)], owner)[:MAX_DESCRIPTIONS],
|
|
1269
|
+
"calls": calls[(owner, name)],
|
|
1270
|
+
"callCount": len(calls[(owner, name)]),
|
|
1271
|
+
"awaitedCount": sum(1 for c in calls[(owner, name)] if c["awaited"]),
|
|
1272
|
+
"directlyAwaitedCount": sum(1 for c in calls[(owner, name)] if c["directlyAwaited"]),
|
|
1273
|
+
"unverifiedCalls": unverified[(owner, name)],
|
|
1274
|
+
"otherReceiverCount": other[(owner, name)],
|
|
1275
|
+
} for owner, name in keys if calls[(owner, name)]]
|
|
1276
|
+
# A method with only unverified calls is not called: §2.5's async rule cites a first call. Its unverified
|
|
1277
|
+
# calls still reach the output, in notCalledUnverified.
|
|
1278
|
+
not_called = [f"{owner}.{name}" if owner else name for owner, name in keys if not calls[(owner, name)]]
|
|
1279
|
+
not_called_unverified = [{"name": name, "owner": owner, "unverifiedCalls": unverified[(owner, name)]}
|
|
1280
|
+
for owner, name in keys if not calls[(owner, name)] and unverified[(owner, name)]]
|
|
739
1281
|
return {
|
|
740
1282
|
"skillPackagePath": inp["skillPackagePath"],
|
|
741
1283
|
"files": [rel for rel, _ in files],
|
|
742
1284
|
"exports": exports,
|
|
743
1285
|
"notCalled": not_called,
|
|
744
|
-
"
|
|
1286
|
+
"notCalledUnverified": not_called_unverified,
|
|
1287
|
+
"summary": {"names": len(keys), "called": len(exports), "notCalled": len(not_called)},
|
|
745
1288
|
}
|
|
746
1289
|
|
|
747
1290
|
|
|
@@ -770,26 +1313,92 @@ def _build_parser():
|
|
|
770
1313
|
help="JSON object passed via flag (overrides positional).")
|
|
771
1314
|
src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
|
|
772
1315
|
src.add_argument("--inventory", metavar="PATH",
|
|
773
|
-
help="the validated inventory file: its
|
|
1316
|
+
help="the validated inventory file: its functions and methods, and the `methods` of each "
|
|
1317
|
+
"class, interface, struct, trait or type entry tied to it (needs --skill-dir)")
|
|
774
1318
|
parser.add_argument("--skill-dir", metavar="DIR", help="with --inventory: the skill package")
|
|
775
1319
|
return parser
|
|
776
1320
|
|
|
777
1321
|
|
|
778
|
-
# The inventory kinds §2.5 checks: the ones a caller awaits or calls.
|
|
1322
|
+
# The inventory kinds §2.5 checks: the ones a caller awaits or calls. The
|
|
1323
|
+
# `methods` items of a class, interface, struct, trait or type entry
|
|
1324
|
+
# (METHOD_LIST_KINDS, and no other kind's) are checked too, each tied to its
|
|
1325
|
+
# entry.
|
|
779
1326
|
CALLABLE_KINDS = ("function", "method")
|
|
1327
|
+
METHOD_LIST_KINDS = ("class", "interface", "struct", "trait", "type")
|
|
1328
|
+
# The kinds a method can be documented on: a receiver that names one is that owner's.
|
|
1329
|
+
OWNER_KINDS = ("class", "struct", "interface", "trait", "type", "enum", "adapter")
|
|
1330
|
+
# Words a `methods` item may write before the method's name: `async next_uuid(dep=None)`.
|
|
1331
|
+
METHOD_MODIFIERS = DECLARATION_WORDS | {"classmethod", "staticmethod"}
|
|
1332
|
+
MODIFIER_RE = re.compile(f"((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)\\s+(?=[{IDENT_CHARS}*\\[])")
|
|
1333
|
+
DECORATOR_RE = re.compile(f"@{TYPE_PATH}")
|
|
1334
|
+
GO_RECEIVER_ITEM_RE = re.compile(r"^func[ \t]*\([^()]*\)[ \t]*")
|
|
1335
|
+
QUALIFIED = f"(?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*(?:(?:\\.|::|#)(?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)*"
|
|
1336
|
+
METHOD_CALLEE_RE = re.compile(
|
|
1337
|
+
f"(?<![{IDENT_CHARS}.:#])({QUALIFIED})[ \\t]*(?:<[^<>()\\n]*(?:<[^<>()\\n]*>[^<>()\\n]*)*>)?[ \\t]*\\(")
|
|
1338
|
+
METHOD_LEAD_RE = re.compile(f"({QUALIFIED})")
|
|
1339
|
+
AWAIT_LEAD_RE = re.compile(f"await(?![{IDENT_CHARS}])")
|
|
1340
|
+
DUNDER_RE = re.compile(r"__\w+__")
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _method_name(item):
|
|
1344
|
+
"""The method a class entry's `methods` item names, as the skill writes it (`async next_uuid(self) -> UUID`
|
|
1345
|
+
gives `next_uuid`), or None: a dunder (`__init__`), an item that starts with `await`, and one that holds
|
|
1346
|
+
only modifiers (`async`, `async *[Symbol.asyncIterator]()`) name no method."""
|
|
1347
|
+
text = item.strip().lstrip(".#")
|
|
1348
|
+
while (m := DECORATOR_RE.match(text)): # `@property name`, `@lru_cache(maxsize=1) compute(x)`
|
|
1349
|
+
end = m.end()
|
|
1350
|
+
if text.startswith("(", end):
|
|
1351
|
+
close = _matching_close(text, end)
|
|
1352
|
+
if close is None:
|
|
1353
|
+
return None
|
|
1354
|
+
end = close + 1
|
|
1355
|
+
text = text[end:].lstrip()
|
|
1356
|
+
text = GO_RECEIVER_ITEM_RE.sub("", text, count=1) # `func (c *Client) Fetch(ctx) error`
|
|
1357
|
+
while (m := MODIFIER_RE.match(text)) and m.group(1) in METHOD_MODIFIERS:
|
|
1358
|
+
text = text[m.end():].lstrip("*") # `async *stream()`, a generator
|
|
1359
|
+
if AWAIT_LEAD_RE.match(text):
|
|
1360
|
+
return None
|
|
1361
|
+
m = METHOD_CALLEE_RE.search(text)
|
|
1362
|
+
if not m:
|
|
1363
|
+
m = METHOD_LEAD_RE.match(text)
|
|
1364
|
+
if not m or m.group(1) in METHOD_MODIFIERS:
|
|
1365
|
+
return None
|
|
1366
|
+
name = _last_part(m.group(1)) or m.group(1)
|
|
1367
|
+
return None if DUNDER_RE.fullmatch(name) else name
|
|
780
1368
|
|
|
781
1369
|
|
|
782
1370
|
def inventory_input(path, skill_dir):
|
|
783
|
-
"""The JSON input for the
|
|
1371
|
+
"""The JSON input for the functions and methods of a validated inventory file.
|
|
1372
|
+
|
|
1373
|
+
`names`: each `function` entry, and each `method` entry with no non-empty
|
|
1374
|
+
string `owner`. `methods`: each other `method` entry with its owner, and
|
|
1375
|
+
each method the `methods` list of a class, interface, struct, trait or
|
|
1376
|
+
type entry names, owned by that entry. `owners`: each class-like entry and
|
|
1377
|
+
each method's owner.
|
|
1378
|
+
"""
|
|
784
1379
|
data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
785
1380
|
if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
|
|
786
1381
|
data = data["inventory"] # validate-inventory.py's whole result
|
|
787
1382
|
exports = data.get("exports") if isinstance(data, dict) else None
|
|
788
1383
|
if not isinstance(exports, list):
|
|
789
1384
|
raise ValueError(f"--inventory {path} holds no `exports` list")
|
|
790
|
-
names = [
|
|
791
|
-
|
|
792
|
-
|
|
1385
|
+
names, methods, owners = [], [], []
|
|
1386
|
+
for entry in exports:
|
|
1387
|
+
if not (isinstance(entry, dict) and isinstance(entry.get("name"), str)):
|
|
1388
|
+
continue
|
|
1389
|
+
name, kind, owner = entry["name"], entry.get("kind"), entry.get("owner")
|
|
1390
|
+
if kind == "method" and isinstance(owner, str) and owner.strip():
|
|
1391
|
+
methods.append({"name": name, "owner": owner})
|
|
1392
|
+
owners.append(owner)
|
|
1393
|
+
elif kind in CALLABLE_KINDS:
|
|
1394
|
+
names.append(name)
|
|
1395
|
+
if kind in OWNER_KINDS:
|
|
1396
|
+
owners.append(name)
|
|
1397
|
+
listed = entry["methods"] if kind in METHOD_LIST_KINDS and isinstance(entry.get("methods"), list) else []
|
|
1398
|
+
for method in filter(None, (_method_name(item) for item in listed if isinstance(item, str))):
|
|
1399
|
+
methods.append({"name": method, "owner": name})
|
|
1400
|
+
owners.append(name)
|
|
1401
|
+
return {"names": names, "methods": methods, "owners": list(dict.fromkeys(owners)), "skillPackagePath": skill_dir}
|
|
793
1402
|
|
|
794
1403
|
|
|
795
1404
|
def _resolve_input(args):
|