<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
 xmlns:content="http://purl.org/rss/1.0/modules/content/"
 xmlns:wfw="http://wellformedweb.org/CommentAPI/"
 xmlns:dc="http://purl.org/dc/elements/1.1/"
 xmlns:atom="http://www.w3.org/2005/Atom"
 xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
 xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
 xmlns:georss="http://www.georss.org/georss"
 xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
 xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<atom:link href="https://blog.tanminhho.com/posts/rss.xml" rel="self" type="application/rss+xml" />
<title>All Posts</title>
<link>https://blog.tanminhho.com/posts</link>
<description><![CDATA[All posts from blog.tanminhho.com]]></description>
<language>en</language>
<lastBuildDate>Fri, 03 Jul 2026 11:17:56 +0200</lastBuildDate>
<generator>Emacs 30.2 org-publish-rss.el 0.8</generator>
<item>
<title>Part 1 - Type text, robot engraves it</title>
<link>https://blog.tanminhho.com/posts/EngraVis/20260617-type_text_robot_engraves_it.html</link>
<pubDate>Wed, 24 Jun 2026 11:44:13 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/EngraVis/20260617-type_text_robot_engraves_it.html</guid>
<description>
<![CDATA[<p>
In the winter term 2025/2026, I worked on a semester-long university project that became the foundation of EngaVis: let
a user type text on a PC and have a KUKA robot engrave or draw it onto parts at defined positions. When I first scoped
the problem, two solutions came to my mind.
</p>

<p>
The first one was to write a dedicated program for each character in the alphabet, which is simple in concept, but
immediately problematic. I&rsquo;d need to write around 70-90 programs just to cover basic characters. Each one would also
need to accept position parameters to offset coordinates, otherwise I could only draw at a fixed location. On top of
that, character size would also be hardcoded, requiring separate programs for every size variant.
</p>

<p>
While I could perform all these transformations inside KRL (KUKA Robot Language), but KRL is a limited language designed
primarily for telling the robot to move from point A to point B and interact with tools. My gut told me this path would
become an unmaintainable mess fast.
</p>

<p>
This led me to the second solution: vectorise all characters, calculate their positions, and transform (e.g. translate,
scale) these coordinates programmatically, then send the resulting motion commands to the robot. To make the second
approach work, I used a KUKA add-on package called EKI (Ethernet KRL Interface). EKI allows an external computer to send
motion commands directly to the KUKA controller over a standard network connection. The robot side simply reads the
incoming data and executes the commands.
</p>

<p>
This splits the system into two subsystems:
</p>

<ul class="org-ul">
<li>The command generator: Takes the user&rsquo;s input text, vectorises the characters, and derives all motion commands (such
as LIN for linear movement and PTP for point-to-point movement).</li>
<li>The command sender: Transmits those commands to the robot controller via TCP, which EKI supports natively.</li>
</ul>

<p>
EKI supports either XML or binary data formats. After weighing the options, I chose binary for two reasons. First,
binary is more compact, therefore, I could pack more commands into a single packet. Second, and more importantly, since
the command generator is independent of any KUKA-specific format, it could be reused with robots from other
manufacturers. After some research, I found that binary is widely supported across the industry, making it the obvious
choice for a vendor-agnostic design.
</p>

<p>
However, there was one remaining problem: I couldn&rsquo;t simply append commands one after another into a raw binary stream.
Doing so would make future extensions (new command types, new robot integrations) brittle and difficult. I needed a
defined structure, a protocol that defines how data is structured and validated. My idea was to use this
protocol to make the system predictable, extensible, and clean.
</p>

<p>
Designing that protocol became the next challenge, which I&rsquo;ll cover in Part 2.
</p>
]]>
</description></item>
<item>
<title>Chrono Vista</title>
<link>https://blog.tanminhho.com/posts/ChronoVista/index.html</link>
<pubDate>Sat, 13 Jun 2026 17:24:04 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/ChronoVista/index.html</guid>
<description>
<![CDATA[]]>
</description></item>
<item>
<title>Synchronisation Orchestrator</title>
<link>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260703-synchronisation_orchestrator.html</link>
<pubDate>Fri, 03 Jul 2026 00:00:00 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260703-synchronisation_orchestrator.html</guid>
<description>
<![CDATA[<div id="outline-container-org47c71b2" class="outline-2">
<h2 id="org47c71b2"><span class="section-number-2">1.</span> Overview</h2>
<div class="outline-text-2" id="text-1">
<p>
The <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260630-datasources.html">Datasources</a> doc outlines three independent metadata axes on each record, e.g. transport, format, and category
metadata. The <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html">Synchronisation Subsystem Architecture</a> doc defines the per-category pipeline components (fetcher, mapper,
repository, and service) but does not specify how a fetcher is assembled from transport- and format-specific pieces.
This lack of connection makes the coordination within the subsystem impossible. Therefore, in order to solve this
problem, this document aims to design the orchestrator, which is the missing layer that reads a datasource record and
wires the two together.
</p>
</div>
</div>
<div id="outline-container-org024a2d2" class="outline-2">
<h2 id="org024a2d2"><span class="section-number-2">2.</span> Decision</h2>
<div class="outline-text-2" id="text-2">
<p>
The orchestrator uses three independent registries, one per metadata axis, composed in sequence:
</p>

<ul class="org-ul">
<li><b>Transport registry</b>: maps transport to a fetch function constructor.</li>
<li><b>Format registry</b>: maps format to a decoder.</li>
<li><b>Category registry</b>: maps category to a pipeline factory that accepts a composed fetcher and returns a wired pipeline.</li>
</ul>

<p>
The transport fetch function and format decoder are composed into a single fetcher, satisfying the fetcher role already
defined in the architecture doc.
</p>
</div>
</div>
<div id="outline-container-orgbd3d604" class="outline-2">
<h2 id="orgbd3d604"><span class="section-number-2">3.</span> Alternatives Considered</h2>
<div class="outline-text-2" id="text-3">
</div>
<div id="outline-container-orgc83bf03" class="outline-3">
<h3 id="orgc83bf03"><span class="section-number-3">3.1.</span> Single-tier registry</h3>
<div class="outline-text-3" id="text-3-1">
<p>
A single registry keyed by a composite (transport, format, category) tuple, mapping directly to a fully constructed
pipeline.
</p>

<ul class="org-ul">
<li><b>Pro:</b> Only one registry to maintain and no intermediate composition step.</li>
<li><b>Con:</b> Combinatorial explosion of registrations and duplicating logic across combinations that share the same Transport
or format. Furthermore, it does not reuse the existing per-axis metadata split already established in the Datasource
doc.</li>
</ul>
</div>
</div>
<div id="outline-container-org12de17c" class="outline-3">
<h3 id="org12de17c"><span class="section-number-3">3.2.</span> Factory with inline branching</h3>
<div class="outline-text-3" id="text-3-2">
<p>
A single factory function takes the Datasource record and internally branches (via type-switch) on transport, format,
and category to construct the pipeline.
</p>

<ul class="org-ul">
<li><b>Pro:</b> Simple call site, no registries.</li>
<li><b>Con:</b> Can lead to &ldquo;Factory Bloat&rdquo; if there are many unique combinations.</li>
</ul>
</div>
</div>
<div id="outline-container-org7f5dc7f" class="outline-3">
<h3 id="org7f5dc7f"><span class="section-number-3">3.3.</span> Chosen: Multi-tier registry with thin factory wrapper</h3>
<div class="outline-text-3" id="text-3-3">
<ul class="org-ul">
<li><b>Pro:</b> New transport, format, or category values are added via registration, not by editing core dispatch logic.</li>
<li><b>Con:</b> format/category compatibility is enforced only implicitly, at the type-assertion site, not by the type system.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-org917beab" class="outline-2">
<h2 id="org917beab"><span class="section-number-2">4.</span> Requirements</h2>
<div class="outline-text-2" id="text-4">
</div>
<div id="outline-container-orgdbecae4" class="outline-3">
<h3 id="orgdbecae4"><span class="section-number-3">4.1.</span> Functional Requirements</h3>
<div class="outline-text-3" id="text-4-1">
<ul class="org-ul">
<li>The orchestrator must resolve transport, format, and category independently from a single datasource record.</li>
<li>The composed fetcher must satisfy the fetcher role defined in the <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html">Synchronisation Subsystem Architecture</a>.</li>
<li>The category registry&rsquo;s pipeline factory must return a type satisfying the existing <code>Sync</code> interface.</li>
</ul>
</div>
</div>
<div id="outline-container-org1f487fd" class="outline-3">
<h3 id="org1f487fd"><span class="section-number-3">4.2.</span> Non-Functional Requirements</h3>
<div class="outline-text-3" id="text-4-2">
<ul class="org-ul">
<li>Adding a new transport, format, or category must not require modifying the orchestrator&rsquo;s core composition logic, only
registering new entries.</li>
<li>The type assertion between decoder output and mapper input must fail with a typed, catchable error rather than
panicking.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-org234aa3f" class="outline-2">
<h2 id="org234aa3f"><span class="section-number-2">5.</span> Future Considerations</h2>
<div class="outline-text-2" id="text-5">
</div>
<div id="outline-container-orgf0c193e" class="outline-3">
<h3 id="orgf0c193e"><span class="section-number-3">5.1.</span> Format/Category compatibility validation</h3>
<div class="outline-text-3" id="text-5-1">
<p>
Currently, a mismatched format/category pairing (e.g. a CSV format registered under a category whose mapper expects iCal
structs) is only caught at the type assertion inside the pipeline factory, at sync time. Two follow-up validation layers
are being considered:
</p>

<ul class="org-ul">
<li>Registration-time validation: check the (format, category) pair against a known-compatible-pairs table when a
datasource record is created or updated, rejecting invalid combinations before the first sync attempt.</li>
<li>Sync-time validation: ensure the type assertion inside the pipeline factory returns a descriptive error (not a
panic), surfaced through the existing per-datasource logging hooks defined in the <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260630-datasources.html">Datasources</a> doc.</li>
</ul>

<p>
This is deferred to a later revision once real-world format/category combinations are better understood.
</p>
</div>
</div>
<div id="outline-container-orge7e5180" class="outline-3">
<h3 id="orge7e5180"><span class="section-number-3">5.2.</span> Generic Decoder interface</h3>
<div class="outline-text-3" id="text-5-2">
<p>
Revisit whether a generic <code>Decoder[T]</code> is worthwhile once the number of format implementations grows large enough that the
single untyped assertion point becomes a maintenance burden.
</p>
</div>
</div>
</div>
]]>
</description></item>
<item>
<title>Synchronisation Subsystem Architecture</title>
<link>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html</link>
<pubDate>Fri, 26 Jun 2026 00:00:00 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html</guid>
<description>
<![CDATA[<div id="outline-container-org67c5a0a" class="outline-2">
<h2 id="org67c5a0a"><span class="section-number-2">1.</span> Overview</h2>
<div class="outline-text-2" id="text-1">
<p>
This document outlines the architectural design for the synchronisation subsystem within the data ingestion pipeline.
The primary objective is to fetch raw data from external sources, transform it into domain records, and persist it into the
database.
</p>

<p>
The synchronisation subsystem needs to interact with different data types or more specifically different external
systems. This means the raw data may come via REST API, or gRPC, or simply a database with the potential of different
authorisation and methods.
</p>
</div>
</div>
<div id="outline-container-org1eeb07c" class="outline-2">
<h2 id="org1eeb07c"><span class="section-number-2">2.</span> Data Pipeline</h2>
<div class="outline-text-2" id="text-2">
<p>
One of the major challenges when fetching data from multiple data sources for the same data type is that the raw input
may have slightly different formats. This increases the friction in the analysis layer when interacting with these data.
This means each data type should have its own &ldquo;universal&rdquo; structure, so it removes the &ldquo;where do the data come from&rdquo; in
subsequent layers. This approach does have some friction when integrating new data sources, but it is still better to
pay a small price in one single place rather than along the way up to the visualisation. Therefore, the data flow in
syncrhonisation subsytem becomes: Fetch → Validate → Map → Persist.
</p>
</div>
</div>
<div id="outline-container-org8262da6" class="outline-2">
<h2 id="org8262da6"><span class="section-number-2">3.</span> System Decomposition</h2>
<div class="outline-text-2" id="text-3">
<p>
Based on the data flow, there are three primary components in the pipeline:
</p>

<ul class="org-ul">
<li>Fetcher: Responsible for fetching raw data.</li>
<li>Mapper: Transforms raw input from the fetcher into specified domain records.</li>
<li>Repository: Manages the interaction between the application domain and the database.</li>
<li>Service: Orchestrates the flow between the above components (derived below).</li>
</ul>

<p>
However, there are several choices of how to organise these for easy maintainability and extendability:
</p>
</div>
<div id="outline-container-orga0cd72e" class="outline-3">
<h3 id="orga0cd72e"><span class="section-number-3">3.1.</span> One global service for each component</h3>
<div class="outline-text-3" id="text-3-1">
<p>
Due to the fact already mentioned above, each service will become very complex with lots of different logic for
different data types. This may be fine if the list of supported data types is known and the logic could be shared, but
considering this is not the case, making this option not suitable.
</p>
</div>
</div>
<div id="outline-container-org5fa2385" class="outline-3">
<h3 id="org5fa2385"><span class="section-number-3">3.2.</span> Each data type has its onw components</h3>
<div class="outline-text-3" id="text-3-2">
<p>
Given each data type its own fetcher, mapper, and repository, will isolate the edge cases of one data type from the
other ones. This means each component could focus on doing one thing well without caring about the other data types,
making the implementation in most cases small enough to comprehend. However, this also means every new data type must
implement all components, even if some are trivial. But because different data types will require different ways of
fetching and mapping, the duplication of this option is minimal. Therefore, this is a better option for the long term.
</p>

<p>
To keep higher layers simple, an orchestrator service per data type is required to manage the Fetch → Validate → Map →
Persist flow and handle edge cases internally.
</p>

<p>
Since the main loop must execute sync for all data types uniformly, these services need a shared contract. Three options
were considered:
</p>

<ul class="org-ul">
<li><b>Function callback</b> <code>func(ctx context.Context) error</code>: Lightweight, but loses the ability to attach additional methods
(e.g. <code>Name()</code>, <code>IsEnabled()</code>) to the contract later without breaking all call sites.</li>
<li><b>Base struct with embedding</b>: Allows shared behaviour, but Go&rsquo;s composition model means the base struct cannot enforce
that subtypes implement core logic, weakening the contract.</li>
<li><b>Interface</b>: Explicit contract, zero-cost abstraction, idiomatic Go. New data types are forced to satisfy the contract
at compile time, and the registry pattern becomes trivial to implement.</li>
</ul>

<p>
Therefore, an interface is chosen:
</p>

<div class="org-src-container">
<pre class="src src-go">type Sync interface{
  Run(ctx context.Context) error
}
</pre>
</div>

<p>
That leaves one question left to answer: How to organise these components?
</p>
</div>
<div id="outline-container-org0972f74" class="outline-4">
<h4 id="org0972f74"><span class="section-number-4">3.2.1.</span> Option 1: Per role</h4>
<div class="outline-text-4" id="text-3-2-1">
<p>
That means all fetchers in the same subpackage, as well as all mappers and repositories in their own subpackages. This
approach makes it easy to discover all code sharing the same responsibility in one place, but also makes the code for
one data type scatter around the code base, making it hard to discover and maintain. For that reason, this option is
rejected.
</p>
</div>
</div>
<div id="outline-container-org8430a57" class="outline-4">
<h4 id="org8430a57"><span class="section-number-4">3.2.2.</span> Option 2: Per data type</h4>
<div class="outline-text-4" id="text-3-2-2">
<p>
Each data type is its own package and contains all four components: fetcher, mapper, repository, and service. This
approach follows the idea of &ldquo;what uses together, comes together&rdquo;. This makes discoverability of all related code for
one specific data type easy, while not preventing share logic between components between data types. Therefore, this
option is chosen.
</p>
</div>
</div>
</div>
</div>
]]>
</description></item>
<item>
<title>Datasources</title>
<link>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260630-datasources.html</link>
<pubDate>Tue, 30 Jun 2026 00:00:00 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260630-datasources.html</guid>
<description>
<![CDATA[<div id="outline-container-org72d679b" class="outline-2">
<h2 id="org72d679b"><span class="section-number-2">1.</span> Overview</h2>
<div class="outline-text-2" id="text-1">
<p>
This document outlines the design and requirements for the datasource management within the synchronisation subsystem. A
<b>Datasource</b> is a configuration entity that describes &ldquo;where&rdquo; and &ldquo;how&rdquo; of raw data ingestion. The core idea is to treat
datasources as configuration-driven entities, allowing the synchronisation engine to dynamically assemble pipelines
based on provided metadata rather than hard-coded logic. The goal of datasource is to provide enough information and
metadata for higher layer to construct correct pipeline.
</p>
</div>
</div>
<div id="outline-container-org9f71005" class="outline-2">
<h2 id="org9f71005"><span class="section-number-2">2.</span> Context</h2>
<div class="outline-text-2" id="text-2">
<p>
The synchronisation subsystem must ingest raw data from various external sources (HTTP APIs, relational databases, and
prospectively gRPC services, file systems, and WebSockets). Without a unified abstraction, each new source type would
require changes to the core engine, creating a maintenance burden. The datasource entity solves this by encapsulating
all source-specific configuration behind a unified interface, so the orchestrator can drive behaviour purely from
metadata. Furthermore, the datasource component is responsible solely for acquisition and initial mapping, as it does
not handle business logic, data transformation for analysis, or long-term storage optimisation.
</p>
</div>
</div>
<div id="outline-container-org805169c" class="outline-2">
<h2 id="org805169c"><span class="section-number-2">3.</span> Decision</h2>
<div class="outline-text-2" id="text-3">
<p>
Datasource is a first-class, versioned configuration entity stored in the persistence layer (as per <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260604-data_pipeline_persistence_design.html">Data Pipeline
Persistence Design</a>). The orchestrator reads datasource records at runtime to instantiate the appropriate pipeline.
components, as described in the <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html">Synchronisation Subsystem Architecture</a>. Furthermore, the configuration blob should be
encrypted using <b>ChaCha20-Poly1305</b>, because it may contain sensitive fields.
</p>
</div>
</div>
<div id="outline-container-org2d8a8cb" class="outline-2">
<h2 id="org2d8a8cb"><span class="section-number-2">4.</span> Alternatives Considered</h2>
<div class="outline-text-2" id="text-4">
</div>
<div id="outline-container-org005d80b" class="outline-3">
<h3 id="org005d80b"><span class="section-number-3">4.1.</span> Plain-text configuration files (e.g., YAML/TOML)</h3>
<div class="outline-text-3" id="text-4-1">
<p>
Datasources are described as flat config files on disk.
</p>

<ul class="org-ul">
<li><b>Pro:</b> Human-readable, easy to diff in version control.</li>
<li><b>Con:</b> No lifecycle management (active/inactive, versioning). Credential handling is insecure by default. Operationally
impractical for dynamic addition or removal at runtime.</li>
</ul>
</div>
</div>
<div id="outline-container-org0e4ea53" class="outline-3">
<h3 id="org0e4ea53"><span class="section-number-3">4.2.</span> Chosen: Versioned, encrypted database records</h3>
<div class="outline-text-3" id="text-4-2">
<p>
Datasources are stored as versioned rows in the persistence layer with an encrypted config blob.
</p>

<ul class="org-ul">
<li><b>Pro:</b> Supports runtime addition/modification without redeployment. Encryption is enforced at the model layer.
Versioning enables non-destructive schema migrations.</li>
<li><b>Con:</b> Requires a migration strategy for config schema evolution. Adds a decryption step in the hot path (acceptable
given ChaCha20-Poly1305 performance characteristics).</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-org4a509c5" class="outline-2">
<h2 id="org4a509c5"><span class="section-number-2">5.</span> Requirements</h2>
<div class="outline-text-2" id="text-5">
</div>
<div id="outline-container-orga1fdd06" class="outline-3">
<h3 id="orga1fdd06"><span class="section-number-3">5.1.</span> Functional Requirements</h3>
<div class="outline-text-3" id="text-5-1">
<ul class="org-ul">
<li>The system should support at minimum two protocol families: HTTP and Database.</li>
<li>The configuration must provide enough metadata (e.g., source type, endpoint, credentials, etc.) for a higher-level
orchestrator to instantiate the correct pipeline components, as outlined in <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html">Synchronisation Subsystem Architecture</a>.</li>
<li>Each datasource must at least support lifecycle metadata such as refresh interval, scheduling priority, and active
status.</li>
</ul>
</div>
</div>
<div id="outline-container-org913d679" class="outline-3">
<h3 id="org913d679"><span class="section-number-3">5.2.</span> Non-Functional Requirements</h3>
<div class="outline-text-3" id="text-5-2">
<ul class="org-ul">
<li>All sensitive configuration fields must be encrypted at rest using ChaCha20-Poly1305.</li>
<li>Input validation must be performed on all configuration fields to prevent injection attacks (e.g., SQL injection in DB
connection strings).</li>
<li>The architecture must allow for the addition of new source types (e.g., gRPC, File System, WebSockets) with minimal
friction, ideally via a registry/factory pattern rather than conditional branching.</li>
<li>The system should provide hooks for logging sync status, errors, and latency per datasource.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-orgd429f47" class="outline-2">
<h2 id="orgd429f47"><span class="section-number-2">6.</span> Data Model</h2>
<div class="outline-text-2" id="text-6">
<p>
The datasource entity should consist of:
</p>

<ul class="org-ul">
<li>ID: Unique identifier</li>
<li>Version: Schema version for the configuration blob (to handle migrations)</li>
<li>Name: Human-readable name</li>
<li>Transport: Type of transport, e.g. HTTP, Database, etc.</li>
<li>Format: Payload decoding, e.g. iCal, JSON, CSV, native SQL rows, etc.</li>
<li>Category: <a href="https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260626-synchronisation_subsystem_architecture.html">Synchronisation pipeline type</a>, e.g. timeline, finance, etc.</li>
<li>Config: Encrypted blob containing connection details</li>
<li>Sync policy: Metadata regarding frequency and priority
<ul class="org-ul">
<li>Interval: Duration (e.g., 5m, 1h).</li>
<li>Priority: Integer for scheduling order.</li>
<li>Active: Boolean flag.</li>
</ul></li>
</ul>
</div>
</div>
<div id="outline-container-org0d9582c" class="outline-2">
<h2 id="org0d9582c"><span class="section-number-2">7.</span> Future Considerations</h2>
<div class="outline-text-2" id="text-7">
<ul class="org-ul">
<li>Rate limiting: For HTTP sources, implementing per-datasource rate limiting to respect provider constraints.</li>
<li>Circuit Breaker: Implementing a circuit breaker to prevent failing datasources from exhausting system resources.</li>
<li>Partial Sync/Checkpointing: Store &ldquo;last synced&rdquo; offsets (e.g., timestamps or IDs) to allow resuming interrupted syncs.</li>
</ul>
</div>
</div>
]]>
</description></item>
<item>
<title>Data Pipeline and Persistence Design</title>
<link>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260604-data_pipeline_persistence_design.html</link>
<pubDate>Thu, 04 Jun 2026 00:00:00 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/ChronoVista/Design-Docs/20260604-data_pipeline_persistence_design.html</guid>
<description>
<![CDATA[<div id="outline-container-org61648ba" class="outline-2">
<h2 id="org61648ba"><span class="section-number-2">1.</span> Overview</h2>
<div class="outline-text-2" id="text-1">
<p>
This document captures the design decisions made for the data ingestion pipeline, persistence strategy, and the boundary
between the synchorinisation and analysis subsystems.
</p>

<p>
<i>Notes: The analysis subsystem and ingestion layer will be documented in detail in subsequent documents.</i>
</p>
</div>
</div>
<div id="outline-container-orgbc42dae" class="outline-2">
<h2 id="orgbc42dae"><span class="section-number-2">2.</span> System Decomposition</h2>
<div class="outline-text-2" id="text-2">
<p>
The system could be divided into two separated subsystems with distinct responsibilities:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Subsystem</th>
<th scope="col" class="org-left">Responsibility</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Synchronisation</td>
<td class="org-left">Fetch raw data, parse it, and persist into DB</td>
</tr>

<tr>
<td class="org-left">Analysis</td>
<td class="org-left">Execute queries, render charts, compose dashboards and reports</td>
</tr>
</tbody>
</table>

<p>
The synchronisation subsystem produces tables in DB, while the analysis subsystem consumes them.
</p>
</div>
</div>
<div id="outline-container-org4ca00e5" class="outline-2">
<h2 id="org4ca00e5"><span class="section-number-2">3.</span> Synchronisation Subsytem</h2>
<div class="outline-text-2" id="text-3">
</div>
<div id="outline-container-org7cc70ec" class="outline-3">
<h3 id="org7cc70ec"><span class="section-number-3">3.1.</span> Design Principle</h3>
<div class="outline-text-3" id="text-3-1">
<p>
New data types, e.g. finance, health, etc., require a new adapter. The role of this adapter is to persist raw input from
a specified datasource to a specific table in DB. Keep in mind, the adapter does not handle how to fetch data from
datasouce, since this is the responsibility of a separated layer.
</p>
</div>
</div>
<div id="outline-container-org88d5a81" class="outline-3">
<h3 id="org88d5a81"><span class="section-number-3">3.2.</span> Alternatives</h3>
<div class="outline-text-3" id="text-3-2">
<p>
While the alternative, which is allowing runtime-defined transformation logic such as user-supplied SQL expressions
evaluated during ingestion, offers better flexibility when integrating with new data types, but introduces an
unacceptable security surface. Because the queries could contain anything, this may lead to at worst SQL injection and
at best loss of data because of side effects in queries. By following this alternative, the complexity of validation of
the runtime-defined logic also increases to make sure it is safe to be executed.
</p>
</div>
</div>
<div id="outline-container-orge545783" class="outline-3">
<h3 id="orge545783"><span class="section-number-3">3.3.</span> Adapter Contract</h3>
<div class="outline-text-3" id="text-3-3">
<p>
Each adapter takes a raw input, transform this data, and persist to DB. That means, an adapter is composed of two parts:
</p>

<ul class="org-ul">
<li>Mapper: maps the raw input into a specified domain record</li>
<li>Repository: handles the interaction between the application and the DB</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-org2165472" class="outline-2">
<h2 id="org2165472"><span class="section-number-2">4.</span> Analysis Subsystem</h2>
<div class="outline-text-2" id="text-4">
</div>
<div id="outline-container-orga0904c0" class="outline-3">
<h3 id="orga0904c0"><span class="section-number-3">4.1.</span> Design Principles</h3>
<div class="outline-text-3" id="text-4-1">
<p>
The analysis subsystem is modelled on three principles: SQL-first, query-centric, and decoupled from ingestion concerns.
User interact with DB tables directly through the UI by writing queries, defining transformation views, configuring
charts, and composing dashboards.
</p>
</div>
</div>
</div>
]]>
</description></item>
<item>
<title>Kortex</title>
<link>https://blog.tanminhho.com/posts/Kortex/index.html</link>
<pubDate>Tue, 30 Jun 2026 21:10:02 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/Kortex/index.html</guid>
<description>
<![CDATA[]]>
</description></item>
<item>
<title>Architecture and Core Functionality</title>
<link>https://blog.tanminhho.com/posts/Kortex/Design-Docs/20260628-architecture_and_core_functionality.html</link>
<pubDate>Sun, 28 Jun 2026 00:00:00 +0200</pubDate>
<guid>https://blog.tanminhho.com/posts/Kortex/Design-Docs/20260628-architecture_and_core_functionality.html</guid>
<description>
<![CDATA[<div id="outline-container-orgce3e8af" class="outline-2">
<h2 id="orgce3e8af"><span class="section-number-2">1.</span> Overview</h2>
<div class="outline-text-2" id="text-1">
<p>
Kortex is a knowledge management and retrieval system designed to provide information retrieval from trusted sources. By
leveraging a Graph RAG architecture, Kortex prioritises the preservation of document relationships, especially those
inherent in the Zettelkasten method over isolated data points.
</p>

<p>
The initial targeted user is me alone. Trusted sources are: my owner&rsquo;s Zettelkasten vault (org-roam), Wikipedia, and a
curated set of blogs.
</p>
</div>
</div>
<div id="outline-container-org4b3e1ea" class="outline-2">
<h2 id="org4b3e1ea"><span class="section-number-2">2.</span> Core Goals</h2>
<div class="outline-text-2" id="text-2">
<ul class="org-ul">
<li>Retrieval must be strictly anchored in sources explicitly chosen and trusted by the user.</li>
<li>Kortex must exploit and preserve the links between documents. This is critical because the value of a Zettelkasten
lies in its interconnect rather than individual notes.</li>
<li>The system aims for a streamlined experience, providing at minimum a graph-based RAG organised by collections, with
optional user-facing features like folder-based chat interfaces.</li>
</ul>
</div>
</div>
<div id="outline-container-orgc17e63e" class="outline-2">
<h2 id="orgc17e63e"><span class="section-number-2">3.</span> Functional Requirements</h2>
<div class="outline-text-2" id="text-3">
</div>
<div id="outline-container-org1efc597" class="outline-3">
<h3 id="org1efc597"><span class="section-number-3">3.1.</span> Ingestion Pipeline</h3>
<div class="outline-text-3" id="text-3-1">
<p>
The system will handle the full lifecycle of raw data into the knowledge base:
</p>

<ul class="org-ul">
<li>Fetch &amp; Parse: Retrieve and structure raw content.</li>
<li>Chunk &amp; Embed: Segment content into manageable units. Because notes in Zettelkasten vault are short and atomic,
therefore, these files should be embedded whole, while semantic chunking is only triggered above a defined token
threshold to avoid splitting an already-atomic idea.</li>
<li>Upsert: Simultaneously update the vector store and the graph store, with optional detecting and recovering from
partial failures.</li>
</ul>
</div>
<div id="outline-container-org7a082d0" class="outline-4">
<h4 id="org7a082d0"><span class="section-number-4">3.1.1.</span> Wikipedia Ingestion Strategy</h4>
<div class="outline-text-4" id="text-3-1-1">
<p>
A full English Wikipedia dump exceeds 20+ GB, making naive full ingestion impractical for a single-user local system.
Two pure strategies were considered and rejected in favour of a hybrid:
</p>

<ul class="org-ul">
<li>On-demand fetch at query time: avoids storage bloat but adds latency to every query touching un-ingested content, and
is bound by Wikipedia&rsquo;s live API rate limits.</li>
<li>Pre-pull by category only: bounds the corpus to relevant topics, but raises the hard question of distinguishing
&ldquo;article missing from KB&rdquo; vs. &ldquo;article does not exist&rdquo; without a live lookup.</li>
</ul>

<p>
Adopted hybrid approach:
</p>

<ul class="org-ul">
<li>Pre-pull: fetch articles under user-selected categories (recursively, to a configurable depth), building a
deterministic seed corpus scoped to actual interest.</li>
<li>Existence check at query time: when a query references an article not found in the KB, issue a lightweight metadata
call (not a full content fetch) to determine whether the article exists at all.</li>
<li>Miss handling: if the article exists but is not yet ingested, the current query is answered using only what is already
in the KB. The missing article is queued for asynchronous fetch, parse, chunk, embed, and upsert.</li>
<li>Background sync: a scheduled job re-walks the category tree for new or changed pages, batching requests within
Wikimedia rate limits, decoupling ingestion latency from query latency entirely.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-orgaf26272" class="outline-3">
<h3 id="orgaf26272"><span class="section-number-3">3.2.</span> Retrieval Engine</h3>
<div class="outline-text-3" id="text-3-2">
<p>
Kortex will employ a multi-stage retrieval strategy to maximise accuracy:
</p>

<ul class="org-ul">
<li>Hybrid search: Execute lexical (BM25) and vector search in parallel. Lexical search ensures precision for specific
terms and acronyms, while semantic search captures broader intent and nuances.</li>
<li>Fusion: Results are merged using Reciprocal Rank Fusion. Fusion parameters (rank constant k, per-lane candidate
window) are explicit configuration values, not hardcoded defaults, to allow empirical tuning.</li>
<li>Graph Expansion: Results are expanded along the document graph to capture relational context that vector embeddings
alone might overlook. Expansion depth defaults to one hop but is degree-aware: hub notes with very high link counts
are capped in how many neighbours they contribute, preventing context-window flooding from a single popular note.</li>
<li>Re-ranking &amp; Pruning: Final results are re-ranked by cosine similarity and a dedicated reranker (cross-encoder), then
pruned to fit the maximum context window.</li>
</ul>
</div>
</div>
<div id="outline-container-org1168ccd" class="outline-3">
<h3 id="org1168ccd"><span class="section-number-3">3.3.</span> Knowledge Management</h3>
<div class="outline-text-3" id="text-3-3">
<ul class="org-ul">
<li>Collection management: Users can organise knowledge into distinct collections.</li>
<li>Inspection: Tools to inspect documents and visualise the underlying graph.</li>
<li>Synchronisation: A dedicated mechanism to trigger data updates.</li>
</ul>
</div>
</div>
<div id="outline-container-orgb91c600" class="outline-3">
<h3 id="orgb91c600"><span class="section-number-3">3.4.</span> Chat Interface</h3>
<div class="outline-text-3" id="text-3-4">
<ul class="org-ul">
<li>Conversation management: Handle multi-turn dialogues.</li>
<li>Folder organisation: Provide a hierarchical structure for organising distinct conversation threads.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-orgdefe4b4" class="outline-2">
<h2 id="orgdefe4b4"><span class="section-number-2">4.</span> Technical Rationale and Design Principles</h2>
<div class="outline-text-2" id="text-4">
</div>
<div id="outline-container-org9100b83" class="outline-3">
<h3 id="org9100b83"><span class="section-number-3">4.1.</span> Why Graph RAG?</h3>
<div class="outline-text-3" id="text-4-1">
<p>
Relying solely on semantic (vector) retrieval has inherent limitations regarding structural and relational knowledge.
Vector embeddings are excellent for topical similarity but often struggle to represent specific network-based
relationships. By incorporating a graph structure, Kortex ensures that explicit links and structural dependencies are
not lost during the retrieval process.
</p>
</div>
</div>
<div id="outline-container-orgaf476a3" class="outline-3">
<h3 id="orgaf476a3"><span class="section-number-3">4.2.</span> Hybrid Search Strategy</h3>
<div class="outline-text-3" id="text-4-2">
<p>
To achieve a robust retrieval process that satisfies both literal and conceptual intents, Kortex utilises hybrid search:
</p>

<ul class="org-ul">
<li>Lexical Matching: Highly effective for capturing specific terms, acronyms, and exact identifiers that vector models
might "smooth over”.</li>
<li>Semantic Search: Complements lexical matching by capturing broader intent and conceptual nuances.</li>
</ul>
</div>
</div>
<div id="outline-container-org9c131a8" class="outline-3">
<h3 id="org9c131a8"><span class="section-number-3">4.3.</span> Performance Considerations</h3>
<div class="outline-text-3" id="text-4-3">
<p>
Lexical indexing (Inverted Indices) will be leveraged to maintain high throughput and minimal CPU/memory overhead across
massive datasets, balancing the higher resource intensity required by high-dimensional vector calculations. Where
Applicable, parallel processing will be utilised to reduce end-to-end time by executing independent computations
simultaneously.
</p>
</div>
</div>
<div id="outline-container-org30f2142" class="outline-3">
<h3 id="org30f2142"><span class="section-number-3">4.4.</span> Scoping and Consistency Notes</h3>
<div class="outline-text-3" id="text-4-4">
<ul class="org-ul">
<li>Single-user scope: Collections and the graph store operate in a single namespace. No auth/multi-tenancy is required at
this stage, but this assumption should be revisited before any future multi-user migration.</li>
<li>All ingestion paths (Vault, Wikipedia, Blogs) pass through the same trust-filtering and upsert-consistency mechanisms
described above, ensuring uniform behaviour regardless of source type.</li>
</ul>
</div>
</div>
</div>
]]>
</description></item>
</channel>
</rss>
