@deftai/directive-core 0.66.0 → 0.66.1

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.
@@ -1,6 +1,17 @@
1
1
  import { type ScmCallFn } from "./reconcile-issues.js";
2
2
  export declare const INGEST_STATUSES: readonly ["proposed", "pending", "active"];
3
3
  export type IngestStatus = (typeof INGEST_STATUSES)[number];
4
+ /** GitHub issue comment thread entry (REST `repos/.../issues/N/comments`). */
5
+ export interface IssueComment {
6
+ readonly id?: number;
7
+ readonly body?: string;
8
+ readonly user?: {
9
+ readonly login?: string;
10
+ };
11
+ readonly created_at?: string;
12
+ }
13
+ /** Enriched on issues after `fetchIssue` when the comment thread is non-empty (#2143). */
14
+ export declare const ISSUE_COMMENT_THREAD_KEY: "issueCommentThread";
4
15
  export declare function bodyControlCharacterLabels(body: string): string[];
5
16
  export declare function warnBodyControlCharacters(number: number, body: string): void;
6
17
  export declare function extractPlanItems(body: string): Record<string, string>[];
@@ -8,6 +19,9 @@ export declare function extractAcSectionItems(text: string): Record<string, stri
8
19
  export declare function extractCrossRefs(body: string, repoUrl: string, exclude?: ReadonlySet<number>): Record<string, string>[];
9
20
  export declare function provenanceIssueNumber(data: Record<string, unknown>): number | null;
10
21
  export declare function scanProvenanceRefs(vbriefDir: string): Map<number, string[]>;
22
+ export declare function composeOverviewWithComments(body: string, comments: readonly IssueComment[]): string;
23
+ export declare function issueCommentThread(issue: Record<string, unknown>): IssueComment[];
24
+ export declare function issueCommentsAlreadyFetched(issue: Record<string, unknown>): boolean;
11
25
  export declare function buildIssueVbrief(issue: Record<string, unknown>, status: IngestStatus, repoUrl: string): [Record<string, unknown>, string];
12
26
  export declare function targetFilename(number: number, title: string): string;
