devinside.Marcin Borecki Ideas. Systems. Evidence.
← The notebook

Engineering

A Blog That Can Explain Its Sources

An expert blog asks readers to trust more than its sentences. It asks them to trust the author's selection of evidence, interpretation of that evidence and willingness to correct a mistake.

Explore the mechanism ↓
Editorial ink and watercolor illustration for A citation needs a version, not just a URL.

START WITH THE MECHANISM

Interactive companion and laboratory update: 12 November 2026. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.

THE FULL INVESTIGATION

A Blog That Can Explain Its Sources

Editorial context: 10 September 2026 · Prepared and updated: 12 November 2026
Dates and technology context

This is a retrospective editorial timeline. Topic context: 10 September 2026. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.

The actual source-ledger HTTP companion ran in November2026 on .NET9.0.4. The September2026 MCP/.NET10 design remains a separate proposal; no MCP transport execution is claimed.

An expert blog asks readers to trust more than its sentences. It asks them to trust the author's selection of evidence, interpretation of that evidence and willingness to correct a mistake. Adding an AI assistant creates a new problem: the assistant can make a claim in the blog's voice without preserving the editorial work that made the original article credible.

A useful source-aware assistant should be able to explain where a claim came from, which version of the source it used, and where the evidence stops. A link appended to a fluent answer is too weak a contract. The link might be real while the claim is unsupported, outdated or broader than the source permits.

I would build the source system before building the conversational interface. The first product is a trustworthy evidence ledger. Search, citations and generated explanations become consumers of that ledger.

Separate an article from its evidence

A blog article is an authored argument. It may combine official documentation, a reproducible experiment, an external observation and an original opinion. These are different kinds of support, and a source system should preserve the distinction.

Official documentation can establish that an API exists in a version. A benchmark can establish a measured result under stated conditions. Neither automatically proves a broad architectural recommendation. The recommendation contains judgment, and readers should be able to see where that judgment begins.

Represent sources as independent records rather than burying them in free-form article text. Record the URL, title, publisher, observed version, access date, relevant publication date where known, and the purpose for which the article relies on the source. Do not manufacture a publication date from a page's recent crawl date.

Provenance is a useful organizing concept here: it describes entities, activities and agents involved in producing information. The W3C PROV family provides a formal vocabulary, although a small blog need not implement the full model to benefit from preserving these relationships. W3C PROV overview

The practical question is simple: can a reader follow an important statement back through the evidence and understand what the author actually established?

Dates answer different questions

01 / See the idea

Three dates, three different questions

A hypothetical source is published, becomes effective and is checked at different times.

A document published on day 1 becomes effective on day 10 and is checked on day 20. Checking it later does not make its rule valid for a day-5 decision.

Interpretation and evidence
  • Publication describes appearance.
  • Effective time describes applicability.
  • Checked time describes observation.
conceptual/hypothetical, not measured

A technical source has several clocks. Publication time describes when the source appeared. Effective time describes when a feature or rule became applicable. Observation time describes when we checked the page. The article's own context date describes the period about which it makes a claim.

Conflating these clocks creates accidental anachronisms. A current documentation page may describe an API added after the historical scenario in an essay. Checking that page today proves current documentation content, not historical availability.

For a version-sensitive claim, prefer a release announcement, a tagged source file, a package version or versioned documentation. Store the exact artifact identifier when possible. A major version alone can be insufficient when a patch or preview introduced the relevant API.

Unknown dates should remain unknown. If a page has no reliable publication date, store the date we checked it and say what that establishes. Honesty about missing metadata is more useful than a complete-looking table built from guesses.

This also affects corrections. A statement can have been reasonable under an earlier version and still be wrong for a current reader. The correction should explain whether the original evidence was mistaken, the product changed, or the recommendation no longer fits the intended context.

Keep a source version, not just a URL

URLs are references to resources whose representations can change. A content hash helps identify a captured representation, but it does not prove that the content is true, that the publisher endorsed an interpretation, or that the capture has redistribution rights.

