@ttsc/lint 0.33.0-next.1 → 0.33.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.
@@ -6,6 +6,7 @@ import (
6
6
  "crypto/sha256"
7
7
  "encoding/hex"
8
8
  "encoding/json"
9
+ "errors"
9
10
  "fmt"
10
11
  "io"
11
12
  "net/url"
@@ -16,6 +17,7 @@ import (
16
17
  "sort"
17
18
  "strings"
18
19
  "sync"
20
+ "syscall"
19
21
  "time"
20
22
 
21
23
  "github.com/samchon/ttsc/packages/ttsc/driver/windowsjunction"
@@ -3860,7 +3862,18 @@ func loadTypeScriptConfigEvaluationWithin(
3860
3862
  location string,
3861
3863
  resolutionRoot string,
3862
3864
  ) (evaluatedConfigFile, error) {
3863
- tempDir, err := os.MkdirTemp(loaderTempBase(location, os.TempDir()), "ttsc-lint-config-")
3865
+ systemTemp := os.TempDir()
3866
+ preferred := loaderIdentityTempBase(location, systemTemp, os.Getenv("TTSC_NATIVE_PLUGIN_RECORD_ROOT"))
3867
+ var tempDir string
3868
+ var err error
3869
+ if preferred != "" {
3870
+ tempDir, err = os.MkdirTemp(preferred, "ttsc-lint-config-")
3871
+ }
3872
+ if preferred == "" || (err != nil &&
3873
+ (os.IsPermission(err) || os.IsNotExist(err) ||
3874
+ errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.EROFS))) {
3875
+ tempDir, err = os.MkdirTemp(loaderTempBase(location, systemTemp), "ttsc-lint-config-")
3876
+ }
3864
3877
  if err != nil {
3865
3878
  return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: create config loader tempdir: %w", err)
3866
3879
  }
@@ -5341,6 +5354,71 @@ func loaderRootDir(outDir string) string {
5341
5354
  return filepath.ToSlash(vol + `\`)
5342
5355
  }
5343
5356
 
5357
+ // loaderIdentityTempBase accepts an already-created identity directory from
5358
+ // this native plugin invocation's record store when TEMP is on another volume
5359
+ // or contains the config directory. A loader beneath that directory
5360
+ // cannot move the config directory's own metadata. The store and config must
5361
+ // match the config's lexical drive when one exists. The identity directory must
5362
+ // be a real direct child rather than a link or an ancestor of the config directory. An empty
5363
+ // result leaves the established loader placement in control.
5364
+ func loaderIdentityTempBase(location, systemTemp, recordRoot string) string {
5365
+ if recordRoot == "" || !filepath.IsAbs(recordRoot) {
5366
+ return ""
5367
+ }
5368
+ if !filepath.IsAbs(location) {
5369
+ return ""
5370
+ }
5371
+ volume := filepath.VolumeName(location)
5372
+ configDir := filepath.Dir(location)
5373
+ physicalConfigDir, err := loaderCanonicalDirectory(configDir)
5374
+ if err != nil {
5375
+ return ""
5376
+ }
5377
+ if strings.EqualFold(filepath.VolumeName(systemTemp), volume) {
5378
+ lexicalContains := false
5379
+ if volume != "" {
5380
+ lexicalContains, err = loaderContainsDirectory(systemTemp, configDir)
5381
+ if err != nil {
5382
+ return ""
5383
+ }
5384
+ }
5385
+ if !lexicalContains {
5386
+ physicalTemp, err := loaderCanonicalDirectory(systemTemp)
5387
+ if err != nil {
5388
+ return ""
5389
+ }
5390
+ physicalContains, err := loaderContainsDirectory(physicalTemp, physicalConfigDir)
5391
+ if err != nil || !physicalContains {
5392
+ return ""
5393
+ }
5394
+ }
5395
+ }
5396
+ physicalRoot, err := loaderCanonicalDirectory(recordRoot)
5397
+ if err != nil {
5398
+ return ""
5399
+ }
5400
+ identity := filepath.Join(recordRoot, "identities")
5401
+ entry, err := os.Lstat(identity)
5402
+ if err != nil || !entry.IsDir() || entry.Mode()&os.ModeSymlink != 0 {
5403
+ return ""
5404
+ }
5405
+ if _, err := os.Readlink(identity); err == nil {
5406
+ return ""
5407
+ }
5408
+ physicalIdentity, err := loaderCanonicalDirectory(identity)
5409
+ if err != nil || filepath.Dir(physicalIdentity) != physicalRoot ||
5410
+ !strings.EqualFold(filepath.VolumeName(physicalIdentity), volume) {
5411
+ return ""
5412
+ }
5413
+ if strings.EqualFold(filepath.VolumeName(physicalIdentity), filepath.VolumeName(physicalConfigDir)) {
5414
+ contained, err := loaderContainsDirectory(physicalIdentity, physicalConfigDir)
5415
+ if err != nil || contained {
5416
+ return ""
5417
+ }
5418
+ }
5419
+ return physicalIdentity
5420
+ }
5421
+
5344
5422
  // loaderTempBase picks the parent directory for the ephemeral config-loader
5345
5423
  // tree. The system temp dir is the default, but when it sits on a different
5346
5424
  // volume than the config file (Windows: TEMP on `C:`, project on `D:`) the
@@ -0,0 +1,34 @@
1
+ //go:build !windows
2
+
3
+ package linthost
4
+
5
+ import (
6
+ "os"
7
+ "path/filepath"
8
+ )
9
+
10
+ // loaderCanonicalDirectory resolves the existing directory on non-Windows hosts.
11
+ func loaderCanonicalDirectory(path string) (string, error) {
12
+ return filepath.EvalSymlinks(path)
13
+ }
14
+
15
+ // loaderContainsDirectory compares opened filesystem identities so case-only
16
+ // aliases on a case-insensitive POSIX volume cannot hide ancestry.
17
+ func loaderContainsDirectory(base, child string) (bool, error) {
18
+ baseInfo, err := os.Stat(base)
19
+ if err != nil {
20
+ return false, err
21
+ }
22
+ for current := filepath.Clean(child); ; current = filepath.Dir(current) {
23
+ info, err := os.Stat(current)
24
+ if err != nil {
25
+ return false, err
26
+ }
27
+ if os.SameFile(baseInfo, info) {
28
+ return true, nil
29
+ }
30
+ if parent := filepath.Dir(current); parent == current {
31
+ return false, nil
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,60 @@
1
+ //go:build windows
2
+
3
+ package linthost
4
+
5
+ import (
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ "golang.org/x/sys/windows"
10
+ )
11
+
12
+ // loaderCanonicalDirectory returns the opened directory's final filesystem
13
+ // path, including through NTFS junction ancestors that EvalSymlinks can miss.
14
+ func loaderCanonicalDirectory(path string) (string, error) {
15
+ pointer, err := windows.UTF16PtrFromString(path)
16
+ if err != nil {
17
+ return "", err
18
+ }
19
+ handle, err := windows.CreateFile(
20
+ pointer,
21
+ windows.FILE_READ_ATTRIBUTES,
22
+ windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
23
+ nil,
24
+ windows.OPEN_EXISTING,
25
+ windows.FILE_FLAG_BACKUP_SEMANTICS,
26
+ 0,
27
+ )
28
+ if err != nil {
29
+ return "", err
30
+ }
31
+ defer windows.CloseHandle(handle)
32
+
33
+ buffer := make([]uint16, windows.MAX_PATH)
34
+ for {
35
+ length, err := windows.GetFinalPathNameByHandle(handle, &buffer[0], uint32(len(buffer)), 0)
36
+ if err != nil {
37
+ return "", err
38
+ }
39
+ if length < uint32(len(buffer)) {
40
+ resolved := windows.UTF16ToString(buffer[:length])
41
+ if strings.HasPrefix(resolved, `\\?\UNC\`) {
42
+ resolved = `\\` + strings.TrimPrefix(resolved, `\\?\UNC\`)
43
+ } else {
44
+ resolved = strings.TrimPrefix(resolved, `\\?\`)
45
+ }
46
+ return filepath.Clean(resolved), nil
47
+ }
48
+ buffer = make([]uint16, length+1)
49
+ }
50
+ }
51
+
52
+ // loaderContainsDirectory compares Windows path spelling after final-path resolution.
53
+ func loaderContainsDirectory(base, child string) (bool, error) {
54
+ relative, err := filepath.Rel(base, child)
55
+ if err != nil {
56
+ return false, err
57
+ }
58
+ return relative == "." ||
59
+ (relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative)), nil
60
+ }
@@ -0,0 +1,117 @@
1
+ //go:build !windows
2
+
3
+ package linthost
4
+
5
+ import (
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ // TestLoaderIdentityTempBaseKeepsPOSIXConfigRootStable verifies an existing
13
+ // record store receives lint-loader scratch when TMPDIR is the config root.
14
+ //
15
+ // 1. Create an ordinary project and a separate record store with identities.
16
+ // 2. Select identities for project-root TMPDIR and create/remove scratch without changing the project directory.
17
+ // 3. Preserve independent TMPDIR and relative-config fallback, and reject linked or ancestor identities.
18
+ //
19
+ // @evidence contracts/testing.md#behavioral-verification The lint selector returns the existing identity directory for project-root TMPDIR, where actual temporary creation and removal leave project metadata and identity contents stable; unsupported layouts return no preference.
20
+ // @evidence contracts/testing.md#independent-expectations Authored sibling directories and before/after project metadata establish safe placement without using the selector as the oracle; an empty identities listing detects leaked scratch.
21
+ // @evidence contracts/testing.md#distinguishing-cases Project-root, symlinked, and available case-only project-root TMPDIR select identities, while independent TMPDIR, relative config, missing or linked identity, and an identity ancestor of config preserve fallback.
22
+ // @evidence contracts/testing.md#execution-ownership This named Go unit calls the lint selector and native filesystem operations in-process without launching ttsx or a plugin host.
23
+ func TestLoaderIdentityTempBaseKeepsPOSIXConfigRootStable(t *testing.T) {
24
+ root := t.TempDir()
25
+ project := filepath.Join(root, "project")
26
+ recordRoot := filepath.Join(root, "cache")
27
+ identity := filepath.Join(recordRoot, "identities")
28
+ for _, dir := range []string{project, identity} {
29
+ if err := os.MkdirAll(dir, 0o755); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ }
33
+ config := filepath.Join(project, "lint.config.ts")
34
+ want, err := filepath.EvalSymlinks(identity)
35
+ if err != nil {
36
+ t.Fatal(err)
37
+ }
38
+ if got := loaderIdentityTempBase(config, project, recordRoot); got != want {
39
+ t.Fatalf("project-root TMPDIR selected %q, want %q", got, want)
40
+ }
41
+ before, err := os.Stat(project)
42
+ if err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ scratch, err := os.MkdirTemp(want, "ttsc-lint-config-")
46
+ if err != nil {
47
+ t.Fatal(err)
48
+ }
49
+ t.Cleanup(func() {
50
+ if err := os.RemoveAll(scratch); err != nil {
51
+ t.Errorf("remove identity scratch: %v", err)
52
+ }
53
+ })
54
+ if err := os.RemoveAll(scratch); err != nil {
55
+ t.Fatal(err)
56
+ }
57
+ after, err := os.Stat(project)
58
+ if err != nil {
59
+ t.Fatal(err)
60
+ }
61
+ if !before.ModTime().Equal(after.ModTime()) {
62
+ t.Fatalf("project directory timestamp changed: %v -> %v", before.ModTime(), after.ModTime())
63
+ }
64
+ entries, err := os.ReadDir(identity)
65
+ if err != nil || len(entries) != 0 {
66
+ t.Fatalf("identity scratch remains: %v, %v", entries, err)
67
+ }
68
+ separateTemp := filepath.Join(root, "separate-temp")
69
+ if err := os.Mkdir(separateTemp, 0o755); err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ if got := loaderIdentityTempBase(config, separateTemp, recordRoot); got != "" {
73
+ t.Fatalf("independent TMPDIR selected identities: %q", got)
74
+ }
75
+ if got := loaderIdentityTempBase("lint.config.ts", project, recordRoot); got != "" {
76
+ t.Fatalf("relative config selected identities: %q", got)
77
+ }
78
+ linkedTemp := filepath.Join(root, "linked-temp")
79
+ if err := os.Symlink(project, linkedTemp); err != nil {
80
+ t.Fatal(err)
81
+ }
82
+ if got := loaderIdentityTempBase(config, linkedTemp, recordRoot); got != want {
83
+ t.Fatalf("linked project-root TMPDIR selected %q, want %q", got, want)
84
+ }
85
+ missingRoot := filepath.Join(root, "missing-cache")
86
+ if got := loaderIdentityTempBase(config, project, missingRoot); got != "" {
87
+ t.Fatalf("missing store selected identities: %q", got)
88
+ }
89
+ linkedIdentityRoot := filepath.Join(root, "linked-identity-cache")
90
+ if err := os.Mkdir(linkedIdentityRoot, 0o755); err != nil {
91
+ t.Fatal(err)
92
+ }
93
+ if err := os.Symlink(identity, filepath.Join(linkedIdentityRoot, "identities")); err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ if got := loaderIdentityTempBase(config, project, linkedIdentityRoot); got != "" {
97
+ t.Fatalf("linked identity child selected: %q", got)
98
+ }
99
+ nested := filepath.Join(identity, "nested")
100
+ if err := os.Mkdir(nested, 0o755); err != nil {
101
+ t.Fatal(err)
102
+ }
103
+ if got := loaderIdentityTempBase(filepath.Join(nested, "lint.config.ts"), project, recordRoot); got != "" {
104
+ t.Fatalf("identity ancestor of config selected: %q", got)
105
+ }
106
+ alternateCase := strings.ToUpper(project)
107
+ projectInfo, projectErr := os.Stat(project)
108
+ aliasInfo, aliasErr := os.Stat(alternateCase)
109
+ if projectErr == nil && aliasErr == nil && os.SameFile(projectInfo, aliasInfo) {
110
+ if got := loaderIdentityTempBase(config, alternateCase, recordRoot); got != want {
111
+ t.Fatalf("case-only ancestor TMPDIR selected %q, want %q", got, want)
112
+ }
113
+ if got := loaderIdentityTempBase(filepath.Join(strings.ToUpper(identity), "nested", "lint.config.ts"), project, recordRoot); got != "" {
114
+ t.Fatalf("case-only identity ancestor selected: %q", got)
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,212 @@
1
+ //go:build windows
2
+
3
+ package linthost
4
+
5
+ import (
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ )
12
+
13
+ // TestWindowsLoaderIdentityTempBasePreservesConfigDirectory verifies a native
14
+ // config loader can use an already-open record store without moving the config
15
+ // directory's metadata during temporary loader creation and removal.
16
+ //
17
+ // 1. Choose a different simulated system-temp drive and an existing identity directory.
18
+ // 2. Create and remove a loader there, then assert the config directory's timestamp stays fixed.
19
+ // 3. Accept project-root and same-drive junction stores and ancestor TEMP, then reject missing, blocked, linked, and config-ancestor identities.
20
+ // 4. Accept lexical-volume identities for a cross-drive config junction and reject a physical-volume store.
21
+ //
22
+ // @evidence contracts/testing.md#behavioral-verification The selected identity directory hosts actual MkdirTemp/RemoveAll work without changing the config directory timestamp or retaining scratch; unsupported stores return no preference and legacy placement remains on the config volume.
23
+ // @evidence contracts/testing.md#independent-expectations Windows directory metadata changes when a direct child is created or removed; authored sibling, blocked, missing, junction, ancestor, and same-volume layouts define independent valid and invalid inputs.
24
+ // @evidence contracts/testing.md#distinguishing-cases Preexisting sibling, project-root, same-drive junction and ancestor TEMP stores are accepted, while independent same-drive TEMP, absent, blocked or linked identities, a cross-drive cache store, and an identity ancestor of the config are rejected. The established fallback selector remains covered by TestWindowsLoaderTempBaseStaysOnConfigVolume.
25
+ // @evidence contracts/testing.md#execution-ownership This Go unit invokes the real filesystem selector and native temporary directory calls in-process; cmd creates only the Windows junction fixture, and no ttsx child or native plugin build runs.
26
+ func TestWindowsLoaderIdentityTempBasePreservesConfigDirectory(t *testing.T) {
27
+ root := t.TempDir()
28
+ project := filepath.Join(root, "project")
29
+ recordRoot := filepath.Join(root, "cache")
30
+ identity := filepath.Join(recordRoot, "identities")
31
+ for _, dir := range []string{project, identity} {
32
+ if err := os.MkdirAll(dir, 0o755); err != nil {
33
+ t.Fatal(err)
34
+ }
35
+ }
36
+ config := filepath.Join(project, "lint.config.ts")
37
+ otherVolumeTemp := `Z:\ttsc-fake-temp`
38
+ if strings.EqualFold(filepath.VolumeName(root), "Z:") {
39
+ otherVolumeTemp = `Y:\ttsc-fake-temp`
40
+ }
41
+ want, err := filepath.EvalSymlinks(identity)
42
+ if err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, recordRoot); got != want {
46
+ t.Fatalf("identity scratch base mismatch: %q != %q", got, want)
47
+ }
48
+ before, err := os.Stat(project)
49
+ if err != nil {
50
+ t.Fatal(err)
51
+ }
52
+ temporary, err := os.MkdirTemp(want, "ttsc-lint-config-")
53
+ if err != nil {
54
+ t.Fatal(err)
55
+ }
56
+ if err := os.RemoveAll(temporary); err != nil {
57
+ t.Fatal(err)
58
+ }
59
+ after, err := os.Stat(project)
60
+ if err != nil {
61
+ t.Fatal(err)
62
+ }
63
+ if !before.ModTime().Equal(after.ModTime()) {
64
+ t.Fatalf("config directory timestamp moved: %v -> %v", before.ModTime(), after.ModTime())
65
+ }
66
+ entries, err := os.ReadDir(identity)
67
+ if err != nil {
68
+ t.Fatal(err)
69
+ }
70
+ for _, entry := range entries {
71
+ if strings.HasPrefix(entry.Name(), "ttsc-lint-config-") {
72
+ t.Fatalf("identity scratch remains after cleanup: %s", entry.Name())
73
+ }
74
+ }
75
+ projectIdentity := filepath.Join(project, "identities")
76
+ if err := os.Mkdir(projectIdentity, 0o755); err != nil {
77
+ t.Fatal(err)
78
+ }
79
+ projectWant, err := filepath.EvalSymlinks(projectIdentity)
80
+ if err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, project); got != projectWant {
84
+ t.Fatalf("project-root store mismatch: %q != %q", got, projectWant)
85
+ }
86
+ linkedProject := filepath.Join(root, "linked-project")
87
+ if output, err := exec.Command("cmd", "/c", "mklink", "/J", linkedProject, project).CombinedOutput(); err != nil {
88
+ t.Fatalf("same-drive config junction failed: %v: %s", err, output)
89
+ }
90
+ if got := loaderIdentityTempBase(filepath.Join(linkedProject, "lint.config.ts"), otherVolumeTemp, recordRoot); got != want {
91
+ t.Fatalf("same-drive linked config did not select canonical identities: %q", got)
92
+ }
93
+ linkedRecordRoot := filepath.Join(root, "linked-record-root")
94
+ if output, err := exec.Command("cmd", "/c", "mklink", "/J", linkedRecordRoot, recordRoot).CombinedOutput(); err != nil {
95
+ t.Fatalf("same-drive record junction failed: %v: %s", err, output)
96
+ }
97
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, linkedRecordRoot); got != want {
98
+ t.Fatalf("same-drive linked record root did not select canonical identities: %q", got)
99
+ }
100
+ before, err = os.Stat(project)
101
+ if err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ temporary, err = os.MkdirTemp(projectWant, "ttsc-lint-config-")
105
+ if err != nil {
106
+ t.Fatal(err)
107
+ }
108
+ if err := os.RemoveAll(temporary); err != nil {
109
+ t.Fatal(err)
110
+ }
111
+ after, err = os.Stat(project)
112
+ if err != nil {
113
+ t.Fatal(err)
114
+ }
115
+ if !before.ModTime().Equal(after.ModTime()) {
116
+ t.Fatalf("project-root store moved config directory timestamp: %v -> %v", before.ModTime(), after.ModTime())
117
+ }
118
+ separateTemp := filepath.Join(root, "separate-temp")
119
+ if err := os.Mkdir(separateTemp, 0o755); err != nil {
120
+ t.Fatal(err)
121
+ }
122
+ if got := loaderIdentityTempBase(config, separateTemp, recordRoot); got != "" {
123
+ t.Fatalf("independent same-volume temp unexpectedly selected cache: %q", got)
124
+ }
125
+ for _, ancestorTemp := range []string{project, root} {
126
+ if got := loaderIdentityTempBase(config, ancestorTemp, recordRoot); got != want {
127
+ t.Fatalf("ancestor TEMP did not select identities: %q", got)
128
+ }
129
+ }
130
+ linkedTemp := filepath.Join(root, "linked-temp")
131
+ if output, err := exec.Command("cmd", "/c", "mklink", "/J", linkedTemp, project).CombinedOutput(); err != nil {
132
+ t.Fatalf("TEMP junction failed: %v: %s", err, output)
133
+ }
134
+ if got := loaderIdentityTempBase(config, linkedTemp, recordRoot); got != want {
135
+ t.Fatalf("physical ancestor TEMP did not select identities: %q", got)
136
+ }
137
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, filepath.Join(root, "missing")); got != "" {
138
+ t.Fatalf("missing store unexpectedly selected cache: %q", got)
139
+ }
140
+ blockedRoot := filepath.Join(root, "blocked-cache")
141
+ if err := os.Mkdir(blockedRoot, 0o755); err != nil {
142
+ t.Fatal(err)
143
+ }
144
+ if err := os.WriteFile(filepath.Join(blockedRoot, "identities"), nil, 0o644); err != nil {
145
+ t.Fatal(err)
146
+ }
147
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, blockedRoot); got != "" {
148
+ t.Fatalf("blocked identity child unexpectedly selected cache: %q", got)
149
+ }
150
+ if fallback := loaderTempBase(config, otherVolumeTemp); fallback == "" ||
151
+ !strings.EqualFold(filepath.VolumeName(fallback), filepath.VolumeName(config)) {
152
+ t.Fatalf("blocked identity fallback left config volume: %q", fallback)
153
+ }
154
+ if err := os.Mkdir(filepath.Join(identity, "nested"), 0o755); err != nil {
155
+ t.Fatal(err)
156
+ }
157
+ if got := loaderIdentityTempBase(filepath.Join(identity, "nested", "lint.config.ts"), otherVolumeTemp, recordRoot); got != "" {
158
+ t.Fatalf("config inside identity cache unexpectedly selected cache: %q", got)
159
+ }
160
+
161
+ linkedRoot := filepath.Join(root, "linked-cache")
162
+ realIdentity := filepath.Join(root, "real-identities")
163
+ for _, dir := range []string{linkedRoot, realIdentity} {
164
+ if err := os.MkdirAll(dir, 0o755); err != nil {
165
+ t.Fatal(err)
166
+ }
167
+ }
168
+ junction := filepath.Join(linkedRoot, "identities")
169
+ if output, err := exec.Command("cmd", "/c", "mklink", "/J", junction, realIdentity).CombinedOutput(); err != nil {
170
+ t.Fatalf("mklink /J failed: %v: %s", err, output)
171
+ }
172
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, linkedRoot); got != "" {
173
+ t.Fatalf("linked identities unexpectedly selected cache: %q", got)
174
+ }
175
+ checkout, err := os.Getwd()
176
+ if err != nil {
177
+ t.Fatal(err)
178
+ }
179
+ if !strings.EqualFold(filepath.VolumeName(checkout), filepath.VolumeName(root)) {
180
+ checkoutRoot := filepath.Clean(filepath.Join(checkout, "..", "..", ".."))
181
+ otherPhysical, err := os.MkdirTemp(checkoutRoot, "ttsc-lint-junction-test-")
182
+ if err != nil {
183
+ t.Fatal(err)
184
+ }
185
+ t.Cleanup(func() {
186
+ if err := os.RemoveAll(otherPhysical); err != nil {
187
+ t.Errorf("remove cross-volume fixture: %v", err)
188
+ }
189
+ })
190
+ alias := filepath.Join(root, "linked-cache-root")
191
+ if err := os.Mkdir(filepath.Join(otherPhysical, "identities"), 0o755); err != nil {
192
+ t.Fatal(err)
193
+ }
194
+ if output, err := exec.Command("cmd", "/c", "mklink", "/J", alias, otherPhysical).CombinedOutput(); err != nil {
195
+ t.Fatalf("cross-volume mklink /J failed: %v: %s", err, output)
196
+ }
197
+ if got := loaderIdentityTempBase(config, otherVolumeTemp, alias); got != "" {
198
+ t.Fatalf("cross-volume cache junction unexpectedly selected: %q", got)
199
+ }
200
+ linkedConfigRoot := filepath.Join(root, "linked-cross-volume-config")
201
+ if output, err := exec.Command("cmd", "/c", "mklink", "/J", linkedConfigRoot, otherPhysical).CombinedOutput(); err != nil {
202
+ t.Fatalf("cross-volume config junction failed: %v: %s", err, output)
203
+ }
204
+ linkedConfig := filepath.Join(linkedConfigRoot, "lint.config.ts")
205
+ if got := loaderIdentityTempBase(linkedConfig, otherVolumeTemp, recordRoot); got != want {
206
+ t.Fatalf("lexical-volume identities not selected for linked config: %q", got)
207
+ }
208
+ if got := loaderIdentityTempBase(linkedConfig, otherVolumeTemp, otherPhysical); got != "" {
209
+ t.Fatalf("physical-volume identities selected for linked config: %q", got)
210
+ }
211
+ }
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.33.0-next.1",
3
+ "version": "0.33.1",
4
4
  "description": "Reference ttsc plugin: ESLint-style lint rules over the TypeScript-Go Program used by the type-check pass.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -38,7 +38,7 @@
38
38
  "@types/node": "^25.3.0",
39
39
  "rimraf": "^6.1.2",
40
40
  "typescript": "^7.0.2",
41
- "ttsc": "0.33.0-next.1"
41
+ "ttsc": "0.33.1"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",