@deftai/directive-core 0.77.0 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.d.ts +1 -0
  8. package/dist/doctor/main.js +48 -0
  9. package/dist/doctor/payload-staleness.js +7 -1
  10. package/dist/doctor/types.d.ts +3 -0
  11. package/dist/eval/triggers.d.ts +76 -0
  12. package/dist/eval/triggers.js +259 -0
  13. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  14. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  15. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  16. package/dist/eval-triggers-relocation/index.js +2 -0
  17. package/dist/hooks/dispatcher.d.ts +43 -0
  18. package/dist/hooks/dispatcher.js +173 -0
  19. package/dist/hooks/index.d.ts +3 -0
  20. package/dist/hooks/index.js +3 -0
  21. package/dist/hooks/scope.d.ts +12 -0
  22. package/dist/hooks/scope.js +46 -0
  23. package/dist/hooks/tools.d.ts +4 -0
  24. package/dist/hooks/tools.js +23 -0
  25. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  26. package/dist/init-deposit/agent-hooks.js +238 -0
  27. package/dist/init-deposit/hygiene.js +4 -0
  28. package/dist/init-deposit/index.d.ts +1 -0
  29. package/dist/init-deposit/index.js +1 -0
  30. package/dist/init-deposit/init-deposit.js +4 -0
  31. package/dist/init-deposit/prettierignore.d.ts +21 -0
  32. package/dist/init-deposit/prettierignore.js +83 -0
  33. package/dist/init-deposit/refresh.d.ts +3 -1
  34. package/dist/init-deposit/refresh.js +31 -40
  35. package/dist/init-deposit/scaffold.js +6 -12
  36. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  37. package/dist/init-deposit/xbrief-projections.js +117 -0
  38. package/dist/intake/reconcile-issues.d.ts +11 -0
  39. package/dist/intake/reconcile-issues.js +166 -51
  40. package/dist/issue-sync/index.d.ts +2 -0
  41. package/dist/issue-sync/index.js +2 -0
  42. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  43. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  44. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  45. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  46. package/dist/platform/index.d.ts +1 -0
  47. package/dist/platform/index.js +1 -0
  48. package/dist/platform/platform-capabilities.d.ts +5 -0
  49. package/dist/platform/platform-capabilities.js +9 -0
  50. package/dist/platform/shell-context.d.ts +30 -0
  51. package/dist/platform/shell-context.js +111 -0
  52. package/dist/pr-monitor/cadence.d.ts +6 -0
  53. package/dist/pr-monitor/cadence.js +13 -0
  54. package/dist/pr-monitor/index.d.ts +1 -1
  55. package/dist/pr-monitor/index.js +1 -1
  56. package/dist/pr-monitor/monitor.d.ts +7 -0
  57. package/dist/pr-monitor/monitor.js +59 -13
  58. package/dist/preflight-cache/evaluate.d.ts +3 -8
  59. package/dist/preflight-cache/evaluate.js +19 -19
  60. package/dist/release/build-dist.js +1 -0
  61. package/dist/release/constants.d.ts +2 -0
  62. package/dist/release/constants.js +8 -1
  63. package/dist/release/flags.js +26 -0
  64. package/dist/release/issue-state-fetch.d.ts +27 -0
  65. package/dist/release/issue-state-fetch.js +146 -0
  66. package/dist/release/main.js +1 -0
  67. package/dist/release/native-steps.js +6 -4
  68. package/dist/release/pipeline.js +15 -5
  69. package/dist/release/preflight.d.ts +6 -2
  70. package/dist/release/preflight.js +19 -5
  71. package/dist/release/types.d.ts +3 -1
  72. package/dist/release-e2e/constants.js +6 -6
  73. package/dist/release-e2e/flags.d.ts +2 -0
  74. package/dist/release-e2e/flags.js +8 -4
  75. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  76. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  77. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  78. package/dist/release-e2e/main.js +15 -9
  79. package/dist/release-e2e/types.d.ts +4 -2
  80. package/dist/resolution/classify.d.ts +1 -1
  81. package/dist/resolution/classify.js +36 -11
  82. package/dist/scope/project-definition-sync.js +2 -150
  83. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  84. package/dist/scope/registry-artifact-sync.js +159 -0
  85. package/dist/scope/specification-sync.d.ts +3 -0
  86. package/dist/scope/specification-sync.js +16 -0
  87. package/dist/scope/transition.js +40 -12
  88. package/dist/session/cache-recovery.d.ts +19 -0
  89. package/dist/session/cache-recovery.js +26 -0
  90. package/dist/session/session-start.d.ts +2 -0
  91. package/dist/session/session-start.js +14 -3
  92. package/dist/session/verify-session-ritual.d.ts +17 -0
  93. package/dist/session/verify-session-ritual.js +64 -0
  94. package/dist/triage/help/registry-data.d.ts +12 -1
  95. package/dist/triage/help/registry-data.js +22 -1
  96. package/dist/triage/welcome/default-mode.js +2 -0
  97. package/dist/triage/welcome/onboard.js +2 -0
  98. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  99. package/dist/ts-check-lane/run-lane.js +9 -0
  100. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  101. package/dist/vbrief-reconcile/labels.js +14 -0
  102. package/dist/vbrief-reconcile/main.d.ts +1 -0
  103. package/dist/vbrief-reconcile/main.js +13 -3
  104. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  105. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  106. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  107. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  108. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  109. package/dist/verify-env/agent-hooks.d.ts +11 -0
  110. package/dist/verify-env/agent-hooks.js +46 -0
  111. package/dist/verify-env/command-spawn.d.ts +5 -0
  112. package/dist/verify-env/command-spawn.js +28 -9
  113. package/dist/verify-env/index.d.ts +1 -0
  114. package/dist/verify-env/index.js +1 -0
  115. package/dist/verify-env/verify-hooks-installed.js +5 -5
  116. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  117. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  118. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  119. package/dist/vitest-runner/coverage-debt.js +155 -0
  120. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  121. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  122. package/dist/xbrief-migrate/detect.js +46 -1
  123. package/dist/xbrief-migrate/migrate-project.js +19 -4
  124. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  125. package/dist/xbrief-migrate/migration-containment.js +37 -0
  126. package/package.json +19 -3
