@buba_71/levit 0.3.4 → 0.8.2
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/README.md +192 -7
- package/dist/bin/cli.js +67 -24
- package/dist/src/commands/decision.js +74 -0
- package/dist/src/commands/feature.js +159 -0
- package/dist/src/commands/handoff.js +71 -0
- package/dist/src/commands/index.js +13 -0
- package/dist/src/commands/init.js +53 -39
- package/dist/src/commands/validate.js +84 -0
- package/dist/src/core/cli_args.js +35 -0
- package/dist/src/core/error_helper.js +93 -0
- package/dist/src/core/errors.js +25 -0
- package/dist/src/core/frontmatter.js +62 -0
- package/dist/src/core/ids.js +34 -0
- package/dist/src/core/levit_project.js +30 -0
- package/dist/src/core/logger.js +77 -0
- package/dist/src/core/table.js +63 -0
- package/dist/src/core/write_file.js +15 -0
- package/dist/src/init.js +9 -0
- package/dist/src/readers/decision_reader.js +47 -0
- package/dist/src/readers/feature_reader.js +48 -0
- package/dist/src/readers/handoff_reader.js +47 -0
- package/dist/src/services/decision_service.js +49 -0
- package/dist/src/services/feature_service.js +89 -0
- package/dist/src/services/handoff_service.js +37 -0
- package/dist/src/services/manifest_service.js +89 -0
- package/dist/src/services/project_service.js +39 -0
- package/dist/src/services/validation_service.js +461 -0
- package/dist/src/types/domain.js +2 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/manifest.js +26 -0
- package/dist/tests/cli/integration.test.js +165 -0
- package/dist/tests/services/decision_service.test.js +27 -0
- package/dist/tests/services/feature_service.test.js +42 -0
- package/dist/tests/services/handoff_service.test.js +28 -0
- package/dist/tests/services/manifest_service.test.js +189 -0
- package/dist/tests/services/validation_service.test.js +196 -0
- package/package.json +7 -2
- package/templates/default/.github/workflows/README.md +56 -0
- package/templates/default/.github/workflows/levit-validate.yml +93 -0
- package/templates/default/.gitlab-ci.yml +73 -0
- package/templates/default/.levit/AGENT_CONTRACT.md +34 -0
- package/templates/default/.levit/AGENT_ONBOARDING.md +29 -0
- package/templates/default/.levit/evals/README.md +14 -0
- package/templates/default/.levit/evals/conformance.eval.ts +16 -0
- package/templates/default/.levit/features/INTENT.md +32 -0
- package/templates/default/.levit/features/README.md +11 -0
- package/templates/default/.levit/handoff/.gitkeep +0 -0
- package/templates/default/.levit/prompts/global-rules.md +10 -0
- package/templates/default/.levit/prompts/refactoring-guidelines.md +9 -0
- package/templates/default/.levit/workflows/example-task.md +9 -0
- package/templates/default/.levit/workflows/submit-for-review.md +18 -0
- package/templates/default/HUMAN_AGENT_MANAGER.md +654 -0
- package/templates/default/MIGRATION_GUIDE.md +597 -0
- package/templates/default/README.md +49 -11
- package/templates/default/SOCIAL_CONTRACT.md +5 -0
- package/templates/symfony/.github/workflows/README.md +56 -0
- package/templates/symfony/.github/workflows/levit-validate.yml +82 -0
- package/templates/symfony/.gitlab-ci.yml +62 -0
- package/templates/symfony/.levit/AGENT_CONTRACT.md +34 -0
- package/templates/symfony/.levit/AGENT_ONBOARDING.md +124 -0
- package/templates/symfony/.levit/decisions/.gitkeep +0 -0
- package/templates/symfony/.levit/features/INTENT.md +32 -0
- package/templates/symfony/.levit/features/README.md +11 -0
- package/templates/symfony/.levit/handoff/.gitkeep +0 -0
- package/templates/symfony/.levit/prompts/global-rules.md +10 -0
- package/templates/symfony/.levit/prompts/refactoring-guidelines.md +9 -0
- package/templates/symfony/.levit/workflows/example-task.md +9 -0
- package/templates/symfony/.levit/workflows/submit-for-review.md +18 -0
- package/templates/symfony/HUMAN_AGENT_MANAGER.md +654 -0
- package/templates/symfony/MIGRATION_GUIDE.md +597 -0
- package/templates/symfony/README.md +101 -0
- package/templates/symfony/SOCIAL_CONTRACT.md +34 -0
- package/dist/tests/init.test.js +0 -58
- package/templates/default/features/README.md +0 -12
- /package/templates/default/{agents → .levit/agents}/AGENTS.md +0 -0
- /package/templates/default/{agents → .levit/agents}/boundaries.md +0 -0
- /package/templates/default/{package.json → .levit/decisions/.gitkeep} +0 -0
- /package/templates/default/{docs → .levit/docs}/architecture.md +0 -0
- /package/templates/default/{pipelines → .levit/pipelines}/README.md +0 -0
- /package/templates/default/{roles → .levit/roles}/README.md +0 -0
- /package/templates/default/{roles → .levit/roles}/devops.md +0 -0
- /package/templates/default/{roles → .levit/roles}/qa.md +0 -0
- /package/templates/default/{roles → .levit/roles}/security.md +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const decision_service_1 = require("../../src/services/decision_service");
|
|
12
|
+
(0, node_test_1.default)("DecisionService.createDecision generates correct structure", () => {
|
|
13
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "decision-service-test-"));
|
|
14
|
+
const decisionsDir = node_path_1.default.join(tempDir, ".levit", "decisions");
|
|
15
|
+
fs_extra_1.default.ensureDirSync(decisionsDir);
|
|
16
|
+
decision_service_1.DecisionService.createDecision(tempDir, {
|
|
17
|
+
title: "Test Architecture",
|
|
18
|
+
id: "005",
|
|
19
|
+
featureRef: ".levit/features/001.md"
|
|
20
|
+
});
|
|
21
|
+
const expectedFile = node_path_1.default.join(decisionsDir, "ADR-005-test-architecture.md");
|
|
22
|
+
node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
|
|
23
|
+
const content = fs_extra_1.default.readFileSync(expectedFile, "utf-8");
|
|
24
|
+
node_assert_1.default.ok(content.includes("id: ADR-005"));
|
|
25
|
+
node_assert_1.default.ok(content.includes("depends_on: [.levit/features/001.md]"));
|
|
26
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const feature_service_1 = require("../../src/services/feature_service");
|
|
12
|
+
(0, node_test_1.default)("FeatureService.createFeature generates correct file", () => {
|
|
13
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "feature-service-test-"));
|
|
14
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
15
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
16
|
+
feature_service_1.FeatureService.createFeature(tempDir, {
|
|
17
|
+
title: "Unit Test Feature",
|
|
18
|
+
slug: "unit-test-feature",
|
|
19
|
+
id: "999"
|
|
20
|
+
});
|
|
21
|
+
const expectedFile = node_path_1.default.join(featuresDir, "999-unit-test-feature.md");
|
|
22
|
+
node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
|
|
23
|
+
const content = fs_extra_1.default.readFileSync(expectedFile, "utf-8");
|
|
24
|
+
node_assert_1.default.ok(content.includes("id: 999"));
|
|
25
|
+
// Slug is currently only used for filename, not in content
|
|
26
|
+
// assert.ok(content.includes("slug: unit-test-feature"));
|
|
27
|
+
node_assert_1.default.ok(content.includes("# INTENT: Unit Test Feature"));
|
|
28
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
(0, node_test_1.default)("FeatureService.createFeature auto-generates ID", () => {
|
|
31
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "feature-service-test-"));
|
|
32
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
33
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
34
|
+
feature_service_1.FeatureService.createFeature(tempDir, {
|
|
35
|
+
title: "Auto ID Feature",
|
|
36
|
+
slug: "auto-id"
|
|
37
|
+
});
|
|
38
|
+
// Should be 001-auto-id.md since directory is empty
|
|
39
|
+
const expectedFile = node_path_1.default.join(featuresDir, "001-auto-id.md");
|
|
40
|
+
node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
|
|
41
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const handoff_service_1 = require("../../src/services/handoff_service");
|
|
12
|
+
(0, node_test_1.default)("HandoffService.createHandoff generates correct file name", () => {
|
|
13
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "handoff-service-test-"));
|
|
14
|
+
const handoffDir = node_path_1.default.join(tempDir, ".levit", "handoff");
|
|
15
|
+
fs_extra_1.default.ensureDirSync(handoffDir);
|
|
16
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
17
|
+
handoff_service_1.HandoffService.createHandoff(tempDir, {
|
|
18
|
+
feature: ".levit/features/login.md",
|
|
19
|
+
role: "qa"
|
|
20
|
+
});
|
|
21
|
+
// format: YYYY-MM-DD-filename-role.md
|
|
22
|
+
const expectedFile = node_path_1.default.join(handoffDir, `${date}-login-qa.md`);
|
|
23
|
+
node_assert_1.default.ok(fs_extra_1.default.existsSync(expectedFile));
|
|
24
|
+
const content = fs_extra_1.default.readFileSync(expectedFile, "utf-8");
|
|
25
|
+
node_assert_1.default.ok(content.includes(`id: HAND-${date}-qa`));
|
|
26
|
+
node_assert_1.default.ok(content.includes("owner: qa"));
|
|
27
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const manifest_service_1 = require("../../src/services/manifest_service");
|
|
12
|
+
(0, node_test_1.default)("ManifestService.read returns default manifest when file doesn't exist", () => {
|
|
13
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
14
|
+
const manifest = manifest_service_1.ManifestService.read(tempDir);
|
|
15
|
+
node_assert_1.default.strictEqual(manifest.project.name, "my-project");
|
|
16
|
+
node_assert_1.default.strictEqual(manifest.version, "1.0.0");
|
|
17
|
+
node_assert_1.default.deepStrictEqual(manifest.features, []);
|
|
18
|
+
node_assert_1.default.deepStrictEqual(manifest.roles, []);
|
|
19
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
(0, node_test_1.default)("ManifestService.read reads existing manifest", () => {
|
|
22
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
23
|
+
const manifestPath = node_path_1.default.join(tempDir, "levit.json");
|
|
24
|
+
const testManifest = {
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
project: {
|
|
27
|
+
name: "test-project",
|
|
28
|
+
description: "Test"
|
|
29
|
+
},
|
|
30
|
+
governance: {
|
|
31
|
+
autonomy_level: "medium",
|
|
32
|
+
risk_tolerance: "medium"
|
|
33
|
+
},
|
|
34
|
+
features: [],
|
|
35
|
+
roles: [],
|
|
36
|
+
constraints: {},
|
|
37
|
+
paths: {
|
|
38
|
+
features: "features",
|
|
39
|
+
decisions: ".levit/decisions",
|
|
40
|
+
handoffs: ".levit/handoff"
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
fs_extra_1.default.writeJsonSync(manifestPath, testManifest, { spaces: 2 });
|
|
44
|
+
const manifest = manifest_service_1.ManifestService.read(tempDir);
|
|
45
|
+
node_assert_1.default.strictEqual(manifest.project.name, "test-project");
|
|
46
|
+
node_assert_1.default.strictEqual(manifest.governance.autonomy_level, "medium");
|
|
47
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.default)("ManifestService.write creates manifest file", () => {
|
|
50
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
51
|
+
const manifestPath = node_path_1.default.join(tempDir, "levit.json");
|
|
52
|
+
const testManifest = {
|
|
53
|
+
version: "1.0.0",
|
|
54
|
+
project: {
|
|
55
|
+
name: "write-test"
|
|
56
|
+
},
|
|
57
|
+
governance: {
|
|
58
|
+
autonomy_level: "low",
|
|
59
|
+
risk_tolerance: "low"
|
|
60
|
+
},
|
|
61
|
+
features: [],
|
|
62
|
+
roles: [],
|
|
63
|
+
constraints: {},
|
|
64
|
+
paths: {
|
|
65
|
+
features: "features",
|
|
66
|
+
decisions: ".levit/decisions",
|
|
67
|
+
handoffs: ".levit/handoff"
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
manifest_service_1.ManifestService.write(tempDir, testManifest);
|
|
71
|
+
node_assert_1.default.ok(fs_extra_1.default.existsSync(manifestPath));
|
|
72
|
+
const written = fs_extra_1.default.readJsonSync(manifestPath);
|
|
73
|
+
node_assert_1.default.strictEqual(written.project.name, "write-test");
|
|
74
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
(0, node_test_1.default)("ManifestService.sync discovers features from filesystem", () => {
|
|
77
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
78
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
79
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
80
|
+
// Create a test feature file
|
|
81
|
+
const featureContent = `---
|
|
82
|
+
id: 001
|
|
83
|
+
status: active
|
|
84
|
+
owner: human
|
|
85
|
+
last_updated: 2026-01-01
|
|
86
|
+
risk_level: low
|
|
87
|
+
depends_on: []
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
# INTENT: Test Feature
|
|
91
|
+
`;
|
|
92
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test-feature.md"), featureContent);
|
|
93
|
+
const manifest = manifest_service_1.ManifestService.sync(tempDir);
|
|
94
|
+
node_assert_1.default.strictEqual(manifest.features.length, 1);
|
|
95
|
+
node_assert_1.default.strictEqual(manifest.features[0].id, "001");
|
|
96
|
+
node_assert_1.default.strictEqual(manifest.features[0].slug, "test-feature");
|
|
97
|
+
node_assert_1.default.strictEqual(manifest.features[0].title, "Test Feature");
|
|
98
|
+
node_assert_1.default.strictEqual(manifest.features[0].status, "active");
|
|
99
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.default)("ManifestService.sync discovers roles from filesystem", () => {
|
|
102
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
103
|
+
const rolesDir = node_path_1.default.join(tempDir, ".levit", "roles");
|
|
104
|
+
fs_extra_1.default.ensureDirSync(rolesDir);
|
|
105
|
+
// Create a test role file
|
|
106
|
+
const roleContent = `# Security Role
|
|
107
|
+
This is the security role description.
|
|
108
|
+
`;
|
|
109
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(rolesDir, "security.md"), roleContent);
|
|
110
|
+
const manifest = manifest_service_1.ManifestService.sync(tempDir);
|
|
111
|
+
node_assert_1.default.strictEqual(manifest.roles.length, 1);
|
|
112
|
+
node_assert_1.default.strictEqual(manifest.roles[0].name, "security");
|
|
113
|
+
node_assert_1.default.strictEqual(manifest.roles[0].description, "Security Role");
|
|
114
|
+
node_assert_1.default.strictEqual(manifest.roles[0].path, ".levit/roles/security.md");
|
|
115
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
(0, node_test_1.default)("ManifestService.sync ignores README files", () => {
|
|
118
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
119
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
120
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
121
|
+
// Create README and INTENT files (should be ignored)
|
|
122
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "README.md"), "# Features");
|
|
123
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "INTENT.md"), "# Intent Template");
|
|
124
|
+
// Create actual feature
|
|
125
|
+
const featureContent = `---
|
|
126
|
+
id: 001
|
|
127
|
+
status: active
|
|
128
|
+
owner: human
|
|
129
|
+
last_updated: 2026-01-01
|
|
130
|
+
risk_level: low
|
|
131
|
+
depends_on: []
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# INTENT: Real Feature
|
|
135
|
+
`;
|
|
136
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-real-feature.md"), featureContent);
|
|
137
|
+
const manifest = manifest_service_1.ManifestService.sync(tempDir);
|
|
138
|
+
node_assert_1.default.strictEqual(manifest.features.length, 1);
|
|
139
|
+
node_assert_1.default.strictEqual(manifest.features[0].slug, "real-feature");
|
|
140
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
141
|
+
});
|
|
142
|
+
(0, node_test_1.default)("ManifestService.sync handles missing directories gracefully", () => {
|
|
143
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
144
|
+
// No features or roles directories
|
|
145
|
+
const manifest = manifest_service_1.ManifestService.sync(tempDir);
|
|
146
|
+
node_assert_1.default.deepStrictEqual(manifest.features, []);
|
|
147
|
+
node_assert_1.default.deepStrictEqual(manifest.roles, []);
|
|
148
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
149
|
+
});
|
|
150
|
+
(0, node_test_1.default)("ManifestService.sync handles features with invalid frontmatter gracefully", () => {
|
|
151
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
152
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
153
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
154
|
+
// Create feature with invalid frontmatter (missing closing ---)
|
|
155
|
+
const invalidFeature = `---
|
|
156
|
+
id: 001
|
|
157
|
+
status: active
|
|
158
|
+
# Missing closing delimiter
|
|
159
|
+
|
|
160
|
+
# INTENT: Invalid Feature
|
|
161
|
+
`;
|
|
162
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-invalid.md"), invalidFeature);
|
|
163
|
+
// Should not throw, but feature might have default values
|
|
164
|
+
const manifest = manifest_service_1.ManifestService.sync(tempDir);
|
|
165
|
+
// Should still discover the file (by filename)
|
|
166
|
+
node_assert_1.default.strictEqual(manifest.features.length, 1);
|
|
167
|
+
node_assert_1.default.strictEqual(manifest.features[0].id, "001");
|
|
168
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
169
|
+
});
|
|
170
|
+
(0, node_test_1.default)("ManifestService.sync extracts title from INTENT header", () => {
|
|
171
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "manifest-test-"));
|
|
172
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
173
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
174
|
+
const featureContent = `---
|
|
175
|
+
id: 002
|
|
176
|
+
status: draft
|
|
177
|
+
owner: human
|
|
178
|
+
last_updated: 2026-01-01
|
|
179
|
+
risk_level: medium
|
|
180
|
+
depends_on: []
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
# INTENT: Complex Feature Title With Spaces
|
|
184
|
+
`;
|
|
185
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "002-complex.md"), featureContent);
|
|
186
|
+
const manifest = manifest_service_1.ManifestService.sync(tempDir);
|
|
187
|
+
node_assert_1.default.strictEqual(manifest.features[0].title, "Complex Feature Title With Spaces");
|
|
188
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
189
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const validation_service_1 = require("../../src/services/validation_service");
|
|
12
|
+
(0, node_test_1.default)("ValidationService reports errors if core directories are missing", () => {
|
|
13
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
14
|
+
// Validate empty dir
|
|
15
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
16
|
+
// Expect invalid
|
|
17
|
+
node_assert_1.default.strictEqual(result.valid, false);
|
|
18
|
+
node_assert_1.default.ok(result.metrics.errors > 0, "Should have errors");
|
|
19
|
+
// Check specific error code presence
|
|
20
|
+
const missingDirs = result.issues.filter(i => i.code === "MISSING_DIRECTORY");
|
|
21
|
+
node_assert_1.default.ok(missingDirs.length > 0, "Should report missing directories");
|
|
22
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
(0, node_test_1.default)("ValidationService passes for valid structure", () => {
|
|
25
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-succ-"));
|
|
26
|
+
// Scout scaffolding: .levit/features, .levit/decisions, .levit/handoff, core files
|
|
27
|
+
const dirs = [".levit/features", ".levit/decisions", ".levit/handoff"];
|
|
28
|
+
dirs.forEach(d => fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, d)));
|
|
29
|
+
const files = ["SOCIAL_CONTRACT.md", ".levit/AGENT_CONTRACT.md", ".levit/AGENT_ONBOARDING.md"];
|
|
30
|
+
files.forEach(f => fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, f), "content"));
|
|
31
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
32
|
+
node_assert_1.default.strictEqual(result.valid, true);
|
|
33
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
(0, node_test_1.default)("ValidationService detects feature with invalid frontmatter", () => {
|
|
36
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
37
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
38
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
39
|
+
// Create core structure
|
|
40
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
|
|
41
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
|
|
42
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
|
|
43
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
|
|
44
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
|
|
45
|
+
// Create feature with missing required fields
|
|
46
|
+
const invalidFeature = `---
|
|
47
|
+
id: 001
|
|
48
|
+
status: active
|
|
49
|
+
# Missing: owner, last_updated, risk_level, depends_on
|
|
50
|
+
|
|
51
|
+
# INTENT: Test Feature
|
|
52
|
+
`;
|
|
53
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test.md"), invalidFeature);
|
|
54
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
55
|
+
node_assert_1.default.strictEqual(result.valid, false);
|
|
56
|
+
const frontmatterErrors = result.issues.filter(i => i.code === "INVALID_FRONTMATTER");
|
|
57
|
+
node_assert_1.default.ok(frontmatterErrors.length > 0, "Should report invalid frontmatter");
|
|
58
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
(0, node_test_1.default)("ValidationService detects feature without INTENT header", () => {
|
|
61
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
62
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
63
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
64
|
+
// Create core structure
|
|
65
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
|
|
66
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
|
|
67
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
|
|
68
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
|
|
69
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
|
|
70
|
+
// Create feature without INTENT header
|
|
71
|
+
const featureWithoutIntent = `---
|
|
72
|
+
id: 001
|
|
73
|
+
status: active
|
|
74
|
+
owner: human
|
|
75
|
+
last_updated: 2026-01-01
|
|
76
|
+
risk_level: low
|
|
77
|
+
depends_on: []
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# Some other header
|
|
81
|
+
`;
|
|
82
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test.md"), featureWithoutIntent);
|
|
83
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
84
|
+
node_assert_1.default.strictEqual(result.valid, false);
|
|
85
|
+
const structureErrors = result.issues.filter(i => i.code === "INVALID_STRUCTURE");
|
|
86
|
+
node_assert_1.default.ok(structureErrors.length > 0, "Should report missing INTENT header");
|
|
87
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
(0, node_test_1.default)("ValidationService detects decision with invalid frontmatter", () => {
|
|
90
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
91
|
+
const decisionsDir = node_path_1.default.join(tempDir, ".levit/decisions");
|
|
92
|
+
fs_extra_1.default.ensureDirSync(decisionsDir);
|
|
93
|
+
// Create core structure
|
|
94
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "features"));
|
|
95
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "handoff"));
|
|
96
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
|
|
97
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
|
|
98
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
|
|
99
|
+
// Create decision with invalid YAML
|
|
100
|
+
const invalidDecision = `---
|
|
101
|
+
id: ADR-001
|
|
102
|
+
status: draft
|
|
103
|
+
owner: human
|
|
104
|
+
last_updated: 2026-01-01
|
|
105
|
+
risk_level: low
|
|
106
|
+
depends_on: [invalid: yaml: syntax]
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
# ADR 001: Test
|
|
110
|
+
`;
|
|
111
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(decisionsDir, "ADR-001-test.md"), invalidDecision);
|
|
112
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
113
|
+
node_assert_1.default.strictEqual(result.valid, false);
|
|
114
|
+
const frontmatterErrors = result.issues.filter(i => i.code === "INVALID_FRONTMATTER");
|
|
115
|
+
node_assert_1.default.ok(frontmatterErrors.length > 0, "Should report invalid YAML");
|
|
116
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
(0, node_test_1.default)("ValidationService detects handoff with missing frontmatter", () => {
|
|
119
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
120
|
+
const handoffDir = node_path_1.default.join(tempDir, ".levit/handoff");
|
|
121
|
+
fs_extra_1.default.ensureDirSync(handoffDir);
|
|
122
|
+
// Create core structure
|
|
123
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "features"));
|
|
124
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit", "decisions"));
|
|
125
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
|
|
126
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
|
|
127
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
|
|
128
|
+
// Create handoff without frontmatter
|
|
129
|
+
const handoffWithoutFrontmatter = `# Agent Handoff
|
|
130
|
+
No frontmatter here
|
|
131
|
+
`;
|
|
132
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(handoffDir, "2026-01-01-test.md"), handoffWithoutFrontmatter);
|
|
133
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
134
|
+
node_assert_1.default.strictEqual(result.valid, false);
|
|
135
|
+
const frontmatterErrors = result.issues.filter(i => i.code === "INVALID_FRONTMATTER");
|
|
136
|
+
node_assert_1.default.ok(frontmatterErrors.length > 0, "Should report missing frontmatter");
|
|
137
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
138
|
+
});
|
|
139
|
+
(0, node_test_1.default)("ValidationService reports warning when no features exist", () => {
|
|
140
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
141
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
142
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
143
|
+
// Create core structure
|
|
144
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
|
|
145
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
|
|
146
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
|
|
147
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
|
|
148
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
|
|
149
|
+
// No features in features directory
|
|
150
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
151
|
+
node_assert_1.default.strictEqual(result.valid, true, "Should be valid (only warning)");
|
|
152
|
+
node_assert_1.default.ok(result.metrics.warnings > 0, "Should have warnings");
|
|
153
|
+
const noFeaturesWarning = result.issues.find(i => i.code === "NO_FEATURES");
|
|
154
|
+
node_assert_1.default.ok(noFeaturesWarning, "Should report no features warning");
|
|
155
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
156
|
+
});
|
|
157
|
+
(0, node_test_1.default)("ValidationService counts files scanned correctly", () => {
|
|
158
|
+
const tempDir = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "validation-service-test-"));
|
|
159
|
+
const featuresDir = node_path_1.default.join(tempDir, ".levit", "features");
|
|
160
|
+
fs_extra_1.default.ensureDirSync(featuresDir);
|
|
161
|
+
// Create core structure
|
|
162
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/decisions"));
|
|
163
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(tempDir, ".levit/handoff"));
|
|
164
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, "SOCIAL_CONTRACT.md"), "content");
|
|
165
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_CONTRACT.md"), "content");
|
|
166
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/AGENT_ONBOARDING.md"), "content");
|
|
167
|
+
// Create valid feature
|
|
168
|
+
const validFeature = `---
|
|
169
|
+
id: 001
|
|
170
|
+
status: active
|
|
171
|
+
owner: human
|
|
172
|
+
last_updated: 2026-01-01
|
|
173
|
+
risk_level: low
|
|
174
|
+
depends_on: []
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
# INTENT: Test Feature
|
|
178
|
+
`;
|
|
179
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(featuresDir, "001-test.md"), validFeature);
|
|
180
|
+
// Create valid decision
|
|
181
|
+
const validDecision = `---
|
|
182
|
+
id: ADR-001
|
|
183
|
+
status: draft
|
|
184
|
+
owner: human
|
|
185
|
+
last_updated: 2026-01-01
|
|
186
|
+
risk_level: low
|
|
187
|
+
depends_on: []
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
# ADR 001: Test Decision
|
|
191
|
+
`;
|
|
192
|
+
fs_extra_1.default.writeFileSync(node_path_1.default.join(tempDir, ".levit/decisions", "ADR-001-test.md"), validDecision);
|
|
193
|
+
const result = validation_service_1.ValidationService.validate(tempDir);
|
|
194
|
+
node_assert_1.default.strictEqual(result.metrics.filesScanned, 2, "Should scan 2 files (1 feature + 1 decision)");
|
|
195
|
+
fs_extra_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
196
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buba_71/levit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Hybrid starter kit for Antigravity projects",
|
|
5
5
|
"author": "David BUBA",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,14 +19,19 @@
|
|
|
19
19
|
"main": "dist/src/init.js",
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc && chmod +x dist/bin/cli.js",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
22
23
|
"start": "node dist/bin/cli.js",
|
|
23
24
|
"test": "npm run build && node --test dist/tests"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"fs-extra": "^11.2.0"
|
|
27
|
+
"fs-extra": "^11.2.0",
|
|
28
|
+
"js-yaml": "^4.1.1",
|
|
29
|
+
"chalk": "^4.1.2",
|
|
30
|
+
"cli-table3": "^0.6.5"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"@types/fs-extra": "^11.0.4",
|
|
34
|
+
"@types/js-yaml": "^4.0.9",
|
|
30
35
|
"@types/node": "^20.11.0",
|
|
31
36
|
"typescript": "^5.3.0"
|
|
32
37
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# GitHub Actions Workflows
|
|
2
|
+
|
|
3
|
+
This directory contains CI/CD workflows for levit-kit projects.
|
|
4
|
+
|
|
5
|
+
## Available Workflows
|
|
6
|
+
|
|
7
|
+
### `levit-validate.yml`
|
|
8
|
+
|
|
9
|
+
Validates the levit-kit project structure on every push and pull request.
|
|
10
|
+
|
|
11
|
+
**What it does**:
|
|
12
|
+
- Runs `levit validate` to check project structure
|
|
13
|
+
- Fails the build if validation errors are found
|
|
14
|
+
- Uploads validation results as artifacts
|
|
15
|
+
- Displays results in GitHub Actions summary
|
|
16
|
+
|
|
17
|
+
**When it runs**:
|
|
18
|
+
- On pushes to `main`, `master`, or `develop` branches
|
|
19
|
+
- On pull requests targeting these branches
|
|
20
|
+
|
|
21
|
+
**How to use**:
|
|
22
|
+
1. This workflow is automatically included in levit-kit projects
|
|
23
|
+
2. No configuration needed - it works out of the box
|
|
24
|
+
3. Customize the `on:` section if you use different branch names
|
|
25
|
+
|
|
26
|
+
**Customization**:
|
|
27
|
+
Edit `.github/workflows/levit-validate.yml` to:
|
|
28
|
+
- Change branch names
|
|
29
|
+
- Add additional validation steps
|
|
30
|
+
- Integrate with other workflows
|
|
31
|
+
- Add notifications
|
|
32
|
+
|
|
33
|
+
## Adding Custom Workflows
|
|
34
|
+
|
|
35
|
+
You can add additional workflows for:
|
|
36
|
+
- Feature status checks
|
|
37
|
+
- Decision record validation
|
|
38
|
+
- Handoff completeness checks
|
|
39
|
+
- Custom evals
|
|
40
|
+
|
|
41
|
+
Example structure:
|
|
42
|
+
```yaml
|
|
43
|
+
name: Custom Validation
|
|
44
|
+
on: [push, pull_request]
|
|
45
|
+
jobs:
|
|
46
|
+
custom-check:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- run: your-custom-script.sh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
*For more information, see the [levit-kit documentation](https://github.com/buba71/levit-kit).*
|
|
56
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
name: Validate Levit Project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, master, develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
validate:
|
|
11
|
+
name: Validate levit-kit Structure
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout code
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Setup Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '20'
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
|
|
24
|
+
- name: Validate levit-kit structure
|
|
25
|
+
id: validate
|
|
26
|
+
run: |
|
|
27
|
+
npx @buba_71/levit validate --json > validation.json 2>&1 || VALIDATION_EXIT=$?
|
|
28
|
+
if [ -n "$VALIDATION_EXIT" ]; then
|
|
29
|
+
echo "validation_failed=true" >> $GITHUB_OUTPUT
|
|
30
|
+
else
|
|
31
|
+
echo "validation_failed=false" >> $GITHUB_OUTPUT
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- name: Display validation results
|
|
35
|
+
if: always()
|
|
36
|
+
run: |
|
|
37
|
+
if [ -f validation.json ]; then
|
|
38
|
+
echo "## Validation Results" >> $GITHUB_STEP_SUMMARY
|
|
39
|
+
echo '```json' >> $GITHUB_STEP_SUMMARY
|
|
40
|
+
cat validation.json >> $GITHUB_STEP_SUMMARY
|
|
41
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
42
|
+
|
|
43
|
+
# Check for errors in JSON output
|
|
44
|
+
ERRORS=$(node -e "
|
|
45
|
+
try {
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
const content = fs.readFileSync('validation.json', 'utf8');
|
|
48
|
+
const lines = content.split('\\n').filter(l => l.trim());
|
|
49
|
+
let errorCount = 0;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
try {
|
|
52
|
+
const obj = JSON.parse(line);
|
|
53
|
+
// Check for ERROR level or validation failure messages
|
|
54
|
+
if (obj.level === 'ERROR' ||
|
|
55
|
+
(obj.message && (
|
|
56
|
+
obj.message.toLowerCase().includes('error') ||
|
|
57
|
+
obj.message.toLowerCase().includes('validation failed') ||
|
|
58
|
+
obj.message.toLowerCase().includes('failed')
|
|
59
|
+
))) {
|
|
60
|
+
errorCount++;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Non-JSON lines might be error messages
|
|
64
|
+
if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
|
|
65
|
+
errorCount++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log(errorCount);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.log('0');
|
|
72
|
+
}
|
|
73
|
+
" || echo "0")
|
|
74
|
+
|
|
75
|
+
if [ "$ERRORS" -gt 0 ] || [ "${{ steps.validate.outputs.validation_failed }}" == "true" ]; then
|
|
76
|
+
echo "❌ Validation failed"
|
|
77
|
+
exit 1
|
|
78
|
+
else
|
|
79
|
+
echo "✅ Validation passed"
|
|
80
|
+
fi
|
|
81
|
+
else
|
|
82
|
+
echo "⚠️ No validation output found"
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
- name: Upload validation results
|
|
87
|
+
if: always()
|
|
88
|
+
uses: actions/upload-artifact@v3
|
|
89
|
+
with:
|
|
90
|
+
name: validation-results
|
|
91
|
+
path: validation.json
|
|
92
|
+
if-no-files-found: ignore
|
|
93
|
+
|