Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .claude/skills/canicode-roundtrip/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,10 @@ Instance-child guard and per-rule prompts — **[Appendix Strategy B](https://gi

##### Strategy B group componentize — Phase 3 (`missing-component:structure-repetition`)

When `applyStrategy === "structural-mod"` AND `question.ruleId === "missing-component"` AND `question.subType === "structure-repetition"` AND `question.groupMembers` is set, the question represents a fingerprint group of N FRAMEs the user can componentize-and-swap in one batch. The group spans both same-parent siblings and cross-parent matches found by the Stage 3 scope-wide pass (#557). Render the per-question prompt with the **group size** explicitly so the designer knows the scope before answering. Substitute `{nodeName}` with `question.nodeName` and `{others}` with `question.groupMembers.length - 1` (the count excluding the first member that becomes the new component); render in the user's session language:
When `applyStrategy === "structural-mod"` AND `question.ruleId === "missing-component"` AND `question.subType === "structure-repetition"` AND `question.groupMembers` is set, the question represents a fingerprint group of N FRAMEs the user can componentize-and-swap in one batch. The group spans both same-parent siblings and cross-parent matches found by the Stage 3 scope-wide pass (#557). Render the per-question prompt with the **group size** explicitly so the designer knows the scope before answering. Substitute `{nodeName}` with `question.nodeName`, `{others}` with `question.othersCount`, and `{total}` with `question.totalCount`; render in the user's session language:

- Korean: `> "{nodeName}" 외에 동일한 구조의 frame이 {others}개 더 있습니다 (총 {others + 1}개). 모두 컴포넌트화 할까요? (yes/no)`
- English: `> "{nodeName}" and {others} other frame(s) share the same structure ({others + 1} total). Componentize the whole group? (yes/no)`
- Korean: `> "{nodeName}" 외에 동일한 구조의 frame이 {others}개 더 있습니다 (총 {total}개). 모두 컴포넌트화 할까요? (yes/no)`
- English: `> "{nodeName}" and {others} other frame(s) share the same structure ({total} total). Componentize the whole group? (yes/no)`

On `yes`, compute the file-wide existing component name set once (decision C uses this for the suffix), then call the group orchestrator. On `no` / `skip`, drop the question without writing anything; the gotcha state is captured in the SKILL's section markdown either way.

Expand Down
5 changes: 5 additions & 0 deletions src/core/contracts/gotcha-survey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ export const GotchaSurveyQuestionSchema = z.object({
// = 2). A future rule emitting `[]` or `[oneId]` is a programming error,
// not a runtime case to handle gracefully.
groupMembers: z.array(z.string()).min(2).optional(),
// Pre-computed scalars derived from `groupMembers` (ADR-016: no arithmetic
// in SKILL.md prose). Only present when `groupMembers` is set.
// othersCount = groupMembers.length - 1 (≥1), totalCount = groupMembers.length (≥2).
othersCount: z.number().int().min(1).optional(),
totalCount: z.number().int().min(2).optional(),
});

export type GotchaSurveyQuestion = z.infer<typeof GotchaSurveyQuestionSchema>;
Expand Down
27 changes: 27 additions & 0 deletions src/core/gotcha/survey-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,11 +1528,36 @@ describe("generateGotchaSurvey", () => {
);
expect(survey.questions).toHaveLength(1);
expect(survey.questions[0]?.groupMembers).toEqual(["fA", "fB", "fC"]);
// ADR-016: pre-computed scalars derived from groupMembers
expect(survey.questions[0]?.othersCount).toBe(2); // 3 members → 2 others
expect(survey.questions[0]?.totalCount).toBe(3); // 3 members total
// Schema validation — `groupMembers: z.array(z.string()).optional()`
const parsed = GotchaSurveySchema.safeParse(survey);
expect(parsed.success).toBe(true);
});

it("computes othersCount/totalCount correctly for minimum 2-member group", () => {
const issues = [
makeIssue({
ruleId: "missing-component",
category: "code-quality",
severity: "risk",
nodeId: "fA",
nodePath: "Root > A",
subType: "structure-repetition",
groupMembers: ["fA", "fB"],
}),
];
const survey = generateGotchaSurvey(
makeResult(issues),
makeScoreReport("C"),
);
expect(survey.questions).toHaveLength(1);
expect(survey.questions[0]?.othersCount).toBe(1); // 2 members → 1 other
expect(survey.questions[0]?.totalCount).toBe(2); // 2 members total
expect(GotchaSurveySchema.safeParse(survey).success).toBe(true);
});

it("omits groupMembers when the violation does not carry one (non-group rules)", () => {
const issues = [
makeIssue({
Expand All @@ -1548,6 +1573,8 @@ describe("generateGotchaSurvey", () => {
);
expect(survey.questions).toHaveLength(1);
expect(survey.questions[0]?.groupMembers).toBeUndefined();
expect(survey.questions[0]?.othersCount).toBeUndefined();
expect(survey.questions[0]?.totalCount).toBeUndefined();
});
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/core/gotcha/survey-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,14 @@ function mapToQuestion(
// #560 / Phase 3 delta 4a: thread groupMembers through from the
// violation. Currently only populated by `missing-component`
// Stage 3; non-group rules pass undefined and the field is omitted.
// ADR-016: pre-compute othersCount / totalCount so SKILL.md prose needs
// no arithmetic.
...(issue.violation.groupMembers !== undefined
? { groupMembers: issue.violation.groupMembers }
? {
groupMembers: issue.violation.groupMembers,
othersCount: issue.violation.groupMembers.length - 1,
totalCount: issue.violation.groupMembers.length,
}
: {}),
};
}
Expand Down
Loading