@deftai/directive-core 0.66.1 → 0.67.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-advisory/evaluate.d.ts +44 -0
- package/dist/agents-md-advisory/evaluate.js +115 -0
- package/dist/agents-md-advisory/index.d.ts +2 -0
- package/dist/agents-md-advisory/index.js +2 -0
- package/dist/agents-md-budget/evaluate.d.ts +38 -0
- package/dist/agents-md-budget/evaluate.js +152 -0
- package/dist/agents-md-budget/index.d.ts +2 -0
- package/dist/agents-md-budget/index.js +2 -0
- package/dist/codebase/map.d.ts +1 -0
- package/dist/codebase/map.js +5 -2
- package/dist/doctor/constants.d.ts +5 -2
- package/dist/doctor/constants.js +15 -1
- package/dist/doctor/flags.js +5 -1
- package/dist/doctor/main.js +99 -8
- package/dist/doctor/paths.d.ts +1 -0
- package/dist/doctor/paths.js +55 -0
- package/dist/doctor/types.d.ts +7 -0
- package/dist/intake/github-auth-modes.d.ts +1 -0
- package/dist/intake/github-auth-modes.js +1 -0
- package/dist/intake/issue-ingest.d.ts +6 -2
- package/dist/intake/issue-ingest.js +65 -9
- package/dist/platform/agents-consumer-header.d.ts +13 -0
- package/dist/platform/agents-consumer-header.js +57 -0
- package/dist/platform/agents-md.js +4 -1
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/agents-md-advisory.d.ts +52 -0
- package/dist/policy/agents-md-advisory.js +63 -0
- package/dist/policy/agents-md-budget.d.ts +24 -0
- package/dist/policy/agents-md-budget.js +89 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
- package/dist/pr-merge-readiness/ci-gate.js +79 -0
- package/dist/pr-merge-readiness/compute.d.ts +5 -1
- package/dist/pr-merge-readiness/compute.js +90 -6
- package/dist/pr-merge-readiness/gh.d.ts +8 -0
- package/dist/pr-merge-readiness/gh.js +30 -5
- package/dist/pr-merge-readiness/index.d.ts +3 -0
- package/dist/pr-merge-readiness/index.js +2 -0
- package/dist/pr-merge-readiness/main.d.ts +3 -0
- package/dist/pr-merge-readiness/main.js +73 -6
- package/dist/pr-merge-readiness/output.js +20 -0
- package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
- package/dist/pr-merge-readiness/slizard-gate.js +119 -0
- package/dist/preflight-cache/evaluate.d.ts +1 -0
- package/dist/preflight-cache/evaluate.js +17 -10
- package/dist/render/project-render.d.ts +16 -2
- package/dist/render/project-render.js +55 -22
- package/dist/swarm/launch.d.ts +6 -0
- package/dist/swarm/launch.js +2 -2
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +11 -10
- package/dist/swarm/routing.d.ts +9 -0
- package/dist/swarm/routing.js +46 -0
- package/dist/swarm/verify-review-clean-cli.js +25 -1
- package/dist/swarm/verify-review-clean.d.ts +9 -1
- package/dist/swarm/verify-review-clean.js +76 -3
- package/dist/verify-source/biome-config.d.ts +41 -0
- package/dist/verify-source/biome-config.js +97 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/xbrief-migrate/agents-header.d.ts +69 -0
- package/dist/xbrief-migrate/agents-header.js +179 -0
- package/dist/xbrief-migrate/drift-gate.js +2 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
- package/dist/xbrief-migrate/fs-helpers.js +11 -0
- package/dist/xbrief-migrate/index.d.ts +1 -0
- package/dist/xbrief-migrate/index.js +1 -0
- package/dist/xbrief-migrate/migrate-project.js +29 -10
- package/package.json +11 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildCiSummaryLine, evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
|
|
5
|
+
import { defaultRunGh, fetchCheckRunsRest, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
|
|
5
6
|
import { evaluateGates, parseGreptileBody } from "../pr-merge-readiness/index.js";
|
|
7
|
+
import { evaluateSlizardGate, isSlizardCheck } from "../pr-merge-readiness/slizard-gate.js";
|
|
6
8
|
import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
|
|
7
9
|
function extractPrFromUri(uri) {
|
|
8
10
|
const pullIdx = uri.indexOf("/pull/");
|
|
@@ -101,7 +103,7 @@ export function resolveCohortFromVbriefs(vbriefGlobs) {
|
|
|
101
103
|
}
|
|
102
104
|
return { prNumbers: seenPrs, failures };
|
|
103
105
|
}
|
|
104
|
-
export function evaluatePr(prNumber, repo, runGh = defaultRunGh) {
|
|
106
|
+
export function evaluatePr(prNumber, repo, runGh = defaultRunGh, options = {}) {
|
|
105
107
|
const headSha = fetchPrHeadSha(prNumber, repo, runGh);
|
|
106
108
|
if (headSha === null) {
|
|
107
109
|
return null;
|
|
@@ -112,12 +114,71 @@ export function evaluatePr(prNumber, repo, runGh = defaultRunGh) {
|
|
|
112
114
|
}
|
|
113
115
|
const verdict = parseGreptileBody(body);
|
|
114
116
|
const failures = evaluateGates(prNumber, headSha, verdict);
|
|
117
|
+
let ciSummary = null;
|
|
118
|
+
let slizardSummary = null;
|
|
119
|
+
if (failures.length === 0) {
|
|
120
|
+
if (repo === null) {
|
|
121
|
+
failures.push("Could not resolve repo for required CI check-run gate. " +
|
|
122
|
+
"Use --repo OWNER/REPO or run from a checked-out repository.");
|
|
123
|
+
ciSummary = {
|
|
124
|
+
ready_state: "blocked",
|
|
125
|
+
error: "repo unresolved for check-runs lookup",
|
|
126
|
+
};
|
|
127
|
+
slizardSummary = { ready_state: "skipped", present: false };
|
|
128
|
+
}
|
|
129
|
+
else if (options.skipCi === true) {
|
|
130
|
+
const ci = evaluateCiGate([], { skipCi: true });
|
|
131
|
+
ciSummary = {
|
|
132
|
+
...ci.summary,
|
|
133
|
+
summary_line: "CI check-runs: skipped (--skip-ci)",
|
|
134
|
+
};
|
|
135
|
+
const slizard = evaluateSlizardGate([], options);
|
|
136
|
+
failures.push(...slizard.failures);
|
|
137
|
+
slizardSummary = { ...slizard.summary };
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const checks = fetchCheckRunsRest(headSha, repo, runGh);
|
|
141
|
+
if (checks.summary === null) {
|
|
142
|
+
failures.push("Required CI check-runs could not be fetched; fail closed by default (#2169). " +
|
|
143
|
+
`Root cause: ${checks.error}`);
|
|
144
|
+
ciSummary = {
|
|
145
|
+
ready_state: "blocked",
|
|
146
|
+
error: checks.error,
|
|
147
|
+
checked_count: 0,
|
|
148
|
+
ignored_checks: [...(options.ignoreCheckNames ?? [])],
|
|
149
|
+
failed_required: [],
|
|
150
|
+
pending_required: [],
|
|
151
|
+
conclusions: [],
|
|
152
|
+
};
|
|
153
|
+
slizardSummary = { ready_state: "skipped", present: false };
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const slizard = evaluateSlizardGate(checks.checkRuns, options);
|
|
157
|
+
// SLizard is scored by the structured gate; exclude it from the generic CI set.
|
|
158
|
+
const slizardNames = checks.checkRuns
|
|
159
|
+
.filter((r) => isSlizardCheck(r.name))
|
|
160
|
+
.map((r) => r.name);
|
|
161
|
+
const ci = evaluateCiGate(checks.checkRuns, {
|
|
162
|
+
skipCi: options.skipCi,
|
|
163
|
+
ignoreCheckNames: [...(options.ignoreCheckNames ?? []), ...slizardNames],
|
|
164
|
+
});
|
|
165
|
+
failures.push(...ci.failures, ...slizard.failures);
|
|
166
|
+
ciSummary = {
|
|
167
|
+
...ci.summary,
|
|
168
|
+
summary_line: buildCiSummaryLine(ci.summary),
|
|
169
|
+
};
|
|
170
|
+
slizardSummary = { ...slizard.summary };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
115
174
|
return {
|
|
116
175
|
pr_number: prNumber,
|
|
117
176
|
head_sha: headSha,
|
|
118
177
|
verdict: { ...verdict },
|
|
119
178
|
failures: [...failures],
|
|
120
179
|
clean: failures.length === 0,
|
|
180
|
+
ci_summary: ciSummary,
|
|
181
|
+
slizard_summary: slizardSummary,
|
|
121
182
|
};
|
|
122
183
|
}
|
|
123
184
|
export function cohortResultToDict(cohort) {
|
|
@@ -131,6 +192,8 @@ export function cohortResultToDict(cohort) {
|
|
|
131
192
|
clean: r.clean,
|
|
132
193
|
verdict: r.verdict,
|
|
133
194
|
failures: r.failures,
|
|
195
|
+
ci_summary: r.ci_summary,
|
|
196
|
+
slizard_summary: r.slizard_summary,
|
|
134
197
|
})),
|
|
135
198
|
resolution_errors: cohort.resolution_errors,
|
|
136
199
|
};
|
|
@@ -159,6 +222,12 @@ export function renderReviewCleanText(cohort) {
|
|
|
159
222
|
lines.push(` Findings: P0=${String(v.p0Count ?? 0)} ` +
|
|
160
223
|
`P1=${String(v.p1Count ?? 0)} P2=${String(v.p2Count ?? 0)}`);
|
|
161
224
|
lines.push(` Errored sentinel: ${v.errored === true ? "True" : "False"}`);
|
|
225
|
+
if (r.ci_summary && typeof r.ci_summary.summary_line === "string") {
|
|
226
|
+
lines.push(` ${r.ci_summary.summary_line}`);
|
|
227
|
+
}
|
|
228
|
+
if (r.slizard_summary && typeof r.slizard_summary.summary_line === "string") {
|
|
229
|
+
lines.push(` ${r.slizard_summary.summary_line}`);
|
|
230
|
+
}
|
|
162
231
|
r.failures.forEach((fail, index) => {
|
|
163
232
|
lines.push(` [${index + 1}] ${fail}`);
|
|
164
233
|
});
|
|
@@ -212,7 +281,11 @@ export function verifyReviewClean(args) {
|
|
|
212
281
|
const runGh = args.runGh ?? defaultRunGh;
|
|
213
282
|
const prResults = [];
|
|
214
283
|
for (const prNum of prNumbers) {
|
|
215
|
-
const perPr = evaluatePr(prNum, args.repo ?? null, runGh
|
|
284
|
+
const perPr = evaluatePr(prNum, args.repo ?? null, runGh, {
|
|
285
|
+
skipCi: args.skipCi,
|
|
286
|
+
skipSlizard: args.skipSlizard,
|
|
287
|
+
ignoreCheckNames: args.ciIgnoreChecks,
|
|
288
|
+
});
|
|
216
289
|
if (perPr === null) {
|
|
217
290
|
return { exitCode: EXIT_EXTERNAL_ERROR, stdout: "", stderr: "" };
|
|
218
291
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* biome-config.ts -- guard against #2190 (biome check . reports
|
|
3
|
+
* recommended-preset diagnostics as errors vs warnings non-deterministically
|
|
4
|
+
* on a pinned biome version).
|
|
5
|
+
*
|
|
6
|
+
* A preset-inherited rule severity can silently change tier across a biome
|
|
7
|
+
* version bump (or, per #2190, apparently even across runs on the identical
|
|
8
|
+
* pinned version). Pinning `noUnusedVariables` / `noNonNullAssertion` to an
|
|
9
|
+
* explicit non-"error" severity in biome.json makes the tier config-owned so
|
|
10
|
+
* it cannot flip a required CI check to failing on pre-existing diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
/** The two rules #2190 observed flipping severity in CI (see docs/analysis/2026-07-02-2190-biome-determinism.md). */
|
|
13
|
+
export declare const GUARDED_RULES: ReadonlyArray<{
|
|
14
|
+
readonly group: string;
|
|
15
|
+
readonly rule: string;
|
|
16
|
+
}>;
|
|
17
|
+
export interface RuleSeverityFinding {
|
|
18
|
+
readonly group: string;
|
|
19
|
+
readonly rule: string;
|
|
20
|
+
/** The literal severity value found, or null when the rule has no explicit entry. */
|
|
21
|
+
readonly severity: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface BiomeConfigGuardResult {
|
|
24
|
+
readonly code: 0 | 1 | 2;
|
|
25
|
+
readonly findings: readonly RuleSeverityFinding[];
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a biome.json document (already JSON.parse'd) and report the explicit
|
|
30
|
+
* severity declared for each guarded rule.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findRuleSeverities(biomeConfig: unknown, rules?: ReadonlyArray<{
|
|
33
|
+
readonly group: string;
|
|
34
|
+
readonly rule: string;
|
|
35
|
+
}>): RuleSeverityFinding[];
|
|
36
|
+
/**
|
|
37
|
+
* Evaluate biome.json at `projectRoot` and fail closed when a guarded rule
|
|
38
|
+
* has no explicit severity, or an explicit severity of "error".
|
|
39
|
+
*/
|
|
40
|
+
export declare function evaluateBiomeConfigGuard(projectRoot?: string): BiomeConfigGuardResult;
|
|
41
|
+
//# sourceMappingURL=biome-config.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* biome-config.ts -- guard against #2190 (biome check . reports
|
|
3
|
+
* recommended-preset diagnostics as errors vs warnings non-deterministically
|
|
4
|
+
* on a pinned biome version).
|
|
5
|
+
*
|
|
6
|
+
* A preset-inherited rule severity can silently change tier across a biome
|
|
7
|
+
* version bump (or, per #2190, apparently even across runs on the identical
|
|
8
|
+
* pinned version). Pinning `noUnusedVariables` / `noNonNullAssertion` to an
|
|
9
|
+
* explicit non-"error" severity in biome.json makes the tier config-owned so
|
|
10
|
+
* it cannot flip a required CI check to failing on pre-existing diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
/** The two rules #2190 observed flipping severity in CI (see docs/analysis/2026-07-02-2190-biome-determinism.md). */
|
|
15
|
+
export const GUARDED_RULES = [
|
|
16
|
+
{ group: "correctness", rule: "noUnusedVariables" },
|
|
17
|
+
{ group: "style", rule: "noNonNullAssertion" },
|
|
18
|
+
];
|
|
19
|
+
/** Severity levels biome accepts that never fail the build. */
|
|
20
|
+
const NON_ERROR_LEVELS = new Set(["warn", "info", "off"]);
|
|
21
|
+
function extractSeverity(rulesNode, group, rule) {
|
|
22
|
+
if (typeof rulesNode !== "object" || rulesNode === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const groupNode = rulesNode[group];
|
|
26
|
+
if (typeof groupNode !== "object" || groupNode === null) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const ruleNode = groupNode[rule];
|
|
30
|
+
if (typeof ruleNode === "string") {
|
|
31
|
+
return ruleNode;
|
|
32
|
+
}
|
|
33
|
+
if (typeof ruleNode === "object" && ruleNode !== null) {
|
|
34
|
+
const level = ruleNode.level;
|
|
35
|
+
return typeof level === "string" ? level : null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a biome.json document (already JSON.parse'd) and report the explicit
|
|
41
|
+
* severity declared for each guarded rule.
|
|
42
|
+
*/
|
|
43
|
+
export function findRuleSeverities(biomeConfig, rules = GUARDED_RULES) {
|
|
44
|
+
const linter = typeof biomeConfig === "object" && biomeConfig !== null
|
|
45
|
+
? biomeConfig.linter
|
|
46
|
+
: null;
|
|
47
|
+
const rulesNode = typeof linter === "object" && linter !== null
|
|
48
|
+
? linter.rules
|
|
49
|
+
: null;
|
|
50
|
+
return rules.map(({ group, rule }) => ({
|
|
51
|
+
group,
|
|
52
|
+
rule,
|
|
53
|
+
severity: extractSeverity(rulesNode, group, rule),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Evaluate biome.json at `projectRoot` and fail closed when a guarded rule
|
|
58
|
+
* has no explicit severity, or an explicit severity of "error".
|
|
59
|
+
*/
|
|
60
|
+
export function evaluateBiomeConfigGuard(projectRoot = ".") {
|
|
61
|
+
const path = join(projectRoot, "biome.json");
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
raw = readFileSync(path, { encoding: "utf8" });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
return { code: 2, findings: [], message: `biome-config: cannot read ${path}: ${msg}` };
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return { code: 2, findings: [], message: `biome-config: ${path} is not valid JSON: ${msg}` };
|
|
77
|
+
}
|
|
78
|
+
const findings = findRuleSeverities(parsed);
|
|
79
|
+
const bad = findings.filter((f) => f.severity === null || !NON_ERROR_LEVELS.has(f.severity));
|
|
80
|
+
if (bad.length > 0) {
|
|
81
|
+
const lines = bad.map((f) => ` linter.rules.${f.group}.${f.rule}: ${f.severity === null ? "not explicitly set (inherits preset default)" : `"${f.severity}"`}`);
|
|
82
|
+
return {
|
|
83
|
+
code: 1,
|
|
84
|
+
findings,
|
|
85
|
+
message: "biome-config: guarded rule(s) lack an explicit non-error severity in biome.json (#2190 " +
|
|
86
|
+
"-- a preset-inherited severity can flip to error non-deterministically):\n" +
|
|
87
|
+
`${lines.join("\n")}\n` +
|
|
88
|
+
' Fix: declare "warn" (or "info") explicitly under linter.rules.<group>.<rule> in biome.json.',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
code: 0,
|
|
93
|
+
findings,
|
|
94
|
+
message: `biome-config: ${findings.length} guarded rule(s) have explicit non-error severities (#2190).`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=biome-config.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./biome-config.js";
|
|
1
2
|
export * from "./code-structure-validate.js";
|
|
2
3
|
export * from "./content-manifest.js";
|
|
3
4
|
export { CANONICAL_SCHEMA_REL, type ContractDriftOptions, type ContractDriftResult, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, ordered set of legacy crossover tokens rewritten in the UNMANAGED
|
|
3
|
+
* region of a consumer AGENTS.md after `migrate:xbrief` (#2154 / Option A).
|
|
4
|
+
*
|
|
5
|
+
* Each entry is a mechanical path / verb literal — NOT freeform prose. The
|
|
6
|
+
* casing-only `vBRIEF format` product-description token from the issue table is
|
|
7
|
+
* intentionally excluded so freeform prose survives untouched. The tokens are
|
|
8
|
+
* disjoint substrings (`.vbrief.json` has no trailing slash, `vbrief:preflight`
|
|
9
|
+
* uses a colon, `vbrief/` requires a slash), so replacement order does not
|
|
10
|
+
* change the result and a second pass is a guaranteed no-op (idempotent).
|
|
11
|
+
*/
|
|
12
|
+
export declare const LEGACY_HEADER_TOKENS: ReadonlyArray<{
|
|
13
|
+
readonly legacy: string;
|
|
14
|
+
readonly migrated: string;
|
|
15
|
+
}>;
|
|
16
|
+
export interface HeaderTokenReplacement {
|
|
17
|
+
readonly legacy: string;
|
|
18
|
+
readonly migrated: string;
|
|
19
|
+
readonly count: number;
|
|
20
|
+
}
|
|
21
|
+
export interface HeaderRewriteResult {
|
|
22
|
+
readonly content: string;
|
|
23
|
+
readonly changed: boolean;
|
|
24
|
+
readonly replacements: HeaderTokenReplacement[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rewrite known legacy `vbrief` crossover tokens in the UNMANAGED region(s) of
|
|
28
|
+
* an AGENTS.md document, leaving every `<!-- deft:managed-section ... -->` block
|
|
29
|
+
* byte-for-byte intact. Idempotent: running on already-migrated content is a
|
|
30
|
+
* no-op. Managed sections are located by literal markers on the raw text so no
|
|
31
|
+
* line-ending normalisation is performed (UTF-8 / CRLF safe).
|
|
32
|
+
*/
|
|
33
|
+
export declare function rewriteUnmanagedHeaderTokens(content: string): HeaderRewriteResult;
|
|
34
|
+
export interface HeaderPatchOutcome {
|
|
35
|
+
readonly kind: "patched" | "clean" | "absent" | "failed";
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly replacements: HeaderTokenReplacement[];
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read AGENTS.md at `projectRoot`, rewrite legacy tokens in the unmanaged
|
|
42
|
+
* header, and write the result back only when something changed. Returns a
|
|
43
|
+
* structured outcome so the caller can log a LEGACY-REPORT-style summary.
|
|
44
|
+
*
|
|
45
|
+
* Non-fatal: a write failure (read-only file, full disk) is captured as a
|
|
46
|
+
* `failed` outcome rather than thrown, so a post-migration header patch can
|
|
47
|
+
* never crash a migration that already succeeded.
|
|
48
|
+
*/
|
|
49
|
+
export declare function patchAgentsMdHeader(projectRoot: string, seams?: {
|
|
50
|
+
readText?: (path: string) => string | null;
|
|
51
|
+
writeText?: (path: string, text: string) => void;
|
|
52
|
+
}): HeaderPatchOutcome;
|
|
53
|
+
/** Human-readable one-line summary of a header patch outcome (LEGACY-REPORT style). */
|
|
54
|
+
export declare function renderHeaderPatchSummary(outcome: HeaderPatchOutcome): string;
|
|
55
|
+
export interface StaleHeaderDetection {
|
|
56
|
+
readonly stale: boolean;
|
|
57
|
+
readonly matches: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Detect the #2154 half-migrated state: the `xbrief/` tree exists (lifecycle
|
|
61
|
+
* migration + managed-section refresh already happened) yet the UNMANAGED
|
|
62
|
+
* AGENTS.md header still references legacy `vbrief` path / verb literals — a
|
|
63
|
+
* regression `deft doctor` cannot see because the managed-section byte compare
|
|
64
|
+
* passes (#1308). Returns the matched legacy tokens for the signpost.
|
|
65
|
+
*/
|
|
66
|
+
export declare function detectStaleUnmanagedHeader(projectRoot: string, readText?: (path: string) => string | null): StaleHeaderDetection;
|
|
67
|
+
/** One-line doctor / ritual signpost for the #2154 stale-header regression. */
|
|
68
|
+
export declare function renderStaleHeaderLine(projectRoot: string, readText?: (path: string) => string | null): string;
|
|
69
|
+
//# sourceMappingURL=agents-header.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { iterManagedSections } from "../platform/agents-md.js";
|
|
4
|
+
import { MIGRATED_ARTIFACT_DIR } from "./constants.js";
|
|
5
|
+
import { isDirectory } from "./fs-helpers.js";
|
|
6
|
+
/**
|
|
7
|
+
* Bounded, ordered set of legacy crossover tokens rewritten in the UNMANAGED
|
|
8
|
+
* region of a consumer AGENTS.md after `migrate:xbrief` (#2154 / Option A).
|
|
9
|
+
*
|
|
10
|
+
* Each entry is a mechanical path / verb literal — NOT freeform prose. The
|
|
11
|
+
* casing-only `vBRIEF format` product-description token from the issue table is
|
|
12
|
+
* intentionally excluded so freeform prose survives untouched. The tokens are
|
|
13
|
+
* disjoint substrings (`.vbrief.json` has no trailing slash, `vbrief:preflight`
|
|
14
|
+
* uses a colon, `vbrief/` requires a slash), so replacement order does not
|
|
15
|
+
* change the result and a second pass is a guaranteed no-op (idempotent).
|
|
16
|
+
*/
|
|
17
|
+
export const LEGACY_HEADER_TOKENS = [
|
|
18
|
+
{ legacy: ".vbrief.json", migrated: ".xbrief.json" },
|
|
19
|
+
{ legacy: "vbrief:preflight", migrated: "xbrief:preflight" },
|
|
20
|
+
{ legacy: "vbrief/", migrated: "xbrief/" },
|
|
21
|
+
];
|
|
22
|
+
function countOccurrences(haystack, needle) {
|
|
23
|
+
if (needle.length === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
let count = 0;
|
|
26
|
+
let index = haystack.indexOf(needle);
|
|
27
|
+
while (index !== -1) {
|
|
28
|
+
count += 1;
|
|
29
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
30
|
+
}
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
/** Rewrite the bounded legacy tokens inside a single unmanaged text slice. */
|
|
34
|
+
function rewriteSlice(slice, tally) {
|
|
35
|
+
let next = slice;
|
|
36
|
+
for (const { legacy, migrated } of LEGACY_HEADER_TOKENS) {
|
|
37
|
+
const occurrences = countOccurrences(next, legacy);
|
|
38
|
+
if (occurrences === 0)
|
|
39
|
+
continue;
|
|
40
|
+
tally.set(legacy, (tally.get(legacy) ?? 0) + occurrences);
|
|
41
|
+
next = next.replaceAll(legacy, migrated);
|
|
42
|
+
}
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rewrite known legacy `vbrief` crossover tokens in the UNMANAGED region(s) of
|
|
47
|
+
* an AGENTS.md document, leaving every `<!-- deft:managed-section ... -->` block
|
|
48
|
+
* byte-for-byte intact. Idempotent: running on already-migrated content is a
|
|
49
|
+
* no-op. Managed sections are located by literal markers on the raw text so no
|
|
50
|
+
* line-ending normalisation is performed (UTF-8 / CRLF safe).
|
|
51
|
+
*/
|
|
52
|
+
export function rewriteUnmanagedHeaderTokens(content) {
|
|
53
|
+
const managed = iterManagedSections(content);
|
|
54
|
+
const tally = new Map();
|
|
55
|
+
let out = "";
|
|
56
|
+
let cursor = 0;
|
|
57
|
+
for (const [start, end] of managed) {
|
|
58
|
+
// Unmanaged slice before this managed block: eligible for rewrite.
|
|
59
|
+
out += rewriteSlice(content.slice(cursor, start), tally);
|
|
60
|
+
// Managed block: preserved verbatim.
|
|
61
|
+
out += content.slice(start, end);
|
|
62
|
+
cursor = end;
|
|
63
|
+
}
|
|
64
|
+
// Trailing unmanaged slice after the last managed block (or the whole file
|
|
65
|
+
// when there is no managed section at all).
|
|
66
|
+
out += rewriteSlice(content.slice(cursor), tally);
|
|
67
|
+
const replacements = LEGACY_HEADER_TOKENS.filter((t) => tally.has(t.legacy)).map((t) => ({
|
|
68
|
+
legacy: t.legacy,
|
|
69
|
+
migrated: t.migrated,
|
|
70
|
+
count: tally.get(t.legacy) ?? 0,
|
|
71
|
+
}));
|
|
72
|
+
return { content: out, changed: out !== content, replacements };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read AGENTS.md at `projectRoot`, rewrite legacy tokens in the unmanaged
|
|
76
|
+
* header, and write the result back only when something changed. Returns a
|
|
77
|
+
* structured outcome so the caller can log a LEGACY-REPORT-style summary.
|
|
78
|
+
*
|
|
79
|
+
* Non-fatal: a write failure (read-only file, full disk) is captured as a
|
|
80
|
+
* `failed` outcome rather than thrown, so a post-migration header patch can
|
|
81
|
+
* never crash a migration that already succeeded.
|
|
82
|
+
*/
|
|
83
|
+
export function patchAgentsMdHeader(projectRoot, seams = {}) {
|
|
84
|
+
const agentsPath = join(projectRoot, "AGENTS.md");
|
|
85
|
+
const readText = seams.readText ??
|
|
86
|
+
((path) => {
|
|
87
|
+
try {
|
|
88
|
+
if (!existsSync(path))
|
|
89
|
+
return null;
|
|
90
|
+
return readFileSync(path, "utf8");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const writeText = seams.writeText ?? ((path, text) => writeFileSync(path, text, "utf8"));
|
|
97
|
+
const existing = readText(agentsPath);
|
|
98
|
+
if (existing === null) {
|
|
99
|
+
return { kind: "absent", path: agentsPath, replacements: [] };
|
|
100
|
+
}
|
|
101
|
+
const result = rewriteUnmanagedHeaderTokens(existing);
|
|
102
|
+
if (!result.changed) {
|
|
103
|
+
return { kind: "clean", path: agentsPath, replacements: [] };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
writeText(agentsPath, result.content);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return {
|
|
110
|
+
kind: "failed",
|
|
111
|
+
path: agentsPath,
|
|
112
|
+
replacements: [],
|
|
113
|
+
error: err instanceof Error ? err.message : String(err),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { kind: "patched", path: agentsPath, replacements: result.replacements };
|
|
117
|
+
}
|
|
118
|
+
/** Human-readable one-line summary of a header patch outcome (LEGACY-REPORT style). */
|
|
119
|
+
export function renderHeaderPatchSummary(outcome) {
|
|
120
|
+
if (outcome.kind === "absent") {
|
|
121
|
+
return "AGENTS.md unmanaged header: no AGENTS.md present — nothing to patch.";
|
|
122
|
+
}
|
|
123
|
+
if (outcome.kind === "clean") {
|
|
124
|
+
return "AGENTS.md unmanaged header: no legacy vbrief tokens found — nothing to patch.";
|
|
125
|
+
}
|
|
126
|
+
if (outcome.kind === "failed") {
|
|
127
|
+
return (`AGENTS.md unmanaged header: patch failed (${outcome.error ?? "unknown error"}) — ` +
|
|
128
|
+
"re-run `deft migrate:xbrief` (idempotent) or hand-edit the header.");
|
|
129
|
+
}
|
|
130
|
+
const total = outcome.replacements.reduce((sum, r) => sum + r.count, 0);
|
|
131
|
+
const detail = outcome.replacements.map((r) => `${r.legacy} ×${r.count}`).join(", ");
|
|
132
|
+
return `AGENTS.md unmanaged header: rewrote ${total} legacy vbrief token(s) -> xbrief (${detail}).`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Detect the #2154 half-migrated state: the `xbrief/` tree exists (lifecycle
|
|
136
|
+
* migration + managed-section refresh already happened) yet the UNMANAGED
|
|
137
|
+
* AGENTS.md header still references legacy `vbrief` path / verb literals — a
|
|
138
|
+
* regression `deft doctor` cannot see because the managed-section byte compare
|
|
139
|
+
* passes (#1308). Returns the matched legacy tokens for the signpost.
|
|
140
|
+
*/
|
|
141
|
+
export function detectStaleUnmanagedHeader(projectRoot, readText = (path) => {
|
|
142
|
+
try {
|
|
143
|
+
if (!existsSync(path))
|
|
144
|
+
return null;
|
|
145
|
+
return readFileSync(path, "utf8");
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}) {
|
|
151
|
+
if (!isDirectory(join(projectRoot, MIGRATED_ARTIFACT_DIR))) {
|
|
152
|
+
return { stale: false, matches: [] };
|
|
153
|
+
}
|
|
154
|
+
const content = readText(join(projectRoot, "AGENTS.md"));
|
|
155
|
+
if (content === null) {
|
|
156
|
+
return { stale: false, matches: [] };
|
|
157
|
+
}
|
|
158
|
+
const managed = iterManagedSections(content);
|
|
159
|
+
let unmanaged = "";
|
|
160
|
+
let cursor = 0;
|
|
161
|
+
for (const [start, end] of managed) {
|
|
162
|
+
unmanaged += content.slice(cursor, start);
|
|
163
|
+
cursor = end;
|
|
164
|
+
}
|
|
165
|
+
unmanaged += content.slice(cursor);
|
|
166
|
+
const matches = LEGACY_HEADER_TOKENS.filter((t) => unmanaged.includes(t.legacy)).map((t) => t.legacy);
|
|
167
|
+
return { stale: matches.length > 0, matches };
|
|
168
|
+
}
|
|
169
|
+
/** One-line doctor / ritual signpost for the #2154 stale-header regression. */
|
|
170
|
+
export function renderStaleHeaderLine(projectRoot, readText) {
|
|
171
|
+
const { stale, matches } = detectStaleUnmanagedHeader(projectRoot, readText);
|
|
172
|
+
if (!stale) {
|
|
173
|
+
return "AGENTS.md header drift: none -- unmanaged header has no legacy vbrief path literals.";
|
|
174
|
+
}
|
|
175
|
+
return (`AGENTS.md header drift: xbrief/ tree present but the unmanaged AGENTS.md header still ` +
|
|
176
|
+
`references legacy token(s) ${matches.join(", ")}. ` +
|
|
177
|
+
"Run `deft migrate:xbrief` (idempotent) to rewrite them, or hand-edit the header.");
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=agents-header.js.map
|
|
@@ -38,6 +38,8 @@ import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIG
|
|
|
38
38
|
export const LEGACY_REFERENCE_PREFIX = "x-vbrief/";
|
|
39
39
|
/** Default allowlist: sanctioned data-plane trees that legitimately retain legacy tokens for back-compat. */
|
|
40
40
|
export const BUILTIN_ALLOW_LIST = [
|
|
41
|
+
// Vendored npm deposit (#2146): C1 flatten maps content/vbrief/* -> .deft/core/vbrief/*.
|
|
42
|
+
".deft/core/vbrief/**",
|
|
41
43
|
// Shipped vBRIEF surface (#1875 C3) + conformance round-trip fixtures (#715).
|
|
42
44
|
"content/vbrief/**",
|
|
43
45
|
// Test fixtures exercising the legacy read path + pre-cutover migration (#2108 / #2110)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
/** True when `path` exists and is a directory; false on any stat error. */
|
|
3
|
+
export function isDirectory(path) {
|
|
4
|
+
try {
|
|
5
|
+
return statSync(path).isDirectory();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=fs-helpers.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { detectStaleUnmanagedHeader, type HeaderPatchOutcome, type HeaderRewriteResult, type HeaderTokenReplacement, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, type StaleHeaderDetection, } from "./agents-header.js";
|
|
1
2
|
export { detectLegacyVbriefLayout, type LegacyVbriefLayoutDetection, } from "./detect.js";
|
|
2
3
|
export { BUILTIN_ALLOW_LIST, type DriftEvaluateOptions, type DriftEvaluateResult, type DriftFinding, type DriftScanMode, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
|
|
3
4
|
export { emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, type XbriefMigrationArgs, type XbriefMigrationIo, type XbriefMigrationOutcome, } from "./migrate-project.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { detectStaleUnmanagedHeader, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, } from "./agents-header.js";
|
|
1
2
|
export { detectLegacyVbriefLayout, } from "./detect.js";
|
|
2
3
|
export { BUILTIN_ALLOW_LIST, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
|
|
3
4
|
export { emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, } from "./migrate-project.js";
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync,
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { checkGitClean } from "../migrate-preflight/index.js";
|
|
4
4
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
5
|
+
import { patchAgentsMdHeader, renderHeaderPatchSummary } from "./agents-header.js";
|
|
5
6
|
import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, } from "./constants.js";
|
|
6
7
|
import { detectLegacyVbriefLayout } from "./detect.js";
|
|
8
|
+
import { isDirectory } from "./fs-helpers.js";
|
|
7
9
|
import { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
|
|
8
10
|
import { rewriteEmbeddedTokens, transformArtifactV06ToV08Transactional } from "./transforms.js";
|
|
9
|
-
function isDirectory(path) {
|
|
10
|
-
try {
|
|
11
|
-
return statSync(path).isDirectory();
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
11
|
function collectFiles(root, acc = []) {
|
|
18
12
|
if (!isDirectory(root)) {
|
|
19
13
|
return acc;
|
|
@@ -177,6 +171,26 @@ export function emitXbriefMigration(outcome, io, options = {}) {
|
|
|
177
171
|
}
|
|
178
172
|
}
|
|
179
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Rewrite legacy `vbrief` crossover tokens in the UNMANAGED AGENTS.md header
|
|
176
|
+
* after lifecycle migration + agents:refresh (#2154). The managed section is
|
|
177
|
+
* left byte-for-byte intact; only the freeform header/tail path literals are
|
|
178
|
+
* patched. Idempotent and non-fatal — a header with no legacy tokens is a
|
|
179
|
+
* clean no-op, and a write failure surfaces as a `failed` outcome on stderr
|
|
180
|
+
* rather than an exception. Always returns 0; the migration itself already
|
|
181
|
+
* succeeded, so a header-patch hiccup must not fail the whole command.
|
|
182
|
+
*/
|
|
183
|
+
function runHeaderPatch(projectRoot, io) {
|
|
184
|
+
const outcome = patchAgentsMdHeader(projectRoot);
|
|
185
|
+
const summary = `${renderHeaderPatchSummary(outcome)}\n`;
|
|
186
|
+
if (outcome.kind === "failed") {
|
|
187
|
+
io.writeErr(summary);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
io.writeOut(summary);
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
180
194
|
/** End-to-end migrate:xbrief handler including optional agents:refresh (#2110). */
|
|
181
195
|
export function runXbriefMigrationCli(args, io) {
|
|
182
196
|
const outcome = runXbriefMigration(args, io);
|
|
@@ -184,6 +198,11 @@ export function runXbriefMigrationCli(args, io) {
|
|
|
184
198
|
if (code !== 0 || outcome.kind !== "migrated") {
|
|
185
199
|
return code;
|
|
186
200
|
}
|
|
187
|
-
|
|
201
|
+
const projectRoot = resolve(args.projectRoot);
|
|
202
|
+
const refreshCode = runAgentsRefresh(projectRoot, args.frameworkRoot, io);
|
|
203
|
+
if (refreshCode !== 0) {
|
|
204
|
+
return refreshCode;
|
|
205
|
+
}
|
|
206
|
+
return runHeaderPatch(projectRoot, io);
|
|
188
207
|
}
|
|
189
208
|
//# sourceMappingURL=migrate-project.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"types": "./dist/wip-cap/index.d.ts",
|
|
31
31
|
"default": "./dist/wip-cap/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./agents-md-budget": {
|
|
34
|
+
"types": "./dist/agents-md-budget/index.d.ts",
|
|
35
|
+
"default": "./dist/agents-md-budget/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./agents-md-advisory": {
|
|
38
|
+
"types": "./dist/agents-md-advisory/index.d.ts",
|
|
39
|
+
"default": "./dist/agents-md-advisory/index.js"
|
|
40
|
+
},
|
|
33
41
|
"./xbrief-migrate": {
|
|
34
42
|
"types": "./dist/xbrief-migrate/index.d.ts",
|
|
35
43
|
"default": "./dist/xbrief-migrate/index.js"
|
|
@@ -237,8 +245,8 @@
|
|
|
237
245
|
"provenance": true
|
|
238
246
|
},
|
|
239
247
|
"dependencies": {
|
|
240
|
-
"@deftai/directive-content": "^0.
|
|
241
|
-
"@deftai/directive-types": "^0.
|
|
248
|
+
"@deftai/directive-content": "^0.67.0",
|
|
249
|
+
"@deftai/directive-types": "^0.67.0",
|
|
242
250
|
"archiver": "^8.0.0"
|
|
243
251
|
},
|
|
244
252
|
"scripts": {
|