ontoref/reflection/bin/jjw-ncl-merge.nu

165 lines
5.7 KiB
Text
Raw Normal View History

feat: domain extension system, VCS abstraction, personal/provisioning domains, web subpages Domain extension system (ADR-012): bash-layer dispatch activates repo_kind-conditional CLI domains. install.nu copies domains/ tree; short_alias wrappers generated (personal, prov). ore help and describe capabilities domain-aware. personal domain (PersonalOntology): career skills/talks/publications/positioning, CFP pipeline (Watching→Delivered), opportunities lifecycle, content pipeline, Sessionize integration. Daemon pages: /career, /personal. provisioning domain (DevWorkspace/Mixed): FSM state, next transitions, connections graph, gates, workspace card, capabilities, backlog. Daemon page: /provisioning. VCS abstraction layer (ADR-013): reflection/modules/vcs.nu — uniform jj/git API via filesystem detection (.jj/ vs .git/). opmode.nu and git-event.nu migrated off ^git. reflection/bin/jjw.nu — jj + ontoref + Radicle agent workspace lifecycle. jjw-ncl-merge.nu registered as jj merge tool for .ontology/ NCL conflicts. init-repo.nu for new_project mode. jj/rad not in ontoref requirements — belong in orchestration project manifests. 'Framework RepoKind: ontology/schemas/manifest.ncl gains 'Framework variant; ontoref self-identifies as framework — no domain activates for the protocol itself. Web presence: personal.html and provisioning.html domain subpages. index.html gains "Project Types — Domain Extensions" section with type cards and subpage links. Nav compacted (Arch/Prov labels, solid backdrop-filter background). on+re: vcs-abstraction (adrs: adr-013) and agent-workspace-orchestration Practice nodes; 21 manifest capabilities; state.ncl catalysts updated.
2026-04-07 23:08:29 +01:00
#!/usr/bin/env nu
# reflection/bin/jjw-ncl-merge.nu — 3-way NCL merge driver for jj resolve.
#
# jj calls this as:
# nu jjw-ncl-merge.nu <left> <right> <base> <output>
#
# Register as a jj merge tool in ~/.config/jj/config.toml:
# [merge-tools.ncl]
# program = "nu"
# args = ["/path/to/reflection/bin/jjw-ncl-merge.nu", "$left", "$right", "$base", "$output"]
#
# Usage from jjw:
# jj resolve --tool ncl -- .ontology/core.ncl
#
# Strategy (two-layer):
# 1. git merge-file — 3-way text merge. Handles disjoint record additions
# without parsing NCL: side A adds node_x, side B adds node_y, both
# added to the same record → clean text merge.
# 2. nickel export — validates the text-merged result is legal NCL.
# Catches pathological cases where text merge succeeded but produced
# structurally invalid NCL (e.g., duplicate field names in different
# positions, mismatched let-bindings).
#
# Exit 0: $output written with merged content.
# Exit 1: auto-merge not possible — jj will fall back to manual resolution.
#
# Requires: git, nickel in PATH.
# ONTOREF_ROOT or ONTOREF_PROJECT_ROOT must be set (provides --import-path
# so nickel can locate schema.ncl and other imports).
const ONLY_ONTOLOGY_FILES = [
"core.ncl",
"state.ncl",
"gate.ncl",
"manifest.ncl",
]
def main [
left: path, # jj "ours" side (current workspace change)
right: path, # jj "theirs" side (the other side being merged)
base: path, # jj common ancestor
output: path, # jj expects the resolved content here
]: nothing -> nothing {
let left_name = ($left | path basename)
let right_name = ($right | path basename)
# ── Verify both sides are valid NCL before attempting merge ──────────────
let import_path = resolve-import-path $left
let check_left = (validate-ncl $left $import_path)
let check_right = (validate-ncl $right $import_path)
if not $check_left.ok {
error make {
msg: $"left side is not valid NCL:\n($check_left.err)"
}
}
if not $check_right.ok {
error make {
msg: $"right side is not valid NCL:\n($check_right.err)"
}
}
# ── Layer 1: git merge-file (3-way text merge) ───────────────────────────
# --stdout writes result to stdout; none of the input files are modified.
# Exits 0 = clean, 1 = conflict markers present, >1 = error.
let merge_r = (do {
^git merge-file --stdout ($left) ($base) ($right)
} | complete)
if $merge_r.exit_code > 1 {
error make {
msg: $"git merge-file error (exit ($merge_r.exit_code)): ($merge_r.stderr | str trim)"
}
}
if $merge_r.exit_code == 1 {
let conflicts = (count-conflict-markers $merge_r.stdout)
error make {
msg: $"text-level merge has ($conflicts) conflict(s) — manual resolution required.\nUse: jj resolve -r @ -- <file>"
}
}
let merged_text = $merge_r.stdout
# ── Layer 2: validate merged NCL ─────────────────────────────────────────
# Write to a temp file so nickel can read it; defer ensures cleanup even
# if nickel export throws or the process is interrupted.
let tmp_merged = (mktemp --suffix ".ncl")
defer { rm -f $tmp_merged }
$merged_text | save --force $tmp_merged
let validate = (validate-ncl $tmp_merged $import_path)
if not $validate.ok {
error make {
msg: $"text merge succeeded but result is invalid NCL:\n($validate.err)\nManual resolution required."
}
}
# ── Write output ─────────────────────────────────────────────────────────
$merged_text | save --force $output
print $" NCL auto-merge: ($left_name) ← ($right_name) [text+NCL clean]"
}
# Resolve the nickel --import-path from the file location or env.
#
# jj places its temp merge files somewhere under .jj/rewrite/ — they retain
# the original filename but the directory is different. We resolve the import
# path from env vars (set by ontoref sessions) or fall back to cwd.
def resolve-import-path [file: path]: nothing -> string {
let from_env = (
$env.ONTOREF_PROJECT_ROOT? | default (
$env.ONTOREF_ROOT? | default ""
)
)
if ($from_env | is-not-empty) { return $from_env }
# Best-effort: walk up from the file's real path looking for .ontology/
let mut dir = ($file | path expand | path dirname)
for _ in 0..8 {
if ($dir | path join ".ontology" | path exists) { return $dir }
let parent = ($dir | path dirname)
if $parent == $dir { break } # reached filesystem root
$dir = $parent
}
pwd | path expand
}
# Run nickel export on a file, return {ok: bool, err: string}.
def validate-ncl [file: path, import_path: string]: nothing -> record {
let r = (do {
^nickel export --import-path $import_path $file
} | complete)
if $r.exit_code == 0 {
{ ok: true, err: "" }
} else {
{ ok: false, err: ($r.stderr | str trim) }
}
}
# Count jj/git conflict markers in merged text.
def count-conflict-markers [text: string]: nothing -> int {
$text | lines | where { |l| ($l | str starts-with "<<<<<<<") } | length
}
#[test]
def test-conflict-marker-count [] {
let text = "<<<<<<< left\nfoo\n=======\nbar\n>>>>>>> right\n"
assert equal (count-conflict-markers $text) 1
}
#[test]
def test-conflict-marker-count-zero [] {
let text = "let s = import \"schema.ncl\" in\n{ node_a = {} }\n"
assert equal (count-conflict-markers $text) 0
}