Skip to content

breaking: typed expression API (Tex) for v3 - #37

Merged
DecSmith42 merged 17 commits into
mainfrom
claude/project-thread-snwqnb
Oct 16, 2026
Merged

DecSmith42 merged 17 commits into
mainfrom
claude/project-thread-snwqnb

Conversation

@DecSmith42

@DecSmith42 DecSmith42 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Dec · project thread

Before: workflow values and conditions are built from the untyped TextExpression tree, with manual Evaluate() and Concat calls and a hand-written context catalogue. For example: TextExpressions.Target.ParamOutput(...).Contains("-").EqualTo(false).

After: everything starts from one entry point, Tex. Conditions are typed BoolExpr values written with C# operators, such as !Tex.ParamOutput(...).Contains("-") and Tex.GitHub.EventName.Eq("push") && Tex.Success(). YAML values are Templates built from interpolated strings, such as $"{Tex.GitHub.Workflow}-{Tex.GitHub.Ref}". The same expressions print as GitHub Actions or Azure DevOps syntax.

This is a breaking change: the TextExpression API is removed. The branch's breaking: commit bumps main to 3.0.0 (the last release is v2.1.0).

How:

  • Core: adds a typed expression core: Expr with Bool, String, Number and Json roots; the Tex entry point; a Template interpolated-string handler; and an ExpressionPrinter base class.
  • Platform contexts: Tex.GitHub and Tex.DevOps are C# 14 extension members. The GitHub catalogue is generated by an incremental source generator from Contexts/github.contexts.
  • GitHub Actions: the model and writer use the new types. Conditions that start with ! are wrapped in ${{ }} so YAML doesn't read them as tags.
  • Azure DevOps: the model and writer use the new types. References are embedded as macros ($(x)), template expressions (${{ }}) or runtime expressions ($[ ]) by evaluation phase. Combinations Azure DevOps can't evaluate throw NotSupportedException with guidance, instead of writing YAML that misbehaves.
  • Docs: the README, docs pages, api/index.md and AGENTS.md are rewritten. Runnable samples live in samples/ and are checked by SamplesTests.

Checked:

  • 424 tests pass, and the public API snapshots are accepted. Every docs sample was compiled, and its output was checked against the writers. DocFX builds without warnings.
  • Atom was ported against locally packed packages (Atom branch claude/project-thread-snwqnb). All Atom tests pass. The Atom 5.0 PR follows once this release candidate is published.
  • All eleven repos were regenerated with this code and compared against Atom main. The only change is the deliberate one: if: contains(...) == false becomes if: ${{ !contains(...) }}.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct

Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved context-generation and expression-rendering errors can produce incorrect or invalid workflows.

4 open findings
What changed in this PR

This proof of concept introduces the v3 typed expression API and ports GitHub Actions generation to it. Azure DevOps, Dependabot, and documentation migration remain outside this PR’s scope.

Changes:

  • Adds typed expressions, interpolated templates, and expression printers.
  • Generates the GitHub context catalogue from a data file.
  • Migrates GitHub Actions models, writer tests, and public API snapshots.
