@ttsc/evidence 0.30.4 → 0.31.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 (66) hide show
  1. package/README.md +14 -6
  2. package/lib/index.d.ts +2 -3
  3. package/lib/index.js +2 -3
  4. package/lib/internal/canonicalDigest.js +12 -6
  5. package/lib/internal/loadPrismaModels.js +52 -7
  6. package/lib/internal/loadSwaggerOperations.js +253 -59
  7. package/lib/internal/normalizeSwaggerDocument.js +320 -2
  8. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
  9. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
  10. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
  11. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
  12. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
  13. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
  14. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
  15. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
  16. package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
  17. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
  18. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
  19. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
  20. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
  21. package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
  22. package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
  23. package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
  24. package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
  25. package/lib/structures/index.d.ts +4 -0
  26. package/lib/structures/index.js +4 -0
  27. package/native/base.go +25 -23
  28. package/native/bridge_trace.go +348 -0
  29. package/native/comments.go +1 -1
  30. package/native/config.go +6 -3
  31. package/native/declaration.go +17 -43
  32. package/native/documentation_regions.go +214 -0
  33. package/native/documented.go +5 -4
  34. package/native/exports.go +1 -1
  35. package/native/fingerprint.go +2 -8
  36. package/native/glob.go +4 -1
  37. package/native/graph.go +59 -23
  38. package/native/graph_nodes.go +2 -1
  39. package/native/hints.go +22 -18
  40. package/native/imports.go +7 -6
  41. package/native/input_reader.go +83 -0
  42. package/native/inputs.go +13 -12
  43. package/native/loader.go +36 -24
  44. package/native/markdown.go +587 -177
  45. package/native/markdown_capture.go +223 -0
  46. package/native/model.go +2 -1
  47. package/native/prisma.go +110 -25
  48. package/native/prisma_cache.go +12 -8
  49. package/native/prisma_locator.go +70 -33
  50. package/native/review.go +8 -7
  51. package/native/rooted_typescript.go +3 -4
  52. package/native/swagger.go +123 -15
  53. package/native/swagger_cache.go +16 -12
  54. package/native/todo.go +12 -0
  55. package/native/typescript.go +129 -25
  56. package/package.json +9 -7
  57. package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
  58. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
  59. package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
  60. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
  61. package/lib/typings/index.d.ts +0 -5
  62. package/lib/typings/index.js +0 -21
  63. /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
  64. /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
  65. /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
  66. /package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.js +0 -0
