@deftai/directive-core 0.68.1 → 0.70.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/deposit/contain.d.ts +41 -0
- package/dist/deposit/contain.js +113 -0
- package/dist/deposit/copy-tree.js +8 -2
- package/dist/doctor/checks.d.ts +11 -0
- package/dist/doctor/checks.js +75 -2
- package/dist/doctor/constants.d.ts +9 -0
- package/dist/doctor/constants.js +11 -0
- package/dist/doctor/main.d.ts +47 -1
- package/dist/doctor/main.js +406 -3
- package/dist/doctor/manifest.d.ts +20 -0
- package/dist/doctor/manifest.js +22 -0
- package/dist/doctor/payload-staleness.d.ts +8 -0
- package/dist/doctor/payload-staleness.js +11 -8
- package/dist/doctor/types.d.ts +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.d.ts +18 -0
- package/dist/init-deposit/gitignore.js +94 -15
- package/dist/init-deposit/headless-manifest.d.ts +87 -0
- package/dist/init-deposit/headless-manifest.js +261 -0
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -0
- package/dist/init-deposit/init-deposit.js +6 -0
- package/dist/init-deposit/init-dispatch.d.ts +90 -0
- package/dist/init-deposit/init-dispatch.js +170 -0
- package/dist/init-deposit/refresh.d.ts +79 -2
- package/dist/init-deposit/refresh.js +182 -5
- package/dist/init-deposit/scaffold.d.ts +25 -0
- package/dist/init-deposit/scaffold.js +57 -0
- package/dist/init-deposit/untrack-core.d.ts +84 -0
- package/dist/init-deposit/untrack-core.js +150 -0
- package/dist/intake/issue-ingest.d.ts +11 -0
- package/dist/intake/issue-ingest.js +86 -11
- package/dist/policy/plan-extensions.d.ts +31 -0
- package/dist/policy/plan-extensions.js +45 -0
- package/dist/policy/wip.d.ts +2 -2
- package/dist/policy/wip.js +2 -2
- package/dist/resolution/classify.d.ts +41 -0
- package/dist/resolution/classify.js +138 -0
- package/dist/resolution/engine-ladder.d.ts +98 -0
- package/dist/resolution/engine-ladder.js +185 -0
- package/dist/resolution/index.d.ts +19 -0
- package/dist/resolution/index.js +19 -0
- package/dist/resolution/integrity.d.ts +48 -0
- package/dist/resolution/integrity.js +80 -0
- package/dist/resolution/package-manager.d.ts +77 -0
- package/dist/resolution/package-manager.js +103 -0
- package/dist/resolution/pin.d.ts +73 -0
- package/dist/resolution/pin.js +169 -0
- package/dist/resolution/plan.d.ts +66 -0
- package/dist/resolution/plan.js +219 -0
- package/dist/resolution/skew-policy.d.ts +46 -0
- package/dist/resolution/skew-policy.js +120 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +28 -1
- package/dist/swarm/routing.js +10 -3
- package/dist/triage/bulk/index.d.ts +11 -2
- package/dist/triage/bulk/index.js +41 -4
- package/dist/triage/help/registry-data.d.ts +5 -5
- package/dist/triage/help/registry-data.js +18 -5
- package/dist/triage/scope/cli.d.ts +2 -1
- package/dist/triage/scope/cli.js +30 -6
- package/dist/triage/scope/mutations.d.ts +10 -0
- package/dist/triage/scope/mutations.js +22 -0
- package/dist/triage/welcome/constants.d.ts +1 -2
- package/dist/triage/welcome/constants.js +1 -2
- package/dist/triage/welcome/index.d.ts +1 -0
- package/dist/triage/welcome/index.js +1 -0
- package/dist/triage/welcome/onboard.d.ts +35 -0
- package/dist/triage/welcome/onboard.js +94 -0
- package/dist/umbrella-current-shape/index.d.ts +25 -1
- package/dist/umbrella-current-shape/index.js +84 -4
- package/dist/user-config/index.d.ts +2 -0
- package/dist/user-config/index.js +2 -0
- package/dist/user-config/resolve-user-md.d.ts +76 -0
- package/dist/user-config/resolve-user-md.js +120 -0
- package/dist/vbrief-build/project-definition-io.d.ts +6 -0
- package/dist/vbrief-build/project-definition-io.js +7 -2
- package/dist/vbrief-validate/precutover.js +7 -3
- package/dist/xbrief-migrate/constants.d.ts +10 -0
- package/dist/xbrief-migrate/constants.js +19 -0
- package/dist/xbrief-migrate/detect.d.ts +34 -0
- package/dist/xbrief-migrate/detect.js +33 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +13 -0
- package/dist/xbrief-migrate/fs-helpers.js +46 -1
- package/dist/xbrief-migrate/index.d.ts +4 -2
- package/dist/xbrief-migrate/index.js +4 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +23 -1
- package/dist/xbrief-migrate/migrate-project.js +97 -8
- package/dist/xbrief-migrate/signpost.d.ts +7 -1
- package/dist/xbrief-migrate/signpost.js +18 -3
- package/package.json +7 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-manager abstraction for the resolution spine (#2197, from #2264).
|
|
3
|
+
*
|
|
4
|
+
* The documented install/upgrade path historically hard-coded npm
|
|
5
|
+
* (`npm i -g @deftai/directive`). pnpm-managed setups have no first-class
|
|
6
|
+
* flow, and mixing an npm global into a pnpm environment breaks PATH/shim/store
|
|
7
|
+
* consistency. This module makes the *command rendering* package-manager aware
|
|
8
|
+
* so the resolution plan and doctor emit the right form for the active manager.
|
|
9
|
+
*
|
|
10
|
+
* Two load-bearing facts keep this small (locked on issue #2197):
|
|
11
|
+
* 1. NO additional registry. pnpm resolves from the same npm registry
|
|
12
|
+
* (`registry.npmjs.org`) by default; the published `@deftai/directive`
|
|
13
|
+
* tarball is unchanged. No renderer here ever emits a `--registry` flag.
|
|
14
|
+
* 2. The internal `.deft/.cli/<platform>` sandbox vendoring STAYS on npm
|
|
15
|
+
* (its `node_modules/.bin` layout is validated by `integrity.ts` and is
|
|
16
|
+
* gitignored / package-manager-invisible). Only the global, project-local,
|
|
17
|
+
* ephemeral, and upgrade command forms vary by package manager.
|
|
18
|
+
*
|
|
19
|
+
* Every function here is PURE (no I/O): detection consumes an injected fact-set
|
|
20
|
+
* so the whole surface is unit-testable without touching the filesystem or env.
|
|
21
|
+
*/
|
|
22
|
+
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
23
|
+
export const DEFAULT_PACKAGE_MANAGER = "npm";
|
|
24
|
+
/** Canonical published engine package name. */
|
|
25
|
+
export const ENGINE_PACKAGE = "@deftai/directive";
|
|
26
|
+
function normalizePackageManager(value) {
|
|
27
|
+
const v = value.trim().toLowerCase();
|
|
28
|
+
if (v.startsWith("pnpm"))
|
|
29
|
+
return "pnpm";
|
|
30
|
+
if (v.startsWith("npm"))
|
|
31
|
+
return "npm";
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Detect the active package manager. Precedence (first match wins):
|
|
36
|
+
* 1. `DEFT_PACKAGE_MANAGER` env override
|
|
37
|
+
* 2. `packageManager` field / Corepack shim
|
|
38
|
+
* 3. `pnpm-lock.yaml` present
|
|
39
|
+
* 4. `npm_config_user_agent` (set by the manager that spawned the process)
|
|
40
|
+
* 5. default: npm
|
|
41
|
+
*/
|
|
42
|
+
export function detectPackageManager(input = {}) {
|
|
43
|
+
const env = input.env ?? {};
|
|
44
|
+
const override = env.DEFT_PACKAGE_MANAGER;
|
|
45
|
+
if (typeof override === "string" && override.trim() !== "") {
|
|
46
|
+
const pm = normalizePackageManager(override);
|
|
47
|
+
if (pm)
|
|
48
|
+
return pm;
|
|
49
|
+
}
|
|
50
|
+
if (input.packageManagerField != null && input.packageManagerField.trim() !== "") {
|
|
51
|
+
const pm = normalizePackageManager(input.packageManagerField);
|
|
52
|
+
if (pm)
|
|
53
|
+
return pm;
|
|
54
|
+
}
|
|
55
|
+
if (input.pnpmLockPresent)
|
|
56
|
+
return "pnpm";
|
|
57
|
+
const ua = env.npm_config_user_agent;
|
|
58
|
+
if (typeof ua === "string" && ua.trim() !== "") {
|
|
59
|
+
const pm = normalizePackageManager(ua);
|
|
60
|
+
if (pm)
|
|
61
|
+
return pm;
|
|
62
|
+
}
|
|
63
|
+
return DEFAULT_PACKAGE_MANAGER;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render a global install command, e.g. `npm i -g @deftai/directive@0.65.0`
|
|
67
|
+
* (npm) or `pnpm add -g @deftai/directive@0.65.0` (pnpm). `spec` is the full
|
|
68
|
+
* package spec including any `@version` suffix.
|
|
69
|
+
*/
|
|
70
|
+
export function renderGlobalInstall(pm, spec = ENGINE_PACKAGE) {
|
|
71
|
+
return pm === "pnpm" ? `pnpm add -g ${spec}` : `npm i -g ${spec}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render a project-local (dev-dependency) install command. pnpm-managed repos
|
|
75
|
+
* that prefer not to install globally use this.
|
|
76
|
+
*/
|
|
77
|
+
export function renderProjectInstall(pm, spec = ENGINE_PACKAGE) {
|
|
78
|
+
return pm === "pnpm" ? `pnpm add -D ${spec}` : `npm install --save-dev ${spec}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Render an ephemeral (no-install) invocation, e.g. `npx @deftai/directive update`
|
|
82
|
+
* (npm) or `pnpm dlx @deftai/directive update` (pnpm).
|
|
83
|
+
*/
|
|
84
|
+
export function renderEphemeral(pm, subcommand, pkg = ENGINE_PACKAGE) {
|
|
85
|
+
const runner = pm === "pnpm" ? "pnpm dlx" : "npx";
|
|
86
|
+
const tail = subcommand.trim() === "" ? "" : ` ${subcommand.trim()}`;
|
|
87
|
+
return `${runner} ${pkg}${tail}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Render the full command matrix for a package manager at a given spec
|
|
91
|
+
* (defaults to `@latest`). Single-sourced so docs, doctor, and the plan all
|
|
92
|
+
* derive from the same renderers.
|
|
93
|
+
*/
|
|
94
|
+
export function renderPackageManagerCommands(pm, spec = `${ENGINE_PACKAGE}@latest`) {
|
|
95
|
+
return {
|
|
96
|
+
packageManager: pm,
|
|
97
|
+
globalInstall: renderGlobalInstall(pm, spec),
|
|
98
|
+
projectInstall: renderProjectInstall(pm, spec),
|
|
99
|
+
ephemeralUpdate: renderEphemeral(pm, "update"),
|
|
100
|
+
upgradeOneLiner: renderGlobalInstall(pm, `${ENGINE_PACKAGE}@latest`),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=package-manager.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version pin + reconciliation for the resolution spine (#2264, absorbs #2199).
|
|
3
|
+
*
|
|
4
|
+
* The committed `package.json` devDependency on `@deftai/directive` (exact,
|
|
5
|
+
* `"private": true`) is the CANONICAL pin — read before directive runs, npm-native
|
|
6
|
+
* so the engine ladder stays plain npm. This module reads that pin and reconciles
|
|
7
|
+
* it against the reachable engine version, the `.deft/core/VERSION` content marker,
|
|
8
|
+
* and the AGENTS.md managed-section `sha=`.
|
|
9
|
+
*
|
|
10
|
+
* Semver helpers here are the single comparison primitive reused by the skew
|
|
11
|
+
* policy and the engine ladder so version ordering is defined in exactly one place.
|
|
12
|
+
*/
|
|
13
|
+
/** The canonical committed dependency the pin lives on. */
|
|
14
|
+
export declare const PIN_DEPENDENCY_NAME = "@deftai/directive";
|
|
15
|
+
/** A parsed semver triple (prerelease / build metadata dropped). */
|
|
16
|
+
export type SemverTriple = readonly [number, number, number];
|
|
17
|
+
/** Parse a semver prefix (`major.minor.patch`); null when not exact semver. */
|
|
18
|
+
export declare function parseSemver(version: string | null | undefined): SemverTriple | null;
|
|
19
|
+
/** True when `version` is an EXACT pin (no range operators like `^` / `~` / `*`). */
|
|
20
|
+
export declare function isExactPin(version: string | null | undefined): boolean;
|
|
21
|
+
/** Compare two versions: -1 (a<b), 0 (a==b), 1 (a>b); null when either is unparseable. */
|
|
22
|
+
export declare function compareSemver(a: string | null, b: string | null): -1 | 0 | 1 | null;
|
|
23
|
+
/** True when `a >= b` under semver ordering; false when either is unparseable. */
|
|
24
|
+
export declare function semverGte(a: string | null, b: string | null): boolean;
|
|
25
|
+
export interface PinReadSeams {
|
|
26
|
+
readonly isFile?: (p: string) => boolean;
|
|
27
|
+
readonly readText?: (p: string) => string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface PinReadResult {
|
|
30
|
+
/** Exact pinned version (no `v` prefix), or null when absent / non-exact. */
|
|
31
|
+
readonly pinVersion: string | null;
|
|
32
|
+
/** Raw devDependency spec string as written, or null. */
|
|
33
|
+
readonly rawSpec: string | null;
|
|
34
|
+
/** `package.json` declares `"private": true`. */
|
|
35
|
+
readonly isPrivate: boolean;
|
|
36
|
+
/** The pin spec is present but NOT an exact version (range operator used). */
|
|
37
|
+
readonly nonExact: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read the committed `package.json` pin on `@deftai/directive`. The pin is
|
|
41
|
+
* canonical only when it is an EXACT version; a range spec is reported via
|
|
42
|
+
* `nonExact` so callers can warn rather than silently trusting a floating range.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readPin(projectRoot: string, seams?: PinReadSeams): PinReadResult;
|
|
45
|
+
export interface ReconcileInputs {
|
|
46
|
+
/** Canonical pin from `package.json`. */
|
|
47
|
+
readonly pinVersion: string | null;
|
|
48
|
+
/** Reachable engine version, or null when unreachable. */
|
|
49
|
+
readonly engineVersion: string | null;
|
|
50
|
+
/** `.deft/core/VERSION` content marker, or null. */
|
|
51
|
+
readonly contentVersion: string | null;
|
|
52
|
+
/** AGENTS.md managed-section `sha=`, or null. */
|
|
53
|
+
readonly managedSectionSha: string | null;
|
|
54
|
+
/** The sha the engine would stamp for the current payload, when known. */
|
|
55
|
+
readonly expectedManagedSectionSha?: string | null;
|
|
56
|
+
}
|
|
57
|
+
export interface ReconcileResult {
|
|
58
|
+
/** No mismatches detected against the pin. */
|
|
59
|
+
readonly consistent: boolean;
|
|
60
|
+
/** Deposited content (`VERSION`) is behind the pin (needs forward-migration). */
|
|
61
|
+
readonly contentBehindPin: boolean;
|
|
62
|
+
/** The managed-section sha is present but disagrees with the expected sha. */
|
|
63
|
+
readonly managedShaMismatch: boolean;
|
|
64
|
+
/** Human-facing mismatch reasons (empty when consistent). */
|
|
65
|
+
readonly mismatches: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reconcile the pin against the engine, the deposited content marker, and the
|
|
69
|
+
* AGENTS managed-section sha. The managed-section `sha=` is part of the
|
|
70
|
+
* reconciliation, not just `VERSION` (per the #2264 acceptance).
|
|
71
|
+
*/
|
|
72
|
+
export declare function reconcileVersions(inputs: ReconcileInputs): ReconcileResult;
|
|
73
|
+
//# sourceMappingURL=pin.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version pin + reconciliation for the resolution spine (#2264, absorbs #2199).
|
|
3
|
+
*
|
|
4
|
+
* The committed `package.json` devDependency on `@deftai/directive` (exact,
|
|
5
|
+
* `"private": true`) is the CANONICAL pin — read before directive runs, npm-native
|
|
6
|
+
* so the engine ladder stays plain npm. This module reads that pin and reconciles
|
|
7
|
+
* it against the reachable engine version, the `.deft/core/VERSION` content marker,
|
|
8
|
+
* and the AGENTS.md managed-section `sha=`.
|
|
9
|
+
*
|
|
10
|
+
* Semver helpers here are the single comparison primitive reused by the skew
|
|
11
|
+
* policy and the engine ladder so version ordering is defined in exactly one place.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
/** The canonical committed dependency the pin lives on. */
|
|
16
|
+
export const PIN_DEPENDENCY_NAME = "@deftai/directive";
|
|
17
|
+
const BARE_SEMVER_RE = /^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$/;
|
|
18
|
+
const EXACT_SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
19
|
+
/** Parse a semver prefix (`major.minor.patch`); null when not exact semver. */
|
|
20
|
+
export function parseSemver(version) {
|
|
21
|
+
if (typeof version !== "string")
|
|
22
|
+
return null;
|
|
23
|
+
const normalized = version.trim().replace(/^v/i, "");
|
|
24
|
+
const match = EXACT_SEMVER_RE.exec(normalized);
|
|
25
|
+
if (!match)
|
|
26
|
+
return null;
|
|
27
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
28
|
+
}
|
|
29
|
+
/** True when `version` is an EXACT pin (no range operators like `^` / `~` / `*`). */
|
|
30
|
+
export function isExactPin(version) {
|
|
31
|
+
if (typeof version !== "string")
|
|
32
|
+
return false;
|
|
33
|
+
return BARE_SEMVER_RE.test(version.trim().replace(/^v/i, ""));
|
|
34
|
+
}
|
|
35
|
+
/** Compare two versions: -1 (a<b), 0 (a==b), 1 (a>b); null when either is unparseable. */
|
|
36
|
+
export function compareSemver(a, b) {
|
|
37
|
+
const pa = parseSemver(a);
|
|
38
|
+
const pb = parseSemver(b);
|
|
39
|
+
if (pa === null || pb === null)
|
|
40
|
+
return null;
|
|
41
|
+
for (let i = 0; i < 3; i += 1) {
|
|
42
|
+
const av = pa[i] ?? 0;
|
|
43
|
+
const bv = pb[i] ?? 0;
|
|
44
|
+
if (av > bv)
|
|
45
|
+
return 1;
|
|
46
|
+
if (av < bv)
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
/** True when `a >= b` under semver ordering; false when either is unparseable. */
|
|
52
|
+
export function semverGte(a, b) {
|
|
53
|
+
const cmp = compareSemver(a, b);
|
|
54
|
+
return cmp === 0 || cmp === 1;
|
|
55
|
+
}
|
|
56
|
+
function defaultIsFile(p) {
|
|
57
|
+
try {
|
|
58
|
+
return existsSync(p);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function defaultReadText(p) {
|
|
65
|
+
try {
|
|
66
|
+
return readFileSync(p, "utf8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function readDependencySpec(pkg) {
|
|
73
|
+
for (const block of ["devDependencies", "dependencies"]) {
|
|
74
|
+
const deps = pkg[block];
|
|
75
|
+
if (typeof deps === "object" && deps !== null && !Array.isArray(deps)) {
|
|
76
|
+
const spec = deps[PIN_DEPENDENCY_NAME];
|
|
77
|
+
if (typeof spec === "string" && spec.trim().length > 0) {
|
|
78
|
+
return spec.trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read the committed `package.json` pin on `@deftai/directive`. The pin is
|
|
86
|
+
* canonical only when it is an EXACT version; a range spec is reported via
|
|
87
|
+
* `nonExact` so callers can warn rather than silently trusting a floating range.
|
|
88
|
+
*/
|
|
89
|
+
export function readPin(projectRoot, seams = {}) {
|
|
90
|
+
const isFile = seams.isFile ?? defaultIsFile;
|
|
91
|
+
const readText = seams.readText ?? defaultReadText;
|
|
92
|
+
const absent = {
|
|
93
|
+
pinVersion: null,
|
|
94
|
+
rawSpec: null,
|
|
95
|
+
isPrivate: false,
|
|
96
|
+
nonExact: false,
|
|
97
|
+
};
|
|
98
|
+
const path = join(projectRoot, "package.json");
|
|
99
|
+
if (!isFile(path))
|
|
100
|
+
return absent;
|
|
101
|
+
const text = readText(path);
|
|
102
|
+
if (text === null)
|
|
103
|
+
return absent;
|
|
104
|
+
let pkg;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(text);
|
|
107
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
108
|
+
return absent;
|
|
109
|
+
pkg = parsed;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return absent;
|
|
113
|
+
}
|
|
114
|
+
const isPrivate = pkg.private === true;
|
|
115
|
+
const rawSpec = readDependencySpec(pkg);
|
|
116
|
+
if (rawSpec === null)
|
|
117
|
+
return { pinVersion: null, rawSpec: null, isPrivate, nonExact: false };
|
|
118
|
+
if (!isExactPin(rawSpec)) {
|
|
119
|
+
return { pinVersion: null, rawSpec, isPrivate, nonExact: true };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
pinVersion: rawSpec.replace(/^v/i, ""),
|
|
123
|
+
rawSpec,
|
|
124
|
+
isPrivate,
|
|
125
|
+
nonExact: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reconcile the pin against the engine, the deposited content marker, and the
|
|
130
|
+
* AGENTS managed-section sha. The managed-section `sha=` is part of the
|
|
131
|
+
* reconciliation, not just `VERSION` (per the #2264 acceptance).
|
|
132
|
+
*/
|
|
133
|
+
export function reconcileVersions(inputs) {
|
|
134
|
+
const { pinVersion, engineVersion, contentVersion, managedSectionSha } = inputs;
|
|
135
|
+
const mismatches = [];
|
|
136
|
+
const contentCmp = compareSemver(contentVersion, pinVersion);
|
|
137
|
+
const contentBehindPin = contentCmp === -1;
|
|
138
|
+
if (contentBehindPin) {
|
|
139
|
+
mismatches.push(`.deft/core/VERSION (${contentVersion}) is behind the package.json pin (${pinVersion}); content forward-migration required`);
|
|
140
|
+
}
|
|
141
|
+
else if (contentCmp === 1) {
|
|
142
|
+
mismatches.push(`.deft/core/VERSION (${contentVersion}) is ahead of the package.json pin (${pinVersion})`);
|
|
143
|
+
}
|
|
144
|
+
if (engineVersion !== null && pinVersion !== null) {
|
|
145
|
+
const engineCmp = compareSemver(engineVersion, pinVersion);
|
|
146
|
+
if (engineCmp === -1) {
|
|
147
|
+
mismatches.push(`engine (${engineVersion}) is behind the package.json pin (${pinVersion})`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let managedShaMismatch = false;
|
|
151
|
+
const expected = inputs.expectedManagedSectionSha;
|
|
152
|
+
if (typeof expected === "string" && expected.length > 0) {
|
|
153
|
+
if (managedSectionSha === null) {
|
|
154
|
+
managedShaMismatch = true;
|
|
155
|
+
mismatches.push("AGENTS.md managed-section sha is absent but an expected sha is known");
|
|
156
|
+
}
|
|
157
|
+
else if (managedSectionSha !== expected) {
|
|
158
|
+
managedShaMismatch = true;
|
|
159
|
+
mismatches.push(`AGENTS.md managed-section sha (${managedSectionSha}) does not match expected (${expected})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
consistent: mismatches.length === 0,
|
|
164
|
+
contentBehindPin,
|
|
165
|
+
managedShaMismatch,
|
|
166
|
+
mismatches,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=pin.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single ordered precedence table for the resolution spine (#2264 / epic #2203).
|
|
3
|
+
*
|
|
4
|
+
* `plan()` takes the orthogonal fact-set from `classify()` and collapses it into
|
|
5
|
+
* exactly ONE recommended action, emitting the versioned public
|
|
6
|
+
* {@link ResolutionPlan} schema. This is the single source of truth every
|
|
7
|
+
* consumption context (init / update / doctor / headless) derives from — there
|
|
8
|
+
* is no second classifier downstream (closes the #537 split-source drift risk).
|
|
9
|
+
*
|
|
10
|
+
* SCOPE (keystone): this delivers the decision spine. It does NOT rewire the
|
|
11
|
+
* user-facing behavior of init / update / doctor / headless (children B–E,
|
|
12
|
+
* #2265–#2268), which consume `plan()`. The `files` array is therefore empty in
|
|
13
|
+
* the spine; downstream children populate it.
|
|
14
|
+
*/
|
|
15
|
+
import type { PlanPolicy, ResolutionFacts, ResolutionFile, ResolutionPlan } from "@deftai/directive-types";
|
|
16
|
+
import type { LadderDecision } from "./engine-ladder.js";
|
|
17
|
+
import { type PackageManager } from "./package-manager.js";
|
|
18
|
+
import { type SkewResult } from "./skew-policy.js";
|
|
19
|
+
export interface PlanOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Pre-computed engine ladder decision, supplied when the global engine is
|
|
22
|
+
* unreachable and the caller resolved it via `decideEngineLadder`/`resolveEngine`
|
|
23
|
+
* (warm sandbox / cold sandbox / registry-down). When omitted, `plan()` uses the
|
|
24
|
+
* in-process reachable engine facts.
|
|
25
|
+
*/
|
|
26
|
+
readonly engineResolution?: LadderDecision | null;
|
|
27
|
+
/** Pre-computed skew result; when omitted `plan()` computes it from facts + policy. */
|
|
28
|
+
readonly skew?: SkewResult | null;
|
|
29
|
+
/** Interactive session (a human can answer a skew prompt). */
|
|
30
|
+
readonly interactive?: boolean;
|
|
31
|
+
/** Environment map for the `DEFT_ACCEPT_ENGINE_SKEW` escape hatch. */
|
|
32
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
33
|
+
/** The `--accept-engine-jump` flag was supplied. */
|
|
34
|
+
readonly acceptEngineJump?: boolean;
|
|
35
|
+
/** Platform id, used only to render the sandbox install command. */
|
|
36
|
+
readonly platform?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Files a downstream consumer has already materialised for the plan to carry
|
|
39
|
+
* in {@link ResolutionPlan.files}. The keystone spine leaves `files` empty;
|
|
40
|
+
* children (the #2268 headless emitter) collect the file set out-of-band and
|
|
41
|
+
* thread it here so the emitted manifest is single-sourced from `plan()`
|
|
42
|
+
* rather than a separate "what would init produce" reimplementation. When
|
|
43
|
+
* omitted, `files` stays empty exactly as before.
|
|
44
|
+
*/
|
|
45
|
+
readonly files?: readonly ResolutionFile[];
|
|
46
|
+
/**
|
|
47
|
+
* Active package manager for rendering install/upgrade command strings
|
|
48
|
+
* (#2197). Defaults to npm so existing behaviour and tests are unchanged;
|
|
49
|
+
* callers detect the manager via `detectPackageManager()` and thread it here.
|
|
50
|
+
* The sandbox (`install-sandbox`) rung stays on npm regardless of this value
|
|
51
|
+
* -- the `.deft/.cli/<platform>` layout is npm-shaped and package-manager
|
|
52
|
+
* invisible (locked non-goal, issue #2197).
|
|
53
|
+
*/
|
|
54
|
+
readonly packageManager?: PackageManager;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The single ordered precedence table. First matching row wins; each `plan()`
|
|
58
|
+
* call returns exactly one recommended action.
|
|
59
|
+
*
|
|
60
|
+
* When `options.files` is supplied, the resolved plan carries that file set in
|
|
61
|
+
* {@link ResolutionPlan.files} (the #2268 headless single-sourcing seam); the
|
|
62
|
+
* precedence decision itself is unchanged. With no `files` option the array
|
|
63
|
+
* stays empty exactly as the keystone spine emitted it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function plan(facts: ResolutionFacts, policy?: PlanPolicy, options?: PlanOptions): ResolutionPlan;
|
|
66
|
+
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single ordered precedence table for the resolution spine (#2264 / epic #2203).
|
|
3
|
+
*
|
|
4
|
+
* `plan()` takes the orthogonal fact-set from `classify()` and collapses it into
|
|
5
|
+
* exactly ONE recommended action, emitting the versioned public
|
|
6
|
+
* {@link ResolutionPlan} schema. This is the single source of truth every
|
|
7
|
+
* consumption context (init / update / doctor / headless) derives from — there
|
|
8
|
+
* is no second classifier downstream (closes the #537 split-source drift risk).
|
|
9
|
+
*
|
|
10
|
+
* SCOPE (keystone): this delivers the decision spine. It does NOT rewire the
|
|
11
|
+
* user-facing behavior of init / update / doctor / headless (children B–E,
|
|
12
|
+
* #2265–#2268), which consume `plan()`. The `files` array is therefore empty in
|
|
13
|
+
* the spine; downstream children populate it.
|
|
14
|
+
*/
|
|
15
|
+
import { RESOLUTION_PLAN_SCHEMA_VERSION } from "@deftai/directive-types";
|
|
16
|
+
import { ENGINE_PACKAGE, renderEphemeral, renderGlobalInstall, } from "./package-manager.js";
|
|
17
|
+
import { reconcileVersions } from "./pin.js";
|
|
18
|
+
import { evaluateSkew } from "./skew-policy.js";
|
|
19
|
+
const RUNG_TO_MODE = {
|
|
20
|
+
global: "proceed",
|
|
21
|
+
local: "proceed",
|
|
22
|
+
"install-global": "install-global",
|
|
23
|
+
"install-sandbox": "install-sandbox",
|
|
24
|
+
"install-staged": "install-staged",
|
|
25
|
+
"hard-fail": "blocked",
|
|
26
|
+
};
|
|
27
|
+
function pinSuffix(pinVersion) {
|
|
28
|
+
return pinVersion ? `@${pinVersion}` : "@<pin>";
|
|
29
|
+
}
|
|
30
|
+
function makePlan(mode, nextAction, warnings) {
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: RESOLUTION_PLAN_SCHEMA_VERSION,
|
|
33
|
+
mode,
|
|
34
|
+
files: [],
|
|
35
|
+
nextAction,
|
|
36
|
+
warnings,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function legacyVbriefWarning(facts) {
|
|
40
|
+
if (facts.hasVbrief && !facts.hasXbrief) {
|
|
41
|
+
return "legacy vbrief/ tree present without xbrief/; run `directive migrate:xbrief`";
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** Resolve the effective engine version from either the ladder decision or in-process facts. */
|
|
46
|
+
function effectiveEngineVersion(facts, engineResolution) {
|
|
47
|
+
if (engineResolution?.usable)
|
|
48
|
+
return engineResolution.resolvedVersion;
|
|
49
|
+
if (facts.engineReachable)
|
|
50
|
+
return facts.engineVersion;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function contentStale(facts) {
|
|
54
|
+
const recon = reconcileVersions({
|
|
55
|
+
pinVersion: facts.pinVersion,
|
|
56
|
+
engineVersion: facts.engineVersion,
|
|
57
|
+
contentVersion: facts.deftCorePayloadVersion,
|
|
58
|
+
managedSectionSha: facts.managedSectionSha,
|
|
59
|
+
});
|
|
60
|
+
if (recon.contentBehindPin)
|
|
61
|
+
return true;
|
|
62
|
+
// A deposit that carries no readable managed-section sha is treated as stale.
|
|
63
|
+
if (facts.hasManagedSection && facts.managedSectionSha === null)
|
|
64
|
+
return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The single ordered precedence table. First matching row wins; each `plan()`
|
|
69
|
+
* call returns exactly one recommended action.
|
|
70
|
+
*
|
|
71
|
+
* When `options.files` is supplied, the resolved plan carries that file set in
|
|
72
|
+
* {@link ResolutionPlan.files} (the #2268 headless single-sourcing seam); the
|
|
73
|
+
* precedence decision itself is unchanged. With no `files` option the array
|
|
74
|
+
* stays empty exactly as the keystone spine emitted it.
|
|
75
|
+
*/
|
|
76
|
+
export function plan(facts, policy = {}, options = {}) {
|
|
77
|
+
const resolved = resolvePlan(facts, policy, options);
|
|
78
|
+
const files = options.files ?? [];
|
|
79
|
+
return files.length === 0 ? resolved : { ...resolved, files };
|
|
80
|
+
}
|
|
81
|
+
function resolvePlan(facts, policy, options) {
|
|
82
|
+
const warnings = [];
|
|
83
|
+
const pm = options.packageManager ?? "npm";
|
|
84
|
+
const legacyWarning = legacyVbriefWarning(facts);
|
|
85
|
+
if (legacyWarning)
|
|
86
|
+
warnings.push(legacyWarning);
|
|
87
|
+
// Row 1: pre-cutover artifacts must migrate before anything else.
|
|
88
|
+
if (facts.preCutoverArtifacts) {
|
|
89
|
+
return makePlan("migrate", {
|
|
90
|
+
command: null,
|
|
91
|
+
rootCause: "pre-v0.20 document-model artifacts detected",
|
|
92
|
+
remediation: "Migrate with the frozen pre-v0.20 bridge before any gate runs. See UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).",
|
|
93
|
+
}, warnings);
|
|
94
|
+
}
|
|
95
|
+
// Row 2: no usable deposit — deposit / reconstitute one.
|
|
96
|
+
if (!facts.hasDeftCore) {
|
|
97
|
+
const rootCause = facts.hasManagedSection
|
|
98
|
+
? "AGENTS.md carries a managed section but the .deft/core/ payload is absent (hybrid deposit not reconstituted)"
|
|
99
|
+
: facts.hasAppCode || facts.hasGit
|
|
100
|
+
? "brownfield project without a Deft deposit"
|
|
101
|
+
: "greenfield project without a Deft deposit";
|
|
102
|
+
return makePlan("init", {
|
|
103
|
+
command: renderEphemeral(pm, "init"),
|
|
104
|
+
rootCause,
|
|
105
|
+
remediation: "Deposit (or reconstitute) the .deft/core/ payload with `directive init`.",
|
|
106
|
+
}, warnings);
|
|
107
|
+
}
|
|
108
|
+
// Row 3+: deposit present — resolve the engine dimension.
|
|
109
|
+
const engineResolution = options.engineResolution ?? null;
|
|
110
|
+
// 3a: a supplied ladder decision that requires an install (global engine
|
|
111
|
+
// unreachable path: warm/cold sandbox, registry-down).
|
|
112
|
+
if (engineResolution && !engineResolution.usable) {
|
|
113
|
+
const mode = RUNG_TO_MODE[engineResolution.rung];
|
|
114
|
+
return planForInstallRung(engineResolution, mode, facts, options, warnings);
|
|
115
|
+
}
|
|
116
|
+
const effectiveEngine = effectiveEngineVersion(facts, engineResolution);
|
|
117
|
+
// 3b: no engine reachable and no ladder resolution supplied — cannot resolve.
|
|
118
|
+
if (effectiveEngine === null) {
|
|
119
|
+
return makePlan("blocked", {
|
|
120
|
+
command: null,
|
|
121
|
+
rootCause: "no Directive engine is reachable in the execution environment",
|
|
122
|
+
remediation: "Resolve the engine via the global-first ladder (resolveEngine) / bootstrap before running any gate.",
|
|
123
|
+
}, warnings);
|
|
124
|
+
}
|
|
125
|
+
// 3c: no committed pin — cannot reconcile; proceed but warn.
|
|
126
|
+
if (facts.pinVersion === null) {
|
|
127
|
+
warnings.push("no committed package.json pin on @deftai/directive; skipping engine/pin reconciliation");
|
|
128
|
+
return makePlan("proceed", {
|
|
129
|
+
command: null,
|
|
130
|
+
rootCause: `engine ${effectiveEngine} reachable; no pin to reconcile against`,
|
|
131
|
+
remediation: "Run the requested gate. Consider committing an exact pin (unblocks #2269).",
|
|
132
|
+
}, warnings);
|
|
133
|
+
}
|
|
134
|
+
// 3d: reconcile engine against the pin via the three-band skew policy.
|
|
135
|
+
const skew = options.skew ??
|
|
136
|
+
evaluateSkew(effectiveEngine, facts.pinVersion, {
|
|
137
|
+
engineSkewWindow: policy.engineSkewWindow,
|
|
138
|
+
acceptEngineJump: options.acceptEngineJump,
|
|
139
|
+
interactive: options.interactive,
|
|
140
|
+
env: options.env,
|
|
141
|
+
});
|
|
142
|
+
if (skew.message)
|
|
143
|
+
warnings.push(skew.message);
|
|
144
|
+
switch (skew.decision) {
|
|
145
|
+
case "reject-global":
|
|
146
|
+
return makePlan("install-global", {
|
|
147
|
+
command: renderGlobalInstall(pm, `${ENGINE_PACKAGE}${pinSuffix(facts.pinVersion)}`),
|
|
148
|
+
rootCause: `engine ${effectiveEngine} is behind pin ${facts.pinVersion}`,
|
|
149
|
+
remediation: "Install the pinned engine (or fall through the ladder to a local install).",
|
|
150
|
+
}, warnings);
|
|
151
|
+
case "fail-closed":
|
|
152
|
+
return makePlan("blocked", {
|
|
153
|
+
command: `directive <gate> --accept-engine-jump`,
|
|
154
|
+
rootCause: `engine ${effectiveEngine} is a large jump ahead of pin ${facts.pinVersion}`,
|
|
155
|
+
remediation: "Confirm the jump with --accept-engine-jump or DEFT_ACCEPT_ENGINE_SKEW=1 after reviewing the delta.",
|
|
156
|
+
}, warnings);
|
|
157
|
+
case "prompt":
|
|
158
|
+
return makePlan("blocked", {
|
|
159
|
+
command: null,
|
|
160
|
+
rootCause: `engine ${effectiveEngine} is a large jump ahead of pin ${facts.pinVersion} (interactive)`,
|
|
161
|
+
remediation: "Prompt the operator to confirm the engine jump, or pass --accept-engine-jump.",
|
|
162
|
+
}, warnings);
|
|
163
|
+
default: {
|
|
164
|
+
// proceed-silent | proceed-loud-update
|
|
165
|
+
if (skew.requiresUpdateFirst || contentStale(facts)) {
|
|
166
|
+
return makePlan("update", {
|
|
167
|
+
command: renderEphemeral(pm, "update"),
|
|
168
|
+
rootCause: skew.requiresUpdateFirst
|
|
169
|
+
? `engine ${effectiveEngine} is ahead of pin ${facts.pinVersion} within the skew window`
|
|
170
|
+
: `deposited content is behind pin ${facts.pinVersion}`,
|
|
171
|
+
remediation: "Forward-migrate content with `directive update`, then run the gate.",
|
|
172
|
+
}, warnings);
|
|
173
|
+
}
|
|
174
|
+
return makePlan("proceed", {
|
|
175
|
+
command: null,
|
|
176
|
+
rootCause: `engine ${effectiveEngine} matches pin ${facts.pinVersion} and content is current`,
|
|
177
|
+
remediation: "Run the requested gate.",
|
|
178
|
+
}, warnings);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function planForInstallRung(engineResolution, mode, facts, options, warnings) {
|
|
183
|
+
const platform = options.platform ?? "<platform>";
|
|
184
|
+
const pm = options.packageManager ?? "npm";
|
|
185
|
+
const pinnedSuffix = pinSuffix(facts.pinVersion);
|
|
186
|
+
const withTrace = [...warnings, `ladder: ${engineResolution.trace}`];
|
|
187
|
+
switch (mode) {
|
|
188
|
+
case "install-global":
|
|
189
|
+
return makePlan("install-global", {
|
|
190
|
+
command: renderGlobalInstall(pm, `${ENGINE_PACKAGE}${pinnedSuffix}`),
|
|
191
|
+
rootCause: engineResolution.reason,
|
|
192
|
+
remediation: pm === "pnpm"
|
|
193
|
+
? "Install the pinned engine into the pnpm global bin (ensure PNPM_HOME is on PATH; run `pnpm setup` if needed)."
|
|
194
|
+
: "Install the pinned engine into the global npm prefix.",
|
|
195
|
+
}, withTrace);
|
|
196
|
+
case "install-sandbox":
|
|
197
|
+
// Locked non-goal (#2197): the .deft/.cli/<platform> sandbox stays on npm
|
|
198
|
+
// regardless of the caller's package manager -- its node_modules/.bin
|
|
199
|
+
// layout is npm-shaped and validated by integrity.ts.
|
|
200
|
+
return makePlan("install-sandbox", {
|
|
201
|
+
command: `npm install --prefix .deft/.cli/${platform} @deftai/directive${pinnedSuffix}`,
|
|
202
|
+
rootCause: engineResolution.reason,
|
|
203
|
+
remediation: "Install the pinned engine into the sandbox-local .deft/.cli/<platform> prefix.",
|
|
204
|
+
}, withTrace);
|
|
205
|
+
case "install-staged":
|
|
206
|
+
return makePlan("install-staged", {
|
|
207
|
+
command: null,
|
|
208
|
+
rootCause: engineResolution.reason,
|
|
209
|
+
remediation: "Install from the pre-staged tarball / vendored payload (registry is down).",
|
|
210
|
+
}, withTrace);
|
|
211
|
+
default:
|
|
212
|
+
return makePlan("blocked", {
|
|
213
|
+
command: null,
|
|
214
|
+
rootCause: engineResolution.reason,
|
|
215
|
+
remediation: "Stage a Directive payload (registry down and no staged tarball).",
|
|
216
|
+
}, withTrace);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-band engine-vs-pin skew policy for the resolution spine (#2264, #2199).
|
|
3
|
+
*
|
|
4
|
+
* Content is always forward-migrated (`update`) before any gate runs; this
|
|
5
|
+
* policy governs whether a globally-reachable engine that differs from the pin
|
|
6
|
+
* may be used, and how loudly:
|
|
7
|
+
*
|
|
8
|
+
* - `engine == pin` -> proceed silently (trace only).
|
|
9
|
+
* - `engine > pin` within the skew window -> proceed, emit a loud delta, `update` first.
|
|
10
|
+
* - `engine > pin` beyond the window -> fail closed non-interactively (require
|
|
11
|
+
* `--accept-engine-jump`; prompt when interactive).
|
|
12
|
+
* `DEFT_ACCEPT_ENGINE_SKEW=1` is the CI escape.
|
|
13
|
+
* - `engine < pin` -> reject the global, fall through the ladder.
|
|
14
|
+
*/
|
|
15
|
+
/** Default skew window pre-1.0, measured in minor versions. */
|
|
16
|
+
export declare const DEFAULT_ENGINE_SKEW_WINDOW = 3;
|
|
17
|
+
/** Environment variable that acts as the CI / non-interactive escape hatch. */
|
|
18
|
+
export declare const ACCEPT_ENGINE_SKEW_ENV = "DEFT_ACCEPT_ENGINE_SKEW";
|
|
19
|
+
export type SkewBand = "match" | "within-window" | "beyond-window" | "engine-behind" | "unknown";
|
|
20
|
+
export type SkewDecision = "proceed-silent" | "proceed-loud-update" | "prompt" | "fail-closed" | "reject-global";
|
|
21
|
+
export interface SkewOptions {
|
|
22
|
+
/** Skew window (minor versions pre-1.0; ignored post-1.0 where "same major" applies). */
|
|
23
|
+
readonly engineSkewWindow?: number | null;
|
|
24
|
+
/** The `--accept-engine-jump` flag was supplied. */
|
|
25
|
+
readonly acceptEngineJump?: boolean;
|
|
26
|
+
/** Whether the session is interactive (a human can answer a prompt). */
|
|
27
|
+
readonly interactive?: boolean;
|
|
28
|
+
/** Environment map for the `DEFT_ACCEPT_ENGINE_SKEW` escape hatch. */
|
|
29
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
30
|
+
}
|
|
31
|
+
export interface SkewResult {
|
|
32
|
+
readonly band: SkewBand;
|
|
33
|
+
readonly decision: SkewDecision;
|
|
34
|
+
/** Loud delta / fail-closed message, or null when silent. */
|
|
35
|
+
readonly message: string | null;
|
|
36
|
+
/** The recommended flow must run `update` before proceeding. */
|
|
37
|
+
readonly requiresUpdateFirst: boolean;
|
|
38
|
+
/** An escape hatch (flag or env) was used to permit a beyond-window jump. */
|
|
39
|
+
readonly escapeHatchUsed: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Evaluate the three-band skew policy for a reachable engine against the pin.
|
|
43
|
+
* Unparseable versions fail closed (a version we cannot order is not safe to run).
|
|
44
|
+
*/
|
|
45
|
+
export declare function evaluateSkew(engineVersion: string | null, pinVersion: string | null, opts?: SkewOptions): SkewResult;
|
|
46
|
+
//# sourceMappingURL=skew-policy.d.ts.map
|