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
15 changes: 15 additions & 0 deletions .changeset/generator-payload-alignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,18 @@ differing only in case, which are one file on a case-insensitive filesystem),
and one path being an ancestor of another. The whole set is assessed as a
unit, so a refused delivery leaves no directory behind rather than a
half-written rule that verifies as broken two steps from the cause.

The generated API types now carry the delivery union, and the client narrows
on it.

`rules` is published as `SingleContent | Sg | Vale | Runtime` rather than one
shape with optional fields, so a runtime file set states `signature` as
required. Reading `content` or `tests` off a rule no longer type-checks
without asking which variant arrived, which is the property doing its job:
the client cannot treat an unsigned runtime rule as deliverable.

It also closes a case that reached the filesystem. A payload carrying neither
`files` nor `content` fell through to the single-content branch and handed
`yaml.stringify` an `undefined`, which returns the string `"undefined"`
rather than throwing. The rule file was created and its contents were that
word. It is now refused before the directory exists.
35 changes: 35 additions & 0 deletions packages/cli/src/api/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,41 @@ export type GeneratedRule = NonNullable<RuleStatusData["rules"]>[number];
/** Sidecar metadata keyed by rule filename */
export type RuleMetadata = NonNullable<RuleStatusData["meta"]>;

/**
* A rule delivered as a file set, discriminated on `engine`.
*
* The service now publishes `rules` as a union rather than one shape with
* optional fields, so `rule.content` no longer type-checks without asking
* which variant this is. That is the schema working: a runtime file set
* REQUIRES a signature, and a type that let every field be read off any
* variant could not express that.
*/
export type DeliveredFileSetRule = Extract<GeneratedRule, { files: unknown }>;

/** A rule delivered as one `content` object, the pre-file-set envelope. */
export type SingleContentRule = Exclude<GeneratedRule, { files: unknown }>;

/**
* Whether this rule arrived as a file set.
*
* Keyed on `files` rather than on `engine`, because `engine` is what the
* file-set variants have in COMMON and `files` is what separates them from the
* single-content one. Narrowing on the wrong field reads as equivalent and
* silently admits a shape the branch cannot handle.
*/
export function isFileSetRule(
rule: GeneratedRule
): rule is DeliveredFileSetRule {
return (rule as { files?: unknown }).files !== undefined;
}

/** Whether this rule arrived as a single `content` object. */
export function isSingleContentRule(
rule: GeneratedRule
): rule is SingleContentRule {
return !isFileSetRule(rule);
}

// --- Helpers ---

/** Extract error details from an untyped error response body */
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/commands/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { defineCommand } from "citty";
import { ZodError } from "zod";

import { identityFailureCode, resolveIdentity } from "../auth/identity";
import { submitRule, pollRuleStatus, iterateRule } from "../api/rules";
import {
submitRule,
pollRuleStatus,
iterateRule,
isSingleContentRule,
} from "../api/rules";
import {
writeRuleFile,
writeRuleTestFile,
Expand Down Expand Up @@ -238,7 +243,10 @@ const createCommand = defineCommand({
const ruleFile = await writeRuleFile(cwd, rule);
writtenFiles.push(ruleFile);

if (rule.tests) {
// A file set carries its fixtures as ordinary files under
// `.tests/`, already written by `writeRuleFile`. Only the
// single-content envelope has a separate `tests` to write.
if (isSingleContentRule(rule) && rule.tests) {
Comment thread
thecodedrift marked this conversation as resolved.
const testFile = await writeRuleTestFile(cwd, rule, timestamp);
writtenFiles.push(testFile);
}
Expand Down Expand Up @@ -475,7 +483,10 @@ const improveCommand = defineCommand({
const ruleFile = await writeRuleFile(cwd, rule);
writtenFiles.push(ruleFile);

if (rule.tests) {
// A file set carries its fixtures as ordinary files under
// `.tests/`, already written by `writeRuleFile`. Only the
// single-content envelope has a separate `tests` to write.
if (isSingleContentRule(rule) && rule.tests) {
const testFile = await writeRuleTestFile(cwd, rule, timestamp);
writtenFiles.push(testFile);
}
Expand Down
Loading
Loading