@@ -0,0 +1,93 @@
1
+ import type { ITtscEvidenceDocumentedConfig } from "./ITtscEvidenceDocumentedConfig";
2
+ import type { ITtscEvidenceGraphConfig } from "./ITtscEvidenceGraphConfig";
3
+ /**
4
+ * Rule names of the Evidence lint contributor with their option contracts.
5
+ *
6
+ * Pass this interface as the generic argument of `ITtscLintConfig` so every
7
+ * Evidence rule receives exact severity-plus-options checking, and a rule that
8
+ * takes no options accepts a severity only:
9
+ *
10
+ * ```ts
11
+ * import type { ITtscEvidenceRules } from "@ttsc/evidence";
12
+ * import type { ITtscLintConfig } from "@ttsc/lint";
13
+ *
14
+ * export default {
15
+ * rules: { "evidence/singular": "error" },
16
+ * } satisfies ITtscLintConfig<ITtscEvidenceRules>;
17
+ * ```
18
+ *
19
+ * @evidence contracts/common.md#principled-implementation Each rule name maps to the options object the rule accepts, or to `void` when it takes severity only, which is exactly the shape the lint package's contributor overlay consumes.
20
+ * @evidence contracts/common.md#clear-and-simple-design One ordinary exported interface lists the five rules once, so no ambient module augmentation is needed and the member comments stay beside the names they describe.
21
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The options of the two configurable rules reference their declared config types rather than an unknown slot, and the optionless rules are not given an invented options object.
22
+ * @evidence contracts/common.md#meaningful-documentation The example shows the user side of the pattern and each member explains the rule it names, following the documentation guidance.
23
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation This interface lists rule names and option types and names no path, file, filesystem or process itself.
24
+ */
25
+ export interface ITtscEvidenceRules {
26
+ /**
27
+ * Declares this project's evidence graph.
28
+ *
29
+ * The claims define the citing populations and the independently complete
30
+ * evidence references each one must acknowledge.
31
+ */
32
+ "evidence/graph": ITtscEvidenceGraphConfig;
33
+ /**
34
+ * Requires a JSDoc block on every selected export.
35
+ *
36
+ * A JSDoc block is the only place a TypeScript declaration's `@evidence` tag
37
+ * is read from, so an export without one cannot cite anything.
38
+ */
39
+ "evidence/documented": ITtscEvidenceDocumentedConfig;
40
+ /**
41
+ * Requires one public identity per TypeScript file, named after the file.
42
+ *
43
+ * The counted unit is an identity rather than an export, so declaration
44
+ * merging of a single name stays legal: `export interface ISomething` beside
45
+ * `export namespace ISomething`, `export class Something` beside `export
46
+ * namespace Something`, and `export const something` beside `export default
47
+ * something` are each one identity.
48
+ *
49
+ * A file that only re-exports owns no identity and is never reported, and an
50
+ * `index` file is exempt from the name match while still limited to one
51
+ * identity.
52
+ *
53
+ * The rule takes no options; per-directory scoping belongs in the outer
54
+ * `files` setting of `lint.config.ts`.
55
+ */
56
+ "evidence/singular": void;
57
+ /**
58
+ * Reports every JSDoc `@todo` tag in a checked file.
59
+ *
60
+ * A remaining `@todo` is a contract the declaration has not realized yet, so
61
+ * each tag fails the build with its own text until the declaration is
62
+ * realized and the tag removed. Every declaration's block is read, exported
63
+ * or not, on any symbol kind.
64
+ *
65
+ * The rule takes no options; per-directory scoping belongs in the outer
66
+ * `files` setting of `lint.config.ts`.
67
+ */
68
+ "evidence/todo": void;
69
+ /**
70
+ * Requires a review beside every acknowledgement.
71
+ *
72
+ * Every `@evidence` and `@evidenceExclude` on a public identity must be
73
+ * answered by `@evidenceReview` or `@evidenceExcludeReview`, respectively,
74
+ * naming the same target. The citation states why this declaration answers
75
+ * for that target; the review states what was verified. Those are different
76
+ * questions, and only the first one is written unless something asks for the
77
+ * second.
78
+ *
79
+ * A review is an annotation of a citation, never an acknowledgement of a
80
+ * unit. It discharges no coverage, contributes no host to `uniqueEvidence`,
81
+ * and counts toward no `singleEvidencePerSymbol` total, so enabling this rule
82
+ * cannot change a single `evidence/graph` diagnostic.
83
+ *
84
+ * The optional `#`-prefixed fingerprint token is carried without being
85
+ * interpreted here. `evidence/graph` validates it against the cited content
86
+ * under a reference's `requireReview`, which is what makes a review expire
87
+ * when the thing it reviewed moves.
88
+ *
89
+ * The rule takes no options; per-directory scoping belongs in the outer
90
+ * `files` setting of `lint.config.ts`.
91
+ */
92
+ "evidence/review": void;
93
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A Markdown node kind that can become an evidence unit or host an evidence
3
+ * declaration.
4
+ *
5
+ * `"file"` is the document root. `"h1"` through `"h4"` select ATX heading
6
+ * sections at that exact level; Setext headings and H5/H6 headings are outside
7
+ * this contract.
8
+ *
9
+ * These nodes form the Markdown outline: the file contains every heading, and a
10
+ * heading contains the lower-level headings before the next heading of equal or
11
+ * higher level. An `@evidence` target, or an `@evidenceExclude` target allowed
12
+ * by its reference policy, acknowledges its selected node and every selected
13
+ * descendant. A reference selector still defines which descendants are
14
+ * obligations; an unselected ancestor remains addressable as their aggregate
15
+ * scope.
16
+ *
17
+ * A file evidence target is its path relative to the reference's root, or the
18
+ * project root when none is declared, with `/` separators. A declaration may
19
+ * spell those separators as `/` or `\`. A heading target appends its anchor,
20
+ * such as `docs/orders.md#create-order`. An explicit `{#anchor}` suffix wins.
21
+ * Its anchor must start with an ASCII letter or digit and may then contain
22
+ * ASCII letters, digits, `.`, `_`, `:`, and `-`.
23
+ *
24
+ * Without an explicit anchor, the heading becomes a lowercase slug: letters,
25
+ * numbers, and `_` remain; whitespace and `-` collapse to `-`; other
26
+ * punctuation is removed. Two selected headings that produce the same target
27
+ * are ambiguous and need distinct explicit anchors.
28
+ *
29
+ * Targets are one whitespace-delimited declaration token. A Markdown source
30
+ * path therefore cannot contain whitespace; the rule reports such a file with a
31
+ * rename diagnostic instead of creating an impossible obligation.
32
+ *
33
+ * @evidence contracts/common.md#principled-implementation The literal union represents exactly the supported file and ATX H1-H4 outline nodes; explicit anchors and generated slugs address those nodes under the documented target grammar.
34
+ * @evidence contracts/common.md#clear-and-simple-design One shared outline vocabulary serves both declaration hosts and reference units without introducing a separate enum or parser-node facade.
35
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Unsupported heading forms and whitespace-containing source addresses are stated limitations rather than silently fabricated targets.
36
+ * @evidence contracts/common.md#meaningful-documentation The comment gives supported headings, containment, anchor precedence, slug construction and address limitations in distinct paragraphs before the tags.
37
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation This type lists Markdown unit kinds and names no path, file, filesystem or process.
38
+ */
39
+ export type TtscEvidenceGraphMarkdownSymbol = "file" | "h1" | "h2" | "h3" | "h4";
@@ -34,5 +34,11 @@
34
34
  * claim may separately accept `@evidenceExclude` in an unattached top-level
35
35
  * triple-slash run in a matching file; that carrier has no Prisma symbol and
36
36
  * never hosts `@evidence`.
37
+ *
38
+ * @evidence contracts/common.md#principled-implementation Models, stored fields and relation fields match Prisma's parsed datamodel distinctions; views share model shape while enums and other schema settings intentionally have no graph unit.
39
+ * @evidence contracts/common.md#clear-and-simple-design The three-value union is reused on both sides of the graph and leaves parsing, namespacing and address construction to their owning operations.
40
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Relation classification comes from the parser rather than an attribute-presence heuristic that would miss inverse relation fields.
41
+ * @evidence contracts/common.md#meaningful-documentation Distinct paragraphs explain semantic classification, containment, prisma-prefixed targets, cross-file identity and unsupported schema constructs.
42
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation This type lists Prisma unit kinds and names no path, file, filesystem or process.
37
43
  */
38
44
  export type TtscEvidenceGraphPrismaSymbol = "model" | "column" | "relation";
@@ -94,5 +94,11 @@
94
94
  * `@evidenceExclude` when its file belongs to a claim, even when that claim's
95
95
  * selector omits its kind. The selector still controls `@evidence`; an
96
96
  * unsupported or unexported declaration carries neither form.
97
+ *
98
+ * @evidence contracts/common.md#principled-implementation The three semantic kinds distinguish types, callable contracts and data contracts using the documented syntactic host rules; enums, accessors and hidden members remain outside those rules.
99
+ * @evidence contracts/common.md#clear-and-simple-design A single literal union is shared by claims, references and documentation selection, avoiding divergent kind vocabularies across rules.
100
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Classification follows declared syntax and public identity rather than guessing callable aliases with an unavailable checker or special-casing a consumer's declarations.
101
+ * @evidence contracts/common.md#meaningful-documentation The native comment explains callable versus data classification, containment, export aliases, type-only visibility and exact addressing so users can select and cite contracts without inferring the parser.
102
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation This type lists TypeScript unit kinds and names no path, file, filesystem or process.
97
103
  */
98
104
  export type TtscEvidenceGraphTypeScriptSymbol = "type" | "function" | "property";
@@ -11,3 +11,7 @@ export * from "./ITtscEvidenceGraphReferenceBase";
11
11
  export * from "./ITtscEvidenceGraphSwaggerReference";
12
12
  export * from "./ITtscEvidenceGraphTypeScriptClaim";
13
13
  export * from "./ITtscEvidenceGraphTypeScriptReference";
14
+ export * from "./ITtscEvidenceRules";
15
+ export * from "./TtscEvidenceGraphMarkdownSymbol";
16
+ export * from "./TtscEvidenceGraphPrismaSymbol";
17
+ export * from "./TtscEvidenceGraphTypeScriptSymbol";
@@ -27,3 +27,7 @@ __exportStar(require("./ITtscEvidenceGraphReferenceBase"), exports);
27
27
  __exportStar(require("./ITtscEvidenceGraphSwaggerReference"), exports);
28
28
  __exportStar(require("./ITtscEvidenceGraphTypeScriptClaim"), exports);
29
29
  __exportStar(require("./ITtscEvidenceGraphTypeScriptReference"), exports);
30
+ __exportStar(require("./ITtscEvidenceRules"), exports);
31
+ __exportStar(require("./TtscEvidenceGraphMarkdownSymbol"), exports);
32
+ __exportStar(require("./TtscEvidenceGraphPrismaSymbol"), exports);
33
+ __exportStar(require("./TtscEvidenceGraphTypeScriptSymbol"), exports);
package/native/base.go CHANGED
@@ -3,7 +3,6 @@ package evidence
3
3
  import (
4
4
  "errors"
5
5
  "io/fs"
6
- "os"
7
6
  "path"
8
7
  "path/filepath"
9
8
  "sort"
@@ -28,6 +27,7 @@ import (
28
27
  // does: moving the base moves the address space with it (`graph.go`,
29
28
  // `materializePackageGlobReference`).
30
29
  type populationBase struct {
30
+ inputs evidenceInputReader
31
31
  // Absolute is the canonical directory, and the identity two populations are
32
32
  // judged the same base by.
33
33
  Absolute string
@@ -226,9 +226,11 @@ func resolveGraphBases(root string, config *graphConfig) {
226
226
  for claimIndex := range config.Claims {
227
227
  claim := &config.Claims[claimIndex]
228
228
  claim.Base = resolvePopulationBase(root, claim.Root)
229
+ claim.Base.inputs = config.inputs
229
230
  for referenceIndex := range claim.References {
230
231
  reference := &claim.References[referenceIndex]
231
232
  reference.Base = resolvePopulationBase(root, reference.Root)
233
+ reference.Base.inputs = config.inputs
232
234
  }
233
235
  }
234
236
  }
@@ -288,13 +290,14 @@ func configuredBases(config graphConfig, kind artifactKind) []populationBase {
288
290
  //
289
291
  // The absolute fallback belongs to the default base alone, which is the only
290
292
  // base with no declared spelling, and the project root is then the only thing
291
- // left to name. Of the four callers, two reach it: `unlistableBaseProblem` and
292
- // `unresolvedBaseProblem`, both of which a base with no declared root can arrive
293
- // at. `describePopulation` returns on the default base before it names anything,
294
- // and `describeBaseDirectoryProblem` is only ever entered through
295
- // `baseDirectoryProblem`, which does the same. Neither of the two that reach it
296
- // asks for an edit to a property that is not there, which is what makes the
297
- // spelling usable where no property exists.
293
+ // left to name. `unlistableBaseProblem`, `unresolvedBaseProblem`, and the two
294
+ // rooted TypeScript reference messages can arrive there with a default base,
295
+ // because `root: "."` resolves back onto the project root.
296
+ // `describePopulation` returns on the default base before it names anything, and
297
+ // `describeBaseDirectoryProblem` is only ever entered through
298
+ // `baseDirectoryProblem`, which does the same. None of the callers that reach
299
+ // the fallback asks for an edit to a property that is not there, which is what
300
+ // makes the spelling usable where no property exists.
298
301
  func populationRootLabel(base populationBase) string {
299
302
  if base.Declared == "" {
300
303
  return filepath.ToSlash(base.Absolute)
@@ -322,7 +325,7 @@ func baseDirectoryProblem(base populationBase, kind artifactKind) string {
322
325
  if base.Default {
323
326
  return ""
324
327
  }
325
- info, err := os.Stat(base.Absolute)
328
+ info, err := base.inputs.Stat(base.Absolute)
326
329
  if err == nil && info.IsDir() {
327
330
  return ""
328
331
  }
@@ -349,7 +352,7 @@ func baseDirectoryProblem(base populationBase, kind artifactKind) string {
349
352
  // is that a path means the same thing on both.
350
353
  occupied := err == nil
351
354
  if errors.Is(err, fs.ErrNotExist) {
352
- if _, linkErr := os.Lstat(base.Absolute); linkErr == nil {
355
+ if _, linkErr := base.inputs.Lstat(base.Absolute); linkErr == nil {
353
356
  occupied = true
354
357
  }
355
358
  }
@@ -510,8 +513,7 @@ func describeBaseDirectoryProblem(
510
513
  // Every component is resolved, not only the leaf. A link on an ancestor is
511
514
  // exactly what `os.Lstat` of the leaf cannot see, and it is the shape a package
512
515
  // manager installs: the workspace dependency is the link and the root an author
513
- // declares is a directory inside it. Resolving only the leaf left that silent,
514
- // which is what #1269 recorded.
516
+ // declares is a directory inside it. Resolving only the leaf leaves that silent.
515
517
  //
516
518
  // The answer is verified rather than trusted, at every component and not only
517
519
  // at the last. `resolveLinkedDirectory` gives up after a fixed number of hops
@@ -529,19 +531,18 @@ func describeBaseDirectoryProblem(
529
531
  // A base that is not a link costs one `os.Lstat` per path component plus one
530
532
  // for this answer, where resolving the leaf alone cost two. It is paid per call
531
533
  // rather than per file, and the call count is the graph's own shape rather than
532
- // this function's: claim-side populations are materialized before the whole
533
- // configuration, so a base is asked once for each load it appears in. That is
534
- // at most twice for a Markdown or Prisma claim base and once for its reference
535
- // bases, and twice those numbers for TypeScript, whose gate asks beside the
534
+ // this function's. Markdown captures the first resolution for each base within
535
+ // one Check. Prisma resolves each load, at most twice for a claim base and once
536
+ // for a reference base. TypeScript asks twice those numbers: its gate asks beside the
536
537
  // pass that builds its match table and whose governance pass builds one more
537
538
  // from the configuration as declared. All of it stays off the loop this feeds,
538
539
  // which is every source file of the project.
539
540
  func resolvedBaseDirectory(base populationBase) (string, bool) {
540
- from, resolved := resolveLinkedPath(base.Absolute)
541
+ from, resolved := resolveLinkedPath(base.Absolute, base.inputs)
541
542
  if !resolved {
542
543
  return from, false
543
544
  }
544
- info, err := os.Lstat(from)
545
+ info, err := base.inputs.Lstat(from)
545
546
  return from, err == nil && info.IsDir()
546
547
  }
547
548
 
@@ -564,9 +565,10 @@ func resolvedBaseDirectory(base populationBase) (string, bool) {
564
565
  //
565
566
  // A component whose chain outran the resolver ends the walk. Joining the rest
566
567
  // onto it would produce a path the filesystem opens and every string
567
- // comparison misses, which is #1269 exactly, one component further up than the
568
- // case that opened it.
569
- func resolveLinkedPath(absolute string) (string, bool) {
568
+ // comparison misses, which is the same silent failure as an unresolved leaf, one component
569
+ // further up.
570
+ func resolveLinkedPath(absolute string, readers ...evidenceInputReader) (string, bool) {
571
+ inputs := inputReader(readers)
570
572
  volume := filepath.VolumeName(absolute)
571
573
  rest := absolute[len(volume):]
572
574
  current := volume + string(filepath.Separator)
@@ -576,7 +578,7 @@ func resolveLinkedPath(absolute string) (string, bool) {
576
578
  continue
577
579
  }
578
580
  walked = true
579
- resolved, settled := resolveLinkedDirectory(filepath.Join(current, segment))
581
+ resolved, settled := resolveLinkedDirectory(filepath.Join(current, segment), inputs)
580
582
  if !settled {
581
583
  return filepath.FromSlash(resolved), false
582
584
  }
@@ -748,7 +750,7 @@ func unreadableWalkEntryProblem(
748
750
  // path such as `C:docs` is not absolute by the path API yet resolves against
749
751
  // whatever directory that drive is currently on, so it names a different
750
752
  // location on two machines while looking superficially safe — the rationale
751
- // `glob_honors_public_path_contract_test.go` records for the same rejection in
753
+ // `glob_rejects_windows_drive_paths_test.go` records for the same rejection in
752
754
  // `files`.
753
755
  //
754
756
  // `..` is accepted here, and that is the whole point of the property. It is
@@ -0,0 +1,348 @@
1
+ package evidence
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "io"
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ "runtime"
11
+ "strconv"
12
+ "strings"
13
+ "sync"
14
+ "syscall"
15
+ "time"
16
+ )
17
+
18
+ const evidenceTraceLimit = 256 * 1024 * 1024
19
+ const evidenceTraceFailureReserve = 4096
20
+
21
+ var evidenceTraceOnce sync.Once
22
+ var evidenceTraceSink *evidenceTraceWriter
23
+
24
+ // evidenceTraceWriter owns one private opt-in measurement stream, never a
25
+ // product reply or a cache. Its mutex serializes counters, budget and appends.
26
+ // Files live in the coordinator's existing absolute scratch directory; the
27
+ // coordinator retains them until actual processes join and inspection ends.
28
+ //
29
+ // Principled implementation: A process PID plus CreateTemp nonce separates
30
+ // concurrent writers; exclusive native creation establishes stream identity.
31
+ // Clear and simple design: One writer owns every event and payload budget.
32
+ // Prohibited implementation shortcuts: Disabled observation does no IO, and
33
+ // capture failures neither supply results nor change protocol or cache values.
34
+ // Meaningful documentation: Ownership, activation, limits and failure effects
35
+ // are stated here; this private type is not a product configuration surface.
36
+ // OS-neutral implementation: Native absolute paths and Go file operations own
37
+ // the side channel without a shell or assumptions about filesystem case.
38
+ // Bound retention and release resources: No file handle stays open between
39
+ // writes. Total bytes are capped; file retention belongs to the coordinator.
40
+ type evidenceTraceWriter struct {
41
+ mutex sync.Mutex
42
+ directory string
43
+ path string
44
+ instance string
45
+ pid int
46
+ ordinal uint64
47
+ sequence uint64
48
+ bytes int
49
+ }
50
+
51
+ // evidenceBridgeTrace pairs one actual lookup with its subsequent Run/parse.
52
+ // An unpaired direct normalizer call explicitly lacks a native lookup value.
53
+ // It retains only the writer and ordinal, not replies or historical requests.
54
+ type evidenceBridgeTrace struct {
55
+ writer *evidenceTraceWriter
56
+ invocation string
57
+ kind string
58
+ nativeLookup bool
59
+ }
60
+
61
+ // evidenceTraceEvent is the shared schema-v1 envelope. PID names the observed
62
+ // subject, while WriterPID/Instance identify the writer. A pre-start failure
63
+ // has PID zero; an invocation does not imply an actual process or Program.
64
+ type evidenceTraceEvent struct {
65
+ Schema int `json:"schema"`
66
+ Event string `json:"event"`
67
+ WriterPID int `json:"writerPid"`
68
+ Instance string `json:"instance"`
69
+ Sequence uint64 `json:"sequence"`
70
+ At string `json:"at"`
71
+ Invocation string `json:"invocation"`
72
+ PID int `json:"pid"`
73
+ Data any `json:"data"`
74
+ }
75
+
76
+ // newEvidenceBridgeTrace allocates a process-local invocation only when the
77
+ // coordinator opted in before the first request. The sink is initialized once
78
+ // per process, so later environment edits cannot split one writer's budget.
79
+ //
80
+ // Principled implementation: The ordinal is allocated under the writer mutex;
81
+ // it is private trace context and is never marshaled into a product request.
82
+ // Clear and simple design: The existing callers carry this optional pointer.
83
+ // Prohibited implementation shortcuts: A missing/invalid sink disables capture
84
+ // but does not fabricate a successful event; the coordinator detects absence.
85
+ // Efficient algorithms: Initialization is once; each allocation is constant
86
+ // work and retains no request content or completed invocation map.
87
+ // Reuse equivalent work: The same writer/budget serves both bridge owners;
88
+ // product computations and cache admission remain with their existing owners.
89
+ // Bound retention and release resources: Only counters/mutex/path persist.
90
+ // Initialization closes its exclusive stream handle before returning.
91
+ // OS-neutral implementation: The opt-in path must be an existing absolute
92
+ // native directory. Helper-owned temp nonces and PIDs do not encode source paths.
93
+ func newEvidenceBridgeTrace(kind string) *evidenceBridgeTrace {
94
+ evidenceTraceOnce.Do(func() {
95
+ directory := os.Getenv("TTSC_E2E_TRACE")
96
+ if directory == "" || !filepath.IsAbs(directory) {
97
+ return
98
+ }
99
+ info, err := os.Stat(directory)
100
+ if err != nil || !info.IsDir() {
101
+ return
102
+ }
103
+ writer := &evidenceTraceWriter{directory: directory, pid: os.Getpid()}
104
+ prefix := strconv.Itoa(writer.pid) + "-"
105
+ file, err := os.CreateTemp(directory, prefix+"*.jsonl")
106
+ if err != nil {
107
+ return
108
+ }
109
+ writer.path = file.Name()
110
+ writer.instance = strings.TrimSuffix(strings.TrimPrefix(filepath.Base(writer.path), prefix), ".jsonl")
111
+ if err := file.Close(); err != nil {
112
+ return
113
+ }
114
+ evidenceTraceSink = writer
115
+ })
116
+ writer := evidenceTraceSink
117
+ if writer == nil {
118
+ return nil
119
+ }
120
+ writer.mutex.Lock()
121
+ writer.ordinal++
122
+ invocation := strconv.FormatUint(writer.ordinal, 10)
123
+ writer.mutex.Unlock()
124
+ return &evidenceBridgeTrace{writer: writer, invocation: invocation, kind: kind}
125
+ }
126
+
127
+ // record appends one observed event, with failures reported only to the trace.
128
+ // The caller's actual values are serialized, never generated as an oracle.
129
+ // Each event's transient encoding costs its data size; no event is retained.
130
+ func (trace *evidenceBridgeTrace) record(event string, pid int, data map[string]any) {
131
+ if trace == nil {
132
+ return
133
+ }
134
+ writer := trace.writer
135
+ writer.mutex.Lock()
136
+ defer writer.mutex.Unlock()
137
+ if err := writer.append(trace.invocation, event, pid, data, false); err != nil {
138
+ // One bounded failure attempt avoids recursion when the sink itself fails.
139
+ _ = writer.append(trace.invocation, "trace-integrity-failure", writer.pid,
140
+ map[string]any{"bridge": trace.kind, "operation": event, "error": err.Error()}, true)
141
+ }
142
+ }
143
+
144
+ // append is called with the writer lock held. It charges attempted bytes before
145
+ // IO, bounds the complete stream, and closes the append handle on every path.
146
+ // A reserved tail permits an integrity event after the ordinary budget ends.
147
+ // Core invocation includes the writer instance; the private ordinal remains
148
+ // unchanged for payload filenames and all events use this same boundary.
149
+ // A transient shallow copy preserves caller data while binding the actual Go
150
+ // writer runtime; it does not identify the Node child or installed artifact.
151
+ // It never prints or returns an error to the product operation.
152
+ func (writer *evidenceTraceWriter) append(invocation string, event string, pid int, data map[string]any, failure bool) error {
153
+ writer.sequence++
154
+ observedData := make(map[string]any, len(data)+1)
155
+ for key, value := range data {
156
+ observedData[key] = value
157
+ }
158
+ observedData["writerRuntime"] = runtime.Version()
159
+ encoded, err := json.Marshal(evidenceTraceEvent{
160
+ Schema: 1, Event: event, WriterPID: writer.pid, Instance: writer.instance,
161
+ Sequence: writer.sequence, At: time.Now().UTC().Format(time.RFC3339Nano),
162
+ Invocation: writer.instance + ":" + invocation, PID: pid, Data: observedData,
163
+ })
164
+ if err != nil {
165
+ return err
166
+ }
167
+ encoded = append(encoded, '\n')
168
+ limit := evidenceTraceLimit - evidenceTraceFailureReserve
169
+ if failure {
170
+ limit = evidenceTraceLimit
171
+ }
172
+ if len(encoded) > limit-writer.bytes {
173
+ return errors.New("Evidence trace writer byte budget exceeded")
174
+ }
175
+ writer.bytes += len(encoded)
176
+ file, err := os.OpenFile(writer.path, os.O_WRONLY|os.O_APPEND, 0o600)
177
+ if err != nil {
178
+ return err
179
+ }
180
+ count, writeErr := file.Write(encoded)
181
+ closeErr := file.Close()
182
+ if writeErr != nil {
183
+ return writeErr
184
+ }
185
+ if count != len(encoded) {
186
+ return io.ErrShortWrite
187
+ }
188
+ return closeErr
189
+ }
190
+
191
+ // capture saves exactly the existing bounded stdout bytes, not reconstructed
192
+ // JSON. A product overflow makes the observation too-large even if its buffered
193
+ // prefix was saved; it cannot serve as a complete wire oracle. Payload names
194
+ // contain only writer-owned identities. No buffer survives this invocation.
195
+ //
196
+ // Principled implementation: Raw bytes and capture status are separate; a
197
+ // prefix, IO failure or budget refusal never becomes a successful observation.
198
+ // Efficient algorithms: One write of the existing slice adds no raw-byte copy.
199
+ // Bound retention and release resources: The existing product limit and 64MiB
200
+ // observation cap bound each payload; the writer's shared 256MiB budget bounds
201
+ // all files/events. Exclusive creation and immediate close own every handle.
202
+ // OS-neutral implementation: filepath.Join and Go IO resolve only a helper-
203
+ // owned basename within the established native scratch directory.
204
+ func (trace *evidenceBridgeTrace) capture(content []byte, productLimit int, exceeded bool) map[string]any {
205
+ if trace == nil {
206
+ return nil
207
+ }
208
+ writer := trace.writer
209
+ writer.mutex.Lock()
210
+ defer writer.mutex.Unlock()
211
+ metadata := map[string]any{"observedByteLength": len(content), "capture": "complete", "bufferedOnly": true}
212
+ name := strconv.Itoa(writer.pid) + "-" + writer.instance + "-" + trace.invocation + "-stdout.bin"
213
+ if productLimit > 64*1024*1024 {
214
+ productLimit = 64 * 1024 * 1024
215
+ }
216
+ charge := len(content)
217
+ if charge == 0 {
218
+ // Empty payloads still consume a file identity; a byte-only zero charge
219
+ // would allow unlimited empty files after the regular budget was exhausted.
220
+ charge = 1
221
+ }
222
+ if len(content) > productLimit || charge > evidenceTraceLimit-evidenceTraceFailureReserve-writer.bytes {
223
+ metadata["capture"] = "too-large"
224
+ _ = writer.append(trace.invocation, "trace-integrity-failure", writer.pid,
225
+ map[string]any{"bridge": trace.kind, "operation": "stdout-capture", "error": "Evidence trace payload budget exceeded"}, true)
226
+ return metadata
227
+ }
228
+ writer.bytes += charge
229
+ file, err := os.OpenFile(filepath.Join(writer.directory, name), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
230
+ if err == nil {
231
+ var count int
232
+ count, err = file.Write(content)
233
+ closeErr := file.Close()
234
+ if err == nil && count != len(content) {
235
+ err = io.ErrShortWrite
236
+ }
237
+ if err == nil {
238
+ err = closeErr
239
+ }
240
+ }
241
+ metadata["path"] = name
242
+ if err != nil {
243
+ metadata["capture"] = "IO-failed"
244
+ metadata["error"] = err.Error()
245
+ } else if exceeded {
246
+ metadata["capture"] = "too-large"
247
+ }
248
+ if err != nil || exceeded {
249
+ _ = writer.append(trace.invocation, "trace-integrity-failure", writer.pid,
250
+ map[string]any{"bridge": trace.kind, "operation": "stdout-capture", "capture": metadata}, true)
251
+ }
252
+ return metadata
253
+ }
254
+
255
+ // attempt observes the actual prepared command without changing its streams
256
+ // or executable. The timestamp is a lower call bound, not an OS spawn time.
257
+ // Observation IO contributes opt-in overhead inside the unchanged timeout.
258
+ func (trace *evidenceBridgeTrace) attempt(command *exec.Cmd) time.Time {
259
+ if trace == nil {
260
+ return time.Time{}
261
+ }
262
+ lower := time.Now().UTC()
263
+ trace.record("process-attempt", 0, map[string]any{
264
+ "bridge": trace.kind, "argv": command.Args, "cwd": command.Dir,
265
+ "executable": command.Path, "startLowerBound": lower.Format(time.RFC3339Nano),
266
+ "started": false, "observation": "before-Cmd.Run",
267
+ })
268
+ return lower
269
+ }
270
+
271
+ // preparationFailure records a real failure before any Run was attempted.
272
+ // Missing stdout is absence, not an invented empty JSON response. This event
273
+ // cannot count a child start or successful parse and never replaces the error.
274
+ func (trace *evidenceBridgeTrace) preparationFailure(err error) {
275
+ if trace == nil {
276
+ return
277
+ }
278
+ trace.record("bridge-result", trace.writer.pid, map[string]any{
279
+ "bridge": trace.kind, "nativeLookup": trace.nativeLookup, "preparationError": err.Error(),
280
+ "stdout": map[string]any{"capture": "absent", "observedByteLength": 0},
281
+ "unmarshalOutcome": "not-attempted", "runAttempted": false,
282
+ })
283
+ }
284
+
285
+ // result observes completed Run and unmarshal boundaries. The stdout capture
286
+ // is the original buffered slice; outcome IDs come only from successful actual
287
+ // native unmarshal. ProcessState is a native observation, not descendant proof.
288
+ // Signal is read only when the native Sys value exposes supported signal
289
+ // methods. Unavailable authority is distinct from an observed nonsignal exit;
290
+ // native ProcessState text is retained without parsing it into a guessed signal.
291
+ func (trace *evidenceBridgeTrace) result(command *exec.Cmd, stdout *limitedBuffer, stderr *limitedBuffer,
292
+ start time.Time, end time.Time, runErr error, parseOutcome string, parseErr error,
293
+ documents []string, problems []string,
294
+ ) {
295
+ if trace == nil {
296
+ return
297
+ }
298
+ pid := 0
299
+ if command.Process != nil {
300
+ pid = command.Process.Pid
301
+ }
302
+ var status *int
303
+ var signal any
304
+ var signalNumber any
305
+ signalObserved := false
306
+ processState := ""
307
+ if command.ProcessState != nil {
308
+ value := command.ProcessState.ExitCode()
309
+ status = &value
310
+ processState = command.ProcessState.String()
311
+ if state, ok := command.ProcessState.Sys().(interface {
312
+ Signaled() bool
313
+ Signal() syscall.Signal
314
+ }); ok {
315
+ signalObserved = true
316
+ if state.Signaled() {
317
+ signal = state.Signal().String()
318
+ signalNumber = int(state.Signal())
319
+ }
320
+ }
321
+ }
322
+ message := ""
323
+ if runErr != nil {
324
+ message = runErr.Error()
325
+ }
326
+ trace.record("process-result", pid, map[string]any{
327
+ "bridge": trace.kind, "argv": command.Args, "cwd": command.Dir, "executable": command.Path,
328
+ "started": command.Process != nil, "exitObserved": command.ProcessState != nil,
329
+ "status": status, "signal": signal, "signalNumber": signalNumber, "signalObserved": signalObserved,
330
+ "nativeProcessState": processState, "error": message, "join": "Cmd.Run-return",
331
+ "startLowerBound": start.Format(time.RFC3339Nano), "startUpperBound": end.Format(time.RFC3339Nano),
332
+ "stdoutLimitExceeded": stdout.Exceeded, "stderrLimitExceeded": stderr.Exceeded,
333
+ })
334
+ metadata := map[string]any{"capture": "absent", "observedByteLength": 0}
335
+ if command.Process != nil {
336
+ metadata = trace.capture(stdout.Bytes(), stdout.Limit, stdout.Exceeded)
337
+ }
338
+ message = ""
339
+ if parseErr != nil {
340
+ message = parseErr.Error()
341
+ }
342
+ trace.record("bridge-result", trace.writer.pid, map[string]any{
343
+ "bridge": trace.kind, "nativeLookup": trace.nativeLookup,
344
+ "runAttempted": true,
345
+ "stdout": metadata, "unmarshalOutcome": parseOutcome, "unmarshalError": message,
346
+ "documentIds": documents, "problemIds": problems,
347
+ })
348
+ }