@deftai/directive-core 0.73.1 → 0.74.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/check/orchestrator.d.ts +3 -0
- package/dist/check/orchestrator.js +2 -1
- package/dist/codebase/map.js +2 -0
- package/dist/doctor/checks.d.ts +9 -0
- package/dist/doctor/checks.js +67 -0
- package/dist/doctor/main.js +2 -1
- package/dist/fs/projection-containment.d.ts +35 -0
- package/dist/fs/projection-containment.js +118 -0
- package/dist/init-deposit/scaffold.d.ts +21 -0
- package/dist/init-deposit/scaffold.js +79 -6
- package/dist/pr-merge-readiness/constants.d.ts +4 -0
- package/dist/pr-merge-readiness/constants.js +4 -0
- package/dist/pr-merge-readiness/evaluate.js +3 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +8 -6
- package/dist/pr-merge-readiness/mergeability.js +15 -10
- package/dist/pr-merge-readiness/output.js +12 -6
- package/dist/pr-merge-readiness/parse.d.ts +1 -0
- package/dist/pr-merge-readiness/parse.js +9 -1
- package/dist/pr-merge-readiness/types.d.ts +2 -0
- package/dist/preflight-cache/evaluate.d.ts +2 -0
- package/dist/preflight-cache/evaluate.js +14 -3
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +2 -0
- package/dist/release/preflight.d.ts +2 -0
- package/dist/release/preflight.js +14 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.js +24 -1
- package/dist/scope/open-umbrella-warning.d.ts +12 -0
- package/dist/scope/open-umbrella-warning.js +403 -0
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +69 -58
- package/dist/vbrief-reconcile/umbrellas.js +12 -11
- package/dist/xbrief-migrate/migrate-project.js +9 -0
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findLastReviewedCommitSha } from "../text/redos-safe.js";
|
|
2
|
-
import { CONFIDENCE_RE, GREPTILE_ERRORED_SENTINEL, INFORMAL_CLEAN_SIGNAL_RE, P0_BADGE, P1_BADGE, SECTION_RE, } from "./constants.js";
|
|
2
|
+
import { CONFIDENCE_RE, GREPTILE_ERRORED_SENTINEL, GREPTILE_EXCLUDED_AUTHOR_PHRASE, GREPTILE_STATUS_MARKER, INFORMAL_CLEAN_SIGNAL_RE, P0_BADGE, P1_BADGE, SECTION_RE, } from "./constants.js";
|
|
3
3
|
export function emptyVerdict() {
|
|
4
4
|
return {
|
|
5
5
|
found: false,
|
|
@@ -10,9 +10,15 @@ export function emptyVerdict() {
|
|
|
10
10
|
p1Count: 0,
|
|
11
11
|
p2Count: 0,
|
|
12
12
|
informalClean: false,
|
|
13
|
+
excludedAuthor: false,
|
|
13
14
|
rawBodyExcerpt: "",
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
export function isGreptileExcludedAuthor(body) {
|
|
18
|
+
const lower = body.toLowerCase();
|
|
19
|
+
return (lower.includes(GREPTILE_EXCLUDED_AUTHOR_PHRASE) &&
|
|
20
|
+
(lower.includes("greptile-status") || body.includes(GREPTILE_STATUS_MARKER)));
|
|
21
|
+
}
|
|
16
22
|
export function isInformalCleanMissingCanonicalFields(verdict, body) {
|
|
17
23
|
if (!verdict.found || verdict.errored) {
|
|
18
24
|
return false;
|
|
@@ -42,6 +48,7 @@ export function parseGreptileBody(body) {
|
|
|
42
48
|
return emptyVerdict();
|
|
43
49
|
}
|
|
44
50
|
const errored = body.trim().startsWith(GREPTILE_ERRORED_SENTINEL);
|
|
51
|
+
const excludedAuthor = isGreptileExcludedAuthor(body);
|
|
45
52
|
const lastReviewedSha = findLastReviewedCommitSha(body);
|
|
46
53
|
const confMatch = CONFIDENCE_RE.exec(body);
|
|
47
54
|
const confidence = confMatch?.groups?.score !== undefined ? Number(confMatch.groups.score) : null;
|
|
@@ -73,6 +80,7 @@ export function parseGreptileBody(body) {
|
|
|
73
80
|
p1Count,
|
|
74
81
|
p2Count,
|
|
75
82
|
informalClean: false,
|
|
83
|
+
excludedAuthor,
|
|
76
84
|
rawBodyExcerpt: body.slice(0, 200),
|
|
77
85
|
};
|
|
78
86
|
if (isInformalCleanMissingCanonicalFields(verdict, body)) {
|
|
@@ -7,6 +7,8 @@ export interface GreptileVerdict {
|
|
|
7
7
|
readonly p1Count: number;
|
|
8
8
|
readonly p2Count: number;
|
|
9
9
|
readonly informalClean: boolean;
|
|
10
|
+
/** Greptile deliberately skipped review for an excluded PR author (#2375). */
|
|
11
|
+
readonly excludedAuthor: boolean;
|
|
10
12
|
readonly rawBodyExcerpt: string;
|
|
11
13
|
}
|
|
12
14
|
export interface GateResult {
|
|
@@ -17,6 +17,8 @@ export declare const CANDIDATES_RELPATH: string;
|
|
|
17
17
|
export declare const DEFAULT_MAX_AGE_HOURS = 24;
|
|
18
18
|
export declare const ENV_MAX_AGE_HOURS = "DEFT_CACHE_MAX_AGE_HOURS";
|
|
19
19
|
export declare const ENV_TRIAGE_REPO = "DEFT_TRIAGE_REPO";
|
|
20
|
+
/** Set by releaseSubprocessEnv() during task release Step 5 (#2386). */
|
|
21
|
+
export declare const ENV_RELEASE_PREFLIGHT = "DEFT_RELEASE_PREFLIGHT";
|
|
20
22
|
export declare const REQUIRED_DECISION = "accept";
|
|
21
23
|
export interface GateResult {
|
|
22
24
|
readonly code: number;
|
|
@@ -27,7 +27,14 @@ export const CANDIDATES_RELPATH = join("xbrief", ".triage-cache", "candidates.js
|
|
|
27
27
|
export const DEFAULT_MAX_AGE_HOURS = 24;
|
|
28
28
|
export const ENV_MAX_AGE_HOURS = "DEFT_CACHE_MAX_AGE_HOURS";
|
|
29
29
|
export const ENV_TRIAGE_REPO = "DEFT_TRIAGE_REPO";
|
|
30
|
+
/** Set by releaseSubprocessEnv() during task release Step 5 (#2386). */
|
|
31
|
+
export const ENV_RELEASE_PREFLIGHT = "DEFT_RELEASE_PREFLIGHT";
|
|
30
32
|
export const REQUIRED_DECISION = "accept";
|
|
33
|
+
const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
34
|
+
function releasePreflightBypassActive() {
|
|
35
|
+
const raw = process.env[ENV_RELEASE_PREFLIGHT] ?? "";
|
|
36
|
+
return TRUTHY.has(raw.trim().toLowerCase());
|
|
37
|
+
}
|
|
31
38
|
// ---------------------------------------------------------------------------
|
|
32
39
|
// Repo discovery helpers
|
|
33
40
|
// ---------------------------------------------------------------------------
|
|
@@ -327,6 +334,7 @@ const REMEDIATION_NO_CANDIDATES = [
|
|
|
327
334
|
export function evaluate(projectRoot, options = {}) {
|
|
328
335
|
const source = options.source ?? DEFAULT_SOURCE;
|
|
329
336
|
const allowStale = options.allowStale ?? false;
|
|
337
|
+
const releasePreflightStaleBypass = releasePreflightBypassActive();
|
|
330
338
|
const allowMissingBootstrap = options.allowMissingBootstrap ?? false;
|
|
331
339
|
const nowFn = options.nowFn ?? (() => new Date());
|
|
332
340
|
const envMaxAge = process.env[ENV_MAX_AGE_HOURS];
|
|
@@ -423,11 +431,14 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
423
431
|
// empty-cache case (no entries at all) preserves its prior staleness result.
|
|
424
432
|
const allCandidatesClosed = minFetchedAt === null && rawCandidatePaths.length > 0;
|
|
425
433
|
const stale = allCandidatesClosed ? false : ageH > maxAgeHours;
|
|
426
|
-
// Step 5b: --allow-stale bypass
|
|
427
|
-
if (stale && allowStale) {
|
|
434
|
+
// Step 5b: --allow-stale bypass (or release Step-5 age bypass only, #2386)
|
|
435
|
+
if (stale && (allowStale || releasePreflightStaleBypass)) {
|
|
436
|
+
const bypassReason = options.allowStale
|
|
437
|
+
? "--allow-stale is set"
|
|
438
|
+
: `${ENV_RELEASE_PREFLIGHT} is set (release pre-flight)`;
|
|
428
439
|
const warning = [
|
|
429
440
|
`⚠ deft cache-fresh: cache is ${ageH.toFixed(1)}h old (max-age=${maxAgeHours}h) but`,
|
|
430
|
-
`
|
|
441
|
+
` ${bypassReason} -- proceeding with audit-trail warning.`,
|
|
431
442
|
].join("\n");
|
|
432
443
|
if (options.forIssue !== undefined && options.forIssue !== null) {
|
|
433
444
|
const forResult = evaluateForIssue(resolvedRepo, options.forIssue, candidatesPath, cacheRoot, source, scopeRules, projectRoot);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { EvaluateOptions, GateResult } from "./evaluate.js";
|
|
2
|
-
export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, } from "./evaluate.js";
|
|
2
|
+
export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_RELEASE_PREFLIGHT, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, } from "./evaluate.js";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, } from "./evaluate.js";
|
|
1
|
+
export { CACHE_DIR_NAME, CANDIDATES_RELPATH, DEFAULT_MAX_AGE_HOURS, DEFAULT_SOURCE, ENV_MAX_AGE_HOURS, ENV_RELEASE_PREFLIGHT, ENV_TRIAGE_REPO, evaluate, REQUIRED_DECISION, } from "./evaluate.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -16,6 +16,8 @@ export declare const VERIFY_DRAFT_INTERVAL_SECONDS = 1;
|
|
|
16
16
|
export declare const RELEASE_ARTIFACTS: readonly ["CHANGELOG.md", "ROADMAP.md"];
|
|
17
17
|
export declare const BRANCH_GATE_BYPASS_ENV = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
|
|
18
18
|
export declare const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH_VERBS";
|
|
19
|
+
/** Set by releaseSubprocessEnv() so Step-5 pre-flight skips triage-cache staleness (#2386). */
|
|
20
|
+
export declare const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
|
|
19
21
|
export declare const PYPROJECT_VERSION_LINE_RE: RegExp;
|
|
20
22
|
/** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
|
|
21
23
|
export declare const RELEASE_HELP: string;
|
|
@@ -24,6 +24,8 @@ export const VERIFY_DRAFT_INTERVAL_SECONDS = 1.0;
|
|
|
24
24
|
export const RELEASE_ARTIFACTS = ["CHANGELOG.md", "ROADMAP.md"];
|
|
25
25
|
export const BRANCH_GATE_BYPASS_ENV = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
|
|
26
26
|
export const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH_VERBS";
|
|
27
|
+
/** Set by releaseSubprocessEnv() so Step-5 pre-flight skips triage-cache staleness (#2386). */
|
|
28
|
+
export const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
|
|
27
29
|
export const PYPROJECT_VERSION_LINE_RE = /version\s*=\s*"[^"]*"/;
|
|
28
30
|
/** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
|
|
29
31
|
export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--skip-release] [--allow-dirty]\n" +
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* dispatches `check:framework-source`.
|
|
9
9
|
*/
|
|
10
10
|
import { type CheckOrchestratorSeams } from "../check/orchestrator.js";
|
|
11
|
+
/** Step-5-only env: branch bypass plus release pre-flight cache staleness tolerance (#2386). */
|
|
12
|
+
export declare function releaseCheckEnv(base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
13
|
/** Seams for test isolation of the native release pre-flight. */
|
|
12
14
|
export interface ReleasePreflightSeams {
|
|
13
15
|
/** Override the check dispatcher (default: dispatchTaskCheck from check/orchestrator). */
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
* dispatches `check:framework-source`.
|
|
9
9
|
*/
|
|
10
10
|
import { dispatchTaskCheck } from "../check/orchestrator.js";
|
|
11
|
+
import { RELEASE_PREFLIGHT_ENV } from "./constants.js";
|
|
12
|
+
import { releaseSubprocessEnv } from "./git.js";
|
|
13
|
+
/** Step-5-only env: branch bypass plus release pre-flight cache staleness tolerance (#2386). */
|
|
14
|
+
export function releaseCheckEnv(base = process.env) {
|
|
15
|
+
return {
|
|
16
|
+
...releaseSubprocessEnv(base),
|
|
17
|
+
[RELEASE_PREFLIGHT_ENV]: "1",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* Run the native TypeScript `task check` as the release pre-flight.
|
|
13
22
|
*
|
|
@@ -18,7 +27,11 @@ import { dispatchTaskCheck } from "../check/orchestrator.js";
|
|
|
18
27
|
*/
|
|
19
28
|
export function runReleaseCheck(projectRoot, seams = {}) {
|
|
20
29
|
const dispatch = seams.dispatchCheck ?? dispatchTaskCheck;
|
|
21
|
-
const
|
|
30
|
+
const checkSeams = {
|
|
31
|
+
...seams.checkSeams,
|
|
32
|
+
env: releaseCheckEnv(seams.checkSeams?.env ?? process.env),
|
|
33
|
+
};
|
|
34
|
+
const code = dispatch(projectRoot, projectRoot, checkSeams);
|
|
22
35
|
if (code === 0) {
|
|
23
36
|
return [true, "ran native TypeScript task check"];
|
|
24
37
|
}
|
package/dist/scope/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./constants.js";
|
|
|
3
3
|
export * from "./decomposed-refs.js";
|
|
4
4
|
export * from "./demote.js";
|
|
5
5
|
export * from "./main.js";
|
|
6
|
+
export * from "./open-umbrella-warning.js";
|
|
6
7
|
export * from "./project-context.js";
|
|
7
8
|
export * from "./transition.js";
|
|
8
9
|
export * from "./undo.js";
|
package/dist/scope/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./constants.js";
|
|
|
3
3
|
export * from "./decomposed-refs.js";
|
|
4
4
|
export * from "./demote.js";
|
|
5
5
|
export * from "./main.js";
|
|
6
|
+
export * from "./open-umbrella-warning.js";
|
|
6
7
|
export * from "./project-context.js";
|
|
7
8
|
export * from "./transition.js";
|
|
8
9
|
export * from "./undo.js";
|
package/dist/scope/main.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
|
|
3
4
|
import { canonicalLogPath, readAll } from "./audit-log.js";
|
|
4
5
|
import { TRANSITIONS } from "./constants.js";
|
|
5
6
|
import { batchDemote, DEFAULT_OLDER_THAN_DAYS, demoteOne, resolveDemoteFilePath, resolveFilePath, resolveProjectRootStrict, } from "./demote.js";
|
|
7
|
+
import { completedPathForScopeMove, findOpenUmbrellaReferences, renderOpenUmbrellaWarning, } from "./open-umbrella-warning.js";
|
|
6
8
|
import { resolveProjectRoot } from "./project-context.js";
|
|
7
9
|
import { recordWipCapOverride, runTransition } from "./transition.js";
|
|
8
10
|
import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoOne, } from "./undo.js";
|
|
@@ -88,6 +90,27 @@ export function lifecycleMain(argv) {
|
|
|
88
90
|
"audit: vbrief/.eval/scope-lifecycle.jsonl entry tagged wip_cap_override (#1124).\n");
|
|
89
91
|
}
|
|
90
92
|
process.stdout.write(`${result.message}\n`);
|
|
93
|
+
if (action === "complete") {
|
|
94
|
+
try {
|
|
95
|
+
const completedPath = completedPathForScopeMove(filePath);
|
|
96
|
+
const rootForWarning = resolveProjectRoot(projectRoot) ?? dirname(dirname(dirname(completedPath)));
|
|
97
|
+
const refs = findOpenUmbrellaReferences(rootForWarning, completedPath);
|
|
98
|
+
const warning = renderOpenUmbrellaWarning(refs);
|
|
99
|
+
if (warning.length > 0) {
|
|
100
|
+
process.stderr.write(`${warning}\n`);
|
|
101
|
+
const [, outcome] = reconcileUmbrellas(rootForWarning);
|
|
102
|
+
// Only surface a report when reconcile actually changed something or
|
|
103
|
+
// recorded per-epic errors — skip vacuous setup failures (exit 2 with
|
|
104
|
+
// empty buckets) so a missing lifecycle root does not spam stderr.
|
|
105
|
+
if (outcome.changed.length > 0 || outcome.errors.length > 0) {
|
|
106
|
+
process.stderr.write(`${renderUmbrellasReport(outcome)}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* best-effort drift warning + reconcile; lifecycle success remains authoritative */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
91
114
|
return 0;
|
|
92
115
|
}
|
|
93
116
|
process.stderr.write(`Error: ${result.message}\n`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface OpenUmbrellaReference {
|
|
2
|
+
readonly repo: string | null;
|
|
3
|
+
readonly issueNumber: number | null;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly path: string | null;
|
|
6
|
+
readonly sources: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** Find open umbrella/tracker surfaces that still mention a just-completed scope. */
|
|
9
|
+
export declare function findOpenUmbrellaReferences(projectRoot: string, completedScopePath: string): OpenUmbrellaReference[];
|
|
10
|
+
export declare function renderOpenUmbrellaWarning(refs: readonly OpenUmbrellaReference[]): string;
|
|
11
|
+
export declare function completedPathForScopeMove(filePath: string): string;
|
|
12
|
+
//# sourceMappingURL=open-umbrella-warning.d.ts.map
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
|
+
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
5
|
+
import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "../triage/queue/constants.js";
|
|
6
|
+
import { parseGithubIssueUri } from "../triage/reconcile/parse-uri.js";
|
|
7
|
+
import { collectPlanRefs, resolveVbriefRef } from "./vbrief-ref.js";
|
|
8
|
+
const OPEN_FOLDERS = ["proposed", "pending", "active"];
|
|
9
|
+
const TRACKER_LABELS = new Set(["epic", "meta", "tracker", "type:tracker", "status:tracker"]);
|
|
10
|
+
function readJson(path) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
14
|
+
? parsed
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function planOf(data) {
|
|
22
|
+
const plan = data?.plan;
|
|
23
|
+
return typeof plan === "object" && plan !== null && !Array.isArray(plan)
|
|
24
|
+
? plan
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
function issueRefsFromPlan(plan) {
|
|
28
|
+
const refs = plan?.references;
|
|
29
|
+
if (!Array.isArray(refs)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const out = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
for (const ref of refs) {
|
|
35
|
+
if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const typed = ref;
|
|
39
|
+
if (!referenceTypeMatches(String(typed.type ?? ""), "github-issue")) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const [repo, number] = parseGithubIssueUri(typed.uri);
|
|
43
|
+
if (number === null) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const key = repo === null ? `bare:${number}` : `${repo}:${number}`;
|
|
47
|
+
if (seen.has(key)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
seen.add(key);
|
|
51
|
+
out.push({ repo, number });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function labelsFromRaw(raw) {
|
|
56
|
+
if (!Array.isArray(raw)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const label of raw) {
|
|
61
|
+
if (typeof label === "string") {
|
|
62
|
+
out.push(label);
|
|
63
|
+
}
|
|
64
|
+
else if (typeof label === "object" && label !== null && !Array.isArray(label)) {
|
|
65
|
+
const name = label.name;
|
|
66
|
+
if (typeof name === "string") {
|
|
67
|
+
out.push(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function subIssueTotal(raw) {
|
|
74
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const total = raw.total;
|
|
78
|
+
return typeof total === "number" && Number.isFinite(total) ? total : 0;
|
|
79
|
+
}
|
|
80
|
+
function readTextIfPresent(path) {
|
|
81
|
+
if (!existsSync(path)) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return readFileSync(path, "utf8");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readCachedIssue(projectRoot, repo, number) {
|
|
92
|
+
const [owner, name] = repo.split("/", 2);
|
|
93
|
+
if (!owner || !name) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const issueDir = join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, owner, name, String(number));
|
|
97
|
+
const rawPath = join(issueDir, "raw.json");
|
|
98
|
+
const raw = readJson(rawPath);
|
|
99
|
+
if (raw === null) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const body = typeof raw.body === "string" ? raw.body : "";
|
|
103
|
+
const content = readTextIfPresent(join(issueDir, "content.md"));
|
|
104
|
+
return {
|
|
105
|
+
repo,
|
|
106
|
+
number: typeof raw.number === "number" && Number.isFinite(raw.number) ? raw.number : number,
|
|
107
|
+
title: typeof raw.title === "string" ? raw.title : `#${number}`,
|
|
108
|
+
state: typeof raw.state === "string" ? raw.state.toLowerCase() : "open",
|
|
109
|
+
body: content.length > 0 ? `${body}\n${content}` : body,
|
|
110
|
+
labels: labelsFromRaw(raw.labels).map((label) => label.toLowerCase()),
|
|
111
|
+
subIssuesTotal: subIssueTotal(raw.sub_issues_summary),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function cachedState(projectRoot, ref) {
|
|
115
|
+
if (ref.repo === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const cached = readCachedIssue(projectRoot, ref.repo, ref.number);
|
|
119
|
+
if (cached === null) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return cached.state === "closed" ? "closed" : "open";
|
|
123
|
+
}
|
|
124
|
+
function chooseOpenIssueRef(projectRoot, refs) {
|
|
125
|
+
if (refs.length === 0) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
let firstUnknown = null;
|
|
129
|
+
for (const ref of refs) {
|
|
130
|
+
const state = cachedState(projectRoot, ref);
|
|
131
|
+
if (state === "open") {
|
|
132
|
+
return ref;
|
|
133
|
+
}
|
|
134
|
+
if (state === null && firstUnknown === null) {
|
|
135
|
+
firstUnknown = ref;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return firstUnknown;
|
|
139
|
+
}
|
|
140
|
+
function refUriMatchesPath(uri, targetPaths, vbriefRoot) {
|
|
141
|
+
if (typeof uri !== "string" || uri.length === 0) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const resolved = resolveVbriefRef(uri, vbriefRoot);
|
|
145
|
+
if (resolved === null) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return targetPaths.has(resolve(resolved));
|
|
149
|
+
}
|
|
150
|
+
function planReferenceMatchesTarget(ref, targetPaths, vbriefRoot) {
|
|
151
|
+
if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const typed = ref;
|
|
155
|
+
if (!referenceTypeMatches(String(typed.type ?? ""), "plan")) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return refUriMatchesPath(typed.uri, targetPaths, vbriefRoot);
|
|
159
|
+
}
|
|
160
|
+
function planReferencesTarget(plan, targetPaths, vbriefRoot) {
|
|
161
|
+
const refs = plan.references;
|
|
162
|
+
if (!Array.isArray(refs)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return refs.some((ref) => planReferenceMatchesTarget(ref, targetPaths, vbriefRoot));
|
|
166
|
+
}
|
|
167
|
+
function planRefsTarget(plan, targetPaths, vbriefRoot) {
|
|
168
|
+
return collectPlanRefs(plan).some((uri) => refUriMatchesPath(uri, targetPaths, vbriefRoot));
|
|
169
|
+
}
|
|
170
|
+
function relToRoot(path, root) {
|
|
171
|
+
return relative(resolve(root), resolve(path)).replace(/\\/g, "/");
|
|
172
|
+
}
|
|
173
|
+
function addReference(out, ref) {
|
|
174
|
+
const key = ref.repo !== null && ref.issueNumber !== null
|
|
175
|
+
? `${ref.repo}#${ref.issueNumber}`
|
|
176
|
+
: (ref.path ?? ref.title);
|
|
177
|
+
const existing = out.get(key);
|
|
178
|
+
if (existing !== undefined) {
|
|
179
|
+
existing.sources.add(ref.source);
|
|
180
|
+
if (existing.title.startsWith("#") && !ref.title.startsWith("#")) {
|
|
181
|
+
existing.title = ref.title;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
out.set(key, {
|
|
186
|
+
repo: ref.repo,
|
|
187
|
+
issueNumber: ref.issueNumber,
|
|
188
|
+
title: ref.title,
|
|
189
|
+
path: ref.path,
|
|
190
|
+
sources: new Set([ref.source]),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function titleForLocalCandidate(plan, issueRef, projectRoot) {
|
|
194
|
+
if (issueRef?.repo !== null && issueRef?.repo !== undefined) {
|
|
195
|
+
const cached = readCachedIssue(projectRoot, issueRef.repo, issueRef.number);
|
|
196
|
+
if (cached !== null) {
|
|
197
|
+
return cached.title;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return typeof plan.title === "string" && plan.title.length > 0
|
|
201
|
+
? plan.title
|
|
202
|
+
: issueRef !== null
|
|
203
|
+
? `#${issueRef.number}`
|
|
204
|
+
: "open scope";
|
|
205
|
+
}
|
|
206
|
+
function findLocalReferences(projectRoot, completedScopePath, completedPlan, out) {
|
|
207
|
+
const vbriefRoot = dirname(dirname(resolve(completedScopePath)));
|
|
208
|
+
const targetScopePaths = new Set([
|
|
209
|
+
completedScopePath,
|
|
210
|
+
...OPEN_FOLDERS.map((folder) => join(vbriefRoot, folder, basename(completedScopePath))),
|
|
211
|
+
].map((path) => resolve(path)));
|
|
212
|
+
const targetParentPaths = collectPlanRefs(completedPlan)
|
|
213
|
+
.map((uri) => resolveVbriefRef(uri, vbriefRoot))
|
|
214
|
+
.filter((path) => path !== null)
|
|
215
|
+
.map((path) => resolve(path));
|
|
216
|
+
for (const folder of OPEN_FOLDERS) {
|
|
217
|
+
const folderPath = join(vbriefRoot, folder);
|
|
218
|
+
if (!existsSync(folderPath)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const name of readdirSync(folderPath)
|
|
222
|
+
.filter((entry) => hasArtifactSuffix(entry))
|
|
223
|
+
.sort()) {
|
|
224
|
+
const path = join(folderPath, name);
|
|
225
|
+
const plan = planOf(readJson(path));
|
|
226
|
+
if (plan === null) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const sources = [];
|
|
230
|
+
if (planReferencesTarget(plan, targetScopePaths, vbriefRoot)) {
|
|
231
|
+
sources.push("plan.references");
|
|
232
|
+
}
|
|
233
|
+
if (planRefsTarget(plan, targetScopePaths, vbriefRoot)) {
|
|
234
|
+
sources.push("planRef");
|
|
235
|
+
}
|
|
236
|
+
if (targetParentPaths.includes(resolve(path))) {
|
|
237
|
+
sources.push("completed planRef");
|
|
238
|
+
}
|
|
239
|
+
if (sources.length === 0) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const issueRefs = issueRefsFromPlan(plan);
|
|
243
|
+
const issueRef = chooseOpenIssueRef(projectRoot, issueRefs);
|
|
244
|
+
if (issueRefs.length > 0 && issueRef === null) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const relPath = relToRoot(path, projectRoot);
|
|
248
|
+
for (const source of sources) {
|
|
249
|
+
addReference(out, {
|
|
250
|
+
repo: issueRef?.repo ?? null,
|
|
251
|
+
issueNumber: issueRef?.number ?? null,
|
|
252
|
+
title: titleForLocalCandidate(plan, issueRef, projectRoot),
|
|
253
|
+
path: relPath,
|
|
254
|
+
source,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function cachedRepoDirs(projectRoot) {
|
|
261
|
+
const base = join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE);
|
|
262
|
+
if (!existsSync(base)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
const repos = [];
|
|
266
|
+
for (const ownerEntry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
267
|
+
if (!ownerEntry.isDirectory()) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const owner = ownerEntry.name;
|
|
271
|
+
const ownerPath = join(base, owner);
|
|
272
|
+
for (const repoEntry of readdirSync(ownerPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
273
|
+
if (!repoEntry.isDirectory()) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const name = repoEntry.name;
|
|
277
|
+
repos.push(`${owner}/${name}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return repos;
|
|
281
|
+
}
|
|
282
|
+
function issueNumberToken(issueNumber) {
|
|
283
|
+
return Number.isSafeInteger(issueNumber) && issueNumber > 0 ? String(issueNumber) : null;
|
|
284
|
+
}
|
|
285
|
+
function escapeRegExp(value) {
|
|
286
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
287
|
+
}
|
|
288
|
+
function textMentionsIssue(text, ref, cachedRepo) {
|
|
289
|
+
const token = issueNumberToken(ref.number);
|
|
290
|
+
if (token === null) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const hash = new RegExp(`(^|[^A-Za-z0-9_#])#${token}(?!\\d)`);
|
|
294
|
+
if (ref.repo === cachedRepo && hash.test(text)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (ref.repo === null) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const repo = escapeRegExp(ref.repo);
|
|
301
|
+
const qualifiedUrl = new RegExp(`(?:github\\.com|api\\.github\\.com/repos)/${repo}/issues/${token}(?:\\D|$)`);
|
|
302
|
+
return qualifiedUrl.test(text);
|
|
303
|
+
}
|
|
304
|
+
function looksLikeTracker(issue) {
|
|
305
|
+
if (issue.subIssuesTotal > 0) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
if (issue.labels.some((label) => TRACKER_LABELS.has(label) || label.includes("umbrella"))) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return /\b(epic|omnibus|tracker|umbrella)\b/i.test(issue.title);
|
|
312
|
+
}
|
|
313
|
+
function findCachedIssueBodyReferences(projectRoot, targetIssueRefs, out) {
|
|
314
|
+
const targetNumbers = new Set(targetIssueRefs.map((ref) => ref.number));
|
|
315
|
+
if (targetNumbers.size === 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!targetIssueRefs.some((ref) => ref.repo !== null)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
for (const repo of cachedRepoDirs(projectRoot)) {
|
|
322
|
+
const [owner, name] = repo.split("/", 2);
|
|
323
|
+
if (!owner || !name) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const repoDir = join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, owner, name);
|
|
327
|
+
for (const entry of readdirSync(repoDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
328
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const issueNumber = Number(entry.name);
|
|
332
|
+
if (targetNumbers.has(issueNumber)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const issue = readCachedIssue(projectRoot, repo, issueNumber);
|
|
336
|
+
if (issue === null || issue.state !== "open" || !looksLikeTracker(issue)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const haystack = `${issue.title}\n${issue.body}`;
|
|
340
|
+
if (!targetIssueRefs.some((ref) => textMentionsIssue(haystack, ref, repo))) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
addReference(out, {
|
|
344
|
+
repo,
|
|
345
|
+
issueNumber: issue.number,
|
|
346
|
+
title: issue.title,
|
|
347
|
+
path: null,
|
|
348
|
+
source: "cached issue body",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Find open umbrella/tracker surfaces that still mention a just-completed scope. */
|
|
354
|
+
export function findOpenUmbrellaReferences(projectRoot, completedScopePath) {
|
|
355
|
+
const data = readJson(completedScopePath);
|
|
356
|
+
const completedPlan = planOf(data);
|
|
357
|
+
if (completedPlan === null) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
const out = new Map();
|
|
361
|
+
findLocalReferences(projectRoot, completedScopePath, completedPlan, out);
|
|
362
|
+
findCachedIssueBodyReferences(projectRoot, issueRefsFromPlan(completedPlan), out);
|
|
363
|
+
return [...out.values()]
|
|
364
|
+
.map((ref) => ({
|
|
365
|
+
repo: ref.repo,
|
|
366
|
+
issueNumber: ref.issueNumber,
|
|
367
|
+
title: ref.title,
|
|
368
|
+
path: ref.path,
|
|
369
|
+
sources: [...ref.sources].sort(),
|
|
370
|
+
}))
|
|
371
|
+
.sort((a, b) => {
|
|
372
|
+
const aKey = a.issueNumber !== null
|
|
373
|
+
? a.repo === null
|
|
374
|
+
? `bare:#${a.issueNumber}`
|
|
375
|
+
: `${a.repo}#${a.issueNumber}`
|
|
376
|
+
: (a.path ?? "");
|
|
377
|
+
const bKey = b.issueNumber !== null
|
|
378
|
+
? b.repo === null
|
|
379
|
+
? `bare:#${b.issueNumber}`
|
|
380
|
+
: `${b.repo}#${b.issueNumber}`
|
|
381
|
+
: (b.path ?? "");
|
|
382
|
+
return aKey.localeCompare(bKey);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function displayReference(ref) {
|
|
386
|
+
const issue = ref.issueNumber !== null ? `#${ref.issueNumber}${ref.repo ? ` (${ref.repo})` : ""}` : null;
|
|
387
|
+
const path = ref.path !== null ? ref.path : null;
|
|
388
|
+
const name = issue ?? path ?? ref.title;
|
|
389
|
+
return `${name}: ${ref.title}`;
|
|
390
|
+
}
|
|
391
|
+
export function renderOpenUmbrellaWarning(refs) {
|
|
392
|
+
if (refs.length === 0) {
|
|
393
|
+
return "";
|
|
394
|
+
}
|
|
395
|
+
const names = refs.map(displayReference).join("; ");
|
|
396
|
+
return (`Warning: scope:complete found open umbrella/tracker reference(s) to the completed scope: ${names}. ` +
|
|
397
|
+
"Run `task vbrief:reconcile:umbrellas` to refresh current-shape comments.");
|
|
398
|
+
}
|
|
399
|
+
export function completedPathForScopeMove(filePath) {
|
|
400
|
+
const resolved = resolve(filePath);
|
|
401
|
+
return join(dirname(dirname(resolved)), "completed", basename(resolved));
|
|
402
|
+
}
|
|
403
|
+
//# sourceMappingURL=open-umbrella-warning.js.map
|