Commit Graph

3254 Commits

Author SHA1 Message Date
devops-bot[bot] 63aebe2c00 Update preview URLs in registry 2026-04-23 16:00:34 +00:00
Jordan Ritter 577aec070d chore(showcase-aimock): remove wrapper Dockerfile (Phase 3)
Railway now pulls ghcr.io/copilotkit/aimock:latest directly
via a startCommand that fetches fixtures from GitHub raw URLs.
The wrapper Dockerfile that copied fixtures into the image is
no longer needed.

Also updates README.md to reflect the new architecture:
- References Railway startCommand instead of Dockerfile
- Removes the Dockerfile entry from the fixtures listing
- Updates related workflow descriptions
2026-04-23 08:58:09 -07:00
Sam Julien 85046381df docs-sync(needs-review): sync from main (189c45fb4) [NEEDS REVIEW] (#4119)
⚠️ **Docs sync — MANUAL REVIEW REQUIRED**

This PR was auto-opened because the docs-sync script detected
showcase-local modifications overlapping with upstream changes.

The script attempted a best-effort 3-way merge:

- Where `git merge-file` produced a clean merge, the merged content was
written.
- Where `git merge-file` produced conflict markers, **upstream content
was written as-is** and showcase-local modifications were overridden.
**Manual review required.**

### Review items

```
Files where 3-way merge FAILED — upstream content written as-is, local modifications overridden. Manual review REQUIRED before merging this PR:
  - docs/snippets/shared/generative-ui/tool-rendering.mdx

Files auto-merged via 3-way merge (clean, no conflict markers — still worth a glance):
  - docs/content/docs/integrations/langgraph/generative-ui/state-rendering.mdx
```

### Source

- Upstream ref:
[`189c45fb4`](https://github.com/CopilotKit/CopilotKit/commit/189c45fb4)
- Workflow run:
https://github.com/CopilotKit/CopilotKit/actions/runs/24736203326

**Review before merging.** Auto-merge is intentionally disabled
for `needs-review` PRs — confirm the upstream-wins sections
preserve any intentional showcase-local divergence you want to
keep, then merge manually.
2026-04-23 08:48:05 -07:00
Sam Julien 467fcfe06e fix(shell-docs): restore stripped import lines in reference docs
The initial shell-docs content-import left 21 reference pages with missing
import statements: 8 component pages had empty `## Import` code fences, and
13 hook pages had `## Signature` fences whose leading `import { X } from
"@copilotkit/react-core/v2";` line was stripped.

This commit restores the import(s) in the Import and Signature fences only.
Usage examples and other code blocks are intentionally not touched.

- 8 components: fill empty Import fence with named (or default for
  CopilotChatView) import + styles.css side-effect import
- 13 hooks: prepend import line(s) at the top of the Signature fence.
  - useComponent: 3 imports (z, type ComponentType, useComponent)
  - useRenderTool: imports injected into both Wildcard and Named overload fences
  - useCopilotChatConfiguration: imports injected into both Provider and
    hook ### Signature subblocks
2026-04-23 08:34:37 -07:00
Sam Julien fe76378ca0 fix(shell-docs): hide TOC on integrations matrix page 2026-04-23 08:34:37 -07:00
github-actions[bot] a8b08ab49d style: auto-fix formatting 2026-04-23 08:34:37 -07:00
Sam Julien bc65153aa9 docs(shell-docs): add intro sentences before code examples that directly follow headings 2026-04-23 08:34:37 -07:00
Sam Julien 795b0a9115 fix(shell-docs): strip all hardcoded IntegrationGrid headings from MDX files 2026-04-23 08:34:37 -07:00
Sam Julien 4f7530002b fix(shell-docs): remove duplicate backend header on index page 2026-04-23 08:34:37 -07:00
Sam Julien cc4a22e6ed feat(shell-docs): add table of contents (TOC) support 2026-04-23 08:34:37 -07:00
Sam Julien 1644fb8422 fix(shell-docs): align index page card styles to site CSS variables 2026-04-23 08:34:37 -07:00
Sam Julien 224871e135 fix(shell-docs): move 'Choose your AI backend' heading into IntegrationGrid component 2026-04-23 08:34:37 -07:00
Sam Julien e36792d04b fix(shell-docs): hide IntegrationGrid on framework-scoped routes 2026-04-23 08:34:37 -07:00
Sam Julien 81dced2e8e docs(shell-docs): add bridging text between InlineDemo and gif on prebuilt component pages 2026-04-23 08:34:37 -07:00
Sam Julien 1ad171b620 docs(shell-docs): writing pass to remove em-dash overuse and fix hardcoded framework references
Replace em-dashes in prose across ~37 shell-docs files with appropriate
punctuation (colons, semicolons, commas, parens, periods). Also fixes a
hardcoded "LangGraph Python" reference in quickstart.mdx bridging text,
adds a user-friendly placeholder when no framework is selected on snippet
pages, and makes heading code font size proportional rather than fixed.
2026-04-23 08:34:37 -07:00
Sam Julien 824553b251 chore(showcase): update generated data and quickstart default integration
- Update registry.json, demo-content.json, status.json, constraints.json,
  docs-status.json across shell/shell-docs/shell-dojo
- Add integration="langgraph-python" default to quickstart InlineDemo so
  the base unscoped page shows a demo instead of being empty
2026-04-23 08:34:37 -07:00
Sam Julien b7090a5d07 feat(shell-docs): build real feature matrix, fix card underlines, add backend intro
- Replace FeatureMatrix stub with a real server-rendered table reading from
  registry — 17 integrations × 12 curated feature columns, sticky first column,
  ✓/— cells, integration names link to /{slug} landing pages
- Fix matrix links from /integrations/{slug} (404) to /{slug}
- Remove redundant "Feature comparison" section from index.mdx (FeatureMatrix
  was a stub pointing to same destination as IntegrationGrid)
- Add .reference-content .not-prose a rule to suppress underlines on card grids
  inside MDX without breaking prose link styling
- Add intro sentence to "Explore by AI backend" section
2026-04-23 08:34:37 -07:00
Sam Julien 9582d4770a fix(shell-docs): fix empty FeatureIntegrations and matrix 404
- Correct 3 mismatched feature IDs in MDX (generative-ui-tool-based ->
  gen-ui-tool-based, frontend-tools-sync -> frontend-tools, reasoning ->
  agentic-chat-reasoning) so FeatureIntegrations renders integration chips
- Remove stale 'matrix' from RESERVED_ROUTE_SLUGS (no app/matrix/ route exists)
- Replace broken /matrix links in IntegrationGrid and search-modal with /
  since the matrix lives in shell-dashboard, not shell-docs
2026-04-23 08:34:37 -07:00
Sam Julien e038284edf Merge branch 'main' into docs-sync/needs-review/189c45fb4-1776791837 2026-04-23 08:34:12 -07:00
Jordan Ritter 0dccfe81af fix(showcase-ops): correct e2e_smoke key_template (underscore not hyphen)
The YAML had key_template "e2e-smoke:${name}" (hyphen) but every other
reference — DIMENSIONS constant, alert rules, kind: e2e_smoke — uses
the underscore form. This mismatch caused the producer to write
e2e-smoke:<slug> while the dashboard filtered for e2e_smoke, leaving
17 orphan hyphen rows and zero matches on the dashboard.

Changing the YAML key_template aligns the emitted probe key with the
dashboard filter. Driver code (deriveSlug) splits on ':' regardless
of prefix, so no driver/test changes are required.
2026-04-23 07:30:45 -07:00
github-actions[bot] 152c96e078 style: auto-fix formatting 2026-04-23 12:34:15 +00:00
Alem Tuzlak ad84b8a3f3 docs(showcase-langgraph-python): add Docs tab content for 24 demos
The showcase shell's demo Docs tab was rendering "No documentation
available" for 25 of 31 langgraph-python demos because their demo
folders lacked a README.md. The bundler pulls each demo's Docs content
from README.md at the demo folder root.

This adds READMEs for the 24 manifest-listed demos that were missing
one, following the 3-section template used by the 6 pre-existing
READMEs (What This Demo Shows / How to Interact / Technical Details).
An orphan shared-state-read/README.md whose route doesn't match any
manifest demo is deleted.

Also extends the check-binaries pre-commit hook allowlist to cover
the shell-docs/shell-dojo copies of demo-content.json. The bundler
emits the same content to all three shells, but only the shell path
was previously allowlisted, so any bundle regen that grew past 1MB
got blocked.

As an incidental side effect of running the bundler, demo-content.json
also drops stale entries for langgraph-python::open-gen-ui and
langgraph-python::open-gen-ui-advanced — those folders exist in source
but are not listed in manifest.yaml.
2026-04-23 14:32:07 +02:00
github-actions[bot] 630f6743d9 style: auto-fix formatting 2026-04-23 12:11:52 +00:00
Alem Tuzlak 968f2fe378 fix(showcase-shell): separate CLI init command from live demos
The cli-start entry in each integration's demos[] is a copy-paste CLI
command, not a runnable demo, but the profile page rendered it as a
Live Demo tile whose drawer iframe loaded ${backend_url}undefined.

Split demos into liveDemos (runnable) and commandDemos (command-only)
and render commandDemos in a new "Get Started" section above the
Live Demos grid, mirroring how the dashboard already handles them.
2026-04-23 14:08:54 +02:00
Jordan Ritter 5a6718be93 docs(showcase-aimock): Railway service reconstruction reference (#4190)
## Summary
- Adds `showcase/aimock/RAILWAY.md` — reconstruction reference for the
Railway service
- Documents image, startCommand flags, env vars, deploy recipe, and the
legacy Dockerfile cleanup path
- Supports Phase 2 of the aimock wrapper-elimination work

## Why
Railway service config (image + startCommand + env) previously lived
only in Railway's state. If Railway loses it, the Notion plan was the
only reconstruction source. This persists an authoritative copy in-repo
so any reviewer can rebuild the service from scratch.

Values (project ID, environment ID, public domain) were queried live
from Railway GraphQL at doc-writing time.

## Test plan
- Docs-only, no runtime impact
- Verify every referenced URL renders on GitHub after merge
- Commitlint + CI green
2026-04-22 20:06:46 -07:00
Jordan Ritter 123ae66b87 docs(showcase-aimock): add Railway service reconstruction reference 2026-04-22 20:02:15 -07:00
github-actions[bot] 60841026dc style: auto-fix formatting 2026-04-23 02:26:06 +00:00
Sam Julien b33e88332a Merge branch 'main' into docs-sync/needs-review/189c45fb4-1776791837 2026-04-22 19:24:47 -07:00
devops-bot[bot] 4ae9af088e Update preview URLs in registry 2026-04-23 02:12:08 +00:00
Sam Julien 4873761c2e Merge branch 'main' into docs-sync/needs-review/189c45fb4-1776791837 2026-04-22 19:04:59 -07:00
Jordan Ritter 8d3eba5104 fix(showcase-ops): honor excludes in probe discovery
The railway-services DiscoverySource wrapped its filter schema in an
outer {filter: FilterSchema} ConfigSchema, but the probe-invoker calls
source.enumerate(ctx, cfg.discovery.filter ?? {}) — it passes the filter
contents DIRECTLY, not a wrapped object. At runtime cfg.filter was
always undefined, so namePrefix AND nameExcludes silently defaulted to
undefined and ZERO filtering happened.

Impact: all 7 infra services declared in smoke.yml's nameExcludes
(showcase-ops, showcase-pocketbase, showcase-shell,
showcase-shell-dashboard, showcase-shell-docs, showcase-shell-dojo,
showcase-aimock) produced smoke:/health:/agent: ProbeResults on every
15-minute tick. PocketBase carried ~21 false-red rows (7 services × 3
dimensions) across smoke:*, health:*, agent:*; after this fix the
discovery source filters them out and only the 34 user-facing
package+starter services emit probe rows (102 legitimate rows).

Fix: drop the outer wrapper. ConfigSchema IS the filter block — parse
rawConfig against it directly. Matches the documented enumerate()
contract in probes/types.ts ("config is discovery.filter") and the
shape pnpm-packages has used all along.

Tests updated: the prior discovery tests passed a {filter: {...}}
wrapped argument that only parsed because passthrough() swallowed the
misshape; they now pass the flat filter object the invoker actually
hands the source. Added one regression test that enumerates two real
showcase services plus all 7 infra services and asserts the seven are
dropped before the per-service env fetch (saves seven variables
round-trips per tick as a side benefit).
2026-04-22 18:54:57 -07:00
Jordan Ritter c32831def0 fix(showcase-ops): correct Railway GQL query for service discovery (#4189)
## Summary

The `railway-services` DiscoverySource in `showcase/ops` passed
`environmentId` as an argument to `Service.serviceInstances` in its
project-level GraphQL query. Railway's current schema does not accept
that argument — every discovery tick 400'd with:

```
railway gql 400: Unknown argument "environmentId" on field "Service.serviceInstances".
```

Result: no probes ticked, PocketBase stopped receiving fresh rows, alert
pipeline starved of data.

Fix: drop the invalid field argument. The returned
`serviceInstances.edges` are already filtered by `environmentId`
client-side a few lines down (`svc.serviceInstances.edges.find((e) =>
e.node.environmentId === environmentId)`), so there is no semantic
change — the argument was dead weight that tripped GraphQL validation.

## Why

- Blocks `railway-services` from enumerating anything, which blocks
every downstream probe that fans out across Railway services
(image-drift, smoke, e2e-smoke, redirect-decom).
- Unblocks the fresh-redeploy of showcase-ops that carries the #4180
merge — that PR's fix is in the running image but never reachable
because no probes tick.

## Verification

Introspection confirmed the current shape of `Service.serviceInstances`:

- `Query { project(id: String!) }` exists and returns `Project`
- `Project.services` paginates to `ServiceEdge { node: Service }`
- `Service.serviceInstances` accepts no field arguments
- `ServiceInstance.environmentId` is present, so client-side filtering
is correct

Direct `curl` against `backboard.railway.com/graphql/v2` with the fixed
query returned 41 services for the CopilotKit showcase project, matching
the expected ~35 (17 starters + 17 packages + shell + aimock + a few
infra services).

## Test plan

New test: `omits environmentId arg from Service.serviceInstances
selection to match current Railway schema`

- Mocks a Railway endpoint that rejects any query whose
`serviceInstances` call includes parenthesised arguments with the
production 400 error body, and accepts the arg-less form.
- Red on pre-fix code: `DiscoverySourceBackendError: railway gql 400:
{"errors":[{"message":"Unknown argument \"environmentId\" on field
\"Service.serviceInstances\".",...}]}`
- Green after fix; full suite 743/743.

- [x] `pnpm --filter @copilotkit/showcase-ops typecheck`
- [x] `pnpm --filter @copilotkit/showcase-ops test` (743 passed, was 742
+ 1 new regression test)
- [x] `pnpm format`
- [x] `oxlint` clean on touched files (one pre-existing unused-import
warning left alone — not in this diff)
2026-04-22 17:50:42 -07:00
Jordan Ritter 404fcd3da6 fix(showcase-ops): correct Railway GQL query for service discovery
The `railway-services` DiscoverySource was passing
`environmentId` as an argument to `Service.serviceInstances`,
but Railway's current GraphQL schema doesn't accept it. Every
discovery tick 400'd with `Unknown argument "environmentId"
on field "Service.serviceInstances"` and no probes ran.

Drop the field argument; the returned instances are filtered
by `environmentId` client-side in the subsequent loop (existing
behaviour), so no semantic change — the bad arg was dead
weight that tripped validation.

Add a regression test that scripts Railway's exact 400 when
the old query shape is sent and asserts the source succeeds
against a schema-accurate mock. Verified against the live
Railway schema via introspection and a direct curl.
2026-04-22 17:45:10 -07:00
Jordan Ritter 2653bd5342 fix: revert flushedAt to null when async aggregation flush resolves suppressed
The Promise branch of applyFlushResult set flushedAt optimistically
before the promise resolved, and the .then handler never reverted it
when the flush was suppressed. Since onAggregationFlush is async, this
broke the A5 "keep bucket live for re-threshold" contract in production.
The sync branch test did not exercise the Promise path. Added
bucket_stays_live_when_flush_suppressed_async to lock the invariant.
2026-04-22 17:35:19 -07:00
Jordan Ritter fd1179dcf7 chore: drop double-sanitize on smoke driver errorDesc
probeOne and probeAgent catch-blocks both assigned
`errorDesc: sanitizeErrorDesc(errorDesc)` where the local `errorDesc`
had already been sanitized on the line above. Double-sanitize is
harmless today because sanitizeErrorDesc is idempotent on the token
set we emit, but it would turn into a silent escape-on-escape
regression the moment the sanitizer grew a non-idempotent rule
(e.g. entity-encoding `&` → `&amp;`). Drop the outer wrap in both
sites.
2026-04-22 17:35:19 -07:00
Jordan Ritter bd89c29577 feat: validate aggregation.template for triple-brace safety
validateTripleBrace previously scanned only rule.template.text and
rule.on_error.template.text. A rule with a malformed
`{{{firstSignal.unsafe}}}` reference in aggregation.template passed
load validation and rendered the unsafe value at runtime via
Mustache.render inside onAggregationFlush — same Slack mrkdwn-injection
surface R21-a closed for on_error.template.

Pull aggregation.template into the same scan:
- firstSignal.* and lastSignal.* are aliases for signal.* in the
  aggregation render context; normalize both prefixes to `signal.`
  before applying the existing per-dimension slackSafe allowlist.
- `count` and `services` are engine-injected scalars (number / string)
  and are always loader-known-safe — allow their bare form for symmetry
  with the existing `{{count}}` usage in fleet rules.

Tests: rule-loader.test.ts adds
"rejects aggregation.template with triple-brace on an unsafe signal.*
field" under the new "validateTripleBrace covers aggregation.template"
describe.
2026-04-22 17:35:19 -07:00
Jordan Ritter e274e26745 fix: guard Run link on missing runId, route smoke endpoint link to signal.url
A2 — Run link guard
Every L1–L4 red-tick alert used to render the dashboard Run link
unconditionally, so a tick with no runId (invariant probes, cron ticks
without a propagated workflow run) emitted a broken
`<https://dashboard/.../runs/|Run>` in every alert. Wrap the token in
`{{#event.runId}}…{{/event.runId}}` across all four red-tick YAMLs
(smoke/agent/chat/tools) so the entire "· <…|Run>" chunk disappears when
runId is absent. The Showcase dashboard link stays unguarded (always
valid).

A3 — smoke endpoint link from signal.url
The smoke-red-tick template referenced `{{{signal.links.smoke}}}` and
`{{{signal.links.health}}}` section-guarded on those fields. The driver-
emitted SmokeDriverSignal (probes/drivers/smoke.ts) has no `links`
object — it exposes `url` instead, the URL that was actually probed. So
the guard sections were always empty and the link never rendered. Swap
the template to reference `{{{signal.url}}}` directly and emit a single
"endpoint" link when present. The other three red-tick YAMLs never
referenced `signal.links`; leave them untouched.

SMOKE_SLACK_SAFE_FIELDS updated: drop `links.smoke` / `links.health`,
add `url`. errorDesc stays safe.

Tests:
- render-red-tick.test.ts: parametrize across all four YAMLs, assert
  renders_no_run_link_when_runId_missing (output contains no
  `/runs/|` or `/runs/ |`) and renders_run_link_when_runId_present
  (output contains `/runs/run-abc|Run`). Add
  renders_endpoint_link_from_signal_url to cover the smoke endpoint
  link swap directly.
- rule-loader.test.ts: update the smoke-red-tick full-YAML tests to
  seed `signal.url` and assert `<…|endpoint>` instead of seeding
  signal.links.
2026-04-22 17:35:18 -07:00
Jordan Ritter bbfb2bd27c fix: aggregation store correctness — ingress gate, async drain, suppressed flush, empty groupBy, NUL key separator
Alert engine (handleStatusChanged):
- A1: call resolveTriggers on aggregated-rule ingress and skip when the
  triggered set is purely red_to_green. Prevents recovery events from
  ingesting into the fleet bucket and firing <!channel> on recoveries.
  error transitions also skip ingest — aggregated rules don't support
  onError today.
- B3: dispatchCronAlert returns early when rule.aggregation is set. A
  rule ever declaring both cron triggers and aggregation would otherwise
  render the empty top-level template or bypass aggregation entirely.

Alert engine (onAggregationFlush + drain wiring):
- A5: return "suppressed" when the bootstrap / rate-limit gate short-
  circuits dispatch, so the store keeps the bucket live. Pre-fix a
  bootstrap-suppressed threshold flush was silently lost — the bucket's
  flushedAt was marked and post-bootstrap ticks couldn't re-trigger.
- A4: drain() is now async. Register a SIGTERM handler that awaits
  drain() so in-flight buckets actually dispatch before process exit;
  keep beforeExit + stop() as best-effort fire-and-forget (Node
  beforeExit cannot truly await). Remove both listeners in stop() for
  test-cycle symmetry.

AggregationBucketStore (aggregation.ts):
- drain() returns Promise<void> and awaits each onFlush invocation via
  Promise.allSettled.
- OnFlushCallback returns void | "suppressed" | Promise<…>. When the
  callback returns "suppressed", flushedAt stays null so the bucket stays
  live for re-ingestion after the engine gate reopens.
- A7: groupBy is optional; absent/empty collapses the bucket key to
  rule.id alone. Schema updated so AggregationSchema.groupBy is
  z.array(string).optional() (pre-fix .min(1) forced a decorative
  [dimension] that always resolved to one bucket anyway).
- B4: intra-key field separator is NUL instead of "&" so a groupBy
  value containing "&" + "=" cannot collide with a distinct grouping.
  buildBucketKey and buildCompositeDedupeKey both match.

Schema (rules/schema.ts):
- B1: remove unused aggregation.targets field. The engine has always
  used rule.targets for aggregation dispatch; the schema field was
  silently dropped.

Tests (alert-engine.test.ts):
- A1 engine-level: red_to_green on an aggregated rule does NOT dispatch,
  green_to_red DOES dispatch (minMatches: 1, empty groupBy).
- A4: drain_awaits_onflush_promises — async onFlush resolves before
  drain's promise resolves.
- A5: bucket_stays_live_when_flush_suppressed — onFlush returning
  "suppressed" keeps the bucket live; a follow-up ingestion re-fires.
- A6: fix TDZ in bootstrap_suppression_respected — declare rule before
  the onFlush closure that captures it.
- A7: no_groupBy_uses_ruleid_only_as_bucket_scope — three signals
  across distinct dimensions all collapse into one rule-id bucket when
  groupBy is empty.
- B4: bucket_key_collision_safe_with_special_chars — concrete collision
  {b:"y&c=z"} vs {b:"y",c:"z"} is rejected by the NUL separator.
2026-04-22 17:35:18 -07:00
Jordan Ritter 407a0b9b93 fix: skip red_to_green in aggregation ingress, remove from fleet trigger lists
Fleet aggregation rules previously listed red_to_green alongside green_to_red
and sustained_red, which meant any recovery event ingested into the fleet
bucket and could fire a <!channel> aggregation alert on recovery. Remove
red_to_green from all four fleet YAMLs (smoke/agent/chat/tools) — the engine
ingress gate that actually skips recovery-only matches is added in the
subsequent commit. While here, drop the decorative groupBy: [dimension] on
every fleet rule: signal.dimension already partitions traffic to one
dimension per rule, so the groupBy expanded to undefined → "" at runtime
and silently collapsed to a single bucket per rule. The new schema treats
absent/empty groupBy as an explicit "single bucket per rule" contract.
2026-04-22 17:35:18 -07:00
Jordan Ritter a97497aba5 style: apply oxfmt to alert-engine.test.ts aggregation block 2026-04-22 17:35:18 -07:00
Jordan Ritter cc9edb2f72 feat: fleet-level red alert rules for smoke, agent, chat, tools
Four aggregated rules using groupBy=[dimension], windowMs=120s
(one 90s probe tick plus slack), minMatches=3. Each carries the
<!channel> escalation that moved off the per-service red-tick rules
so channel gets paged exactly once for a fleet-wide outage instead
of N times. rate_limit.window=10m prevents re-escalation across
consecutive aggregation windows.
2026-04-22 17:35:18 -07:00
Jordan Ritter 27f2362180 feat: wire aggregation ingress into alert-engine, expand safe-fields
Aggregated rules bypass per-match dispatch and ingress-side gates
(rate_limit, dedupe, bootstrap); the composite FLUSH callback still
honors bootstrap suppression and rate_limit against the composite
dedupe key so successive window expiries for the same groupValues
collapse to one dispatch.

Add errorDesc to SMOKE_SLACK_SAFE_FIELDS and introduce an inline
L1-L4 safe-field set (errorDesc only) for the agent/chat/tools
dimensions — they don't have dedicated probe modules, so the allow-
list is defined at the orchestrator's dimension-registration site.
rule-loader.test.ts loadRealRules() mirrors the orchestrator wiring
so the full-config-dir test path still validates successfully.

Migrate the smoke-red-tick "escalated block renders !channel"
assertion onto the new fleet rule — <!channel> now lives on
smoke-red-fleet's aggregation template, not on the per-service
red-tick template.
2026-04-22 17:35:18 -07:00
Jordan Ritter 98e73fa314 feat: cross-service alert aggregation store and bucket-dedupe key
AggregationBucketStore collects matching signals keyed by rule-defined
groupBy, fires onFlush once the bucket reaches minMatches (threshold),
or at windowMs expiry when below threshold (timer); drains outstanding
buckets on beforeExit. buildCompositeDedupeKey stabilizes rate-limit
across consecutive windows with the same groupValues.

CompiledRule gains optional `aggregation` field; schema.ts adds a
strict AggregationSchema so a typoed aggregation key fails load rather
than silently arming a bucket with undefined.
2026-04-22 17:35:18 -07:00
Jordan Ritter 9ffae55f51 test: red aggregation tests for fleet alerts
Eight cases covering threshold flush, window-expiry flush, per-match
suppression during aggregated windows, independent groupBy buckets,
composite dedupe via buildCompositeDedupeKey, bootstrap suppression,
and TTL eviction. Expected RED until Agent 5 creates aggregation.ts.
2026-04-22 17:35:17 -07:00
Jordan Ritter d1b93914be fix: sanitize errorDesc before Slack render
Strip HTML tags, script/style bodies (including entity-encoded), Slack
mrkdwn controls (<!channel>, <!here>, <@U…>), backticks. Decode entities
first so entity-smuggled script payloads are caught. Cap at 120 chars
with U+2026 ellipsis. Apply at 8 call sites in smoke.ts.
2026-04-22 17:35:17 -07:00
Jordan Ritter 7954d7d63a fix: guard optional links and un-escape sanitized errorDesc in red-tick alerts
Wrap each {{signal.links.<key>}} reference in a Mustache section guard
so absent links emit nothing. Switch {{signal.errorDesc}} to triple-brace
{{{signal.errorDesc}}}; the value is pre-sanitized in smoke.ts's
sanitizeErrorDesc. Drop <!channel> from per-service rules — fleet rules
(introduced in a following commit) carry it instead.

No Mustache partials are wired in the renderer (render/renderer.ts uses
Mustache.render directly with no `partials:` arg), so the guarded-link
pattern stays inline across four YAMLs rather than being extracted.
2026-04-22 17:34:42 -07:00
Jordan Ritter 7caa469741 test: red assertions for conditional link rendering and errorDesc escaping
Cover all four red-tick alert YAMLs. Missing links should emit nothing
instead of empty <|label> brackets. Sanitized errorDesc should render
literal characters, not HTML-entity-escaped.
2026-04-22 17:34:42 -07:00
Jordan Ritter 058903c1c9 fix(showcase-ops): probe contract CR R4 — regex, invariants, regression tests
- Widen classifyShape package regex to accept hyphen-bearing multi-segment
  names (showcase-langgraph-python, showcase-claude-sdk-typescript,
  showcase-ms-agent-dotnet, showcase-crewai-crews) without firing the
  name-shape-unknown warn on every tick.
- Broaden classifyShape warn trigger: any name that matches neither the
  starter nor widened-package pattern now warns, including non-showcase-
  prefixed names (my-random-service, copilotkit-cloud) and mixed-case
  variants. Returns package as a safe default; warn is the audit trail.
- Drop NormalisedSmokeInput discovery-arm index signature so input.url in
  a mode==="discovery" branch is a TS error rather than silently unknown.
- Drop the normaliseMode early-return arm that trusted raw.mode verbatim.
  Field presence is the sole discriminator, keeping schema-parsed and
  raw-input paths consistent. Bad-shape inputs now throw loud.
- Restore parse-time invariants on smokeInputSchema via a union-level
  superRefine that rejects bare {key}, discovery-missing-publicUrl, and
  mixed-mode {key,url,name,publicUrl} payloads that the discovery arm's
  passthrough would otherwise absorb.
- Add regression tests for the primary-key rewrite in discovery mode
  (smoke:showcase-ag2 -> smoke:ag2, smoke:showcase-starter-ag2 ->
  smoke:starter-ag2) and verbatim pass-through in static mode.
- Add end-to-end package-shape test coverage for deriveSlug feeding
  demosResolver (showcase-langgraph-python -> langgraph-python registry
  lookup) and the throwing-resolver swallow behaviour as a regression
  guard against the deferred fail-loud change.
2026-04-22 16:57:22 -07:00
Jordan Ritter 89ba0a88fc fix(showcase-ops): probe contract CR R2 — classifier + signal + discriminated inputs
Applies Round 2 code review findings to the starter-probe contract:

Critical:
- classifyShape now warns on every showcase-* name that fails both the
  strict starter pattern and the single-segment package pattern; typos
  (showcase-strater-*) and hyphen-bearing multi-segment names surface
  audit warns instead of silently falling to package.
- C2 (add "skipped" state to the aggregate ProbeResult) deferred: the
  aggregate state lives on the shared ProbeState type used by the
  transition-detector, alert-engine, and status-writer; extending it to
  a new literal ripples across the whole state machine and six files
  outside scope, and would materially change alert semantics. Flagged
  as follow-up.

Major:
- resolveShape consolidated into discovery/railway-services.ts; both
  smoke and e2e-smoke drivers import it and thread ctx.logger so the
  classifier's audit warn fires on the driver path.
- Silent fallback path (neither name nor shape supplied) now emits a
  structured debug log (resolve-shape-fallback) so the assumption is
  greppable if it breaks.
- E2eSmokeStarterSignal gains errorDesc?: never, turning the "no failure
  reason on a skipped-green row" invariant into a compile error.
- smokeInputSchema refactored to a zod union over static + discovery
  branches; runtime normaliseMode() maps legacy inputs onto a
  NormalisedSmokeInput with a required mode discriminator, replacing
  the non-null assertion on input.url and the ad-hoc refine()s.
- Tests and driver comments stripped of CR-process tags (C1:, R1
  bundle, etc.); test-side two-GETs rationale shrunk to a one-line
  pointer so the authoritative copy lives in the driver.
- Added classifyShape + resolveShape audit tests: warn fires on typo
  names, not on well-formed roots or starters, and debug fires on the
  name/shape-absent fallback.

Minor:
- Warn event key namespaced to discovery.railway-services.name-shape-unknown.
- deriveSlug JSDoc moved to sit directly above deriveSlug in e2e-smoke.
- SHOWCASE_SHAPES inlined into showcaseShapeSchema (no external consumers).
- Starter signal carries a structured skipReason: "starter-shape" enum
  alongside the human-readable note.
- Removed dead "unreachable" discriminator asserts in the starter
  contract test.
2026-04-22 16:36:22 -07:00
Jordan Ritter 8612342b8e fix(showcase-ops): harden probe contract — CR R1 bundle
Critical
- smoke + e2e-smoke drivers resolve `shape` via the classifier when
  `input.name` is present and throw on explicit-vs-classifier
  disagreement. Silent-defaulting at the driver boundary inverted the
  whole starter-shape fix; the driver now fails loud on wiring drift.

Majors
- E2eSmokeSignal is a discriminated union keyed on `shape`. Starter
  variant carries `note` (never rendered as failure) instead of
  `errorDesc`, so alert templates stop treating a green short-circuit
  as a failure reason.
- Starter shape runs TWO independent GETs against `/api/health` (smoke
  and health feed separate alert dimensions that dashboards correlate;
  a single-GET reuse would make the rows byte-identical and defeat the
  signal).
- smoke.test.ts uses a real `node:http` server to exercise genuine 308
  redirects for the agent POST; prior tests accepted a synthetic 404
  response and would have let a `redirect: "follow"` regression pass.
- New tests: starter 404 on /api/health, single-source shape enum
  assertion on discovery+package and discovery+starter redirect option,
  mixed-driver wiring via `.passthrough()` spread, shape-mismatch
  throws on both drivers.
- e2e-smoke: starter short-circuit ordering locked in via a throwing
  demos resolver; full starter signal contract asserted field-by-field.

Minors
- Single-source the shape enum: `SHOWCASE_SHAPES`, `ShowcaseServiceShape`,
  `showcaseShapeSchema` exported from railway-services; both driver
  schemas and `deriveUrls` now import the shared types.
- `classifyShape` emits a one-shot warning on umbrella-match-but-unusual
  names (`showcase-`, `showcase-starter`) when a logger is supplied.
- Static-mode rejects `url` + `shape` combo via zod refine — shape
  applies only to discovered `publicUrl`.
- Drop rot-prone counts (17, 34, 51), "CR A1", "Procedure 0" and
  "previously"/"old contract" phrasing. Trim WHAT comments that restate
  code; keep WHY that explains contract decisions.
2026-04-22 16:13:45 -07:00