@ttsc/evidence 0.28.6 → 0.30.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.
@@ -1,3 +1,4 @@
1
+ import type { TtscLintSeverity } from "@ttsc/lint";
1
2
  import type { ITtscEvidenceGraphReference } from "./ITtscEvidenceGraphReference";
2
3
  /**
3
4
  * What every claim declares: which artifact kind makes the claim, where its
@@ -21,6 +22,12 @@ export interface ITtscEvidenceGraphClaimBase<Type extends string> {
21
22
  * configuration entries.
22
23
  */
23
24
  name?: string;
25
+ /**
26
+ * Diagnostic level for this claim. Omit it or use `undefined` to inherit the
27
+ * outer `evidence/graph` rule level. A reference may override this level
28
+ * again. `"off"` disables the entire claim, including its references.
29
+ */
30
+ severity?: TtscLintSeverity | undefined;
24
31
  /**
25
32
  * Excludes this claim from graph loading and evaluation.
26
33
  *
@@ -1,3 +1,4 @@
1
+ import type { TtscLintSeverity } from "@ttsc/lint";
1
2
  /**
2
3
  * What every evidence population declares: which artifact kind it materializes,
3
4
  * and how strictly the owning claim must acknowledge it.
@@ -25,6 +26,12 @@
25
26
  export interface ITtscEvidenceGraphReferenceBase<Type extends string> {
26
27
  /** Identifies the artifact kind this population materializes. */
27
28
  type: Type;
29
+ /**
30
+ * Diagnostic level for this reference's obligation. Omit it or use
31
+ * `undefined` to inherit the claim level, then the outer `evidence/graph`
32
+ * rule level. `"off"` disables this reference's population and obligation.
33
+ */
34
+ severity?: TtscLintSeverity | undefined;
28
35
  /**
29
36
  * Whether this reference refuses `@evidenceExclude` as an acknowledgement.
30
37
  *
@@ -13,13 +13,25 @@ import type { ITtscEvidenceGraphReferenceBase } from "./ITtscEvidenceGraphRefere
13
13
  * contracts deserve documentary proof.
14
14
  */
