@deftai/directive-core 0.64.0 → 0.65.0
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/content-contracts/standards/_helpers.js +2 -0
- package/dist/intake/issue-ingest.js +6 -6
- package/dist/intake/markdown-scanners.d.ts +3 -1
- package/dist/intake/markdown-scanners.js +10 -3
- package/dist/intake/reconcile-issues.js +2 -0
- package/dist/preflight-cache/evaluate.js +3 -40
- package/dist/triage/queue/build-queue.js +6 -0
- package/dist/triage/queue/cache.js +9 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/scope-ignores-filter.d.ts +12 -0
- package/dist/triage/queue/scope-ignores-filter.js +50 -0
- package/dist/triage/queue/types.d.ts +4 -0
- package/dist/triage/subscribe/index.js +1 -1
- package/dist/triage/summary/index.d.ts +33 -0
- package/dist/triage/summary/index.js +173 -1
- package/dist/triage/welcome/default-mode.d.ts +1 -0
- package/dist/triage/welcome/default-mode.js +1 -0
- package/dist/triage/welcome/summary.d.ts +2 -0
- package/dist/triage/welcome/summary.js +8 -2
- package/dist/verify-source/contract-drift.d.ts +29 -0
- package/dist/verify-source/contract-drift.js +152 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/package.json +3 -3
|
@@ -63,6 +63,8 @@ const SKIP_DIRS = new Set([
|
|
|
63
63
|
"dist",
|
|
64
64
|
"tests",
|
|
65
65
|
".deft-cache",
|
|
66
|
+
// Swarm worktrees under .deft-scratch/ are not framework content (#1656).
|
|
67
|
+
".deft-scratch",
|
|
66
68
|
// node_modules holds third-party markdown (dependency READMEs) that is not
|
|
67
69
|
// framework content; scanning it produced volatile, version-pinned cases that
|
|
68
70
|
// broke CI when dependency versions drifted (#1993 follow-up).
|
|
@@ -6,7 +6,7 @@ import { resolveProjectRoot } from "../scope/project-context.js";
|
|
|
6
6
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
7
7
|
import { slugify, TODAY } from "../vbrief-build/build.js";
|
|
8
8
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
9
|
-
import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, } from "./markdown-scanners.js";
|
|
9
|
+
import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
|
|
10
10
|
import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
|
|
11
11
|
export const INGEST_STATUSES = ["proposed", "pending", "active"];
|
|
12
12
|
const STATUS_MAP = {
|
|
@@ -70,7 +70,7 @@ export function extractPlanItems(body) {
|
|
|
70
70
|
if (body.length === 0) {
|
|
71
71
|
return [];
|
|
72
72
|
}
|
|
73
|
-
const text =
|
|
73
|
+
const text = stripFencedCodeBlocks(body);
|
|
74
74
|
const checkboxItems = parseCheckboxItems(text);
|
|
75
75
|
if (checkboxItems.length > 0) {
|
|
76
76
|
return checkboxItems.map((item) => ({ title: item.title, status: item.status }));
|
|
@@ -313,11 +313,11 @@ export function fetchSingleIssue(repo, number, options = {}) {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
export function fetchIssue(repo, number, options = {}) {
|
|
316
|
-
const
|
|
317
|
-
if (
|
|
318
|
-
return
|
|
316
|
+
const live = fetchSingleIssue(repo, number, options);
|
|
317
|
+
if (live !== null) {
|
|
318
|
+
return live;
|
|
319
319
|
}
|
|
320
|
-
return
|
|
320
|
+
return fetchFromCache(repo, number, options);
|
|
321
321
|
}
|
|
322
322
|
export function ingestOne(issue, options) {
|
|
323
323
|
const number = Number(issue.number);
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Linear-time markdown scanners for issue body parsing (#1784 / #1248).
|
|
3
3
|
* Avoids nested/polynomial regex on issue bodies (ReDoS-safe).
|
|
4
4
|
*/
|
|
5
|
-
/** Strip fenced
|
|
5
|
+
/** Strip fenced code blocks only (preserves inline backtick spans). */
|
|
6
|
+
export declare function stripFencedCodeBlocks(body: string): string;
|
|
7
|
+
/** Strip fenced and inline code spans before cross-ref extraction. */
|
|
6
8
|
export declare function stripCodeBlocks(body: string): string;
|
|
7
9
|
export interface CheckboxItem {
|
|
8
10
|
readonly title: string;
|
|
@@ -2,16 +2,23 @@
|
|
|
2
2
|
* Linear-time markdown scanners for issue body parsing (#1784 / #1248).
|
|
3
3
|
* Avoids nested/polynomial regex on issue bodies (ReDoS-safe).
|
|
4
4
|
*/
|
|
5
|
-
/** Strip fenced
|
|
5
|
+
/** Strip fenced code blocks only (preserves inline backtick spans). */
|
|
6
|
+
export function stripFencedCodeBlocks(body) {
|
|
7
|
+
if (body.length === 0) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
return stripFencedCodeBlocksInner(body);
|
|
11
|
+
}
|
|
12
|
+
/** Strip fenced and inline code spans before cross-ref extraction. */
|
|
6
13
|
export function stripCodeBlocks(body) {
|
|
7
14
|
if (body.length === 0) {
|
|
8
15
|
return "";
|
|
9
16
|
}
|
|
10
|
-
let text =
|
|
17
|
+
let text = stripFencedCodeBlocksInner(body);
|
|
11
18
|
text = stripInlineCode(text);
|
|
12
19
|
return text;
|
|
13
20
|
}
|
|
14
|
-
function
|
|
21
|
+
function stripFencedCodeBlocksInner(text) {
|
|
15
22
|
let out = "";
|
|
16
23
|
let i = 0;
|
|
17
24
|
while (i < text.length) {
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { call } from "../scm/call.js";
|
|
5
|
+
import { updateDecomposedChildBackReferences } from "../scope/decomposed-refs.js";
|
|
5
6
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
6
7
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
7
8
|
export const LIFECYCLE_FOLDERS = [
|
|
@@ -643,6 +644,7 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
|
|
|
643
644
|
failures.push(`failed to move ${relPath} -> ${destFolder}/`);
|
|
644
645
|
continue;
|
|
645
646
|
}
|
|
647
|
+
updateDecomposedChildBackReferences(data, src, dst, vbriefDir);
|
|
646
648
|
moved += 1;
|
|
647
649
|
}
|
|
648
650
|
return [moved, skipped, failures];
|
|
@@ -14,6 +14,7 @@ import { execFileSync } from "node:child_process";
|
|
|
14
14
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
15
15
|
import { join, relative } from "node:path";
|
|
16
16
|
import { probeCacheDrift } from "../cache/fetch.js";
|
|
17
|
+
import { latestDecisionForIssue as auditLatestDecisionForIssue } from "../triage/actions/candidates-log.js";
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Public constants (mirror preflight_cache.py)
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -258,43 +259,6 @@ function candidatesLogState(candidatesPath) {
|
|
|
258
259
|
return "absent";
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
|
-
function parseCandidates(candidatesPath) {
|
|
262
|
-
if (!existsSync(candidatesPath))
|
|
263
|
-
return [];
|
|
264
|
-
try {
|
|
265
|
-
const text = readFileSync(candidatesPath, "utf8");
|
|
266
|
-
const entries = [];
|
|
267
|
-
for (const line of text.split("\n")) {
|
|
268
|
-
const trimmed = line.trim();
|
|
269
|
-
if (!trimmed)
|
|
270
|
-
continue;
|
|
271
|
-
try {
|
|
272
|
-
const data = JSON.parse(trimmed);
|
|
273
|
-
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
274
|
-
const d = data;
|
|
275
|
-
entries.push({
|
|
276
|
-
issue: Number(d.issue ?? d.issue_number ?? 0),
|
|
277
|
-
repo: String(d.repo ?? ""),
|
|
278
|
-
decision: String(d.decision ?? ""),
|
|
279
|
-
ts: String(d.ts ?? d.timestamp ?? ""),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
/* skip malformed lines */
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return entries;
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
function latestDecisionForIssue(candidates, repo, issueNumber) {
|
|
294
|
-
// Take all entries matching this issue (most recent last)
|
|
295
|
-
const matching = candidates.filter((c) => c.issue === issueNumber && c.repo === repo);
|
|
296
|
-
return matching[matching.length - 1] ?? null;
|
|
297
|
-
}
|
|
298
262
|
// ---------------------------------------------------------------------------
|
|
299
263
|
// Main evaluate function
|
|
300
264
|
// ---------------------------------------------------------------------------
|
|
@@ -541,9 +505,8 @@ function evaluateForIssue(repo, issueNumber, candidatesPath, cacheRoot, source,
|
|
|
541
505
|
}
|
|
542
506
|
}
|
|
543
507
|
}
|
|
544
|
-
// Latest-decision check
|
|
545
|
-
const
|
|
546
|
-
const latest = latestDecisionForIssue(candidates, repo, issueNumber);
|
|
508
|
+
// Latest-decision check — shared audit-log reader (#1887 / candidates-log.ts)
|
|
509
|
+
const latest = auditLatestDecisionForIssue(issueNumber, repo, candidatesPath);
|
|
547
510
|
if (latest === null) {
|
|
548
511
|
return {
|
|
549
512
|
code: 1,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GROUP_ORDER } from "./constants.js";
|
|
2
2
|
import { deriveGroup } from "./derive-group.js";
|
|
3
|
+
import { hasActiveScopeIgnores, isCachedIssueScopeIgnored, } from "./scope-ignores-filter.js";
|
|
3
4
|
import { compareSelectionKeys, matchedLabelFor, withinGroupSortKey } from "./selection.js";
|
|
4
5
|
function resolveRank(issue, number, rankByNumber) {
|
|
5
6
|
let candidate = rankByNumber.get(number);
|
|
@@ -71,6 +72,8 @@ export function buildQueue(issues, auditEntries, options) {
|
|
|
71
72
|
const dropNetNew = Boolean(opts.finishBeforeStart && opts.wipAtCap);
|
|
72
73
|
const includeBlocked = Boolean(opts.includeBlocked);
|
|
73
74
|
const limit = opts.limit;
|
|
75
|
+
const scopeIgnores = opts.scopeIgnores;
|
|
76
|
+
const filterScopeIgnores = scopeIgnores !== undefined && hasActiveScopeIgnores(scopeIgnores);
|
|
74
77
|
const decisions = latestDecisionsByIssue(auditEntries);
|
|
75
78
|
const grouped = new Map();
|
|
76
79
|
for (const group of GROUP_ORDER) {
|
|
@@ -81,6 +84,9 @@ export function buildQueue(issues, auditEntries, options) {
|
|
|
81
84
|
if (typeof n !== "number") {
|
|
82
85
|
continue;
|
|
83
86
|
}
|
|
87
|
+
if (filterScopeIgnores && isCachedIssueScopeIgnored(issue, scopeIgnores)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
84
90
|
const isContinuation = resolveContinuation(issue, n, continuationNumbers);
|
|
85
91
|
const isOrphan = orphanIssueNumbers.has(n);
|
|
86
92
|
if (dropNetNew && !isContinuation && !isOrphan) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { extractAuthor, extractMilestone } from "../scope-drift/cache-walker.js";
|
|
3
4
|
import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, DEFAULT_SLICES_LOG_REL_PATH, } from "./constants.js";
|
|
5
|
+
import { hasActiveScopeIgnores, isRawIssueScopeIgnored, resolveScopeIgnores, } from "./scope-ignores-filter.js";
|
|
4
6
|
import { blockedByIssueNumber, rankByIssueNumber } from "./scope-walk.js";
|
|
5
7
|
function cachedState(issue) {
|
|
6
8
|
if (issue === undefined) {
|
|
@@ -116,6 +118,8 @@ export function loadCachedIssues(repo, options) {
|
|
|
116
118
|
}
|
|
117
119
|
const rankMap = rankByIssueNumber(root);
|
|
118
120
|
const blockedSet = blockedByIssueNumber(root);
|
|
121
|
+
const scopeIgnores = resolveScopeIgnores(root);
|
|
122
|
+
const filterScopeIgnores = hasActiveScopeIgnores(scopeIgnores);
|
|
119
123
|
const issues = [];
|
|
120
124
|
for (const entryName of readdirSync(base)) {
|
|
121
125
|
if (!/^\d+$/.test(entryName)) {
|
|
@@ -150,11 +154,16 @@ export function loadCachedIssues(repo, options) {
|
|
|
150
154
|
if (state !== "open" && !options.includeClosed) {
|
|
151
155
|
continue;
|
|
152
156
|
}
|
|
157
|
+
if (filterScopeIgnores && isRawIssueScopeIgnored(payload, scopeIgnores)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
153
160
|
issues.push({
|
|
154
161
|
number: n,
|
|
155
162
|
title: typeof payload.title === "string" ? payload.title : "",
|
|
156
163
|
state,
|
|
157
164
|
labels: parseLabels(payload.labels),
|
|
165
|
+
author: extractAuthor(payload),
|
|
166
|
+
milestone: extractMilestone(payload),
|
|
158
167
|
updatedAt: typeof payload.updated_at === "string" ? payload.updated_at : "",
|
|
159
168
|
createdAt: typeof payload.created_at === "string" ? payload.created_at : "",
|
|
160
169
|
metadataRank: rankMap.get(n) ?? null,
|
|
@@ -6,6 +6,7 @@ export * from "./derive-group.js";
|
|
|
6
6
|
export * from "./ranking-labels.js";
|
|
7
7
|
export * from "./render.js";
|
|
8
8
|
export * from "./repo.js";
|
|
9
|
+
export * from "./scope-ignores-filter.js";
|
|
9
10
|
export * from "./scope-walk.js";
|
|
10
11
|
export * from "./selection.js";
|
|
11
12
|
export * from "./types.js";
|
|
@@ -6,6 +6,7 @@ export * from "./derive-group.js";
|
|
|
6
6
|
export * from "./ranking-labels.js";
|
|
7
7
|
export * from "./render.js";
|
|
8
8
|
export * from "./repo.js";
|
|
9
|
+
export * from "./scope-ignores-filter.js";
|
|
9
10
|
export * from "./scope-walk.js";
|
|
10
11
|
export * from "./selection.js";
|
|
11
12
|
export * from "./types.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveScopeIgnores, type ScopeIgnores } from "../scope-drift/scope-rules.js";
|
|
2
|
+
export { resolveScopeIgnores, type ScopeIgnores };
|
|
3
|
+
export declare function hasActiveScopeIgnores(ignores: ScopeIgnores): boolean;
|
|
4
|
+
/** True when a cached raw.json payload matches any triageScopeIgnores entry. */
|
|
5
|
+
export declare function isRawIssueScopeIgnored(raw: Record<string, unknown>, ignores: ScopeIgnores): boolean;
|
|
6
|
+
/** True when a queue CachedIssue matches any triageScopeIgnores entry. */
|
|
7
|
+
export declare function isCachedIssueScopeIgnored(issue: {
|
|
8
|
+
readonly labels: readonly string[];
|
|
9
|
+
readonly author?: string;
|
|
10
|
+
readonly milestone?: string;
|
|
11
|
+
}, ignores: ScopeIgnores): boolean;
|
|
12
|
+
//# sourceMappingURL=scope-ignores-filter.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { extractAuthor, extractLabels, extractMilestone } from "../scope-drift/cache-walker.js";
|
|
2
|
+
import { resolveScopeIgnores } from "../scope-drift/scope-rules.js";
|
|
3
|
+
export { resolveScopeIgnores };
|
|
4
|
+
export function hasActiveScopeIgnores(ignores) {
|
|
5
|
+
return ignores.labels.size > 0 || ignores.milestones.size > 0 || ignores.authors.size > 0;
|
|
6
|
+
}
|
|
7
|
+
/** True when a cached raw.json payload matches any triageScopeIgnores entry. */
|
|
8
|
+
export function isRawIssueScopeIgnored(raw, ignores) {
|
|
9
|
+
if (ignores.authors.size > 0 && ignores.authors.has(extractAuthor(raw))) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (ignores.labels.size > 0) {
|
|
13
|
+
for (const label of extractLabels(raw)) {
|
|
14
|
+
if (ignores.labels.has(label)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (ignores.milestones.size > 0) {
|
|
20
|
+
const milestone = extractMilestone(raw);
|
|
21
|
+
if (milestone && ignores.milestones.has(milestone)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** True when a queue CachedIssue matches any triageScopeIgnores entry. */
|
|
28
|
+
export function isCachedIssueScopeIgnored(issue, ignores) {
|
|
29
|
+
if (ignores.authors.size > 0 &&
|
|
30
|
+
issue.author !== undefined &&
|
|
31
|
+
issue.author.length > 0 &&
|
|
32
|
+
ignores.authors.has(issue.author)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (ignores.labels.size > 0) {
|
|
36
|
+
for (const label of issue.labels) {
|
|
37
|
+
if (ignores.labels.has(label)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (ignores.milestones.size > 0 &&
|
|
43
|
+
issue.milestone !== undefined &&
|
|
44
|
+
issue.milestone.length > 0 &&
|
|
45
|
+
ignores.milestones.has(issue.milestone)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=scope-ignores-filter.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QueueGroup } from "./constants.js";
|
|
2
|
+
import type { ScopeIgnores } from "./scope-ignores-filter.js";
|
|
2
3
|
/** One ranked row in buildQueue. */
|
|
3
4
|
export interface QueueItem {
|
|
4
5
|
readonly number: number;
|
|
@@ -25,6 +26,7 @@ export interface QueueBuildOptions {
|
|
|
25
26
|
readonly blockedIssueNumbers?: ReadonlySet<number>;
|
|
26
27
|
readonly includeBlocked?: boolean;
|
|
27
28
|
readonly limit?: number | null;
|
|
29
|
+
readonly scopeIgnores?: ScopeIgnores;
|
|
28
30
|
}
|
|
29
31
|
/** Cached issue row produced by loadCachedIssues. */
|
|
30
32
|
export interface CachedIssue {
|
|
@@ -32,6 +34,8 @@ export interface CachedIssue {
|
|
|
32
34
|
readonly title: string;
|
|
33
35
|
readonly state: string;
|
|
34
36
|
readonly labels: readonly string[];
|
|
37
|
+
readonly author?: string;
|
|
38
|
+
readonly milestone?: string;
|
|
35
39
|
readonly updatedAt: string;
|
|
36
40
|
readonly createdAt: string;
|
|
37
41
|
readonly metadataRank: number | null;
|
|
@@ -298,6 +298,6 @@ export function subscribe(projectRoot, options = {}) {
|
|
|
298
298
|
export function unsubscribe(projectRoot, options = {}) {
|
|
299
299
|
return mutate(projectRoot, { op: "unsubscribe", ...options });
|
|
300
300
|
}
|
|
301
|
-
export const RECONCILE_HINT = " Reconciliation: run `task triage:bootstrap
|
|
301
|
+
export const RECONCILE_HINT = " Reconciliation: run `task triage:bootstrap` to " +
|
|
302
302
|
"backfill / mark out-of-scope cached entries.";
|
|
303
303
|
//# sourceMappingURL=index.js.map
|
|
@@ -8,6 +8,8 @@ export declare const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
|
|
|
8
8
|
export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
9
9
|
export declare const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
|
|
10
10
|
export declare const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
|
|
11
|
+
/** D2 repeat-suppression window for session-start triage one-liner (#1122 / #1279). */
|
|
12
|
+
export declare const D2_SUPPRESSION_WINDOW_HOURS = 4;
|
|
11
13
|
export declare const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
|
|
12
14
|
export declare const WIP_LIFECYCLE_DIRS: readonly ["pending", "active"];
|
|
13
15
|
export declare const FILESYSTEM_IN_FLIGHT_FOLDER = "active";
|
|
@@ -45,6 +47,10 @@ export declare function countFilesystemInFlight(projectRoot: string): number;
|
|
|
45
47
|
export declare function isTriageScopeExplicitlyConfigured(projectRoot: string): boolean;
|
|
46
48
|
/** Read `plan.policy.wipCap`; always returns an integer cap. */
|
|
47
49
|
export declare function resolveWipCapInt(projectRoot: string): number;
|
|
50
|
+
/** Read `raw.json` state for a cached github-issue entry; defaults to open when absent. */
|
|
51
|
+
export declare function readCachedIssueState(cacheRoot: string, repo: string, issueNumber: number): string;
|
|
52
|
+
/** True when a cached issue should participate in summary classification counts. */
|
|
53
|
+
export declare function isCachedIssueOpen(cacheRoot: string, repo: string, issueNumber: number): boolean;
|
|
48
54
|
export declare function computeSummary(projectRoot: string, options?: ComputeSummaryOptions): SummaryResult;
|
|
49
55
|
export declare function formatOneLiner(result: SummaryResult, options?: {
|
|
50
56
|
maxChars?: number;
|
|
@@ -58,6 +64,33 @@ export declare function summaryResultToRecord(result: SummaryResult, options: {
|
|
|
58
64
|
emittedAt: string;
|
|
59
65
|
line: string;
|
|
60
66
|
}): Record<string, unknown>;
|
|
67
|
+
/** Structured fields that drive operator-visible triage summary output (#1279). */
|
|
68
|
+
export type SuppressionKeyFields = {
|
|
69
|
+
readonly cache_empty: boolean;
|
|
70
|
+
readonly untriaged: number;
|
|
71
|
+
readonly stale_defer: number;
|
|
72
|
+
readonly in_flight: number;
|
|
73
|
+
readonly in_flight_filesystem: number;
|
|
74
|
+
readonly in_flight_cache_scoped: number;
|
|
75
|
+
readonly triage_scope_configured: boolean;
|
|
76
|
+
readonly wip_count: number;
|
|
77
|
+
readonly wip_cap: number;
|
|
78
|
+
readonly repos: readonly string[];
|
|
79
|
+
readonly scope_drift: number;
|
|
80
|
+
readonly reconcilable: number;
|
|
81
|
+
};
|
|
82
|
+
export declare function suppressionKeyFieldsFromResult(result: SummaryResult): SuppressionKeyFields;
|
|
83
|
+
/** Extract D2 suppression key fields from a summary-history JSONL record. */
|
|
84
|
+
export declare function suppressionKeyFieldsFromRecord(record: Record<string, unknown>): SuppressionKeyFields | null;
|
|
85
|
+
/** Canonical stable hash for D2 suppression comparison (#1279). */
|
|
86
|
+
export declare function suppressionKey(result: SummaryResult): string;
|
|
87
|
+
export declare function suppressionKeyFromRecord(record: Record<string, unknown>): string | null;
|
|
88
|
+
/** Read the most recent JSONL record from summary-history.jsonl. */
|
|
89
|
+
export declare function readLastHistoryRecord(historyPath: string): Record<string, unknown> | null;
|
|
90
|
+
/** True when D2 should skip stdout emission (key unchanged within 4h). */
|
|
91
|
+
export declare function shouldSuppressD2Emission(result: SummaryResult, historyPath: string, options?: {
|
|
92
|
+
now?: Date;
|
|
93
|
+
}): boolean;
|
|
61
94
|
/** Match Python `json.dumps(..., sort_keys=True, ensure_ascii=False)` spacing. */
|
|
62
95
|
export declare function pythonStyleStringify(value: unknown): string;
|
|
63
96
|
/** Append a single JSONL record to summary-history.jsonl. Never throws. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join, resolve as pathResolve } from "node:path";
|
|
3
4
|
import { loadProjectDefinition, PROJECT_DEFINITION_REL_PATH } from "../../policy/resolve.js";
|
|
@@ -16,6 +17,8 @@ export const CANDIDATES_LOG_REL_PATH = AUDIT_LOG_REL_PATH;
|
|
|
16
17
|
export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
17
18
|
export const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
|
|
18
19
|
export const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
|
|
20
|
+
/** D2 repeat-suppression window for session-start triage one-liner (#1122 / #1279). */
|
|
21
|
+
export const D2_SUPPRESSION_WINDOW_HOURS = 4;
|
|
19
22
|
export const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
|
|
20
23
|
export const WIP_LIFECYCLE_DIRS = ["pending", "active"];
|
|
21
24
|
export const FILESYSTEM_IN_FLIGHT_FOLDER = "active";
|
|
@@ -155,6 +158,29 @@ export function resolveWipCapInt(projectRoot) {
|
|
|
155
158
|
function decisionKey(repo, issueNumber) {
|
|
156
159
|
return `${repo}\0${issueNumber}`;
|
|
157
160
|
}
|
|
161
|
+
/** Read `raw.json` state for a cached github-issue entry; defaults to open when absent. */
|
|
162
|
+
export function readCachedIssueState(cacheRoot, repo, issueNumber) {
|
|
163
|
+
const [owner, name] = repo.split("/", 2);
|
|
164
|
+
const rawPath = join(cacheRoot, CACHE_SOURCE, owner ?? "", name ?? "", String(issueNumber), "raw.json");
|
|
165
|
+
if (!existsSync(rawPath)) {
|
|
166
|
+
return "open";
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const raw = JSON.parse(readFileSync(rawPath, { encoding: "utf8" }));
|
|
170
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
171
|
+
return "open";
|
|
172
|
+
}
|
|
173
|
+
const stateRaw = raw.state ?? "open";
|
|
174
|
+
return typeof stateRaw === "string" ? stateRaw.toLowerCase() : "open";
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return "open";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** True when a cached issue should participate in summary classification counts. */
|
|
181
|
+
export function isCachedIssueOpen(cacheRoot, repo, issueNumber) {
|
|
182
|
+
return readCachedIssueState(cacheRoot, repo, issueNumber) !== "closed";
|
|
183
|
+
}
|
|
158
184
|
// ---------------------------------------------------------------------------
|
|
159
185
|
// compute / format / persist
|
|
160
186
|
// ---------------------------------------------------------------------------
|
|
@@ -191,6 +217,9 @@ export function computeSummary(projectRoot, options = {}) {
|
|
|
191
217
|
let staleDefer = 0;
|
|
192
218
|
const noDecisionKeys = [];
|
|
193
219
|
for (const [repo, issueNumber] of cached) {
|
|
220
|
+
if (!isCachedIssueOpen(resolvedCacheRoot, repo, issueNumber)) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
194
223
|
const decision = decisions.get(decisionKey(repo, issueNumber));
|
|
195
224
|
if (decision === undefined || decision === "reset" || !TRIAGED_DECISIONS.has(decision)) {
|
|
196
225
|
untriaged += 1;
|
|
@@ -319,6 +348,11 @@ export function summaryResultToRecord(result, options) {
|
|
|
319
348
|
schema: SUMMARY_HISTORY_SCHEMA,
|
|
320
349
|
emitted_at: options.emittedAt,
|
|
321
350
|
line: options.line,
|
|
351
|
+
...suppressionKeyFieldsFromResult(result),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
export function suppressionKeyFieldsFromResult(result) {
|
|
355
|
+
return {
|
|
322
356
|
cache_empty: result.cacheEmpty,
|
|
323
357
|
untriaged: result.untriaged,
|
|
324
358
|
stale_defer: result.staleDefer,
|
|
@@ -328,11 +362,149 @@ export function summaryResultToRecord(result, options) {
|
|
|
328
362
|
triage_scope_configured: result.triageScopeConfigured,
|
|
329
363
|
wip_count: result.wipCount,
|
|
330
364
|
wip_cap: result.wipCap,
|
|
331
|
-
repos: [...result.repos],
|
|
365
|
+
repos: [...result.repos].sort(),
|
|
332
366
|
scope_drift: result.scopeDrift,
|
|
333
367
|
reconcilable: result.reconcilable,
|
|
334
368
|
};
|
|
335
369
|
}
|
|
370
|
+
function readNumericField(record, key) {
|
|
371
|
+
const value = record[key];
|
|
372
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
373
|
+
}
|
|
374
|
+
function readBooleanField(record, key) {
|
|
375
|
+
const value = record[key];
|
|
376
|
+
return typeof value === "boolean" ? value : null;
|
|
377
|
+
}
|
|
378
|
+
function readStringArrayField(record, key) {
|
|
379
|
+
const value = record[key];
|
|
380
|
+
if (!Array.isArray(value)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
if (!value.every((entry) => typeof entry === "string")) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return [...value].sort();
|
|
387
|
+
}
|
|
388
|
+
/** Extract D2 suppression key fields from a summary-history JSONL record. */
|
|
389
|
+
export function suppressionKeyFieldsFromRecord(record) {
|
|
390
|
+
const cacheEmpty = readBooleanField(record, "cache_empty");
|
|
391
|
+
const untriaged = readNumericField(record, "untriaged");
|
|
392
|
+
const staleDefer = readNumericField(record, "stale_defer");
|
|
393
|
+
const inFlight = readNumericField(record, "in_flight");
|
|
394
|
+
const inFlightFilesystem = readNumericField(record, "in_flight_filesystem");
|
|
395
|
+
const inFlightCacheScoped = readNumericField(record, "in_flight_cache_scoped");
|
|
396
|
+
const triageScopeConfigured = readBooleanField(record, "triage_scope_configured");
|
|
397
|
+
const wipCount = readNumericField(record, "wip_count");
|
|
398
|
+
const wipCap = readNumericField(record, "wip_cap");
|
|
399
|
+
const repos = readStringArrayField(record, "repos");
|
|
400
|
+
const scopeDrift = readNumericField(record, "scope_drift");
|
|
401
|
+
const reconcilable = readNumericField(record, "reconcilable");
|
|
402
|
+
if (cacheEmpty === null ||
|
|
403
|
+
untriaged === null ||
|
|
404
|
+
staleDefer === null ||
|
|
405
|
+
inFlight === null ||
|
|
406
|
+
inFlightFilesystem === null ||
|
|
407
|
+
inFlightCacheScoped === null ||
|
|
408
|
+
triageScopeConfigured === null ||
|
|
409
|
+
wipCount === null ||
|
|
410
|
+
wipCap === null ||
|
|
411
|
+
repos === null ||
|
|
412
|
+
scopeDrift === null ||
|
|
413
|
+
reconcilable === null) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
cache_empty: cacheEmpty,
|
|
418
|
+
untriaged,
|
|
419
|
+
stale_defer: staleDefer,
|
|
420
|
+
in_flight: inFlight,
|
|
421
|
+
in_flight_filesystem: inFlightFilesystem,
|
|
422
|
+
in_flight_cache_scoped: inFlightCacheScoped,
|
|
423
|
+
triage_scope_configured: triageScopeConfigured,
|
|
424
|
+
wip_count: wipCount,
|
|
425
|
+
wip_cap: wipCap,
|
|
426
|
+
repos,
|
|
427
|
+
scope_drift: scopeDrift,
|
|
428
|
+
reconcilable,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/** Canonical stable hash for D2 suppression comparison (#1279). */
|
|
432
|
+
export function suppressionKey(result) {
|
|
433
|
+
return createHash("sha256")
|
|
434
|
+
.update(pythonStyleStringify(suppressionKeyFieldsFromResult(result)))
|
|
435
|
+
.digest("hex");
|
|
436
|
+
}
|
|
437
|
+
export function suppressionKeyFromRecord(record) {
|
|
438
|
+
const fields = suppressionKeyFieldsFromRecord(record);
|
|
439
|
+
if (fields === null) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return createHash("sha256").update(pythonStyleStringify(fields)).digest("hex");
|
|
443
|
+
}
|
|
444
|
+
function parseHistoryEmittedAt(record) {
|
|
445
|
+
const raw = record.emitted_at;
|
|
446
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
let candidate = raw.trim();
|
|
450
|
+
if (candidate.endsWith("Z")) {
|
|
451
|
+
candidate = `${candidate.slice(0, -1)}+00:00`;
|
|
452
|
+
}
|
|
453
|
+
const parsed = new Date(candidate);
|
|
454
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
455
|
+
}
|
|
456
|
+
/** Read the most recent JSONL record from summary-history.jsonl. */
|
|
457
|
+
export function readLastHistoryRecord(historyPath) {
|
|
458
|
+
if (!existsSync(historyPath)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
let text;
|
|
462
|
+
try {
|
|
463
|
+
text = readFileSync(historyPath, { encoding: "utf8" });
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
469
|
+
if (lines.length === 0) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const last = lines[lines.length - 1];
|
|
473
|
+
if (last === undefined) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const parsed = JSON.parse(last);
|
|
478
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
479
|
+
return parsed;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
/** True when D2 should skip stdout emission (key unchanged within 4h). */
|
|
488
|
+
export function shouldSuppressD2Emission(result, historyPath, options = {}) {
|
|
489
|
+
const prior = readLastHistoryRecord(historyPath);
|
|
490
|
+
if (prior === null) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
const emittedAt = parseHistoryEmittedAt(prior);
|
|
494
|
+
if (emittedAt === null) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const now = options.now ?? new Date();
|
|
498
|
+
const ageMs = now.getTime() - emittedAt.getTime();
|
|
499
|
+
if (ageMs < 0 || ageMs >= D2_SUPPRESSION_WINDOW_HOURS * 3_600_000) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const priorKey = suppressionKeyFromRecord(prior);
|
|
503
|
+
if (priorKey === null) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
return priorKey === suppressionKey(result);
|
|
507
|
+
}
|
|
336
508
|
function sortKeysDeep(value) {
|
|
337
509
|
if (value === null || typeof value !== "object") {
|
|
338
510
|
return value;
|
|
@@ -16,6 +16,7 @@ export interface DefaultModeOptions {
|
|
|
16
16
|
readonly writeHistory?: boolean;
|
|
17
17
|
readonly taskPrefix?: string | null;
|
|
18
18
|
readonly selfHealFn?: (projectRoot: string) => void;
|
|
19
|
+
readonly now?: Date;
|
|
19
20
|
}
|
|
20
21
|
/** Non-interactive default mode (#1309). */
|
|
21
22
|
export declare function runDefaultMode(projectRoot: string, options?: DefaultModeOptions): WelcomeOutcome;
|
|
@@ -19,5 +19,7 @@ export declare function appendHistory(historyPath: string, result: SummaryResult
|
|
|
19
19
|
export declare function emitOneliner(projectRoot: string, options?: {
|
|
20
20
|
writeHistory?: boolean;
|
|
21
21
|
output?: (line: string) => void;
|
|
22
|
+
now?: Date;
|
|
23
|
+
applyD2Suppression?: boolean;
|
|
22
24
|
}): string;
|
|
23
25
|
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -3,7 +3,8 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { countVbriefWip, DEFAULT_WIP_CAP, resolveWipCap } from "../../policy/wip.js";
|
|
4
4
|
import { countReconcilable } from "../reconcile/reconcile.js";
|
|
5
5
|
import { computeDrift } from "../scope-drift/compute.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SUMMARY_HISTORY_REL_PATH, shouldSuppressD2Emission } from "../summary/index.js";
|
|
7
|
+
import { CACHE_DIR_NAME, CACHE_SOURCE, EMPTY_CACHE_LINE, MAX_LINE_CHARS, SUMMARY_HISTORY_SCHEMA, WIP_WARN_GLYPH, } from "./constants.js";
|
|
7
8
|
const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
|
|
8
9
|
const FILESYSTEM_IN_FLIGHT_STATUS = "running";
|
|
9
10
|
const TRIAGED_DECISIONS = new Set([
|
|
@@ -291,12 +292,17 @@ export function appendHistory(historyPath, result, line) {
|
|
|
291
292
|
export function emitOneliner(projectRoot, options = {}) {
|
|
292
293
|
const result = computeSummary(projectRoot);
|
|
293
294
|
const line = formatSummary(result);
|
|
295
|
+
const historyPath = join(resolve(projectRoot), SUMMARY_HISTORY_REL_PATH);
|
|
296
|
+
const applySuppression = options.applyD2Suppression !== false;
|
|
297
|
+
if (applySuppression && shouldSuppressD2Emission(result, historyPath, { now: options.now })) {
|
|
298
|
+
return line;
|
|
299
|
+
}
|
|
294
300
|
const out = options.output ?? ((l) => process.stdout.write(`${l}\n`));
|
|
295
301
|
for (const physicalLine of line.split("\n")) {
|
|
296
302
|
out(physicalLine);
|
|
297
303
|
}
|
|
298
304
|
if (options.writeHistory !== false) {
|
|
299
|
-
appendHistory(
|
|
305
|
+
appendHistory(historyPath, result, line);
|
|
300
306
|
}
|
|
301
307
|
return line;
|
|
302
308
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contract-drift.ts — deterministic gate for the public contract layer (#1799).
|
|
3
|
+
*
|
|
4
|
+
* Ensures:
|
|
5
|
+
* 1. packages/types/schemas/vbrief-core-0.6.schema.json matches the canonical
|
|
6
|
+
* content/vbrief/schemas/vbrief-core.schema.json byte-for-byte.
|
|
7
|
+
* 2. @deftai/directive-types Status enum matches the schema Status enum.
|
|
8
|
+
* 3. @deftai/directive-types VBRIEF_VERSION matches the schema version const.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes: 0 clean / 1 drift / 2 config error.
|
|
11
|
+
*/
|
|
12
|
+
import { type GateExitCode } from "@deftai/directive-types";
|
|
13
|
+
export declare const EXIT_OK = 0;
|
|
14
|
+
export declare const EXIT_DRIFT = 1;
|
|
15
|
+
export declare const EXIT_CONFIG_ERROR = 2;
|
|
16
|
+
export declare const CANONICAL_SCHEMA_REL = "content/vbrief/schemas/vbrief-core.schema.json";
|
|
17
|
+
export declare const PUBLISHED_SCHEMA_REL = "packages/types/schemas/vbrief-core-0.6.schema.json";
|
|
18
|
+
export interface ContractDriftResult {
|
|
19
|
+
readonly code: GateExitCode;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly stream: "stdout" | "stderr";
|
|
22
|
+
}
|
|
23
|
+
export interface ContractDriftOptions {
|
|
24
|
+
readonly root: string;
|
|
25
|
+
readonly readText?: (path: string) => string;
|
|
26
|
+
}
|
|
27
|
+
/** Evaluate contract drift for the directive source tree. */
|
|
28
|
+
export declare function evaluateContractDrift(projectRoot: string, options?: Partial<ContractDriftOptions>): ContractDriftResult;
|
|
29
|
+
//# sourceMappingURL=contract-drift.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contract-drift.ts — deterministic gate for the public contract layer (#1799).
|
|
3
|
+
*
|
|
4
|
+
* Ensures:
|
|
5
|
+
* 1. packages/types/schemas/vbrief-core-0.6.schema.json matches the canonical
|
|
6
|
+
* content/vbrief/schemas/vbrief-core.schema.json byte-for-byte.
|
|
7
|
+
* 2. @deftai/directive-types Status enum matches the schema Status enum.
|
|
8
|
+
* 3. @deftai/directive-types VBRIEF_VERSION matches the schema version const.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes: 0 clean / 1 drift / 2 config error.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { VALID_STATUSES, VBRIEF_VERSION } from "@deftai/directive-types";
|
|
15
|
+
export const EXIT_OK = 0;
|
|
16
|
+
export const EXIT_DRIFT = 1;
|
|
17
|
+
export const EXIT_CONFIG_ERROR = 2;
|
|
18
|
+
export const CANONICAL_SCHEMA_REL = "content/vbrief/schemas/vbrief-core.schema.json";
|
|
19
|
+
export const PUBLISHED_SCHEMA_REL = "packages/types/schemas/vbrief-core-0.6.schema.json";
|
|
20
|
+
function defaultReadText(path) {
|
|
21
|
+
return readFileSync(path, "utf8");
|
|
22
|
+
}
|
|
23
|
+
function loadSchemaJson(text, label) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
27
|
+
throw new Error(`${label} is not a JSON object`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
33
|
+
throw new Error(`${label} is not valid JSON: ${detail}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function schemaStatusEnum(schema) {
|
|
37
|
+
const defs = schema.$defs;
|
|
38
|
+
if (typeof defs !== "object" || defs === null || Array.isArray(defs)) {
|
|
39
|
+
throw new Error("schema missing $defs");
|
|
40
|
+
}
|
|
41
|
+
const status = defs.Status;
|
|
42
|
+
if (typeof status !== "object" || status === null || Array.isArray(status)) {
|
|
43
|
+
throw new Error("schema missing $defs.Status");
|
|
44
|
+
}
|
|
45
|
+
const enumValues = status.enum;
|
|
46
|
+
if (!Array.isArray(enumValues) || enumValues.some((v) => typeof v !== "string")) {
|
|
47
|
+
throw new Error("schema $defs.Status.enum must be a string array");
|
|
48
|
+
}
|
|
49
|
+
return [...enumValues];
|
|
50
|
+
}
|
|
51
|
+
function schemaVersionConst(schema) {
|
|
52
|
+
const defs = schema.$defs;
|
|
53
|
+
if (typeof defs !== "object" || defs === null || Array.isArray(defs)) {
|
|
54
|
+
throw new Error("schema missing $defs");
|
|
55
|
+
}
|
|
56
|
+
const info = defs.vBRIEFInfo;
|
|
57
|
+
if (typeof info !== "object" || info === null || Array.isArray(info)) {
|
|
58
|
+
throw new Error("schema missing $defs.vBRIEFInfo");
|
|
59
|
+
}
|
|
60
|
+
const properties = info.properties;
|
|
61
|
+
if (typeof properties !== "object" || properties === null || Array.isArray(properties)) {
|
|
62
|
+
throw new Error("schema missing $defs.vBRIEFInfo.properties");
|
|
63
|
+
}
|
|
64
|
+
const version = properties.version;
|
|
65
|
+
if (typeof version !== "object" || version === null || Array.isArray(version)) {
|
|
66
|
+
throw new Error("schema missing $defs.vBRIEFInfo.properties.version");
|
|
67
|
+
}
|
|
68
|
+
const constValue = version.const;
|
|
69
|
+
if (typeof constValue !== "string") {
|
|
70
|
+
throw new Error("schema $defs.vBRIEFInfo.properties.version.const must be a string");
|
|
71
|
+
}
|
|
72
|
+
return constValue;
|
|
73
|
+
}
|
|
74
|
+
function sortedStrings(values) {
|
|
75
|
+
return [...values].sort();
|
|
76
|
+
}
|
|
77
|
+
/** Evaluate contract drift for the directive source tree. */
|
|
78
|
+
export function evaluateContractDrift(projectRoot, options = {}) {
|
|
79
|
+
const root = resolve(projectRoot);
|
|
80
|
+
const readText = options.readText ?? defaultReadText;
|
|
81
|
+
const canonicalPath = join(root, CANONICAL_SCHEMA_REL);
|
|
82
|
+
const publishedPath = join(root, PUBLISHED_SCHEMA_REL);
|
|
83
|
+
let canonicalText;
|
|
84
|
+
let publishedText;
|
|
85
|
+
try {
|
|
86
|
+
canonicalText = readText(canonicalPath);
|
|
87
|
+
publishedText = readText(publishedPath);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return {
|
|
92
|
+
code: EXIT_CONFIG_ERROR,
|
|
93
|
+
message: `contract-drift: config error — could not read schema files (${detail}). Run pnpm --prefix packages/types run prebuild.`,
|
|
94
|
+
stream: "stderr",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (canonicalText !== publishedText) {
|
|
98
|
+
return {
|
|
99
|
+
code: EXIT_DRIFT,
|
|
100
|
+
message: `contract-drift: ${PUBLISHED_SCHEMA_REL} is out of sync with ${CANONICAL_SCHEMA_REL}. ` +
|
|
101
|
+
"Run: pnpm --prefix packages/types run prebuild",
|
|
102
|
+
stream: "stderr",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
let schema;
|
|
106
|
+
try {
|
|
107
|
+
schema = loadSchemaJson(canonicalText, CANONICAL_SCHEMA_REL);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
111
|
+
return {
|
|
112
|
+
code: EXIT_CONFIG_ERROR,
|
|
113
|
+
message: `contract-drift: config error — ${detail}`,
|
|
114
|
+
stream: "stderr",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const schemaStatuses = sortedStrings(schemaStatusEnum(schema));
|
|
119
|
+
const typeStatuses = sortedStrings(VALID_STATUSES);
|
|
120
|
+
if (schemaStatuses.join("|") !== typeStatuses.join("|")) {
|
|
121
|
+
return {
|
|
122
|
+
code: EXIT_DRIFT,
|
|
123
|
+
message: "contract-drift: VALID_STATUSES in @deftai/directive-types diverges from " +
|
|
124
|
+
`$defs.Status.enum in ${CANONICAL_SCHEMA_REL}.`,
|
|
125
|
+
stream: "stderr",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const schemaVersion = schemaVersionConst(schema);
|
|
129
|
+
if (schemaVersion !== VBRIEF_VERSION) {
|
|
130
|
+
return {
|
|
131
|
+
code: EXIT_DRIFT,
|
|
132
|
+
message: "contract-drift: VBRIEF_VERSION in @deftai/directive-types diverges from " +
|
|
133
|
+
"schema vBRIEFInfo.version const.",
|
|
134
|
+
stream: "stderr",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
140
|
+
return {
|
|
141
|
+
code: EXIT_CONFIG_ERROR,
|
|
142
|
+
message: `contract-drift: config error — ${detail}`,
|
|
143
|
+
stream: "stderr",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
code: EXIT_OK,
|
|
148
|
+
message: "contract-drift: canonical schema, published copy, and TS contract constants are in sync.",
|
|
149
|
+
stream: "stdout",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=contract-drift.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./code-structure-validate.js";
|
|
2
2
|
export * from "./content-manifest.js";
|
|
3
|
+
export { CANONICAL_SCHEMA_REL, type ContractDriftOptions, type ContractDriftResult, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
|
|
3
4
|
export * from "./cursor-tier1.js";
|
|
4
5
|
export * from "./python-call-scan.js";
|
|
5
6
|
export * from "./rule-ownership-lint.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./code-structure-validate.js";
|
|
2
2
|
export * from "./content-manifest.js";
|
|
3
|
+
export { CANONICAL_SCHEMA_REL, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
|
|
3
4
|
export * from "./cursor-tier1.js";
|
|
4
5
|
export * from "./python-call-scan.js";
|
|
5
6
|
export * from "./rule-ownership-lint.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -229,8 +229,8 @@
|
|
|
229
229
|
"provenance": true
|
|
230
230
|
},
|
|
231
231
|
"dependencies": {
|
|
232
|
-
"@deftai/directive-content": "^0.
|
|
233
|
-
"@deftai/directive-types": "^0.
|
|
232
|
+
"@deftai/directive-content": "^0.65.0",
|
|
233
|
+
"@deftai/directive-types": "^0.65.0",
|
|
234
234
|
"archiver": "^8.0.0"
|
|
235
235
|
},
|
|
236
236
|
"scripts": {
|