@@ -46,12 +46,23 @@ function packTarballPath(packDir, pkgDir) {
46
46
  const scoped = manifest.name.replaceAll("@", "").replaceAll("/", "-");
47
47
  return join(packDir, `${scoped}-${manifest.version}.tgz`);
48
48
  }
49
- function runStep(spawn, label, cmd, args, options = {}) {
49
+ function runStep(spawn, label, cmd, args, options = {}, onProgress) {
50
+ onProgress?.(`greenfield smoke: ${label} — starting`);
51
+ const startedMs = Date.now();
50
52
  const result = spawn(cmd, args, options);
53
+ const elapsedMs = Date.now() - startedMs;
51
54
  if (result.status !== 0) {
52
55
  const detail = (result.stderr || result.stdout || "").trim();
53
- return [false, `${label} failed (exit ${result.status}): ${detail.slice(-800)}`];
56
+ const timeoutHint = options.timeoutMs !== undefined && result.status === 128
57
+ ? `; subprocess killed after ${elapsedMs}ms (spawn budget ${options.timeoutMs}ms — likely hang or timeout)`
58
+ : "";
59
+ onProgress?.(`greenfield smoke: ${label} — failed (exit ${result.status}) after ${elapsedMs}ms`);
60
+ return [
61
+ false,
62
+ `${label} failed (exit ${result.status})${timeoutHint}: ${detail.slice(-800) || "(no captured output)"}`,
63
+ ];
54
64
  }
65
+ onProgress?.(`greenfield smoke: ${label} — OK (${elapsedMs}ms)`);
55
66
  return [true, `${label} OK`];
56
67
  }
57
68
  /**
@@ -77,6 +88,8 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
77
88
  return [false, "greenfield-python-free-smoke FAIL: pnpm command prefix is empty"];
78
89
  }
79
90
  const spawn = seams.spawnText ?? spawnText;
91
+ const onProgress = options.onProgress;
92
+ onProgress?.("greenfield smoke: workspace prep starting");
80
93
  const work = mkdtempSync(join(tmpdir(), "deft-greenfield-smoke-"));
81
94
  const packDir = join(work, "packs");
82
95
  mkdirSync(packDir, { recursive: true });
@@ -98,24 +111,28 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
98
111
  cwd: repoRoot,
99
112
  env: envBase,
100
113
  timeoutMs: 120_000,
101
- });
114
+ }, onProgress);
102
115
  if (!ok)
103
116
  return [false, `greenfield smoke: ${reason}`];
104
117
  [ok, reason] = runStep(spawn, "pnpm build", pnpmCmd, [...pnpmArgs, "run", "build"], {
105
118
  cwd: repoRoot,
106
119
  env: envBase,
107
120
  timeoutMs: 120_000,
108
- });
121
+ }, onProgress);
109
122
  if (!ok)
110
123
  return [false, `greenfield smoke: ${reason}`];
111
124
  }
125
+ else {
126
+ onProgress?.("greenfield smoke: skipping workspace prep (DEFT_GREENFIELD_SKIP_PREP=1)");
127
+ }
128
+ onProgress?.("greenfield smoke: aligning npm package versions");
112
129
  [ok, reason] = alignNpmPackageVersions(repoRoot, SMOKE_VERSION);
113
130
  if (!ok)
114
131
  return [false, `greenfield smoke: ${reason}`];
115
132
  const packed = [];
116
133
  for (const pkg of NPM_PUBLISH_PACKAGES) {
117
134
  const pkgDir = join(repoRoot, "packages", pkg);
118
- [ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 });
135
+ [ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 }, onProgress);
119
136
  if (!ok)
120
137
  return [false, `greenfield smoke: ${reason}`];
121
138
  const tgz = packTarballPath(packDir, pkgDir);
@@ -127,7 +144,7 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
127
144
  [ok, reason] = runStep(spawn, "npm install -g", npm, ["install", "-g", ...packed], {
128
145
  env: envBase,
129
146
  timeoutMs: 120_000,
130
- });
147
+ }, onProgress);
131
148
  if (!ok)
132
149
  return [false, `greenfield smoke: ${reason}`];
133
150
  const deft = join(npmPrefix, "bin", "deft");
@@ -139,9 +156,10 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
139
156
  cwd: work,
140
157
  env: pyFree,
141
158
  timeoutMs: 120_000,
142
- });
159
+ }, onProgress);
143
160
  if (!ok)
144
161
  return [false, `greenfield smoke: ${reason}`];
162
+ onProgress?.("greenfield smoke: seeding fixture PROJECT-DEFINITION");
145
163
  seedMinimalProjectDefinition(projectDir);
146
164
  const depositDir = join(projectDir, ".deft", "core");
147
165
  const artifacts = collectPythonArtifacts(depositDir);
@@ -159,13 +177,15 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
159
177
  PATH: `${join(npmPrefix, "bin")}:${pyFree.PATH ?? ""}`,
160
178
  DEFT_SESSION_RITUAL_SKIP: "1",
161
179
  };
180
+ onProgress?.("greenfield smoke: running consumer task deft:check (engine-invoke path)");
162
181
  [ok, reason] = runStep(spawn, "task deft:check", task, ["deft:check"], {
163
182
  cwd: projectDir,
164
183
  env: checkEnv,
165
184
  timeoutMs: 180_000,
166
- });
185
+ }, onProgress);
167
186
  if (!ok)
168
187
  return [false, `greenfield smoke: ${reason}`];
188
+ onProgress?.("greenfield smoke: all steps passed");
169
189
  return [
170
190
  true,
171
191
  "greenfield-python-free-smoke: directive init + task deft:check passed with Python absent from PATH",
@@ -1,6 +1,6 @@
1
1
  import { resolveProjectRoot } from "../release/paths.js";
2
2
  import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_E2E_HELP } from "./constants.js";
3
- import { emit, generateRepoSlug, parseE2EFlags } from "./flags.js";
3
+ import { emit, formatTempRepoCleanupHint, generateRepoSlug, parseE2EFlags } from "./flags.js";
4
4
  import { destroyTempRepo, provisionTempRepo } from "./gh-ops.js";
5
5
  import { runLegacyBridgeLeg } from "./legacy-bridge-leg.js";
6
6
  import { runRehearsal } from "./rehearsal.js";
@@ -18,7 +18,13 @@ export function runE2e(config, seams = {}) {
18
18
  emit("Legacy-bridge leg", "DRYRUN (would run the pinned legacy -> bridge -> npm-hybrid migration leg; reads the " +
19
19
  "Tier-0 SoT lastGoInstaller(), pending-pin while null)");
20
20
  }
21
- emit("Destroy temp repo", `DRYRUN (would run \`gh repo delete ${owner}/${slug} --yes\`)`);
21
+ const cleanupHint = formatTempRepoCleanupHint(owner, slug);
22
+ if (config.destroyRepo) {
23
+ emit("Destroy temp repo", `DRYRUN (would run \`${cleanupHint}\`)`);
24
+ }
25
+ else {
26
+ emit("Temp repo cleanup", `DRYRUN (would keep ${owner}/${slug}; manual cleanup: ${cleanupHint})`);
27
+ }
22
28
  return EXIT_OK;
23
29
  }
24
30
  // Opt-in #1912 leg: local + self-contained (no temp GitHub repo), so run it
@@ -49,19 +55,19 @@ export function runE2e(config, seams = {}) {
49
55
  }
50
56
  }
51
57
  finally {
52
- if (config.keepRepo) {
53
- emit(`Destroy ${owner}/${slug}`, "SKIP (--keep-repo set; manual cleanup required: " +
54
- `gh repo delete ${owner}/${slug} --yes)`);
55
- }
56
- else {
58
+ const cleanupHint = formatTempRepoCleanupHint(owner, slug);
59
+ if (config.destroyRepo) {
57
60
  const [destroyOk, destroyReason] = destroyTempRepo(owner, slug, seams);
58
61
  if (destroyOk) {
59
62
  emit(`Destroy ${owner}/${slug}`, `OK (${destroyReason})`);
60
63
  }
61
64
  else {
62
- emit(`Destroy ${owner}/${slug}`, `WARN (${destroyReason}); manual cleanup hint: gh repo delete ${owner}/${slug} --yes`);
65
+ emit(`Destroy ${owner}/${slug}`, `WARN (${destroyReason}); manual cleanup: ${cleanupHint}`);
63
66
  }
64
67
  }
68
+ else {
69
+ emit(`Temp repo ${owner}/${slug}`, `KEEP (manual cleanup: ${cleanupHint})`);
70
+ }
65
71
  }
66
72
  return rehearsalRc !== EXIT_OK ? rehearsalRc : legacyBridgeRc;
67
73
  }
@@ -84,7 +90,7 @@ export function cmdReleaseE2e(args, seams = {}) {
84
90
  owner: flags.owner,
85
91
  projectRoot,
86
92
  dryRun: flags.dryRun,
87
- keepRepo: flags.keepRepo,
93
+ destroyRepo: flags.destroyRepo,
88
94
  skipNpm: flags.skipNpm,
89
95
  legacyBridge: flags.legacyBridge,
90
96
  repoSlug: null,
@@ -3,7 +3,8 @@ export interface E2EConfig {
3
3
  owner: string;
4
4
  projectRoot: string;
5
5
  dryRun: boolean;
6
- keepRepo: boolean;
6
+ /** When true, attempt gh repo delete after rehearsal (#2572 opt-in privileged mode). */
7
+ destroyRepo: boolean;
7
8
  /** When true, skip the npm publish dry-run rehearsal step (#1910). */
8
9
  skipNpm: boolean;
9
10
  /**
@@ -20,7 +21,8 @@ export interface ParsedE2EFlags {
20
21
  help: boolean;
21
22
  owner: string;
22
23
  dryRun: boolean;
23
- keepRepo: boolean;
24
+ /** When true, attempt gh repo delete after rehearsal (#2572 opt-in privileged mode). */
25
+ destroyRepo: boolean;
24
26
  projectRoot: string | null;
25
27
  skipNpm: boolean;
26
28
  /** Opt-in: run the pinned legacy->bridge->npm-hybrid migration leg (#1912). */
@@ -30,7 +30,7 @@ export interface ClassifySeams {
30
30
  /** Pre-cutover probe; defaults to the shared `detectPreCutover` detector. */
31
31
  readonly preCutoverProbe?: (cwd: string) => boolean;
32
32
  }
33
- /** Default engine probe: try `directive --version`, then `deft --version`. */
33
+ /** Default engine probe: in-process identity, then `directive` / `deft` on PATH. */
34
34
  export declare function defaultEngineProbe(): EngineProbeResult;
35
35
  /**
36
36
  * Classify a project directory into the orthogonal resolution fact-set. All I/O
@@ -11,13 +11,16 @@
11
11
  * - deposited payload version-> `../doctor/manifest.ts` + `../init-deposit/constants.ts`
12
12
  * - committed pin -> `./pin.ts`
13
13
  */
14
- import { execFileSync } from "node:child_process";
15
14
  import { existsSync, readFileSync, statSync } from "node:fs";
16
- import { join } from "node:path";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
17
  import { locateManifest, manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
18
+ import { readCorePackageVersion } from "../engine-version.js";
18
19
  import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
19
20
  import { extractManagedSection, parseManagedSectionAttrs } from "../platform/agents-md.js";
21
+ import { ENV_VAR } from "../platform/constants.js";
20
22
  import { detectPreCutover } from "../vbrief-validate/precutover.js";
23
+ import { resolveCommandOnPath, spawnCommandText } from "../verify-env/command-spawn.js";
21
24
  import { readPin } from "./pin.js";
22
25
  /** App-source markers used for the `hasAppCode` heuristic. */
23
26
  const APP_CODE_MARKERS = [
@@ -57,22 +60,44 @@ function defaultReadText(p) {
57
60
  return null;
58
61
  }
59
62
  }
60
- function probeEngineVersion(binary) {
63
+ function parseVersionFromOutput(out) {
64
+ const match = SEMVER_IN_TEXT_RE.exec(out);
65
+ return match?.[1] ?? null;
66
+ }
67
+ /**
68
+ * Prefer the engine version from the already-loaded core package when doctor /
69
+ * update are executing inside `@deftai/directive` (#2606).
70
+ */
71
+ function readInProcessEngineVersion() {
72
+ const envVersion = process.env[ENV_VAR]?.trim();
73
+ if (envVersion) {
74
+ return parseVersionFromOutput(envVersion) ?? envVersion;
75
+ }
61
76
  try {
62
- const out = execFileSync(binary, ["--version"], {
63
- encoding: "utf8",
64
- timeout: 5000,
65
- stdio: ["ignore", "pipe", "ignore"],
66
- });
67
- const match = SEMVER_IN_TEXT_RE.exec(out);
68
- return match?.[1] ?? null;
77
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
78
+ if (!existsSync(pkgPath)) {
79
+ return null;
80
+ }
81
+ return readCorePackageVersion();
69
82
  }
70
83
  catch {
71
84
  return null;
72
85
  }
73
86
  }
74
- /** Default engine probe: try `directive --version`, then `deft --version`. */
87
+ function probeEngineVersion(binary) {
88
+ const resolved = resolveCommandOnPath(binary);
89
+ const result = spawnCommandText(resolved ?? binary, ["--version"], { timeoutMs: 5000 });
90
+ if (result.status !== 0) {
91
+ return null;
92
+ }
93
+ return parseVersionFromOutput(result.stdout);
94
+ }
95
+ /** Default engine probe: in-process identity, then `directive` / `deft` on PATH. */
75
96
  export function defaultEngineProbe() {
97
+ const inProcess = readInProcessEngineVersion();
98
+ if (inProcess !== null) {
99
+ return { reachable: true, version: inProcess };
100
+ }
76
101
  for (const binary of ["directive", "deft"]) {
77
102
  const version = probeEngineVersion(binary);
78
103
  if (version !== null) {
@@ -1,158 +1,10 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
1
  import { dirname, resolve } from "node:path";
3
- import { referenceTypeMatches } from "@deftai/directive-types";
4
2
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
- import { formatVbriefJson } from "./vbrief-json.js";
6
- import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
7
- function rewriteProjectDefinitionPlanReference(ref, oldResolved, newRel, vbriefRoot) {
8
- if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
9
- return false;
10
- }
11
- const r = ref;
12
- if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
13
- return false;
14
- }
15
- const resolved = resolveVbriefRef(r.uri, vbriefRoot);
16
- if (resolved === null || resolve(resolved) !== resolve(oldResolved)) {
17
- return false;
18
- }
19
- const uri = r.uri;
20
- const newUri = typeof uri === "string" && uri.startsWith("file://") ? `file://${newRel}` : newRel;
21
- if (newUri === uri) {
22
- return false;
23
- }
24
- r.uri = newUri;
25
- return true;
26
- }
27
- function projectItemReferencesScope(item, oldResolved, newResolved, vbriefRoot) {
28
- const metadata = item.metadata;
29
- if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
30
- const meta = metadata;
31
- const sourcePath = meta.source_path;
32
- if (typeof sourcePath === "string") {
33
- const resolved = resolveVbriefRef(sourcePath, vbriefRoot);
34
- if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
35
- return true;
36
- }
37
- }
38
- const metadataRefs = meta.references;
39
- if (Array.isArray(metadataRefs)) {
40
- for (const ref of metadataRefs) {
41
- if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
42
- continue;
43
- }
44
- const r = ref;
45
- if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
46
- continue;
47
- }
48
- const resolved = resolveVbriefRef(r.uri, vbriefRoot);
49
- if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
50
- return true;
51
- }
52
- }
53
- }
54
- }
55
- const refs = item.references;
56
- if (Array.isArray(refs)) {
57
- for (const ref of refs) {
58
- if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
59
- continue;
60
- }
61
- const r = ref;
62
- if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
63
- continue;
64
- }
65
- const resolved = resolveVbriefRef(r.uri, vbriefRoot);
66
- if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
67
- return true;
68
- }
69
- }
70
- }
71
- return false;
72
- }
73
- function projectItemMatchesScope(item, scopeData, oldPath, newPath, vbriefRoot) {
74
- const oldResolved = resolve(oldPath);
75
- const newResolved = resolve(newPath);
76
- if (projectItemReferencesScope(item, oldResolved, newResolved, vbriefRoot)) {
77
- return true;
78
- }
79
- const itemId = item.id;
80
- if (typeof itemId === "string" &&
81
- scopeIdsForFilename(resolve(newPath).split(/[/\\]/).pop() ?? "").has(itemId)) {
82
- return true;
83
- }
84
- const scopePlan = scopeData.plan;
85
- const scopeTitle = typeof scopePlan === "object" && scopePlan !== null && !Array.isArray(scopePlan)
86
- ? scopePlan.title
87
- : undefined;
88
- const itemTitle = item.title;
89
- return (typeof scopeTitle === "string" && typeof itemTitle === "string" && itemTitle === scopeTitle);
90
- }
3
+ import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
91
4
  /** Best-effort sync of PROJECT-DEFINITION after a lifecycle move (#1527). */
