@toolbaux/guardian 0.1.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/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/adapters/csharp-adapter.js +149 -0
- package/dist/adapters/go-adapter.js +96 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/java-adapter.js +122 -0
- package/dist/adapters/python-adapter.js +183 -0
- package/dist/adapters/runner.js +69 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/typescript-adapter.js +179 -0
- package/dist/benchmarking/framework.js +91 -0
- package/dist/cli.js +343 -0
- package/dist/commands/analyze-depth.js +43 -0
- package/dist/commands/api-spec-extractor.js +52 -0
- package/dist/commands/breaking-change-analyzer.js +334 -0
- package/dist/commands/config-compliance.js +219 -0
- package/dist/commands/constraints.js +221 -0
- package/dist/commands/context.js +101 -0
- package/dist/commands/data-flow-tracer.js +291 -0
- package/dist/commands/dependency-impact-analyzer.js +27 -0
- package/dist/commands/diff.js +146 -0
- package/dist/commands/discrepancy.js +71 -0
- package/dist/commands/doc-generate.js +163 -0
- package/dist/commands/doc-html.js +120 -0
- package/dist/commands/drift.js +88 -0
- package/dist/commands/extract.js +16 -0
- package/dist/commands/feature-context.js +116 -0
- package/dist/commands/generate.js +339 -0
- package/dist/commands/guard.js +182 -0
- package/dist/commands/init.js +209 -0
- package/dist/commands/intel.js +20 -0
- package/dist/commands/license-dependency-auditor.js +33 -0
- package/dist/commands/performance-hotspot-profiler.js +42 -0
- package/dist/commands/search.js +314 -0
- package/dist/commands/security-boundary-auditor.js +359 -0
- package/dist/commands/simulate.js +294 -0
- package/dist/commands/summary.js +27 -0
- package/dist/commands/test-coverage-mapper.js +264 -0
- package/dist/commands/verify-drift.js +62 -0
- package/dist/config.js +441 -0
- package/dist/extract/ai-context-hints.js +107 -0
- package/dist/extract/analyzers/backend.js +1704 -0
- package/dist/extract/analyzers/depth.js +264 -0
- package/dist/extract/analyzers/frontend.js +2221 -0
- package/dist/extract/api-usage-tracker.js +19 -0
- package/dist/extract/cache.js +53 -0
- package/dist/extract/codebase-intel.js +190 -0
- package/dist/extract/compress.js +452 -0
- package/dist/extract/context-block.js +356 -0
- package/dist/extract/contracts.js +183 -0
- package/dist/extract/discrepancies.js +233 -0
- package/dist/extract/docs-loader.js +110 -0
- package/dist/extract/docs.js +2379 -0
- package/dist/extract/drift.js +1578 -0
- package/dist/extract/duplicates.js +435 -0
- package/dist/extract/feature-arcs.js +138 -0
- package/dist/extract/graph.js +76 -0
- package/dist/extract/html-doc.js +1409 -0
- package/dist/extract/ignore.js +45 -0
- package/dist/extract/index.js +455 -0
- package/dist/extract/llm-client.js +159 -0
- package/dist/extract/pattern-registry.js +141 -0
- package/dist/extract/product-doc.js +497 -0
- package/dist/extract/python.js +1202 -0
- package/dist/extract/runtime.js +193 -0
- package/dist/extract/schema-evolution-validator.js +35 -0
- package/dist/extract/test-gap-analyzer.js +20 -0
- package/dist/extract/tests.js +74 -0
- package/dist/extract/types.js +1 -0
- package/dist/extract/validate-backend.js +30 -0
- package/dist/extract/writer.js +11 -0
- package/dist/output-layout.js +37 -0
- package/dist/project-discovery.js +309 -0
- package/dist/schema/architecture.js +350 -0
- package/dist/schema/feature-spec.js +89 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/ux.js +46 -0
- package/package.json +75 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { createIgnoreMatcher } from "./ignore.js";
|
|
5
|
+
function toPosix(p) {
|
|
6
|
+
return p.split(path.sep).join("/");
|
|
7
|
+
}
|
|
8
|
+
function isDockerfile(name) {
|
|
9
|
+
return name === "Dockerfile" || name.startsWith("Dockerfile.");
|
|
10
|
+
}
|
|
11
|
+
function isComposeFile(name) {
|
|
12
|
+
return (name === "docker-compose.yml" ||
|
|
13
|
+
name === "docker-compose.yaml" ||
|
|
14
|
+
name === "compose.yml" ||
|
|
15
|
+
name === "compose.yaml");
|
|
16
|
+
}
|
|
17
|
+
async function listFiles(root, ignore) {
|
|
18
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
19
|
+
const files = [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = path.join(root, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
if (ignore.isIgnoredDir(entry.name, fullPath)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
files.push(...(await listFiles(fullPath, ignore)));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (entry.isFile()) {
|
|
30
|
+
files.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
function normalizeEnvironment(env) {
|
|
36
|
+
if (!env) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(env)) {
|
|
40
|
+
return env.map((entry) => String(entry));
|
|
41
|
+
}
|
|
42
|
+
if (typeof env === "object") {
|
|
43
|
+
return Object.entries(env).map(([key, value]) => `${key}=${String(value)}`);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function normalizeArray(value) {
|
|
48
|
+
if (!value) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return value.map((entry) => String(entry));
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function parseComposeServices(doc, source) {
|
|
57
|
+
if (!doc || typeof doc !== "object") {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const services = doc.services;
|
|
61
|
+
if (!services || typeof services !== "object") {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const result = [];
|
|
65
|
+
for (const [name, raw] of Object.entries(services)) {
|
|
66
|
+
if (!raw || typeof raw !== "object") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const service = raw;
|
|
70
|
+
const entry = {
|
|
71
|
+
name,
|
|
72
|
+
source,
|
|
73
|
+
image: typeof service.image === "string" ? service.image : undefined,
|
|
74
|
+
build: typeof service.build === "string" ? service.build : undefined,
|
|
75
|
+
ports: normalizeArray(service.ports),
|
|
76
|
+
environment: normalizeEnvironment(service.environment),
|
|
77
|
+
depends_on: normalizeArray(service.depends_on)
|
|
78
|
+
};
|
|
79
|
+
result.push(entry);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
export async function analyzeRuntime(projectRoot, config) {
|
|
84
|
+
const root = path.resolve(projectRoot);
|
|
85
|
+
const ignore = createIgnoreMatcher(config, root);
|
|
86
|
+
const files = await listFiles(root, ignore);
|
|
87
|
+
const dockerfiles = [];
|
|
88
|
+
const services = [];
|
|
89
|
+
const manifests = [];
|
|
90
|
+
const shell_scripts = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const name = path.basename(file);
|
|
93
|
+
const relative = toPosix(path.relative(root, file));
|
|
94
|
+
if (name.endsWith(".sh") || name.endsWith(".bash") || name.endsWith(".zsh") || name.endsWith(".bat") || name.endsWith(".ps1")) {
|
|
95
|
+
shell_scripts.push(relative);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const isRoot = !relative.includes("/");
|
|
99
|
+
const explicitConfigs = new Set([
|
|
100
|
+
"package.json", "pyproject.toml", "requirements.txt", "go.mod", "go.sum",
|
|
101
|
+
"pom.xml", "build.gradle", "Makefile", "makefile", "tslint.json",
|
|
102
|
+
"eslint.config.js", "tsconfig.json", ".npmrc", "yarn.lock", "package-lock.json"
|
|
103
|
+
]);
|
|
104
|
+
if (explicitConfigs.has(name) ||
|
|
105
|
+
(relative.startsWith(".github/") && (name.endsWith(".yml") || name.endsWith(".yaml"))) ||
|
|
106
|
+
(isRoot && (name.endsWith(".json") || name.endsWith(".yaml") || name.endsWith(".yml") || name.endsWith(".md") || name.endsWith(".toml")))) {
|
|
107
|
+
if (!isComposeFile(name)) {
|
|
108
|
+
const manifest = await parseManifest(file, relative, name);
|
|
109
|
+
if (manifest) {
|
|
110
|
+
manifests.push(manifest);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (isDockerfile(name)) {
|
|
115
|
+
dockerfiles.push(relative);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (isComposeFile(name)) {
|
|
119
|
+
try {
|
|
120
|
+
const raw = await fs.readFile(file, "utf8");
|
|
121
|
+
const doc = yaml.load(raw);
|
|
122
|
+
services.push(...parseComposeServices(doc, relative));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
dockerfiles.sort((a, b) => a.localeCompare(b));
|
|
130
|
+
manifests.sort((a, b) => a.file.localeCompare(b.file));
|
|
131
|
+
shell_scripts.sort((a, b) => a.localeCompare(b));
|
|
132
|
+
services.sort((a, b) => {
|
|
133
|
+
const nameCmp = a.name.localeCompare(b.name);
|
|
134
|
+
if (nameCmp !== 0)
|
|
135
|
+
return nameCmp;
|
|
136
|
+
return a.source.localeCompare(b.source);
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
dockerfiles,
|
|
140
|
+
services,
|
|
141
|
+
manifests,
|
|
142
|
+
shell_scripts
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function parseManifest(file, relative, name) {
|
|
146
|
+
try {
|
|
147
|
+
const raw = await fs.readFile(file, "utf8");
|
|
148
|
+
if (name === "package.json") {
|
|
149
|
+
const parsed = JSON.parse(raw);
|
|
150
|
+
const commands = parsed.scripts ? Object.keys(parsed.scripts) : [];
|
|
151
|
+
const dependencies = parsed.dependencies ? Object.keys(parsed.dependencies) : [];
|
|
152
|
+
const dev_dependencies = parsed.devDependencies ? Object.keys(parsed.devDependencies) : [];
|
|
153
|
+
return { file: relative, kind: "npm", commands, dependencies, dev_dependencies };
|
|
154
|
+
}
|
|
155
|
+
if (name === "Makefile" || name === "makefile") {
|
|
156
|
+
const commands = [];
|
|
157
|
+
const regex = /^([a-zA-Z0-9_-]+):/gm;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = regex.exec(raw)) !== null) {
|
|
160
|
+
if (match[1] && match[1] !== ".PHONY")
|
|
161
|
+
commands.push(match[1]);
|
|
162
|
+
}
|
|
163
|
+
return { file: relative, kind: "makefile", commands };
|
|
164
|
+
}
|
|
165
|
+
if (relative.startsWith(".github/") && (name.endsWith(".yml") || name.endsWith(".yaml"))) {
|
|
166
|
+
const doc = yaml.load(raw);
|
|
167
|
+
if (doc && typeof doc === "object") {
|
|
168
|
+
const description = doc.name || undefined;
|
|
169
|
+
let commands = [];
|
|
170
|
+
if (doc.jobs && typeof doc.jobs === "object") {
|
|
171
|
+
commands = Object.keys(doc.jobs);
|
|
172
|
+
}
|
|
173
|
+
return { file: relative, kind: "github-action", description, commands };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (name === "pyproject.toml") {
|
|
177
|
+
return { file: relative, kind: "poetry" };
|
|
178
|
+
}
|
|
179
|
+
if (name === "go.mod" || name === "go.sum") {
|
|
180
|
+
return { file: relative, kind: "go" };
|
|
181
|
+
}
|
|
182
|
+
if (name === "pom.xml" || name === "build.gradle") {
|
|
183
|
+
return { file: relative, kind: "maven" };
|
|
184
|
+
}
|
|
185
|
+
if (name.endsWith(".md")) {
|
|
186
|
+
return { file: relative, kind: "doc" };
|
|
187
|
+
}
|
|
188
|
+
return { file: relative, kind: "unknown" };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { analyzeBackend } from "./analyzers/backend.js";
|
|
2
|
+
export async function validateSchemaEvolution(backendRoot, previousSchemas) {
|
|
3
|
+
const backendAnalysis = await analyzeBackend(backendRoot, {});
|
|
4
|
+
const currentSchemas = backendAnalysis.endpoints.map((endpoint) => ({
|
|
5
|
+
path: endpoint.path,
|
|
6
|
+
method: endpoint.method,
|
|
7
|
+
request_schema: endpoint.request_schema,
|
|
8
|
+
response_schema: endpoint.response_schema
|
|
9
|
+
}));
|
|
10
|
+
const validationResults = [];
|
|
11
|
+
for (const currentSchema of currentSchemas) {
|
|
12
|
+
const previousSchema = previousSchemas.find((schema) => schema.path === currentSchema.path && schema.method === currentSchema.method);
|
|
13
|
+
const validationResult = {
|
|
14
|
+
endpoint: `${currentSchema.method} ${currentSchema.path}`,
|
|
15
|
+
compatible: true,
|
|
16
|
+
issues: []
|
|
17
|
+
};
|
|
18
|
+
if (!previousSchema) {
|
|
19
|
+
validationResult.compatible = false;
|
|
20
|
+
validationResult.issues.push("New endpoint detected.");
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (JSON.stringify(currentSchema.request_schema) !== JSON.stringify(previousSchema.request_schema)) {
|
|
24
|
+
validationResult.compatible = false;
|
|
25
|
+
validationResult.issues.push("Request schema changed.");
|
|
26
|
+
}
|
|
27
|
+
if (JSON.stringify(currentSchema.response_schema) !== JSON.stringify(previousSchema.response_schema)) {
|
|
28
|
+
validationResult.compatible = false;
|
|
29
|
+
validationResult.issues.push("Response schema changed.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
validationResults.push(validationResult);
|
|
33
|
+
}
|
|
34
|
+
return validationResults;
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { analyzeBackend } from "./analyzers/backend.js";
|
|
2
|
+
import { analyzeTests } from "./analyzers/tests.js";
|
|
3
|
+
export async function analyzeTestGaps(backendRoot, testRoot) {
|
|
4
|
+
const backendAnalysis = await analyzeBackend(backendRoot, {});
|
|
5
|
+
const testAnalysis = await analyzeTests(testRoot, {});
|
|
6
|
+
const untestedEndpoints = backendAnalysis.endpoints.filter((endpoint) => {
|
|
7
|
+
return !testAnalysis.testedEndpoints.some((tested) => tested.path === endpoint.path && tested.method === endpoint.method);
|
|
8
|
+
});
|
|
9
|
+
const testStubs = untestedEndpoints.map((endpoint) => {
|
|
10
|
+
return {
|
|
11
|
+
testFile: `${testRoot}/generated/${endpoint.method}_${endpoint.path.replace(/\//g, "_")}.test.ts`,
|
|
12
|
+
content: `describe('${endpoint.method} ${endpoint.path}', () => {
|
|
13
|
+
it('should handle ${endpoint.method} requests', async () => {
|
|
14
|
+
// TODO: Implement test logic
|
|
15
|
+
});
|
|
16
|
+
});`
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
return testStubs;
|
|
20
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function computeTestCoverage(files) {
|
|
3
|
+
const testFiles = new Set();
|
|
4
|
+
const sourceFiles = new Set();
|
|
5
|
+
for (const file of files) {
|
|
6
|
+
const isTest = file.includes(".test.") ||
|
|
7
|
+
file.includes(".spec.") ||
|
|
8
|
+
file.includes("_test.") ||
|
|
9
|
+
file.includes("test_") ||
|
|
10
|
+
file.startsWith("tests/") ||
|
|
11
|
+
file.includes("/tests/");
|
|
12
|
+
if (isTest) {
|
|
13
|
+
testFiles.add(file);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
sourceFiles.add(file);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const coverageMap = [];
|
|
20
|
+
const coveredSourceFiles = new Set();
|
|
21
|
+
for (const testFile of testFiles) {
|
|
22
|
+
const basename = path.basename(testFile);
|
|
23
|
+
let potentialSourceName = basename
|
|
24
|
+
.replace(/\.test\./, ".")
|
|
25
|
+
.replace(/\.spec\./, ".")
|
|
26
|
+
.replace(/_test\./, ".")
|
|
27
|
+
.replace(/^test_/, "");
|
|
28
|
+
// Search for an exact match in sourceFiles
|
|
29
|
+
let exactMatch = null;
|
|
30
|
+
let implicitMatch = null;
|
|
31
|
+
for (const sourceFile of sourceFiles) {
|
|
32
|
+
if (path.basename(sourceFile) === potentialSourceName) {
|
|
33
|
+
// If they share exactly the same dirname logic, it's exact
|
|
34
|
+
// Otherwise, it's an implicit match
|
|
35
|
+
if (path.dirname(testFile).replace(/\/tests?(\/|$)/, "/") === path.dirname(sourceFile)) {
|
|
36
|
+
exactMatch = sourceFile;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
implicitMatch = sourceFile;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const match = exactMatch ?? implicitMatch;
|
|
45
|
+
if (match) {
|
|
46
|
+
coveredSourceFiles.add(match);
|
|
47
|
+
coverageMap.push({
|
|
48
|
+
test_file: testFile,
|
|
49
|
+
source_file: match,
|
|
50
|
+
match_type: exactMatch ? "exact" : "implicit"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
coverageMap.push({
|
|
55
|
+
test_file: testFile,
|
|
56
|
+
source_file: null,
|
|
57
|
+
match_type: "none"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const untestedSourceFiles = Array.from(sourceFiles)
|
|
62
|
+
.filter((file) => !coveredSourceFiles.has(file))
|
|
63
|
+
.sort((a, b) => a.localeCompare(b));
|
|
64
|
+
const testFilesMissingSource = coverageMap
|
|
65
|
+
.filter((cov) => cov.match_type === "none")
|
|
66
|
+
.map((cov) => cov.test_file)
|
|
67
|
+
.sort((a, b) => a.localeCompare(b));
|
|
68
|
+
coverageMap.sort((a, b) => a.test_file.localeCompare(b.test_file));
|
|
69
|
+
return {
|
|
70
|
+
untested_source_files: untestedSourceFiles,
|
|
71
|
+
test_files_missing_source: testFilesMissingSource,
|
|
72
|
+
coverage_map: coverageMap
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { analyzeBackend } from "./analyzers/backend.js";
|
|
2
|
+
export async function validateBackendEndpoints(backendRoot, expectedContracts) {
|
|
3
|
+
const backendAnalysis = await analyzeBackend(backendRoot, {});
|
|
4
|
+
const validationResults = [];
|
|
5
|
+
for (const endpoint of backendAnalysis.endpoints) {
|
|
6
|
+
const validationResult = {
|
|
7
|
+
endpoint: `${endpoint.method} ${endpoint.path}`,
|
|
8
|
+
valid: true,
|
|
9
|
+
issues: []
|
|
10
|
+
};
|
|
11
|
+
const expectedContract = expectedContracts.find((contract) => contract.path === endpoint.path && contract.method === endpoint.method);
|
|
12
|
+
if (!expectedContract) {
|
|
13
|
+
validationResult.valid = false;
|
|
14
|
+
validationResult.issues.push("No matching contract found.");
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Validate request and response schemas
|
|
18
|
+
if (JSON.stringify(endpoint.request_schema) !== JSON.stringify(expectedContract.request_schema)) {
|
|
19
|
+
validationResult.valid = false;
|
|
20
|
+
validationResult.issues.push("Request schema mismatch.");
|
|
21
|
+
}
|
|
22
|
+
if (JSON.stringify(endpoint.response_schema) !== JSON.stringify(expectedContract.response_schema)) {
|
|
23
|
+
validationResult.valid = false;
|
|
24
|
+
validationResult.issues.push("Response schema mismatch.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
validationResults.push(validationResult);
|
|
28
|
+
}
|
|
29
|
+
return validationResults;
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
export async function writeSnapshots(outputDir, architecture, ux) {
|
|
5
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
6
|
+
const architecturePath = path.join(outputDir, "architecture.snapshot.yaml");
|
|
7
|
+
const uxPath = path.join(outputDir, "ux.snapshot.yaml");
|
|
8
|
+
await fs.writeFile(architecturePath, yaml.dump(architecture, { noRefs: true, lineWidth: 120 }));
|
|
9
|
+
await fs.writeFile(uxPath, yaml.dump(ux, { noRefs: true, lineWidth: 120 }));
|
|
10
|
+
return { architecturePath, uxPath };
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
export function getOutputLayout(outputRoot, internalDir = "internal") {
|
|
4
|
+
const rootDir = path.resolve(outputRoot);
|
|
5
|
+
const machineDir = path.join(rootDir, "machine");
|
|
6
|
+
return {
|
|
7
|
+
rootDir,
|
|
8
|
+
machineDir,
|
|
9
|
+
machineDocsDir: path.join(machineDir, "docs"),
|
|
10
|
+
machineInternalDir: path.join(machineDir, "docs", internalDir),
|
|
11
|
+
humanDir: path.join(rootDir, "human")
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function resolveMachineInputDir(input) {
|
|
15
|
+
const resolved = path.resolve(input || "specs-out");
|
|
16
|
+
const directSnapshot = await hasMachineSnapshots(resolved);
|
|
17
|
+
if (directSnapshot) {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
const machineDir = path.join(resolved, "machine");
|
|
21
|
+
if (await hasMachineSnapshots(machineDir)) {
|
|
22
|
+
return machineDir;
|
|
23
|
+
}
|
|
24
|
+
return machineDir;
|
|
25
|
+
}
|
|
26
|
+
async function hasMachineSnapshots(dir) {
|
|
27
|
+
try {
|
|
28
|
+
await Promise.all([
|
|
29
|
+
fs.stat(path.join(dir, "architecture.snapshot.yaml")),
|
|
30
|
+
fs.stat(path.join(dir, "ux.snapshot.yaml"))
|
|
31
|
+
]);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|