@turri-dev/cli 0.2.1 → 0.2.2

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.
@@ -160,10 +160,21 @@ export interface PlaceAnswer {
160
160
  /** What of the person's wishes it did not follow, and why. */
161
161
  notes?: string;
162
162
  }
163
- /** A group to title: the number the agent answers with, and its functional specs' text. */
163
+ /**
164
+ * A group to title: the number the agent answers with, and its functional specs' text - or, for a pulse that makes the
165
+ * whole of an edit, `changes`: what the edit does to the ***plain the code is built from.
166
+ */
164
167
  export interface TitleGroup {
165
168
  number: number;
166
169
  specs: string[];
170
+ changes?: TitleChange[];
171
+ }
172
+ /** A change a pulse makes, as the title agent is shown it: what it is, how it changes, its text now - and before, changed. */
173
+ export interface TitleChange {
174
+ kind: "changed" | "taken out" | "added";
175
+ what: string;
176
+ text: string;
177
+ before?: string;
167
178
  }
168
179
  export interface TitleAnswer {
169
180
  titles: Array<{
@@ -175,7 +186,8 @@ export interface TitleAnswer {
175
186
  * The judgements, each asked again with what was wrong with its answer - the last answer given, whether it holds
176
187
  * or not: the plan checks it again. Injectable, so the plan is testable without a model. `title` titles the groups no
177
188
  * agent titled - the conformance tests', and a grouping that fell back - for what each pulse does, rather than its
178
- * first functional spec's first line; without it, they keep that.
189
+ * first functional spec's first line; without it, they keep that. It titles a pulse that makes the whole of an edit
190
+ * too, shown the edit's changes; without it, that pulse is titled by `editTitle`.
179
191
  */
180
192
  export interface ChunkAgents {
181
193
  group(specs: NewSpec[], check: (answer: GroupAnswer) => string[], steering?: Steering[], maxSpecs?: number, built?: BuiltChange[]): Promise<GroupAnswer | undefined>;
@@ -188,10 +200,15 @@ export interface ChunkAgents {
188
200
  * is built as the new ones are. One taken out builds nothing, and does not.
189
201
  */
190
202
  export declare function groupProblems(specs: NewSpec[], answer: GroupAnswer, maxSpecs?: number, built?: BuiltChange[]): string[];
191
- /** What is wrong with a titling: every group titled once, by its number, none empty, none too long. */
203
+ /** What is wrong with a titling: every group titled once, by its number, none empty, none too long, none naming the act. */
192
204
  export declare function titleProblems(groups: TitleGroup[], answer: TitleAnswer): string[];
193
205
  /** What is wrong with a placement: every new req once, each where there is such a job. */
194
206
  export declare function placeProblems(input: PlaceInput, answer: PlaceAnswer): string[];
207
+ /**
208
+ * The title of a pulse that makes the whole of an edit, when no agent gave one: the first line of the first functional
209
+ * spec it changes - else of its first change - and how many more it makes. Never the bare word.
210
+ */
211
+ export declare function editTitle(changes: SpecChange[]): string;
195
212
  export declare function planChunks(input: PlanInput, agents?: ChunkAgents, notes?: string[]): Promise<DraftJob[]>;
196
213
  export declare function groupPrompt(specs: NewSpec[], steering?: Steering[], maxSpecs?: number, built?: BuiltChange[]): string;
197
214
  export declare function placePrompt(input: PlaceInput): string;
@@ -79,6 +79,20 @@ export const TITLE_CHARS = 70;
79
79
  function fitsTitle(title) {
80
80
  return title.trim().split(/\s+/).length <= TITLE_WORDS && title.trim().length <= TITLE_CHARS;
81
81
  }
82
+ /** Whether a title names the act of regenerating - "regenerate", "Regenerate the scenery" - rather than the change. */
83
+ function namesTheAct(title) {
84
+ return /^regenerat(e|es|ed|ing)\b/i.test(title.trim());
85
+ }
86
+ /** What is wrong with one title, or nothing: empty, too long, or naming the act rather than the change. */
87
+ function titleProblem(title, empty) {
88
+ if (title.trim() === "")
89
+ return empty;
90
+ if (!fitsTitle(title))
91
+ return `"${title}" is too long for a title: at most ${TITLE_WORDS} words and ${TITLE_CHARS} characters.`;
92
+ if (namesTheAct(title))
93
+ return `"${title}" names the act of regenerating: say what the pulse changes in the software.`;
94
+ return undefined;
95
+ }
82
96
  /**
83
97
  * A title held to `TITLE_WORDS` and `TITLE_CHARS` - or less, for room beside it - cut at a word, with an ellipsis. Text
84
98
  * cut already is cut again without stacking a second: several first lines joined, each cut, end on one ellipsis.
@@ -101,7 +115,10 @@ const RETRIES = 2;
101
115
  * its scenarios, each under its scenario-id, where the tree has them: the conformance track's.
102
116
  */
103
117
  function textOf(change) {
104
- const line = change.after ?? change.before ?? "";
118
+ return textOfLine(change.after ?? change.before ?? "");
119
+ }
120
+ /** An item's text from its line in a tree: its markdown, and its scenarios, if any. */
121
+ function textOfLine(line) {
105
122
  try {
106
123
  const item = JSON.parse(line);
107
124
  const scenarios = [...(item[CONFORMANCE_TESTS] ?? []), ...(item[ACCEPTANCE_TESTS] ?? [])].map((scenario) => `# ${scenario.id ?? ""}\n${scenario.text ?? ""}`);
@@ -181,14 +198,13 @@ function newSpecProblems(specs, answer, maxSpecs, built) {
181
198
  if (!nextTo(group.specs[i - 1], group.specs[i]))
182
199
  problems.push(`"${group.title}" groups ${group.specs.join(", ")}: only functional specs next to each other in the same module go together.`);
183
200
  }
184
- if (group.title.trim() === "")
185
- problems.push("Every group needs a title.");
186
- else if (!fitsTitle(group.title))
187
- problems.push(`"${group.title}" is too long for a title: at most ${TITLE_WORDS} words and ${TITLE_CHARS} characters.`);
201
+ const wrong = titleProblem(group.title, "Every group needs a title.");
202
+ if (wrong !== undefined)
203
+ problems.push(wrong);
188
204
  });
189
205
  return problems;
190
206
  }
191
- /** What is wrong with a titling: every group titled once, by its number, none empty, none too long. */
207
+ /** What is wrong with a titling: every group titled once, by its number, none empty, none too long, none naming the act. */
192
208
  export function titleProblems(groups, answer) {
193
209
  const problems = [];
194
210
  const numbers = groups.map((group) => group.number);
@@ -197,10 +213,9 @@ export function titleProblems(groups, answer) {
197
213
  if (!numbers.includes(entry.group))
198
214
  problems.push(`There is no group ${entry.group}.`);
199
215
  seen.set(entry.group, (seen.get(entry.group) ?? 0) + 1);
200
- if (entry.title.trim() === "")
201
- problems.push(`Group ${entry.group} needs a title.`);
202
- else if (!fitsTitle(entry.title))
203
- problems.push(`"${entry.title}" is too long for a title: at most ${TITLE_WORDS} words and ${TITLE_CHARS} characters.`);
216
+ const wrong = titleProblem(entry.title, `Group ${entry.group} needs a title.`);
217
+ if (wrong !== undefined)
218
+ problems.push(wrong);
204
219
  }
205
220
  for (const number of numbers) {
206
221
  const times = seen.get(number) ?? 0;
@@ -323,7 +338,34 @@ function draftOf(chunks, net) {
323
338
  }
324
339
  /** A spec's first line, as a group's title when no agent gave one. */
325
340
  function titleOf(change) {
326
- return shortTitle(textOf(change).split("\n")[0].replace(/^-\s*/, ""));
341
+ return shortTitle(firstLineOf(change));
342
+ }
343
+ function firstLineOf(change) {
344
+ return textOf(change).split("\n")[0].replace(/^-\s*/, "");
345
+ }
346
+ const isFunctionalSpec = (change) => change.section === FUNCTIONAL_REQUIREMENTS || change.section === REQUIRED_FUNCTIONAL_SPECS;
347
+ /**
348
+ * The title of a pulse that makes the whole of an edit, when no agent gave one: the first line of the first functional
349
+ * spec it changes - else of its first change - and how many more it makes. Never the bare word.
350
+ */
351
+ export function editTitle(changes) {
352
+ const [first, ...rest] = [...changes.filter(isFunctionalSpec), ...changes.filter((change) => !isFunctionalSpec(change))];
353
+ if (first === undefined)
354
+ return "No change";
355
+ const line = first.kind === "remove" ? `Takes out: ${firstLineOf(first)}` : firstLineOf(first);
356
+ if (rest.length === 0)
357
+ return shortTitle(line);
358
+ const more = `, and ${rest.length} more`;
359
+ return `${shortTitle(line, TITLE_CHARS - more.length, TITLE_WORDS - 3).replace(/[.,;:]+$/, "")}${more}`;
360
+ }
361
+ /** A change as the title agent is shown it: what it is, how it changes, its text now - and before, for a change. */
362
+ function titleChangeOf(change) {
363
+ return {
364
+ kind: change.kind === "change" ? "changed" : change.kind === "remove" ? "taken out" : "added",
365
+ what: WHAT[change.section] ?? change.section,
366
+ text: textOf(change),
367
+ ...(change.kind === "change" && change.before !== undefined ? { before: textOfLine(change.before) } : {}),
368
+ };
327
369
  }
328
370
  /**
329
371
  * Cut the queue's work - from `input.base` to `input.current` - into jobs, as the three steps say. Returns the draft
@@ -472,10 +514,29 @@ async function cut(input, agents, notes) {
472
514
  const builtShown = builtChanges.map((change, index) => builtChangeOf(change, index + 1, input.moduleName));
473
515
  /** Where each change to what is built goes: with the group holding a new functional spec - by that spec's number - or first. */
474
516
  const builtTo = new Map();
475
- // Named as the person asks for it: "shorty.plain regenerate".
476
- const whole = [{ title: "regenerate", reason: "the whole of the work, in one job", changes: net.map((change) => change.id) }];
517
+ /**
518
+ * The whole of the work in one job - an edit that only changes what is built, or one no plan of holds together -
519
+ * titled by what the edit does: by the title agent, shown every change, else by what it changes first.
520
+ */
521
+ const wholeJob = async () => {
522
+ const whole = { title: "", reason: "the whole of the work, in one job", changes: net.map((change) => change.id) };
523
+ if (checkDraft(input, [whole]).length > 0)
524
+ return [];
525
+ const fallback = editTitle(net);
526
+ if (agents?.title === undefined)
527
+ return [{ ...whole, title: fallback }];
528
+ reportStage("planning the queue: a title for the pulse");
529
+ const toTitle = [{ number: 1, specs: [], changes: net.map(titleChangeOf) }];
530
+ const answer = (await agents.title(toTitle, (given) => titleProblems(toTitle, given), input.track === "conformance" ? "conformance" : "code").catch((error) => plannerFailed("title", error))) ?? undefined;
531
+ const problems = answer === undefined ? [] : titleProblems(toTitle, answer);
532
+ if (answer !== undefined && problems.length === 0)
533
+ return [{ ...whole, title: answer.titles[0].title.trim() }];
534
+ record({ kind: "planner.fellBack", step: "title", why: answer === undefined ? "no title was given" : firstLine(problems.join(" "), 300) });
535
+ say(answer === undefined ? `the title was not given - the pulse titled by what it changes first: "${fallback}"` : `the title did not hold (${problems.join(" ")}) - the pulse titled by what it changes first: "${fallback}"`);
536
+ return [{ ...whole, title: fallback }];
537
+ };
477
538
  if (specs.length === 0)
478
- return checkDraft(input, whole).length === 0 ? whole : [];
539
+ return wholeJob();
479
540
  // 1. Which functional specs go together.
480
541
  reportStage(`planning the queue: which of the ${specs.length} functional spec${specs.length === 1 ? "" : "s"} to build go together`);
481
542
  const numbered = specs.map((change, index) => {
@@ -661,7 +722,7 @@ async function cut(input, agents, notes) {
661
722
  const simple = draftOf(chunks, net);
662
723
  if (checkDraft(input, simple).length === 0)
663
724
  return simple;
664
- return checkDraft(input, whole).length === 0 ? whole : [];
725
+ return wholeJob();
665
726
  }
666
727
  // ---------------------------------------------------------------------------------------------
667
728
  // The agents.
@@ -752,8 +813,25 @@ export function titlePrompt(groups, track) {
752
813
  ? "Give each group a title that says what its pulse tests - the behaviour, not the spec's wording."
753
814
  : "Give each group a title that says what its pulse builds - the feature, not the spec's wording.",
754
815
  `At most ${TITLE_WORDS} words and ${TITLE_CHARS} characters: it heads the pulse where the person watches the work, and a longer one is cut short there.`,
816
+ ...(groups.some((group) => group.changes !== undefined)
817
+ ? [
818
+ "",
819
+ "A group of changes is one edit to software already built: its pulse makes the whole of it. Title it by what the",
820
+ "edit does to the software, briefly - \"Expired links answer 410 and the stats page goes\", \"Greeter greets by",
821
+ "name\". A definition or an implementation req changed is titled by what it changes in what is built - \"Links are",
822
+ "kept for 30 days\" - never by the section it is in.",
823
+ ]
824
+ : []),
825
+ "Name the change, never the act: \"regenerate\", \"Regenerate the scenery\" and \"Changes\" are not titles.",
755
826
  "",
756
- ...groups.map((group) => [`<group number="${group.number}">`, ...group.specs.map((spec) => `<functional spec>\n${spec.trim()}\n</functional spec>`), "</group>"].join("\n")),
827
+ ...groups.map((group) => [
828
+ `<group number="${group.number}">`,
829
+ ...group.specs.map((spec) => `<functional spec>\n${spec.trim()}\n</functional spec>`),
830
+ ...(group.changes ?? []).map((change) => change.before !== undefined
831
+ ? `<change kind="${change.kind}" what="${change.what}">\n<before>\n${change.before.trim()}\n</before>\n<now>\n${change.text.trim()}\n</now>\n</change>`
832
+ : `<change kind="${change.kind}" what="${change.what}">\n${change.text.trim()}\n</change>`),
833
+ "</group>",
834
+ ].join("\n")),
757
835
  "",
758
836
  "Call submit_titles once, with a title for every group, by its number.",
759
837
  ].join("\n");
@@ -184,7 +184,7 @@ A job that regenerates the whole of a new module's \*\*\*plain is too big to bui
184
184
  2. **The definitions each group uses**, in code: the concepts its functional specs name, and the concepts those definitions name in turn. A definition goes with the first group that uses it; later groups have it built. One nothing uses goes with the last.
185
185
  3. **Where each new implementation req goes.** An agent places each with the group it is about - or with the first, when it is about everything (the language, the framework).
186
186
 
187
- Each pulse's title says what it does - at most 10 words and 70 characters, as it heads the pulse where the person watches the work. The grouping agent titles its groups; groups no agent made - the conformance tests', and a grouping that fell back - are titled by a third agent, told what each pulse does: what it builds, or, for the conformance tests, what they test. Without it, or when it fails, a pulse is titled by its functional specs' first lines, cut short.
187
+ Each pulse's title says what it does - at most 10 words and 70 characters, as it heads the pulse where the person watches the work. The grouping agent titles its groups; groups no agent made - the conformance tests', and a grouping that fell back - are titled by a third agent, told what each pulse does: what it builds, or, for the conformance tests, what they test. Without it, or when it fails, a pulse is titled by its functional specs' first lines, cut short. A pulse that makes the whole of an edit in one go - an edit that only changes what is built (a functional spec reworded, a definition or an implementation req changed, none new and none marked to regenerate again), or one no plan of holds together - is titled by the same agent, shown every change, before and after: by what the edit does (`Expired links answer 410 and the stats page goes`), and an edit to definitions or implementation reqs alone by what it changes in what is built (`Links are kept for 30 days`), never by its section. Without it, or when it fails, it is titled by the first line of the first functional spec it changes - else of its first change - and how many more (`Follow-link answers 410 for expired links, and 2 more`). A title names the change, never the act: one that starts with "regenerate" does not hold. Pulses already in the ledger keep the titles they were recorded with.
188
188
 
189
189
  Changes to what is built - an item edited or taken out, a functional spec built worded differently - go where the same agent places each: with the group of new functional specs it is about, by default - so one edit that adds a functional spec, changes another and takes out a third is one pulse - or first, in a pulse of its own, when it reshapes what the new work is built on (a concept many functional specs use redefined, one others rely on taken out, a large rewrite) or has nothing to do with it. A functional spec changed counts toward its group's `max-specs-per-pulse`; one taken out does not. Every answer is checked: every new functional spec once, in order, next to each other, at most `max-specs-per-pulse`; every change to what is built placed once; every req placed once; every group titled once, within the limit. One that does not hold goes back to its agent, twice at most, and then the plan is a functional spec a job with every req in the first. A group whose functional specs and the definitions they bring pass `max-lines-per-pulse` (120 unset) is split into as few pulses as fit - its functional specs kept together in runs, in order, as long as each run stays within it, `Comments (1 of 2)`, `Comments (2 of 2)` - not a functional spec a job; its reqs do not count, as a split leaves them with its first job whatever happens. The planner runs on the chat's model in a conversation, and on the harness model under `turri regenerate`. No agent reviews its plan: the harness checks it - every change in exactly one job, \*\*\*plain holding together after each, the last ending on \*\*\*plain as it is.
190
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {