@deftai/directive-core 0.56.2 → 0.57.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.
@@ -14,6 +14,11 @@ export declare function checkInstallPathConsistency(projectRoot: string, install
14
14
  * `skip` for the canonical / greenfield layout (nothing to signpost).
15
15
  */
16
16
  export declare function checkLegacyLayout(projectRoot: string, seams?: CheckSeams): CheckResult;
17
+ /**
18
+ * #1997: signpost a canonical-vendored `.deft/core/` deposit that has not yet
19
+ * been stamped npm-managed. Local-only (no network).
20
+ */
21
+ export declare function checkCanonicalVendoredNpmSignpost(projectRoot: string, seams?: CheckSeams): CheckResult;
17
22
  export declare function deriveExitCode(checks: readonly CheckResult[], errors: readonly string[]): number;
18
23
  export declare function runChecksImpl(projectRoot: string, seams?: CheckSeams & {
19
24
  isDir?: (p: string) => boolean;
@@ -1,8 +1,9 @@
1
1
  import { join } from "node:path";
2
2
  import { detectLegacyLayout, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
3
+ import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY, NPM_MANAGED_SENTINEL_VALUE, } from "../init-deposit/migrate.js";
3
4
  import { findSkillPathsInText } from "../text/redos-safe.js";
4
- import { GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL } from "./constants.js";
5
- import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestTagToVersion, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
5
+ import { CANONICAL_UPGRADE_COMMAND, GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL, } from "./constants.js";
6
+ import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestTagToVersion, parseInstallManifest, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
6
7
  import { readTextSafe } from "./paths.js";
7
8
  function readText(path, seams) {
8
9
  return (seams.readText ?? readTextSafe)(path);
@@ -312,11 +313,63 @@ export function checkLegacyLayout(projectRoot, seams = {}) {
312
313
  },
313
314
  };
314
315
  }
316
+ /**
317
+ * #1997: signpost a canonical-vendored `.deft/core/` deposit that has not yet
318
+ * been stamped npm-managed. Local-only (no network).
319
+ */
320
+ export function checkCanonicalVendoredNpmSignpost(projectRoot, seams = {}) {
321
+ const readText = seams.readText ?? readTextSafe;
322
+ const isFile = seams.isFile ?? ((p) => readText(p) !== null);
323
+ const manifestPath = detectCanonicalVendoredManifest(projectRoot, isFile);
324
+ if (manifestPath === null) {
325
+ return {
326
+ name: "canonical-vendored-npm-signpost",
327
+ status: "skip",
328
+ detail: "No canonical-vendored .deft/core/ deposit (nothing to signpost).",
329
+ data: { canonical_vendored: false },
330
+ };
331
+ }
332
+ const text = readText(manifestPath);
333
+ if (text === null) {
334
+ return {
335
+ name: "canonical-vendored-npm-signpost",
336
+ status: "skip",
337
+ detail: "Canonical-vendored manifest unreadable.",
338
+ data: { canonical_vendored: true },
339
+ };
340
+ }
341
+ const manifest = parseInstallManifest(text);
342
+ if (isNpmManaged(manifest)) {
343
+ return {
344
+ name: "canonical-vendored-npm-signpost",
345
+ status: "skip",
346
+ detail: "Deposit is already npm-managed (hybrid).",
347
+ data: { canonical_vendored: true, npm_managed: true },
348
+ };
349
+ }
350
+ const detail = "Canonical-vendored install (.deft/core/) is not yet npm-managed. " +
351
+ "Post-freeze upgrades run via npm: install the engine with " +
352
+ `\`${CANONICAL_UPGRADE_COMMAND}\`, then run \`directive migrate\` ` +
353
+ `to stamp provenance. See ${UPGRADING_DOC_URL}.`;
354
+ return {
355
+ name: "canonical-vendored-npm-signpost",
356
+ status: "fail",
357
+ detail,
358
+ data: {
359
+ canonical_vendored: true,
360
+ npm_managed: false,
361
+ manifest_path: manifestPath,
362
+ sentinel_key: NPM_MANAGED_SENTINEL_KEY,
363
+ sentinel_value: NPM_MANAGED_SENTINEL_VALUE,
364
+ upgrading_doc_url: UPGRADING_DOC_URL,
365
+ },
366
+ };
367
+ }
315
368
  export function deriveExitCode(checks, errors) {
316
369
  if (errors.length > 0 || checks.some((c) => c.status === "error")) {
317
370
  return 2;
318
371
  }
319
- if (checks.some((c) => c.status === "fail")) {
372
+ if (checks.some((c) => c.status === "fail" && c.name !== "canonical-vendored-npm-signpost")) {
320
373
  return 1;
321
374
  }
322
375
  return 0;
@@ -349,6 +402,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
349
402
  });
350
403
  checks.push(checkManifestAgreement(projectRoot, null, seams));
351
404
  checks.push(checkLegacyLayout(projectRoot, seams));
405
+ checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
352
406
  return {
353
407
  projectRoot,
354
408
  installRoot: null,
@@ -362,6 +416,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
362
416
  checks.push(checkManifestAgreement(projectRoot, installRoot, seams));
363
417
  checks.push(checkInstallPathConsistency(projectRoot, installRoot, seams));
364
418
  checks.push(checkLegacyLayout(projectRoot, seams));
419
+ checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
365
420
  return {
366
421
  projectRoot,
367
422
  installRoot,
@@ -10,7 +10,9 @@ export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--re
10
10
  export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
11
11
  export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/agents-entry.md", "content/skills/deft-directive-build/SKILL.md"];
12
12
  export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
13
- export declare const CANONICAL_UPGRADE_COMMAND = "deft-install --yes --upgrade --repo-root . --json";
13
+ /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
14
+ export declare const CANONICAL_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
15
+ export declare const NPM_PACKAGE_NAME = "@deftai/directive";
14
16
  export declare const CLEAN_WINDOW_HOURS = 24;
15
17
  export declare const DIRTY_WINDOW_HOURS = 4;
16
18
  export declare const ENV_STATE_PATH = "DEFT_DOCTOR_STATE_PATH";
@@ -41,7 +41,9 @@ export const DEFT_REPO_POSITIVE_MARKERS = [
41
41
  // same check works for a source checkout (content/<dir>) and a flattened
42
42
  // consumer deposit (<dir>).
43
43
  export const EXPECTED_CONTENT_DIRS = ["languages", "strategies", "skills", "templates"];
44
- export const CANONICAL_UPGRADE_COMMAND = "deft-install --yes --upgrade --repo-root . --json";
44
+ /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
45
+ export const CANONICAL_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
46
+ export const NPM_PACKAGE_NAME = "@deftai/directive";
45
47
  export const CLEAN_WINDOW_HOURS = 24;
46
48
  export const DIRTY_WINDOW_HOURS = 4;
47
49
  export const ENV_STATE_PATH = "DEFT_DOCTOR_STATE_PATH";
@@ -10,6 +10,7 @@ import { pythonJsonDump } from "./json.js";
10
10
  import { createPlainSink } from "./output.js";
11
11
  import { resolveDefaultFrameworkRoot, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
12
12
  import { runPayloadStalenessCheck } from "./payload-staleness.js";
13
+ import { runLocalSignpostChecks } from "./signpost-checks.js";
13
14
  import { classifyTaskfileInclude, formatMissingIncludeSnippet, resolveConsumerTaskfile, } from "./taskfile.js";
14
15
  import { defaultWhich } from "./which.js";
15
16
  export function cmdDoctor(args, seams = {}) {
@@ -33,6 +34,11 @@ export function cmdDoctor(args, seams = {}) {
33
34
  const state = (seams.readState ?? readState)(projectRoot);
34
35
  const decision = decideThrottle(state, nowFn());
35
36
  if (decision.skip) {
37
+ const throttleFindings = [];
38
+ const throttleSink = createPlainSink({ jsonMode, quietMode });
39
+ runLocalSignpostChecks(projectRoot, throttleSink, (finding) => {
40
+ throttleFindings.push(finding);
41
+ }, seams);
36
42
  const hint = decision.dirty
37
43
  ? "run `deft doctor --full` to re-probe or address findings"
38
44
  : "--full forces";
@@ -45,12 +51,17 @@ export function cmdDoctor(args, seams = {}) {
45
51
  last_error_count: decision.lastErrorCount,
46
52
  next_eligible_at: formatIsoZ(decision.nextEligibleAt),
47
53
  hint,
54
+ ...(throttleFindings.length > 0 ? { signpost_findings: throttleFindings } : {}),
48
55
  };
49
56
  process.stdout.write(`${pythonJsonDump(payload)}\n`);
50
57
  }
51
58
  else {
52
59
  process.stdout.write(`${renderDoctorStatusLine(decision, nowFn())}\n`);
53
60
  }
61
+ const signpostWarnings = throttleFindings.filter((f) => f.severity === "warning").length;
62
+ if (signpostWarnings > 0 && !jsonMode) {
63
+ throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local layout / npm-migration note(s) above (throttle-skipped full probe).`);
64
+ }
54
65
  return decision.dirty ? 1 : 0;
55
66
  }
56
67
  }
@@ -239,6 +250,19 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
239
250
  sink.info(`${name}: skip -- ${detail}`);
240
251
  continue;
241
252
  }
253
+ if ((name === "legacy-layout" || name === "canonical-vendored-npm-signpost") &&
254
+ status === "fail") {
255
+ sink.warn(`${name}: ${detail}`);
256
+ addFinding({
257
+ severity: "warning",
258
+ message: detail || `${name} ${status}`,
259
+ check: `install-integrity:${name}`,
260
+ install_check: name,
261
+ status,
262
+ data: entry.data ?? {},
263
+ });
264
+ continue;
265
+ }
242
266
  if (status === "error") {
243
267
  sink.error(`${name}: error -- ${detail}`);
244
268
  }
@@ -8,6 +8,10 @@ export interface PayloadStalenessSeams {
8
8
  ok: boolean;
9
9
  stdout: string;
10
10
  };
11
+ readonly runNpmViewVersion?: () => {
12
+ ok: boolean;
13
+ version: string;
14
+ };
11
15
  }
12
16
  export declare function runPayloadStalenessCheck(projectRoot: string, sink: OutputSink, addFinding: (finding: Finding) => void, seams?: PayloadStalenessSeams): void;
13
17
  //# sourceMappingURL=payload-staleness.d.ts.map
@@ -1,13 +1,13 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { dirname, join } from "node:path";
3
- import { CANONICAL_UPGRADE_COMMAND } from "./constants.js";
3
+ import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME } from "./constants.js";
4
4
  import { locateManifest, parseInstallManifest } from "./manifest.js";
5
5
  import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
6
6
  function isDeftFrameworkRepo(projectRoot, readText = readTextSafe) {
7
7
  try {
8
8
  const agents = join(projectRoot, "AGENTS.md");
9
9
  const text = readText(agents);
10
- return text !== null && text.includes("Deft — Development Framework (deft repo)");
10
+ return text?.includes("Deft — Development Framework (deft repo)") ?? false;
11
11
  }
12
12
  catch {
13
13
  return false;
@@ -37,6 +37,74 @@ function parseRemoteSha(stdout) {
37
37
  const firstLine = stdout.split("\n").find((ln) => ln.trim()) ?? "";
38
38
  return firstLine.trim().split(/\s+/)[0] ?? "";
39
39
  }
40
+ function parseSemver(version) {
41
+ const normalized = version.trim().replace(/^v/i, "");
42
+ const parts = [];
43
+ for (const segment of normalized.split(".")) {
44
+ const numeric = Number.parseInt(segment.split("-")[0] ?? "", 10);
45
+ if (Number.isNaN(numeric)) {
46
+ break;
47
+ }
48
+ parts.push(numeric);
49
+ }
50
+ return parts.length > 0 ? parts : [0];
51
+ }
52
+ function semverLessThan(left, right) {
53
+ const a = parseSemver(left);
54
+ const b = parseSemver(right);
55
+ const len = Math.max(a.length, b.length);
56
+ for (let i = 0; i < len; i += 1) {
57
+ const av = a[i] ?? 0;
58
+ const bv = b[i] ?? 0;
59
+ if (av < bv) {
60
+ return true;
61
+ }
62
+ if (av > bv) {
63
+ return false;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+ function manifestVersion(ref, tag) {
69
+ const candidate = (tag || ref).trim().replace(/^refs\/tags\//, "");
70
+ const normalized = candidate.replace(/^v/i, "");
71
+ if (!/^\d+(?:\.\d+)*/.test(normalized)) {
72
+ return "";
73
+ }
74
+ return normalized;
75
+ }
76
+ function defaultNpmViewVersion() {
77
+ const proc = spawnSync("npm", ["view", NPM_PACKAGE_NAME, "version"], {
78
+ encoding: "utf8",
79
+ timeout: 15_000,
80
+ });
81
+ const version = (proc.stdout ?? "").trim().split("\n")[0]?.trim() ?? "";
82
+ return { ok: proc.status === 0 && version.length > 0, version };
83
+ }
84
+ function emitUnverified(checkName, reason, sink, addFinding) {
85
+ const msg = `payload currency UNVERIFIED — ${reason}`;
86
+ sink.warn(msg);
87
+ addFinding({
88
+ severity: "warning",
89
+ message: msg,
90
+ check: checkName,
91
+ status: "unverified",
92
+ });
93
+ }
94
+ function emitStale(checkName, installedLabel, remoteLabel, ref, sink, addFinding, extras = {}, behindWord = "remote") {
95
+ const msg = `Framework payload is stale (installed ${installedLabel} behind ${behindWord} ${remoteLabel} for ref '${ref}'). ` +
96
+ `Recommendation: run \`${CANONICAL_UPGRADE_COMMAND}\` from any shell with Node ≥ 20.`;
97
+ sink.warn(msg);
98
+ addFinding({
99
+ severity: "warning",
100
+ message: msg,
101
+ check: checkName,
102
+ status: "stale",
103
+ ref,
104
+ suggestion: CANONICAL_UPGRADE_COMMAND,
105
+ ...extras,
106
+ });
107
+ }
40
108
  export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams = {}) {
41
109
  const checkName = "payload-staleness";
42
110
  const readText = seams.readText ?? readTextSafe;
@@ -48,6 +116,7 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
48
116
  message: "inside framework repo (no install manifest)",
49
117
  check: checkName,
50
118
  status: "skip",
119
+ reason: "not-applicable",
51
120
  });
52
121
  return;
53
122
  }
@@ -64,7 +133,13 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
64
133
  }
65
134
  if (manifestPath === null || !isFile(manifestPath)) {
66
135
  sink.info(`${checkName}: skip -- no install manifest found (pre-v0.28 or legacy state)`);
67
- addFinding({ severity: "skip", message: "no manifest", check: checkName, status: "skip" });
136
+ addFinding({
137
+ severity: "skip",
138
+ message: "no manifest",
139
+ check: checkName,
140
+ status: "skip",
141
+ reason: "not-applicable",
142
+ });
68
143
  return;
69
144
  }
70
145
  const text = readText(manifestPath);
@@ -75,12 +150,14 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
75
150
  message: "manifest unreadable",
76
151
  check: checkName,
77
152
  status: "skip",
153
+ reason: "not-applicable",
78
154
  });
79
155
  return;
80
156
  }
81
157
  const manifest = parseInstallManifest(text);
82
158
  const installedSha = (manifest.sha ?? "").trim();
83
159
  const ref = (manifest.ref ?? manifest.tag ?? "").trim();
160
+ const tag = (manifest.tag ?? "").trim();
84
161
  if (!installedSha) {
85
162
  sink.info(`${checkName}: skip -- manifest has no sha (incomplete provenance)`);
86
163
  addFinding({
@@ -88,6 +165,7 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
88
165
  message: "no sha in manifest",
89
166
  check: checkName,
90
167
  status: "skip",
168
+ reason: "not-applicable",
91
169
  });
92
170
  return;
93
171
  }
@@ -98,6 +176,7 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
98
176
  message: "no ref/tag in manifest",
99
177
  check: checkName,
100
178
  status: "skip",
179
+ reason: "not-applicable",
101
180
  });
