ttsc 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.
- package/cmd/ttscgraph/serve_alternative_members_and_reads_refresh_test.go +95 -0
- package/driver/configloaderscratch/canonical_directory_other.go +34 -0
- package/driver/configloaderscratch/canonical_directory_windows.go +60 -0
- package/driver/configloaderscratch/identity_temp_base.go +81 -0
- package/driver/configloaderscratch/identity_temp_base_posix_test.go +124 -0
- package/driver/configloaderscratch/identity_temp_base_windows_test.go +156 -0
- package/driver/emit_provenance.go +7 -5
- package/driver/resolutioninputs/recorder.cjs +16 -4
- package/driver/rewrite.go +25 -16
- package/driver/write_file_if_changed.go +69 -0
- package/driver/write_file_if_changed_test.go +148 -0
- package/internal/graph/alternative_member_declarations_resolve_uses_test.go +145 -0
- package/internal/graph/build.go +60 -18
- package/internal/graph/declaration_members.go +143 -0
- package/internal/graph/edges.go +108 -36
- package/internal/graph/namespace_private_declarations_keep_lexical_identity_test.go +85 -0
- package/internal/graph/resolve.go +30 -5
- package/internal/graph/value_reads_cover_expression_positions_test.go +88 -0
- package/lib/compiler/internal/build/BuildExecution.d.ts +11 -7
- package/lib/compiler/internal/build/BuildExecution.js +22 -8
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +10 -10
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js +19 -14
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
- package/lib/compiler/internal/build/RunBuildOptions.d.ts +8 -1
- package/lib/compiler/internal/build/TsgoArguments.d.ts +16 -0
- package/lib/compiler/internal/build/TsgoArguments.js +19 -0
- package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.d.ts +16 -13
- package/lib/compiler/internal/captureProcessOutput.js +22 -14
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +5 -1
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.d.ts +43 -0
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js +106 -0
- package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js.map +1 -0
- package/lib/launcher/internal/runtime/installRuntimeHooks.js +15 -29
- package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/ttscserver/runTtscserver.js +1 -0
- package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
- package/lib/plugin/internal/CapabilityResolutionFormat.js +4 -3
- package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
- package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
- package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
- package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
- package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +3 -3
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
- package/lib/plugin/internal/load/descriptorRecordEnvironment.d.ts +15 -0
- package/lib/plugin/internal/load/descriptorRecordEnvironment.js +24 -0
- package/lib/plugin/internal/load/descriptorRecordEnvironment.js.map +1 -0
- package/lib/plugin/internal/load/loadProjectPlugins.js +28 -26
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.d.ts +18 -0
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js +46 -0
- package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js.map +1 -0
- package/package.json +8 -8
- package/src/compiler/internal/build/BuildExecution.ts +30 -10
- package/src/compiler/internal/build/ResidentCheckWatchSession.ts +23 -22
- package/src/compiler/internal/build/RunBuildOptions.ts +9 -1
- package/src/compiler/internal/build/TsgoArguments.ts +22 -0
- package/src/compiler/internal/captureProcessOutput.ts +28 -14
- package/src/compiler/internal/startResidentTransform.ts +7 -1
- package/src/compiler/internal/transformProjectInMemory.ts +9 -1
- package/src/launcher/internal/runtime/PluginDescriptorInputReport.ts +100 -0
- package/src/launcher/internal/runtime/installRuntimeHooks.ts +57 -55
- package/src/launcher/internal/ttscserver/runTtscserver.ts +5 -0
- package/src/plugin/internal/CapabilityResolutionFormat.ts +4 -3
- package/src/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
- package/src/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
- package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +3 -3
- package/src/plugin/internal/load/descriptorRecordEnvironment.ts +24 -0
- package/src/plugin/internal/load/loadProjectPlugins.ts +45 -29
- package/src/plugin/internal/source/selectedRecordIdentityTempParent.ts +40 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Verifies reported graph facts survive resident edits and barrel imports.
|
|
9
|
+
//
|
|
10
|
+
// 1. Load namespace-private functions, a nested type member and a parameter property through a barrel.
|
|
11
|
+
// 2. Rename the nested member and replace a class expression in the same resident session.
|
|
12
|
+
// 3. Require current serialized nodes and uses, reject stale members and false namespace calls, and compare every generation with a full build.
|
|
13
|
+
//
|
|
14
|
+
// @evidence contracts/testing.md#behavioral-verification Resident shard publication must preserve lexical identities and actual value/member uses, then remove obsolete members after edits. Literal serialized endpoints test semantics independently of full-build equivalence.
|
|
15
|
+
// @evidence contracts/testing.md#independent-expectations Source-authored deep/changed and render/replaced names prescribe current and stale nodes. Only B.hidden calls target, reader accesses num, change accesses the current Props.nested member, cost accesses Sale.price and draw calls the current Widget member.
|
|
16
|
+
// @evidence contracts/testing.md#distinguishing-cases Initial and renamed generations cover cross-file barrel aliases, namespace-private declarations, value returns, nested type members, parameter properties and named/anonymous class expressions. Old member names and A.hidden-to-target provide negative controls; direct syntax cases belong to the graph package's source units.
|
|
17
|
+
// @evidence contracts/testing.md#execution-ownership This Go source unit owns one temporary project and in-process graphSession, closes it and invokes native preparation/projection directly without building or launching a product process.
|
|
18
|
+
func TestServeAlternativeMembersAndReadsRefresh(t *testing.T) {
|
|
19
|
+
root := t.TempDir()
|
|
20
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{"compilerOptions":{"target":"ES2022","module":"commonjs","strict":true},"include":["src"]}`)
|
|
21
|
+
writeGraphFile(t, filepath.Join(root, "src", "barrel.ts"), "export * from './api';\n")
|
|
22
|
+
var session *graphSession
|
|
23
|
+
for _, state := range []struct{ field, method, className string }{{"deep", "render", "Internal"}, {"changed", "replaced", ""}} {
|
|
24
|
+
writeGraphFile(t, filepath.Join(root, "src", "api.ts"), `export const num = 1;
|
|
25
|
+
export function target() { return 0; }
|
|
26
|
+
export namespace A { function hidden() { return 1; } export const a = hidden(); }
|
|
27
|
+
export namespace B { function hidden() { return target(); } export const b = hidden(); }
|
|
28
|
+
export type Props = { nested: { `+state.field+`: number } };
|
|
29
|
+
export class Sale { constructor(public readonly price: number) {} }
|
|
30
|
+
export const Widget = class `+state.className+` { `+state.method+`() { return num; } };
|
|
31
|
+
`)
|
|
32
|
+
writeGraphFile(t, filepath.Join(root, "src", "use.ts"), `import { num, Sale, Widget, type Props } from './barrel';
|
|
33
|
+
export function reader() { return num; }
|
|
34
|
+
export function change(props: Props) { return props.nested.`+state.field+`; }
|
|
35
|
+
export function cost(sale: Sale) { return sale.price; }
|
|
36
|
+
export function draw() { return new Widget().`+state.method+`(); }
|
|
37
|
+
`)
|
|
38
|
+
if session == nil {
|
|
39
|
+
var err error
|
|
40
|
+
session, err = newGraphSession(root, "tsconfig.json")
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatal(err)
|
|
43
|
+
}
|
|
44
|
+
defer session.Close()
|
|
45
|
+
}
|
|
46
|
+
snapshot, _, _, err := snapshotGraphShardState(session)
|
|
47
|
+
if err != nil || snapshot == nil {
|
|
48
|
+
t.Fatalf("snapshot: %v", err)
|
|
49
|
+
}
|
|
50
|
+
assertServeShardFactsMatchFullDump(t, session)
|
|
51
|
+
dump, err := projectGraphDump(session)
|
|
52
|
+
if err != nil {
|
|
53
|
+
t.Fatal(err)
|
|
54
|
+
}
|
|
55
|
+
ids := map[string]string{}
|
|
56
|
+
for _, node := range dump.Nodes {
|
|
57
|
+
name := node.QualifiedName
|
|
58
|
+
if name == "" {
|
|
59
|
+
name = node.Name
|
|
60
|
+
}
|
|
61
|
+
ids[name] = node.ID
|
|
62
|
+
if (name == "Props.nested.deep" && state.field != "deep") || (name == "Widget.render" && state.method != "render") {
|
|
63
|
+
t.Errorf("stale declaration %s", name)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for _, pair := range []struct {
|
|
67
|
+
from, to, kind string
|
|
68
|
+
want bool
|
|
69
|
+
}{
|
|
70
|
+
{"reader", "num", "accesses", true},
|
|
71
|
+
{"change", "Props.nested." + state.field, "accesses", true},
|
|
72
|
+
{"cost", "Sale.price", "accesses", true},
|
|
73
|
+
{"draw", "Widget." + state.method, "calls", true},
|
|
74
|
+
{"Widget." + state.method, "num", "accesses", true},
|
|
75
|
+
{"A.a", "A.hidden", "calls", true},
|
|
76
|
+
{"B.b", "B.hidden", "calls", true},
|
|
77
|
+
{"B.hidden", "target", "calls", true},
|
|
78
|
+
{"A.hidden", "target", "calls", false},
|
|
79
|
+
} {
|
|
80
|
+
if ids[pair.from] == "" || ids[pair.to] == "" {
|
|
81
|
+
t.Errorf("missing endpoint %s or %s", pair.from, pair.to)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
found := false
|
|
85
|
+
for _, edge := range dump.Edges {
|
|
86
|
+
if edge.From == ids[pair.from] && edge.To == ids[pair.to] && edge.Kind == pair.kind {
|
|
87
|
+
found = true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if found != pair.want {
|
|
91
|
+
t.Errorf("%s -> %s (%s) = %t, want %t", pair.from, pair.to, pair.kind, found, pair.want)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package configloaderscratch
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// canonicalDirectory resolves the existing directory on non-Windows hosts.
|
|
11
|
+
func canonicalDirectory(path string) (string, error) {
|
|
12
|
+
return filepath.EvalSymlinks(path)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// containsDirectory compares opened filesystem identities so case-only aliases
|
|
16
|
+
// on a case-insensitive POSIX volume cannot hide an ancestor relationship.
|
|
17
|
+
func containsDirectory(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 configloaderscratch
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"golang.org/x/sys/windows"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// canonicalDirectory returns the opened directory's final filesystem path,
|
|
13
|
+
// including through NTFS junction ancestors that EvalSymlinks can leave intact.
|
|
14
|
+
func canonicalDirectory(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
|
+
// containsDirectory compares Windows path spelling after final-path resolution.
|
|
53
|
+
func containsDirectory(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,81 @@
|
|
|
1
|
+
package configloaderscratch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// IdentityTempBase selects the existing identities directory in the current
|
|
10
|
+
// invocation's record store when Windows TEMP is on another drive or TEMP
|
|
11
|
+
// contains the config directory. The directory must be an ordinary direct child
|
|
12
|
+
// on the config's lexical drive when one exists, and it cannot contain the config
|
|
13
|
+
// directory. Scratch changes only the existing identities directory's entries, even when that
|
|
14
|
+
// directory is below the config directory. An empty result preserves the
|
|
15
|
+
// loader's existing placement.
|
|
16
|
+
//
|
|
17
|
+
// @evidence contracts/common.md#principled-implementation The selected record store already owns its identities child before plugin evaluation; resolved directory identities, child metadata, Windows lexical drive comparison, and ancestry checks ensure identities is not the config directory or its ancestor. Scratch changes only identities entries, not the config directory's direct entries or metadata.
|
|
18
|
+
// @evidence contracts/common.md#clear-and-simple-design This selector only chooses an optional base; each loader retains its own historical TEMP fallback and creation/cleanup lifetime.
|
|
19
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The caller supplies its selected store root, and each path premise is checked on disk rather than inferred from a package or operating-system name.
|
|
20
|
+
// @evidence contracts/common.md#meaningful-documentation The comment states the store's ownership, required directory relationship, and empty-result meaning.
|
|
21
|
+
// @evidence contracts/portability.md#os-neutral-implementation IsAbs accepts POSIX absolute paths despite their empty VolumeName; EvalSymlinks and SameFile resolve POSIX links and case aliases. On Windows, VolumeName keeps TypeScript-Go drive spelling and opened directory handles resolve junctions. Unavailable directories fail closed; no URL form or process argument is used.
|
|
22
|
+
// @evidence contracts/performance.md#efficient-algorithms Windows uses at most four directory opens and constant path operations; POSIX compares directory identities along at most two bounded ancestor walks. Neither scans directory contents or source bytes.
|
|
23
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work The store and its links may change between config loads, so each call observes current metadata.
|
|
24
|
+
// @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Metadata operations close within their calls; this selector retains no handle, directory, or task, while its caller owns temporary creation and removal.
|
|
25
|
+
func IdentityTempBase(location, systemTemp, recordRoot string) string {
|
|
26
|
+
if recordRoot == "" || !filepath.IsAbs(recordRoot) {
|
|
27
|
+
return ""
|
|
28
|
+
}
|
|
29
|
+
if !filepath.IsAbs(location) {
|
|
30
|
+
return ""
|
|
31
|
+
}
|
|
32
|
+
volume := filepath.VolumeName(location)
|
|
33
|
+
configDir := filepath.Dir(location)
|
|
34
|
+
physicalConfigDir, err := canonicalDirectory(configDir)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return ""
|
|
37
|
+
}
|
|
38
|
+
if strings.EqualFold(filepath.VolumeName(systemTemp), volume) {
|
|
39
|
+
lexicalContains := false
|
|
40
|
+
if volume != "" {
|
|
41
|
+
lexicalContains, err = containsDirectory(systemTemp, configDir)
|
|
42
|
+
if err != nil {
|
|
43
|
+
return ""
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if !lexicalContains {
|
|
47
|
+
physicalTemp, err := canonicalDirectory(systemTemp)
|
|
48
|
+
if err != nil {
|
|
49
|
+
return ""
|
|
50
|
+
}
|
|
51
|
+
physicalContains, err := containsDirectory(physicalTemp, physicalConfigDir)
|
|
52
|
+
if err != nil || !physicalContains {
|
|
53
|
+
return ""
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
physicalRoot, err := canonicalDirectory(recordRoot)
|
|
58
|
+
if err != nil {
|
|
59
|
+
return ""
|
|
60
|
+
}
|
|
61
|
+
identity := filepath.Join(recordRoot, "identities")
|
|
62
|
+
entry, err := os.Lstat(identity)
|
|
63
|
+
if err != nil || !entry.IsDir() || entry.Mode()&os.ModeSymlink != 0 {
|
|
64
|
+
return ""
|
|
65
|
+
}
|
|
66
|
+
if _, err := os.Readlink(identity); err == nil {
|
|
67
|
+
return ""
|
|
68
|
+
}
|
|
69
|
+
physicalIdentity, err := canonicalDirectory(identity)
|
|
70
|
+
if err != nil || filepath.Dir(physicalIdentity) != physicalRoot ||
|
|
71
|
+
!strings.EqualFold(filepath.VolumeName(physicalIdentity), volume) {
|
|
72
|
+
return ""
|
|
73
|
+
}
|
|
74
|
+
if strings.EqualFold(filepath.VolumeName(physicalIdentity), filepath.VolumeName(physicalConfigDir)) {
|
|
75
|
+
contained, err := containsDirectory(physicalIdentity, physicalConfigDir)
|
|
76
|
+
if err != nil || contained {
|
|
77
|
+
return ""
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return physicalIdentity
|
|
81
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package configloaderscratch
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// TestIdentityTempBaseKeepsPOSIXConfigRootStable verifies an existing record
|
|
13
|
+
// store receives loader scratch when TMPDIR contains the config directory.
|
|
14
|
+
//
|
|
15
|
+
// 1. Create a project and a separate record store with an ordinary identities child.
|
|
16
|
+
// 2. Select identities for ancestor TMPDIR, then create and remove scratch without changing the project directory.
|
|
17
|
+
// 3. Preserve independent TMPDIR and relative-config behavior, and reject linked or ancestor identities.
|
|
18
|
+
//
|
|
19
|
+
// @evidence contracts/testing.md#behavioral-verification The selector returns an existing identities child for ancestor TMPDIR, and actual MkdirTemp/RemoveAll there leaves the project timestamp and cache contents unchanged; unsupported layouts return no preference.
|
|
20
|
+
// @evidence contracts/testing.md#independent-expectations The authored sibling directories, before/after project stat, and empty identity listing independently establish placement and cleanup instead of deriving them from the selector.
|
|
21
|
+
// @evidence contracts/testing.md#distinguishing-cases Ancestor, symlinked ancestor, and available case-only ancestor TMPDIR select identities; independent TMPDIR, relative config, missing or linked identity, and an identity ancestor of the config keep the existing fallback.
|
|
22
|
+
// @evidence contracts/testing.md#execution-ownership This named Go unit exercises the portable selector and filesystem directly without launching a compiler, config evaluator, or native plugin.
|
|
23
|
+
func TestIdentityTempBaseKeepsPOSIXConfigRootStable(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, "banner.config.ts")
|
|
34
|
+
want, err := filepath.EvalSymlinks(identity)
|
|
35
|
+
if err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
if got := IdentityTempBase(config, project, recordRoot); got != want {
|
|
39
|
+
t.Fatalf("ancestor 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-banner-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 := IdentityTempBase(config, separateTemp, recordRoot); got != "" {
|
|
73
|
+
t.Fatalf("independent TMPDIR selected identities: %q", got)
|
|
74
|
+
}
|
|
75
|
+
if got := IdentityTempBase("banner.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 := IdentityTempBase(config, linkedTemp, recordRoot); got != want {
|
|
83
|
+
t.Fatalf("linked ancestor TMPDIR selected %q, want %q", got, want)
|
|
84
|
+
}
|
|
85
|
+
linkedRoot := filepath.Join(root, "linked-cache")
|
|
86
|
+
if err := os.Symlink(recordRoot, linkedRoot); err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
if got := IdentityTempBase(config, project, linkedRoot); got != want {
|
|
90
|
+
t.Fatalf("linked store root selected %q, want %q", got, want)
|
|
91
|
+
}
|
|
92
|
+
missingRoot := filepath.Join(root, "missing-cache")
|
|
93
|
+
if got := IdentityTempBase(config, project, missingRoot); got != "" {
|
|
94
|
+
t.Fatalf("missing store selected identities: %q", got)
|
|
95
|
+
}
|
|
96
|
+
linkedIdentityRoot := filepath.Join(root, "linked-identity-cache")
|
|
97
|
+
if err := os.Mkdir(linkedIdentityRoot, 0o755); err != nil {
|
|
98
|
+
t.Fatal(err)
|
|
99
|
+
}
|
|
100
|
+
if err := os.Symlink(identity, filepath.Join(linkedIdentityRoot, "identities")); err != nil {
|
|
101
|
+
t.Fatal(err)
|
|
102
|
+
}
|
|
103
|
+
if got := IdentityTempBase(config, project, linkedIdentityRoot); got != "" {
|
|
104
|
+
t.Fatalf("linked identity child selected: %q", got)
|
|
105
|
+
}
|
|
106
|
+
nested := filepath.Join(identity, "nested")
|
|
107
|
+
if err := os.Mkdir(nested, 0o755); err != nil {
|
|
108
|
+
t.Fatal(err)
|
|
109
|
+
}
|
|
110
|
+
if got := IdentityTempBase(filepath.Join(nested, "banner.config.ts"), project, recordRoot); got != "" {
|
|
111
|
+
t.Fatalf("identity ancestor of config selected: %q", got)
|
|
112
|
+
}
|
|
113
|
+
alternateCase := strings.ToUpper(project)
|
|
114
|
+
projectInfo, projectErr := os.Stat(project)
|
|
115
|
+
aliasInfo, aliasErr := os.Stat(alternateCase)
|
|
116
|
+
if projectErr == nil && aliasErr == nil && os.SameFile(projectInfo, aliasInfo) {
|
|
117
|
+
if got := IdentityTempBase(config, alternateCase, recordRoot); got != want {
|
|
118
|
+
t.Fatalf("case-only ancestor TMPDIR selected %q, want %q", got, want)
|
|
119
|
+
}
|
|
120
|
+
if got := IdentityTempBase(filepath.Join(strings.ToUpper(identity), "nested", "banner.config.ts"), project, recordRoot); got != "" {
|
|
121
|
+
t.Fatalf("case-only identity ancestor selected: %q", got)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package configloaderscratch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"os/exec"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestIdentityTempBaseUsesOnlyAnExistingOrdinaryChild verifies the shared
|
|
12
|
+
// loader selector accepts an owned sibling directory and same-drive junction
|
|
13
|
+
// ancestors while rejecting linked children, missing stores, and ancestors.
|
|
14
|
+
//
|
|
15
|
+
// 1. Create an ordinary project and record-store sibling, then select its preexisting identities child.
|
|
16
|
+
// 2. Accept same-drive cache/config junctions and ancestor TEMP, but preserve independent same-drive TEMP.
|
|
17
|
+
// 3. Accept lexical-volume identities for a cross-drive config junction and reject a physical-volume store.
|
|
18
|
+
// 4. Create and remove a scratch child under the selected base and check the project directory stays fixed.
|
|
19
|
+
//
|
|
20
|
+
// @evidence contracts/testing.md#behavioral-verification Calls the shared selector on real Windows directories and junctions, then performs temporary creation and removal at its selected base; unsupported layouts return no preferred base and the project directory's timestamp stays unchanged.
|
|
21
|
+
// @evidence contracts/testing.md#independent-expectations The authored sibling, missing child, linked child, same-drive junction and ancestor layouts define valid and invalid paths before the selector runs; the project stat and empty identities listing independently detect root churn and scratch residue.
|
|
22
|
+
// @evidence contracts/testing.md#distinguishing-cases Same-drive cache/config junctions and ancestor TEMP contrast with a cross-drive config junction, independent same-volume TEMP, missing and linked identities, and a config inside identities; callers separately test legacy fallback creation.
|
|
23
|
+
// @evidence contracts/testing.md#execution-ownership This named Go unit calls the shared filesystem selector directly; cmd only creates junction fixtures, and no compiler, config evaluator, or native plugin runs.
|
|
24
|
+
func TestIdentityTempBaseUsesOnlyAnExistingOrdinaryChild(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
project := filepath.Join(root, "project")
|
|
27
|
+
recordRoot := filepath.Join(root, "cache")
|
|
28
|
+
identity := filepath.Join(recordRoot, "identities")
|
|
29
|
+
for _, dir := range []string{project, identity} {
|
|
30
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
config := filepath.Join(project, "banner.config.ts")
|
|
35
|
+
otherTemp := `Z:\ttsc-test-temp`
|
|
36
|
+
if strings.EqualFold(filepath.VolumeName(root), "Z:") {
|
|
37
|
+
otherTemp = `Y:\ttsc-test-temp`
|
|
38
|
+
}
|
|
39
|
+
physicalIdentity, err := filepath.EvalSymlinks(identity)
|
|
40
|
+
if err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
if got := IdentityTempBase(config, otherTemp, recordRoot); got != physicalIdentity {
|
|
44
|
+
t.Fatalf("identity base mismatch: %q != %q", got, physicalIdentity)
|
|
45
|
+
}
|
|
46
|
+
separateTemp := filepath.Join(root, "separate-temp")
|
|
47
|
+
if err := os.Mkdir(separateTemp, 0o755); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
if got := IdentityTempBase(config, separateTemp, recordRoot); got != "" {
|
|
51
|
+
t.Fatalf("independent same-volume TEMP selected identities: %q", got)
|
|
52
|
+
}
|
|
53
|
+
for _, ancestorTemp := range []string{project, root} {
|
|
54
|
+
if got := IdentityTempBase(config, ancestorTemp, recordRoot); got != physicalIdentity {
|
|
55
|
+
t.Fatalf("ancestor TEMP did not select identities: %q", got)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if got := IdentityTempBase(config, otherTemp, filepath.Join(root, "missing")); got != "" {
|
|
59
|
+
t.Fatalf("missing store selected identities: %q", got)
|
|
60
|
+
}
|
|
61
|
+
before, err := os.Stat(project)
|
|
62
|
+
if err != nil {
|
|
63
|
+
t.Fatal(err)
|
|
64
|
+
}
|
|
65
|
+
scratch, err := os.MkdirTemp(physicalIdentity, "ttsc-config-loader-")
|
|
66
|
+
if err != nil {
|
|
67
|
+
t.Fatal(err)
|
|
68
|
+
}
|
|
69
|
+
t.Cleanup(func() {
|
|
70
|
+
if err := os.RemoveAll(scratch); err != nil {
|
|
71
|
+
t.Errorf("remove identity scratch: %v", err)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
if err := os.RemoveAll(scratch); err != nil {
|
|
75
|
+
t.Fatal(err)
|
|
76
|
+
}
|
|
77
|
+
after, err := os.Stat(project)
|
|
78
|
+
if err != nil {
|
|
79
|
+
t.Fatal(err)
|
|
80
|
+
}
|
|
81
|
+
if !before.ModTime().Equal(after.ModTime()) {
|
|
82
|
+
t.Fatalf("project directory timestamp changed: %v -> %v", before.ModTime(), after.ModTime())
|
|
83
|
+
}
|
|
84
|
+
entries, err := os.ReadDir(identity)
|
|
85
|
+
if err != nil || len(entries) != 0 {
|
|
86
|
+
t.Fatalf("identity scratch remains: %v, %v", entries, err)
|
|
87
|
+
}
|
|
88
|
+
linkedRoot := filepath.Join(root, "linked-cache")
|
|
89
|
+
if out, err := exec.Command("cmd", "/c", "mklink", "/J", linkedRoot, recordRoot).CombinedOutput(); err != nil {
|
|
90
|
+
t.Fatalf("cache-root junction failed: %v: %s", err, out)
|
|
91
|
+
}
|
|
92
|
+
if got := IdentityTempBase(config, otherTemp, linkedRoot); got != physicalIdentity {
|
|
93
|
+
t.Fatalf("same-drive linked root did not select canonical identities: %q", got)
|
|
94
|
+
}
|
|
95
|
+
linkedProject := filepath.Join(root, "linked-project")
|
|
96
|
+
if out, err := exec.Command("cmd", "/c", "mklink", "/J", linkedProject, project).CombinedOutput(); err != nil {
|
|
97
|
+
t.Fatalf("project junction failed: %v: %s", err, out)
|
|
98
|
+
}
|
|
99
|
+
if got := IdentityTempBase(filepath.Join(linkedProject, "banner.config.ts"), otherTemp, recordRoot); got != physicalIdentity {
|
|
100
|
+
t.Fatalf("same-drive linked config did not select canonical identities: %q", got)
|
|
101
|
+
}
|
|
102
|
+
linkedTemp := filepath.Join(root, "linked-temp")
|
|
103
|
+
if out, err := exec.Command("cmd", "/c", "mklink", "/J", linkedTemp, project).CombinedOutput(); err != nil {
|
|
104
|
+
t.Fatalf("TEMP junction failed: %v: %s", err, out)
|
|
105
|
+
}
|
|
106
|
+
if got := IdentityTempBase(config, linkedTemp, recordRoot); got != physicalIdentity {
|
|
107
|
+
t.Fatalf("physical ancestor TEMP did not select identities: %q", got)
|
|
108
|
+
}
|
|
109
|
+
linkedIdentityRoot := filepath.Join(root, "linked-identity-cache")
|
|
110
|
+
if err := os.Mkdir(linkedIdentityRoot, 0o755); err != nil {
|
|
111
|
+
t.Fatal(err)
|
|
112
|
+
}
|
|
113
|
+
if out, err := exec.Command("cmd", "/c", "mklink", "/J", filepath.Join(linkedIdentityRoot, "identities"), identity).CombinedOutput(); err != nil {
|
|
114
|
+
t.Fatalf("identity junction failed: %v: %s", err, out)
|
|
115
|
+
}
|
|
116
|
+
if got := IdentityTempBase(config, otherTemp, linkedIdentityRoot); got != "" {
|
|
117
|
+
t.Fatalf("linked identities selected: %q", got)
|
|
118
|
+
}
|
|
119
|
+
nested := filepath.Join(identity, "nested")
|
|
120
|
+
if err := os.Mkdir(nested, 0o755); err != nil {
|
|
121
|
+
t.Fatal(err)
|
|
122
|
+
}
|
|
123
|
+
if got := IdentityTempBase(filepath.Join(nested, "banner.config.ts"), otherTemp, recordRoot); got != "" {
|
|
124
|
+
t.Fatalf("config inside identities selected ancestor: %q", got)
|
|
125
|
+
}
|
|
126
|
+
checkout, err := os.Getwd()
|
|
127
|
+
if err != nil {
|
|
128
|
+
t.Fatal(err)
|
|
129
|
+
}
|
|
130
|
+
if !strings.EqualFold(filepath.VolumeName(checkout), filepath.VolumeName(root)) {
|
|
131
|
+
packageRoot := filepath.Clean(filepath.Join(checkout, "..", ".."))
|
|
132
|
+
otherPhysical, err := os.MkdirTemp(packageRoot, "ttsc-banner-junction-test-")
|
|
133
|
+
if err != nil {
|
|
134
|
+
t.Fatal(err)
|
|
135
|
+
}
|
|
136
|
+
t.Cleanup(func() {
|
|
137
|
+
if err := os.RemoveAll(otherPhysical); err != nil {
|
|
138
|
+
t.Errorf("remove cross-volume fixture: %v", err)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
if err := os.Mkdir(filepath.Join(otherPhysical, "identities"), 0o755); err != nil {
|
|
142
|
+
t.Fatal(err)
|
|
143
|
+
}
|
|
144
|
+
configAlias := filepath.Join(root, "linked-other-volume-config")
|
|
145
|
+
if out, err := exec.Command("cmd", "/c", "mklink", "/J", configAlias, otherPhysical).CombinedOutput(); err != nil {
|
|
146
|
+
t.Fatalf("cross-volume config junction failed: %v: %s", err, out)
|
|
147
|
+
}
|
|
148
|
+
linkedConfig := filepath.Join(configAlias, "banner.config.ts")
|
|
149
|
+
if got := IdentityTempBase(linkedConfig, otherTemp, recordRoot); got != physicalIdentity {
|
|
150
|
+
t.Fatalf("lexical-volume identities not selected for linked config: %q", got)
|
|
151
|
+
}
|
|
152
|
+
if got := IdentityTempBase(linkedConfig, otherTemp, otherPhysical); got != "" {
|
|
153
|
+
t.Fatalf("physical-volume identities selected for linked config: %q", got)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -14,15 +14,17 @@ import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
// NewEmitProvenanceRecorder wraps a writer and returns a build-owned snapshot
|
|
17
|
-
// function for successfully
|
|
17
|
+
// function for successfully materialized JavaScript outputs. The compiler's actual
|
|
18
18
|
// eligible source list and output resolver determine candidates; neither sibling
|
|
19
19
|
// filenames nor source maps establish ownership.
|
|
20
20
|
//
|
|
21
21
|
// Snapshot keys are absolute native output paths. Values are sorted, distinct
|
|
22
22
|
// physical source paths recorded by this compiler generation's input observer.
|
|
23
|
-
// An empty value means an output was
|
|
23
|
+
// An empty value means an output was emitted but its eligible owners or some
|
|
24
24
|
// owner's physical proof were unavailable or inconsistent. Multiple values
|
|
25
|
-
// preserve ambiguity. An empty object means no JavaScript
|
|
25
|
+
// preserve ambiguity. An empty object means no JavaScript output succeeded.
|
|
26
|
+
// A writer may leave an existing byte-identical output in place and return
|
|
27
|
+
// success; that output remains part of this generation's emitted result.
|
|
26
28
|
//
|
|
27
29
|
// Call the snapshot after emission finishes. It remains attached to its captured
|
|
28
30
|
// generation's ledger and never resolves an alias against post-build disk state.
|
|
@@ -32,12 +34,12 @@ import (
|
|
|
32
34
|
// the supplied writer must not reenter either returned closure. Writer errors
|
|
33
35
|
// prevent recording that callback even if the writer already had partial effects.
|
|
34
36
|
//
|
|
35
|
-
// @evidence contracts/common.md#principled-implementation Native eligible sources and resolved script destinations intersect only successful unskipped
|
|
37
|
+
// @evidence contracts/common.md#principled-implementation Native eligible sources and resolved script destinations intersect only successful unskipped emit callbacks, including byte-identical existing outputs; all candidate generation-time physical proofs are required, with unknown and multiple owners represented explicitly.
|
|
36
38
|
// @evidence contracts/common.md#clear-and-simple-design One compiler-owned candidate index, one serialized writer and one snapshot separate eligibility, successful publication and physical provenance without a same-stem fallback.
|
|
37
39
|
// @evidence contracts/common.md#prohibited-implementation-shortcuts The recorder delegates supported emit-path helpers and actual writers; later realpath, fixture filenames or extension precedence cannot fabricate a unique source owner.
|
|
38
40
|
// @evidence contracts/common.md#meaningful-documentation Native paragraphs explain snapshot timing, native keys, physical proof, ambiguity, unknown values and the non-atomic ledger limitation following the documentation skill.
|
|
39
41
|
// @evidence contracts/portability.md#os-neutral-implementation Compiler path normalization and its coordinate case policy index candidate collisions; recorded native physical paths prove source identity separately from that policy, preserving lexical output names without an OS-derived identity guess.
|
|
40
|
-
// @evidence contracts/performance.md#efficient-algorithms Latched linked hooks and native eligibility/output-path helpers precede the source index. Candidate/path normalization and text keys contribute work; each callback delegates arbitrary writer work, then records a successful script row. Snapshot visits each
|
|
42
|
+
// @evidence contracts/performance.md#efficient-algorithms Latched linked hooks and native eligibility/output-path helpers precede the source index. Candidate/path normalization and text keys contribute work; each callback delegates arbitrary writer work, then records a successful script row even when the writer reuses matching bytes. Snapshot visits each row's candidates, compares captured proofs, deduplicates physical paths and sorts each owner list with path-text comparison costs; shared candidate lists can be revisited across output spellings.
|
|
41
43
|
// @evidence contracts/performance.md#reuse-equivalent-work One generation's native eligibility and output paths serve every writer callback; the snapshot reuses captured input observations without re-reading source bytes or recomputing an independent compiler Program.
|
|
42
44
|
// @evidence contracts/performance.md#bound-retention-and-release-resources Returned closures retain the captured input ledger and uncapped candidate/write path lists until their owner releases them. Snapshots allocate independent maps/slices; delegated writer resource release belongs to that writer. Shared locking serializes writer and snapshot with no deadline or reentrant acquisition; no independent descriptor or lease is acquired here.
|
|
43
45
|
func (p *Program) NewEmitProvenanceRecorder(writeFile shimcompiler.WriteFile) (shimcompiler.WriteFile, func() map[string][]string, error) {
|
|
@@ -775,10 +775,10 @@ function visitImportTargetCandidates(specifier, parent, extensions, visit) {
|
|
|
775
775
|
* the input set without a hash, even when resolution observation is
|
|
776
776
|
* complete.
|
|
777
777
|
*
|
|
778
|
-
* @evidence contracts/common.md#principled-implementation Before/after metadata plus repeated content and physical observations withdraw contradictory per-input proofs; the separate complete flag reports known observation capability gaps.
|
|
778
|
+
* @evidence contracts/common.md#principled-implementation Before/after metadata plus repeated content and physical observations withdraw contradictory per-input proofs; a selected file must have positive content and physical proof, while the separate complete flag reports known observation capability gaps.
|
|
779
779
|
* @evidence contracts/common.md#clear-and-simple-design One evaluation owns its input ledger, resolution tokens and final envelope; callers explicitly invalidate capability rather than manipulating proof maps.
|
|
780
780
|
* @evidence contracts/common.md#prohibited-implementation-shortcuts Unknown observations retain missing proofs and incomplete capability remains false; no expected digest or resolver monkeypatch makes reuse appear valid.
|
|
781
|
-
* @evidence contracts/common.md#meaningful-documentation JSDoc describes hook ownership, entry recording, final reread and capability versus stability; returned operations have native descriptions and tags are separated.
|
|
781
|
+
* @evidence contracts/common.md#meaningful-documentation JSDoc describes hook ownership, entry recording, selected-file proof, final reread and capability versus stability; returned operations have native descriptions and tags are separated.
|
|
782
782
|
* @evidence contracts/portability.md#os-neutral-implementation Native stat/realpath and URL conversion represent paths and identities; symlink ancestors are observed without equating an OS name to filesystem case policy.
|
|
783
783
|
* @evidence contracts/performance.md#efficient-algorithms Sets/maps index paths, but candidate expansion and repeated pre/post/final observations still pay native stat/realpath/ancestor-link work and full file hashing; finish visits its captured input list and sorts output paths, with uncapped byte, path and manifest-expansion costs.
|
|
784
784
|
* @evidence contracts/performance.md#reuse-equivalent-work Distinct candidates share the evaluation ledger and expanded bases; the first witness stays authoritative and contradictory later reads revoke its proof instead of refreshing it.
|
|
@@ -931,8 +931,8 @@ function createResolutionInputRecorder(options) {
|
|
|
931
931
|
},
|
|
932
932
|
/**
|
|
933
933
|
* Settle a resolution: commit the fingerprints of the candidates it could
|
|
934
|
-
* have read, record them again as they are now, and
|
|
935
|
-
* selected.
|
|
934
|
+
* have read, record them again as they are now, and require positive proof
|
|
935
|
+
* for the file selected by a successful resolution.
|
|
936
936
|
*
|
|
937
937
|
* @param {{
|
|
938
938
|
* importPending: object[];
|
|
@@ -1008,6 +1008,18 @@ function createResolutionInputRecorder(options) {
|
|
|
1008
1008
|
recordedBases,
|
|
1009
1009
|
);
|
|
1010
1010
|
recordFile(resolved);
|
|
1011
|
+
const selected = asFile(resolved);
|
|
1012
|
+
if (
|
|
1013
|
+
selected !== undefined &&
|
|
1014
|
+
(typeof hashes.get(selected) !== "string" ||
|
|
1015
|
+
typeof realpaths.get(selected) !== "string")
|
|
1016
|
+
) {
|
|
1017
|
+
inputs.add(selected);
|
|
1018
|
+
hashes.delete(selected);
|
|
1019
|
+
realpaths.delete(selected);
|
|
1020
|
+
signatures.delete(selected);
|
|
1021
|
+
unstable.add(selected);
|
|
1022
|
+
}
|
|
1011
1023
|
},
|
|
1012
1024
|
recordFile,
|
|
1013
1025
|
|