@deftai/directive-core 0.71.1 → 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 (94) 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/events/attribution-enrichment.d.ts +35 -0
  10. package/dist/events/attribution-enrichment.js +74 -0
  11. package/dist/events/attribution-ledger.js +6 -1
  12. package/dist/init-deposit/gitignore.js +6 -0
  13. package/dist/init-deposit/hygiene.d.ts +24 -0
  14. package/dist/init-deposit/hygiene.js +55 -0
  15. package/dist/init-deposit/refresh.js +22 -6
  16. package/dist/init-deposit/scaffold.d.ts +11 -1
  17. package/dist/init-deposit/scaffold.js +4 -4
  18. package/dist/integration-e2e/helpers.js +7 -7
  19. package/dist/layout/index.d.ts +1 -1
  20. package/dist/layout/index.js +1 -1
  21. package/dist/layout/resolve.d.ts +47 -31
  22. package/dist/layout/resolve.js +93 -36
  23. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  24. package/dist/lifecycle/lifecycle-hygiene.js +1 -1
  25. package/dist/migrate-preflight/index.js +10 -10
  26. package/dist/policy/index.js +3 -3
  27. package/dist/policy/resolve.d.ts +2 -2
  28. package/dist/policy/resolve.js +11 -3
  29. package/dist/policy/value-feedback-autoenable.d.ts +33 -0
  30. package/dist/policy/value-feedback-autoenable.js +67 -0
  31. package/dist/policy/value-feedback.d.ts +30 -5
  32. package/dist/policy/value-feedback.js +52 -34
  33. package/dist/policy/wip.d.ts +1 -1
  34. package/dist/policy/wip.js +15 -3
  35. package/dist/preflight-cache/evaluate.js +1 -1
  36. package/dist/release/native-steps.js +14 -2
  37. package/dist/render/export-spec.js +2 -2
  38. package/dist/render/project-render.js +40 -9
  39. package/dist/scope/decompose.js +12 -2
  40. package/dist/session/resume-conditions.js +2 -2
  41. package/dist/session/ritual-sentinel.d.ts +1 -1
  42. package/dist/session/ritual-sentinel.js +6 -4
  43. package/dist/slice/constants.d.ts +1 -1
  44. package/dist/slice/constants.js +1 -1
  45. package/dist/slice/project-context.js +1 -1
  46. package/dist/spec-authority/resolver.js +9 -2
  47. package/dist/swarm/complete-cohort.js +14 -1
  48. package/dist/swarm/readiness.js +33 -3
  49. package/dist/task-surface/index.js +3 -3
  50. package/dist/triage/actions/candidates-log.d.ts +1 -1
  51. package/dist/triage/bootstrap/gitignore.js +22 -10
  52. package/dist/triage/bootstrap/index.d.ts +1 -1
  53. package/dist/triage/bootstrap/index.js +9 -3
  54. package/dist/triage/cache-path.d.ts +1 -1
  55. package/dist/triage/cache-path.js +32 -6
  56. package/dist/triage/classify/index.d.ts +1 -1
  57. package/dist/triage/classify/index.js +11 -4
  58. package/dist/triage/help/registry-data.d.ts +2 -2
  59. package/dist/triage/help/registry-data.js +2 -2
  60. package/dist/triage/queue/constants.d.ts +4 -4
  61. package/dist/triage/queue/constants.js +4 -4
  62. package/dist/triage/queue/scope-walk.js +7 -1
  63. package/dist/triage/reconcile/reconcile.js +23 -3
  64. package/dist/triage/reconcile/types.d.ts +1 -1
  65. package/dist/triage/reconcile/types.js +1 -1
  66. package/dist/triage/refresh/refresh.js +9 -2
  67. package/dist/triage/scope/constants.d.ts +2 -2
  68. package/dist/triage/scope/constants.js +2 -2
  69. package/dist/triage/scope/renderers.js +7 -1
  70. package/dist/triage/smoketest/index.js +9 -6
  71. package/dist/triage/subscribe/index.d.ts +2 -2
  72. package/dist/triage/subscribe/index.js +2 -2
  73. package/dist/triage/summary/index.d.ts +2 -2
  74. package/dist/triage/summary/index.js +1 -1
  75. package/dist/triage/summary/reconcilable.d.ts +1 -1
  76. package/dist/triage/welcome/constants.d.ts +2 -2
  77. package/dist/triage/welcome/constants.js +2 -2
  78. package/dist/triage/welcome/onboard.js +8 -1
  79. package/dist/triage/welcome/prior-state.js +14 -2
  80. package/dist/triage/welcome/summary.js +14 -2
  81. package/dist/triage/welcome/writers.js +7 -1
  82. package/dist/validate-content/validate-strategy-output.js +11 -1
  83. package/dist/vbrief-build/parity-scenarios.js +3 -3
  84. package/dist/vbrief-reconcile/labels.js +16 -1
  85. package/dist/vbrief-reconcile/parity-scenarios.js +10 -10
  86. package/dist/vbrief-reconcile/umbrellas.js +16 -1
  87. package/dist/vbrief-validate/main.js +13 -1
  88. package/dist/vbrief-validate/precutover.js +33 -5
  89. package/dist/vbrief-validate/staleness.d.ts +1 -1
  90. package/dist/vbrief-validate/staleness.js +5 -2
  91. package/dist/verify-source/code-structure-validate.js +7 -1
  92. package/dist/xbrief-migrate/signpost.d.ts +4 -4
  93. package/dist/xbrief-migrate/signpost.js +11 -15
  94. 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
  }
@@ -0,0 +1,35 @@
1
+ /** Record-shape version for attribution ledger entries (#2376). Bump on shape changes. */
2
+ export declare const ATTRIBUTION_SCHEMA_VERSION: 1;
3
+ /** Project-local, gitignored install-identity file. */
4
+ export declare const INSTALL_ID_REL: string;
5
+ /** Identity + provenance fields stamped onto every attribution record (#2376). */
6
+ export interface AttributionEnrichment {
7
+ /** Normalized `owner/name` of the origin remote, or null when unavailable. */
8
+ readonly repo: string | null;
9
+ /** directive engine version at emit time. */
10
+ readonly directive_version: string;
11
+ /** Stable per-checkout uuid, or null when it cannot be read/created. */
12
+ readonly install_id: string | null;
13
+ /** Attribution record-shape version. */
14
+ readonly schema_version: number;
15
+ }
16
+ /**
17
+ * Read-or-create a stable per-checkout install id in `.deft-cache/install-id`.
18
+ * Best-effort: returns null (never throws) if the file cannot be read or written.
19
+ */
20
+ export declare function resolveInstallId(projectRoot: string): string | null;
21
+ export interface BuildAttributionEnrichmentOptions {
22
+ /** Test seam for origin-repo resolution; defaults to `inferRepoFromGit`. */
23
+ readonly repoResolver?: (projectRoot: string) => string | null;
24
+ }
25
+ /**
26
+ * Build the identity/provenance enrichment stamped on attribution records (#2376).
27
+ *
28
+ * Memoized per `projectRoot` (identity/version/repo are stable within a session)
29
+ * so a caller that emits many signals in one run does not spawn a git process and
30
+ * open the install-id file per event -- mirrors `detectOriginOrg`'s cache to keep
31
+ * this off the hot emit path (#2377 review). A custom `repoResolver` bypasses the
32
+ * cache so tests stay deterministic. Every field degrades gracefully; never throws.
33
+ */
34
+ export declare function buildAttributionEnrichment(projectRoot: string, options?: BuildAttributionEnrichmentOptions): AttributionEnrichment;
35
+ //# sourceMappingURL=attribution-enrichment.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { readCorePackageVersion } from "../engine-version.js";
5
+ import { inferRepoFromGit } from "../triage/queue/repo.js";
6
+ /** Record-shape version for attribution ledger entries (#2376). Bump on shape changes. */
7
+ export const ATTRIBUTION_SCHEMA_VERSION = 1;
8
+ /** Project-local, gitignored install-identity file. */
9
+ export const INSTALL_ID_REL = join(".deft-cache", "install-id");
10
+ /**
11
+ * Read-or-create a stable per-checkout install id in `.deft-cache/install-id`.
12
+ * Best-effort: returns null (never throws) if the file cannot be read or written.
13
+ */
14
+ export function resolveInstallId(projectRoot) {
15
+ const path = resolve(projectRoot, INSTALL_ID_REL);
16
+ try {
17
+ if (existsSync(path)) {
18
+ const existing = readFileSync(path, "utf8").trim();
19
+ if (existing.length > 0) {
20
+ return existing;
21
+ }
22
+ }
23
+ }
24
+ catch {
25
+ // fall through and try to (re)create
26
+ }
27
+ try {
28
+ const id = randomUUID();
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ writeFileSync(path, `${id}\n`, "utf8");
31
+ return id;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ const enrichmentCache = new Map();
38
+ /**
39
+ * Build the identity/provenance enrichment stamped on attribution records (#2376).
40
+ *
41
+ * Memoized per `projectRoot` (identity/version/repo are stable within a session)
42
+ * so a caller that emits many signals in one run does not spawn a git process and
43
+ * open the install-id file per event -- mirrors `detectOriginOrg`'s cache to keep
44
+ * this off the hot emit path (#2377 review). A custom `repoResolver` bypasses the
45
+ * cache so tests stay deterministic. Every field degrades gracefully; never throws.
46
+ */
47
+ export function buildAttributionEnrichment(projectRoot, options = {}) {
48
+ const useCache = options.repoResolver === undefined;
49
+ if (useCache) {
50
+ const cached = enrichmentCache.get(projectRoot);
51
+ if (cached !== undefined) {
52
+ return cached;
53
+ }
54
+ }
55
+ const resolver = options.repoResolver ?? inferRepoFromGit;
56
+ let repo = null;
57
+ try {
58
+ repo = resolver(projectRoot);
59
+ }
60
+ catch {
61
+ repo = null;
62
+ }
63
+ const enrichment = {
64
+ repo,
65
+ directive_version: readCorePackageVersion(),
66
+ install_id: resolveInstallId(projectRoot),
67
+ schema_version: ATTRIBUTION_SCHEMA_VERSION,
68
+ };
69
+ if (useCache) {
70
+ enrichmentCache.set(projectRoot, enrichment);
71
+ }
72
+ return enrichment;
73
+ }
74
+ //# sourceMappingURL=attribution-enrichment.js.map
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
2
2
  import { DEFAULT_EVENT_LOG, emit } from "../lifecycle/events.js";
3
3
  import { isValueFeedbackPathAllowed, resolveValueFeedback, } from "../policy/value-feedback.js";
4
4
  import { ATTRIBUTION_EVENT_NAMES } from "./attribution-constants.js";
5
+ import { buildAttributionEnrichment } from "./attribution-enrichment.js";
5
6
  export { ALL_ATTRIBUTION_EVENT_NAMES, ATTRIBUTION_EVENT_NAMES, ATTRIBUTION_REQUIRED_PAYLOAD, } from "./attribution-constants.js";
6
7
  function resolveLedgerLogPath(projectRoot, logPath) {
7
8
  if (logPath !== undefined && logPath !== null) {
@@ -28,9 +29,13 @@ export function emitAttributionSignal(name, payload, options) {
28
29
  }
29
30
  const signalClass = signalClassForEvent(name);
30
31
  const logPath = resolveLedgerLogPath(options.projectRoot, options.logPath);
32
+ const enrichment = buildAttributionEnrichment(options.projectRoot);
33
+ // Authoritative fields (signal_class + provenance enrichment) are spread LAST
34
+ // so a caller payload can never silently shadow them (#2377 review).
31
35
  return emit(name, {
32
- signal_class: signalClass,
33
36
  ...payload,
37
+ signal_class: signalClass,
38
+ ...enrichment,
34
39
  }, { logPath });
35
40
  }
36
41
  catch {
@@ -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