@deftai/directive-core 0.55.2 → 0.56.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/cache/errors.d.ts +4 -1
- package/dist/cache/errors.js +8 -2
- package/dist/cache/fetch.d.ts +50 -0
- package/dist/cache/fetch.js +224 -3
- package/dist/cache/main.js +10 -5
- package/dist/cache/operations.js +6 -4
- package/dist/deposit/copy-tree.d.ts +16 -0
- package/dist/deposit/copy-tree.js +58 -0
- package/dist/deposit/resolve-content.d.ts +28 -0
- package/dist/deposit/resolve-content.js +70 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +37 -0
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/gitignore.d.ts +51 -0
- package/dist/init-deposit/gitignore.js +200 -0
- package/dist/init-deposit/index.d.ts +6 -0
- package/dist/init-deposit/index.js +6 -0
- package/dist/init-deposit/init-deposit.d.ts +44 -0
- package/dist/init-deposit/init-deposit.js +197 -0
- package/dist/init-deposit/legacy-detect.d.ts +61 -0
- package/dist/init-deposit/legacy-detect.js +223 -0
- package/dist/init-deposit/migrate.d.ts +88 -0
- package/dist/init-deposit/migrate.js +196 -0
- package/dist/init-deposit/refresh.d.ts +50 -0
- package/dist/init-deposit/refresh.js +292 -0
- package/dist/init-deposit/scaffold.d.ts +41 -0
- package/dist/init-deposit/scaffold.js +753 -0
- package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
- package/dist/legacy-bridge/bridge-drift.js +171 -0
- package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
- package/dist/legacy-bridge/freeze-gate.js +168 -0
- package/dist/legacy-bridge/index.d.ts +4 -0
- package/dist/legacy-bridge/index.js +4 -0
- package/dist/legacy-bridge/sot.d.ts +45 -0
- package/dist/legacy-bridge/sot.js +49 -0
- package/dist/preflight-cache/evaluate.d.ts +11 -0
- package/dist/preflight-cache/evaluate.js +63 -13
- package/dist/release-e2e/flags.js +5 -1
- package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
- package/dist/release-e2e/legacy-bridge-leg.js +460 -0
- package/dist/release-e2e/main.d.ts +2 -1
- package/dist/release-e2e/main.js +17 -1
- package/dist/release-e2e/types.d.ts +9 -0
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/project-render.js +4 -3
- package/dist/render/roadmap-render.d.ts +4 -2
- package/dist/render/roadmap-render.js +14 -7
- package/dist/triage/actions/candidates-log.d.ts +12 -1
- package/dist/triage/actions/candidates-log.js +44 -7
- package/dist/triage/actions/index.d.ts +25 -1
- package/dist/triage/actions/index.js +94 -3
- package/dist/triage/summary/index.d.ts +1 -3
- package/dist/triage/summary/index.js +6 -57
- package/dist/triage/summary/reconcilable.js +4 -33
- package/dist/triage/welcome/default-mode.d.ts +1 -0
- package/dist/triage/welcome/default-mode.js +6 -0
- package/dist/vbrief-validate/project-definition.js +4 -69
- package/dist/vbrief-validate/registry-status.d.ts +20 -0
- package/dist/vbrief-validate/registry-status.js +85 -0
- package/package.json +11 -2
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import type { CandidatesLog } from "./types.js";
|
|
1
|
+
import type { AuditEntry, CandidatesLog } from "./types.js";
|
|
2
2
|
export declare const AUDIT_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
|
|
3
|
+
/** Read every well-formed audit-log row in insertion order (#1698 shared reader). */
|
|
4
|
+
export declare function readAuditLog(logPath: string, repo?: string | null): AuditEntry[];
|
|
5
|
+
/** Return every entry for ``(repo, issue_number)`` in insertion order. */
|
|
6
|
+
export declare function findByIssue(issueNumber: number, repo: string, logPath: string): AuditEntry[];
|
|
7
|
+
/**
|
|
8
|
+
* Canonical latest decision for ``(repo, issue_number)`` — no actor filter.
|
|
9
|
+
* Backfilled ``agent:bootstrap`` / ``agent:reconcile`` entries count equally.
|
|
10
|
+
*/
|
|
11
|
+
export declare function latestDecisionForIssue(issueNumber: number, repo: string, logPath: string): AuditEntry | null;
|
|
12
|
+
/** Collapse audit-log entries to ``{(repo, issue_number): decision}`` by timestamp. */
|
|
13
|
+
export declare function latestDecisions(entries: Iterable<AuditEntry | Record<string, unknown>>): Map<string, string>;
|
|
3
14
|
/** Filesystem-backed candidates audit log (mirrors ``scripts/candidates_log.py``). */
|
|
4
15
|
export declare function createCandidatesLog(defaultProjectRoot: string): CandidatesLog;
|
|
5
16
|
/** Remove the audit-log line whose ``decision_id`` matches (rollback path). */
|
|
@@ -113,7 +113,8 @@ function stableStringify(entry) {
|
|
|
113
113
|
}
|
|
114
114
|
return JSON.stringify(sorted);
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
/** Read every well-formed audit-log row in insertion order (#1698 shared reader). */
|
|
117
|
+
export function readAuditLog(logPath, repo = null) {
|
|
117
118
|
if (!existsSync(logPath)) {
|
|
118
119
|
return [];
|
|
119
120
|
}
|
|
@@ -132,7 +133,47 @@ function readAll(repo, logPath) {
|
|
|
132
133
|
continue;
|
|
133
134
|
out.push(row);
|
|
134
135
|
}
|
|
135
|
-
catch {
|
|
136
|
+
catch {
|
|
137
|
+
// tolerate malformed lines — mirrors candidates_log.read_all
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** Return every entry for ``(repo, issue_number)`` in insertion order. */
|
|
143
|
+
export function findByIssue(issueNumber, repo, logPath) {
|
|
144
|
+
return readAuditLog(logPath, repo).filter((row) => row.issue_number === issueNumber);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Canonical latest decision for ``(repo, issue_number)`` — no actor filter.
|
|
148
|
+
* Backfilled ``agent:bootstrap`` / ``agent:reconcile`` entries count equally.
|
|
149
|
+
*/
|
|
150
|
+
export function latestDecisionForIssue(issueNumber, repo, logPath) {
|
|
151
|
+
const rows = findByIssue(issueNumber, repo, logPath);
|
|
152
|
+
if (rows.length === 0)
|
|
153
|
+
return null;
|
|
154
|
+
rows.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
155
|
+
return rows.at(-1) ?? null;
|
|
156
|
+
}
|
|
157
|
+
/** Collapse audit-log entries to ``{(repo, issue_number): decision}`` by timestamp. */
|
|
158
|
+
export function latestDecisions(entries) {
|
|
159
|
+
const rows = [];
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const repo = entry.repo;
|
|
162
|
+
const issueNumber = entry.issue_number;
|
|
163
|
+
const decision = entry.decision;
|
|
164
|
+
const timestamp = entry.timestamp;
|
|
165
|
+
if (typeof repo === "string" &&
|
|
166
|
+
typeof issueNumber === "number" &&
|
|
167
|
+
Number.isInteger(issueNumber) &&
|
|
168
|
+
typeof decision === "string" &&
|
|
169
|
+
typeof timestamp === "string") {
|
|
170
|
+
rows.push([timestamp, repo, issueNumber, decision]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
rows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
174
|
+
const out = new Map();
|
|
175
|
+
for (const [_ts, repo, n, decision] of rows) {
|
|
176
|
+
out.set(`${repo}\0${n}`, decision);
|
|
136
177
|
}
|
|
137
178
|
return out;
|
|
138
179
|
}
|
|
@@ -148,11 +189,7 @@ export function createCandidatesLog(defaultProjectRoot) {
|
|
|
148
189
|
},
|
|
149
190
|
latestDecision(issueNumber, repo, options) {
|
|
150
191
|
const logPath = resolveLogPath(defaultProjectRoot, options?.path);
|
|
151
|
-
|
|
152
|
-
if (rows.length === 0)
|
|
153
|
-
return null;
|
|
154
|
-
rows.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
155
|
-
return rows.at(-1) ?? null;
|
|
192
|
+
return latestDecisionForIssue(issueNumber, repo, logPath);
|
|
156
193
|
},
|
|
157
194
|
newDecisionId() {
|
|
158
195
|
return randomUUID();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AcceptOptions, AuditEntry, DeferOptions, RejectOptions, TriageActionsDeps } from "./types.js";
|
|
2
|
-
export { AUDIT_LOG_REL_PATH, createCandidatesLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
|
|
2
|
+
export { AUDIT_LOG_REL_PATH, createCandidatesLog, findByIssue, latestDecisionForIssue, latestDecisions, readAuditLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
|
|
3
3
|
export { CandidatesLogError, ResumeGrammarError, TriageError, UpstreamCloseError, } from "./errors.js";
|
|
4
4
|
export { parseResumeOn } from "./resume-on.js";
|
|
5
5
|
export type { AcceptOptions, AuditEntry, CandidatesLog, DeferOptions, IssueIngest, RejectOptions, ScmRunner, TriageActionsDeps, } from "./types.js";
|
|
@@ -8,12 +8,36 @@ export declare const REJECTED_LABEL_COLOR = "B60205";
|
|
|
8
8
|
export declare const REJECTED_LABEL_DESCRIPTION = "Issue rejected during deft triage";
|
|
9
9
|
/** Default dependency bundle for production CLI use. */
|
|
10
10
|
export declare function createDefaultDeps(projectRoot: string): TriageActionsDeps;
|
|
11
|
+
/** Return audit entries for ``issueNumber`` ordered ascending by timestamp. */
|
|
12
|
+
export declare function history(issueNumber: number, repo: string, _deps: TriageActionsDeps, options?: {
|
|
13
|
+
projectRoot?: string;
|
|
14
|
+
}): AuditEntry[];
|
|
11
15
|
/** Record an accept audit entry and delegate vBRIEF authoring to issue_ingest. */
|
|
12
16
|
export declare function accept(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: AcceptOptions): string;
|
|
13
17
|
/** Close upstream, best-effort label, record reject audit entry. */
|
|
14
18
|
export declare function reject(issueNumber: number, repo: string, reason: string, deps: TriageActionsDeps, options?: RejectOptions): string;
|
|
15
19
|
/** Record a defer audit entry (#1123 -- structured reason + resume_on). */
|
|
16
20
|
export declare function deferAction(issueNumber: number, repo: string, reason: string | null | undefined, deps: TriageActionsDeps, options?: DeferOptions): string;
|
|
21
|
+
/** Record a needs-ac audit entry and best-effort post an AC-request comment upstream. */
|
|
22
|
+
export declare function needsAc(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: {
|
|
23
|
+
actor?: string | null;
|
|
24
|
+
comment?: string | null;
|
|
25
|
+
projectRoot?: string;
|
|
26
|
+
}): string;
|
|
27
|
+
/** Validate duplicate target in cache and record a mark-duplicate audit entry. */
|
|
28
|
+
export declare function markDuplicate(issueNumber: number, repo: string, ofN: number, deps: TriageActionsDeps, options?: {
|
|
29
|
+
actor?: string | null;
|
|
30
|
+
projectRoot?: string;
|
|
31
|
+
}): string;
|
|
32
|
+
/** Return the latest decision for ``issueNumber`` in ``repo`` (null if none). */
|
|
33
|
+
export declare function status(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: {
|
|
34
|
+
projectRoot?: string;
|
|
35
|
+
}): AuditEntry | null;
|
|
36
|
+
/** Record a reset audit entry referencing the prior decision_id (history is never deleted). */
|
|
37
|
+
export declare function reset(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: {
|
|
38
|
+
actor?: string | null;
|
|
39
|
+
projectRoot?: string;
|
|
40
|
+
}): string;
|
|
17
41
|
/** Format a decision entry for CLI ``status`` / ``history`` output. */
|
|
18
42
|
export declare function formatDecision(entry: AuditEntry | null): string;
|
|
19
43
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CacheNotFoundError } from "../../cache/errors.js";
|
|
5
|
+
import { cacheGet } from "../../cache/operations.js";
|
|
4
6
|
import { call } from "../../scm/call.js";
|
|
5
7
|
import { ScmStubError } from "../../scm/errors.js";
|
|
6
|
-
import { createCandidatesLog, resolveAuditLogPath, rollbackAuditEntry } from "./candidates-log.js";
|
|
8
|
+
import { createCandidatesLog, findByIssue, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
|
|
7
9
|
import { TriageError, UpstreamCloseError } from "./errors.js";
|
|
8
10
|
import { parseResumeOn } from "./resume-on.js";
|
|
9
|
-
export { AUDIT_LOG_REL_PATH, createCandidatesLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
|
|
11
|
+
export { AUDIT_LOG_REL_PATH, createCandidatesLog, findByIssue, latestDecisionForIssue, latestDecisions, readAuditLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
|
|
10
12
|
export { CandidatesLogError, ResumeGrammarError, TriageError, UpstreamCloseError, } from "./errors.js";
|
|
11
13
|
export { parseResumeOn } from "./resume-on.js";
|
|
12
14
|
export const REJECTED_LABEL = "triage-rejected";
|
|
@@ -14,6 +16,8 @@ export const REJECTED_LABEL_COLOR = "B60205";
|
|
|
14
16
|
export const REJECTED_LABEL_DESCRIPTION = "Issue rejected during deft triage";
|
|
15
17
|
const TERMINAL_DECISIONS = new Set(["accept", "reject", "mark-duplicate"]);
|
|
16
18
|
const DEFAULT_ACTOR = "agent:triage";
|
|
19
|
+
const DEFAULT_NEEDS_AC_COMMENT = "This issue lacks acceptance criteria. Please add a Test/Acceptance " +
|
|
20
|
+
"narrative before this can be triaged. (deft #845)";
|
|
17
21
|
function resolveDeftRoot() {
|
|
18
22
|
if (process.env.DEFT_ROOT !== undefined && process.env.DEFT_ROOT.length > 0) {
|
|
19
23
|
return resolve(process.env.DEFT_ROOT);
|
|
@@ -88,13 +92,24 @@ function defaultIssueIngest(deftRoot) {
|
|
|
88
92
|
/** Default dependency bundle for production CLI use. */
|
|
89
93
|
export function createDefaultDeps(projectRoot) {
|
|
90
94
|
const deftRoot = resolveDeftRoot();
|
|
91
|
-
|
|
95
|
+
const deps = {
|
|
92
96
|
candidatesLog: createCandidatesLog(projectRoot),
|
|
93
97
|
issueIngest: defaultIssueIngest(deftRoot),
|
|
94
98
|
scm: defaultScmRunner(),
|
|
95
99
|
nowIso: defaultNowIso,
|
|
96
100
|
stderr: (message) => process.stderr.write(`${message}\n`),
|
|
97
101
|
};
|
|
102
|
+
if (process.env.DEFT_TRIAGE_ACTIONS_PARITY === "1") {
|
|
103
|
+
return {
|
|
104
|
+
...deps,
|
|
105
|
+
scm: {
|
|
106
|
+
call() {
|
|
107
|
+
throw new UpstreamCloseError("gh disabled for parity");
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return deps;
|
|
98
113
|
}
|
|
99
114
|
function buildEntry(deps, decision, issueNumber, repo, actor, extras = {}) {
|
|
100
115
|
const entry = {
|
|
@@ -111,6 +126,16 @@ function buildEntry(deps, decision, issueNumber, repo, actor, extras = {}) {
|
|
|
111
126
|
function logPathFor(projectRoot) {
|
|
112
127
|
return resolveAuditLogPath(projectRoot);
|
|
113
128
|
}
|
|
129
|
+
function cacheRootFor(projectRoot) {
|
|
130
|
+
return join(projectRoot, ".deft-cache");
|
|
131
|
+
}
|
|
132
|
+
/** Return audit entries for ``issueNumber`` ordered ascending by timestamp. */
|
|
133
|
+
export function history(issueNumber, repo, _deps, options = {}) {
|
|
134
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
135
|
+
const entries = findByIssue(issueNumber, repo, logPathFor(projectRoot));
|
|
136
|
+
entries.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
137
|
+
return entries;
|
|
138
|
+
}
|
|
114
139
|
function isIdempotentRepeat(deps, issueNumber, repo, decision, projectRoot, linkedTo) {
|
|
115
140
|
if (!TERMINAL_DECISIONS.has(decision))
|
|
116
141
|
return null;
|
|
@@ -277,6 +302,72 @@ export function deferAction(issueNumber, repo, reason, deps, options = {}) {
|
|
|
277
302
|
const logPath = logPathFor(projectRoot);
|
|
278
303
|
return deps.candidatesLog.append(entry, { path: logPath });
|
|
279
304
|
}
|
|
305
|
+
/** Record a needs-ac audit entry and best-effort post an AC-request comment upstream. */
|
|
306
|
+
export function needsAc(issueNumber, repo, deps, options = {}) {
|
|
307
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
308
|
+
const body = options.comment ?? DEFAULT_NEEDS_AC_COMMENT;
|
|
309
|
+
const actor = resolveActor(options.actor);
|
|
310
|
+
const entry = buildEntry(deps, "needs-ac", issueNumber, repo, actor, { reason: body });
|
|
311
|
+
const logPath = logPathFor(projectRoot);
|
|
312
|
+
const decisionId = deps.candidatesLog.append(entry, { path: logPath });
|
|
313
|
+
const writeErr = deps.stderr ?? ((message) => process.stderr.write(`${message}\n`));
|
|
314
|
+
try {
|
|
315
|
+
runGh(deps, ["issue", "comment", String(issueNumber), "--repo", repo, "--body", body]);
|
|
316
|
+
}
|
|
317
|
+
catch (exc) {
|
|
318
|
+
writeErr(`triage_actions: needs-ac comment not posted for #${issueNumber} (${repo}): ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
319
|
+
}
|
|
320
|
+
return decisionId;
|
|
321
|
+
}
|
|
322
|
+
/** Validate duplicate target in cache and record a mark-duplicate audit entry. */
|
|
323
|
+
export function markDuplicate(issueNumber, repo, ofN, deps, options = {}) {
|
|
324
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
325
|
+
if (ofN === issueNumber) {
|
|
326
|
+
throw new TriageError(`mark-duplicate target #${ofN} cannot equal source #${issueNumber}`);
|
|
327
|
+
}
|
|
328
|
+
const key = `${repo}/${ofN}`;
|
|
329
|
+
try {
|
|
330
|
+
cacheGet("github-issue", key, { allowStale: true, cacheRoot: cacheRootFor(projectRoot) });
|
|
331
|
+
}
|
|
332
|
+
catch (exc) {
|
|
333
|
+
if (exc instanceof CacheNotFoundError) {
|
|
334
|
+
throw new TriageError(`mark-duplicate target #${ofN} not found in cache for ${repo}: ${exc.message}`);
|
|
335
|
+
}
|
|
336
|
+
throw exc;
|
|
337
|
+
}
|
|
338
|
+
const prior = isIdempotentRepeat(deps, issueNumber, repo, "mark-duplicate", projectRoot, ofN);
|
|
339
|
+
if (prior !== null) {
|
|
340
|
+
return prior.decision_id;
|
|
341
|
+
}
|
|
342
|
+
const actor = resolveActor(options.actor);
|
|
343
|
+
const entry = buildEntry(deps, "mark-duplicate", issueNumber, repo, actor, { linked_to: ofN });
|
|
344
|
+
const logPath = logPathFor(projectRoot);
|
|
345
|
+
return deps.candidatesLog.append(entry, { path: logPath });
|
|
346
|
+
}
|
|
347
|
+
/** Return the latest decision for ``issueNumber`` in ``repo`` (null if none). */
|
|
348
|
+
export function status(issueNumber, repo, deps, options = {}) {
|
|
349
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
350
|
+
return deps.candidatesLog.latestDecision(issueNumber, repo, {
|
|
351
|
+
path: logPathFor(projectRoot),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/** Record a reset audit entry referencing the prior decision_id (history is never deleted). */
|
|
355
|
+
export function reset(issueNumber, repo, deps, options = {}) {
|
|
356
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
357
|
+
const logPath = logPathFor(projectRoot);
|
|
358
|
+
const prior = deps.candidatesLog.latestDecision(issueNumber, repo, { path: logPath });
|
|
359
|
+
if (prior === null) {
|
|
360
|
+
throw new TriageError(`cannot reset #${issueNumber}: no prior decision recorded for ${repo}`);
|
|
361
|
+
}
|
|
362
|
+
if (prior.decision === "reset") {
|
|
363
|
+
return prior.decision_id;
|
|
364
|
+
}
|
|
365
|
+
const actor = resolveActor(options.actor);
|
|
366
|
+
const entry = buildEntry(deps, "reset", issueNumber, repo, actor, {
|
|
367
|
+
prior_decision_id: prior.decision_id,
|
|
368
|
+
});
|
|
369
|
+
return deps.candidatesLog.append(entry, { path: logPath });
|
|
370
|
+
}
|
|
280
371
|
/** Format a decision entry for CLI ``status`` / ``history`` output. */
|
|
281
372
|
export function formatDecision(entry) {
|
|
282
373
|
if (entry === null) {
|
|
@@ -5,6 +5,7 @@ export { DEFAULT_WIP_CAP, PROJECT_DEFINITION_REL_PATH };
|
|
|
5
5
|
export declare const CACHE_DIR_NAME = ".deft-cache";
|
|
6
6
|
export declare const CACHE_SOURCE = "github-issue";
|
|
7
7
|
export declare const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
|
|
8
|
+
export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
8
9
|
export declare const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
|
|
9
10
|
export declare const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
|
|
10
11
|
export declare const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
|
|
@@ -38,9 +39,6 @@ export declare function utcIso(date?: Date): string;
|
|
|
38
39
|
export declare function isPosIntDirName(name: string): boolean;
|
|
39
40
|
/** Walk `<cache_root>/github-issue/<owner>/<repo>/<N>/` cache entries. */
|
|
40
41
|
export declare function iterCachedIssues(cacheRoot: string): Array<[string, number]>;
|
|
41
|
-
export declare function readAuditLog(logPath: string): Record<string, unknown>[];
|
|
42
|
-
/** Collapse audit-log entries to `{(repo, issue_number): decision}`. */
|
|
43
|
-
export declare function latestDecisions(entries: Iterable<Record<string, unknown>>): Map<string, string>;
|
|
44
42
|
/** Count filesystem-truth in-flight vBRIEFs (#1270). */
|
|
45
43
|
export declare function countFilesystemInFlight(projectRoot: string): number;
|
|
46
44
|
/** True iff `plan.policy.triageScope` is a non-empty list of dict rules. */
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from
|
|
|
2
2
|
import { join, resolve as pathResolve } from "node:path";
|
|
3
3
|
import { loadProjectDefinition, PROJECT_DEFINITION_REL_PATH } from "../../policy/resolve.js";
|
|
4
4
|
import { countVbriefWip, DEFAULT_WIP_CAP, resolveWipCap } from "../../policy/wip.js";
|
|
5
|
+
import { AUDIT_LOG_REL_PATH, latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
5
6
|
import { countReconcilable } from "./reconcilable.js";
|
|
6
7
|
import { computeScopeDriftTotal } from "./scope-drift.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -11,7 +12,8 @@ export const MAX_LINE_CHARS = 120;
|
|
|
11
12
|
export { DEFAULT_WIP_CAP, PROJECT_DEFINITION_REL_PATH };
|
|
12
13
|
export const CACHE_DIR_NAME = ".deft-cache";
|
|
13
14
|
export const CACHE_SOURCE = "github-issue";
|
|
14
|
-
export const CANDIDATES_LOG_REL_PATH =
|
|
15
|
+
export const CANDIDATES_LOG_REL_PATH = AUDIT_LOG_REL_PATH;
|
|
16
|
+
export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
|
|
15
17
|
export const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
|
|
16
18
|
export const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
|
|
17
19
|
export const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
|
|
@@ -83,62 +85,6 @@ export function iterCachedIssues(cacheRoot) {
|
|
|
83
85
|
}
|
|
84
86
|
return out;
|
|
85
87
|
}
|
|
86
|
-
export function readAuditLog(logPath) {
|
|
87
|
-
if (!existsSync(logPath)) {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
let text;
|
|
91
|
-
try {
|
|
92
|
-
text = readFileSync(logPath, { encoding: "utf8" });
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
const out = [];
|
|
98
|
-
for (const raw of text.split("\n")) {
|
|
99
|
-
const stripped = raw.trim();
|
|
100
|
-
if (stripped.length === 0) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
const obj = JSON.parse(stripped);
|
|
105
|
-
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
|
|
106
|
-
out.push(obj);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// tolerate malformed lines
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
/** Collapse audit-log entries to `{(repo, issue_number): decision}`. */
|
|
116
|
-
export function latestDecisions(entries) {
|
|
117
|
-
const rows = [];
|
|
118
|
-
for (const entry of entries) {
|
|
119
|
-
const repo = entry.repo;
|
|
120
|
-
const issueNumber = entry.issue_number;
|
|
121
|
-
const decision = entry.decision;
|
|
122
|
-
const timestamp = entry.timestamp;
|
|
123
|
-
if (typeof repo !== "string" ||
|
|
124
|
-
typeof issueNumber !== "number" ||
|
|
125
|
-
!Number.isInteger(issueNumber) ||
|
|
126
|
-
typeof decision !== "string" ||
|
|
127
|
-
typeof timestamp !== "string") {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
rows.push([timestamp, repo, issueNumber, decision]);
|
|
131
|
-
}
|
|
132
|
-
rows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
133
|
-
const out = new Map();
|
|
134
|
-
for (const [_ts, repo, n, decision] of rows) {
|
|
135
|
-
out.set(`${repo}\0${n}`, decision);
|
|
136
|
-
}
|
|
137
|
-
return out;
|
|
138
|
-
}
|
|
139
|
-
function decisionKey(repo, issueNumber) {
|
|
140
|
-
return `${repo}\0${issueNumber}`;
|
|
141
|
-
}
|
|
142
88
|
// ---------------------------------------------------------------------------
|
|
143
89
|
// vBRIEF counters
|
|
144
90
|
// ---------------------------------------------------------------------------
|
|
@@ -206,6 +152,9 @@ export function isTriageScopeExplicitlyConfigured(projectRoot) {
|
|
|
206
152
|
export function resolveWipCapInt(projectRoot) {
|
|
207
153
|
return resolveWipCap(projectRoot).cap;
|
|
208
154
|
}
|
|
155
|
+
function decisionKey(repo, issueNumber) {
|
|
156
|
+
return `${repo}\0${issueNumber}`;
|
|
157
|
+
}
|
|
209
158
|
// ---------------------------------------------------------------------------
|
|
210
159
|
// compute / format / persist
|
|
211
160
|
// ---------------------------------------------------------------------------
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { AUDIT_LOG_REL_PATH, readAuditLog } from "../actions/candidates-log.js";
|
|
3
4
|
/** Lifecycle folders scanned for reconcilable vBRIEFs (#1468). */
|
|
4
5
|
export const BACKFILL_FOLDERS = ["proposed", "pending", "active"];
|
|
5
|
-
export const CANDIDATES_LOG_REL_PATH =
|
|
6
|
+
export const CANDIDATES_LOG_REL_PATH = AUDIT_LOG_REL_PATH;
|
|
6
7
|
function parseGithubIssueUri(uri) {
|
|
7
8
|
if (typeof uri !== "string" || uri.length === 0) {
|
|
8
9
|
return [null, null];
|
|
@@ -47,38 +48,8 @@ function extractIssueRef(data) {
|
|
|
47
48
|
}
|
|
48
49
|
function existingAuditRefs(auditPath) {
|
|
49
50
|
const seen = new Set();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
let text;
|
|
54
|
-
try {
|
|
55
|
-
text = readFileSync(auditPath, { encoding: "utf8" });
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return seen;
|
|
59
|
-
}
|
|
60
|
-
for (const raw of text.split("\n")) {
|
|
61
|
-
const stripped = raw.trim();
|
|
62
|
-
if (stripped.length === 0) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const entry = JSON.parse(stripped);
|
|
67
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const obj = entry;
|
|
71
|
-
const repo = obj.repo;
|
|
72
|
-
const issueNumber = obj.issue_number;
|
|
73
|
-
if (typeof repo === "string" &&
|
|
74
|
-
typeof issueNumber === "number" &&
|
|
75
|
-
Number.isInteger(issueNumber)) {
|
|
76
|
-
seen.add(`${repo}\0${issueNumber}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// tolerate malformed lines
|
|
81
|
-
}
|
|
51
|
+
for (const entry of readAuditLog(auditPath)) {
|
|
52
|
+
seen.add(auditKey(entry.repo, entry.issue_number));
|
|
82
53
|
}
|
|
83
54
|
return seen;
|
|
84
55
|
}
|
|
@@ -15,6 +15,7 @@ export interface DefaultModeOptions {
|
|
|
15
15
|
readonly output?: (line: string) => void;
|
|
16
16
|
readonly writeHistory?: boolean;
|
|
17
17
|
readonly taskPrefix?: string | null;
|
|
18
|
+
readonly selfHealFn?: (projectRoot: string) => void;
|
|
18
19
|
}
|
|
19
20
|
/** Non-interactive default mode (#1309). */
|
|
20
21
|
export declare function runDefaultMode(projectRoot: string, options?: DefaultModeOptions): WelcomeOutcome;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { maybeSelfHealCache } from "../../cache/fetch.js";
|
|
2
3
|
import { FIRST_TIME_NUDGE, INCOMPLETE_NUDGE_TEMPLATE } from "./constants.js";
|
|
3
4
|
import { classifyOnboarding, detectPriorState } from "./prior-state.js";
|
|
4
5
|
import { emitOneliner } from "./summary.js";
|
|
@@ -29,6 +30,11 @@ export function runDefaultMode(projectRoot, options = {}) {
|
|
|
29
30
|
exitCode: 0,
|
|
30
31
|
bootstrapAction: null,
|
|
31
32
|
};
|
|
33
|
+
const heal = options.selfHealFn ??
|
|
34
|
+
((root) => {
|
|
35
|
+
maybeSelfHealCache(resolve(root));
|
|
36
|
+
});
|
|
37
|
+
heal(projectRoot);
|
|
32
38
|
emitOneliner(projectRoot, {
|
|
33
39
|
writeHistory: options.writeHistory !== false,
|
|
34
40
|
output: out,
|
|
@@ -1,72 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { isRelativeTo, resolveRefPath, scopeIdsForRefUri } from "./paths.js";
|
|
3
|
+
import { isRelativeTo, resolveRefPath } from "./paths.js";
|
|
5
4
|
import { runProjectDefinitionHooks } from "./plan-hooks.js";
|
|
5
|
+
import { formatRegistryStatusMismatch, registryStatusScopeUris } from "./registry-status.js";
|
|
6
6
|
import { validateProjectDefNarratives } from "./schema.js";
|
|
7
|
-
function itemLocalScopeUris(item, plan) {
|
|
8
|
-
const uris = [];
|
|
9
|
-
const metadata = item.metadata;
|
|
10
|
-
if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
|
|
11
|
-
const meta = metadata;
|
|
12
|
-
const sourcePath = meta.source_path;
|
|
13
|
-
if (typeof sourcePath === "string" && sourcePath) {
|
|
14
|
-
uris.push(sourcePath);
|
|
15
|
-
}
|
|
16
|
-
const metadataRefs = meta.references;
|
|
17
|
-
if (Array.isArray(metadataRefs)) {
|
|
18
|
-
for (const ref of metadataRefs) {
|
|
19
|
-
if (typeof ref === "object" &&
|
|
20
|
-
ref !== null &&
|
|
21
|
-
!Array.isArray(ref) &&
|
|
22
|
-
ref.type === "x-vbrief/plan") {
|
|
23
|
-
const uri = ref.uri;
|
|
24
|
-
if (typeof uri === "string" && uri) {
|
|
25
|
-
uris.push(uri);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const refs = item.references;
|
|
32
|
-
if (Array.isArray(refs)) {
|
|
33
|
-
for (const ref of refs) {
|
|
34
|
-
if (typeof ref === "object" &&
|
|
35
|
-
ref !== null &&
|
|
36
|
-
!Array.isArray(ref) &&
|
|
37
|
-
ref.type === "x-vbrief/plan") {
|
|
38
|
-
const uri = ref.uri;
|
|
39
|
-
if (typeof uri === "string" && uri) {
|
|
40
|
-
uris.push(uri);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const itemId = item.id;
|
|
46
|
-
const itemTitle = item.title;
|
|
47
|
-
const planRefs = plan.references;
|
|
48
|
-
if (Array.isArray(planRefs)) {
|
|
49
|
-
for (const ref of planRefs) {
|
|
50
|
-
if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const refObj = ref;
|
|
54
|
-
if (refObj.type !== "x-vbrief/plan") {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const uri = refObj.uri;
|
|
58
|
-
if (typeof uri !== "string" || !uri) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const titleMatches = typeof itemTitle === "string" && refObj.title === itemTitle;
|
|
62
|
-
const idMatches = typeof itemId === "string" && scopeIdsForRefUri(uri).has(itemId);
|
|
63
|
-
if (titleMatches || idMatches) {
|
|
64
|
-
uris.push(uri);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return [...new Set(uris)];
|
|
69
|
-
}
|
|
70
7
|
function validateProjectRegistryScopeStatus(item, itemIndex, plan, filepath, vbriefDir) {
|
|
71
8
|
const errors = [];
|
|
72
9
|
const itemStatus = item.status;
|
|
@@ -74,7 +11,7 @@ function validateProjectRegistryScopeStatus(item, itemIndex, plan, filepath, vbr
|
|
|
74
11
|
return errors;
|
|
75
12
|
}
|
|
76
13
|
const resolvedRoot = resolve(vbriefDir);
|
|
77
|
-
for (const uri of
|
|
14
|
+
for (const uri of registryStatusScopeUris(item, plan)) {
|
|
78
15
|
if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("#")) {
|
|
79
16
|
continue;
|
|
80
17
|
}
|
|
@@ -98,9 +35,7 @@ function validateProjectRegistryScopeStatus(item, itemIndex, plan, filepath, vbr
|
|
|
98
35
|
}
|
|
99
36
|
const scopeStatus = scopePlan.status;
|
|
100
37
|
if (typeof scopeStatus === "string" && scopeStatus !== itemStatus) {
|
|
101
|
-
errors.push(
|
|
102
|
-
`but referenced scope '${uri}' has plan.status ${pyStrRepr(scopeStatus)} ` +
|
|
103
|
-
"(D3 registry-status)");
|
|
38
|
+
errors.push(formatRegistryStatusMismatch(filepath, itemIndex, itemStatus, uri, scopeStatus));
|
|
104
39
|
}
|
|
105
40
|
}
|
|
106
41
|
return errors;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JsonObject } from "./schema.js";
|
|
2
|
+
export declare const REGISTRY_SCOPE_LINK_TYPE = "x-vbrief/plan";
|
|
3
|
+
/** Derive the registry item status from a scope vBRIEF (render side). */
|
|
4
|
+
export declare function deriveRegistryItemStatus(planStatus: unknown, lifecycleFolder: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* References copied onto a registry item from a scope vBRIEF.
|
|
7
|
+
*
|
|
8
|
+
* Scope-local ``x-vbrief/plan`` links (decompose children, epic forward refs)
|
|
9
|
+
* stay on the scope file only; copying them into PROJECT-DEFINITION item
|
|
10
|
+
* metadata makes D3 registry-status falsely compare the item row against
|
|
11
|
+
* mixed-status siblings (#1696).
|
|
12
|
+
*/
|
|
13
|
+
export declare function registryMetadataReferencesFromScope(references: unknown): unknown[];
|
|
14
|
+
/**
|
|
15
|
+
* Local scope URIs whose ``plan.status`` must agree with the registry item
|
|
16
|
+
* status (D3). Shared by render output expectations and the validator.
|
|
17
|
+
*/
|
|
18
|
+
export declare function registryStatusScopeUris(item: JsonObject, plan: JsonObject): string[];
|
|
19
|
+
export declare function formatRegistryStatusMismatch(filepath: string, itemIndex: number, itemStatus: string, uri: string, scopeStatus: string): string;
|
|
20
|
+
//# sourceMappingURL=registry-status.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { scopeIdsForRefUri } from "./paths.js";
|
|
2
|
+
export const REGISTRY_SCOPE_LINK_TYPE = "x-vbrief/plan";
|
|
3
|
+
/** Derive the registry item status from a scope vBRIEF (render side). */
|
|
4
|
+
export function deriveRegistryItemStatus(planStatus, lifecycleFolder) {
|
|
5
|
+
if (typeof planStatus === "string" && planStatus.length > 0) {
|
|
6
|
+
return planStatus;
|
|
7
|
+
}
|
|
8
|
+
return lifecycleFolder;
|
|
9
|
+
}
|
|
10
|
+
function isScopeLinkRef(ref) {
|
|
11
|
+
return (typeof ref === "object" &&
|
|
12
|
+
ref !== null &&
|
|
13
|
+
!Array.isArray(ref) &&
|
|
14
|
+
ref.type === REGISTRY_SCOPE_LINK_TYPE);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* References copied onto a registry item from a scope vBRIEF.
|
|
18
|
+
*
|
|
19
|
+
* Scope-local ``x-vbrief/plan`` links (decompose children, epic forward refs)
|
|
20
|
+
* stay on the scope file only; copying them into PROJECT-DEFINITION item
|
|
21
|
+
* metadata makes D3 registry-status falsely compare the item row against
|
|
22
|
+
* mixed-status siblings (#1696).
|
|
23
|
+
*/
|
|
24
|
+
export function registryMetadataReferencesFromScope(references) {
|
|
25
|
+
if (!Array.isArray(references)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return references.filter((ref) => !isScopeLinkRef(ref));
|
|
29
|
+
}
|
|
30
|
+
function pushPlanScopeLink(uris, ref) {
|
|
31
|
+
if (!isScopeLinkRef(ref)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const uri = ref.uri;
|
|
35
|
+
if (typeof uri === "string" && uri.length > 0) {
|
|
36
|
+
uris.push(uri);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Local scope URIs whose ``plan.status`` must agree with the registry item
|
|
41
|
+
* status (D3). Shared by render output expectations and the validator.
|
|
42
|
+
*/
|
|
43
|
+
export function registryStatusScopeUris(item, plan) {
|
|
44
|
+
const uris = [];
|
|
45
|
+
const metadata = item.metadata;
|
|
46
|
+
if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
|
|
47
|
+
const meta = metadata;
|
|
48
|
+
const sourcePath = meta.source_path;
|
|
49
|
+
if (typeof sourcePath === "string" && sourcePath.length > 0) {
|
|
50
|
+
uris.push(sourcePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const itemRefs = item.references;
|
|
54
|
+
if (Array.isArray(itemRefs)) {
|
|
55
|
+
for (const ref of itemRefs) {
|
|
56
|
+
pushPlanScopeLink(uris, ref);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const itemId = item.id;
|
|
60
|
+
const itemTitle = item.title;
|
|
61
|
+
const planRefs = plan.references;
|
|
62
|
+
if (Array.isArray(planRefs)) {
|
|
63
|
+
for (const ref of planRefs) {
|
|
64
|
+
if (!isScopeLinkRef(ref)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const uri = ref.uri;
|
|
68
|
+
if (typeof uri !== "string" || uri.length === 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const titleMatches = typeof itemTitle === "string" && ref.title === itemTitle;
|
|
72
|
+
const idMatches = typeof itemId === "string" && scopeIdsForRefUri(uri).has(itemId);
|
|
73
|
+
if (titleMatches || idMatches) {
|
|
74
|
+
uris.push(uri);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...new Set(uris)];
|
|
79
|
+
}
|
|
80
|
+
export function formatRegistryStatusMismatch(filepath, itemIndex, itemStatus, uri, scopeStatus) {
|
|
81
|
+
return (`${filepath}: items[${itemIndex}] status is '${itemStatus}' ` +
|
|
82
|
+
`but referenced scope '${uri}' has plan.status '${scopeStatus}' ` +
|
|
83
|
+
"(D3 registry-status)");
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=registry-status.js.map
|