BlogDoFT.Ciir 0.5.0

dotnet tool install --global BlogDoFT.Ciir --version 0.5.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local BlogDoFT.Ciir --version 0.5.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=BlogDoFT.Ciir&version=0.5.0
                    
nuke :add-package BlogDoFT.Ciir --version 0.5.0
                    

CIIR — Code Intelligence Intermediate Representation

A .NET application that statically analyzes C# source code and produces CIIR (Code Intelligence Intermediate Representation): a normalized, language-independent representation of analyzed code, intended as the input contract for downstream code-intelligence tooling — embedding generation, dependency/call graphs, architectural analysis, impact analysis, and documentation generation.

See docs/ciir-specification.md for what the CIIR concepts mean, and schemas/ciir.schema.json for the formal contract of what a valid CIIR document looks like.

Architecture

Hexagonal (ports & adapters). The core owns the domain logic ("analyze source → produce CIIR") independent of any delivery mechanism or I/O technology:

flowchart TD
    subgraph EntryPoints["Entry points"]
        CLI["CLI"]
        API["future API"]
    end

    CLI --> App["Application"]
    API --> App

    App --> Analyzer["Analyzer Contract"]
    App --> Writers["Writers"]

    Roslyn["C# / Roslyn"] -. implements .-> Analyzer