File Description
tests/​Invex.StructuredText.Tests/​PublicApiTests.VerifyPublicApiSurface.verified.txt Records new core APIs.
tests/​Invex.StructuredText.GithubActions.Tests/​PublicApiTests.VerifyPublicApiSurface.verified.txt Records catalogue and printer APIs.
tests/​Invex.StructuredText.GithubActions.Tests/​PublicApiSurfaceTests.cs Uses the new printer assembly anchor.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubExpressionPrinterTests.cs Adds typed expression rendering tests.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubExpressionFormatterTests.cs Removes legacy formatter tests.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubActionWriterStepTests.cs Migrates step fixtures.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubActionWriterPropertyTests.cs Migrates scalar fixtures.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubActionWriterPipelineTests.cs Migrates workflow fixtures.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubActionWriterJobTests.cs Migrates job fixtures.
tests/​Invex.StructuredText.GithubActions.Tests/​GithubActionWriterHelper.cs Updates shared fixtures.
src/​Invex.StructuredText/​Expressions/​Typed/​Template.cs Adds interpolated templates.
src/​Invex.StructuredText/​Expressions/​Typed/​Factories.cs Adds reference and function factories.
src/​Invex.StructuredText/​Expressions/​Typed/​ExprNodes.cs Defines typed expression nodes.
src/​Invex.StructuredText/​Expressions/​Typed/​ExpressionPrinter.cs Adds shared expression rendering.
src/​Invex.StructuredText/​Expressions/​Typed/​Expr.cs Defines typed roots and operators.
src/​Invex.StructuredText/​_usings.cs Imports invariant-culture support.
src/​Invex.StructuredText.GithubActions/​Invex.StructuredText.GithubActions.csproj Wires in catalogue generation.
src/​Invex.StructuredText.GithubActions/​GithubExpressionPrinter.cs Adds GitHub expression syntax.
src/​Invex.StructuredText.GithubActions/​GithubExpressionFormatter.cs Removes the legacy formatter.
src/​Invex.StructuredText.GithubActions/​GithubActionWriter.cs Uses templates and typed conditions.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Strategy.cs Converts strategy values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Step.cs Types conditions and step values.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Snapshot.cs Converts snapshot values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​RunsOn.cs Converts runner values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Matrix.cs Converts matrix values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Job.cs Migrates job expressions and dependencies.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​GithubAction.cs Converts workflow values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Environment.cs Converts deployment values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Credentials.cs Converts credentials to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Container.cs Converts container values to templates.
src/​Invex.StructuredText.GithubActions/​GithubActionModel/​Concurrency.cs Converts concurrency values to templates.
src/​Invex.StructuredText.GithubActions/​Contexts/​github.contexts Defines the typed context catalogue.
src/​Invex.StructuredText.Generators/​Invex.StructuredText.Generators.csproj Adds the build-time generator project.
src/​Invex.StructuredText.Generators/​ContextCatalogueGenerator.cs Generates context APIs from data.
Invex.StructuredText.slnx Registers the generator project.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Script serialization can alter command contents, and some typed conditions produce invalid YAML.

1 open finding
4 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Quote literal-first conditions in YAML

src/​Invex.StructuredText.GithubActions/​GithubExpressionPrinter.cs:32

A typed comparison can start with a string literal: new StringLiteral("main").Eq(GitHubContexts.Ref) prints 'main' == github.ref. The writer leaves this unchanged, producing invalid YAML (if: 'main' == github.ref) because the quoted scalar ends before the comparison. Wrap conditions beginning with quotes as well as !, checking after leading whitespace so raw negated conditions are also protected. Add a writer regression test for a literal-first comparison.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The generated workflow sample can silently pass an empty version to downstream jobs when GitVersion is unavailable.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The explicitly non-mergeable public API prototype needs human review, with incomplete migrations and an unresolved YAML serialization regression.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The explicitly non-mergeable API redesign needs human review, with unresolved serialization and API concerns.

1 open finding
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Support primitive arguments for Expr parameters

src/​Invex.StructuredText/​Expressions/​Typed/​Expr.cs:12

The implicit primitive conversions are declared only on the typed subclasses, so C# does not consider them when the target parameter type is Expr. As a result, Fn.Format("{0}", "hello") and Fn.FromJson("[1]").Contains(1) do not compile; callers must explicitly cast the literals to StringExpr or NumberExpr. Support primitive arguments consistently through conversions on Expr or suitable helper overloads, and add tests that compile these calls.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The deliberately non-mergeable API redesign needs human review and still has unresolved correctness issues.

3 open findings
1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The intentionally incomplete prototype changes broad public APIs and retains a serialization defect, requiring human review before integration.

1 open finding
3 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity CRLF line endings trigger false stray-carriage-return failures

tests/​Invex.StructuredText.GithubActions.Tests/​GithubActionWriterStepTests.cs:247

