@deftai/directive-core 0.75.0 → 0.76.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/agents-md-budget/evaluate.js +23 -3
- package/dist/agents-md-budget/skill-frontmatter.d.ts +2 -0
- package/dist/agents-md-budget/skill-frontmatter.js +4 -3
- package/dist/cache/operations.d.ts +2 -0
- package/dist/cache/operations.js +16 -1
- package/dist/init-deposit/scaffold.js +28 -17
- package/dist/intake/issue-ingest.js +23 -4
- package/dist/packaging/openpackage-tiers.d.ts +12 -0
- package/dist/packaging/openpackage-tiers.js +42 -0
- package/dist/preflight/evaluate.d.ts +5 -2
- package/dist/preflight/evaluate.js +9 -4
- package/dist/preflight/index.d.ts +1 -1
- package/dist/preflight/index.js +1 -1
- package/dist/release/pipeline-fixture.d.ts +3 -0
- package/dist/release/pipeline-fixture.js +11 -0
- package/dist/release/pipeline.js +4 -0
- package/dist/scope/transition.js +17 -2
- package/dist/scope/vbrief-ref.d.ts +6 -0
- package/dist/scope/vbrief-ref.js +21 -2
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/posture.d.ts +50 -0
- package/dist/session/posture.js +152 -0
- package/dist/session/ritual-sentinel.d.ts +2 -0
- package/dist/session/ritual-sentinel.js +3 -0
- package/dist/session/session-start.d.ts +8 -0
- package/dist/session/session-start.js +43 -0
- package/dist/session/verify-session-ritual.d.ts +6 -0
- package/dist/session/verify-session-ritual.js +67 -4
- package/dist/triage/bootstrap/gitignore.js +8 -4
- package/dist/triage/cache-path.js +16 -3
- package/dist/triage/welcome/writers.js +2 -0
- package/dist/ts-check-lane/index.d.ts +1 -1
- package/dist/ts-check-lane/index.js +1 -1
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +15 -0
- package/dist/vbrief-build/constants.d.ts +3 -3
- package/dist/vbrief-build/constants.js +4 -3
- package/dist/vbrief-build/index.d.ts +1 -1
- package/dist/vbrief-build/index.js +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { getOpenPackageDefaultInstallTier, resolveOpenPackageTierSkills, } from "../packaging/openpackage-tiers.js";
|
|
3
4
|
import { AGENTS_MANAGED_CLOSE } from "../platform/constants.js";
|
|
4
5
|
import { resolveAgentsMdBudget, } from "../policy/agents-md-budget.js";
|
|
5
6
|
import { measureSkillFrontmatter } from "./skill-frontmatter.js";
|
|
@@ -123,12 +124,23 @@ function resolveHarnessProfile(budget, projectRoot) {
|
|
|
123
124
|
}
|
|
124
125
|
return "none";
|
|
125
126
|
}
|
|
126
|
-
function
|
|
127
|
+
function resolveBootstrapSkillTier(budget, projectRoot) {
|
|
127
128
|
const env = process.env.DEFT_AGENTS_MD_BUDGET_SKILL_TIER?.trim();
|
|
128
129
|
if (env === "daily-core" || env === "all" || env === "none") {
|
|
129
130
|
return env;
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
+
if (budget?.skillFrontmatterTier !== undefined) {
|
|
133
|
+
return budget.skillFrontmatterTier;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
if (getOpenPackageDefaultInstallTier(projectRoot) === "daily-core") {
|
|
137
|
+
return "daily-core";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// No OpenPackage manifest at this project root — fall through.
|
|
142
|
+
}
|
|
143
|
+
return "all";
|
|
132
144
|
}
|
|
133
145
|
/** Measure managed + DD-3 skill frontmatter + bootstrap hooks. */
|
|
134
146
|
export function measureBootstrapSurface(projectRoot, managedText, budget) {
|
|
@@ -137,11 +149,19 @@ export function measureBootstrapSurface(projectRoot, managedText, budget) {
|
|
|
137
149
|
return managedResult;
|
|
138
150
|
}
|
|
139
151
|
const harnessProfile = resolveHarnessProfile(budget, projectRoot);
|
|
140
|
-
const tier =
|
|
152
|
+
const tier = resolveBootstrapSkillTier(budget, projectRoot);
|
|
153
|
+
let dailyCoreSkills;
|
|
154
|
+
try {
|
|
155
|
+
dailyCoreSkills = resolveOpenPackageTierSkills(projectRoot, "daily-core");
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Maintainer trees without packaging/openpackage fall back to hardcoded daily-core.
|
|
159
|
+
}
|
|
141
160
|
const skillFrontmatter = measureSkillFrontmatter(projectRoot, {
|
|
142
161
|
harnessProfile,
|
|
143
162
|
tier,
|
|
144
163
|
bytesPerToken: ABSOLUTE_BYTES_PER_TOKEN_ESTIMATE,
|
|
164
|
+
dailyCoreSkills,
|
|
145
165
|
});
|
|
146
166
|
const totalBytes = managedResult.bytes + skillFrontmatter.bytes + BOOTSTRAP_HOOK_BYTES;
|
|
147
167
|
return {
|
|
@@ -27,6 +27,8 @@ export interface MeasureSkillFrontmatterOptions {
|
|
|
27
27
|
readonly tier?: SkillFrontmatterTier;
|
|
28
28
|
readonly harnessProfile?: HarnessProfile;
|
|
29
29
|
readonly bytesPerToken?: number;
|
|
30
|
+
/** Override daily-core skill names (from OpenPackage resolveTierSkills). */
|
|
31
|
+
readonly dailyCoreSkills?: readonly string[];
|
|
30
32
|
}
|
|
31
33
|
/** Measure harness-injected skill frontmatter bytes for the configured profile/tier. */
|
|
32
34
|
export declare function measureSkillFrontmatter(projectRoot: string, options?: MeasureSkillFrontmatterOptions): SkillFrontmatterMeasure;
|
|
@@ -61,14 +61,14 @@ function listSkillDirs(skillsRoot) {
|
|
|
61
61
|
.map((entry) => entry.name)
|
|
62
62
|
.sort();
|
|
63
63
|
}
|
|
64
|
-
function
|
|
64
|
+
function skillInSelectedTier(skillName, tier, dailyCore) {
|
|
65
65
|
if (tier === "none") {
|
|
66
66
|
return false;
|
|
67
67
|
}
|
|
68
68
|
if (tier === "all") {
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
|
-
return
|
|
71
|
+
return dailyCore.has(skillName);
|
|
72
72
|
}
|
|
73
73
|
/** Measure harness-injected skill frontmatter bytes for the configured profile/tier. */
|
|
74
74
|
export function measureSkillFrontmatter(projectRoot, options = {}) {
|
|
@@ -76,6 +76,7 @@ export function measureSkillFrontmatter(projectRoot, options = {}) {
|
|
|
76
76
|
const tier = options.tier ?? "all";
|
|
77
77
|
const bytesPerToken = options.bytesPerToken ?? 4;
|
|
78
78
|
const skillsRoot = options.skillsRoot ?? defaultSkillsRoot(projectRoot);
|
|
79
|
+
const dailyCore = options.dailyCoreSkills !== undefined ? new Set(options.dailyCoreSkills) : DAILY_CORE_SET;
|
|
79
80
|
if (harnessProfile === "none" || tier === "none") {
|
|
80
81
|
return {
|
|
81
82
|
bytes: 0,
|
|
@@ -88,7 +89,7 @@ export function measureSkillFrontmatter(projectRoot, options = {}) {
|
|
|
88
89
|
}
|
|
89
90
|
const entries = [];
|
|
90
91
|
for (const skillName of listSkillDirs(skillsRoot)) {
|
|
91
|
-
if (!
|
|
92
|
+
if (!skillInSelectedTier(skillName, tier, dailyCore)) {
|
|
92
93
|
continue;
|
|
93
94
|
}
|
|
94
95
|
const skillPath = join(skillsRoot, skillName, "SKILL.md");
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
1
2
|
import { type CacheCaps, type EntryUsage } from "./quota.js";
|
|
2
3
|
import { SCANNER_VERSION } from "./scanner.js";
|
|
3
4
|
import { type Clock } from "./time.js";
|
|
4
5
|
import type { CacheGetOptions, CachePutOptions, GetResult, PutResult } from "./types.js";
|
|
6
|
+
export { ProjectionContainmentError as CacheContainmentError };
|
|
5
7
|
/** Write a cache entry (mirrors `cache.cache_put`). */
|
|
6
8
|
export declare function cachePut(source: string, key: string, raw: Record<string, unknown>, options?: CachePutOptions): PutResult;
|
|
7
9
|
/** Read a cache entry (mirrors `cache.cache_get`). */
|
package/dist/cache/operations.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
3
4
|
import { pyRepr } from "../scm/py-format.js";
|
|
4
5
|
import { ALLOWED_SOURCES, REPO_RE, SOURCE_TTL_SECONDS } from "./constants.js";
|
|
5
6
|
import { CacheCapBreachedError, CacheError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
|
|
@@ -10,6 +11,15 @@ import { enforceCaps, predictEvictionSet, resolveCaps, } from "./quota.js";
|
|
|
10
11
|
import { flagsForMeta, SCANNER_VERSION, scan } from "./scanner.js";
|
|
11
12
|
import { addSeconds, parseIso, systemClock, utcIso } from "./time.js";
|
|
12
13
|
import { validateMeta } from "./validate.js";
|
|
14
|
+
export { ProjectionContainmentError as CacheContainmentError };
|
|
15
|
+
/** Refuse symlink-escaping `.deft-cache` before mkdir/read/write (#2470). */
|
|
16
|
+
function assertWritableCachePath(cacheRoot, ...segments) {
|
|
17
|
+
const cacheAbs = resolve(cacheRoot);
|
|
18
|
+
const projectDir = dirname(cacheAbs);
|
|
19
|
+
const target = segments.length > 0 ? join(cacheAbs, ...segments) : cacheAbs;
|
|
20
|
+
assertProjectionContained(projectDir, target);
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
13
23
|
function renderContent(source, raw) {
|
|
14
24
|
if (source === "github-issue") {
|
|
15
25
|
const number = raw.number;
|
|
@@ -82,6 +92,7 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
82
92
|
}
|
|
83
93
|
const expires = addSeconds(fetched, ttl);
|
|
84
94
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
95
|
+
assertWritableCachePath(cacheRoot);
|
|
85
96
|
const edir = entryDir(source, key, cacheRoot);
|
|
86
97
|
const rawText = pythonJsonDump(raw);
|
|
87
98
|
const rawSize = Buffer.byteLength(rawText, "utf8");
|
|
@@ -182,6 +193,7 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
182
193
|
export function cacheGet(source, key, options = {}) {
|
|
183
194
|
const clock = options.clock ?? systemClock;
|
|
184
195
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
196
|
+
assertWritableCachePath(cacheRoot);
|
|
185
197
|
const allowStale = options.allowStale ?? true;
|
|
186
198
|
const edir = entryDir(source, key, cacheRoot);
|
|
187
199
|
const metaRelPath = `${source}/${key}/meta.json`;
|
|
@@ -220,6 +232,7 @@ export function cacheInvalidate(source, key, options = {}) {
|
|
|
220
232
|
const clock = options.clock ?? systemClock;
|
|
221
233
|
validateKey(source, key);
|
|
222
234
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
235
|
+
assertWritableCachePath(cacheRoot);
|
|
223
236
|
const edir = entryDir(source, key, cacheRoot);
|
|
224
237
|
const existed = existsSync(edir);
|
|
225
238
|
if (existed)
|
|
@@ -252,6 +265,7 @@ export function cachePrune(options = {}) {
|
|
|
252
265
|
throw new CacheError(`--older-than-days must be >= 0 (got ${JSON.stringify(olderThanDays)})`);
|
|
253
266
|
}
|
|
254
267
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
268
|
+
assertWritableCachePath(cacheRoot);
|
|
255
269
|
if (!existsSync(cacheRoot))
|
|
256
270
|
return [];
|
|
257
271
|
const cutoff = addSeconds(clock.now(), -olderThanDays * 24 * 60 * 60);
|
|
@@ -313,6 +327,7 @@ function collectMetaPathsUnder(srcRoot) {
|
|
|
313
327
|
export function cachePruneToCap(options = {}) {
|
|
314
328
|
const clock = options.clock ?? systemClock;
|
|
315
329
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
330
|
+
assertWritableCachePath(cacheRoot);
|
|
316
331
|
const resolved = options.caps ?? resolveCaps();
|
|
317
332
|
if (!resolved.maxBytes && !resolved.maxEntries)
|
|
318
333
|
return [];
|
|
@@ -10,12 +10,19 @@ import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
|
10
10
|
import { platform } from "node:os";
|
|
11
11
|
import { dirname, join, relative } from "node:path";
|
|
12
12
|
import { copyTree } from "../deposit/copy-tree.js";
|
|
13
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
13
14
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
14
15
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
15
16
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
16
17
|
import { installerManagedGuardEre } from "./hygiene.js";
|
|
17
18
|
export { CANONICAL_INSTALL_ROOT };
|
|
18
19
|
export const CORE_GLOB = ".deft/core/**";
|
|
20
|
+
/** Refuse init/update projection writes that escape via repo-controlled symlinks (#2446). */
|
|
21
|
+
function projectionTarget(projectDir, ...relSegments) {
|
|
22
|
+
const target = join(projectDir, ...relSegments);
|
|
23
|
+
assertProjectionContained(projectDir, target);
|
|
24
|
+
return target;
|
|
25
|
+
}
|
|
19
26
|
const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
|
|
20
27
|
const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
|
|
21
28
|
const FRAMEWORK_SELF_TEST_REL = ".deft/core/tests";
|
|
@@ -232,7 +239,7 @@ export const PIN_DEPENDENCY_NAME = "@deftai/directive";
|
|
|
232
239
|
*/
|
|
233
240
|
export function ensurePackageJsonPin(projectDir, version, io) {
|
|
234
241
|
const pinVersion = version.trim().replace(/^v/i, "");
|
|
235
|
-
const path =
|
|
242
|
+
const path = projectionTarget(projectDir, "package.json");
|
|
236
243
|
const existed = existsSync(path);
|
|
237
244
|
let pkg = {};
|
|
238
245
|
if (existed) {
|
|
@@ -284,7 +291,7 @@ export function writeAgentsMd(projectDir, deftDir, io) {
|
|
|
284
291
|
if (typeof newContent !== "string") {
|
|
285
292
|
throw new Error("AGENTS.md render produced no content");
|
|
286
293
|
}
|
|
287
|
-
const path =
|
|
294
|
+
const path = projectionTarget(projectDir, "AGENTS.md");
|
|
288
295
|
writeFileSync(path, newContent, "utf8");
|
|
289
296
|
if (state === "absent") {
|
|
290
297
|
io.printf("AGENTS.md created.\n");
|
|
@@ -294,9 +301,9 @@ export function writeAgentsMd(projectDir, deftDir, io) {
|
|
|
294
301
|
}
|
|
295
302
|
return true;
|
|
296
303
|
}
|
|
297
|
-
async function ensureVbriefLifecycleDirs(
|
|
304
|
+
async function ensureVbriefLifecycleDirs(projectDir) {
|
|
298
305
|
for (const sub of VBRIEF_LIFECYCLE_DIRS) {
|
|
299
|
-
const dir =
|
|
306
|
+
const dir = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, sub);
|
|
300
307
|
await mkdir(dir, { recursive: true, mode: 0o755 });
|
|
301
308
|
const gitkeep = join(dir, ".gitkeep");
|
|
302
309
|
try {
|
|
@@ -323,9 +330,9 @@ function vbriefLifecycleDirsPresent(consumerVbrief) {
|
|
|
323
330
|
});
|
|
324
331
|
}
|
|
325
332
|
export async function writeConsumerVbrief(projectDir, deftDir, io) {
|
|
326
|
-
const consumerVbrief =
|
|
327
|
-
const schemasDst =
|
|
328
|
-
const vbriefMdDst =
|
|
333
|
+
const consumerVbrief = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR);
|
|
334
|
+
const schemasDst = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, "schemas");
|
|
335
|
+
const vbriefMdDst = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, "vbrief.md");
|
|
329
336
|
const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
|
|
330
337
|
const vbriefMdPresent = existsSync(vbriefMdDst) && statSync(vbriefMdDst).isFile();
|
|
331
338
|
const lifecyclePresent = vbriefLifecycleDirsPresent(consumerVbrief);
|
|
@@ -352,20 +359,21 @@ export async function writeConsumerVbrief(projectDir, deftDir, io) {
|
|
|
352
359
|
writeFileSync(vbriefMdDst, VBRIEF_README_BODY, "utf8");
|
|
353
360
|
}
|
|
354
361
|
}
|
|
355
|
-
await ensureVbriefLifecycleDirs(
|
|
362
|
+
await ensureVbriefLifecycleDirs(projectDir);
|
|
356
363
|
io.printf("vbrief/ deposited at project root (schemas + vbrief.md + lifecycle dirs).\n");
|
|
357
364
|
return true;
|
|
358
365
|
}
|
|
359
366
|
export function writeAgentsSkills(projectDir, io) {
|
|
367
|
+
projectionTarget(projectDir, ".agents");
|
|
360
368
|
const allExist = AGENTS_SKILLS.every((skill) => existsSync(join(projectDir, ".agents", "skills", skill.dir, "SKILL.md")));
|
|
361
369
|
if (allExist) {
|
|
362
370
|
io.printf(".agents/skills/ already present — skipping.\n");
|
|
363
371
|
return false;
|
|
364
372
|
}
|
|
365
373
|
for (const skill of AGENTS_SKILLS) {
|
|
366
|
-
const dir =
|
|
374
|
+
const dir = projectionTarget(projectDir, ".agents", "skills", skill.dir);
|
|
367
375
|
mkdirSync(dir, { recursive: true });
|
|
368
|
-
const path =
|
|
376
|
+
const path = projectionTarget(projectDir, ".agents", "skills", skill.dir, "SKILL.md");
|
|
369
377
|
if (existsSync(path))
|
|
370
378
|
continue;
|
|
371
379
|
writeFileSync(path, skill.content, "utf8");
|
|
@@ -412,7 +420,7 @@ function insertDeftIncludeAfterIncludesLine(content) {
|
|
|
412
420
|
return { content, ok: false };
|
|
413
421
|
}
|
|
414
422
|
export function ensureTaskfile(projectDir, io) {
|
|
415
|
-
const path =
|
|
423
|
+
const path = projectionTarget(projectDir, "Taskfile.yml");
|
|
416
424
|
let existing = "";
|
|
417
425
|
if (existsSync(path)) {
|
|
418
426
|
existing = readFileSync(path, "utf8");
|
|
@@ -466,7 +474,7 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
466
474
|
io.printf(`git hooks source ${srcDir} absent — skipping hook wiring.\n`);
|
|
467
475
|
return false;
|
|
468
476
|
}
|
|
469
|
-
const dstDir =
|
|
477
|
+
const dstDir = projectionTarget(projectDir, ".githooks");
|
|
470
478
|
mkdirSync(dstDir, { recursive: true });
|
|
471
479
|
let filesDeposited = false;
|
|
472
480
|
for (const name of [...HOOK_FILENAMES, ...HOOK_SUPPORT_FILENAMES]) {
|
|
@@ -474,7 +482,7 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
474
482
|
if (!existsSync(src))
|
|
475
483
|
continue;
|
|
476
484
|
const data = readFileSync(src);
|
|
477
|
-
const dst =
|
|
485
|
+
const dst = projectionTarget(projectDir, ".githooks", name);
|
|
478
486
|
const existing = existsSync(dst) ? readFileSync(dst) : null;
|
|
479
487
|
const isHookScript = HOOK_FILENAMES.includes(name);
|
|
480
488
|
if (!existing?.equals(data)) {
|
|
@@ -656,7 +664,7 @@ function coreGuardWorkflowContent() {
|
|
|
656
664
|
' echo "OK: no mixed framework + app changes."\n');
|
|
657
665
|
}
|
|
658
666
|
export function ensureGitattributes(projectDir, io) {
|
|
659
|
-
const path =
|
|
667
|
+
const path = projectionTarget(projectDir, ".gitattributes");
|
|
660
668
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
661
669
|
const present = new Set(existing.split("\n").map((line) => line.trim()));
|
|
662
670
|
const additions = CORE_GITATTRIBUTES_LINES.filter((line) => !present.has(line));
|
|
@@ -691,7 +699,7 @@ function appendGreptilePattern(patterns, glob) {
|
|
|
691
699
|
return `${patterns}\n${glob}`;
|
|
692
700
|
}
|
|
693
701
|
export function ensureGreptileIgnore(projectDir, io) {
|
|
694
|
-
const path =
|
|
702
|
+
const path = projectionTarget(projectDir, "greptile.json");
|
|
695
703
|
const fileExisted = existsSync(path);
|
|
696
704
|
let raw = fileExisted ? readFileSync(path, "utf8") : "";
|
|
697
705
|
if (!raw.trim())
|
|
@@ -766,7 +774,7 @@ function insertCodeqlPathsIgnore(content, glob) {
|
|
|
766
774
|
return { content, ok: false };
|
|
767
775
|
}
|
|
768
776
|
export function ensureCodeqlPathsIgnore(projectDir, io) {
|
|
769
|
-
const path =
|
|
777
|
+
const path = projectionTarget(projectDir, CODEQL_CONFIG_REL);
|
|
770
778
|
if (!existsSync(path)) {
|
|
771
779
|
mkdirSync(dirname(path), { recursive: true });
|
|
772
780
|
writeFileSync(path, codeqlConfigDefault(), "utf8");
|
|
@@ -787,7 +795,7 @@ export function ensureCodeqlPathsIgnore(projectDir, io) {
|
|
|
787
795
|
return true;
|
|
788
796
|
}
|
|
789
797
|
export function ensureCoreGuardWorkflow(projectDir, io) {
|
|
790
|
-
const path =
|
|
798
|
+
const path = projectionTarget(projectDir, CORE_GUARD_WORKFLOW_REL);
|
|
791
799
|
const desired = coreGuardWorkflowContent();
|
|
792
800
|
if (existsSync(path)) {
|
|
793
801
|
const existing = readFileSync(path, "utf8");
|
|
@@ -866,6 +874,9 @@ export async function depositNeutralization(projectDir, io, options = {}) {
|
|
|
866
874
|
await step();
|
|
867
875
|
}
|
|
868
876
|
catch (cause) {
|
|
877
|
+
if (cause instanceof ProjectionContainmentError) {
|
|
878
|
+
throw cause;
|
|
879
|
+
}
|
|
869
880
|
io.printf(`Warning: neutralization step failed: ${String(cause)}\n`);
|
|
870
881
|
}
|
|
871
882
|
}
|
|
@@ -8,7 +8,7 @@ import { resolveProjectRoot } from "../scope/project-context.js";
|
|
|
8
8
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
9
9
|
import { slugify, TODAY } from "../vbrief-build/build.js";
|
|
10
10
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
11
|
-
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY,
|
|
11
|
+
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
|
|
12
12
|
import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
|
|
13
13
|
import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
|
|
14
14
|
export const INGEST_STATUSES = ["proposed", "pending", "active"];
|
|
@@ -78,7 +78,7 @@ function resolveIngestEmissionLayout(vbriefDir) {
|
|
|
78
78
|
return {
|
|
79
79
|
artifactSuffix: LEGACY_ARTIFACT_SUFFIX,
|
|
80
80
|
infoRootKey: LEGACY_INFO_ROOT_KEY,
|
|
81
|
-
infoVersion:
|
|
81
|
+
infoVersion: EMITTED_VBRIEF_VERSION,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
return {
|
|
@@ -300,11 +300,25 @@ export function issueCommentThread(issue) {
|
|
|
300
300
|
export function issueCommentsAlreadyFetched(issue) {
|
|
301
301
|
return Object.hasOwn(issue, ISSUE_COMMENT_THREAD_KEY);
|
|
302
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Run quarantine `scan()` on untrusted ingest text (#2447). Fail closed on a
|
|
305
|
+
* credential hard-fail; return the fenced/quarantined transform for soft signals.
|
|
306
|
+
*/
|
|
307
|
+
function scanUntrustedIngestText(issueNumber, text) {
|
|
308
|
+
const scanResult = scan(text);
|
|
309
|
+
const hardFails = scanResult.flags.filter((f) => f.severity === "hard-fail");
|
|
310
|
+
if (hardFails.length > 0) {
|
|
311
|
+
throw new ScannerHardFailError(issueNumber, scanResult.flags);
|
|
312
|
+
}
|
|
313
|
+
return scanResult.transformed_content;
|
|
314
|
+
}
|
|
303
315
|
export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
|
|
304
316
|
const number = Number(issue.number);
|
|
305
|
-
const
|
|
317
|
+
const titleRaw = (typeof issue.title === "string" && issue.title.length > 0
|
|
306
318
|
? issue.title
|
|
307
319
|
: `Issue #${number}`) || `Issue #${number}`;
|
|
320
|
+
// #2447: quarantine-scan issue title before it lands in agent-authoritative fields.
|
|
321
|
+
const title = scanUntrustedIngestText(number, titleRaw);
|
|
308
322
|
const url = (typeof issue.url === "string" && issue.url.length > 0 ? issue.url : "") ||
|
|
309
323
|
(repoUrl.length > 0 ? `${repoUrl}/issues/${number}` : "");
|
|
310
324
|
const bodyRaw = issue.body;
|
|
@@ -345,7 +359,12 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
|
|
|
345
359
|
if (labelNames.length > 0) {
|
|
346
360
|
narratives.Labels = labelNames.join(", ");
|
|
347
361
|
}
|
|
348
|
-
const
|
|
362
|
+
const planItemsRaw = bodyStr.length > 0 ? extractPlanItems(bodyStr) : [];
|
|
363
|
+
// #2447: scan each derived plan-item title (empty-body issues still scan title above).
|
|
364
|
+
const planItems = planItemsRaw.map((item) => ({
|
|
365
|
+
...item,
|
|
366
|
+
title: scanUntrustedIngestText(number, String(item.title ?? "")),
|
|
367
|
+
}));
|
|
349
368
|
const plan = {
|
|
350
369
|
title,
|
|
351
370
|
status: planStatus,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type OpenPackageTierName = "daily-core" | "standard" | "advanced";
|
|
2
|
+
export interface OpenPackageTierManifest {
|
|
3
|
+
readonly defaultInstallTier: OpenPackageTierName;
|
|
4
|
+
readonly tiers: Record<OpenPackageTierName, {
|
|
5
|
+
skills: string[];
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
/** Default OpenPackage install tier from deft-tiers.json (#2494). */
|
|
9
|
+
export declare function getOpenPackageDefaultInstallTier(repoRoot: string): OpenPackageTierName;
|
|
10
|
+
/** Skill names for an OpenPackage tier selection (`all` = every mapped skill). */
|
|
11
|
+
export declare function resolveOpenPackageTierSkills(repoRoot: string, tier: OpenPackageTierName | "all"): string[];
|
|
12
|
+
//# sourceMappingURL=openpackage-tiers.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const TIER_NAMES = ["daily-core", "standard", "advanced"];
|
|
4
|
+
function isOpenPackageTierName(value) {
|
|
5
|
+
return TIER_NAMES.includes(value);
|
|
6
|
+
}
|
|
7
|
+
function loadOpenPackageTierManifest(repoRoot) {
|
|
8
|
+
const path = join(repoRoot, "packaging", "openpackage", "deft-tiers.json");
|
|
9
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
10
|
+
if (parsed === null || typeof parsed !== "object" || !parsed.tiers) {
|
|
11
|
+
throw new Error("deft-tiers.json must be an object with a tiers field");
|
|
12
|
+
}
|
|
13
|
+
for (const tier of TIER_NAMES) {
|
|
14
|
+
const block = parsed.tiers[tier];
|
|
15
|
+
if (!block || !Array.isArray(block.skills)) {
|
|
16
|
+
throw new Error(`deft-tiers.json missing tiers.${tier}.skills`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const defaultInstallTier = parsed.defaultInstallTier ?? "daily-core";
|
|
20
|
+
if (!isOpenPackageTierName(defaultInstallTier)) {
|
|
21
|
+
throw new Error(`deft-tiers.json defaultInstallTier must be one of ${TIER_NAMES.join(", ")}; got ${defaultInstallTier}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
defaultInstallTier,
|
|
25
|
+
tiers: parsed.tiers,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveTierSkills(manifest, tier) {
|
|
29
|
+
if (tier === "all") {
|
|
30
|
+
return [...new Set(TIER_NAMES.flatMap((name) => manifest.tiers[name].skills))];
|
|
31
|
+
}
|
|
32
|
+
return [...manifest.tiers[tier].skills];
|
|
33
|
+
}
|
|
34
|
+
/** Default OpenPackage install tier from deft-tiers.json (#2494). */
|
|
35
|
+
export function getOpenPackageDefaultInstallTier(repoRoot) {
|
|
36
|
+
return loadOpenPackageTierManifest(repoRoot).defaultInstallTier;
|
|
37
|
+
}
|
|
38
|
+
/** Skill names for an OpenPackage tier selection (`all` = every mapped skill). */
|
|
39
|
+
export function resolveOpenPackageTierSkills(repoRoot, tier) {
|
|
40
|
+
return resolveTierSkills(loadOpenPackageTierManifest(repoRoot), tier);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=openpackage-tiers.js.map
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
export declare const ACTIVE_FOLDER = "active";
|
|
3
3
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
4
4
|
export declare const ELIGIBLE_STATUS = "running";
|
|
5
|
-
/** Actionable redirect appended to every reject path (#810). */
|
|
6
|
-
export declare const ACTIVATE_HINT = "Run `task vbrief:activate {path}` before spawning an implementation agent.";
|
|
5
|
+
/** Actionable redirect appended to every reject path (#810 / #2449). */
|
|
6
|
+
export declare const ACTIVATE_HINT = "Run `task scope:activate -- {path}` (or legacy `task vbrief:activate -- {path}`) before spawning an implementation agent.";
|
|
7
|
+
/** Lifecycle folder names eligible for implementation (#810). */
|
|
8
|
+
export declare const ELIGIBLE_LIFECYCLE_DIRS: readonly ["xbrief/active", "vbrief/active"];
|
|
9
|
+
export declare const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight -- xbrief/active/<story>.xbrief.json` (legacy: `task vbrief:preflight -- <path>`).";
|
|
7
10
|
/** Result of a vBRIEF preflight evaluation; mirrors the Python `evaluate` tuple. */
|
|
8
11
|
export interface EvaluateResult {
|
|
9
12
|
readonly exitCode: 0 | 1;
|
|
@@ -4,14 +4,17 @@ import { basename, dirname } from "node:path";
|
|
|
4
4
|
export const ACTIVE_FOLDER = "active";
|
|
5
5
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
6
6
|
export const ELIGIBLE_STATUS = "running";
|
|
7
|
-
/** Actionable redirect appended to every reject path (#810). */
|
|
8
|
-
export const ACTIVATE_HINT = "Run `task vbrief:activate {path}` before spawning an implementation agent.";
|
|
7
|
+
/** Actionable redirect appended to every reject path (#810 / #2449). */
|
|
8
|
+
export const ACTIVATE_HINT = "Run `task scope:activate -- {path}` (or legacy `task vbrief:activate -- {path}`) before spawning an implementation agent.";
|
|
9
|
+
/** Lifecycle folder names eligible for implementation (#810). */
|
|
10
|
+
export const ELIGIBLE_LIFECYCLE_DIRS = ["xbrief/active", "vbrief/active"];
|
|
11
|
+
export const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight -- xbrief/active/<story>.xbrief.json` (legacy: `task vbrief:preflight -- <path>`).";
|
|
9
12
|
/** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
|
|
10
13
|
export function formatActivateHint(path) {
|
|
11
14
|
return ACTIVATE_HINT.replace("{path}", () => path);
|
|
12
15
|
}
|
|
13
16
|
function buildReject(path, reason) {
|
|
14
|
-
return `${reason}\n ${formatActivateHint(path)}`;
|
|
17
|
+
return `${reason}\n ${PREFLIGHT_USAGE_HINT}\n ${formatActivateHint(path)}`;
|
|
15
18
|
}
|
|
16
19
|
/** Map Node `JSON.parse` errors to CPython `json.JSONDecodeError.msg` for parity (#1721). */
|
|
17
20
|
function nodeJsonErrorToPythonMsg(nodeMessage) {
|
|
@@ -91,10 +94,12 @@ export function evaluate(vbriefPath) {
|
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
96
|
const folder = basename(dirname(vbriefPath));
|
|
97
|
+
const parent = basename(dirname(dirname(vbriefPath)));
|
|
98
|
+
const lifecycleDir = `${parent}/${folder}`;
|
|
94
99
|
if (folder !== ACTIVE_FOLDER) {
|
|
95
100
|
return {
|
|
96
101
|
exitCode: 1,
|
|
97
|
-
message: buildReject(path, `
|
|
102
|
+
message: buildReject(path, `xBRIEF is in ${lifecycleDir}/ -- only xbrief/active/ (or legacy vbrief/active/) is eligible for implementation.`),
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
const record = payload;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { EvaluateResult } from "./evaluate.js";
|
|
2
|
-
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, } from "./evaluate.js";
|
|
2
|
+
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_LIFECYCLE_DIRS, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, PREFLIGHT_USAGE_HINT, } from "./evaluate.js";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/preflight/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, } from "./evaluate.js";
|
|
1
|
+
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_LIFECYCLE_DIRS, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, PREFLIGHT_USAGE_HINT, } from "./evaluate.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Seed a real on-disk project root for pipeline write-path tests (#2470). */
|
|
5
|
+
export function seedReleaseProjectDir(changelog = `## [Unreleased]\n\n### Added\n- x\n`) {
|
|
6
|
+
const dir = mkdtempSync(join(tmpdir(), "release-proj-"));
|
|
7
|
+
writeFileSync(join(dir, "CHANGELOG.md"), changelog, "utf8");
|
|
8
|
+
writeFileSync(join(dir, "ROADMAP.md"), "# Roadmap\n", "utf8");
|
|
9
|
+
return dir;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=pipeline-fixture.js.map
|
package/dist/release/pipeline.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { assertProjectionContained } from "../fs/projection-containment.js";
|
|
3
4
|
import { prependUpgradeBanner, promoteChangelog, sectionForVersion } from "./changelog.js";
|
|
4
5
|
import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, TOTAL_STEPS, VERIFY_DRAFT_INTERVAL_SECONDS, VERIFY_DRAFT_MAX_ATTEMPTS, } from "./constants.js";
|
|
5
6
|
import { checkTagAvailable, createGithubRelease, readTextFile, verifyReleaseDraft } from "./gh.js";
|
|
@@ -16,6 +17,7 @@ export function runPipeline(config, seams = {}) {
|
|
|
16
17
|
const version = config.version;
|
|
17
18
|
const today = (seams.todayIso ?? todayIso)();
|
|
18
19
|
const changelogPath = join(projectRoot, "CHANGELOG.md");
|
|
20
|
+
const roadmapPath = join(projectRoot, "ROADMAP.md");
|
|
19
21
|
const readFile = seams.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
20
22
|
const writeFile = seams.writeFile ?? ((p, c) => writeFileSync(p, c, "utf8"));
|
|
21
23
|
const fileExists = seams.fileExists ?? ((p) => existsSync(p));
|
|
@@ -149,6 +151,7 @@ export function runPipeline(config, seams = {}) {
|
|
|
149
151
|
emit(6, label, `DRYRUN (would rewrite CHANGELOG.md: ## [Unreleased] -> ## [${version}] - ${today}; new compare link added;${summaryNote})`);
|
|
150
152
|
}
|
|
151
153
|
else {
|
|
154
|
+
assertProjectionContained(projectRoot, changelogPath);
|
|
152
155
|
writeFile(changelogPath, promotedChangelog);
|
|
153
156
|
emit(6, label, `OK (## [${version}] - ${today};${summaryNote})`);
|
|
154
157
|
}
|
|
@@ -158,6 +161,7 @@ export function runPipeline(config, seams = {}) {
|
|
|
158
161
|
emit(7, label, "DRYRUN (would run task roadmap:render)");
|
|
159
162
|
}
|
|
160
163
|
else {
|
|
164
|
+
assertProjectionContained(projectRoot, roadmapPath);
|
|
161
165
|
const [ok, reason] = refreshRoadmapFn(projectRoot);
|
|
162
166
|
if (ok) {
|
|
163
167
|
emit(7, label, `OK (${reason})`);
|
package/dist/scope/transition.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
|
|
4
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
5
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
5
6
|
import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
|
|
6
7
|
import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
@@ -76,11 +77,25 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
76
77
|
message: `No-op: ${basename} is already in ${currentFolder}/ (status: ${currentStatus})`,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
80
|
+
const vbriefRoot = dirname(dirname(resolvedPath));
|
|
81
|
+
const projectRoot = dirname(vbriefRoot);
|
|
82
|
+
if (targetFolder !== null) {
|
|
83
|
+
const destDir = join(vbriefRoot, targetFolder);
|
|
84
|
+
try {
|
|
85
|
+
// #2447: refuse lifecycle moves when the destination folder escapes the checkout.
|
|
86
|
+
// Run before mutating the source file so a refusal leaves lifecycle state intact.
|
|
87
|
+
assertProjectionContained(projectRoot, destDir);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof ProjectionContainmentError) {
|
|
91
|
+
return { ok: false, message: err.message };
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
79
96
|
const nowIso = utcNowIso(now);
|
|
80
97
|
planObj.status = targetStatus;
|
|
81
98
|
planObj.updated = nowIso;
|
|
82
|
-
const vbriefRoot = dirname(dirname(resolvedPath));
|
|
83
|
-
const projectRoot = dirname(vbriefRoot);
|
|
84
99
|
if (act === "complete") {
|
|
85
100
|
stampCompletionMetadata(planObj, projectRoot, nowIso);
|
|
86
101
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
1
2
|
import { type ResolveLifecycleArtifactRefOptions } from "../layout/lifecycle-ref.js";
|
|
3
|
+
export { ProjectionContainmentError as LifecycleRefContainmentError };
|
|
4
|
+
/** Reject absolute or `..`-escaping lifecycle refs before path resolution (#2470). */
|
|
5
|
+
export declare function rejectEscapingLifecycleRel(rel: string): void;
|
|
6
|
+
/** Assert a resolved lifecycle artifact path stays under the lifecycle root (#2470). */
|
|
7
|
+
export declare function assertLifecycleArtifactContained(lifecycleRoot: string, resolvedPath: string): void;
|
|
2
8
|
/** Resolve a vBRIEF reference URI to an absolute path, or null. */
|
|
3
9
|
export declare function resolveVbriefRef(uri: unknown, vbriefDir: string, options?: ResolveLifecycleArtifactRefOptions): string | null;
|
|
4
10
|
/** Collect planRef values from the plan root and top-level items. */
|
package/dist/scope/vbrief-ref.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import { resolve, sep } from "node:path";
|
|
1
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
2
2
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
3
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
3
4
|
import { resolveLifecycleArtifactRef, } from "../layout/lifecycle-ref.js";
|
|
4
5
|
import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
|
|
6
|
+
export { ProjectionContainmentError as LifecycleRefContainmentError };
|
|
7
|
+
/** Reject absolute or `..`-escaping lifecycle refs before path resolution (#2470). */
|
|
8
|
+
export function rejectEscapingLifecycleRel(rel) {
|
|
9
|
+
if (isAbsolute(rel)) {
|
|
10
|
+
throw new ProjectionContainmentError(`lifecycle ref refused: absolute path ${rel} is not allowed under the lifecycle root`, { projectDir: "", targetPath: rel, offendingPath: rel });
|
|
11
|
+
}
|
|
12
|
+
const segments = rel.split(/[/\\]+/).filter((segment) => segment.length > 0);
|
|
13
|
+
if (segments.includes("..")) {
|
|
14
|
+
throw new ProjectionContainmentError(`lifecycle ref refused: path ${rel} contains parent traversal (..)`, { projectDir: "", targetPath: rel, offendingPath: rel });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Assert a resolved lifecycle artifact path stays under the lifecycle root (#2470). */
|
|
18
|
+
export function assertLifecycleArtifactContained(lifecycleRoot, resolvedPath) {
|
|
19
|
+
assertProjectionContained(lifecycleRoot, resolvedPath);
|
|
20
|
+
}
|
|
5
21
|
/** Resolve a vBRIEF reference URI to an absolute path, or null. */
|
|
6
22
|
export function resolveVbriefRef(uri, vbriefDir, options = {}) {
|
|
7
23
|
if (typeof uri !== "string" || uri.length === 0) {
|
|
@@ -17,7 +33,10 @@ export function resolveVbriefRef(uri, vbriefDir, options = {}) {
|
|
|
17
33
|
else {
|
|
18
34
|
rel = uri;
|
|
19
35
|
}
|
|
20
|
-
|
|
36
|
+
rejectEscapingLifecycleRel(rel);
|
|
37
|
+
const resolved = resolveLifecycleArtifactRef(rel, vbriefDir, options);
|
|
38
|
+
assertLifecycleArtifactContained(vbriefDir, resolved);
|
|
39
|
+
return resolved;
|
|
21
40
|
}
|
|
22
41
|
/** Collect planRef values from the plan root and top-level items. */
|
|
23
42
|
export function collectPlanRefs(plan) {
|