92
5
  export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
93
- const newRel = relativeToVbrief(newPath, vbriefRoot);
94
- if (newRel === null) {
95
- return;
96
- }
97
6
  const projectRoot = dirname(resolve(vbriefRoot));
98
7
  const projectDefPath = resolveProjectDefinitionPath(projectRoot);
99
- if (!existsSync(projectDefPath)) {
100
- return;
101
- }
102
- try {
103
- const projectDef = JSON.parse(readFileSync(projectDefPath, "utf8"));
104
- const plan = projectDef.plan;
105
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
106
- return;
107
- }
108
- const p = plan;
109
- let changed = false;
110
- const oldResolved = resolve(oldPath);
111
- const refs = p.references;
112
- if (Array.isArray(refs)) {
113
- for (const ref of refs) {
114
- if (rewriteProjectDefinitionPlanReference(ref, oldResolved, newRel, vbriefRoot)) {
115
- changed = true;
116
- }
117
- }
118
- }
119
- const items = p.items;
120
- if (Array.isArray(items)) {
121
- for (const item of items) {
122
- if (typeof item !== "object" || item === null || Array.isArray(item)) {
123
- continue;
124
- }
125
- const i = item;
126
- if (!projectItemMatchesScope(i, scopeData, oldPath, newPath, vbriefRoot)) {
127
- continue;
128
- }
129
- if (i.status !== targetStatus) {
130
- i.status = targetStatus;
131
- changed = true;
132
- }
133
- let metadata = i.metadata;
134
- if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
135
- metadata = {};
136
- i.metadata = metadata;
137
- }
138
- const meta = metadata;
139
- const targetFolder = dirname(resolve(newPath)).split(/[/\\]/).pop() ?? "";
140
- if (meta.source_path !== newRel) {
141
- meta.source_path = newRel;
142
- changed = true;
143
- }
144
- if (meta.lifecycle_folder !== targetFolder) {
145
- meta.lifecycle_folder = targetFolder;
146
- changed = true;
147
- }
148
- }
149
- }
150
- if (changed) {
151
- writeFileSync(projectDefPath, formatVbriefJson(projectDef), "utf8");
152
- }
153
- }
154
- catch {
155
- /* best-effort */
156
- }
8
+ syncRegistryArtifactAfterScopeMove(projectDefPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus);
157
9
  }
158
10
  //# sourceMappingURL=project-definition-sync.js.map
@@ -0,0 +1,3 @@
1
+ /** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
2
+ export declare function syncRegistryArtifactAfterScopeMove(registryPath: string, scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
3
+ //# sourceMappingURL=registry-artifact-sync.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { formatVbriefJson } from "./vbrief-json.js";
5
+ import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
6
+ function rewriteRegistryPlanReference(ref, oldResolved, newRel, vbriefRoot) {
7
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
8
+ return false;
9
+ }
10
+ const r = ref;
11
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
12
+ return false;
13
+ }
14
+ const resolved = resolveVbriefRef(r.uri, vbriefRoot);
15
+ if (resolved === null || resolve(resolved) !== resolve(oldResolved)) {
16
+ return false;
17
+ }
18
+ const uri = r.uri;
19
+ const newUri = typeof uri === "string" && uri.startsWith("file://") ? `file://${newRel}` : newRel;
20
+ if (newUri === uri) {
21
+ return false;
22
+ }
23
+ r.uri = newUri;
24
+ return true;
25
+ }
26
+ function registryItemReferencesScope(item, oldResolved, newResolved, vbriefRoot) {
27
+ const metadata = item.metadata;
28
+ if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
29
+ const meta = metadata;
30
+ const sourcePath = meta.source_path;
31
+ if (typeof sourcePath === "string") {
32
+ const resolved = resolveVbriefRef(sourcePath, vbriefRoot);
33
+ if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
34
+ return true;
35
+ }
36
+ }
37
+ const metadataRefs = meta.references;
38
+ if (Array.isArray(metadataRefs)) {
39
+ for (const ref of metadataRefs) {
40
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
41
+ continue;
42
+ }
43
+ const r = ref;
44
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
45
+ continue;
46
+ }
47
+ const resolved = resolveVbriefRef(r.uri, vbriefRoot);
48
+ if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
49
+ return true;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ const refs = item.references;
55
+ if (Array.isArray(refs)) {
56
+ for (const ref of refs) {
57
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
58
+ continue;
59
+ }
60
+ const r = ref;
61
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
62
+ continue;
63
+ }
64
+ const resolved = resolveVbriefRef(r.uri, vbriefRoot);
65
+ if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
66
+ return true;
67
+ }
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ function registryItemMatchesScope(item, scopeData, oldPath, newPath, vbriefRoot) {
73
+ const oldResolved = resolve(oldPath);
74
+ const newResolved = resolve(newPath);
75
+ if (registryItemReferencesScope(item, oldResolved, newResolved, vbriefRoot)) {
76
+ return true;
77
+ }
78
+ const itemId = item.id;
79
+ if (typeof itemId === "string" &&
80
+ scopeIdsForFilename(resolve(newPath).split(/[/\\]/).pop() ?? "").has(itemId)) {
81
+ return true;
82
+ }
83
+ const scopePlan = scopeData.plan;
84
+ const scopeTitle = typeof scopePlan === "object" && scopePlan !== null && !Array.isArray(scopePlan)
85
+ ? scopePlan.title
86
+ : undefined;
87
+ const itemTitle = item.title;
88
+ return (typeof scopeTitle === "string" && typeof itemTitle === "string" && itemTitle === scopeTitle);
89
+ }
90
+ /** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
91
+ export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
92
+ const newRel = relativeToVbrief(newPath, vbriefRoot);
93
+ if (newRel === null) {
94
+ return;
95
+ }
96
+ if (!existsSync(registryPath)) {
97
+ return;
98
+ }
99
+ try {
100
+ const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
101
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
102
+ return;
103
+ }
104
+ const registry = parsed;
105
+ const plan = registry.plan;
106
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
107
+ return;
108
+ }
109
+ const p = plan;
110
+ let changed = false;
111
+ const oldResolved = resolve(oldPath);
112
+ const refs = p.references;
113
+ if (Array.isArray(refs)) {
114
+ for (const ref of refs) {
115
+ if (rewriteRegistryPlanReference(ref, oldResolved, newRel, vbriefRoot)) {
116
+ changed = true;
117
+ }
118
+ }
119
+ }
120
+ const items = p.items;
121
+ if (Array.isArray(items)) {
122
+ for (const item of items) {
123
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
124
+ continue;
125
+ }
126
+ const i = item;
127
+ if (!registryItemMatchesScope(i, scopeData, oldPath, newPath, vbriefRoot)) {
128
+ continue;
129
+ }
130
+ if (i.status !== targetStatus) {
131
+ i.status = targetStatus;
132
+ changed = true;
133
+ }
134
+ let metadata = i.metadata;
135
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
136
+ metadata = {};
137
+ i.metadata = metadata;
138
+ }
139
+ const meta = metadata;
140
+ const targetFolder = dirname(resolve(newPath)).split(/[/\\]/).pop() ?? "";
141
+ if (meta.source_path !== newRel) {
142
+ meta.source_path = newRel;
143
+ changed = true;
144
+ }
145
+ if (meta.lifecycle_folder !== targetFolder) {
146
+ meta.lifecycle_folder = targetFolder;
147
+ changed = true;
148
+ }
149
+ }
150
+ }
151
+ if (changed) {
152
+ writeFileSync(registryPath, formatVbriefJson(registry), "utf8");
153
+ }
154
+ }
155
+ catch {
156
+ /* best-effort */
157
+ }
158
+ }
159
+ //# sourceMappingURL=registry-artifact-sync.js.map
@@ -0,0 +1,3 @@
1
+ /** Best-effort sync of specification.xbrief.json after a lifecycle move (#2566). */
2
+ export declare function syncSpecificationAfterScopeMove(scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
3
+ //# sourceMappingURL=specification-sync.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { resolveSpecArtifactPath } from "../layout/resolve.js";
3
+ import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
4
+ /** Best-effort sync of specification.xbrief.json after a lifecycle move (#2566). */
5
+ export function syncSpecificationAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
6
+ const projectRoot = dirname(resolve(vbriefRoot));
7
+ let specPath;
8
+ try {
9
+ specPath = resolveSpecArtifactPath(projectRoot);
10
+ }
11
+ catch {
12
+ return;
13
+ }
14
+ syncRegistryArtifactAfterScopeMove(specPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus);
15
+ }
16
+ //# sourceMappingURL=specification-sync.js.map