generated from coulomb/repo-seed
Import Markitect resource manifests
This commit is contained in:
13
examples/markitect/ambiguous_resource_manifest.yaml
Normal file
13
examples/markitect/ambiguous_resource_manifest.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
id: markitect-ambiguous-example
|
||||||
|
system: markitect-tool
|
||||||
|
caring_profile: caring-0.4.0-rc2
|
||||||
|
resources:
|
||||||
|
- id: document:ambiguous-note
|
||||||
|
type: document
|
||||||
|
parent: knowledge-base:markitect-example
|
||||||
|
path: examples/policy/ambiguous-note.md
|
||||||
|
actions:
|
||||||
|
- read
|
||||||
|
metadata:
|
||||||
|
source: examples/markitect/ambiguous_resource_manifest.yaml
|
||||||
|
flex_auth_contract: resource-registration-v0
|
||||||
207
internal/markitect/importer.go
Normal file
207
internal/markitect/importer.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package markitect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/netkingdom/flex-auth/internal/registry"
|
||||||
|
"github.com/netkingdom/flex-auth/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SystemID is the protected-system id emitted by Markitect manifests.
|
||||||
|
SystemID = "markitect-tool"
|
||||||
|
|
||||||
|
namespaceVersion = "markitect-resource-namespace-v1"
|
||||||
|
namespaceDoc = "docs/markitect-resource-namespace.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Diagnostic describes a Markitect manifest import finding.
|
||||||
|
type Diagnostic struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Resource string `json:"resource,omitempty"`
|
||||||
|
Fields []string `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportResult captures the normalized manifest and diagnostics.
|
||||||
|
type ImportResult struct {
|
||||||
|
Manifest api.ResourceManifest `json:"manifest"`
|
||||||
|
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type classification struct {
|
||||||
|
scopeLevel api.ScopeLevel
|
||||||
|
planes []api.Plane
|
||||||
|
parentTypes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var classifications = map[string]classification{
|
||||||
|
"knowledge_base": {
|
||||||
|
scopeLevel: api.ScopeLevelWorkspace,
|
||||||
|
planes: []api.Plane{api.PlaneIntent, api.PlaneData},
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
scopeLevel: api.ScopeLevelProject,
|
||||||
|
planes: []api.Plane{api.PlaneBuild, api.PlaneData},
|
||||||
|
parentTypes: []string{"knowledge_base"},
|
||||||
|
},
|
||||||
|
"document": {
|
||||||
|
scopeLevel: api.ScopeLevelResource,
|
||||||
|
planes: []api.Plane{api.PlaneData},
|
||||||
|
parentTypes: []string{"repository", "knowledge_base"},
|
||||||
|
},
|
||||||
|
"section": {
|
||||||
|
scopeLevel: api.ScopeLevelSubresource,
|
||||||
|
planes: []api.Plane{api.PlaneData},
|
||||||
|
parentTypes: []string{"document"},
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
scopeLevel: api.ScopeLevelField,
|
||||||
|
planes: []api.Plane{api.PlaneData},
|
||||||
|
parentTypes: []string{"section", "document"},
|
||||||
|
},
|
||||||
|
"context_package": {
|
||||||
|
scopeLevel: api.ScopeLevelDataset,
|
||||||
|
planes: []api.Plane{api.PlaneIntent, api.PlaneData, api.PlanePolicy},
|
||||||
|
parentTypes: []string{"knowledge_base", "repository", "document"},
|
||||||
|
},
|
||||||
|
"workflow_artifact": {
|
||||||
|
scopeLevel: api.ScopeLevelProcess,
|
||||||
|
planes: []api.Plane{api.PlaneExecution, api.PlaneData, api.PlaneAudit},
|
||||||
|
parentTypes: []string{"context_package", "document"},
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
scopeLevel: api.ScopeLevelRecord,
|
||||||
|
planes: []api.Plane{api.PlaneData, api.PlaneAudit},
|
||||||
|
parentTypes: []string{"workflow_artifact", "context_package", "document"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportResourceManifest validates, enriches, and imports a Markitect manifest.
|
||||||
|
func ImportResourceManifest(store *registry.Store, manifest api.ResourceManifest) (ImportResult, error) {
|
||||||
|
if store == nil {
|
||||||
|
return ImportResult{}, fmt.Errorf("registry store is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ImportResult{
|
||||||
|
Manifest: EnrichResourceManifest(manifest),
|
||||||
|
Diagnostics: ValidateResourceManifest(manifest),
|
||||||
|
}
|
||||||
|
if hasError(result.Diagnostics) {
|
||||||
|
return result, fmt.Errorf("markitect resource manifest has import errors")
|
||||||
|
}
|
||||||
|
if err := store.ImportResourceManifest(result.Manifest); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateResourceManifest returns Markitect-specific import diagnostics.
|
||||||
|
func ValidateResourceManifest(manifest api.ResourceManifest) []Diagnostic {
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
if manifest.ID == "" {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-MANIFEST-ID", "", "manifest id is required", "id"))
|
||||||
|
}
|
||||||
|
if manifest.System != SystemID {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-SYSTEM", "", fmt.Sprintf("manifest system must be %q", SystemID), "system"))
|
||||||
|
}
|
||||||
|
if manifest.Metadata["flex_auth_contract"] != api.FlexAuthContractV0 {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-CONTRACT", "", "metadata.flex_auth_contract must declare resource-registration-v0", "metadata.flex_auth_contract"))
|
||||||
|
}
|
||||||
|
if manifest.CaringProfile == "" {
|
||||||
|
diagnostics = append(diagnostics, warningDiagnostic("MARKITECT-CARING-PROFILE", "", "missing caring_profile; importer will default to caring-0.4.0-rc2", "caring_profile"))
|
||||||
|
} else if manifest.CaringProfile != api.CaringProfileCaring040RC2 {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-CARING-PROFILE", "", fmt.Sprintf("unsupported caring_profile %q", manifest.CaringProfile), "caring_profile"))
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceTypes := make(map[string]string, len(manifest.Resources))
|
||||||
|
for _, resource := range manifest.Resources {
|
||||||
|
resourceTypes[resource.ID] = resource.Type
|
||||||
|
}
|
||||||
|
for _, resource := range manifest.Resources {
|
||||||
|
diagnostics = append(diagnostics, validateResource(resource, resourceTypes)...)
|
||||||
|
}
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnrichResourceManifest adds namespace and CARING classification metadata.
|
||||||
|
func EnrichResourceManifest(manifest api.ResourceManifest) api.ResourceManifest {
|
||||||
|
out := manifest
|
||||||
|
if out.CaringProfile == "" {
|
||||||
|
out.CaringProfile = api.CaringProfileCaring040RC2
|
||||||
|
}
|
||||||
|
out.Metadata = copyMap(out.Metadata)
|
||||||
|
out.Metadata["markitect_namespace"] = namespaceVersion
|
||||||
|
out.Metadata["markitect_namespace_doc"] = namespaceDoc
|
||||||
|
|
||||||
|
out.Resources = append([]api.Resource(nil), manifest.Resources...)
|
||||||
|
for i, resource := range out.Resources {
|
||||||
|
if class, ok := classifications[resource.Type]; ok {
|
||||||
|
resource.Attributes = copyMap(resource.Attributes)
|
||||||
|
resource.Attributes["markitect_resource_type"] = resource.Type
|
||||||
|
resource.Attributes["caring_scope_level"] = class.scopeLevel
|
||||||
|
resource.Attributes["caring_planes"] = append([]api.Plane(nil), class.planes...)
|
||||||
|
out.Resources[i] = resource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateResource(resource api.Resource, resourceTypes map[string]string) []Diagnostic {
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
if resource.ID == "" {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-RESOURCE-ID", "", "resource id is required", "resources[].id"))
|
||||||
|
}
|
||||||
|
if resource.Type == "" {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-RESOURCE-TYPE", resource.ID, "resource type is required", "resources[].type"))
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
class, ok := classifications[resource.Type]
|
||||||
|
if !ok {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-RESOURCE-TYPE", resource.ID, fmt.Sprintf("unknown Markitect resource type %q", resource.Type), "resources[].type"))
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
if resource.Parent != "" {
|
||||||
|
if parentType, ok := resourceTypes[resource.Parent]; ok && !slices.Contains(class.parentTypes, parentType) {
|
||||||
|
diagnostics = append(diagnostics, errorDiagnostic("MARKITECT-PARENT-TYPE", resource.ID, fmt.Sprintf("resource type %q cannot be parented by %q", resource.Type, parentType), "resources[].parent"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resource.Parent == "" && len(class.parentTypes) > 0 {
|
||||||
|
diagnostics = append(diagnostics, warningDiagnostic("MARKITECT-PARENT-MISSING", resource.ID, fmt.Sprintf("resource type %q usually declares a parent", resource.Type), "resources[].parent"))
|
||||||
|
}
|
||||||
|
if len(resource.Labels) == 0 {
|
||||||
|
diagnostics = append(diagnostics, warningDiagnostic("MARKITECT-LABELS-MISSING", resource.ID, "resource has no labels; CARING exposure classification may be ambiguous", "resources[].labels"))
|
||||||
|
}
|
||||||
|
if resource.TrustZone == "" {
|
||||||
|
diagnostics = append(diagnostics, warningDiagnostic("MARKITECT-TRUST-ZONE-MISSING", resource.ID, "resource has no trust_zone; CARING exposure classification may be ambiguous", "resources[].trust_zone"))
|
||||||
|
}
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorDiagnostic(code, resource, message string, fields ...string) Diagnostic {
|
||||||
|
return Diagnostic{Code: code, Severity: "error", Resource: resource, Message: message, Fields: fields}
|
||||||
|
}
|
||||||
|
|
||||||
|
func warningDiagnostic(code, resource, message string, fields ...string) Diagnostic {
|
||||||
|
return Diagnostic{Code: code, Severity: "warning", Resource: resource, Message: message, Fields: fields}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasError(diagnostics []Diagnostic) bool {
|
||||||
|
for _, diagnostic := range diagnostics {
|
||||||
|
if diagnostic.Severity == "error" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMap(in map[string]any) map[string]any {
|
||||||
|
out := make(map[string]any, len(in))
|
||||||
|
for key, value := range in {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
129
internal/markitect/importer_test.go
Normal file
129
internal/markitect/importer_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package markitect_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netkingdom/flex-auth/internal/markitect"
|
||||||
|
"github.com/netkingdom/flex-auth/internal/registry"
|
||||||
|
"github.com/netkingdom/flex-auth/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImportResourceManifestLoadsPinnedMarkitectShape(t *testing.T) {
|
||||||
|
var manifest api.ResourceManifest
|
||||||
|
loadYAML(t, filepath.Join("..", "..", "examples", "markitect", "resource_manifest.yaml"), &manifest)
|
||||||
|
|
||||||
|
store := registry.NewStore()
|
||||||
|
result, err := markitect.ImportResourceManifest(store, manifest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ImportResourceManifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Manifest.CaringProfile != api.CaringProfileCaring040RC2 {
|
||||||
|
t.Fatalf("CaringProfile = %q; want %q", result.Manifest.CaringProfile, api.CaringProfileCaring040RC2)
|
||||||
|
}
|
||||||
|
if !hasDiagnostic(result.Diagnostics, "MARKITECT-CARING-PROFILE", "warning") {
|
||||||
|
t.Fatalf("diagnostics = %+v; want missing CARING profile warning", result.Diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, ok := store.Resource(markitect.SystemID, "document:internal-note")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("document:internal-note was not imported")
|
||||||
|
}
|
||||||
|
if resource.Attributes["caring_scope_level"] != api.ScopeLevelResource {
|
||||||
|
t.Fatalf("caring_scope_level = %v; want Resource", resource.Attributes["caring_scope_level"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportNamespaceResourceManifestClassifiesAllResources(t *testing.T) {
|
||||||
|
var manifest api.ResourceManifest
|
||||||
|
loadYAML(t, filepath.Join("..", "..", "examples", "markitect", "namespace_resource_manifest.yaml"), &manifest)
|
||||||
|
|
||||||
|
store := registry.NewStore()
|
||||||
|
result, err := markitect.ImportResourceManifest(store, manifest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ImportResourceManifest: %v\n%+v", err, result.Diagnostics)
|
||||||
|
}
|
||||||
|
if hasSeverity(result.Diagnostics, "error") {
|
||||||
|
t.Fatalf("diagnostics = %+v; did not expect errors", result.Diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
span, ok := store.Resource(markitect.SystemID, "span:internal-note#risk:customer-email")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("span resource was not imported")
|
||||||
|
}
|
||||||
|
if span.Attributes["caring_scope_level"] != api.ScopeLevelField {
|
||||||
|
t.Fatalf("span caring_scope_level = %v; want Field", span.Attributes["caring_scope_level"])
|
||||||
|
}
|
||||||
|
if span.TrustZone != "restricted" {
|
||||||
|
t.Fatalf("span.TrustZone = %q; want restricted", span.TrustZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportAmbiguousManifestReportsClassificationWarnings(t *testing.T) {
|
||||||
|
var manifest api.ResourceManifest
|
||||||
|
loadYAML(t, filepath.Join("..", "..", "examples", "markitect", "ambiguous_resource_manifest.yaml"), &manifest)
|
||||||
|
|
||||||
|
store := registry.NewStore()
|
||||||
|
result, err := markitect.ImportResourceManifest(store, manifest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ImportResourceManifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasDiagnostic(result.Diagnostics, "MARKITECT-LABELS-MISSING", "warning") {
|
||||||
|
t.Fatalf("diagnostics = %+v; want labels warning", result.Diagnostics)
|
||||||
|
}
|
||||||
|
if !hasDiagnostic(result.Diagnostics, "MARKITECT-TRUST-ZONE-MISSING", "warning") {
|
||||||
|
t.Fatalf("diagnostics = %+v; want trust zone warning", result.Diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportRejectsUnknownMarkitectResourceType(t *testing.T) {
|
||||||
|
manifest := api.ResourceManifest{
|
||||||
|
ID: "bad",
|
||||||
|
System: markitect.SystemID,
|
||||||
|
CaringProfile: api.CaringProfileCaring040RC2,
|
||||||
|
Resources: []api.Resource{
|
||||||
|
{ID: "unknown:1", Type: "unknown_type", Labels: []string{"internal"}, TrustZone: "internal"},
|
||||||
|
},
|
||||||
|
Metadata: map[string]any{"flex_auth_contract": api.FlexAuthContractV0},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := markitect.ImportResourceManifest(registry.NewStore(), manifest)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ImportResourceManifest accepted unknown resource type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDiagnostic(diagnostics []markitect.Diagnostic, code, severity string) bool {
|
||||||
|
for _, diagnostic := range diagnostics {
|
||||||
|
if diagnostic.Code == code && diagnostic.Severity == severity {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSeverity(diagnostics []markitect.Diagnostic, severity string) bool {
|
||||||
|
for _, diagnostic := range diagnostics {
|
||||||
|
if diagnostic.Severity == severity {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadYAML(t *testing.T, path string, out any) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, out); err != nil {
|
||||||
|
t.Fatalf("unmarshal %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ each resource level to CARING scope and plane values.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLEX-WP-0003-T002
|
id: FLEX-WP-0003-T002
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "90082eaf-37f5-492f-a884-ff8eec0eccaa"
|
state_hub_task_id: "90082eaf-37f5-492f-a884-ff8eec0eccaa"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user