15
15
  export interface ITtscEvidenceGraphTypeScriptReference extends ITtscEvidenceGraphReferenceBase<"typescript"> {
16
+ /**
17
+ * Explicit directory whose TypeScript exports are read from disk.
18
+ *
19
+ * Resolves relative to the lint project's root, or accepts an absolute path.
20
+ * Files patterns select entry modules relative to this directory. Their
21
+ * re-exports must remain inside it. Active Program sources take precedence
22
+ * over disk snapshots. The directory is watched for changes and new files.
23
+ *
24
+ * Cannot combine with package. Omit both to select only the active Program.
25
+ * File-qualified citation paths remain relative to the citing file.
26
+ */
27
+ root?: string;
16
28
  /**
17
29
  * Installed package whose declarations form this population.
18
30
  *
19
- * Omit it to select the active project. When present, {@link files} resolves
20
- * against the package root instead of the project root, so the globs read as
21
- * a consumer thinks of the package rather than carrying its `node_modules`
22
- * location.
31
+ * Omit it to select the active project or the explicit {@link root}. When
32
+ * present, {@link files} resolves against the package root instead of the
33
+ * project root, so the globs read as a consumer thinks of the package rather
34
+ * than carrying its `node_modules` location.
23
35
  *
24
36
  * A package population is read from disk rather than from the `ttsc` program.
25
37
  * That is the point: a symbol nothing imports is absent from the program by
@@ -39,8 +51,8 @@ export interface ITtscEvidenceGraphTypeScriptReference extends ITtscEvidenceGrap
39
51
  * `ttsc` project. A matching file outside the project's `tsconfig` program is
40
52
  * not available to the rule and does not count as a match.
41
53
  *
42
- * When {@link package} is set these are package-relative instead, and the
43
- * files are read from disk rather than from the program.
54
+ * With {@link root} or {@link package}, these are relative to that directory or
55
+ * package and entry files can be read from disk outside the Program.
44
56
  *
45
57
  * These are globs, not regular expressions. `*` matches within one path
46
58
  * segment, `**` crosses any number of path segments, and `?` matches one
@@ -58,8 +58,9 @@
58
58
  * prepend their namespace, such as `Orders.create`. A namespace itself uses its
59
59
  * qualified name, such as `Orders` or `Outer.Inner`. A local declaration
60
60
  * exposed as `export { Local as Public }` therefore uses `Public`. A named
61
- * default declaration keeps its declaration name; anonymous and default-only
62
- * aliases have no stable target and are not selected. Members of an ambient
61
+ * default declaration or default alias is publicly addressed as `default`;
62
+ * anonymous default classes and functions also have that stable address. A
63
+ * default-only local binding is not a named export. Members of an ambient
63
64
  * namespace are public without their own `export` modifier. A type-only alias
64
65
  * exposes a namespace or a class, its public type-space descendants, and
65
66
  * everything an object-shaped type alias, or an interface no class merges with,
@@ -79,17 +80,15 @@
79
80
  * form. Static class members use `ClassName.member`; instance members use
80
81
  * `ClassName.prototype.member`, and a constructor parameter property is an
81
82
  * instance member addressed that way. Computed names are not selected, even
82
- * when their expression is a literal. Literal names must be whitespace-free
83
- * because a declaration target is one whitespace-delimited token. A dot inside
84
- * a literal name is rendered unchanged; if that spelling collides with
85
- * qualification, the target is ambiguous.
83
+ * when their expression is a literal. File-qualified accessors use bracketed
84
+ * JSON strings for literal names, including spaces and dots. Existing inline
85
+ * links keep their dotted spelling and report ambiguous literal qualification.
86
86
  *
87
- * These targets deliberately omit file paths. If selected files expose the same
88
- * qualified target, a declaration using that target is ambiguous; rename or
89
- * further qualify the public symbols. A re-export whose declaration lives in
90
- * another file does not create a second unit in the barrel file. TypeScript
91
- * target characters are matched exactly; Markdown path-separator normalization
92
- * does not rewrite literal symbol names.
87
+ * A code citation identifies its module through a file path or the citing
88
+ * TypeScript module's imports. A re-export whose declaration lives in another
89
+ * file does not create a second unit in the barrel file. TypeScript target
90
+ * characters are matched exactly; Markdown path-separator normalization does
91
+ * not rewrite literal symbol names.
93
92
  *
94
93
  * Every supported public declaration described here may carry
95
94
  * `@evidenceExclude` when its file belongs to a claim, even when that claim's
package/native/config.go CHANGED
@@ -8,6 +8,8 @@ import (
8
8
  "path"
9
9
  "sort"
10
10
  "strings"
11
+
12
+ "github.com/samchon/ttsc/packages/lint/rule"
11
13
  )
12
14
 
13
15
  func decodeGraphConfig(raw json.RawMessage) (graphConfig, []string) {
@@ -54,7 +56,7 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
54
56
  }
55
57
  problems := rejectUnknownFields(
56
58
  object,
57
- []string{"type", "name", "disabled", "root", "files", "evidenceExcludeCarriers", "symbol", "reference"},
59
+ []string{"type", "name", "severity", "disabled", "root", "files", "evidenceExcludeCarriers", "symbol", "reference"},
58
60
  graphRuleName,
59
61
  path,
60
62
  )
@@ -78,6 +80,8 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
78
80
  problems = append(problems, "Invalid evidence/graph configuration at "+path+".disabled: expected a boolean.")
79
81
  }
80
82
  }
83
+ severity, severityProblems := decodeGraphSeverity(object["severity"], path+".severity")
84
+ problems = append(problems, severityProblems...)
81
85
  root, rootProblems := decodeClaimRoot(object["root"], kind, path+".root")
82
86
  problems = append(problems, rootProblems...)
83
87
  files, fileProblems := decodeFiles(object["files"], path+".files")
@@ -98,6 +102,7 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
98
102
  }
99
103
  return claimSpec{
100
104
  Index: index,
105
+ Severity: severity,
101
106
  Type: kind,
102
107
  Name: name,
103
108
  Disabled: disabled,
@@ -109,16 +114,26 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
109
114
  }, nil
110
115
  }
111
116
 
112
- // enabledGraphConfig removes explicitly staged claims after their public
117
+ // enabledGraphConfig removes disabled claims and references after their public
113
118
  // configuration has been validated. Keeping the filter separate from decoding
114
- // preserves original claim indexes and prevents disabled entries from hiding a
119
+ // preserves original indexes and prevents disabled entries from hiding a
115
120
  // malformed shape.
116
121
  func enabledGraphConfig(config graphConfig) graphConfig {
117
122
  claims := make([]claimSpec, 0, len(config.Claims))
118
123
  for _, claim := range config.Claims {
119
- if claim.Disabled {
124
+ if claim.Disabled || claim.Severity != nil && *claim.Severity == rule.SeverityOff {
120
125
  continue
121
126
  }
127
+ references := make([]referenceSpec, 0, len(claim.References))
128
+ for _, reference := range claim.References {
129
+ if reference.Severity == nil || *reference.Severity != rule.SeverityOff {
130
+ references = append(references, reference)
131
+ }
132
+ }
133
+ if len(references) == 0 {
134
+ continue
135
+ }
136
+ claim.References = references
122
137
  claims = append(claims, claim)
123
138
  }
124
139
  config.Claims = claims
@@ -183,6 +198,7 @@ func decodeReference(
183
198
  object,
184
199
  []string{
185
200
  "type",
201
+ "severity",
186
202
  "noEvidenceExclude",
187
203
  "uniqueEvidence",
188
204
  "singleEvidencePerSymbol",
@@ -204,6 +220,8 @@ func decodeReference(
204
220
  if problem := rejectForeignTypeScriptReference(claimKind, kind, path); problem != "" {
205
221
  problems = append(problems, problem)
206
222
  }
223
+ severity, severityProblems := decodeGraphSeverity(object["severity"], path+".severity")
224
+ problems = append(problems, severityProblems...)
207
225
  root, rootProblems := decodeRoot(object["root"], kind, path+".root")
208
226
  problems = append(problems, rootProblems...)
209
227
  policy, policyProblems := decodeReferencePolicy(object, kind, path)
@@ -265,14 +283,16 @@ func decodeReference(
265
283
  return referenceSpec{}, problems
266
284
  }
267
285
  return referenceSpec{
268
- Index: index,
269
- Type: kind,
270
- Policy: policy,
271
- Root: root,
272
- Files: files,
273
- Source: source,
274
- Package: packageName,
275
- Symbols: symbols,
286
+ Severity: severity,
287
+ Index: index,
288
+ Type: kind,
289
+ Policy: policy,
290
+ Root: root,
291
+ Rooted: kind == artifactTypeScript && object["root"] != nil,
292
+ Files: files,
293
+ Source: source,
294
+ Package: packageName,
295
+ Symbols: symbols,
276
296
  }, nil
277
297
  }
278
298
 
@@ -415,24 +435,8 @@ func rejectChecklistConflicts(
415
435
  return problems
416
436
  }
417
437
 
418
- // rejectForeignTypeScriptReference refuses a code population to a claim that
419
- // cannot address one.
420
- //
421
- // A symbol is cited through an inline link, and that link resolves in the
422
- // citing module's import scope — which only a TypeScript file has. Every other
423
- // claim would have to fall back to matching the bare name against one
424
- // repository-wide table, and that makes symbol-name uniqueness across the whole
425
- // repository load-bearing: two modules exporting `IPage` make the citation
426
- // impossible, and the only repair such a diagnostic can offer is renaming
427
- // production code to suit a lint rule.
428
- //
429
- // The check is here rather than at resolution because a configuration error
430
- // belongs where the configuration is read. Reported once per reference, before
431
- // any file is opened, instead of once per citation that later fails to resolve.
432
- //
433
- // What this gives up is the decision it reverses: documentation can no longer
434
- // cite code, and the inverse obligation is not the same one. Do not restore the fallback
435
- // without restoring that record.
438
+ // Code citations require an import scope or the Markdown/TypeScript file-link
439
+ // grammar. Bare repository-wide symbol lookup remains unavailable.
436
440
  func rejectForeignTypeScriptReference(
437
441
  claimKind artifactKind,
438
442
  referenceKind artifactKind,
@@ -440,13 +444,13 @@ func rejectForeignTypeScriptReference(
440
444
  ) string {
441
445
  if referenceKind != artifactTypeScript ||
442
446
  claimKind == artifactTypeScript ||
447
+ claimKind == artifactMarkdown ||
443
448
  claimKind == "" {
444
449
  return ""
445
450
  }
446
451
  return "Invalid evidence/graph configuration at " + path +
447
- ".type: only a TypeScript claim can cite TypeScript evidence, because a symbol citation resolves through the citing module's imports and a " +
448
- string(claimKind) +
449
- " comment has none. Invert the obligation so the code cites this artifact, or move the citation into TypeScript."
452
+ ".type: TypeScript evidence requires a Markdown or TypeScript claim; a " + string(claimKind) +
453
+ " claim does not support file-qualified code citations. Move the citation to a supported claim."
450
454
  }
451
455
 
452
456
  // decodeTypeScriptReference reads the two ways a TypeScript population is
@@ -463,6 +467,9 @@ func decodeTypeScriptReference(
463
467
  problems := []string{}
464
468
  reference := referenceSpec{}
465
469
  if raw, exists := object["package"]; exists {
470
+ if _, rooted := object["root"]; rooted {
471
+ problems = append(problems, "Invalid evidence/graph configuration at "+path+": 'root' and 'package' select different TypeScript populations and cannot be combined. Choose one base and its 'files' patterns.")
472
+ }
466
473
  var value string
467
474
  if err := json.Unmarshal(raw, &value); err != nil {
468
475
  problems = append(problems, "Invalid evidence/graph configuration at "+path+".package: expected an installed package name.")
@@ -569,15 +576,8 @@ func decodeClaimRoot(
569
576
  return decodePopulationRoot(raw, configPath)
570
577
  }
571
578
 
572
- // decodeRoot reads the directory a Markdown or Prisma population resolves
573
- // against.
574
- //
575
- // The property is refused on the two artifact kinds that cannot use it, and
576
- // each refusal names the channel that artifact does have. A TypeScript
577
- // population is materialized from the ttsc Program, so a directory outside the
578
- // project contains no file it could ever reach — `package` is the escape there,
579
- // and it already exists. A Swagger reference names one exact document, so the
580
- // location belongs in `file`, where it is visible without a second property.
579
+ // decodeRoot reads a population directory. A TypeScript reference uses its
580
+ // presence to opt into disk loading; Swagger still owns one exact file.
581
581
  func decodeRoot(
582
582
  raw json.RawMessage,
583
583
  kind artifactKind,
@@ -587,11 +587,7 @@ func decodeRoot(
587
587
  return "", nil
588
588
  }
589
589
  switch kind {
590
- case artifactMarkdown, artifactPrisma:
591
- case artifactTypeScript:
592
- return "", []string{
593
- "Invalid evidence/graph configuration at " + configPath + ": a TypeScript reference selects the active ttsc program with 'files', or an installed package with 'package'; only a TypeScript claim accepts 'root'.",
594
- }
590
+ case artifactMarkdown, artifactPrisma, artifactTypeScript:
595
591
  case artifactSwagger:
596
592
  return "", []string{
597
593
  "Invalid evidence/graph configuration at " + configPath + ": a Swagger reference owns one exact document; write the ancestor-relative or absolute location in 'file' instead.",
@@ -35,10 +35,14 @@ func commentHidingTag(comment string) string {
35
35
  comment = strings.TrimPrefix(comment, "/**")
36
36
  comment = strings.TrimPrefix(comment, "/*")
37
37
  comment = strings.TrimSuffix(comment, "*/")
38
+ fence := commentFence{}
38
39
  for _, rawLine := range strings.Split(comment, "\n") {
39
40
  line := strings.TrimSpace(rawLine)
40
41
  line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
41
42
  line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
43
+ if fence.consume(line) {
44
+ continue
45
+ }
42
46
  for _, tag := range hiddenDeclarationTags {
43
47
  if line != tag && !strings.HasPrefix(line, tag+" ") {
44
48
  continue
@@ -75,6 +79,7 @@ func parseCommentDeclarations(
75
79
  lineOffset int
76
80
  }
77
81
  var pending *pendingDeclaration
82
+ fence := commentFence{}
78
83
  parsed := []parsedDeclaration{}
79
84
  flush := func() {
80
85
  if pending == nil {
@@ -92,6 +97,12 @@ func parseCommentDeclarations(
92
97
  for index, rawLine := range lines {
93
98
  line := strings.TrimSpace(rawLine)
94
99
  line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
100
+ if fence.consume(line) {
101
+ if pending != nil {
102
+ pending.body = append(pending.body, line)
103
+ }
104
+ continue
105
+ }
95
106
  tag, body, found := declarationLine(line)
96
107
  if found {
97
108
  flush()
@@ -136,6 +147,7 @@ func declarationLine(line string) (tagKind, string, bool) {
136
147
  }{
137
148
  {marker: "@evidenceExclude", tag: tagExclude},
138
149
  {marker: "@evidence", tag: tagEvidence},
150
+ {marker: "@link", tag: tagEvidence},
139
151
  } {
140
152
  if !strings.HasPrefix(line, candidate.marker) {
141
153
  continue
@@ -144,7 +156,11 @@ func declarationLine(line string) (tagKind, string, bool) {
144
156
  if remainder != "" && remainder[0] != ' ' && remainder[0] != '\t' {
145
157
  continue
146
158
  }
147
- return candidate.tag, strings.TrimSpace(remainder), true
159
+ body := strings.TrimSpace(remainder)
160
+ if candidate.marker == "@link" {
161
+ body = fileLinkPrefix + body
162
+ }
163
+ return candidate.tag, body, true
148
164
  }
149
165
  return "", "", false
150
166
  }
@@ -161,6 +177,9 @@ func splitDeclarationBody(body string) (string, string) {
161
177
  if body == "" {
162
178
  return "", ""
163
179
  }
180
+ if target, reason, found := splitFileLinkBody(body); found {
181
+ return target, reason
182
+ }
164
183
  if target, reason, found := splitInlineLinkBody(body); found {
165
184
  return target, reason
166
185
  }
@@ -221,6 +240,9 @@ func inlineLinkTarget(target string) string {
221
240
 
222
241
  // displayTarget renders a target the way its author wrote it.
223
242
  func displayTarget(target string) string {
243
+ if isFileLinkTarget(target) {
244
+ return strings.TrimPrefix(target, fileLinkPrefix)
245
+ }
224
246
  if isInlineLinkTarget(target) {
225
247
  return "{@link " + inlineLinkTarget(target) + "}"
226
248
  }
@@ -315,6 +337,7 @@ func parseReviews(comment string) []parsedReview {
315
337
  comment = strings.TrimSuffix(comment, "*/")
316
338
  reviews := []parsedReview{}
317
339
  var pending *parsedReview
340
+ fence := commentFence{}
318
341
  var body []string
319
342
  flush := func() {
320
343
  if pending == nil {
@@ -333,6 +356,12 @@ func parseReviews(comment string) []parsedReview {
333
356
  line := strings.TrimSpace(rawLine)
334
357
  line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
335
358
  line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
359
+ if fence.consume(line) {
360
+ if pending != nil {
361
+ body = append(body, line)
362
+ }
363
+ continue
364
+ }
336
365
  if reviews, remainder, opened := reviewLine(line); opened {
337
366
  flush()
338
367
  pending = &parsedReview{
@@ -442,6 +471,26 @@ func containsWhitespace(value string) bool {
442
471
  return false
443
472
  }
444
473
 
474
+ // Fences inside documentation are examples, not declarations. Their text is
475
+ // still retained when it belongs to a pending reason or review description.
476
+ type commentFence struct {
477
+ marker rune
478
+ length int
479
+ }
480
+
481
+ func (fence *commentFence) consume(line string) bool {
482
+ marker, length, remainder, opened := markdownFence(line)
483
+ if opened {
484
+ if fence.marker == 0 {
485
+ fence.marker, fence.length = marker, length
486
+ } else if marker == fence.marker && length >= fence.length && strings.TrimSpace(remainder) == "" {
487
+ fence.marker, fence.length = 0, 0
488
+ }
489
+ return true
490
+ }
491
+ return fence.marker != 0
492
+ }
493
+
445
494
  func lineAt(content string, offset int) int {
446
495
  if offset < 0 {
447
496
  return 1
@@ -0,0 +1,62 @@
1
+ package evidence
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // Default aliases retain the named declaration's identity. Anonymous class
6
+ // and function declarations own the module's default identity directly.
7
+ func typeScriptDeclarationName(node *shimast.Node) string {
8
+ if node == nil {
9
+ return ""
10
+ }
11
+ if name := declarationName(node.Name()); name != "" {
12
+ return name
13
+ }
14
+ if isDefaultExported(node) {
15
+ return "default"
16
+ }
17
+ return ""
18
+ }
19
+
20
+ func collectDefaultExportBindings(statements *shimast.NodeList) map[string]exportedName {
21
+ bindings := map[string]exportedName{}
22
+ if statements == nil {
23
+ return bindings
24
+ }
25
+ for _, statement := range statements.Nodes {
26
+ if statement == nil {
27
+ continue
28
+ }
29
+ if statement.Kind == shimast.KindExportAssignment {
30
+ assignment := statement.AsExportAssignment()
31
+ expression := assignment.Expression
32
+ if !assignment.IsExportEquals && expression != nil && expression.Kind == shimast.KindIdentifier {
33
+ local := declarationName(expression)
34
+ bindings[local] = exportedName{Public: local}
35
+ }
36
+ }
37
+ if statement.Kind != shimast.KindExportDeclaration {
38
+ continue
39
+ }
40
+ declaration := statement.AsExportDeclaration()
41
+ if declaration.ModuleSpecifier != nil || declaration.ExportClause == nil || declaration.ExportClause.Kind != shimast.KindNamedExports {
42
+ continue
43
+ }
44
+ for _, element := range declaration.ExportClause.AsNamedExports().Elements.Nodes {
45
+ specifier := element.AsExportSpecifier()
46
+ if declarationName(specifier.Name()) != "default" {
47
+ continue
48
+ }
49
+ local := specifier.PropertyName
50
+ if local == nil {
51
+ local = specifier.Name()
52
+ }
53
+ name := declarationName(local)
54
+ typeOnly := declaration.IsTypeOnly || specifier.IsTypeOnly
55
+ if previous, exists := bindings[name]; exists {
56
+ typeOnly = typeOnly && previous.TypeOnly
57
+ }
58
+ bindings[name] = exportedName{Public: name, TypeOnly: typeOnly}
59
+ }
60
+ }
61
+ return bindings
62
+ }