@ttsc/evidence 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/lib/index.d.ts +65 -0
  4. package/lib/index.js +87 -0
  5. package/lib/internal/loadPrismaModels.d.ts +1 -0
  6. package/lib/internal/loadPrismaModels.js +197 -0
  7. package/lib/internal/loadSwaggerOperations.d.ts +1 -0
  8. package/lib/internal/loadSwaggerOperations.js +156 -0
  9. package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
  10. package/lib/internal/normalizeSwaggerDocument.js +7 -0
  11. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
  12. package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
  13. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
  14. package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
  15. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
  16. package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
  17. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
  18. package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
  19. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
  20. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
  21. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
  22. package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
  23. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
  24. package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
  25. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
  26. package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
  27. package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
  28. package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
  29. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
  30. package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
  31. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
  32. package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
  33. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
  34. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
  35. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
  36. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
  37. package/lib/structures/index.d.ts +13 -0
  38. package/lib/structures/index.js +29 -0
  39. package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
  40. package/lib/typings/ITtscLintContributorRules.js +2 -0
  41. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
  42. package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
  43. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
  44. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
  45. package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
  46. package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
  47. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
  48. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
  49. package/lib/typings/index.d.ts +5 -0
  50. package/lib/typings/index.js +21 -0
  51. package/native/base.go +298 -0
  52. package/native/config.go +798 -0
  53. package/native/declaration.go +240 -0
  54. package/native/documented.go +353 -0
  55. package/native/exports.go +484 -0
  56. package/native/glob.go +207 -0
  57. package/native/graph.go +1409 -0
  58. package/native/health.go +45 -0
  59. package/native/hints.go +261 -0
  60. package/native/imports.go +373 -0
  61. package/native/inputs.go +125 -0
  62. package/native/loader.go +424 -0
  63. package/native/markdown.go +423 -0
  64. package/native/model.go +296 -0
  65. package/native/prisma.go +719 -0
  66. package/native/prisma_cache.go +213 -0
  67. package/native/prisma_locator.go +417 -0
  68. package/native/singular.go +369 -0
  69. package/native/swagger.go +331 -0
  70. package/native/swagger_cache.go +274 -0
  71. package/native/todo.go +138 -0
  72. package/native/typescript.go +1199 -0
  73. package/native/typescript_cache.go +96 -0
  74. package/package.json +64 -0
