devrites 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +55 -43
  3. package/SECURITY.md +72 -58
  4. package/docs/agents/issue-driven-rites.md +5 -5
  5. package/docs/architecture.md +61 -45
  6. package/docs/capability-surface-selection.md +3 -1
  7. package/docs/cli.md +9 -2
  8. package/docs/command-map.md +6 -2
  9. package/docs/engine/agent-contract.md +5 -3
  10. package/docs/engine/commands.md +27 -14
  11. package/docs/engine/state-schema.md +8 -4
  12. package/docs/engine/workspace-schema.md +5 -4
  13. package/docs/flow.md +18 -7
  14. package/docs/porting-to-a-new-harness.md +6 -2
  15. package/docs/release.md +10 -9
  16. package/docs/skills.md +1 -1
  17. package/docs/templates/CRITIC_REVIEW_PACKET_TEMPLATE.md +35 -0
  18. package/docs/usage.md +26 -16
  19. package/engine/internal/lib/packageexistence.go +729 -52
  20. package/engine/internal/lib/packageexistence_test.go +343 -0
  21. package/engine/internal/lib/util.go +1 -1
  22. package/pack/.claude/skills/devrites-lib/SKILL.md +7 -9
  23. package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +11 -3
  24. package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +2 -3
  25. package/pack/.claude/skills/rite/SKILL.md +6 -7
  26. package/pack/.claude/skills/rite/reference/menu.md +5 -5
  27. package/pack/.claude/skills/rite-adopt/SKILL.md +2 -3
  28. package/pack/generated/claude/skills/devrites-lib/SKILL.md +7 -9
  29. package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +11 -3
  30. package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +2 -3
  31. package/pack/generated/claude/skills/rite/SKILL.md +6 -7
  32. package/pack/generated/claude/skills/rite/reference/menu.md +5 -5
  33. package/pack/generated/claude/skills/rite-adopt/SKILL.md +2 -3
  34. package/pack/generated/codex/skills/devrites-lib/SKILL.md +7 -9
  35. package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +11 -3
  36. package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +2 -3
  37. package/pack/generated/codex/skills/rite/SKILL.md +6 -7
  38. package/pack/generated/codex/skills/rite/reference/menu.md +5 -5
  39. package/pack/generated/codex/skills/rite-adopt/SKILL.md +2 -3
  40. package/package.json +1 -1
  41. package/scripts/skills-inventory.mjs +18 -0
@@ -1,24 +1,31 @@
1
1
  package lib
2
2
 
3
3
  import (
4
+ "bytes"
5
+ "encoding/json"
4
6
  "fmt"
7
+ "go/parser"
8
+ "go/token"
5
9
  "io"
6
10
  "os"
7
11
  "os/exec"
8
12
  "path/filepath"
9
13
  "regexp"
10
14
  "sort"
15
+ "strconv"
11
16
  "strings"
12
17
  )
13
18
 
14
- // Patterns for reducing an added import line down to a bare top-level package
15
- // name. Quoted module specifiers are authoritative for JS/TS/Go/Rust style
16
- // imports; unquoted Python imports are only considered when a line has no
17
- // quoted specifier, so local JS binding names are never treated as packages.
19
+ // Import patterns are language-specific: JS/TS uses quoted specifiers, Go uses
20
+ // the standard parser, and Python/Rust use their import declaration syntax.
18
21
  var (
19
22
  quotedModule = regexp.MustCompile(`\b(?:from|import|require)[[:space:]]*(?:\([[:space:]]*)?['"]([^'"]+)['"]`)
20
23
  pythonImport = regexp.MustCompile(`^\+?[[:space:]]*import[[:space:]]+([A-Za-z0-9_./-]+)`)
21
24
  pythonFrom = regexp.MustCompile(`^\+?[[:space:]]*from[[:space:]]+([A-Za-z0-9_./-]+)[[:space:]]+import\b`)
25
+ rustUse = regexp.MustCompile(`^[[:space:]]*(?:pub(?:\([^)]*\))?[[:space:]]+)?use[[:space:]]+(?:::)?(?:r#)?([A-Za-z_][A-Za-z0-9_]*)(?:::|[[:space:]]+as\b|[[:space:]]*;)`)
26
+ rustExtern = regexp.MustCompile(`^[[:space:]]*(?:pub[[:space:]]+)?extern[[:space:]]+crate[[:space:]]+(?:r#)?([A-Za-z_][A-Za-z0-9_]*)\b`)
27
+ requirement = regexp.MustCompile(`^[[:space:]]*([A-Za-z0-9][A-Za-z0-9._-]*)[[:space:]]*(?:\[[^]]*\])?[[:space:]]*(?:@|[<>=!~;]|$)`)
28
+ quotedValue = regexp.MustCompile(`["']([^"']+)["']`)
22
29
  scopedPackage = regexp.MustCompile(`^(@[^/]+/[^/]+).*`) // @scope/name/... -> @scope/name
23
30
  firstSegment = regexp.MustCompile(`^([^@][^/]+)/.*`) // name/sub/... -> name
24
31
  localPath = regexp.MustCompile(`^(\.|/|[A-Z]:)`) // relative / absolute / drive path
@@ -29,6 +36,33 @@ var (
29
36
  // manifestFiles are the dependency manifests a declared package can appear in.
30
37
  var manifestFiles = []string{"package.json", "requirements.txt", "pyproject.toml", "Pipfile", "go.mod", "Cargo.toml"}
31
38
 
39
+ type importedModule struct {
40
+ sourcePath string
41
+ specifier string
42
+ }
43
+
44
+ type packageResolution struct {
45
+ name string
46
+ resolved bool
47
+ manifestFound bool
48
+ ignored bool
49
+ }
50
+
51
+ type packageResolver struct {
52
+ extensions string
53
+ extract func(string, string, []string) []string
54
+ resolve func(string, string, string) packageResolution
55
+ }
56
+
57
+ var packageResolvers = []packageResolver{
58
+ {extensions: " .js .jsx .mjs .cjs .ts .tsx .mts .cts .svelte .vue ", extract: quotedModulesFromAddedLines, resolve: resolveJavaScriptImport},
59
+ {extensions: " .go ", extract: goModulesFromAddedLinesForSource, resolve: resolveGoImport},
60
+ {extensions: " .py .pyi ", extract: pythonModulesFromAddedLines, resolve: resolvePythonImport},
61
+ {extensions: " .rs ", extract: rustModulesFromAddedLines, resolve: resolveRustImport},
62
+ }
63
+
64
+ var fallbackPackageResolver = packageResolver{extract: quotedModulesFromAddedLines, resolve: resolveGenericImport}
65
+
32
66
  // PackageExistence catches hallucinated or typo-squatted dependencies: every new
33
67
  // third-party import in the slice diff must be DECLARED in a project manifest, not
34
68
  // merely imported. It is deterministic and offline — it checks the manifest, not a
@@ -62,35 +96,46 @@ func PackageExistence(root string, args []string, stdout, stderr io.Writer) int
62
96
  }
63
97
  }
