provisioning/reflection/modes/provisioning-dag-integrity.ncl

109 lines
7 KiB
Text
Raw Permalink Normal View History

2026-05-12 02:40:14 +01:00
let s = import "reflection/schema.ncl" in
# Mode: provisioning-dag-integrity
# Audits the full extension capability DAG for structural integrity:
# 1. All taskservs have provides/requires/conflicts_with fields populated
# 2. Every Required capability declared in `requires` is provided by at least one other taskserv
# 3. ConflictsWith pairs: if both sides exist in the registry, emit an error
# 4. No circular capability dependencies (A provides X, B requires X and provides Y, A requires Y)
# 5. Summary: capability coverage table + unresolved gaps + conflict pairs
#
# Run from provisioning root with:
# nickel export --format json --import-path <ontoref-path> reflection/modes/provisioning-dag-integrity.ncl
{
id = "provisioning-dag-integrity",
trigger = "Audit extension capability DAG for completeness, resolution coverage, and conflict safety",
strategy = 'Override,
preconditions = [
"nickel is in PATH",
"jq is in PATH",
"catalog/taskservs/ directory exists with at least one metadata.ncl",
"NICKEL_IMPORT_PATH includes the provisioning/ root",
],
guards = [
{
id = "taskservs_dir_exists",
cmd = "test -d catalog/taskservs",
reason = "catalog/taskservs/ directory not found — run from provisioning root",
severity = 'Block,
},
],
steps = [
{
id = "export_all_metadata",
action = "export_taskserv_metadata",
actor = 'Agent,
cmd = "for ts in catalog/taskservs/*/; do name=$(basename $ts); meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then echo \"{\\\"name\\\": \\\"$name\\\", \\\"meta\\\": $(nickel export --format json --import-path . $meta 2>/dev/null || echo 'null')}\"; fi; done | jq -s 'map(select(.meta != null))'",
depends_on = [],
on_error = { strategy = 'Stop },
note = "Export all taskserv metadata.ncl files as JSON. Filters out taskservs with missing or broken metadata.",
},
{
id = "check_fields_populated",
action = "verify_capability_fields_present",
actor = 'Agent,
cmd = "for ts in catalog/taskservs/*/; do name=$(basename $ts); meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then result=$(nickel export --format json --import-path . $meta 2>/dev/null); if [ $? -ne 0 ]; then echo \"EXPORT_FAIL: $name\"; continue; fi; has_provides=$(echo $result | jq 'has(\"provides\")'); has_requires=$(echo $result | jq 'has(\"requires\")'); has_conflicts=$(echo $result | jq 'has(\"conflicts_with\")'); if [ \"$has_provides\" != 'true' ] || [ \"$has_requires\" != 'true' ] || [ \"$has_conflicts\" != 'true' ]; then echo \"MISSING_FIELDS: $name (provides=$has_provides requires=$has_requires conflicts_with=$has_conflicts)\"; else echo \"OK: $name\"; fi; fi; done",
depends_on = [],
on_error = { strategy = 'Continue },
note = "Check that every taskserv metadata.ncl has provides, requires, and conflicts_with fields. MISSING_FIELDS lines indicate incomplete migrations.",
},
{
id = "build_provides_index",
action = "index_all_capabilities",
actor = 'Agent,
cmd = "for ts in catalog/taskservs/*/; do meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then nickel export --format json --import-path . $meta 2>/dev/null | jq -r --arg ts \"$(basename $ts)\" '.provides[]? | [$ts, .id] | @tsv'; fi; done | sort",
depends_on = [{ step = "check_fields_populated", kind = 'Always }],
on_error = { strategy = 'Stop },
note = "Build the capability provides index: one line per (taskserv, capability_id) pair. Used in resolution step.",
},
{
id = "resolve_required_capabilities",
action = "check_required_dep_coverage",
actor = 'Agent,
cmd = "provides=$(for ts in catalog/taskservs/*/; do meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then nickel export --format json --import-path . $meta 2>/dev/null | jq -r '.provides[]?.id'; fi; done | sort -u); for ts in catalog/taskservs/*/; do name=$(basename $ts); meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then nickel export --format json --import-path . $meta 2>/dev/null | jq -r --arg ts \"$name\" '.requires[]? | select(.kind == \"Required\") | .capability' | while read cap; do if echo \"$provides\" | grep -qx \"$cap\"; then echo \"RESOLVED: $ts requires $cap\"; else echo \"UNRESOLVED: $ts requires $cap (Required — no provider found)\"; fi; done; fi; done",
depends_on = [{ step = "build_provides_index", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "For every Required capability in requires[], check that at least one other taskserv provides it. UNRESOLVED lines are hard errors — the formula will fail at runtime.",
},
{
id = "detect_conflict_pairs",
action = "check_conflictswith_pairs",
actor = 'Agent,
cmd = "all_names=$(for ts in catalog/taskservs/*/; do basename $ts; done); for ts in catalog/taskservs/*/; do name=$(basename $ts); meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then nickel export --format json --import-path . $meta 2>/dev/null | jq -r --arg ts \"$name\" '.conflicts_with[]?' | while read conflicting; do if echo \"$all_names\" | grep -qx \"$conflicting\"; then echo \"CONFLICT_PAIR: $name conflicts_with $conflicting (both present in registry)\"; fi; done; fi; done | sort -u",
depends_on = [{ step = "build_provides_index", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "Detect declared ConflictsWith pairs where both extensions exist in the registry. These pairs must not appear together in the same formula. Emit as audit info — enforcement happens in provisioning-validate-formula.",
},
{
id = "integrity_report",
action = "emit_integrity_summary",
actor = 'Agent,
cmd = "echo '--- Extension Capability DAG Integrity Report ---' && echo '' && echo '== Capability Provides Index ==' && for ts in catalog/taskservs/*/; do meta=$ts/metadata.ncl; if [ -f \"$meta\" ]; then caps=$(nickel export --format json --import-path . $meta 2>/dev/null | jq -r '[.provides[]?.id] | join(\", \")' 2>/dev/null); reqs=$(nickel export --format json --import-path . $meta 2>/dev/null | jq -r '[.requires[]? | \"\\(.capability)[\\(.kind)]\"] | join(\", \")' 2>/dev/null); echo \" $(basename $ts): provides=[$caps] requires=[$reqs]\"; fi; done && echo '' && echo 'Run with --verbose to show RESOLVED/UNRESOLVED resolution details.'",
depends_on = [
{ step = "resolve_required_capabilities", kind = 'Always },
{ step = "detect_conflict_pairs", kind = 'Always },
],
on_error = { strategy = 'Stop },
note = "Print the full capability coverage table showing what each taskserv provides and requires, then a resolution summary.",
},
],
postconditions = [
"All taskservs have provides/requires/conflicts_with fields (or MISSING_FIELDS flagged)",
"All Required capabilities resolve to at least one provider (or UNRESOLVED flagged)",
"ConflictsWith pairs identified (enforcement delegated to provisioning-validate-formula per-formula)",
"Capability coverage table emitted",
],
} | (s.Mode String)