From ccbebadd3c9d4baa0d9f6ee7d64fcd6054920b92 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 24 May 2026 19:33:22 +0200 Subject: [PATCH] Add DARC validation flag for operator docs (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new @DarcValidated annotation on operator functions records that a reviewer (not the original author) has gone through Document / Assess / Research / Code for the function's documentation. The KSP processor threads it through operators.json into a badge above each function's signature on the generated page (✅ DARC-validated / ⚠ prose stub / ✖ generated facts only) and into a new "Validated" column on the operator coverage matrix. Single source of truth in code, queryable in the JSON, enforceable in CI. Why an annotation and not a partial-side flag: validation is something the build can enforce. Keeping it in source means a doc-only review costs a KSP recompile and an operators.json diff — the accepted price for one authoritative location. New Antora page contributing/darc-workflow.adoc defines DARC for the project end to end (general workflow + operator-doc specialisation + criteria for setting the annotation). It is self-authoritative; the GitHub wiki is no longer treated as the canonical source. The darc_feature_request issue template was renamed from Define/Contribute to Document/Code so it matches. Bundled: operator-doc-schema-v1.json caught up with what the processor actually emits (composite modality, inherited backend status, version=unknown, description note type) — validateOperatorSchema goes from 0/4 valid to 4/4. Also drops contributing/native-ffm-plan.adoc from the published site (it read as a PRD/issue, not user-facing reference docs); its content is preserved as an untracked draft at the repo root and the 9 incoming xrefs were surgically updated. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ISSUE_TEMPLATE/darc_feature_request.md | 4 +- .../main/kotlin/GenerateDocumentationTask.kt | 76 ++++- .../main/kotlin/models/DocumentationModels.kt | 7 +- .../schemas/operator-doc-schema-v1.json | 30 +- docs/modules/ROOT/images/darc-logo.png | Bin 0 -> 32685 bytes docs/modules/ROOT/nav.adoc | 2 +- .../pages/contributing/darc-workflow.adoc | 229 ++++++++++++++ .../ROOT/pages/contributing/index.adoc | 1 - .../pages/contributing/matmul-kernels.adoc | 4 +- .../pages/contributing/native-ffm-plan.adoc | 285 ------------------ .../perf/quantized-simd-kernels.adoc | 4 +- .../pages/explanation/perf/simd-kernels.adoc | 5 +- .../ROOT/pages/reference/architecture.adoc | 6 +- .../reference/operators/generated/index.adoc | 2 +- .../operators/generated/similarity.adoc | 2 + .../operators/generated/tensorops.adoc | 229 ++++++++++++++ .../pages/reference/ops-status-matrix.adoc | 128 ++++---- .../sk/ainet/lang/tensor/ops/TensorOps.kt | 2 + .../kotlin/sk/ainet/lang/ops/DarcValidated.kt | 33 ++ .../lang/ops/ksp/OperatorDocProcessor.kt | 78 ++++- .../lang/ops/metadata/DocumentationModels.kt | 7 +- .../lang/ops/ksp/OperatorDocProcessorTest.kt | 80 +++++ 22 files changed, 824 insertions(+), 390 deletions(-) create mode 100644 docs/modules/ROOT/images/darc-logo.png create mode 100644 docs/modules/ROOT/pages/contributing/darc-workflow.adoc delete mode 100644 docs/modules/ROOT/pages/contributing/native-ffm-plan.adoc create mode 100644 skainet-lang/skainet-lang-ksp-annotations/src/commonMain/kotlin/sk/ainet/lang/ops/DarcValidated.kt diff --git a/.github/ISSUE_TEMPLATE/darc_feature_request.md b/.github/ISSUE_TEMPLATE/darc_feature_request.md index e1475b3bb..59862c892 100644 --- a/.github/ISSUE_TEMPLATE/darc_feature_request.md +++ b/.github/ISSUE_TEMPLATE/darc_feature_request.md @@ -6,7 +6,7 @@ labels: enhancement assignees: "" --- -# 🧠 D: DEFINE — Problem & Opportunity +# 🧠 D: DOCUMENT — Problem & Opportunity **What is the problem, limitation, or opportunity? Why does this matter for SKaiNET?** @@ -60,7 +60,7 @@ Document research tasks or open questions that must be answered before implement --- -# 🛠️ C: CONTRIBUTE — Implementation Plan +# 🛠️ C: CODE — Implementation Plan Break down actionable steps required to deliver this feature: diff --git a/build-logic/convention/src/main/kotlin/GenerateDocumentationTask.kt b/build-logic/convention/src/main/kotlin/GenerateDocumentationTask.kt index 459d3fa68..dabbae093 100644 --- a/build-logic/convention/src/main/kotlin/GenerateDocumentationTask.kt +++ b/build-logic/convention/src/main/kotlin/GenerateDocumentationTask.kt @@ -108,10 +108,20 @@ abstract class GenerateDocumentationTask : DefaultTask() { .toSortedSet() .toList() - // Row view: (operator, function) pair -> per-backend status. - data class Row(val operator: String, val function: String, val status: Map) + // Row view carries the per-function status plus DARC validation + // signal so the matrix can render both in one pass. + val partialsRoot = derivePartialsRoot(outputDir) + data class Row( + val operator: String, + val function: FunctionDoc, + val hasPartial: Boolean, + ) val rows: List = module.operators.flatMap { op -> - op.functions.map { fn -> Row(op.name, fn.name, fn.statusByBackend) } + op.functions.map { fn -> + val relative = "ops/${op.name.lowercase()}/${fn.name.lowercase()}.adoc" + val present = partialsRoot?.let { File(it, relative).isFile } == true + Row(op.name, fn, present) + } } matrixFile.writeText(buildString { @@ -120,7 +130,7 @@ abstract class GenerateDocumentationTask : DefaultTask() { appendLine("") appendLine("Generated from `operators.json` version `${module.version}` on ${formatTimestamp(module.timestamp)}.") appendLine("") - appendLine("Rows are `Operator.function` pairs; columns are backends that appear in any function's `statusByBackend` map. A missing entry means the backend makes no claim about the function — treat it as \"unknown\", not \"not supported\".") + appendLine("Rows are `Operator.function` pairs. The `Validated` column shows whether the function's documentation has been DARC-validated by a reviewer (see xref:contributing/darc-workflow.adoc[DARC workflow]). Remaining columns are backends that appear in any function's `statusByBackend` map — a missing entry means the backend makes no claim about the function (treat it as \"unknown\", not \"not supported\").") appendLine("") if (rows.isEmpty() || allBackends.isEmpty()) { appendLine("NOTE: No backend status information found in the source data.") @@ -128,32 +138,32 @@ abstract class GenerateDocumentationTask : DefaultTask() { return@buildString } - // Table header: 1 col for the row label + 1 col per backend. - val colSpec = (listOf("2") + List(allBackends.size) { "1" }).joinToString(",") + // Header: row label, validation column, then one column per backend. + val colSpec = (listOf("2", "1") + List(allBackends.size) { "1" }).joinToString(",") appendLine("[cols=\"$colSpec\", options=\"header\"]") appendLine("|===") - append("| Operator.function ") + append("| Operator.function | Validated ") allBackends.forEach { append("| $it ") } appendLine("") appendLine("") rows.forEach { row -> - append("| `${row.operator}.${row.function}` ") + append("| `${row.operator}.${row.function.name}` ") + append("| ${darcCell(row.function, row.hasPartial)} ") allBackends.forEach { backend -> - val raw = row.status[backend] + val raw = row.function.statusByBackend[backend] val cell = if (raw == null) "—" else shortStatus(raw) append("| $cell ") } appendLine("") } - // Totals footer: number of "done" rows per backend out - // of total row count. A status counts as done when it - // maps to the green check in shortStatus. + // Totals footer: validated count followed by per-backend done count. appendLine("") - append("| *Done* ") + val validatedCount = rows.count { it.function.validated } + append("| *Done* | *$validatedCount / ${rows.size}* ") allBackends.forEach { backend -> - val n = rows.count { isDone(it.status[backend]) } + val n = rows.count { isDone(it.function.statusByBackend[backend]) } append("| *$n / ${rows.size}* ") } appendLine("") @@ -163,6 +173,42 @@ abstract class GenerateDocumentationTask : DefaultTask() { }) } + /** + * One-line DARC validation badge rendered above each function's + * signature on the generated page. + * + * Three states, ordered from strongest to weakest signal: + * - validated: a reviewer (not the original author) has signed off + * on the partial prose. Carries the validator and date. + * - prose without validation: a partial exists but no + * `@DarcValidated` annotation backs it. Treated as a stub. + * - no prose: only auto-generated facts (signature, parameters, + * return type). The reader is told explicitly so they don't + * mistake terseness for completeness. + * + * The bracketed CSS class is a hook for the Antora UI bundle to + * style the badge; the visible text is the contract. + */ + private fun darcBadge(function: FunctionDoc, hasPartial: Boolean): String = when { + function.validated -> { + val on = function.validatedOn.ifBlank { "an unspecified date" } + val by = function.validatedBy.ifBlank { "an unspecified reviewer" } + "[.darc-validated]#✅ DARC-validated by $by on $on#" + } + hasPartial -> "[.darc-stub]#⚠ Prose present but not DARC-validated#" + else -> "[.darc-none]#✖ Generated facts only (no human prose)#" + } + + /** + * Emoji-only DARC status for the coverage matrix column. + * Matches the three branches of [darcBadge]. + */ + private fun darcCell(function: FunctionDoc, hasPartial: Boolean): String = when { + function.validated -> "✅" + hasPartial -> "⚠" + else -> "✖" + } + /** * Short emoji-only rendering of a backend status, for use in the * compact matrix cells. @@ -316,6 +362,8 @@ abstract class GenerateDocumentationTask : DefaultTask() { builder.apply { appendLine("== ${function.name}") appendLine("") + appendLine(darcBadge(function, hasPartial)) + appendLine("") appendLine("=== Signature") appendLine("") appendLine("[source,kotlin]") diff --git a/build-logic/convention/src/main/kotlin/models/DocumentationModels.kt b/build-logic/convention/src/main/kotlin/models/DocumentationModels.kt index fd176a336..7b8717d9f 100644 --- a/build-logic/convention/src/main/kotlin/models/DocumentationModels.kt +++ b/build-logic/convention/src/main/kotlin/models/DocumentationModels.kt @@ -29,7 +29,12 @@ data class FunctionDoc( val parameters: List = emptyList(), val returnType: String, val statusByBackend: Map = emptyMap(), - val notes: List = emptyList() + val notes: List = emptyList(), + val validated: Boolean = false, + val validatedBy: String = "", + val validatedOn: String = "", + val validatedCommit: String = "", + val referencesChecked: Boolean = true, ) @Serializable diff --git a/build-logic/convention/src/main/resources/schemas/operator-doc-schema-v1.json b/build-logic/convention/src/main/resources/schemas/operator-doc-schema-v1.json index 05e361e07..53b50512e 100644 --- a/build-logic/convention/src/main/resources/schemas/operator-doc-schema-v1.json +++ b/build-logic/convention/src/main/resources/schemas/operator-doc-schema-v1.json @@ -13,8 +13,8 @@ }, "version": { "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", - "description": "Semantic version of the framework" + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$|^unknown$", + "description": "Semantic version of the framework, or 'unknown' when the KSP processor was invoked without the skainet.version option (e.g. from a unit-test fixture)." }, "commit": { "type": "string", @@ -57,7 +57,7 @@ }, "modality": { "type": "string", - "enum": ["core", "vision", "nlp"], + "enum": ["core", "composite", "vision", "nlp"], "description": "Modality category of the operator" }, "functions": { @@ -101,7 +101,7 @@ "patternProperties": { "^[a-zA-Z][a-zA-Z0-9_]*$": { "type": "string", - "enum": ["implemented", "not_implemented", "in_progress"], + "enum": ["implemented", "inherited", "in_progress", "not_implemented"], "description": "Implementation status for this backend" } }, @@ -114,6 +114,26 @@ "$ref": "#/$defs/Note" }, "description": "Array of notes associated with the function" + }, + "validated": { + "type": "boolean", + "description": "Whether the function's documentation has been DARC-validated by a reviewer. Sourced from the @DarcValidated annotation on the function." + }, + "validatedBy": { + "type": "string", + "description": "Identity of the reviewer who DARC-validated this function's documentation." + }, + "validatedOn": { + "type": "string", + "description": "ISO-8601 date the documentation was DARC-validated." + }, + "validatedCommit": { + "type": "string", + "description": "Optional short git SHA pinning the validated prose to a specific revision." + }, + "referencesChecked": { + "type": "boolean", + "description": "Whether the reviewer verified that every citation in the partial still resolves and supports its claim." } }, "required": ["name", "signature", "parameters", "returnType", "statusByBackend", "notes"], @@ -145,7 +165,7 @@ "properties": { "type": { "type": "string", - "enum": ["owner", "issue"], + "enum": ["owner", "issue", "description"], "description": "Type of the note" }, "backend": { diff --git a/docs/modules/ROOT/images/darc-logo.png b/docs/modules/ROOT/images/darc-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..393abf9dae4c20f0443ef026ffffec174873980a GIT binary patch literal 32685 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 KNYzW2hWtM|xy=^< literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 136a3b98e..7899d82ce 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -33,9 +33,9 @@ .Contributing * xref:contributing/index.adoc[Audience and scope] +* xref:contributing/darc-workflow.adoc[DARC: advanced contribution workflow] * xref:contributing/build-from-source.adoc[Build from source] * xref:contributing/dtype-model.adoc[The SKaiNET dtype model] * xref:contributing/benchmarks.adoc[Engine benchmark program] * xref:contributing/matmul-kernels.adoc[Reading the matmul benchmark] * xref:contributing/register-bench-runner.adoc[Register a self-hosted bench runner] -* xref:contributing/native-ffm-plan.adoc[Plan: native FFM kernel provider] diff --git a/docs/modules/ROOT/pages/contributing/darc-workflow.adoc b/docs/modules/ROOT/pages/contributing/darc-workflow.adoc new file mode 100644 index 000000000..d552fb2e3 --- /dev/null +++ b/docs/modules/ROOT/pages/contributing/darc-workflow.adoc @@ -0,0 +1,229 @@ += DARC: Advanced Contribution Workflow +:description: SKaiNET's general Document / Assess / Research / Code workflow for advanced contributions, plus its specialisation for operator documentation. + +image::darc-logo.png[DARC,240] + +[NOTE] +==== +**Audience: SKaiNET maintainers and contributors taking on non-trivial +work.** Library *consumers* do not need this page. Routine fixes — a +typo, an obvious bug, a CI tweak — also do not need DARC; the workflow +exists for contributions where the open question "is this even the +right thing to build?" is real. +==== + +== What DARC is + +DARC is SKaiNET's cyclical, document-driven workflow for advanced +contributions: new operators, new modules, algorithmic changes, kernel +strategies, format readers, anything where the design decisions matter +as much as the code. This page is the authoritative definition — the +four phases, when they apply, and how their outcomes get encoded in +source. + +DARC is *cyclical* and *document-driven*: the prose is the deliverable +that survives the iteration, the code follows. Teams can enter at any +phase and revisit earlier ones — a code review that surfaces a stale +assumption sends the work back to Research, not forward to release. + +== The four phases + +[cols="1,4",options="header"] +|=== +| Phase | Purpose + +| *D — Document* +| Capture *what* and *why* in source-controlled prose before the code +exists. The artefact lives in `docs/modules/ROOT/...` as AsciiDoc, or +on the relevant feature proposal issue (see the +https://github.com/SKaiNET-developers/SKaiNET/blob/develop/.github/ISSUE_TEMPLATE/darc_feature_request.md[DARC +Feature Proposal] template). Documentation is a first-class part of +the design, not a write-up of an already-shipped decision. + +| *A — Assess* +| Validate the proposal against ground truth: reference implementations +(PyTorch, JAX, NumPy), benchmarks, numerical-stability edge cases, +cross-platform behaviour (JVM, Android, native). The output is a +decision: approve, send back to Research, or back to Document if the +problem statement turned out to be wrong. + +| *R — Research* +| Survey existing solutions, dependencies, prior art, papers. Resolve +the open questions raised in Document and Assess. Every claim that +ends up in the final prose has a citation a reader can follow. + +| *C — Code* +| Implement against the documented design with Gitflow, KtLint / +Detekt, unit and integration tests, CI green. The Code phase produces +running software, but the *design* it implements was settled in the +prior phases — Code is execution, not invention. +|=== + +Repeat until the artefact converges. The minimum is one full cycle; +in practice most non-trivial contributions go through the loop more +than once. + +== When DARC applies + +[cols="1,1",options="header"] +|=== +| Use DARC when | Skip DARC when + +| Adding a new operator, layer, module, format reader, or backend. +| Fixing a typo or an obvious one-line bug. + +| Changing public API surface or numerical behaviour of existing code. +| Reformatting, renaming a variable, removing dead code. + +| Introducing a new dependency, build tool, or CI workflow. +| Bumping a dependency version with no behavioural change. + +| Algorithmic work where the design decisions are not self-evident from +the diff (kernel strategies, quantisation schemes, attention variants). +| Adding a missing test for already-shipped behaviour. +|=== + +The signal for "DARC applies" is whether a reasonable maintainer six +months from now would want to know *why* the change is shaped the way +it is. If yes, the work belongs in DARC; if the diff speaks for +itself, it doesn't. + +== Specialisation: operator documentation + +Operator documentation is the largest single application of DARC in +the project today — a four-figure number of method-level pages, each +fusing auto-derived API facts with human-written math, intuition, +examples, and references. Because the surface is huge and uneven, +operator docs are the only DARC artefact that carries a +machine-checkable validation flag. + +=== Mapping DARC to operator-doc work + +[cols="1,4",options="header"] +|=== +| Phase | What it means for an operator + +| *D — Document* +| Write or refresh the partial at +`docs/modules/ROOT/partials/ops/\{operator}/\{function}.adoc`. The +partial is sliced into the generated reference page by the `math`, +`intuition`, `examples`, `references` AsciiDoc tags. + +| *A — Assess* +| Validate the prose against ground truth: a reference implementation +(PyTorch / JAX / NumPy), known numerical-stability edge cases, and +SKaiNET's runtime. Run every documented example end-to-end; shapes, +dtypes, and numerical answers must match. + +| *R — Research* +| Every non-trivial claim has a citation. Reviewer clicks every link +and confirms it still resolves and supports the claim. Dead or +drifted citations are a hard fail. + +| *C — Code* +| Confirm the implementation matches the documented contract. +`statusByBackend` reflects reality — no `inherited` rows that should +be `implemented`, no `implemented` rows whose backend currently +throws. +|=== + +=== The `@DarcValidated` annotation + +When an operator function has passed DARC end-to-end, the *reviewer* +(not the original author) annotates the function in source: + +[source,kotlin] +---- +import sk.ainet.lang.ops.DarcValidated + +public interface TensorOps { + @DarcValidated(by = "First Last ", on = "2026-05-24") + public fun matmul(a: Tensor, b: Tensor): Tensor +} +---- + +The KSP processor (`OperatorDocProcessor`) picks up the annotation +and threads it through `operators.json` into the generated reference +page and the coverage matrix: + +[cols="1,4",options="header"] +|=== +| Signal | Meaning + +| `✅ DARC-validated by … on …` +| `@DarcValidated` is present. A reviewer has gone through all four +phases. + +| `⚠ Prose present but not DARC-validated` +| A partial exists at `partials/ops/\{op}/\{fn}.adoc` but no +annotation backs it. Treat the prose as a stub. + +| `✖ Generated facts only (no human prose)` +| No partial on disk and no annotation. The reader sees only the +auto-derived signature, parameters, return type, and backend status. +|=== + +The same three states appear as `✅ / ⚠ / ✖` in the *Validated* column +of xref:reference/ops-status-matrix.adoc[the operator coverage matrix]. + +=== Criteria for setting `@DarcValidated` + +All of the following must be true before the annotation goes in: + +. The reviewer is not the original author. Two pairs of eyes on the + prose is the entire point. +. *D — Document.* The partial has non-empty `math`, `intuition`, + `examples`, and `references` tags. None are placeholders. +. *A — Assess.* Every example in the partial has been executed + against the SKaiNET implementation and produces the documented + result. Boundary cases for any numerical-stability claims have + been spot-checked. +. *R — Research.* Every citation resolves and supports the specific + claim it backs. Broken or drifted links are a hard fail. +. *C — Code.* `statusByBackend` reflects reality. + +Set `referencesChecked = false` only as a deliberate signal — e.g. a +citation behind a paywall the reviewer could not open. The badge +still renders as validated; the flag is there so a future reviewer +knows what was skipped. + +=== Updating an already-validated operator + +Editing the prose of a `@DarcValidated` function invalidates the +validation. The contributor making the change must: + +. Remove the `@DarcValidated` annotation in the same change that + edits the partial. The page falls back to the ⚠ badge until a + fresh review. +. Open a new review request once the change is in. + +This is the price of keeping the validation signal in code: any +prose change has to flow through a fresh review before the badge +comes back. + +== Where DARC lives in the repo + +[cols="2,3",options="header"] +|=== +| Location | Role + +| This page (`docs/modules/ROOT/pages/contributing/darc-workflow.adoc`) +| Authoritative definition of the workflow, when it applies, and how +its outcomes are encoded. + +| `.github/ISSUE_TEMPLATE/darc_feature_request.md` +| Issue template for new DARC proposals. Section headers match the +four phases: Document / Assess / Research / Code. + +| `skainet-lang-ksp-annotations/.../DarcValidated.kt` +| The annotation that records a passed DARC review on an operator +function. `SOURCE` retention, `FUNCTION` target. + +| `skainet-lang-ksp-processor/.../OperatorDocProcessor.kt` +| Reads `@DarcValidated` per function and writes the validation +fields into `operators.json`. + +| `build-logic/convention/.../GenerateDocumentationTask.kt` +| Renders the badge above each function's signature and the +*Validated* column in the coverage matrix. +|=== diff --git a/docs/modules/ROOT/pages/contributing/index.adoc b/docs/modules/ROOT/pages/contributing/index.adoc index 4e6320f27..5d1d3fbb8 100644 --- a/docs/modules/ROOT/pages/contributing/index.adoc +++ b/docs/modules/ROOT/pages/contributing/index.adoc @@ -32,7 +32,6 @@ Concretely: | xref:contributing/benchmarks.adoc[Engine benchmark program] | The Phoronix Test Suite / OpenBenchmarking publication path: methodology, manifest, lanes, CI workflow, replay. | xref:contributing/matmul-kernels.adoc[Reading the matmul benchmark] | What the published numbers mean — scalar vs. Panama vs. quantized regimes, roofline reasoning, and a checklist for interpreting any new measurement. | xref:contributing/register-bench-runner.adoc[Register a self-hosted bench runner] | The one-time operator setup that lights up the full-publish CI lane on a Linux x86 box. -| xref:contributing/native-ffm-plan.adoc[Plan: native FFM kernel provider] | Forward-looking design doc for the priority-100 kernel provider that closes the gap to native BLAS. |=== == What this section deliberately does *not* cover diff --git a/docs/modules/ROOT/pages/contributing/matmul-kernels.adoc b/docs/modules/ROOT/pages/contributing/matmul-kernels.adoc index 4f546793e..66d6c2d41 100644 --- a/docs/modules/ROOT/pages/contributing/matmul-kernels.adoc +++ b/docs/modules/ROOT/pages/contributing/matmul-kernels.adoc @@ -314,8 +314,7 @@ This kernel is now **compute-bound on a single core, near the single-core ceiling**. The remaining headroom requires architectural moves: AVX-512 (16 lanes — 2× headroom; needs a wider microkernel set), multi-threading (6 cores — 6× headroom; needs `ith`/`nth` -SPI), or a hand-tuned native kernel via FFM -(xref:contributing/native-ffm-plan.adoc[]). +SPI), or a hand-tuned native kernel via FFM. If you take only one comparison away from this page: **the scalar → Panama jump (1× → 13×) is the biggest single performance @@ -540,4 +539,3 @@ It is **not** the right answer when: * xref:explanation/perf/simd-kernels.adoc[How SIMD kernels are built] — the FP32 Panama kernel walk-through and the full kernel-provider SPI rationale. * xref:explanation/perf/quantized-simd-kernels.adoc[How quantized SIMD kernels are built] — inner loops for Q4_0 / Q4_K / Q6_K / Q8_0. * xref:explanation/perf/jvm-cpu.adoc[JVM CPU performance] — JVM flags, vector module enablement, JIT considerations. -* xref:contributing/native-ffm-plan.adoc[Plan: native FFM kernel provider] — the path to the priority-100 native kernels. diff --git a/docs/modules/ROOT/pages/contributing/native-ffm-plan.adoc b/docs/modules/ROOT/pages/contributing/native-ffm-plan.adoc deleted file mode 100644 index 436a72c14..000000000 --- a/docs/modules/ROOT/pages/contributing/native-ffm-plan.adoc +++ /dev/null @@ -1,285 +0,0 @@ -= Plan: Native (FFM) Kernel Provider -:description: Where the JVM Vector kernels stop, what a native priority-100 provider would look like, and when to build it. - -[NOTE] -==== -**Audience: SKaiNET contributors.** Design doc for a kernel backend -that is not yet shipped. Library users do not need to read this; the -in-process Panama Vector kernels are the production code path today. -==== - -This page is a *plan*, not shipped code. The intent is to capture -enough detail that the design doesn't drift between the time someone -decides to start the work and the moment a PR is opened. The earlier -content of this page lived briefly in `NATIVE_FFM_KERNEL_PROVIDER.md` -at the repo root and was removed on advice of "ship the release first, -keep the plan in docs"; this is its permanent home. - -== Where the JVM Vector kernels run out - -After the M5 milestone work landed (PRs #554–#565 across the 0.21.0 -release), every CPU matmul path goes through the kernel SPI — see -xref:explanation/perf/simd-kernels.adoc[] and -xref:explanation/perf/quantized-simd-kernels.adoc[]. The Panama Vector -provider runs at: - -* ~73 GFLOPS on FP32 4096² matmul (Apple Silicon NEON) -* ~73 GFLOPS on Q4_K 4096² matmul-vector (same regime; fused dequant -adds essentially zero cost on top of the FMA) - -That's already in the ggml NEON ballpark in absolute terms. But -ggml's hand-tuned NEON / AVX2 still outruns the JVM Vector API on: - -* dense FLOPs/cycle on shapes the Vector API can't tile-block -optimally (the 8×8×128 default is heuristic) -* AVX-512 VNNI fused INT8 dot products -* NEON `bf16` / `fp16` SDOT instructions -* future SVE / SME — none of which the Vector API exposes portably -today - -A native provider closes that gap and unlocks two follow-ons that -*can't* be built on the Vector API alone: - -. *M4 ↔ M5 zero-copy.* Mmap'd Q4_K weights stay as `MemorySegment` -views; a native kernel reads the same pages with no heap copy and -no staging buffer. -. *Hardware-specific lanes* unreachable from portable Vector code. - -== Provider shape - -[cols="1,1,1",options="header"] -|=== -| Priority | Provider | Status -| 0 | `ScalarKernelProvider` | shipped (PR #554) -| 50 | `PanamaVectorKernelProvider` | shipped (PRs #557, #560 + ServiceLoader #559) -| *100* | *`NativeKernelProvider` (FFM)* | *this plan* -|=== - -The `KernelRegistry.bestAvailable()` cascade means: when the native -lib loads, native wins; when it doesn't (sandbox, missing arch, JDK -without FFM, kill-switch flipped), Panama wins; on Native targets and -JS / Wasm where neither is available, scalar wins. No code change -above the registry layer. - -== Goals - -. *A `NativeKernelProvider` registered at priority 100* that on JDK -21+ wins `KernelRegistry.bestAvailable()` over Panama whenever the -native lib loads successfully. -. *A first concrete kernel: native Q4_K matmul.* It must: -.. take a `MemorySegment` for both input (FP32) and packed Q4_K -weights (canonical ggml layout — same as `Q4_KBlockTensorData` -and `matmulF32Q4_KMemSeg`); -.. produce numerically equivalent output to -`PanamaVectorQ4KMatmulKernel` within `1e-4` relative tolerance -(same parity bar `PanamaVectorQ4KMatmulKernelTest` uses); -.. clear *≥2.5×* over the prior Q4_K scalar dequant baseline — the -M5 success metric — on the bench shapes from -`QuantizedMatmulBench` (1024², 4096×1024, 4096²). -. *Optional follow-on kernels* — Q6_K, Q8_0, FP32 — share the build -system but each ship as a separate small PR. -. *One supported architecture for the first PR* (likely Apple -Silicon NEON since that's the development hardware in use), with a -clear extension path for `linuxX64` AVX2 / `linuxArm64` NEON. - -== Non-goals - -* *JNI.* The roadmap explicitly says "FFM not JNI". JNI's per-call -overhead and the global JNI lock are wrong for hot per-token -kernels; FFM (Java 22 stable, Java 21 preview) gives near-zero -overhead native calls and direct `MemorySegment` ABI. -* *Cross-compilation matrix on day one.* The first PR can ship just -one (host-arch) variant; CI cross-arch builds come later. -* *Replacing Panama.* Panama remains the priority-50 fallback for -environments that can't load native libs (sandboxes, Wasm, Native -targets, JDK without `jdk.incubator.vector`). -* *Distribution via pre-built native artifacts on Maven Central.* -Out of scope for the first PR — local build only. Publishing -classifier JARs comes in a separate plan. - -== Architecture - -=== Module layout - -[source] ----- -skainet-backends/ - skainet-backend-native-cpu/ # NEW - src/ - jvmMain/kotlin/sk/ainet/exec/kernel/ # Kotlin side - NativeKernelProvider.kt # priority=100, isAvailable()=libLoaded - NativeQ4KMatmulKernel.kt # implements Q4KMatmulKernel via FFM - NativeLibraryLoader.kt # System.loadLibrary, locate, version - jvmMain/resources/META-INF/services/ - sk.ainet.backend.api.kernel.KernelProvider # appends NativeKernelProviderFactory - jvmTest/kotlin/sk/ainet/exec/kernel/ - NativeQ4KMatmulKernelTest.kt # parity vs PanamaVectorQ4KMatmulKernel - native/ # native source tree - c/ - q4k_matmul.c # ggml-style hand-tuned kernel - q4k_matmul.h - CMakeLists.txt # or Bazel BUILD - build.gradle.kts # Gradle wrapper that invokes CMake ----- - -The native library compiles to a shared object (`libskainet_kernels.dylib` -on macOS, `.so` on Linux, `.dll` on Windows) and is packaged into the -module's resources for `System.loadLibrary` discovery. - -=== FFM binding pattern - -Single C entry point per kernel: - -[source,c] ----- -// q4k_matmul.h -void skainet_q4k_matmul( - const float* input, // FP32 input vector, length input_dim - const uint8_t* weight, // packed Q4_K bytes (canonical ggml layout) - int32_t weight_byte_offset, - int32_t input_dim, - int32_t output_dim, - float* output, // FP32 output, length output_dim - int32_t output_offset -); ----- - -Kotlin side: - -[source,kotlin] ----- -internal object NativeQ4KMatmulKernel : Q4KMatmulKernel { - private val handle: MethodHandle = run { - val arena = Arena.ofAuto() - val symbol = NativeLibraryLoader.lib.find("skainet_q4k_matmul").orElseThrow() - Linker.nativeLinker().downcallHandle( - symbol, - FunctionDescriptor.ofVoid( - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, - ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, - ValueLayout.ADDRESS, ValueLayout.JAVA_INT, - ), - ) - } - - override fun matmul( - input: FloatArray, inputOffset: Int, - weight: ByteArray, weightByteOffset: Int, - inputDim: Int, outputDim: Int, - output: FloatArray, outputOffset: Int, - ) { - // Heap arrays: pass via temporary off-heap MemorySegment + bulk copy, - // OR (preferred) overload with a MemorySegment-input variant for - // mmap'd weights to avoid the copy. - } -} ----- - -The cleaner path is to introduce a sibling `Q4KMemSegMatmulKernel` -SPI (mentioned as out-of-scope in PR #563) that takes `MemorySegment` -directly, and have the native provider implement *that* — no heap -copy. The `Q4KMatmulKernel` (`ByteArray`) variant can wrap the -MemSeg one with a temporary `Arena.ofConfined()` copy if needed for -legacy callers. - -=== Build system - -*Gradle + CMake* is the path of least resistance: - -* A new Gradle module (or hand-rolled `Exec` tasks) invokes CMake -for the native module's `build` task. -* Native artifacts land in `build/native//` and are copied -into `src/jvmMain/resources/native/-/` so -`System.loadLibrary` finds them. -* Kotlin compile depends on the native artifact being built first. - -The xnnpack backend already in the repo -(`skainet-backends/skainet-backend-xnnpack/`) demonstrates a similar -pattern — Gradle invokes CMake to build a native lib via cinterop. -*Reuse that template* rather than reinventing. - -== Staged delivery - -PRs in order, each independently mergeable: - -. *`skainet-backend-native-cpu` module scaffolding.* Gradle module, -`build.gradle.kts` wired to invoke CMake, a *trivial* C kernel -(e.g. just multiplies its first input by 2.0) to prove the FFM -pipeline end-to-end. `NativeKernelProvider` that's `isAvailable() -= false` until the real kernel lands. Sets up CI artifact path on -host arch. -. *First real native kernel: Q4_K matmul (Apple Silicon NEON).* -Hand-tuned kernel, parity tests vs `PanamaVectorQ4KMatmulKernel`, -JMH bench variant added to `QuantizedMatmulBench`. -. *`Q4KMemSegMatmulKernel` SPI sibling + native variant.* Closes -the M4↔M5 zero-copy story for mmap'd weights. -. *`linuxX64` AVX2 variant + cross-arch CI build.* The -cross-compilation matrix story. -. *Optional: native FP32 matmul, native Q6_K, native Q8_0.* Same -shape as PRs 2–3, one per format. - -The first PR is the largest in scaffolding terms (~500–800 LoC of -build glue + 1 trivial kernel), but every subsequent PR is small and -template-able. - -== Success metrics - -* *PR 2 sign-off*: native Q4_K matmul on Apple Silicon clears *≥2.5×* -over the scalar Q4_K dequant-then-matmul baseline at 4096² (the M5 -milestone target). For reference: Panama Q4_K SIMD already exceeds -this metric (~73 GFLOPS, see -xref:explanation/perf/quantized-simd-kernels.adoc[]), so the bar is -"beats Panama by a meaningful margin", probably ≥1.5× over Panama. -* *PR 3 sign-off*: Q4_K MemSeg native path is faster than the Panama -Q4_K MemSeg path from PR #563, with no heap copy in the timed -region. -* *No regression on JVM-only environments* — when the native lib -fails to load (sandbox, missing arch, kill-switch), `bestAvailable()` -cleanly falls through to Panama, and existing tests / benches show -the same numbers as today. - -== Risks & open questions - -. *JDK 21 preview FFM vs JDK 22 stable.* FFM left preview in Java 22. -The repo currently builds on JDK 21 with `--enable-preview ---add-modules jdk.incubator.vector`. Recommendation: stay on 21 -preview; flip to 22 in a separate toolchain-bump PR. -. *`MethodHandle` invocation overhead.* Even with FFM, each native -call has a small fixed cost (~µs). For the smallest matmul shapes -(e.g. 256² FP32) this could swamp the FLOPs win. Mitigation: route -small inputs to Panama and large inputs to native at the -registry/provider level, OR accept that the win is sized for -production-relevant shapes (4096²+). -. *Native code quality and maintenance.* Hand-tuned NEON / AVX2 in C -is harder to audit than Kotlin Vector API code. Mitigation: keep -kernels small (<300 LoC each), parity-test exhaustively, prefer -porting from ggml's reference (BSD-licensed, well-vetted) over -writing from scratch. -. *Distribution.* Native artifacts complicate Maven Central -publication (need `` per OS/arch). Not a blocker for -the first internal-use PR; a separate "publish native classifier -JARs" plan will be needed before community use. -. *Cross-arch CI cost.* Building NEON natively on Apple Silicon CI -plus AVX2 on linuxX64 plus Android NDK doubles or triples build -time. The xnnpack backend's existing CI matrix is a precedent — -reuse the same approach. -. *Native `MemorySegment` lifetime.* The Kotlin caller owns the -`Arena` for arrays it copies in. The native kernel must NOT retain -pointers past the FFM call return. Document this contract in -`NativeQ4KMatmulKernel.matmul` kdoc. - -== When to start - -Trigger conditions (any one): - -* Real workload demands the native ≥2.5× target (Panama Q4_K stops -being fast enough on a customer machine). -* A community contributor offers a hand-tuned NEON / AVX2 Q4_K -kernel that's measurably faster than Panama. -* A second M5 metric (e.g. SDPA throughput, training-loop -throughput) needs hand-tuned native code. - -Until then: *pause.* The Panama provider is doing the -milestone-equivalent work in absolute terms, and adding a native -build system is a meaningful complexity tax to take on -speculatively. diff --git a/docs/modules/ROOT/pages/explanation/perf/quantized-simd-kernels.adoc b/docs/modules/ROOT/pages/explanation/perf/quantized-simd-kernels.adoc index 23ab9e9d0..f3cf8a0c9 100644 --- a/docs/modules/ROOT/pages/explanation/perf/quantized-simd-kernels.adoc +++ b/docs/modules/ROOT/pages/explanation/perf/quantized-simd-kernels.adoc @@ -230,6 +230,4 @@ shape as the Q4_K rewrite, with the lane-interleave done via |=== For the kernel SPI itself, see -xref:explanation/perf/simd-kernels.adoc[]. For the planned native FFM -provider that would replace the Vector path on supported hosts, see -xref:contributing/native-ffm-plan.adoc[]. +xref:explanation/perf/simd-kernels.adoc[]. diff --git a/docs/modules/ROOT/pages/explanation/perf/simd-kernels.adoc b/docs/modules/ROOT/pages/explanation/perf/simd-kernels.adoc index 810b32381..ea2c3081f 100644 --- a/docs/modules/ROOT/pages/explanation/perf/simd-kernels.adoc +++ b/docs/modules/ROOT/pages/explanation/perf/simd-kernels.adoc @@ -31,7 +31,7 @@ don't carry that"). Three providers ship with the CPU backend today: | Provider | Priority | When available | Notes | `ScalarKernelProvider` | 0 | always | Three-loop reference; the parity baseline. | `PanamaVectorKernelProvider` | 50 | JDK 21+ with `--add-modules jdk.incubator.vector` and `skainet.cpu.vector.enabled != false` | Tile-blocked FMA; the production winner on every supported JVM. -| (future) `NativeKernelProvider` | 100 | JDK 22+ with the native lib loaded | Captured as a plan in xref:contributing/native-ffm-plan.adoc[]; not yet shipped. +| (future) `NativeKernelProvider` | 100 | JDK 22+ with the native lib loaded | Designed but not yet shipped. |=== == Why the SPI exists @@ -239,6 +239,3 @@ no-regression change. For quantized matmul (Q4_K, Q6_K, Q8_0, Q4_0) — same story, different inner loop — see xref:explanation/perf/quantized-simd-kernels.adoc[]. - -For the still-unbuilt native FFM provider, see -xref:contributing/native-ffm-plan.adoc[]. diff --git a/docs/modules/ROOT/pages/reference/architecture.adoc b/docs/modules/ROOT/pages/reference/architecture.adoc index 0430a8992..9fb4759d9 100644 --- a/docs/modules/ROOT/pages/reference/architecture.adoc +++ b/docs/modules/ROOT/pages/reference/architecture.adoc @@ -131,8 +131,7 @@ Introduced in 0.21.0 (PRs #554, #559, #562). The static structure: ---- Three live providers ship; a fourth (priority 100, native FFM) is -captured as a plan in xref:contributing/native-ffm-plan.adoc[] -and not yet built. For *how* the kernels are implemented, see +designed but not yet built. For *how* the kernels are implemented, see xref:explanation/perf/simd-kernels.adoc[] (FP32) and xref:explanation/perf/quantized-simd-kernels.adoc[] (quantized). @@ -268,8 +267,7 @@ canary. * *No native FFM provider yet.* The literal M5 milestone metric (`≥2.5×` for Q4_K) is met by Panama in absolute terms but not in the "native vs JVM" framing the metric originally specified. -Mitigation: `xref:contributing/native-ffm-plan.adoc[]` -documents what shipping it would look like. +The priority-100 native provider is designed but not yet shipped. * *Two reverted optimizations on develop history.* MemSeg pool (commit 8642b322) and intra-op matmul parallelism (commit 9ed633b6) were both tried and reverted. Re-attempts need a diff --git a/docs/modules/ROOT/pages/reference/operators/generated/index.adoc b/docs/modules/ROOT/pages/reference/operators/generated/index.adoc index 9eab83e0b..3c2fd15dc 100644 --- a/docs/modules/ROOT/pages/reference/operators/generated/index.adoc +++ b/docs/modules/ROOT/pages/reference/operators/generated/index.adoc @@ -1,6 +1,6 @@ = AI-NET Operators Reference -Generated from version `0.19.0` on 2026-04-15 +Generated from version `0.23.0` on 2026-05-24 == Operators by Modality diff --git a/docs/modules/ROOT/pages/reference/operators/generated/similarity.adoc b/docs/modules/ROOT/pages/reference/operators/generated/similarity.adoc index 0901bb891..f3c0cde52 100644 --- a/docs/modules/ROOT/pages/reference/operators/generated/similarity.adoc +++ b/docs/modules/ROOT/pages/reference/operators/generated/similarity.adoc @@ -6,6 +6,8 @@ Modality: Composite == cosineDistance +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] diff --git a/docs/modules/ROOT/pages/reference/operators/generated/tensorops.adoc b/docs/modules/ROOT/pages/reference/operators/generated/tensorops.adoc index cd3c671b7..fd54fe42c 100644 --- a/docs/modules/ROOT/pages/reference/operators/generated/tensorops.adoc +++ b/docs/modules/ROOT/pages/reference/operators/generated/tensorops.adoc @@ -6,6 +6,8 @@ Modality: Core == add +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -24,6 +26,8 @@ fun add(a:Tensor, b:Tensor): Tensor == subtract +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -42,6 +46,8 @@ fun subtract(a:Tensor, b:Tensor): Tensor == multiply +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -60,6 +66,8 @@ fun multiply(a:Tensor, b:Tensor): Tensor == divide +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -78,6 +86,8 @@ fun divide(a:Tensor, b:Tensor): Tensor == addScalar +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -96,6 +106,8 @@ fun addScalar(a:Tensor, b:Number): Tensor == subScalar +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -114,6 +126,8 @@ fun subScalar(a:Tensor, b:Number): Tensor == mulScalar +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -132,6 +146,8 @@ fun mulScalar(a:Tensor, b:Number): Tensor == divScalar +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -150,6 +166,8 @@ fun divScalar(a:Tensor, b:Number): Tensor == rsubScalar +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -168,6 +186,8 @@ fun rsubScalar(a:Number, b:Tensor): Tensor == rdivScalar +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -186,6 +206,8 @@ fun rdivScalar(a:Number, b:Tensor): Tensor == matmul +[.darc-validated]#✅ DARC-validated by SKaiNET docs maintainers on 2026-05-24# + === Signature [source,kotlin] @@ -222,6 +244,8 @@ include::partial$ops/tensorops/matmul.adoc[tag=references,optional] == transpose +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -237,8 +261,32 @@ fun transpose(tensor:Tensor): Tensor `Tensor` +== permute + +[.darc-none]#✖ Generated facts only (no human prose)# + +=== Signature + +[source,kotlin] +---- +fun permute(tensor:Tensor, axes:IntArray): Tensor +---- + +=== Parameters + +* `tensor: Tensor` + input tensor, any rank ≥ 1 +* `axes: IntArray` + a permutation of `0..tensor.rank-1` (length must equal `tensor.rank`, every value in `[0, rank)` exactly once) + +=== Return Type + +`Tensor` + == conv1d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -262,6 +310,8 @@ fun conv1d(input:Tensor, weight:Tensor, bias:Tensor, stride:Int, padding:Int, di == conv2d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -285,6 +335,8 @@ fun conv2d(input:Tensor, weight:Tensor, bias:Tensor, stride:Pair, padding:Pair, == conv3d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -308,6 +360,8 @@ fun conv3d(input:Tensor, weight:Tensor, bias:Tensor, stride:Triple, padding:Trip == convTranspose1d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -332,6 +386,8 @@ fun convTranspose1d(input:Tensor, weight:Tensor, bias:Tensor, stride:Int, paddin == maxPool2d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -352,6 +408,8 @@ fun maxPool2d(input:Tensor, kernelSize:Pair, stride:Pair, padding:Pair): Tensor == avgPool2d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -373,6 +431,8 @@ fun avgPool2d(input:Tensor, kernelSize:Pair, stride:Pair, padding:Pair, countInc == upsample2d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -393,6 +453,8 @@ fun upsample2d(input:Tensor, scale:Pair, mode:UpsampleMode, alignCorners:Boolean == reshape +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -411,6 +473,8 @@ fun reshape(tensor:Tensor, newShape:Shape): Tensor == flatten +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -430,6 +494,8 @@ fun flatten(tensor:Tensor, startDim:Int, endDim:Int): Tensor == concat +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -448,6 +514,8 @@ fun concat(tensors:List, dim:Int): Tensor == split +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -467,6 +535,8 @@ fun split(tensor:Tensor, splitSize:Int, dim:Int): List == squeeze +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -485,6 +555,8 @@ fun squeeze(tensor:Tensor, dim:Int): Tensor == unsqueeze +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -503,6 +575,8 @@ fun unsqueeze(tensor:Tensor, dim:Int): Tensor == relu +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -520,6 +594,8 @@ fun relu(tensor:Tensor): Tensor == leakyRelu +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -538,6 +614,8 @@ fun leakyRelu(tensor:Tensor, negativeSlope:Float): Tensor == elu +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -556,6 +634,8 @@ fun elu(tensor:Tensor, alpha:Float): Tensor == softmax +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -574,6 +654,8 @@ fun softmax(tensor:Tensor, dim:Int): Tensor == logSoftmax +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -592,6 +674,8 @@ fun logSoftmax(tensor:Tensor, dim:Int): Tensor == sigmoid +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -609,6 +693,8 @@ fun sigmoid(tensor:Tensor): Tensor == silu +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -626,6 +712,8 @@ fun silu(tensor:Tensor): Tensor == gelu +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -643,6 +731,8 @@ fun gelu(tensor:Tensor): Tensor == sum +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -661,6 +751,8 @@ fun sum(tensor:Tensor, dim:Int): Tensor == mean +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -679,6 +771,8 @@ fun mean(tensor:Tensor, dim:Int): Tensor == variance +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -697,6 +791,8 @@ fun variance(tensor:Tensor, dim:Int): Tensor == sqrt +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -712,8 +808,107 @@ fun sqrt(tensor:Tensor): Tensor `Tensor` +== pow + +[.darc-none]#✖ Generated facts only (no human prose)# + +=== Signature + +[source,kotlin] +---- +fun pow(a:Tensor, b:Tensor): Tensor +---- + +=== Parameters + +* `a: Tensor` +* `b: Tensor` + +=== Return Type + +`Tensor` + +== powScalar + +[.darc-none]#✖ Generated facts only (no human prose)# + +=== Signature + +[source,kotlin] +---- +fun powScalar(a:Tensor, n:Number): Tensor +---- + +=== Parameters + +* `a: Tensor` +* `n: Number` + +=== Return Type + +`Tensor` + +== log + +[.darc-none]#✖ Generated facts only (no human prose)# + +=== Signature + +[source,kotlin] +---- +fun log(tensor:Tensor): Tensor +---- + +=== Parameters + +* `tensor: Tensor` + +=== Return Type + +`Tensor` + +== log2 + +[.darc-none]#✖ Generated facts only (no human prose)# + +=== Signature + +[source,kotlin] +---- +fun log2(tensor:Tensor): Tensor +---- + +=== Parameters + +* `tensor: Tensor` + +=== Return Type + +`Tensor` + +== log10 + +[.darc-none]#✖ Generated facts only (no human prose)# + +=== Signature + +[source,kotlin] +---- +fun log10(tensor:Tensor): Tensor +---- + +=== Parameters + +* `tensor: Tensor` + +=== Return Type + +`Tensor` + == abs +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -731,6 +926,8 @@ fun abs(tensor:Tensor): Tensor == sign +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -748,6 +945,8 @@ fun sign(tensor:Tensor): Tensor == clamp +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -767,6 +966,8 @@ fun clamp(tensor:Tensor, minVal:Float, maxVal:Float): Tensor == narrow +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -787,6 +988,8 @@ fun narrow(tensor:Tensor, dim:Int, start:Int, length:Int): Tensor == pad2d +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -808,6 +1011,8 @@ fun pad2d(tensor:Tensor, padLeft:Int, padRight:Int, padTop:Int, padBottom:Int): == unfold +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -828,6 +1033,8 @@ fun unfold(tensor:Tensor, dim:Int, size:Int, step:Int): Tensor == lt +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -846,6 +1053,8 @@ fun lt(tensor:Tensor, value:Float): Tensor == ge +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -864,6 +1073,8 @@ fun ge(tensor:Tensor, value:Float): Tensor == tril +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -882,6 +1093,8 @@ fun tril(tensor:Tensor, k:Int): Tensor == convert +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -900,6 +1113,8 @@ fun convert(tensor:Tensor, targetType:TTo): Tensor == gather +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -919,6 +1134,8 @@ fun gather(input:Tensor, indices:Tensor, dim:Int): Tensor == indexSelect +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -938,6 +1155,8 @@ fun indexSelect(input:Tensor, indices:Tensor, dim:Int): Tensor == exp +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -955,6 +1174,8 @@ fun exp(tensor:Tensor): Tensor == expm1 +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -972,6 +1193,8 @@ fun expm1(tensor:Tensor): Tensor == sin +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -989,6 +1212,8 @@ fun sin(tensor:Tensor): Tensor == cos +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -1006,6 +1231,8 @@ fun cos(tensor:Tensor): Tensor == tanh +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] @@ -1023,6 +1250,8 @@ fun tanh(tensor:Tensor): Tensor == scaledDotProductAttention +[.darc-none]#✖ Generated facts only (no human prose)# + === Signature [source,kotlin] diff --git a/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc b/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc index 46ace7bef..2398a9c93 100644 --- a/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc +++ b/docs/modules/ROOT/pages/reference/ops-status-matrix.adoc @@ -1,72 +1,78 @@ = Operator Coverage Matrix :description: Cross-backend status for every operator function in SKaiNET. -Generated from `operators.json` version `0.19.0` on 2026-04-15. +Generated from `operators.json` version `0.23.0` on 2026-05-24. -Rows are `Operator.function` pairs; columns are backends that appear in any function's `statusByBackend` map. A missing entry means the backend makes no claim about the function — treat it as "unknown", not "not supported". +Rows are `Operator.function` pairs. The `Validated` column shows whether the function's documentation has been DARC-validated by a reviewer (see xref:contributing/darc-workflow.adoc[DARC workflow]). Remaining columns are backends that appear in any function's `statusByBackend` map — a missing entry means the backend makes no claim about the function (treat it as "unknown", not "not supported"). -[cols="2,1,1,1", options="header"] +[cols="2,1,1,1,1", options="header"] |=== -| Operator.function | apple | cpu | wasm +| Operator.function | Validated | apple | cpu | wasm -| `TensorOps.add` | — | — | — -| `TensorOps.subtract` | — | — | — -| `TensorOps.multiply` | — | — | — -| `TensorOps.divide` | — | — | — -| `TensorOps.addScalar` | — | — | — -| `TensorOps.subScalar` | — | — | — -| `TensorOps.mulScalar` | — | — | — -| `TensorOps.divScalar` | — | — | — -| `TensorOps.rsubScalar` | — | — | — -| `TensorOps.rdivScalar` | — | — | — -| `TensorOps.matmul` | — | — | — -| `TensorOps.transpose` | — | — | — -| `TensorOps.conv1d` | — | — | — -| `TensorOps.conv2d` | — | — | — -| `TensorOps.conv3d` | — | — | — -| `TensorOps.convTranspose1d` | — | — | — -| `TensorOps.maxPool2d` | — | — | — -| `TensorOps.avgPool2d` | — | — | — -| `TensorOps.upsample2d` | — | — | — -| `TensorOps.reshape` | — | — | — -| `TensorOps.flatten` | — | — | — -| `TensorOps.concat` | — | — | — -| `TensorOps.split` | — | — | — -| `TensorOps.squeeze` | — | — | — -| `TensorOps.unsqueeze` | — | — | — -| `TensorOps.relu` | — | — | — -| `TensorOps.leakyRelu` | — | — | — -| `TensorOps.elu` | — | — | — -| `TensorOps.softmax` | — | — | — -| `TensorOps.logSoftmax` | — | — | — -| `TensorOps.sigmoid` | — | — | — -| `TensorOps.silu` | — | — | — -| `TensorOps.gelu` | — | — | — -| `TensorOps.sum` | — | — | — -| `TensorOps.mean` | — | — | — -| `TensorOps.variance` | — | — | — -| `TensorOps.sqrt` | — | — | — -| `TensorOps.abs` | — | — | — -| `TensorOps.sign` | — | — | — -| `TensorOps.clamp` | — | — | — -| `TensorOps.narrow` | — | — | — -| `TensorOps.pad2d` | — | — | — -| `TensorOps.unfold` | — | — | — -| `TensorOps.lt` | — | — | — -| `TensorOps.ge` | — | — | — -| `TensorOps.tril` | — | — | — -| `TensorOps.convert` | — | — | — -| `TensorOps.gather` | — | — | — -| `TensorOps.indexSelect` | — | — | — -| `TensorOps.exp` | — | — | — -| `TensorOps.expm1` | — | — | — -| `TensorOps.sin` | — | — | — -| `TensorOps.cos` | — | — | — -| `TensorOps.tanh` | — | — | — -| `TensorOps.scaledDotProductAttention` | — | — | — -| `Similarity.cosineDistance` | ✅ | ✅ | ✅ +| `TensorOps.add` | ✖ | — | — | — +| `TensorOps.subtract` | ✖ | — | — | — +| `TensorOps.multiply` | ✖ | — | — | — +| `TensorOps.divide` | ✖ | — | — | — +| `TensorOps.addScalar` | ✖ | — | — | — +| `TensorOps.subScalar` | ✖ | — | — | — +| `TensorOps.mulScalar` | ✖ | — | — | — +| `TensorOps.divScalar` | ✖ | — | — | — +| `TensorOps.rsubScalar` | ✖ | — | — | — +| `TensorOps.rdivScalar` | ✖ | — | — | — +| `TensorOps.matmul` | ✅ | — | — | — +| `TensorOps.transpose` | ✖ | — | — | — +| `TensorOps.permute` | ✖ | — | — | — +| `TensorOps.conv1d` | ✖ | — | — | — +| `TensorOps.conv2d` | ✖ | — | — | — +| `TensorOps.conv3d` | ✖ | — | — | — +| `TensorOps.convTranspose1d` | ✖ | — | — | — +| `TensorOps.maxPool2d` | ✖ | — | — | — +| `TensorOps.avgPool2d` | ✖ | — | — | — +| `TensorOps.upsample2d` | ✖ | — | — | — +| `TensorOps.reshape` | ✖ | — | — | — +| `TensorOps.flatten` | ✖ | — | — | — +| `TensorOps.concat` | ✖ | — | — | — +| `TensorOps.split` | ✖ | — | — | — +| `TensorOps.squeeze` | ✖ | — | — | — +| `TensorOps.unsqueeze` | ✖ | — | — | — +| `TensorOps.relu` | ✖ | — | — | — +| `TensorOps.leakyRelu` | ✖ | — | — | — +| `TensorOps.elu` | ✖ | — | — | — +| `TensorOps.softmax` | ✖ | — | — | — +| `TensorOps.logSoftmax` | ✖ | — | — | — +| `TensorOps.sigmoid` | ✖ | — | — | — +| `TensorOps.silu` | ✖ | — | — | — +| `TensorOps.gelu` | ✖ | — | — | — +| `TensorOps.sum` | ✖ | — | — | — +| `TensorOps.mean` | ✖ | — | — | — +| `TensorOps.variance` | ✖ | — | — | — +| `TensorOps.sqrt` | ✖ | — | — | — +| `TensorOps.pow` | ✖ | — | — | — +| `TensorOps.powScalar` | ✖ | — | — | — +| `TensorOps.log` | ✖ | — | — | — +| `TensorOps.log2` | ✖ | — | — | — +| `TensorOps.log10` | ✖ | — | — | — +| `TensorOps.abs` | ✖ | — | — | — +| `TensorOps.sign` | ✖ | — | — | — +| `TensorOps.clamp` | ✖ | — | — | — +| `TensorOps.narrow` | ✖ | — | — | — +| `TensorOps.pad2d` | ✖ | — | — | — +| `TensorOps.unfold` | ✖ | — | — | — +| `TensorOps.lt` | ✖ | — | — | — +| `TensorOps.ge` | ✖ | — | — | — +| `TensorOps.tril` | ✖ | — | — | — +| `TensorOps.convert` | ✖ | — | — | — +| `TensorOps.gather` | ✖ | — | — | — +| `TensorOps.indexSelect` | ✖ | — | — | — +| `TensorOps.exp` | ✖ | — | — | — +| `TensorOps.expm1` | ✖ | — | — | — +| `TensorOps.sin` | ✖ | — | — | — +| `TensorOps.cos` | ✖ | — | — | — +| `TensorOps.tanh` | ✖ | — | — | — +| `TensorOps.scaledDotProductAttention` | ✖ | — | — | — +| `Similarity.cosineDistance` | ✖ | ✅ | ✅ | ✅ -| *Done* | *1 / 56* | *1 / 56* | *1 / 56* +| *Done* | *1 / 62* | *1 / 62* | *1 / 62* | *1 / 62* |=== Per-function detail including notes lives in xref:reference/operators/generated/index.adoc[Operator reference]. diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorOps.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorOps.kt index 6d2962e13..7a0f5082f 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorOps.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorOps.kt @@ -7,6 +7,7 @@ import sk.ainet.lang.trace.GenerateTracingWrapper import sk.ainet.lang.trace.Diff import sk.ainet.lang.nn.dsl.GenerateNetworkDsl import sk.ainet.lang.nn.dsl.ActivationDsl +import sk.ainet.lang.ops.DarcValidated @GenerateTracingWrapper @GenerateNetworkDsl @@ -49,6 +50,7 @@ public interface TensorOps { * broadcast against [a] using the usual broadcasting rules. */ @Diff + @DarcValidated(by = "SKaiNET docs maintainers", on = "2026-05-24") public fun matmul(a: Tensor, b: Tensor): Tensor @Diff public fun transpose(tensor: Tensor): Tensor diff --git a/skainet-lang/skainet-lang-ksp-annotations/src/commonMain/kotlin/sk/ainet/lang/ops/DarcValidated.kt b/skainet-lang/skainet-lang-ksp-annotations/src/commonMain/kotlin/sk/ainet/lang/ops/DarcValidated.kt new file mode 100644 index 000000000..a6a086b6a --- /dev/null +++ b/skainet-lang/skainet-lang-ksp-annotations/src/commonMain/kotlin/sk/ainet/lang/ops/DarcValidated.kt @@ -0,0 +1,33 @@ +package sk.ainet.lang.ops + +/** + * Marks a [TensorOp] function as DARC-validated. + * + * A reviewer (not the original author) has gone through Document, Assess, + * Research, and Code for this function — read the partial prose, checked the + * math against a reference implementation, verified the citations, and + * confirmed the runtime behaviour matches the documented contract. + * + * Picked up by `OperatorDocProcessor` and rendered as a badge on the + * generated operator page plus a column in the ops coverage matrix. + * + * See `contributing/darc-workflow.adoc` for the exact criteria. + * + * @param by Validator identity. Free-form, but `"First Last "` + * is the convention so the same string can be linked back to git history. + * @param on ISO-8601 date the validation completed, e.g. `"2026-05-24"`. + * @param commit Optional short SHA pinning the validated prose. Empty when + * the validation is not pinned to a specific revision. + * @param referencesChecked Whether the reviewer verified that every link and + * citation in the partial still resolves and supports the claim it backs. + * Defaults to `true` because if it weren't, the validation would not have + * passed; set to `false` only as a deliberate documentation signal. + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +public annotation class DarcValidated( + val by: String, + val on: String, + val commit: String = "", + val referencesChecked: Boolean = true, +) diff --git a/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessor.kt b/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessor.kt index e50b6455d..6472c9562 100644 --- a/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessor.kt +++ b/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessor.kt @@ -30,7 +30,14 @@ data class FunctionDoc( val parameters: List, val returnType: String, val statusByBackend: Map, - val notes: List + val notes: List, + // DARC validation metadata. `validated = false` means @DarcValidated is + // absent — the generator will render a "not validated" badge. + val validated: Boolean = false, + val validatedBy: String = "", + val validatedOn: String = "", + val validatedCommit: String = "", + val referencesChecked: Boolean = true, ) data class ParameterDoc( @@ -180,13 +187,19 @@ class OperatorDocProcessor( } statusByBackend[backendId] = if (overrides) "implemented" else "inherited" } + val validation = extractDarcValidation(fn) FunctionDoc( name = fn.simpleName.asString(), signature = fn.toSignatureString(), parameters = extractParameters(fn), returnType = extractReturnType(fn), statusByBackend = statusByBackend, - notes = emptyList() + notes = emptyList(), + validated = validation.validated, + validatedBy = validation.by, + validatedOn = validation.on, + validatedCommit = validation.commit, + referencesChecked = validation.referencesChecked, ) } @@ -260,13 +273,56 @@ class OperatorDocProcessor( } private fun createFunctionDoc(function: KSFunctionDeclaration): FunctionDoc { + val validation = extractDarcValidation(function) return FunctionDoc( name = function.simpleName.asString(), signature = function.toSignatureString(), parameters = extractParameters(function), returnType = extractReturnType(function), statusByBackend = deriveStatusByBackend(function), - notes = deriveNotes(function) + notes = deriveNotes(function), + validated = validation.validated, + validatedBy = validation.by, + validatedOn = validation.on, + validatedCommit = validation.commit, + referencesChecked = validation.referencesChecked, + ) + } + + private data class DarcValidation( + val validated: Boolean, + val by: String, + val on: String, + val commit: String, + val referencesChecked: Boolean, + ) + + /** + * Read the `@DarcValidated` annotation off a function, if present. + * Returns a sentinel with `validated = false` when the annotation is + * absent, which the generator renders as the "not validated" badge. + */ + private fun extractDarcValidation(function: KSFunctionDeclaration): DarcValidation { + val annotation = function.annotations.find { + it.shortName.asString() == "DarcValidated" + } ?: return DarcValidation(false, "", "", "", true) + + val by = annotation.arguments.find { it.name?.asString() == "by" } + ?.value?.toString().orEmpty() + val on = annotation.arguments.find { it.name?.asString() == "on" } + ?.value?.toString().orEmpty() + val commit = annotation.arguments.find { it.name?.asString() == "commit" } + ?.value?.toString().orEmpty() + val refsChecked = (annotation.arguments.find { + it.name?.asString() == "referencesChecked" + }?.value as? Boolean) ?: true + + return DarcValidation( + validated = true, + by = by, + on = on, + commit = commit, + referencesChecked = refsChecked, ) } @@ -509,7 +565,21 @@ class OperatorDocProcessor( append("{\"type\": \"${escapeJson(note.type)}\", \"backend\": \"${escapeJson(note.backend)}\", \"content\": \"${escapeJson(note.content)}\"}") if (noteIndex < function.notes.size - 1) append(", ") } - append("]\n") + append("]") + + // DARC validation block. Only emitted when an actual + // @DarcValidated annotation is present, so unannotated + // functions keep the JSON narrow. + if (function.validated) { + append(",\n") + append(" \"validated\": true,\n") + append(" \"validatedBy\": \"${escapeJson(function.validatedBy)}\",\n") + append(" \"validatedOn\": \"${escapeJson(function.validatedOn)}\",\n") + append(" \"validatedCommit\": \"${escapeJson(function.validatedCommit)}\",\n") + append(" \"referencesChecked\": ${function.referencesChecked}\n") + } else { + append("\n") + } append(" }") if (funcIndex < operator.functions.size - 1) append(",") diff --git a/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/metadata/DocumentationModels.kt b/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/metadata/DocumentationModels.kt index 98ec26128..e92079779 100644 --- a/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/metadata/DocumentationModels.kt +++ b/skainet-lang/skainet-lang-ksp-processor/src/main/kotlin/sk/ainet/lang/ops/metadata/DocumentationModels.kt @@ -36,7 +36,12 @@ data class FunctionDoc( val parameters: List, val returnType: String, val statusByBackend: Map, - val notes: List + val notes: List, + val validated: Boolean = false, + val validatedBy: String = "", + val validatedOn: String = "", + val validatedCommit: String = "", + val referencesChecked: Boolean = true, ) /** diff --git a/skainet-lang/skainet-lang-ksp-processor/src/test/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessorTest.kt b/skainet-lang/skainet-lang-ksp-processor/src/test/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessorTest.kt index dcbc6a8b1..07ce9c556 100644 --- a/skainet-lang/skainet-lang-ksp-processor/src/test/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessorTest.kt +++ b/skainet-lang/skainet-lang-ksp-processor/src/test/kotlin/sk/ainet/lang/ops/ksp/OperatorDocProcessorTest.kt @@ -67,6 +67,86 @@ class OperatorDocProcessorTest { // but the key requirement is that the test compiles and runs } + @Test + fun testDarcValidatedFlowsIntoJson() { + // Two functions annotated with @InProgress so the processor picks + // them up via the annotation-discovery path. Only one of them + // carries @DarcValidated — we expect that one (and only that one) + // to land in operators.json with the validation block. + val sourceCode = """ + package sk.ainet.lang.ops + + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + @Retention(AnnotationRetention.SOURCE) + annotation class InProgress( + vararg val backends: String, + val owner: String = "", + val issue: String = "" + ) + + @Target(AnnotationTarget.FUNCTION) + @Retention(AnnotationRetention.SOURCE) + annotation class DarcValidated( + val by: String, + val on: String, + val commit: String = "", + val referencesChecked: Boolean = true, + ) + + @InProgress("cpu", owner = "ops-team", issue = "GH-1") + @DarcValidated(by = "Reviewer One", on = "2026-05-24") + fun validatedFn(): String = "ok" + + @InProgress("cpu", owner = "ops-team", issue = "GH-2") + fun plainFn(): String = "ok" + """.trimIndent() + + val source = SourceFile.kotlin("sk/ainet/lang/ops/TestDarcOps.kt", sourceCode) + + val compilation = KotlinCompilation().apply { + sources = listOf(source) + configureKsp {} + symbolProcessorProviders = mutableListOf(OperatorDocProcessorProvider()) + inheritClassPath = true + messageOutputStream = System.out + } + + val result = compilation.compile() + val output = result.messages + println("[DEBUG_LOG] Compilation result: ${result.exitCode}") + println("[DEBUG_LOG] Output messages: $output") + + assertTrue(output.contains("Generated operators.json"), + "Processor should generate operators.json") + + // Locate the JSON the processor just wrote. KSP code generator + // outputs resources somewhere under the compilation's working + // dir; the layout is version-dependent, so search rather than + // hard-code a path. + val operatorsJson = compilation.workingDir.walkTopDown() + .firstOrNull { it.isFile && it.name == "operators.json" } + assertTrue(operatorsJson != null && operatorsJson.exists(), + "operators.json should be present in compilation output") + val text = operatorsJson.readText() + println("[DEBUG_LOG] operators.json contents: $text") + + assertTrue(text.contains("\"validatedFn\""), + "validatedFn should be present in JSON") + assertTrue(text.contains("\"validated\": true"), + "validated=true should be emitted for the annotated function") + assertTrue(text.contains("\"validatedBy\": \"Reviewer One\""), + "validatedBy should carry the reviewer identity") + assertTrue(text.contains("\"validatedOn\": \"2026-05-24\""), + "validatedOn should carry the ISO date") + + // The unannotated function must NOT carry a validation block. + // The processor only emits the keys when validated=true, so a + // single occurrence in the file is the expected count. + val validatedKeyCount = Regex("\"validated\":\\s*true").findAll(text).count() + assertTrue(validatedKeyCount == 1, + "Exactly one function should emit validated=true (got $validatedKeyCount)") + } + @Test fun testDslOpAnnotationProcessing() { val sourceCode = """