@@ -0,0 +1,274 @@
1
+ package evidence
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "os"
7
+ "strings"
8
+ "sync"
9
+ )
10
+
11
+ // swaggerCacheLimit bounds the cache so a resident host cannot grow without
12
+ // end. A project configures a handful of Swagger documents, not hundreds, so
13
+ // this is a backstop against a pathological configuration rather than a tuning
14
+ // knob — and the oldest entry is dropped rather than the whole map cleared, so
15
+ // a project sitting exactly on the limit still gets hits.
16
+ const swaggerCacheLimit = 64
17
+
18
+ // swaggerDocuments remembers normalization outcomes by document content.
19
+ //
20
+ // This is the rule's own state, not the project state the host publishes. The
21
+ // `@ttsc/lint` contributor contract forbids caching a `SetState` value across Program
22
+ // cycles because the host owns that wrapper's lifetime; nothing there governs
23
+ // memory a rule keeps for itself, and the parallel walk never reaches this
24
+ // because a project rule runs before any file rule. The mutex is not for that
25
+ // walk — it is because a resident host may hold several projects at once, and
26
+ // paying for a lock on a path that skips a process spawn is not a trade worth
27
+ // thinking about.
28
+ //
29
+ // The entry lives as long as the process does. Both hosts that reuse it are
30
+ // line-protocol loops in one process, so exit disposes it; the contributor API
31
+ // exposes no earlier disposal hook, which is why the bound above exists rather
32
+ // than being a tuning knob.
33
+ var swaggerDocuments = newSwaggerCache()
34
+
35
+ // swaggerRemoteDocuments remembers a URL's operations for the process lifetime,
36
+ // keyed by the address rather than by content.
37
+ //
38
+ // A remote document has no key without fetching it, so it cannot be revalidated
39
+ // the way a local file is. The choice is therefore not "cache or revalidate" but
40
+ // "fetch once or fetch forever", and a resident session that refetched on every
41
+ // rebuild would put a network round trip on the edit loop — and make an editor
42
+ // depend on connectivity to report anything at all.
43
+ //
44
+ // The freshness this gives up is real and bounded by the process: a served
45
+ // document that changes mid-session is not seen until the session restarts. A
46
+ // one-shot `ttsc check` is a fresh process, so it always fetches.
47
+ //
48
+ // **A rejection is never remembered here.** The key is the address, so nothing
49
+ // an author can do would clear it — one refused connection would poison the URL
50
+ // for the whole session with no edit able to invalidate the entry. Failure stays
51
+ // per-evaluation, which is also what makes a transient outage recoverable
52
+ // without restarting the editor.
53
+ var swaggerRemoteDocuments = newSwaggerCache()
54
+
55
+ // swaggerDocumentOutcome is what one document's bytes normalized to.
56
+ //
57
+ // Failure is remembered as deliberately as success. A document the normalizer
58
+ // rejects is one the author is midway through fixing, and while they fix it
59
+ // every unrelated TypeScript save would otherwise pay a fresh process start to
60
+ // be told the same thing again. Nothing is cached forever: repairing the
61
+ // document changes its bytes, which changes the key, which misses.
62
+ //
63
+ // Rejected is a separate flag rather than a non-empty Problem, because the
64
+ // reason is the normalizer's string and this rule does not get to assume it is
65
+ // non-empty. Inferring rejection from the message would turn a reason-less
66
+ // failure into zero operations and no diagnostic — a rejected document that
67
+ // reads exactly like an empty but passing one, which is the shape
68
+ // `test_evidence_graph_reports_swagger_source_failures` exists to forbid.
69
+ type swaggerDocumentOutcome struct {
70
+ Operations []swaggerOperation
71
+ Rejected bool
72
+ Problem string
73
+ }
74
+
75
+ func newSwaggerCache() *swaggerCache {
76
+ return &swaggerCache{entries: map[string]swaggerDocumentOutcome{}}
77
+ }
78
+
79
+ type swaggerCache struct {
80
+ mutex sync.Mutex
81
+ entries map[string]swaggerDocumentOutcome
82
+ order []string
83
+ }
84
+
85
+ func (cache *swaggerCache) lookup(digest string) (swaggerDocumentOutcome, bool) {
86
+ if digest == "" {
87
+ return swaggerDocumentOutcome{}, false
88
+ }
89
+ cache.mutex.Lock()
90
+ defer cache.mutex.Unlock()
91
+ outcome, hit := cache.entries[digest]
92
+ if !hit {
93
+ return swaggerDocumentOutcome{}, false
94
+ }
95
+ // Copied out because the caller builds units from it while another cycle
96
+ // may be reading the same entry.
97
+ return swaggerDocumentOutcome{
98
+ Operations: append([]swaggerOperation(nil), outcome.Operations...),
99
+ Rejected: outcome.Rejected,
100
+ Problem: outcome.Problem,
101
+ }, true
102
+ }
103
+
104
+ func (cache *swaggerCache) store(digest string, outcome swaggerDocumentOutcome) {
105
+ if digest == "" {
106
+ return
107
+ }
108
+ cache.mutex.Lock()
109
+ defer cache.mutex.Unlock()
110
+ if _, exists := cache.entries[digest]; exists {
111
+ return
112
+ }
113
+ if len(cache.order) >= swaggerCacheLimit {
114
+ delete(cache.entries, cache.order[0])
115
+ cache.order = cache.order[1:]
116
+ }
117
+ cache.entries[digest] = swaggerDocumentOutcome{
118
+ Operations: append([]swaggerOperation(nil), outcome.Operations...),
119
+ Rejected: outcome.Rejected,
120
+ Problem: outcome.Problem,
121
+ }
122
+ cache.order = append(cache.order, digest)
123
+ }
124
+
125
+ // swaggerContentDigests hashes each local source's bytes.
126
+ //
127
+ // The digest is the whole cache key, which is what makes staleness structural
128
+ // rather than improbable: identical bytes normalize to identical operations, so
129
+ // a hit can only return what the current file means. An edit, a truncation, or
130
+ // a same-length replacement all change the digest and miss. A missing or
131
+ // unreadable file yields no digest and is normalized as before, which keeps the
132
+ // normalizer's own diagnostic for it.
133
+ //
134
+ // An HTTP(S) source never participates. A URL has no validator without a
135
+ // fetch, and the fetch is most of what the normalizer costs, so a remote
136
+ // document cannot be shown unchanged without paying the price of finding out.
137
+ func swaggerContentDigests(root string, sources []string) map[string]string {
138
+ digests := map[string]string{}
139
+ for _, source := range sources {
140
+ digest := swaggerContentDigest(root, source)
141
+ if digest != "" {
142
+ digests[source] = digest
143
+ }
144
+ }
145
+ return digests
146
+ }
147
+
148
+ func swaggerContentDigest(root string, source string) string {
149
+ if isRemoteSwaggerSource(source) {
150
+ return ""
151
+ }
152
+ content, err := os.ReadFile(swaggerSourcePath(root, source))
153
+ if err != nil {
154
+ return ""
155
+ }
156
+ return swaggerDigestOf(content)
157
+ }
158
+
159
+ func swaggerDigestOf(content []byte) string {
160
+ sum := sha256.Sum256(content)
161
+ return hex.EncodeToString(sum[:])
162
+ }
163
+
164
+ // swaggerSourcePath locates a local document, which may sit outside the project.
165
+ func swaggerSourcePath(root string, source string) string {
166
+ return resolveProjectPath(root, source)
167
+ }
168
+
169
+ // rememberSwaggerDocument records an outcome under the bytes it was produced
170
+ // from, as reported by the process that produced it.
171
+ //
172
+ // The digest is the normalizer's own, never the one this process took
173
+ // beforehand. The normalizer opens the file again, in another process, after
174
+ // this one read it — so keying on the earlier digest would let a write landing
175
+ // inside that window bind one document's operations to another document's
176
+ // bytes. That is a hit which answers with the wrong document and keeps doing
177
+ // so, which is the one failure a cache must not be able to produce.
178
+ //
179
+ // A source is skipped when the normalizer reports no digest, which is how a
180
+ // remote document stays out of the cache no matter what it returns.
181
+ func rememberSwaggerDocument(
182
+ source string,
183
+ digest string,
184
+ outcome swaggerDocumentOutcome,
185
+ ) {
186
+ if isRemoteSwaggerSource(source) {
187
+ if !outcome.Rejected {
188
+ swaggerRemoteDocuments.store(source, outcome)
189
+ }
190
+ return
191
+ }
192
+ if digest == "" {
193
+ return
194
+ }
195
+ swaggerDocuments.store(digest, outcome)
196
+ }
197
+
198
+ // lookupSwaggerDocument answers from whichever memory owns this source.
199
+ //
200
+ // The two are separate caches rather than one map with two key shapes, because
201
+ // they answer different questions: a local entry means "these bytes normalize
202
+ // to this", which stays true forever, while a remote entry means "this address
203
+ // answered this, once", which is a decision about freshness rather than a fact
204
+ // about content.
205
+ func lookupSwaggerDocument(
206
+ source string,
207
+ digest string,
208
+ ) (swaggerDocumentOutcome, bool) {
209
+ if isRemoteSwaggerSource(source) {
210
+ return swaggerRemoteDocuments.lookup(source)
211
+ }
212
+ return swaggerDocuments.lookup(digest)
213
+ }
214
+
215
+ // swaggerUnitsFromOutcome rebuilds one source's units from a remembered
216
+ // outcome.
217
+ //
218
+ // Units are rebuilt per source rather than remembered, because a unit carries
219
+ // its source in its identity while the operations do not. That is also what
220
+ // lets two sources holding identical bytes share one entry: what was cached is
221
+ // a property of the document, not of where it was found.
222
+ func swaggerUnitsFromOutcome(
223
+ source string,
224
+ inventory *artifactInventory,
225
+ outcome swaggerDocumentOutcome,
226
+ ) []string {
227
+ if inventory == nil {
228
+ return nil
229
+ }
230
+ if outcome.Rejected {
231
+ message := swaggerNormalizationFailure(source, outcome.Problem)
232
+ inventory.LoadFailed = true
233
+ inventory.Problems = append(inventory.Problems, inventoryProblem{
234
+ Symbol: "operation",
235
+ Message: message,
236
+ })
237
+ return []string{message}
238
+ }
239
+ problems := []string{}
240
+ for _, operation := range outcome.Operations {
241
+ unit, problem := swaggerOperationUnit(source, operation)
242
+ if problem != "" {
243
+ inventory.Problems = append(inventory.Problems, inventoryProblem{
244
+ Symbol: "operation",
245
+ Message: problem,
246
+ })
247
+ problems = append(problems, problem)
248
+ continue
249
+ }
250
+ inventory.Units = append(inventory.Units, unit)
251
+ }
252
+ sortUnits(inventory.Units)
253
+ return problems
254
+ }
255
+
256
+ // swaggerNormalizationFailure words a rejected source identically whether the
257
+ // rejection arrived from the normalizer this cycle or from memory, so a reader
258
+ // cannot tell the two apart and has no reason to want to.
259
+ //
260
+ // A reason-less rejection still gets a sentence. The reason comes from another
261
+ // process and this rule cannot require it to be non-empty, and a diagnostic
262
+ // that trails off after a colon reads like a formatting bug rather than a
263
+ // broken document.
264
+ func swaggerNormalizationFailure(source string, message string) string {
265
+ reason := strings.TrimSpace(message)
266
+ if reason == "" {
267
+ reason = "the normalizer reported no reason"
268
+ }
269
+ return "Evidence graph could not normalize Swagger source '" +
270
+ displaySwaggerSource(source) +
271
+ "' to @typia/interface OpenApi.IDocument: " +
272
+ reason +
273
+ ". Fix the file or URL so @typia/utils can upgrade it."
274
+ }
package/native/todo.go ADDED
@@ -0,0 +1,138 @@
1
+ package evidence
2
+
3
+ import (
4
+ "strings"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+
8
+ "github.com/samchon/ttsc/packages/lint/rule"
9
+ )
10
+
11
+ // todoRule reports every JSDoc '@todo' tag in a checked file.
12
+ //
13
+ // A stub enters the world carrying a '@todo' that names what its realization
14
+ // still owes, and no other rule ever calls that debt: the graph judges
15
+ // citations, `evidence/documented` judges presence, and a block whose only
16
+ // content is a '@todo' satisfies both. A remaining tag is therefore an
17
+ // unrealized contract shipping as done, so the rule turns each one into a
18
+ // finding — the realize ledger, read straight out of the build.
19
+ //
20
+ // There is no selection to configure. A debt on a local helper is as unrealized
21
+ // as one on an export, so every declaration's block is read, and the rule takes
22
+ // no options; per-directory scoping belongs in the outer `files` setting of
23
+ // `lint.config.ts`, and the host refuses a configured options object on the
24
+ // rule's behalf.
25
+ type todoRule struct{}
26
+
27
+ func (todoRule) Name() string { return todoRuleName }
28
+
29
+ func (todoRule) Visits() []shimast.Kind {
30
+ return []shimast.Kind{shimast.KindSourceFile}
31
+ }
32
+
33
+ func (todoRule) NeedsTypeChecker() bool { return false }
34
+
35
+ func (todoRule) VisitsDeclarationFiles() bool { return false }
36
+
37
+ func (todoRule) AcceptsTtscLintOptions() bool { return false }
38
+
39
+ func (todoRule) Check(ctx *rule.Context, node *shimast.Node) {
40
+ if ctx == nil || ctx.File == nil || node == nil {
41
+ return
42
+ }
43
+ if node.Kind != shimast.KindSourceFile {
44
+ return
45
+ }
46
+ content := ctx.File.Text()
47
+ // One physical block is one ledger entry. TypeScript cascades a leading
48
+ // block onto nested nodes — a variable statement and each of its
49
+ // declarations all answer for one comment — so blocks are deduplicated by
50
+ // position. The walk visits parents before children, which anchors each
51
+ // finding on the outermost declaration the block precedes: the node whose
52
+ // leading trivia the block actually is, and the one `Report` underlines
53
+ // correctly because it skips that trivia.
54
+ seen := map[int]bool{}
55
+ walkTypeScriptNode(node, func(current *shimast.Node) {
56
+ for _, doc := range current.JSDoc(ctx.File) {
57
+ if doc == nil || doc.Pos() < 0 || doc.End() > len(content) {
58
+ continue
59
+ }
60
+ if seen[doc.Pos()] {
61
+ continue
62
+ }
63
+ seen[doc.Pos()] = true
64
+ for _, entry := range todoEntries(content[doc.Pos():doc.End()]) {
65
+ ctx.Report(current, todoMessage(entry))
66
+ }
67
+ }
68
+ })
69
+ }
70
+
71
+ func init() { rule.Register(todoRule{}) }
72
+
73
+ func todoMessage(entry string) string {
74
+ found := "Unrealized '@todo'"
75
+ if entry != "" {
76
+ found += ": '" + entry + "'"
77
+ }
78
+ return found +
79
+ ". A '@todo' tag names work this declaration still owes, so the contract it promises is not realized yet. Realize the declaration and remove the tag."
80
+ }
81
+
82
+ // todoEntries lists the text of every '@todo' tag one JSDoc block carries.
83
+ //
84
+ // The scan mirrors the declaration parser's shape: strip the block decoration,
85
+ // walk lines, and let any other '@'-opening line close the entry above it, so a
86
+ // '@todo' shares a block with '@param' or '@evidence' without swallowing them.
87
+ // The tag name matches case-insensitively, because '@TODO' is the same promise
88
+ // shouted.
89
+ func todoEntries(comment string) []string {
90
+ comment = strings.TrimSpace(comment)
91
+ comment = strings.TrimPrefix(comment, "/**")
92
+ comment = strings.TrimPrefix(comment, "/*")
93
+ comment = strings.TrimSuffix(comment, "*/")
94
+ entries := []string{}
95
+ var pending []string
96
+ flush := func() {
97
+ if pending == nil {
98
+ return
99
+ }
100
+ entries = append(entries, strings.TrimSpace(strings.Join(pending, " ")))
101
+ pending = nil
102
+ }
103
+ for _, rawLine := range strings.Split(comment, "\n") {
104
+ line := strings.TrimSpace(rawLine)
105
+ line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
106
+ if remainder, opened := todoLine(line); opened {
107
+ flush()
108
+ pending = []string{remainder}
109
+ continue
110
+ }
111
+ if strings.HasPrefix(line, "@") {
112
+ flush()
113
+ continue
114
+ }
115
+ if pending != nil && line != "" {
116
+ pending = append(pending, line)
117
+ }
118
+ }
119
+ flush()
120
+ return entries
121
+ }
122
+
123
+ // todoLine reports whether a line opens a '@todo' tag, and with what remainder.
124
+ //
125
+ // The boundary check keeps a longer tag out: '@todos' is some other tool's tag,
126
+ // not a shouting variant of this one, and matching it would report a debt the
127
+ // author never recorded.
128
+ func todoLine(line string) (string, bool) {
129
+ const marker = "@todo"
130
+ if len(line) < len(marker) || !strings.EqualFold(line[:len(marker)], marker) {
131
+ return "", false
132
+ }
133
+ remainder := line[len(marker):]
134
+ if remainder != "" && remainder[0] != ' ' && remainder[0] != '\t' {
135
+ return "", false
136
+ }
137
+ return strings.TrimSpace(remainder), true
138
+ }