102
181
  return;
103
182
  }
@@ -110,56 +189,47 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
110
189
  });
111
190
  return { ok: proc.status === 0, stdout: proc.stdout ?? "" };
112
191
  });
192
+ const runNpmView = seams.runNpmViewVersion ?? defaultNpmViewVersion;
113
193
  let remoteResult;
114
194
  try {
115
195
  remoteResult = runLsRemote(deftDir, ref);
116
196
  }
117
197
  catch {
118
- sink.info(`${checkName}: skip -- could not probe remote (Error)`);
119
- addFinding({
120
- severity: "skip",
121
- message: "remote probe failed",
122
- check: checkName,
123
- status: "skip",
124
- });
125
- return;
126
- }
127
- if (!remoteResult.ok) {
128
- sink.info(`${checkName}: skip -- git ls-remote failed (no network or no origin)`);
129
- addFinding({
130
- severity: "skip",
131
- message: "ls-remote unavailable",
132
- check: checkName,
133
- status: "skip",
134
- });
135
- return;
136
- }
137
- const remoteSha = parseRemoteSha(remoteResult.stdout);
138
- if (!remoteSha) {
139
- sink.info(`${checkName}: skip -- ls-remote produced no sha`);
140
- addFinding({
141
- severity: "skip",
142
- message: "no remote sha",
143
- check: checkName,
144
- status: "skip",
145
- });
146
- return;
198
+ remoteResult = { ok: false, stdout: "" };
199
+ }
200
+ if (remoteResult.ok) {
201
+ const remoteSha = parseRemoteSha(remoteResult.stdout);
202
+ if (remoteSha) {
203
+ if (installedSha === remoteSha) {
204
+ sink.info(`${checkName}: current (sha matches remote)`);
205
+ return;
206
+ }
207
+ emitStale(checkName, `sha ${installedSha.slice(0, 8)}...`, `sha ${remoteSha.slice(0, 8)}...`, ref, sink, addFinding, { installed_sha: installedSha, remote_sha: remoteSha, resolver: "git-ls-remote" });
208
+ return;
209
+ }
147
210
  }
