@homeostat/core 0.1.5 → 0.1.7

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.
@@ -56,6 +56,11 @@ export interface RunReport {
56
56
  }[];
57
57
  /** Review PRs from earlier runs that a reviewer merged or closed since. */
58
58
  settled: number;
59
+ /** Review threads read this run that gave no lesson, and why (one-off, skip marker, too long...). */
60
+ skipped?: {
61
+ url: string;
62
+ reason: string;
63
+ }[];
59
64
  lessons: {
60
65
  member: string;
61
66
  id: string;
@@ -107,7 +107,7 @@ export async function curate(input) {
107
107
  }
108
108
  const { thread: safe, redacted } = redactThread(thread);
109
109
  result.redacted += redacted;
110
- const draft = await input.drafter.draft(safe, { areas: config.areas });
110
+ let draft = await input.drafter.draft(safe, { areas: config.areas });
111
111
  if (input.patcher) {
112
112
  await docPatchStep({ files, config, result, today: input.today }, input.patcher, safe, url);
113
113
  }
@@ -115,17 +115,34 @@ export async function curate(input) {
115
115
  skip("no lasting rule in the thread");
116
116
  continue;
117
117
  }
118
- const area = draft.area === CORE_AREA || config.areas[draft.area] ? draft.area : CORE_AREA;
119
- let entry = buildEntry(config, {
120
- draft: { ...draft, area },
121
- id: uniqueId(slugify(draft.title) ?? `review-${pr.id}-${thread.root.id}`, ids),
122
- url,
123
- author: reviewer.id,
124
- today: input.today,
125
- });
126
- const errors = validate({ ...files, [entry.path]: entry.text }).issues.filter((i) => i.file === entry.path && i.severity === "error");
118
+ const check = (d) => {
119
+ const area = d.area === CORE_AREA || config.areas[d.area] ? d.area : CORE_AREA;
120
+ const built = buildEntry(config, {
121
+ draft: { ...d, area },
122
+ id: uniqueId(slugify(d.title) ?? `review-${pr.id}-${thread.root.id}`, ids),
123
+ url,
124
+ author: reviewer.id,
125
+ today: input.today,
126
+ });
127
+ const errs = validate({ ...files, [built.path]: built.text }).issues.filter((i) => i.file === built.path && i.severity === "error");
128
+ return { built, errs };
129
+ };
127
130
  const isRisk = (i) => i.code.startsWith("risk.");
128
- const blocking = errors.filter((i) => !isRisk(i));
131
+ let { built: entry, errs: errors } = check(draft);
132
+ let blocking = errors.filter((i) => !isRisk(i));
133
+ // A draft over a length limit is worth one rewrite rather than losing the lesson.
134
+ if (blocking.length > 0 && blocking.every(isLengthIssue)) {
135
+ const problems = blocking.map((i) => i.message);
136
+ const second = await input.drafter.draft(safe, {
137
+ areas: config.areas,
138
+ revise: { previous: draft, problems },
139
+ });
140
+ if (second) {
141
+ draft = second;
142
+ ({ built: entry, errs: errors } = check(draft));
143
+ blocking = errors.filter((i) => !isRisk(i));
144
+ }
145
+ }
129
146
  if (blocking.length > 0) {
130
147
  skip(`draft did not validate: ${blocking.map((i) => i.message).join("; ")}`);
131
148
  continue;
@@ -214,6 +231,8 @@ async function docPatchStep(ctx, patcher, thread, url) {
214
231
  ctx.result.docs.push({ path: proposal.path, text, sections: [section], risks });
215
232
  }
216
233
  }
234
+ /** Over a word or character limit: fixable by rewriting shorter. */
235
+ const isLengthIssue = (i) => i.code.endsWith(".tooLong") || /at most \d+ characters/.test(i.message);
217
236
  function uniqueId(base, taken) {
218
237
  let id = base;
219
238
  for (let n = 2; taken.has(id); n++)
@@ -10,6 +10,11 @@ export interface Draft {
10
10
  export interface DraftContext {
11
11
  /** Area name → path globs, from homeostat.yaml. `core` is always available. */
12
12
  areas: Record<string, readonly string[]>;
13
+ /** A second try: the previous draft and the limits it broke. */
14
+ revise?: {
15
+ previous: Draft;
16
+ problems: string[];
17
+ };
13
18
  }
14
19
  /** Turns one review thread into at most one rule; null when the thread holds no lasting lesson. */
15
20
  export interface Drafter {
@@ -37,7 +37,19 @@ ${conversation}
37
37
  </thread>
38
38
 
39
39
  Areas and the paths they cover:
40
- ${areas.map((a) => `- ${a}`).join("\n")}`;
40
+ ${areas.map((a) => `- ${a}`).join("\n")}${ctx.revise
41
+ ? `
42
+
43
+ Your previous draft broke these limits:
44
+ ${ctx.revise.problems.map((p) => `- ${p}`).join("\n")}
45
+
46
+ Previous draft:
47
+ Title: ${ctx.revise.previous.title}
48
+ Rule: ${ctx.revise.previous.rule}
49
+ Why: ${ctx.revise.previous.why}
50
+
51
+ Write it again within the limits, keeping every name and fact it states.`
52
+ : ""}`;
41
53
  }
42
54
  /** Drafts with Claude through either runtime (API or Claude Code). */
43
55
  export class ClaudeDrafter {
@@ -45,7 +45,7 @@ export function buildDigest(input) {
45
45
  ];
46
46
  const quiet = body.length === 0;
47
47
  return [
48
- `*Homeostat weekly digest* (since ${since})`,
48
+ `*Homeostat ${days === 7 ? "weekly digest" : `digest, last ${days} days`}* (since ${since})`,
49
49
  "",
50
50
  ...(quiet ? ["A quiet week: no new or changed rules, documents, reviews or flags."] : body),
51
51
  ]
@@ -1,2 +1,2 @@
1
1
  /** The release version; `scripts/version.mjs` keeps it in step with every package.json. */
2
- export declare const VERSION = "0.1.5";
2
+ export declare const VERSION = "0.1.7";
@@ -1,2 +1,2 @@
1
1
  /** The release version; `scripts/version.mjs` keeps it in step with every package.json. */
2
- export const VERSION = "0.1.5";
2
+ export const VERSION = "0.1.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeostat/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Schemas, validation, scoring and agents behind homeostat, the team knowledge base for coding agents.",
5
5
  "keywords": [
6
6
  "homeostat",