@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.
- package/dist/doctor/checks.d.ts +5 -0
- package/dist/doctor/checks.js +58 -3
- package/dist/doctor/constants.d.ts +3 -1
- package/dist/doctor/constants.js +3 -1
- package/dist/doctor/main.js +24 -0
- package/dist/doctor/payload-staleness.d.ts +4 -0
- package/dist/doctor/payload-staleness.js +116 -46
- package/dist/doctor/signpost-checks.d.ts +9 -0
- package/dist/doctor/signpost-checks.js +28 -0
- package/dist/release-e2e/constants.d.ts +4 -0
- package/dist/release-e2e/constants.js +8 -0
- package/dist/release-e2e/npm-ops.d.ts +13 -0
- package/dist/release-e2e/npm-ops.js +120 -2
- package/dist/release-e2e/rehearsal.js +8 -2
- package/package.json +3 -3
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -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;
|
package/dist/doctor/checks.js
CHANGED
|
@@ -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
|
-
|
|
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";
|
package/dist/doctor/constants.js
CHANGED
|
@@ -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
|
-
|
|
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";
|
package/dist/doctor/main.js
CHANGED
|
@@ -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
|
|
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({
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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.
|
|
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.
|
|
213
|
-
"@deftai/directive-types": "^0.
|
|
212
|
+
"@deftai/directive-content": "^0.57.0",
|
|
213
|
+
"@deftai/directive-types": "^0.57.0"
|
|
214
214
|
},
|
|
215
215
|
"scripts": {
|
|
216
216
|
"build": "tsc -b",
|