@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.
- package/dist/intake/issue-ingest.d.ts +24 -0
- package/dist/intake/issue-ingest.js +102 -8
- package/package.json +3 -3
|
@@ -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 (
|
|
225
|
-
warnBodyControlCharacters(number,
|
|
226
|
-
narratives.Overview =
|
|
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 (
|
|
250
|
-
references.push(...extractCrossRefs(
|
|
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
|
-
|
|
400
|
+
const comments = fetchIssueComments(repo, number, options);
|
|
401
|
+
return attachIssueCommentThread(live, comments);
|
|
320
402
|
}
|
|
321
|
-
|
|
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
|
|
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.
|
|
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.
|
|
241
|
-
"@deftai/directive-types": "^0.66.
|
|
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": {
|