20 KiB
VS Renderer Specification
Project: kane-diagnostics
Version: 1.0
Applies to: hubzilla/addon/vs01/vs01.php and any future addon that renders VS schema forms
Prerequisite reading: All ten VS schema files in hubzilla/addon/vs01/vital-signs/
Purpose
This document specifies how the PHP renderer reads the VS JSON schemas and produces the form interface. The renderer knows nothing about what a Vital Sign is. It only knows how to render what the schema describes. All diagnostic logic, field structure, flag conditions, and display instructions live in the JSON. The renderer's job is to execute those instructions faithfully.
This is the contract between the schema and the code. When the schema changes, the renderer does not change — unless a new field type or behavior is introduced that the renderer does not yet handle. In that case this document is revised first, then the renderer is updated.
Schema Loading
The renderer loads schemas from addon/vs01/vital-signs/. Files are named VS-{NN}-{slug}.json. The renderer loads all files in this directory on each request. File order is determined by the numeric prefix — VS-01 before VS-02, and so on.
function vs01_load_schemas() {
$dir = 'addon/vs01/vital-signs/';
$files = glob($dir . 'VS-*.json');
if (!$files) return [];
sort($files);
$schemas = [];
foreach ($files as $file) {
$raw = @file_get_contents($file);
if (!$raw) continue;
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) continue;
$code = $data['_meta']['code'] ?? null;
if ($code) $schemas[$code] = $data;
}
return $schemas;
}
If a schema file cannot be read or parsed, it is skipped and a visible error is logged. The renderer never silently ignores a missing or malformed schema.
Access State and Perspective Routing
Each schema defines three perspective slots: homeowner, professional, public_record. Each slot carries an access_state value. The renderer maps the current visitor's access state to the correct perspective slot.
| Access state | Perspective rendered |
|---|---|
public |
No form — access wall only |
participant |
homeowner perspective |
professional |
professional perspective |
operator |
All three perspectives — read-only view of homeowner and professional, editable public_record |
The professional access state corresponds to a Tier-I or Tier-II listed professional whose entry in listings.json has status: active. The renderer calls vs01_access_state() and routes accordingly. A professional visitor sees the homeowner record as read-only and their own professional slot as editable.
The operator sees all three slots. The homeowner and professional slots are read-only in the operator view. The public_record slot is editable only if immutable_after_submit is false or no public record submission exists yet for this VS code and association.
Form Structure
Each VS renders as a single form. The form contains:
- A VS header — code, title, diagnostic question from
_meta - A verification sources panel — expandable, lists each source with label, URL, and instruction
- The perspective slot — the fields for the current visitor's access state
- A submit button
- A CSRF token field
The form action posts to /vs01 with hidden fields for vs_code and perspective.
Field Types
The renderer implements one function per field type. All seven types are defined here. No other field type may appear in a schema without a corresponding entry in this spec and a corresponding renderer function.
text
Single-line text input.
function vs01_render_field_text($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$ph = vs01_h($field['placeholder'] ?? '');
$req = !empty($field['required']) ? 'required' : '';
return '
<div class="vs01-field vs01-field-text" data-field-id="' . $id . '">
<label for="' . $id . '">' . $label . '</label>
<input type="text" id="' . $id . '" name="fields[' . $id . ']"
value="' . vs01_h($value) . '"
placeholder="' . $ph . '" ' . $req . '>
</div>
';
}
number
Numeric input. Accepts integers and decimals.
function vs01_render_field_number($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$ph = vs01_h($field['placeholder'] ?? '');
$req = !empty($field['required']) ? 'required' : '';
return '
<div class="vs01-field vs01-field-number" data-field-id="' . $id . '">
<label for="' . $id . '">' . $label . '</label>
<input type="number" id="' . $id . '" name="fields[' . $id . ']"
value="' . vs01_h($value) . '"
placeholder="' . $ph . '" ' . $req . '>
</div>
';
}
date
Date input. Renders as <input type="date">. The PHP renderer does not impose locale formatting — the browser handles display. Stored values are ISO 8601 (YYYY-MM-DD).
function vs01_render_field_date($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$req = !empty($field['required']) ? 'required' : '';
return '
<div class="vs01-field vs01-field-date" data-field-id="' . $id . '">
<label for="' . $id . '">' . $label . '</label>
<input type="date" id="' . $id . '" name="fields[' . $id . ']"
value="' . vs01_h($value) . '" ' . $req . '>
</div>
';
}
boolean
Renders as two radio buttons — Yes and No. Stored values are true and false as strings.
function vs01_render_field_boolean($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$req = !empty($field['required']) ? 'required' : '';
$yes = ($value === 'true') ? 'checked' : '';
$no = ($value === 'false') ? 'checked' : '';
return '
<div class="vs01-field vs01-field-boolean" data-field-id="' . $id . '">
<fieldset>
<legend>' . $label . '</legend>
<label>
<input type="radio" name="fields[' . $id . ']"
value="true" ' . $yes . ' ' . $req . '> Yes
</label>
<label>
<input type="radio" name="fields[' . $id . ']"
value="false" ' . $no . '> No
</label>
</fieldset>
</div>
';
}
select
Dropdown. Options come from $field['options'] — each option is an array with value and label.
function vs01_render_field_select($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$req = !empty($field['required']) ? 'required' : '';
$options = $field['options'] ?? [];
$out = '<div class="vs01-field vs01-field-select" data-field-id="' . $id . '">';
$out .= '<label for="' . $id . '">' . $label . '</label>';
$out .= '<select id="' . $id . '" name="fields[' . $id . ']" ' . $req . '>';
$out .= '<option value="">— select —</option>';
foreach ($options as $opt) {
$v = vs01_h($opt['value']);
$l = vs01_h($opt['label']);
$sel = ($value === $opt['value']) ? 'selected' : '';
$out .= '<option value="' . $v . '" ' . $sel . '>' . $l . '</option>';
}
$out .= '</select></div>';
return $out;
}
textarea
Multi-line text input.
function vs01_render_field_textarea($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$ph = vs01_h($field['placeholder'] ?? '');
$req = !empty($field['required']) ? 'required' : '';
return '
<div class="vs01-field vs01-field-textarea" data-field-id="' . $id . '">
<label for="' . $id . '">' . $label . '</label>
<textarea id="' . $id . '" name="fields[' . $id . ']"
placeholder="' . $ph . '" rows="5" ' . $req . '>'
. vs01_h($value) .
'</textarea>
</div>
';
}
multiselect
Checkbox group. Options come from $field['options']. Stored as a JSON-encoded array of selected values. Introduced in VS-08 for pro_dsc_cascade.
function vs01_render_field_multiselect($field, $value = '') {
$id = vs01_h($field['id']);
$label = vs01_h($field['label']);
$options = $field['options'] ?? [];
$selected = $value ? json_decode($value, true) : [];
if (!is_array($selected)) $selected = [];
$out = '<div class="vs01-field vs01-field-multiselect" data-field-id="' . $id . '">';
$out .= '<fieldset><legend>' . $label . '</legend>';
foreach ($options as $opt) {
$v = vs01_h($opt['value']);
$l = vs01_h($opt['label']);
$chk = in_array($opt['value'], $selected) ? 'checked' : '';
$out .= '<label>
<input type="checkbox" name="fields[' . $id . '][]"
value="' . $v . '" ' . $chk . '> ' . $l . '
</label>';
}
$out .= '</fieldset></div>';
return $out;
}
Field Dispatcher
A single function dispatches to the correct type renderer.
function vs01_render_field($field, $submitted_values = []) {
$id = $field['id'] ?? '';
$type = $field['type'] ?? 'text';
$value = $submitted_values[$id] ?? '';
$html = '';
switch ($type) {
case 'text': $html = vs01_render_field_text($field, $value); break;
case 'number': $html = vs01_render_field_number($field, $value); break;
case 'date': $html = vs01_render_field_date($field, $value); break;
case 'boolean': $html = vs01_render_field_boolean($field, $value); break;
case 'select': $html = vs01_render_field_select($field, $value); break;
case 'textarea': $html = vs01_render_field_textarea($field, $value); break;
case 'multiselect': $html = vs01_render_field_multiselect($field, $value); break;
default:
$html = '<div class="vs01-field-error">Unknown field type: '
. vs01_h($type) . '</div>';
}
$html .= vs01_render_diagnostic_flag($field, $value);
return $html;
}
Conditional Display — depends_on
A field with a depends_on property is hidden until the condition is met. The renderer outputs the field wrapped in a container with a data-depends-on attribute. JavaScript handles show/hide. PHP always renders the field — JavaScript controls visibility.
function vs01_wrap_depends_on($field, $html) {
if (empty($field['depends_on'])) return $html;
$dep_field = vs01_h($field['depends_on']['field']);
$dep_value = $field['depends_on']['value'];
if (is_array($dep_value)) {
$dep_value = vs01_h(json_encode($dep_value));
} else {
$dep_value = vs01_h((string) $dep_value);
}
return '
<div class="vs01-conditional"
data-depends-on="' . $dep_field . '"
data-depends-value="' . $dep_value . '"
style="display:none;">
' . $html . '
</div>
';
}
The JavaScript in vs01.js reads data-depends-on and data-depends-value on page load and on every change event, showing or hiding the container accordingly.
Diagnostic Flags
A field with diagnostic_flag: true renders a flag panel immediately after the field when the flag condition is met. The renderer evaluates the flag condition against the current submitted value. On initial load with no submitted value, no flag is shown. After submission, flags are evaluated server-side and rendered with the form.
function vs01_render_diagnostic_flag($field, $value) {
if (empty($field['diagnostic_flag'])) return '';
$condition = $field['flag_condition'] ?? null;
if ($condition === null) return '';
$triggered = false;
if (is_array($condition)) {
$triggered = in_array($value, $condition);
} elseif (is_bool($condition)) {
$triggered = ($value === ($condition ? 'true' : 'false'));
} else {
$triggered = ($value === (string) $condition);
}
if (!$triggered) return '';
$label = vs01_h($field['flag_label'] ?? 'Diagnostic condition');
$note = isset($field['flag_note'])
? '<p class="vs01-flag-note">' . vs01_h($field['flag_note']) . '</p>'
: '';
$severity = vs01_flag_severity($field);
return '
<div class="vs01-flag vs01-flag-' . $severity . '" role="alert">
<span class="vs01-flag-label">' . $label . '</span>
' . $note . '
</div>
';
}
Flag Severity
Three severity levels control visual prominence. The schema does not carry an explicit severity field — severity is derived from the field id prefix and VS code.
function vs01_flag_severity($field) {
$id = $field['id'] ?? '';
// Self-dealing loop — VS-03 pro_selfdealing_verified
if ($id === 'pro_selfdealing_verified') return 'critical';
// Dissolution cascade — VS-08 pro_standing_verified
if ($id === 'pro_standing_verified') return 'critical';
// Compound condition fields — VS-06 pro_incumbent_perpetuation_risk
if ($id === 'pro_incumbent_perpetuation_risk') return 'critical';
// Professional verification flags default to high
if (str_starts_with($id, 'pro_')) return 'high';
// Homeowner observation flags default to standard
return 'standard';
}
CSS classes: vs01-flag-critical, vs01-flag-high, vs01-flag-standard. Visual treatment defined in vs01.css.
Compound Condition Detection
Three compound conditions span multiple VS codes. They are evaluated when the operator views the full VS record for an association. They are not evaluated per-field or per-VS — they are evaluated once across the complete VS record set.
Incumbent Perpetuation Triad
Triggered when all three conditions are present in the same association record:
- VS-02:
ho_quorum_achieved=yes_undocumentedORpro_quorum_structurally_achievable=false - VS-05:
pro_enforcement_of_void_provision=true - VS-06:
pro_incumbent_perpetuation_risk=true
Display: a banner above the VS navigation reading — "Incumbent perpetuation triad present — quorum failure, outdated election procedures, and absence of qualification requirements confirmed for this association."
Dissolution Cascade
Triggered when VS-08 pro_standing_verified is dissolved_administrative, dissolved_voluntary, or not_found.
Display: a banner listing the affected DSC categories — DSC-01, DSC-02, DSC-04, DSC-10 — with the note: "Association dissolved — all board authority, assessment authority, lien authority, and foreclosure authority affected."
Financial Unverifiability
Triggered when VS-09 pro_audit_verified is none AND any of the following are present in the same record:
- VS-04:
pro_ledger_reconcilable=noorrecords_unavailable - VS-04:
pro_attorney_fees_authorized=no - VS-09:
pro_financial_records_reconcilable=noornot_produced
Display: a note reading — "Financial claims in this record cannot be independently verified. No independent audit exists and financial records are incomplete or unproduced."
Cross-VS Reference Notes
Two VS schema fields explicitly reference another VS code:
- VS-05 flag on unrecorded amendments: "See VS-10 for the full Declaration recording currency analysis."
- VS-10
pro_vs05_cross_reference: links to the VS-05 record for this association. - VS-06 flag on Bylaws not requiring certification: "See VS-03 for the full manager certification analysis."
The renderer outputs these as linked callouts when the referenced VS code exists in the loaded schema set.
function vs01_render_cross_reference($vs_code, $label) {
return '
<div class="vs01-cross-ref">
<span class="vs01-cross-ref-label">See also:</span>
<a href="/vs01?vs=' . vs01_h($vs_code) . '">' . vs01_h($label) . '</a>
</div>
';
}
Immutable Public Record
The public_record perspective slot carries immutable_after_submit: true. Once a public record submission exists for a VS code and association, the renderer outputs the public record slot as a read-only display, not a form. No submit button is shown. A note reads: "Public record — immutable. Contact the operator to correct spelling or citation errors."
The renderer checks for an existing public record submission before rendering the public record perspective:
function vs01_public_record_exists($vs_code, $association_id) {
// TODO: query orchestrator or local TMP store for existing public record
// Returns true if a submitted and promoted public record exists
return false;
}
Verification Sources Panel
Each schema _meta block carries a verification_sources array. The renderer outputs this as a collapsible panel above the form fields.
function vs01_render_verification_sources($meta) {
$sources = $meta['verification_sources'] ?? [];
if (empty($sources)) return '';
$out = '<div class="vs01-verification-sources">';
$out .= '<button type="button" class="vs01-sources-toggle"
data-bs-toggle="collapse"
data-bs-target="#vs01-sources-' . vs01_h($meta['code']) . '">
Where to verify this
</button>';
$out .= '<div class="collapse" id="vs01-sources-' . vs01_h($meta['code']) . '">';
foreach ($sources as $source) {
$label = vs01_h($source['label'] ?? '');
$url = vs01_h($source['url'] ?? '');
$instruction = vs01_h($source['instruction'] ?? '');
$out .= '<div class="vs01-source">';
$out .= '<a href="' . $url . '" target="_blank" rel="noopener">' . $label . '</a>';
$out .= '<p>' . $instruction . '</p>';
$out .= '</div>';
}
$out .= '</div></div>';
return $out;
}
Information Record Distinction
VS-07 and VS-09 homeowner perspectives are information records — the homeowner records what was communicated, not what they verified. The renderer signals this distinction with a note above the homeowner fields for these two VS codes, drawn from the schema _meta.renderer_notes field.
function vs01_render_perspective_note($schema, $perspective_key) {
$notes = $schema['_meta']['renderer_notes'] ?? '';
if (!$notes) return '';
// Only render the note for homeowner perspective on VS-07 and VS-09
$code = $schema['_meta']['code'] ?? '';
if ($perspective_key !== 'homeowner') return '';
if (!in_array($code, ['VS-07', 'VS-09'])) return '';
return '<div class="vs01-perspective-note">' . vs01_h($notes) . '</div>';
}
Spool POST Handler
On form submission the renderer validates required fields, assembles the spool envelope, and POSTs to the orchestrator receiver. The envelope follows the contract defined in each schema's spool block.
function vs01_build_spool_envelope($vs_code, $perspective, $fields, $association_address) {
return [
'vs_code' => $vs_code,
'perspective' => $perspective,
'association_address' => $association_address,
'submitted_at' => date('c'),
'standing' => vs01_access_state(),
'fields' => $fields,
];
}
The envelope is JSON-encoded and POSTed with the node token header to the receiver URL from config.json. On success the renderer shows a confirmation. On failure it shows a visible error — never silent failure.
File Size Constraint
The renderer functions defined in this spec, when implemented in vs01.php, must not cause the file to exceed 500 lines. If the combined function set approaches that limit, extract the renderer functions into vs01_renderer.php as a required file. The split point is a single require_once in vs01.php — the addon entry point remains clean.
// In vs01.php if extraction is needed:
require_once 'addon/vs01/vs01_renderer.php';
This extraction does not require a spec revision — it is an implementation decision the developer makes when the line count requires it.