HTTP validators such as entity tags can help with representation validation and conditional requests, subject to the server's semantics. They should not be treated as universal document version numbers. An application still needs its own source record and capture policy. HTTP semantics, RFC 9110

For material we own, retaining the published Markdown and its repository revision is straightforward. For external material, retain permitted metadata, locators, concise notes and limited excerpts where appropriate. A source ledger is not a license to mirror entire websites.

The following application-owned C# model illustrates the distinction. It is not an SDK type and has not been compiled or executed. The example assumes a .NET 10 application, but the data model is independent of a particular search service.

public sealed record SourceVersion(
    string SourceId,
    Uri CanonicalUrl,
    string Title,
    DateTimeOffset CheckedAt,
    DateOnly? PublishedOn,
    string? ProductVersion,
    string RepresentationHash,
    string RetentionPolicyId);

public sealed record EvidenceReference(
    string EvidenceId,
    string SourceId,
    string RepresentationHash,
    string Locator,
    string SupportedClaim,
    string EvidenceKind);

SupportedClaim is an editorial assertion that requires review. It is not magically derived from hashing the source. Locator might identify a section, paragraph or code symbol, depending on the source format. The system should distinguish a broken locator from a missing source and from a source that no longer supports the claim.

Search finds candidates; retrieval establishes the bundle

02 / A concrete example

One URL does not mean one source version

Two hypothetical representations at the same URL have different hashes and claim support.

An article cites source version A and its section locator, but the URL now serves version B. The ledger must retain the version relationship or mark support unresolved instead of silently substituting B.

Interpretation and evidence
  • Store the source version.
  • A hash identifies bytes, not truth.
  • Resolve the cited locator against that version.
conceptual/hypothetical, not measured

Search is an excellent way to discover potentially relevant material. It should not silently decide that every matching passage is suitable evidence. A search result can be outdated, access-restricted, incomplete or relevant to the wrong version.

Separate candidate discovery from evidence retrieval. A search operation returns stable evidence identifiers with short descriptions. A retrieval operation resolves selected identifiers into a bounded, versioned bundle after checking permissions and source status. Generation receives that bundle and is required to cite only its identifiers.

Azure AI Search can support text and vector retrieval for candidate discovery. The application's source ledger supplies the editorial meaning and version relationships that a relevance score cannot establish by itself. Azure AI Search vector overview

If exposing these operations through MCP, keep the distinction in the tool contracts. The protocol defines tool discovery and invocation with schemas. A schema describes permitted input shape; it does not prove that the tool's results are trustworthy or that a caller is authorized for every source. MCP tools specification, version 2025-11-25

The architecture does not require embedding arbitrary SDK calls in this article. The official C# SDK reached version 1.0 before this historical context, but the important boundary is application-owned: a model proposes evidence IDs, and trusted application code resolves them. Official MCP C# SDK version 1.0 announcement

Do not turn citation retrieval into arbitrary browsing

A tempting tool design accepts any URL and returns its contents. That creates a much larger security and operational problem than looking up approved evidence. The URL might resolve to a private address, redirect unexpectedly, return an enormous response or embed instructions designed to influence the model.

For a blog assistant, begin with an allowlisted source ledger. Resolve source IDs on the server. Apply explicit network, response-size, content-type and timeout controls during ingestion. Recheck redirects and destination policy where external fetching is required. Do not let an answer-generation request become an unrestricted network client.

Treat retrieved text as data. A paragraph telling the assistant to ignore its rules is still source content, not an instruction from the user or publisher of the application. The source system should preserve it as evidence only when relevant, without granting it control over tool permissions.

MCP's security guidance discusses risks around authorization, token handling and confused-deputy behavior. Those concerns reinforce the need to keep identity and policy in application code rather than inferring authority from tool text. MCP security guidance for the 2025-11-25 specification

Read-only tools still need boundaries. They can disclose unpublished drafts, confidential notes or restricted source extracts. A tool that cannot modify a database can nevertheless cause harm by revealing the wrong row.

Validate citations in layers

03 / Watch the mechanism

Citation checks build on one another

Mechanical resolution, semantic support and composed inference are three separate checks.
Enlarge diagram

Static view. Play to see how the mechanism unfolds.

A hypothetical citation resolves correctly and its passage supports a narrow claim. Combining it with a result from another environment can still create an unsupported comparison.

Interpretation and evidence
  • Resolve the actual evidence.
  • Check conditions on each claim.
  • Review the inference across sources.
conceptual/hypothetical, not measured

The first layer checks mechanics. Does every citation refer to an item in the actual evidence bundle? Does the source version match? Is the locator available? Can the current reader access the material the interface promises?

The second layer checks support. Does the cited passage establish the associated claim? Does the answer preserve conditions and limitations? A source that says an optimization can help under a specific workload should not become evidence that the optimization always helps.

The third layer checks composition. Several individually supported statements can still form an unsupported conclusion when combined. An assistant might join a performance result from one environment with a cost estimate from another and imply a comparison that nobody measured.

Deterministic validation handles some of the first layer. Semantic review is needed for much of the rest. Model-assisted checks can prioritize suspicious claims, but they should not erase the need for human editorial judgment on consequential or controversial content.

Present the distinction in the interface. A citation can be mechanically verified while its interpretation awaits review. Avoid a single green badge that suggests every dimension of correctness has been established.

Let disagreement remain visible

Sources sometimes conflict because they describe different versions, environments or assumptions. Sometimes they simply disagree. A source-aware blog should expose that disagreement rather than instructing the assistant to smooth it into a single confident answer.

Ask the system to identify the competing claims and the conditions under which each applies. If the evidence does not resolve the conflict, say so. The author's judgment can still be useful, but it should appear as judgment with reasons, not as an invented consensus.

The same rule applies to the author's older work. A new article may supersede an earlier recommendation. Preserve the relationship and add a correction notice or contextual pointer where appropriate. Removing every trace of changed thinking makes the archive look cleaner while making it less informative.

An expert's credibility does not depend on never changing a conclusion. It depends partly on making the change intelligible. A provenance system can support that habit instead of merely decorating answers with links.

Test the evidence path before the conversation

Build a small set of source scenarios: a changed page, a missing page, an obsolete package version, conflicting sources, an unauthorized draft and a valid source whose passage does not support the proposed claim. Check ingestion, retrieval and citation rendering independently.

Then test questions that depend on those scenarios. The expected outcome may be an answer, a qualification, a request for version information or a refusal to claim more than the evidence supports. Preserve the retrieved bundle so a reviewer can distinguish a retrieval failure from an answer-generation failure.

Measure source resolution failures, stale references, unsupported citations and the time needed to inspect a claim. Count corrections as useful feedback rather than hiding them as embarrassment. These broader editorial measurements remain a proposed acceptance process; the later implementation section reports only the narrower HTTP contract that was actually executed.

A small manual source ledger may be enough at first. Automating an unclear editorial process merely makes unclear evidence move faster. Start with a few articles whose important claims can be inspected end to end, then add retrieval and conversational features when the underlying contract is reliable.

The blog's strongest promise is not that its assistant always has an answer. It is that important answers remain connected to inspectable evidence, and that readers can see the boundary between a documented fact, an observation and an argument. That is a useful foundation for an expert publication even if the chat interface is never built.

A small executable ledger, built after the original design

Implementation note, prepared 12 November 2026. The earlier sections preserve the September 2026 design context. The following laboratory was implemented and checked later; it does not imply an earlier execution or deployment.

The new reference application implements a deliberately narrow part of the design: read-only HTTP access to a synthetic source ledger. It does not connect to a hosted model, search engine or MCP server. Its purpose is to demonstrate the date, version and representation checks before those more complicated consumers are introduced.

The executable project is in production/M32/lab. It uses locally installed SDK 9.0.203, targets net9.0 and pins runtime 9.0.4 with roll-forward disabled. This differs from the earlier illustrative .NET 10 model because the laboratory uses an installed baseline whose APIs predate the original context. The actual run still belongs to November 2026. The older runtime is a reproducibility condition of a temporary local experiment, not a recommendation for operating a public service.

