@deftai/directive-core 0.68.0 → 0.69.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/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 +352 -2
- package/dist/doctor/payload-staleness.d.ts +8 -0
- package/dist/doctor/payload-staleness.js +11 -8
- package/dist/doctor/types.d.ts +48 -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/hygiene.js +11 -0
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -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 +177 -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/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/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/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
|
@@ -212,6 +212,63 @@ export function writeInstallManifest(projectDir, deftDir, fields) {
|
|
|
212
212
|
writeFileSync(path, body, "utf8");
|
|
213
213
|
return path;
|
|
214
214
|
}
|
|
215
|
+
/** Canonical committed pin dependency name (mirrors resolution/pin.ts). */
|
|
216
|
+
export const PIN_DEPENDENCY_NAME = "@deftai/directive";
|
|
217
|
+
/**
|
|
218
|
+
* Write the committed `package.json` version pin on `@deftai/directive`
|
|
219
|
+
* (exact devDependency; `"private": true` preserved) so the resolution spine
|
|
220
|
+
* has a canonical, npm-native pin to read before directive runs. This unblocks
|
|
221
|
+
* #2269 (which is gated on the pin existing).
|
|
222
|
+
*
|
|
223
|
+
* Behavior:
|
|
224
|
+
* - existing `package.json` -> the exact devDependency pin is set/updated;
|
|
225
|
+
* an existing `"private"` value is preserved verbatim (never clobbered).
|
|
226
|
+
* - absent `package.json` -> a minimal `{ "private": true, ... }` is created.
|
|
227
|
+
* - idempotent -> re-running with the same pin makes no change.
|
|
228
|
+
*
|
|
229
|
+
* This is a deposit primitive; wiring it into the `init` verb flow is owned by
|
|
230
|
+
* #2265 (the init-consumes-plan child), per the #2264 scope guard.
|
|
231
|
+
*/
|
|
232
|
+
export function ensurePackageJsonPin(projectDir, version, io) {
|
|
233
|
+
const pinVersion = version.trim().replace(/^v/i, "");
|
|
234
|
+
const path = join(projectDir, "package.json");
|
|
235
|
+
const existed = existsSync(path);
|
|
236
|
+
let pkg = {};
|
|
237
|
+
if (existed) {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
240
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
241
|
+
pkg = parsed;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
throw new Error("package.json root must be a JSON object");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (cause) {
|
|
248
|
+
throw new Error(`could not parse package.json (leaving it unchanged): ${String(cause)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Fresh scaffold: a consumer deposit is a non-published workspace.
|
|
253
|
+
pkg.private = true;
|
|
254
|
+
}
|
|
255
|
+
const devDeps = typeof pkg.devDependencies === "object" &&
|
|
256
|
+
pkg.devDependencies !== null &&
|
|
257
|
+
!Array.isArray(pkg.devDependencies)
|
|
258
|
+
? { ...pkg.devDependencies }
|
|
259
|
+
: {};
|
|
260
|
+
if (devDeps[PIN_DEPENDENCY_NAME] === pinVersion) {
|
|
261
|
+
io.printf(`package.json already pins ${PIN_DEPENDENCY_NAME}@${pinVersion} — skipping.\n`);
|
|
262
|
+
return { changed: false, pinVersion, created: false };
|
|
263
|
+
}
|
|
264
|
+
devDeps[PIN_DEPENDENCY_NAME] = pinVersion;
|
|
265
|
+
pkg.devDependencies = devDeps;
|
|
266
|
+
writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
267
|
+
io.printf(existed
|
|
268
|
+
? `package.json updated: pinned ${PIN_DEPENDENCY_NAME}@${pinVersion} (exact).\n`
|
|
269
|
+
: `package.json created: pinned ${PIN_DEPENDENCY_NAME}@${pinVersion} (exact, private).\n`);
|
|
270
|
+
return { changed: true, pinVersion, created: !existed };
|
|
271
|
+
}
|
|
215
272
|
export function writeAgentsMd(projectDir, deftDir, io) {
|
|
216
273
|
const plan = agentsRefreshPlan(projectDir, { frameworkRoot: deftDir });
|
|
217
274
|
const state = plan.state;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate --untrack-core`: the one-time vendored→hybrid `.deft/core` un-commit
|
|
3
|
+
* (#2269). Owns the single destructive `git rm --cached -r .deft/core` index
|
|
4
|
+
* mutation in the framework — `init`/`update` only ever write the
|
|
5
|
+
* non-destructive `.gitignore` entry (boundary asserted by the colocated test).
|
|
6
|
+
*
|
|
7
|
+
* Safety contract:
|
|
8
|
+
* - NEVER touches working-tree content. `git rm --cached` removes files from
|
|
9
|
+
* the index only; the on-disk deposit is left intact.
|
|
10
|
+
* - Gated on a committed `package.json` pin existing (#2264 / Decision 2):
|
|
11
|
+
* once `.deft/core` is un-committed, content can be reconstituted from the
|
|
12
|
+
* pinned engine, so nothing is lost. With no pin, un-committing could leave
|
|
13
|
+
* the deposit unrecoverable, so we refuse.
|
|
14
|
+
* - Idempotent: once the deposit is un-tracked and ignored, re-running is a
|
|
15
|
+
* no-op that mutates nothing.
|
|
16
|
+
*
|
|
17
|
+
* The doctor detection that SURFACES this verb (the "payload tracked but should
|
|
18
|
+
* be ignored" signpost emitting `Next command: directive migrate --untrack-core`)
|
|
19
|
+
* is owned by child D (#2267); this module provides only the verb it points at.
|
|
20
|
+
*
|
|
21
|
+
* Refs #2269, #2264, #2123, #2124, #2203.
|
|
22
|
+
*/
|
|
23
|
+
import { type PinReadResult } from "../resolution/pin.js";
|
|
24
|
+
import type { InitDepositIo } from "./constants.js";
|
|
25
|
+
import { type GitLsFiles } from "./gitignore.js";
|
|
26
|
+
/** The deposit path un-committed from the git index (never from the working tree). */
|
|
27
|
+
export declare const UNTRACK_CORE_PATH = ".deft/core";
|
|
28
|
+
export type UntrackCoreOutcome = "untracked" | "already-clean" | "refused-missing-pin" | "git-error";
|
|
29
|
+
export interface UntrackCoreResult {
|
|
30
|
+
readonly outcome: UntrackCoreOutcome;
|
|
31
|
+
/** 0: untracked / already-clean · 1: refused (missing pin) · 2: git error. */
|
|
32
|
+
readonly exitCode: 0 | 1 | 2;
|
|
33
|
+
/** Whether `.deft/core` was tracked in git when the verb ran. */
|
|
34
|
+
readonly deftCoreTracked: boolean;
|
|
35
|
+
/** The committed exact pin version, or null when absent / non-exact. */
|
|
36
|
+
readonly pinVersion: string | null;
|
|
37
|
+
/** Whether the `.gitignore` reconciliation changed the file. */
|
|
38
|
+
readonly gitignoreChanged: boolean;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
}
|
|
41
|
+
/** Outcome of the injected `git rm --cached -r <paths>` runner. */
|
|
42
|
+
export interface GitRmCachedResult {
|
|
43
|
+
readonly ok: boolean;
|
|
44
|
+
/** stdout on success, or the error detail on failure. */
|
|
45
|
+
readonly detail: string;
|
|
46
|
+
}
|
|
47
|
+
/** Injected git runner for the destructive index mutation (default shells out). */
|
|
48
|
+
export type GitRmCached = (projectDir: string, paths: readonly string[]) => GitRmCachedResult;
|
|
49
|
+
export interface UntrackCoreSeams {
|
|
50
|
+
/** Probe whether `.deft/core` is git-tracked (default: `git ls-files`). */
|
|
51
|
+
gitLsFiles?: GitLsFiles;
|
|
52
|
+
/** Run `git rm --cached -r <paths>` (default: shells out to git). */
|
|
53
|
+
gitRmCached?: GitRmCached;
|
|
54
|
+
/** Read the committed `package.json` pin (default: resolution/pin.ts readPin). */
|
|
55
|
+
readPin?: (projectRoot: string) => PinReadResult;
|
|
56
|
+
/** Reconcile `.gitignore` (default: ensureUntrackCoreGitignoreLines). */
|
|
57
|
+
ensureGitignore?: (projectDir: string, io: InitDepositIo) => {
|
|
58
|
+
changed: boolean;
|
|
59
|
+
deftCoreIgnored: boolean;
|
|
60
|
+
};
|
|
61
|
+
/** Output sink for reconciliation messages (default: no-op; the CLI prints the summary). */
|
|
62
|
+
io?: InitDepositIo;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Orchestrate the vendored→hybrid `.deft/core` un-commit for `projectRoot`.
|
|
66
|
+
* Pure of process exit; the CLI wrapper maps {@link UntrackCoreResult.exitCode}
|
|
67
|
+
* to a process code. Three primary outcomes (untracked / already-clean /
|
|
68
|
+
* refused-missing-pin) plus a git-error fallthrough.
|
|
69
|
+
*/
|
|
70
|
+
export declare function untrackCore(projectRoot: string, seams?: UntrackCoreSeams): UntrackCoreResult;
|
|
71
|
+
export interface RunUntrackCoreCliOptions {
|
|
72
|
+
readonly projectDir: string;
|
|
73
|
+
readonly jsonOut: boolean;
|
|
74
|
+
readonly writeOut: (text: string) => void;
|
|
75
|
+
readonly writeErr: (text: string) => void;
|
|
76
|
+
readonly seams?: UntrackCoreSeams;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* CLI-facing wrapper: runs the un-commit, emits JSON or human output, maps the
|
|
80
|
+
* outcome to a 0/1/2 exit code. Refusals and git errors print to stderr;
|
|
81
|
+
* success prints to stdout.
|
|
82
|
+
*/
|
|
83
|
+
export declare function runUntrackCoreCli(options: RunUntrackCoreCliOptions): number;
|
|
84
|
+
//# sourceMappingURL=untrack-core.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `migrate --untrack-core`: the one-time vendored→hybrid `.deft/core` un-commit
|
|
3
|
+
* (#2269). Owns the single destructive `git rm --cached -r .deft/core` index
|
|
4
|
+
* mutation in the framework — `init`/`update` only ever write the
|
|
5
|
+
* non-destructive `.gitignore` entry (boundary asserted by the colocated test).
|
|
6
|
+
*
|
|
7
|
+
* Safety contract:
|
|
8
|
+
* - NEVER touches working-tree content. `git rm --cached` removes files from
|
|
9
|
+
* the index only; the on-disk deposit is left intact.
|
|
10
|
+
* - Gated on a committed `package.json` pin existing (#2264 / Decision 2):
|
|
11
|
+
* once `.deft/core` is un-committed, content can be reconstituted from the
|
|
12
|
+
* pinned engine, so nothing is lost. With no pin, un-committing could leave
|
|
13
|
+
* the deposit unrecoverable, so we refuse.
|
|
14
|
+
* - Idempotent: once the deposit is un-tracked and ignored, re-running is a
|
|
15
|
+
* no-op that mutates nothing.
|
|
16
|
+
*
|
|
17
|
+
* The doctor detection that SURFACES this verb (the "payload tracked but should
|
|
18
|
+
* be ignored" signpost emitting `Next command: directive migrate --untrack-core`)
|
|
19
|
+
* is owned by child D (#2267); this module provides only the verb it points at.
|
|
20
|
+
*
|
|
21
|
+
* Refs #2269, #2264, #2123, #2124, #2203.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { PIN_DEPENDENCY_NAME, readPin } from "../resolution/pin.js";
|
|
25
|
+
import { ensureUntrackCoreGitignoreLines, isDepositTrackedInGit, } from "./gitignore.js";
|
|
26
|
+
/** The deposit path un-committed from the git index (never from the working tree). */
|
|
27
|
+
export const UNTRACK_CORE_PATH = ".deft/core";
|
|
28
|
+
/** Default `git rm --cached -r <paths>`: index-only removal, working tree preserved. */
|
|
29
|
+
function defaultGitRmCached(projectDir, paths) {
|
|
30
|
+
try {
|
|
31
|
+
const out = execFileSync("git", ["rm", "--cached", "-r", "--", ...paths], {
|
|
32
|
+
cwd: projectDir,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
return { ok: true, detail: out };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return { ok: false, detail };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function refusedMissingPinMessage(pin) {
|
|
44
|
+
const reason = pin.nonExact
|
|
45
|
+
? `package.json pins ${PIN_DEPENDENCY_NAME} with a non-exact range spec (${pin.rawSpec ?? "?"})`
|
|
46
|
+
: `no committed exact package.json pin on ${PIN_DEPENDENCY_NAME} was found`;
|
|
47
|
+
return ("directive migrate --untrack-core: refusing to run git rm --cached on .deft/core because " +
|
|
48
|
+
`${reason}. Un-committing without a committed exact pin could leave the deposit content ` +
|
|
49
|
+
"unrecoverable (nothing to reconstitute from). Write and commit an exact pin " +
|
|
50
|
+
"(e.g. via `directive init` / `directive update`), then re-run.");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Orchestrate the vendored→hybrid `.deft/core` un-commit for `projectRoot`.
|
|
54
|
+
* Pure of process exit; the CLI wrapper maps {@link UntrackCoreResult.exitCode}
|
|
55
|
+
* to a process code. Three primary outcomes (untracked / already-clean /
|
|
56
|
+
* refused-missing-pin) plus a git-error fallthrough.
|
|
57
|
+
*/
|
|
58
|
+
export function untrackCore(projectRoot, seams = {}) {
|
|
59
|
+
const gitRmCached = seams.gitRmCached ?? defaultGitRmCached;
|
|
60
|
+
const readPinFn = seams.readPin ?? readPin;
|
|
61
|
+
const ensureGitignore = seams.ensureGitignore ?? ensureUntrackCoreGitignoreLines;
|
|
62
|
+
const io = seams.io ?? { printf: () => { } };
|
|
63
|
+
const tracked = isDepositTrackedInGit(projectRoot, seams.gitLsFiles);
|
|
64
|
+
// Not tracked (or no git repo / git unavailable): nothing destructive to do.
|
|
65
|
+
// Reconcile the ignore entry so the layout is unambiguous, then report clean.
|
|
66
|
+
if (tracked !== true) {
|
|
67
|
+
const gi = ensureGitignore(projectRoot, io);
|
|
68
|
+
return {
|
|
69
|
+
outcome: "already-clean",
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
deftCoreTracked: false,
|
|
72
|
+
pinVersion: readPinFn(projectRoot).pinVersion,
|
|
73
|
+
gitignoreChanged: gi.changed,
|
|
74
|
+
message: `directive migrate --untrack-core: .deft/core is not tracked in git — nothing to ` +
|
|
75
|
+
`un-commit. .gitignore ${gi.changed ? "reconciled (now ignores .deft/core/)" : "already ignores it"}.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Tracked: gate on the committed pin so content stays reconstitutable.
|
|
79
|
+
// Refuse on either an absent pin OR a non-exact range spec — a range cannot
|
|
80
|
+
// deterministically reconstitute the exact deposit, so it is not a safe gate.
|
|
81
|
+
// The `|| pin.nonExact` makes the invariant self-documenting and robust to any
|
|
82
|
+
// future `readPin` that resolves a version alongside `nonExact: true` (#2269).
|
|
83
|
+
const pin = readPinFn(projectRoot);
|
|
84
|
+
if (pin.pinVersion === null || pin.nonExact) {
|
|
85
|
+
return {
|
|
86
|
+
outcome: "refused-missing-pin",
|
|
87
|
+
exitCode: 1,
|
|
88
|
+
deftCoreTracked: true,
|
|
89
|
+
pinVersion: null,
|
|
90
|
+
gitignoreChanged: false,
|
|
91
|
+
message: refusedMissingPinMessage(pin),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const rm = gitRmCached(projectRoot, [UNTRACK_CORE_PATH]);
|
|
95
|
+
if (!rm.ok) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: "git-error",
|
|
98
|
+
exitCode: 2,
|
|
99
|
+
deftCoreTracked: true,
|
|
100
|
+
pinVersion: pin.pinVersion,
|
|
101
|
+
gitignoreChanged: false,
|
|
102
|
+
message: `directive migrate --untrack-core: git rm --cached failed: ${rm.detail.trim()}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const gi = ensureGitignore(projectRoot, io);
|
|
106
|
+
return {
|
|
107
|
+
outcome: "untracked",
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
deftCoreTracked: true,
|
|
110
|
+
pinVersion: pin.pinVersion,
|
|
111
|
+
gitignoreChanged: gi.changed,
|
|
112
|
+
message: `directive migrate --untrack-core: removed ${UNTRACK_CORE_PATH} from the git index ` +
|
|
113
|
+
`(working tree untouched); pin ${pin.pinVersion} lets \`directive update\` reconstitute ` +
|
|
114
|
+
`content. .gitignore ${gi.changed ? "reconciled (now ignores .deft/core/)" : "already ignores it"}. ` +
|
|
115
|
+
"Commit the removal to complete the un-commit.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function buildUntrackCoreSummaryJson(result, projectDir) {
|
|
119
|
+
return {
|
|
120
|
+
success: result.exitCode === 0,
|
|
121
|
+
action: "migrate-untrack-core",
|
|
122
|
+
outcome: result.outcome,
|
|
123
|
+
exit_code: result.exitCode,
|
|
124
|
+
project_dir: projectDir,
|
|
125
|
+
deft_core_tracked: result.deftCoreTracked,
|
|
126
|
+
pin_version: result.pinVersion,
|
|
127
|
+
gitignore_changed: result.gitignoreChanged,
|
|
128
|
+
message: result.message,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* CLI-facing wrapper: runs the un-commit, emits JSON or human output, maps the
|
|
133
|
+
* outcome to a 0/1/2 exit code. Refusals and git errors print to stderr;
|
|
134
|
+
* success prints to stdout.
|
|
135
|
+
*/
|
|
136
|
+
export function runUntrackCoreCli(options) {
|
|
137
|
+
const result = untrackCore(options.projectDir, { ...options.seams, io: { printf: () => { } } });
|
|
138
|
+
if (options.jsonOut) {
|
|
139
|
+
options.writeOut(`${JSON.stringify(buildUntrackCoreSummaryJson(result, options.projectDir), null, 2)}\n`);
|
|
140
|
+
return result.exitCode;
|
|
141
|
+
}
|
|
142
|
+
if (result.exitCode === 0) {
|
|
143
|
+
options.writeOut(`${result.message}\n`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
options.writeErr(`${result.message}\n`);
|
|
147
|
+
}
|
|
148
|
+
return result.exitCode;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=untrack-core.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orthogonal fact-set classifier for the resolution spine (#2264 / epic #2203).
|
|
3
|
+
*
|
|
4
|
+
* `classify()` returns a flat, independent fact-set — never a single collapsed
|
|
5
|
+
* enum. `plan()` owns the collapse into one recommended action, so there is
|
|
6
|
+
* exactly one precedence table in the system.
|
|
7
|
+
*
|
|
8
|
+
* It REUSES the existing detectors rather than re-implementing them:
|
|
9
|
+
* - pre-cutover artifacts -> `../vbrief-validate/precutover.ts`
|
|
10
|
+
* - managed-section sha -> `../platform/agents-md.ts::parseManagedSectionAttrs`
|
|
11
|
+
* - deposited payload version-> `../doctor/manifest.ts` + `../init-deposit/constants.ts`
|
|
12
|
+
* - committed pin -> `./pin.ts`
|
|
13
|
+
*/
|
|
14
|
+
import type { ResolutionFacts } from "@deftai/directive-types";
|
|
15
|
+
/** Result of probing whether an engine is reachable in the execution environment. */
|
|
16
|
+
export interface EngineProbeResult {
|
|
17
|
+
readonly reachable: boolean;
|
|
18
|
+
readonly version: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface ClassifySeams {
|
|
21
|
+
readonly isFile?: (p: string) => boolean;
|
|
22
|
+
readonly isDir?: (p: string) => boolean;
|
|
23
|
+
readonly readText?: (p: string) => string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Probe for a reachable engine IN THE ENVIRONMENT THE CALLER IS RUNNING IN.
|
|
26
|
+
* Injected so classification is deterministic + offline in tests; the default
|
|
27
|
+
* shells out to the `directive` / `deft` CLI (the #2124 execution-env probe).
|
|
28
|
+
*/
|
|
29
|
+
readonly engineProbe?: () => EngineProbeResult;
|
|
30
|
+
/** Pre-cutover probe; defaults to the shared `detectPreCutover` detector. */
|
|
31
|
+
readonly preCutoverProbe?: (cwd: string) => boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Default engine probe: try `directive --version`, then `deft --version`. */
|
|
34
|
+
export declare function defaultEngineProbe(): EngineProbeResult;
|
|
35
|
+
/**
|
|
36
|
+
* Classify a project directory into the orthogonal resolution fact-set. All I/O
|
|
37
|
+
* is behind injectable seams so callers (tests, sandboxed runtimes) can supply a
|
|
38
|
+
* deterministic view of the filesystem and the engine-reachability probe.
|
|
39
|
+
*/
|
|
40
|
+
export declare function classify(cwd: string, seams?: ClassifySeams): ResolutionFacts;
|
|
41
|
+
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orthogonal fact-set classifier for the resolution spine (#2264 / epic #2203).
|
|
3
|
+
*
|
|
4
|
+
* `classify()` returns a flat, independent fact-set — never a single collapsed
|
|
5
|
+
* enum. `plan()` owns the collapse into one recommended action, so there is
|
|
6
|
+
* exactly one precedence table in the system.
|
|
7
|
+
*
|
|
8
|
+
* It REUSES the existing detectors rather than re-implementing them:
|
|
9
|
+
* - pre-cutover artifacts -> `../vbrief-validate/precutover.ts`
|
|
10
|
+
* - managed-section sha -> `../platform/agents-md.ts::parseManagedSectionAttrs`
|
|
11
|
+
* - deposited payload version-> `../doctor/manifest.ts` + `../init-deposit/constants.ts`
|
|
12
|
+
* - committed pin -> `./pin.ts`
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { locateManifest, manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
|
|
18
|
+
import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
|
|
19
|
+
import { extractManagedSection, parseManagedSectionAttrs } from "../platform/agents-md.js";
|
|
20
|
+
import { detectPreCutover } from "../vbrief-validate/precutover.js";
|
|
21
|
+
import { readPin } from "./pin.js";
|
|
22
|
+
/** App-source markers used for the `hasAppCode` heuristic. */
|
|
23
|
+
const APP_CODE_MARKERS = [
|
|
24
|
+
"package.json",
|
|
25
|
+
"pyproject.toml",
|
|
26
|
+
"go.mod",
|
|
27
|
+
"Cargo.toml",
|
|
28
|
+
"pom.xml",
|
|
29
|
+
"build.gradle",
|
|
30
|
+
"Gemfile",
|
|
31
|
+
"src",
|
|
32
|
+
];
|
|
33
|
+
const SEMVER_IN_TEXT_RE = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
|
|
34
|
+
function defaultIsFile(p) {
|
|
35
|
+
try {
|
|
36
|
+
return statSync(p).isFile();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function defaultIsDir(p) {
|
|
43
|
+
try {
|
|
44
|
+
return statSync(p).isDirectory();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function defaultReadText(p) {
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(p))
|
|
53
|
+
return null;
|
|
54
|
+
return readFileSync(p, "utf8");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function probeEngineVersion(binary) {
|
|
61
|
+
try {
|
|
62
|
+
const out = execFileSync(binary, ["--version"], {
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
timeout: 5000,
|
|
65
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
66
|
+
});
|
|
67
|
+
const match = SEMVER_IN_TEXT_RE.exec(out);
|
|
68
|
+
return match?.[1] ?? null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Default engine probe: try `directive --version`, then `deft --version`. */
|
|
75
|
+
export function defaultEngineProbe() {
|
|
76
|
+
for (const binary of ["directive", "deft"]) {
|
|
77
|
+
const version = probeEngineVersion(binary);
|
|
78
|
+
if (version !== null) {
|
|
79
|
+
return { reachable: true, version };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { reachable: false, version: null };
|
|
83
|
+
}
|
|
84
|
+
function detectAppCode(cwd, isFile, isDir) {
|
|
85
|
+
return APP_CODE_MARKERS.some((marker) => marker === "src" ? isDir(join(cwd, marker)) : isFile(join(cwd, marker)));
|
|
86
|
+
}
|
|
87
|
+
function readManagedSection(cwd, readText) {
|
|
88
|
+
const text = readText(join(cwd, "AGENTS.md"));
|
|
89
|
+
if (text === null)
|
|
90
|
+
return { hasManagedSection: false, managedSectionSha: null };
|
|
91
|
+
const section = extractManagedSection(text);
|
|
92
|
+
if (section === null)
|
|
93
|
+
return { hasManagedSection: false, managedSectionSha: null };
|
|
94
|
+
const attrs = parseManagedSectionAttrs(section);
|
|
95
|
+
return { hasManagedSection: true, managedSectionSha: attrs?.sha ?? null };
|
|
96
|
+
}
|
|
97
|
+
function readPayloadVersion(cwd, hasDeftCore, isFile, readText) {
|
|
98
|
+
if (!hasDeftCore)
|
|
99
|
+
return null;
|
|
100
|
+
const manifestPath = locateManifest(cwd, CANONICAL_INSTALL_ROOT, isFile);
|
|
101
|
+
if (manifestPath === null)
|
|
102
|
+
return null;
|
|
103
|
+
const text = readText(manifestPath);
|
|
104
|
+
if (text === null)
|
|
105
|
+
return null;
|
|
106
|
+
return manifestTagToVersion(parseInstallManifest(text));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Classify a project directory into the orthogonal resolution fact-set. All I/O
|
|
110
|
+
* is behind injectable seams so callers (tests, sandboxed runtimes) can supply a
|
|
111
|
+
* deterministic view of the filesystem and the engine-reachability probe.
|
|
112
|
+
*/
|
|
113
|
+
export function classify(cwd, seams = {}) {
|
|
114
|
+
const isFile = seams.isFile ?? defaultIsFile;
|
|
115
|
+
const isDir = seams.isDir ?? defaultIsDir;
|
|
116
|
+
const readText = seams.readText ?? defaultReadText;
|
|
117
|
+
const engineProbe = seams.engineProbe ?? defaultEngineProbe;
|
|
118
|
+
const preCutoverProbe = seams.preCutoverProbe ?? ((dir) => detectPreCutover(dir).preCutover);
|
|
119
|
+
const hasDeftCore = isDir(join(cwd, CANONICAL_INSTALL_ROOT));
|
|
120
|
+
const { hasManagedSection, managedSectionSha } = readManagedSection(cwd, readText);
|
|
121
|
+
const engine = engineProbe();
|
|
122
|
+
const pin = readPin(cwd, { isFile, readText });
|
|
123
|
+
return {
|
|
124
|
+
hasGit: isDir(join(cwd, ".git")) || isFile(join(cwd, ".git")),
|
|
125
|
+
hasAppCode: detectAppCode(cwd, isFile, isDir),
|
|
126
|
+
hasDeftCore,
|
|
127
|
+
deftCorePayloadVersion: readPayloadVersion(cwd, hasDeftCore, isFile, readText),
|
|
128
|
+
hasManagedSection,
|
|
129
|
+
managedSectionSha,
|
|
130
|
+
hasVbrief: isDir(join(cwd, "vbrief")),
|
|
131
|
+
hasXbrief: isDir(join(cwd, "xbrief")),
|
|
132
|
+
preCutoverArtifacts: preCutoverProbe(cwd),
|
|
133
|
+
engineReachable: engine.reachable,
|
|
134
|
+
engineVersion: engine.version,
|
|
135
|
+
pinVersion: pin.pinVersion,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global-first engine-resolution ladder for the resolution spine (#2264, from #2124).
|
|
3
|
+
*
|
|
4
|
+
* The defect this closes is `execution-env != install-env`: an agent can read
|
|
5
|
+
* every rule in `.deft/core/` yet execute no gate because the engine that is
|
|
6
|
+
* reachable in the environment it is ACTUALLY running in is absent or stale.
|
|
7
|
+
* The ladder detects that mismatch and self-heals only then:
|
|
8
|
+
*
|
|
9
|
+
* 1. global `deft` reachable AND >= pin -> use it
|
|
10
|
+
* 2. local `.deft/.cli/<platform>` reachable, intact, >= pin -> use it
|
|
11
|
+
* 3. must install:
|
|
12
|
+
* a. registry up + global prefix writable -> npm i -g @deftai/directive@<pin>
|
|
13
|
+
* b. registry up + global prefix NOT writable (sandbox) -> npm install --prefix .deft/.cli/<platform>
|
|
14
|
+
* c. registry down + staged tarball -> install from staged payload
|
|
15
|
+
* d. registry down + no tarball -> hard-fail ("stage this")
|
|
16
|
+
*
|
|
17
|
+
* `decideEngineLadder` is a PURE decision function (no I/O). The side-effecting
|
|
18
|
+
* install is factored behind an injected runner in `resolveEngine` so the whole
|
|
19
|
+
* self-heal path is unit-testable without touching the network.
|
|
20
|
+
*/
|
|
21
|
+
import type { IntegrityResult } from "./integrity.js";
|
|
22
|
+
export type LadderRung = "global" | "local" | "install-global" | "install-sandbox" | "install-staged" | "hard-fail";
|
|
23
|
+
export interface LocalEngineFacts {
|
|
24
|
+
/** Version reported by the local engine, or null. */
|
|
25
|
+
readonly version: string | null;
|
|
26
|
+
/** Integrity classification of `.deft/.cli/<platform>`. */
|
|
27
|
+
readonly integrity: IntegrityResult;
|
|
28
|
+
}
|
|
29
|
+
export interface LadderFacts {
|
|
30
|
+
/** Canonical committed pin. */
|
|
31
|
+
readonly pinVersion: string | null;
|
|
32
|
+
/** Version of the globally-reachable engine, or null when absent. */
|
|
33
|
+
readonly globalEngineVersion: string | null;
|
|
34
|
+
/** Local engine facts, or null when never installed. */
|
|
35
|
+
readonly localEngine: LocalEngineFacts | null;
|
|
36
|
+
/** The npm registry is reachable. */
|
|
37
|
+
readonly registryUp: boolean;
|
|
38
|
+
/** The global npm prefix is writable (false inside a sandbox). */
|
|
39
|
+
readonly globalPrefixWritable: boolean;
|
|
40
|
+
/** A pre-staged tarball / vendored payload is available for offline install. */
|
|
41
|
+
readonly stagedTarballAvailable: boolean;
|
|
42
|
+
/** Platform id (for trace / install target). */
|
|
43
|
+
readonly platform: string;
|
|
44
|
+
}
|
|
45
|
+
export interface LadderDecision {
|
|
46
|
+
readonly rung: LadderRung;
|
|
47
|
+
/** True when the engine is resolved WITHOUT an install (global or local). */
|
|
48
|
+
readonly usable: boolean;
|
|
49
|
+
/** Version the ladder resolved to when usable, else null. */
|
|
50
|
+
readonly resolvedVersion: string | null;
|
|
51
|
+
/** Structured trace of every rung evaluated. */
|
|
52
|
+
readonly trace: string;
|
|
53
|
+
/** One-line reason for the chosen rung. */
|
|
54
|
+
readonly reason: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pure global-first ladder decision. Emits a structured trace describing each
|
|
58
|
+
* rung it evaluated and why it was skipped or chosen.
|
|
59
|
+
*/
|
|
60
|
+
export declare function decideEngineLadder(facts: LadderFacts): LadderDecision;
|
|
61
|
+
/** Outcome of a side-effecting engine install. */
|
|
62
|
+
export interface EngineInstallOutcome {
|
|
63
|
+
readonly installed: boolean;
|
|
64
|
+
/** Version the install produced, or null on failure. */
|
|
65
|
+
readonly version: string | null;
|
|
66
|
+
readonly detail: string;
|
|
67
|
+
}
|
|
68
|
+
/** Injected side-effecting install runner (npm i -g / --prefix / staged). */
|
|
69
|
+
export type EngineInstallRunner = (context: {
|
|
70
|
+
readonly rung: Extract<LadderRung, "install-global" | "install-sandbox" | "install-staged">;
|
|
71
|
+
readonly pinVersion: string | null;
|
|
72
|
+
readonly platform: string;
|
|
73
|
+
}) => EngineInstallOutcome;
|
|
74
|
+
/** Injected content re-projection (`update`) run after a fresh install. */
|
|
75
|
+
export type ReprojectRunner = (version: string | null) => void;
|
|
76
|
+
export interface ResolveEngineOptions {
|
|
77
|
+
readonly installRunner?: EngineInstallRunner;
|
|
78
|
+
readonly reproject?: ReprojectRunner;
|
|
79
|
+
}
|
|
80
|
+
export interface EngineResolution {
|
|
81
|
+
readonly decision: LadderDecision;
|
|
82
|
+
/** The install outcome when a rung required an install, else null. */
|
|
83
|
+
readonly installOutcome: EngineInstallOutcome | null;
|
|
84
|
+
/** The engine version resolved after any install, or null when unresolved. */
|
|
85
|
+
readonly resolvedVersion: string | null;
|
|
86
|
+
/** True when the ladder healed a mismatch (installed) with zero manual steps. */
|
|
87
|
+
readonly selfHealed: boolean;
|
|
88
|
+
/** Full structured trace including any install + re-projection. */
|
|
89
|
+
readonly trace: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the engine by composing the pure ladder decision with an injected
|
|
93
|
+
* install runner. When a rung requires an install, the runner performs it and
|
|
94
|
+
* (on success) the re-projection runner forward-migrates content — yielding the
|
|
95
|
+
* "self-heals with zero manual npm/PATH steps" behavior with a structured trace.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveEngine(facts: LadderFacts, options?: ResolveEngineOptions): EngineResolution;
|
|
98
|
+
//# sourceMappingURL=engine-ladder.d.ts.map
|