@ttsc/evidence 0.30.4 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/lib/index.d.ts +2 -3
- package/lib/index.js +2 -3
- package/lib/internal/canonicalDigest.js +12 -6
- package/lib/internal/loadPrismaModels.js +52 -7
- package/lib/internal/loadSwaggerOperations.js +253 -59
- package/lib/internal/normalizeSwaggerDocument.js +320 -2
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
- package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
- package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
- package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
- package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
- package/lib/structures/index.d.ts +4 -0
- package/lib/structures/index.js +4 -0
- package/native/base.go +25 -23
- package/native/bridge_trace.go +348 -0
- package/native/comments.go +1 -1
- package/native/config.go +6 -3
- package/native/declaration.go +17 -43
- package/native/documentation_regions.go +214 -0
- package/native/documented.go +5 -4
- package/native/exports.go +1 -1
- package/native/fingerprint.go +2 -8
- package/native/glob.go +4 -1
- package/native/graph.go +59 -23
- package/native/graph_nodes.go +2 -1
- package/native/hints.go +22 -18
- package/native/imports.go +7 -6
- package/native/input_reader.go +83 -0
- package/native/inputs.go +13 -12
- package/native/loader.go +36 -24
- package/native/markdown.go +587 -177
- package/native/markdown_capture.go +223 -0
- package/native/model.go +2 -1
- package/native/prisma.go +110 -25
- package/native/prisma_cache.go +12 -8
- package/native/prisma_locator.go +70 -33
- package/native/review.go +8 -7
- package/native/rooted_typescript.go +3 -4
- package/native/swagger.go +123 -15
- package/native/swagger_cache.go +16 -12
- package/native/todo.go +12 -0
- package/native/typescript.go +129 -25
- package/package.json +9 -7
- package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
- package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
- package/lib/typings/index.d.ts +0 -5
- package/lib/typings/index.js +0 -21
- /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
- /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";
|
package/lib/structures/index.js
CHANGED
|
@@ -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.
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// `
|
|
296
|
-
//
|
|
297
|
-
//
|
|
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 :=
|
|
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 :=
|
|
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
|
|
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
|
|
533
|
-
//
|
|
534
|
-
//
|
|
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 :=
|
|
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
|
|
568
|
-
//
|
|
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
|
-
// `
|
|
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
|
+
}
|