diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 2b4ded4..f90ae2d 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -312,20 +312,23 @@ jobs: dotnet build --configuration Release dotnet pack --no-build --configuration Release --output ./artifacts - - name: Resolve NuGet package id + - name: Resolve NuGet package ids id: package if: >- steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' || (steps.check_release.outputs.should_release == 'true' && steps.check_release.outputs.skip_bump == 'true') run: | - PACKAGE_ID=$(sed -n 's:.*\(.*\).*:\1:p' Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj | head -n 1) - if [ -z "$PACKAGE_ID" ]; then - PACKAGE_ID=clink + CLI_ID=$(sed -n 's:.*\(.*\).*:\1:p' Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj | head -n 1) + if [ -z "$CLI_ID" ]; then + CLI_ID=clink fi - PACKAGE_ID_LOWER=$(echo "$PACKAGE_ID" | tr '[:upper:]' '[:lower:]') - echo "id=$PACKAGE_ID" >> "$GITHUB_OUTPUT" - echo "flat_container_id=$PACKAGE_ID_LOWER" >> "$GITHUB_OUTPUT" + LIB_ID=$(sed -n 's:.*\(.*\).*:\1:p' Foundation.Data.Doublets.Cli.Library/Foundation.Data.Doublets.Cli.Library.csproj | head -n 1) + if [ -z "$LIB_ID" ]; then + LIB_ID=Foundation.Data.Doublets.Cli + fi + echo "id=$CLI_ID" >> "$GITHUB_OUTPUT" + echo "library_id=$LIB_ID" >> "$GITHUB_OUTPUT" - name: Validate NuGet API key # Upfront validation surfaces an expired/invalid NUGET_API_KEY before @@ -360,7 +363,7 @@ jobs: echo "published=false" >> "$GITHUB_OUTPUT" fi - - name: Verify package on NuGet + - name: Verify CLI package on NuGet if: >- steps.nuget_publish.outputs.published == 'true' && ( steps.version.outputs.version_committed == 'true' || @@ -371,6 +374,17 @@ jobs: --package-id "${{ steps.package.outputs.id }}" \ --release-version "${{ steps.release_version.outputs.version }}" + - name: Verify library package on NuGet + if: >- + steps.nuget_publish.outputs.published == 'true' && ( + steps.version.outputs.version_committed == 'true' || + steps.version.outputs.already_released == 'true' || + (steps.check_release.outputs.should_release == 'true' && steps.check_release.outputs.skip_bump == 'true')) + run: | + node scripts/wait-for-nuget.mjs \ + --package-id "${{ steps.package.outputs.library_id }}" \ + --release-version "${{ steps.release_version.outputs.version }}" + - name: Create GitHub Release if: >- steps.version.outputs.version_committed == 'true' || @@ -386,6 +400,7 @@ jobs: --tag-prefix "csharp-v" \ --language "C#" \ --package-id "${{ steps.package.outputs.id }}" \ + --package-id "${{ steps.package.outputs.library_id }}" \ --changelog-path "csharp/CHANGELOG.md" \ --assets-glob "csharp/artifacts/*.nupkg" @@ -432,19 +447,22 @@ jobs: dotnet build --configuration Release dotnet pack --no-build --configuration Release --output ./artifacts - - name: Resolve NuGet package id + - name: Resolve NuGet package ids id: package if: >- steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' run: | - PACKAGE_ID=$(sed -n 's:.*\(.*\).*:\1:p' Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj | head -n 1) - if [ -z "$PACKAGE_ID" ]; then - PACKAGE_ID=clink + CLI_ID=$(sed -n 's:.*\(.*\).*:\1:p' Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj | head -n 1) + if [ -z "$CLI_ID" ]; then + CLI_ID=clink fi - PACKAGE_ID_LOWER=$(echo "$PACKAGE_ID" | tr '[:upper:]' '[:lower:]') - echo "id=$PACKAGE_ID" >> "$GITHUB_OUTPUT" - echo "flat_container_id=$PACKAGE_ID_LOWER" >> "$GITHUB_OUTPUT" + LIB_ID=$(sed -n 's:.*\(.*\).*:\1:p' Foundation.Data.Doublets.Cli.Library/Foundation.Data.Doublets.Cli.Library.csproj | head -n 1) + if [ -z "$LIB_ID" ]; then + LIB_ID=Foundation.Data.Doublets.Cli + fi + echo "id=$CLI_ID" >> "$GITHUB_OUTPUT" + echo "library_id=$LIB_ID" >> "$GITHUB_OUTPUT" - name: Publish to NuGet id: nuget_publish @@ -462,7 +480,7 @@ jobs: echo "published=false" >> "$GITHUB_OUTPUT" fi - - name: Verify package on NuGet + - name: Verify CLI package on NuGet if: >- steps.nuget_publish.outputs.published == 'true' && ( steps.version.outputs.version_committed == 'true' || @@ -472,6 +490,16 @@ jobs: --package-id "${{ steps.package.outputs.id }}" \ --release-version "${{ steps.version.outputs.new_version }}" + - name: Verify library package on NuGet + if: >- + steps.nuget_publish.outputs.published == 'true' && ( + steps.version.outputs.version_committed == 'true' || + steps.version.outputs.already_released == 'true') + run: | + node scripts/wait-for-nuget.mjs \ + --package-id "${{ steps.package.outputs.library_id }}" \ + --release-version "${{ steps.version.outputs.new_version }}" + - name: Create GitHub Release if: >- steps.version.outputs.version_committed == 'true' || @@ -486,5 +514,6 @@ jobs: --tag-prefix "csharp-v" \ --language "C#" \ --package-id "${{ steps.package.outputs.id }}" \ + --package-id "${{ steps.package.outputs.library_id }}" \ --changelog-path "csharp/CHANGELOG.md" \ --assets-glob "csharp/artifacts/*.nupkg" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..de17de0 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,159 @@ +name: Docs + +# Build and deploy a unified API reference site that hosts both the C# +# (`Foundation.Data.Doublets.Cli`) and Rust (`link-cli`) library docs. +# +# GitHub Pages allows only one deployment per repository, so this workflow +# combines DocFX-generated C# docs (under `/csharp/`) and `cargo doc`-generated +# Rust docs (under `/rust/`) into a single site that also includes a small +# landing page linking out to both. The deploy job runs on pushes to `main` +# and on manual dispatch, mirroring the AI driven development pipeline +# templates' approach (see docs/case-studies/issue-92/templates). + +on: + push: + branches: [main] + paths: + - 'csharp/Foundation.Data.Doublets.Cli.Library/**' + - 'csharp/docs/**' + - 'csharp/docfx.json' + - 'rust/src/**' + - 'rust/Cargo.toml' + - '.github/workflows/docs.yml' + pull_request: + branches: [main] + paths: + - 'csharp/Foundation.Data.Doublets.Cli.Library/**' + - 'csharp/docs/**' + - 'csharp/docfx.json' + - 'rust/src/**' + - 'rust/Cargo.toml' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: docs-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build documentation + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-docs-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-docs- + + - name: Install DocFX + run: dotnet tool update -g docfx + + - name: Restore C# dependencies + working-directory: csharp + run: dotnet restore + + - name: Build C# documentation + working-directory: csharp + run: docfx docfx.json -o _site + + - name: Build Rust documentation + run: cargo doc --manifest-path rust/Cargo.toml --no-deps --all-features + + - name: Assemble unified site + run: | + set -euo pipefail + mkdir -p _site/csharp _site/rust + cp -R csharp/_site/. _site/csharp/ + cp -R rust/target/doc/. _site/rust/ + # Landing page that links into both sub-sites. + cat > _site/index.html <<'HTML' + + + + + link-cli API documentation + + + + +

link-cli API documentation

+

Generated reference for the C# and Rust library packages that + ship alongside the clink CLI.

+ +

Source: github.com/link-foundation/link-cli

+ + + HTML + + - name: List unified site (debug) + run: | + echo "::group::_site tree" + find _site -maxdepth 3 -print + echo "::endgroup::" + + - name: Configure GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/configure-pages@v6 + + - name: Upload GitHub Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/upload-pages-artifact@v5 + with: + path: _site + + deploy: + name: Deploy to GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + + - name: Print resolved deployment URL (debug) + run: | + echo "Pages deployed to: ${{ steps.deployment.outputs.page_url }}" diff --git a/.gitignore b/.gitignore index a5cbf17..eb388a0 100644 --- a/.gitignore +++ b/.gitignore @@ -418,3 +418,9 @@ ci-logs/ gh-data/ experiments/ .playwright-mcp/ + +# Generated API documentation +csharp/_site/ +csharp/docs/api/*.yml +csharp/docs/api/*.manifest +_site/ diff --git a/README.md b/README.md index 28c1642..c811bac 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,10 @@ [![C# CI/CD Pipeline](https://github.com/link-foundation/link-cli/actions/workflows/csharp.yml/badge.svg)](https://github.com/link-foundation/link-cli/actions/workflows/csharp.yml) [![Rust CI/CD Pipeline](https://github.com/link-foundation/link-cli/actions/workflows/rust.yml/badge.svg)](https://github.com/link-foundation/link-cli/actions/workflows/rust.yml) [![WebAssembly CI](https://github.com/link-foundation/link-cli/actions/workflows/wasm.yml/badge.svg)](https://github.com/link-foundation/link-cli/actions/workflows/wasm.yml) -[![NuGet](https://img.shields.io/nuget/v/clink?logo=nuget&label=NuGet)](https://www.nuget.org/packages/clink) +[![NuGet (clink)](https://img.shields.io/nuget/v/clink?logo=nuget&label=clink)](https://www.nuget.org/packages/clink) +[![NuGet (library)](https://img.shields.io/nuget/v/Foundation.Data.Doublets.Cli?logo=nuget&label=Foundation.Data.Doublets.Cli)](https://www.nuget.org/packages/Foundation.Data.Doublets.Cli) [![Crates.io](https://img.shields.io/crates/v/link-cli?logo=rust&label=Crates.io)](https://crates.io/crates/link-cli) +[![Docs.rs](https://docs.rs/link-cli/badge.svg)](https://docs.rs/link-cli) [![C# Release](https://img.shields.io/github/v/release/link-foundation/link-cli?filter=csharp-v*&label=C%23%20release)](https://github.com/link-foundation/link-cli/releases?q=C%23&expanded=true) [![Rust Release](https://img.shields.io/github/v/release/link-foundation/link-cli?filter=rust-v*&label=Rust%20release)](https://github.com/link-foundation/link-cli/releases?q=Rust&expanded=true) @@ -12,9 +14,15 @@ It is based on [associative theory](https://habr.com/ru/articles/895896) (also in [ru](https://habr.com/ru/articles/804617)) and [Links Notation](https://github.com/linksplatform/Protocols.Lino) (also in [ru](https://github.com/linksplatform/Protocols.Lino/blob/main/README.ru.md)) -It includes a production C# CLI package, a Rust CLI/library package, and a -Rust-powered WebAssembly browser workbench built on [links data store](https://github.com/linksplatform?view_as=public) +It includes a production C# CLI/library pair on NuGet, a Rust CLI/library +crate on Crates.io, and a Rust-powered WebAssembly browser workbench built on +[links data store](https://github.com/linksplatform?view_as=public) concepts (see also in [ru](https://github.com/linksplatform/.github/blob/main/profile/README.ru.md)). +Both ecosystems ship a runnable CLI and a reusable public library with full +auto-generated API documentation (DocFX for C#, `cargo doc`/docs.rs for Rust) +so external projects can embed the parser, query processors, decorators, +named/pinned types, persistent transformation triggers, and LiNo I/O without +re-implementing any of the internals. ## WebAssembly Browser Workbench @@ -31,29 +39,53 @@ package built from `doublets-rs`. - [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md): implemented and planned requirements collected from issues and PR comments. - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): repository layout, major components, dependencies, storage files, and CI. - [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md): deeper explanation of query processing, references, import/export, triggers, and the WebAssembly workbench. -- [docs/case-studies/issue-71/README.md](docs/case-studies/issue-71/README.md): evidence and analysis behind this documentation refresh. +- [docs/case-studies/issue-71/README.md](docs/case-studies/issue-71/README.md): evidence and analysis behind the original documentation refresh. +- [docs/case-studies/issue-92/README.md](docs/case-studies/issue-92/README.md): evidence and analysis behind the dual CLI + library packaging and unified API documentation site. + +### API references + +- C# library: package page at and the DocFX-generated reference on [GitHub Pages](https://link-foundation.github.io/link-cli/csharp/). +- Rust library: (also mirrored on [GitHub Pages](https://link-foundation.github.io/link-cli/rust/link_cli/)). +- Combined landing page: . ## Installation Language package documentation: -- [C# NuGet tool](csharp/README.md) -- [Rust crate](rust/README.md) +- [C# NuGet tool and library](csharp/README.md) +- [Rust crate (CLI + library)](rust/README.md) + +Each language ships **both** a CLI binary and a public library, so external +projects can either run the tool directly or pull in the parser, query +processors, decorators, and LiNo I/O via a package reference. -This CLI tool can be installed globally as `clink` using single command (that will work if you have [.NET](https://dotnet.microsoft.com/en-us/download) installed): +### C# (.NET) ```bash +# CLI: install the `clink` command globally. dotnet tool install --global clink + +# Library: embed the parser, processors, and decorators in another .NET project. +dotnet add package Foundation.Data.Doublets.Cli ``` Screenshot 2025-05-16 at 5 48 06 AM -The NuGet tool is the C# implementation and exposes the complete production -command surface, including persistent transformation triggers. The Rust -implementation under `rust/` mirrors the core query engine, named references, -LiNo import/export, structure formatting, and the WebAssembly workbench API. -Persistent transformation trigger CLI options currently exist only in the C# -tool. +### Rust + +```bash +# CLI: build and install the `clink` binary from crates.io. +cargo install link-cli + +# Library: pull `link_cli` into your own Cargo project. +cargo add link-cli +``` + +The NuGet CLI tool is the C# implementation and exposes the complete production +command surface, including persistent transformation triggers. The Rust crate +mirrors the core query engine, named references, LiNo import/export, structure +formatting, and the WebAssembly workbench API. Persistent transformation +trigger CLI options currently exist only in the C# tool. This tool provides all CRUD operations for links using single [substitution operation](https://en.wikipedia.org/wiki/Markov_algorithm) ([ru](https://ru.wikipedia.org/wiki/Нормальный_алгоритм)) which is turing complete. diff --git a/csharp/.changeset/issue-92-library-package.md b/csharp/.changeset/issue-92-library-package.md new file mode 100644 index 0000000..4ec4e03 --- /dev/null +++ b/csharp/.changeset/issue-92-library-package.md @@ -0,0 +1,17 @@ +--- +'Foundation.Data.Doublets.Cli': minor +--- + +Split the C# distribution into two NuGet packages so external .NET +projects can consume the public library without pulling in the +`dotnet tool` packaging: + +- `clink` — unchanged dotnet tool, now built from a CLI csproj that only + contains `Program.cs` and `System.CommandLine` wiring. +- `Foundation.Data.Doublets.Cli` — new library package that ships the + parser, query processors (basic / advanced / mixed), `ChangesSimplifier`, + named/pinned type decorators, persistent transformation trigger + decorator, LiNo I/O adapters, the `UnicodeStringStorage` extension, and + every other reusable building block. Generated XML doc comments are + packed alongside the assembly and rendered into a DocFX site published + to GitHub Pages. diff --git a/csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs b/csharp/Foundation.Data.Doublets.Cli.Library/AdvancedMixedQueryProcessor.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/AdvancedMixedQueryProcessor.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/AdvancedMixedQueryProcessor.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/BasicQueryProcessor.cs b/csharp/Foundation.Data.Doublets.Cli.Library/BasicQueryProcessor.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/BasicQueryProcessor.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/BasicQueryProcessor.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/ChangesSimplifier.cs b/csharp/Foundation.Data.Doublets.Cli.Library/ChangesSimplifier.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/ChangesSimplifier.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/ChangesSimplifier.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/EnumerableExtensions.cs b/csharp/Foundation.Data.Doublets.Cli.Library/EnumerableExtensions.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/EnumerableExtensions.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/EnumerableExtensions.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/Exceptions.cs b/csharp/Foundation.Data.Doublets.Cli.Library/Exceptions.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/Exceptions.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/Exceptions.cs diff --git a/csharp/Foundation.Data.Doublets.Cli.Library/Foundation.Data.Doublets.Cli.Library.csproj b/csharp/Foundation.Data.Doublets.Cli.Library/Foundation.Data.Doublets.Cli.Library.csproj new file mode 100644 index 0000000..f781788 --- /dev/null +++ b/csharp/Foundation.Data.Doublets.Cli.Library/Foundation.Data.Doublets.Cli.Library.csproj @@ -0,0 +1,39 @@ + + + + net8 + enable + enable + Foundation.Data.Doublets.Cli + Foundation.Data.Doublets.Cli + + + + link-foundation + Public library exposing the parser, query processors, decorators, named/pinned types, persistent transformation triggers, and LiNo I/O used by the clink CLI. + Foundation.Data.Doublets.Cli + 2.4.0 + Unlicense + README.md + https://github.com/link-foundation/link-cli + git + links;doublets;lino;cli;library + https://github.com/link-foundation/link-cli + + + + true + $(NoWarn);CS1591 + + + + + + + + + + + + + diff --git a/csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs b/csharp/Foundation.Data.Doublets.Cli.Library/INamedTypes.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/INamedTypes.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/INamedTypes.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/LinksExtensions.cs b/csharp/Foundation.Data.Doublets.Cli.Library/LinksExtensions.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/LinksExtensions.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/LinksExtensions.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/LinoDatabaseInput.cs b/csharp/Foundation.Data.Doublets.Cli.Library/LinoDatabaseInput.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/LinoDatabaseInput.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/LinoDatabaseInput.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs b/csharp/Foundation.Data.Doublets.Cli.Library/LinoDatabaseOutput.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/LinoDatabaseOutput.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/MixedQueryProcessor.cs b/csharp/Foundation.Data.Doublets.Cli.Library/MixedQueryProcessor.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/MixedQueryProcessor.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/MixedQueryProcessor.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs b/csharp/Foundation.Data.Doublets.Cli.Library/NamedLinks.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/NamedLinks.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/NamedLinks.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedLinksDecorator.cs b/csharp/Foundation.Data.Doublets.Cli.Library/NamedLinksDecorator.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/NamedLinksDecorator.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/NamedLinksDecorator.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs b/csharp/Foundation.Data.Doublets.Cli.Library/NamedTypesDecorator.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/NamedTypesDecorator.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/NamedTypesDecorator.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/PersistentTransformationDecorator.cs b/csharp/Foundation.Data.Doublets.Cli.Library/PersistentTransformationDecorator.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/PersistentTransformationDecorator.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/PersistentTransformationDecorator.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/PinnedTypes.cs b/csharp/Foundation.Data.Doublets.Cli.Library/PinnedTypes.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/PinnedTypes.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/PinnedTypes.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/PinnedTypesDecorator.cs b/csharp/Foundation.Data.Doublets.Cli.Library/PinnedTypesDecorator.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/PinnedTypesDecorator.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/PinnedTypesDecorator.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/QueryConstants.cs b/csharp/Foundation.Data.Doublets.Cli.Library/QueryConstants.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/QueryConstants.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/QueryConstants.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/SimpleLinksDecorator.cs b/csharp/Foundation.Data.Doublets.Cli.Library/SimpleLinksDecorator.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/SimpleLinksDecorator.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/SimpleLinksDecorator.cs diff --git a/csharp/Foundation.Data.Doublets.Cli/UnicodeStringStorage.cs b/csharp/Foundation.Data.Doublets.Cli.Library/UnicodeStringStorage.cs similarity index 100% rename from csharp/Foundation.Data.Doublets.Cli/UnicodeStringStorage.cs rename to csharp/Foundation.Data.Doublets.Cli.Library/UnicodeStringStorage.cs diff --git a/csharp/Foundation.Data.Doublets.Cli.Tests/Foundation.Data.Doublets.Cli.Tests.csproj b/csharp/Foundation.Data.Doublets.Cli.Tests/Foundation.Data.Doublets.Cli.Tests.csproj index 2eac051..f620323 100644 --- a/csharp/Foundation.Data.Doublets.Cli.Tests/Foundation.Data.Doublets.Cli.Tests.csproj +++ b/csharp/Foundation.Data.Doublets.Cli.Tests/Foundation.Data.Doublets.Cli.Tests.csproj @@ -9,18 +9,19 @@ true - - - - - - + + + + + + + diff --git a/csharp/Foundation.Data.Doublets.Cli.sln b/csharp/Foundation.Data.Doublets.Cli.sln index 7897516..18f6b3b 100644 --- a/csharp/Foundation.Data.Doublets.Cli.sln +++ b/csharp/Foundation.Data.Doublets.Cli.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundation.Data.Doublets.Cl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundation.Data.Doublets.Cli", "Foundation.Data.Doublets.Cli\Foundation.Data.Doublets.Cli.csproj", "{85953C48-A60C-483B-8F9F-047119860251}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundation.Data.Doublets.Cli.Library", "Foundation.Data.Doublets.Cli.Library\Foundation.Data.Doublets.Cli.Library.csproj", "{56A05160-D764-417A-B9E2-98A124D599AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {85953C48-A60C-483B-8F9F-047119860251}.Debug|Any CPU.Build.0 = Debug|Any CPU {85953C48-A60C-483B-8F9F-047119860251}.Release|Any CPU.ActiveCfg = Release|Any CPU {85953C48-A60C-483B-8F9F-047119860251}.Release|Any CPU.Build.0 = Release|Any CPU + {56A05160-D764-417A-B9E2-98A124D599AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56A05160-D764-417A-B9E2-98A124D599AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56A05160-D764-417A-B9E2-98A124D599AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56A05160-D764-417A-B9E2-98A124D599AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj b/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj index c81b07e..36d494b 100644 --- a/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj +++ b/csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,25 +9,29 @@ clink README.md Foundation.Data.Doublets.Cli + clink link-foundation - A CLI tool for links manipulation. + A CLI tool for links manipulation. The reusable library API ships separately as the Foundation.Data.Doublets.Cli NuGet package. clink 2.4.0 Unlicense https://github.com/link-foundation/link-cli + git + cli;links;doublets;lino;tool + https://github.com/link-foundation/link-cli - - - - + + + + diff --git a/csharp/README.md b/csharp/README.md index 6d14bf0..9062ddb 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -1,11 +1,21 @@ # clink C# Package [![C# CI/CD Pipeline](https://github.com/link-foundation/link-cli/actions/workflows/csharp.yml/badge.svg)](https://github.com/link-foundation/link-cli/actions/workflows/csharp.yml) -[![NuGet](https://img.shields.io/nuget/v/clink?logo=nuget&label=NuGet)](https://www.nuget.org/packages/clink) +[![NuGet (clink)](https://img.shields.io/nuget/v/clink?logo=nuget&label=clink)](https://www.nuget.org/packages/clink) +[![NuGet (library)](https://img.shields.io/nuget/v/Foundation.Data.Doublets.Cli?logo=nuget&label=Foundation.Data.Doublets.Cli)](https://www.nuget.org/packages/Foundation.Data.Doublets.Cli) [![GitHub Release](https://img.shields.io/github/v/release/link-foundation/link-cli?filter=csharp-v*&label=C%23%20release)](https://github.com/link-foundation/link-cli/releases) -This directory contains the production .NET CLI implementation published as -the NuGet tool package `clink`. +This directory contains the production .NET implementation, published as two +NuGet packages from a shared source tree: + +| Package | Project | Install | Use case | +| --- | --- | --- | --- | +| [`clink`](https://www.nuget.org/packages/clink) | `Foundation.Data.Doublets.Cli/` | `dotnet tool install --global clink` | Run the CLI without writing any C#. | +| [`Foundation.Data.Doublets.Cli`](https://www.nuget.org/packages/Foundation.Data.Doublets.Cli) | `Foundation.Data.Doublets.Cli.Library/` | `dotnet add package Foundation.Data.Doublets.Cli` | Embed the parser, query processors, decorators, named/pinned types, persistent transformation trigger decorator, and LiNo I/O in another .NET project. | + +The CLI csproj only contains `Program.cs` plus the `System.CommandLine` +wiring; every reusable type lives in the library project so external apps +can recreate or extend the CLI without re-implementing any of the internals. ## Install @@ -19,15 +29,27 @@ Update an existing installation: dotnet tool update --global clink ``` +To consume the library inside another .NET project: + +```bash +dotnet add package Foundation.Data.Doublets.Cli +``` + +API documentation is generated by DocFX from the library's XML doc +comments and is published to GitHub Pages alongside the Rust rustdoc +site by `.github/workflows/docs.yml`. + ## Use ```bash clink '() ((1 1))' --changes --after ``` -The C# package exposes the complete command surface, including persistent +The CLI exposes the complete command surface, including persistent transformation triggers with `--always`, `--once`, `--never`, `--triggers`, -`--triggers-file`, and `--embed-triggers`. +`--triggers-file`, and `--embed-triggers`. Each option is implemented in +the public library, so other .NET applications can call into the same +processors directly. ## Develop diff --git a/csharp/docfx.json b/csharp/docfx.json new file mode 100644 index 0000000..1d936cb --- /dev/null +++ b/csharp/docfx.json @@ -0,0 +1,44 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "Foundation.Data.Doublets.Cli.Library", + "files": ["**/*.csproj"] + } + ], + "dest": "docs/api", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "allowCompilationErrors": false + } + ], + "build": { + "content": [ + { + "files": ["**/*.{md,yml}"], + "src": "docs", + "exclude": ["_site/**", "obj/**"] + } + ], + "resource": [ + { + "files": ["images/**"], + "src": "docs", + "exclude": ["_site/**", "obj/**"] + } + ], + "output": "_site", + "globalMetadata": { + "_appName": "Foundation.Data.Doublets.Cli", + "_appTitle": "Foundation.Data.Doublets.Cli API Documentation", + "_enableSearch": true, + "pdf": false + }, + "template": ["default", "modern"] + } +} diff --git a/csharp/docs/index.md b/csharp/docs/index.md new file mode 100644 index 0000000..863c50c --- /dev/null +++ b/csharp/docs/index.md @@ -0,0 +1,16 @@ +# Foundation.Data.Doublets.Cli + +The `Foundation.Data.Doublets.Cli` NuGet package exposes the parser, query +processors (basic, advanced, mixed), `ChangesSimplifier`, the named/pinned +type decorators, the persistent transformation trigger decorator, LiNo +import/export adapters, the `UnicodeStringStorage` extension, and every +other building block the [`clink`](https://www.nuget.org/packages/clink) +.NET tool is composed of. External .NET projects can pull it in via +`` and recreate or extend the CLI behavior without +re-implementing any of the underlying machinery. + +The companion `clink` package keeps shipping as a `dotnet tool` so users +can install the CLI with `dotnet tool install --global clink`. + +Browse the [API reference](api/Foundation.Data.Doublets.Cli.yml) for the +full list of namespaces and types. diff --git a/csharp/docs/toc.yml b/csharp/docs/toc.yml new file mode 100644 index 0000000..af59a15 --- /dev/null +++ b/csharp/docs/toc.yml @@ -0,0 +1,4 @@ +- name: Overview + href: index.md +- name: API Reference + href: api/ diff --git a/csharp/scripts/create-github-release.mjs b/csharp/scripts/create-github-release.mjs index 9c5183c..89d6781 100644 --- a/csharp/scripts/create-github-release.mjs +++ b/csharp/scripts/create-github-release.mjs @@ -39,38 +39,73 @@ export function getArg(argv, name, fallback = null) { return fallback; } +/** + * Collect every occurrence of `--flag value` and `--flag=value` style arguments. + * Used when a single release covers multiple NuGet packages (e.g. the CLI tool + * package plus its sibling library package). + * @param {string[]} argv + * @param {string} name + * @returns {string[]} + */ +export function getAllArgs(argv, name) { + const flag = `--${name}`; + const eqPrefix = `${flag}=`; + const values = []; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === flag) { + if (argv[i + 1] !== undefined) { + values.push(argv[i + 1]); + i++; + } + continue; + } + if (a.startsWith(eqPrefix)) { + values.push(a.slice(eqPrefix.length)); + } + } + return values; +} + /** * Build NuGet version + downloads badges that link to a specific package version. * Mirrors the Rust release script that emits Crates.io + Docs.rs badges. - * @param {string} packageId NuGet package id (e.g. `clink`). + * Accepts either a single package id (legacy callers) or an array — when an + * array is supplied each id's two badges are joined onto one line. + * @param {string|string[]} packageIds NuGet package id(s). * @param {string} version Bare semver string (e.g. `2.4.0`). - * @returns {string} A single line of two markdown badges separated by a space. + * @returns {string} A single line of markdown badges. */ -export function buildNugetBadges(packageId, version) { - const id = encodeURIComponent(packageId); +export function buildNugetBadges(packageIds, version) { + const ids = Array.isArray(packageIds) ? packageIds : [packageIds]; const versionPath = encodeURIComponent(version); - const versionUrl = `https://www.nuget.org/packages/${id}/${versionPath}`; - const versionBadge = `[![NuGet](https://img.shields.io/nuget/v/${id}?logo=nuget&label=NuGet)](${versionUrl})`; - const downloadsBadge = `[![NuGet Downloads](https://img.shields.io/nuget/dt/${id}?logo=nuget&label=downloads)](${versionUrl})`; - return `${versionBadge} ${downloadsBadge}`; + const lines = ids.map((packageId) => { + const id = encodeURIComponent(packageId); + const versionUrl = `https://www.nuget.org/packages/${id}/${versionPath}`; + const versionBadge = `[![NuGet](https://img.shields.io/nuget/v/${id}?logo=nuget&label=NuGet)](${versionUrl})`; + const downloadsBadge = `[![NuGet Downloads](https://img.shields.io/nuget/dt/${id}?logo=nuget&label=downloads)](${versionUrl})`; + return `${versionBadge} ${downloadsBadge}`; + }); + return lines.join('\n'); } /** - * Prepend NuGet badges to release notes when a package id is known and badges - * are not already present. Returns the notes unchanged otherwise. + * Prepend NuGet badges to release notes when one or more package ids are + * known and shield.io NuGet badges are not already present. * @param {string} releaseNotes - * @param {string} packageId + * @param {string|string[]} packageIds * @param {string} version * @returns {string} */ -export function prependNugetBadges(releaseNotes, packageId, version) { - if (!packageId || !version) { +export function prependNugetBadges(releaseNotes, packageIds, version) { + const ids = (Array.isArray(packageIds) ? packageIds : [packageIds]).filter(Boolean); + if (ids.length === 0 || !version) { return releaseNotes; } if (/img\.shields\.io\/nuget\//i.test(releaseNotes)) { return releaseNotes; } - return `${buildNugetBadges(packageId, version)}\n\n${releaseNotes}`; + return `${buildNugetBadges(ids, version)}\n\n${releaseNotes}`; } /** @@ -101,21 +136,28 @@ export function getChangelogForVersion(version, changelogPath) { /** * Build the GitHub release API payload. - * @param {{changelogPath: string, language: string, packageId: string, releaseVersion: string, tagPrefix: string}} options + * @param {{changelogPath: string, language: string, packageId?: string, packageIds?: string[], releaseVersion: string, tagPrefix: string}} options * @returns {{tag_name: string, name: string, body: string}} */ export function buildReleasePayload({ changelogPath, language, packageId, + packageIds, releaseVersion, tagPrefix, }) { + const ids = (packageIds && packageIds.length > 0 + ? packageIds + : [packageId].filter(Boolean)); const changelogNotes = getChangelogForVersion(releaseVersion, changelogPath); - const notesWithPackage = packageId - ? `${changelogNotes}\n\nPackage: \`${packageId}\`` - : changelogNotes; - const body = prependNugetBadges(notesWithPackage, packageId, releaseVersion); + const footer = ids.length === 0 + ? '' + : ids.length === 1 + ? `\n\nPackage: \`${ids[0]}\`` + : `\n\nPackages: ${ids.map((id) => `\`${id}\``).join(', ')}`; + const notesWithPackage = `${changelogNotes}${footer}`; + const body = prependNugetBadges(notesWithPackage, ids, releaseVersion); return { tag_name: `${tagPrefix}${releaseVersion}`, @@ -155,7 +197,8 @@ function main(argv) { const tagPrefix = getArg(argv, 'tag-prefix', 'v'); const changelogPath = getArg(argv, 'changelog-path', 'CHANGELOG.md'); const language = getArg(argv, 'language', ''); - const packageId = getArg(argv, 'package-id', ''); + const packageIds = getAllArgs(argv, 'package-id'); + const packageId = packageIds[0] || ''; const assetsGlob = getArg(argv, 'assets-glob', ''); const dryRun = argv.includes('--dry-run'); @@ -171,6 +214,7 @@ function main(argv) { changelogPath, language, packageId, + packageIds, releaseVersion: version, tagPrefix, }); diff --git a/csharp/scripts/release-scripts.test.mjs b/csharp/scripts/release-scripts.test.mjs index 3a13244..f7cd853 100644 --- a/csharp/scripts/release-scripts.test.mjs +++ b/csharp/scripts/release-scripts.test.mjs @@ -193,6 +193,48 @@ test('prependNugetBadges is a no-op without a package id', () => { assert.equal(prependNugetBadges(notes, 'clink', ''), notes); }); +test('buildNugetBadges accepts multiple package ids', () => { + const badges = buildNugetBadges(['clink', 'Foundation.Data.Doublets.Cli'], '2.4.0'); + + assert.match(badges, /img\.shields\.io\/nuget\/v\/clink\?/); + assert.match( + badges, + /img\.shields\.io\/nuget\/v\/Foundation\.Data\.Doublets\.Cli\?/ + ); + assert.match(badges, /img\.shields\.io\/nuget\/dt\/clink\?/); + assert.match( + badges, + /img\.shields\.io\/nuget\/dt\/Foundation\.Data\.Doublets\.Cli\?/ + ); +}); + +test('buildReleasePayload includes badges for both CLI and library packages', () => { + const dir = mkdtempSync(join(tmpdir(), 'link-cli-dual-release-payload-')); + const changelog = join(dir, 'CHANGELOG.md'); + writeFileSync( + changelog, + '# Changelog\n\n## [2.4.0] - 2026-05-12\n\nDual package release.\n' + ); + + const payload = buildReleasePayload({ + changelogPath: changelog, + language: 'C#', + packageIds: ['clink', 'Foundation.Data.Doublets.Cli'], + releaseVersion: '2.4.0', + tagPrefix: 'csharp-v', + }); + + assert.match(payload.body, /img\.shields\.io\/nuget\/v\/clink\?/); + assert.match( + payload.body, + /img\.shields\.io\/nuget\/v\/Foundation\.Data\.Doublets\.Cli\?/ + ); + assert.match( + payload.body, + /Packages: `clink`, `Foundation\.Data\.Doublets\.Cli`/ + ); +}); + test('buildReleasePayload places NuGet badges above the package footer', () => { const dir = mkdtempSync(join(tmpdir(), 'link-cli-release-payload-')); const changelog = join(dir, 'CHANGELOG.md'); diff --git a/csharp/scripts/version-and-commit.mjs b/csharp/scripts/version-and-commit.mjs index 10a8f8b..1ea9d5e 100644 --- a/csharp/scripts/version-and-commit.mjs +++ b/csharp/scripts/version-and-commit.mjs @@ -23,6 +23,12 @@ import { execSync } from 'child_process'; // Package name must match the package name in the changeset files const PACKAGE_NAME = 'Foundation.Data.Doublets.Cli'; const CSPROJ_PATH = 'csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj'; +// The library project ships as a separate NuGet package +// (`Foundation.Data.Doublets.Cli`) and must share the CLI tool's release +// version so both packages reference each other consistently. The path +// is optional — when the library csproj does not exist (e.g. inside unit +// tests for this script), the bump silently skips the second update. +const LIBRARY_CSPROJ_PATH = 'csharp/Foundation.Data.Doublets.Cli.Library/Foundation.Data.Doublets.Cli.Library.csproj'; const CHANGESET_DIR = 'csharp/.changeset'; const CHANGELOG_FILE = 'csharp/CHANGELOG.md'; @@ -110,17 +116,29 @@ function calculateNewVersion(current, bumpType) { } /** - * Update version in csproj + * Update version in a csproj at the given path. + * @param {string} path * @param {string} newVersion */ -function updateCsproj(newVersion) { - let csproj = readFileSync(CSPROJ_PATH, 'utf-8'); +function updateCsprojAt(path, newVersion) { + let csproj = readFileSync(path, 'utf-8'); csproj = csproj.replace( /[^<]+<\/Version>/, `${newVersion}` ); - writeFileSync(CSPROJ_PATH, csproj, 'utf-8'); - console.log(`Updated csproj to version ${newVersion}`); + writeFileSync(path, csproj, 'utf-8'); + console.log(`Updated ${path} to version ${newVersion}`); +} + +/** + * Update version in the CLI csproj and, when present, the library csproj. + * @param {string} newVersion + */ +function updateCsproj(newVersion) { + updateCsprojAt(CSPROJ_PATH, newVersion); + if (existsSync(LIBRARY_CSPROJ_PATH)) { + updateCsprojAt(LIBRARY_CSPROJ_PATH, newVersion); + } } /** @@ -368,7 +386,11 @@ try { } // Stage all changed files - exec(`git add ${CSPROJ_PATH} ${CHANGELOG_FILE} ${CHANGESET_DIR}/`); + const stagedPaths = [CSPROJ_PATH, CHANGELOG_FILE, `${CHANGESET_DIR}/`]; + if (existsSync(LIBRARY_CSPROJ_PATH)) { + stagedPaths.push(LIBRARY_CSPROJ_PATH); + } + exec(`git add ${stagedPaths.join(' ')}`); // Check if there are changes to commit try { diff --git a/docs/case-studies/issue-92/README.md b/docs/case-studies/issue-92/README.md new file mode 100644 index 0000000..3169d97 --- /dev/null +++ b/docs/case-studies/issue-92/README.md @@ -0,0 +1,255 @@ +# Issue 92 Case Study: Ship public library packages alongside CLI for both C# and Rust + +Issue: https://github.com/link-foundation/link-cli/issues/92 + +Prepared PR: [#93](https://github.com/link-foundation/link-cli/pull/93) + +## Restated requirements + +From the issue body and the linked templates, broken into testable requirements: + +1. **R1 – C# NuGet must publish a public library**, not only a CLI tool. Any + external .NET project must be able to add a `` and reuse + all the public APIs of `Foundation.Data.Doublets.Cli` (parser, query + processors, decorators, storage adapters, named/pinned types, persistent + transformation decorator, LiNo import/export). +2. **R2 – Rust Crates.io must publish a public library**, not only a CLI + binary. Any external Rust project must be able to `cargo add link-cli` and + reuse all the public APIs of `link_cli` (storage, parser, query processor, + named types, LiNo I/O). +3. **R3 – C# CLI must remain available** as a .NET global tool published to + NuGet (`dotnet tool install --global clink`). +4. **R4 – Rust CLI must remain available** as a binary published to Crates.io + (`cargo install link-cli`). +5. **R5 – Both languages must ship the same feature surface in the library** + that the CLI uses. The library should not be a thin subset. +6. **R6 – Automatically generated API documentation** must be published for + both libraries. + - C#: `true` plus + a DocFX-built site hosted on GitHub Pages (matching the + [csharp-ai-driven-development-pipeline-template](https://github.com/link-foundation/csharp-ai-driven-development-pipeline-template)). + - Rust: docs.rs already builds rustdoc for published crates; on top of + that, the rust template also deploys `cargo doc --no-deps --all-features` + to GitHub Pages (`deploy-docs` job in + [rust-ai-driven-development-pipeline-template/.github/workflows/release.yml](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/blob/main/.github/workflows/release.yml)). +7. **R7 – Reuse CI/CD best practices** from the four templates + (`csharp-`, `rust-`, `js-`, `python-ai-driven-development-pipeline-template`) + and report any defects discovered in the templates themselves. +8. **R8 – Compile case-study data** to `./docs/case-studies/issue-92/`, list + every requirement, propose solutions, and search online for additional + facts. +9. **R9 – Plan and execute everything in a single pull request** (PR #93). + +## Evidence captured in this folder + +- `github-data/issue-92.json` and `github-data/issue-92-comments.json` — the + upstream issue and its comments at investigation time. +- `github-data/pr-93.json` — PR snapshot. +- `templates//file-tree.txt` — full file listing of each of + the four templates. +- `templates/csharp-ai-driven-development-pipeline-template/MyPackage.csproj.snapshot` + — the template's library `.csproj` showing `true` + and the absence of `true`. This is the structure + the link-cli C# project should follow for its library half. +- `templates/csharp-ai-driven-development-pipeline-template/docfx.json.snapshot` + and `docs.yml.snapshot` — DocFX configuration and the GitHub Pages workflow + to deploy the resulting site. +- `templates/csharp-ai-driven-development-pipeline-template/Directory.Build.props.snapshot` + — common build properties used by the template (strict warnings, analyzers, + latest C# version). +- `templates/rust-ai-driven-development-pipeline-template/Cargo.toml.snapshot` + — the template `Cargo.toml` defining both `[lib]` and `[[bin]]` and showing + the `[lints.*]` blocks plus `[profile.release]` LTO/strip settings. +- `templates/rust-ai-driven-development-pipeline-template/lib.rs.snapshot` — + the template's library entry point. +- `templates/rust-ai-driven-development-pipeline-template/release.yml.snapshot` + — the full pipeline including the `deploy-docs` job (lines 636–675) that + publishes `cargo doc --no-deps --all-features` to GitHub Pages. + +## Online research + +- Microsoft Learn — _".NET tools"_ documentation explains that a `dotnet tool` + package is a NuGet package that wraps a console application. It is consumed + via `dotnet tool install`, **not** via `PackageReference`. Source: + . +- Microsoft Learn — _"Create a NuGet package using MSBuild"_ — a single + `.csproj` produces one NuGet package per build. Packing both a tool and a + library from the same code base therefore requires two projects (or two + packs of the same project with different `` values). Source: + . +- docs.rs `link-cli` page confirms that the Rust crate is already discoverable + on docs.rs because it has a `[lib]` target; documentation coverage at + investigation time was 19.19 %. Source: . +- docs.rs _"About / Builds"_ documents that every crate published to crates.io + is built; bin-only crates without a `[lib]` produce no rustdoc. Source: + . +- NuGet supports embedding XML documentation in a package via the SDK's + `true` MSBuild + property. Source: . +- DocFX is the de-facto static site generator for .NET XML doc comments and + is what the csharp template uses. Source: + . + +## Root cause + +`csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj` +declared both `Exe` and `true` +on a **single project**: + +```xml +Exe +net8 +true +clink +clink +``` + +As a .NET tool, the resulting NuGet package can only be installed with +`dotnet tool install --global clink`. .NET tools intentionally cannot be +consumed by other projects via `` — the package layout +under `tools//any/` is not the layout `restore` resolves to when adding +a library dependency. The implication is that: + +- All the source code (parser, query processors, named-type decorator, + pinned-type decorator, persistent transformation decorator, LiNo I/O, + Unicode string storage) is already **`public`** in the assembly, but +- It is shipped inside a tool-only NuGet package, so downstream .NET code + cannot reuse it. + +The Rust side does not have this defect because `rust/Cargo.toml` already +declares both `[lib] name = "link_cli"` and `[[bin]] name = "clink"`. The +crate is therefore consumed in three ways already: + +- `cargo install link-cli` → installs the `clink` binary; +- `cargo add link-cli` → adds the `link_cli` library; +- `https://docs.rs/link-cli` → renders rustdoc automatically. + +What is missing on the Rust side is: + +- The `deploy-docs` workflow job that the rust template publishes to GitHub + Pages — useful as a single homepage for the project's API docs. +- Any explicit "this crate is also a library" callout in `rust/README.md`. + +## Solution + +### S1 — Split the C# project into a library and a tool + +Refactor `csharp/`: + +``` +csharp/ +├── Foundation.Data.Doublets.Cli.Library/ # NEW: library project +│ ├── Foundation.Data.Doublets.Cli.Library.csproj +│ └── *.cs # moved from CLI project +├── Foundation.Data.Doublets.Cli/ # CLI tool project (unchanged id `clink`) +│ ├── Foundation.Data.Doublets.Cli.csproj # now references Library, contains only Program.cs +│ └── Program.cs +└── Foundation.Data.Doublets.Cli.Tests/ # references Library +``` + +- Library `.csproj`: regular SDK library, no ``, with + `Foundation.Data.Doublets.Cli`, + `true`, and explicit + package metadata mirroring the template. +- CLI `.csproj`: keeps `true`, + `clink`, + `clink`, and adds a `` to the + library plus `` for the library NuGet so the resolved + tool package depends on the published library at the same version. The + tool's source set shrinks to `Program.cs`. +- All existing namespaces (`Foundation.Data.Doublets.Cli`) stay the same so + the test project (and any downstream code) keeps compiling. +- `Foundation.Data.Doublets.Cli.sln` adds the new library project. + +### S2 — Pack and publish both NuGet packages + +Update `.github/workflows/csharp.yml` so that both the `release` and +`instant-release` jobs: + +- run `dotnet pack` for **all** packable projects in the solution + (i.e. drop the implicit single-project assumption); +- run `dotnet nuget push ./artifacts/*.nupkg ... --skip-duplicate` so that + both the library and the tool get pushed; +- still validate that the library appears on NuGet via the existing + `wait-for-nuget.mjs` script (now parameterized over both ids). + +`csharp/scripts/check-release-needed.mjs` already reads +`` from `Foundation.Data.Doublets.Cli.csproj`; the library project +mirrors that version so both packages share a release cycle. + +### S3 — Generate and host C# API docs + +- Add `docs/case-studies/issue-92/...` (this case study) plus + `csharp/docfx.json` configured to pick up the library project. +- Add `.github/workflows/csharp-docs.yml`, structurally identical to the + csharp template's `docs.yml`, gated to `main` and manual dispatch only, + building with `docfx csharp/docfx.json -o csharp/_site` and deploying via + `actions/deploy-pages@v5`. +- Set `true` on the + library project so each NuGet build embeds the XML docs. + +### S4 — Add Rust API docs deploy + tighten lib metadata + +- Surface that the crate is also a library in `rust/README.md`. +- Tighten `rust/Cargo.toml`: + - Add `documentation = "https://docs.rs/link-cli"`. + - Add `categories = ["command-line-utilities", "database", "data-structures"]`. +- Add the `deploy-docs` job to `.github/workflows/rust.yml`, gated to + `push to main` / `workflow_dispatch`, that runs + `cargo doc --no-deps --all-features` from `rust/` and publishes + `rust/target/doc` via `actions/upload-pages-artifact@v5` + + `actions/deploy-pages@v5`. Mirrors the rust template's job (lines 636–675). + +### S5 — README and examples + +- Update root `README.md` to list both NuGet packages and a one-liner + example for each (CLI install vs. library reference). +- Update `csharp/README.md` and `rust/README.md` to document the library + surface and link to the generated docs. +- Add `examples/library-csharp/` and `examples/library-rust/` minimal sample + projects so contributors can validate the library experience locally. + +### S6 — Tests for the library surface + +- Existing xUnit tests already use the public APIs; they automatically cover + the library after the project split. +- Add an integration test that builds the library `.csproj` and a tiny + consumer `.csproj` to assert that referencing the produced `.nupkg` works + end to end (rather than the implicit `ProjectReference`). +- Existing Rust integration tests in `rust/tests/` already exercise the + `link_cli::*` library surface and remain unchanged. + +## Template defects to flag (R7) + +While comparing, no defect was found that prevents the templates from +generating both a library and a CLI — the csharp template ships **only** a +library and the rust template ships **only** a library (no `[[bin]]`). +Neither template demonstrates the joint case directly. This is not a defect +per se, but it explains why the link-cli C# project diverged: the template +did not show the dual-project pattern. + +Follow-up suggestion (filed as an upstream improvement in the link-cli PR +description rather than a separate issue, per the case-study convention): +extend the csharp template with a second project that demonstrates packing +the library as both a regular NuGet and a `PackAsTool` CLI sharing the same +library. If the link-foundation maintainers agree, file +`link-foundation/csharp-ai-driven-development-pipeline-template#new` once +this PR ships. + +## Verification plan + +- `dotnet build csharp/Foundation.Data.Doublets.Cli.sln -c Release` succeeds + with `TreatWarningsAsErrors=true` once XML doc warnings are silenced + (`CS1591` allow-list, per template). +- `dotnet test csharp/Foundation.Data.Doublets.Cli.sln -c Release` keeps + passing without test changes. +- `dotnet pack csharp/Foundation.Data.Doublets.Cli.Library/...csproj` + produces a `Foundation.Data.Doublets.Cli..nupkg` whose + `lib/net8.0/` folder contains the assembly and `*.xml` doc file. +- `dotnet pack csharp/Foundation.Data.Doublets.Cli/...csproj` produces a + `clink..nupkg` with `tools/net8.0/any/` content. +- `cargo build --manifest-path rust/Cargo.toml --release` keeps working. +- `cargo doc --no-deps --all-features --manifest-path rust/Cargo.toml` + produces `rust/target/doc/link_cli/index.html`. +- CI on PR #93 keeps passing across `ubuntu-latest`, `macos-latest`, + `windows-latest`. diff --git a/docs/case-studies/issue-92/github-data/issue-92-comments.json b/docs/case-studies/issue-92/github-data/issue-92-comments.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/docs/case-studies/issue-92/github-data/issue-92-comments.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/case-studies/issue-92/github-data/issue-92.json b/docs/case-studies/issue-92/github-data/issue-92.json new file mode 100644 index 0000000..d4cf289 --- /dev/null +++ b/docs/case-studies/issue-92/github-data/issue-92.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/92","repository_url":"https://api.github.com/repos/link-foundation/link-cli","labels_url":"https://api.github.com/repos/link-foundation/link-cli/issues/92/labels{/name}","comments_url":"https://api.github.com/repos/link-foundation/link-cli/issues/92/comments","events_url":"https://api.github.com/repos/link-foundation/link-cli/issues/92/events","html_url":"https://github.com/link-foundation/link-cli/issues/92","id":4454097794,"node_id":"I_kwDONXCAbs8AAAABCXwjgg","number":92,"title":"Double check we provide not only CLI, but also public library in NuGet and Crates IO","user":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":7821083708,"node_id":"LA_kwDONXCAbs8AAAAB0ixEPA","url":"https://api.github.com/repos/link-foundation/link-cli/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"},{"id":7821083712,"node_id":"LA_kwDONXCAbs8AAAAB0ixEQA","url":"https://api.github.com/repos/link-foundation/link-cli/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"}],"state":"open","locked":false,"assignees":[],"milestone":null,"comments":0,"created_at":"2026-05-15T13:01:14Z","updated_at":"2026-05-15T13:01:14Z","closed_at":null,"assignee":null,"author_association":"MEMBER","type":{"id":22969357,"node_id":"IT_kwDOCoAzvc4BXnwN","name":"Feature","description":"A request, idea, or new functionality","color":"blue","created_at":"2024-07-20T19:06:39Z","updated_at":"2024-10-08T23:47:30Z","is_enabled":true},"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"All our features should be publicly exportable, so any external project should be able to recreate or customize the logic of our CLI with and without CLI.\n\nThat should be done for both Rust and C#, both language should do all the same exports as libraries.\n\nSo our NuGet and Crate should have both library and CLI for both languages with fullest possible features support.\n\nAlso we need to make sure for libraries we have automatically generated documentation.\n\nUse all the best practices from CI/CD templates (check full file tree to compare for all GitHub workflow and CI/CD scripts file), if the same issue is found in template report issue also in templates:\n- https://github.com/link-foundation/js-ai-driven-development-pipeline-template\n- https://github.com/link-foundation/rust-ai-driven-development-pipeline-template\n- https://github.com/link-foundation/python-ai-driven-development-pipeline-template\n- https://github.com/link-foundation/csharp-ai-driven-development-pipeline-template\n\nWe should compare all files, so we don't have more CI/CD errors in the future and reuse all the best practices from these templates.\n\nWe need to collect data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), list of each and all requirements from the issue, and propose possible solutions and solution plans for each requirement (we should also check known existing components/libraries, that solve similar problem or can help in solutions).\n\nPlease plan and execute everything in a single pull request, you have unlimited time and context, as context auto-compacts and you can continue indefinitely, until it is each and every requirement fully addressed, and everything is totally done.","closed_by":null,"reactions":{"url":"https://api.github.com/repos/link-foundation/link-cli/issues/92/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/link-foundation/link-cli/issues/92/timeline","performed_via_github_app":null,"state_reason":null,"pinned_comment":null} \ No newline at end of file diff --git a/docs/case-studies/issue-92/github-data/pr-93.json b/docs/case-studies/issue-92/github-data/pr-93.json new file mode 100644 index 0000000..eb8c390 --- /dev/null +++ b/docs/case-studies/issue-92/github-data/pr-93.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/link-foundation/link-cli/pulls/93","id":3689932382,"node_id":"PR_kwDONXCAbs7b7-Ze","html_url":"https://github.com/link-foundation/link-cli/pull/93","diff_url":"https://github.com/link-foundation/link-cli/pull/93.diff","patch_url":"https://github.com/link-foundation/link-cli/pull/93.patch","issue_url":"https://api.github.com/repos/link-foundation/link-cli/issues/93","number":93,"state":"open","locked":false,"title":"[WIP] Double check we provide not only CLI, but also public library in NuGet and Crates IO","user":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"## 🤖 AI-Powered Solution Draft\n\nThis pull request is being automatically generated to solve issue #92.\n\n### 📋 Issue Reference\nFixes #92\n\n### 🚧 Status\n**Work in Progress** - The AI assistant is currently analyzing and implementing the solution draft.\n\n### 📝 Implementation Details\n_Details will be added as the solution draft is developed..._\n\n---\n*This PR was created automatically by the AI issue solver*","created_at":"2026-05-15T13:43:50Z","updated_at":"2026-05-15T13:43:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"78cf7061a5e64545337a40b1a61ad398b116bd10","assignees":[{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false}],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":true,"commits_url":"https://api.github.com/repos/link-foundation/link-cli/pulls/93/commits","review_comments_url":"https://api.github.com/repos/link-foundation/link-cli/pulls/93/comments","review_comment_url":"https://api.github.com/repos/link-foundation/link-cli/pulls/comments{/number}","comments_url":"https://api.github.com/repos/link-foundation/link-cli/issues/93/comments","statuses_url":"https://api.github.com/repos/link-foundation/link-cli/statuses/78fad2d52a44fc196383ac839f889a0c7a011e56","head":{"label":"link-foundation:issue-92-ce7d2849a3ea","ref":"issue-92-ce7d2849a3ea","sha":"78fad2d52a44fc196383ac839f889a0c7a011e56","user":{"login":"link-foundation","id":176174013,"node_id":"O_kgDOCoAzvQ","avatar_url":"https://avatars.githubusercontent.com/u/176174013?v=4","gravatar_id":"","url":"https://api.github.com/users/link-foundation","html_url":"https://github.com/link-foundation","followers_url":"https://api.github.com/users/link-foundation/followers","following_url":"https://api.github.com/users/link-foundation/following{/other_user}","gists_url":"https://api.github.com/users/link-foundation/gists{/gist_id}","starred_url":"https://api.github.com/users/link-foundation/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/link-foundation/subscriptions","organizations_url":"https://api.github.com/users/link-foundation/orgs","repos_url":"https://api.github.com/users/link-foundation/repos","events_url":"https://api.github.com/users/link-foundation/events{/privacy}","received_events_url":"https://api.github.com/users/link-foundation/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":896565358,"node_id":"R_kgDONXCAbg","name":"link-cli","full_name":"link-foundation/link-cli","private":false,"owner":{"login":"link-foundation","id":176174013,"node_id":"O_kgDOCoAzvQ","avatar_url":"https://avatars.githubusercontent.com/u/176174013?v=4","gravatar_id":"","url":"https://api.github.com/users/link-foundation","html_url":"https://github.com/link-foundation","followers_url":"https://api.github.com/users/link-foundation/followers","following_url":"https://api.github.com/users/link-foundation/following{/other_user}","gists_url":"https://api.github.com/users/link-foundation/gists{/gist_id}","starred_url":"https://api.github.com/users/link-foundation/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/link-foundation/subscriptions","organizations_url":"https://api.github.com/users/link-foundation/orgs","repos_url":"https://api.github.com/users/link-foundation/repos","events_url":"https://api.github.com/users/link-foundation/events{/privacy}","received_events_url":"https://api.github.com/users/link-foundation/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/link-foundation/link-cli","description":"A CLI tool to manipulate links.","fork":false,"url":"https://api.github.com/repos/link-foundation/link-cli","forks_url":"https://api.github.com/repos/link-foundation/link-cli/forks","keys_url":"https://api.github.com/repos/link-foundation/link-cli/keys{/key_id}","collaborators_url":"https://api.github.com/repos/link-foundation/link-cli/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/link-foundation/link-cli/teams","hooks_url":"https://api.github.com/repos/link-foundation/link-cli/hooks","issue_events_url":"https://api.github.com/repos/link-foundation/link-cli/issues/events{/number}","events_url":"https://api.github.com/repos/link-foundation/link-cli/events","assignees_url":"https://api.github.com/repos/link-foundation/link-cli/assignees{/user}","branches_url":"https://api.github.com/repos/link-foundation/link-cli/branches{/branch}","tags_url":"https://api.github.com/repos/link-foundation/link-cli/tags","blobs_url":"https://api.github.com/repos/link-foundation/link-cli/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/link-foundation/link-cli/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/link-foundation/link-cli/git/refs{/sha}","trees_url":"https://api.github.com/repos/link-foundation/link-cli/git/trees{/sha}","statuses_url":"https://api.github.com/repos/link-foundation/link-cli/statuses/{sha}","languages_url":"https://api.github.com/repos/link-foundation/link-cli/languages","stargazers_url":"https://api.github.com/repos/link-foundation/link-cli/stargazers","contributors_url":"https://api.github.com/repos/link-foundation/link-cli/contributors","subscribers_url":"https://api.github.com/repos/link-foundation/link-cli/subscribers","subscription_url":"https://api.github.com/repos/link-foundation/link-cli/subscription","commits_url":"https://api.github.com/repos/link-foundation/link-cli/commits{/sha}","git_commits_url":"https://api.github.com/repos/link-foundation/link-cli/git/commits{/sha}","comments_url":"https://api.github.com/repos/link-foundation/link-cli/comments{/number}","issue_comment_url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments{/number}","contents_url":"https://api.github.com/repos/link-foundation/link-cli/contents/{+path}","compare_url":"https://api.github.com/repos/link-foundation/link-cli/compare/{base}...{head}","merges_url":"https://api.github.com/repos/link-foundation/link-cli/merges","archive_url":"https://api.github.com/repos/link-foundation/link-cli/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/link-foundation/link-cli/downloads","issues_url":"https://api.github.com/repos/link-foundation/link-cli/issues{/number}","pulls_url":"https://api.github.com/repos/link-foundation/link-cli/pulls{/number}","milestones_url":"https://api.github.com/repos/link-foundation/link-cli/milestones{/number}","notifications_url":"https://api.github.com/repos/link-foundation/link-cli/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/link-foundation/link-cli/labels{/name}","releases_url":"https://api.github.com/repos/link-foundation/link-cli/releases{/id}","deployments_url":"https://api.github.com/repos/link-foundation/link-cli/deployments","created_at":"2024-11-30T17:46:38Z","updated_at":"2026-05-13T08:00:30Z","pushed_at":"2026-05-15T13:43:44Z","git_url":"git://github.com/link-foundation/link-cli.git","ssh_url":"git@github.com:link-foundation/link-cli.git","clone_url":"https://github.com/link-foundation/link-cli.git","svn_url":"https://github.com/link-foundation/link-cli","homepage":"https://link-foundation.github.io/link-cli/","size":4583,"stargazers_count":9,"watchers_count":9,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":21,"license":{"key":"unlicense","name":"The Unlicense","spdx_id":"Unlicense","url":"https://api.github.com/licenses/unlicense","node_id":"MDc6TGljZW5zZTE1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":21,"watchers":9,"default_branch":"main"}},"base":{"label":"link-foundation:main","ref":"main","sha":"5a0b56d49e0362daef3ea801f17f12e57ef14ece","user":{"login":"link-foundation","id":176174013,"node_id":"O_kgDOCoAzvQ","avatar_url":"https://avatars.githubusercontent.com/u/176174013?v=4","gravatar_id":"","url":"https://api.github.com/users/link-foundation","html_url":"https://github.com/link-foundation","followers_url":"https://api.github.com/users/link-foundation/followers","following_url":"https://api.github.com/users/link-foundation/following{/other_user}","gists_url":"https://api.github.com/users/link-foundation/gists{/gist_id}","starred_url":"https://api.github.com/users/link-foundation/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/link-foundation/subscriptions","organizations_url":"https://api.github.com/users/link-foundation/orgs","repos_url":"https://api.github.com/users/link-foundation/repos","events_url":"https://api.github.com/users/link-foundation/events{/privacy}","received_events_url":"https://api.github.com/users/link-foundation/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":896565358,"node_id":"R_kgDONXCAbg","name":"link-cli","full_name":"link-foundation/link-cli","private":false,"owner":{"login":"link-foundation","id":176174013,"node_id":"O_kgDOCoAzvQ","avatar_url":"https://avatars.githubusercontent.com/u/176174013?v=4","gravatar_id":"","url":"https://api.github.com/users/link-foundation","html_url":"https://github.com/link-foundation","followers_url":"https://api.github.com/users/link-foundation/followers","following_url":"https://api.github.com/users/link-foundation/following{/other_user}","gists_url":"https://api.github.com/users/link-foundation/gists{/gist_id}","starred_url":"https://api.github.com/users/link-foundation/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/link-foundation/subscriptions","organizations_url":"https://api.github.com/users/link-foundation/orgs","repos_url":"https://api.github.com/users/link-foundation/repos","events_url":"https://api.github.com/users/link-foundation/events{/privacy}","received_events_url":"https://api.github.com/users/link-foundation/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/link-foundation/link-cli","description":"A CLI tool to manipulate links.","fork":false,"url":"https://api.github.com/repos/link-foundation/link-cli","forks_url":"https://api.github.com/repos/link-foundation/link-cli/forks","keys_url":"https://api.github.com/repos/link-foundation/link-cli/keys{/key_id}","collaborators_url":"https://api.github.com/repos/link-foundation/link-cli/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/link-foundation/link-cli/teams","hooks_url":"https://api.github.com/repos/link-foundation/link-cli/hooks","issue_events_url":"https://api.github.com/repos/link-foundation/link-cli/issues/events{/number}","events_url":"https://api.github.com/repos/link-foundation/link-cli/events","assignees_url":"https://api.github.com/repos/link-foundation/link-cli/assignees{/user}","branches_url":"https://api.github.com/repos/link-foundation/link-cli/branches{/branch}","tags_url":"https://api.github.com/repos/link-foundation/link-cli/tags","blobs_url":"https://api.github.com/repos/link-foundation/link-cli/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/link-foundation/link-cli/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/link-foundation/link-cli/git/refs{/sha}","trees_url":"https://api.github.com/repos/link-foundation/link-cli/git/trees{/sha}","statuses_url":"https://api.github.com/repos/link-foundation/link-cli/statuses/{sha}","languages_url":"https://api.github.com/repos/link-foundation/link-cli/languages","stargazers_url":"https://api.github.com/repos/link-foundation/link-cli/stargazers","contributors_url":"https://api.github.com/repos/link-foundation/link-cli/contributors","subscribers_url":"https://api.github.com/repos/link-foundation/link-cli/subscribers","subscription_url":"https://api.github.com/repos/link-foundation/link-cli/subscription","commits_url":"https://api.github.com/repos/link-foundation/link-cli/commits{/sha}","git_commits_url":"https://api.github.com/repos/link-foundation/link-cli/git/commits{/sha}","comments_url":"https://api.github.com/repos/link-foundation/link-cli/comments{/number}","issue_comment_url":"https://api.github.com/repos/link-foundation/link-cli/issues/comments{/number}","contents_url":"https://api.github.com/repos/link-foundation/link-cli/contents/{+path}","compare_url":"https://api.github.com/repos/link-foundation/link-cli/compare/{base}...{head}","merges_url":"https://api.github.com/repos/link-foundation/link-cli/merges","archive_url":"https://api.github.com/repos/link-foundation/link-cli/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/link-foundation/link-cli/downloads","issues_url":"https://api.github.com/repos/link-foundation/link-cli/issues{/number}","pulls_url":"https://api.github.com/repos/link-foundation/link-cli/pulls{/number}","milestones_url":"https://api.github.com/repos/link-foundation/link-cli/milestones{/number}","notifications_url":"https://api.github.com/repos/link-foundation/link-cli/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/link-foundation/link-cli/labels{/name}","releases_url":"https://api.github.com/repos/link-foundation/link-cli/releases{/id}","deployments_url":"https://api.github.com/repos/link-foundation/link-cli/deployments","created_at":"2024-11-30T17:46:38Z","updated_at":"2026-05-13T08:00:30Z","pushed_at":"2026-05-15T13:43:44Z","git_url":"git://github.com/link-foundation/link-cli.git","ssh_url":"git@github.com:link-foundation/link-cli.git","clone_url":"https://github.com/link-foundation/link-cli.git","svn_url":"https://github.com/link-foundation/link-cli","homepage":"https://link-foundation.github.io/link-cli/","size":4583,"stargazers_count":9,"watchers_count":9,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":21,"license":{"key":"unlicense","name":"The Unlicense","spdx_id":"Unlicense","url":"https://api.github.com/licenses/unlicense","node_id":"MDc6TGljZW5zZTE1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":21,"watchers":9,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/link-foundation/link-cli/pulls/93"},"html":{"href":"https://github.com/link-foundation/link-cli/pull/93"},"issue":{"href":"https://api.github.com/repos/link-foundation/link-cli/issues/93"},"comments":{"href":"https://api.github.com/repos/link-foundation/link-cli/issues/93/comments"},"review_comments":{"href":"https://api.github.com/repos/link-foundation/link-cli/pulls/93/comments"},"review_comment":{"href":"https://api.github.com/repos/link-foundation/link-cli/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/link-foundation/link-cli/pulls/93/commits"},"statuses":{"href":"https://api.github.com/repos/link-foundation/link-cli/statuses/78fad2d52a44fc196383ac839f889a0c7a011e56"}},"author_association":"MEMBER","auto_merge":null,"assignee":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"clean","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":0,"changed_files":1} \ No newline at end of file diff --git a/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/Directory.Build.props.snapshot b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/Directory.Build.props.snapshot new file mode 100644 index 0000000..54cdb4e --- /dev/null +++ b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/Directory.Build.props.snapshot @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + latest + + + true + 9999 + true + true + latest-all + + diff --git a/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/MyPackage.csproj.snapshot b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/MyPackage.csproj.snapshot new file mode 100644 index 0000000..daf8488 --- /dev/null +++ b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/MyPackage.csproj.snapshot @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + latest + + + MyPackage + 0.3.0 + Your Name + A C# package template for AI-driven development + Unlicense + README.md + https://github.com/link-foundation/csharp-ai-driven-development-pipeline-template + git + template;csharp;ai-driven + + + true + 9999 + true + true + latest-all + + + true + $(NoWarn);CS1591 + + + + + + + diff --git a/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/docfx.json.snapshot b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/docfx.json.snapshot new file mode 100644 index 0000000..80033ac --- /dev/null +++ b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/docfx.json.snapshot @@ -0,0 +1,44 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "src/MyPackage", + "files": ["**/*.csproj"] + } + ], + "dest": "docs/api", + "includePrivateMembers": false, + "disableGitFeatures": false, + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "flattened", + "memberLayout": "samePage", + "allowCompilationErrors": false + } + ], + "build": { + "content": [ + { + "files": ["**/*.{md,yml}"], + "src": "docs", + "exclude": ["_site/**", "obj/**"] + } + ], + "resource": [ + { + "files": ["images/**"], + "src": "docs", + "exclude": ["_site/**", "obj/**"] + } + ], + "output": "_site", + "globalMetadata": { + "_appName": "MyPackage", + "_appTitle": "MyPackage API Documentation", + "_enableSearch": true, + "pdf": false + }, + "template": ["default", "modern"] + } +} diff --git a/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/docs.yml.snapshot b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/docs.yml.snapshot new file mode 100644 index 0000000..35b1388 --- /dev/null +++ b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/docs.yml.snapshot @@ -0,0 +1,96 @@ +name: docs + +# Build and deploy DocFX API documentation to GitHub Pages. +# +# Build runs on every push to main and on PRs that touch docs, sources, or +# this workflow. The deploy job is gated on `push` to `main` (and manual +# dispatch) — never on `release: published`, so a fresh repository starts +# publishing as soon as `main` lands. See issue #15 for the failure mode +# that gating on releases produces. + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'src/**' + - 'docfx.json' + - '.github/workflows/docs.yml' + pull_request: + branches: [main] + paths: + - 'docs/**' + - 'src/**' + - 'docfx.json' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + +jobs: + build: + name: Build documentation + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Install DocFX + run: dotnet tool update -g docfx + + - name: Restore dependencies + run: dotnet restore + + - name: Build documentation site + run: docfx docfx.json -o _site + + - name: List built site (debug) + run: | + echo "::group::_site tree" + find _site -maxdepth 3 -print + echo "::endgroup::" + + - name: Configure GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/configure-pages@v6 + + - name: Upload GitHub Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/upload-pages-artifact@v5 + with: + path: _site + + deploy: + name: Deploy to GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + + - name: Print resolved deployment URL (debug) + run: | + echo "Pages deployed to: ${{ steps.deployment.outputs.page_url }}" diff --git a/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/file-tree.txt b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/file-tree.txt new file mode 100644 index 0000000..6f748d5 --- /dev/null +++ b/docs/case-studies/issue-92/templates/csharp-ai-driven-development-pipeline-template/file-tree.txt @@ -0,0 +1,39 @@ +./.changeset/README.md +./.changeset/config.json +./.editorconfig +./.github/workflows/docs.yml +./.github/workflows/release.yml +./.gitignore +./.gitkeep +./.pre-commit-config.yaml +./CHANGELOG.md +./CONTRIBUTING.md +./Directory.Build.props +./LICENSE +./MyPackage.sln +./README.md +./docfx.json +./docs/index.md +./docs/toc.yml +./examples/BasicUsage.cs +./examples/BasicUsage.csproj +./experiments/reproduce-bug.sh +./scripts/bump-version.mjs +./scripts/check-file-size.mjs +./scripts/check-release-needed.mjs +./scripts/check-release-needed.test.mjs +./scripts/create-github-release.mjs +./scripts/create-github-release.test.mjs +./scripts/detect-code-changes.mjs +./scripts/merge-changesets.mjs +./scripts/validate-changeset.mjs +./scripts/version-and-commit.mjs +./scripts/version-and-commit.test.mjs +./scripts/wait-for-nuget.mjs +./scripts/wait-for-nuget.test.mjs +./src/MyPackage/Calculator.cs +./src/MyPackage/MyPackage.csproj +./src/MyPackage/PackageInfo.cs +./tests/MyPackage.Tests/CalculatorTests.cs +./tests/MyPackage.Tests/MyPackage.Tests.csproj +./tests/MyPackage.Tests/PackageInfoTests.cs diff --git a/docs/case-studies/issue-92/templates/js-ai-driven-development-pipeline-template/file-tree.txt b/docs/case-studies/issue-92/templates/js-ai-driven-development-pipeline-template/file-tree.txt new file mode 100644 index 0000000..90fc7c4 --- /dev/null +++ b/docs/case-studies/issue-92/templates/js-ai-driven-development-pipeline-template/file-tree.txt @@ -0,0 +1,305 @@ +./.changeset/README.md +./.changeset/config.json +./.github/actions/publish-dockerhub/action.yml +./.github/workflows/example-app.yml +./.github/workflows/links.yml +./.github/workflows/release.yml +./.gitignore +./.gitkeep +./.husky/pre-commit +./.jscpd.json +./.lycheeignore +./.prettierignore +./.prettierrc +./.secretlintrc.json +./CHANGELOG.md +./LICENSE +./README.md +./bin/example-package-name.js +./bunfig.toml +./deno.json +./docs/BEST-PRACTICES.md +./docs/CONTRIBUTING.md +./docs/case-studies/issue-13/README.md +./docs/case-studies/issue-13/hive-mind-issue-960.json +./docs/case-studies/issue-13/hive-mind-pr-961-diff.txt +./docs/case-studies/issue-13/hive-mind-pr-961.json +./docs/case-studies/issue-21/README.md +./docs/case-studies/issue-21/ci-logs/run-20803315337.txt +./docs/case-studies/issue-21/ci-logs/run-20885464993.txt +./docs/case-studies/issue-21/issue-111-data.txt +./docs/case-studies/issue-21/issue-113-data.txt +./docs/case-studies/issue-21/pr-112-data.json +./docs/case-studies/issue-21/pr-112-diff.patch +./docs/case-studies/issue-21/pr-114-data.json +./docs/case-studies/issue-21/pr-114-diff.patch +./docs/case-studies/issue-23/README.md +./docs/case-studies/issue-23/data/hive-mind-check-version.mjs +./docs/case-studies/issue-23/data/hive-mind-ci.yml +./docs/case-studies/issue-23/data/hive-mind-eslint.config.mjs +./docs/case-studies/issue-23/data/hive-mind-release.yml +./docs/case-studies/issue-23/data/issue-1126-details.txt +./docs/case-studies/issue-23/data/issue-1141-comments.json +./docs/case-studies/issue-23/data/issue-1141-details.txt +./docs/case-studies/issue-23/data/pr-1127-conversation-comments.json +./docs/case-studies/issue-23/data/pr-1127-diff.txt +./docs/case-studies/issue-23/data/pr-1127-review-comments.json +./docs/case-studies/issue-23/data/pr-1142-conversation-comments.json +./docs/case-studies/issue-23/data/pr-1142-diff.txt +./docs/case-studies/issue-23/data/pr-1142-review-comments.json +./docs/case-studies/issue-25/DETAILED-COMPARISON.md +./docs/case-studies/issue-25/README.md +./docs/case-studies/issue-25/data/hive-mind-file-tree.txt +./docs/case-studies/issue-25/data/issue-1274-case-study.md +./docs/case-studies/issue-25/data/issue-1278-case-study.md +./docs/case-studies/issue-25/data/template-file-tree.txt +./docs/case-studies/issue-29/README.md +./docs/case-studies/issue-3/README.md +./docs/case-studies/issue-3/created-issues.md +./docs/case-studies/issue-3/issue-data.json +./docs/case-studies/issue-3/original-format-release-notes.mjs +./docs/case-studies/issue-3/reference-pr-59-diff.txt +./docs/case-studies/issue-3/reference-pr-59.json +./docs/case-studies/issue-3/release-v0.1.0.json +./docs/case-studies/issue-3/repositories-with-same-script.json +./docs/case-studies/issue-3/research-notes.md +./docs/case-studies/issue-31/README.md +./docs/case-studies/issue-31/web-capture-pr49-commits.json +./docs/case-studies/issue-33/README.md +./docs/case-studies/issue-33/ci-logs/release-24395209194.txt +./docs/case-studies/issue-33/ci-logs/upstream-nodejs-62430.json +./docs/case-studies/issue-33/ci-logs/upstream-npm-cli-9151.json +./docs/case-studies/issue-33/ci-logs/upstream-runner-images-13883.json +./docs/case-studies/issue-36/README.md +./docs/case-studies/issue-36/ci-logs/release-24399965550.txt +./docs/case-studies/issue-38/CASE-STUDY.md +./docs/case-studies/issue-40/CICD-COMPARISON.md +./docs/case-studies/issue-40/README.md +./docs/case-studies/issue-40/data/ci-run-25212337438.json +./docs/case-studies/issue-40/data/ci-runs-branch.json +./docs/case-studies/issue-40/data/downstream-web-capture-issue-98.json +./docs/case-studies/issue-40/data/downstream-web-capture-pr-99.diff +./docs/case-studies/issue-40/data/downstream-web-capture-pr-99.json +./docs/case-studies/issue-40/data/issue-40.json +./docs/case-studies/issue-40/data/js-cicd-files.txt +./docs/case-studies/issue-40/data/js-template-file-tree.txt +./docs/case-studies/issue-40/data/pr-43.json +./docs/case-studies/issue-40/data/related-js-merged-prs.json +./docs/case-studies/issue-40/data/rust-cicd-files.txt +./docs/case-studies/issue-40/data/rust-template-file-tree.txt +./docs/case-studies/issue-40/data/rust-template-head.txt +./docs/case-studies/issue-40/data/shields-broken-prefixed-badge.svg +./docs/case-studies/issue-40/data/shields-broken-prefixed-prerelease-badge.svg +./docs/case-studies/issue-40/data/shields-working-normalized-badge.svg +./docs/case-studies/issue-40/data/shields-working-prerelease-badge.svg +./docs/case-studies/issue-40/rust-template/create-github-release.rs +./docs/case-studies/issue-40/rust-template/release.yml +./docs/case-studies/issue-41/README.md +./docs/case-studies/issue-41/data/hive-mind-check-file-line-limits.sh +./docs/case-studies/issue-41/data/hive-mind-file-tree.txt +./docs/case-studies/issue-41/data/hive-mind-issue-1593-case-study.md +./docs/case-studies/issue-41/data/hive-mind-issue-1593-comments.json +./docs/case-studies/issue-41/data/hive-mind-issue-1593.json +./docs/case-studies/issue-41/data/hive-mind-issue-1730-case-study.md +./docs/case-studies/issue-41/data/hive-mind-issue-1730-comments.json +./docs/case-studies/issue-41/data/hive-mind-issue-1730.json +./docs/case-studies/issue-41/data/js-template-check-file-line-limits-before.sh +./docs/case-studies/issue-41/data/js-template-eslint.config.js +./docs/case-studies/issue-41/data/js-template-file-tree.txt +./docs/case-studies/issue-41/data/js-template-issue-41-comments.json +./docs/case-studies/issue-41/data/js-template-issue-41.json +./docs/case-studies/issue-41/data/js-template-release.yml +./docs/case-studies/issue-41/data/js-template-warn-threshold-search-before.json +./docs/case-studies/issue-41/data/rust-template-check-file-size.rs +./docs/case-studies/issue-41/data/rust-template-created-issue-url.txt +./docs/case-studies/issue-41/data/rust-template-file-tree.txt +./docs/case-studies/issue-41/data/rust-template-issue-40.json +./docs/case-studies/issue-41/data/rust-template-issues.json +./docs/case-studies/issue-41/data/rust-template-max-lines-search.json +./docs/case-studies/issue-41/data/rust-template-release.yml +./docs/case-studies/issue-42/README.md +./docs/case-studies/issue-42/data/ci-runs-branch.json +./docs/case-studies/issue-42/data/issue-42-comments.json +./docs/case-studies/issue-42/data/issue-42.json +./docs/case-studies/issue-42/data/js-template-file-tree.txt +./docs/case-studies/issue-42/data/js-template-pre-fix-head.txt +./docs/case-studies/issue-42/data/link-foundation-my-package-search.txt +./docs/case-studies/issue-42/data/link-foundation-package-name-search.txt +./docs/case-studies/issue-42/data/pr-45-conversation-comments.json +./docs/case-studies/issue-42/data/pr-45-review-comments.json +./docs/case-studies/issue-42/data/pr-45-reviews.json +./docs/case-studies/issue-42/data/pr-45.json +./docs/case-studies/issue-42/data/related-merged-prs-check-release-needed.json +./docs/case-studies/issue-42/data/related-merged-prs-publish-to-npm.json +./docs/case-studies/issue-42/data/rust-template-ci-cd-findings.txt +./docs/case-studies/issue-42/data/rust-template-file-tree.txt +./docs/case-studies/issue-42/data/rust-template-head.txt +./docs/case-studies/issue-56/README.md +./docs/case-studies/issue-56/artifacts/universal-app-mobile.png +./docs/case-studies/issue-56/artifacts/universal-app-web.png +./docs/case-studies/issue-56/data/actions-checkout-release.json +./docs/case-studies/issue-56/data/actions-configure-pages-release.json +./docs/case-studies/issue-56/data/actions-deploy-pages-release.json +./docs/case-studies/issue-56/data/actions-setup-node-release.json +./docs/case-studies/issue-56/data/actions-upload-artifact-release.json +./docs/case-studies/issue-56/data/actions-upload-pages-artifact-release.json +./docs/case-studies/issue-56/data/bun-test-final.log +./docs/case-studies/issue-56/data/changeset-status-after-stage.log +./docs/case-studies/issue-56/data/check-file-line-limits-final-2.log +./docs/case-studies/issue-56/data/check-mjs-syntax-final-2.log +./docs/case-studies/issue-56/data/deep-sdk-capacitor.config.ts +./docs/case-studies/issue-56/data/deep-sdk-electron-package.json +./docs/case-studies/issue-56/data/deep-sdk-file-tree.txt +./docs/case-studies/issue-56/data/deep-sdk-gh-pages.yml +./docs/case-studies/issue-56/data/deep-sdk-package.json +./docs/case-studies/issue-56/data/deep-sdk-repo.json +./docs/case-studies/issue-56/data/deno-test-final.log +./docs/case-studies/issue-56/data/example-desktop-package-final-2.log +./docs/case-studies/issue-56/data/example-mobile-sync-final-2.log +./docs/case-studies/issue-56/data/example-web-build-final-2.log +./docs/case-studies/issue-56/data/issue-56-comments.json +./docs/case-studies/issue-56/data/issue-56.json +./docs/case-studies/issue-56/data/link-foundation-code-search.json +./docs/case-studies/issue-56/data/npm-capacitor-cli.json +./docs/case-studies/issue-56/data/npm-capacitor-core.json +./docs/case-studies/issue-56/data/npm-check-final-4.log +./docs/case-studies/issue-56/data/npm-electron-forge-cli.json +./docs/case-studies/issue-56/data/npm-install-root.log +./docs/case-studies/issue-56/data/npm-install-universal-app-node20-compatible.log +./docs/case-studies/issue-56/data/npm-test-final-3.log +./docs/case-studies/issue-56/data/npm-vite.json +./docs/case-studies/issue-56/data/pr-57.json +./docs/case-studies/issue-56/data/recent-merged-prs.json +./docs/case-studies/issue-56/data/universal-app-test-before.log +./docs/case-studies/issue-56/data/universal-app-test-final-2.log +./docs/case-studies/issue-56/data/validate-changeset-final.log +./docs/case-studies/issue-56/data/vk-bot-desktop-build-renderer.mjs +./docs/case-studies/issue-56/data/vk-bot-desktop-electron-main.cjs +./docs/case-studies/issue-56/data/vk-bot-desktop-file-tree.txt +./docs/case-studies/issue-56/data/vk-bot-desktop-js-workflow.yml +./docs/case-studies/issue-56/data/vk-bot-desktop-package.json +./docs/case-studies/issue-56/data/vk-bot-desktop-repo.json +./docs/case-studies/issue-58/README.md +./docs/case-studies/issue-58/data/actions-configure-pages-release.json +./docs/case-studies/issue-58/data/actions-deploy-pages-release.json +./docs/case-studies/issue-58/data/actions-upload-artifact-release.json +./docs/case-studies/issue-58/data/actions-upload-pages-artifact-release.json +./docs/case-studies/issue-58/data/bun-test.log +./docs/case-studies/issue-58/data/check-file-line-limits.log +./docs/case-studies/issue-58/data/check-mjs-syntax.log +./docs/case-studies/issue-58/data/checks-and-release-25733140225.json +./docs/case-studies/issue-58/data/checks-and-release-25733140225.log +./docs/case-studies/issue-58/data/checks-and-release-25743983223.log +./docs/case-studies/issue-58/data/ci-run-25743983223.json +./docs/case-studies/issue-58/data/csharp-template-file-tree.txt +./docs/case-studies/issue-58/data/csharp-template-release.yml +./docs/case-studies/issue-58/data/deno-test.log +./docs/case-studies/issue-58/data/example-app-25733140224.json +./docs/case-studies/issue-58/data/example-app-25733140224.log +./docs/case-studies/issue-58/data/example-desktop-package.log +./docs/case-studies/issue-58/data/example-mobile-sync.log +./docs/case-studies/issue-58/data/example-web-build.log +./docs/case-studies/issue-58/data/issue-58-comments.json +./docs/case-studies/issue-58/data/issue-58.json +./docs/case-studies/issue-58/data/js-template-file-tree.txt +./docs/case-studies/issue-58/data/link-foundation-example-package-name-search.json +./docs/case-studies/issue-58/data/main-ci-runs.json +./docs/case-studies/issue-58/data/npm-check-final.log +./docs/case-studies/issue-58/data/npm-example-package-name-view.json +./docs/case-studies/issue-58/data/npm-global-install.log +./docs/case-studies/issue-58/data/npm-install-after-metadata.log +./docs/case-studies/issue-58/data/npm-install-universal-app.log +./docs/case-studies/issue-58/data/npm-install.log +./docs/case-studies/issue-58/data/npm-pack-dry-run-final.json +./docs/case-studies/issue-58/data/npm-test-2.log +./docs/case-studies/issue-58/data/npm-whoami.log +./docs/case-studies/issue-58/data/pages-enable-result.json +./docs/case-studies/issue-58/data/pages-status-after-enable.json +./docs/case-studies/issue-58/data/pr-57.diff +./docs/case-studies/issue-58/data/pr-57.json +./docs/case-studies/issue-58/data/pr-59-conversation-comments.json +./docs/case-studies/issue-58/data/pr-59-review-comments.json +./docs/case-studies/issue-58/data/pr-59-reviews.json +./docs/case-studies/issue-58/data/pr-59.json +./docs/case-studies/issue-58/data/python-template-file-tree.txt +./docs/case-studies/issue-58/data/python-template-release.yml +./docs/case-studies/issue-58/data/regression-after-2.log +./docs/case-studies/issue-58/data/regression-after-3.log +./docs/case-studies/issue-58/data/regression-after.log +./docs/case-studies/issue-58/data/regression-before.log +./docs/case-studies/issue-58/data/rust-template-file-tree.txt +./docs/case-studies/issue-58/data/rust-template-release.yml +./docs/case-studies/issue-58/data/secretlint.log +./docs/case-studies/issue-58/data/validate-changeset.log +./docs/case-studies/issue-7/BEST-PRACTICES-COMPARISON.md +./docs/case-studies/issue-7/FORMATTER-COMPARISON.md +./docs/case-studies/issue-7/current-repository-analysis.json +./docs/case-studies/issue-7/effect-template-analysis.json +./docs/screenshots/example-app/example-app-en-dark.png +./docs/screenshots/example-app/example-app-en-light.png +./docs/screenshots/example-app/example-app-ru-dark.png +./docs/screenshots/example-app/example-app-ru-light.png +./docs/screenshots/example-app/example-app.png +./eslint.config.js +./examples/basic-usage.js +./examples/universal-app/README.md +./examples/universal-app/capacitor.config.json +./examples/universal-app/electron/main.cjs +./examples/universal-app/electron/preload.cjs +./examples/universal-app/forge.config.cjs +./examples/universal-app/index.html +./examples/universal-app/package-lock.json +./examples/universal-app/package.json +./examples/universal-app/public/favicon.svg +./examples/universal-app/src/App.js +./examples/universal-app/src/main.js +./examples/universal-app/src/styles.css +./examples/universal-app/vite.config.js +./experiments/test-changeset-scripts.mjs +./experiments/test-check-release-needed.mjs +./experiments/test-detect-changes.mjs +./experiments/test-failure-detection.mjs +./experiments/test-format-major-changes.mjs +./experiments/test-format-minor-changes.mjs +./experiments/test-format-no-hash.mjs +./experiments/test-format-patch-changes.mjs +./package-lock.json +./package.json +./scripts/changeset-version.mjs +./scripts/check-changesets.mjs +./scripts/check-docker-publish.mjs +./scripts/check-file-line-limits.sh +./scripts/check-mjs-syntax.sh +./scripts/check-release-needed.mjs +./scripts/check-version.mjs +./scripts/check-web-archive.mjs +./scripts/create-github-release.mjs +./scripts/create-manual-changeset.mjs +./scripts/detect-code-changes.mjs +./scripts/format-github-release.mjs +./scripts/format-release-notes-helpers.mjs +./scripts/format-release-notes.mjs +./scripts/instant-version-bump.mjs +./scripts/js-paths.mjs +./scripts/merge-changesets.mjs +./scripts/package-info.mjs +./scripts/publish-to-npm.mjs +./scripts/setup-npm.mjs +./scripts/simulate-fresh-merge.sh +./scripts/update-preview-images.mjs +./scripts/validate-changeset.mjs +./scripts/version-and-commit.mjs +./scripts/wait-for-npm.mjs +./src/index.d.ts +./src/index.js +./tests/check-file-line-limits.test.js +./tests/ci-timeouts.test.js +./tests/create-github-release.test.js +./tests/docker-publish.test.js +./tests/index.test.js +./tests/package-info.test.js +./tests/package-metadata.test.js +./tests/release-badge.test.js +./tests/setup-npm.test.js +./tests/tag-prefix.test.js +./tests/universal-app.test.js diff --git a/docs/case-studies/issue-92/templates/python-ai-driven-development-pipeline-template/file-tree.txt b/docs/case-studies/issue-92/templates/python-ai-driven-development-pipeline-template/file-tree.txt new file mode 100644 index 0000000..8ca9200 --- /dev/null +++ b/docs/case-studies/issue-92/templates/python-ai-driven-development-pipeline-template/file-tree.txt @@ -0,0 +1,28 @@ +./.github/workflows/release.yml +./.gitignore +./.pre-commit-config.yaml +./.ruff.toml +./CHANGELOG.md +./CONTRIBUTING.md +./LICENSE +./README.md +./changelog.d/20251218_133759_drakonard_issue_1_3b50e2f12be6.md +./changelog.d/20260509_204000_issue_6_release_metadata.md +./changelog.d/README.md +./changelog.d/fragment_template.md.j2 +./examples/basic_usage.py +./pyproject.toml +./scripts/bump_version.py +./scripts/check_file_size.py +./scripts/create_github_release.py +./scripts/create_manual_changeset.py +./scripts/detect_code_changes.py +./scripts/format_release_notes.py +./scripts/publish_to_pypi.py +./scripts/validate_changeset.py +./scripts/version_and_commit.py +./src/my_package/__init__.py +./src/my_package/py.typed +./tests/__init__.py +./tests/test_create_github_release.py +./tests/test_my_package.py diff --git a/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/Cargo.toml.snapshot b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/Cargo.toml.snapshot new file mode 100644 index 0000000..51bf120 --- /dev/null +++ b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/Cargo.toml.snapshot @@ -0,0 +1,55 @@ +[package] +name = "example-sum-package-name" +version = "0.13.0" +edition = "2021" +description = "A Rust package template for AI-driven development" +readme = "README.md" +license = "Unlicense" +keywords = ["template", "rust", "ai-driven"] +categories = ["development-tools"] +repository = "https://github.com/link-foundation/rust-ai-driven-development-pipeline-template" +documentation = "https://github.com/link-foundation/rust-ai-driven-development-pipeline-template" +rust-version = "1.70" + +[lib] +name = "example_sum_package_name" +path = "src/lib.rs" + +[[bin]] +name = "example-sum-package-name" +path = "src/main.rs" + +[dependencies] +lino-arguments = "0.3" +clap = { version = "4.4", features = ["derive", "env"] } + +[dev-dependencies] +regex = "1" +walkdir = "2" + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } + +# Allow some common patterns +module_name_repetitions = "allow" +too_many_lines = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" + +[[test]] +name = "unit" +path = "tests/unit/mod.rs" + +[[test]] +name = "integration" +path = "tests/integration/mod.rs" + +[profile.release] +lto = true +codegen-units = 1 +strip = true diff --git a/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/file-tree.txt b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/file-tree.txt new file mode 100644 index 0000000..debdf89 --- /dev/null +++ b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/file-tree.txt @@ -0,0 +1,122 @@ +./.github/workflows/release.yml +./.gitignore +./.gitkeep +./.pre-commit-config.yaml +./CHANGELOG.md +./CONTRIBUTING.md +./Cargo.lock +./Cargo.toml +./LICENSE +./README.md +./changelog.d/20251227_224645_changeset_support.md +./changelog.d/20251229_143823_fix_ci_workflow_dependencies.md +./changelog.d/20251231_115800_fix_readme_script_references.md +./changelog.d/20260107_apply_best_practices.md +./changelog.d/20260108_171124_fix_changelog_check.md +./changelog.d/20260108_171435_prevent_manual_version_modification.md +./changelog.d/20260108_apply_lino_objects_codec_fixes.md +./changelog.d/20260111_multi_language_support.md +./changelog.d/20260119_best_practices_from_browser_commander.md +./changelog.d/20260311_translate_scripts_to_rust.md +./changelog.d/20260413-ci-cd-best-practices.md +./changelog.d/20260413_fix_cargo_token_fallback.md +./changelog.d/20260413_fix_crates_io_check.md +./changelog.d/20260413_fix_lookahead_regex.md +./changelog.d/20260414_fix_per_commit_diff.md +./changelog.d/20260415_fix_workspace_release_scripts.md +./changelog.d/20260501_decouple_docs_deploy.md +./changelog.d/20260503_111500_ci_timeouts.md +./changelog.d/20260503_111700_file_size_warning_threshold.md +./changelog.d/20260509_031015_human_readable_release_titles.md +./changelog.d/20260509_205000_docker_hub_release_publishing.md +./changelog.d/20260512_172908_github_pages_artifact_deploy.md +./changelog.d/20260512_230053_document_pages_source_prerequisite.md +./changelog.d/README.md +./docs/case-studies/issue-11/README.md +./docs/case-studies/issue-11/analysis-crates-io.md +./docs/case-studies/issue-11/analysis-set-output.md +./docs/case-studies/issue-11/analysis-workflow-dispatch.md +./docs/case-studies/issue-11/online-research.md +./docs/case-studies/issue-17/README.md +./docs/case-studies/issue-19/README.md +./docs/case-studies/issue-19/ci-logs/ci-run-20885464993.log.gz +./docs/case-studies/issue-19/pr-114-data/issue-113-details.txt +./docs/case-studies/issue-19/pr-114-data/pr-commits.json +./docs/case-studies/issue-19/pr-114-data/pr-conversation-comments.json +./docs/case-studies/issue-19/pr-114-data/pr-details.json +./docs/case-studies/issue-19/pr-114-data/pr-diff.patch +./docs/case-studies/issue-19/pr-114-data/pr-review-comments.json +./docs/case-studies/issue-19/pr-114-data/pr-reviews.json +./docs/case-studies/issue-19/pr-114-data/solution-draft-log-1.txt.gz +./docs/case-studies/issue-19/pr-114-data/solution-draft-log-2.txt.gz +./docs/case-studies/issue-21/README.md +./docs/case-studies/issue-21/browser-commander-issue-27.md +./docs/case-studies/issue-21/browser-commander-issue-29.md +./docs/case-studies/issue-21/browser-commander-issue-31.md +./docs/case-studies/issue-21/browser-commander-issue-33.md +./docs/case-studies/issue-21/browser-commander-rust.yml +./docs/case-studies/issue-25/README.md +./docs/case-studies/issue-29/README.md +./docs/case-studies/issue-32/README.md +./docs/case-studies/issue-34/README.md +./docs/case-studies/issue-38/README.md +./docs/case-studies/issue-38/raw-data/downstream-meta-after-run-24985948212.json +./docs/case-studies/issue-38/raw-data/downstream-meta-after-run-24985948212.log.gz +./docs/case-studies/issue-38/raw-data/downstream-meta-before-run-24983875003.json +./docs/case-studies/issue-38/raw-data/downstream-meta-before-run-24983875003.log.gz +./docs/case-studies/issue-38/raw-data/downstream-meta-ontology-issue-3.json +./docs/case-studies/issue-38/raw-data/downstream-meta-ontology-pr-4.json +./docs/case-studies/issue-38/raw-data/issue-38-comments.json +./docs/case-studies/issue-38/raw-data/issue-38.json +./docs/case-studies/issue-38/raw-data/js-template-issue-search.json +./docs/case-studies/issue-38/raw-data/main-run-24465255225.json +./docs/case-studies/issue-38/raw-data/main-run-24465255225.log.gz +./docs/case-studies/issue-38/raw-data/main-runs.json +./docs/case-studies/issue-38/raw-data/pr-39-conversation-comments.json +./docs/case-studies/issue-38/raw-data/pr-39-review-comments.json +./docs/case-studies/issue-38/raw-data/pr-39-reviews.json +./docs/case-studies/issue-38/raw-data/pr-39.json +./docs/case-studies/issue-38/raw-data/pr-branch-runs.json +./docs/case-studies/issue-38/raw-data/pr-run-25212295127.json +./docs/case-studies/issue-38/raw-data/pr-run-25212295127.log.gz +./docs/case-studies/issue-38/raw-data/rust-template-issue-search.json +./docs/case-studies/issue-38/template-data/js-template-ci-tree.txt +./docs/case-studies/issue-38/template-data/js-template-links.yml +./docs/case-studies/issue-38/template-data/js-template-release.yml +./docs/case-studies/issue-38/template-data/rust-template-ci-tree.txt +./docs/case-studies/issue-38/template-data/rust-template-release-after.yml +./docs/case-studies/issue-38/template-data/rust-template-release-before.yml +./docs/ci-cd/troubleshooting.md +./examples/basic_usage.rs +./experiments/test-changelog-parsing.rs +./experiments/test-crates-io-check.rs +./experiments/test-detect-code-changes.sh +./experiments/test-version-check-dependencies.sh +./experiments/test-version-check.sh +./scripts/bump-version.rs +./scripts/check-changelog-fragment.rs +./scripts/check-file-size.rs +./scripts/check-release-needed.rs +./scripts/check-version-modification.rs +./scripts/collect-changelog.rs +./scripts/create-changelog-fragment.rs +./scripts/create-github-release.rs +./scripts/detect-code-changes.rs +./scripts/get-bump-type.rs +./scripts/get-version.rs +./scripts/git-config.rs +./scripts/publish-crate.rs +./scripts/rust-paths.rs +./scripts/version-and-commit.rs +./scripts/wait-for-crate.rs +./src/lib.rs +./src/main.rs +./src/sum.rs +./tests/integration/mod.rs +./tests/integration/sum.rs +./tests/unit/ci-cd/changelog_parsing.rs +./tests/unit/ci-cd/mod.rs +./tests/unit/ci-cd/workflow_release.rs +./tests/unit/ci-cd/workspace_manifest_resolution.rs +./tests/unit/mod.rs +./tests/unit/sum.rs diff --git a/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/lib.rs.snapshot b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/lib.rs.snapshot new file mode 100644 index 0000000..490125b --- /dev/null +++ b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/lib.rs.snapshot @@ -0,0 +1,3 @@ +pub mod sum; + +pub use sum::sum; diff --git a/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/release.yml.snapshot b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/release.yml.snapshot new file mode 100644 index 0000000..b43695f --- /dev/null +++ b/docs/case-studies/issue-92/templates/rust-ai-driven-development-pipeline-template/release.yml.snapshot @@ -0,0 +1,675 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + release_mode: + description: 'Manual release mode' + required: true + type: choice + default: 'instant' + options: + - instant + - changelog-pr + bump_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Release description (optional)' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref == 'refs/heads/main' }} + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + # Support both CARGO_REGISTRY_TOKEN (cargo's native env var) and CARGO_TOKEN (for backwards compatibility) + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + # Optional: set repository variable DOCKERHUB_IMAGE to namespace/image to publish Docker Hub releases. + DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE }} + +jobs: + # === DETECT CHANGES - determines which jobs should run === + detect-changes: + name: Detect Changes + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name != 'workflow_dispatch' + outputs: + rs-changed: ${{ steps.changes.outputs.rs-changed }} + toml-changed: ${{ steps.changes.outputs.toml-changed }} + docs-changed: ${{ steps.changes.outputs.docs-changed }} + workflow-changed: ${{ steps.changes.outputs.workflow-changed }} + any-code-changed: ${{ steps.changes.outputs.any-code-changed }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Detect changes + id: changes + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + run: rust-script scripts/detect-code-changes.rs + + # === CHANGELOG CHECK - only runs on PRs with code changes === + # Docs-only PRs (./docs folder, markdown files) don't require changelog fragments + changelog: + name: Changelog Fragment Check + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [detect-changes] + if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Check for changelog fragments + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + run: rust-script scripts/check-changelog-fragment.rs + + # === VERSION CHECK - prevents manual version modification in PRs === + # This ensures versions are only modified by the automated release pipeline + version-check: + name: Version Modification Check + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Check for manual version changes + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_BASE_REF: ${{ github.base_ref }} + run: rust-script scripts/check-version-modification.rs + + # === LINT AND FORMAT CHECK === + # Lint runs independently of changelog check - it's a fast check that should always run + # See: https://github.com/link-assistant/hive-mind/pull/1024 for why this dependency was removed + lint: + name: Lint and Format Check + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [detect-changes] + # Note: always() is required because detect-changes is skipped on workflow_dispatch, + # and without always(), this job would also be skipped even though its condition includes workflow_dispatch. + # See: https://github.com/actions/runner/issues/491 + if: | + always() && !cancelled() && ( + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.rs-changed == 'true' || + needs.detect-changes.outputs.toml-changed == 'true' || + needs.detect-changes.outputs.docs-changed == 'true' || + needs.detect-changes.outputs.workflow-changed == 'true' + ) + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Install rust-script + run: cargo install rust-script + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run Clippy + run: cargo clippy --all-targets --all-features + + - name: Check file size limit + run: rust-script scripts/check-file-size.rs + + # === TEST === + # Test runs independently of changelog check + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + needs: [detect-changes, changelog] + # Run if: push event, OR changelog succeeded, OR changelog was skipped (docs-only PR) + if: always() && !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run tests + run: cargo test --all-features --verbose + + - name: Run doc tests + run: cargo test --doc --verbose + + # === CODE COVERAGE === + # Generate and upload code coverage using cargo-llvm-cov + coverage: + name: Code Coverage + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [detect-changes] + if: | + always() && !cancelled() && ( + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.rs-changed == 'true' || + needs.detect-changes.outputs.toml-changed == 'true' + ) + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-coverage-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-coverage- + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Generate code coverage + run: cargo llvm-cov --all-features --lcov --output-path lcov.info + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: lcov.info + fail_ci_if_error: false + + # === BUILD === + # Build package - only runs if lint and test pass + build: + name: Build Package + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [lint, test] + if: always() && !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + + - name: Build release + run: cargo build --release --verbose + + - name: Check package + run: cargo package --list --allow-dirty + + # === AUTO RELEASE === + # Automatic release on push to main using changelog fragments + # This job automatically bumps version based on fragments in changelog.d/ + auto-release: + name: Auto Release + needs: [lint, test, build] + # Note: always() ensures consistent behavior with other jobs that depend on jobs using always(). + if: | + always() && !cancelled() && + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + needs.build.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME || secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Configure git + run: rust-script scripts/git-config.rs + + - name: Determine bump type from changelog fragments + id: bump_type + run: rust-script scripts/get-bump-type.rs + + - name: Check if version already released or no fragments + id: check + env: + HAS_FRAGMENTS: ${{ steps.bump_type.outputs.has_fragments }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: rust-script scripts/check-release-needed.rs + + - name: Collect changelog and bump version + id: version + if: steps.check.outputs.should_release == 'true' && steps.check.outputs.skip_bump != 'true' + run: | + rust-script scripts/version-and-commit.rs \ + --bump-type "${{ steps.bump_type.outputs.bump_type }}" + + - name: Get current version + id: current_version + if: steps.check.outputs.should_release == 'true' + run: rust-script scripts/get-version.rs + + - name: Build release + if: steps.check.outputs.should_release == 'true' + run: cargo build --release + + - name: Publish to Crates.io + if: steps.check.outputs.should_release == 'true' && steps.check.outputs.crate_published != 'true' + id: publish-crate + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + run: rust-script scripts/publish-crate.rs + + - name: Wait for Crate availability on Crates.io + if: steps.check.outputs.should_release == 'true' + run: rust-script scripts/wait-for-crate.rs --release-version "${{ steps.current_version.outputs.version }}" + + - name: Configure Docker Hub publishing + if: steps.check.outputs.should_release == 'true' + id: dockerhub + run: | + disable_dockerhub() { + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "$1" + } + + if [ -z "$DOCKERHUB_IMAGE" ]; then + disable_dockerhub "Docker Hub publishing disabled: DOCKERHUB_IMAGE repository variable is not set" + exit 0 + fi + + if [ ! -f Dockerfile ]; then + disable_dockerhub "Docker Hub publishing disabled: Dockerfile was not found at repository root" + exit 0 + fi + + if [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_TOKEN" ]; then + echo "::error::Docker Hub publishing requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN" + echo "Set DOCKERHUB_USERNAME as a repository variable or secret, and DOCKERHUB_TOKEN as a secret." + exit 1 + fi + + echo "enabled=true" >> "$GITHUB_OUTPUT" + echo "docker_hub_url=https://hub.docker.com/r/${DOCKERHUB_IMAGE}" >> "$GITHUB_OUTPUT" + + - name: Log in to Docker Hub + if: steps.dockerhub.outputs.enabled == 'true' + uses: docker/login-action@v4 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + if: steps.dockerhub.outputs.enabled == 'true' + uses: docker/setup-buildx-action@v4 + + - name: Extract Docker metadata + if: steps.dockerhub.outputs.enabled == 'true' + id: docker-meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.DOCKERHUB_IMAGE }} + tags: | + type=raw,value=latest + type=raw,value=${{ steps.current_version.outputs.version }} + labels: | + org.opencontainers.image.version=${{ steps.current_version.outputs.version }} + + - name: Publish Docker image to Docker Hub + if: steps.dockerhub.outputs.enabled == 'true' + uses: docker/build-push-action@v7 + with: + context: . + push: true + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + + - name: Create GitHub Release + if: steps.check.outputs.should_release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCKER_HUB_URL: ${{ steps.dockerhub.outputs.docker_hub_url }} + run: | + # Use new_version from version-and-commit when available (tag-checked), else fall back to Cargo.toml version + RELEASE_VERSION="${{ steps.version.outputs.new_version }}" + if [ -z "$RELEASE_VERSION" ]; then + RELEASE_VERSION="${{ steps.current_version.outputs.version }}" + fi + + release_args=( + --release-version "$RELEASE_VERSION" + --repository "${{ github.repository }}" + ) + if [ -n "$DOCKER_HUB_URL" ]; then + release_args+=(--docker-hub-url "$DOCKER_HUB_URL") + fi + rust-script scripts/create-github-release.rs "${release_args[@]}" + + # === MANUAL INSTANT RELEASE === + # Manual release via workflow_dispatch - only after CI passes + manual-release: + name: Instant Release + needs: [lint, test, build] + # Note: always() is required to evaluate the condition when dependencies use always(). + # The build job ensures lint and test passed before this job runs. + if: | + always() && !cancelled() && + github.event_name == 'workflow_dispatch' && + github.event.inputs.release_mode == 'instant' && + needs.build.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME || secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Configure git + run: rust-script scripts/git-config.rs + + - name: Collect changelog fragments + run: rust-script scripts/collect-changelog.rs + + - name: Version and commit + id: version + env: + BUMP_TYPE: ${{ github.event.inputs.bump_type }} + DESCRIPTION: ${{ github.event.inputs.description }} + run: rust-script scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Build release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + run: cargo build --release + + - name: Publish to Crates.io + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + id: publish-crate + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + run: rust-script scripts/publish-crate.rs + + - name: Wait for Crate availability on Crates.io + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + run: rust-script scripts/wait-for-crate.rs --release-version "${{ steps.version.outputs.new_version }}" + + - name: Configure Docker Hub publishing + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + id: dockerhub + run: | + disable_dockerhub() { + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "$1" + } + + if [ -z "$DOCKERHUB_IMAGE" ]; then + disable_dockerhub "Docker Hub publishing disabled: DOCKERHUB_IMAGE repository variable is not set" + exit 0 + fi + + if [ ! -f Dockerfile ]; then + disable_dockerhub "Docker Hub publishing disabled: Dockerfile was not found at repository root" + exit 0 + fi + + if [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_TOKEN" ]; then + echo "::error::Docker Hub publishing requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN" + echo "Set DOCKERHUB_USERNAME as a repository variable or secret, and DOCKERHUB_TOKEN as a secret." + exit 1 + fi + + echo "enabled=true" >> "$GITHUB_OUTPUT" + echo "docker_hub_url=https://hub.docker.com/r/${DOCKERHUB_IMAGE}" >> "$GITHUB_OUTPUT" + + - name: Log in to Docker Hub + if: steps.dockerhub.outputs.enabled == 'true' + uses: docker/login-action@v4 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ env.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + if: steps.dockerhub.outputs.enabled == 'true' + uses: docker/setup-buildx-action@v4 + + - name: Extract Docker metadata + if: steps.dockerhub.outputs.enabled == 'true' + id: docker-meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.DOCKERHUB_IMAGE }} + tags: | + type=raw,value=latest + type=raw,value=${{ steps.version.outputs.new_version }} + labels: | + org.opencontainers.image.version=${{ steps.version.outputs.new_version }} + + - name: Publish Docker image to Docker Hub + if: steps.dockerhub.outputs.enabled == 'true' + uses: docker/build-push-action@v7 + with: + context: . + push: true + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + + - name: Create GitHub Release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCKER_HUB_URL: ${{ steps.dockerhub.outputs.docker_hub_url }} + run: | + release_args=( + --release-version "${{ steps.version.outputs.new_version }}" + --repository "${{ github.repository }}" + ) + if [ -n "$DOCKER_HUB_URL" ]; then + release_args+=(--docker-hub-url "$DOCKER_HUB_URL") + fi + rust-script scripts/create-github-release.rs "${release_args[@]}" + + # === MANUAL CHANGELOG PR === + changelog-pr: + name: Create Changelog PR + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changelog-pr' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Create changelog fragment + env: + BUMP_TYPE: ${{ github.event.inputs.bump_type }} + DESCRIPTION: ${{ github.event.inputs.description }} + run: rust-script scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: add changelog for manual ${{ github.event.inputs.bump_type }} release' + branch: changelog-manual-release-${{ github.run_id }} + delete-branch: true + title: 'chore: manual ${{ github.event.inputs.bump_type }} release' + body: | + ## Manual Release Request + + This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release. + + ### Release Details + - **Type:** ${{ github.event.inputs.bump_type }} + - **Description:** ${{ github.event.inputs.description || 'Manual release' }} + - **Triggered by:** @${{ github.actor }} + + ### Next Steps + 1. Review the changelog fragment in this PR + 2. Merge this PR to main + 3. The automated release workflow will publish to crates.io and create a GitHub release + + # === DEPLOY DOCUMENTATION === + # Deploy Rust API documentation to GitHub Pages after a successful package build. + # Keep this independent from package/GitHub release publication so the website + # still updates when the release path fails. Use the official Pages artifact + # deployment path so repositories configured with "GitHub Actions" as their + # Pages source fail this job if Pages cannot deploy. + # + # One-time setup: in the repository's Settings -> Pages, set Source to + # "GitHub Actions". Without this, the first run fails on actions/deploy-pages + # with "Get Pages site failed" / "Failed to create deployment". This cannot be + # configured from a workflow. See README.md "Deploying API documentation". + deploy-docs: + name: Deploy Rust Documentation + needs: [build] + if: | + !cancelled() && + needs.build.result == 'success' && ( + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant') + ) + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v6 + with: + ref: main + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build documentation + run: cargo doc --no-deps --all-features + + - name: Configure GitHub Pages + uses: actions/configure-pages@v6 + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: target/doc + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f928079..20c3e42 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,13 +2,15 @@ name = "link-cli" version = "0.1.0" edition = "2021" -description = "A CLI tool for links manipulation" +description = "A CLI tool and reusable library for links manipulation backed by a LiNo-notation doublet storage engine." license = "Unlicense" repository = "https://github.com/link-foundation/link-cli" +documentation = "https://docs.rs/link-cli" +homepage = "https://github.com/link-foundation/link-cli" readme = "README.md" authors = ["link-foundation"] keywords = ["links", "doublets", "cli", "database"] -categories = ["command-line-utilities", "database"] +categories = ["command-line-utilities", "database", "data-structures"] [lib] name = "link_cli" diff --git a/rust/README.md b/rust/README.md index debb0ef..3fdf273 100644 --- a/rust/README.md +++ b/rust/README.md @@ -5,17 +5,29 @@ [![Docs.rs](https://docs.rs/link-cli/badge.svg)](https://docs.rs/link-cli) [![GitHub Release](https://img.shields.io/github/v/release/link-foundation/link-cli?filter=rust-v*&label=Rust%20release)](https://github.com/link-foundation/link-cli/releases) -This directory contains the Rust `link-cli` library and native `clink` binary. -It mirrors the core query processor, named references, LiNo import/export, and -structure formatting used by the production C# tool. The WebAssembly wrapper -crate lives in `rust/wasm/` and depends on this package. +This directory contains the Rust `link-cli` crate, which publishes both a +reusable `[lib]` (`link_cli`) and the `clink` `[[bin]]` from the same +package. It mirrors the core query processor, named references, LiNo +import/export, and structure formatting used by the production C# tool. +The WebAssembly wrapper crate lives in `rust/wasm/` and depends on this +package. ## Install ```bash +# Build and install the CLI binary. cargo install link-cli ``` +```bash +# Or pull in the public API to build your own tooling. +cargo add link-cli +``` + +API documentation for every published version is hosted on +[docs.rs/link-cli](https://docs.rs/link-cli). A copy is also published to +GitHub Pages alongside the C# DocFX site by `.github/workflows/docs.yml`. + ## Use ```bash diff --git a/rust/changelog.d/20260515_140000_issue_92_library_metadata.md b/rust/changelog.d/20260515_140000_issue_92_library_metadata.md new file mode 100644 index 0000000..e2ef4b8 --- /dev/null +++ b/rust/changelog.d/20260515_140000_issue_92_library_metadata.md @@ -0,0 +1,8 @@ +--- +bump: patch +--- + +Documented the dual `[lib]` + `[[bin]]` nature of the `link-cli` crate and +added crates.io metadata (`description`, `documentation`, `homepage`, +`keywords`, `categories`) so the public library surface is discoverable +from crates.io and docs.rs.