KB Semantics Phase 2 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax.
Goal: Cluster manual sections that mean the same thing across manuals (local, HDBSCAN over section vectors), and expose the clusters as a /semantics web page in the knowledgebase app + a dashboard tile.
Architecture: Extend /home/levander/kb-vectors/ — pull phase-1 chunk vectors from Qdrant, mean-pool to section vectors, HDBSCAN cluster them, label locally from headings, write clusters.json. Add a read-only /semantics route to the running knowledgebase Flask app and a dashboard tile.
Tech Stack: Python venv (numpy, scikit-learn, hdbscan, qdrant-client — all in the kb-vectors venv), Qdrant manuals collection, the knowledgebase Flask app, the home-portal static dashboard.
Design: 2026-07-24-kb-semantics-phase2-design
Global Constraints
- Code under
/home/levander/kb-vectors/(cluster pipeline) and edits to/home/levander/knowledgebase/app.py+/home/levander/home-portal/index.html. Runs aslevander, NOT root (Qdrant reachable via docker as levander; app edits are levander-owned). - No comments/docstrings/annotations in any code (hard user rule). Self-explanatory naming only.
- Do not run
git commit. Each task ends in verification. - Read-only over Qdrant and the KB docs — must not modify the
manualscollection, the vectors, or any manual file. - Reuse the existing
kb-vectorsvenv; add deps:scikit-learn hdbscan numpy(numpy already present via sentence-transformers). - CPU + thread-capped: prefix heavy python with
OMP_NUM_THREADS=4 MKL_NUM_THREADS=4andnice -n 10(per the PSU-load lesson). Serialize vs other heavy jobs. - Section = one
NNN-*.mdpage = points sharing(manual_id, page)payload. Section vector = L2-normalized mean of its chunk vectors. - HDBSCAN:
metric="euclidean"on unit-norm vectors (≈ cosine),min_cluster_size=2default. Noise label = -1, excluded from clusters but counted. clusters.jsonlives at/home/levander/kb-vectors/clusters.json.- The knowledgebase app currently binds
127.0.0.1:8092and serves the mkdocssite/at/plus/upload,/jobs,/api/jobs. New routes must not shadow the static catch-all/<path:path>— register specific routes (Flask/Werkzeug orders them first, but verify). - Run commands on the box:
/usr/bin/ssh levander@telep-mainframe. On this Mac baresshis broken — always/usr/bin/ssh.
Test command: cd /home/levander/kb-vectors && venv/bin/python -m unittest <module> -v
Task 1: Deps + sectionvecs.py (pull + mean-pool section vectors)
Files:
- Create:
/home/levander/kb-vectors/sectionvecs.py - Test:
/home/levander/kb-vectors/test_sectionvecs.py
Interfaces:
-
Produces:
mean_pool(vectors) -> list[float]— L2-normalized mean of a list of equal-length vectorsgroup_sections(points) -> tuple[list[list[float]], list[dict]]— from a list of{"vector":[...], "payload":{manual_id,folder,page,page_url,heading}}, group by(manual_id, page)(sorted), return(section_vectors, section_meta)where each meta ={manual_id, folder, page, page_url, heading}load_sections(client) -> tuple[list[list[float]], list[dict]]— scroll all points from Qdrantmanuals(with vectors), callgroup_sections
-
Step 1: Install deps
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/pip install -q scikit-learn hdbscan && venv/bin/python -c "import sklearn, hdbscan, numpy; print(\"sklearn\", sklearn.__version__, \"hdbscan ok\", \"numpy\", numpy.__version__)"'Expected: prints versions, no import error. (hdbscan may compile; be patient.)
- Step 2: Write the failing test
Create /home/levander/kb-vectors/test_sectionvecs.py:
import math
import unittest
from sectionvecs import group_sections, mean_pool
def norm(v):
return math.sqrt(sum(x * x for x in v))
class TestMeanPool(unittest.TestCase):
def test_normalized(self):
out = mean_pool([[3.0, 0.0], [0.0, 0.0]])
self.assertAlmostEqual(norm(out), 1.0, places=6)
def test_mean_direction(self):
out = mean_pool([[1.0, 0.0], [0.0, 1.0]])
self.assertAlmostEqual(out[0], out[1], places=6)
class TestGroupSections(unittest.TestCase):
def _points(self):
return [
{"vector": [1.0, 0.0], "payload": {"manual_id": "m/a", "folder": "m", "page": "001-x", "page_url": "m/a/001-x/", "heading": "X"}},
{"vector": [0.0, 1.0], "payload": {"manual_id": "m/a", "folder": "m", "page": "001-x", "page_url": "m/a/001-x/", "heading": "X"}},
{"vector": [1.0, 0.0], "payload": {"manual_id": "m/b", "folder": "m", "page": "002-y", "page_url": "m/b/002-y/", "heading": "Y"}},
]
def test_two_sections(self):
vecs, meta = group_sections(self._points())
self.assertEqual(len(vecs), 2)
self.assertEqual(len(meta), 2)
def test_meta_fields(self):
vecs, meta = group_sections(self._points())
self.assertEqual(meta[0]["manual_id"], "m/a")
self.assertEqual(meta[0]["page_url"], "m/a/001-x/")
self.assertEqual(meta[0]["heading"], "X")
def test_pooled_normalized(self):
vecs, meta = group_sections(self._points())
self.assertAlmostEqual(norm(vecs[0]), 1.0, places=6)
def test_sorted_deterministic(self):
vecs, meta = group_sections(self._points())
self.assertEqual([m["manual_id"] for m in meta], ["m/a", "m/b"])
if __name__ == "__main__":
unittest.main()- Step 3: Run the test to verify it fails
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/python -m unittest test_sectionvecs -v'Expected: FAIL with ModuleNotFoundError: No module named 'sectionvecs'
- Step 4: Write the implementation
Create /home/levander/kb-vectors/sectionvecs.py:
import math
from index import COLLECTION
def mean_pool(vectors):
dim = len(vectors[0])
total = [0.0] * dim
for vector in vectors:
for i in range(dim):
total[i] += vector[i]
mean = [x / len(vectors) for x in total]
length = math.sqrt(sum(x * x for x in mean))
if length == 0:
return mean
return [x / length for x in mean]
def group_sections(points):
groups = {}
order = []
for point in points:
payload = point["payload"]
key = (payload["manual_id"], payload["page"])
if key not in groups:
groups[key] = {"vectors": [], "meta": {
"manual_id": payload["manual_id"],
"folder": payload.get("folder", payload["manual_id"].split("/")[0]),
"page": payload["page"],
"page_url": payload["page_url"],
"heading": payload.get("heading", ""),
}}
order.append(key)
groups[key]["vectors"].append(point["vector"])
order.sort()
section_vectors = []
section_meta = []
for key in order:
section_vectors.append(mean_pool(groups[key]["vectors"]))
section_meta.append(groups[key]["meta"])
return section_vectors, section_meta
def load_sections(client):
points = []
offset = None
while True:
batch, offset = client.scroll(
COLLECTION, limit=512, offset=offset, with_vectors=True, with_payload=True
)
for record in batch:
points.append({"vector": list(record.vector), "payload": dict(record.payload)})
if offset is None:
break
return group_sections(points)- Step 5: Run the test to verify it passes
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/python -m unittest test_sectionvecs -v'Expected: OK — 6 tests pass.
- Step 6: Verify against the real corpus
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && OMP_NUM_THREADS=4 venv/bin/python -c "
from index import get_client
from sectionvecs import load_sections
v, m = load_sections(get_client())
print(\"sections:\", len(v), \"| dim:\", len(v[0]))
print(\"distinct manuals:\", len(set(x[\"manual_id\"] for x in m)))
"'Expected: a few hundred sections, dim 1024, ~15+ distinct manuals.
Task 2: labels.py (representative pick + local label)
Files:
- Create:
/home/levander/kb-vectors/labels.py - Test:
/home/levander/kb-vectors/test_labels.py
Interfaces:
-
Consumes: nothing
-
Produces:
representative_index(member_indices, vectors) -> int— the member index whose vector is nearest (max dot product) to the members’ normalized centroidmake_label(rep_meta) -> str—rep_meta["heading"]trimmed; if empty, a prettifiedpage(stripNNN-prefix, hyphens→spaces, title-ish)
-
Step 1: Write the failing test
Create /home/levander/kb-vectors/test_labels.py:
import unittest
from labels import make_label, representative_index
class TestRepresentative(unittest.TestCase):
def test_picks_central_member(self):
vectors = [[1.0, 0.0], [0.9, 0.1], [-1.0, 0.0]]
idx = representative_index([0, 1], vectors)
self.assertIn(idx, [0, 1])
def test_central_of_three(self):
vectors = [[1.0, 0.0], [0.0, 1.0], [0.71, 0.71]]
idx = representative_index([0, 1, 2], vectors)
self.assertEqual(idx, 2)
class TestMakeLabel(unittest.TestCase):
def test_heading_used(self):
self.assertEqual(make_label({"heading": "Rear Axle", "page": "007-rear-axle"}), "Rear Axle")
def test_empty_heading_fallback_to_page(self):
self.assertEqual(make_label({"heading": "", "page": "007-rear-axle-service"}), "Rear Axle Service")
def test_strips_numeric_prefix(self):
self.assertEqual(make_label({"heading": " ", "page": "012-brake-bleeding"}), "Brake Bleeding")
if __name__ == "__main__":
unittest.main()- Step 2: Run the test to verify it fails
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/python -m unittest test_labels -v'Expected: FAIL with ModuleNotFoundError: No module named 'labels'
- Step 3: Write the implementation
Create /home/levander/kb-vectors/labels.py:
import math
import re
_PREFIX = re.compile(r"^\d+-")
def representative_index(member_indices, vectors):
dim = len(vectors[member_indices[0]])
centroid = [0.0] * dim
for i in member_indices:
for d in range(dim):
centroid[d] += vectors[i][d]
length = math.sqrt(sum(x * x for x in centroid))
if length:
centroid = [x / length for x in centroid]
best = member_indices[0]
best_score = None
for i in member_indices:
score = sum(vectors[i][d] * centroid[d] for d in range(dim))
if best_score is None or score > best_score:
best_score = score
best = i
return best
def make_label(rep_meta):
heading = (rep_meta.get("heading") or "").strip()
if heading:
return heading
page = _PREFIX.sub("", rep_meta.get("page", ""))
return " ".join(word.capitalize() for word in page.replace("-", " ").split())- Step 4: Run the test to verify it passes
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/python -m unittest test_labels -v'Expected: OK — 5 tests pass.
Task 3: cluster.py + kbclust.py (HDBSCAN + artifact + CLI)
Files:
- Create:
/home/levander/kb-vectors/cluster.py - Create:
/home/levander/kb-vectors/kbclust.py - Test:
/home/levander/kb-vectors/test_cluster.py
Interfaces:
-
Consumes:
sectionvecs.load_sections,labels.representative_index,labels.make_label -
Produces:
build_clusters(vectors, meta, min_cluster_size=2) -> tuple[list[dict], int]— returns(clusters, noise_count); each cluster{id, label, size, span, source_manuals, members:[{manual_id, page_url, heading}]}, sorted byspandesc thensizedesc,id= position in that orderwrite_clusters(clusters, path)/load_clusters(path) -> list[dict]CLUSTERS_PATH = "/home/levander/kb-vectors/clusters.json"
-
Step 1: Write the failing test
Create /home/levander/kb-vectors/test_cluster.py:
import unittest
from cluster import build_clusters
def meta(manual_id, page, heading):
return {"manual_id": manual_id, "folder": manual_id.split("/")[0], "page": page, "page_url": manual_id + "/" + page + "/", "heading": heading}
class TestBuildClusters(unittest.TestCase):
def _data(self):
vectors = [
[1.0, 0.0, 0.0], [0.99, 0.01, 0.0], [0.98, 0.02, 0.0],
[0.0, 1.0, 0.0], [0.0, 0.99, 0.01],
]
metas = [
meta("vitara/workshop", "001-diff", "Differential"),
meta("chevy/geo", "005-diff", "Rear axle"),
meta("kick-fix/axles", "001-axle", "Axle"),
meta("vitara/workshop", "010-brake", "Brakes"),
meta("chevy/geo", "020-brake", "Brake bleed"),
]
return vectors, metas
def test_forms_clusters(self):
vectors, metas = self._data()
clusters, noise = build_clusters(vectors, metas, min_cluster_size=2)
self.assertGreaterEqual(len(clusters), 2)
def test_cross_manual_span(self):
vectors, metas = self._data()
clusters, noise = build_clusters(vectors, metas, min_cluster_size=2)
diff = [c for c in clusters if any("diff" in m["page_url"] or "axle" in m["page_url"] for m in c["members"])][0]
self.assertGreaterEqual(diff["span"], 2)
self.assertEqual(diff["span"], len(diff["source_manuals"]))
def test_sorted_by_span(self):
vectors, metas = self._data()
clusters, noise = build_clusters(vectors, metas, min_cluster_size=2)
spans = [c["span"] for c in clusters]
self.assertEqual(spans, sorted(spans, reverse=True))
def test_ids_sequential(self):
vectors, metas = self._data()
clusters, noise = build_clusters(vectors, metas, min_cluster_size=2)
self.assertEqual([c["id"] for c in clusters], list(range(len(clusters))))
def test_member_shape(self):
vectors, metas = self._data()
clusters, noise = build_clusters(vectors, metas, min_cluster_size=2)
m = clusters[0]["members"][0]
self.assertEqual(set(m.keys()), {"manual_id", "page_url", "heading"})
if __name__ == "__main__":
unittest.main()- Step 2: Run the test to verify it fails
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/python -m unittest test_cluster -v'Expected: FAIL with ModuleNotFoundError: No module named 'cluster'
- Step 3: Write cluster.py
Create /home/levander/kb-vectors/cluster.py:
import json
import hdbscan
import numpy as np
from labels import make_label, representative_index
CLUSTERS_PATH = "/home/levander/kb-vectors/clusters.json"
def build_clusters(vectors, meta, min_cluster_size=2):
array = np.array(vectors, dtype=np.float64)
labeler = hdbscan.HDBSCAN(min_cluster_size=min_cluster_size, min_samples=1, metric="euclidean")
assignments = labeler.fit_predict(array)
noise_count = int((assignments == -1).sum())
by_label = {}
for i, label in enumerate(assignments):
if label == -1:
continue
by_label.setdefault(int(label), []).append(i)
clusters = []
for members in by_label.values():
rep = representative_index(members, vectors)
source_manuals = sorted(set(meta[i]["manual_id"] for i in members))
clusters.append({
"label": make_label(meta[rep]),
"size": len(members),
"span": len(source_manuals),
"source_manuals": source_manuals,
"members": [
{"manual_id": meta[i]["manual_id"], "page_url": meta[i]["page_url"], "heading": meta[i]["heading"]}
for i in members
],
})
clusters.sort(key=lambda c: (c["span"], c["size"]), reverse=True)
for position, cluster in enumerate(clusters):
cluster["id"] = position
return clusters, noise_count
def write_clusters(clusters, path=CLUSTERS_PATH):
with open(path, "w", encoding="utf-8") as handle:
json.dump({"clusters": clusters}, handle, ensure_ascii=False, indent=1)
def load_clusters(path=CLUSTERS_PATH):
try:
with open(path, encoding="utf-8") as handle:
return json.load(handle).get("clusters", [])
except (OSError, ValueError):
return []- Step 4: Run the test to verify it passes
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && venv/bin/python -m unittest test_cluster -v'Expected: OK — 5 tests pass.
- Step 5: Write kbclust.py
Create /home/levander/kb-vectors/kbclust.py:
import argparse
from cluster import CLUSTERS_PATH, build_clusters, load_clusters, write_clusters
from index import get_client
from sectionvecs import load_sections
def main():
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
build_parser = sub.add_parser("build")
build_parser.add_argument("--min-cluster-size", type=int, default=2)
sub.add_parser("list")
show_parser = sub.add_parser("show")
show_parser.add_argument("id", type=int)
args = parser.parse_args()
if args.command == "build":
vectors, meta = load_sections(get_client())
clusters, noise = build_clusters(vectors, meta, min_cluster_size=args.min_cluster_size)
write_clusters(clusters)
cross = sum(1 for c in clusters if c["span"] >= 2)
print("sections:", len(vectors), "clusters:", len(clusters), "cross-manual:", cross, "noise:", noise)
print("written:", CLUSTERS_PATH)
elif args.command == "list":
for c in load_clusters():
print("#%d span=%d size=%d %s" % (c["id"], c["span"], c["size"], c["label"]))
elif args.command == "show":
clusters = load_clusters()
match = [c for c in clusters if c["id"] == args.id]
if not match:
print("no cluster", args.id)
return
c = match[0]
print("#%d %s (span %d)" % (c["id"], c["label"], c["span"]))
for m in c["members"]:
print(" %-34s %s | %s" % (m["manual_id"], m["page_url"], m["heading"][:40]))
if __name__ == "__main__":
main()- Step 6: Build clusters on the real corpus + quality proof
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && OMP_NUM_THREADS=4 MKL_NUM_THREADS=4 nice -n 10 venv/bin/python kbclust.py build && echo "=== top cross-manual clusters ===" && venv/bin/python kbclust.py list | head -20'Expected: a few hundred sections, a non-trivial cluster count with several cross-manual (span ≥ 2), a small noise count; the top of list shows high-span clusters (the dedup targets).
- Step 7: Inspect a known-overlap cluster
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/kb-vectors && for id in $(venv/bin/python kbclust.py list | grep -iE "diff|axle|brake|oxygen|timing" | head -3 | grep -oE "^#[0-9]+" | tr -d "#"); do echo "--- cluster $id ---"; venv/bin/python kbclust.py show $id; done'Expected: at least one cluster whose members span multiple manuals for the same topic (e.g. a differential/axle cluster with workshop + geo-tracker + kick-fix/axles). This is the real proof the clustering is meaningful.
Task 4: /semantics + /api/clusters routes in the knowledgebase app
Files:
- Modify:
/home/levander/knowledgebase/app.py - Test:
/home/levander/knowledgebase/test_semantics.py
Interfaces:
-
Consumes:
clusters.jsonat/home/levander/kb-vectors/clusters.json -
Produces:
GET /api/clusters(JSON),GET /semantics(HTML) -
Step 1: Write the failing test
Create /home/levander/knowledgebase/test_semantics.py:
import json
import os
import unittest
import app as kbapp
class TestSemantics(unittest.TestCase):
def setUp(self):
self.client = kbapp.app.test_client()
self.sample = {"clusters": [
{"id": 0, "label": "Differential", "size": 3, "span": 2,
"source_manuals": ["chevy/geo", "vitara/workshop"],
"members": [{"manual_id": "vitara/workshop", "page_url": "vitara/workshop/001-diff/", "heading": "Differential"}]}
]}
os.makedirs(os.path.dirname(kbapp.CLUSTERS_PATH), exist_ok=True)
self._backup = None
if os.path.exists(kbapp.CLUSTERS_PATH):
with open(kbapp.CLUSTERS_PATH) as handle:
self._backup = handle.read()
with open(kbapp.CLUSTERS_PATH, "w") as handle:
json.dump(self.sample, handle)
def tearDown(self):
if self._backup is not None:
with open(kbapp.CLUSTERS_PATH, "w") as handle:
handle.write(self._backup)
def test_api_returns_clusters(self):
response = self.client.get("/api/clusters")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get_json()["clusters"][0]["label"], "Differential")
def test_semantics_page_renders(self):
response = self.client.get("/semantics")
self.assertEqual(response.status_code, 200)
self.assertIn(b"Differential", response.data)
self.assertIn(b"vitara/workshop/001-diff/", response.data)
def test_semantics_ok_when_missing(self):
os.remove(kbapp.CLUSTERS_PATH)
response = self.client.get("/semantics")
self.assertEqual(response.status_code, 200)
if __name__ == "__main__":
unittest.main()- Step 2: Run the test to verify it fails
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/knowledgebase && venv/bin/python -m unittest test_semantics -v'Expected: FAIL — AttributeError: module 'app' has no attribute 'CLUSTERS_PATH' (or route 404).
- Step 3: Add the routes to app.py
In /home/levander/knowledgebase/app.py, add near the top (after the existing imports and app = Flask(__name__) config block):
import json as _json
CLUSTERS_PATH = "/home/levander/kb-vectors/clusters.json"
SEMANTICS_HTML = """<!doctype html><html><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1"><title>Szemantika</title>
<style>body{font-family:system-ui;max-width:60em;margin:1.5em auto;padding:0 1em;background:#0f1115;color:#e6e8ee}
a{color:#4f8cff}.c{border:1px solid #2a2f3a;border-radius:12px;padding:.8em 1em;margin:.7em 0;background:#1a1d24}
.h{font-weight:600;font-size:1.05em}.s{color:#9aa3b2;font-size:.85em;margin:.2em 0 .5em}
.m{font-size:.9em;margin:.15em 0}.b{display:inline-block;background:#2f6bff;color:#fff;border-radius:8px;padding:0 .5em;font-size:.8em;margin-left:.4em}</style>
</head><body><h1>Szemantika — kapcsolódó szakaszok</h1>
<p class=s>{{summary}}</p>{% for c in clusters %}<div class=c>
<div class=h>{{c.label}}<span class=b>{{c.span}} forrás</span></div>
<div class=s>{{c.source_manuals|join(' · ')}}</div>
{% for m in c.members %}<div class=m><a href="/{{m.page_url}}">{{m.manual_id}}</a> — {{m.heading}}</div>{% endfor %}
</div>{% endfor %}{% if not clusters %}<p>Nincs klaszter. Futtasd: <code>kbclust.py build</code></p>{% endif %}
<p><a href="/">← Vissza a tudásbázishoz</a></p></body></html>"""
def _load_clusters():
try:
with open(CLUSTERS_PATH, encoding="utf-8") as handle:
return _json.load(handle).get("clusters", [])
except (OSError, ValueError):
return []Then add the two routes (place them BEFORE the catch-all /<path:path> site route):
@app.route("/api/clusters")
def api_clusters():
return jsonify({"clusters": _load_clusters()})
@app.route("/semantics")
def semantics():
clusters = _load_clusters()
cross = sum(1 for c in clusters if c.get("span", 0) >= 2)
summary = "%d klaszter, ebből %d több kézikönyvet érint" % (len(clusters), cross)
return render_template_string(SEMANTICS_HTML, clusters=clusters, summary=summary)(jsonify and render_template_string are already imported in app.py — verify; if not, add them to the existing flask import line.)
- Step 4: Run the test to verify it passes
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/knowledgebase && venv/bin/python -m unittest test_semantics -v'Expected: OK — 3 tests pass.
- Step 5: Full knowledgebase suite still green (no route shadowing/regression)
/usr/bin/ssh levander@telep-mainframe 'cd /home/levander/knowledgebase && venv/bin/python -m unittest discover -p "test_*.py" 2>&1 | tail -3'Expected: OK — all prior app tests still pass (26+) plus the 3 new ones.
- Step 6: Restart the service + verify live over the tailnet
/usr/bin/ssh levander@telep-mainframe 'sudo systemctl restart knowledgebase && sleep 4 && systemctl is-active knowledgebase'
curl -s -o /dev/null -w "semantics=%{http_code}\n" --max-time 15 "https://knowledgebase.taild4189d.ts.net/semantics"
curl -s --max-time 15 "https://knowledgebase.taild4189d.ts.net/api/clusters" | head -c 200; echo
curl -s --max-time 15 "https://knowledgebase.taild4189d.ts.net/semantics" | grep -oE "forrás|klaszter" | head -1Expected: semantics=200, the api returns the clusters JSON, and the page contains the Hungarian labels. Confirm a member link (e.g. copy a page_url from the api) resolves 200.
Task 5: Dashboard “Szemantika” tile
Files:
-
Modify:
/home/levander/home-portal/index.html -
Step 1: Add the tile
Add this tile into the <main> grid of /home/levander/home-portal/index.html, after the Tudásbázis tile:
<a class="tile" href="https://knowledgebase.taild4189d.ts.net/semantics">
<span class="icon">🧭</span>
<span class="meta"><span class="name">Szemantika</span><span class="desc">Kapcsolódó szakaszok kézikönyvek között</span></span>
</a>Apply it:
/usr/bin/ssh levander@telep-mainframe 'python3 - <<PY
path="/home/levander/home-portal/index.html"
html=open(path,encoding="utf-8").read()
tile=(
" <a class=\"tile\" href=\"https://knowledgebase.taild4189d.ts.net/semantics\">\n"
" <span class=\"icon\">🧭</span>\n"
" <span class=\"meta\"><span class=\"name\">Szemantika</span><span class=\"desc\">Kapcsolódó szakaszok kézikönyvek között</span></span>\n"
" </a>\n"
)
anchor="https://knowledgebase.taild4189d.ts.net\">"
if "Szemantika" not in html:
idx=html.index("</a>", html.index("Tudásbázis"))+len("</a>\n")
html=html[:idx]+tile+html[idx:]
open(path,"w",encoding="utf-8").write(html)
print("tile added")
else:
print("tile already present")
PY'- Step 2: Verify the tile is live
curl -s --max-time 15 "https://home.taild4189d.ts.net/" | grep -oE "Szemantika" | head -1Expected: Szemantika (the static server serves the updated file directly; no restart needed).
- Step 3: End-to-end click-path check
echo "dashboard -> semantics -> a KB section:"
curl -s -o /dev/null -w "dashboard=%{http_code}\n" --max-time 15 "https://home.taild4189d.ts.net/"
curl -s -o /dev/null -w "semantics=%{http_code}\n" --max-time 15 "https://knowledgebase.taild4189d.ts.net/semantics"
MEMBER=$(curl -s --max-time 15 "https://knowledgebase.taild4189d.ts.net/api/clusters" | python3 -c "import json,sys; c=json.load(sys.stdin)['clusters']; print(c[0]['members'][0]['page_url'] if c else '')" 2>/dev/null)
echo "first member: $MEMBER"
[ -n "$MEMBER" ] && curl -s -o /dev/null -w "member-page=%{http_code}\n" --max-time 15 "https://knowledgebase.taild4189d.ts.net/$MEMBER"Expected: all 200 — the dashboard tile opens /semantics, and a cluster member links to a real KB section page.
Self-review notes
Spec coverage: section unit via (manual_id,page) grouping + mean-pool (Task 1); HDBSCAN min_cluster_size=2 with noise bucket (Task 3); local heading label + representative pick, pluggable (Task 2); cross-manual span ranking + clusters.json (Task 3); /semantics + /api/clusters on the existing app, no new service (Task 4); dashboard tile (Task 5); read-only, thread-capped, re-runnable (Global Constraints, verified in Task 3 build); quality proof that known-overlap topics cluster cross-manual (Task 3 Step 7).
Deliberate deviations: git commit steps omitted (user’s rule). Unit tests are on the pure logic (mean_pool, group_sections, representative_index, make_label, build_clusters on synthetic vectors); the real HDBSCAN-on-corpus quality is proven by the Task 3 build + inspect steps (not mockable meaningfully). CLI list/show are exercised in Task 3 Steps 6-7 rather than separate unit tests (thin argparse wrappers over tested functions).