This assertion fails on Windows even when the input's carriage returns are normalized correctly. StructuredTextWriter.WriteLine uses StringBuilder.AppendLine, so generated lines end with \r\n on Windows. Remove valid CRLF pairs before checking for stray carriage returns.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 16:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The breaking API redesign is explicitly a non-merge-ready proof of concept with deferred migrations requiring human review.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 15, 2026 06:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Custom indentation widths can still produce invalid YAML block-scalar indicators.

1 open finding

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 15, 2026 06:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The explicitly non-merge-ready prototype introduces broad breaking API changes and incomplete migrations that require human architectural review.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity Keep core XML summary platform-neutral

src/​Invex.StructuredText/​Expressions/​Typed/​Tex.cs:7

Keep this core XML summary platform-neutral, as required by AGENTS.md:90–92. Describe platform extensions generically here; the concrete Tex.GitHub example already belongs in Contexts/TexGitHubExtensions.cs in the GitHub Actions package.

🧠 Review effort: Balanced

claude added 12 commits October 15, 2026 14:51
- samples/Invex.StructuredText.Samples: conditions, values, references,
  functions and escape hatches, and a complete CI workflow. SamplesTests
  snapshots each sample's output, and samples/README.md shows it.
- The writer now writes `group:` under `concurrency:` (it was missing).
- Multi-line run steps are always written as a literal block (`run: |`).
  After another property they were written as a YAML list, and block lines
  were quoted, which changed the script.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
- Generator: top-level members are keyed by full path, so github.env and the
  env context no longer merge. Colliding github.* members are named GitHubEnv
  and GitHubJob; Env and Job are the contexts. Duplicate names now report STG002.
- Indexers keep the key whole. The printer writes ['key'] (escaped) where
  .key isn't valid, for contexts and for parsed JSON alike.
- PrintCondition checks the first non-whitespace character for '!'.
- Raw text that isn't a plain dotted path has the lowest precedence, so it is
  parenthesized under !, && and ||, and still written verbatim on its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
…syntax

- Multi-line values keep their blank lines (only trailing ones are dropped),
  in both the literal-block and scalar paths, so here-documents survive.