The application starts Kestrel on a dynamically assigned loopback port, creates an HTTP client and exercises the public routes. It stops and disposes the server in finally. There is no long-running service left behind and no external data ingestion. The fixture content is invented for this experiment and explicitly labelled synthetic.

Minimal API route handlers provide the HTTP boundary. The runtime can perform parameter binding and serialize the returned records, but the application remains responsible for the meaning of version selection and refusal. A route that successfully returns JSON has not, by that fact alone, resolved the historical question correctly. ASP.NET Core route handlers.

Keeping this first implementation small is a way to make the promise testable. It is not a claim that an in-memory fixture ledger constitutes a complete editorial platform.

The fixture deliberately separates three dates

01 / See the idea

Published does not mean effective

The synthetic v2 policy is published on1July and becomes effective on1August. A request about15July is refused as outside its effective interval; observation on12November does not change either date.

The HTTP resolver can acknowledge that v2 was published while refusing to apply its rule to15July. The observation date records our inspection and never substitutes for historical publication.

Interpretation and evidence
  • Separate publication from effective time.
  • Keep observation as its own fact.
  • Use an explicit context date when resolving a claim.
Actual synthetic fixture and HTTP interval checks; not a real policy history.

The sample source called retry-policy has two versions. Version v1 is marked published on 1 June 2026, effective from 10 June and effective until 1 August. Version v2 is marked published on 1 July but effective from 1 August. These dates describe a fictional policy, not a real product release.

That separation creates a useful question: what should the service return for v2 when the requested context is 15 July? The source has been published, but the new policy is not yet applicable. Treating publication as the start of validity would produce the wrong answer for this fixture.

The inverse question matters too. A requested context before 1 July cannot be supported by v2 as published evidence, even though the application can read the record today. The fact that a source is in the current database does not establish that it existed at the time about which the user asks.

The third date, observation, records when this fixture was prepared and inspected. It is not used to invent publication history. An additional undated-note source has a null publication date. The source record can be retrieved, but an attempt to use it as evidence of historical availability receives a specific refusal.

The new record therefore separates the relevant fields:

public sealed record SourceVersion(
    string SourceId, string Version, string Title, string Content,
    string Sha256, DateOnly? PublishedOn,
    DateOnly EffectiveFrom, DateOnly? EffectiveUntil,
    DateOnly ObservedOn, string Provenance);

The effective interval uses an inclusive start and exclusive end. At exactly 1 August, v1 no longer applies and v2 begins to apply. The tests exercise both sides of that boundary. This convention belongs to the application contract; a record type does not choose it automatically.

Version selection is a required part of the question

The source route is GET /sources/{id}?version=.... Omitting the version returns version_required with status 400. The service does not quietly select the newest record, because doing so would change a version-sensitive question into a different question without telling the caller.

For discovery, GET /sources/{id}/versions returns the available versions and their dates. Discovery and selection remain separate operations. A future interface could help a reader choose, but the server's resolution boundary still receives an explicit choice.

The claim route adds an explicit contextDate in yyyy-MM-dd form. Missing or malformed dates are rejected before the ledger attempts to establish applicability. This avoids turning a regional date-format guess into a historical conclusion.

For example:

GET /claims/attempt-limit?version=v1&contextDate=2026-07-15
GET /claims/attempt-limit?version=v2&contextDate=2026-09-10
GET /claims/attempt-limit?version=v2&contextDate=2026-07-15

The first two requests resolve to different evidence records. The third receives outside_effective_interval, despite referring to a source already published by that date. The distinction is intentional and visible to the client.

The HTTP statuses are a compact envelope around application-specific refusal codes. A missing record receives 404. Missing required input receives 400. A known record that conflicts with the requested version, date or representation receives 409. These choices are this API's documented contract, not a claim that the HTTP standard dictates the domain model. The client should inspect the refusal code rather than infer every reason from a status alone.

A claim resolves through a representation, not just a title

