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
in deterministic dependency order.
- fingerprints the normalized artifact key, prompt and profile identifiers,
normalized Scriptorium executable and config logical identities, effective
render-debug behavior, session-relative output identity, sorted dependency
keys, ordered input declarations and semantic identities, validated current
dependency-output identities, and sorted effective Scriptorium variables
(including Narratio's sticky session variable).
effective render-debug behavior, session-relative output identity, sorted
dependency keys, ordered input declarations and semantic identities,
validated current dependency-output identities, and sorted effective
Scriptorium variables (including Narratio's sticky session variable).
- provides read-only reconciliation that classifies each configured record as
current, stale, missing, failed, legacy, or otherwise non-resumable, and
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,
materialize outputs, update status, or create run records.
- 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
visible only inside Scriptorium—such as a file privately loaded by its config
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.
type analyzeFingerprintPayload struct {
Version int `json:"version"`
Scriptorium analyzeFingerprintScriptorium `json:"scriptorium"`
Artifact analyzeFingerprintArtifact `json:"artifact"`
Inputs []analyzeFingerprintInput `json:"inputs"`
DependencyOutputs []analyzeFingerprintDependencyOutput `json:"dependency_outputs"`
Variables []analyzeFingerprintVariable `json:"variables"`
}
type analyzeFingerprintScriptorium struct {
ExecutableIdentity string `json:"executable_identity"`
ConfigIdentity string `json:"config_identity"`
}
type analyzeFingerprintArtifact struct {
Key string `json:"key"`
PromptID string `json:"prompt_id"`
@@ -169,10 +163,6 @@ func computeAnalyzeArtifactFingerprint(
}
payload := analyzeFingerprintPayload{
Version: manifest.AnalyzeFingerprintContractVersion,
Scriptorium: analyzeFingerprintScriptorium{
ExecutableIdentity: normalizedAnalyzeExternalPathIdentity(scriptoriumCfg.Binary),
ConfigIdentity: normalizedAnalyzeExternalPathIdentity(scriptoriumCfg.ConfigPath),
},
Artifact: analyzeFingerprintArtifact{
Key: key, PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID,
EffectiveRenderDebug: resolveRenderDebugEnabled(scriptoriumCfg.RenderDebug, artifactCfg.RenderDebug),
@@ -260,18 +250,6 @@ func normalizedAnalyzeDependencyKeys(values []string) []string {
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) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {

View File

@@ -18,8 +18,6 @@ func TestAnalyzeFingerprintCanonicalPayloadSensitivity(t *testing.T) {
mutate func(*analyzeFingerprintPayload)
}{
{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: "prompt", mutate: func(p *analyzeFingerprintPayload) { p.Artifact.PromptID = "prompt.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: "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: "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 {
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: "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: "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: "optional input appears", options: fingerprintFixtureOptions{producerRunID: "run-a", dependencyBody: "notes\n", includePlayers: true}},
}
@@ -170,9 +167,6 @@ func analyzeFingerprintPayloadFixture() analyzeFingerprintPayload {
}
return analyzeFingerprintPayload{
Version: manifest.AnalyzeFingerprintContractVersion,
Scriptorium: analyzeFingerprintScriptorium{
ExecutableIdentity: "scriptorium", ConfigIdentity: "config.yml",
},
Artifact: analyzeFingerprintArtifact{
Key: "session_recap", PromptID: "dnd.session_recap", ProfileID: "local",
EffectiveRenderDebug: true, OutputIdentity: "artifacts/session_recap.md",