../panel/data/master_architecture_index.md
../panel/data/panel_manifest.json
../panel/data/panel_content_index.json
../panel/data/project_scope_canonical.json
-
../panel/data/devon_panel_chat_checkpoint.md
+
../panel/data/devon_continuity.md
../panel/data/cas.md
../panel/data/cgs.md
@@ -834,17 +868,30 @@
let HUB = null;
async function loadHub() {
- const res = await fetch('../panel/data/hub_index.json');
- if (!res.ok) throw new Error('hub load fail');
- HUB = await res.json();
+ if (HUB) return HUB;
+
+ const [hubRes, treeRes] = await Promise.all([
+ fetch('../panel/data/hub_index.json', { cache: 'no-store' }),
+ fetch('../panel/data/panel_canonical_tree.json', { cache: 'no-store' })
+ ]);
+
+ if (!hubRes.ok) throw new Error('hub load fail');
+ if (!treeRes.ok) throw new Error('canonical tree load fail');
+
+ const [hubIndex, canonicalTree] = await Promise.all([
+ hubRes.json(),
+ treeRes.json()
+ ]);
+
+ HUB = {
+ index: hubIndex,
+ tree: canonicalTree
+ };
+
+ return HUB;
}
-const state = {
- phaseId: HUB.phases[0].id,
- categoryId: HUB.categories[0].id,
- docId: HUB.categories[0].docs[0].id,
- mode: "architecture"
- };
+let state = { phaseId: null, categoryId: null, docId: null, selectedSubcategory: null, mode: "architecture" };
const treeRoot = document.getElementById("tree-root");
const phaseStrip = document.getElementById("phase-strip");
@@ -862,37 +909,138 @@
const docMap = document.getElementById("doc-map");
const docViewer = document.getElementById("doc-viewer");
const relationMap = document.getElementById("relation-map");
+ const docCompletenessSummary = document.getElementById("doc-completeness-summary");
+ const docCompletenessList = document.getElementById("doc-completeness-list");
const btnViewArchitecture = document.getElementById("btn-view-architecture");
const btnViewContract = document.getElementById("btn-view-contract");
const btnViewSource = document.getElementById("btn-view-source");
+ function hubIndex(){
+ return HUB && HUB.index ? HUB.index : { phases: [], categories: [] };
+ }
- function getCategory(categoryId){
- return HUB.categories.find(c => c.id === categoryId);
- }
+ function hubTree(){
+ return HUB && HUB.tree ? HUB.tree : { phases: [] };
+ }
- function getDoc(categoryId, docId){
- const category = getCategory(categoryId);
- return category ? category.docs.find(d => d.id === docId) : null;
- }
+ function getCategory(categoryId){
+ return (hubIndex().categories || []).find(c => c.id === categoryId) || null;
+ }
+
+ function getDoc(categoryId, docId){
+ const category = getCategory(categoryId);
+ return category ? ((category.docs || []).find(d => d.id === docId) || null) : null;
+ }
+
+ function getPhase(phaseId){
+ return (hubIndex().phases || []).find(p => p.id === phaseId) || null;
+ }
+
+ function getTreePhase(phaseId){
+ return (hubTree().phases || []).find(p => p.id === phaseId) || null;
+ }
+
+ function getTreeCategory(phaseId, categoryId){
+ const phase = getTreePhase(phaseId);
+ return phase ? ((phase.categories || []).find(c => c.id === categoryId) || null) : null;
+ }
+
+ function getSubcategories(phaseId, categoryId){
+ const category = getTreeCategory(phaseId, categoryId);
+ return category && Array.isArray(category.subcategories) ? category.subcategories : [];
+ }
+
+ function getSelectedSubcategory(){
+ const subcategories = getSubcategories(state.phaseId, state.categoryId);
+ if (!subcategories.length) return null;
+ if (state.selectedSubcategory) {
+ const hit = subcategories.find(s => s.id === state.selectedSubcategory);
+ if (hit) return hit;
+ }
+ return subcategories[0];
+ }
- function getPhase(phaseId){
- return HUB.phases.find(p => p.id === phaseId);
- }
function metricData(){
- const totalCategories = HUB.categories.length;
- const totalDocs = HUB.categories.reduce((acc, cat) => acc + cat.docs.length, 0);
- const totalPhases = HUB.phases.length;
+ const totalCategories = hubIndex().categories.length;
+ const totalDocs = hubIndex().categories.reduce((acc, cat) => acc + ((cat.docs || []).length), 0);
+ const totalPhases = hubIndex().phases.length;
const totalSources = totalDocs;
+ const categoriesWithGaps = hubIndex().categories.filter(cat => !Array.isArray(cat.docs) || !cat.docs.length).length;
return [
{ value: totalPhases, label: "Phases", note: "macro build stages" },
{ value: totalCategories, label: "Categories", note: "navigation domains" },
{ value: totalDocs, label: "Documents", note: "canonical source set" },
- { value: totalSources, label: "Mapped Sources", note: "doc-to-source bindings" }
+ { value: totalSources, label: "Mapped Sources", note: "doc-to-source bindings" },
+ { value: totalCategories - categoriesWithGaps, label: "Ready Categories", note: "categories with mapped docs" }
];
}
+ function documentationCompletenessSnapshot(){
+ const categories = Array.isArray(hubIndex().categories) ? hubIndex().categories : [];
+ const phases = Array.isArray(hubIndex().phases) ? hubIndex().phases : [];
+ const docs = categories.reduce((acc, cat) => acc + (Array.isArray(cat.docs) ? cat.docs.length : 0), 0);
+
+ const categoriesWithDocs = categories.filter(cat => Array.isArray(cat.docs) && cat.docs.length > 0);
+ const missingCategoryDocs = categories.filter(cat => !Array.isArray(cat.docs) || cat.docs.length === 0);
+
+ const docsMissingPhase = [];
+ const docsMissingDepends = [];
+ const docsMissingUsedBy = [];
+
+ categories.forEach(cat => {
+ (Array.isArray(cat.docs) ? cat.docs : []).forEach(doc => {
+ if (!doc.phase) docsMissingPhase.push(doc.label || doc.id || "unnamed-doc");
+ if (!Array.isArray(doc.depends_on) || doc.depends_on.length === 0) docsMissingDepends.push(doc.label || doc.id || "unnamed-doc");
+ if (!Array.isArray(doc.used_by) || doc.used_by.length === 0) docsMissingUsedBy.push(doc.label || doc.id || "unnamed-doc");
+ });
+ });
+
+ return {
+ totalPhases: phases.length,
+ totalCategories: categories.length,
+ totalDocs: docs,
+ categoriesWithDocs: categoriesWithDocs.length,
+ missingCategoryDocs,
+ docsMissingPhase,
+ docsMissingDepends,
+ docsMissingUsedBy
+ };
+ }
+
+ function buildDocumentationCompleteness(){
+ if (!docCompletenessSummary || !docCompletenessList) return;
+
+ const snap = documentationCompletenessSnapshot();
+ const categoriesWithoutDocs = snap.missingCategoryDocs.length;
+ const docsMissingPhase = snap.docsMissingPhase.length;
+ const docsMissingDepends = snap.docsMissingDepends.length;
+ const docsMissingUsedBy = snap.docsMissingUsedBy.length;
+
+ const categoriesReady = snap.totalCategories - categoriesWithoutDocs;
+ const totalGapCount = categoriesWithoutDocs + docsMissingPhase + docsMissingDepends + docsMissingUsedBy;
+
+ docCompletenessSummary.textContent =
+ totalGapCount === 0
+ ? "Structural coverage is complete for the current Hub surface."
+ : "Structural coverage is active, but mapped gaps still exist and are listed below.";
+
+ docCompletenessList.innerHTML = "";
+
+ [
+ { title: "Covered categories", text: `${categoriesReady} / ${snap.totalCategories} categories with mapped docs.` },
+ { title: "Category gaps", text: categoriesWithoutDocs ? `${categoriesWithoutDocs} categories still have no mapped docs.` : "No category-level doc gaps detected." },
+ { title: "Phase binding gaps", text: docsMissingPhase ? `${docsMissingPhase} docs still have no explicit phase binding.` : "No phase-binding gaps detected." },
+ { title: "Dependency gaps", text: docsMissingDepends ? `${docsMissingDepends} docs still have no depends_on mapping.` : "No depends_on gaps detected." },
+ { title: "Usage gaps", text: docsMissingUsedBy ? `${docsMissingUsedBy} docs still have no used_by mapping.` : "No used_by gaps detected." }
+ ].forEach(item => {
+ const el = document.createElement("div");
+ el.className = "mini-item";
+ el.innerHTML = `
${item.title}${item.text}`;
+ docCompletenessList.appendChild(el);
+ });
+ }
+
function buildMetrics(){
metricGrid.innerHTML = "";
metricData().forEach(item => {
@@ -906,10 +1054,9 @@
metricGrid.appendChild(el);
});
}
-
function buildPhases(){
phaseStrip.innerHTML = "";
- HUB.phases.forEach(phase => {
+ (hubIndex().phases || []).forEach(phase => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "phase" + (phase.id === state.phaseId ? " active" : "");
@@ -921,68 +1068,86 @@
btn.title = phase.summary;
btn.addEventListener("click", () => {
state.phaseId = phase.id;
- const matchingCategory = HUB.categories.find(cat => cat.docs.some(doc => doc.phase === phase.id));
- if (matchingCategory) {
- state.categoryId = matchingCategory.id;
- state.docId = matchingCategory.docs[0].id;
+
+ const phaseTree = getTreePhase(phase.id);
+ const firstCategory = phaseTree && Array.isArray(phaseTree.categories)
+ ? phaseTree.categories[0]
+ : null;
+
+ if (firstCategory) {
+ state.categoryId = firstCategory.id;
+ state.selectedSubcategory = null;
}
+
+ state.docId = null;
render();
});
phaseStrip.appendChild(btn);
});
}
-
function buildTree(){
treeRoot.innerHTML = "";
- HUB.categories.forEach(cat => {
- const node = document.createElement("div");
- const isOpen = cat.id === state.categoryId;
- node.className = "tree-node" + (isOpen ? " open" : "");
-
- const head = document.createElement("div");
- head.className = "tree-head";
- head.innerHTML = `
+
+ (hubIndex().phases || []).forEach(phase => {
+ const phaseNode = document.createElement("div");
+ const phaseOpen = phase.id === state.phaseId;
+ phaseNode.className = "tree-node" + (phaseOpen ? " open" : "");
+
+ const phaseHead = document.createElement("div");
+ phaseHead.className = "tree-head";
+ phaseHead.innerHTML = `
-
${cat.title}
-
${cat.sub}
+
${phase.name}
+
${phase.summary || ""}
-
${cat.badge}
+
${phase.badge || phase.step || ""}
`;
- head.addEventListener("click", () => {
- state.categoryId = cat.id;
- const firstDoc = cat.docs[0];
- if (firstDoc) {
- state.docId = firstDoc.id;
- state.phaseId = firstDoc.phase;
+ phaseHead.addEventListener("click", () => {
+ state.phaseId = phase.id;
+
+ const phaseTree = getTreePhase(phase.id);
+ const firstCategory = phaseTree && Array.isArray(phaseTree.categories)
+ ? phaseTree.categories[0]
+ : null;
+
+ if (firstCategory) {
+ state.categoryId = firstCategory.id;
+ state.selectedSubcategory = null;
}
+
+ state.docId = null;
render();
});
- const children = document.createElement("div");
- children.className = "tree-children";
+ const phaseChildren = document.createElement("div");
+ phaseChildren.className = "tree-children";
- cat.docs.forEach(doc => {
+ const phaseTree = getTreePhase(phase.id);
+ (phaseTree && Array.isArray(phaseTree.categories) ? phaseTree.categories : []).forEach(catTree => {
+ const catMeta = getCategory(catTree.id) || {};
const btn = document.createElement("button");
btn.type = "button";
- btn.className = "tree-doc" + (doc.id === state.docId ? " active" : "");
- btn.innerHTML = `${doc.label}
${doc.layer} • ${doc.type}`;
+ btn.className = "tree-doc" + (catTree.id === state.categoryId ? " active" : "");
+ btn.innerHTML = `${catMeta.title || catTree.id}
${catMeta.sub || "category"}`;
btn.addEventListener("click", (ev) => {
ev.stopPropagation();
- state.categoryId = cat.id;
- state.docId = doc.id;
- state.phaseId = doc.phase;
+ state.phaseId = phase.id;
+ state.categoryId = catTree.id;
+ state.selectedSubcategory = null;
+ state.docId = null;
render();
});
- children.appendChild(btn);
+ phaseChildren.appendChild(btn);
});
- node.appendChild(head);
- node.appendChild(children);
- treeRoot.appendChild(node);
+ phaseNode.appendChild(phaseHead);
+ phaseNode.appendChild(phaseChildren);
+ treeRoot.appendChild(phaseNode);
});
}
function buildPhaseMap(){
+
const phase = getPhase(state.phaseId);
if (!phase) return;
@@ -1225,16 +1390,37 @@
};
}
- function render(){
+
+function init(){
+ state.phaseId = HUB.phases[0].id;
+ state.categoryId = HUB.categories[0].id;
+ state.docId = HUB.categories[0].docs[0].id;
+
+ bindModes();
+ render();
+}
+
+function render(){
buildTree();
buildPhases();
buildMetrics();
+ buildDocumentationCompleteness();
buildPhaseMap();
renderDocViewer();
}
- bindModes();
- render();
-
+
+
+(async () => {
+ try {
+ await loadHub();
+ init();
+ } catch (e) {
+ console.error("BOOT_FAIL", e);
+ document.body.innerHTML = "
Failed to load HUB
";
+ }
+})();
+
+