Each EvidenceLink identifies a claim, source, version, expected representation hash and locator. The resolver looks up the exact source/version pair and checks the representation before returning the bundle. A deliberately broken fixture contains a mismatched expected hash so this rejection can be exercised over HTTP.

The hash is computed over the UTF-8 bytes of the fixture's content string using SHA-256. This is a precisely defined representation for the laboratory. It is not a hash of a web page's visual appearance, a guarantee about editorial truth, or a signature by the publisher. The API used here computes a hash of supplied data; meaning must come from the surrounding contract. SHA256.HashData.

The resolver's core checks are intentionally readable:

if (link.Sha256 != source.Sha256 ||
    Hash(source.Content) != source.Sha256)
    return new("hash_mismatch", "Evidence does not resolve to the expected source representation.");
if (source.PublishedOn is null)
    return new("publication_unknown", "Observation date is not a substitute for publication date.");
if (source.PublishedOn > date)
    return new("not_published_by_context", "Source publication is later than the requested context.");

The remaining check enforces the effective interval. A successful response includes the claim link, the source record and the requested date. It also carries the explicit label mechanical_resolution_only_not_semantic_proof.

That label matters. The resolver can establish that the selected records are mechanically consistent. It does not prove that the author's claim is a fair interpretation of the passage. The synthetic evidence uses editorial_assertion as its support kind for exactly that reason. Replacing the label with a green verified badge would promise more than the implementation establishes.

What the real HTTP checks establish

The correctness command is:

dotnet run -c Release -- check

It is a console runner that starts the actual application and sends HTTP requests through Kestrel. It is not a set of dictionary lookups described as integration testing. The completed local run passed 20 checks with zero failures and zero skips on runtime 9.0.4.

The checks include source discovery, retrieval of an exact version and independent assertions that publication and effective dates differ as intended. They verify the returned content hash, the correct evidence link for each claim version and the exact switch at the exclusive end of the earlier interval.

Negative cases cover a missing version, unknown version, missing source, absent or malformed context date, publication after the requested context, an inapplicable effective interval, a conflicting evidence version, an unknown publication date and a broken representation hash. A POST to the source route receives 405 because there is no mutation handler.

These observations establish the included HTTP contract under the supplied fixtures. They do not establish authorization for private documents. The corpus is wholly synthetic and public within the local application; no claim is made that a production identity system has been implemented.

The checks also do not measure semantic support, retrieval relevance or generated-answer quality. There is no model invocation to evaluate. This limitation is useful because it makes the first acceptance boundary clear: can the service refuse an ill-specified or inconsistent evidence request before a conversational layer gets a chance to turn it into a fluent answer?

The answer for these declared cases is supported by executed requests and assertions, not by the existence of the route definitions alone.

Measure the resolver boundary without calling it answer quality

The measurement command sends sequential loopback requests to two routes: an exact source retrieval and a resolved claim bundle. The latter performs the representation and date checks and returns more structured information. It is not a direct competitor that should be expected to cost the same as the simpler route.

dotnet bin/Release/net9.0/EvidenceApi.dll measure 1000

Each route receives 50 warm-up requests before the measured batch. The measured work includes the HTTP request, routing, lookup, checks applicable to that route, JSON serialization, response-body transfer and client-side JSON parsing. Client and server run in the same process on loopback. Application startup is outside the measured interval.

The runner checks response success throughout and validates the resolved bundle label. It records total response bytes and elapsed time for the batch. That is enough to describe this bounded operation, but it is not a load test or a model-quality evaluation.

The integration owner ran the command in the exclusive measurement lane. The result cannot establish how the system scales to a large corpus or a remote deployment. The ledger contains only a few immutable records, and the requests are sequential.

This is an appropriate first measurement because it explains the cost of the mechanism actually implemented. A future indexed store, permission service or remote dependency would introduce a different boundary and require its own comparison.

The observed cost belongs to this small HTTP contract

02 / See the idea

Two real HTTP boundaries, two payloads

