Exclude external paths from analysis fingerprints

This commit is contained in:
2026-08-29 20:45:27 +00:00
parent 8657a28bdb
commit 0dc8ff9b52
3 changed files with 7 additions and 36 deletions

View File

@@ -67,11 +67,10 @@ Supported source families:
JSON payload and no map serialization. Configured artifacts are fingerprinted JSON payload and no map serialization. Configured artifacts are fingerprinted
in deterministic dependency order. in deterministic dependency order.
- fingerprints the normalized artifact key, prompt and profile identifiers, - fingerprints the normalized artifact key, prompt and profile identifiers,
normalized Scriptorium executable and config logical identities, effective effective render-debug behavior, session-relative output identity, sorted
render-debug behavior, session-relative output identity, sorted dependency dependency keys, ordered input declarations and semantic identities,
keys, ordered input declarations and semantic identities, validated current validated current dependency-output identities, and sorted effective
dependency-output identities, and sorted effective Scriptorium variables Scriptorium variables (including Narratio's sticky session variable).
(including Narratio's sticky session variable).
- provides read-only reconciliation that classifies each configured record as - provides read-only reconciliation that classifies each configured record as
current, stale, missing, failed, legacy, or otherwise non-resumable, and current, stale, missing, failed, legacy, or otherwise non-resumable, and
separately identifies manifest records removed from current configuration. separately identifies manifest records removed from current configuration.
@@ -155,7 +154,7 @@ Supported source families:
- input-identity resolution is read-only: it does not invoke adapters, - input-identity resolution is read-only: it does not invoke adapters,
materialize outputs, update status, or create run records. materialize outputs, update status, or create run records.
- fingerprints exclude timeouts, retries, timestamps, producer and Narratio run - fingerprints exclude timeouts, retries, timestamps, producer and Narratio run
IDs, absolute executable/config/workspace roots, diagnostic locations, and IDs, executable and config paths, workspace roots, diagnostic locations, and
executable or private transitive configuration contents. A change that is executable or private transitive configuration contents. A change that is
visible only inside Scriptorium—such as a file privately loaded by its config visible only inside Scriptorium—such as a file privately loaded by its config
path—requires an explicit forced regeneration. path—requires an explicit forced regeneration.

View File

@@ -49,18 +49,12 @@ type analyzeDependencyOutputIdentity struct {
// as SHA-256 input. It contains slices and fixed-field structs, never maps. // as SHA-256 input. It contains slices and fixed-field structs, never maps.
type analyzeFingerprintPayload struct { type analyzeFingerprintPayload struct {
Version int `json:"version"` Version int `json:"version"`
Scriptorium analyzeFingerprintScriptorium `json:"scriptorium"`
Artifact analyzeFingerprintArtifact `json:"artifact"` Artifact analyzeFingerprintArtifact `json:"artifact"`
Inputs []analyzeFingerprintInput `json:"inputs"` Inputs []analyzeFingerprintInput `json:"inputs"`
DependencyOutputs []analyzeFingerprintDependencyOutput `json:"dependency_outputs"` DependencyOutputs []analyzeFingerprintDependencyOutput `json:"dependency_outputs"`
Variables []analyzeFingerprintVariable `json:"variables"` Variables []analyzeFingerprintVariable `json:"variables"`
} }
type analyzeFingerprintScriptorium struct {
ExecutableIdentity string `json:"executable_identity"`
ConfigIdentity string `json:"config_identity"`
}
type analyzeFingerprintArtifact struct { type analyzeFingerprintArtifact struct {
Key string `json:"key"` Key string `json:"key"`
PromptID string `json:"prompt_id"` PromptID string `json:"prompt_id"`
@@ -169,10 +163,6 @@ func computeAnalyzeArtifactFingerprint(
} }
payload := analyzeFingerprintPayload{ payload := analyzeFingerprintPayload{
Version: manifest.AnalyzeFingerprintContractVersion, Version: manifest.AnalyzeFingerprintContractVersion,
Scriptorium: analyzeFingerprintScriptorium{
ExecutableIdentity: normalizedAnalyzeExternalPathIdentity(scriptoriumCfg.Binary),
ConfigIdentity: normalizedAnalyzeExternalPathIdentity(scriptoriumCfg.ConfigPath),
},
Artifact: analyzeFingerprintArtifact{ Artifact: analyzeFingerprintArtifact{
Key: key, PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID, Key: key, PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID,
EffectiveRenderDebug: resolveRenderDebugEnabled(scriptoriumCfg.RenderDebug, artifactCfg.RenderDebug), EffectiveRenderDebug: resolveRenderDebugEnabled(scriptoriumCfg.RenderDebug, artifactCfg.RenderDebug),
@@ -260,18 +250,6 @@ func normalizedAnalyzeDependencyKeys(values []string) []string {
return result return result
} }
func normalizedAnalyzeExternalPathIdentity(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
cleaned := filepath.Clean(trimmed)
if filepath.IsAbs(cleaned) {
return filepath.Base(cleaned)
}
return filepath.ToSlash(cleaned)
}
func normalizedAnalyzeOutputIdentity(value string) (string, error) { func normalizedAnalyzeOutputIdentity(value string) (string, error) {
trimmed := strings.TrimSpace(value) trimmed := strings.TrimSpace(value)
if trimmed == "" { if trimmed == "" {

View File

@@ -18,8 +18,6 @@ func TestAnalyzeFingerprintCanonicalPayloadSensitivity(t *testing.T) {
mutate func(*analyzeFingerprintPayload) mutate func(*analyzeFingerprintPayload)
}{ }{
{name: "version", mutate: func(p *analyzeFingerprintPayload) { p.Version++ }}, {name: "version", mutate: func(p *analyzeFingerprintPayload) { p.Version++ }},
{name: "scriptorium executable", mutate: func(p *analyzeFingerprintPayload) { p.Scriptorium.ExecutableIdentity = "other" }},
{name: "scriptorium config", mutate: func(p *analyzeFingerprintPayload) { p.Scriptorium.ConfigIdentity = "other.yml" }},
{name: "artifact key", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.Key = "other" }}, {name: "artifact key", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.Key = "other" }},
{name: "prompt", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.PromptID = "prompt.changed" }}, {name: "prompt", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.PromptID = "prompt.changed" }},
{name: "profile", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.ProfileID = "profile.changed" }}, {name: "profile", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.ProfileID = "profile.changed" }},
@@ -87,7 +85,8 @@ func TestComputeAnalyzeFingerprintsIsDeterministicAcrossIncidentalDifferences(t
{name: "producer run", options: fingerprintFixtureOptions{producerRunID: "run-b", binary: "/opt/a/scriptorium", configPath: "/etc/a/scriptorium.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(100, 0).UTC()}}, {name: "producer run", options: fingerprintFixtureOptions{producerRunID: "run-b", binary: "/opt/a/scriptorium", configPath: "/etc/a/scriptorium.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(100, 0).UTC()}},
{name: "timestamp", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "/opt/a/scriptorium", configPath: "/etc/a/scriptorium.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(999, 0).UTC()}}, {name: "timestamp", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "/opt/a/scriptorium", configPath: "/etc/a/scriptorium.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(999, 0).UTC()}},
{name: "timeouts", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "/opt/a/scriptorium", configPath: "/etc/a/scriptorium.yml", topTimeout: "8m", artifactTimeout: "9m", updatedAt: time.Unix(100, 0).UTC()}}, {name: "timeouts", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "/opt/a/scriptorium", configPath: "/etc/a/scriptorium.yml", topTimeout: "8m", artifactTimeout: "9m", updatedAt: time.Unix(100, 0).UTC()}},
{name: "absolute executable and config paths", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "/srv/b/scriptorium", configPath: "/srv/b/scriptorium.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(100, 0).UTC()}}, {name: "absolute executable and config paths", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "/srv/b/scriptorium-v2", configPath: "/srv/b/alternate.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(100, 0).UTC()}},
{name: "relative executable and config paths", options: fingerprintFixtureOptions{producerRunID: "run-a", binary: "bin/scriptorium-v2", configPath: "config/alternate.yml", topTimeout: "1m", artifactTimeout: "2m", updatedAt: time.Unix(100, 0).UTC()}},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -109,8 +108,6 @@ func TestComputeAnalyzeFingerprintsTracksEffectiveConfiguration(t *testing.T) {
{name: "render debug", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", renderDebug: true}}, {name: "render debug", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", renderDebug: true}},
{name: "output", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", outputPath: "artifacts/changed.md"}}, {name: "output", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", outputPath: "artifacts/changed.md"}},
{name: "effective variable", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", label: "changed"}}, {name: "effective variable", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", label: "changed"}},
{name: "executable identity", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", binary: "/opt/bin/other"}},
{name: "config identity", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", configPath: "/etc/scriptorium/other.yml"}},
{name: "dependency bytes", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "other\n"}}, {name: "dependency bytes", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "other\n"}},
{name: "optional input appears", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", includePlayers: true}}, {name: "optional input appears", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", includePlayers: true}},
} }
@@ -170,9 +167,6 @@ func analyzeFingerprintPayloadFixture() analyzeFingerprintPayload {
} }
return analyzeFingerprintPayload{ return analyzeFingerprintPayload{
Version: manifest.AnalyzeFingerprintContractVersion, Version: manifest.AnalyzeFingerprintContractVersion,
Scriptorium: analyzeFingerprintScriptorium{
ExecutableIdentity: "scriptorium", ConfigIdentity: "config.yml",
},
Artifact: analyzeFingerprintArtifact{ Artifact: analyzeFingerprintArtifact{
Key: "session_recap", PromptID: "dnd.session_recap", ProfileID: "local", Key: "session_recap", PromptID: "dnd.session_recap", ProfileID: "local",
EffectiveRenderDebug: true, OutputIdentity: "artifacts/session_recap.md", EffectiveRenderDebug: true, OutputIdentity: "artifacts/session_recap.md",