@deftai/directive-core 0.68.1 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/deposit/contain.d.ts +41 -0
  2. package/dist/deposit/contain.js +113 -0
  3. package/dist/deposit/copy-tree.js +8 -2
  4. package/dist/doctor/checks.d.ts +11 -0
  5. package/dist/doctor/checks.js +75 -2
  6. package/dist/doctor/constants.d.ts +9 -0
  7. package/dist/doctor/constants.js +11 -0
  8. package/dist/doctor/main.d.ts +47 -1
  9. package/dist/doctor/main.js +406 -3
  10. package/dist/doctor/manifest.d.ts +20 -0
  11. package/dist/doctor/manifest.js +22 -0
  12. package/dist/doctor/payload-staleness.d.ts +8 -0
  13. package/dist/doctor/payload-staleness.js +11 -8
  14. package/dist/doctor/types.d.ts +56 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +2 -0
  17. package/dist/init-deposit/gitignore.d.ts +18 -0
  18. package/dist/init-deposit/gitignore.js +94 -15
  19. package/dist/init-deposit/headless-manifest.d.ts +87 -0
  20. package/dist/init-deposit/headless-manifest.js +261 -0
  21. package/dist/init-deposit/index.d.ts +3 -0
  22. package/dist/init-deposit/index.js +3 -0
  23. package/dist/init-deposit/init-deposit.js +6 -0
  24. package/dist/init-deposit/init-dispatch.d.ts +90 -0
  25. package/dist/init-deposit/init-dispatch.js +170 -0
  26. package/dist/init-deposit/refresh.d.ts +79 -2
  27. package/dist/init-deposit/refresh.js +182 -5
  28. package/dist/init-deposit/scaffold.d.ts +25 -0
  29. package/dist/init-deposit/scaffold.js +57 -0
  30. package/dist/init-deposit/untrack-core.d.ts +84 -0
  31. package/dist/init-deposit/untrack-core.js +150 -0
  32. package/dist/intake/issue-ingest.d.ts +11 -0
  33. package/dist/intake/issue-ingest.js +86 -11
  34. package/dist/policy/plan-extensions.d.ts +31 -0
  35. package/dist/policy/plan-extensions.js +45 -0
  36. package/dist/policy/wip.d.ts +2 -2
  37. package/dist/policy/wip.js +2 -2
  38. package/dist/resolution/classify.d.ts +41 -0
  39. package/dist/resolution/classify.js +138 -0
  40. package/dist/resolution/engine-ladder.d.ts +98 -0
  41. package/dist/resolution/engine-ladder.js +185 -0
  42. package/dist/resolution/index.d.ts +19 -0
  43. package/dist/resolution/index.js +19 -0
  44. package/dist/resolution/integrity.d.ts +48 -0
  45. package/dist/resolution/integrity.js +80 -0
  46. package/dist/resolution/package-manager.d.ts +77 -0
  47. package/dist/resolution/package-manager.js +103 -0
  48. package/dist/resolution/pin.d.ts +73 -0
  49. package/dist/resolution/pin.js +169 -0
  50. package/dist/resolution/plan.d.ts +66 -0
  51. package/dist/resolution/plan.js +219 -0
  52. package/dist/resolution/skew-policy.d.ts +46 -0
  53. package/dist/resolution/skew-policy.js +120 -0
  54. package/dist/session/session-start.d.ts +2 -0
  55. package/dist/session/session-start.js +28 -1
  56. package/dist/swarm/routing.js +10 -3
  57. package/dist/triage/bulk/index.d.ts +11 -2
  58. package/dist/triage/bulk/index.js +41 -4
  59. package/dist/triage/help/registry-data.d.ts +5 -5
  60. package/dist/triage/help/registry-data.js +18 -5
  61. package/dist/triage/scope/cli.d.ts +2 -1
  62. package/dist/triage/scope/cli.js +30 -6
  63. package/dist/triage/scope/mutations.d.ts +10 -0
  64. package/dist/triage/scope/mutations.js +22 -0
  65. package/dist/triage/welcome/constants.d.ts +1 -2
  66. package/dist/triage/welcome/constants.js +1 -2
  67. package/dist/triage/welcome/index.d.ts +1 -0
  68. package/dist/triage/welcome/index.js +1 -0
  69. package/dist/triage/welcome/onboard.d.ts +35 -0
  70. package/dist/triage/welcome/onboard.js +94 -0
  71. package/dist/umbrella-current-shape/index.d.ts +25 -1
  72. package/dist/umbrella-current-shape/index.js +84 -4
  73. package/dist/user-config/index.d.ts +2 -0
  74. package/dist/user-config/index.js +2 -0
  75. package/dist/user-config/resolve-user-md.d.ts +76 -0
  76. package/dist/user-config/resolve-user-md.js +120 -0
  77. package/dist/vbrief-build/project-definition-io.d.ts +6 -0
  78. package/dist/vbrief-build/project-definition-io.js +7 -2
  79. package/dist/vbrief-validate/precutover.js +7 -3
  80. package/dist/xbrief-migrate/constants.d.ts +10 -0
  81. package/dist/xbrief-migrate/constants.js +19 -0
  82. package/dist/xbrief-migrate/detect.d.ts +34 -0
  83. package/dist/xbrief-migrate/detect.js +33 -0
  84. package/dist/xbrief-migrate/fs-helpers.d.ts +13 -0
  85. package/dist/xbrief-migrate/fs-helpers.js +46 -1
  86. package/dist/xbrief-migrate/index.d.ts +4 -2
  87. package/dist/xbrief-migrate/index.js +4 -2
  88. package/dist/xbrief-migrate/migrate-project.d.ts +23 -1
  89. package/dist/xbrief-migrate/migrate-project.js +97 -8
  90. package/dist/xbrief-migrate/signpost.d.ts +7 -1
  91. package/dist/xbrief-migrate/signpost.js +18 -3
  92. package/package.json +7 -3
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LEGACY_VBRIEF_VERSION, MIGRATED_ARTIFACT_DIR, VBRIEF_REFERENCE_PREFIX, } from "./constants.js";
4
+ import { hasVbriefDeprecationMarker, isEffectivelyEmptyDir } from "./fs-helpers.js";
4
5
  function isDirectory(path) {
5
6
  try {
6
7
  return statSync(path).isDirectory();
@@ -78,4 +79,36 @@ export function detectLegacyVbriefLayout(projectRoot) {
78
79
  const reasonList = [...reasons];
79
80
  return { legacyLayout: reasonList.length > 0, reasons: reasonList };
80
81
  }
82
+ /**
83
+ * Classify the convergence state of the `vbrief/` and `xbrief/` lifecycle roots
84
+ * so the orchestrator can converge to a single unambiguous root (#2270). Unlike
85
+ * {@link detectLegacyVbriefLayout} (which only asks "is there anything legacy?"),
86
+ * this probe distinguishes the ambiguous dual-empty-root case from an already
87
+ * unambiguous layout, and never falls back to a `run migrate:xbrief` dead end
88
+ * for a `vbrief/` that is merely an empty leftover.
89
+ */
90
+ export function detectXbriefConvergence(projectRoot) {
91
+ const legacyDir = join(projectRoot, LEGACY_ARTIFACT_DIR);
92
+ const migratedDir = join(projectRoot, MIGRATED_ARTIFACT_DIR);
93
+ const vbriefPresent = isDirectory(legacyDir);
94
+ const vbriefHasMarker = vbriefPresent && hasVbriefDeprecationMarker(legacyDir);
95
+ const vbriefEmpty = vbriefPresent && !vbriefHasMarker && isEffectivelyEmptyDir(legacyDir);
96
+ const xbriefPresent = isDirectory(migratedDir);
97
+ const xbriefHasContent = xbriefPresent && !isEffectivelyEmptyDir(migratedDir);
98
+ let state;
99
+ if (vbriefHasMarker) {
100
+ state = "xbrief-marker";
101
+ }
102
+ else if (!vbriefPresent) {
103
+ state = xbriefHasContent ? "xbrief-only" : "none";
104
+ }
105
+ else if (vbriefEmpty) {
106
+ state = "empty-vbrief";
107
+ }
108
+ else {
109
+ // vbrief/ holds real content and carries no marker.
110
+ state = xbriefHasContent ? "dual-populated" : "legacy-only";
111
+ }
112
+ return { state, vbriefPresent, vbriefEmpty, vbriefHasMarker, xbriefPresent, xbriefHasContent };
113
+ }
81
114
  //# sourceMappingURL=detect.js.map
@@ -1,3 +1,16 @@
1
1
  /** True when `path` exists and is a directory; false on any stat error. */
2
2
  export declare function isDirectory(path: string): boolean;
3
+ /**
4
+ * True when `dir` holds no meaningful content at any depth — a "fully migrated"
5
+ * lifecycle root. Empty subdirectories and VCS placeholder files (`.gitkeep`,
6
+ * `.keep`) are ignored, so a scaffolded-but-empty `vbrief/proposed/` tree still
7
+ * counts as empty. A missing directory is trivially empty (#2270).
8
+ */
9
+ export declare function isEffectivelyEmptyDir(dir: string): boolean;
10
+ /**
11
+ * True when `dir` carries a deft-written deprecation marker identifying it as a
12
+ * retained-for-read-compat legacy root rather than an active source of truth
13
+ * (#2270). Matched on the sentinel so an unrelated `DEPRECATED.md` is ignored.
14
+ */
15
+ export declare function hasVbriefDeprecationMarker(dir: string): boolean;
3
16
  //# sourceMappingURL=fs-helpers.d.ts.map
@@ -1,4 +1,6 @@
1
- import { statSync } from "node:fs";
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
2
4
  /** True when `path` exists and is a directory; false on any stat error. */
3
5
  export function isDirectory(path) {
4
6
  try {
@@ -8,4 +10,47 @@ export function isDirectory(path) {
8
10
  return false;
9
11
  }
10
12
  }
13
+ /** Placeholder files that do not count as real content when judging emptiness. */
14
+ const PLACEHOLDER_FILES = new Set([".gitkeep", ".keep"]);
15
+ /**
16
+ * True when `dir` holds no meaningful content at any depth — a "fully migrated"
17
+ * lifecycle root. Empty subdirectories and VCS placeholder files (`.gitkeep`,
18
+ * `.keep`) are ignored, so a scaffolded-but-empty `vbrief/proposed/` tree still
19
+ * counts as empty. A missing directory is trivially empty (#2270).
20
+ */
21
+ export function isEffectivelyEmptyDir(dir) {
22
+ if (!isDirectory(dir)) {
23
+ return true;
24
+ }
25
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
26
+ if (entry.isFile()) {
27
+ if (PLACEHOLDER_FILES.has(entry.name)) {
28
+ continue;
29
+ }
30
+ return false;
31
+ }
32
+ // A symlink (to a file or dir) is real content: `isFile`/`isDirectory` both
33
+ // report false for symlink dirents, so treat it explicitly as non-empty.
34
+ if (entry.isSymbolicLink()) {
35
+ return false;
36
+ }
37
+ if (entry.isDirectory() && !isEffectivelyEmptyDir(join(dir, entry.name))) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ /**
44
+ * True when `dir` carries a deft-written deprecation marker identifying it as a
45
+ * retained-for-read-compat legacy root rather than an active source of truth
46
+ * (#2270). Matched on the sentinel so an unrelated `DEPRECATED.md` is ignored.
47
+ */
48
+ export function hasVbriefDeprecationMarker(dir) {
49
+ try {
50
+ return readFileSync(join(dir, VBRIEF_DEPRECATION_MARKER_FILENAME), "utf8").includes(VBRIEF_DEPRECATION_MARKER_SENTINEL);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
11
56
  //# sourceMappingURL=fs-helpers.js.map
@@ -1,7 +1,9 @@
1
1
  export { detectStaleUnmanagedHeader, type HeaderPatchOutcome, type HeaderRewriteResult, type HeaderTokenReplacement, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, type StaleHeaderDetection, } from "./agents-header.js";
2
- export { detectLegacyVbriefLayout, type LegacyVbriefLayoutDetection, } from "./detect.js";
2
+ export { VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
3
+ export { detectLegacyVbriefLayout, detectXbriefConvergence, type LegacyVbriefLayoutDetection, type XbriefConvergenceDetection, type XbriefConvergenceState, } from "./detect.js";
3
4
  export { BUILTIN_ALLOW_LIST, type DriftEvaluateOptions, type DriftEvaluateResult, type DriftFinding, type DriftScanMode, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
4
- export { emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, type XbriefMigrationArgs, type XbriefMigrationIo, type XbriefMigrationOutcome, } from "./migrate-project.js";
5
+ export { hasVbriefDeprecationMarker, isDirectory, isEffectivelyEmptyDir, } from "./fs-helpers.js";
6
+ export { convergeLegacyVbriefRoot, emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, type VbriefConvergeAction, type XbriefMigrationArgs, type XbriefMigrationIo, type XbriefMigrationOutcome, } from "./migrate-project.js";
5
7
  export { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
6
8
  export { assertFeatureEmissionAllowed, assertLayoutAwareWritePath, FeatureEmissionRejectedError, type JsonObject, type JsonValue, readDeclaredArtifactVersion, resolveLayoutAwareRelativePath, rewriteEmbeddedTokens, SplitLayoutRejectedError, TransformError, transformArtifactV06ToV08, transformArtifactV06ToV08Transactional, } from "./transforms.js";
7
9
  export { isPatchOnlyUpgrade, parseSemverPrefix } from "./version.js";
@@ -1,7 +1,9 @@
1
1
  export { detectStaleUnmanagedHeader, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, } from "./agents-header.js";
2
- export { detectLegacyVbriefLayout, } from "./detect.js";
2
+ export { VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
3
+ export { detectLegacyVbriefLayout, detectXbriefConvergence, } from "./detect.js";
3
4
  export { BUILTIN_ALLOW_LIST, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
4
- export { emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, } from "./migrate-project.js";
5
+ export { hasVbriefDeprecationMarker, isDirectory, isEffectivelyEmptyDir, } from "./fs-helpers.js";
6
+ export { convergeLegacyVbriefRoot, emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, } from "./migrate-project.js";
5
7
  export { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
6
8
  export { assertFeatureEmissionAllowed, assertLayoutAwareWritePath, FeatureEmissionRejectedError, readDeclaredArtifactVersion, resolveLayoutAwareRelativePath, rewriteEmbeddedTokens, SplitLayoutRejectedError, TransformError, transformArtifactV06ToV08, transformArtifactV06ToV08Transactional, } from "./transforms.js";
7
9
  export { isPatchOnlyUpgrade, parseSemverPrefix } from "./version.js";
@@ -2,11 +2,18 @@ export interface XbriefMigrationArgs {
2
2
  readonly projectRoot: string;
3
3
  readonly frameworkRoot?: string;
4
4
  readonly force?: boolean;
5
+ /**
6
+ * Retain a fully-migrated `vbrief/` for read-compatibility behind an explicit
7
+ * deprecation marker instead of removing it (#2270). Default: remove.
8
+ */
9
+ readonly keepLegacy?: boolean;
5
10
  }
6
11
  export interface XbriefMigrationIo {
7
12
  writeOut: (text: string) => void;
8
13
  writeErr: (text: string) => void;
9
14
  }
15
+ /** How the legacy `vbrief/` root was converged to an unambiguous state (#2270). */
16
+ export type VbriefConvergeAction = "removed" | "marker";
10
17
  export type XbriefMigrationOutcome = {
11
18
  readonly kind: "noop";
12
19
  readonly message: string;
@@ -17,11 +24,26 @@ export type XbriefMigrationOutcome = {
17
24
  readonly kind: "migrated";
18
25
  readonly backupDir: string;
19
26
  readonly files: number;
27
+ } | {
28
+ readonly kind: "converged";
29
+ readonly action: VbriefConvergeAction;
30
+ readonly already: boolean;
31
+ readonly message: string;
20
32
  } | {
21
33
  readonly kind: "config";
22
34
  readonly message: string;
23
35
  };
24
- /** Core orchestrator for the consumer xbrief rename (#2110). */
36
+ /**
37
+ * Converge a leftover legacy `vbrief/` root to an unambiguous state (#2270).
38
+ * An effectively-empty tree is removed by default (single canonical root); a
39
+ * tree with real content — or an empty tree when `retain` is requested — is
40
+ * kept for read-compat behind an explicit deprecation marker. Idempotent: a
41
+ * missing dir or an already-marked dir is a no-op.
42
+ */
43
+ export declare function convergeLegacyVbriefRoot(projectRoot: string, options: {
44
+ retain: boolean;
45
+ }): VbriefConvergeAction;
46
+ /** Core orchestrator for the consumer xbrief rename (#2110) + convergence (#2270). */
25
47
  export declare function runXbriefMigration(args: XbriefMigrationArgs, _io: XbriefMigrationIo): XbriefMigrationOutcome;
26
48
  /** Map a migration outcome to a process exit code and emit operator guidance. */
27
49
  export declare function emitXbriefMigration(outcome: XbriefMigrationOutcome, io: XbriefMigrationIo, options?: {
@@ -3,9 +3,9 @@ import { dirname, join, relative, resolve } from "node:path";
3
3
  import { checkGitClean } from "../migrate-preflight/index.js";
4
4
  import { agentsRefreshPlan } from "../platform/agents-md.js";
5
5
  import { patchAgentsMdHeader, renderHeaderPatchSummary } from "./agents-header.js";
6
- import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, } from "./constants.js";
7
- import { detectLegacyVbriefLayout } from "./detect.js";
8
- import { isDirectory } from "./fs-helpers.js";
6
+ import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, } from "./constants.js";
7
+ import { detectLegacyVbriefLayout, detectXbriefConvergence } from "./detect.js";
8
+ import { hasVbriefDeprecationMarker, isDirectory, isEffectivelyEmptyDir } from "./fs-helpers.js";
9
9
  import { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
10
10
  import { rewriteEmbeddedTokens, transformArtifactV06ToV08Transactional } from "./transforms.js";
11
11
  function collectFiles(root, acc = []) {
@@ -52,7 +52,7 @@ function backupLegacyTree(projectRoot, legacyDir) {
52
52
  cpSync(legacyDir, join(backupRoot, LEGACY_ARTIFACT_DIR), { recursive: true });
53
53
  return backupRoot;
54
54
  }
55
- function migrateLegacyTree(projectRoot, legacyDir) {
55
+ function migrateLegacyTree(projectRoot, legacyDir, options) {
56
56
  const migratedDir = join(projectRoot, MIGRATED_ARTIFACT_DIR);
57
57
  if (existsSync(migratedDir)) {
58
58
  throw new Error(`refusing to migrate: '${MIGRATED_ARTIFACT_DIR}/' already exists alongside '${LEGACY_ARTIFACT_DIR}/'`);
@@ -71,7 +71,15 @@ function migrateLegacyTree(projectRoot, legacyDir) {
71
71
  writeMigratedFile(srcPath, destPath);
72
72
  }
73
73
  renameOrReplace(stagedDir, migratedDir);
74
- rmSync(legacyDir, { recursive: true, force: true });
74
+ // Converge to a single unambiguous root: the fully-migrated legacy tree is
75
+ // either removed (default) or retained for read-compat behind an explicit
76
+ // deprecation marker so it never looks like an active source of truth (#2270).
77
+ if (options.keepLegacy) {
78
+ writeVbriefDeprecationMarker(legacyDir);
79
+ }
80
+ else {
81
+ rmSync(legacyDir, { recursive: true, force: true });
82
+ }
75
83
  return { backupDir, files: files.length };
76
84
  }
77
85
  catch (err) {
@@ -79,6 +87,36 @@ function migrateLegacyTree(projectRoot, legacyDir) {
79
87
  throw err;
80
88
  }
81
89
  }
90
+ /** Idempotently write the legacy-root deprecation marker (#2270). */
91
+ function writeVbriefDeprecationMarker(legacyDir) {
92
+ mkdirSync(legacyDir, { recursive: true });
93
+ if (hasVbriefDeprecationMarker(legacyDir)) {
94
+ return;
95
+ }
96
+ writeFileSync(join(legacyDir, VBRIEF_DEPRECATION_MARKER_FILENAME), VBRIEF_DEPRECATION_MARKER_BODY, "utf8");
97
+ }
98
+ /**
99
+ * Converge a leftover legacy `vbrief/` root to an unambiguous state (#2270).
100
+ * An effectively-empty tree is removed by default (single canonical root); a
101
+ * tree with real content — or an empty tree when `retain` is requested — is
102
+ * kept for read-compat behind an explicit deprecation marker. Idempotent: a
103
+ * missing dir or an already-marked dir is a no-op.
104
+ */
105
+ export function convergeLegacyVbriefRoot(projectRoot, options) {
106
+ const legacyDir = join(projectRoot, LEGACY_ARTIFACT_DIR);
107
+ if (!isDirectory(legacyDir)) {
108
+ return "removed";
109
+ }
110
+ if (hasVbriefDeprecationMarker(legacyDir)) {
111
+ return "marker";
112
+ }
113
+ if (isEffectivelyEmptyDir(legacyDir) && !options.retain) {
114
+ rmSync(legacyDir, { recursive: true, force: true });
115
+ return "removed";
116
+ }
117
+ writeVbriefDeprecationMarker(legacyDir);
118
+ return "marker";
119
+ }
82
120
  function renameOrReplace(src, dest) {
83
121
  if (existsSync(dest)) {
84
122
  rmSync(dest, { recursive: true, force: true });
@@ -107,9 +145,58 @@ function runAgentsRefresh(projectRoot, frameworkRoot, io) {
107
145
  io.writeOut(`AGENTS.md updated (state=${state}).\n`);
108
146
  return 0;
109
147
  }
110
- /** Core orchestrator for the consumer xbrief rename (#2110). */
148
+ /** Core orchestrator for the consumer xbrief rename (#2110) + convergence (#2270). */
111
149
  export function runXbriefMigration(args, _io) {
112
150
  const projectRoot = resolve(args.projectRoot);
151
+ const legacyDir = join(projectRoot, LEGACY_ARTIFACT_DIR);
152
+ const keepLegacy = args.keepLegacy ?? false;
153
+ const convergence = detectXbriefConvergence(projectRoot);
154
+ switch (convergence.state) {
155
+ // Already converged: legacy root is retained behind an explicit marker.
156
+ // Rerun is a pure no-op (no re-removal, no duplicate marker) — idempotent.
157
+ case "xbrief-marker":
158
+ return {
159
+ kind: "converged",
160
+ action: "marker",
161
+ already: true,
162
+ message: `Legacy '${LEGACY_ARTIFACT_DIR}/' already carries a deprecation marker — layout already converged.`,
163
+ };
164
+ // Ambiguous dual-empty root: a fully-migrated empty legacy tree. Converge to
165
+ // a single canonical root by removing it (or marking it when read-compat
166
+ // retention is requested); never leave two indistinguishable empty roots.
167
+ // Read-compat retention is only honored when a canonical xbrief/ actually
168
+ // has content — marking an empty legacy root with no canonical replacement
169
+ // would strand the project behind a marker that claims a migration that
170
+ // never happened, so that case always removes the stray empty root.
171
+ case "empty-vbrief": {
172
+ const retain = keepLegacy && convergence.xbriefHasContent;
173
+ const action = convergeLegacyVbriefRoot(projectRoot, { retain });
174
+ let message;
175
+ if (action === "removed") {
176
+ message = convergence.xbriefHasContent
177
+ ? `Converged layout: removed empty legacy '${LEGACY_ARTIFACT_DIR}/' — single '${MIGRATED_ARTIFACT_DIR}/' root.`
178
+ : `Converged layout: removed empty legacy '${LEGACY_ARTIFACT_DIR}/' — no canonical '${MIGRATED_ARTIFACT_DIR}/' root to migrate to yet.`;
179
+ }
180
+ else {
181
+ message = `Converged layout: wrote deprecation marker to legacy '${LEGACY_ARTIFACT_DIR}/' (retained for read-compat).`;
182
+ }
183
+ return { kind: "converged", action, already: false, message };
184
+ }
185
+ // Legacy content coexisting with a populated canonical xbrief/. We never
186
+ // destructively merge; converge non-destructively by marking the legacy
187
+ // tree deprecated so it no longer looks like an active source of truth.
188
+ case "dual-populated": {
189
+ convergeLegacyVbriefRoot(projectRoot, { retain: true });
190
+ return {
191
+ kind: "converged",
192
+ action: "marker",
193
+ already: false,
194
+ message: `Converged layout: '${MIGRATED_ARTIFACT_DIR}/' is canonical; wrote deprecation marker to legacy '${LEGACY_ARTIFACT_DIR}/' (retained for read-compat).`,
195
+ };
196
+ }
197
+ default:
198
+ break;
199
+ }
113
200
  const detection = detectLegacyVbriefLayout(projectRoot);
114
201
  if (!detection.legacyLayout) {
115
202
  return {
@@ -117,7 +204,6 @@ export function runXbriefMigration(args, _io) {
117
204
  message: "Project is already on the xbrief layout — nothing to migrate.",
118
205
  };
119
206
  }
120
- const legacyDir = join(projectRoot, LEGACY_ARTIFACT_DIR);
121
207
  if (!isDirectory(legacyDir)) {
122
208
  return {
123
209
  kind: "config",
@@ -134,7 +220,7 @@ export function runXbriefMigration(args, _io) {
134
220
  }
135
221
  }
136
222
  try {
137
- const { backupDir, files } = migrateLegacyTree(projectRoot, legacyDir);
223
+ const { backupDir, files } = migrateLegacyTree(projectRoot, legacyDir, { keepLegacy });
138
224
  return { kind: "migrated", backupDir, files };
139
225
  }
140
226
  catch (err) {
@@ -165,6 +251,9 @@ export function emitXbriefMigration(outcome, io, options = {}) {
165
251
  io.writeOut(`Migrated ${outcome.files} file(s) from ${LEGACY_ARTIFACT_DIR}/ to ${MIGRATED_ARTIFACT_DIR}/.\n` +
166
252
  `Backup written to ${outcome.backupDir}.\n`);
167
253
  return 0;
254
+ case "converged":
255
+ io.writeOut(`${outcome.message}\n`);
256
+ return 0;
168
257
  default: {
169
258
  const _exhaustive = outcome;
170
259
  return _exhaustive;
@@ -1,5 +1,11 @@
1
1
  /** Operator guidance for the TS-native xbrief rename (#2034 / #2110). */
2
2
  export declare function xbriefMigrationGuidance(): string;
3
- /** One-line doctor / ritual signpost mirroring `renderPrecutoverLine` (#2110). */
3
+ /**
4
+ * One-line doctor / ritual signpost mirroring `renderPrecutoverLine` (#2110),
5
+ * reporting an unambiguous convergence state (#2270). A migrated project reads
6
+ * as `xbrief active` plus either `vbrief legacy marker` or `vbrief removed`; a
7
+ * stray empty legacy root is called out as a pending convergence rather than a
8
+ * generic "run migrate" dead end — never a dual-empty-root ambiguity.
9
+ */
4
10
  export declare function renderXbriefMigrationLine(projectRoot: string): string;
5
11
  //# sourceMappingURL=signpost.d.ts.map
@@ -1,13 +1,28 @@
1
- import { detectLegacyVbriefLayout } from "./detect.js";
1
+ import { detectLegacyVbriefLayout, detectXbriefConvergence } from "./detect.js";
2
2
  /** Operator guidance for the TS-native xbrief rename (#2034 / #2110). */
3
3
  export function xbriefMigrationGuidance() {
4
4
  return "Run `deft migrate:xbrief` (or `task migrate:xbrief`) to convert vbrief/ to xbrief/ safely.";
5
5
  }
6
- /** One-line doctor / ritual signpost mirroring `renderPrecutoverLine` (#2110). */
6
+ /**
7
+ * One-line doctor / ritual signpost mirroring `renderPrecutoverLine` (#2110),
8
+ * reporting an unambiguous convergence state (#2270). A migrated project reads
9
+ * as `xbrief active` plus either `vbrief legacy marker` or `vbrief removed`; a
10
+ * stray empty legacy root is called out as a pending convergence rather than a
11
+ * generic "run migrate" dead end — never a dual-empty-root ambiguity.
12
+ */
7
13
  export function renderXbriefMigrationLine(projectRoot) {
14
+ const convergence = detectXbriefConvergence(projectRoot);
15
+ // Converged: legacy vbrief/ retained for read-compat behind an explicit marker.
16
+ if (convergence.state === "xbrief-marker") {
17
+ return "xBrief migration: converged -- xbrief active, vbrief legacy marker (read-compat).";
18
+ }
19
+ // Ambiguous dual-empty root: canonical xbrief/ (or none) plus a stray empty vbrief/.
20
+ if (convergence.state === "empty-vbrief") {
21
+ return `xBrief migration: converge pending -- xbrief active, empty legacy vbrief/ present. ${xbriefMigrationGuidance()}`;
22
+ }
8
23
  const { legacyLayout, reasons } = detectLegacyVbriefLayout(projectRoot);
9
24
  if (!legacyLayout) {
10
- return "xBrief migration: none -- project is on the xbrief layout.";
25
+ return "xBrief migration: none -- xbrief active, vbrief removed.";
11
26
  }
12
27
  const maxReasons = 3;
13
28
  const shown = reasons.slice(0, maxReasons);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.68.1",
3
+ "version": "0.70.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -90,6 +90,10 @@
90
90
  "types": "./dist/release-e2e/index.d.ts",
91
91
  "default": "./dist/release-e2e/index.js"
92
92
  },
93
+ "./resolution": {
94
+ "types": "./dist/resolution/index.d.ts",
95
+ "default": "./dist/resolution/index.js"
96
+ },
93
97
  "./pr-merge-readiness": {
94
98
  "types": "./dist/pr-merge-readiness/index.d.ts",
95
99
  "default": "./dist/pr-merge-readiness/index.js"
@@ -245,8 +249,8 @@
245
249
  "provenance": true
246
250
  },
247
251
  "dependencies": {
248
- "@deftai/directive-content": "^0.68.1",
249
- "@deftai/directive-types": "^0.68.1",
252
+ "@deftai/directive-content": "^0.70.0",
253
+ "@deftai/directive-types": "^0.70.0",
250
254
  "archiver": "^8.0.0"
251
255
  },
252
256
  "scripts": {