@deftai/directive-core 0.60.0 → 0.61.1
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/init-deposit/refresh.js +48 -1
- package/dist/init-deposit/scaffold.d.ts +7 -0
- package/dist/init-deposit/scaffold.js +7 -2
- package/dist/install-upgrade/index.js +88 -12
- package/dist/render/export-spec.d.ts +17 -0
- package/dist/render/export-spec.js +104 -0
- package/dist/render/index.d.ts +2 -0
- package/dist/render/index.js +2 -0
- package/dist/render/scope-outlook.d.ts +9 -0
- package/dist/render/scope-outlook.js +248 -0
- package/dist/render/spec-render.js +3 -192
- package/dist/spec-authority/constants.d.ts +12 -0
- package/dist/spec-authority/constants.js +34 -0
- package/dist/spec-authority/index.d.ts +5 -0
- package/dist/spec-authority/index.js +5 -0
- package/dist/spec-authority/migration-fidelity.d.ts +6 -0
- package/dist/spec-authority/migration-fidelity.js +96 -0
- package/dist/spec-authority/narratives.d.ts +6 -0
- package/dist/spec-authority/narratives.js +84 -0
- package/dist/spec-authority/resolver.d.ts +15 -0
- package/dist/spec-authority/resolver.js +52 -0
- package/dist/validate-content/validate-strategy-output.js +5 -26
- package/dist/vbrief-validate/main.js +5 -0
- package/dist/vbrief-validate/precutover.d.ts +4 -18
- package/dist/vbrief-validate/precutover.js +17 -23
- package/dist/verify-env/verify-hooks-installed.d.ts +3 -3
- package/dist/verify-env/verify-hooks-installed.js +59 -27
- package/package.json +3 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { SPECIFICATION_NARRATIVE_KEY_ORDER } from "../render/constants.js";
|
|
3
|
+
import { STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS } from "./constants.js";
|
|
4
|
+
function loadJson(path) {
|
|
5
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
6
|
+
}
|
|
7
|
+
function planNarratives(doc) {
|
|
8
|
+
const plan = doc.plan;
|
|
9
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan))
|
|
10
|
+
return {};
|
|
11
|
+
const narratives = plan.narratives;
|
|
12
|
+
if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives))
|
|
13
|
+
return {};
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [key, val] of Object.entries(narratives)) {
|
|
16
|
+
if (typeof val === "string")
|
|
17
|
+
out[key] = val;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function isExcludedNarrativeKey(key) {
|
|
22
|
+
return STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS.has(key.toLowerCase().replace(/\s+/g, ""));
|
|
23
|
+
}
|
|
24
|
+
/** Merge product narratives per Wave 0 locked precedence. */
|
|
25
|
+
export function resolveExportNarratives(authority) {
|
|
26
|
+
const pd = loadJson(authority.projectDefPath);
|
|
27
|
+
const pdNarratives = planNarratives(pd);
|
|
28
|
+
if (authority.kind === "greenfield") {
|
|
29
|
+
const filtered = {};
|
|
30
|
+
for (const [key, val] of Object.entries(pdNarratives)) {
|
|
31
|
+
if (!isExcludedNarrativeKey(key) && val.trim())
|
|
32
|
+
filtered[key] = val;
|
|
33
|
+
}
|
|
34
|
+
return filtered;
|
|
35
|
+
}
|
|
36
|
+
const specPath = authority.specPath;
|
|
37
|
+
if (!specPath)
|
|
38
|
+
return pdNarratives;
|
|
39
|
+
const spec = loadJson(specPath);
|
|
40
|
+
const specNarratives = planNarratives(spec);
|
|
41
|
+
const merged = {};
|
|
42
|
+
for (const [key, val] of Object.entries(specNarratives)) {
|
|
43
|
+
if (!isExcludedNarrativeKey(key) && val.trim())
|
|
44
|
+
merged[key] = val;
|
|
45
|
+
}
|
|
46
|
+
// PD identity additive when missing from spec.
|
|
47
|
+
const identityHints = ["overview", "architecture", "risksandunknowns", "risks", "unknowns"];
|
|
48
|
+
for (const [key, val] of Object.entries(pdNarratives)) {
|
|
49
|
+
if (!val.trim() || isExcludedNarrativeKey(key))
|
|
50
|
+
continue;
|
|
51
|
+
const lower = key.toLowerCase().replace(/\s+/g, "");
|
|
52
|
+
const isIdentity = identityHints.some((h) => lower.includes(h)) || lower === "overview";
|
|
53
|
+
if (!isIdentity)
|
|
54
|
+
continue;
|
|
55
|
+
const specHasKey = Object.keys(merged).some((k) => k.toLowerCase() === lower);
|
|
56
|
+
if (!specHasKey)
|
|
57
|
+
merged[key] = val;
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
61
|
+
export function renderNarrativeSections(narratives) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
const rendered = new Set();
|
|
64
|
+
for (const key of SPECIFICATION_NARRATIVE_KEY_ORDER) {
|
|
65
|
+
const val = narratives[key];
|
|
66
|
+
if (val?.trim()) {
|
|
67
|
+
lines.push(`## ${key}\n`, `${val}\n`);
|
|
68
|
+
rendered.add(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const key of Object.keys(narratives).sort()) {
|
|
72
|
+
if (rendered.has(key) || !narratives[key]?.trim())
|
|
73
|
+
continue;
|
|
74
|
+
lines.push(`## ${key}\n`, `${narratives[key]}\n`);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
export function greenfieldOverviewNonEmpty(authority) {
|
|
79
|
+
const pd = loadJson(authority.projectDefPath);
|
|
80
|
+
const narratives = planNarratives(pd);
|
|
81
|
+
const overview = Object.entries(narratives).find(([k]) => k.toLowerCase() === "overview");
|
|
82
|
+
return overview !== undefined && overview[1].trim().length > 0;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=narratives.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SpecAuthorityKind = "full-spec" | "greenfield";
|
|
2
|
+
export interface ResolvedSpecAuthority {
|
|
3
|
+
readonly kind: SpecAuthorityKind;
|
|
4
|
+
readonly projectRoot: string;
|
|
5
|
+
readonly vbriefDir: string;
|
|
6
|
+
readonly projectDefPath: string;
|
|
7
|
+
readonly specPath: string | null;
|
|
8
|
+
readonly banner: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveSpecAuthority(projectRoot: string): ResolvedSpecAuthority | null;
|
|
11
|
+
export declare function readSpecMarkdown(projectRoot: string): string;
|
|
12
|
+
export declare function isFullSpecState(projectRoot: string): boolean;
|
|
13
|
+
export declare function isGreenfieldSpecExport(projectRoot: string): boolean;
|
|
14
|
+
export declare function isCurrentGeneratedSpecification(projectRoot: string): boolean;
|
|
15
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SPEC_RENDER_BANNER } from "../render/constants.js";
|
|
4
|
+
import { EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, GENERATED_SPEC_SOURCE_PD, GENERATED_SPEC_SOURCE_SPEC, } from "./constants.js";
|
|
5
|
+
const SPEC_REL = join("vbrief", "specification.vbrief.json");
|
|
6
|
+
const PD_REL = join("vbrief", "PROJECT-DEFINITION.vbrief.json");
|
|
7
|
+
export function resolveSpecAuthority(projectRoot) {
|
|
8
|
+
const root = projectRoot;
|
|
9
|
+
const vbriefDir = join(root, "vbrief");
|
|
10
|
+
const projectDefPath = join(root, PD_REL);
|
|
11
|
+
if (!existsSync(projectDefPath))
|
|
12
|
+
return null;
|
|
13
|
+
const specPath = join(root, SPEC_REL);
|
|
14
|
+
const hasSpec = existsSync(specPath);
|
|
15
|
+
const kind = hasSpec ? "full-spec" : "greenfield";
|
|
16
|
+
const banner = kind === "full-spec" ? SPEC_RENDER_BANNER : EXPORT_SPEC_PD_BANNER;
|
|
17
|
+
return {
|
|
18
|
+
kind,
|
|
19
|
+
projectRoot: root,
|
|
20
|
+
vbriefDir,
|
|
21
|
+
projectDefPath,
|
|
22
|
+
specPath: hasSpec ? specPath : null,
|
|
23
|
+
banner,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function readSpecMarkdown(projectRoot) {
|
|
27
|
+
const path = join(projectRoot, "SPECIFICATION.md");
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(path, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function isFullSpecState(projectRoot) {
|
|
36
|
+
const authority = resolveSpecAuthority(projectRoot);
|
|
37
|
+
if (authority?.kind !== "full-spec")
|
|
38
|
+
return false;
|
|
39
|
+
const specMd = readSpecMarkdown(projectRoot);
|
|
40
|
+
return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd.includes(GENERATED_SPEC_SOURCE_SPEC);
|
|
41
|
+
}
|
|
42
|
+
export function isGreenfieldSpecExport(projectRoot) {
|
|
43
|
+
const authority = resolveSpecAuthority(projectRoot);
|
|
44
|
+
if (authority?.kind !== "greenfield")
|
|
45
|
+
return false;
|
|
46
|
+
const specMd = readSpecMarkdown(projectRoot);
|
|
47
|
+
return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd.includes(GENERATED_SPEC_SOURCE_PD);
|
|
48
|
+
}
|
|
49
|
+
export function isCurrentGeneratedSpecification(projectRoot) {
|
|
50
|
+
return isFullSpecState(projectRoot) || isGreenfieldSpecExport(projectRoot);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { checkSpecMigrationFidelity } from "../spec-authority/migration-fidelity.js";
|
|
4
|
+
import { isFullSpecState } from "../spec-authority/resolver.js";
|
|
3
5
|
import { isDatePrefixedVbriefFilename } from "./filename.js";
|
|
4
|
-
const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
5
|
-
const GENERATED_SPEC_SOURCE = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
6
6
|
const LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
7
|
-
function readTextSafe(path) {
|
|
8
|
-
try {
|
|
9
|
-
return readFileSync(path, "utf8");
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return "";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
7
|
function isDirSafe(path) {
|
|
16
8
|
try {
|
|
17
9
|
return statSync(path).isDirectory();
|
|
@@ -30,20 +22,6 @@ function isDeftFrameworkRoot(projectRoot) {
|
|
|
30
22
|
(isDirSafe(join(projectRoot, "content", "strategies")) ||
|
|
31
23
|
isDirSafe(join(projectRoot, "strategies"))));
|
|
32
24
|
}
|
|
33
|
-
function hasCompleteLifecycle(vbriefDir) {
|
|
34
|
-
return LIFECYCLE_DIRS.every((folder) => {
|
|
35
|
-
const p = join(vbriefDir, folder);
|
|
36
|
-
return existsSync(p) && statSync(p).isDirectory();
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function isPostCutoverFullSpecState(projectRoot) {
|
|
40
|
-
const vbriefDir = join(projectRoot, "vbrief");
|
|
41
|
-
const specMd = readTextSafe(join(projectRoot, "SPECIFICATION.md"));
|
|
42
|
-
return (existsSync(join(vbriefDir, "PROJECT-DEFINITION.vbrief.json")) &&
|
|
43
|
-
hasCompleteLifecycle(vbriefDir) &&
|
|
44
|
-
specMd.includes(GENERATED_SPEC_PURPOSE) &&
|
|
45
|
-
specMd.includes(GENERATED_SPEC_SOURCE));
|
|
46
|
-
}
|
|
47
25
|
/** Pure validator returning error strings (empty == pass). */
|
|
48
26
|
export function validateStrategyOutput(projectRoot, strict = false) {
|
|
49
27
|
const root = resolve(projectRoot);
|
|
@@ -63,7 +41,7 @@ export function validateStrategyOutput(projectRoot, strict = false) {
|
|
|
63
41
|
"(see v0-20-contract.md and task project:render).");
|
|
64
42
|
}
|
|
65
43
|
const specLegacy = join(vbriefDir, "specification.vbrief.json");
|
|
66
|
-
if (existsSync(specLegacy) && !isDeftFrameworkRoot(root) && !
|
|
44
|
+
if (existsSync(specLegacy) && !isDeftFrameworkRoot(root) && !isFullSpecState(root)) {
|
|
67
45
|
errors.push("Legacy artifact vbrief/specification.vbrief.json present. " +
|
|
68
46
|
"v0.20 strategies MUST NOT dual-write the old specification.vbrief.json " +
|
|
69
47
|
"alongside scope vBRIEFs in the lifecycle folders. " +
|
|
@@ -85,6 +63,7 @@ export function validateStrategyOutput(projectRoot, strict = false) {
|
|
|
85
63
|
}
|
|
86
64
|
}
|
|
87
65
|
}
|
|
66
|
+
errors.push(...checkSpecMigrationFidelity(root));
|
|
88
67
|
return errors;
|
|
89
68
|
}
|
|
90
69
|
const FAILURE_HEADER = "❌ Strategy output shape validation FAILED (v0.20 contract gate)";
|
|
@@ -122,6 +122,11 @@ export function cmdVbriefValidate(argv) {
|
|
|
122
122
|
if (argv[0] === "conformance") {
|
|
123
123
|
return runConformance(argv.slice(1));
|
|
124
124
|
}
|
|
125
|
+
if (argv.includes("--staged") ||
|
|
126
|
+
(argv.includes("--all") &&
|
|
127
|
+
!argv.some((a) => a === "--vbrief-dir" || a.startsWith("--vbrief-dir=")))) {
|
|
128
|
+
return runConformance(argv);
|
|
129
|
+
}
|
|
125
130
|
return runValidate(argv);
|
|
126
131
|
}
|
|
127
132
|
//# sourceMappingURL=main.js.map
|
|
@@ -1,35 +1,21 @@
|
|
|
1
|
+
import { isFullSpecState, isGreenfieldSpecExport } from "../spec-authority/resolver.js";
|
|
1
2
|
import { DEPRECATION_SENTINEL } from "../vbrief-build/constants.js";
|
|
2
3
|
export { DEPRECATION_SENTINEL as DEPRECATED_REDIRECT_SENTINEL };
|
|
3
4
|
export declare function missingLifecycleFolders(projectRoot: string): string[];
|
|
4
5
|
/** Return true when markdown content is a migration redirect stub. */
|
|
5
6
|
export declare function isDeprecationRedirect(content: string): boolean;
|
|
7
|
+
/** Full-spec generated export (specification.vbrief.json source line). */
|
|
6
8
|
export declare function isGeneratedSpecificationExport(projectRoot: string, content: string): boolean;
|
|
7
|
-
/** Return true for a fully current
|
|
9
|
+
/** Return true for a fully current generated spec export (full-spec or greenfield). */
|
|
8
10
|
export declare function isCurrentGeneratedSpecification(projectRoot: string, content: string): boolean;
|
|
9
11
|
/** Return root artifact filenames that are legacy pre-v0.20 inputs (#793 / migrate preflight). */
|
|
10
12
|
export declare function detectPreCutoverLegacy(projectRoot: string): string[];
|
|
11
13
|
/** Structured result of a pre-cutover (pre-v0.20 document model) probe. */
|
|
12
14
|
export interface PrecutoverDetection {
|
|
13
|
-
/** True when any legacy pre-v0.20 artifact still needs migration. */
|
|
14
15
|
preCutover: boolean;
|
|
15
|
-
/** Human-readable reasons; empty when the project is on the current model. */
|
|
16
16
|
reasons: string[];
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Detect whether ``projectRoot`` still carries pre-v0.20 (pre-cutover) document-model
|
|
20
|
-
* artifacts that need migration. Mirrors the AGENTS.md "Pre-Cutover Check" rule and the
|
|
21
|
-
* legacy ``scripts/_precutover.py`` helper: a project is pre-cutover when any of the
|
|
22
|
-
* following hold:
|
|
23
|
-
*
|
|
24
|
-
* - ``SPECIFICATION.md`` exists and is neither a deprecation redirect nor a current
|
|
25
|
-
* generated spec export;
|
|
26
|
-
* - ``PROJECT.md`` exists and is not a deprecation redirect;
|
|
27
|
-
* - ``vbrief/`` exists but is missing one or more lifecycle folders.
|
|
28
|
-
*/
|
|
29
18
|
export declare function detectPreCutover(projectRoot: string): PrecutoverDetection;
|
|
30
|
-
/**
|
|
31
|
-
* Render the single-line pre-cutover status string surfaced by ``deft doctor``.
|
|
32
|
-
* Flags the migration-needed state with reasons, or reports a clean current-model state.
|
|
33
|
-
*/
|
|
34
19
|
export declare function renderPrecutoverLine(projectRoot: string): string;
|
|
20
|
+
export { isFullSpecState, isGreenfieldSpecExport };
|
|
35
21
|
//# sourceMappingURL=precutover.d.ts.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { GENERATED_SPEC_PURPOSE, GENERATED_SPEC_SOURCE_SPEC } from "../spec-authority/constants.js";
|
|
4
|
+
import { isFullSpecState, isGreenfieldSpecExport } from "../spec-authority/resolver.js";
|
|
3
5
|
import { DEPRECATION_SENTINEL } from "../vbrief-build/constants.js";
|
|
4
6
|
export { DEPRECATION_SENTINEL as DEPRECATED_REDIRECT_SENTINEL };
|
|
5
7
|
const DEPRECATION_REDIRECT_PURPOSE = "<!-- Purpose: deprecation redirect -->";
|
|
6
|
-
const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
7
|
-
const GENERATED_SPEC_SOURCE = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
8
8
|
const SPEC_SOURCE_RELPATH = join("vbrief", "specification.vbrief.json");
|
|
9
9
|
const LIFECYCLE_FOLDERS = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
10
10
|
export function missingLifecycleFolders(projectRoot) {
|
|
@@ -18,14 +18,19 @@ function hasCompleteLifecycle(projectRoot) {
|
|
|
18
18
|
export function isDeprecationRedirect(content) {
|
|
19
19
|
return content.includes(DEPRECATION_SENTINEL) || content.includes(DEPRECATION_REDIRECT_PURPOSE);
|
|
20
20
|
}
|
|
21
|
+
/** Full-spec generated export (specification.vbrief.json source line). */
|
|
21
22
|
export function isGeneratedSpecificationExport(projectRoot, content) {
|
|
22
23
|
return (content.includes(GENERATED_SPEC_PURPOSE) &&
|
|
23
|
-
content.includes(
|
|
24
|
+
content.includes(GENERATED_SPEC_SOURCE_SPEC) &&
|
|
24
25
|
existsSync(join(projectRoot, SPEC_SOURCE_RELPATH)));
|
|
25
26
|
}
|
|
26
|
-
/** Return true for a fully current
|
|
27
|
+
/** Return true for a fully current generated spec export (full-spec or greenfield). */
|
|
27
28
|
export function isCurrentGeneratedSpecification(projectRoot, content) {
|
|
28
|
-
|
|
29
|
+
if (!hasCompleteLifecycle(projectRoot))
|
|
30
|
+
return false;
|
|
31
|
+
if (isGeneratedSpecificationExport(projectRoot, content))
|
|
32
|
+
return true;
|
|
33
|
+
return isGreenfieldSpecExport(projectRoot);
|
|
29
34
|
}
|
|
30
35
|
function isFile(path) {
|
|
31
36
|
try {
|
|
@@ -46,8 +51,11 @@ function safeReadText(path) {
|
|
|
46
51
|
function rootMarkdownIsLegacy(projectRoot, filename, content) {
|
|
47
52
|
if (isDeprecationRedirect(content))
|
|
48
53
|
return false;
|
|
49
|
-
if (filename === "SPECIFICATION.md"
|
|
50
|
-
|
|
54
|
+
if (filename === "SPECIFICATION.md") {
|
|
55
|
+
if (isGeneratedSpecificationExport(projectRoot, content))
|
|
56
|
+
return false;
|
|
57
|
+
if (isGreenfieldSpecExport(projectRoot))
|
|
58
|
+
return false;
|
|
51
59
|
}
|
|
52
60
|
return filename === "SPECIFICATION.md" || filename === "PROJECT.md";
|
|
53
61
|
}
|
|
@@ -65,17 +73,6 @@ export function detectPreCutoverLegacy(projectRoot) {
|
|
|
65
73
|
}
|
|
66
74
|
return legacy;
|
|
67
75
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Detect whether ``projectRoot`` still carries pre-v0.20 (pre-cutover) document-model
|
|
70
|
-
* artifacts that need migration. Mirrors the AGENTS.md "Pre-Cutover Check" rule and the
|
|
71
|
-
* legacy ``scripts/_precutover.py`` helper: a project is pre-cutover when any of the
|
|
72
|
-
* following hold:
|
|
73
|
-
*
|
|
74
|
-
* - ``SPECIFICATION.md`` exists and is neither a deprecation redirect nor a current
|
|
75
|
-
* generated spec export;
|
|
76
|
-
* - ``PROJECT.md`` exists and is not a deprecation redirect;
|
|
77
|
-
* - ``vbrief/`` exists but is missing one or more lifecycle folders.
|
|
78
|
-
*/
|
|
79
76
|
export function detectPreCutover(projectRoot) {
|
|
80
77
|
const reasons = [];
|
|
81
78
|
const specPath = join(projectRoot, "SPECIFICATION.md");
|
|
@@ -101,17 +98,14 @@ export function detectPreCutover(projectRoot) {
|
|
|
101
98
|
}
|
|
102
99
|
return { preCutover: reasons.length > 0, reasons };
|
|
103
100
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Render the single-line pre-cutover status string surfaced by ``deft doctor``.
|
|
106
|
-
* Flags the migration-needed state with reasons, or reports a clean current-model state.
|
|
107
|
-
*/
|
|
108
101
|
export function renderPrecutoverLine(projectRoot) {
|
|
109
102
|
const { preCutover, reasons } = detectPreCutover(projectRoot);
|
|
110
103
|
if (!preCutover) {
|
|
111
104
|
return "Pre-cutover: none -- project is on the current vBRIEF document model.";
|
|
112
105
|
}
|
|
113
|
-
// Collapse any embedded newlines so the status line stays a single line (CWE-116).
|
|
114
106
|
const summary = reasons.join("; ").replace(/\r?\n/g, " ");
|
|
115
107
|
return `Pre-cutover: migration needed -- ${summary}. Run \`deft migrate:vbrief\` to migrate.`;
|
|
116
108
|
}
|
|
109
|
+
// Re-export for classify callers (#2013).
|
|
110
|
+
export { isFullSpecState, isGreenfieldSpecExport };
|
|
117
111
|
//# sourceMappingURL=precutover.js.map
|
|
@@ -5,9 +5,9 @@ export interface EvaluateResult {
|
|
|
5
5
|
readonly stream: OutputStream;
|
|
6
6
|
}
|
|
7
7
|
export declare const REQUIRED_HOOKS: readonly ["pre-commit", "pre-push"];
|
|
8
|
-
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const
|
|
8
|
+
/** Substrings each hook must contain when it dispatches through the deft CLI (#2049). */
|
|
9
|
+
export declare const PRE_COMMIT_DEFT_COMMANDS: readonly ["verify:branch", "verify:encoding"];
|
|
10
|
+
export declare const PRE_PUSH_DEFT_COMMANDS: readonly ["preflight-gh"];
|
|
11
11
|
export type GitConfigReader = (projectRoot: string) => {
|
|
12
12
|
hooksPath: string | null;
|
|
13
13
|
error: string | null;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import * as childProcess from "node:child_process";
|
|
2
|
-
import { accessSync, constants, statSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
export const REQUIRED_HOOKS = ["pre-commit", "pre-push"];
|
|
5
|
-
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/** Substrings each hook must contain when it dispatches through the deft CLI (#2049). */
|
|
6
|
+
export const PRE_COMMIT_DEFT_COMMANDS = ["verify:branch", "verify:encoding"];
|
|
7
|
+
export const PRE_PUSH_DEFT_COMMANDS = ["preflight-gh"];
|
|
8
|
+
/** Patterns that indicate legacy Python-dispatched hooks (pre-#2049). */
|
|
9
|
+
const LEGACY_HOOK_PATTERNS = [
|
|
10
|
+
/\.py\b/i,
|
|
11
|
+
/\bpython\b/i,
|
|
12
|
+
/\bdeft_py\b/,
|
|
13
|
+
/\bSCRIPTS_DIR\b/,
|
|
14
|
+
/\bpreflight_branch\.py\b/,
|
|
10
15
|
];
|
|
11
|
-
export const SCRIPTS_DIR_CANDIDATES = ["scripts", ".deft/core/scripts", "deft/scripts"];
|
|
12
16
|
function defaultGitConfigReader(projectRoot) {
|
|
13
17
|
try {
|
|
14
18
|
const stdout = childProcess.execFileSync("git", ["-C", projectRoot, "config", "--get", "core.hooksPath"], {
|
|
@@ -45,15 +49,6 @@ function isDirectory(path) {
|
|
|
45
49
|
return false;
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
|
-
function resolveScriptsDir(projectRoot) {
|
|
49
|
-
for (const rel of SCRIPTS_DIR_CANDIDATES) {
|
|
50
|
-
const candidate = join(projectRoot, rel);
|
|
51
|
-
if (isFile(join(candidate, SCRIPTS_PROBE))) {
|
|
52
|
-
return candidate;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
52
|
function isPosix(platform) {
|
|
58
53
|
return platform !== "win32";
|
|
59
54
|
}
|
|
@@ -66,6 +61,36 @@ function hookExecutable(hookPath) {
|
|
|
66
61
|
return false;
|
|
67
62
|
}
|
|
68
63
|
}
|
|
64
|
+
function readHookContent(hookPath) {
|
|
65
|
+
try {
|
|
66
|
+
return readFileSync(hookPath, "utf8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function usesLegacyPythonDispatch(content) {
|
|
73
|
+
return LEGACY_HOOK_PATTERNS.some((pattern) => pattern.test(content));
|
|
74
|
+
}
|
|
75
|
+
function hookInvokesDeftCli(content, requiredCommands) {
|
|
76
|
+
if (!/\bdeft\b/.test(content))
|
|
77
|
+
return false;
|
|
78
|
+
if (usesLegacyPythonDispatch(content))
|
|
79
|
+
return false;
|
|
80
|
+
return requiredCommands.every((cmd) => content.includes(cmd));
|
|
81
|
+
}
|
|
82
|
+
function validateHookContent(hookName, content, requiredCommands) {
|
|
83
|
+
if (content === null) {
|
|
84
|
+
return `${hookName}: unreadable hook file`;
|
|
85
|
+
}
|
|
86
|
+
if (usesLegacyPythonDispatch(content)) {
|
|
87
|
+
return `${hookName}: still dispatches through Python scripts (expected deft CLI only, #2049)`;
|
|
88
|
+
}
|
|
89
|
+
if (!hookInvokesDeftCli(content, requiredCommands)) {
|
|
90
|
+
return `${hookName}: missing required deft CLI gate(s): ${requiredCommands.join(", ")}`;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
69
94
|
/** Pure evaluator mirroring scripts/verify_hooks_installed.py::evaluate. */
|
|
70
95
|
export function evaluate(projectRoot, options = {}) {
|
|
71
96
|
const root = resolve(projectRoot);
|
|
@@ -132,30 +157,37 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
132
157
|
};
|
|
133
158
|
}
|
|
134
159
|
}
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
160
|
+
const preCommitIssue = validateHookContent("pre-commit", readHookContent(join(hooksDir, "pre-commit")), PRE_COMMIT_DEFT_COMMANDS);
|
|
161
|
+
if (preCommitIssue) {
|
|
162
|
+
return {
|
|
163
|
+
code: 1,
|
|
164
|
+
message: `❌ deft hooks wired but NON-FUNCTIONAL: ${preCommitIssue} (#2049).\n` +
|
|
165
|
+
" Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
|
|
166
|
+
stream: "stderr",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const prePushContent = readHookContent(join(hooksDir, "pre-push"));
|
|
170
|
+
const prePushIssue = validateHookContent("pre-push", prePushContent, PRE_PUSH_DEFT_COMMANDS);
|
|
171
|
+
if (prePushIssue) {
|
|
137
172
|
return {
|
|
138
173
|
code: 1,
|
|
139
|
-
message:
|
|
140
|
-
|
|
141
|
-
" Recovery: re-run the deft installer so the payload is present.",
|
|
174
|
+
message: `❌ deft hooks wired but NON-FUNCTIONAL: ${prePushIssue} (#2049).\n` +
|
|
175
|
+
" Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
|
|
142
176
|
stream: "stderr",
|
|
143
177
|
};
|
|
144
178
|
}
|
|
145
|
-
|
|
146
|
-
if (missingScripts.length > 0) {
|
|
179
|
+
if (prePushContent?.includes("verify:branch")) {
|
|
147
180
|
return {
|
|
148
181
|
code: 1,
|
|
149
|
-
message:
|
|
150
|
-
|
|
151
|
-
" Recovery: re-run the deft installer to restore the payload.",
|
|
182
|
+
message: "❌ deft hooks wired but NON-FUNCTIONAL: pre-push must not invoke verify:branch (#1814).\n" +
|
|
183
|
+
" Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
|
|
152
184
|
stream: "stderr",
|
|
153
185
|
};
|
|
154
186
|
}
|
|
155
187
|
return {
|
|
156
188
|
code: 0,
|
|
157
189
|
message: `✓ deft hooks installed and functional: core.hooksPath=${hooksPath}, ` +
|
|
158
|
-
`hooks ${REQUIRED_HOOKS.join(", ")} present
|
|
190
|
+
`hooks ${REQUIRED_HOOKS.join(", ")} present and dispatch via deft CLI (#2049).`,
|
|
159
191
|
stream: "stdout",
|
|
160
192
|
};
|
|
161
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.1",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -221,8 +221,8 @@
|
|
|
221
221
|
"provenance": true
|
|
222
222
|
},
|
|
223
223
|
"dependencies": {
|
|
224
|
-
"@deftai/directive-content": "^0.
|
|
225
|
-
"@deftai/directive-types": "^0.
|
|
224
|
+
"@deftai/directive-content": "^0.61.1",
|
|
225
|
+
"@deftai/directive-types": "^0.61.1"
|
|
226
226
|
},
|
|
227
227
|
"scripts": {
|
|
228
228
|
"build": "tsc -b",
|