@deftai/directive-core 0.66.0 → 0.66.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.
package/dist/doctor/paths.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export declare function resolvePath(pathStr: string, cwd?: string): string;
|
|
3
3
|
/** Best-effort version string (mirrors Python `_resolve_version`). */
|
|
4
4
|
export declare function resolveVersion(frameworkRoot?: string): string;
|
|
5
|
+
export declare function resolveFrameworkRootForProject(projectRoot: string, explicitRoot?: string | null): string;
|
|
5
6
|
/** Return the deft framework repo root (the directory containing `main.md`). */
|
|
6
7
|
export declare function resolveDefaultFrameworkRoot(): string;
|
|
7
8
|
/** Directory containing doctor implementation (Python: scripts/). */
|
package/dist/doctor/paths.js
CHANGED
|
@@ -32,6 +32,61 @@ export function resolveVersion(frameworkRoot) {
|
|
|
32
32
|
}
|
|
33
33
|
return "dev";
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the framework root for a CLI command running against `projectRoot`.
|
|
37
|
+
*
|
|
38
|
+
* Priority: explicit `--deft-root` / `--framework-root` → `DEFT_ROOT` env →
|
|
39
|
+
* maintainer source checkout at `projectRoot` (when `main.md` + framework markers
|
|
40
|
+
* are present, even if a dogfood `.deft/core` deposit also exists) → consumer
|
|
41
|
+
* deposit (`.deft/core`, legacy `deft/`) → maintainer checkout walk
|
|
42
|
+
* (`resolveDefaultFrameworkRoot`).
|
|
43
|
+
*
|
|
44
|
+
* Refs #2146 (npm global `deft` must not default to `node_modules/vbrief/schemas`).
|
|
45
|
+
*/
|
|
46
|
+
function isFrameworkSourceCheckoutAt(projectRoot) {
|
|
47
|
+
try {
|
|
48
|
+
if (!statSync(join(projectRoot, "main.md")).isFile()) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return DEFT_REPO_POSITIVE_MARKERS.every((marker) => {
|
|
56
|
+
try {
|
|
57
|
+
return statSync(join(projectRoot, marker)).isFile();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function resolveFrameworkRootForProject(projectRoot, explicitRoot) {
|
|
65
|
+
const explicit = explicitRoot?.trim();
|
|
66
|
+
if (explicit) {
|
|
67
|
+
return resolve(explicit);
|
|
68
|
+
}
|
|
69
|
+
const envRoot = process.env.DEFT_ROOT?.trim();
|
|
70
|
+
if (envRoot) {
|
|
71
|
+
return resolve(envRoot);
|
|
72
|
+
}
|
|
73
|
+
const root = resolve(projectRoot);
|
|
74
|
+
if (isFrameworkSourceCheckoutAt(root)) {
|
|
75
|
+
return root;
|
|
76
|
+
}
|
|
77
|
+
for (const rel of [join(".deft", "core"), "deft"]) {
|
|
78
|
+
const candidate = join(root, rel);
|
|
79
|
+
try {
|
|
80
|
+
if (statSync(candidate).isDirectory()) {
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// try next candidate
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return resolveDefaultFrameworkRoot();
|
|
89
|
+
}
|
|
35
90
|
/** Return the deft framework repo root (the directory containing `main.md`). */
|
|
36
91
|
export function resolveDefaultFrameworkRoot() {
|
|
37
92
|
const envRoot = process.env.DEFT_ROOT?.trim();
|
|
@@ -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;
|
|
@@ -38,6 +38,8 @@ import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIG
|
|
|
38
38
|
export const LEGACY_REFERENCE_PREFIX = "x-vbrief/";
|
|
39
39
|
/** Default allowlist: sanctioned data-plane trees that legitimately retain legacy tokens for back-compat. */
|
|
40
40
|
export const BUILTIN_ALLOW_LIST = [
|
|
41
|
+
// Vendored npm deposit (#2146): C1 flatten maps content/vbrief/* -> .deft/core/vbrief/*.
|
|
42
|
+
".deft/core/vbrief/**",
|
|
41
43
|
// Shipped vBRIEF surface (#1875 C3) + conformance round-trip fixtures (#715).
|
|
42
44
|
"content/vbrief/**",
|
|
43
45
|
// Test fixtures exercising the legacy read path + pre-cutover migration (#2108 / #2110)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.2",
|
|
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.2",
|
|
241
|
+
"@deftai/directive-types": "^0.66.2",
|
|
242
242
|
"archiver": "^8.0.0"
|
|
243
243
|
},
|
|
244
244
|
"scripts": {
|