Exact source median177.053milliseconds per1000requests, resolved claim median197.028milliseconds; payloads366and764bytes perrequest. These are batch medians, not latency percentiles or answer-quality measures.

The claim route validates more conditions and returns a larger body than exact source retrieval. Their difference cannot isolate the cost of hashing or date checks because serialization, transfer and parsing also differ.

Interpretation and evidence
  • Keep lookup timing separate from semantic quality.
  • Compare the actual payload and work performed.
  • Do not turn three batch averages into a latency percentile.
production/M32/benchmark-results.json; three real process runs, all exit0 and responses valid.

Three fresh processes each executed both route measurements on runtime 9.0.4. All processes exited successfully, and all six batches returned successful, validated responses. Each batch contained 1,000 measured requests after 50 warm-up requests. The retained output is production/M32/benchmark-results.json.

Route boundary Median batch time Range across three batches Median average per request Response bytes per request
Exact source record 177.053 ms 176.594–187.651 ms 0.177053 ms 366
Resolved claim bundle 197.028 ms 196.820–210.556 ms 0.197028 ms 764

These are medians of batch-level observations, not request-latency percentiles. The response-byte figure describes the body read by the client, not a complete network accounting that includes transport and HTTP overhead.

The claim route performed more validation and returned a larger document. Its higher median here does not isolate the cost of hashing, checking a date or resolving an evidence link. Serialization, transfer and parsing differ too. To attribute the difference to one mechanism would require an additional controlled experiment; this comparison does not provide that evidence.

The routes also ran in a fixed order within each process. The source route preceded the claim route, and each received its own warm-up. Three process repetitions expose some variability, but they do not eliminate runtime warm-up effects or uncontrolled activity elsewhere on the shared developer machine.

The useful conclusion is correspondingly narrow. The real API completed these small sequential lookup and resolution workloads, with the measured cost and payload sizes shown above. It did not measure a hosted model, the truth of a generated answer, concurrent capacity or a production user's network experience. The correctness checks, rather than the stopwatch, establish the documented refusal behavior.

This is still a meaningful step beyond a diagram of an evidence service. We now have executable routes, a defined corpus, actual rejection cases and raw timing records. Those artifacts allow the next implementation decision to begin with a concrete boundary instead of an assumption that the proposed design must be inexpensive or correct.

Keep the next implementation smaller than the promise

The most useful next step is not necessarily a chat interface. It may be replacing the fixtures with a handful of the publication's own claims and manually checked evidence references. That would expose editorial questions the current HTTP tests cannot answer: how precise a locator should be, how a correction supersedes an earlier assertion, and who confirms that a passage still supports a claim.

An external-source ingestion path would require additional controls. The current service accepts identifiers, not arbitrary URLs, and performs no network fetch on a claim request. It should not acquire unrestricted browsing merely because another component would find that convenient.

Likewise, a private corpus would require an identity and authorization design. A public synthetic fixture cannot demonstrate that another reader is entitled to see a real unpublished draft. Permission checks would need to occur before a bundle is returned and before its content is available to a generating system.

Those are further obligations, not defects to conceal with broader language. The present implementation demonstrates a small, real part of a citation-first architecture. It can name a version, preserve different kinds of date, detect an inconsistent representation and refuse to pretend that missing historical evidence exists.

A trustworthy knowledge interface grows by making each added promise observable. Its strength comes from the boundaries it can explain as clearly as from the answers it can return.

Historical versions and verification boundaries

These pins existed before the context date. Availability and static API checks do not establish a successful build or a production-safe patch level. Execution claims are limited to explicitly identified companion checks.

ComponentVersionAvailable bySource
.NET SDK10.0.1002025-11-11Primary record
.NET runtime10.0.02025-11-11Primary record
ASP.NET Core shared framework10.0.02025-11-11Primary record
C#12.02024-12-03Primary record
ModelContextProtocol1.0.02026-03-05Primary record

Download the complete source API laboratory and execution records.

THE AUTHOR

Marcin Borecki

Developer, entrepreneur and business angel. Writing about AI, software, data and the evidence behind technical decisions.

About the author →