148
- if (installedSha === remoteSha) {
149
- sink.info(`${checkName}: current (sha matches remote)`);
211
+ const npmResult = runNpmView();
212
+ const installedVersion = manifestVersion(ref, tag);
213
+ if (npmResult.ok && installedVersion) {
214
+ if (semverLessThan(installedVersion, npmResult.version)) {
215
+ emitStale(checkName, `v${installedVersion}`, `v${npmResult.version}`, ref, sink, addFinding, {
216
+ installed_version: installedVersion,
217
+ remote_version: npmResult.version,
218
+ resolver: "npm-view",
219
+ }, "npm registry");
220
+ return;
221
+ }
222
+ if (installedVersion === npmResult.version.replace(/^v/i, "")) {
223
+ sink.info(`${checkName}: current (version matches npm registry)`);
224
+ return;
225
+ }
226
+ sink.info(`${checkName}: current (installed version >= npm registry)`);
150
227
  return;
151
228
  }
152
- const msg = `Framework payload is stale (installed sha ${installedSha.slice(0, 8)}... behind remote ${remoteSha.slice(0, 8)}... for ref '${ref}'). Recommendation: run the canonical headless upgrader \`${CANONICAL_UPGRADE_COMMAND}\` from your project root to pull the latest payload (drop \`--json\` for human-readable output). On an installer binary predating the headless flags, download the latest deft-install from GitHub Releases first.`;
153
- sink.warn(msg);
154
- addFinding({
155
- severity: "warning",
156
- message: msg,
157
- check: checkName,
158
- status: "stale",
159
- installed_sha: installedSha,
160
- remote_sha: remoteSha,
161
- ref,
162
- suggestion: CANONICAL_UPGRADE_COMMAND,
163
- });
229
+ const reason = remoteResult.ok
230
+ ? "ls-remote produced no sha and npm registry fallback unavailable"
231
+ : "could not reach remote (git ls-remote / npm view both unavailable)";
232
+ sink.info(`${checkName}: skip -- ${reason}`);
233
+ emitUnverified(checkName, reason, sink, addFinding);
164
234
  }
165
235
  //# sourceMappingURL=payload-staleness.js.map
@@ -0,0 +1,9 @@
1
+ import { type CheckSeams } from "./checks.js";
2
+ import type { OutputSink } from "./output.js";
3
+ import type { Finding } from "./types.js";
4
+ export interface LocalSignpostSeams extends CheckSeams {
5
+ readonly runningInsideDeftRepo?: (root: string) => boolean;
6
+ }
7
+ /** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
8
+ export declare function runLocalSignpostChecks(projectRoot: string, sink: OutputSink, addFinding: (finding: Finding) => void, seams?: LocalSignpostSeams): void;
9
+ //# sourceMappingURL=signpost-checks.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { checkCanonicalVendoredNpmSignpost, checkLegacyLayout } from "./checks.js";
2
+ import { runningInsideDeftRepo } from "./paths.js";
3
+ /** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
4
+ export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}) {
5
+ const insideDeft = seams.runningInsideDeftRepo?.(projectRoot) ?? runningInsideDeftRepo(projectRoot, seams);
6
+ if (insideDeft) {
7
+ return;
8
+ }
9
+ for (const result of [
10
+ checkLegacyLayout(projectRoot, seams),
11
+ checkCanonicalVendoredNpmSignpost(projectRoot, seams),
12
+ ]) {
13
+ if (result.status === "skip") {
14
+ continue;
15
+ }
16
+ if (result.status === "fail") {
17
+ sink.warn(result.detail);
18
+ addFinding({
19
+ severity: "warning",
20
+ message: result.detail,
21
+ check: result.name,
22
+ status: result.status,
23
+ data: result.data ?? {},
24
+ });
25
+ }
26
+ }
27
+ }
28
+ //# sourceMappingURL=signpost-checks.js.map
@@ -16,6 +16,10 @@ export declare const NPM_E2E_REHEARSAL_TAG = "e2e-rehearsal";
16
16
  export declare const NPM_INSTALL_TIMEOUT_SECONDS = 600;
17
17
  export declare const NPM_BUILD_TIMEOUT_SECONDS = 600;
18
18
  export declare const NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS = 180;
19
+ export declare const NPM_PACK_TIMEOUT_SECONDS = 300;
20
+ export declare const NPM_INSTALL_RUN_TIMEOUT_SECONDS = 300;
21
+ /** Fail the install+run smoke when stderr/stdout carries a module-resolution error. */
22
+ export declare const MODULE_NOT_FOUND_MARKERS: readonly ["Cannot find module", "ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"];
19
23
  export declare const RELEASE_ENTRYPOINT_TIMEOUT_SECONDS = 600;
20
24
  export declare const ROLLBACK_ENTRYPOINT_TIMEOUT_SECONDS = 300;
21
25
  export declare const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
@@ -16,6 +16,14 @@ export const NPM_E2E_REHEARSAL_TAG = "e2e-rehearsal";
16
16
  export const NPM_INSTALL_TIMEOUT_SECONDS = 600;
17
17
  export const NPM_BUILD_TIMEOUT_SECONDS = 600;
18
18
  export const NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS = 180;
19
+ export const NPM_PACK_TIMEOUT_SECONDS = 300;
20
+ export const NPM_INSTALL_RUN_TIMEOUT_SECONDS = 300;
21
+ /** Fail the install+run smoke when stderr/stdout carries a module-resolution error. */
22
+ export const MODULE_NOT_FOUND_MARKERS = [
23
+ "Cannot find module",
24
+ "ERR_MODULE_NOT_FOUND",
25
+ "MODULE_NOT_FOUND",
26
+ ];
19
27
  export const RELEASE_ENTRYPOINT_TIMEOUT_SECONDS = 600.0;
20
28
  export const ROLLBACK_ENTRYPOINT_TIMEOUT_SECONDS = 300.0;
21
29
  export const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
@@ -28,4 +28,17 @@ export declare function alignNpmPackageVersions(cloneDir: string, version: strin
28
28
  * Returns [ok, reason] like verifyDraftRelease / verifyTag.
29
29
  */
30
30
  export declare function rehearseNpmPublish(cloneDir: string, version: string, seams?: E2ESeams): [boolean, string];
31
+ /**
32
+ * Publish-layout install+run smoke (#1996, #2010): pack the four
33
+ * @deftai/directive* packages, install the tarballs into a clean flat
34
+ * node_modules, then run `directive --version` (exit-0 liveness) and
35
+ * `directive doctor` (deep-import coverage) so import-resolution bugs like
36
+ * #1993 sub-problem 1 surface before a real npm publish. The smoke gates on
37
+ * module-not-found markers, NOT on the doctor's pass/fail verdict -- a full
38
+ * doctor check exits non-zero in a bare consumer layout, which is benign
39
+ * (#2010).
40
+ */
41
+ export declare function rehearseNpmInstallAndRun(cloneDir: string, version: string, seams?: E2ESeams, options?: {
42
+ skipWorkspacePrep?: boolean;
43
+ }): [boolean, string];
31
44
  //# sourceMappingURL=npm-ops.d.ts.map
@@ -1,7 +1,7 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { defaultWhich, spawnText } from "../release/spawn.js";
4
- import { NPM_BUILD_TIMEOUT_SECONDS, NPM_E2E_REHEARSAL_TAG, NPM_INSTALL_TIMEOUT_SECONDS, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, NPM_PUBLISH_PACKAGES, } from "./constants.js";
4
+ import { MODULE_NOT_FOUND_MARKERS, NPM_BUILD_TIMEOUT_SECONDS, NPM_E2E_REHEARSAL_TAG, NPM_INSTALL_RUN_TIMEOUT_SECONDS, NPM_INSTALL_TIMEOUT_SECONDS, NPM_PACK_TIMEOUT_SECONDS, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, NPM_PUBLISH_PACKAGES, } from "./constants.js";
5
5
  /**
6
6
  * npm publish dry-run rehearsal (#1910) -- the TS port of the
7
7
  * scripts/release_e2e.py helpers of the same name. Mirrors
@@ -148,4 +148,122 @@ export function rehearseNpmPublish(cloneDir, version, seams = {}) {
148
148
  `(types -> core -> content -> cli) at v${version}`,
149
149
  ];
150
150
  }
151
+ function moduleResolutionFailure(output) {
152
+ for (const marker of MODULE_NOT_FOUND_MARKERS) {
153
+ if (output.includes(marker))
154
+ return marker;
155
+ }
156
+ return null;
157
+ }
158
+ /**
159
+ * Publish-layout install+run smoke (#1996, #2010): pack the four
160
+ * @deftai/directive* packages, install the tarballs into a clean flat
161
+ * node_modules, then run `directive --version` (exit-0 liveness) and
162
+ * `directive doctor` (deep-import coverage) so import-resolution bugs like
163
+ * #1993 sub-problem 1 surface before a real npm publish. The smoke gates on
164
+ * module-not-found markers, NOT on the doctor's pass/fail verdict -- a full
165
+ * doctor check exits non-zero in a bare consumer layout, which is benign
166
+ * (#2010).
167
+ */
168
+ export function rehearseNpmInstallAndRun(cloneDir, version, seams = {}, options = {}) {
169
+ const which = resolveWhich(seams);
170
+ const npmPath = which("npm");
171
+ if (npmPath === null) {
172
+ return [true, "SKIP (npm not on PATH; Node-less operator)"];
173
+ }
174
+ const pnpmCmd = resolvePnpm(seams);
175
+ if (pnpmCmd === null) {
176
+ return [
177
+ false,
178
+ "npm present but neither pnpm nor corepack is on PATH -- " +
179
+ "cannot build the workspace for install+run smoke",
180
+ ];
181
+ }
182
+ const env = { ...process.env, DEFT_PROJECT_ROOT: cloneDir };
183
+ let ok = true;
184
+ let reason = "";
185
+ if (!options.skipWorkspacePrep) {
186
+ let [ok, reason] = runNpmStep([...pnpmCmd, "install", "--frozen-lockfile"], cloneDir, env, "pnpm install", NPM_INSTALL_TIMEOUT_SECONDS, seams);
187
+ if (!ok)
188
+ return [false, reason];
189
+ [ok, reason] = runNpmStep([...pnpmCmd, "-w", "run", "build"], cloneDir, env, "pnpm build", NPM_BUILD_TIMEOUT_SECONDS, seams);
190
+ if (!ok)
191
+ return [false, reason];
192
+ [ok, reason] = alignNpmPackageVersions(cloneDir, version);
193
+ if (!ok)
194
+ return [false, reason];
195
+ }
196
+ const packDir = join(cloneDir, ".deft-e2e-packs");
197
+ mkdirSync(packDir, { recursive: true });
198
+ const tgzPaths = [];
199
+ for (const pkg of NPM_PUBLISH_PACKAGES) {
200
+ const pkgDir = join(cloneDir, "packages", pkg);
201
+ [ok, reason] = runNpmStep([npmPath, "pack", "--pack-destination", packDir], pkgDir, env, `npm pack packages/${pkg}`, NPM_PACK_TIMEOUT_SECONDS, seams);
202
+ if (!ok)
203
+ return [false, reason];
204
+ const manifest = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
205
+ if (manifest === null ||
206
+ typeof manifest.name !== "string" ||
207
+ typeof manifest.version !== "string") {
208
+ return [false, `version-align FAIL: invalid manifest in packages/${pkg}`];
209
+ }
210
+ const scoped = manifest.name.replaceAll("@", "").replaceAll("/", "-");
211
+ tgzPaths.push(join(packDir, `${scoped}-${manifest.version}.tgz`));
212
+ }
213
+ const consumerDir = join(cloneDir, ".deft-e2e-consumer");
214
+ mkdirSync(consumerDir, { recursive: true });
215
+ writeFileSync(join(consumerDir, "package.json"), `${JSON.stringify({ name: "deft-e2e-consumer", private: true, version: "0.0.0" }, null, 2)}\n`, "utf8");
216
+ const installArgs = [npmPath, "install", ...tgzPaths];
217
+ [ok, reason] = runNpmStep(installArgs, consumerDir, env, "npm install packed tarballs", NPM_INSTALL_RUN_TIMEOUT_SECONDS, seams);
218
+ if (!ok)
219
+ return [false, reason];
220
+ const spawn = seams.spawnText ?? spawnText;
221
+ const cliBin = join(consumerDir, "node_modules", "@deftai", "directive", "dist", "bin.js");
222
+ // Liveness probe (#2010): `--version` loads the cli + core engine via
223
+ // engineInfo(), exercising the cross-package import path that #1993 broke,
224
+ // and reliably exits 0 on a healthy install. This is the clean exit-0 gate;
225
+ // gating the smoke on the doctor's exit code instead is a false positive
226
+ // because a full doctor check exits non-zero in a bare consumer layout.
227
+ const versionRun = spawn(process.execPath, [cliBin, "--version"], {
228
+ cwd: consumerDir,
229
+ env,
230
+ timeoutMs: NPM_INSTALL_RUN_TIMEOUT_SECONDS * 1000,
231
+ });
232
+ const versionOut = `${versionRun.stdout ?? ""}\n${versionRun.stderr ?? ""}`;
233
+ let resolutionHit = moduleResolutionFailure(versionOut);
234
+ if (resolutionHit !== null) {
235
+ return [
236
+ false,
237
+ `install+run smoke: module resolution error on --version (${resolutionHit}): ${versionOut.trim().slice(-800)}`,
238
+ ];
239
+ }
240
+ if (versionRun.status !== 0) {
241
+ return [
242
+ false,
243
+ `install+run smoke: directive --version exited ${versionRun.status}: ${versionOut.trim().slice(-500)}`,
244
+ ];
245
+ }
246
+ // Deep-import probe (#2010): run the doctor verb to exercise the deeper
247
+ // cross-package import graph that #1993 sub-problem 1 broke. The doctor
248
+ // legitimately exits non-zero in a bare consumer layout (e.g. no root
249
+ // Taskfile.yml), so gate ONLY on the module-not-found markers, NOT on the
250
+ // doctor's pass/fail verdict.
251
+ const doctorRun = spawn(process.execPath, [cliBin, "doctor"], {
252
+ cwd: consumerDir,
253
+ env,
254
+ timeoutMs: NPM_INSTALL_RUN_TIMEOUT_SECONDS * 1000,
255
+ });
256
+ const doctorOut = `${doctorRun.stdout ?? ""}\n${doctorRun.stderr ?? ""}`;
257
+ resolutionHit = moduleResolutionFailure(doctorOut);
258
+ if (resolutionHit !== null) {
259
+ return [
260
+ false,
261
+ `install+run smoke: module resolution error on doctor (${resolutionHit}): ${doctorOut.trim().slice(-800)}`,
262
+ ];
263
+ }
264
+ return [
265
+ true,
266
+ `packed + installed 4 packages at v${version}; ran directive --version (exit 0) + doctor without module-not-found`,
267
+ ];
268
+ }
151
269
  //# sourceMappingURL=npm-ops.js.map
@@ -6,7 +6,7 @@ import { dispatchTaskRelease, dispatchTaskReleaseRollback } from "./entrypoint.j
6
6
  import { emit } from "./flags.js";
7
7
  import { verifyDraftRelease } from "./gh-ops.js";
8
8
  import { cloneRepoToTemp, pushMirror, setOriginToTempRepo, verifyTag } from "./git-ops.js";
9
- import { rehearseNpmPublish } from "./npm-ops.js";
9
+ import { rehearseNpmInstallAndRun, rehearseNpmPublish } from "./npm-ops.js";
10
10
  export function runRehearsal(owner, slug, projectRoot, version = REHEARSAL_VERSION, seams = {}, skipNpm = false) {
11
11
  const repoFull = `${owner}/${slug}`;
12
12
  const mkdtemp = seams.mkdtemp ?? ((prefix) => mkdtempSync(join(tmpdir(), prefix)));
@@ -24,6 +24,10 @@ export function runRehearsal(owner, slug, projectRoot, version = REHEARSAL_VERSI
24
24
  ];
25
25
  if (!skipNpm) {
26
26
  steps.push(["npm publish dry-run", () => rehearseNpmPublish(cloneDir, version, seams)]);
27
+ steps.push([
28
+ "npm install+run smoke",
29
+ () => rehearseNpmInstallAndRun(cloneDir, version, seams, { skipWorkspacePrep: true }),
30
+ ]);
27
31
  }
28
32
  steps.push([
29
33
  "task release:rollback",
@@ -36,7 +40,9 @@ export function runRehearsal(owner, slug, projectRoot, version = REHEARSAL_VERSI
36
40
  return [false, `${label}: ${reason}`];
37
41
  }
38
42
  }
39
- const npmNote = skipNpm ? " (npm dry-run skipped)" : " -> npm publish dry-run";
43
+ const npmNote = skipNpm
44
+ ? " (npm dry-run skipped)"
45
+ : " -> npm publish dry-run -> npm install+run smoke";
40
46
  return [
41
47
  true,
42
48
  `pipeline-mirror rehearsal succeeded against ${repoFull} ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.56.2",
3
+ "version": "0.57.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -209,8 +209,8 @@
209
209
  "provenance": true
210
210
  },
211
211
  "dependencies": {
212
- "@deftai/directive-content": "^0.56.2",
213
- "@deftai/directive-types": "^0.56.2"
212
+ "@deftai/directive-content": "^0.57.0",
213
+ "@deftai/directive-types": "^0.57.0"
214
214
  },
215
215
  "scripts": {
216
216
  "build": "tsc -b",