Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ My purpose here is singular: to bring Scribe into the world in its highest possi
| Document | What it covers |
| --- | --- |
| [README](../README.md) | Overview, quick start, component table, doc index |
| [Glossary](../docs/glossary.md) | Canonical definitions of Shape DSL vocabulary (Shape, Focus, Lens, Projection, Violation, Relation) and text-emission primitives |

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compass entry for the new glossary says it defines “Relation”, but this PR renames the concept/API to Prism. Update this description (and any other references) to match the current vocabulary so contributors aren’t pointed at the old term.

Suggested change
| [Glossary](../docs/glossary.md) | Canonical definitions of Shape DSL vocabulary (Shape, Focus, Lens, Projection, Violation, Relation) and text-emission primitives |
| [Glossary](../docs/glossary.md) | Canonical definitions of Shape DSL vocabulary (Shape, Focus, Lens, Projection, Violation, Prism) and text-emission primitives |

Copilot uses AI. Check for mistakes.
| [The Shape DSL](../docs/dsl.md) | Overview of Scribe's query language — building blocks, composition rules, fluent vs query-comprehension form, three-consumer materialisation |
| [Writing Generators](../docs/writing-generators.md) | Transform -> Register -> Render pattern, Quill usage guide |
| [Project Setup](../docs/project-setup.md) | .csproj configuration, packaging, Stubs, LocalDev multi-repo workflow with automatic local NuGet resolution |
| [Quill Reference](../docs/quill-reference.md) | Complete Quill API reference |
Expand Down
14 changes: 14 additions & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Local dev override. Lets MSBuild's SDK resolver find the locally-packed
Scribe.Sdk (-dev.*) in the workspace-shared artifacts dir so changes to
the SDK source run in Scribe's own repo builds without a publish round-trip.
Safe to keep checked in: nuget.org remains the default feed for everything
else, and the Local source is only consulted when a matching version exists.
-->
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="LocalArtifacts" value="..\.artifacts\packages" />
</packageSources>
</configuration>
20 changes: 10 additions & 10 deletions Scribe.Ink/Shapes/CacheCorrectnessAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace Scribe.Ink.Shapes;

/// <summary>
/// SCRIBE200 — forbid Roslyn reference types on any TModel passed to
/// <c>Shape&lt;TModel&gt;.Project&lt;TModel&gt;</c>. Holding an <c>ISymbol</c>,
/// <c>TypeShape.Etch&lt;TModel&gt;</c>. Holding an <c>ISymbol</c>,
/// <c>SyntaxNode</c>, <c>Compilation</c>, <c>SemanticModel</c>, <c>SyntaxTree</c>,
/// <c>Location</c>, or <c>AttributeData</c> in a cached model defeats the
/// <c>Location</c>, or <c>AttributeData</c> in an etched model defeats the
/// incremental generator cache: those objects identity-compare per compilation.
/// Extract the primitive data you need into strings, equatable arrays, or
/// <see cref="Scribe.Cache.LocationInfo"/> instead.
Expand All @@ -21,8 +21,8 @@ public sealed class CacheCorrectnessAnalyzer : DiagnosticAnalyzer

private static readonly DiagnosticDescriptor Descriptor = new(
id: DiagnosticId,
title: "Cache-hostile type in Shape projection model",
messageFormat: "Member '{0}' of type '{1}' is a Roslyn reference type ('{2}') — storing it in a Shape projection model defeats incremental caching. Extract primitives or use LocationInfo instead.",
title: "Cache-hostile type in etched Shape model",
messageFormat: "Member '{0}' of type '{1}' is a Roslyn reference type ('{2}') — storing it in an etched Shape model defeats incremental caching. Extract primitives or use LocationInfo instead.",
category: "Scribe.Cache",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
Expand All @@ -39,8 +39,8 @@ public override void Initialize(AnalysisContext context)

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
var shapeBuilder = context.Compilation.GetTypeByMetadataName("Scribe.Shapes.ShapeBuilder");
if (shapeBuilder is null)
var typeShape = context.Compilation.GetTypeByMetadataName("Scribe.Shapes.TypeShape");
if (typeShape is null)
{
return;
}
Expand All @@ -52,7 +52,7 @@ private static void OnCompilationStart(CompilationStartAnalysisContext context)
}

context.RegisterOperationAction(
ctx => AnalyzeInvocation(ctx, shapeBuilder, forbidden),
ctx => AnalyzeInvocation(ctx, typeShape, forbidden),
OperationKind.Invocation);
}

Expand Down Expand Up @@ -81,7 +81,7 @@ void Add(string metadataName)

