@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.
Files changed (64) hide show
  1. package/dist/cache/errors.d.ts +4 -1
  2. package/dist/cache/errors.js +8 -2
  3. package/dist/cache/fetch.d.ts +50 -0
  4. package/dist/cache/fetch.js +224 -3
  5. package/dist/cache/main.js +10 -5
  6. package/dist/cache/operations.js +6 -4
  7. package/dist/deposit/copy-tree.d.ts +16 -0
  8. package/dist/deposit/copy-tree.js +58 -0
  9. package/dist/deposit/resolve-content.d.ts +28 -0
  10. package/dist/deposit/resolve-content.js +70 -0
  11. package/dist/doctor/checks.d.ts +6 -0
  12. package/dist/doctor/checks.js +37 -0
  13. package/dist/doctor/constants.d.ts +2 -0
  14. package/dist/doctor/constants.js +8 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/init-deposit/gitignore.d.ts +51 -0
  18. package/dist/init-deposit/gitignore.js +200 -0
  19. package/dist/init-deposit/index.d.ts +6 -0
  20. package/dist/init-deposit/index.js +6 -0
  21. package/dist/init-deposit/init-deposit.d.ts +44 -0
  22. package/dist/init-deposit/init-deposit.js +197 -0
  23. package/dist/init-deposit/legacy-detect.d.ts +61 -0
  24. package/dist/init-deposit/legacy-detect.js +223 -0
  25. package/dist/init-deposit/migrate.d.ts +88 -0
  26. package/dist/init-deposit/migrate.js +196 -0
  27. package/dist/init-deposit/refresh.d.ts +50 -0
  28. package/dist/init-deposit/refresh.js +292 -0
  29. package/dist/init-deposit/scaffold.d.ts +41 -0
  30. package/dist/init-deposit/scaffold.js +753 -0
  31. package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
  32. package/dist/legacy-bridge/bridge-drift.js +171 -0
  33. package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
  34. package/dist/legacy-bridge/freeze-gate.js +168 -0
  35. package/dist/legacy-bridge/index.d.ts +4 -0
  36. package/dist/legacy-bridge/index.js +4 -0
  37. package/dist/legacy-bridge/sot.d.ts +45 -0
  38. package/dist/legacy-bridge/sot.js +49 -0
  39. package/dist/preflight-cache/evaluate.d.ts +11 -0
  40. package/dist/preflight-cache/evaluate.js +63 -13
  41. package/dist/release-e2e/flags.js +5 -1
  42. package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
  43. package/dist/release-e2e/legacy-bridge-leg.js +460 -0
  44. package/dist/release-e2e/main.d.ts +2 -1
  45. package/dist/release-e2e/main.js +17 -1
  46. package/dist/release-e2e/types.d.ts +9 -0
  47. package/dist/render/index.d.ts +1 -1
  48. package/dist/render/index.js +1 -1
  49. package/dist/render/project-render.js +4 -3
  50. package/dist/render/roadmap-render.d.ts +4 -2
  51. package/dist/render/roadmap-render.js +14 -7
  52. package/dist/triage/actions/candidates-log.d.ts +12 -1
  53. package/dist/triage/actions/candidates-log.js +44 -7
  54. package/dist/triage/actions/index.d.ts +25 -1
  55. package/dist/triage/actions/index.js +94 -3
  56. package/dist/triage/summary/index.d.ts +1 -3
  57. package/dist/triage/summary/index.js +6 -57
  58. package/dist/triage/summary/reconcilable.js +4 -33
  59. package/dist/triage/welcome/default-mode.d.ts +1 -0
  60. package/dist/triage/welcome/default-mode.js +6 -0
  61. package/dist/vbrief-validate/project-definition.js +4 -69
  62. package/dist/vbrief-validate/registry-status.d.ts +20 -0
  63. package/dist/vbrief-validate/registry-status.js +85 -0
  64. 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
- function readAll(repo, logPath) {
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
- const rows = readAll(repo, logPath).filter((row) => row.issue_number === issueNumber);
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
- return {
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 = "vbrief/.eval/candidates.jsonl";
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 = "vbrief/.eval/candidates.jsonl";
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
- if (!existsSync(auditPath)) {
51
- return seen;
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 { pyStrRepr } from "../triage/scope/python-repr.js";
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 itemLocalScopeUris(item, plan)) {
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(`${filepath}: items[${itemIndex}] status is ${pyStrRepr(itemStatus)} ` +
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