13
27
  export interface FetchIssueOptions {
@@ -16,6 +30,10 @@ export interface FetchIssueOptions {
16
30
  readonly scmCall?: ScmCallFn;
17
31
  }
18
32
  export declare function fetchFromCache(repo: string, number: number, options?: FetchIssueOptions): Record<string, unknown> | null;
33
+ export declare function fetchIssueComments(repo: string, number: number, options?: FetchIssueOptions): IssueComment[];
34
+ export declare function attachIssueCommentThread(issue: Record<string, unknown>, comments: readonly IssueComment[]): Record<string, unknown>;
35
+ export declare function repoSlugFromUrl(repoUrl: string): string | null;
36
+ export declare function enrichIssueWithComments(issue: Record<string, unknown>, repoUrl: string, options?: FetchIssueOptions): Record<string, unknown>;
19
37
  export declare function fetchSingleIssue(repo: string, number: number, options?: FetchIssueOptions): Record<string, unknown> | null;
20
38
  export declare function fetchIssue(repo: string, number: number, options?: FetchIssueOptions): Record<string, unknown> | null;
21
39
  export type IngestResult = "created" | "dryrun" | "duplicate";
@@ -25,6 +43,9 @@ export declare function ingestOne(issue: Record<string, unknown>, options: {
25
43
  repoUrl: string;
26
44
  dryRun?: boolean;
27
45
  existingRefs?: Map<number, string[]>;
46
+ scmCall?: ScmCallFn;
47
+ cwd?: string | null;
48
+ cacheRoot?: string | null;
28
49
  }): [IngestResult, string | null, string];
29
50
  export declare function ingestBulk(issues: Record<string, unknown>[], options: {
30
51
  vbriefDir: string;
@@ -32,6 +53,9 @@ export declare function ingestBulk(issues: Record<string, unknown>[], options: {
32
53
  repoUrl: string;
33
54
  label?: string | null;
34
55
  dryRun?: boolean;
56
+ scmCall?: ScmCallFn;
57
+ cwd?: string | null;
58
+ cacheRoot?: string | null;
35
59
  }): Record<string, string[] | number>;
36
60
  export declare function resolveRepoUrl(repo: string): string;
37
61
  export interface IssueIngestCliArgs {
@@ -10,6 +10,8 @@ import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
10
10
  import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
11
11
  import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
12
12
  export const INGEST_STATUSES = ["proposed", "pending", "active"];
13
+ /** Enriched on issues after `fetchIssue` when the comment thread is non-empty (#2143). */
14
+ export const ISSUE_COMMENT_THREAD_KEY = "issueCommentThread";
13
15
  const STATUS_MAP = {
14
16
  proposed: ["proposed", "proposed"],
15
17
  pending: ["pending", "pending"],
@@ -192,6 +194,42 @@ export function scanProvenanceRefs(vbriefDir) {
192
194
  }
193
195
  return issueToVbriefs;
194
196
  }
197
+ export function composeOverviewWithComments(body, comments) {
198
+ if (comments.length === 0) {
199
+ return body;
200
+ }
201
+ const parts = [];
202
+ if (body.length > 0) {
203
+ parts.push(body);
204
+ parts.push("");
205
+ parts.push("---");
206
+ parts.push("");
207
+ }
208
+ parts.push("## Issue comment thread");
209
+ parts.push("");
210
+ parts.push("_The issue body is the original write-up; maintainer comments below may supersede it. Read the full thread before building a dispatch envelope (#2143)._");
211
+ parts.push("");
212
+ for (const comment of comments) {
213
+ const author = comment.user?.login ?? "unknown";
214
+ const when = comment.created_at ?? "";
215
+ const header = when.length > 0 ? `### Comment by @${author} (${when})` : `### Comment by @${author}`;
216
+ parts.push(header);
217
+ parts.push("");
218
+ parts.push(typeof comment.body === "string" ? comment.body : "");
219
+ parts.push("");
220
+ }
221
+ return parts.join("\n").trimEnd();
222
+ }
223
+ export function issueCommentThread(issue) {
224
+ const raw = issue[ISSUE_COMMENT_THREAD_KEY];
225
+ if (!Array.isArray(raw)) {
226
+ return [];
227
+ }
228
+ return raw.filter((entry) => entry !== null && typeof entry === "object");
229
+ }
230
+ export function issueCommentsAlreadyFetched(issue) {
231
+ return Object.hasOwn(issue, ISSUE_COMMENT_THREAD_KEY);
232
+ }
195
233
  export function buildIssueVbrief(issue, status, repoUrl) {
196
234
  const number = Number(issue.number);
197
235
  const title = (typeof issue.title === "string" && issue.title.length > 0
@@ -201,6 +239,8 @@ export function buildIssueVbrief(issue, status, repoUrl) {
201
239
  (repoUrl.length > 0 ? `${repoUrl}/issues/${number}` : "");
202
240
  const bodyRaw = issue.body;
203
241
  const bodyStr = typeof bodyRaw === "string" && bodyRaw.length > 0 ? bodyRaw : "";
242
+ const commentThread = issueCommentThread(issue);
243
+ const overviewSource = commentThread.length > 0 ? composeOverviewWithComments(bodyStr, commentThread) : bodyStr;
204
244
  const labelsRaw = issue.labels;
205
245
  const labelNames = [];
206
246
  if (Array.isArray(labelsRaw)) {
@@ -221,9 +261,9 @@ export function buildIssueVbrief(issue, status, repoUrl) {
221
261
  Description: title,
222
262
  Origin: url.length > 0 ? `Ingested from ${url}` : `Ingested from issue #${number}`,
223
263
  };
224
- if (bodyStr.length > 0) {
225
- warnBodyControlCharacters(number, bodyStr);
226
- narratives.Overview = bodyStr;
264
+ if (overviewSource.length > 0) {
265
+ warnBodyControlCharacters(number, overviewSource);
266
+ narratives.Overview = overviewSource;
227
267
  }
228
268
  if (labelNames.length > 0) {
229
269
  narratives.Labels = labelNames.join(", ");
@@ -246,8 +286,8 @@ export function buildIssueVbrief(issue, status, repoUrl) {
246
286
  title: `Issue #${number}: ${title}`,
247
287
  },
248
288
  ];
249
- if (bodyStr.length > 0 && repoUrl.length > 0) {
250
- references.push(...extractCrossRefs(bodyStr, repoUrl, new Set([number])));
289
+ if (overviewSource.length > 0 && repoUrl.length > 0) {
290
+ references.push(...extractCrossRefs(overviewSource, repoUrl, new Set([number])));
251
291
  }
252
292
  plan.references = references;
253
293
  }
@@ -284,6 +324,47 @@ export function fetchFromCache(repo, number, options = {}) {
284
324
  return null;
285
325
  }
286
326
  }
327
+ export function fetchIssueComments(repo, number, options = {}) {
328
+ const scmCall = options.scmCall ?? call;
329
+ let result;
330
+ try {
331
+ result = scmCall("github-issue", "api", [`repos/${repo}/issues/${number}/comments`], {
332
+ timeout: 30,
333
+ cwd: options.cwd ?? undefined,
334
+ });
335
+ }
336
+ catch {
337
+ return [];
338
+ }
339
+ if (result.returncode !== 0) {
340
+ return [];
341
+ }
342
+ try {
343
+ const parsed = JSON.parse(result.stdout);
344
+ return Array.isArray(parsed) ? parsed : [];
345
+ }
346
+ catch {
347
+ return [];
348
+ }
349
+ }
350
+ export function attachIssueCommentThread(issue, comments) {
351
+ return { ...issue, [ISSUE_COMMENT_THREAD_KEY]: [...comments] };
352
+ }
353
+ export function repoSlugFromUrl(repoUrl) {
354
+ const match = /github\.com\/([^/\s]+\/[^/\s]+)/.exec(repoUrl);
355
+ return match?.[1] ?? null;
356
+ }
357
+ export function enrichIssueWithComments(issue, repoUrl, options = {}) {
358
+ if (issueCommentsAlreadyFetched(issue)) {
359
+ return issue;
360
+ }
361
+ const repo = repoSlugFromUrl(repoUrl);
362
+ const number = Number(issue.number);
363
+ if (repo === null || !Number.isFinite(number)) {
364
+ return issue;
365
+ }
366
+ return attachIssueCommentThread(issue, fetchIssueComments(repo, number, options));
367
+ }
287
368
  export function fetchSingleIssue(repo, number, options = {}) {
288
369
  const scmCall = options.scmCall ?? call;
289
370
  let result;
@@ -316,9 +397,15 @@ export function fetchSingleIssue(repo, number, options = {}) {
316
397
  export function fetchIssue(repo, number, options = {}) {
317
398
  const live = fetchSingleIssue(repo, number, options);
318
399
  if (live !== null) {
319
- return live;
400
+ const comments = fetchIssueComments(repo, number, options);
401
+ return attachIssueCommentThread(live, comments);
320
402
  }
321
- return fetchFromCache(repo, number, options);
403
+ const cached = fetchFromCache(repo, number, options);
404
+ if (cached === null) {
405
+ return null;
406
+ }
407
+ const comments = fetchIssueComments(repo, number, options);
408
+ return attachIssueCommentThread(cached, comments);
322
409
  }
323
410
  export function ingestOne(issue, options) {
324
411
  const number = Number(issue.number);
@@ -331,7 +418,12 @@ export function ingestOne(issue, options) {
331
418
  `#${number} already ingested at ${existing}`,
332
419
  ];
333
420
  }
334
- const [vbrief, folder] = buildIssueVbrief(issue, options.status, options.repoUrl);
421
+ const enriched = enrichIssueWithComments(issue, options.repoUrl, {
422
+ scmCall: options.scmCall,
423
+ cwd: options.cwd,
424
+ cacheRoot: options.cacheRoot,
425
+ });
426
+ const [vbrief, folder] = buildIssueVbrief(enriched, options.status, options.repoUrl);
335
427
  const filename = targetFilename(number, String(issue.title ?? ""));
336
428
  const target = join(options.vbriefDir, folder, filename);
337
429
  if (options.dryRun) {
@@ -429,6 +521,7 @@ export function issueIngestMain(args) {
429
521
  repoUrl,
430
522
  label: args.label,
431
523
  dryRun: args.dryRun,
524
+ cwd: projectRoot,
432
525
  });
433
526
  const created = summary.created;
434
527
  const duplicate = summary.duplicate;
@@ -454,6 +547,7 @@ export function issueIngestMain(args) {
454
547
  status,
455
548
  repoUrl,
456
549
  dryRun: args.dryRun,
550
+ cwd: projectRoot,
457
551
  });
458
552
  process.stdout.write(`${msg}\n`);
459
553
  return result === "duplicate" ? 1 : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.66.0",
3
+ "version": "0.66.1",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -237,8 +237,8 @@
237
237
  "provenance": true
238
238
  },
239
239
  "dependencies": {
240
- "@deftai/directive-content": "^0.66.0",
241
- "@deftai/directive-types": "^0.66.0",
240
+ "@deftai/directive-content": "^0.66.1",
241
+ "@deftai/directive-types": "^0.66.1",
242
242
  "archiver": "^8.0.0"
243
243
  },
244
244
  "scripts": {