- Conditions whose first non-whitespace character is YAML syntax (a quote,
  !, &, *, [, {, ...) are wrapped in ${{ }}, e.g. 'main' == github.ref.
- As a safety net, Format quotes any value that starts with YAML syntax or has
  leading or trailing whitespace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
A value with a trailing newline was quoted with the newline still in it,
leaving the closing quote unindented on the next line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
…epted

- A first-property run value containing only '\r' line breaks now takes the
  literal-block path instead of being written inline.
- Expr has implicit conversions from string, double and bool, so
  Fn.Format("{0}", "hello") and FromJson(...).Contains(1) compile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
…icit context Path

- A literal block whose first line is indented now gets `|2`, so YAML
  doesn't take its indentation from that line and drop the rest.
- `$"flag={true}"` renders `flag=true` instead of an expression.
- Generated context records implement IContextPath.Path explicitly, so a
  catalogue member called path compiles; github.event.workflow_run.path
  added as the regression case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
…nt size

The `- key: |` path places content 2 * IndentSize - 2 in from its key and
the plain `key: |` path IndentSize, so a writer with a custom indent size
gets the right indicator. Tests cover four-space indentation for a script
and an env value, and the CR-only test no longer trips on Windows newlines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
- Tex replaces Ref, Status and Fn, and adds Join and Raw, so everything
  that creates an expression is one "Tex." away.
- Tex.Raw returns a RawExpr: a condition that also converts to string,
  number and JSON expressions (replaces the per-type Raw factories).
- The generated GitHub catalogue is now an instance class, reached
  through a Tex.GitHub extension property.
- `using static Invex.StructuredText.Expressions.Tex;` drops the prefix.

Generated output is unchanged; only sample labels moved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
Indent size 1 or 6+ would produce |0 or |10 for a block whose first line
is indented; the writer now throws a clear NotSupportedException instead
of writing invalid YAML. Other blocks are unaffected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
- DevopsExpressionPrinter replaces DevopsExpressionFormatter. In templates,
  secrets, variables, matrix values and step outputs render as $(name),
  job outputs and Tex.DevOps.Runtime(...) as $[ ... ], and anything else
  as ${{ ... }}. Conditions use and()/eq()/succeeded() and so on.
- Model values are Template, conditions are BoolExpr and lists are
  IReadOnlyList<Template>, matching the GitHub Actions package.
- Tex.DevOps adds common predefined variables (Build, System, Agent,
  Pipeline.Workspace) and Runtime(...).
- ExpressionPrinter gets a RenderEmbedded hook for platforms with more
  than one embedding syntax.

Writer output is unchanged: every pipeline writer test passes as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
Nothing uses TextExpression, TextExpressions, TextExpressionFormatter,
WorkflowExpression<T> or the collection wrappers any more: both writers
and Atom are on Tex, Expr and Template. Their tests go with them; the
typed core is covered by the GitHub Actions and Azure DevOps printer,
writer and sample tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
@DecSmith42
DecSmith42 force-pushed the claude/project-thread-snwqnb branch from 21eb78f to 9b81418 Compare October 15, 2026 14:53
Rewrites the README, AGENTS.md, api/index.md and the docs pages for Tex, Expr and
Template, and removes the old TextExpression material. Every code sample was compiled
and its output checked against the writers.

Also renames the escape-hatch sample label to Tex.Raw and deletes the orphaned
PublicApiSurfaceTests snapshots (the test class is PublicApiTests).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Azure expression rendering can select incorrect evaluation phases and emit invalid or unevaluated pipeline values.

5 open findings

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 15, 2026 15:04
- Templates that read a job output or result, or contain a runtime marker, are
  rendered as $[ ... ], including through casts and compound expressions.
- A runtime expression combined with other text throws NotSupportedException,
  since Azure DevOps only evaluates $[ ... ] as a whole value.
- join takes the separator first in Azure DevOps, with ',' when none is given.
- Array membership (In, JsonExpr.Contains) prints containsValue in Azure DevOps
  and contains in GitHub Actions.
- A runtime marker nested in an expression no longer adds grouping parentheses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot AI balanced review requested due to automatic review settings October 15, 2026 15:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Azure expression rendering still has unresolved identifier and evaluation-phase errors that can invalidate pipelines or lose output values.

4 open findings
1 resolved since last review

🧠 Review effort: Balanced

- Inputs (parameters) in conditions or runtime expressions throw, since
  parameters only exist at compile time. Conditions go through a new
  DevopsExpressionPrinter.PrintCondition.
- Secrets, matrix values and step outputs inside a compound compile-time
  expression throw; interpolating them directly still writes a macro.
- Names that aren't valid Azure DevOps identifiers (such as dry-run) use
  ['name'] syntax.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhDUQU8aoPYyyfUXRi1ct
Copilot AI balanced review requested due to automatic review settings October 15, 2026 15:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

This cross-platform public API replacement needs maintainer review, with an unresolved Azure evaluation-phase defect.

0 open findings

4 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Preserve runtime availability for compound Azure variable references

src/​Invex.StructuredText.AzureDevopsPipelines/​DevopsPipelineWriter.cs:1152

A value such as Tex.Format("build-{0}", Tex.DevOps.Build.BuildId) is rendered as ${{ format('build-{0}', variables['Build.BuildId']) }}. Build.BuildId is unavailable during Azure template expansion, so the run ID is lost even though direct interpolation works as a macro. In DevopsExpressionPrinter.RenderEmbedded, compound VariableRef expressions bypass the runtime checks used for secrets, matrix values and step outputs. Preserve runtime availability for these references, or reject unsupported compositions and require Tex.DevOps.Runtime(...). Add a writer regression test using the predefined build ID.

🧠 Review effort: Balanced

@DecSmith42
DecSmith42 marked this pull request as ready for review October 16, 2026 11:11
@DecSmith42 DecSmith42 changed the title Proof of concept: typed expression API for v3 breaking: typed expression API (Tex) for v3 Oct 16, 2026
@DecSmith42
DecSmith42 merged commit 70a2842 into main Oct 16, 2026
8 checks passed
@DecSmith42
DecSmith42 deleted the claude/project-thread-snwqnb branch October 16, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants