@deftai/directive-core 0.60.0 → 0.61.1

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.
@@ -7,13 +7,14 @@
7
7
  *
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
- import { existsSync, readFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
11
11
  import { join, resolve } from "node:path";
12
12
  import { copyTree } from "../deposit/copy-tree.js";
13
13
  import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
14
14
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
15
15
  import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
16
16
  import { readCorePackageVersion } from "../engine-version.js";
17
+ import { DEV_FALLBACK } from "../platform/constants.js";
17
18
  import { gitPorcelain } from "../story-ready/git.js";
18
19
  import { parseInitArgv } from "./init-deposit.js";
19
20
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
@@ -67,6 +68,46 @@ function readRecordedDepositVersion(deftDir) {
67
68
  return null;
68
69
  }
69
70
  }
71
+ /** Prior `managed_by` provenance sentinel from the deposit manifest, if any (#2056). */
72
+ function readRecordedManagedBy(deftDir) {
73
+ const manifestPath = join(deftDir, "VERSION");
74
+ if (!existsSync(manifestPath))
75
+ return null;
76
+ try {
77
+ const value = (parseInstallManifest(readFileSync(manifestPath, "utf8")).managed_by ?? "").trim();
78
+ return value || null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Regenerate the bare `.deft-version` derivative from the deposited content
86
+ * version in the same transaction as the payload swap (#2055). The canonical
87
+ * marker lives at `vbrief/.deft-version`; fall back to the project root only
88
+ * when `vbrief/` is absent. Never persist the dev fallback.
89
+ */
90
+ function syncBareVersionMarker(projectDir, version) {
91
+ const normalized = normalizeVersion(version);
92
+ if (!normalized || normalized === DEV_FALLBACK)
93
+ return;
94
+ const vbriefDir = join(projectDir, "vbrief");
95
+ let targetDir = projectDir;
96
+ try {
97
+ if (statSync(vbriefDir).isDirectory())
98
+ targetDir = vbriefDir;
99
+ }
100
+ catch {
101
+ // vbrief/ absent — write the root-level derivative instead
102
+ }
103
+ try {
104
+ mkdirSync(targetDir, { recursive: true });
105
+ writeFileSync(join(targetDir, ".deft-version"), `${normalized}\n`, "utf8");
106
+ }
107
+ catch {
108
+ // best-effort, mirrors install-upgrade marker write
109
+ }
110
+ }
70
111
  function isInstallerManagedPath(path) {
71
112
  if (INSTALLER_MANAGED_EXACT.has(path))
72
113
  return true;
@@ -208,6 +249,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
208
249
  const readPackageVersion = seams.readPackageVersion ?? readCorePackageVersion;
209
250
  const contentRoot = await resolveContent();
210
251
  const previousDepositVersion = readRecordedDepositVersion(deftDir);
252
+ const previousManagedBy = readRecordedManagedBy(deftDir);
211
253
  const engineVersion = readEngine();
212
254
  const contentVersion = readContentPackageVersion(contentRoot, readPackageVersion);
213
255
  const versionSkewNotice = buildVersionSkewNotice(engineVersion, contentVersion, previousDepositVersion);
@@ -221,8 +263,13 @@ export async function runRefreshDeposit(args, io, seams = {}) {
221
263
  installRoot: CANONICAL_INSTALL_ROOT,
222
264
  fetchedAt: nowIso(),
223
265
  fetchedBy: "directive-update",
266
+ ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
224
267
  };
225
268
  writeInstallManifest(projectDir, deftDir, manifestFields);
269
+ // #2055: regenerate the bare .deft-version derivative so it agrees with the
270
+ // freshly written manifest tag (otherwise doctor's manifest-agreement check
271
+ // fails and the operator must hand-edit the marker).
272
+ syncBareVersionMarker(projectDir, contentVersion);
226
273
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
227
274
  await depositNeutralization(projectDir, io);
228
275
  const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
@@ -18,6 +18,13 @@ export interface InstallManifestFields {
18
18
  installRoot: string;
19
19
  fetchedAt: string;
20
20
  fetchedBy: string;
21
+ /**
22
+ * Provenance sentinel (#2056). When the prior manifest was stamped
23
+ * `managed_by: 'npm'` by the npm-migration path, callers thread it through a
24
+ * manifest rebuild so a routine `directive update` / `install-upgrade` does not
25
+ * regress provenance and re-arm the doctor signpost.
26
+ */
27
+ managedBy?: string;
21
28
  }
22
29
  export declare function buildInstallManifestText(fields: InstallManifestFields): string;
23
30
  export declare function writeInstallManifest(projectDir: string, deftDir: string, fields: InstallManifestFields): string;
@@ -187,12 +187,17 @@ export function buildInstallManifestText(fields) {
187
187
  effectiveTag = `v${effectiveTag}`;
188
188
  }
189
189
  const effectiveRef = fields.ref || effectiveTag;
190
- return (`ref: '${effectiveRef}'\n` +
190
+ let body = `ref: '${effectiveRef}'\n` +
191
191
  `sha: '${fields.sha}'\n` +
192
192
  `tag: '${effectiveTag}'\n` +
193
193
  `install_root: '${fields.installRoot}'\n` +
194
194
  `fetched_at: '${fields.fetchedAt}'\n` +
195
- `fetched_by: '${fields.fetchedBy}'\n`);
195
+ `fetched_by: '${fields.fetchedBy}'\n`;
196
+ const managedBy = fields.managedBy?.trim();
197
+ if (managedBy) {
198
+ body += `managed_by: '${managedBy}'\n`;
199
+ }
200
+ return body;
196
201
  }
197
202
  export function writeInstallManifest(projectDir, deftDir, fields) {
198
203
  const installRoot = fields.installRoot ||
@@ -1,11 +1,75 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
- import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
3
+ import { locateManifest, manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
4
+ import { readCorePackageVersion } from "../engine-version.js";
4
5
  import { buildInstallManifestText, CANONICAL_INSTALL_ROOT, } from "../init-deposit/scaffold.js";
5
6
  import { agentsRefreshPlan } from "../platform/agents-md.js";
6
7
  import { DEV_FALLBACK } from "../platform/constants.js";
7
8
  import { resolveVersion } from "../platform/resolve-version.js";
8
9
  import { detectPreCutoverLegacy } from "../vbrief-validate/precutover.js";
10
+ const ENGINE_PACKAGE_FALLBACK = "0.0.0";
11
+ /**
12
+ * Resolve the framework version for an upgrade (#2053).
13
+ *
14
+ * `resolveVersion({ frameworkRoot })` reads env / install-manifest / .deft-version
15
+ * / git against the *engine* framework root. When `deft install-upgrade` runs from
16
+ * a global npm install, that root is the npm package directory — which carries no
17
+ * install manifest, no bare marker, and is not its own git repo — so the chain
18
+ * dead-ends at `0.0.0-dev`. Recover by auto-detecting the *consumer project's*
19
+ * deposited manifest, then fall back to the engine package.json version, before
20
+ * surrendering to the dev fallback.
21
+ */
22
+ function resolveUpgradeVersion(frameworkRoot, projectRoot) {
23
+ const primary = resolveVersion({ frameworkRoot });
24
+ if (primary !== DEV_FALLBACK)
25
+ return primary;
26
+ const manifestPath = locateManifest(projectRoot, null);
27
+ if (manifestPath) {
28
+ try {
29
+ const tag = manifestTagToVersion(parseInstallManifest(readFileSync(manifestPath, "utf8")));
30
+ if (tag)
31
+ return tag;
32
+ }
33
+ catch {
34
+ // fall through to package.json fallback
35
+ }
36
+ }
37
+ const pkgVersion = readCorePackageVersion();
38
+ if (pkgVersion && pkgVersion !== ENGINE_PACKAGE_FALLBACK)
39
+ return pkgVersion;
40
+ return DEV_FALLBACK;
41
+ }
42
+ /**
43
+ * The consumer's deposited framework install root (`.deft/core`, then legacy
44
+ * `deft/`), if present. Used as the AGENTS.md render root and manifest target so
45
+ * the refresh renders from the *deposited* templates that match the installed
46
+ * payload -- not the (possibly newer) templates bundled in a global npm engine.
47
+ */
48
+ function resolveInstallRoot(projectRoot) {
49
+ for (const candidate of [join(projectRoot, ".deft", "core"), join(projectRoot, "deft")]) {
50
+ try {
51
+ if (statSync(candidate).isDirectory())
52
+ return candidate;
53
+ }
54
+ catch {
55
+ // try next candidate
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /** Prior `managed_by` provenance sentinel from an install manifest, if any (#2056). */
61
+ function readManagedByAt(installRoot) {
62
+ const manifestPath = join(installRoot, "VERSION");
63
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile())
64
+ return null;
65
+ try {
66
+ const value = (parseInstallManifest(readFileSync(manifestPath, "utf8")).managed_by ?? "").trim();
67
+ return value || null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
9
73
  function versionMarkerPaths(projectRoot) {
10
74
  return [join(projectRoot, "vbrief", ".deft-version"), join(projectRoot, ".deft-version")];
11
75
  }
@@ -46,6 +110,7 @@ function deriveInstallRootString(installRoot, projectRoot) {
46
110
  function writeInstallManifestAt(installRoot, projectRoot, version) {
47
111
  if (version.replace(/^v/, "") === DEV_FALLBACK)
48
112
  return null;
113
+ const priorManagedBy = readManagedByAt(installRoot);
49
114
  const fields = {
50
115
  ref: version.startsWith("v") ? version : `v${version}`,
51
116
  sha: "content-package",
@@ -53,6 +118,7 @@ function writeInstallManifestAt(installRoot, projectRoot, version) {
53
118
  installRoot: deriveInstallRootString(installRoot, projectRoot),
54
119
  fetchedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
55
120
  fetchedBy: "deft-upgrade",
121
+ ...(priorManagedBy ? { managedBy: priorManagedBy } : {}),
56
122
  };
57
123
  try {
58
124
  mkdirSync(installRoot, { recursive: true });
@@ -111,13 +177,19 @@ function runAgentsRefresh(projectRoot, frameworkRoot, io) {
111
177
  export function runInstallUpgrade(args, io) {
112
178
  const projectRoot = resolve(args.projectRoot);
113
179
  const frameworkRoot = resolve(args.frameworkRoot);
114
- const version = resolveVersion({ frameworkRoot });
180
+ const version = resolveUpgradeVersion(frameworkRoot, projectRoot);
115
181
  const normalizedVersion = version.startsWith("v") ? version.slice(1) : version;
182
+ // Render AGENTS.md from the deposited install root so the managed section
183
+ // matches the installed payload, not the engine's bundled templates (which a
184
+ // global npm `deft` may carry at a different version). Falls back to the engine
185
+ // framework root when no deposit is present.
186
+ const installRoot = resolveInstallRoot(projectRoot);
187
+ const agentsRoot = installRoot ?? frameworkRoot;
116
188
  io.writeOut(`Deft CLI v${normalizedVersion} - Upgrade\n\n`);
117
189
  const recorded = readVersionMarker(projectRoot);
118
190
  if (recorded === normalizedVersion) {
119
191
  io.writeOut(`Project already at ${normalizedVersion}. Nothing to do.\n`);
120
- return runAgentsRefresh(projectRoot, frameworkRoot, io);
192
+ return runAgentsRefresh(projectRoot, agentsRoot, io);
121
193
  }
122
194
  const legacy = detectPreCutoverLegacy(projectRoot);
123
195
  if (legacy.length > 0) {
@@ -126,21 +198,25 @@ export function runInstallUpgrade(args, io) {
126
198
  const vbriefDir = join(projectRoot, "vbrief");
127
199
  const targetDir = existsSync(vbriefDir) && statSync(vbriefDir).isDirectory() ? vbriefDir : projectRoot;
128
200
  writeVersionMarker(targetDir, normalizedVersion);
129
- let writtenManifestPath = null;
130
- for (const installCandidate of [join(projectRoot, ".deft", "core"), join(projectRoot, "deft")]) {
131
- if (existsSync(installCandidate) && statSync(installCandidate).isDirectory()) {
132
- writtenManifestPath = writeInstallManifestAt(installCandidate, projectRoot, normalizedVersion);
133
- break;
134
- }
135
- }
201
+ const writtenManifestPath = installRoot !== null
202
+ ? writeInstallManifestAt(installRoot, projectRoot, normalizedVersion)
203
+ : null;
136
204
  migrateLegacyInstallManifest(projectRoot, writtenManifestPath);
137
- if (recorded === null) {
205
+ if (normalizedVersion === DEV_FALLBACK) {
206
+ // #2053: the marker + manifest writers above no-op on the dev fallback, so do
207
+ // not claim a marker update that never happened.
208
+ io.writeOut("Could not resolve a published framework version (resolved 0.0.0-dev); " +
209
+ ".deft-version marker and install manifest were left unchanged. On a consumer " +
210
+ "install, ensure <project>/.deft/core/VERSION carries a real tag, or upgrade the " +
211
+ "engine with `npm i -g @deftai/directive@latest`.\n");
212
+ }
213
+ else if (recorded === null) {
138
214
  io.writeOut(`Recorded framework version ${normalizedVersion} in .deft-version.\n`);
139
215
  }
140
216
  else {
141
217
  io.writeOut(`Updated .deft-version from ${recorded} to ${normalizedVersion}.\n`);
142
218
  }
143
219
  io.writeOut("If legacy SPECIFICATION.md or PROJECT.md content remains, run `task migrate:vbrief` to complete the upgrade.\n");
144
- return runAgentsRefresh(projectRoot, frameworkRoot, io);
220
+ return runAgentsRefresh(projectRoot, agentsRoot, io);
145
221
  }
146
222
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,17 @@
1
+ export type ExportAudience = "stakeholder" | "internal";
2
+ export interface ExportSpecOptions {
3
+ readonly projectRoot?: string;
4
+ readonly outPath?: string;
5
+ readonly audience?: ExportAudience;
6
+ readonly includeScopes?: boolean;
7
+ readonly proposedLimit?: number;
8
+ }
9
+ export type ExportSpecResult = readonly [boolean, string];
10
+ /** Unified spec export (#2013 / #1502). */
11
+ export declare function exportSpec(options?: ExportSpecOptions): ExportSpecResult;
12
+ export declare function parseExportSpecArgv(argv: readonly string[]): {
13
+ options: ExportSpecOptions;
14
+ errors: string[];
15
+ };
16
+ export declare function exportSpecMain(argv: readonly string[]): number;
17
+ //# sourceMappingURL=export-spec.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { greenfieldOverviewNonEmpty, renderNarrativeSections, resolveExportNarratives, } from "../spec-authority/narratives.js";
4
+ import { resolveSpecAuthority } from "../spec-authority/resolver.js";
5
+ import { buildScopeOutlookSection } from "./scope-outlook.js";
6
+ import { validateSpec } from "./spec-validate.js";
7
+ function loadPlanTitle(path, fallback) {
8
+ try {
9
+ const doc = JSON.parse(readFileSync(path, "utf8"));
10
+ const plan = doc.plan;
11
+ if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
12
+ return String(plan.title ?? fallback);
13
+ }
14
+ }
15
+ catch {
16
+ /* ignore */
17
+ }
18
+ return fallback;
19
+ }
20
+ /** Unified spec export (#2013 / #1502). */
21
+ export function exportSpec(options = {}) {
22
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
23
+ const outPath = options.outPath ?? join(projectRoot, "SPECIFICATION.md");
24
+ const audience = options.audience ?? "stakeholder";
25
+ const includeScopes = options.includeScopes ?? true;
26
+ const includeProposed = audience === "internal";
27
+ const authority = resolveSpecAuthority(projectRoot);
28
+ if (!authority) {
29
+ return [false, "✗ Missing vbrief/PROJECT-DEFINITION.vbrief.json — cannot export spec."];
30
+ }
31
+ if (authority.kind === "full-spec" && authority.specPath) {
32
+ const [ok, msg] = validateSpec(authority.specPath);
33
+ if (!ok)
34
+ return [false, msg];
35
+ }
36
+ else if (!greenfieldOverviewNonEmpty(authority)) {
37
+ return [
38
+ false,
39
+ "⚠ PROJECT-DEFINITION.vbrief.json Overview narrative is empty (D3). Populate Overview before export.",
40
+ ];
41
+ }
42
+ const narratives = resolveExportNarratives(authority);
43
+ const title = authority.kind === "full-spec" && authority.specPath
44
+ ? loadPlanTitle(authority.specPath, "Specification")
45
+ : loadPlanTitle(authority.projectDefPath, "Specification");
46
+ const lines = [
47
+ authority.banner,
48
+ `# ${title}\n`,
49
+ ...renderNarrativeSections(narratives),
50
+ ];
51
+ if (includeScopes) {
52
+ const scopeLines = buildScopeOutlookSection(authority.vbriefDir, {
53
+ includeProposed,
54
+ proposedLimit: options.proposedLimit,
55
+ });
56
+ if (scopeLines.length > 0)
57
+ lines.push(...scopeLines);
58
+ }
59
+ writeFileSync(outPath, lines.join("\n"), "utf8");
60
+ return [true, `✓ Exported spec to ${outPath}`];
61
+ }
62
+ export function parseExportSpecArgv(argv) {
63
+ const options = {};
64
+ const errors = [];
65
+ const positional = [];
66
+ for (const arg of argv) {
67
+ if (arg === "--audience=stakeholder" || arg === "--audience=internal") {
68
+ options.audience = arg.split("=")[1];
69
+ continue;
70
+ }
71
+ if (arg.startsWith("--proposed-limit=")) {
72
+ const n = Number(arg.split("=", 2)[1]);
73
+ if (Number.isFinite(n) && n > 0)
74
+ options.proposedLimit = n;
75
+ continue;
76
+ }
77
+ if (arg === "--no-scopes") {
78
+ options.includeScopes = false;
79
+ continue;
80
+ }
81
+ if (arg.startsWith("--")) {
82
+ errors.push(`Unknown flag: ${arg}`);
83
+ continue;
84
+ }
85
+ positional.push(arg);
86
+ }
87
+ if (positional[0])
88
+ options.projectRoot = positional[0];
89
+ if (positional[1])
90
+ options.outPath = positional[1];
91
+ return { options: options, errors };
92
+ }
93
+ export function exportSpecMain(argv) {
94
+ const { options, errors } = parseExportSpecArgv(argv);
95
+ if (errors.length > 0) {
96
+ for (const e of errors)
97
+ console.error(e);
98
+ return 2;
99
+ }
100
+ const [ok, msg] = exportSpec(options);
101
+ console.log(msg);
102
+ return ok ? 0 : 1;
103
+ }
104
+ //# sourceMappingURL=export-spec.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./constants.js";
2
+ export { type ExportAudience, type ExportSpecOptions, exportSpec, exportSpecMain, parseExportSpecArgv, } from "./export-spec.js";
2
3
  export * as frameworkCommands from "./framework-commands.js";
3
4
  export { availableCommands, cmdCoreValidate, formatFrameworkCommand, hasCommand, main as frameworkCommandsMain, normalizeTaskSeparator, runFrameworkCommand, } from "./framework-commands.js";
4
5
  export * as prdRender from "./prd-render.js";
@@ -7,6 +8,7 @@ export * as projectRender from "./project-render.js";
7
8
  export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
8
9
  export * as roadmapRender from "./roadmap-render.js";
9
10
  export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
11
+ export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
10
12
  export * as specRender from "./spec-render.js";
11
13
  export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
12
14
  export * as specValidate from "./spec-validate.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./constants.js";
2
+ export { exportSpec, exportSpecMain, parseExportSpecArgv, } from "./export-spec.js";
2
3
  export * as frameworkCommands from "./framework-commands.js";
3
4
  export { availableCommands, cmdCoreValidate, formatFrameworkCommand, hasCommand, main as frameworkCommandsMain, normalizeTaskSeparator, runFrameworkCommand, } from "./framework-commands.js";
4
5
  export * as prdRender from "./prd-render.js";
@@ -7,6 +8,7 @@ export * as projectRender from "./project-render.js";
7
8
  export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
8
9
  export * as roadmapRender from "./roadmap-render.js";
9
10
  export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
11
+ export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
10
12
  export * as specRender from "./spec-render.js";
11
13
  export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
12
14
  export * as specValidate from "./spec-validate.js";
@@ -0,0 +1,9 @@
1
+ export interface ScopeOutlookOptions {
2
+ readonly includeProposed?: boolean;
3
+ readonly proposedLimit?: number;
4
+ }
5
+ /** Build Scope outlook section per #2013 Wave 0 §4. */
6
+ export declare function buildScopeOutlookSection(vbriefDir: string, options?: ScopeOutlookOptions): string[];
7
+ /** Legacy wrapper: stakeholder export (no proposed). */
8
+ export declare function aggregateScopeSection(vbriefDir: string): string[];
9
+ //# sourceMappingURL=scope-outlook.d.ts.map