@deftai/directive-core 0.73.1 → 0.74.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/check/orchestrator.d.ts +3 -0
- package/dist/check/orchestrator.js +2 -1
- package/dist/codebase/map.js +2 -0
- package/dist/doctor/checks.d.ts +9 -0
- package/dist/doctor/checks.js +67 -0
- package/dist/doctor/main.js +2 -1
- package/dist/fs/projection-containment.d.ts +35 -0
- package/dist/fs/projection-containment.js +118 -0
- package/dist/init-deposit/scaffold.d.ts +21 -0
- package/dist/init-deposit/scaffold.js +79 -6
- package/dist/pr-merge-readiness/constants.d.ts +4 -0
- package/dist/pr-merge-readiness/constants.js +4 -0
- package/dist/pr-merge-readiness/evaluate.js +3 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +8 -6
- package/dist/pr-merge-readiness/mergeability.js +15 -10
- package/dist/pr-merge-readiness/output.js +12 -6
- package/dist/pr-merge-readiness/parse.d.ts +1 -0
- package/dist/pr-merge-readiness/parse.js +9 -1
- package/dist/pr-merge-readiness/types.d.ts +2 -0
- package/dist/preflight-cache/evaluate.d.ts +2 -0
- package/dist/preflight-cache/evaluate.js +14 -3
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +2 -0
- package/dist/release/preflight.d.ts +2 -0
- package/dist/release/preflight.js +14 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.js +24 -1
- package/dist/scope/open-umbrella-warning.d.ts +12 -0
- package/dist/scope/open-umbrella-warning.js +403 -0
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +69 -58
- package/dist/vbrief-reconcile/umbrellas.js +12 -11
- package/dist/xbrief-migrate/migrate-project.js +9 -0
- package/package.json +3 -3
|
@@ -19,10 +19,13 @@ export interface CheckOrchestratorSeams {
|
|
|
19
19
|
readonly spawnFn?: (cmd: string, args: string[], opts: {
|
|
20
20
|
cwd: string;
|
|
21
21
|
stdio: string;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
22
23
|
}) => {
|
|
23
24
|
status: number | null;
|
|
24
25
|
error?: Error;
|
|
25
26
|
};
|
|
27
|
+
/** Child-process environment (default: process.env). */
|
|
28
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* True when `path` is the directive framework source checkout root (not a
|
|
@@ -77,6 +77,7 @@ export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
|
|
|
77
77
|
const result = spawn(taskBin, [target, "--taskfile", taskfilePath], {
|
|
78
78
|
cwd,
|
|
79
79
|
stdio: "inherit",
|
|
80
|
+
env: seams.env,
|
|
80
81
|
});
|
|
81
82
|
if (result.error !== undefined) {
|
|
82
83
|
process.stderr.write(`check: failed to invoke task: ${result.error.message}\n`);
|
|
@@ -88,7 +89,7 @@ function defaultSpawn(cmd, args, opts) {
|
|
|
88
89
|
const result = spawnSync(cmd, args, {
|
|
89
90
|
cwd: opts.cwd,
|
|
90
91
|
stdio: opts.stdio,
|
|
91
|
-
env:
|
|
92
|
+
env: opts.env ?? process.env,
|
|
92
93
|
});
|
|
93
94
|
return { status: result.status, error: result.error };
|
|
94
95
|
}
|
package/dist/codebase/map.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { assertProjectionContained } from "../fs/projection-containment.js";
|
|
3
4
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
4
5
|
import { extractCodeStructure, loadJsonFile } from "../verify-source/code-structure-validate.js";
|
|
5
6
|
import { CodeStructureConfigError, configErrorToDict, defaultCodeStructurePath, } from "./default-extractor.js";
|
|
@@ -239,6 +240,7 @@ export function writeCodebaseMap(projectRoot, options) {
|
|
|
239
240
|
const resolvedOutput = isAbsolute(options.outputPath)
|
|
240
241
|
? options.outputPath
|
|
241
242
|
: join(projectRoot, options.outputPath);
|
|
243
|
+
assertProjectionContained(projectRoot, resolvedOutput);
|
|
242
244
|
if (!options.force && !isDeftGenerated(resolvedOutput)) {
|
|
243
245
|
throw new Error(`refusing to overwrite non-generated MAP at ${resolvedOutput}; ` +
|
|
244
246
|
`missing '${GENERATED_SENTINEL}' banner`);
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -30,6 +30,15 @@ export declare function checkCanonicalVendoredNpmSignpost(projectRoot: string, s
|
|
|
30
30
|
* deposit is still a working install, just an unpinned one.
|
|
31
31
|
*/
|
|
32
32
|
export declare function checkManifestVersionReportable(projectRoot: string, installRoot: string | null, seams?: CheckSeams): CheckResult;
|
|
33
|
+
/** Relative path to the deposited v0.6 schema file superseded by xbrief-core-0.8.schema.json. */
|
|
34
|
+
export declare const STALE_VBRIEF_CORE_SCHEMA_REL: string;
|
|
35
|
+
/**
|
|
36
|
+
* #2368: on an already-migrated xbrief/ layout, a stale deposited
|
|
37
|
+
* `xbrief/schemas/vbrief-core.schema.json` (vBRIEFInfo v0.6) must NOT route
|
|
38
|
+
* operators to `migrate:xbrief` — that verb requires a vbrief/ tree. Point at
|
|
39
|
+
* `directive update` instead. Advisory only (exit-exempt).
|
|
40
|
+
*/
|
|
41
|
+
export declare function checkStaleXbriefSchemaDeposit(projectRoot: string, seams?: CheckSeams): CheckResult;
|
|
33
42
|
/**
|
|
34
43
|
* #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
|
|
35
44
|
* entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
|
package/dist/doctor/checks.js
CHANGED
|
@@ -5,6 +5,8 @@ import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY
|
|
|
5
5
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
6
6
|
import { findSkillPathsInText } from "../text/redos-safe.js";
|
|
7
7
|
import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
|
|
8
|
+
import { LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
9
|
+
import { detectXbriefConvergence } from "../xbrief-migrate/detect.js";
|
|
8
10
|
import { CANONICAL_UPGRADE_COMMAND, GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL, } from "./constants.js";
|
|
9
11
|
import { isDeprecationRedirectStub, locateManifest, manifestCandidatePaths, manifestReportableVersion, manifestTagToVersion, parseInstallManifest, parseInstallRootFromAgentsMd, parseManifest, } from "./manifest.js";
|
|
10
12
|
import { readTextSafe } from "./paths.js";
|
|
@@ -459,6 +461,68 @@ function collectGitignorePresent(text) {
|
|
|
459
461
|
function gitignoreLineIsCovered(present, line) {
|
|
460
462
|
return present.has(line);
|
|
461
463
|
}
|
|
464
|
+
/** Relative path to the deposited v0.6 schema file superseded by xbrief-core-0.8.schema.json. */
|
|
465
|
+
export const STALE_VBRIEF_CORE_SCHEMA_REL = join(MIGRATED_ARTIFACT_DIR, "schemas", "vbrief-core.schema.json");
|
|
466
|
+
const MIGRATED_XBRIEF_LAYOUT_STATES = new Set([
|
|
467
|
+
"xbrief-only",
|
|
468
|
+
"xbrief-marker",
|
|
469
|
+
]);
|
|
470
|
+
/**
|
|
471
|
+
* #2368: on an already-migrated xbrief/ layout, a stale deposited
|
|
472
|
+
* `xbrief/schemas/vbrief-core.schema.json` (vBRIEFInfo v0.6) must NOT route
|
|
473
|
+
* operators to `migrate:xbrief` — that verb requires a vbrief/ tree. Point at
|
|
474
|
+
* `directive update` instead. Advisory only (exit-exempt).
|
|
475
|
+
*/
|
|
476
|
+
export function checkStaleXbriefSchemaDeposit(projectRoot, seams = {}) {
|
|
477
|
+
const checkName = "stale-xbrief-schema-deposit";
|
|
478
|
+
const convergence = detectXbriefConvergence(projectRoot);
|
|
479
|
+
if (!MIGRATED_XBRIEF_LAYOUT_STATES.has(convergence.state)) {
|
|
480
|
+
return {
|
|
481
|
+
name: checkName,
|
|
482
|
+
status: "skip",
|
|
483
|
+
detail: "Project is not on a fully migrated xbrief/ layout; stale schema deposit check does not apply.",
|
|
484
|
+
data: { convergence_state: convergence.state },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const schemaPath = join(projectRoot, STALE_VBRIEF_CORE_SCHEMA_REL);
|
|
488
|
+
const isFile = seams.isFile ?? ((p) => readText(p, seams) !== null);
|
|
489
|
+
if (!isFile(schemaPath)) {
|
|
490
|
+
return {
|
|
491
|
+
name: checkName,
|
|
492
|
+
status: "skip",
|
|
493
|
+
detail: `No deposited schema at ${STALE_VBRIEF_CORE_SCHEMA_REL}.`,
|
|
494
|
+
data: { schema_path: schemaPath },
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const text = readText(schemaPath, seams);
|
|
498
|
+
if (text === null) {
|
|
499
|
+
return {
|
|
500
|
+
name: checkName,
|
|
501
|
+
status: "skip",
|
|
502
|
+
detail: `Deposited schema at ${STALE_VBRIEF_CORE_SCHEMA_REL} is unreadable.`,
|
|
503
|
+
data: { schema_path: schemaPath },
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (!text.includes(`"${LEGACY_INFO_ROOT_KEY}"`)) {
|
|
507
|
+
return {
|
|
508
|
+
name: checkName,
|
|
509
|
+
status: "pass",
|
|
510
|
+
detail: `Deposited schema at ${STALE_VBRIEF_CORE_SCHEMA_REL} is current (no ${LEGACY_INFO_ROOT_KEY} root key).`,
|
|
511
|
+
data: { schema_path: schemaPath },
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
name: checkName,
|
|
516
|
+
status: "fail",
|
|
517
|
+
detail: `Stale deposited schema at ${STALE_VBRIEF_CORE_SCHEMA_REL} still carries the legacy ${LEGACY_INFO_ROOT_KEY} root key on an already-migrated xbrief/ layout. ` +
|
|
518
|
+
"Run `directive update` to refresh deposited schema files — not `deft migrate:xbrief` (no vbrief/ tree remains to convert).",
|
|
519
|
+
data: {
|
|
520
|
+
schema_path: schemaPath,
|
|
521
|
+
convergence_state: convergence.state,
|
|
522
|
+
suggested_fix: "directive update",
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
462
526
|
/**
|
|
463
527
|
* #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
|
|
464
528
|
* entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
|
|
@@ -515,6 +579,7 @@ export function deriveExitCode(checks, errors) {
|
|
|
515
579
|
"canonical-vendored-npm-signpost",
|
|
516
580
|
"manifest-version-reportable",
|
|
517
581
|
"gitignore-coverage",
|
|
582
|
+
"stale-xbrief-schema-deposit",
|
|
518
583
|
]);
|
|
519
584
|
if (errors.length > 0 || checks.some((c) => c.status === "error")) {
|
|
520
585
|
return 2;
|
|
@@ -554,6 +619,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
554
619
|
checks.push(checkManifestVersionReportable(projectRoot, null, seams));
|
|
555
620
|
checks.push(checkLegacyLayout(projectRoot, seams));
|
|
556
621
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
622
|
+
checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
|
|
557
623
|
checks.push(checkGitignoreCoverage(projectRoot, seams));
|
|
558
624
|
return {
|
|
559
625
|
projectRoot,
|
|
@@ -570,6 +636,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
570
636
|
checks.push(checkInstallPathConsistency(projectRoot, installRoot, seams));
|
|
571
637
|
checks.push(checkLegacyLayout(projectRoot, seams));
|
|
572
638
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
639
|
+
checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
|
|
573
640
|
checks.push(checkGitignoreCoverage(projectRoot, seams));
|
|
574
641
|
return {
|
|
575
642
|
projectRoot,
|
package/dist/doctor/main.js
CHANGED
|
@@ -374,7 +374,8 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
|
|
|
374
374
|
if ((name === "legacy-layout" ||
|
|
375
375
|
name === "canonical-vendored-npm-signpost" ||
|
|
376
376
|
name === "manifest-version-reportable" ||
|
|
377
|
-
name === "gitignore-coverage"
|
|
377
|
+
name === "gitignore-coverage" ||
|
|
378
|
+
name === "stale-xbrief-schema-deposit") &&
|
|
378
379
|
status === "fail") {
|
|
379
380
|
sink.warn(`${name}: ${detail}`);
|
|
380
381
|
addFinding({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projection-containment.ts — repo-controlled projection symlink guard (#2413).
|
|
3
|
+
*
|
|
4
|
+
* Projection writers (`codebase:map`, `triage:bootstrap`) write to well-known
|
|
5
|
+
* paths such as `.planning/codebase/MAP.md`, `.gitignore`, and `.gitattributes`.
|
|
6
|
+
* If a malicious repo author commits one of those paths (or a parent) as a
|
|
7
|
+
* symlink escaping the project tree, routine operator commands would follow the
|
|
8
|
+
* link and create/overwrite files outside the checkout.
|
|
9
|
+
*
|
|
10
|
+
* `assertProjectionContained` anchors on `realpath(projectDir)`, walks each
|
|
11
|
+
* existing component down to the write target, rejects symlinks that escape the
|
|
12
|
+
* tree, and asserts the deepest existing ancestor stays under the project root.
|
|
13
|
+
*
|
|
14
|
+
* Refs #2413.
|
|
15
|
+
*/
|
|
16
|
+
/** Non-zero exit code for a projection-containment refusal (needs-action). */
|
|
17
|
+
export declare const PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE = 2;
|
|
18
|
+
/** Thrown when a projection write target escapes the project tree. */
|
|
19
|
+
export declare class ProjectionContainmentError extends Error {
|
|
20
|
+
readonly projectDir: string;
|
|
21
|
+
readonly targetPath: string;
|
|
22
|
+
readonly offendingPath: string;
|
|
23
|
+
constructor(message: string, details: {
|
|
24
|
+
projectDir: string;
|
|
25
|
+
targetPath: string;
|
|
26
|
+
offendingPath: string;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Refuse projection writes when the target (or a parent) is a symlink that
|
|
31
|
+
* escapes the resolved project tree. Throws {@link ProjectionContainmentError};
|
|
32
|
+
* MUST be called BEFORE any projection read/write/mkdir.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertProjectionContained(projectDir: string, targetPath: string): void;
|
|
35
|
+
//# sourceMappingURL=projection-containment.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projection-containment.ts — repo-controlled projection symlink guard (#2413).
|
|
3
|
+
*
|
|
4
|
+
* Projection writers (`codebase:map`, `triage:bootstrap`) write to well-known
|
|
5
|
+
* paths such as `.planning/codebase/MAP.md`, `.gitignore`, and `.gitattributes`.
|
|
6
|
+
* If a malicious repo author commits one of those paths (or a parent) as a
|
|
7
|
+
* symlink escaping the project tree, routine operator commands would follow the
|
|
8
|
+
* link and create/overwrite files outside the checkout.
|
|
9
|
+
*
|
|
10
|
+
* `assertProjectionContained` anchors on `realpath(projectDir)`, walks each
|
|
11
|
+
* existing component down to the write target, rejects symlinks that escape the
|
|
12
|
+
* tree, and asserts the deepest existing ancestor stays under the project root.
|
|
13
|
+
*
|
|
14
|
+
* Refs #2413.
|
|
15
|
+
*/
|
|
16
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
17
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
18
|
+
/** Non-zero exit code for a projection-containment refusal (needs-action). */
|
|
19
|
+
export const PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE = 2;
|
|
20
|
+
/** Thrown when a projection write target escapes the project tree. */
|
|
21
|
+
export class ProjectionContainmentError extends Error {
|
|
22
|
+
projectDir;
|
|
23
|
+
targetPath;
|
|
24
|
+
offendingPath;
|
|
25
|
+
constructor(message, details) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "ProjectionContainmentError";
|
|
28
|
+
this.projectDir = details.projectDir;
|
|
29
|
+
this.targetPath = details.targetPath;
|
|
30
|
+
this.offendingPath = details.offendingPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Path-SEGMENT containment: is `child` equal to `parent` or nested under it?
|
|
35
|
+
* Uses `path.relative` so `/foo` is NOT treated as containing `/foobar`.
|
|
36
|
+
*/
|
|
37
|
+
function isContained(parent, child) {
|
|
38
|
+
if (parent === child) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const rel = relative(parent, child);
|
|
42
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Refuse projection writes when the target (or a parent) is a symlink that
|
|
46
|
+
* escapes the resolved project tree. Throws {@link ProjectionContainmentError};
|
|
47
|
+
* MUST be called BEFORE any projection read/write/mkdir.
|
|
48
|
+
*/
|
|
49
|
+
export function assertProjectionContained(projectDir, targetPath) {
|
|
50
|
+
const projectAbs = resolve(projectDir);
|
|
51
|
+
let projectReal;
|
|
52
|
+
try {
|
|
53
|
+
projectReal = realpathSync(projectAbs);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new ProjectionContainmentError(`projection write refused: project directory ${projectAbs} does not exist`, { projectDir: projectAbs, targetPath: resolve(targetPath), offendingPath: projectAbs });
|
|
57
|
+
}
|
|
58
|
+
const targetAbs = resolve(targetPath);
|
|
59
|
+
const rel = relative(projectAbs, targetAbs);
|
|
60
|
+
if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel)) {
|
|
61
|
+
throw new ProjectionContainmentError(`projection write refused: target ${targetAbs} is not nested under the project tree ${projectAbs}`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: targetAbs });
|
|
62
|
+
}
|
|
63
|
+
const segments = rel.split(/[\\/]+/).filter((segment) => segment.length > 0);
|
|
64
|
+
let current = projectAbs;
|
|
65
|
+
let deepestExistingReal = projectReal;
|
|
66
|
+
for (const segment of segments) {
|
|
67
|
+
current = join(current, segment);
|
|
68
|
+
let info;
|
|
69
|
+
try {
|
|
70
|
+
info = lstatSync(current);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (info.isSymbolicLink()) {
|
|
76
|
+
let linkReal;
|
|
77
|
+
try {
|
|
78
|
+
linkReal = realpathSync(current);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new ProjectionContainmentError(`projection write refused: ${current} is a broken/dangling symlink on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
82
|
+
}
|
|
83
|
+
if (!isContained(projectReal, linkReal)) {
|
|
84
|
+
throw new ProjectionContainmentError(`projection write refused: ${current} is a symlink escaping the project tree ` +
|
|
85
|
+
`(resolves to ${linkReal}, outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
86
|
+
}
|
|
87
|
+
deepestExistingReal = linkReal;
|
|
88
|
+
current = linkReal;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
try {
|
|
92
|
+
deepestExistingReal = realpathSync(current);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new ProjectionContainmentError(`projection write refused: could not resolve ${current} on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const targetReal = realpathSync(targetAbs);
|
|
101
|
+
if (!isContained(projectReal, targetReal)) {
|
|
102
|
+
throw new ProjectionContainmentError(`projection write refused: projection target resolves outside the project tree ` +
|
|
103
|
+
`(${targetReal} is outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: targetReal });
|
|
104
|
+
}
|
|
105
|
+
deepestExistingReal = targetReal;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof ProjectionContainmentError) {
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
// Target does not exist yet; parent walk above is sufficient.
|
|
112
|
+
}
|
|
113
|
+
if (!isContained(projectReal, deepestExistingReal)) {
|
|
114
|
+
throw new ProjectionContainmentError(`projection write refused: projection path escapes the project tree ` +
|
|
115
|
+
`(${deepestExistingReal} is outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: deepestExistingReal });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=projection-containment.js.map
|
|
@@ -61,6 +61,27 @@ export interface GitHooksSeams {
|
|
|
61
61
|
setHooksPath?: (projectDir: string, value: string) => boolean;
|
|
62
62
|
}
|
|
63
63
|
export declare function writeConsumerGitHooks(projectDir: string, deftDir: string, io: InitDepositIo, seams?: GitHooksSeams): boolean;
|
|
64
|
+
/** Commit SHA for `actions/checkout` deposited in deft-core-guard.yml (#1672 / #1072). */
|
|
65
|
+
export declare const CORE_GUARD_CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
|
|
66
|
+
/** Tag comment paired with {@link CORE_GUARD_CHECKOUT_SHA}. */
|
|
67
|
+
export declare const CORE_GUARD_CHECKOUT_TAG = "v7.0.0";
|
|
68
|
+
/**
|
|
69
|
+
* Extract the `uses: actions/checkout@…` step line from a guard workflow, if present.
|
|
70
|
+
* Uses linear string scanning (no regex) to avoid CodeQL js/polynomial-redos (#1672).
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractCoreGuardCheckoutUsesLine(content: string): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Whether refresh should keep the existing checkout pin instead of the template pin.
|
|
75
|
+
* Legacy framework-deposited `@v4` tags migrate forward; consumer/Dependabot bumps do not.
|
|
76
|
+
*/
|
|
77
|
+
export declare function shouldPreserveCoreGuardCheckoutPin(existingUsesLine: string, desiredUsesLine: string): boolean;
|
|
78
|
+
/** Canonical SHA-pinned checkout step for the deposited deft-core-guard workflow. */
|
|
79
|
+
export declare function coreGuardCheckoutUsesLine(sha?: string, tag?: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* On refresh, preserve an existing consumer `actions/checkout@…` pin while updating
|
|
82
|
+
* the managed guard script / allowlist body (#1672 option 1).
|
|
83
|
+
*/
|
|
84
|
+
export declare function mergeCoreGuardWorkflowRefresh(existing: string, desired: string): string;
|
|
64
85
|
export declare function ensureGitattributes(projectDir: string, io: InitDepositIo): boolean;
|
|
65
86
|
export declare function ensureGreptileIgnore(projectDir: string, io: InitDepositIo): boolean;
|
|
66
87
|
export declare function ensureCodeqlPathsIgnore(projectDir: string, io: InitDepositIo): boolean;
|
|
@@ -538,6 +538,78 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
538
538
|
function githubActionsExpr(expression) {
|
|
539
539
|
return ["$", "{{ ", expression, " }}"].join("");
|
|
540
540
|
}
|
|
541
|
+
/** Commit SHA for `actions/checkout` deposited in deft-core-guard.yml (#1672 / #1072). */
|
|
542
|
+
export const CORE_GUARD_CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
|
|
543
|
+
/** Tag comment paired with {@link CORE_GUARD_CHECKOUT_SHA}. */
|
|
544
|
+
export const CORE_GUARD_CHECKOUT_TAG = "v7.0.0";
|
|
545
|
+
const CHECKOUT_USES_PREFIX = "- uses: actions/checkout@";
|
|
546
|
+
/**
|
|
547
|
+
* Extract the `uses: actions/checkout@…` step line from a guard workflow, if present.
|
|
548
|
+
* Uses linear string scanning (no regex) to avoid CodeQL js/polynomial-redos (#1672).
|
|
549
|
+
*/
|
|
550
|
+
export function extractCoreGuardCheckoutUsesLine(content) {
|
|
551
|
+
for (const line of content.split("\n")) {
|
|
552
|
+
const trimmed = line.trimStart();
|
|
553
|
+
if (!trimmed.startsWith(CHECKOUT_USES_PREFIX))
|
|
554
|
+
continue;
|
|
555
|
+
// YAML step lines are indented; a bare top-level match is not a step.
|
|
556
|
+
if (trimmed.length === line.length)
|
|
557
|
+
continue;
|
|
558
|
+
return line;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
/** Ref after `actions/checkout@` (tag, SHA, or major), stopping at whitespace. */
|
|
563
|
+
function checkoutActionRef(usesLine) {
|
|
564
|
+
const idx = usesLine.indexOf(CHECKOUT_USES_PREFIX);
|
|
565
|
+
if (idx < 0)
|
|
566
|
+
return null;
|
|
567
|
+
const after = usesLine.slice(idx + CHECKOUT_USES_PREFIX.length);
|
|
568
|
+
let end = after.length;
|
|
569
|
+
for (let i = 0; i < after.length; i += 1) {
|
|
570
|
+
const c = after[i];
|
|
571
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
572
|
+
end = i;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const ref = after.slice(0, end);
|
|
577
|
+
return ref.length > 0 ? ref : null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Whether refresh should keep the existing checkout pin instead of the template pin.
|
|
581
|
+
* Legacy framework-deposited `@v4` tags migrate forward; consumer/Dependabot bumps do not.
|
|
582
|
+
*/
|
|
583
|
+
export function shouldPreserveCoreGuardCheckoutPin(existingUsesLine, desiredUsesLine) {
|
|
584
|
+
if (existingUsesLine === desiredUsesLine)
|
|
585
|
+
return false;
|
|
586
|
+
const existingRef = checkoutActionRef(existingUsesLine);
|
|
587
|
+
if (!existingRef)
|
|
588
|
+
return false;
|
|
589
|
+
// Stale framework-deposited floating major tag — migrate to SHA template (#1672).
|
|
590
|
+
return existingRef !== "v4";
|
|
591
|
+
}
|
|
592
|
+
/** Canonical SHA-pinned checkout step for the deposited deft-core-guard workflow. */
|
|
593
|
+
export function coreGuardCheckoutUsesLine(sha = CORE_GUARD_CHECKOUT_SHA, tag = CORE_GUARD_CHECKOUT_TAG) {
|
|
594
|
+
return ` - uses: actions/checkout@${sha} # ${tag}`;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* On refresh, preserve an existing consumer `actions/checkout@…` pin while updating
|
|
598
|
+
* the managed guard script / allowlist body (#1672 option 1).
|
|
599
|
+
*/
|
|
600
|
+
export function mergeCoreGuardWorkflowRefresh(existing, desired) {
|
|
601
|
+
const existingCheckout = extractCoreGuardCheckoutUsesLine(existing);
|
|
602
|
+
if (!existingCheckout)
|
|
603
|
+
return desired;
|
|
604
|
+
const desiredCheckout = extractCoreGuardCheckoutUsesLine(desired);
|
|
605
|
+
if (!desiredCheckout || !shouldPreserveCoreGuardCheckoutPin(existingCheckout, desiredCheckout)) {
|
|
606
|
+
return desired;
|
|
607
|
+
}
|
|
608
|
+
const idx = desired.indexOf(desiredCheckout);
|
|
609
|
+
if (idx < 0)
|
|
610
|
+
return desired;
|
|
611
|
+
return desired.slice(0, idx) + existingCheckout + desired.slice(idx + desiredCheckout.length);
|
|
612
|
+
}
|
|
541
613
|
function coreGuardWorkflowContent() {
|
|
542
614
|
const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
|
|
543
615
|
const headSha = githubActionsExpr("github.event.pull_request.head.sha");
|
|
@@ -555,7 +627,7 @@ function coreGuardWorkflowContent() {
|
|
|
555
627
|
" no-mixed-core-and-app:\n" +
|
|
556
628
|
" runs-on: ubuntu-latest\n" +
|
|
557
629
|
" steps:\n" +
|
|
558
|
-
|
|
630
|
+
`${coreGuardCheckoutUsesLine()}\n` +
|
|
559
631
|
" with:\n" +
|
|
560
632
|
" fetch-depth: 0\n" +
|
|
561
633
|
" - name: Refuse PRs that mix .deft/core/** with non-framework paths\n" +
|
|
@@ -715,15 +787,16 @@ export function ensureCoreGuardWorkflow(projectDir, io) {
|
|
|
715
787
|
const desired = coreGuardWorkflowContent();
|
|
716
788
|
if (existsSync(path)) {
|
|
717
789
|
const existing = readFileSync(path, "utf8");
|
|
718
|
-
if (existing === desired) {
|
|
719
|
-
io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
790
|
if (!existing.includes("name: deft-core-guard")) {
|
|
723
791
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} present but not deft-managed — leaving unchanged.\n`);
|
|
724
792
|
return false;
|
|
725
793
|
}
|
|
726
|
-
|
|
794
|
+
const refreshed = mergeCoreGuardWorkflowRefresh(existing, desired);
|
|
795
|
+
if (existing === refreshed) {
|
|
796
|
+
io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
writeFileSync(path, refreshed, "utf8");
|
|
727
800
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} refreshed: deft-core-guard allowlist updated (#1478).\n`);
|
|
728
801
|
return true;
|
|
729
802
|
}
|
|
@@ -3,6 +3,10 @@ export declare const EXIT_MERGE_BLOCKED = 1;
|
|
|
3
3
|
export declare const EXIT_EXTERNAL_ERROR = 2;
|
|
4
4
|
export declare const GREPTILE_LOGIN = "greptile-apps[bot]";
|
|
5
5
|
export declare const GREPTILE_ERRORED_SENTINEL = "Greptile encountered an error while reviewing this PR";
|
|
6
|
+
/** Greptile status marker on excluded-author skip comments (#2375). */
|
|
7
|
+
export declare const GREPTILE_STATUS_MARKER = "<!-- greptile-status -->";
|
|
8
|
+
/** Substring Greptile posts when the PR author is on the excluded-authors list (#2375). */
|
|
9
|
+
export declare const GREPTILE_EXCLUDED_AUTHOR_PHRASE = "excluded authors list";
|
|
6
10
|
export declare const LAST_REVIEWED_RE: RegExp;
|
|
7
11
|
export declare const CONFIDENCE_RE: RegExp;
|
|
8
12
|
export declare const P0_BADGE = "<img alt=\"P0\"";
|
|
@@ -3,6 +3,10 @@ export const EXIT_MERGE_BLOCKED = 1;
|
|
|
3
3
|
export const EXIT_EXTERNAL_ERROR = 2;
|
|
4
4
|
export const GREPTILE_LOGIN = "greptile-apps[bot]";
|
|
5
5
|
export const GREPTILE_ERRORED_SENTINEL = "Greptile encountered an error while reviewing this PR";
|
|
6
|
+
/** Greptile status marker on excluded-author skip comments (#2375). */
|
|
7
|
+
export const GREPTILE_STATUS_MARKER = "<!-- greptile-status -->";
|
|
8
|
+
/** Substring Greptile posts when the PR author is on the excluded-authors list (#2375). */
|
|
9
|
+
export const GREPTILE_EXCLUDED_AUTHOR_PHRASE = "excluded authors list";
|
|
6
10
|
export const LAST_REVIEWED_RE = /Last reviewed commit:\s*\[[^\]]*\]\(https?:\/\/github\.com\/[^/]+\/[^/]+\/commit\/(?<sha>[0-9a-f]{7,40})/g;
|
|
7
11
|
export const CONFIDENCE_RE = /Confidence Score:\s*(?<score>\d+)\s*\/\s*5/i;
|
|
8
12
|
export const P0_BADGE = '<img alt="P0"';
|
|
@@ -8,6 +8,9 @@ export function evaluateGates(_prNumber, headSha, verdict) {
|
|
|
8
8
|
"Wait for the review to land before merging (see #796 late-bot-review re-check).");
|
|
9
9
|
return failures;
|
|
10
10
|
}
|
|
11
|
+
if (verdict.excludedAuthor) {
|
|
12
|
+
return failures;
|
|
13
|
+
}
|
|
11
14
|
if (verdict.errored) {
|
|
12
15
|
failures.push("Greptile review is in the ERRORED state on the current HEAD (#526). " +
|
|
13
16
|
"Retry via @greptileai or escalate per " +
|
|
@@ -31,13 +31,15 @@ export declare function verdictShaIsStale(verdict: GreptileVerdict, headSha: str
|
|
|
31
31
|
* Classify whether the verdict-based merge block is ONLY "soft" (#2260).
|
|
32
32
|
*
|
|
33
33
|
* A soft block means the review verdict is absent or pinned to a prior head
|
|
34
|
-
* SHA
|
|
35
|
-
*
|
|
34
|
+
* SHA without carrying real blocker-class findings (rebased staleness with no
|
|
35
|
+
* P0/P1/errored/low-confidence) -- i.e. the review has not spoken about the
|
|
36
|
+
* CURRENT head in a blocking way. It is safe to reconcile a soft block against
|
|
37
|
+
* GitHub mergeability.
|
|
36
38
|
*
|
|
37
|
-
* A HARD block -- a genuine P0/P1 finding
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* A HARD block -- a genuine P0/P1 finding (even on a stale SHA), an ERRORED
|
|
40
|
+
* review, or a low confidence score -- is NEVER soft; those must keep blocking
|
|
41
|
+
* regardless of GitHub mergeability (guardrail: do not merge a PR with a real
|
|
42
|
+
* P0/P1 review finding).
|
|
41
43
|
*/
|
|
42
44
|
export declare function verdictBlockIsSoftOnly(verdict: GreptileVerdict, headSha: string | null): boolean;
|
|
43
45
|
//# sourceMappingURL=mergeability.d.ts.map
|
|
@@ -65,13 +65,15 @@ export function verdictShaIsStale(verdict, headSha) {
|
|
|
65
65
|
* Classify whether the verdict-based merge block is ONLY "soft" (#2260).
|
|
66
66
|
*
|
|
67
67
|
* A soft block means the review verdict is absent or pinned to a prior head
|
|
68
|
-
* SHA
|
|
69
|
-
*
|
|
68
|
+
* SHA without carrying real blocker-class findings (rebased staleness with no
|
|
69
|
+
* P0/P1/errored/low-confidence) -- i.e. the review has not spoken about the
|
|
70
|
+
* CURRENT head in a blocking way. It is safe to reconcile a soft block against
|
|
71
|
+
* GitHub mergeability.
|
|
70
72
|
*
|
|
71
|
-
* A HARD block -- a genuine P0/P1 finding
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* A HARD block -- a genuine P0/P1 finding (even on a stale SHA), an ERRORED
|
|
74
|
+
* review, or a low confidence score -- is NEVER soft; those must keep blocking
|
|
75
|
+
* regardless of GitHub mergeability (guardrail: do not merge a PR with a real
|
|
76
|
+
* P0/P1 review finding).
|
|
75
77
|
*/
|
|
76
78
|
export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
77
79
|
// Absent: no Greptile rolling-summary comment at all.
|
|
@@ -83,12 +85,11 @@ export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
|
83
85
|
if (verdict.informalClean) {
|
|
84
86
|
return false;
|
|
85
87
|
}
|
|
86
|
-
//
|
|
87
|
-
if (
|
|
88
|
+
// Excluded-author skip is an intentional N/A reviewer state (#2375).
|
|
89
|
+
if (verdict.excludedAuthor) {
|
|
88
90
|
return true;
|
|
89
91
|
}
|
|
90
|
-
//
|
|
91
|
-
// finding is a hard block and must not be overridden.
|
|
92
|
+
// Blocker-class signals apply even when the verdict SHA is stale (#2382).
|
|
92
93
|
if (verdict.errored) {
|
|
93
94
|
return false;
|
|
94
95
|
}
|
|
@@ -98,6 +99,10 @@ export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
|
98
99
|
if (verdict.p0Count > 0 || verdict.p1Count > 0) {
|
|
99
100
|
return false;
|
|
100
101
|
}
|
|
102
|
+
// Stale without blocker findings: review has not spoken about the current head.
|
|
103
|
+
if (verdictShaIsStale(verdict, headSha)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
101
106
|
// No genuine finding: the block is a missing-canonical-field / not-yet-posted
|
|
102
107
|
// wait, which GitHub mergeability may resolve.
|
|
103
108
|
return true;
|
|
@@ -10,6 +10,7 @@ function verdictToDict(verdict) {
|
|
|
10
10
|
p1_count: verdict.p1Count,
|
|
11
11
|
p2_count: verdict.p2Count,
|
|
12
12
|
informal_clean: verdict.informalClean,
|
|
13
|
+
excluded_author: verdict.excludedAuthor,
|
|
13
14
|
raw_body_excerpt: verdict.rawBodyExcerpt,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -47,12 +48,17 @@ export function printHuman(result) {
|
|
|
47
48
|
const lines = [];
|
|
48
49
|
lines.push(`PR #${result.prNumber} merge-readiness check (via=${result.via})`);
|
|
49
50
|
lines.push(` HEAD SHA: ${result.headSha ?? "<unknown>"}`);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
51
|
+
if (result.verdict.excludedAuthor) {
|
|
52
|
+
lines.push(" Greptile review: skipped (author excluded)");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(` Greptile reviewed: ${result.verdict.lastReviewedSha ?? "<not parsed>"}`);
|
|
56
|
+
const confidenceStr = result.verdict.confidence !== null ? String(result.verdict.confidence) : "<not parsed>";
|
|
57
|
+
lines.push(` Confidence: ${confidenceStr}/5`);
|
|
58
|
+
lines.push(` Findings: P0=${result.verdict.p0Count} ` +
|
|
59
|
+
`P1=${result.verdict.p1Count} P2=${result.verdict.p2Count}`);
|
|
60
|
+
lines.push(` Errored sentinel: ${result.verdict.errored ? "True" : "False"}`);
|
|
61
|
+
}
|
|
56
62
|
const ciBlock = result.partialData.ci;
|
|
57
63
|
if (ciBlock !== null && typeof ciBlock === "object" && !Array.isArray(ciBlock)) {
|
|
58
64
|
const ci = ciBlock;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GreptileVerdict } from "./types.js";
|
|
2
2
|
export declare function emptyVerdict(): GreptileVerdict;
|
|
3
|
+
export declare function isGreptileExcludedAuthor(body: string): boolean;
|
|
3
4
|
export declare function isInformalCleanMissingCanonicalFields(verdict: GreptileVerdict, body: string): boolean;
|
|
4
5
|
/** Parse a Greptile rolling-summary comment body into a structured verdict. */
|
|
5
6
|
export declare function parseGreptileBody(body: string): GreptileVerdict;
|