feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
{% block title %}Config — {{ slug }} — Ontoref{% endblock title %}
|
|
|
|
|
{% block nav_config %}active{% endblock nav_config %}
|
|
|
|
|
{% block nav_group_dev %}active{% endblock nav_group_dev %}
|
|
|
|
|
|
|
|
|
|
{% block head %}
|
|
|
|
|
{% endblock head %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="mb-6 flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-2xl font-bold">Config Surface</h1>
|
|
|
|
|
<p class="text-base-content/50 text-sm mt-1">
|
|
|
|
|
<span class="font-mono">{{ config_root }}</span>
|
|
|
|
|
<span class="mx-1">·</span>
|
|
|
|
|
<span class="font-mono">{{ entry_point }}</span>
|
|
|
|
|
<span class="mx-1">·</span>
|
|
|
|
|
<span class="badge badge-neutral badge-xs font-mono">{{ kind }}</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="badge badge-lg {% if overall_status == 'Ok' %}badge-success{% elif overall_status == 'Warning' %}badge-warning{% else %}badge-error{% endif %}">
|
|
|
|
|
{{ overall_status }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{% if not has_config_surface %}
|
|
|
|
|
<div class="flex flex-col items-center justify-center py-16 text-base-content/40 text-sm">
|
|
|
|
|
<p>No <code class="font-mono">config_surface</code> in <code class="font-mono">.ontology/manifest.ncl</code>.</p>
|
|
|
|
|
<p class="mt-2">Add a <code class="font-mono">config_surface</code> field to enable config management.</p>
|
|
|
|
|
</div>
|
|
|
|
|
{% else %}
|
|
|
|
|
|
|
|
|
|
{% for section in sections %}
|
|
|
|
|
<div class="card bg-base-200 rounded-lg mb-4">
|
|
|
|
|
<div class="card-body p-4">
|
|
|
|
|
<!-- Section header -->
|
|
|
|
|
<div class="flex items-start justify-between gap-2 mb-3">
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
|
|
|
<h2 class="font-bold font-mono text-lg">{{ section.id }}</h2>
|
|
|
|
|
{% if section.coherence %}
|
|
|
|
|
{% set coh = section.coherence %}
|
2026-03-29 00:19:56 +00:00
|
|
|
<span class="badge badge-xs font-mono {% if coh.status == 'Ok' %}badge-success{% elif coh.status == 'Warning' %}badge-warning{% else %}badge-error{% endif %}">{{ coh.status }}</span>
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
{% endif %}
|
|
|
|
|
{% if not section.mutable %}
|
|
|
|
|
<span class="badge badge-ghost badge-xs">read-only</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
{% if section.description %}
|
|
|
|
|
<p class="text-sm text-base-content/70 mt-0.5">{{ section.description }}</p>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if section.rationale %}
|
|
|
|
|
<details class="mt-1">
|
|
|
|
|
<summary class="text-xs text-base-content/50 cursor-pointer hover:text-base-content/80">Why</summary>
|
|
|
|
|
<p class="text-xs text-base-content/60 mt-1 pl-2 border-l-2 border-base-300">{{ section.rationale }}</p>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
{% if section.mutable and current_role == "admin" %}
|
|
|
|
|
<button class="btn btn-xs btn-outline btn-primary"
|
|
|
|
|
onclick="openEditModal('{{ section.id }}', {{ section.current_values | json_encode() }})">
|
|
|
|
|
Edit
|
|
|
|
|
</button>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Consumers -->
|
|
|
|
|
{% if section.consumers %}
|
|
|
|
|
<div class="flex flex-wrap gap-1.5 mb-3">
|
|
|
|
|
{% for c in section.consumers %}
|
2026-03-29 00:19:56 +00:00
|
|
|
<span class="badge badge-ghost badge-xs font-mono {% if c.kind == 'RustStruct' %}text-orange-400{% elif c.kind == 'NuScript' %}text-cyan-400{% elif c.kind == 'CiPipeline' %}text-purple-400{% else %}text-yellow-400{% endif %}" title="{{ c.ref }}">
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
{{ c.kind | replace(from='RustStruct', to='Rust') | replace(from='NuScript', to='Nu') | replace(from='CiPipeline', to='CI') | replace(from='External', to='Ext') }}:{{ c.id }}
|
|
|
|
|
</span>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
<!-- Unclaimed fields warning -->
|
|
|
|
|
{% if section.coherence and section.coherence.unclaimed_fields %}
|
|
|
|
|
<div class="alert alert-warning py-2 px-3 text-xs mb-3">
|
|
|
|
|
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span>Unclaimed fields: <span class="font-mono">{{ section.coherence.unclaimed_fields | join(sep=", ") }}</span></span>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
<!-- Current values -->
|
|
|
|
|
{% if section.current_values %}
|
|
|
|
|
<details class="text-xs">
|
|
|
|
|
<summary class="cursor-pointer text-base-content/50 hover:text-base-content/80 mb-1">
|
|
|
|
|
Current values
|
|
|
|
|
<span class="badge badge-ghost badge-xs ml-1">{{ section.file }}</span>
|
|
|
|
|
</summary>
|
|
|
|
|
<pre class="bg-base-300 p-3 rounded overflow-x-auto text-xs mt-2">{{ section.current_values | json_encode(pretty=true) }}</pre>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
<!-- Override history -->
|
|
|
|
|
{% if section.overrides and section.overrides | length > 0 %}
|
|
|
|
|
<details class="text-xs mt-2">
|
|
|
|
|
<summary class="cursor-pointer text-base-content/50 hover:text-base-content/80">
|
|
|
|
|
Override history <span class="badge badge-warning badge-xs ml-1">{{ section.overrides | length }}</span>
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="mt-2 space-y-1">
|
|
|
|
|
{% for o in section.overrides %}
|
|
|
|
|
<div class="bg-base-300 p-2 rounded text-xs">
|
|
|
|
|
<span class="font-mono text-warning">{{ o.field }}</span>
|
|
|
|
|
<span class="text-base-content/50 mx-1">{{ o.from }} → {{ o.to }}</span>
|
|
|
|
|
{% if o.reason %}<span class="text-base-content/60">— {{ o.reason }}</span>{% endif %}
|
|
|
|
|
{% if o.ts %}<span class="text-base-content/40 ml-2">{{ o.ts }}</span>{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
|
|
|
|
<!-- Edit modal -->
|
|
|
|
|
<dialog id="edit-modal" class="modal">
|
|
|
|
|
<div class="modal-box w-11/12 max-w-2xl">
|
|
|
|
|
<h3 class="font-bold text-lg mb-4">Edit config section: <span id="edit-section-id" class="font-mono text-primary"></span></h3>
|
|
|
|
|
<div class="form-control mb-4">
|
|
|
|
|
<label class="label"><span class="label-text text-xs">Values (JSON)</span></label>
|
|
|
|
|
<textarea id="edit-values" class="textarea textarea-bordered font-mono text-sm h-48 resize-none" placeholder='{"key": "value"}'></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-control mb-4">
|
|
|
|
|
<label class="label"><span class="label-text text-xs">Reason for change</span></label>
|
|
|
|
|
<input type="text" id="edit-reason" class="input input-bordered input-sm" placeholder="e.g. port conflict with another service">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-2 items-center mb-4">
|
|
|
|
|
<input type="checkbox" id="edit-dry-run" class="checkbox checkbox-sm" checked>
|
|
|
|
|
<label for="edit-dry-run" class="text-sm">Dry run (preview only)</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="edit-result" class="hidden">
|
|
|
|
|
<div class="divider text-xs">Preview</div>
|
|
|
|
|
<pre id="edit-result-body" class="bg-base-300 p-3 rounded text-xs overflow-x-auto max-h-48"></pre>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-action">
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick="document.getElementById('edit-modal').close()">Cancel</button>
|
|
|
|
|
<button class="btn btn-outline btn-sm" onclick="submitConfig(true)">Preview</button>
|
|
|
|
|
<button class="btn btn-primary btn-sm" id="apply-btn" disabled onclick="submitConfig(false)">Apply</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</dialog>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
let _editSection = '';
|
|
|
|
|
function openEditModal(id, values) {
|
|
|
|
|
_editSection = id;
|
|
|
|
|
document.getElementById('edit-section-id').textContent = id;
|
|
|
|
|
document.getElementById('edit-values').value = JSON.stringify(values, null, 2);
|
|
|
|
|
document.getElementById('edit-reason').value = '';
|
|
|
|
|
document.getElementById('edit-dry-run').checked = true;
|
|
|
|
|
document.getElementById('edit-result').classList.add('hidden');
|
|
|
|
|
document.getElementById('apply-btn').disabled = true;
|
|
|
|
|
document.getElementById('edit-modal').showModal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitConfig(dryRun) {
|
|
|
|
|
let values;
|
|
|
|
|
try { values = JSON.parse(document.getElementById('edit-values').value); }
|
|
|
|
|
catch(e) { alert('Invalid JSON: ' + e.message); return; }
|
|
|
|
|
const reason = document.getElementById('edit-reason').value;
|
|
|
|
|
const body = { values, dry_run: dryRun, reason };
|
|
|
|
|
const res = await fetch(`/api/projects/{{ slug }}/config/${_editSection}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
const resultEl = document.getElementById('edit-result');
|
|
|
|
|
const bodyEl = document.getElementById('edit-result-body');
|
|
|
|
|
bodyEl.textContent = JSON.stringify(data, null, 2);
|
|
|
|
|
resultEl.classList.remove('hidden');
|
|
|
|
|
if (dryRun) {
|
|
|
|
|
document.getElementById('apply-btn').disabled = false;
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById('apply-btn').disabled = true;
|
|
|
|
|
if (res.ok) { setTimeout(() => { document.getElementById('edit-modal').close(); window.location.reload(); }, 800); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% endblock content %}
|