@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,94 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { maybeSelfHealCache } from "../../cache/fetch.js";
|
|
4
|
+
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
5
|
+
import { DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, TRIAGE_SKILL_PATH } from "./constants.js";
|
|
6
|
+
import { formatWelcomeCommand } from "./default-mode.js";
|
|
7
|
+
import { detectPriorState } from "./prior-state.js";
|
|
8
|
+
import { emitOneliner } from "./summary.js";
|
|
9
|
+
import { previewWipRelief, subscriptionPreset, writeTriageScope, writeWipCap } from "./writers.js";
|
|
10
|
+
/** Default triage-scope preset applied when `--onboard` is run without `--preset`. */
|
|
11
|
+
export const DEFAULT_ONBOARD_PRESET = "small";
|
|
12
|
+
function failure(exitCode) {
|
|
13
|
+
return {
|
|
14
|
+
exitCode,
|
|
15
|
+
presetApplied: null,
|
|
16
|
+
triageScopeChanged: false,
|
|
17
|
+
wipCapApplied: null,
|
|
18
|
+
wipCapChanged: false,
|
|
19
|
+
reliefOffered: false,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Non-interactive onboarding for `deft triage:welcome --onboard` (#2295).
|
|
24
|
+
*
|
|
25
|
+
* The welcome nudges point every fresh consumer at `--onboard`; this wires that
|
|
26
|
+
* command to the already-tested core writers instead of the old "not
|
|
27
|
+
* implemented" stub. It writes the chosen triage-scope preset and (optionally)
|
|
28
|
+
* a WIP cap via {@link writeTriageScope} / {@link writeWipCap} -- which already
|
|
29
|
+
* target the canonical `x-directive/policy` key and migrate any legacy bare
|
|
30
|
+
* block -- previews WIP relief when at/over cap, then prints a completion
|
|
31
|
+
* summary and the next-step guidance. Flag-driven by design: agents/CI pass
|
|
32
|
+
* `--preset` / `--wip-cap`; sensible defaults apply otherwise.
|
|
33
|
+
*/
|
|
34
|
+
export function runOnboardMode(projectRoot, options = {}) {
|
|
35
|
+
const out = options.output ?? ((line) => process.stdout.write(`${line}\n`));
|
|
36
|
+
const preset = (options.preset ?? DEFAULT_ONBOARD_PRESET).trim() || DEFAULT_ONBOARD_PRESET;
|
|
37
|
+
if (!Object.hasOwn(SUBSCRIPTION_PRESETS, preset)) {
|
|
38
|
+
out(`[welcome] Unknown --preset '${preset}'. Choose one of: ${Object.keys(SUBSCRIPTION_PRESETS).join(", ")}.`);
|
|
39
|
+
return failure(2);
|
|
40
|
+
}
|
|
41
|
+
const wipCap = options.wipCap ?? null;
|
|
42
|
+
if (wipCap !== null && (!Number.isInteger(wipCap) || wipCap < 1)) {
|
|
43
|
+
out(`[welcome] --wip-cap must be a positive integer, got ${JSON.stringify(options.wipCap)}.`);
|
|
44
|
+
return failure(2);
|
|
45
|
+
}
|
|
46
|
+
const pdPath = resolveProjectDefinitionPath(projectRoot);
|
|
47
|
+
if (!existsSync(pdPath)) {
|
|
48
|
+
out(`[welcome] No project definition found at ${pdPath}. Run project setup first (deft-directive-setup) before onboarding triage.`);
|
|
49
|
+
return failure(2);
|
|
50
|
+
}
|
|
51
|
+
const heal = options.selfHealFn ??
|
|
52
|
+
((root) => {
|
|
53
|
+
maybeSelfHealCache(resolve(root));
|
|
54
|
+
});
|
|
55
|
+
heal(projectRoot);
|
|
56
|
+
const rules = subscriptionPreset(preset);
|
|
57
|
+
const [triageScopeChanged] = writeTriageScope(projectRoot, rules, { presetLabel: preset });
|
|
58
|
+
let wipCapChanged = false;
|
|
59
|
+
if (wipCap !== null) {
|
|
60
|
+
[wipCapChanged] = writeWipCap(projectRoot, wipCap);
|
|
61
|
+
}
|
|
62
|
+
emitOneliner(projectRoot, {
|
|
63
|
+
writeHistory: options.writeHistory !== false,
|
|
64
|
+
now: options.now,
|
|
65
|
+
output: out,
|
|
66
|
+
applyD2Suppression: false,
|
|
67
|
+
});
|
|
68
|
+
const state = detectPriorState(projectRoot);
|
|
69
|
+
let reliefOffered = false;
|
|
70
|
+
if (state.wipCount >= state.wipCap) {
|
|
71
|
+
const relief = previewWipRelief(projectRoot);
|
|
72
|
+
if (relief.eligibleCount > 0) {
|
|
73
|
+
reliefOffered = true;
|
|
74
|
+
const demote = formatWelcomeCommand(["scope:demote", "--batch", "--older-than-days", String(relief.olderThanDays)], options.taskPrefix).replace(/\r?\n/g, " ");
|
|
75
|
+
const eligibleCount = String(relief.eligibleCount).replace(/\r?\n/g, " ");
|
|
76
|
+
out(`[welcome] WIP ${state.wipCount}/${state.wipCap} at/over cap -- ${eligibleCount} pending scope(s) ` +
|
|
77
|
+
`older than ${relief.olderThanDays}d are demote-eligible. Relieve with \`${demote}\`.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const capNote = wipCap !== null ? `, wipCap ${wipCap}` : ` (wipCap default ${DEFAULT_WIP_CAP})`;
|
|
81
|
+
out(`[welcome] Onboarding applied: triage scope preset '${preset}'${capNote}.`);
|
|
82
|
+
const bootstrap = formatWelcomeCommand(["triage:bootstrap"], options.taskPrefix);
|
|
83
|
+
const queue = formatWelcomeCommand(["triage:queue", "--limit=10"], options.taskPrefix);
|
|
84
|
+
out(`[welcome] Next: run \`${bootstrap}\` to populate the triage cache, then \`${queue}\` to pick work. See ${TRIAGE_SKILL_PATH}.`);
|
|
85
|
+
return {
|
|
86
|
+
exitCode: 0,
|
|
87
|
+
presetApplied: preset,
|
|
88
|
+
triageScopeChanged,
|
|
89
|
+
wipCapApplied: wipCap,
|
|
90
|
+
wipCapChanged,
|
|
91
|
+
reliefOffered,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=onboard.js.map
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2307: only comments authored by a repo maintainer may be treated as the
|
|
3
|
+
* authoritative current-shape state. GitHub's `author_association` on an issue
|
|
4
|
+
* comment is the trust signal; anything outside this set (CONTRIBUTOR, NONE,
|
|
5
|
+
* FIRST_TIME_CONTRIBUTOR, ...) is an untrusted third party and cannot forge a
|
|
6
|
+
* higher-pass current-shape comment.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAINTAINER_ASSOCIATIONS: ReadonlySet<string>;
|
|
9
|
+
/** True when a comment's author_association marks it as maintainer-authored (#2307). */
|
|
10
|
+
export declare function isMaintainerAuthored(association: string): boolean;
|
|
1
11
|
/** Matches `## Current shape (as of pass-N)` — same pattern as vbrief-reconcile/umbrellas.ts. */
|
|
2
12
|
export declare const CURRENT_SHAPE_HEADER_RE: RegExp;
|
|
3
13
|
export declare const SECTION_MARKERS: {
|
|
@@ -20,6 +30,8 @@ export interface IssueComment {
|
|
|
20
30
|
readonly body: string;
|
|
21
31
|
readonly htmlUrl: string;
|
|
22
32
|
readonly updatedAt: string;
|
|
33
|
+
readonly authorLogin: string;
|
|
34
|
+
readonly authorAssociation: string;
|
|
23
35
|
}
|
|
24
36
|
export interface CurrentShapeComment extends IssueComment {
|
|
25
37
|
readonly pass: number;
|
|
@@ -37,6 +49,8 @@ export interface CurrentShapeResult {
|
|
|
37
49
|
readonly htmlUrl: string;
|
|
38
50
|
readonly pass: number;
|
|
39
51
|
readonly body: string;
|
|
52
|
+
readonly authorLogin: string;
|
|
53
|
+
readonly authorAssociation: string;
|
|
40
54
|
readonly sections: SectionPresence;
|
|
41
55
|
}
|
|
42
56
|
export type ScmFetcher = (repo: string, issueNumber: number) => IssueComment[] | {
|
|
@@ -55,11 +69,20 @@ export interface RunCurrentShapeOptions {
|
|
|
55
69
|
/** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
|
|
56
70
|
export declare function parseCommentsFromGhStdout(stdout: string): IssueComment[];
|
|
57
71
|
export declare function extractPassFromBody(body: string): number | null;
|
|
58
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Pick the canonical comment — highest pass-N; tie-break by comment id (latest).
|
|
74
|
+
*
|
|
75
|
+
* #2307: only MAINTAINER-authored comments (author_association in
|
|
76
|
+
* {OWNER, MEMBER, COLLABORATOR}) are eligible. A non-maintainer higher-pass
|
|
77
|
+
* comment is ignored, which defeats the forged-higher-pass primitive: an
|
|
78
|
+
* attacker cannot inject authoritative state by simply commenting with a bigger
|
|
79
|
+
* pass number.
|
|
80
|
+
*/
|
|
59
81
|
export declare function selectCurrentShapeComment(comments: readonly IssueComment[]): CurrentShapeComment | null;
|
|
60
82
|
export declare function detectSections(body: string): SectionPresence;
|
|
61
83
|
export declare function sectionsRecord(presence: SectionPresence): Record<string, boolean>;
|
|
62
84
|
export declare const NO_CURRENT_SHAPE_MESSAGE: string;
|
|
85
|
+
export declare const NON_MAINTAINER_CURRENT_SHAPE_MESSAGE: string;
|
|
63
86
|
export declare function fetchCurrentShape(options: {
|
|
64
87
|
repo: string;
|
|
65
88
|
issueNumber: number;
|
|
@@ -75,6 +98,7 @@ export declare function fetchCurrentShape(options: {
|
|
|
75
98
|
export declare function emitCurrentShape(result: CurrentShapeResult, options: {
|
|
76
99
|
jsonMode: boolean;
|
|
77
100
|
writeOut: (text: string) => void;
|
|
101
|
+
writeErr?: (text: string) => void;
|
|
78
102
|
}): number;
|
|
79
103
|
export declare function runCurrentShape(options: RunCurrentShapeOptions): number;
|
|
80
104
|
export declare function parseCurrentShapeArgv(argv: readonly string[]): {
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { scan } from "../cache/scanner.js";
|
|
2
3
|
import { resolveBinary } from "../scm/binary.js";
|
|
3
4
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
4
5
|
import { resolveRepo } from "../triage/queue/repo.js";
|
|
6
|
+
/**
|
|
7
|
+
* #2307: only comments authored by a repo maintainer may be treated as the
|
|
8
|
+
* authoritative current-shape state. GitHub's `author_association` on an issue
|
|
9
|
+
* comment is the trust signal; anything outside this set (CONTRIBUTOR, NONE,
|
|
10
|
+
* FIRST_TIME_CONTRIBUTOR, ...) is an untrusted third party and cannot forge a
|
|
11
|
+
* higher-pass current-shape comment.
|
|
12
|
+
*/
|
|
13
|
+
export const MAINTAINER_ASSOCIATIONS = new Set([
|
|
14
|
+
"OWNER",
|
|
15
|
+
"MEMBER",
|
|
16
|
+
"COLLABORATOR",
|
|
17
|
+
]);
|
|
18
|
+
/** True when a comment's author_association marks it as maintainer-authored (#2307). */
|
|
19
|
+
export function isMaintainerAuthored(association) {
|
|
20
|
+
return MAINTAINER_ASSOCIATIONS.has(association.toUpperCase());
|
|
21
|
+
}
|
|
5
22
|
/** Matches `## Current shape (as of pass-N)` — same pattern as vbrief-reconcile/umbrellas.ts. */
|
|
6
23
|
export const CURRENT_SHAPE_HEADER_RE = /^## Current shape \(as of pass-(\d+)\)/m;
|
|
7
24
|
export const SECTION_MARKERS = {
|
|
@@ -35,11 +52,16 @@ function mapCommentEntry(entry) {
|
|
|
35
52
|
if (typeof rec.id !== "number" || typeof rec.body !== "string") {
|
|
36
53
|
return null;
|
|
37
54
|
}
|
|
55
|
+
const user = typeof rec.user === "object" && rec.user !== null
|
|
56
|
+
? rec.user
|
|
57
|
+
: null;
|
|
38
58
|
return {
|
|
39
59
|
id: rec.id,
|
|
40
60
|
body: rec.body,
|
|
41
61
|
htmlUrl: typeof rec.html_url === "string" ? rec.html_url : "",
|
|
42
62
|
updatedAt: typeof rec.updated_at === "string" ? rec.updated_at : "",
|
|
63
|
+
authorLogin: user !== null && typeof user.login === "string" ? user.login : "",
|
|
64
|
+
authorAssociation: typeof rec.author_association === "string" ? rec.author_association : "",
|
|
43
65
|
};
|
|
44
66
|
}
|
|
45
67
|
/** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
|
|
@@ -135,10 +157,21 @@ export function extractPassFromBody(body) {
|
|
|
135
157
|
const pass = Number(match[1]);
|
|
136
158
|
return Number.isFinite(pass) ? pass : null;
|
|
137
159
|
}
|
|
138
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Pick the canonical comment — highest pass-N; tie-break by comment id (latest).
|
|
162
|
+
*
|
|
163
|
+
* #2307: only MAINTAINER-authored comments (author_association in
|
|
164
|
+
* {OWNER, MEMBER, COLLABORATOR}) are eligible. A non-maintainer higher-pass
|
|
165
|
+
* comment is ignored, which defeats the forged-higher-pass primitive: an
|
|
166
|
+
* attacker cannot inject authoritative state by simply commenting with a bigger
|
|
167
|
+
* pass number.
|
|
168
|
+
*/
|
|
139
169
|
export function selectCurrentShapeComment(comments) {
|
|
140
170
|
let best = null;
|
|
141
171
|
for (const comment of comments) {
|
|
172
|
+
if (!isMaintainerAuthored(comment.authorAssociation)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
142
175
|
const pass = extractPassFromBody(comment.body);
|
|
143
176
|
if (pass === null) {
|
|
144
177
|
continue;
|
|
@@ -188,6 +221,16 @@ export function sectionsRecord(presence) {
|
|
|
188
221
|
export const NO_CURRENT_SHAPE_MESSAGE = "No ## Current shape (as of pass-N) comment found on this issue. " +
|
|
189
222
|
"Create one per AGENTS.md ## Umbrella current-shape convention (#1152) — " +
|
|
190
223
|
"do not fall back to the issue body (stale by design).";
|
|
224
|
+
// #2307 (Greptile review): a structurally valid current-shape comment authored
|
|
225
|
+
// by a non-maintainer is filtered out for provenance and would otherwise be
|
|
226
|
+
// reported with the generic "not found" message above -- indistinguishable from
|
|
227
|
+
// genuine absence, which makes --strict especially confusing to debug. This
|
|
228
|
+
// message names the provenance filter explicitly so the caller knows the
|
|
229
|
+
// comment exists but was ignored, not that it is missing.
|
|
230
|
+
export const NON_MAINTAINER_CURRENT_SHAPE_MESSAGE = "A ## Current shape (as of pass-N) comment exists but was authored by a " +
|
|
231
|
+
"non-maintainer (author_association not in OWNER/MEMBER/COLLABORATOR) and is " +
|
|
232
|
+
"ignored per AGENTS.md ## Umbrella current-shape convention (#1152 / #2307). " +
|
|
233
|
+
"A maintainer must (re-)post the current-shape comment for it to be authoritative.";
|
|
191
234
|
export function fetchCurrentShape(options) {
|
|
192
235
|
const fetcher = options.fetchComments ?? defaultFetchComments;
|
|
193
236
|
const fetched = fetcher(options.repo, options.issueNumber);
|
|
@@ -196,7 +239,15 @@ export function fetchCurrentShape(options) {
|
|
|
196
239
|
}
|
|
197
240
|
const selected = selectCurrentShapeComment(fetched);
|
|
198
241
|
if (selected === null) {
|
|
199
|
-
|
|
242
|
+
// Distinguish provenance-filtered absence from genuine absence (#2307).
|
|
243
|
+
const hadNonMaintainerShape = fetched.some((c) => extractPassFromBody(c.body) !== null && !isMaintainerAuthored(c.authorAssociation));
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: hadNonMaintainerShape
|
|
247
|
+
? NON_MAINTAINER_CURRENT_SHAPE_MESSAGE
|
|
248
|
+
: NO_CURRENT_SHAPE_MESSAGE,
|
|
249
|
+
kind: "not-found",
|
|
250
|
+
};
|
|
200
251
|
}
|
|
201
252
|
const sections = detectSections(selected.body);
|
|
202
253
|
return {
|
|
@@ -208,11 +259,36 @@ export function fetchCurrentShape(options) {
|
|
|
208
259
|
htmlUrl: selected.htmlUrl,
|
|
209
260
|
pass: selected.pass,
|
|
210
261
|
body: selected.body,
|
|
262
|
+
authorLogin: selected.authorLogin,
|
|
263
|
+
authorAssociation: selected.authorAssociation,
|
|
211
264
|
sections,
|
|
212
265
|
},
|
|
213
266
|
};
|
|
214
267
|
}
|
|
215
268
|
export function emitCurrentShape(result, options) {
|
|
269
|
+
// #2307: the selected comment body is still attacker-influencable text (a
|
|
270
|
+
// maintainer can quote/paste untrusted content). Run it through the same
|
|
271
|
+
// quarantine scanner used for cache content and emit the fenced transform so
|
|
272
|
+
// injection-shaped sections are quarantined, never rendered as authoritative
|
|
273
|
+
// instructions.
|
|
274
|
+
const scanned = scan(result.body);
|
|
275
|
+
// #2307 fail-closed (Greptile review): a scanner hard-fail (e.g. a credential
|
|
276
|
+
// pattern) is only FLAGGED by scan() -- detectCredentials sets passed=false
|
|
277
|
+
// but does NOT redact the secret from transformed_content. Emitting the
|
|
278
|
+
// transform regardless would forward the raw credential to stdout / CI logs /
|
|
279
|
+
// JSON consumers. Mirror buildIssueVbrief (#2306, issue:ingest), which throws
|
|
280
|
+
// ScannerHardFailError and writes nothing: refuse to emit and return non-zero.
|
|
281
|
+
if (!scanned.passed) {
|
|
282
|
+
const details = scanned.flags
|
|
283
|
+
.filter((f) => f.severity === "hard-fail")
|
|
284
|
+
.map((f) => f.detail)
|
|
285
|
+
.join("; ");
|
|
286
|
+
const writeErr = options.writeErr ?? ((text) => process.stderr.write(text));
|
|
287
|
+
writeErr(`umbrella:current-shape: refused issue #${result.issueNumber}: quarantine scanner hard-fail` +
|
|
288
|
+
(details.length > 0 ? ` (${details})` : "") +
|
|
289
|
+
" -- nothing written.\n");
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
216
292
|
if (options.jsonMode) {
|
|
217
293
|
const payload = {
|
|
218
294
|
issueNumber: result.issueNumber,
|
|
@@ -220,7 +296,10 @@ export function emitCurrentShape(result, options) {
|
|
|
220
296
|
commentId: result.commentId,
|
|
221
297
|
htmlUrl: result.htmlUrl,
|
|
222
298
|
pass: result.pass,
|
|
223
|
-
body:
|
|
299
|
+
body: scanned.transformed_content,
|
|
300
|
+
authorLogin: result.authorLogin,
|
|
301
|
+
authorAssociation: result.authorAssociation,
|
|
302
|
+
scannerPassed: scanned.passed,
|
|
224
303
|
sections: sectionsRecord(result.sections),
|
|
225
304
|
missingSections: result.sections.missing,
|
|
226
305
|
missingOptionalSections: result.sections.optionalMissing,
|
|
@@ -228,7 +307,7 @@ export function emitCurrentShape(result, options) {
|
|
|
228
307
|
options.writeOut(`${JSON.stringify(payload)}\n`);
|
|
229
308
|
}
|
|
230
309
|
else {
|
|
231
|
-
options.writeOut(`${
|
|
310
|
+
options.writeOut(`${scanned.transformed_content}\n`);
|
|
232
311
|
}
|
|
233
312
|
return 0;
|
|
234
313
|
}
|
|
@@ -260,6 +339,7 @@ export function runCurrentShape(options) {
|
|
|
260
339
|
return emitCurrentShape(fetched.result, {
|
|
261
340
|
jsonMode: options.jsonMode ?? false,
|
|
262
341
|
writeOut,
|
|
342
|
+
writeErr,
|
|
263
343
|
});
|
|
264
344
|
}
|
|
265
345
|
export function parseCurrentShapeArgv(argv) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared USER.md resolver (#2271).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the USER.md user-config file across an explicit first-hit-wins
|
|
5
|
+
* search order so session-start, the CLI bootstrap, and doctor all share a
|
|
6
|
+
* single source of truth for "where does USER.md live?" — and so it resolves
|
|
7
|
+
* automatically in mismatched / headless sandboxes where `$HOME` is not a
|
|
8
|
+
* persistent mount (the #2124 Cowork gap) with zero manual `DEFT_USER_PATH`.
|
|
9
|
+
*
|
|
10
|
+
* Search order (first hit wins):
|
|
11
|
+
* 1. `DEFT_USER_PATH` — explicit override, highest precedence, points at the
|
|
12
|
+
* USER.md file directly. Always wins as the resolved path whether or not
|
|
13
|
+
* the file exists yet (presence is reported separately via `found`).
|
|
14
|
+
* 2. Workspace-local bridged config (`<projectRoot>/.deft/USER.md`) — a
|
|
15
|
+
* persistent location inside the workspace that survives a non-persistent
|
|
16
|
+
* `$HOME`.
|
|
17
|
+
* 3. Bridged platform config dir (`%APPDATA%\deft\USER.md` on Windows,
|
|
18
|
+
* `~/.config/deft/USER.md` elsewhere) — the historical default location.
|
|
19
|
+
* 4. Sensible default (the rung-3 path) + a `no USER.md found; using
|
|
20
|
+
* defaults` diagnostic. This branch NEVER throws.
|
|
21
|
+
*/
|
|
22
|
+
/** Exact diagnostic phrase emitted when USER.md is absent on every rung. */
|
|
23
|
+
export declare const NO_USER_MD_DIAGNOSTIC = "no USER.md found; using defaults";
|
|
24
|
+
/** The USER.md filename joined onto every candidate config directory. */
|
|
25
|
+
export declare const USER_MD_FILENAME = "USER.md";
|
|
26
|
+
/**
|
|
27
|
+
* Workspace-local config directory (relative to the project root). This is the
|
|
28
|
+
* bridged, persistent location inside the workspace that keeps preferences
|
|
29
|
+
* reachable even when the platform config dir under `$HOME` is not mounted.
|
|
30
|
+
*/
|
|
31
|
+
export declare const WORKSPACE_LOCAL_CONFIG_DIR = ".deft";
|
|
32
|
+
/** Which search rung produced the resolved path. */
|
|
33
|
+
export type UserMdRung = "env-override" | "workspace-local" | "platform-config" | "default";
|
|
34
|
+
export interface ResolveUserMdResult {
|
|
35
|
+
/**
|
|
36
|
+
* The resolved USER.md path. For rungs 2-3 this is always an existing file.
|
|
37
|
+
* For rung 1 (`env-override`) it is the override path whether or not the file
|
|
38
|
+
* exists yet (check `found`). For the `default` rung it is the sensible
|
|
39
|
+
* default location (which does not exist).
|
|
40
|
+
*/
|
|
41
|
+
readonly path: string;
|
|
42
|
+
/** Which search rung matched. */
|
|
43
|
+
readonly rung: UserMdRung;
|
|
44
|
+
/** True when a USER.md file actually exists at `path`. */
|
|
45
|
+
readonly found: boolean;
|
|
46
|
+
/** One-line human-readable diagnostic describing the resolution boundary. */
|
|
47
|
+
readonly diagnostic: string;
|
|
48
|
+
/** Candidate paths inspected, in search order (for diagnostics / tests). */
|
|
49
|
+
readonly searched: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
export interface ResolveUserMdOptions {
|
|
52
|
+
/** Workspace root for the workspace-local rung. Defaults to `process.cwd()`. */
|
|
53
|
+
readonly projectRoot?: string;
|
|
54
|
+
/** Environment source (reads `DEFT_USER_PATH` / `APPDATA`). Defaults to `process.env`. */
|
|
55
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
56
|
+
/** Platform id for the platform-config rung. Defaults to `process.platform`. */
|
|
57
|
+
readonly platform?: NodeJS.Platform;
|
|
58
|
+
/** Home directory for the platform-config rung. Defaults to `os.homedir()`. */
|
|
59
|
+
readonly homeDir?: string;
|
|
60
|
+
/** File-existence probe. Defaults to an `existsSync` + `isFile` check. */
|
|
61
|
+
readonly fileExists?: (path: string) => boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the platform config directory (`%APPDATA%\deft` / `~/.config/deft`).
|
|
65
|
+
* Mirrors `init-deposit`'s `userConfigDir()` platform branch but takes the
|
|
66
|
+
* platform / env / home as injectable inputs and deliberately does NOT honor
|
|
67
|
+
* `DEFT_USER_PATH` — that override is rung 1, handled by the caller.
|
|
68
|
+
*/
|
|
69
|
+
export declare function platformUserConfigDir(platform: NodeJS.Platform, env: NodeJS.ProcessEnv, homeDir: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the USER.md path across the first-hit-wins search order. Never
|
|
72
|
+
* throws: an absent USER.md degrades to the sensible default location with a
|
|
73
|
+
* clear `no USER.md found; using defaults` diagnostic.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveUserMdPath(options?: ResolveUserMdOptions): ResolveUserMdResult;
|
|
76
|
+
//# sourceMappingURL=resolve-user-md.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared USER.md resolver (#2271).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the USER.md user-config file across an explicit first-hit-wins
|
|
5
|
+
* search order so session-start, the CLI bootstrap, and doctor all share a
|
|
6
|
+
* single source of truth for "where does USER.md live?" — and so it resolves
|
|
7
|
+
* automatically in mismatched / headless sandboxes where `$HOME` is not a
|
|
8
|
+
* persistent mount (the #2124 Cowork gap) with zero manual `DEFT_USER_PATH`.
|
|
9
|
+
*
|
|
10
|
+
* Search order (first hit wins):
|
|
11
|
+
* 1. `DEFT_USER_PATH` — explicit override, highest precedence, points at the
|
|
12
|
+
* USER.md file directly. Always wins as the resolved path whether or not
|
|
13
|
+
* the file exists yet (presence is reported separately via `found`).
|
|
14
|
+
* 2. Workspace-local bridged config (`<projectRoot>/.deft/USER.md`) — a
|
|
15
|
+
* persistent location inside the workspace that survives a non-persistent
|
|
16
|
+
* `$HOME`.
|
|
17
|
+
* 3. Bridged platform config dir (`%APPDATA%\deft\USER.md` on Windows,
|
|
18
|
+
* `~/.config/deft/USER.md` elsewhere) — the historical default location.
|
|
19
|
+
* 4. Sensible default (the rung-3 path) + a `no USER.md found; using
|
|
20
|
+
* defaults` diagnostic. This branch NEVER throws.
|
|
21
|
+
*/
|
|
22
|
+
import { statSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join, resolve } from "node:path";
|
|
25
|
+
/** Exact diagnostic phrase emitted when USER.md is absent on every rung. */
|
|
26
|
+
export const NO_USER_MD_DIAGNOSTIC = "no USER.md found; using defaults";
|
|
27
|
+
/** The USER.md filename joined onto every candidate config directory. */
|
|
28
|
+
export const USER_MD_FILENAME = "USER.md";
|
|
29
|
+
/**
|
|
30
|
+
* Workspace-local config directory (relative to the project root). This is the
|
|
31
|
+
* bridged, persistent location inside the workspace that keeps preferences
|
|
32
|
+
* reachable even when the platform config dir under `$HOME` is not mounted.
|
|
33
|
+
*/
|
|
34
|
+
export const WORKSPACE_LOCAL_CONFIG_DIR = ".deft";
|
|
35
|
+
function defaultFileExists(path) {
|
|
36
|
+
try {
|
|
37
|
+
return statSync(path).isFile();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Any stat error (ENOENT, permission, ...) means "not a resolvable USER.md
|
|
41
|
+
// file". Deliberately NOT falling back to existsSync, which cannot tell a
|
|
42
|
+
// directory from a file and would report a dir at this path as a found file.
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the platform config directory (`%APPDATA%\deft` / `~/.config/deft`).
|
|
48
|
+
* Mirrors `init-deposit`'s `userConfigDir()` platform branch but takes the
|
|
49
|
+
* platform / env / home as injectable inputs and deliberately does NOT honor
|
|
50
|
+
* `DEFT_USER_PATH` — that override is rung 1, handled by the caller.
|
|
51
|
+
*/
|
|
52
|
+
export function platformUserConfigDir(platform, env, homeDir) {
|
|
53
|
+
if (platform === "win32") {
|
|
54
|
+
const appData = env.APPDATA?.trim();
|
|
55
|
+
return appData ? join(appData, "deft") : join(homeDir, "AppData", "Roaming", "deft");
|
|
56
|
+
}
|
|
57
|
+
return join(homeDir, ".config", "deft");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the USER.md path across the first-hit-wins search order. Never
|
|
61
|
+
* throws: an absent USER.md degrades to the sensible default location with a
|
|
62
|
+
* clear `no USER.md found; using defaults` diagnostic.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveUserMdPath(options = {}) {
|
|
65
|
+
const env = options.env ?? process.env;
|
|
66
|
+
const platform = options.platform ?? process.platform;
|
|
67
|
+
const homeDir = options.homeDir ?? homedir();
|
|
68
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
69
|
+
const fileExists = options.fileExists ?? defaultFileExists;
|
|
70
|
+
const searched = [];
|
|
71
|
+
// Rung 1: DEFT_USER_PATH override (full file path); always wins.
|
|
72
|
+
const override = env.DEFT_USER_PATH?.trim();
|
|
73
|
+
if (override) {
|
|
74
|
+
const overridePath = resolve(override);
|
|
75
|
+
searched.push(overridePath);
|
|
76
|
+
const found = fileExists(overridePath);
|
|
77
|
+
return {
|
|
78
|
+
path: overridePath,
|
|
79
|
+
rung: "env-override",
|
|
80
|
+
found,
|
|
81
|
+
diagnostic: found
|
|
82
|
+
? `USER.md resolved from DEFT_USER_PATH: ${overridePath}`
|
|
83
|
+
: `USER.md path from DEFT_USER_PATH does not exist yet: ${overridePath}`,
|
|
84
|
+
searched,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Rung 2: workspace-local bridged config.
|
|
88
|
+
const workspacePath = resolve(join(projectRoot, WORKSPACE_LOCAL_CONFIG_DIR, USER_MD_FILENAME));
|
|
89
|
+
searched.push(workspacePath);
|
|
90
|
+
if (fileExists(workspacePath)) {
|
|
91
|
+
return {
|
|
92
|
+
path: workspacePath,
|
|
93
|
+
rung: "workspace-local",
|
|
94
|
+
found: true,
|
|
95
|
+
diagnostic: `USER.md resolved from workspace-local config: ${workspacePath}`,
|
|
96
|
+
searched,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Rung 3: bridged platform config dir.
|
|
100
|
+
const platformPath = join(platformUserConfigDir(platform, env, homeDir), USER_MD_FILENAME);
|
|
101
|
+
searched.push(platformPath);
|
|
102
|
+
if (fileExists(platformPath)) {
|
|
103
|
+
return {
|
|
104
|
+
path: platformPath,
|
|
105
|
+
rung: "platform-config",
|
|
106
|
+
found: true,
|
|
107
|
+
diagnostic: `USER.md resolved from platform config dir: ${platformPath}`,
|
|
108
|
+
searched,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Rung 4: sensible default + diagnostic (never throws).
|
|
112
|
+
return {
|
|
113
|
+
path: platformPath,
|
|
114
|
+
rung: "default",
|
|
115
|
+
found: false,
|
|
116
|
+
diagnostic: `${NO_USER_MD_DIAGNOSTIC} (searched: ${searched.join(", ")})`,
|
|
117
|
+
searched,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=resolve-user-md.js.map
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { JsonObject } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Absolute path to the PROJECT-DEFINITION artifact. Layout-aware (#2302):
|
|
4
|
+
* resolves `xbrief/PROJECT-DEFINITION.xbrief.json` on a migrated tree, else the
|
|
5
|
+
* legacy `vbrief/PROJECT-DEFINITION.vbrief.json`, so loader not-found messages
|
|
6
|
+
* name the path that actually applies to the project's layout.
|
|
7
|
+
*/
|
|
2
8
|
export declare function projectDefinitionPath(projectRoot: string): string;
|
|
3
9
|
export interface MutationLockDeps {
|
|
4
10
|
readonly sleepMs?: (ms: number) => void;
|
|
@@ -2,12 +2,17 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
5
|
-
import { PROJECT_DEFINITION_REL_PATH } from "./constants.js";
|
|
6
5
|
import { pythonJsonPretty } from "./json.js";
|
|
7
6
|
import { ProjectDefinitionIOError } from "./types.js";
|
|
8
7
|
const mutationThreadLock = { held: false };
|
|
8
|
+
/**
|
|
9
|
+
* Absolute path to the PROJECT-DEFINITION artifact. Layout-aware (#2302):
|
|
10
|
+
* resolves `xbrief/PROJECT-DEFINITION.xbrief.json` on a migrated tree, else the
|
|
11
|
+
* legacy `vbrief/PROJECT-DEFINITION.vbrief.json`, so loader not-found messages
|
|
12
|
+
* name the path that actually applies to the project's layout.
|
|
13
|
+
*/
|
|
9
14
|
export function projectDefinitionPath(projectRoot) {
|
|
10
|
-
return
|
|
15
|
+
return resolveProjectDefinitionPath(resolve(projectRoot));
|
|
11
16
|
}
|
|
12
17
|
function defaultSleep(ms) {
|
|
13
18
|
const end = Date.now() + ms;
|
|
@@ -103,9 +103,13 @@ export function detectPreCutover(projectRoot) {
|
|
|
103
103
|
/** Last release that ships `scripts/migrate_vbrief.py` on the consumer deposit path (#2068). */
|
|
104
104
|
export const FROZEN_PRECUTOVER_MIGRATION_TAG = "v0.59.0";
|
|
105
105
|
export function frozenPreCutoverMigrationGuidance() {
|
|
106
|
-
return (`Current npm releases no longer ship in-product \`task migrate:vbrief\`.
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
return (`Current npm releases no longer ship in-product \`task migrate:vbrief\`. Best-effort path (anchored on the ` +
|
|
107
|
+
`${FROZEN_PRECUTOVER_MIGRATION_TAG} git tag, from which GitHub serves a source tarball on demand): pin framework ` +
|
|
108
|
+
`${FROZEN_PRECUTOVER_MIGRATION_TAG}, install Python 3.11+ and uv, run \`task migrate:vbrief\` once from that payload, ` +
|
|
109
|
+
`then upgrade to current npm. This is a two-hop chain: pre-v0.20 flat model -> vBRIEF v0.6 (on ${FROZEN_PRECUTOVER_MIGRATION_TAG}) ` +
|
|
110
|
+
`-> xBRIEF via \`deft migrate:xbrief\` on current npm. If the ${FROZEN_PRECUTOVER_MIGRATION_TAG} payload is unavailable, ` +
|
|
111
|
+
`fall back to a manual fresh start: \`directive init\` a new project on current npm and hand-port your spec content. ` +
|
|
112
|
+
`See UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).`);
|
|
109
113
|
}
|
|
110
114
|
export function renderPrecutoverLine(projectRoot) {
|
|
111
115
|
const { preCutover, reasons } = detectPreCutover(projectRoot);
|
|
@@ -10,4 +10,14 @@ export declare const LEGACY_INFO_ROOT_KEY: "vBRIEFInfo";
|
|
|
10
10
|
export declare const MIGRATED_INFO_ROOT_KEY: "xBRIEFInfo";
|
|
11
11
|
/** Item-only status value added in xBRIEF v0.8. */
|
|
12
12
|
export declare const V08_ITEM_STATUS_AUTO: "auto";
|
|
13
|
+
/**
|
|
14
|
+
* Filename of the explicit deprecation marker written into a legacy `vbrief/`
|
|
15
|
+
* root that is retained for read-compatibility after convergence (#2270). Its
|
|
16
|
+
* presence is what tells `doctor` the folder is NOT an active source of truth.
|
|
17
|
+
*/
|
|
18
|
+
export declare const VBRIEF_DEPRECATION_MARKER_FILENAME: "DEPRECATED.md";
|
|
19
|
+
/** Sentinel line identifying a deft-written `vbrief/` deprecation marker (#2270). */
|
|
20
|
+
export declare const VBRIEF_DEPRECATION_MARKER_SENTINEL: "<!-- deft:vbrief-deprecated -->";
|
|
21
|
+
/** Body written into the retained legacy `vbrief/` deprecation marker (#2270). */
|
|
22
|
+
export declare const VBRIEF_DEPRECATION_MARKER_BODY: "<!-- deft:vbrief-deprecated -->\n# Deprecated: legacy `vbrief/` root\n\nThis project has migrated to the `xbrief/` lifecycle layout. This `vbrief/`\ndirectory is retained only for read-compatibility and is **not** an active\nsource of truth. Do not add new scope work here — use `xbrief/` instead.\n\nOnce you no longer need read-compatibility with the legacy layout, delete this\n`vbrief/` directory. Re-running `deft migrate:xbrief` re-checks convergence.\n";
|
|
13
23
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -11,4 +11,23 @@ export const LEGACY_INFO_ROOT_KEY = "vBRIEFInfo";
|
|
|
11
11
|
export const MIGRATED_INFO_ROOT_KEY = "xBRIEFInfo";
|
|
12
12
|
/** Item-only status value added in xBRIEF v0.8. */
|
|
13
13
|
export const V08_ITEM_STATUS_AUTO = "auto";
|
|
14
|
+
/**
|
|
15
|
+
* Filename of the explicit deprecation marker written into a legacy `vbrief/`
|
|
16
|
+
* root that is retained for read-compatibility after convergence (#2270). Its
|
|
17
|
+
* presence is what tells `doctor` the folder is NOT an active source of truth.
|
|
18
|
+
*/
|
|
19
|
+
export const VBRIEF_DEPRECATION_MARKER_FILENAME = "DEPRECATED.md";
|
|
20
|
+
/** Sentinel line identifying a deft-written `vbrief/` deprecation marker (#2270). */
|
|
21
|
+
export const VBRIEF_DEPRECATION_MARKER_SENTINEL = "<!-- deft:vbrief-deprecated -->";
|
|
22
|
+
/** Body written into the retained legacy `vbrief/` deprecation marker (#2270). */
|
|
23
|
+
export const VBRIEF_DEPRECATION_MARKER_BODY = `${VBRIEF_DEPRECATION_MARKER_SENTINEL}
|
|
24
|
+
# Deprecated: legacy \`vbrief/\` root
|
|
25
|
+
|
|
26
|
+
This project has migrated to the \`xbrief/\` lifecycle layout. This \`vbrief/\`
|
|
27
|
+
directory is retained only for read-compatibility and is **not** an active
|
|
28
|
+
source of truth. Do not add new scope work here — use \`xbrief/\` instead.
|
|
29
|
+
|
|
30
|
+
Once you no longer need read-compatibility with the legacy layout, delete this
|
|
31
|
+
\`vbrief/\` directory. Re-running \`deft migrate:xbrief\` re-checks convergence.
|
|
32
|
+
`;
|
|
14
33
|
//# sourceMappingURL=constants.js.map
|
|
@@ -3,9 +3,43 @@ export interface LegacyVbriefLayoutDetection {
|
|
|
3
3
|
legacyLayout: boolean;
|
|
4
4
|
reasons: string[];
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Discriminated convergence state of the `vbrief/` + `xbrief/` lifecycle roots
|
|
8
|
+
* (#2270). Distinguishes the ambiguous dual-empty-root case that `migrate:xbrief`
|
|
9
|
+
* must converge from the states that are already unambiguous.
|
|
10
|
+
*
|
|
11
|
+
* - `none` — neither root carries content (nothing to converge).
|
|
12
|
+
* - `xbrief-only` — canonical `xbrief/` with content and no `vbrief/`.
|
|
13
|
+
* - `xbrief-marker` — `vbrief/` retained behind an explicit deprecation marker.
|
|
14
|
+
* - `empty-vbrief` — a stray, fully-migrated empty `vbrief/` (the ambiguous root
|
|
15
|
+
* to clean up: remove it, or mark it when read-compat retention is requested).
|
|
16
|
+
* - `legacy-only` — `vbrief/` holds content and there is no canonical `xbrief/`;
|
|
17
|
+
* a full migration is required.
|
|
18
|
+
* - `dual-populated` — both roots hold content with no marker; converge by
|
|
19
|
+
* marking the legacy tree deprecated (never destructively merged).
|
|
20
|
+
*/
|
|
21
|
+
export type XbriefConvergenceState = "none" | "xbrief-only" | "xbrief-marker" | "empty-vbrief" | "legacy-only" | "dual-populated";
|
|
22
|
+
/** Structured convergence probe of the two lifecycle roots (#2270). */
|
|
23
|
+
export interface XbriefConvergenceDetection {
|
|
24
|
+
state: XbriefConvergenceState;
|
|
25
|
+
vbriefPresent: boolean;
|
|
26
|
+
vbriefEmpty: boolean;
|
|
27
|
+
vbriefHasMarker: boolean;
|
|
28
|
+
xbriefPresent: boolean;
|
|
29
|
+
xbriefHasContent: boolean;
|
|
30
|
+
}
|
|
6
31
|
/**
|
|
7
32
|
* Detect whether `projectRoot` still uses the legacy vbrief on-disk layout.
|
|
8
33
|
* Mirrors the structured-reasons pattern from `detectPreCutover` (#793).
|
|
9
34
|
*/
|
|
10
35
|
export declare function detectLegacyVbriefLayout(projectRoot: string): LegacyVbriefLayoutDetection;
|
|
36
|
+
/**
|
|
37
|
+
* Classify the convergence state of the `vbrief/` and `xbrief/` lifecycle roots
|
|
38
|
+
* so the orchestrator can converge to a single unambiguous root (#2270). Unlike
|
|
39
|
+
* {@link detectLegacyVbriefLayout} (which only asks "is there anything legacy?"),
|
|
40
|
+
* this probe distinguishes the ambiguous dual-empty-root case from an already
|
|
41
|
+
* unambiguous layout, and never falls back to a `run migrate:xbrief` dead end
|
|
42
|
+
* for a `vbrief/` that is merely an empty leftover.
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectXbriefConvergence(projectRoot: string): XbriefConvergenceDetection;
|
|
11
45
|
//# sourceMappingURL=detect.d.ts.map
|