@deftai/directive-core 0.73.1 → 0.75.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/agents-md-budget/evaluate.d.ts +61 -0
- package/dist/agents-md-budget/evaluate.js +321 -19
- package/dist/agents-md-budget/index.d.ts +1 -0
- package/dist/agents-md-budget/index.js +1 -0
- package/dist/agents-md-budget/skill-frontmatter.d.ts +33 -0
- package/dist/agents-md-budget/skill-frontmatter.js +115 -0
- package/dist/check/orchestrator.d.ts +3 -0
- package/dist/check/orchestrator.js +2 -1
- package/dist/codebase/map.js +2 -0
- package/dist/content-contracts/skills/helpers.d.ts +0 -1
- package/dist/content-contracts/skills/helpers.js +1 -11
- package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
- package/dist/content-contracts/standards/_helpers.js +4 -2
- package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
- package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
- package/dist/doctor/checks.d.ts +9 -0
- package/dist/doctor/checks.js +67 -0
- package/dist/doctor/doctor-state.js +2 -1
- package/dist/doctor/main.js +2 -1
- package/dist/doctor/paths.js +2 -1
- package/dist/eval-health-relocation/evaluate.d.ts +71 -0
- package/dist/eval-health-relocation/evaluate.js +252 -0
- package/dist/eval-health-relocation/index.d.ts +2 -0
- package/dist/eval-health-relocation/index.js +2 -0
- 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 +86 -9
- package/dist/intake/issue-emit.js +2 -2
- package/dist/policy/agents-md-budget.d.ts +23 -0
- package/dist/policy/agents-md-budget.js +75 -4
- 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/release/spawn.js +5 -3
- package/dist/scope/decompose.js +4 -3
- 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/scope/vbrief-ref.js +3 -3
- package/dist/swarm/complete-cohort.js +6 -6
- package/dist/swarm/launch.js +2 -2
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +69 -58
- package/dist/triage/queue/cache.d.ts +14 -0
- package/dist/triage/queue/cache.js +61 -1
- package/dist/validate-content/validate-links.js +2 -2
- package/dist/value/readback.d.ts +1 -0
- package/dist/value/readback.js +5 -1
- package/dist/vbrief-reconcile/umbrellas.js +12 -11
- package/dist/verify-env/toolchain-check.js +20 -5
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +17 -15
- package/dist/xbrief-migrate/migrate-project.js +9 -0
- package/package.json +7 -3
|
@@ -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/release/spawn.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
3
3
|
export function defaultWhich(name) {
|
|
4
|
-
const
|
|
4
|
+
const locator = process.platform === "win32" ? "where" : "which";
|
|
5
|
+
const result = spawnSync(locator, [name], { encoding: "utf8" });
|
|
5
6
|
if (result.status !== 0) {
|
|
6
7
|
return null;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// `where` on Windows may return multiple lines; take the first non-empty match.
|
|
10
|
+
const first = (result.stdout ?? "").split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
11
|
+
return first ? first.trim() : null;
|
|
10
12
|
}
|
|
11
13
|
export function spawnText(cmd, args, options = {}) {
|
|
12
14
|
const result = spawnSync(cmd, [...args], {
|
package/dist/scope/decompose.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that draft, writes the child story vBRIEFs, and updates the parent scope references.
|
|
8
8
|
*/
|
|
9
9
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
11
11
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
12
12
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
|
|
13
13
|
import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
|
|
@@ -415,7 +415,7 @@ function vbriefDir(projectRoot) {
|
|
|
415
415
|
function relToVbrief(vbriefDirPath, path) {
|
|
416
416
|
const resolvedPath = resolve(path);
|
|
417
417
|
const resolvedVbrief = resolve(vbriefDirPath);
|
|
418
|
-
if (resolvedPath.startsWith(
|
|
418
|
+
if (resolvedPath.startsWith(resolvedVbrief + sep) || resolvedPath === resolvedVbrief) {
|
|
419
419
|
return resolvedPath.slice(resolvedVbrief.length + 1).replace(/\\/g, "/");
|
|
420
420
|
}
|
|
421
421
|
throw new DecompositionError(`${path}: path must be inside ${vbriefDirPath}`);
|
|
@@ -823,7 +823,8 @@ export function applyDecomposition(opts) {
|
|
|
823
823
|
if (!LIFECYCLE_FOLDERS.has(outputFolderName)) {
|
|
824
824
|
throw new DecompositionError("output_dir must be a vbrief lifecycle folder");
|
|
825
825
|
}
|
|
826
|
-
|
|
826
|
+
const resolvedVbriefDir = resolve(vbriefDirPath);
|
|
827
|
+
if (!outputDir.startsWith(resolvedVbriefDir + sep) && outputDir !== resolvedVbriefDir) {
|
|
827
828
|
throw new DecompositionError("output_dir must be inside vbrief/");
|
|
828
829
|
}
|
|
829
830
|
if (outputFolderName === "active") {
|
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
|
package/dist/scope/vbrief-ref.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { resolve, sep } from "node:path";
|
|
2
2
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
3
3
|
import { resolveLifecycleArtifactRef, } from "../layout/lifecycle-ref.js";
|
|
4
4
|
import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
|
|
@@ -87,7 +87,7 @@ export function scopeIdsForFilename(filename) {
|
|
|
87
87
|
export function relativeToVbrief(path, vbriefRoot) {
|
|
88
88
|
const resolved = resolve(path);
|
|
89
89
|
const root = resolve(vbriefRoot);
|
|
90
|
-
if (!resolved.startsWith(
|
|
90
|
+
if (!resolved.startsWith(root + sep) && resolved !== root) {
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
93
|
return resolved.slice(root.length + 1).replace(/\\/g, "/");
|
|
@@ -95,7 +95,7 @@ export function relativeToVbrief(path, vbriefRoot) {
|
|
|
95
95
|
export function canonicalRelpath(filePath, projectRoot) {
|
|
96
96
|
const resolved = resolve(filePath);
|
|
97
97
|
const root = resolve(projectRoot);
|
|
98
|
-
if (resolved.startsWith(
|
|
98
|
+
if (resolved.startsWith(root + sep) || resolved === root) {
|
|
99
99
|
return resolved.slice(root.length + (resolved === root ? 0 : 1)).replace(/\\/g, "/");
|
|
100
100
|
}
|
|
101
101
|
return resolved.replace(/\\/g, "/");
|