cclaw-cli 0.48.30 → 0.48.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -42,7 +42,7 @@ export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
42
42
  * the user set them explicitly. Keeps the default template small and honest:
43
43
  * only knobs a new user would meaningfully flip show up.
44
44
  */
45
- type AdvancedConfigKey = "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws";
45
+ type AdvancedConfigKey = "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws" | "optInAudits" | "reviewLoop";
46
46
  /**
47
47
  * Options controlling the serialisation shape of `config.yaml`.
48
48
  *
package/dist/config.js CHANGED
@@ -25,7 +25,9 @@ const ALLOWED_CONFIG_KEYS = new Set([
25
25
  "languageRulePacks",
26
26
  "trackHeuristics",
27
27
  "sliceReview",
28
- "ironLaws"
28
+ "ironLaws",
29
+ "optInAudits",
30
+ "reviewLoop"
29
31
  ]);
30
32
  /**
31
33
  * Config keys removed in the advisory-by-default consolidation. Kept here so
@@ -429,6 +431,76 @@ export async function readConfig(projectRoot) {
429
431
  else {
430
432
  ironLaws = { strictLaws: [] };
431
433
  }
434
+ const optInAuditsRaw = parsed.optInAudits;
435
+ let optInAudits = undefined;
436
+ if (Object.prototype.hasOwnProperty.call(parsed, "optInAudits")) {
437
+ if (!isRecord(optInAuditsRaw)) {
438
+ throw configValidationError(fullPath, `"optInAudits" must be an object`);
439
+ }
440
+ const unknownOptInAuditKeys = Object.keys(optInAuditsRaw).filter((key) => key !== "scopePreAudit" && key !== "staleDiagramAudit");
441
+ if (unknownOptInAuditKeys.length > 0) {
442
+ throw configValidationError(fullPath, `"optInAudits" has unknown key(s): ${unknownOptInAuditKeys.join(", ")}`);
443
+ }
444
+ if (optInAuditsRaw.scopePreAudit !== undefined &&
445
+ typeof optInAuditsRaw.scopePreAudit !== "boolean") {
446
+ throw configValidationError(fullPath, `"optInAudits.scopePreAudit" must be a boolean`);
447
+ }
448
+ if (optInAuditsRaw.staleDiagramAudit !== undefined &&
449
+ typeof optInAuditsRaw.staleDiagramAudit !== "boolean") {
450
+ throw configValidationError(fullPath, `"optInAudits.staleDiagramAudit" must be a boolean`);
451
+ }
452
+ optInAudits = {
453
+ scopePreAudit: typeof optInAuditsRaw.scopePreAudit === "boolean"
454
+ ? optInAuditsRaw.scopePreAudit
455
+ : false,
456
+ staleDiagramAudit: typeof optInAuditsRaw.staleDiagramAudit === "boolean"
457
+ ? optInAuditsRaw.staleDiagramAudit
458
+ : false
459
+ };
460
+ }
461
+ const reviewLoopRaw = parsed.reviewLoop;
462
+ let reviewLoop = undefined;
463
+ if (Object.prototype.hasOwnProperty.call(parsed, "reviewLoop")) {
464
+ if (!isRecord(reviewLoopRaw)) {
465
+ throw configValidationError(fullPath, `"reviewLoop" must be an object`);
466
+ }
467
+ const unknownReviewLoopKeys = Object.keys(reviewLoopRaw).filter((key) => key !== "externalSecondOpinion");
468
+ if (unknownReviewLoopKeys.length > 0) {
469
+ throw configValidationError(fullPath, `"reviewLoop" has unknown key(s): ${unknownReviewLoopKeys.join(", ")}`);
470
+ }
471
+ const externalRaw = reviewLoopRaw.externalSecondOpinion;
472
+ let externalSecondOpinion = undefined;
473
+ if (externalRaw !== undefined) {
474
+ if (!isRecord(externalRaw)) {
475
+ throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" must be an object`);
476
+ }
477
+ const unknownExternalKeys = Object.keys(externalRaw).filter((key) => key !== "enabled" && key !== "model" && key !== "scoreDeltaThreshold");
478
+ if (unknownExternalKeys.length > 0) {
479
+ throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" has unknown key(s): ${unknownExternalKeys.join(", ")}`);
480
+ }
481
+ if (externalRaw.enabled !== undefined && typeof externalRaw.enabled !== "boolean") {
482
+ throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.enabled" must be a boolean`);
483
+ }
484
+ if (externalRaw.model !== undefined && typeof externalRaw.model !== "string") {
485
+ throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.model" must be a string`);
486
+ }
487
+ if (externalRaw.scoreDeltaThreshold !== undefined &&
488
+ (typeof externalRaw.scoreDeltaThreshold !== "number" ||
489
+ Number.isNaN(externalRaw.scoreDeltaThreshold) ||
490
+ externalRaw.scoreDeltaThreshold < 0 ||
491
+ externalRaw.scoreDeltaThreshold > 1)) {
492
+ throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.scoreDeltaThreshold" must be a number between 0 and 1`);
493
+ }
494
+ externalSecondOpinion = {
495
+ enabled: externalRaw.enabled === true,
496
+ model: typeof externalRaw.model === "string" ? externalRaw.model : undefined,
497
+ scoreDeltaThreshold: typeof externalRaw.scoreDeltaThreshold === "number"
498
+ ? externalRaw.scoreDeltaThreshold
499
+ : 0.2
500
+ };
501
+ }
502
+ reviewLoop = { externalSecondOpinion };
503
+ }
432
504
  return {
433
505
  version: parsed.version ?? CCLAW_VERSION,
434
506
  flowVersion: parsed.flowVersion ?? FLOW_VERSION,
@@ -447,7 +519,9 @@ export async function readConfig(projectRoot) {
447
519
  languageRulePacks,
448
520
  trackHeuristics,
449
521
  sliceReview,
450
- ironLaws
522
+ ironLaws,
523
+ optInAudits,
524
+ reviewLoop
451
525
  };
452
526
  }
453
527
  function isMinimalKey(key) {
@@ -470,7 +544,9 @@ function buildSerializableConfig(config, options = {}) {
470
544
  "languageRulePacks",
471
545
  "trackHeuristics",
472
546
  "sliceReview",
473
- "ironLaws"
547
+ "ironLaws",
548
+ "optInAudits",
549
+ "reviewLoop"
474
550
  ];
475
551
  for (const key of ordered) {
476
552
  const value = config[key];
@@ -523,7 +599,9 @@ export async function detectAdvancedKeys(projectRoot) {
523
599
  "languageRulePacks",
524
600
  "trackHeuristics",
525
601
  "sliceReview",
526
- "ironLaws"
602
+ "ironLaws",
603
+ "optInAudits",
604
+ "reviewLoop"
527
605
  ];
528
606
  const present = new Set();
529
607
  for (const key of advancedCandidates) {
@@ -18,18 +18,35 @@ const STAGE_EXAMPLES = {
18
18
  | 2 | Should the validation logic live in a reusable module or stay as shell scripts? | Reusable module. | Architecture: shared TypeScript module imported by CI and local tooling, not duplicated shell scripts. |
19
19
  | 3 | For v1, prioritize rapid delivery or maximum configurability? | Rapid delivery. | Minimal deterministic validation surface; defer plugin/config system to v2. |
20
20
 
21
+ ## Approach Tier
22
+
23
+ - **Tier:** Standard
24
+ - **Why this tier:** Change spans CI + local release workflow and shared module boundaries, but remains bounded to one subsystem.
25
+
26
+ ## Short-Circuit Decision
27
+
28
+ - **Status:** bypassed
29
+ - **Why:** Core requirements were not concrete enough initially; we still needed options + trade-off conversation.
30
+ - **Scope handoff:** Continue full brainstorm flow before scope.
31
+
21
32
  ## Approaches
22
33
 
23
- | Approach | Architecture | Trade-offs | Recommendation |
24
- | --- | --- | --- | --- |
25
- | A: Reusable validation module | Shared TS module with typed validators, imported by CI scripts and local CLI. Existing \`pre-publish.sh\` calls the module. | Medium upfront effort, high reuse. Requires test coverage for the module. | **Recommended** — best balance of reuse and delivery speed. |
26
- | B: Hardened shell scripts | Keep existing script approach, add stricter checks and error messages. | Lowest effort. Weak reuse, CI/local divergence risk grows over time. | Viable fallback if TS module is blocked. |
27
- | C: Full release framework | New release orchestrator with plugin system, config files, rollback commands. | Maximum flexibility. High risk, delivery delay, over-engineered for current needs. | Not recommended for v1. |
34
+ | Approach | Role | Architecture | Trade-offs | Recommendation |
35
+ | --- | --- | --- | --- | --- |
36
+ | A: Reusable validation module | baseline | Shared TS module with typed validators, imported by CI scripts and local CLI. Existing \`pre-publish.sh\` calls the module. | Medium upfront effort, high reuse. Requires test coverage for the module. | **Recommended** — best balance of reuse and delivery speed. |
37
+ | B: Hardened shell scripts | fallback | Keep existing script approach, add stricter checks and error messages. | Lowest effort. Weak reuse, CI/local divergence risk grows over time. | Viable fallback if TS module is blocked. |
38
+ | C: Full release framework | challenger: higher-upside | New release orchestrator with plugin system, config files, rollback commands. | Maximum flexibility. High risk, delivery delay, over-engineered for current needs. | Not recommended for v1. |
39
+
40
+ ## Approach Reaction
41
+
42
+ - **Closest option:** A (reusable validation module).
43
+ - **Concerns:** User wanted to avoid framework-level overbuild and keep v1 delivery speed high.
44
+ - **What changed after reaction:** Recommendation stayed on A, but added explicit fallback path via existing shell entrypoint to reduce migration risk.
28
45
 
29
46
  ## Selected Direction
30
47
 
31
48
  - **Approach:** A — Reusable validation module
32
- - **Rationale:** shared TS module gives consistent behavior in CI and local, avoids script duplication, and stays within the no-new-dependency constraint.
49
+ - **Rationale:** based on user reaction favoring fast delivery and lower complexity, shared TS module gives consistent behavior in CI/local, avoids script duplication, and stays within the no-new-dependency constraint.
33
50
  - **Approval:** approved
34
51
 
35
52
  ## Design
@@ -1,2 +1,6 @@
1
- export declare function ideateCommandContract(): string;
2
- export declare function ideateCommandSkillMarkdown(): string;
1
+ import { type IdeateFrameId } from "./ideate-frames.js";
2
+ export interface IdeateCommandOptions {
3
+ frameIds?: readonly IdeateFrameId[];
4
+ }
5
+ export declare function ideateCommandContract(options?: IdeateCommandOptions): string;
6
+ export declare function ideateCommandSkillMarkdown(options?: IdeateCommandOptions): string;
@@ -1,4 +1,5 @@
1
1
  import { RUNTIME_ROOT } from "../constants.js";
2
+ import { resolveIdeateFrames } from "./ideate-frames.js";
2
3
  const IDEATE_SKILL_FOLDER = "flow-ideate";
3
4
  const IDEATE_SKILL_NAME = "flow-ideate";
4
5
  /**
@@ -18,7 +19,20 @@ const STRUCTURED_ASK_TOOLS = "`AskUserQuestion` on Claude, `AskQuestion` on Curs
18
19
  "`question` on OpenCode when `permission.question: \"allow\"` is set, " +
19
20
  "`request_user_input` on Codex in Plan / Collaboration mode; " +
20
21
  "fall back to a plain-text lettered list when the tool is hidden or errors";
21
- export function ideateCommandContract() {
22
+ function renderFrameBullets(frameIds) {
23
+ return resolveIdeateFrames(frameIds)
24
+ .map((frame) => ` - ${frame.label} (\`${frame.id}\`)`)
25
+ .join("\n");
26
+ }
27
+ function renderFrameNames(frameIds) {
28
+ return resolveIdeateFrames(frameIds)
29
+ .map((frame) => frame.label)
30
+ .join(", ");
31
+ }
32
+ export function ideateCommandContract(options = {}) {
33
+ const frames = resolveIdeateFrames(options.frameIds);
34
+ const frameBullets = renderFrameBullets(options.frameIds);
35
+ const minimumDistinctFrames = Math.min(4, frames.length);
22
36
  return `# /cc-ideate
23
37
 
24
38
  ## Purpose
@@ -51,17 +65,24 @@ same session, or save/discard the backlog.
51
65
  repetition scan.
52
66
  - Elsewhere-software: docs-first grounding (Context7 and official docs).
53
67
  - Elsewhere-non-software: constraints and objective grounding.
54
- 4. **Divergent ideation frames (parallel).** Generate candidates with at least
55
- 4 distinct frames: pain/friction, inversion, assumption-break, leverage,
56
- cross-domain analogy, constraint-flip.
68
+ 4. **Divergent ideation frames (parallel).** Generate candidates with
69
+ configured frames (${frames.length} total):
70
+ ${frameBullets}
71
+ Keep at least ${minimumDistinctFrames} distinct frame outputs in every run.
57
72
  5. **Adversarial critique pass.** For each candidate, write the strongest
58
73
  counter-argument, kill weak ideas, and keep survivors only.
59
74
  6. **Produce 5-10 survivors** with impact (High/Medium/Low),
60
75
  effort (S/M/L), confidence (High/Medium/Low), and one evidence path per
61
76
  survivor.
62
- 7. **Rank by impact/effort**, recommend the top survivor.
77
+ 7. **Rank by impact/effort/confidence** using
78
+ \`(impact points / effort cost) * confidence multiplier\` and recommend
79
+ the top survivor.
63
80
  8. **Write the artifact** at
64
81
  \`${IDEATE_ARTIFACT_PATTERN}\` using the schema in the skill.
82
+ 8.5 **Seed shelf (optional).** For critiqued-out or deferred ideas that still
83
+ show upside, write seed notes to
84
+ \`${RUNTIME_ROOT}/seeds/SEED-<YYYY-MM-DD>-<slug>.md\` with
85
+ \`trigger_when\`, hypothesis, and suggested action.
65
86
  9. **Present the handoff prompt** with four concrete options — not A/B/C
66
87
  letters. Default = "Start /cc on the top recommendation".
67
88
 
@@ -78,10 +99,14 @@ Validate envelopes with:
78
99
 
79
100
  ## Primary skill
80
101
 
81
- **${RUNTIME_ROOT}/skills/${IDEATE_SKILL_FOLDER}/SKILL.md**
102
+ **${RUNTIME_ROOT}/skills/${IDEATE_SKILL_FOLDER}/SKILL.md**
82
103
  `;
83
104
  }
84
- export function ideateCommandSkillMarkdown() {
105
+ export function ideateCommandSkillMarkdown(options = {}) {
106
+ const frames = resolveIdeateFrames(options.frameIds);
107
+ const frameBullets = renderFrameBullets(options.frameIds);
108
+ const minimumDistinctFrames = Math.min(4, frames.length);
109
+ const frameNames = renderFrameNames(options.frameIds);
85
110
  return `---
86
111
  name: ${IDEATE_SKILL_NAME}
87
112
  description: "Repository ideate mode: detect and rank high-leverage improvements, persist a backlog artifact, and hand off to /cc or save/discard."
@@ -150,14 +175,9 @@ Record each finding with exact evidence (path, command, or doc source).
150
175
 
151
176
  Generate candidate ideas by frame, in parallel when possible:
152
177
 
153
- - pain/friction
154
- - inversion
155
- - assumption-break
156
- - leverage
157
- - cross-domain analogy
158
- - constraint-flip
178
+ ${frameBullets}
159
179
 
160
- Require at least 4 distinct frames in every run. Avoid frame-collapse
180
+ Require at least ${minimumDistinctFrames} distinct frames in every run. Avoid frame-collapse
161
181
  (same idea rewritten 6 times). Keep raw outputs for auditability.
162
182
 
163
183
  ### Phase 3 — Critique all, keep survivors
@@ -182,7 +202,8 @@ Only survivors advance to ranking.
182
202
  - **Evidence** — path(s) or command output, inline if short
183
203
  - **Counter-argument** — strongest concern that survived
184
204
  - **Proposed handoff** — exact \`/cc <phrase>\`
185
- 3. Sort by impact/effort ratio; break ties with confidence.
205
+ 3. Sort by score \`(impact points / effort cost) * confidence multiplier\`
206
+ and break ties with rationale strength.
186
207
  4. Compute the artifact filename:
187
208
  - \`slug\` = first 3–5 words of the top recommendation, lowercase,
188
209
  non-alphanumeric collapsed to \`-\`, trimmed. When ideate mode is
@@ -231,7 +252,11 @@ Only survivors advance to ranking.
231
252
  ### I-2 — …
232
253
  \`\`\`
233
254
 
234
- 6. Confirm in chat: "Wrote <path>."
255
+ 6. Optional: for promising non-selected ideas, write
256
+ \`${RUNTIME_ROOT}/seeds/SEED-<YYYY-MM-DD>-<slug>.md\` entries with:
257
+ \`title\`, \`trigger_when\`, \`hypothesis\`, \`action\`, and
258
+ \`source_artifact\` = ideate artifact path.
259
+ 7. Confirm in chat: "Wrote <path>."
235
260
 
236
261
  ### Phase 5 — Handoff prompt
237
262
 
@@ -270,5 +295,7 @@ lettered list with the same four labels. Do not invent extra options.
270
295
  - Do not mutate \`.cclaw/state/flow-state.json\` at any phase.
271
296
  - Do not end the turn with an ungrounded "pick one" question — every
272
297
  option in the handoff prompt must reference a concrete command.
298
+ - Do not collapse all ideas into one frame; distribute across:
299
+ ${frameNames}.
273
300
  `;
274
301
  }
@@ -0,0 +1,31 @@
1
+ export type IdeateFrameId = "pain-friction" | "inversion" | "assumption-break" | "leverage" | "cross-domain-analogy" | "constraint-flip";
2
+ export interface IdeateFrame {
3
+ id: IdeateFrameId;
4
+ label: string;
5
+ prompt: string;
6
+ examplePatterns: string[];
7
+ }
8
+ export interface IdeateFrameDispatchInput {
9
+ focus: string;
10
+ mode: "repo-grounded" | "elsewhere-software" | "elsewhere-non-software";
11
+ signalSummary: string[];
12
+ }
13
+ export interface IdeateFrameDispatchPlanEntry {
14
+ frameId: IdeateFrameId;
15
+ label: string;
16
+ prompt: string;
17
+ }
18
+ export interface IdeateCandidateDraft {
19
+ title: string;
20
+ evidencePath: string;
21
+ summary: string;
22
+ frameId: IdeateFrameId;
23
+ }
24
+ export interface IdeateCandidateMerged extends Omit<IdeateCandidateDraft, "frameId"> {
25
+ frameIds: IdeateFrameId[];
26
+ }
27
+ export declare const DEFAULT_IDEATE_FRAME_IDS: readonly IdeateFrameId[];
28
+ export declare const IDEATE_FRAMES: readonly IdeateFrame[];
29
+ export declare function resolveIdeateFrames(frameIds?: readonly IdeateFrameId[]): IdeateFrame[];
30
+ export declare function buildIdeateFrameDispatchPlan(input: IdeateFrameDispatchInput, frameIds?: readonly IdeateFrameId[]): IdeateFrameDispatchPlanEntry[];
31
+ export declare function dedupeIdeateCandidates(drafts: readonly IdeateCandidateDraft[]): IdeateCandidateMerged[];
@@ -0,0 +1,140 @@
1
+ const FRAME_REGISTRY = {
2
+ "pain-friction": {
3
+ id: "pain-friction",
4
+ label: "pain/friction",
5
+ prompt: "Find repeated friction in the repo workflow. Prioritize changes that eliminate recurring toil, fragile handoffs, or repeated manual recovery.",
6
+ examplePatterns: [
7
+ "Repeated TODO/FIXME hotspots in one subsystem",
8
+ "Flows that require manual retries or ad-hoc scripts",
9
+ "Developer loops slowed by avoidable boilerplate"
10
+ ]
11
+ },
12
+ inversion: {
13
+ id: "inversion",
14
+ label: "inversion",
15
+ prompt: "Invert a dominant assumption in the current implementation (e.g., push vs pull, synchronous vs queued, implicit vs explicit) and evaluate upside.",
16
+ examplePatterns: [
17
+ "Replace optimistic assumptions with fail-closed defaults",
18
+ "Switch from post-facto checks to pre-flight validation",
19
+ "Move from global policy to module-local contracts"
20
+ ]
21
+ },
22
+ "assumption-break": {
23
+ id: "assumption-break",
24
+ label: "assumption-break",
25
+ prompt: "List assumptions that might be false in production. Generate ideas that remain correct even when those assumptions fail.",
26
+ examplePatterns: [
27
+ "Edge paths currently treated as impossible",
28
+ "Implicit coupling between modules with no explicit contract",
29
+ "Latency, scale, or environment assumptions baked into logic"
30
+ ]
31
+ },
32
+ leverage: {
33
+ id: "leverage",
34
+ label: "leverage",
35
+ prompt: "Target interventions with asymmetric payoff: one change that improves multiple stages, teams, or failure classes at once.",
36
+ examplePatterns: [
37
+ "A shared helper replacing duplicated logic in many files",
38
+ "One lint/gate that blocks an entire class of regressions",
39
+ "A protocol update that improves multiple stage outputs"
40
+ ]
41
+ },
42
+ "cross-domain-analogy": {
43
+ id: "cross-domain-analogy",
44
+ label: "cross-domain analogy",
45
+ prompt: "Borrow a proven pattern from another domain and adapt it to this repo. Keep it concrete and grounded in local constraints.",
46
+ examplePatterns: [
47
+ "Apply SRE-style error budgets to planning artifacts",
48
+ "Use security threat-model thinking for reliability design",
49
+ "Import CI release-train discipline into stage completion gates"
50
+ ]
51
+ },
52
+ "constraint-flip": {
53
+ id: "constraint-flip",
54
+ label: "constraint-flip",
55
+ prompt: "Flip one assumed constraint (time, team size, risk tolerance, compatibility) and derive better options under the new boundary.",
56
+ examplePatterns: [
57
+ "Assume near-zero migration window and redesign rollout",
58
+ "Assume one maintainer and optimize for low operational burden",
59
+ "Assume strict auditability and remove ambiguous behaviors"
60
+ ]
61
+ }
62
+ };
63
+ export const DEFAULT_IDEATE_FRAME_IDS = Object.freeze([
64
+ "pain-friction",
65
+ "inversion",
66
+ "assumption-break",
67
+ "leverage",
68
+ "cross-domain-analogy",
69
+ "constraint-flip"
70
+ ]);
71
+ export const IDEATE_FRAMES = Object.freeze(DEFAULT_IDEATE_FRAME_IDS.map((id) => FRAME_REGISTRY[id]));
72
+ export function resolveIdeateFrames(frameIds) {
73
+ if (!frameIds || frameIds.length === 0) {
74
+ return [...IDEATE_FRAMES];
75
+ }
76
+ const seen = new Set();
77
+ const resolved = [];
78
+ for (const rawId of frameIds) {
79
+ if (!DEFAULT_IDEATE_FRAME_IDS.includes(rawId)) {
80
+ throw new Error(`Unknown ideate frame id: ${rawId}`);
81
+ }
82
+ if (seen.has(rawId))
83
+ continue;
84
+ seen.add(rawId);
85
+ resolved.push(FRAME_REGISTRY[rawId]);
86
+ }
87
+ return resolved;
88
+ }
89
+ export function buildIdeateFrameDispatchPlan(input, frameIds) {
90
+ const signalBlock = input.signalSummary.length > 0
91
+ ? input.signalSummary.map((line) => `- ${line}`).join("\n")
92
+ : "- no pre-scan signals captured yet";
93
+ return resolveIdeateFrames(frameIds).map((frame) => ({
94
+ frameId: frame.id,
95
+ label: frame.label,
96
+ prompt: [
97
+ `Frame: ${frame.label} (${frame.id})`,
98
+ `Mode: ${input.mode}`,
99
+ `Focus: ${input.focus || "open-ended scan"}`,
100
+ "",
101
+ "Signal summary:",
102
+ signalBlock,
103
+ "",
104
+ `Frame prompt: ${frame.prompt}`,
105
+ "",
106
+ "Generate 3-5 concrete candidates with repo-grounded evidence."
107
+ ].join("\n")
108
+ }));
109
+ }
110
+ function normalizeCandidateKey(title, evidencePath) {
111
+ const normalizedTitle = title.trim().toLowerCase().replace(/[^a-z0-9]+/gu, " ").trim();
112
+ const normalizedEvidence = evidencePath
113
+ .trim()
114
+ .toLowerCase()
115
+ .replace(/\\/gu, "/");
116
+ return `${normalizedTitle}::${normalizedEvidence}`;
117
+ }
118
+ export function dedupeIdeateCandidates(drafts) {
119
+ const merged = new Map();
120
+ for (const draft of drafts) {
121
+ const key = normalizeCandidateKey(draft.title, draft.evidencePath);
122
+ const existing = merged.get(key);
123
+ if (!existing) {
124
+ merged.set(key, {
125
+ title: draft.title,
126
+ evidencePath: draft.evidencePath,
127
+ summary: draft.summary,
128
+ frameIds: [draft.frameId]
129
+ });
130
+ continue;
131
+ }
132
+ if (!existing.frameIds.includes(draft.frameId)) {
133
+ existing.frameIds.push(draft.frameId);
134
+ }
135
+ if (draft.summary.length > existing.summary.length) {
136
+ existing.summary = draft.summary;
137
+ }
138
+ }
139
+ return [...merged.values()];
140
+ }
@@ -0,0 +1,25 @@
1
+ export type IdeateImpact = "high" | "medium" | "low";
2
+ export type IdeateEffort = "s" | "m" | "l";
3
+ export type IdeateConfidence = "high" | "medium" | "low";
4
+ export interface IdeateCandidateEvaluationInput {
5
+ id: string;
6
+ title: string;
7
+ impact: IdeateImpact;
8
+ effort: IdeateEffort;
9
+ confidence: IdeateConfidence;
10
+ rationaleStrength: number;
11
+ counterArgumentStrength: number;
12
+ }
13
+ export interface IdeateCandidateEvaluation extends IdeateCandidateEvaluationInput {
14
+ disposition: "survivor" | "critiqued-out";
15
+ rankingScore: number;
16
+ }
17
+ export interface IdeateRankingResult {
18
+ survivors: IdeateCandidateEvaluation[];
19
+ critiquedOut: IdeateCandidateEvaluation[];
20
+ recommendationId: string | null;
21
+ }
22
+ export declare function isCritiquedOut(rationaleStrength: number, counterArgumentStrength: number): boolean;
23
+ export declare function scoreIdeateCandidate(impact: IdeateImpact, effort: IdeateEffort, confidence: IdeateConfidence): number;
24
+ export declare function evaluateIdeateCandidate(input: IdeateCandidateEvaluationInput): IdeateCandidateEvaluation;
25
+ export declare function rankIdeateCandidates(inputs: readonly IdeateCandidateEvaluationInput[], maxSurvivors?: number): IdeateRankingResult;
@@ -0,0 +1,65 @@
1
+ const IMPACT_POINTS = {
2
+ high: 9,
3
+ medium: 6,
4
+ low: 3
5
+ };
6
+ const EFFORT_COST = {
7
+ s: 1,
8
+ m: 2,
9
+ l: 3
10
+ };
11
+ const CONFIDENCE_MULTIPLIER = {
12
+ high: 1,
13
+ medium: 0.75,
14
+ low: 0.5
15
+ };
16
+ function clampStrength(value) {
17
+ if (!Number.isFinite(value))
18
+ return 0;
19
+ if (value < 0)
20
+ return 0;
21
+ if (value > 1)
22
+ return 1;
23
+ return value;
24
+ }
25
+ export function isCritiquedOut(rationaleStrength, counterArgumentStrength) {
26
+ return clampStrength(counterArgumentStrength) > clampStrength(rationaleStrength);
27
+ }
28
+ export function scoreIdeateCandidate(impact, effort, confidence) {
29
+ const raw = (IMPACT_POINTS[impact] / EFFORT_COST[effort]) * CONFIDENCE_MULTIPLIER[confidence];
30
+ return Number(raw.toFixed(3));
31
+ }
32
+ export function evaluateIdeateCandidate(input) {
33
+ const disposition = isCritiquedOut(input.rationaleStrength, input.counterArgumentStrength)
34
+ ? "critiqued-out"
35
+ : "survivor";
36
+ return {
37
+ ...input,
38
+ disposition,
39
+ rankingScore: scoreIdeateCandidate(input.impact, input.effort, input.confidence)
40
+ };
41
+ }
42
+ export function rankIdeateCandidates(inputs, maxSurvivors = 10) {
43
+ const evaluated = inputs.map(evaluateIdeateCandidate);
44
+ const survivors = evaluated
45
+ .filter((candidate) => candidate.disposition === "survivor")
46
+ .sort((left, right) => {
47
+ if (right.rankingScore !== left.rankingScore) {
48
+ return right.rankingScore - left.rankingScore;
49
+ }
50
+ if (right.rationaleStrength !== left.rationaleStrength) {
51
+ return right.rationaleStrength - left.rationaleStrength;
52
+ }
53
+ return left.id.localeCompare(right.id);
54
+ })
55
+ .slice(0, Math.max(0, maxSurvivors));
56
+ const survivorIds = new Set(survivors.map((candidate) => candidate.id));
57
+ const critiquedOut = evaluated
58
+ .filter((candidate) => candidate.disposition === "critiqued-out" || !survivorIds.has(candidate.id))
59
+ .sort((left, right) => left.id.localeCompare(right.id));
60
+ return {
61
+ survivors,
62
+ critiquedOut,
63
+ recommendationId: survivors[0]?.id ?? null
64
+ };
65
+ }