64
98
 
65
- var manifests []string
99
+ var rootManifests []string
66
100
  for _, m := range manifestFiles {
67
101
  p := filepath.Join(gitRoot, m)
68
102
  if isFile(p) {
69
- manifests = append(manifests, p)
103
+ rootManifests = append(rootManifests, p)
70
104
  }
71
105
  }
72
- if len(manifests) == 0 {
73
- fmt.Fprintln(stderr, "package-existence: no recognized manifest — skipped.")
74
- return 0
75
- }
76
-
77
- pkgs := extractImportedPackages(gitRoot, ref)
78
106
 
79
- bad := 0
80
- var report strings.Builder
81
- for _, p := range pkgs {
82
- if p == "" || builtinPackage.MatchString(p) {
107
+ imports := extractImportedPackages(gitRoot, ref)
108
+ hasManifest := len(rootManifests) > 0
109
+ findings := map[string]struct{}{}
110
+ for _, imported := range imports {
111
+ result := resolverForSource(imported.sourcePath).resolve(gitRoot, imported.sourcePath, imported.specifier)
112
+ if result.ignored {
83
113
  continue
84
114
  }
85
- if !declaredInManifests(p, manifests) {
86
- bad++
87
- fmt.Fprintf(&report, " - %s: imported but not declared in any manifest\n", p)
115
+ if result.manifestFound {
116
+ hasManifest = true
88
117
  }
118
+ if result.resolved {
119
+ continue
120
+ }
121
+ findings[result.name] = struct{}{}
89
122
  }
90
123
 
91
- if bad > 0 {
92
- fmt.Fprintf(stderr, "package-existence: %d imported package(s) are NOT declared in a manifest — verify each exists and add it via the package manager:\n", bad)
93
- fmt.Fprint(stderr, report.String())
124
+ if !hasManifest {
125
+ fmt.Fprintln(stderr, "package-existence: no recognized manifest — skipped.")
126
+ return 0
127
+ }
128
+
129
+ if len(findings) > 0 {
130
+ packages := make([]string, 0, len(findings))
131
+ for pkg := range findings {
132
+ packages = append(packages, pkg)
133
+ }
134
+ sort.Strings(packages)
135
+ fmt.Fprintf(stderr, "package-existence: %d imported package(s) are NOT declared in a manifest — verify each exists and add it via the package manager:\n", len(packages))
136
+ for _, pkg := range packages {
137
+ fmt.Fprintf(stderr, " - %s: imported but not declared in any manifest\n", pkg)
138
+ }
94
139
  fmt.Fprintln(stderr, "An undeclared import is how hallucinated/typo-squatted packages slip in. Confirm the name on the registry before trusting it.")
95
140
  return 3
96
141
  }
@@ -98,35 +143,584 @@ func PackageExistence(root string, args []string, stdout, stderr io.Writer) int
98
143
  return 0
99
144
  }
100
145
 
101
- // extractImportedPackages returns the sorted, unique top-level package names
102
- // imported by the added ('+') lines of the diff against ref. Each import line is
103
- // reduced to a package name in steps: isolate quoted module specifiers (or, for
104
- // Python, the imported module token), collapse a scoped package or sub-path to
105
- // its top level, and drop local/relative paths.
106
- func extractImportedPackages(gitRoot, ref string) []string {
107
- out, err := exec.Command("git", "-C", gitRoot, "diff", ref).Output()
146
+ // extractImportedPackages retains the source file and raw specifier for each
147
+ // import added by the diff so workspace and alias resolution can use that
148
+ // provenance before the specifier is reduced to a top-level package name.
149
+ func extractImportedPackages(gitRoot, ref string) []importedModule {
150
+ out, err := exec.Command("git", "-C", gitRoot, "diff", "--name-only", "-z", ref, "--").Output()
108
151
  if err != nil {
109
152
  return nil
110
153
  }
111
- seen := map[string]struct{}{}
112
- var pkgs []string
113
- for _, line := range splitLinesNoTrailing(out) {
114
- if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
115
- continue // added lines only, excluding the '+++' file header
154
+ var imports []importedModule
155
+ for rawPath := range bytes.SplitSeq(out, []byte{0}) {
156
+ if len(rawPath) == 0 {
157
+ continue
158
+ }
159
+ sourcePath := filepath.Clean(string(rawPath))
160
+ diff, err := exec.Command("git", "-C", gitRoot, "diff", "--unified=0", ref, "--", sourcePath).Output()
161
+ if err != nil {
162
+ continue
163
+ }
164
+ var addedLines []string
165
+ for _, line := range splitLinesNoTrailing(diff) {
166
+ if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
167
+ continue
168
+ }
169
+ addedLines = append(addedLines, strings.TrimPrefix(line, "+"))
170
+ }
171
+ for _, specifier := range importedModulesFromAddedLines(gitRoot, sourcePath, addedLines) {
172
+ imports = append(imports, importedModule{sourcePath: sourcePath, specifier: specifier})
173
+ }
174
+ }
175
+ return imports
176
+ }
177
+
178
+ func importedModulesFromAddedLines(gitRoot, sourcePath string, lines []string) []string {
179
+ return resolverForSource(sourcePath).extract(gitRoot, sourcePath, lines)
180
+ }
181
+
182
+ func resolverForSource(sourcePath string) packageResolver {
183
+ extension := " " + strings.ToLower(filepath.Ext(sourcePath)) + " "
184
+ for _, resolver := range packageResolvers {
185
+ if strings.Contains(resolver.extensions, extension) {
186
+ return resolver
187
+ }
188
+ }
189
+ return fallbackPackageResolver
190
+ }
191
+
192
+ func resolveJavaScriptImport(gitRoot, sourcePath, specifier string) packageResolution {
193
+ pkg := normalizeImportedPackage(specifier)
194
+ if pkg == "" || builtinPackage.MatchString(pkg) {
195
+ return packageResolution{ignored: true}
196
+ }
197
+ manifest := nearestFile(gitRoot, sourcePath, "package.json")
198
+ return packageResolution{
199
+ name: pkg,
200
+ resolved: importMatchesPathAlias(gitRoot, sourcePath, specifier) || manifest != "" && packageJSONDeclares(pkg, manifest),
201
+ manifestFound: manifest != "",
202
+ }
203
+ }
204
+
205
+ func resolveGoImport(gitRoot, sourcePath, specifier string) packageResolution {
206
+ if isGoStandardLibrary(specifier) {
207
+ return packageResolution{ignored: true}
208
+ }
209
+ resolved, manifestFound := goImportResolved(gitRoot, sourcePath, specifier)
210
+ return packageResolution{name: specifier, resolved: resolved, manifestFound: manifestFound}
211
+ }
212
+
213
+ func resolvePythonImport(gitRoot, sourcePath, specifier string) packageResolution {
214
+ module := topLevelPythonModule(specifier)
215
+ if module == "" || isPythonStandardLibrary(module) {
216
+ return packageResolution{ignored: true}
217
+ }
218
+ resolved, manifestFound := pythonImportResolved(gitRoot, sourcePath, normalizeDistributionName(module))
219
+ return packageResolution{name: module, resolved: resolved, manifestFound: manifestFound}
220
+ }
221
+
222
+ func resolveRustImport(gitRoot, sourcePath, specifier string) packageResolution {
223
+ crate := normalizeRustCrate(specifier)
224
+ if crate == "" || isRustBuiltin(crate) {
225
+ return packageResolution{ignored: true}
226
+ }
227
+ resolved, manifestFound := rustImportResolved(gitRoot, sourcePath, crate)
228
+ return packageResolution{name: crate, resolved: resolved, manifestFound: manifestFound}
229
+ }
230
+
231
+ // resolveGenericImport keeps quoted imports in unregistered source types in the
232
+ // shared pipeline. It deliberately fails closed: without ecosystem semantics,
233
+ // text in an unrelated manifest cannot safely prove that an import is declared.
234
+ func resolveGenericImport(gitRoot, sourcePath, specifier string) packageResolution {
235
+ pkg := normalizeImportedPackage(specifier)
236
+ if pkg == "" {
237
+ return packageResolution{ignored: true}
238
+ }
239
+ return packageResolution{
240
+ name: pkg,
241
+ manifestFound: len(nearestFiles(gitRoot, sourcePath, manifestFiles)) > 0,
242
+ }
243
+ }
244
+
245
+ func quotedModulesFromAddedLines(_, _ string, lines []string) []string {
246
+ var modules []string
247
+ for _, line := range lines {
248
+ modules = append(modules, importedModulesFromLine(line)...)
249
+ }
250
+ return modules
251
+ }
252
+
253
+ func goModulesFromAddedLinesForSource(gitRoot, sourcePath string, lines []string) []string {
254
+ return goModulesFromAddedLines(filepath.Join(gitRoot, sourcePath), lines)
255
+ }
256
+
257
+ func pythonModulesFromAddedLines(_, _ string, lines []string) []string {
258
+ var modules []string
259
+ for _, line := range lines {
260
+ modules = append(modules, pythonModulesFromLine(line)...)
261
+ }
262
+ return modules
263
+ }
264
+
265
+ func rustModulesFromAddedLines(_, _ string, lines []string) []string {
266
+ var modules []string
267
+ for _, line := range lines {
268
+ modules = append(modules, rustModulesFromLine(line)...)
269
+ }
270
+ return modules
271
+ }
272
+
273
+ func goModulesFromAddedLines(sourcePath string, lines []string) []string {
274
+ file, _ := parser.ParseFile(token.NewFileSet(), sourcePath, nil, parser.ImportsOnly|parser.AllErrors)
275
+ if file == nil {
276
+ var modules []string
277
+ for _, line := range lines {
278
+ modules = append(modules, importedModulesFromLine(line)...)
279
+ }
280
+ return modules
281
+ }
282
+ var modules []string
283
+ for _, spec := range file.Imports {
284
+ module, err := strconv.Unquote(spec.Path.Value)
285
+ if err != nil || !addedLinesContainImport(lines, spec.Path.Value, module) {
286
+ continue
287
+ }
288
+ modules = append(modules, module)
289
+ }
290
+ return modules
291
+ }
292
+
293
+ func addedLinesContainImport(lines []string, quoted, module string) bool {
294
+ for _, line := range lines {
295
+ if strings.Contains(line, quoted) || strings.Contains(line, "`"+module+"`") {
296
+ return true
297
+ }
298
+ }
299
+ return false
300
+ }
301
+
302
+ func pythonModulesFromLine(line string) []string {
303
+ line = strings.TrimSpace(stripConfigComment(line, '#'))
304
+ if line == "" {
305
+ return nil
306
+ }
307
+ var modules []string
308
+ for statement := range strings.SplitSeq(line, ";") {
309
+ statement = strings.TrimSpace(statement)
310
+ if match := pythonFrom.FindStringSubmatch(statement); len(match) > 1 {
311
+ if module := topLevelPythonModule(match[1]); module != "" {
312
+ modules = append(modules, module)
313
+ }
314
+ continue
315
+ }
316
+ if !strings.HasPrefix(statement, "import ") {
317
+ continue
318
+ }
319
+ for imported := range strings.SplitSeq(strings.TrimPrefix(statement, "import "), ",") {
320
+ fields := strings.Fields(strings.TrimSpace(imported))
321
+ if len(fields) == 0 {
322
+ continue
323
+ }
324
+ if module := topLevelPythonModule(fields[0]); module != "" {
325
+ modules = append(modules, module)
326
+ }
327
+ }
328
+ }
329
+ return modules
330
+ }
331
+
332
+ func topLevelPythonModule(module string) string {
333
+ module = strings.TrimSpace(module)
334
+ if module == "" || strings.HasPrefix(module, ".") {
335
+ return ""
336
+ }
337
+ return strings.SplitN(module, ".", 2)[0]
338
+ }
339
+
340
+ func rustModulesFromLine(line string) []string {
341
+ line = stripConfigComment(line, '/')
342
+ if match := rustUse.FindStringSubmatch(line); len(match) > 1 {
343
+ return []string{match[1]}
344
+ }
345
+ if match := rustExtern.FindStringSubmatch(line); len(match) > 1 {
346
+ return []string{match[1]}
347
+ }
348
+ return nil
349
+ }
350
+
351
+ func isGoStandardLibrary(module string) bool {
352
+ return strings.Contains(" C archive/tar archive/zip bufio bytes cmp compress/bzip2 compress/flate compress/gzip compress/lzw compress/zlib container/heap container/list container/ring context crypto crypto/aes crypto/cipher crypto/des crypto/dsa crypto/ecdh crypto/ecdsa crypto/ed25519 crypto/elliptic crypto/fips140 crypto/hkdf crypto/hmac crypto/hpke crypto/md5 crypto/mlkem crypto/mlkem/mlkemtest crypto/pbkdf2 crypto/rand crypto/rc4 crypto/rsa crypto/sha1 crypto/sha256 crypto/sha3 crypto/sha512 crypto/subtle crypto/tls crypto/x509 crypto/x509/pkix database/sql database/sql/driver debug/buildinfo debug/dwarf debug/elf debug/gosym debug/macho debug/pe debug/plan9obj embed encoding encoding/ascii85 encoding/asn1 encoding/base32 encoding/base64 encoding/binary encoding/csv encoding/gob encoding/hex encoding/json encoding/pem encoding/xml errors expvar flag fmt go/ast go/build go/build/constraint go/constant go/doc go/doc/comment go/format go/importer go/parser go/printer go/scanner go/token go/types go/version hash hash/adler32 hash/crc32 hash/crc64 hash/fnv hash/maphash html html/template image image/color image/color/palette image/draw image/gif image/jpeg image/png index/suffixarray io io/fs io/ioutil iter log log/slog log/syslog maps math math/big math/bits math/cmplx math/rand math/rand/v2 mime mime/multipart mime/quotedprintable net net/http net/http/cgi net/http/cookiejar net/http/fcgi net/http/httptest net/http/httptrace net/http/httputil net/http/pprof net/mail net/netip net/rpc net/rpc/jsonrpc net/smtp net/textproto net/url os os/exec os/signal os/user path path/filepath plugin reflect regexp regexp/syntax runtime runtime/cgo runtime/coverage runtime/debug runtime/metrics runtime/pprof runtime/race runtime/trace slices sort strconv strings structs sync sync/atomic syscall testing testing/cryptotest testing/fstest testing/iotest testing/quick testing/slogtest testing/synctest text/scanner text/tabwriter text/template text/template/parse time time/tzdata unicode unicode/utf16 unicode/utf8 unique unsafe weak ", " "+module+" ")
353
+ }
354
+
355
+ func goImportResolved(gitRoot, sourcePath, module string) (bool, bool) {
356
+ manifest := nearestFile(gitRoot, sourcePath, "go.mod")
357
+ if manifest == "" {
358
+ return false, false
359
+ }
360
+ data, err := os.ReadFile(manifest)
361
+ if err != nil {
362
+ return false, true
363
+ }
364
+ localModule, required, ok := parseGoMod(string(data))
365
+ if !ok {
366
+ return false, true
367
+ }
368
+ if modulePathContains(localModule, module) {
369
+ return true, true
370
+ }
371
+ for _, dependency := range required {
372
+ if modulePathContains(dependency, module) {
373
+ return true, true
374
+ }
375
+ }
376
+ return false, true
377
+ }
378
+
379
+ func parseGoMod(data string) (string, []string, bool) {
380
+ var module string
381
+ var required []string
382
+ inRequireBlock := false
383
+ for rawLine := range strings.SplitSeq(data, "\n") {
384
+ line := strings.TrimSpace(strings.SplitN(rawLine, "//", 2)[0])
385
+ if line == "" {
386
+ continue
116
387
  }
117
- for _, name := range importedModulesFromLine(line) {
118
- name = normalizeImportedPackage(name)
119
- if name == "" {
388
+ if inRequireBlock {
389
+ if line == ")" {
390
+ inRequireBlock = false
120
391
  continue
121
392
  }
122
- if _, ok := seen[name]; !ok {
123
- seen[name] = struct{}{}
124
- pkgs = append(pkgs, name)
393
+ if fields := strings.Fields(line); len(fields) >= 2 {
394
+ required = append(required, strings.Trim(fields[0], `"`))
395
+ }
396
+ continue
397
+ }
398
+ fields := strings.Fields(line)
399
+ if len(fields) >= 2 && fields[0] == "module" {
400
+ module = strings.Trim(fields[1], `"`)
401
+ }
402
+ if len(fields) >= 2 && fields[0] == "require" {
403
+ if fields[1] == "(" {
404
+ inRequireBlock = true
405
+ } else if len(fields) >= 3 {
406
+ required = append(required, strings.Trim(fields[1], `"`))
407
+ }
408
+ }
409
+ }
410
+ return module, required, module != "" && !inRequireBlock
411
+ }
412
+
413
+ func modulePathContains(module, imported string) bool {
414
+ return module != "" && (imported == module || strings.HasPrefix(imported, module+"/"))
415
+ }
416
+
417
+ func normalizeDistributionName(name string) string {
418
+ name = strings.ToLower(strings.TrimSpace(name))
419
+ var normalized strings.Builder
420
+ separator := false
421
+ for _, char := range name {
422
+ if char == '-' || char == '_' || char == '.' {
423
+ separator = true
424
+ continue
425
+ }
426
+ if separator && normalized.Len() > 0 {
427
+ normalized.WriteByte('-')
428
+ }
429
+ separator = false
430
+ normalized.WriteRune(char)
431
+ }
432
+ return normalized.String()
433
+ }
434
+
435
+ func isPythonStandardLibrary(module string) bool {
436
+ return strings.Contains(" __future__ abc aifc argparse array ast asynchat asyncio asyncore atexit audioop base64 bdb binascii bisect builtins bz2 calendar cgi cgitb chunk cmath cmd code codecs codeop collections colorsys compileall concurrent configparser contextlib contextvars copy copyreg crypt csv ctypes curses dataclasses datetime dbm decimal difflib dis doctest email encodings ensurepip enum errno faulthandler fcntl filecmp fileinput fnmatch fractions ftplib functools gc getopt getpass gettext glob graphlib grp gzip hashlib heapq hmac html http idlelib imaplib imghdr importlib inspect io ipaddress itertools json keyword linecache locale logging lzma mailbox mailcap marshal math mimetypes mmap modulefinder multiprocessing netrc nis nntplib numbers operator optparse os pathlib pdb pickle pickletools pipes pkgutil platform plistlib poplib posix pprint profile pstats pty pwd py_compile pyclbr pydoc queue quopri random re readline reprlib resource rlcompleter runpy sched secrets select selectors shelve shlex shutil signal site smtpd smtplib sndhdr socket socketserver sqlite3 ssl stat statistics string stringprep struct subprocess sunau symtable sys sysconfig tabnanny tarfile telnetlib tempfile termios textwrap threading time timeit tkinter token tokenize tomllib trace traceback tracemalloc tty turtle turtledemo types typing unicodedata unittest urllib uu uuid venv warnings wave weakref webbrowser winreg winsound wsgiref xdrlib xml xmlrpc zipapp zipfile zipimport zlib zoneinfo ", " "+module+" ")
437
+ }
438
+
439
+ func pythonImportResolved(gitRoot, sourcePath, module string) (bool, bool) {
440
+ manifests := nearestFiles(gitRoot, sourcePath, []string{"requirements.txt", "pyproject.toml", "Pipfile"})
441
+ if len(manifests) == 0 {
442
+ return false, false
443
+ }
444
+ declared := map[string]struct{}{}
445
+ for _, manifest := range manifests {
446
+ data, err := os.ReadFile(manifest)
447
+ if err != nil {
448
+ continue
449
+ }
450
+ var packages map[string]struct{}
451
+ switch filepath.Base(manifest) {
452
+ case "requirements.txt":
453
+ packages = requirementPackages(string(data))
454
+ case "pyproject.toml":
455
+ packages, _ = pyprojectPackages(string(data))
456
+ case "Pipfile":
457
+ packages = pipfilePackages(string(data))
458
+ }
459
+ for pkg := range packages {
460
+ declared[pkg] = struct{}{}
461
+ }
462
+ }
463
+ _, ok := declared[module]
464
+ return ok, true
465
+ }
466
+
467
+ func requirementPackages(data string) map[string]struct{} {
468
+ packages := map[string]struct{}{}
469
+ for rawLine := range strings.SplitSeq(data, "\n") {
470
+ addRequirementPackage(packages, stripConfigComment(rawLine, '#'))
471
+ }
472
+ return packages
473
+ }
474
+
475
+ func addRequirementPackage(packages map[string]struct{}, value string) {
476
+ value = strings.TrimSpace(value)
477
+ if value == "" || strings.HasPrefix(value, "-") {
478
+ return
479
+ }
480
+ if match := requirement.FindStringSubmatch(value); len(match) > 1 {
481
+ packages[normalizeDistributionName(match[1])] = struct{}{}
482
+ }
483
+ }
484
+
485
+ func pyprojectPackages(data string) (map[string]struct{}, bool) {
486
+ packages := map[string]struct{}{}
487
+ section := ""
488
+ arrayOpen := false
489
+ for rawLine := range strings.SplitSeq(data, "\n") {
490
+ line := strings.TrimSpace(stripConfigComment(rawLine, '#'))
491
+ if line == "" {
492
+ continue
493
+ }
494
+ if arrayOpen {
495
+ addQuotedRequirements(packages, line)
496
+ if containsUnquotedByte(line, ']') {
497
+ arrayOpen = false
498
+ }
499
+ continue
500
+ }
501
+ if strings.HasPrefix(line, "[") {
502
+ if !strings.HasSuffix(line, "]") {
503
+ return nil, false
504
+ }
505
+ section = strings.TrimSpace(strings.Trim(line, "[]"))
506
+ continue
507
+ }
508
+ key, value, ok := tomlAssignment(line)
509
+ if !ok {
510
+ continue
511
+ }
512
+ if section == "tool.poetry.dependencies" || strings.HasPrefix(section, "tool.poetry.group.") && strings.HasSuffix(section, ".dependencies") {
513
+ if normalizeDistributionName(key) != "python" {
514
+ packages[normalizeDistributionName(key)] = struct{}{}
515
+ }
516
+ continue
517
+ }
518
+ arrayDependency := section == "project" && key == "dependencies" || section == "project.optional-dependencies" || section == "dependency-groups"
519
+ if !arrayDependency {
520
+ continue
521
+ }
522
+ addQuotedRequirements(packages, value)
523
+ arrayOpen = containsUnquotedByte(value, '[') && !containsUnquotedByte(value, ']')
524
+ }
525
+ if arrayOpen {
526
+ return nil, false
527
+ }
528
+ return packages, true
529
+ }
530
+
531
+ func addQuotedRequirements(packages map[string]struct{}, value string) {
532
+ for _, match := range quotedValue.FindAllStringSubmatch(value, -1) {
533
+ if len(match) > 1 {
534
+ addRequirementPackage(packages, match[1])
535
+ }
536
+ }
537
+ }
538
+
539
+ func pipfilePackages(data string) map[string]struct{} {
540
+ packages := map[string]struct{}{}
541
+ section := ""
542
+ for rawLine := range strings.SplitSeq(data, "\n") {
543
+ line := strings.TrimSpace(stripConfigComment(rawLine, '#'))
544
+ if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
545
+ section = strings.TrimSpace(strings.Trim(line, "[]"))
546
+ continue
547
+ }
548
+ if section != "packages" && section != "dev-packages" {
549
+ continue
550
+ }
551
+ if key, _, ok := tomlAssignment(line); ok {
552
+ packages[normalizeDistributionName(key)] = struct{}{}
553
+ }
554
+ }
555
+ return packages
556
+ }
557
+
558
+ func tomlAssignment(line string) (string, string, bool) {
559
+ parts := strings.SplitN(line, "=", 2)
560
+ if len(parts) != 2 {
561
+ return "", "", false
562
+ }
563
+ key := strings.Trim(strings.TrimSpace(parts[0]), `"'`)
564
+ return key, strings.TrimSpace(parts[1]), key != ""
565
+ }
566
+
567
+ func stripConfigComment(line string, marker byte) string {
568
+ quote := byte(0)
569
+ escaped := false
570
+ for index := 0; index < len(line); index++ {
571
+ char := line[index]
572
+ if escaped {
573
+ escaped = false
574
+ continue
575
+ }
576
+ if char == '\\' && quote == '"' {
577
+ escaped = true
578
+ continue
579
+ }
580
+ if char == '\'' || char == '"' {
581
+ if quote == 0 {
582
+ quote = char
583
+ } else if quote == char {
584
+ quote = 0
585
+ }
586
+ continue
587
+ }
588
+ if quote == 0 && char == marker {
589
+ if marker != '/' || index+1 < len(line) && line[index+1] == '/' {
590
+ return line[:index]
591
+ }
592
+ }
593
+ }
594
+ return line
595
+ }
596
+
597
+ func containsUnquotedByte(value string, target byte) bool {
598
+ quote := byte(0)
599
+ escaped := false
600
+ for index := 0; index < len(value); index++ {
601
+ char := value[index]
602
+ if escaped {
603
+ escaped = false
604
+ continue
605
+ }
606
+ if char == '\\' && quote == '"' {
607
+ escaped = true
608
+ continue
609
+ }
610
+ if char == '\'' || char == '"' {
611
+ if quote == 0 {
612
+ quote = char
613
+ } else if quote == char {
614
+ quote = 0
615
+ }
616
+ continue
617
+ }
618
+ if quote == 0 && char == target {
619
+ return true
620
+ }
621
+ }
622
+ return false
623
+ }
624
+
625
+ func normalizeRustCrate(crate string) string {
626
+ crate = strings.TrimPrefix(strings.TrimSpace(crate), "r#")
627
+ crate = strings.SplitN(crate, "::", 2)[0]
628
+ return strings.ReplaceAll(crate, "-", "_")
629
+ }
630
+
631
+ func isRustBuiltin(crate string) bool {
632
+ switch crate {
633
+ case "std", "core", "alloc", "crate", "self", "super":
634
+ return true
635
+ default:
636
+ return false
637
+ }
638
+ }
639
+
640
+ func rustImportResolved(gitRoot, sourcePath, crate string) (bool, bool) {
641
+ manifest := nearestFile(gitRoot, sourcePath, "Cargo.toml")
642
+ if manifest == "" {
643
+ return false, false
644
+ }
645
+ data, err := os.ReadFile(manifest)
646
+ if err != nil {
647
+ return false, true
648
+ }
649
+ localCrate, dependencies := cargoPackages(string(data))
650
+ if crate == localCrate {
651
+ return true, true
652
+ }
653
+ _, ok := dependencies[crate]
654
+ return ok, true
655
+ }
656
+
657
+ func cargoPackages(data string) (string, map[string]struct{}) {
658
+ dependencies := map[string]struct{}{}
659
+ section := ""
660
+ localCrate := ""
661
+ for rawLine := range strings.SplitSeq(data, "\n") {
662
+ line := strings.TrimSpace(stripConfigComment(rawLine, '#'))
663
+ if line == "" {
664
+ continue
665
+ }
666
+ if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
667
+ section = strings.TrimSpace(strings.Trim(line, "[]"))
668
+ if dependency := cargoDependencyTable(section); dependency != "" {
669
+ dependencies[dependency] = struct{}{}
670
+ }
671
+ continue
672
+ }
673
+ key, value, ok := tomlAssignment(line)
674
+ if !ok {
675
+ continue
676
+ }
677
+ if section == "package" && key == "name" {
678
+ localCrate = normalizeRustCrate(strings.Trim(value, `"'`))
679
+ continue
680
+ }
681
+ if isCargoDependencySection(section) && value != "" {
682
+ dependencies[normalizeRustCrate(key)] = struct{}{}
683
+ }
684
+ }
685
+ return localCrate, dependencies
686
+ }
687
+
688
+ func isCargoDependencySection(section string) bool {
689
+ return section == "dependencies" || section == "dev-dependencies" || section == "build-dependencies" ||
690
+ strings.HasPrefix(section, "target.") && (strings.HasSuffix(section, ".dependencies") || strings.HasSuffix(section, ".dev-dependencies") || strings.HasSuffix(section, ".build-dependencies"))
691
+ }
692
+
693
+ func cargoDependencyTable(section string) string {
694
+ for _, prefix := range []string{"dependencies.", "dev-dependencies.", "build-dependencies."} {
695
+ if dependency, ok := strings.CutPrefix(section, prefix); ok {
696
+ return normalizeRustCrate(strings.Trim(dependency, `"'`))
697
+ }
698
+ }
699
+ if strings.HasPrefix(section, "target.") {
700
+ for _, marker := range []string{".dependencies.", ".dev-dependencies.", ".build-dependencies."} {
701
+ if _, dependency, ok := strings.Cut(section, marker); ok {
702
+ return normalizeRustCrate(strings.Trim(dependency, `"'`))
703
+ }
704
+ }
705
+ }
706
+ return ""
707
+ }
708
+
709
+ func nearestFiles(gitRoot, sourcePath string, names []string) []string {
710
+ dir := filepath.Dir(filepath.Join(gitRoot, sourcePath))
711
+ for {
712
+ var manifests []string
713
+ for _, name := range names {
714
+ candidate := filepath.Join(dir, name)
715
+ if isFile(candidate) {
716
+ manifests = append(manifests, candidate)
125
717
  }
126
718
  }
719
+ if len(manifests) > 0 || dir == gitRoot {
720
+ return manifests
721
+ }
722
+ dir = filepath.Dir(dir)
127
723
  }
128
- sort.Strings(pkgs)
129
- return pkgs
130
724
  }
131
725
 
132
726
  func importedModulesFromLine(line string) []string {
@@ -157,19 +751,102 @@ func normalizeImportedPackage(name string) string {
157
751
  return name
158
752
  }
159
753
 
160
- // declaredInManifests reports whether pkg appears as a whole, bounded token
161
- // (case-insensitively) in any manifest — the name matched literally between
162
- // non-identifier characters, so "lodash" does not match "lodasher".
163
- func declaredInManifests(pkg string, manifests []string) bool {
164
- re := regexp.MustCompile(`(?im)(^|[^[:alnum:]_-])` + regexp.QuoteMeta(pkg) + `([^[:alnum:]_-]|$)`)
165
- for _, m := range manifests {
166
- data, err := os.ReadFile(m)
167
- if err != nil {
168
- continue
754
+ func nearestFile(gitRoot, sourcePath, name string) string {
755
+ dir := filepath.Dir(filepath.Join(gitRoot, sourcePath))
756
+ for {
757
+ candidate := filepath.Join(dir, name)
758
+ if isFile(candidate) {
759
+ return candidate
169
760
  }
170
- if re.Match(data) {
761
+ if dir == gitRoot {
762
+ return ""
763
+ }
764
+ dir = filepath.Dir(dir)
765
+ }
766
+ }
767
+
768
+ func packageJSONDeclares(pkg, manifest string) bool {
769
+ data, err := os.ReadFile(manifest)
770
+ if err != nil {
771
+ return false
772
+ }
773
+ var packageJSON struct {
774
+ Dependencies map[string]string `json:"dependencies"`
775
+ DevDependencies map[string]string `json:"devDependencies"`
776
+ PeerDependencies map[string]string `json:"peerDependencies"`
777
+ OptionalDependencies map[string]string `json:"optionalDependencies"`
778
+ }
779
+ if err := json.Unmarshal(data, &packageJSON); err != nil {
780
+ return false
781
+ }
782
+ for _, dependencies := range []map[string]string{
783
+ packageJSON.Dependencies,
784
+ packageJSON.DevDependencies,
785
+ packageJSON.PeerDependencies,
786
+ packageJSON.OptionalDependencies,
787
+ } {
788
+ if _, ok := dependencies[pkg]; ok {
171
789
  return true
172
790
  }
173
791
  }
174
792
  return false
175
793
  }
794
+
795
+ func importMatchesPathAlias(gitRoot, sourcePath, specifier string) bool {
796
+ dir := filepath.Dir(filepath.Join(gitRoot, sourcePath))
797
+ for {
798
+ foundConfig := false
799
+ config := filepath.Join(dir, "tsconfig.json")
800
+ if !isFile(config) {
801
+ config = filepath.Join(dir, "jsconfig.json")
802
+ }
803
+ for _, config := range []string{config, filepath.Join(dir, ".svelte-kit", "tsconfig.json")} {
804
+ if isFile(config) {
805
+ foundConfig = true
806
+ if configMatchesPathAlias(config, specifier) {
807
+ return true
808
+ }
809
+ }
810
+ }
811
+ if foundConfig {
812
+ return false
813
+ }
814
+ if dir == gitRoot {
815
+ return false
816
+ }
817
+ dir = filepath.Dir(dir)
818
+ }
819
+ }
820
+
821
+ func configMatchesPathAlias(configPath, specifier string) bool {
822
+ data, err := os.ReadFile(configPath)
823
+ if err != nil {
824
+ return false
825
+ }
826
+ var config struct {
827
+ CompilerOptions struct {
828
+ Paths map[string][]string `json:"paths"`
829
+ } `json:"compilerOptions"`
830
+ }
831
+ if err := json.Unmarshal(data, &config); err != nil {
832
+ return false
833
+ }
834
+ for alias, targets := range config.CompilerOptions.Paths {
835
+ if len(targets) > 0 && matchesAlias(specifier, alias) {
836
+ return true
837
+ }
838
+ }
839
+ return false
840
+ }
841
+
842
+ func matchesAlias(specifier, alias string) bool {
843
+ if strings.Count(alias, "*") == 0 {
844
+ return specifier == alias
845
+ }
846
+ if strings.Count(alias, "*") != 1 {
847
+ return false
848
+ }
849
+ parts := strings.SplitN(alias, "*", 2)
850
+ return len(specifier) >= len(parts[0])+len(parts[1]) &&
851
+ strings.HasPrefix(specifier, parts[0]) && strings.HasSuffix(specifier, parts[1])
852
+ }