arcshape 0.0.0-stage → 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 +201 -0
- package/README.md +91 -2
- package/bin/arcshape.mjs +93 -0
- package/bin/confluence-pages.mjs +569 -0
- package/bin/confluence.mjs +263 -0
- package/bin/init.mjs +160 -0
- package/bin/product-citations.mjs +104 -0
- package/bin/session.mjs +753 -0
- package/bin/site.mjs +59 -0
- package/bin/sync.mjs +352 -0
- package/bin/validate-links.mjs +98 -0
- package/bin/validate.mjs +126 -0
- package/package.json +62 -4
- package/templates/architecture/01-introduction-and-goals.md +13 -0
- package/templates/architecture/02-constraints.md +13 -0
- package/templates/architecture/03-context-and-scope.md +11 -0
- package/templates/architecture/04-solution-strategy.md +13 -0
- package/templates/architecture/05-building-block-view.md +11 -0
- package/templates/architecture/06-runtime-view.md +9 -0
- package/templates/architecture/07-deployment-view.md +11 -0
- package/templates/architecture/08-crosscutting-concepts.md +13 -0
- package/templates/architecture/09-architecture-decisions.md +9 -0
- package/templates/architecture/10-quality-requirements.md +11 -0
- package/templates/architecture/11-risks-and-technical-debt.md +11 -0
- package/templates/architecture/12-glossary.md +10 -0
- package/templates/architecture/README.md +20 -0
- package/templates/assistants/.agents/skills/arcshape-adr/SKILL.md +16 -0
- package/templates/assistants/.agents/skills/arcshape-architecture/SKILL.md +35 -0
- package/templates/assistants/.agents/skills/arcshape-c4/SKILL.md +13 -0
- package/templates/assistants/.agents/skills/arcshape-docs-sync/SKILL.md +12 -0
- package/templates/assistants/.claude/commands/arcshape/adr.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/c4.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/document.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/reverse-engineer.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/sync.md +5 -0
- package/templates/assistants/.claude/skills/arcshape-adr/SKILL.md +16 -0
- package/templates/assistants/.claude/skills/arcshape-architecture/SKILL.md +35 -0
- package/templates/assistants/.claude/skills/arcshape-c4/SKILL.md +13 -0
- package/templates/assistants/.claude/skills/arcshape-docs-sync/SKILL.md +12 -0
- package/tools/arc42-site/LICENSE +21 -0
- package/tools/arc42-site/NOTICE.md +3 -0
- package/tools/arc42-site/README.md +3 -0
- package/tools/arc42-site/browser-options.mjs +14 -0
- package/tools/arc42-site/capture-evidence.mjs +84 -0
- package/tools/arc42-site/generate.mjs +227 -0
- package/tools/arc42-site/loadDocs.mjs +196 -0
- package/tools/arc42-site/render.mjs +411 -0
- package/tools/arc42-site/shell.mjs +761 -0
- package/tools/arc42-site/validate-diagrams.mjs +27 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate.mjs — arc42 snapshot generator (portable, bundled with the architecture-docs skill).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node generate.mjs <docsDir> [--out <file>] [--title <name>] [--source-ref <ref>]
|
|
7
|
+
*
|
|
8
|
+
* Reads an arc42 documentation folder (README.md + NN-*.md sections with the
|
|
9
|
+
* architecture-docs skill's frontmatter contract) and writes exactly one
|
|
10
|
+
* self-contained HTML file: inline styles, inline pre-rendered Mermaid SVGs,
|
|
11
|
+
* inert JSON data, one ES5 router. No external resources; opens from file://.
|
|
12
|
+
* Generation is deterministic and never modifies authored files.
|
|
13
|
+
*
|
|
14
|
+
* Default output: <docsDir>/../arc42-snapshot.html (sibling of the docs folder)
|
|
15
|
+
*/
|
|
16
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { execFile } from "node:child_process";
|
|
18
|
+
import { promisify } from "node:util";
|
|
19
|
+
import { resolve, join, dirname, relative, sep } from "node:path";
|
|
20
|
+
import { loadDocs, Diagnostic } from "./loadDocs.mjs";
|
|
21
|
+
import {
|
|
22
|
+
renderMarkdown,
|
|
23
|
+
extractMermaidSources,
|
|
24
|
+
renderMermaid,
|
|
25
|
+
} from "./render.mjs";
|
|
26
|
+
import { buildSnapshotHtml } from "./shell.mjs";
|
|
27
|
+
|
|
28
|
+
const run = promisify(execFile);
|
|
29
|
+
const generatedMarker =
|
|
30
|
+
"<!-- Generated by ArcShape from canonical Markdown. -->";
|
|
31
|
+
|
|
32
|
+
function usage(code = 1) {
|
|
33
|
+
process.stdout.write(
|
|
34
|
+
[
|
|
35
|
+
"Usage: node generate.mjs <docsDir> [--out <file>] [--title <name>] [--source-ref <ref>]",
|
|
36
|
+
"",
|
|
37
|
+
" docsDir folder containing README.md and NN-*.md arc42 sections",
|
|
38
|
+
" --out output HTML path (default: <docsDir>/../arc42-snapshot.html)",
|
|
39
|
+
" --title page title override (default: derived from docsDir parent folder)",
|
|
40
|
+
" --source-ref Git branch, tag, or ref for repository source links (default: current branch)",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n"),
|
|
43
|
+
);
|
|
44
|
+
process.exit(code);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const positional = [];
|
|
49
|
+
const flags = {};
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
if (arg === "--out" || arg === "--title" || arg === "--source-ref") {
|
|
53
|
+
if (!argv[i + 1] || argv[i + 1].startsWith("--")) usage(1);
|
|
54
|
+
flags[arg.slice(2)] = argv[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
57
|
+
usage(0);
|
|
58
|
+
} else if (arg.startsWith("-")) {
|
|
59
|
+
usage(1);
|
|
60
|
+
} else {
|
|
61
|
+
positional.push(arg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { positional, flags };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Best-effort git revision of the repository containing docsDir. */
|
|
68
|
+
async function gitRevision(docsDir) {
|
|
69
|
+
try {
|
|
70
|
+
const { stdout } = await run("git", ["rev-parse", "HEAD"], {
|
|
71
|
+
cwd: docsDir,
|
|
72
|
+
});
|
|
73
|
+
return stdout.trim();
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function gitSourceContext(docsDir, sourceRefOverride) {
|
|
80
|
+
try {
|
|
81
|
+
const [root, remote, ref] = await Promise.all([
|
|
82
|
+
run("git", ["rev-parse", "--show-toplevel"], { cwd: docsDir }),
|
|
83
|
+
run("git", ["remote", "get-url", "origin"], { cwd: docsDir }),
|
|
84
|
+
run("git", ["branch", "--show-current"], { cwd: docsDir }),
|
|
85
|
+
]);
|
|
86
|
+
const repositoryUrl = githubRepositoryUrl(remote.stdout.trim());
|
|
87
|
+
return repositoryUrl
|
|
88
|
+
? {
|
|
89
|
+
root: root.stdout.trim(),
|
|
90
|
+
repositoryUrl,
|
|
91
|
+
ref: sourceRefOverride || ref.stdout.trim() || "HEAD",
|
|
92
|
+
}
|
|
93
|
+
: undefined;
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function githubRepositoryUrl(remote) {
|
|
100
|
+
const match =
|
|
101
|
+
/^(?:https:\/\/github\.com\/|git@github\.com:)([^/]+\/[^/]+?)(?:\.git)?$/.exec(
|
|
102
|
+
remote,
|
|
103
|
+
);
|
|
104
|
+
return match ? `https://github.com/${match[1]}` : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
109
|
+
if (positional.length !== 1) usage(1);
|
|
110
|
+
const docsDir = resolve(positional[0]);
|
|
111
|
+
|
|
112
|
+
let docs;
|
|
113
|
+
try {
|
|
114
|
+
docs = await loadDocs(docsDir);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error instanceof Diagnostic) {
|
|
117
|
+
process.stderr.write(`arc42-snapshot: ${error.message}\n`);
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stackName = flags.title ?? docs.stackName;
|
|
124
|
+
const outFile = flags.out
|
|
125
|
+
? resolve(flags.out)
|
|
126
|
+
: join(dirname(docsDir), "arc42-snapshot.html");
|
|
127
|
+
if (!/\.html?$/i.test(outFile)) {
|
|
128
|
+
throw new Diagnostic(
|
|
129
|
+
"--out must name an .html or .htm file; authored Markdown is never an output target",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Route map: section file names and README.md -> hash routes.
|
|
134
|
+
const fileToRoute = new Map(
|
|
135
|
+
docs.sections.map((s) => [s.file, `#/section/${s.number}`]),
|
|
136
|
+
);
|
|
137
|
+
fileToRoute.set(docs.readme.file, "#/");
|
|
138
|
+
|
|
139
|
+
// Pre-render every mermaid diagram across all documents in one batch.
|
|
140
|
+
const readmeMermaid = extractMermaidSources(docs.readme.body);
|
|
141
|
+
const sectionMermaid = docs.sections.map((s) =>
|
|
142
|
+
extractMermaidSources(s.body),
|
|
143
|
+
);
|
|
144
|
+
const allMermaid = [...readmeMermaid, ...sectionMermaid.flat()];
|
|
145
|
+
process.stdout.write(
|
|
146
|
+
`arc42-snapshot: ${docs.sections.length} sections, ${allMermaid.length} mermaid diagram(s)\n`,
|
|
147
|
+
);
|
|
148
|
+
const svgs = await renderMermaid(allMermaid);
|
|
149
|
+
|
|
150
|
+
// Distribute the rendered SVGs back to their documents.
|
|
151
|
+
let cursor = 0;
|
|
152
|
+
const take = (count) => {
|
|
153
|
+
const slice = svgs.slice(cursor, cursor + count);
|
|
154
|
+
cursor += count;
|
|
155
|
+
return slice;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const sourceContext = await gitSourceContext(docsDir, flags["source-ref"]);
|
|
159
|
+
const toSourcePath = (file) =>
|
|
160
|
+
sourceContext
|
|
161
|
+
? relative(sourceContext.root, join(docsDir, file)).split(sep).join("/")
|
|
162
|
+
: undefined;
|
|
163
|
+
|
|
164
|
+
const readmeRendered = await renderMarkdown(docs.readme.body, {
|
|
165
|
+
fileToRoute,
|
|
166
|
+
svgs: take(readmeMermaid.length),
|
|
167
|
+
title: docs.readme.title,
|
|
168
|
+
sourceContext,
|
|
169
|
+
sourceFile: toSourcePath(docs.readme.file),
|
|
170
|
+
});
|
|
171
|
+
const sectionsRendered = [];
|
|
172
|
+
for (let i = 0; i < docs.sections.length; i += 1) {
|
|
173
|
+
const section = docs.sections[i];
|
|
174
|
+
const rendered = await renderMarkdown(section.body, {
|
|
175
|
+
fileToRoute,
|
|
176
|
+
svgs: take(sectionMermaid[i].length),
|
|
177
|
+
currentRoute: `#/section/${section.number}`,
|
|
178
|
+
title: section.title,
|
|
179
|
+
sourceContext,
|
|
180
|
+
sourceFile: toSourcePath(section.file),
|
|
181
|
+
});
|
|
182
|
+
sectionsRendered.push({
|
|
183
|
+
number: section.number,
|
|
184
|
+
title: section.title,
|
|
185
|
+
description: section.description,
|
|
186
|
+
html: rendered.html,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const revision = await gitRevision(docsDir);
|
|
191
|
+
const html = `${generatedMarker}\n${buildSnapshotHtml({
|
|
192
|
+
readme: {
|
|
193
|
+
title: docs.readme.title,
|
|
194
|
+
description: docs.readme.description,
|
|
195
|
+
html: readmeRendered.html,
|
|
196
|
+
},
|
|
197
|
+
sections: sectionsRendered,
|
|
198
|
+
stackName,
|
|
199
|
+
revision,
|
|
200
|
+
})}`;
|
|
201
|
+
|
|
202
|
+
await mkdir(dirname(outFile), { recursive: true });
|
|
203
|
+
try {
|
|
204
|
+
const existing = await readFile(outFile, "utf8");
|
|
205
|
+
if (existing === html) {
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`arc42-snapshot: site is already up to date at ${outFile}\n`,
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!existing.includes(generatedMarker)) {
|
|
212
|
+
throw new Diagnostic(
|
|
213
|
+
`output already exists and is not an ArcShape-generated site: ${outFile}; choose another --out path or move the file`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof Diagnostic) throw error;
|
|
218
|
+
if (error.code !== "ENOENT") throw error;
|
|
219
|
+
}
|
|
220
|
+
await writeFile(outFile, html, "utf8");
|
|
221
|
+
process.stdout.write(`arc42-snapshot: snapshot written to ${outFile}\n`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main().catch((error) => {
|
|
225
|
+
process.stderr.write(`arc42-snapshot: ${error.stack ?? error.message}\n`);
|
|
226
|
+
process.exit(error instanceof Diagnostic ? 2 : 1);
|
|
227
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loadDocs.mjs — scan an arc42 documentation folder, validate the document contract
|
|
3
|
+
* and parse every section. Mirrors productshape's "honest diagnostics" principle:
|
|
4
|
+
* a broken model fails generation rather than producing a page that silently
|
|
5
|
+
* omits part of the documentation.
|
|
6
|
+
*/
|
|
7
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { join, basename, dirname } from "node:path";
|
|
9
|
+
import * as yaml from "js-yaml";
|
|
10
|
+
|
|
11
|
+
const ALLOWED_KEYS = new Set(["title", "arc42-section", "description"]);
|
|
12
|
+
const SECTION_FILE = /^(\d{2})-(.+)\.md$/;
|
|
13
|
+
const SECTION_NUMBERS = Array.from({ length: 12 }, (_, index) =>
|
|
14
|
+
String(index + 1).padStart(2, "0"),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export class Diagnostic extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "Diagnostic";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse the `---\nkey: value\n---` frontmatter block of a Markdown file.
|
|
26
|
+
* Returns { frontmatter, body } or throws a Diagnostic naming the file.
|
|
27
|
+
*/
|
|
28
|
+
export function parseFrontmatter(raw, fileName) {
|
|
29
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new Diagnostic(`${fileName}: missing YAML frontmatter block`);
|
|
32
|
+
}
|
|
33
|
+
let frontmatter;
|
|
34
|
+
try {
|
|
35
|
+
frontmatter = yaml.load(match[1], { schema: yaml.JSON_SCHEMA }) ?? {};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Diagnostic(
|
|
38
|
+
`${fileName}: unparseable frontmatter: ${error.message}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (typeof frontmatter !== "object" || Array.isArray(frontmatter)) {
|
|
42
|
+
throw new Diagnostic(`${fileName}: frontmatter must be a mapping`);
|
|
43
|
+
}
|
|
44
|
+
return { frontmatter, body: match[2] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load and validate an arc42 docs folder.
|
|
49
|
+
*
|
|
50
|
+
* Contract (from the architecture-docs skill):
|
|
51
|
+
* - README.md exists, optionally has title/description, never `arc42-section`.
|
|
52
|
+
* - Section files are named NN-slug.md with zero-padded numbers.
|
|
53
|
+
* - Each has exactly the keys: title, arc42-section, description.
|
|
54
|
+
* - arc42-section values are unique and match the file number.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} docsDir absolute path to the docs/architecture folder
|
|
57
|
+
* @returns {{ readme: object, sections: object[], stackName: string }}
|
|
58
|
+
*/
|
|
59
|
+
export async function loadDocs(docsDir) {
|
|
60
|
+
const entries = await readdir(docsDir, { withFileTypes: true });
|
|
61
|
+
const mdFiles = entries
|
|
62
|
+
.filter((entry) => entry.isFile() && /\.md$/i.test(entry.name))
|
|
63
|
+
.map((e) => e.name)
|
|
64
|
+
.sort();
|
|
65
|
+
|
|
66
|
+
const readmeName = mdFiles.find((n) => n.toLowerCase() === "readme.md");
|
|
67
|
+
if (!readmeName) {
|
|
68
|
+
throw new Diagnostic(
|
|
69
|
+
`${docsDir}: no README.md found — the navigation index is required`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sectionFiles = mdFiles.filter((n) => SECTION_FILE.test(n));
|
|
74
|
+
if (sectionFiles.length === 0) {
|
|
75
|
+
throw new Diagnostic(`${docsDir}: no NN-*.md section files found`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const problems = mdFiles
|
|
79
|
+
.filter((name) => name !== readmeName && !SECTION_FILE.test(name))
|
|
80
|
+
.map(
|
|
81
|
+
(name) =>
|
|
82
|
+
`${name}: unexpected Markdown file; name an arc42 section NN-slug.md or move supporting material into a subdirectory`,
|
|
83
|
+
);
|
|
84
|
+
const sections = [];
|
|
85
|
+
const seenNumbers = new Map();
|
|
86
|
+
|
|
87
|
+
// --- README: navigation index, never a section itself.
|
|
88
|
+
const readmeRaw = await readFile(join(docsDir, readmeName), "utf8");
|
|
89
|
+
const readmeParsed = /^---\r?\n/.test(readmeRaw)
|
|
90
|
+
? parseFrontmatter(readmeRaw, readmeName)
|
|
91
|
+
: { frontmatter: {}, body: readmeRaw };
|
|
92
|
+
for (const key of Object.keys(readmeParsed.frontmatter)) {
|
|
93
|
+
if (key === "arc42-section") {
|
|
94
|
+
problems.push(
|
|
95
|
+
`${readmeName}: README must not carry arc42-section (it is navigation, not a section)`,
|
|
96
|
+
);
|
|
97
|
+
} else if (!["title", "description"].includes(key)) {
|
|
98
|
+
problems.push(`${readmeName}: unexpected frontmatter key "${key}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const key of ["title", "description"]) {
|
|
102
|
+
if (
|
|
103
|
+
key in readmeParsed.frontmatter &&
|
|
104
|
+
(typeof readmeParsed.frontmatter[key] !== "string" ||
|
|
105
|
+
!readmeParsed.frontmatter[key].trim())
|
|
106
|
+
) {
|
|
107
|
+
problems.push(
|
|
108
|
+
`${readmeName}: ${key} must be a nonempty string when present`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Sections.
|
|
114
|
+
for (const name of sectionFiles) {
|
|
115
|
+
const number = SECTION_FILE.exec(name)[1];
|
|
116
|
+
const raw = await readFile(join(docsDir, name), "utf8");
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = parseFrontmatter(raw, name);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
problems.push(error.message);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fm = parsed.frontmatter;
|
|
125
|
+
for (const key of Object.keys(fm)) {
|
|
126
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
127
|
+
problems.push(`${name}: unexpected frontmatter key "${key}"`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const key of ["title", "arc42-section", "description"]) {
|
|
131
|
+
if (!(key in fm)) {
|
|
132
|
+
problems.push(`${name}: missing required frontmatter key "${key}"`);
|
|
133
|
+
} else if (typeof fm[key] !== "string" || !fm[key].trim()) {
|
|
134
|
+
problems.push(`${name}: ${key} must be a nonempty string`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!SECTION_NUMBERS.includes(number)) {
|
|
138
|
+
problems.push(`${name}: section number must be between 01 and 12`);
|
|
139
|
+
}
|
|
140
|
+
if (fm["arc42-section"] !== undefined && fm["arc42-section"] !== number) {
|
|
141
|
+
problems.push(
|
|
142
|
+
`${name}: arc42-section "${fm["arc42-section"]}" does not match file number ${number}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (seenNumbers.has(number)) {
|
|
146
|
+
problems.push(
|
|
147
|
+
`${name}: duplicate section number ${number} (also ${seenNumbers.get(number)})`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
seenNumbers.set(number, name);
|
|
151
|
+
|
|
152
|
+
sections.push({
|
|
153
|
+
number,
|
|
154
|
+
slug: SECTION_FILE.exec(name)[2],
|
|
155
|
+
file: name,
|
|
156
|
+
title: String(fm.title ?? name),
|
|
157
|
+
description: String(fm.description ?? ""),
|
|
158
|
+
body: parsed.body,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const number of SECTION_NUMBERS) {
|
|
163
|
+
if (!seenNumbers.has(number))
|
|
164
|
+
problems.push(`required arc42 section ${number} is missing`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (problems.length > 0) {
|
|
168
|
+
const error = new Diagnostic(
|
|
169
|
+
`arc42 document contract violations:\n - ${problems.join("\n - ")}`,
|
|
170
|
+
);
|
|
171
|
+
error.problems = problems;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
sections.sort((a, b) => a.number.localeCompare(b.number));
|
|
176
|
+
|
|
177
|
+
// Derive a name from the docsDir's parent (e.g. .../my-project/docs/architecture → "my-project").
|
|
178
|
+
// Falls back to the docsDir folder name itself if there is no meaningful parent.
|
|
179
|
+
const parent = basename(dirname(docsDir));
|
|
180
|
+
const grandparent = basename(dirname(dirname(docsDir)));
|
|
181
|
+
const stackName =
|
|
182
|
+
parent === "docs" ? grandparent : parent || basename(docsDir);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
readme: {
|
|
186
|
+
file: readmeName,
|
|
187
|
+
title: String(
|
|
188
|
+
readmeParsed.frontmatter.title ?? "Architecture Documentation",
|
|
189
|
+
),
|
|
190
|
+
description: String(readmeParsed.frontmatter.description ?? ""),
|
|
191
|
+
body: readmeParsed.body,
|
|
192
|
+
},
|
|
193
|
+
sections,
|
|
194
|
+
stackName,
|
|
195
|
+
};
|
|
196
|
+
}
|