@deftai/directive-core 0.72.0 → 0.73.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 (86) hide show
  1. package/dist/category-b-namespace/index.js +16 -2
  2. package/dist/codebase/default-extractor.js +9 -1
  3. package/dist/codebase/map.js +1 -1
  4. package/dist/doctor/checks.d.ts +9 -0
  5. package/dist/doctor/checks.js +85 -5
  6. package/dist/doctor/main.js +2 -1
  7. package/dist/eval/health.js +7 -1
  8. package/dist/eval/readback.js +7 -1
  9. package/dist/init-deposit/gitignore.js +6 -0
  10. package/dist/init-deposit/hygiene.d.ts +24 -0
  11. package/dist/init-deposit/hygiene.js +55 -0
  12. package/dist/init-deposit/refresh.js +22 -6
  13. package/dist/init-deposit/scaffold.d.ts +11 -1
  14. package/dist/init-deposit/scaffold.js +4 -4
  15. package/dist/integration-e2e/helpers.js +7 -7
  16. package/dist/layout/index.d.ts +1 -1
  17. package/dist/layout/index.js +1 -1
  18. package/dist/layout/resolve.d.ts +47 -31
  19. package/dist/layout/resolve.js +93 -36
  20. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  21. package/dist/lifecycle/lifecycle-hygiene.js +1 -1
  22. package/dist/migrate-preflight/index.js +10 -10
  23. package/dist/policy/resolve.d.ts +2 -2
  24. package/dist/policy/resolve.js +11 -3
  25. package/dist/policy/wip.d.ts +1 -1
  26. package/dist/policy/wip.js +15 -3
  27. package/dist/preflight-cache/evaluate.js +1 -1
  28. package/dist/release/native-steps.js +14 -2
  29. package/dist/render/export-spec.js +2 -2
  30. package/dist/render/project-render.js +40 -9
  31. package/dist/scope/decompose.js +12 -2
  32. package/dist/session/resume-conditions.js +2 -2
  33. package/dist/session/ritual-sentinel.d.ts +1 -1
  34. package/dist/session/ritual-sentinel.js +6 -4
  35. package/dist/slice/constants.d.ts +1 -1
  36. package/dist/slice/constants.js +1 -1
  37. package/dist/slice/project-context.js +1 -1
  38. package/dist/spec-authority/resolver.js +9 -2
  39. package/dist/swarm/complete-cohort.js +14 -1
  40. package/dist/swarm/readiness.js +33 -3
  41. package/dist/task-surface/index.js +3 -3
  42. package/dist/triage/actions/candidates-log.d.ts +1 -1
  43. package/dist/triage/bootstrap/gitignore.js +22 -10
  44. package/dist/triage/bootstrap/index.d.ts +1 -1
  45. package/dist/triage/bootstrap/index.js +9 -3
  46. package/dist/triage/cache-path.d.ts +1 -1
  47. package/dist/triage/cache-path.js +32 -6
  48. package/dist/triage/classify/index.d.ts +1 -1
  49. package/dist/triage/classify/index.js +11 -4
  50. package/dist/triage/help/registry-data.d.ts +2 -2
  51. package/dist/triage/help/registry-data.js +2 -2
  52. package/dist/triage/queue/constants.d.ts +4 -4
  53. package/dist/triage/queue/constants.js +4 -4
  54. package/dist/triage/queue/scope-walk.js +7 -1
  55. package/dist/triage/reconcile/reconcile.js +23 -3
  56. package/dist/triage/reconcile/types.d.ts +1 -1
  57. package/dist/triage/reconcile/types.js +1 -1
  58. package/dist/triage/refresh/refresh.js +9 -2
  59. package/dist/triage/scope/constants.d.ts +2 -2
  60. package/dist/triage/scope/constants.js +2 -2
  61. package/dist/triage/scope/renderers.js +7 -1
  62. package/dist/triage/smoketest/index.js +9 -6
  63. package/dist/triage/subscribe/index.d.ts +2 -2
  64. package/dist/triage/subscribe/index.js +2 -2
  65. package/dist/triage/summary/index.d.ts +2 -2
  66. package/dist/triage/summary/index.js +1 -1
  67. package/dist/triage/summary/reconcilable.d.ts +1 -1
  68. package/dist/triage/welcome/constants.d.ts +2 -2
  69. package/dist/triage/welcome/constants.js +2 -2
  70. package/dist/triage/welcome/onboard.js +8 -1
  71. package/dist/triage/welcome/prior-state.js +14 -2
  72. package/dist/triage/welcome/summary.js +14 -2
  73. package/dist/triage/welcome/writers.js +7 -1
  74. package/dist/validate-content/validate-strategy-output.js +11 -1
  75. package/dist/vbrief-build/parity-scenarios.js +3 -3
  76. package/dist/vbrief-reconcile/labels.js +16 -1
  77. package/dist/vbrief-reconcile/parity-scenarios.js +10 -10
  78. package/dist/vbrief-reconcile/umbrellas.js +16 -1
  79. package/dist/vbrief-validate/main.js +13 -1
  80. package/dist/vbrief-validate/precutover.js +33 -5
  81. package/dist/vbrief-validate/staleness.d.ts +1 -1
  82. package/dist/vbrief-validate/staleness.js +5 -2
  83. package/dist/verify-source/code-structure-validate.js +7 -1
  84. package/dist/xbrief-migrate/signpost.d.ts +4 -4
  85. package/dist/xbrief-migrate/signpost.js +11 -15
  86. package/package.json +3 -3
@@ -9,7 +9,7 @@
9
9
  * order-preserving (the namespaced key takes the legacy key's slot, keeping
10
10
  * artifact diffs minimal).
11
11
  */
12
- import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
12
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
13
13
  import { join, relative, resolve } from "node:path";
14
14
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
15
15
  import { LEGACY_PLAN_COMPLETED_NOTE_KEY, LEGACY_PLAN_POLICY_KEY, PLAN_COMPLETED_NOTE_KEY, PLAN_POLICY_KEY, } from "../policy/plan-extensions.js";
@@ -85,7 +85,21 @@ function collectVbriefFiles(dir, acc = []) {
85
85
  */
86
86
  export function migrateCategoryBCorpus(projectRoot) {
87
87
  const root = resolve(projectRoot);
88
- const vbriefDir = resolveLifecycleRoot(root);
88
+ let vbriefDir;
89
+ try {
90
+ vbriefDir = resolveLifecycleRoot(root);
91
+ }
92
+ catch {
93
+ // If xbrief/ layout is absent, fall back to vbrief/ for this pre-migration tool
94
+ // (category-b migration rewrites policy keys regardless of layout stage).
95
+ const legacyDir = join(root, "vbrief");
96
+ if (existsSync(legacyDir)) {
97
+ vbriefDir = legacyDir;
98
+ }
99
+ else {
100
+ return { scanned: 0, changed: [], conflicts: [] };
101
+ }
102
+ }
89
103
  let scanned = 0;
90
104
  const changed = [];
91
105
  const conflicts = [];
@@ -101,7 +101,15 @@ function sourcePathRel(projectRoot, sourcePath) {
101
101
  }
102
102
  /** Return the authored codeStructure source path used by the default extractor. */
103
103
  export function defaultCodeStructurePath(projectRoot, codeStructurePath) {
104
- return codeStructurePath ?? resolveProjectDefinitionPath(projectRoot);
104
+ if (codeStructurePath !== undefined)
105
+ return codeStructurePath;
106
+ try {
107
+ return resolveProjectDefinitionPath(projectRoot);
108
+ }
109
+ catch {
110
+ // No xbrief/ layout; return canonical path so callers get a predictable "not found" result.
111
+ return join(projectRoot, "xbrief", "PROJECT-DEFINITION.xbrief.json");
112
+ }
105
113
  }
106
114
  function stableId(value) {
107
115
  let slug = "";
@@ -10,7 +10,7 @@ export const GENERATED_SENTINEL = "AUTO-GENERATED by task codebase:map";
10
10
  export const DEFAULT_OUTPUT_PATH = ".planning/codebase/MAP.md";
11
11
  export const BANNER_TEMPLATE = "<!-- AUTO-GENERATED by task codebase:map -- DO NOT EDIT MANUALLY -->\n" +
12
12
  "<!-- Purpose: generated codebase MAP projection -->\n" +
13
- "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json " +
13
+ "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json " +
14
14
  "plan.architecture.codeStructure -->\n" +
15
15
  "<!-- Regenerate with: task codebase:map -->\n";
16
16
  export function markdownText(value) {
@@ -30,6 +30,15 @@ export declare function checkCanonicalVendoredNpmSignpost(projectRoot: string, s
30
30
  * deposit is still a working install, just an unpinned one.
31
31
  */
32
32
  export declare function checkManifestVersionReportable(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
33
+ /**
34
+ * #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
35
+ * entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
36
+ * a broken install.
37
+ *
38
+ * Skips when `.gitignore` is absent (greenfield project that has not yet run
39
+ * `directive init` or `directive update`).
40
+ */
41
+ export declare function checkGitignoreCoverage(projectRoot: string, seams?: CheckSeams): CheckResult;
33
42
  export declare function deriveExitCode(checks: readonly CheckResult[], errors: readonly string[]): number;
34
43
  export declare function runChecksImpl(projectRoot: string, seams?: CheckSeams & {
35
44
  isDir?: (p: string) => boolean;
@@ -1,8 +1,10 @@
1
1
  import { join } from "node:path";
2
+ import { CANONICAL_GITIGNORE_BASELINE } from "../init-deposit/gitignore.js";
2
3
  import { detectLegacyLayout, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
3
4
  import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY, NPM_MANAGED_SENTINEL_VALUE, } from "../init-deposit/migrate.js";
4
5
  import { resolveLifecycleRoot } from "../layout/resolve.js";
5
6
  import { findSkillPathsInText } from "../text/redos-safe.js";
7
+ import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
6
8
  import { CANONICAL_UPGRADE_COMMAND, GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL, } from "./constants.js";
7
9
  import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestReportableVersion, manifestTagToVersion, parseInstallManifest, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
8
10
  import { readTextSafe } from "./paths.js";
@@ -127,10 +129,14 @@ export function checkManifestAgreement(projectRoot, installRoot, seams = {}) {
127
129
  const isFile = seams.isFile ?? ((p) => readText(p, seams) !== null);
128
130
  const manifestPath = locateManifest(projectRoot, installRoot, isFile);
129
131
  const expectedManifestPath = manifestPath ?? manifestCandidatePaths(projectRoot, installRoot)[0];
130
- const bareCandidates = [
131
- join(resolveLifecycleRoot(projectRoot), ".deft-version"),
132
- join(projectRoot, ".deft-version"),
133
- ];
132
+ let layoutRoot;
133
+ try {
134
+ layoutRoot = resolveLifecycleRoot(projectRoot);
135
+ }
136
+ catch {
137
+ layoutRoot = projectRoot; // No xbrief/ layout; fall back to project root for bare version check.
138
+ }
139
+ const bareCandidates = [join(layoutRoot, ".deft-version"), join(projectRoot, ".deft-version")];
134
140
  const barePath = bareCandidates.find((p) => isFile(p)) ?? null;
135
141
  const manifestText = manifestPath ? readText(manifestPath, seams) : null;
136
142
  const bareText = barePath ? readText(barePath, seams) : null;
@@ -436,8 +442,80 @@ export function checkManifestVersionReportable(projectRoot, installRoot, seams =
436
442
  data: { manifest_path: manifestPath, version: null, sha: null, source: reportable.source },
437
443
  };
438
444
  }
445
+ /**
446
+ * Collect the normalised (inline-comment-stripped) gitignore patterns from a
447
+ * `.gitignore` file text. Uses the shared `stripGitignoreInlineComment` helper
448
+ * so coverage checks stay consistent with `ensureInitGitignoreLines`.
449
+ */
450
+ function collectGitignorePresent(text) {
451
+ const present = new Set();
452
+ for (const raw of text.split("\n")) {
453
+ const line = stripGitignoreInlineComment(raw);
454
+ if (line)
455
+ present.add(line);
456
+ }
457
+ return present;
458
+ }
459
+ function gitignoreLineIsCovered(present, line) {
460
+ return present.has(line);
461
+ }
462
+ /**
463
+ * #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
464
+ * entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
465
+ * a broken install.
466
+ *
467
+ * Skips when `.gitignore` is absent (greenfield project that has not yet run
468
+ * `directive init` or `directive update`).
469
+ */
470
+ export function checkGitignoreCoverage(projectRoot, seams = {}) {
471
+ const gitignorePath = join(projectRoot, ".gitignore");
472
+ const text = readText(gitignorePath, seams);
473
+ if (text === null) {
474
+ return {
475
+ name: "gitignore-coverage",
476
+ status: "skip",
477
+ detail: ".gitignore not found; run `directive init` or `directive update` to create it.",
478
+ data: { gitignore_path: gitignorePath, missing: [] },
479
+ };
480
+ }
481
+ const present = collectGitignorePresent(text);
482
+ const missing = [];
483
+ for (const line of CANONICAL_GITIGNORE_BASELINE) {
484
+ if (!gitignoreLineIsCovered(present, line)) {
485
+ missing.push(line);
486
+ }
487
+ }
488
+ // Check .deft/core/ separately — only required for hybrid/greenfield installs.
489
+ // We conservatively omit it from the missing list here (tracked-deposit projects
490
+ // legitimately omit it). The update path handles this correctly per layout.
491
+ if (missing.length === 0) {
492
+ return {
493
+ name: "gitignore-coverage",
494
+ status: "pass",
495
+ detail: "All canonical Deft .gitignore entries are present.",
496
+ data: { gitignore_path: gitignorePath, missing: [] },
497
+ };
498
+ }
499
+ return {
500
+ name: "gitignore-coverage",
501
+ status: "fail",
502
+ detail: `Deft .gitignore coverage incomplete: ${missing.length} canonical entr${missing.length === 1 ? "y" : "ies"} ` +
503
+ `missing (${missing.slice(0, 3).join(", ")}${missing.length > 3 ? ", …" : ""}). ` +
504
+ "Run `directive update` to add the missing entries (idempotent, safe on re-run). " +
505
+ `Full missing list: ${JSON.stringify(missing)}.`,
506
+ data: {
507
+ gitignore_path: gitignorePath,
508
+ missing,
509
+ suggested_fix: "directive update",
510
+ },
511
+ };
512
+ }
439
513
  export function deriveExitCode(checks, errors) {
440
- const exitExempt = new Set(["canonical-vendored-npm-signpost", "manifest-version-reportable"]);
514
+ const exitExempt = new Set([
515
+ "canonical-vendored-npm-signpost",
516
+ "manifest-version-reportable",
517
+ "gitignore-coverage",
518
+ ]);
441
519
  if (errors.length > 0 || checks.some((c) => c.status === "error")) {
442
520
  return 2;
443
521
  }
@@ -476,6 +554,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
476
554
  checks.push(checkManifestVersionReportable(projectRoot, null, seams));
477
555
  checks.push(checkLegacyLayout(projectRoot, seams));
478
556
  checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
557
+ checks.push(checkGitignoreCoverage(projectRoot, seams));
479
558
  return {
480
559
  projectRoot,
481
560
  installRoot: null,
@@ -491,6 +570,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
491
570
  checks.push(checkInstallPathConsistency(projectRoot, installRoot, seams));
492
571
  checks.push(checkLegacyLayout(projectRoot, seams));
493
572
  checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
573
+ checks.push(checkGitignoreCoverage(projectRoot, seams));
494
574
  return {
495
575
  projectRoot,
496
576
  installRoot,
@@ -373,7 +373,8 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
373
373
  }
374
374
  if ((name === "legacy-layout" ||
375
375
  name === "canonical-vendored-npm-signpost" ||
376
- name === "manifest-version-reportable") &&
376
+ name === "manifest-version-reportable" ||
377
+ name === "gitignore-coverage") &&
377
378
  status === "fail") {
378
379
  sink.warn(`${name}: ${detail}`);
379
380
  addFinding({
@@ -21,7 +21,13 @@ function isFrameworkSourceCheckout(projectRoot) {
21
21
  return existsSync(manifest) && existsSync(cliPkg);
22
22
  }
23
23
  function loadPlan(projectRoot) {
24
- const filepath = resolveProjectDefinitionPath(projectRoot);
24
+ let filepath;
25
+ try {
26
+ filepath = resolveProjectDefinitionPath(projectRoot);
27
+ }
28
+ catch {
29
+ return null; // No xbrief/ layout; no project definition.
30
+ }
25
31
  if (!existsSync(filepath)) {
26
32
  return null;
27
33
  }
@@ -28,7 +28,13 @@ function parseJsonObjectOrNull(text) {
28
28
  return null;
29
29
  }
30
30
  function readHealthHistoryRows(projectRoot) {
31
- const path = healthHistoryPath(projectRoot);
31
+ let path;
32
+ try {
33
+ path = healthHistoryPath(projectRoot);
34
+ }
35
+ catch {
36
+ return []; // No xbrief/ layout; no health history.
37
+ }
32
38
  if (!existsSync(path)) {
33
39
  return [];
34
40
  }
@@ -44,7 +44,13 @@ export const CANONICAL_GITIGNORE_BASELINE = [
44
44
  "vbrief/*.lock",
45
45
  ".deft/core.bak-*/",
46
46
  ".deft/*.bak-*",
47
+ // xBRIEF-era migration-backup directories created by `deft migrate:xbrief` (#2206).
48
+ ".deft/xbrief-migrate-backup-*/",
47
49
  "*.premigrate.*",
50
+ // Generated version-eval results live under `.eval/results/` (NOT triage-cache)
51
+ // for both the legacy `vbrief/` tree and the post-#2034 `xbrief/` tree (#2206).
52
+ "vbrief/.eval/results/",
53
+ "xbrief/.eval/results/",
48
54
  ];
49
55
  const DEFT_FRAMEWORK_GITIGNORE_HEADER = "# Deft framework: ignore local-only caches and scratch directories\n";
50
56
  const DEFT_CORE_GITIGNORE_RATIONALE = "# Hybrid deposit (#1942): reconstituted by `directive init` like node_modules.\n" +
@@ -37,6 +37,30 @@ export declare function stageFrameworkPaths(projectDir: string, paths: readonly
37
37
  staged: boolean;
38
38
  error: Error | null;
39
39
  };
40
+ /**
41
+ * Framework-internal subdirectories that are shipped as part of the source
42
+ * repository but are NOT included in the `@deftai/directive-content` npm
43
+ * package deposited into `.deft/core/`. If one of these paths survives from
44
+ * a prior git-vendored install, `deft update`'s additive file-swap will never
45
+ * remove it — causing the deposit-hygiene advisory to persist indefinitely.
46
+ *
47
+ * Each entry is a relative path within `.deft/core/`; presence in the CONTENT
48
+ * root overrides the prune (i.e. if the content package ever ships `packages/`
49
+ * again, we won't prune it).
50
+ *
51
+ * Refs #2347.
52
+ */
53
+ export declare const STRAY_DEPOSIT_FRAMEWORK_PATHS: readonly ["packages"];
54
+ export interface PruneStrayDepositPathsResult {
55
+ readonly pruned: string[];
56
+ }
57
+ /**
58
+ * Remove framework-source subdirectories from `.deft/core/` that are not
59
+ * present in the deposited content package (#2347). Silently skips any path
60
+ * that is also present in `contentRoot` (future-safe: if the content package
61
+ * ever ships `packages/` we stop pruning it).
62
+ */
63
+ export declare function pruneStrayDepositPaths(deftDir: string, contentRoot: string, io: InitDepositIo): Promise<PruneStrayDepositPathsResult>;
40
64
  export declare const COMMIT_HYGIENE_BRANCH_NAME = "chore/deft-framework-upgrade";
41
65
  export declare function printCommitGuidance(io: InitDepositIo, paths: readonly string[], staged: boolean): void;
42
66
  export interface DepositStagePathsOptions extends StageFrameworkPathsSeams, FrameworkStagePathsOptions {
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync } from "node:fs";
9
+ import { rm, stat } from "node:fs/promises";
9
10
  import { join, relative } from "node:path";
10
11
  import { gitPorcelain } from "../story-ready/git.js";
11
12
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
@@ -120,6 +121,60 @@ export function stageFrameworkPaths(projectDir, paths, seams = {}) {
120
121
  return { staged: false, error };
121
122
  }
122
123
  }
124
+ /**
125
+ * Framework-internal subdirectories that are shipped as part of the source
126
+ * repository but are NOT included in the `@deftai/directive-content` npm
127
+ * package deposited into `.deft/core/`. If one of these paths survives from
128
+ * a prior git-vendored install, `deft update`'s additive file-swap will never
129
+ * remove it — causing the deposit-hygiene advisory to persist indefinitely.
130
+ *
131
+ * Each entry is a relative path within `.deft/core/`; presence in the CONTENT
132
+ * root overrides the prune (i.e. if the content package ever ships `packages/`
133
+ * again, we won't prune it).
134
+ *
135
+ * Refs #2347.
136
+ */
137
+ export const STRAY_DEPOSIT_FRAMEWORK_PATHS = ["packages"];
138
+ /**
139
+ * Remove framework-source subdirectories from `.deft/core/` that are not
140
+ * present in the deposited content package (#2347). Silently skips any path
141
+ * that is also present in `contentRoot` (future-safe: if the content package
142
+ * ever ships `packages/` we stop pruning it).
143
+ */
144
+ export async function pruneStrayDepositPaths(deftDir, contentRoot, io) {
145
+ const pruned = [];
146
+ for (const rel of STRAY_DEPOSIT_FRAMEWORK_PATHS) {
147
+ const depositPath = join(deftDir, rel);
148
+ const contentPath = join(contentRoot, rel);
149
+ let isInDeposit = false;
150
+ let isInContent = false;
151
+ try {
152
+ isInDeposit = (await stat(depositPath)).isDirectory();
153
+ }
154
+ catch {
155
+ // absent — nothing to prune
156
+ }
157
+ if (!isInDeposit)
158
+ continue;
159
+ try {
160
+ isInContent = (await stat(contentPath)).isDirectory();
161
+ }
162
+ catch {
163
+ // not in content package — safe to prune
164
+ }
165
+ if (isInContent)
166
+ continue;
167
+ try {
168
+ await rm(depositPath, { recursive: true, force: true });
169
+ pruned.push(rel);
170
+ io.printf(`Pruned stray framework-source tree .deft/core/${rel}/ — not shipped by @deftai/directive-content (#2347).\n`);
171
+ }
172
+ catch (cause) {
173
+ io.printf(`Warning: could not prune .deft/core/${rel}/: ${String(cause)}\n`);
174
+ }
175
+ }
176
+ return { pruned };
177
+ }
123
178
  export const COMMIT_HYGIENE_BRANCH_NAME = "chore/deft-framework-upgrade";
124
179
  export function printCommitGuidance(io, paths, staged) {
125
180
  if (paths.length === 0)
@@ -20,8 +20,8 @@ import { resolveLifecycleRoot } from "../layout/resolve.js";
20
20
  import { DEV_FALLBACK } from "../platform/constants.js";
21
21
  import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
22
22
  import { gitPorcelain } from "../story-ready/git.js";
23
- import { ensureInitGitignoreLines } from "./gitignore.js";
24
- import { depositStagePaths, isInstallerManagedPath, printCommitGuidance } from "./hygiene.js";
23
+ import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
24
+ import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, pruneStrayDepositPaths, } from "./hygiene.js";
25
25
  import { parseInitArgv } from "./init-deposit.js";
26
26
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
27
27
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
@@ -155,14 +155,20 @@ function syncBareVersionMarker(projectDir, version) {
155
155
  const normalized = normalizeVersion(version);
156
156
  if (!normalized || normalized === DEV_FALLBACK)
157
157
  return;
158
- const vbriefDir = resolveLifecycleRoot(projectDir);
158
+ let vbriefDir = null;
159
+ try {
160
+ vbriefDir = resolveLifecycleRoot(projectDir);
161
+ }
162
+ catch {
163
+ // No xbrief/ layout present — write the root-level derivative instead.
164
+ }
159
165
  let targetDir = projectDir;
160
166
  try {
161
- if (statSync(vbriefDir).isDirectory())
167
+ if (vbriefDir !== null && statSync(vbriefDir).isDirectory())
162
168
  targetDir = vbriefDir;
163
169
  }
164
170
  catch {
165
- // vbrief/ absent — write the root-level derivative instead
171
+ // xbrief/ absent — write the root-level derivative instead
166
172
  }
167
173
  try {
168
174
  mkdirSync(targetDir, { recursive: true });
@@ -352,6 +358,10 @@ export async function runRefreshDeposit(args, io, seams = {}) {
352
358
  const versionSkewNotice = buildVersionSkewNotice(engineVersion, contentVersion, previousDepositVersion);
353
359
  await copyContent(contentRoot, deftDir);
354
360
  await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
361
+ // #2347: prune framework-source paths that the content package does not ship.
362
+ // The additive file-swap never removes them, causing the deposit-hygiene
363
+ // advisory to persist across every upgrade until manually cleaned.
364
+ await pruneStrayDepositPaths(deftDir, contentRoot, io);
355
365
  const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
356
366
  const manifestFields = {
357
367
  ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
@@ -372,7 +382,13 @@ export async function runRefreshDeposit(args, io, seams = {}) {
372
382
  // fails and the operator must hand-edit the marker).
373
383
  syncBareVersionMarker(projectDir, contentVersion);
374
384
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
375
- await depositNeutralization(projectDir, io);
385
+ // #2148: the deft-core-guard CI workflow is only meaningful when the deposit
386
+ // is git-tracked (committed vendor layout). On an npm-managed (gitignored)
387
+ // deposit it creates untracked noise after every `directive update`. Probe
388
+ // git-tracked status once and share the result with the gitignore upkeep below.
389
+ const depositTracked = isDepositTrackedInGit(projectDir, seams.gitLsFiles);
390
+ const skipGuardWorkflow = depositTracked !== true;
391
+ await depositNeutralization(projectDir, io, { skipGuardWorkflow });
376
392
  // #2266: non-destructive `.gitignore` upkeep for framework-owned paths. This
377
393
  // NEVER un-tracks a committed deposit -- `ensureInitGitignoreLines` only writes
378
394
  // `.gitignore` and leaves an already-tracked `.deft/core` tracked. The
@@ -67,6 +67,16 @@ export declare function ensureCodeqlPathsIgnore(projectDir: string, io: InitDepo
67
67
  export declare function ensureCoreGuardWorkflow(projectDir: string, io: InitDepositIo): boolean;
68
68
  export declare function pruneFrameworkSelfTests(projectDir: string, io: InitDepositIo): Promise<boolean>;
69
69
  export declare function pruneVendoredTsTests(projectDir: string, io: InitDepositIo): Promise<number>;
70
+ export interface DepositNeutralizationOptions {
71
+ /**
72
+ * #2148: skip depositing `.github/workflows/deft-core-guard.yml` when the
73
+ * framework deposit is not git-tracked (npm-managed / gitignored layout). The
74
+ * guard CI check is meaningless when there is no committed `.deft/core/**` to
75
+ * guard; skipping it prevents the file from re-appearing as untracked noise
76
+ * after every `directive update`.
77
+ */
78
+ readonly skipGuardWorkflow?: boolean;
79
+ }
70
80
  /** Best-effort #1430 neutralization deposit (mirrors depositNeutralization). */
71
- export declare function depositNeutralization(projectDir: string, io: InitDepositIo): Promise<void>;
81
+ export declare function depositNeutralization(projectDir: string, io: InitDepositIo, options?: DepositNeutralizationOptions): Promise<void>;
72
82
  //# sourceMappingURL=scaffold.d.ts.map
@@ -10,8 +10,8 @@ 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
12
  import { copyTree } from "../deposit/copy-tree.js";
13
- import { resolveLifecycleRoot } from "../layout/resolve.js";
14
13
  import { agentsRefreshPlan } from "../platform/agents-md.js";
14
+ import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
15
15
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
16
16
  import { installerManagedGuardEre } from "./hygiene.js";
17
17
  export { CANONICAL_INSTALL_ROOT };
@@ -322,7 +322,7 @@ function vbriefLifecycleDirsPresent(consumerVbrief) {
322
322
  });
323
323
  }
324
324
  export async function writeConsumerVbrief(projectDir, deftDir, io) {
325
- const consumerVbrief = resolveLifecycleRoot(projectDir);
325
+ const consumerVbrief = join(projectDir, MIGRATED_ARTIFACT_DIR);
326
326
  const schemasDst = join(consumerVbrief, "schemas");
327
327
  const vbriefMdDst = join(consumerVbrief, "vbrief.md");
328
328
  const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
@@ -774,12 +774,12 @@ export async function pruneVendoredTsTests(projectDir, io) {
774
774
  return removed;
775
775
  }
776
776
  /** Best-effort #1430 neutralization deposit (mirrors depositNeutralization). */
777
- export async function depositNeutralization(projectDir, io) {
777
+ export async function depositNeutralization(projectDir, io, options = {}) {
778
778
  const steps = [
779
779
  () => ensureGitattributes(projectDir, io),
780
780
  () => ensureGreptileIgnore(projectDir, io),
781
781
  () => ensureCodeqlPathsIgnore(projectDir, io),
782
- () => ensureCoreGuardWorkflow(projectDir, io),
782
+ ...(options.skipGuardWorkflow ? [] : [() => ensureCoreGuardWorkflow(projectDir, io)]),
783
783
  () => pruneFrameworkSelfTests(projectDir, io),
784
784
  async () => (await pruneVendoredTsTests(projectDir, io)) > 0,
785
785
  ];
@@ -77,12 +77,12 @@ export function populateCacheLayout(cacheRoot, repo, issueNumbers) {
77
77
  }
78
78
  export function createConsumerProject(parent) {
79
79
  const root = join(parent, "consumer");
80
- mkdirSync(join(root, "vbrief", "proposed"), { recursive: true });
81
- mkdirSync(join(root, "vbrief", "pending"), { recursive: true });
82
- mkdirSync(join(root, "vbrief", "active"), { recursive: true });
83
- mkdirSync(join(root, "vbrief", "completed"), { recursive: true });
84
- mkdirSync(join(root, "vbrief", "cancelled"), { recursive: true });
85
- writeFileSync(join(root, "vbrief", "specification.vbrief.json"), `${JSON.stringify({
80
+ mkdirSync(join(root, "xbrief", "proposed"), { recursive: true });
81
+ mkdirSync(join(root, "xbrief", "pending"), { recursive: true });
82
+ mkdirSync(join(root, "xbrief", "active"), { recursive: true });
83
+ mkdirSync(join(root, "xbrief", "completed"), { recursive: true });
84
+ mkdirSync(join(root, "xbrief", "cancelled"), { recursive: true });
85
+ writeFileSync(join(root, "xbrief", "specification.xbrief.json"), `${JSON.stringify({
86
86
  vBRIEFInfo: { version: "0.5" },
87
87
  plan: {
88
88
  title: "Consumer Project",
@@ -96,7 +96,7 @@ export function createConsumerProject(parent) {
96
96
  return root;
97
97
  }
98
98
  export function writeScopeVbrief(projectRoot, folder, filename, status = "proposed") {
99
- const target = join(projectRoot, "vbrief", folder, filename);
99
+ const target = join(projectRoot, "xbrief", folder, filename);
100
100
  writeFileSync(target, `${JSON.stringify({
101
101
  vBRIEFInfo: { version: "0.5" },
102
102
  plan: {
@@ -1,2 +1,2 @@
1
- export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LIFECYCLE_DIR_NAMES, type LifecycleLayout, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
1
+ export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LIFECYCLE_DIR_NAMES, type LifecycleLayout, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,2 @@
1
- export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LIFECYCLE_DIR_NAMES, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
1
+ export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LIFECYCLE_DIR_NAMES, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
2
2
  //# sourceMappingURL=index.js.map
@@ -1,28 +1,39 @@
1
- import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
2
- export { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, };
1
+ import { LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
2
+ export { LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, };
3
3
  /**
4
- * Lifecycle directory names recognized by the layout-aware resolver, in
5
- * preference order (migrated `xbrief` first, legacy `vbrief` second).
4
+ * Lifecycle directory names recognized by the layout-aware resolver and path
5
+ * validators. Includes both `xbrief` (canonical) and `vbrief` (legacy) so that
6
+ * path-validation helpers remain accurate during and after migration.
6
7
  */
7
8
  export declare const LIFECYCLE_DIR_NAMES: readonly ["xbrief", "vbrief"];
8
9
  /**
9
- * Artifact filename suffixes recognized by the layout-aware resolver, in
10
- * preference order (`.xbrief.json` first, `.vbrief.json` second).
10
+ * Artifact filename suffixes recognized by the layout-aware resolver and path
11
+ * validators. Includes both `.xbrief.json` (canonical) and `.vbrief.json`
12
+ * (legacy) so that path-validation helpers remain accurate during and after
13
+ * migration.
11
14
  */
12
15
  export declare const ARTIFACT_SUFFIXES: readonly [".xbrief.json", ".vbrief.json"];
13
- /** The on-disk lifecycle layout an engine call site should resolve against (#2109). */
16
+ /** The on-disk lifecycle layout an engine call site should resolve against (#2109 / #2112). */
14
17
  export interface LifecycleLayout {
15
- /** The lifecycle directory name -- `xbrief` when migrated, else `vbrief`. */
16
- readonly artifactDir: typeof MIGRATED_ARTIFACT_DIR | typeof LEGACY_ARTIFACT_DIR;
17
- /** The artifact filename suffix matching `artifactDir`. */
18
- readonly artifactSuffix: typeof MIGRATED_ARTIFACT_SUFFIX | typeof LEGACY_ARTIFACT_SUFFIX;
19
- /** The `*BRIEFInfo` root key matching `artifactDir`. */
20
- readonly infoRootKey: typeof MIGRATED_INFO_ROOT_KEY | typeof LEGACY_INFO_ROOT_KEY;
21
- /** Absolute path to the resolved lifecycle root (`<projectRoot>/<artifactDir>`). */
18
+ /** The lifecycle directory name -- always `xbrief` after the vbrief read path removal (#2112). */
19
+ readonly artifactDir: typeof MIGRATED_ARTIFACT_DIR;
20
+ /** The artifact filename suffix -- always `.xbrief.json` after #2112. */
21
+ readonly artifactSuffix: typeof MIGRATED_ARTIFACT_SUFFIX;
22
+ /** The `xBRIEFInfo` root key. */
23
+ readonly infoRootKey: typeof MIGRATED_INFO_ROOT_KEY;
24
+ /** Absolute path to the resolved lifecycle root (`<projectRoot>/xbrief`). */
22
25
  readonly root: string;
23
- /** True when the resolved layout is the migrated `xbrief` layout. */
24
- readonly migrated: boolean;
26
+ /** Always `true` -- the legacy vbrief read path was removed in #2112. */
27
+ readonly migrated: true;
25
28
  }
29
+ /**
30
+ * Returns the layout root for non-layout-sensitive helpers (#2112).
31
+ *
32
+ * - If a valid `xbrief/` layout exists, returns its root.
33
+ * - If only `vbrief/` exists (legacy-only), throws so the caller surfaces the migrate hint.
34
+ * - If neither exists (empty/new project), falls back to `<projectRoot>/xbrief/` canonical path.
35
+ */
36
+ export declare function resolveLayoutRootOrCanonical(projectRoot: string): string;
26
37
  /** True when `name` ends with any recognized artifact suffix (.xbrief.json or .vbrief.json). */
27
38
  export declare function hasArtifactSuffix(name: string): boolean;
28
39
  /**
@@ -31,24 +42,30 @@ export declare function hasArtifactSuffix(name: string): boolean;
31
42
  */
32
43
  export declare function stripArtifactSuffix(name: string): string;
33
44
  /**
34
- * True when a POSIX-style path is a lifecycle artifact under either layout
35
- * root (`xbrief/` or `vbrief/`) carrying either artifact suffix.
45
+ * True when a POSIX-style path is a lifecycle artifact path carrying a
46
+ * recognized lifecycle directory prefix (`xbrief/` or `vbrief/`) and a
47
+ * recognized artifact suffix. The `vbrief/` prefix and `.vbrief.json` suffix
48
+ * remain recognized for path-validation purposes during and after migration.
36
49
  */
37
50
  export declare function isLifecycleArtifactPath(posix: string): boolean;
38
51
  /**
39
- * Resolve the active lifecycle layout for `projectRoot` (#2109 part 1).
52
+ * Resolve the active lifecycle layout for `projectRoot` (#2109 part 1 / #2112).
53
+ *
54
+ * Requires the migrated `xbrief/` layout: the `xbrief/` directory must exist
55
+ * AND contain at least one `.xbrief.json` artifact. If no such layout is found,
56
+ * throws with a clear error directing the operator to run `deft migrate:xbrief`.
57
+ *
58
+ * The legacy `vbrief/` read-path fallback was removed in #2112 (0.73.0 MINOR).
59
+ * Projects that have not migrated must run `deft migrate:xbrief` first.
40
60
  *
41
- * Prefers the migrated `xbrief/` layout only when BOTH the `xbrief/` directory
42
- * exists AND it contains at least one `.xbrief.json` artifact; otherwise falls
43
- * back to the legacy `vbrief/` layout. With only `vbrief/` present (today's
44
- * repo) the result is the legacy layout, so existing behavior is unchanged.
61
+ * @throws {Error} When no `xbrief/` layout with `.xbrief.json` artifacts exists.
45
62
  */
46
63
  export declare function resolveLifecycleLayout(projectRoot: string): LifecycleLayout;
47
64
  /** Convenience accessor for the absolute resolved lifecycle root directory. */
48
65
  export declare function resolveLifecycleRoot(projectRoot: string): string;
49
66
  /**
50
- * Absolute path to a lifecycle folder under the resolved layout root
51
- * (e.g. `<root>/<xbrief|vbrief>/active`). Layout-aware (#2109 part 2a).
67
+ * Absolute path to a lifecycle folder under the resolved `xbrief/` layout root
68
+ * (e.g. `<root>/xbrief/active`). Layout-aware (#2109 part 2a / #2112).
52
69
  */
53
70
  export declare function resolveLifecycleFolder(projectRoot: string, folder: string): string;
54
71
  /** Absolute path to the layout-aware `.eval/` directory for version-eval results (#1703). */
@@ -68,20 +85,19 @@ export declare function projectDefinitionFilename(projectRoot: string): string;
68
85
  export declare function resolveProjectDefinitionPath(projectRoot: string): string;
69
86
  /**
70
87
  * POSIX-style display path to the resolved PROJECT-DEFINITION artifact relative
71
- * to the project root (e.g. `vbrief/PROJECT-DEFINITION.vbrief.json`).
88
+ * to the project root (e.g. `xbrief/PROJECT-DEFINITION.xbrief.json`).
72
89
  */
73
90
  export declare function projectDefinitionRelPath(projectRoot: string): string;
74
91
  /**
75
- * Absolute path to the resolved specification artifact (#2132).
92
+ * Absolute path to the resolved specification artifact (#2132 / #2112).
76
93
  *
77
- * Prefers `xbrief/specification.xbrief.json` on a migrated tree; falls back to
78
- * `vbrief/specification.vbrief.json` on an unmigrated tree. Consistent with the
79
- * layout resolver used by the #2109 Part-2a call-site sweep.
94
+ * Returns `xbrief/specification.xbrief.json`; throws when no migrated xbrief/
95
+ * layout is present (see `resolveLifecycleLayout`).
80
96
  */
81
97
  export declare function resolveSpecArtifactPath(projectRoot: string): string;
82
98
  /**
83
99
  * POSIX-style display path to the resolved specification artifact relative to
84
- * the project root (e.g. `xbrief/specification.xbrief.json`).
100
+ * the project root (`xbrief/specification.xbrief.json`).
85
101
  */
86
102
  export declare function specArtifactRelPath(projectRoot: string): string;
87
103
  //# sourceMappingURL=resolve.d.ts.map