Project Role
src/Ciir.Core The CIIR model itself (records, enums, identity hashing, embeddingText generation). No dependency on Roslyn, the CLI, or any serialization technology.
src/Ciir.Application Ports (ICodeAnalyzer, ICiirWriter, IInputResolver, ...) and the main use case (AnalyzeInputHandler). Depends only on Ciir.Core.
src/Ciir.CSharp The only project allowed to depend on Microsoft.CodeAnalysis*. Implements ICodeAnalyzer using Roslyn (syntax trees + semantic model + MSBuildWorkspace).
src/Ciir.Configuration Implements ICodeAnalyzer for appsettings*.json (parsed into configuration/configuration_key documents) and *.yaml/*.yml (captured as file metadata only). No dependency on Roslyn.
src/Ciir.Indexer.Client Implements ICiirUploader over HTTP: sends ciir.jsonl to the code-ciir-indexer (--send), negotiating a Keycloak token when asked. No dependency on Roslyn.
src/Ciir.Serialization JSONL writer, manifest.json/analysis-report.json writer, and the embedded ciir.schema.json. No dependency on Roslyn.
src/Ciir.Cli The composition root and command-line adapter. Contains no analysis logic — it parses arguments, wires dependency injection, and calls into Ciir.Application.

Dependency direction is enforced by convention (Ciir.Core never references Ciir.Application, Ciir.CSharp, Ciir.Serialization, or Ciir.Cli) and by an architecture test in Ciir.Application.Tests (see "Testing" below).

Build

Requires the .NET 10 SDK.

dotnet restore
dotnet build

The build is warning-free; analyzers (StyleCop, Roslynator, Meziantou) and TreatWarningsAsErrors are enabled solution-wide via Directory.Build.props.

Testing

dotnet test

Run a single test:

dotnet test --filter "FullyQualifiedName~CiirIdentityTests.ComputeId_IsDeterministic"

Test projects mirror src/:

  • Ciir.Core.Tests — identity hashing, modifier ordering, embeddingText generation.
  • Ciir.Application.Tests — input resolution, project discovery/deduplication, orchestration (AnalyzeInputHandler) with faked ports (no Roslyn involved), and the architecture-boundary test that fails the build if Ciir.Core/Ciir.Application ever reference Roslyn.
  • Ciir.CSharp.Tests — the real analysis pipeline against fixtures/BasicSolution, both through a hand-built Compilation (targets the semantic analysis logic in isolation — type/member discovery, relation/condition extraction, embeddingText) and through the real CSharpCodeAnalyzer/MSBuildWorkspace path the CLI actually uses; includes full ciir.schema.json validation and determinism checks.
  • Ciir.Configuration.Tests — appsettings*.json key flattening/value-type mapping and the never-leaks-a-value guarantee, YAML file-metadata capture, and schema-conformance checks.
  • Ciir.Indexer.Client.Tests — the HTTP uploader against a stubbed HttpMessageHandler: multipart shape/order, Bearer/anonymous/client-credentials flows (through the indexer's token gateway), error mapping.
  • Ciir.Serialization.Tests — JSONL serialization shape (camelCase, enum tokens, empty-collection omission) and schema-conformance tests (valid/invalid sample payloads for every required kind).
  • Ciir.Cli.Tests — runs the built ciir executable as a real subprocess and asserts on exit codes and console output (the splash screen and its opt-outs), plus unit tests for the splash screen itself.

Installation

ciir is published on nuget.org as the BlogDoFT.Ciir .NET tool (command: ciir). It needs the .NET 10 SDK (see "Prerequisites" below).

dotnet tool install --global BlogDoFT.Ciir      # install
dotnet tool update --global BlogDoFT.Ciir       # update
dotnet tool uninstall --global BlogDoFT.Ciir    # remove
ciir --version

As a repository-local tool (pins the version for everyone working on the repository):

dotnet new tool-manifest
dotnet tool install BlogDoFT.Ciir
dotnet tool run ciir <path>

Or run it once without installing it, using .NET 10's dnx:

dnx BlogDoFT.Ciir <path>

Prerequisites

The same notices are shown by the splash screen at the start of every run:

  • .NET 10 SDK — the runtime alone is not enough: ciir uses the SDK's MSBuild to open solutions and projects. Without an SDK, ciir exits with code 4 and analyzes nothing.
  • Restore the analyzed projects first (dotnet restore). ciir does not restore; without obj/project.assets.json, NuGet references do not resolve and many relations end up unresolved.
  • A global.json in the analyzed repository may select a different SDK than the one you expect (analysis of a net8.0 project pinned to SDK 8 was verified to work).

Running from source (development)

dotnet run --project src/Ciir.Cli -- <path> [options]

Or build and run the produced binary directly:

dotnet build src/Ciir.Cli
dotnet src/Ciir.Cli/bin/Debug/net10.0/ciir.dll <path> [options]

To try the packaged tool locally (the same check the release workflow runs):

dotnet pack src/Ciir.Cli -c Release -p:Version=0.0.0-local.1 -o artifacts
scripts/verify-tool-package.sh 0.0.0-local.1

Usage

ciir <path> [--output <path>] [--verbose] [--no-progress] [--no-banner] [--include-source] [--fail-on-error]
     [--send [<base-url>] --projectId <guid> [--token <jwt> | --clientId <id> --clientSecret <secret>] [--insecure]]

<path> may be:

  • a solution (*.sln / *.slnx) — every C# project it references is analyzed;
  • a project (*.csproj) — that project is analyzed;
  • a directory — recursively scanned for *.sln/*.slnx/*.csproj (skipping bin/, obj/, .git/, .vs/, node_modules/); a project already referenced by a discovered solution is never analyzed twice just because its .csproj was also found while scanning.

Regardless of which of the above <path> resolves to, every appsettings*.json and *.yaml/ *.yml file anywhere under the resolved analysis root is also captured (same directory exclusions as above) — see Configuration and file metadata.

Option Meaning
--output <path> Output directory (default: ./ciir-output).
--verbose Verbose diagnostic logging.
--no-progress Suppress progress reporting.
--no-banner Suppress the splash screen (also suppressed when the CIIR_NOLOGO environment variable is 1 or true).
--include-source Embed the literal source text of each entity's declaration.
--fail-on-error Exit with a non-zero code if any project fails to load/analyze.
-s, --send [<base-url>] Send the generated ciir.jsonl to the code-ciir-indexer after a successful analysis (see Sending to the indexer).
-pi, --projectId <guid> With --send: the id of the project already registered in the indexer. Required.
-ci, --clientId <id> With --send: Keycloak client id. Only used together with --clientSecret.
-cs, --clientSecret <secret> With --send: Keycloak client secret. Only used together with --clientId.
-t, --token <jwt> With --send: an access token sent as the Bearer token (wins over --clientId/--clientSecret).
--insecure With --send: do not validate the indexer's TLS/SSL certificate (self-signed, private CA, hostname mismatch). Insecure — prints a warning; use only on trusted networks.

Exit codes: 0 success, 1 a project failed under --fail-on-error, 2 invalid arguments/input, 3 writing the output failed, 4 the environment cannot run an analysis (no .NET SDK found), 5 the analysis succeeded but sending ciir.jsonl to the indexer failed.

Example:

ciir ./src --output ./artifacts/ciir --verbose
Sending to the indexer

--send uploads ciir.jsonl to the code-ciir-indexer (POST {base-url}/api/indexer/ciir-uploads), which makes a single pipeline step enough to analyze and publish:

ciir ./src --send https://blogdoft.home.arpa/code-brain --projectId 3f2b1c0e-... \
     --clientId ciir-pipeline --clientSecret "$CIIR_CLIENT_SECRET"

# or, with the base URL taken from the environment and no authentication
CIIR_BASE_URL=http://localhost:5223 ciir ./src -s --projectId 3f2b1c0e-...
  • Base URL: the value of --send, otherwise the CIIR_BASE_URL environment variable; if neither is set, or the value is not an absolute http/https URL, the run fails with exit code 2 before analyzing. Because the value is optional, put <path> before -s (ciir ./src -s).
  • Project: --projectId is required — the indexer rejects uploads to unknown projects.
  • Authentication: --token is sent as Authorization: Bearer. Otherwise, when both --clientId and --clientSecret are given, ciir exchanges them for a token at the indexer's own gateway (POST {base-url}/api/indexer/auth/token) — it never talks to Keycloak itself (the Keycloak client must be confidential with Service accounts enabled). With neither, the file is sent without authentication. Giving only one of --clientId/--clientSecret prints a warning and is treated as neither.
  • TLS: the indexer's certificate is validated by default. When the runner does not trust it (self-signed or private CA — typically The SSL connection could not be established), install the CA on the runner or, as a last resort, pass --insecure, which skips certificate validation for the token and upload requests.
  • The upload only happens after a successful analysis; on success ciir prints the uploadId and status the indexer returned (it does not wait for the indexer to process the file).

See .specs/03-send-to-indexer.md.

Every analysis run starts with a splash screen — the tool name and BlogDoFT in ASCII art, a link to the blog, and the prerequisite notices — followed by the tool's own output. --help and --version never print it, so their output stays script-friendly:

  ____  ___  ___  ____
 / ___||_ _||_ _||  _ \
| |     | |  | | | |_) |
| |___  | |  | | |  _ <
 \____||___||___||_| \_\
  Code Intelligence IR - v0.2.0

 ____   _                 ____          _____  _____
| __ ) | |  ___    __ _  |  _ \   ___  |  ___||_   _|
|  _ \ | | / _ \  / _` | | | | | / _ \ | |_     | |
| |_) || || (_) || (_| | | |_| || (_) ||  _|    | |
|____/ |_| \___/  \__, | |____/  \___/ |_|      |_|
                  |___/

  https://www.blogdoft.com.br/

  Before you start:
   - Requires the .NET 10 SDK (the runtime alone is not enough).
   - Analyzed projects must be restored first (dotnet restore); ciir will not.
   - A global.json in the analyzed repository may select a different SDK.

Discovering projects...
Found 1 project(s).

Generated files

Every run produces, under --output:

ciir-output/
  ciir.jsonl              # one CIIR record per line (JSON Lines — never a JSON array)
  ciir.schema.json         # the CIIR v1 contract, copied verbatim from this repository
  manifest.json            # generator/input/project/file metadata + aggregate statistics
  analysis-report.json     # operational outcome: project/document/relation counts, errors

ciir.jsonl example

{"schemaVersion":"1.0","id":"sha256:...","kind":"method","language":"csharp","project":"Payments.Application","symbol":{"name":"AuthorizeAsync","qualifiedName":"Payments.Application.PaymentService.AuthorizeAsync","canonicalName":"Payments.Application.PaymentService.AuthorizeAsync(Payments.Domain.Order)","container":"Payments.Application.PaymentService"},"method":{"accessibility":"public","modifiers":["async"],"parameters":[{"name":"order","type":"Payments.Domain.Order"}],"returnType":"System.Threading.Tasks.Task<Payments.Domain.PaymentResult>","embeddingReturnType":"Payments.Domain.PaymentResult"},"relations":[{"kind":"calls","target":{"symbol":"Payments.Domain.IPaymentGateway.AuthorizeAsync(Payments.Domain.Order)"},"resolution":{"status":"resolved","origin":"project"}}],"embeddingText":"Entity: method\nQualified name: Payments.Application.PaymentService.AuthorizeAsync\n...","embeddingTextStrategy":"semantic-v1","embeddingTextHash":"sha256:..."}

Properties with no content are omitted rather than serialized as empty structures.

Validating ciir.jsonl against the schema

Any JSON Schema (2020-12) validator works, in any language. For example, with Node's ajv-cli:

npm install -g ajv-cli ajv-formats
while IFS= read -r line; do echo "$line"; done < ciir-output/ciir.jsonl | \
  ajv validate -s ciir-output/ciir.schema.json -d /dev/stdin --spec=draft2020

Or with Python's jsonschema:

import json, jsonschema
schema = json.load(open("ciir-output/ciir.schema.json"))
with open("ciir-output/ciir.jsonl") as f:
    for line in f:
        jsonschema.validate(json.loads(line), schema)

The same validation runs as part of this repository's own test suite (Ciir.Serialization.Tests.SchemaConformanceTests, Ciir.CSharp.Tests integration tests).

Versioning

schemaVersion follows semantic-ish rules: backward-compatible additions bump the minor version (1.0 → 1.1); incompatible changes (removing/renaming a property, changing its meaning) require a major version bump (2.0). The semantics of an existing property are never changed silently.

Building a new language generator

The CIIR contract (schemas/ciir.schema.json + docs/ciir-specification.md) is designed to be implemented independently of this C# generator. A new generator needs to:

  1. Produce documents that validate against schemas/ciir.schema.json.
  2. Compute the deterministic id the same way: sha256(<language>|<project>|<kind>|<canonicalSymbolIdentity>) (see Ciir.Core.Identity.CiirIdentity for the reference implementation).
  3. Follow the semantic rules in docs/ciir-specification.md — in particular, resolve relations from real semantic information (never text/regex-based inference), never fabricate a relation without static evidence, and keep embeddingText a filtered projection rather than a full copy of the document.

Within this repository, the pattern to follow is Ciir.Application.Ports.ICodeAnalyzer: implement it in a new adapter project (analogous to Ciir.CSharp or Ciir.Configuration), and register it in the CLI's composition root (Ciir.Cli/Composition/ServiceCollectionExtensions.cs) — no other project needs to change. AnalyzeInputHandler dispatches to whichever registered analyzer's CanAnalyze claims a given path, so multiple analyzers can coexist.

Known limitations

  • Relation extraction from property/indexer accessors is limited to expression-bodied properties; block-bodied get/set accessors are not walked.
  • Field/property initializer expressions are not analyzed for relations.
  • Static constructors are not represented as constructor documents.
  • No interprocedural or cross-project data-flow analysis; every relation is statically observable within the analyzed project's own compilation.
  • YAML content is never structurally parsed, by design — only file-level metadata (path, hash, size) is captured, since YAML serves too many unrelated purposes (CI, docker-compose, Kubernetes manifests, ...) to model with one schema.
  • Configuration key capture never includes the value itself, only its key path and JSON type — a deliberate privacy guarantee, since appsettings*.json commonly holds secrets.
  • Out of scope for this phase entirely (see the specification): a database backend, real embeddings/LLM calls, a REST API, a graph database, and analyzers for programming languages other than C#.

What's intentionally not implemented yet

Per the project's own specification, this phase deliberately excludes: PostgreSQL/pgvector, real embedding generation or any LLM call, a REST API, Kubernetes/queue integration, a graph database, graph traversal, business-rule extraction, dynamic/runtime instrumentation, a fully serialized control-flow graph, and analyzers for Java/JavaScript/Python. The architecture (ports defined in Ciir.Application) is built so these can be added later as new adapters without changing the core.

Releasing

Releases are tag-driven (see .specs/02-dotnet-tool.md): pushing a vX.Y.Z tag to Forgejo, then mirroring it to GitHub (the mirror workflow can be run manually), triggers .github/workflows/release.yml — build, test, pack, verify the package, and, after approval on the nuget environment, publish to nuget.org. Versions are derived by GitVersion from tags and Conventional Commits.

License

GPL-3.0-only.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.5.0 129 9/27/2026
0.4.0 106 9/27/2026
0.3.0 114 9/20/2026