private static void AnalyzeInvocation(
OperationAnalysisContext context,
INamedTypeSymbol shapeBuilder,
INamedTypeSymbol typeShape,
ImmutableArray<INamedTypeSymbol> forbidden)
{
if (context.Operation is not IInvocationOperation invocation)
Expand All @@ -90,9 +90,9 @@ private static void AnalyzeInvocation(
}

var method = invocation.TargetMethod;
if (method.Name != "Project"
if (method.Name != "Etch"
|| method.TypeArguments.Length != 1
|| !SymbolEqualityComparer.Default.Equals(method.OriginalDefinition.ContainingType, shapeBuilder))
|| !SymbolEqualityComparer.Default.Equals(method.OriginalDefinition.ContainingType, typeShape))
{
return;
}
Expand Down
4 changes: 2 additions & 2 deletions Scribe.Ink/Shapes/IShapeCustomFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace Scribe.Ink.Shapes;
/// User-supplied fix handler for diagnostics emitted with
/// <see cref="Scribe.Shapes.FixKind.Custom"/>. Registered on a shape via
/// <see cref="ShapeInkExtensions.WithCustomFix{TModel}"/> under a
/// <c>customFixTag</c> that the <see cref="Scribe.Shapes.ShapeBuilder.ForEachMember"/>
/// or <see cref="Scribe.Shapes.ShapeBuilder.Check"/> declaration carried.
/// <c>customFixTag</c> that the <see cref="Scribe.Shapes.TypeShape.ForEachMember"/>
/// or <see cref="Scribe.Shapes.TypeShape.Check"/> declaration carried.
/// </summary>
/// <remarks>
/// <para>
Expand Down
4 changes: 2 additions & 2 deletions Scribe.Ink/Shapes/ShapeInkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class ShapeInkExtensions
/// instance in a concrete <c>[ExportCodeFixProvider]</c>-attributed class
/// that delegates its members for deployment.
/// </summary>
public static CodeFixProvider ToFixProvider<TModel>(this Shape<TModel> shape)
public static CodeFixProvider ToInk<TModel>(this Shape<TModel> shape)
where TModel : IEquatable<TModel>
{
if (shape is null)
Expand All @@ -46,7 +46,7 @@ public static CodeFixProvider ToFixProvider<TModel>(this Shape<TModel> shape)
/// Diagnostics whose <c>fixKind</c> is <see cref="FixKind.Custom"/> and whose
/// <c>customFixTag</c> property matches <paramref name="tag"/> will be routed
/// to <paramref name="fix"/> by the provider returned from
/// <see cref="ToFixProvider{TModel}"/>.
/// <see cref="ToInk{TModel}"/>.
/// </summary>
public static Shape<TModel> WithCustomFix<TModel>(
this Shape<TModel> shape, string tag, IShapeCustomFix fix)
Expand Down
4 changes: 2 additions & 2 deletions Scribe.Scriptorium/Scribe.Scriptorium.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<!--
Scriptorium is a build-time-only source generator (Meta type) that runs during
Scribe's own compile to emit Should*/Could*/ShouldNot* variants of each Must*
primitive on ShapeBuilder. Never shipped to consumers.
primitive on TypeShape. Never shipped to consumers.
-->
<Project Sdk="BulletsForHumanity.Scribe.Sdk">
<PropertyGroup>
<ScribeSdkProjectType>Meta</ScribeSdkProjectType>
<PackageId>BulletsForHumanity.Scribe.Scriptorium</PackageId>
<Description>Build-time-only meta-generator that emits Should*/Could*/ShouldNot* variants of each Must* primitive on Scribe's ShapeBuilder. Consumed solution-internally by Scribe via local -dev PackageReference for VS analyzer hot-refresh.</Description>
<Description>Build-time-only meta-generator that emits Should*/Could*/ShouldNot* variants of each Must* primitive on Scribe's TypeShape. Consumed solution-internally by Scribe via local -dev PackageReference for VS analyzer hot-refresh.</Description>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Scribe.Scriptorium;

/// <summary>
/// Meta-generator. Runs at Scribe's own compile time. Scans
/// <c>Scribe.Shapes.ShapeBuilder</c> for every <c>Must*</c> primitive and
/// <c>Scribe.Shapes.TypeShape</c> for every <c>Must*</c> primitive and
/// emits severity variants into a partial class:
/// <list type="bullet">
/// <item><c>Must*</c> (positive) → <c>Should*</c> (Warning) + <c>Could*</c> (Info)</item>
Expand All @@ -19,9 +19,9 @@ namespace Scribe.Scriptorium;
/// only the severity when the caller has not supplied one.
/// </summary>
[Generator(LanguageNames.CSharp)]
public sealed class ShapeBuilderVariantsGenerator : IIncrementalGenerator
public sealed class TypeShapeVariantsGenerator : IIncrementalGenerator
{
private const string ShapeBuilderMetadataName = "Scribe.Shapes.ShapeBuilder";
private const string TypeShapeMetadataName = "Scribe.Shapes.TypeShape";
private const string DiagnosticSpecFqn = "Scribe.Shapes.DiagnosticSpec";

// FullyQualifiedFormat minus UseSpecialTypes, so `System.String` doesn't collapse to the
Expand All @@ -42,14 +42,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
if (!string.IsNullOrEmpty(src))
{
spc.AddSource("ShapeBuilder.Variants.g.cs", src!);
spc.AddSource("TypeShape.Variants.g.cs", src!);
}
});
}

