bmad-module-skill-forge 5.0.0 → 7.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/docs/_data/pinned.yaml +1 -1
- package/docs/agents.md +1 -1
- package/docs/architecture.md +3 -3
- package/docs/campaign.md +1 -1
- package/docs/troubleshooting.md +9 -3
- package/docs/verifying-a-skill.md +9 -9
- package/docs/workflows.md +7 -7
- 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/skf-build-change-manifest.py +332 -54
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- package/src/shared/scripts/skf-emit-result-envelope.py +58 -15
- package/src/shared/scripts/skf-extract-public-api.py +33 -13
- package/src/shared/scripts/skf-parse-gaps.py +3 -2
- package/src/shared/scripts/skf-quick-batch.py +16 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +317 -44
- package/src/skf-analyze-source/SKILL.md +1 -0
- package/src/skf-analyze-source/customize.toml +3 -2
- package/src/skf-analyze-source/references/auto-docs-only.md +2 -2
- package/src/skf-analyze-source/references/generate-briefs.md +5 -3
- package/src/skf-analyze-source/references/headless-contract.md +3 -3
- package/src/skf-analyze-source/references/step-auto-scope.md +2 -2
- package/src/skf-audit-skill/SKILL.md +2 -1
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +1 -0
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-campaign/SKILL.md +1 -0
- package/src/skf-campaign/references/campaign-contracts.md +3 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +2 -2
- package/src/skf-campaign/references/step-07-capstone.md +3 -3
- package/src/skf-campaign/references/step-08-verify.md +3 -3
- package/src/skf-campaign/references/step-09-refine.md +3 -3
- package/src/skf-campaign/references/step-10-export.md +3 -2
- package/src/skf-campaign/scripts/campaign-state.py +22 -5
- package/src/skf-create-skill/SKILL.md +3 -2
- package/src/skf-create-skill/customize.toml +4 -4
- package/src/skf-create-skill/references/batch-mode.md +3 -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 +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +9 -4
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +2 -2
- package/src/skf-create-stack-skill/SKILL.md +1 -0
- 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 +12 -4
- package/src/skf-drop-skill/SKILL.md +1 -0
- 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 +1 -0
- package/src/skf-export-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-forger/SKILL.md +1 -0
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -1
- package/src/skf-quick-skill/references/batch-mode.md +2 -0
- 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 +20 -10
- package/src/skf-refine-architecture/SKILL.md +2 -1
- 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 +1 -0
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +1 -0
- 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 +1 -0
- package/src/skf-test-skill/assets/output-section-formats.md +4 -4
- package/src/skf-test-skill/references/coherence-check.md +8 -6
- package/src/skf-test-skill/references/coverage-check-branches.md +23 -3
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +30 -43
- package/src/skf-test-skill/references/init.md +1 -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/report.md +3 -3
- package/src/skf-test-skill/references/scoring-rules.md +7 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +74 -0
- package/src/skf-test-skill/references/source-access-protocol.md +12 -8
- 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 +78 -13
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +575 -9
- package/src/skf-test-skill/scripts/locate-export-segments.py +591 -63
- package/src/skf-test-skill/scripts/score-signatures.py +937 -76
- package/src/skf-test-skill/scripts/validate-inventory.py +325 -30
- package/src/skf-update-skill/SKILL.md +1 -0
- package/src/skf-update-skill/customize.toml +1 -1
- package/src/skf-update-skill/references/gap-driven.md +28 -22
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +4 -4
- package/src/skf-update-skill/references/report.md +11 -4
- package/src/skf-update-skill/references/write.md +6 -5
- package/src/skf-verify-stack/SKILL.md +2 -1
|
@@ -29,6 +29,50 @@ 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
78
|
`await client.fetchData(...)`), `.await` or `?.await` follows it (Rust), an asynchronous
|
|
@@ -66,6 +110,15 @@ usage-scope). A qualified name (`client.fetch`, `Store::load`) matches only
|
|
|
66
110
|
as written, except that a declaration also matches its last part
|
|
67
111
|
(`def fetch(`): a declaration never carries the qualifier.
|
|
68
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
|
+
|
|
69
122
|
Line model: UTF-8 with a leading byte order mark dropped, split on `\\n`
|
|
70
123
|
(a `\\r\\n` pair loses its `\\r`), so each `line` matches `grep -n`. The
|
|
71
124
|
frontmatter is a leading `---` ... `---` block. A fence opens on three or
|
|
@@ -77,13 +130,33 @@ indent); its section runs to the next heading of the same or a higher level.
|
|
|
77
130
|
Input (one JSON object):
|
|
78
131
|
{
|
|
79
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
|
|
80
135
|
"skillPackagePath": "/path/to/skill" # holds SKILL.md and references/
|
|
81
136
|
}
|
|
82
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
|
+
|
|
83
143
|
or the files: --inventory <file> --skill-dir <dir>. --inventory reads the
|
|
84
|
-
validated inventory validate-inventory.py wrote (its --output)
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
87
160
|
|
|
88
161
|
Output (stdout, one object):
|
|
89
162
|
{
|
|
@@ -94,15 +167,19 @@ Output (stdout, one object):
|
|
|
94
167
|
"name": "fetchData",
|
|
95
168
|
"descriptionCount": 3, # all found; descriptions[] holds the first 12
|
|
96
169
|
"descriptions": [{"file", "line", "endLine", "kind", "section", "header", "text", "truncated"}],
|
|
97
|
-
"
|
|
98
|
-
|
|
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"}],
|
|
99
173
|
"callCount": 2,
|
|
100
174
|
"awaitedCount": 1, # calls whose `awaited` is true
|
|
101
|
-
"directlyAwaitedCount": 1
|
|
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
|
|
102
178
|
}
|
|
103
179
|
],
|
|
104
|
-
"notCalled": ["parseConfig"],
|
|
105
|
-
"
|
|
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
|
|
106
183
|
}
|
|
107
184
|
or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
|
|
108
185
|
|
|
@@ -124,6 +201,7 @@ Exit codes:
|
|
|
124
201
|
from __future__ import annotations
|
|
125
202
|
|
|
126
203
|
import argparse
|
|
204
|
+
import bisect
|
|
127
205
|
import json
|
|
128
206
|
import re
|
|
129
207
|
import sys
|
|
@@ -202,6 +280,21 @@ TYPED_DECLARATIONS = C_FAMILY | frozenset({"groovy", ""})
|
|
|
202
280
|
# A `{` after a call at the start of a line opens a method body here; in
|
|
203
281
|
# Kotlin, Swift or Ruby it can be a trailing closure of a call.
|
|
204
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}]*")
|
|
205
298
|
|
|
206
299
|
# A word right before the name that declares it.
|
|
207
300
|
DECLARATION_WORDS = frozenset({
|
|
@@ -225,6 +318,36 @@ RUN_OWNERS = frozenset({"asyncio", "trio", "anyio"})
|
|
|
225
318
|
BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
226
319
|
CLOSERS = {v: k for k, v in BRACKETS.items()}
|
|
227
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
|
+
|
|
228
351
|
|
|
229
352
|
def make_error(message):
|
|
230
353
|
return {"error": message, "code": "INVALID_INPUT"}
|
|
@@ -563,6 +686,8 @@ def _chain_start(text, pos):
|
|
|
563
686
|
if accessor is None:
|
|
564
687
|
return i
|
|
565
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()`
|
|
566
691
|
start = k
|
|
567
692
|
while start > 0 and text[start - 1] in ")]":
|
|
568
693
|
opened = _matching_open(text, start - 1)
|
|
@@ -577,6 +702,222 @@ def _chain_start(text, pos):
|
|
|
577
702
|
i = start
|
|
578
703
|
|
|
579
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
|
+
|
|
580
921
|
def _await_before(text, start):
|
|
581
922
|
"""True when `await` stands right before the expression that starts at `start`."""
|
|
582
923
|
return bool(AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)))
|
|
@@ -696,8 +1037,41 @@ def _closes_generic(tail):
|
|
|
696
1037
|
return False
|
|
697
1038
|
|
|
698
1039
|
|
|
699
|
-
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):
|
|
700
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)`
|
|
701
1075
|
tail = text[line_start:chain].rstrip(" \t")
|
|
702
1076
|
if chain == name_start and tail:
|
|
703
1077
|
word = WORD_BEFORE_RE.search(tail)
|
|
@@ -725,42 +1099,81 @@ def _is_declaration(text, line_start, chain, name_start, close, language):
|
|
|
725
1099
|
return False
|
|
726
1100
|
|
|
727
1101
|
|
|
728
|
-
def
|
|
729
|
-
"""
|
|
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
|
|
1121
|
+
|
|
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.
|
|
730
1125
|
|
|
731
|
-
`
|
|
732
|
-
|
|
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.
|
|
733
1134
|
"""
|
|
734
1135
|
language = _language(info)
|
|
735
|
-
found = {
|
|
1136
|
+
found = {key: {"calls": [], "unverified": [], "other": 0, "signatures": []}
|
|
1137
|
+
for keys in keys_by_name.values() for key in keys}
|
|
736
1138
|
if language in DATA_LANGUAGES:
|
|
737
1139
|
return found
|
|
738
1140
|
body = lines[opening + 1:closing]
|
|
739
1141
|
text = mask_code("\n".join(body), language)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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))
|
|
754
1168
|
continue
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
"
|
|
761
|
-
|
|
762
|
-
"
|
|
763
|
-
})
|
|
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))
|
|
764
1177
|
return found
|
|
765
1178
|
|
|
766
1179
|
|
|
@@ -774,6 +1187,10 @@ def _validate(inp):
|
|
|
774
1187
|
return "Input must be a JSON object"
|
|
775
1188
|
if not isinstance(inp.get("names"), list):
|
|
776
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"
|
|
777
1194
|
path = inp.get("skillPackagePath")
|
|
778
1195
|
if not isinstance(path, str) or not path:
|
|
779
1196
|
return "skillPackagePath is required: the folder that holds SKILL.md and references/"
|
|
@@ -782,47 +1199,92 @@ def _validate(inp):
|
|
|
782
1199
|
return None
|
|
783
1200
|
|
|
784
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
|
+
|
|
785
1229
|
def locate(inp):
|
|
786
1230
|
"""Pure location over SKILL.md and references/**/*.md under skillPackagePath."""
|
|
787
1231
|
err = _validate(inp)
|
|
788
1232
|
if err:
|
|
789
1233
|
return make_error(err)
|
|
790
|
-
|
|
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))
|
|
791
1239
|
bounded = {name: _bounded(name) for name in names}
|
|
792
1240
|
patterns = {name: (_call_pattern(name), [_call_pattern(p)] if (p := _last_part(name)) else [])
|
|
793
1241
|
for name in names}
|
|
1242
|
+
keys_by_name = {name: [key for key in keys if key[1] == name] for name in names}
|
|
794
1243
|
files = skill_files(inp["skillPackagePath"])
|
|
795
|
-
descriptions = {
|
|
796
|
-
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)
|
|
797
1248
|
for rel, lines in files:
|
|
798
1249
|
start = _frontmatter_end(lines)
|
|
799
1250
|
fenced, code = _fenced_blocks(lines, start)
|
|
800
1251
|
headings, blocks = _prose(lines, start, code)
|
|
801
|
-
|
|
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}
|
|
802
1254
|
for opening, closing, info in fenced:
|
|
803
|
-
for
|
|
804
|
-
calls[
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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"])
|
|
810
1264
|
exports = [{
|
|
811
1265
|
"name": name,
|
|
812
|
-
"
|
|
813
|
-
"
|
|
814
|
-
"
|
|
815
|
-
"
|
|
816
|
-
"
|
|
817
|
-
"
|
|
818
|
-
|
|
819
|
-
|
|
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)]]
|
|
820
1281
|
return {
|
|
821
1282
|
"skillPackagePath": inp["skillPackagePath"],
|
|
822
1283
|
"files": [rel for rel, _ in files],
|
|
823
1284
|
"exports": exports,
|
|
824
1285
|
"notCalled": not_called,
|
|
825
|
-
"
|
|
1286
|
+
"notCalledUnverified": not_called_unverified,
|
|
1287
|
+
"summary": {"names": len(keys), "called": len(exports), "notCalled": len(not_called)},
|
|
826
1288
|
}
|
|
827
1289
|
|
|
828
1290
|
|
|
@@ -851,26 +1313,92 @@ def _build_parser():
|
|
|
851
1313
|
help="JSON object passed via flag (overrides positional).")
|
|
852
1314
|
src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
|
|
853
1315
|
src.add_argument("--inventory", metavar="PATH",
|
|
854
|
-
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)")
|
|
855
1318
|
parser.add_argument("--skill-dir", metavar="DIR", help="with --inventory: the skill package")
|
|
856
1319
|
return parser
|
|
857
1320
|
|
|
858
1321
|
|
|
859
|
-
# 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.
|
|
860
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
|
|
861
1368
|
|
|
862
1369
|
|
|
863
1370
|
def inventory_input(path, skill_dir):
|
|
864
|
-
"""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
|
+
"""
|
|
865
1379
|
data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
866
1380
|
if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
|
|
867
1381
|
data = data["inventory"] # validate-inventory.py's whole result
|
|
868
1382
|
exports = data.get("exports") if isinstance(data, dict) else None
|
|
869
1383
|
if not isinstance(exports, list):
|
|
870
1384
|
raise ValueError(f"--inventory {path} holds no `exports` list")
|
|
871
|
-
names = [
|
|
872
|
-
|
|
873
|
-
|
|
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}
|
|
874
1402
|
|
|
875
1403
|
|
|
876
1404
|
def _resolve_input(args):
|