@deftai/directive-core 0.77.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/empty-populate.d.ts +44 -0
- package/dist/cache/empty-populate.js +117 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/check-updates/index.d.ts +10 -0
- package/dist/check-updates/index.js +81 -1
- package/dist/doctor/main.d.ts +1 -0
- package/dist/doctor/main.js +48 -0
- package/dist/doctor/payload-staleness.js +7 -1
- package/dist/doctor/types.d.ts +3 -0
- package/dist/eval/triggers.d.ts +76 -0
- package/dist/eval/triggers.js +259 -0
- package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
- package/dist/eval-triggers-relocation/evaluate.js +115 -0
- package/dist/eval-triggers-relocation/index.d.ts +2 -0
- package/dist/eval-triggers-relocation/index.js +2 -0
- package/dist/hooks/dispatcher.d.ts +43 -0
- package/dist/hooks/dispatcher.js +173 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/scope.d.ts +12 -0
- package/dist/hooks/scope.js +46 -0
- package/dist/hooks/tools.d.ts +4 -0
- package/dist/hooks/tools.js +23 -0
- package/dist/init-deposit/agent-hooks.d.ts +22 -0
- package/dist/init-deposit/agent-hooks.js +238 -0
- package/dist/init-deposit/hygiene.js +4 -0
- package/dist/init-deposit/index.d.ts +1 -0
- package/dist/init-deposit/index.js +1 -0
- package/dist/init-deposit/init-deposit.js +4 -0
- package/dist/init-deposit/prettierignore.d.ts +21 -0
- package/dist/init-deposit/prettierignore.js +83 -0
- package/dist/init-deposit/refresh.d.ts +3 -1
- package/dist/init-deposit/refresh.js +31 -40
- package/dist/init-deposit/scaffold.js +6 -12
- package/dist/init-deposit/xbrief-projections.d.ts +18 -0
- package/dist/init-deposit/xbrief-projections.js +117 -0
- package/dist/intake/reconcile-issues.d.ts +11 -0
- package/dist/intake/reconcile-issues.js +166 -51
- package/dist/issue-sync/index.d.ts +2 -0
- package/dist/issue-sync/index.js +2 -0
- package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
- package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
- package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
- package/dist/issue-sync/sync-from-xbrief.js +208 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +5 -0
- package/dist/platform/platform-capabilities.js +9 -0
- package/dist/platform/shell-context.d.ts +30 -0
- package/dist/platform/shell-context.js +111 -0
- package/dist/pr-monitor/cadence.d.ts +6 -0
- package/dist/pr-monitor/cadence.js +13 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +7 -0
- package/dist/pr-monitor/monitor.js +59 -13
- package/dist/preflight-cache/evaluate.d.ts +3 -8
- package/dist/preflight-cache/evaluate.js +19 -19
- package/dist/release/build-dist.js +1 -0
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +8 -1
- package/dist/release/flags.js +26 -0
- package/dist/release/issue-state-fetch.d.ts +27 -0
- package/dist/release/issue-state-fetch.js +146 -0
- package/dist/release/main.js +1 -0
- package/dist/release/native-steps.js +6 -4
- package/dist/release/pipeline.js +15 -5
- package/dist/release/preflight.d.ts +6 -2
- package/dist/release/preflight.js +19 -5
- package/dist/release/types.d.ts +3 -1
- package/dist/release-e2e/constants.js +6 -6
- package/dist/release-e2e/flags.d.ts +2 -0
- package/dist/release-e2e/flags.js +8 -4
- package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
- package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
- package/dist/release-e2e/main.js +15 -9
- package/dist/release-e2e/types.d.ts +4 -2
- package/dist/resolution/classify.d.ts +1 -1
- package/dist/resolution/classify.js +36 -11
- package/dist/scope/project-definition-sync.js +2 -150
- package/dist/scope/registry-artifact-sync.d.ts +3 -0
- package/dist/scope/registry-artifact-sync.js +159 -0
- package/dist/scope/specification-sync.d.ts +3 -0
- package/dist/scope/specification-sync.js +16 -0
- package/dist/scope/transition.js +40 -12
- package/dist/session/cache-recovery.d.ts +19 -0
- package/dist/session/cache-recovery.js +26 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +14 -3
- package/dist/session/verify-session-ritual.d.ts +17 -0
- package/dist/session/verify-session-ritual.js +64 -0
- package/dist/triage/help/registry-data.d.ts +12 -1
- package/dist/triage/help/registry-data.js +22 -1
- package/dist/triage/welcome/default-mode.js +2 -0
- package/dist/triage/welcome/onboard.js +2 -0
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +9 -0
- package/dist/vbrief-reconcile/labels.d.ts +3 -0
- package/dist/vbrief-reconcile/labels.js +14 -0
- package/dist/vbrief-reconcile/main.d.ts +1 -0
- package/dist/vbrief-reconcile/main.js +13 -3
- package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
- package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
- package/dist/vbrief-reconcile/repo-guard.js +33 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
- package/dist/vbrief-reconcile/umbrellas.js +17 -1
- package/dist/verify-env/agent-hooks.d.ts +11 -0
- package/dist/verify-env/agent-hooks.js +46 -0
- package/dist/verify-env/command-spawn.d.ts +5 -0
- package/dist/verify-env/command-spawn.js +28 -9
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/dist/verify-env/verify-hooks-installed.js +5 -5
- package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
- package/dist/vitest-runner/coverage-debt.d.ts +50 -0
- package/dist/vitest-runner/coverage-debt.js +155 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
- package/dist/xbrief-migrate/detect.js +46 -1
- package/dist/xbrief-migrate/migrate-project.js +19 -4
- package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
- package/dist/xbrief-migrate/migration-containment.js +37 -0
- package/package.json +19 -3
|
@@ -9,12 +9,12 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync,
|
|
|
9
9
|
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { platform } from "node:os";
|
|
11
11
|
import { dirname, join, relative } from "node:path";
|
|
12
|
-
import { copyTree } from "../deposit/copy-tree.js";
|
|
13
12
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
14
13
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
15
14
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
16
15
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
17
16
|
import { installerManagedGuardEre } from "./hygiene.js";
|
|
17
|
+
import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
|
|
18
18
|
export { CANONICAL_INSTALL_ROOT };
|
|
19
19
|
export const CORE_GLOB = ".deft/core/**";
|
|
20
20
|
/** Refuse init/update projection writes that escape via repo-controlled symlinks (#2446). */
|
|
@@ -336,20 +336,14 @@ export async function writeConsumerVbrief(projectDir, deftDir, io) {
|
|
|
336
336
|
const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
|
|
337
337
|
const vbriefMdPresent = existsSync(vbriefMdDst) && statSync(vbriefMdDst).isFile();
|
|
338
338
|
const lifecyclePresent = vbriefLifecycleDirsPresent(consumerVbrief);
|
|
339
|
+
const schemasChanged = syncConsumerXbriefSchemas(projectDir, deftDir);
|
|
339
340
|
if (schemasPresent && vbriefMdPresent && lifecyclePresent) {
|
|
340
|
-
io.printf(
|
|
341
|
-
|
|
341
|
+
io.printf(schemasChanged
|
|
342
|
+
? "vbrief/ schemas refreshed at project root.\n"
|
|
343
|
+
: "vbrief/ already present at project root — skipping.\n");
|
|
344
|
+
return schemasChanged;
|
|
342
345
|
}
|
|
343
346
|
mkdirSync(consumerVbrief, { recursive: true });
|
|
344
|
-
if (!schemasPresent) {
|
|
345
|
-
const fwSchemas = join(deftDir, "vbrief", "schemas");
|
|
346
|
-
if (existsSync(fwSchemas) && statSync(fwSchemas).isDirectory()) {
|
|
347
|
-
await copyTree(fwSchemas, schemasDst);
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
mkdirSync(schemasDst, { recursive: true });
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
347
|
if (!vbriefMdPresent) {
|
|
354
348
|
const fwVbriefMd = join(deftDir, "vbrief", "vbrief.md");
|
|
355
349
|
if (existsSync(fwVbriefMd)) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent consumer xBRIEF derivatives maintained by init/update (#2595).
|
|
3
|
+
*
|
|
4
|
+
* The framework payload manifest and consumer projections have independent
|
|
5
|
+
* freshness. A current `.deft/core/VERSION` therefore cannot short-circuit
|
|
6
|
+
* these repairs.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Synchronize framework-owned xBRIEF schemas while preserving unknown consumer
|
|
10
|
+
* files. The obsolete v0.6 root schema is the only destination-only file this
|
|
11
|
+
* repair removes.
|
|
12
|
+
*/
|
|
13
|
+
export declare function syncConsumerXbriefSchemas(projectDir: string, deftDir: string): boolean;
|
|
14
|
+
/** Regenerate the bare consumer version derivative, retaining the historical root fallback. */
|
|
15
|
+
export declare function syncBareVersionMarker(projectDir: string, version: string): boolean;
|
|
16
|
+
/** Repair an existing marker without creating root state when no lifecycle artifact exists. */
|
|
17
|
+
export declare function syncExistingBareVersionMarker(projectDir: string, version: string): boolean;
|
|
18
|
+
//# sourceMappingURL=xbrief-projections.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent consumer xBRIEF derivatives maintained by init/update (#2595).
|
|
3
|
+
*
|
|
4
|
+
* The framework payload manifest and consumer projections have independent
|
|
5
|
+
* freshness. A current `.deft/core/VERSION` therefore cannot short-circuit
|
|
6
|
+
* these repairs.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
import { assertProjectionContained } from "../fs/projection-containment.js";
|
|
11
|
+
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
12
|
+
import { DEV_FALLBACK } from "../platform/constants.js";
|
|
13
|
+
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
14
|
+
const OBSOLETE_CORE_SCHEMA = "vbrief-core.schema.json";
|
|
15
|
+
const CURRENT_CORE_SCHEMA = "xbrief-core-0.8.schema.json";
|
|
16
|
+
function normalizeVersion(version) {
|
|
17
|
+
return version.trim().replace(/^v/, "");
|
|
18
|
+
}
|
|
19
|
+
function isDirectory(path) {
|
|
20
|
+
try {
|
|
21
|
+
return statSync(path).isDirectory();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function collectSchemaFiles(root, dir = root, files = []) {
|
|
28
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
29
|
+
const full = join(dir, entry.name);
|
|
30
|
+
if (entry.isSymbolicLink()) {
|
|
31
|
+
throw new Error(`refusing xbrief schema projection from symlink: ${full}`);
|
|
32
|
+
}
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
collectSchemaFiles(root, full, files);
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile()) {
|
|
37
|
+
files.push(relative(root, full));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
function writeFileIfChanged(projectDir, target, content) {
|
|
43
|
+
assertProjectionContained(projectDir, target);
|
|
44
|
+
const desired = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
45
|
+
try {
|
|
46
|
+
if (readFileSync(target).equals(desired))
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Missing or unreadable target is replaced below.
|
|
51
|
+
}
|
|
52
|
+
mkdirSync(join(target, ".."), { recursive: true });
|
|
53
|
+
writeFileSync(target, desired);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Synchronize framework-owned xBRIEF schemas while preserving unknown consumer
|
|
58
|
+
* files. The obsolete v0.6 root schema is the only destination-only file this
|
|
59
|
+
* repair removes.
|
|
60
|
+
*/
|
|
61
|
+
export function syncConsumerXbriefSchemas(projectDir, deftDir) {
|
|
62
|
+
const sourceDir = join(deftDir, "vbrief", "schemas");
|
|
63
|
+
const currentSource = join(sourceDir, CURRENT_CORE_SCHEMA);
|
|
64
|
+
if (!isDirectory(sourceDir) || !existsSync(currentSource) || !statSync(currentSource).isFile()) {
|
|
65
|
+
throw new Error(`cannot project xbrief schemas: framework payload is missing ${CURRENT_CORE_SCHEMA}`);
|
|
66
|
+
}
|
|
67
|
+
const destinationDir = join(projectDir, MIGRATED_ARTIFACT_DIR, "schemas");
|
|
68
|
+
assertProjectionContained(projectDir, destinationDir);
|
|
69
|
+
mkdirSync(destinationDir, { recursive: true });
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const rel of collectSchemaFiles(sourceDir)) {
|
|
72
|
+
if (rel === OBSOLETE_CORE_SCHEMA)
|
|
73
|
+
continue;
|
|
74
|
+
const source = join(sourceDir, rel);
|
|
75
|
+
const destination = join(destinationDir, rel);
|
|
76
|
+
changed = writeFileIfChanged(projectDir, destination, readFileSync(source)) || changed;
|
|
77
|
+
}
|
|
78
|
+
const obsoleteDestination = join(destinationDir, OBSOLETE_CORE_SCHEMA);
|
|
79
|
+
assertProjectionContained(projectDir, obsoleteDestination);
|
|
80
|
+
if (existsSync(obsoleteDestination)) {
|
|
81
|
+
rmSync(obsoleteDestination, { force: true });
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
return changed;
|
|
85
|
+
}
|
|
86
|
+
function syncBareVersionMarkerWithPolicy(projectDir, version, allowRootFallback) {
|
|
87
|
+
const normalized = normalizeVersion(version);
|
|
88
|
+
if (!normalized || normalized === DEV_FALLBACK)
|
|
89
|
+
return false;
|
|
90
|
+
const canonicalRoot = join(projectDir, MIGRATED_ARTIFACT_DIR);
|
|
91
|
+
if (existsSync(canonicalRoot)) {
|
|
92
|
+
assertProjectionContained(projectDir, join(canonicalRoot, ".deft-version"));
|
|
93
|
+
}
|
|
94
|
+
let targetDir = projectDir;
|
|
95
|
+
try {
|
|
96
|
+
targetDir = resolveLifecycleRoot(projectDir);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Preserve the historical root fallback for payload-changing refreshes,
|
|
100
|
+
// and repair an existing fallback on no-op refreshes without creating new
|
|
101
|
+
// untracked state (#2118 / #2595).
|
|
102
|
+
const rootMarker = join(projectDir, ".deft-version");
|
|
103
|
+
if (!allowRootFallback && !existsSync(rootMarker))
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const target = join(targetDir, ".deft-version");
|
|
107
|
+
return writeFileIfChanged(projectDir, target, `${normalized}\n`);
|
|
108
|
+
}
|
|
109
|
+
/** Regenerate the bare consumer version derivative, retaining the historical root fallback. */
|
|
110
|
+
export function syncBareVersionMarker(projectDir, version) {
|
|
111
|
+
return syncBareVersionMarkerWithPolicy(projectDir, version, true);
|
|
112
|
+
}
|
|
113
|
+
/** Repair an existing marker without creating root state when no lifecycle artifact exists. */
|
|
114
|
+
export function syncExistingBareVersionMarker(projectDir, version) {
|
|
115
|
+
return syncBareVersionMarkerWithPolicy(projectDir, version, false);
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=xbrief-projections.js.map
|
|
@@ -26,13 +26,19 @@ export interface FetchIssuesOptions {
|
|
|
26
26
|
export declare function fetchOpenIssues(repo: string, options?: FetchIssuesOptions): Record<string, unknown>[] | null;
|
|
27
27
|
export declare function fetchAllOpenIssues(repo: string, options?: FetchIssuesOptions): Record<string, unknown>[] | null;
|
|
28
28
|
export interface FetchIssueStatesOptions extends FetchIssuesOptions {
|
|
29
|
+
/** @deprecated REST fetch is per-issue; batch size is ignored (#2557 / #954). */
|
|
29
30
|
readonly batchSize?: number;
|
|
30
31
|
}
|
|
31
32
|
export declare function fetchIssueStates(repo: string, issueNumbers: ReadonlySet<number>, options?: FetchIssueStatesOptions): Map<number, IssueState> | null;
|
|
33
|
+
export interface CompletedStatusDriftEntry {
|
|
34
|
+
readonly rel_path: string;
|
|
35
|
+
readonly status: string;
|
|
36
|
+
}
|
|
32
37
|
export interface ReconcileReport {
|
|
33
38
|
linked: Record<string, unknown>[];
|
|
34
39
|
no_open_issue: Record<string, unknown>[];
|
|
35
40
|
unlinked?: Record<string, unknown>[];
|
|
41
|
+
completed_status_drift?: CompletedStatusDriftEntry[];
|
|
36
42
|
summary: Record<string, number>;
|
|
37
43
|
}
|
|
38
44
|
export declare function reconcile(issueToVbriefs: Map<number, string[]>, issueStateMap: Map<number, IssueState | string>): ReconcileReport;
|
|
@@ -42,10 +48,15 @@ export declare function parseParentIssue(data: Record<string, unknown>): number
|
|
|
42
48
|
export declare function parseDecompositionOrigin(data: Record<string, unknown>): number | null;
|
|
43
49
|
export declare function resolveLifecycleAnchor(data: Record<string, unknown>): [number | null, string];
|
|
44
50
|
export declare function scanLifecycleAnchors(vbriefDir: string): Record<string, unknown>[];
|
|
51
|
+
/** Scan completed/ for xBRIEFs whose plan.status is not terminal (D2 drift, #2578). */
|
|
52
|
+
export declare function scanCompletedStatusDrift(vbriefDir: string): CompletedStatusDriftEntry[];
|
|
53
|
+
export declare function attachCompletedStatusDrift(report: ReconcileReport, drift: CompletedStatusDriftEntry[]): ReconcileReport;
|
|
45
54
|
export declare function buildLifecycleReport(anchors: Record<string, unknown>[], issueStateMap: Map<number, IssueState | string>, log?: boolean): ReconcileReport;
|
|
46
55
|
export declare function formatJson(report: ReconcileReport): string;
|
|
47
56
|
export declare function formatMarkdown(report: ReconcileReport): string;
|
|
48
57
|
export declare function applyLifecycleFixes(vbriefDir: string, report: ReconcileReport, projectRoot?: string | null): [number, number, string[]];
|
|
58
|
+
/** Repair completed/ files whose plan.status is not terminal (#2578). */
|
|
59
|
+
export declare function repairCompletedStatusDrift(vbriefDir: string, drift: readonly CompletedStatusDriftEntry[]): [number, number, string[]];
|
|
49
60
|
export declare function detectRepo(): string | null;
|
|
50
61
|
export interface ReconcileCliArgs {
|
|
51
62
|
vbriefDir?: string;
|
|
@@ -6,6 +6,7 @@ import { call } from "../scm/call.js";
|
|
|
6
6
|
import { updateDecomposedChildBackReferences } from "../scope/decomposed-refs.js";
|
|
7
7
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
8
8
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
9
|
+
import { FOLDER_ALLOWED_STATUSES } from "../vbrief-validate/constants.js";
|
|
9
10
|
import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
|
|
10
11
|
export const LIFECYCLE_FOLDERS = [
|
|
11
12
|
"proposed",
|
|
@@ -188,6 +189,52 @@ function splitRepoSlug(repo) {
|
|
|
188
189
|
}
|
|
189
190
|
return [parts[0], parts[1]];
|
|
190
191
|
}
|
|
192
|
+
function normalizeRestIssueState(raw) {
|
|
193
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
194
|
+
return "NOT_FOUND";
|
|
195
|
+
}
|
|
196
|
+
return raw.toUpperCase();
|
|
197
|
+
}
|
|
198
|
+
function normalizeRestStateReason(raw) {
|
|
199
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return raw.toUpperCase();
|
|
203
|
+
}
|
|
204
|
+
function isRestNotFoundResult(result) {
|
|
205
|
+
const stderr = result.stderr.trim();
|
|
206
|
+
return (stderr.includes("404") ||
|
|
207
|
+
stderr.includes("Not Found") ||
|
|
208
|
+
stderr.includes("Could not resolve to an Issue"));
|
|
209
|
+
}
|
|
210
|
+
function issueStateFromRestPayload(payload) {
|
|
211
|
+
return new IssueState(normalizeRestIssueState(payload.state), normalizeRestStateReason(payload.state_reason));
|
|
212
|
+
}
|
|
213
|
+
function fetchOneIssueStateRest(owner, name, issueNumber, scmCall, cwd) {
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
result = scmCall("github-issue", "api", [`repos/${owner}/${name}/issues/${issueNumber}`], {
|
|
217
|
+
timeout: 30,
|
|
218
|
+
cwd,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return "CLI_MISSING";
|
|
223
|
+
}
|
|
224
|
+
if (result.returncode !== 0) {
|
|
225
|
+
if (isRestNotFoundResult(result)) {
|
|
226
|
+
return new IssueState("NOT_FOUND", null);
|
|
227
|
+
}
|
|
228
|
+
process.stderr.write(`Error: gh REST failed fetching issue #${issueNumber}: ${result.stderr.trim()}\n`);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return issueStateFromRestPayload(JSON.parse(result.stdout));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
191
238
|
export function fetchIssueStates(repo, issueNumbers, options = {}) {
|
|
192
239
|
if (issueNumbers.size === 0) {
|
|
193
240
|
return new Map();
|
|
@@ -198,63 +245,19 @@ export function fetchIssueStates(repo, issueNumbers, options = {}) {
|
|
|
198
245
|
return null;
|
|
199
246
|
}
|
|
200
247
|
const [owner, name] = parsed;
|
|
201
|
-
const batchSize = options.batchSize ?? GRAPHQL_BATCH_SIZE;
|
|
202
248
|
const sortedNumbers = [...issueNumbers].sort((a, b) => a - b);
|
|
203
249
|
const states = new Map();
|
|
204
250
|
const scmCall = options.scmCall ?? call;
|
|
205
|
-
for (
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
.map((n) => `i${n}: issue(number: ${n}) { state stateReason }`)
|
|
209
|
-
.join("\n ");
|
|
210
|
-
const query = `query {\n repository(owner: "${owner}", name: "${name}") {\n ${aliases}\n }\n}\n`;
|
|
211
|
-
let result;
|
|
212
|
-
try {
|
|
213
|
-
result = scmCall("github-issue", "api", ["graphql", "-f", `query=${query}`], {
|
|
214
|
-
timeout: 60,
|
|
215
|
-
cwd: options.cwd ?? undefined,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
251
|
+
for (const n of sortedNumbers) {
|
|
252
|
+
const fetched = fetchOneIssueStateRest(owner, name, n, scmCall, options.cwd ?? undefined);
|
|
253
|
+
if (fetched === "CLI_MISSING") {
|
|
219
254
|
process.stderr.write("Error: gh CLI not found. Install GitHub CLI.\n");
|
|
220
255
|
return null;
|
|
221
256
|
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
payload = result.stdout ? JSON.parse(result.stdout) : null;
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
payload = null;
|
|
228
|
-
}
|
|
229
|
-
if (result.returncode !== 0) {
|
|
230
|
-
if (payload === null || typeof payload.data !== "object" || payload.data === null) {
|
|
231
|
-
process.stderr.write(`Error: gh CLI failed: ${result.stderr.trim()}\n`);
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
const firstLine = result.stderr.trim().split("\n")[0] ?? "";
|
|
235
|
-
process.stderr.write(`Warning: gh GraphQL returned partial errors (likely PR numbers referenced as issues): ${firstLine}\n`);
|
|
236
|
-
}
|
|
237
|
-
if (payload === null) {
|
|
238
|
-
process.stderr.write("Error: failed to parse gh CLI graphql output.\n");
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
const repoData = payload.data?.repository;
|
|
242
|
-
if (repoData === null || typeof repoData !== "object" || Array.isArray(repoData)) {
|
|
243
|
-
process.stderr.write("Error: gh CLI graphql response missing repository payload.\n");
|
|
257
|
+
if (fetched === null) {
|
|
244
258
|
return null;
|
|
245
259
|
}
|
|
246
|
-
|
|
247
|
-
const node = repoData[`i${n}`];
|
|
248
|
-
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
249
|
-
const nodeObj = node;
|
|
250
|
-
if (typeof nodeObj.state === "string") {
|
|
251
|
-
const reason = typeof nodeObj.stateReason === "string" ? nodeObj.stateReason : null;
|
|
252
|
-
states.set(n, new IssueState(nodeObj.state, reason));
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
states.set(n, new IssueState("NOT_FOUND", null));
|
|
257
|
-
}
|
|
260
|
+
states.set(n, fetched);
|
|
258
261
|
}
|
|
259
262
|
return states;
|
|
260
263
|
}
|
|
@@ -433,6 +436,52 @@ export function scanLifecycleAnchors(vbriefDir) {
|
|
|
433
436
|
}
|
|
434
437
|
return anchors;
|
|
435
438
|
}
|
|
439
|
+
const COMPLETED_TERMINAL_STATUSES = FOLDER_ALLOWED_STATUSES.completed ?? new Set(["completed", "failed"]);
|
|
440
|
+
/** Scan completed/ for xBRIEFs whose plan.status is not terminal (D2 drift, #2578). */
|
|
441
|
+
export function scanCompletedStatusDrift(vbriefDir) {
|
|
442
|
+
const completedDir = join(vbriefDir, "completed");
|
|
443
|
+
const drift = [];
|
|
444
|
+
try {
|
|
445
|
+
if (!statSync(completedDir).isDirectory()) {
|
|
446
|
+
return drift;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return drift;
|
|
451
|
+
}
|
|
452
|
+
for (const filename of readdirSync(completedDir)
|
|
453
|
+
.filter((f) => hasArtifactSuffix(f))
|
|
454
|
+
.sort()) {
|
|
455
|
+
const relPath = `completed/${filename}`;
|
|
456
|
+
let data;
|
|
457
|
+
try {
|
|
458
|
+
data = JSON.parse(readFileSync(join(completedDir, filename), "utf8"));
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const plan = data.plan;
|
|
464
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const status = String(plan.status ?? "");
|
|
468
|
+
if (status.length === 0 || COMPLETED_TERMINAL_STATUSES.has(status)) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
drift.push({ rel_path: relPath, status });
|
|
472
|
+
}
|
|
473
|
+
return drift;
|
|
474
|
+
}
|
|
475
|
+
export function attachCompletedStatusDrift(report, drift) {
|
|
476
|
+
return {
|
|
477
|
+
...report,
|
|
478
|
+
completed_status_drift: drift,
|
|
479
|
+
summary: {
|
|
480
|
+
...report.summary,
|
|
481
|
+
completed_status_drift_count: drift.length,
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
436
485
|
export function buildLifecycleReport(anchors, issueStateMap, log = true) {
|
|
437
486
|
const linked = [];
|
|
438
487
|
const noOpenIssue = [];
|
|
@@ -525,6 +574,18 @@ export function formatMarkdown(report) {
|
|
|
525
574
|
lines.push("None.");
|
|
526
575
|
}
|
|
527
576
|
lines.push("");
|
|
577
|
+
lines.push("## (d) completed/ xBRIEFs with non-terminal plan.status (D2 drift)", "");
|
|
578
|
+
const drift = report.completed_status_drift ?? [];
|
|
579
|
+
if (drift.length > 0) {
|
|
580
|
+
for (const entry of drift) {
|
|
581
|
+
const safeStatus = entry.status.replace(/\r?\n/g, " ");
|
|
582
|
+
lines.push(`- \`${entry.rel_path}\` -- plan.status='${safeStatus}'`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
lines.push("None.");
|
|
587
|
+
}
|
|
588
|
+
lines.push("");
|
|
528
589
|
return lines.join("\n");
|
|
529
590
|
}
|
|
530
591
|
function utcNowIso() {
|
|
@@ -661,6 +722,54 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
|
|
|
661
722
|
}
|
|
662
723
|
return [moved, skipped, failures];
|
|
663
724
|
}
|
|
725
|
+
/** Repair completed/ files whose plan.status is not terminal (#2578). */
|
|
726
|
+
export function repairCompletedStatusDrift(vbriefDir, drift) {
|
|
727
|
+
let repaired = 0;
|
|
728
|
+
let skipped = 0;
|
|
729
|
+
const failures = [];
|
|
730
|
+
const completedDir = join(vbriefDir, "completed");
|
|
731
|
+
for (const entry of drift) {
|
|
732
|
+
const slash = entry.rel_path.indexOf("/");
|
|
733
|
+
if (slash < 0) {
|
|
734
|
+
failures.push(`unexpected vBRIEF path shape (no folder): ${JSON.stringify(entry.rel_path)}`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const filename = entry.rel_path.slice(slash + 1);
|
|
738
|
+
const src = join(completedDir, filename);
|
|
739
|
+
let data;
|
|
740
|
+
try {
|
|
741
|
+
data = JSON.parse(readFileSync(src, "utf8"));
|
|
742
|
+
}
|
|
743
|
+
catch (exc) {
|
|
744
|
+
failures.push(`failed to parse ${entry.rel_path}: ${String(exc)}`);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const plan = (data.plan ?? {});
|
|
748
|
+
data.plan = plan;
|
|
749
|
+
const currentStatus = String(plan.status ?? "");
|
|
750
|
+
if (COMPLETED_TERMINAL_STATUSES.has(currentStatus)) {
|
|
751
|
+
skipped += 1;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const stamp = utcNowIso();
|
|
755
|
+
plan.status = "completed";
|
|
756
|
+
plan.updated = stamp;
|
|
757
|
+
const infoKey = MIGRATED_INFO_ROOT_KEY in data ? MIGRATED_INFO_ROOT_KEY : LEGACY_INFO_ROOT_KEY;
|
|
758
|
+
const info = (data[infoKey] ?? {});
|
|
759
|
+
data[infoKey] = info;
|
|
760
|
+
info.updated = stamp;
|
|
761
|
+
propagateItemStatus(plan.items, "completed", stamp);
|
|
762
|
+
try {
|
|
763
|
+
writeFileSync(src, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
764
|
+
}
|
|
765
|
+
catch (exc) {
|
|
766
|
+
failures.push(`failed to write ${entry.rel_path}: ${String(exc)}`);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
repaired += 1;
|
|
770
|
+
}
|
|
771
|
+
return [repaired, skipped, failures];
|
|
772
|
+
}
|
|
664
773
|
export function detectRepo() {
|
|
665
774
|
try {
|
|
666
775
|
const stdout = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
@@ -742,6 +851,8 @@ export function reconcileMain(args) {
|
|
|
742
851
|
report = reconcile(issueToVbriefsObj, issueStateMap);
|
|
743
852
|
}
|
|
744
853
|
const fmt = args.format ?? "markdown";
|
|
854
|
+
const completedDrift = scanCompletedStatusDrift(vbriefDir);
|
|
855
|
+
report = attachCompletedStatusDrift(report, completedDrift);
|
|
745
856
|
if (fmt === "json") {
|
|
746
857
|
process.stdout.write(formatJson(report));
|
|
747
858
|
}
|
|
@@ -759,11 +870,15 @@ export function reconcileMain(args) {
|
|
|
759
870
|
return acc;
|
|
760
871
|
}, 0);
|
|
761
872
|
const [moved, skipped, failures] = applyLifecycleFixes(vbriefDir, applyReport, projectRoot);
|
|
873
|
+
const driftAfterMove = scanCompletedStatusDrift(vbriefDir);
|
|
874
|
+
const [driftRepaired, driftSkipped, driftFailures] = repairCompletedStatusDrift(vbriefDir, driftAfterMove);
|
|
875
|
+
const allFailures = [...failures, ...driftFailures];
|
|
762
876
|
process.stderr.write(`[${moved}/${candidates}] vBRIEFs reconciled (moved=${moved}, already-terminal=${skipped}, failures=${failures.length})\n`);
|
|
763
|
-
|
|
877
|
+
process.stderr.write(`[${driftRepaired}/${completedDrift.length}] completed/ status drift repaired (repaired=${driftRepaired}, already-terminal=${driftSkipped}, failures=${driftFailures.length})\n`);
|
|
878
|
+
for (const f of allFailures) {
|
|
764
879
|
process.stderr.write(` -- FAIL: ${f}\n`);
|
|
765
880
|
}
|
|
766
|
-
if (
|
|
881
|
+
if (allFailures.length > 0) {
|
|
767
882
|
return 1;
|
|
768
883
|
}
|
|
769
884
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface ParsedSyncFromXbriefCliArgs {
|
|
3
|
+
path?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
projectRoot?: string;
|
|
6
|
+
repo?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseArgs(argv: readonly string[]): ParsedSyncFromXbriefCliArgs;
|
|
10
|
+
export declare function mainEntry(argv?: string[]): number;
|
|
11
|
+
//# sourceMappingURL=sync-from-xbrief-cli.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { syncFromXbriefMain } from "./sync-from-xbrief.js";
|
|
4
|
+
export function parseArgs(argv) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
7
|
+
const arg = argv[i];
|
|
8
|
+
if (arg === "--dry-run") {
|
|
9
|
+
out.dryRun = true;
|
|
10
|
+
}
|
|
11
|
+
else if (arg === "--project-root") {
|
|
12
|
+
const value = argv[i + 1];
|
|
13
|
+
if (value === undefined) {
|
|
14
|
+
return { ...out, error: "argument --project-root: expected one argument" };
|
|
15
|
+
}
|
|
16
|
+
out.projectRoot = value;
|
|
17
|
+
i += 1;
|
|
18
|
+
}
|
|
19
|
+
else if (arg === "--repo") {
|
|
20
|
+
const value = argv[i + 1];
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
return { ...out, error: "argument --repo: expected one argument" };
|
|
23
|
+
}
|
|
24
|
+
out.repo = value;
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (!arg.startsWith("-")) {
|
|
28
|
+
out.path = arg;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function mainEntry(argv = process.argv.slice(2)) {
|
|
34
|
+
const args = parseArgs(argv);
|
|
35
|
+
if (args.error !== undefined) {
|
|
36
|
+
process.stderr.write(`issue:sync-from-xbrief: ${args.error}\n`);
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
return syncFromXbriefMain(args);
|
|
40
|
+
}
|
|
41
|
+
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
42
|
+
process.exit(mainEntry());
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=sync-from-xbrief-cli.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type RunGhApiFn } from "../intake/github-body.js";
|
|
2
|
+
export declare const SYNC_COMMENT_HEADER = "## xBRIEF sync (deft issue:sync-from-xbrief)";
|
|
3
|
+
export interface OriginIssueTarget {
|
|
4
|
+
readonly repo: string;
|
|
5
|
+
readonly number: number;
|
|
6
|
+
readonly uri: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SyncSnapshot {
|
|
9
|
+
readonly status: string;
|
|
10
|
+
readonly title: string;
|
|
11
|
+
readonly acceptance: string;
|
|
12
|
+
readonly items: readonly {
|
|
13
|
+
title: string;
|
|
14
|
+
status: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
export interface ResolveOriginOptions {
|
|
18
|
+
readonly fallbackRepo?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function resolveOriginIssue(data: Record<string, unknown>, options?: ResolveOriginOptions): OriginIssueTarget | null;
|
|
21
|
+
export declare function extractSyncSnapshot(data: Record<string, unknown>): SyncSnapshot;
|
|
22
|
+
export declare function fingerprintSyncSnapshot(snapshot: SyncSnapshot): string;
|
|
23
|
+
export declare function readStoredFingerprint(data: Record<string, unknown>): string | null;
|
|
24
|
+
export declare function hasMaterialChanges(data: Record<string, unknown>): boolean;
|
|
25
|
+
export declare function sanitizeMarkdownInline(text: string): string;
|
|
26
|
+
export declare function buildSyncComment(data: Record<string, unknown>, xbriefPath: string): string;
|
|
27
|
+
export interface SyncFromXbriefOptions {
|
|
28
|
+
readonly xbriefPath: string;
|
|
29
|
+
readonly dryRun?: boolean;
|
|
30
|
+
readonly projectRoot?: string;
|
|
31
|
+
readonly repo?: string;
|
|
32
|
+
readonly runFn?: RunGhApiFn;
|
|
33
|
+
readonly writeErr?: (message: string) => void;
|
|
34
|
+
readonly writeOut?: (message: string) => void;
|
|
35
|
+
readonly writeFingerprint?: (absPath: string, data: Record<string, unknown>) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function stampIssueSyncFingerprint(data: Record<string, unknown>, origin: OriginIssueTarget): Record<string, unknown>;
|
|
38
|
+
export declare function syncFromXbrief(options: SyncFromXbriefOptions): number;
|
|
39
|
+
export interface SyncFromXbriefCliArgs {
|
|
40
|
+
readonly path?: string;
|
|
41
|
+
readonly dryRun?: boolean;
|
|
42
|
+
readonly projectRoot?: string;
|
|
43
|
+
readonly repo?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function syncFromXbriefMain(args: SyncFromXbriefCliArgs): number;
|
|
46
|
+
//# sourceMappingURL=sync-from-xbrief.d.ts.map
|