mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
ca158c22d8
SPEC-6 U8a (DOC-3371 §1, §2 steps 1-5, §9). Strict apps/1 parsing; an https/no-redirect fetch policy screened at dial time with an admin private-origin list; pending-install reservations under the owner lock and the instance advisory lock, holding the full charge until the fetch ends; the owner preview with every artifact normalized by the importer's own code (normalizeArtifact, prepareCreateFields, both extracted behaviour-preserving and pinned by a parity golden); apps_enabled and the admin apps settings; a background sweep of expired pending installs; migration 118/092. The preview is advisory: it may refuse what the import would accept, never the reverse; U8b re-checks atomically. Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
73 lines
2.8 KiB
Go
73 lines
2.8 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/artifact"
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
// normalizedArtifact is what the importer stores for an artifact: the item's
|
|
// title, content and fields after the forgiving preprocess, and the warnings
|
|
// that preprocess produced.
|
|
type normalizedArtifact struct {
|
|
Title string
|
|
Content string
|
|
// FieldsJSON is the stored fields blob.
|
|
FieldsJSON string
|
|
// Fields is FieldsJSON decoded back into canonical JSON types ([]any,
|
|
// map[string]any), the shape the create path validates.
|
|
Fields map[string]any
|
|
Warnings []string
|
|
}
|
|
|
|
// normalizeArtifact is the artifact importer's preprocess, shared by the human
|
|
// import (handleImportArtifact) and the app installer's preview and
|
|
// provisioning (SPEC-6 U8, DOC-3371 §2 step 3), so the item an owner reviews
|
|
// is the item that is stored. It never mutates art.
|
|
//
|
|
// Its output depends on the destination: select values the schema does not
|
|
// offer are cleared, and a playbook's invocation_slug is de-collided against
|
|
// slugTaken. That is why provisioning re-runs it on its own transaction and
|
|
// compares the digest with the one reviewed.
|
|
//
|
|
// The caller validates the title (models.ValidateItemTitle) first; a title
|
|
// that fails there must not reach this function.
|
|
func normalizeArtifact(art artifact.Artifact, coll *models.Collection, schema models.CollectionSchema, slugTaken func(string) (bool, error)) (*normalizedArtifact, error) {
|
|
// Copy the artifact fields so the preprocess never mutates the decoded
|
|
// value (keeps the parse layer's output immutable from the caller's POV).
|
|
fields := make(map[string]any, len(art.Fields))
|
|
for k, v := range art.Fields {
|
|
fields[k] = v
|
|
}
|
|
warnings := preprocessArtifactFields(art.Kind, schema, fields, slugTaken)
|
|
|
|
fieldsJSON, err := json.Marshal(fields)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal fields: %w", err)
|
|
}
|
|
// Normalize the field map through JSON so its nested types match what the
|
|
// normal create path validates. handleCreateItem builds its fieldMap by
|
|
// unmarshalling the JSON request body, so structured values are canonical
|
|
// JSON types ([]any, map[string]any). The artifact decode produces Go-native
|
|
// types (e.g. arguments as []map[string]any), which ValidateFields' json case
|
|
// rejects — round-tripping fixes that without special-casing any field.
|
|
normalized := make(map[string]any)
|
|
if err := json.Unmarshal(fieldsJSON, &normalized); err != nil {
|
|
return nil, fmt.Errorf("normalize fields: %w", err)
|
|
}
|
|
|
|
body := art.Body
|
|
if footer := artifactProvenanceFooter(art.Provenance); footer != "" {
|
|
body = body + footer
|
|
}
|
|
return &normalizedArtifact{
|
|
Title: models.NormalizeItemTitle(art.Title),
|
|
Content: body,
|
|
FieldsJSON: string(fieldsJSON),
|
|
Fields: normalized,
|
|
Warnings: warnings,
|
|
}, nil
|
|
}
|