private static string? BuildSource(Compilation compilation, CancellationToken ct)
{
var shapeBuilder = compilation.GetTypeByMetadataName(ShapeBuilderMetadataName);
var shapeBuilder = compilation.GetTypeByMetadataName(TypeShapeMetadataName);
if (shapeBuilder is null)
{
return null;
Expand All @@ -74,7 +74,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
sb.AppendLine();
sb.AppendLine("namespace Scribe.Shapes;");
sb.AppendLine();
sb.AppendLine("partial class ShapeBuilder");
sb.AppendLine("partial class TypeShape");
sb.AppendLine("{");

var first = true;
Expand Down Expand Up @@ -108,7 +108,7 @@ private static bool IsVariantTarget(IMethodSymbol method)
return false;
}

if (method.ReturnType.ToDisplayString() != ShapeBuilderMetadataName)
if (method.ReturnType.ToDisplayString() != TypeShapeMetadataName)
{
return false;
}
Expand Down Expand Up @@ -164,7 +164,7 @@ private static void EmitOne(

sb.Append(" /// <summary>").Append(summary).AppendLine("</summary>");

sb.Append(" public global::Scribe.Shapes.ShapeBuilder ").Append(newName);
sb.Append(" public global::Scribe.Shapes.TypeShape ").Append(newName);
AppendTypeParameters(sb, method);
sb.Append('(');
AppendParameterList(sb, method);
Expand Down Expand Up @@ -310,7 +310,7 @@ private static string FormatDefault(object? value, ITypeSymbol type)
private static string FormatXmlRef(IMethodSymbol method)
{
var builder = new StringBuilder();
builder.Append("Scribe.Shapes.ShapeBuilder.").Append(method.Name);
builder.Append("Scribe.Shapes.TypeShape.").Append(method.Name);
if (method.TypeParameters.Length > 0)
{
builder.Append('{');
Expand Down
64 changes: 37 additions & 27 deletions Scribe.Sdk/Sdk/Phases/Meta.targets
Original file line number Diff line number Diff line change
Expand Up @@ -51,35 +51,40 @@
<IsPackable Condition="'$(IsPackable)' == ''">true</IsPackable>
</PropertyGroup>

<!-- ── Packable-only: timestamped version + auto-pack ──────────────────── -->
<!-- Only applies when the consumer opts into publishing across repos. -->
<PropertyGroup Condition="'$(IsPackable)' == 'true'">
<!-- Timestamped dev version — strictly increasing, defeats NuGet cache. -->
<_ScribeMetaTimestamp>$([System.DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))</_ScribeMetaTimestamp>
<Version>0.0.0-dev.$(_ScribeMetaTimestamp)</Version>
<PackageVersion>0.0.0-dev.$(_ScribeMetaTimestamp)</PackageVersion>
<NerdbankGitVersioningEnabled>false</NerdbankGitVersioningEnabled>
<!-- ── Workspace-shared artifacts directory ────────────────────────────── -->
<!-- Meta is a local-dev-only construct. It always targets the shared
$(ScribeArtifactsDir) so sibling consumers find the -dev.* nupkg via the
wildcard-imported $(PackageId).Directory.Packages.targets override.
Derive from $(ScribeRoot) directly — we don't depend on LocalDev having
fired, because Meta projects must behave the same whether the sentinel
is present or not. -->
<PropertyGroup Condition="'$(ScribeArtifactsDir)' == '' and '$(ScribeRoot)' != ''">
<ScribeArtifactsDir>$(ScribeRoot)\.artifacts\</ScribeArtifactsDir>
<ScribePackagesDir>$(ScribeArtifactsDir)packages\</ScribePackagesDir>
</PropertyGroup>

<!-- ── Artifacts directory (normalized with trailing slash) ────────────── -->
<!-- Only route PackageOutputPath to $(ArtifactsPath) when no stronger setter
has fired. LocalDev.props sets it to the shared $(ScribePackagesDir) in
producer mode — that must win so sibling consumers find the -dev.* pkg
via the Directory.Packages.targets override file. -->
<PropertyGroup Condition="'$(ArtifactsPath)' != ''">
<_ScribeMetaArtifactsDir>$([MSBuild]::EnsureTrailingSlash('$(ArtifactsPath)'))</_ScribeMetaArtifactsDir>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">$(_ScribeMetaArtifactsDir)packages\</PackageOutputPath>
<!-- ── Debug auto-pack (sentinel-independent) ──────────────────────────── -->
<!-- Meta's entire reason to exist is build-time-only consumption inside the
workspace. In Debug we always auto-pack and route to the shared packages
dir so consumers resolve the freshest build via VersionOverride. No
sentinel is required — Meta is always "in local mode" when Debug and
$(ScribeRoot) is set. IsLocalScribe is forced so LocalDev.targets'
consumer-side wildcard import picks up this override file too. -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'
and '$(IsPackable)' == 'true'
and '$(ScribePackagesDir)' != ''">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageOutputPath>$(ScribePackagesDir)</PackageOutputPath>
<IsLocalScribe Condition="'$(IsLocalScribe)' != 'true'">true</IsLocalScribe>
</PropertyGroup>

<!-- Register the Meta packages directory as a NuGet source so restore finds
<!-- Register the shared packages directory as a NuGet source so restore finds
sibling Meta-produced packages. Only do this when the directory already
exists — NuGet emits NU1301 ("The local source doesn't exist") otherwise,
which breaks restore on fresh clones and CI where nothing has been packed
yet. The Pack target below is what creates the directory; once it exists,
subsequent restores pick it up. -->
<PropertyGroup Condition="'$(_ScribeMetaArtifactsDir)' != ''
and Exists('$(_ScribeMetaArtifactsDir)packages')">
<RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);$(_ScribeMetaArtifactsDir)packages</RestoreAdditionalProjectSources>
exists — NuGet emits NU1301 ("The local source doesn't exist") otherwise.
The Pack target below creates the directory; subsequent restores pick up. -->
<PropertyGroup Condition="'$(ScribePackagesDir)' != ''
and Exists('$(ScribePackagesDir)')">
<RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);$(ScribePackagesDir)</RestoreAdditionalProjectSources>
</PropertyGroup>

<!-- ── Own DLL + private deps into analyzers/dotnet/cs/ ────────────────── -->
Expand Down Expand Up @@ -113,14 +118,19 @@
</Target>

<!-- ── Emit PackageVersion override file after Pack ────────────────────── -->
<!-- Writes to the workspace-shared $(ScribeArtifactsDir) so MetaConsumer
resolves via the wildcard import in LocalDev.targets. When the full
LocalDev sentinel is also active, LocalDev.targets skips its own
override emission for Meta projects (see guard there) to avoid writing
the same file twice. -->
<Target Name="_ScribeSdkMetaOverride"
AfterTargets="Pack"
Condition="'$(_ScribeMetaArtifactsDir)' != ''
Condition="'$(ScribeArtifactsDir)' != ''
and '$(PackageId)' != ''">
<PropertyGroup>
<_ScribeMetaOverrideVersion>$(NuGetPackageVersion)</_ScribeMetaOverrideVersion>
<_ScribeMetaOverrideVersion Condition="'$(_ScribeMetaOverrideVersion)' == ''">$(PackageVersion)</_ScribeMetaOverrideVersion>
<_ScribeMetaOverridePath>$(_ScribeMetaArtifactsDir)$(PackageId).Directory.Packages.targets</_ScribeMetaOverridePath>
<_ScribeMetaOverridePath>$(ScribeArtifactsDir)$(PackageId).Directory.Packages.targets</_ScribeMetaOverridePath>
</PropertyGroup>
<ItemGroup>
<_ScribeMetaOverrideLine Include="&lt;Project&gt;" />
Expand All @@ -130,7 +140,7 @@
<_ScribeMetaOverrideLine Include=" &lt;/ItemGroup&gt;" />
<_ScribeMetaOverrideLine Include="&lt;/Project&gt;" />
</ItemGroup>
<MakeDir Directories="$(_ScribeMetaArtifactsDir)" />
<MakeDir Directories="$(ScribeArtifactsDir)" />
<WriteLinesToFile File="$(_ScribeMetaOverridePath)"
Lines="@(_ScribeMetaOverrideLine)"
Overwrite="true"
Expand Down
Loading
Loading