@deftai/directive-core 0.80.0 → 0.82.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/content-contracts/skills/helpers.d.ts +6 -0
- package/dist/content-contracts/skills/helpers.js +47 -3
- package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
- package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +139 -0
- package/dist/doctor/main.js +2 -1
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -0
- package/dist/intake/github-body-cli.js +2 -0
- package/dist/intake/github-body.d.ts +11 -0
- package/dist/intake/github-body.js +95 -24
- package/dist/integration-e2e/helpers.js +1 -1
- package/dist/issue-sync/sync-from-xbrief.js +3 -0
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +275 -0
- package/dist/orphan-active/index.d.ts +3 -0
- package/dist/orphan-active/index.js +3 -0
- package/dist/orphan-active/refs.d.ts +16 -0
- package/dist/orphan-active/refs.js +105 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +19 -5
- package/dist/policy/product-signal.d.ts +45 -0
- package/dist/policy/product-signal.js +210 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +18 -0
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +44 -73
- package/dist/pr-wait-mergeable/result.d.ts +2 -1
- package/dist/pr-wait-mergeable/result.js +4 -0
- package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
- package/dist/pr-wait-mergeable/semantic-green.js +202 -0
- package/dist/pr-wait-mergeable/types.d.ts +1 -0
- package/dist/pr-watch/constants.d.ts +4 -4
- package/dist/pr-watch/constants.js +4 -4
- package/dist/pr-watch/watch.js +5 -3
- package/dist/product-signal/actor-name.d.ts +18 -0
- package/dist/product-signal/actor-name.js +94 -0
- package/dist/product-signal/consent.d.ts +30 -0
- package/dist/product-signal/consent.js +116 -0
- package/dist/product-signal/gates.d.ts +17 -0
- package/dist/product-signal/gates.js +66 -0
- package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
- package/dist/product-signal/github-private-sink-adapter.js +274 -0
- package/dist/product-signal/headless.d.ts +8 -0
- package/dist/product-signal/headless.js +32 -0
- package/dist/product-signal/install-context.d.ts +13 -0
- package/dist/product-signal/install-context.js +41 -0
- package/dist/product-signal/local-signal-summary.d.ts +7 -0
- package/dist/product-signal/local-signal-summary.js +173 -0
- package/dist/product-signal/payload.d.ts +90 -0
- package/dist/product-signal/payload.js +124 -0
- package/dist/product-signal/sink-bootstrap.d.ts +29 -0
- package/dist/product-signal/sink-bootstrap.js +108 -0
- package/dist/product-signal/submit-adapter.d.ts +14 -0
- package/dist/product-signal/submit-adapter.js +2 -0
- package/dist/product-signal/submit.d.ts +54 -0
- package/dist/product-signal/submit.js +290 -0
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scm/gh-rest.d.ts +3 -1
- package/dist/scm/gh-rest.js +22 -0
- package/dist/scope/brief-io.d.ts +26 -0
- package/dist/scope/brief-io.js +77 -0
- package/dist/scope/decompose.js +2 -2
- package/dist/scope/decomposed-refs.js +3 -3
- package/dist/scope/demote.js +2 -2
- package/dist/scope/project-definition-sync.d.ts +2 -2
- package/dist/scope/project-definition-sync.js +15 -3
- package/dist/scope/registry-artifact-sync.d.ts +6 -1
- package/dist/scope/registry-artifact-sync.js +31 -13
- package/dist/scope/transition.js +23 -15
- package/dist/scope/undo.js +2 -2
- package/dist/scope/vbrief-json.d.ts +1 -2
- package/dist/scope/vbrief-json.js +1 -4
- package/dist/staleness-tickler/state.js +19 -2
- package/dist/triage/queue/cache.js +5 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
- package/dist/verify-source/skill-external-fetch-gate.js +71 -0
- package/package.json +7 -3
|
@@ -8,6 +8,7 @@ export declare function repoPath(...segments: string[]): string;
|
|
|
8
8
|
* first (SOURCE layout), then fall back to the repo root so root-resident
|
|
9
9
|
* harness entries (AGENTS.md) and the flattened consumer layout still resolve.
|
|
10
10
|
*/
|
|
11
|
+
export declare function resolveContentPathFromRoot(projectRoot: string, relPath: string): string;
|
|
11
12
|
export declare function resolveRepoPath(relPath: string): string;
|
|
12
13
|
export declare function readRepoFile(relPath: string): string;
|
|
13
14
|
export declare function repoFileExists(relPath: string): boolean;
|
|
@@ -15,7 +16,12 @@ export declare function readSkill(relPath: string): string;
|
|
|
15
16
|
export declare function readAgentsMd(): string;
|
|
16
17
|
/** Slice the first `## Returning Sessions` section body out of AGENTS.md. */
|
|
17
18
|
export declare function returningSessionsSection(): string;
|
|
19
|
+
export declare function listSkillMdFilesFromRoot(projectRoot: string): string[];
|
|
18
20
|
export declare function listSkillMdFiles(): string[];
|
|
21
|
+
export declare function listSkillMdEntriesFromRoot(projectRoot: string): ReadonlyArray<{
|
|
22
|
+
path: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}>;
|
|
19
25
|
export declare const RFC2119_LEGEND = "!=MUST, ~=SHOULD";
|
|
20
26
|
export declare const PLATFORM_DETECTION_HEADING = "## Platform Detection";
|
|
21
27
|
export declare const USER_MD_GATE_HEADING = "## USER.md Gate";
|
|
@@ -13,12 +13,15 @@ export function repoPath(...segments) {
|
|
|
13
13
|
* first (SOURCE layout), then fall back to the repo root so root-resident
|
|
14
14
|
* harness entries (AGENTS.md) and the flattened consumer layout still resolve.
|
|
15
15
|
*/
|
|
16
|
-
export function
|
|
17
|
-
const underContent = join(
|
|
16
|
+
export function resolveContentPathFromRoot(projectRoot, relPath) {
|
|
17
|
+
const underContent = join(projectRoot, "content", relPath);
|
|
18
18
|
if (existsSync(underContent)) {
|
|
19
19
|
return underContent;
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return join(projectRoot, relPath);
|
|
22
|
+
}
|
|
23
|
+
export function resolveRepoPath(relPath) {
|
|
24
|
+
return resolveContentPathFromRoot(REPO_ROOT, relPath);
|
|
22
25
|
}
|
|
23
26
|
export function readRepoFile(relPath) {
|
|
24
27
|
return readFileSync(resolveRepoPath(relPath), "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -43,6 +46,26 @@ export function returningSessionsSection() {
|
|
|
43
46
|
const nextHeading = rest.indexOf("\n## ");
|
|
44
47
|
return nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
|
45
48
|
}
|
|
49
|
+
function skillsDirFromRoot(projectRoot) {
|
|
50
|
+
const resolved = resolveContentPathFromRoot(projectRoot, "skills");
|
|
51
|
+
return existsSync(resolved) ? resolved : null;
|
|
52
|
+
}
|
|
53
|
+
export function listSkillMdFilesFromRoot(projectRoot) {
|
|
54
|
+
const skillsDir = skillsDirFromRoot(projectRoot);
|
|
55
|
+
if (skillsDir === null) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
62
|
+
if (existsSync(skillMd)) {
|
|
63
|
+
results.push(join("skills", entry.name, "SKILL.md"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return results.sort();
|
|
68
|
+
}
|
|
46
69
|
export function listSkillMdFiles() {
|
|
47
70
|
const skillsDir = resolveRepoPath("skills");
|
|
48
71
|
const results = [];
|
|
@@ -56,6 +79,27 @@ export function listSkillMdFiles() {
|
|
|
56
79
|
}
|
|
57
80
|
return results.sort();
|
|
58
81
|
}
|
|
82
|
+
export function listSkillMdEntriesFromRoot(projectRoot) {
|
|
83
|
+
const skillsDir = skillsDirFromRoot(projectRoot);
|
|
84
|
+
if (skillsDir === null) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const entries = [];
|
|
88
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
89
|
+
if (!entry.isDirectory()) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
93
|
+
if (!existsSync(skillMd)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
entries.push({
|
|
97
|
+
path: join("skills", entry.name, "SKILL.md"),
|
|
98
|
+
text: readFileSync(skillMd, "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n"),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
102
|
+
}
|
|
59
103
|
export const RFC2119_LEGEND = "!=MUST, ~=SHOULD";
|
|
60
104
|
export const PLATFORM_DETECTION_HEADING = "## Platform Detection";
|
|
61
105
|
export const USER_MD_GATE_HEADING = "## USER.md Gate";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Skill-validation gate: external fetch must not pair with execute/install without mitigation (#1532 / #1936). */
|
|
2
|
+
export type ExternalFetchViolation = {
|
|
3
|
+
skillPath: string;
|
|
4
|
+
detail: string;
|
|
5
|
+
};
|
|
6
|
+
/** Strip YAML frontmatter and HTML comment banners before scanning skill prose. */
|
|
7
|
+
export declare function skillProse(raw: string): string;
|
|
8
|
+
export declare function hasExternalFetchSignal(prose: string): boolean;
|
|
9
|
+
export declare function hasRiskyExternalFetchPattern(prose: string): boolean;
|
|
10
|
+
export declare function hasExecuteFromExternalSignal(prose: string): boolean;
|
|
11
|
+
export declare function hasUntrustedFetchMitigation(prose: string): boolean;
|
|
12
|
+
export declare function analyzeSkillExternalFetch(skillPath: string, rawText: string): ExternalFetchViolation | null;
|
|
13
|
+
/** Collect violations across skill entries (used by verify-source gate + tests). */
|
|
14
|
+
export declare function collectExternalFetchViolations(entries: ReadonlyArray<{
|
|
15
|
+
path: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}>): ExternalFetchViolation[];
|
|
18
|
+
//# sourceMappingURL=skill-external-fetch-gate.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Skill-validation gate: external fetch must not pair with execute/install without mitigation (#1532 / #1936). */
|
|
2
|
+
const EXTERNAL_FETCH_RE = /\b(fetch(?:ing)?(?:\s+\w+){0,6}\s+(?:the\s+)?(?:url|content|article|related\s+urls?|http|https|web\s+page)|fetch\s+and\s+read|webfetch|mcp.*fetch)/i;
|
|
3
|
+
const EXECUTE_FROM_EXTERNAL_RE = /\b(download|install|run|execute)\b.{0,40}\b(?:found|inside|within|in)\b.{0,40}\b(?:fetched|external|url|content|page|article)\b|\bfollow\b.{0,40}\b(?:instructions?|commands?)\b.{0,40}\b(?:download|install|run|execute)\b|\bexecute\b.{0,40}\b(?:commands?|scripts?|instructions?)\b.{0,40}\b(?:found|inside|within)\b/i;
|
|
4
|
+
const FETCH_THEN_EXECUTE_RE = /\bfetch\b.{0,120}\b(?:then|and)\b.{0,60}\b(?:run|execute|install|download)\b/i;
|
|
5
|
+
const RUN_FETCHED_ARTIFACT_RE = /\b(?:run|execute|install)\b.{0,40}\b(?:downloaded|fetched)\b/i;
|
|
6
|
+
const FETCHED_ARTIFACT_RE = /\b(?:downloaded|fetched)\b.{0,40}\b(?:script|binary|executable|tool|installer|package|file)\b/i;
|
|
7
|
+
const FOLLOW_RELATED_URLS_RE = /\bfetch(?:ing)?\s+related\s+urls?\b/i;
|
|
8
|
+
const SECURITY_CONTEXT_HEADING = /## Security context/i;
|
|
9
|
+
const UNTRUSTED_DOCTRINE_RE = /untrusted\s+(?:external\s+)?(?:content|data)/i;
|
|
10
|
+
const FORBID_EXTERNAL_EXECUTE_RE = /⊗[\s\S]{0,200}\b(?:download|install|execute|run)\b[\s\S]{0,200}\b(?:external|fetched|externally|found inside)\b/i;
|
|
11
|
+
/** Strip HTML comment blocks; repeat until stable for CodeQL multi-char sanitization. */
|
|
12
|
+
function stripHtmlComments(text) {
|
|
13
|
+
let body = text;
|
|
14
|
+
let prev = "";
|
|
15
|
+
while (prev !== body) {
|
|
16
|
+
prev = body;
|
|
17
|
+
body = body.replace(/<!--[\s\S]*?-->/g, "");
|
|
18
|
+
}
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
/** Strip YAML frontmatter and HTML comment banners before scanning skill prose. */
|
|
22
|
+
export function skillProse(raw) {
|
|
23
|
+
let body = raw.replace(/\r\n/g, "\n");
|
|
24
|
+
if (body.startsWith("---")) {
|
|
25
|
+
const end = body.indexOf("\n---", 3);
|
|
26
|
+
if (end !== -1) {
|
|
27
|
+
body = body.slice(end + 4);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return stripHtmlComments(body);
|
|
31
|
+
}
|
|
32
|
+
export function hasExternalFetchSignal(prose) {
|
|
33
|
+
return EXTERNAL_FETCH_RE.test(prose);
|
|
34
|
+
}
|
|
35
|
+
export function hasRiskyExternalFetchPattern(prose) {
|
|
36
|
+
if (!hasExternalFetchSignal(prose)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return (hasExecuteFromExternalSignal(prose) ||
|
|
40
|
+
FOLLOW_RELATED_URLS_RE.test(prose) ||
|
|
41
|
+
/\bfetch\b.{0,40}\bfollow\b/i.test(prose));
|
|
42
|
+
}
|
|
43
|
+
export function hasExecuteFromExternalSignal(prose) {
|
|
44
|
+
return (EXECUTE_FROM_EXTERNAL_RE.test(prose) ||
|
|
45
|
+
FETCH_THEN_EXECUTE_RE.test(prose) ||
|
|
46
|
+
RUN_FETCHED_ARTIFACT_RE.test(prose) ||
|
|
47
|
+
FETCHED_ARTIFACT_RE.test(prose));
|
|
48
|
+
}
|
|
49
|
+
export function hasUntrustedFetchMitigation(prose) {
|
|
50
|
+
return (SECURITY_CONTEXT_HEADING.test(prose) &&
|
|
51
|
+
UNTRUSTED_DOCTRINE_RE.test(prose) &&
|
|
52
|
+
FORBID_EXTERNAL_EXECUTE_RE.test(prose));
|
|
53
|
+
}
|
|
54
|
+
export function analyzeSkillExternalFetch(skillPath, rawText) {
|
|
55
|
+
const prose = skillProse(rawText);
|
|
56
|
+
if (!hasExternalFetchSignal(prose)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (!hasRiskyExternalFetchPattern(prose)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (hasUntrustedFetchMitigation(prose)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
skillPath,
|
|
67
|
+
detail: "external fetch or follow-through language without Security context untrusted-data / forbid-execute mitigation (#1936)",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Collect violations across skill entries (used by verify-source gate + tests). */
|
|
71
|
+
export function collectExternalFetchViolations(entries) {
|
|
72
|
+
const violations = [];
|
|
73
|
+
for (const { path, text } of entries) {
|
|
74
|
+
const finding = analyzeSkillExternalFetch(path, text);
|
|
75
|
+
if (finding !== null) {
|
|
76
|
+
violations.push(finding);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return violations;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=skill-external-fetch-gate.js.map
|
package/dist/doctor/checks.d.ts
CHANGED
|
@@ -39,6 +39,12 @@ export declare const STALE_VBRIEF_CORE_SCHEMA_REL: string;
|
|
|
39
39
|
* `directive update` instead. Advisory only (exit-exempt).
|
|
40
40
|
*/
|
|
41
41
|
export declare function checkStaleXbriefSchemaDeposit(projectRoot: string, seams?: CheckSeams): CheckResult;
|
|
42
|
+
/**
|
|
43
|
+
* #2591: advisory hint when a TypeScript project pins bare `typescript@7` alongside
|
|
44
|
+
* typescript-eslint without the `@typescript/typescript6` alias. Exit-exempt (like
|
|
45
|
+
* gitignore-coverage) — adoption guidance, not a broken install.
|
|
46
|
+
*/
|
|
47
|
+
export declare function checkTypescript7SideBySide(projectRoot: string, seams?: CheckSeams): CheckResult;
|
|
42
48
|
/**
|
|
43
49
|
* #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
|
|
44
50
|
* entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
|
package/dist/doctor/checks.js
CHANGED
|
@@ -523,6 +523,142 @@ export function checkStaleXbriefSchemaDeposit(projectRoot, seams = {}) {
|
|
|
523
523
|
},
|
|
524
524
|
};
|
|
525
525
|
}
|
|
526
|
+
const TS7_SIDE_BY_SIDE_CHECK = "typescript-7-side-by-side";
|
|
527
|
+
function depKeyIsTypescriptEslint(key) {
|
|
528
|
+
return key === "typescript-eslint" || key.startsWith("@typescript-eslint/");
|
|
529
|
+
}
|
|
530
|
+
function depKeysIncludeTypescriptEslint(keys) {
|
|
531
|
+
return keys.some(depKeyIsTypescriptEslint);
|
|
532
|
+
}
|
|
533
|
+
function depKeysIncludeEslint(keys) {
|
|
534
|
+
return keys.includes("eslint");
|
|
535
|
+
}
|
|
536
|
+
function typescriptValueIs7Bound(value) {
|
|
537
|
+
const v = value.trim();
|
|
538
|
+
if (!v || v.includes("@typescript/typescript6")) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
if (/npm:typescript@(?:[\^~>=<]*|\d*)7/i.test(v)) {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
if (/^[\^~>=<]*7(?:\.\d|$)/.test(v)) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
if (/^7\.\d/.test(v)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
function resolveTypescriptDepValue(deps, devDeps) {
|
|
553
|
+
if (typeof devDeps.typescript === "string") {
|
|
554
|
+
return devDeps.typescript;
|
|
555
|
+
}
|
|
556
|
+
if (typeof deps.typescript === "string") {
|
|
557
|
+
return deps.typescript;
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* #2591: advisory hint when a TypeScript project pins bare `typescript@7` alongside
|
|
563
|
+
* typescript-eslint without the `@typescript/typescript6` alias. Exit-exempt (like
|
|
564
|
+
* gitignore-coverage) — adoption guidance, not a broken install.
|
|
565
|
+
*/
|
|
566
|
+
export function checkTypescript7SideBySide(projectRoot, seams = {}) {
|
|
567
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
568
|
+
const text = readText(packageJsonPath, seams);
|
|
569
|
+
if (text === null) {
|
|
570
|
+
return {
|
|
571
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
572
|
+
status: "skip",
|
|
573
|
+
detail: "package.json not found; nothing to inspect for TypeScript side-by-side layout.",
|
|
574
|
+
data: { package_json_path: packageJsonPath },
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
let parsed;
|
|
578
|
+
try {
|
|
579
|
+
parsed = JSON.parse(text);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return {
|
|
583
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
584
|
+
status: "skip",
|
|
585
|
+
detail: "package.json is unreadable; skipping TypeScript side-by-side check.",
|
|
586
|
+
data: { package_json_path: packageJsonPath },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
590
|
+
return {
|
|
591
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
592
|
+
status: "skip",
|
|
593
|
+
detail: "package.json has no dependency sections; skipping TypeScript side-by-side check.",
|
|
594
|
+
data: { package_json_path: packageJsonPath },
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const record = parsed;
|
|
598
|
+
const deps = typeof record.dependencies === "object" && record.dependencies !== null
|
|
599
|
+
? record.dependencies
|
|
600
|
+
: {};
|
|
601
|
+
const devDeps = typeof record.devDependencies === "object" && record.devDependencies !== null
|
|
602
|
+
? record.devDependencies
|
|
603
|
+
: {};
|
|
604
|
+
const depKeys = [...Object.keys(deps), ...Object.keys(devDeps)];
|
|
605
|
+
if (depKeys.length === 0) {
|
|
606
|
+
return {
|
|
607
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
608
|
+
status: "skip",
|
|
609
|
+
detail: "package.json has no dependency sections; skipping TypeScript side-by-side check.",
|
|
610
|
+
data: { package_json_path: packageJsonPath },
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if (!depKeysIncludeTypescriptEslint(depKeys)) {
|
|
614
|
+
return {
|
|
615
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
616
|
+
status: "pass",
|
|
617
|
+
detail: "No typescript-eslint packages declared; TypeScript 7 side-by-side alias not required.",
|
|
618
|
+
data: { package_json_path: packageJsonPath },
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (!depKeysIncludeEslint(depKeys)) {
|
|
622
|
+
return {
|
|
623
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
624
|
+
status: "pass",
|
|
625
|
+
detail: "eslint is not declared; TypeScript 7 side-by-side alias not required.",
|
|
626
|
+
data: { package_json_path: packageJsonPath },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const typescriptValue = resolveTypescriptDepValue(deps, devDeps);
|
|
630
|
+
if (typescriptValue === null) {
|
|
631
|
+
return {
|
|
632
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
633
|
+
status: "pass",
|
|
634
|
+
detail: "No typescript dependency declared; TypeScript 7 side-by-side alias not required.",
|
|
635
|
+
data: { package_json_path: packageJsonPath },
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (!typescriptValueIs7Bound(typescriptValue)) {
|
|
639
|
+
return {
|
|
640
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
641
|
+
status: "pass",
|
|
642
|
+
detail: "typescript is not pinned to 7.x (or uses the @typescript/typescript6 alias); side-by-side layout not required.",
|
|
643
|
+
data: {
|
|
644
|
+
package_json_path: packageJsonPath,
|
|
645
|
+
typescript: typescriptValue,
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
name: TS7_SIDE_BY_SIDE_CHECK,
|
|
651
|
+
status: "fail",
|
|
652
|
+
detail: "typescript resolves to 7.x without the @typescript/typescript6 alias while typescript-eslint and eslint are present. " +
|
|
653
|
+
"Until TS 7.1, use the side-by-side alias pattern in languages/typescript.md § TypeScript 7 side-by-side (pre-7.1) " +
|
|
654
|
+
"(Cartograph #111: keep typescript on npm:@typescript/typescript6 and install TS 7 under @typescript/native).",
|
|
655
|
+
data: {
|
|
656
|
+
package_json_path: packageJsonPath,
|
|
657
|
+
typescript: typescriptValue,
|
|
658
|
+
suggested_fix: "languages/typescript.md#typescript-7-side-by-side-pre-71",
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
526
662
|
/**
|
|
527
663
|
* #2206: check that the consumer `.gitignore` carries the canonical Deft baseline
|
|
528
664
|
* entries. Advisory (exit-exempt) because missing entries are an adoption risk, not
|
|
@@ -580,6 +716,7 @@ export function deriveExitCode(checks, errors) {
|
|
|
580
716
|
"manifest-version-reportable",
|
|
581
717
|
"gitignore-coverage",
|
|
582
718
|
"stale-xbrief-schema-deposit",
|
|
719
|
+
"typescript-7-side-by-side",
|
|
583
720
|
]);
|
|
584
721
|
if (errors.length > 0 || checks.some((c) => c.status === "error")) {
|
|
585
722
|
return 2;
|
|
@@ -621,6 +758,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
621
758
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
622
759
|
checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
|
|
623
760
|
checks.push(checkGitignoreCoverage(projectRoot, seams));
|
|
761
|
+
checks.push(checkTypescript7SideBySide(projectRoot, seams));
|
|
624
762
|
return {
|
|
625
763
|
projectRoot,
|
|
626
764
|
installRoot: null,
|
|
@@ -638,6 +776,7 @@ export function runChecksImpl(projectRoot, seams = {}) {
|
|
|
638
776
|
checks.push(checkCanonicalVendoredNpmSignpost(projectRoot, seams));
|
|
639
777
|
checks.push(checkStaleXbriefSchemaDeposit(projectRoot, seams));
|
|
640
778
|
checks.push(checkGitignoreCoverage(projectRoot, seams));
|
|
779
|
+
checks.push(checkTypescript7SideBySide(projectRoot, seams));
|
|
641
780
|
return {
|
|
642
781
|
projectRoot,
|
|
643
782
|
installRoot,
|
package/dist/doctor/main.js
CHANGED
|
@@ -424,7 +424,8 @@ function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
|
|
|
424
424
|
name === "canonical-vendored-npm-signpost" ||
|
|
425
425
|
name === "manifest-version-reportable" ||
|
|
426
426
|
name === "gitignore-coverage" ||
|
|
427
|
-
name === "stale-xbrief-schema-deposit"
|
|
427
|
+
name === "stale-xbrief-schema-deposit" ||
|
|
428
|
+
name === "typescript-7-side-by-side") &&
|
|
428
429
|
status === "fail") {
|
|
429
430
|
sink.warn(`${name}: ${detail}`);
|
|
430
431
|
addFinding({
|
|
@@ -54,6 +54,8 @@ export declare class InstrumentedVbriefCrud {
|
|
|
54
54
|
update(path: string, content: string, options?: {
|
|
55
55
|
trustedWrite?: boolean;
|
|
56
56
|
}): CrudResult;
|
|
57
|
+
/** Record update metrics without persisting (caller owns validated atomic write). */
|
|
58
|
+
recordTrustedUpdate(path: string, content: string): void;
|
|
57
59
|
delete(path: string): CrudResult;
|
|
58
60
|
private recordMetric;
|
|
59
61
|
}
|
|
@@ -248,6 +248,36 @@ export class InstrumentedVbriefCrud {
|
|
|
248
248
|
writeFileSync(path, content, "utf8");
|
|
249
249
|
return { ok: true };
|
|
250
250
|
}
|
|
251
|
+
/** Record update metrics without persisting (caller owns validated atomic write). */
|
|
252
|
+
recordTrustedUpdate(path, content) {
|
|
253
|
+
let parsed;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(content);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
const message = sanitizeInlineMessage(err instanceof Error ? err.message : String(err));
|
|
259
|
+
this.recordMetric({
|
|
260
|
+
operation: "update",
|
|
261
|
+
path,
|
|
262
|
+
schemaValid: false,
|
|
263
|
+
schemaErrors: [`${path}: invalid JSON -- ${message}`],
|
|
264
|
+
inventedKeys: [],
|
|
265
|
+
byteDiffMinimality: null,
|
|
266
|
+
byteDiffChangedRatio: null,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const assessment = assessDocument(path, parsed);
|
|
271
|
+
this.recordMetric({
|
|
272
|
+
operation: "update",
|
|
273
|
+
path,
|
|
274
|
+
schemaValid: assessment.schemaValid,
|
|
275
|
+
schemaErrors: assessment.schemaErrors,
|
|
276
|
+
inventedKeys: assessment.inventedKeys,
|
|
277
|
+
byteDiffMinimality: null,
|
|
278
|
+
byteDiffChangedRatio: null,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
251
281
|
delete(path) {
|
|
252
282
|
if (!existsSync(path)) {
|
|
253
283
|
this.recordMetric({
|
|
@@ -9,6 +9,8 @@ export type RunGhApiFn = (args: readonly string[], options?: {
|
|
|
9
9
|
/** Resolve the live GitHub CLI used for writes and mutation read-back. */
|
|
10
10
|
export declare function resolveLiveGh(): string;
|
|
11
11
|
export declare function readBody(bodyFile: string, stdinText?: string | null): string;
|
|
12
|
+
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607). */
|
|
13
|
+
export declare function verifyBodyPostcondition(intended: string, live: string): void;
|
|
12
14
|
export declare function createIssue(repo: string, options: {
|
|
13
15
|
title: string;
|
|
14
16
|
body: string;
|
|
@@ -35,6 +37,14 @@ export declare function editPrBody(repo: string, pr: number, options: {
|
|
|
35
37
|
binary?: string | null;
|
|
36
38
|
runFn?: RunGhApiFn;
|
|
37
39
|
}): Record<string, unknown>;
|
|
40
|
+
export declare function fetchIssueBody(repo: string, issue: number, options?: {
|
|
41
|
+
binary?: string | null;
|
|
42
|
+
runFn?: RunGhApiFn;
|
|
43
|
+
}): string;
|
|
44
|
+
export declare function writeIssueBodyToFile(repo: string, issue: number, outFile: string, options?: {
|
|
45
|
+
binary?: string | null;
|
|
46
|
+
runFn?: RunGhApiFn;
|
|
47
|
+
}): string;
|
|
38
48
|
export interface GitHubBodyCliArgs {
|
|
39
49
|
command: string;
|
|
40
50
|
repo?: string;
|
|
@@ -43,6 +53,7 @@ export interface GitHubBodyCliArgs {
|
|
|
43
53
|
comment?: number;
|
|
44
54
|
pr?: number;
|
|
45
55
|
bodyFile?: string;
|
|
56
|
+
outFile?: string;
|
|
46
57
|
}
|
|
47
58
|
export declare function githubBodyMain(args: GitHubBodyCliArgs): number;
|
|
48
59
|
//# sourceMappingURL=github-body.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { defaultWhich } from "../scm/binary.js";
|
|
3
3
|
import { call } from "../scm/call.js";
|
|
4
4
|
export const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
@@ -73,14 +73,62 @@ function requireIntField(obj, field) {
|
|
|
73
73
|
}
|
|
74
74
|
return value;
|
|
75
75
|
}
|
|
76
|
+
function requireStringField(obj, field) {
|
|
77
|
+
const value = obj[field];
|
|
78
|
+
if (typeof value !== "string") {
|
|
79
|
+
throw new GitHubBodyError(`response did not include string field ${JSON.stringify(field)}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function countNewlines(text) {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const ch of text) {
|
|
86
|
+
if (ch === "\n")
|
|
87
|
+
count += 1;
|
|
88
|
+
}
|
|
89
|
+
return count;
|
|
90
|
+
}
|
|
91
|
+
function normalizeNewlines(text) {
|
|
92
|
+
return text.replace(/\r\n/g, "\n");
|
|
93
|
+
}
|
|
94
|
+
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607). */
|
|
95
|
+
export function verifyBodyPostcondition(intended, live) {
|
|
96
|
+
if (intended === live) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (normalizeNewlines(intended) === normalizeNewlines(live)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const diagnoses = [];
|
|
103
|
+
if (live.includes("\uFFFD") && !intended.includes("\uFFFD")) {
|
|
104
|
+
diagnoses.push("live body contains U+FFFD replacement character not present in intended payload");
|
|
105
|
+
}
|
|
106
|
+
const intendedNl = countNewlines(intended);
|
|
107
|
+
const liveNl = countNewlines(live);
|
|
108
|
+
if (intendedNl > 0 && liveNl < intendedNl) {
|
|
109
|
+
diagnoses.push(`newline count mismatch (intended ${intendedNl}, live ${liveNl})`);
|
|
110
|
+
}
|
|
111
|
+
if (intended.includes("\n") && intended.replace(/\n/g, " ") === live) {
|
|
112
|
+
diagnoses.push("live body looks like intended newlines were collapsed to spaces (PowerShell string[] join)");
|
|
113
|
+
}
|
|
114
|
+
if (diagnoses.length > 0) {
|
|
115
|
+
throw new GitHubBodyError(`body postcondition failed: ${diagnoses.join("; ")}`);
|
|
116
|
+
}
|
|
117
|
+
throw new GitHubBodyError(`body postcondition failed: re-fetched body does not match intended payload (length intended=${intended.length}, live=${live.length})`);
|
|
118
|
+
}
|
|
76
119
|
function mutateWithReadback(mutationEndpoint, method, payload, readbackEndpoint, options = {}) {
|
|
120
|
+
const intendedBody = typeof payload.body === "string" ? payload.body : undefined;
|
|
77
121
|
const mutation = runGhApiJson([mutationEndpoint, "--method", method, "--input", "-"], {
|
|
78
122
|
inputText: jsonInput(payload),
|
|
79
123
|
binary: options.binary,
|
|
80
124
|
runFn: options.runFn,
|
|
81
125
|
});
|
|
82
126
|
const endpoint = typeof readbackEndpoint === "function" ? readbackEndpoint(mutation) : readbackEndpoint;
|
|
83
|
-
|
|
127
|
+
const readback = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
128
|
+
if (intendedBody !== undefined) {
|
|
129
|
+
verifyBodyPostcondition(intendedBody, requireStringField(readback, "body"));
|
|
130
|
+
}
|
|
131
|
+
return readback;
|
|
84
132
|
}
|
|
85
133
|
export function createIssue(repo, options) {
|
|
86
134
|
const [owner, name] = splitRepo(repo);
|
|
@@ -116,32 +164,55 @@ export function editPrBody(repo, pr, options) {
|
|
|
116
164
|
runFn: options.runFn,
|
|
117
165
|
});
|
|
118
166
|
}
|
|
167
|
+
export function fetchIssueBody(repo, issue, options = {}) {
|
|
168
|
+
const [owner, name] = splitRepo(repo);
|
|
169
|
+
const endpoint = `repos/${owner}/${name}/issues/${issue}`;
|
|
170
|
+
const response = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
171
|
+
return requireStringField(response, "body");
|
|
172
|
+
}
|
|
173
|
+
export function writeIssueBodyToFile(repo, issue, outFile, options = {}) {
|
|
174
|
+
const body = fetchIssueBody(repo, issue, options);
|
|
175
|
+
writeFileSync(outFile, body, { encoding: "utf8" });
|
|
176
|
+
return body;
|
|
177
|
+
}
|
|
119
178
|
export function githubBodyMain(args) {
|
|
120
179
|
try {
|
|
121
|
-
const body = readBody(args.bodyFile ?? "-");
|
|
122
|
-
let result;
|
|
123
180
|
switch (args.command) {
|
|
124
|
-
case "issue-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
result
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
case "issue-fetch": {
|
|
182
|
+
if (args.repo === undefined || args.issue === undefined || args.outFile === undefined) {
|
|
183
|
+
process.stderr.write("error: issue-fetch requires --repo, --issue, and --out-file\n");
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
writeIssueBodyToFile(args.repo, args.issue, args.outFile);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
default: {
|
|
190
|
+
const body = readBody(args.bodyFile ?? "-");
|
|
191
|
+
let result;
|
|
192
|
+
switch (args.command) {
|
|
193
|
+
case "issue-create":
|
|
194
|
+
result = createIssue(args.repo, { title: args.title, body });
|
|
195
|
+
break;
|
|
196
|
+
case "issue-edit":
|
|
197
|
+
result = editIssueBody(args.repo, args.issue, { body });
|
|
198
|
+
break;
|
|
199
|
+
case "comment-create":
|
|
200
|
+
result = createIssueComment(args.repo, args.issue, { body });
|
|
201
|
+
break;
|
|
202
|
+
case "comment-edit":
|
|
203
|
+
result = editIssueCommentBody(args.repo, args.comment, { body });
|
|
204
|
+
break;
|
|
205
|
+
case "pr-edit":
|
|
206
|
+
result = editPrBody(args.repo, args.pr, { body });
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
process.stderr.write(`error: unknown command ${JSON.stringify(args.command)}\n`);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
142
215
|
}
|
|
143
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
144
|
-
return 0;
|
|
145
216
|
}
|
|
146
217
|
catch (exc) {
|
|
147
218
|
process.stderr.write(`error: ${String(exc)}\n`);
|
|
@@ -98,7 +98,7 @@ export function createConsumerProject(parent) {
|
|
|
98
98
|
export function writeScopeVbrief(projectRoot, folder, filename, status = "proposed") {
|
|
99
99
|
const target = join(projectRoot, "xbrief", folder, filename);
|
|
100
100
|
writeFileSync(target, `${JSON.stringify({
|
|
101
|
-
|
|
101
|
+
xBRIEFInfo: { version: "0.8" },
|
|
102
102
|
plan: {
|
|
103
103
|
title: "Consumer fixture scope",
|
|
104
104
|
status,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
4
5
|
import { createIssueComment } from "../intake/github-body.js";
|
|
5
6
|
import { provenanceIssueNumber, repoSlugFromUrl } from "../intake/issue-ingest.js";
|
|
6
7
|
import { extractReferencesFromVbrief, GITHUB_ISSUE_REF_TYPES, parseIssueNumber, } from "../intake/reconcile-issues.js";
|
|
@@ -192,9 +193,11 @@ export function syncFromXbrief(options) {
|
|
|
192
193
|
writeOut(`issue:sync-from-xbrief: posted comment to ${origin.repo}#${origin.number} (id: ${commentResult.id}).`);
|
|
193
194
|
const writeFingerprint = options.writeFingerprint ??
|
|
194
195
|
((targetPath, stamped) => {
|
|
196
|
+
assertWriteTargetSafe(projectRoot, targetPath);
|
|
195
197
|
writeFileSync(targetPath, `${JSON.stringify(stamped, null, 2)}\n`, "utf8");
|
|
196
198
|
});
|
|
197
199
|
try {
|
|
200
|
+
assertWriteTargetSafe(projectRoot, absPath);
|
|
198
201
|
writeFingerprint(absPath, stampIssueSyncFingerprint(data, origin));
|
|
199
202
|
}
|
|
200
203
|
catch (exc) {
|