@ghostbyte/agent-docs-kit 4.0.3 → 5.0.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +43 -38
  3. package/SECURITY.md +11 -0
  4. package/content/catalog.json +191 -22
  5. package/content/documentation/01-system-documentation.md +13 -0
  6. package/content/documentation/02-project-governance.md +13 -0
  7. package/content/documentation/03-development-records.md +13 -0
  8. package/content/documentation/04-testing-evidence.md +13 -0
  9. package/content/documentation/05-operations-runbooks.md +13 -0
  10. package/content/documentation/06-future-proposals.md +13 -0
  11. package/content/documentation/07-archive-governance.md +13 -0
  12. package/content/documentation/full-reference.md +24 -0
  13. package/content/engineering/01-query-performance.md +17 -27
  14. package/content/engineering/02-networking-fundamentals.md +17 -27
  15. package/content/engineering/03-rate-limiting-algorithms.md +18 -28
  16. package/content/engineering/04-production-debugging.md +17 -27
  17. package/content/engineering/05-code-review-practices.md +17 -27
  18. package/content/engineering/06-data-modeling.md +17 -27
  19. package/content/engineering/07-cloud-cost-saving.md +17 -27
  20. package/content/engineering/08-testing-types.md +17 -27
  21. package/content/engineering/09-message-queues.md +17 -27
  22. package/content/engineering/10-api-design-compatibility.md +14 -0
  23. package/content/engineering/11-reliability-slos.md +14 -0
  24. package/content/engineering/12-observability-telemetry.md +14 -0
  25. package/content/engineering/13-deployment-change-safety.md +14 -0
  26. package/content/engineering/14-capacity-concurrency.md +14 -0
  27. package/content/engineering/full-reference.md +24 -144
  28. package/content/security/01-design-architecture.md +11 -19
  29. package/content/security/02-local-development.md +13 -21
  30. package/content/security/03-database-backend.md +18 -26
  31. package/content/security/04-api-layer.md +23 -31
  32. package/content/security/05-frontend-ui.md +16 -24
  33. package/content/security/06-application-security-testing.md +22 -28
  34. package/content/security/07-cicd-release-engineering.md +13 -21
  35. package/content/security/08-cloud-infrastructure.md +17 -25
  36. package/content/security/09-edge-cdn-dns.md +16 -24
  37. package/content/security/10-release-rollout.md +13 -21
  38. package/content/security/11-post-release-monitoring-ir.md +15 -23
  39. package/content/security/12-ongoing-operations.md +24 -32
  40. package/content/security/full-reference.md +30 -3163
  41. package/docs/ARCHITECTURE.md +31 -92
  42. package/docs/CONTENT_MAINTENANCE.md +5 -1
  43. package/docs/GITHUB_AND_NPM_SETUP.md +1 -1
  44. package/docs/RELEASE_PROCESS.md +5 -5
  45. package/package.json +12 -5
  46. package/scripts/build-site.mjs +13 -0
  47. package/scripts/build-skill.mjs +24 -0
  48. package/scripts/check-links.mjs +56 -0
  49. package/scripts/check-review-dates.mjs +21 -0
  50. package/scripts/preview-site.mjs +43 -0
  51. package/scripts/smoke-package.mjs +58 -0
  52. package/scripts/validate-content.mjs +85 -0
  53. package/site/app.js +121 -0
  54. package/site/index.html +346 -0
  55. package/site/styles.css +159 -0
  56. package/skills/manage-project-quality/SKILL.md +18 -2
  57. package/skills/manage-project-quality/assets/documentation-report.md +3 -3
  58. package/skills/manage-project-quality/assets/engineering-report.md +3 -3
  59. package/skills/manage-project-quality/assets/security-report.md +3 -3
  60. package/skills/manage-project-quality/references/documentation-01-system-documentation.md +13 -0
  61. package/skills/manage-project-quality/references/documentation-02-project-governance.md +13 -0
  62. package/skills/manage-project-quality/references/documentation-03-development-records.md +13 -0
  63. package/skills/manage-project-quality/references/documentation-04-testing-evidence.md +13 -0
  64. package/skills/manage-project-quality/references/documentation-05-operations-runbooks.md +13 -0
  65. package/skills/manage-project-quality/references/documentation-06-future-proposals.md +13 -0
  66. package/skills/manage-project-quality/references/documentation-07-archive-governance.md +13 -0
  67. package/skills/manage-project-quality/references/documentation-full-reference.md +24 -0
  68. package/skills/manage-project-quality/references/documentation-governance.md +2 -2
  69. package/skills/manage-project-quality/references/engineering-01-query-performance.md +17 -27
  70. package/skills/manage-project-quality/references/engineering-02-networking-fundamentals.md +17 -27
  71. package/skills/manage-project-quality/references/engineering-03-rate-limiting-algorithms.md +18 -28
  72. package/skills/manage-project-quality/references/engineering-04-production-debugging.md +17 -27
  73. package/skills/manage-project-quality/references/engineering-05-code-review-practices.md +17 -27
  74. package/skills/manage-project-quality/references/engineering-06-data-modeling.md +17 -27
  75. package/skills/manage-project-quality/references/engineering-07-cloud-cost-saving.md +17 -27
  76. package/skills/manage-project-quality/references/engineering-08-testing-types.md +17 -27
  77. package/skills/manage-project-quality/references/engineering-09-message-queues.md +17 -27
  78. package/skills/manage-project-quality/references/engineering-10-api-design-compatibility.md +14 -0
  79. package/skills/manage-project-quality/references/engineering-11-reliability-slos.md +14 -0
  80. package/skills/manage-project-quality/references/engineering-12-observability-telemetry.md +14 -0
  81. package/skills/manage-project-quality/references/engineering-13-deployment-change-safety.md +14 -0
  82. package/skills/manage-project-quality/references/engineering-14-capacity-concurrency.md +14 -0
  83. package/skills/manage-project-quality/references/engineering-full-reference.md +24 -144
  84. package/skills/manage-project-quality/references/engineering-protocol.md +1 -1
  85. package/skills/manage-project-quality/references/security-01-design-architecture.md +11 -19
  86. package/skills/manage-project-quality/references/security-02-local-development.md +13 -21
  87. package/skills/manage-project-quality/references/security-03-database-backend.md +18 -26
  88. package/skills/manage-project-quality/references/security-04-api-layer.md +23 -31
  89. package/skills/manage-project-quality/references/security-05-frontend-ui.md +16 -24
  90. package/skills/manage-project-quality/references/security-06-application-security-testing.md +22 -28
  91. package/skills/manage-project-quality/references/security-07-cicd-release-engineering.md +13 -21
  92. package/skills/manage-project-quality/references/security-08-cloud-infrastructure.md +17 -25
  93. package/skills/manage-project-quality/references/security-09-edge-cdn-dns.md +16 -24
  94. package/skills/manage-project-quality/references/security-10-release-rollout.md +13 -21
  95. package/skills/manage-project-quality/references/security-11-post-release-monitoring-ir.md +15 -23
  96. package/skills/manage-project-quality/references/security-12-ongoing-operations.md +24 -32
  97. package/skills/manage-project-quality/references/security-full-reference.md +30 -3163
  98. package/skills/manage-project-quality/scripts/check-report.mjs +67 -16
  99. package/src/api/index.mjs +3 -0
  100. package/src/cli/arguments.mjs +27 -3
  101. package/src/cli/help.mjs +37 -17
  102. package/src/cli/main.mjs +64 -10
  103. package/src/commands/audit.mjs +146 -31
  104. package/src/commands/doctor.mjs +1 -1
  105. package/src/commands/guide.mjs +12 -0
  106. package/src/commands/list.mjs +1 -1
  107. package/src/commands/setup.mjs +9 -4
  108. package/src/commands/skill.mjs +1 -0
  109. package/src/commands/template.mjs +32 -0
  110. package/src/commands/validate.mjs +36 -4
  111. package/src/core/config.mjs +61 -0
  112. package/src/core/files.mjs +47 -6
  113. package/templates/adr.md +20 -0
  114. package/templates/change-record.md +18 -0
  115. package/templates/incident-review.md +20 -0
  116. package/templates/runbook.md +18 -0
  117. package/templates/test-evidence.md +18 -0
  118. package/tests/cli.test.mjs +226 -0
  119. package/tests/content.test.mjs +18 -0
@@ -0,0 +1,32 @@
1
+ import { resolve } from "node:path";
2
+ import { loadProjectConfig } from "../core/config.mjs";
3
+ import { readTemplate, writeUnique } from "../core/files.mjs";
4
+ import { localDate } from "./audit.mjs";
5
+
6
+ export const templateTypes = Object.freeze({
7
+ adr: { file: "adr.md", directory: "02-project/decisions", label: "Architecture decision record" },
8
+ runbook: { file: "runbook.md", directory: "05-operations/runbooks", label: "Operational runbook" },
9
+ change: { file: "change-record.md", directory: "03-development/changes", label: "Change record" },
10
+ incident: { file: "incident-review.md", directory: "05-operations/incidents", label: "Blameless incident review" },
11
+ evidence: { file: "test-evidence.md", directory: "04-testing/evidence", label: "Test evidence record" }
12
+ });
13
+
14
+ export function listTemplates() {
15
+ for (const [type, value] of Object.entries(templateTypes)) console.log(`${type.padEnd(10)} ${value.label}`);
16
+ return templateTypes;
17
+ }
18
+
19
+ export async function createTemplate(type, options = {}) {
20
+ const selected = templateTypes[type];
21
+ if (!selected) throw new Error(`Unknown template '${type ?? ""}'. Run 'agent-docs template list'.`);
22
+ const { root, config } = await loadProjectConfig(options.cwd);
23
+ const date = localDate();
24
+ const due = new Date();
25
+ due.setDate(due.getDate() + 90);
26
+ const contents = await readTemplate(selected.file, { DATE: date, REVIEW_DUE: localDate(due), TITLE: selected.label });
27
+ const suffix = options.name ? `-${options.name}` : "";
28
+ const output = resolve(root, config.templateOutput, selected.directory, `${date}-${type}${suffix}.md`);
29
+ const result = await writeUnique(output, contents, options);
30
+ console.log(`${result.status} ${result.path}`);
31
+ return result;
32
+ }
@@ -1,20 +1,52 @@
1
- import { access, readFile } from "node:fs/promises";
1
+ import { access, readFile, readdir } from "node:fs/promises";
2
+ import { basename } from "node:path";
2
3
  import { loadCatalog } from "../core/catalog.mjs";
3
4
  import { resolveFromPackage } from "../core/paths.mjs";
4
5
 
5
6
  export async function validatePackage() {
6
7
  const catalog = await loadCatalog();
7
8
  const errors = [];
8
- for (const group of ["security", "engineering"]) {
9
+ const expectedGenerated = new Set();
10
+ for (const group of ["documentation", "security", "engineering"]) {
9
11
  for (const entry of catalog[group]) {
10
- try { await access(resolveFromPackage(entry.source)); }
12
+ let source;
13
+ let generated;
14
+ const generatedName = `${group}-${basename(entry.source)}`;
15
+ expectedGenerated.add(generatedName);
16
+ try {
17
+ source = await readFile(resolveFromPackage(entry.source), "utf8");
18
+ }
11
19
  catch { errors.push(`Missing catalog source: ${entry.source}`); }
20
+ try {
21
+ generated = await readFile(resolveFromPackage("skills/manage-project-quality/references", generatedName), "utf8");
22
+ }
23
+ catch { errors.push(`Missing generated skill reference: ${generatedName}`); }
24
+ if (source !== undefined && generated !== undefined && source !== generated) errors.push(`Stale generated skill reference: ${generatedName}`);
12
25
  }
26
+ const fullName = `${group}-full-reference.md`;
27
+ expectedGenerated.add(fullName);
28
+ try {
29
+ const source = await readFile(resolveFromPackage(`content/${group}/full-reference.md`), "utf8");
30
+ const generated = await readFile(resolveFromPackage("skills/manage-project-quality/references", fullName), "utf8");
31
+ if (source !== generated) errors.push(`Stale generated skill reference: ${fullName}`);
32
+ } catch { errors.push(`Missing ${group} full reference or generated copy.`); }
33
+ }
34
+ for (const name of await readdir(resolveFromPackage("skills/manage-project-quality/references"))) {
35
+ if (/^(?:documentation|security|engineering)-(?:\d{2}-.+|full-reference)\.md$/.test(name) && !expectedGenerated.has(name)) errors.push(`Unexpected generated skill reference: ${name}`);
36
+ }
37
+ if (catalog.schemaVersion !== 2) errors.push("Catalog schemaVersion must be 2.");
38
+ if (catalog.documentation.length !== 7) errors.push("Catalog must contain 7 documentation areas.");
39
+ if (catalog.security.length !== 12) errors.push("Catalog must contain 12 security phases.");
40
+ if (catalog.engineering.length !== 14) errors.push("Catalog must contain 14 engineering topics.");
41
+ for (const template of ["adr.md", "runbook.md", "change-record.md", "incident-review.md", "test-evidence.md"]) {
42
+ try { await access(resolveFromPackage("templates", template)); }
43
+ catch { errors.push(`Missing template: ${template}`); }
13
44
  }
14
45
  const skill = await readFile(resolveFromPackage("skills/manage-project-quality/SKILL.md"), "utf8");
15
46
  if (!skill.includes("name: manage-project-quality")) errors.push("Skill name is invalid.");
16
47
  if (errors.length) throw new Error(errors.join("\n"));
17
- console.log(`PASS catalog (${catalog.security.length} security phases, ${catalog.engineering.length} engineering topics)`);
48
+ console.log(`PASS catalog (${catalog.documentation.length} documentation areas, ${catalog.security.length} security phases, ${catalog.engineering.length} engineering topics)`);
49
+ console.log("PASS documentation templates");
18
50
  console.log("PASS unified skill");
19
51
  return true;
20
52
  }
@@ -0,0 +1,61 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { exists } from "./files.mjs";
4
+
5
+ export const defaultConfig = Object.freeze({
6
+ schemaVersion: 2,
7
+ documentationRoot: "docs",
8
+ documentationReports: "docs/03-development/audits",
9
+ securityReports: "docs/04-testing/security-audits",
10
+ engineeringReports: "docs/03-development/engineering-reviews",
11
+ templateOutput: "docs",
12
+ interactionStyle: "standard",
13
+ maxAgeDays: 180,
14
+ requireMetadata: false,
15
+ requiredDocuments: ["docs/README.md", "docs/01-system/overview.md", "docs/02-project/README.md", "docs/05-operations/README.md"]
16
+ });
17
+
18
+ const configKeys = new Set(Object.keys(defaultConfig));
19
+
20
+ async function nearestExistingPath(path) {
21
+ let current = path;
22
+ while (!(await exists(current))) {
23
+ const parent = dirname(current);
24
+ if (parent === current) return current;
25
+ current = parent;
26
+ }
27
+ return current;
28
+ }
29
+
30
+ async function safeRelativePath(root, value, label) {
31
+ if (typeof value !== "string" || !value.trim() || isAbsolute(value)) throw new Error(`${label} must be a non-empty project-relative path.`);
32
+ const resolved = resolve(root, value);
33
+ const rel = relative(root, resolved);
34
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error(`${label} must stay inside the target project.`);
35
+ const [realRoot, realAncestor] = await Promise.all([realpath(root), realpath(await nearestExistingPath(resolved))]);
36
+ const realRelative = relative(realRoot, realAncestor);
37
+ if (realRelative === ".." || realRelative.startsWith(`..${sep}`) || isAbsolute(realRelative)) throw new Error(`${label} resolves through a link outside the target project.`);
38
+ return value.replaceAll("\\", "/");
39
+ }
40
+
41
+ export async function loadProjectConfig(cwd = process.cwd()) {
42
+ const root = resolve(cwd);
43
+ if (!(await exists(root))) throw new Error(`Target project does not exist: ${root}`);
44
+ const configPath = resolve(root, ".agent-docs/config.json");
45
+ let user = {};
46
+ if (await exists(configPath)) {
47
+ try { user = JSON.parse(await readFile(configPath, "utf8")); }
48
+ catch (error) { throw new Error(`Invalid ${configPath}: ${error.message}`); }
49
+ }
50
+ if (!user || typeof user !== "object" || Array.isArray(user)) throw new Error(`${configPath} must contain a JSON object.`);
51
+ for (const key of Object.keys(user)) if (!configKeys.has(key)) throw new Error(`Unknown agent-docs configuration key '${key}'.`);
52
+ const config = { ...defaultConfig, ...user };
53
+ if (![1, 2].includes(config.schemaVersion)) throw new Error("Unsupported agent-docs configuration schemaVersion.");
54
+ if (!Number.isInteger(config.maxAgeDays) || config.maxAgeDays < 1) throw new Error("maxAgeDays must be a positive integer.");
55
+ if (typeof config.requireMetadata !== "boolean") throw new Error("requireMetadata must be a boolean.");
56
+ if (!["standard", "learning"].includes(config.interactionStyle)) throw new Error("interactionStyle must be standard or learning.");
57
+ if (!Array.isArray(config.requiredDocuments)) throw new Error("requiredDocuments must be an array.");
58
+ for (const key of ["documentationRoot", "documentationReports", "securityReports", "engineeringReports", "templateOutput"]) config[key] = await safeRelativePath(root, config[key], key);
59
+ config.requiredDocuments = await Promise.all(config.requiredDocuments.map((value, index) => safeRelativePath(root, value, `requiredDocuments[${index}]`)));
60
+ return { root, config, configPath };
61
+ }
@@ -1,5 +1,6 @@
1
- import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, relative, resolve } from "node:path";
1
+ import { access, cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname, extname, relative, resolve } from "node:path";
3
4
  import { resolveFromPackage } from "./paths.mjs";
4
5
 
5
6
  export async function exists(path) {
@@ -15,16 +16,56 @@ export async function writeSafe(path, contents, { force = false, dryRun = false
15
16
  if (!force && await exists(path)) return { path, status: "skipped" };
16
17
  if (!dryRun) {
17
18
  await mkdir(dirname(path), { recursive: true });
18
- await writeFile(path, contents, "utf8");
19
+ try { await writeFile(path, contents, { encoding: "utf8", flag: force ? "w" : "wx" }); }
20
+ catch (error) {
21
+ if (!force && error.code === "EEXIST") return { path, status: "skipped" };
22
+ throw error;
23
+ }
19
24
  }
20
25
  return { path, status: dryRun ? "planned" : "created" };
21
26
  }
22
27
 
28
+ export async function availablePath(base, { force = false } = {}) {
29
+ if (force || !(await exists(base))) return base;
30
+ const extension = extname(base);
31
+ const stem = extension ? base.slice(0, -extension.length) : base;
32
+ for (let index = 2; index < 1000; index += 1) {
33
+ const candidate = `${stem}-${index}${extension}`;
34
+ if (!(await exists(candidate))) return candidate;
35
+ }
36
+ throw new Error(`Unable to select an unused path for ${base}.`);
37
+ }
38
+
39
+ export async function writeUnique(base, contents, options = {}) {
40
+ if (options.force) return writeSafe(base, contents, options);
41
+ if (options.dryRun) return writeSafe(await availablePath(base), contents, options);
42
+ const extension = extname(base);
43
+ const stem = extension ? base.slice(0, -extension.length) : base;
44
+ for (let index = 1; index < 1000; index += 1) {
45
+ const candidate = index === 1 ? base : `${stem}-${index}${extension}`;
46
+ const result = await writeSafe(candidate, contents, options);
47
+ if (result.status === "created") return result;
48
+ }
49
+ throw new Error(`Unable to create an unused path for ${base}.`);
50
+ }
51
+
23
52
  export async function copySafe(source, destination, options = {}) {
24
53
  if (!options.force && await exists(destination)) return { path: destination, status: "skipped" };
25
- if (!options.dryRun) {
26
- await mkdir(dirname(destination), { recursive: true });
27
- await cp(source, destination, { recursive: true, force: Boolean(options.force) });
54
+ if (options.dryRun) return { path: destination, status: "planned" };
55
+ await mkdir(dirname(destination), { recursive: true });
56
+ const nonce = `${process.pid}-${Date.now()}-${randomUUID()}`;
57
+ const staged = `${destination}.agent-docs-stage-${nonce}`;
58
+ const backup = `${destination}.agent-docs-backup-${nonce}`;
59
+ await cp(source, staged, { recursive: true, force: false, errorOnExist: true });
60
+ const replacing = await exists(destination);
61
+ try {
62
+ if (replacing) await rename(destination, backup);
63
+ await rename(staged, destination);
64
+ if (replacing) await rm(backup, { recursive: true, force: true });
65
+ } catch (error) {
66
+ await rm(staged, { recursive: true, force: true });
67
+ if (replacing && await exists(backup) && !(await exists(destination))) await rename(backup, destination);
68
+ throw error;
28
69
  }
29
70
  return { path: destination, status: options.dryRun ? "planned" : "created" };
30
71
  }
@@ -0,0 +1,20 @@
1
+ ---
2
+ owner: unassigned
3
+ status: proposed
4
+ lastReviewed: {{DATE}}
5
+ reviewDue: {{REVIEW_DUE}}
6
+ ---
7
+
8
+ # {{TITLE}}
9
+
10
+ ## Context
11
+
12
+ Describe the problem, constraints, and decision drivers.
13
+
14
+ ## Decision
15
+
16
+ Record the chosen option and why it was selected.
17
+
18
+ ## Consequences
19
+
20
+ List benefits, costs, risks, and follow-up work.
@@ -0,0 +1,18 @@
1
+ ---
2
+ owner: unassigned
3
+ status: planned
4
+ lastReviewed: {{DATE}}
5
+ reviewDue: {{REVIEW_DUE}}
6
+ ---
7
+
8
+ # {{TITLE}}
9
+
10
+ ## Intent
11
+
12
+ ## Scope and dependencies
13
+
14
+ ## Validation plan
15
+
16
+ ## Deployment and rollback
17
+
18
+ ## Result
@@ -0,0 +1,20 @@
1
+ ---
2
+ owner: unassigned
3
+ status: draft
4
+ lastReviewed: {{DATE}}
5
+ reviewDue: {{REVIEW_DUE}}
6
+ ---
7
+
8
+ # {{TITLE}}
9
+
10
+ ## Summary and impact
11
+
12
+ ## Timeline
13
+
14
+ ## Contributing conditions
15
+
16
+ ## Detection and response
17
+
18
+ ## Corrective actions
19
+
20
+ Focus on system improvements, not individual blame.
@@ -0,0 +1,18 @@
1
+ ---
2
+ owner: unassigned
3
+ status: draft
4
+ lastReviewed: {{DATE}}
5
+ reviewDue: {{REVIEW_DUE}}
6
+ ---
7
+
8
+ # {{TITLE}}
9
+
10
+ ## Purpose and scope
11
+
12
+ ## Preconditions and access
13
+
14
+ ## Procedure
15
+
16
+ ## Verification
17
+
18
+ ## Rollback and escalation
@@ -0,0 +1,18 @@
1
+ ---
2
+ owner: unassigned
3
+ status: unverified
4
+ lastReviewed: {{DATE}}
5
+ reviewDue: {{REVIEW_DUE}}
6
+ ---
7
+
8
+ # {{TITLE}}
9
+
10
+ ## Claim under test
11
+
12
+ ## Environment and inputs
13
+
14
+ ## Procedure
15
+
16
+ ## Evidence
17
+
18
+ ## Result and limitations
@@ -0,0 +1,226 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { setupProject } from "../src/commands/setup.mjs";
7
+ import { auditSecurity, reviewEngineering } from "../src/commands/audit.mjs";
8
+ import { browserCommand, openWebsite, websiteUrl } from "../src/commands/website.mjs";
9
+ import { helpText } from "../src/cli/help.mjs";
10
+ import { parseArguments, optionsFromFlags } from "../src/cli/arguments.mjs";
11
+ import { auditDocumentation } from "../src/commands/audit.mjs";
12
+ import { createTemplate } from "../src/commands/template.mjs";
13
+ import { validateReport } from "../skills/manage-project-quality/scripts/check-report.mjs";
14
+ import { showDocumentationGuide } from "../src/commands/guide.mjs";
15
+ import { installSkill } from "../src/commands/skill.mjs";
16
+ import { main } from "../src/cli/main.mjs";
17
+
18
+ test("setup is explicit, non-destructive, and repeatable", async () => {
19
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
20
+ try {
21
+ const preview = await setupProject({ cwd: root, dryRun: true });
22
+ assert.ok(preview.every((item) => item.status === "planned"));
23
+ const created = await setupProject({ cwd: root });
24
+ assert.ok(created.every((item) => item.status === "created"));
25
+ const repeated = await setupProject({ cwd: root });
26
+ assert.ok(repeated.every((item) => item.status === "skipped"));
27
+ } finally {
28
+ await rm(root, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ test("security and engineering reports use distinct semantics", async () => {
33
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
34
+ try {
35
+ const security = await auditSecurity("04", { cwd: root });
36
+ const engineering = await reviewEngineering("08", { cwd: root });
37
+ assert.match(await readFile(security.path, "utf8"), /Security worksheet: API Layer/);
38
+ assert.match(await readFile(security.path, "utf8"), /Scope and authorization/);
39
+ assert.deepEqual(validateReport(await readFile(security.path, "utf8")), []);
40
+ const engineeringText = await readFile(engineering.path, "utf8");
41
+ assert.match(engineeringText, /Engineering worksheet: Testing Types/);
42
+ assert.match(engineeringText, /Review context/);
43
+ assert.doesNotMatch(engineeringText, /Authorization: _Required/);
44
+ assert.deepEqual(validateReport(engineeringText), []);
45
+ } finally {
46
+ await rm(root, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test("CLI reports inherit the configured interaction style when no flag is supplied", async () => {
51
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
52
+ try {
53
+ await mkdir(join(root, ".agent-docs"), { recursive: true });
54
+ await writeFile(join(root, ".agent-docs", "config.json"), JSON.stringify({ schemaVersion: 2, documentationRoot: "docs", documentationReports: "docs/audits", securityReports: "docs/security", engineeringReports: "docs/engineering", templateOutput: "docs", interactionStyle: "learning", maxAgeDays: 30, requireMetadata: false, requiredDocuments: [] }));
55
+ const result = await main(["security", "worksheet", "04", "--cwd", root]);
56
+ assert.match(await readFile(result.path, "utf8"), /- Style: learning/);
57
+ } finally { await rm(root, { recursive: true, force: true }); }
58
+ });
59
+
60
+ test("arguments reject unknown options, styles, and installation targets", () => {
61
+ assert.throws(() => parseArguments(["setup", "--mystery"]), /Unknown option/);
62
+ assert.throws(() => optionsFromFlags({ style: "verbose" }), /Invalid style/);
63
+ assert.throws(() => optionsFromFlags({ target: "elsewhere" }), /Invalid target/);
64
+ assert.deepEqual(parseArguments(["audit", "documentation", "--strict"]).positionals, ["audit", "documentation"]);
65
+ });
66
+
67
+ test("documentation audit consumes config and detects broken local links", async () => {
68
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
69
+ try {
70
+ await mkdir(join(root, ".agent-docs"), { recursive: true });
71
+ await mkdir(join(root, "knowledge"), { recursive: true });
72
+ await writeFile(join(root, ".agent-docs", "config.json"), JSON.stringify({ schemaVersion: 2, documentationRoot: "knowledge", documentationReports: "reports", securityReports: "reports/security", engineeringReports: "reports/engineering", templateOutput: "knowledge", maxAgeDays: 30, requireMetadata: false, requiredDocuments: ["knowledge/index.md"] }));
73
+ await writeFile(join(root, "knowledge", "index.md"), "# Index\n\n[Missing](missing.md)\n");
74
+ const result = await auditDocumentation({ cwd: root, format: "json" });
75
+ assert.equal(result.pass, false);
76
+ assert.ok(result.checks.some((row) => row.rule === "broken-link"));
77
+ } finally { await rm(root, { recursive: true, force: true }); }
78
+ });
79
+
80
+ test("templates are explicit and protect existing files", async () => {
81
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
82
+ try {
83
+ const first = await createTemplate("adr", { cwd: root });
84
+ const second = await createTemplate("adr", { cwd: root });
85
+ const named = await createTemplate("adr", { cwd: root, name: "cache-policy" });
86
+ assert.equal(first.status, "created");
87
+ assert.equal(second.status, "created");
88
+ assert.match(second.path, /-adr-2\.md$/);
89
+ assert.match(named.path, /-adr-cache-policy\.md$/);
90
+ assert.match(await readFile(first.path, "utf8"), /# Architecture decision record/);
91
+ } finally { await rm(root, { recursive: true, force: true }); }
92
+ });
93
+
94
+ test("documentation guidance is discoverable from the canonical catalog", async () => {
95
+ const result = await showDocumentationGuide("system");
96
+ assert.equal(result.entry.id, "01");
97
+ assert.match(result.contents, /DOC-01-01/);
98
+ assert.match(result.contents, /# System Documentation/);
99
+ });
100
+
101
+ test("report validation rejects unknown statuses and unsupported claims", () => {
102
+ assert.ok(validateReport("| Check | Status | Evidence | Owner | Verification |\n|---|---|---|---|---|\n| A | Definitely fine | proof | Sam | retest |").length > 0);
103
+ assert.ok(validateReport("| Check | Status | Evidence | Owner | Verification |\n|---|---|---|---|---|\n| A | Pass | | Unassigned | Not verified |").length >= 3);
104
+ assert.deepEqual(validateReport("| Check | Status | Evidence | Owner | Verification |\n|---|---|---|---|---|\n| A | Pass | test output | Sam | rerun test |") , []);
105
+ assert.deepEqual(validateReport("| Check | Status | Evidence | Owner | Verification |\n|---|---|---|---|---|\n| A | Pass | command \\| output | Sam | rerun test |") , []);
106
+ assert.ok(validateReport("| Control ID | Status | Evidence | Owner | Verification |\n|---|---|---|---|---|\n| SEC-01-01 | Applied | proof | Sam | retest |").some((error) => /invalid status/.test(error)));
107
+ assert.ok(validateReport("| Practice ID | Status | Evidence | Owner | Verification |\n|---|---|---|---|---|\n| ENG-01-01 | Pass | proof | Sam | retest |").some((error) => /invalid status/.test(error)));
108
+ });
109
+
110
+ test("documentation audit enforces metadata, same-document anchors, and exact report formats", async () => {
111
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
112
+ try {
113
+ await mkdir(join(root, ".agent-docs"), { recursive: true });
114
+ await mkdir(join(root, "docs"), { recursive: true });
115
+ await writeFile(join(root, ".agent-docs", "config.json"), JSON.stringify({ schemaVersion: 2, documentationRoot: "docs", documentationReports: "reports", securityReports: "reports/security", engineeringReports: "reports/engineering", templateOutput: "docs", interactionStyle: "standard", maxAgeDays: 30, requireMetadata: true, requiredDocuments: ["docs/index.md"] }));
116
+ await writeFile(join(root, "docs", "index.md"), "---\nowner: Docs\nstatus: current\nlastReviewed: 2026-99-99\n---\n# Index\n\n[Missing](#missing)\n\n```text\nTODO\n# Not a heading\n```\n");
117
+ const result = await auditDocumentation({ cwd: root, report: true, format: "text" });
118
+ assert.equal(result.pass, false);
119
+ assert.match(result.reportPath, /\.txt$/);
120
+ assert.ok(result.checks.some((row) => row.rule === "metadata" && /reviewDue/.test(row.detail)));
121
+ assert.ok(result.checks.some((row) => row.rule === "metadata-date"));
122
+ assert.ok(result.checks.some((row) => row.rule === "broken-anchor"));
123
+ assert.ok(!result.checks.some((row) => row.rule === "placeholder"));
124
+ } finally { await rm(root, { recursive: true, force: true }); }
125
+ });
126
+
127
+ test("dry runs select the same collision-safe report path as a real write", async () => {
128
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
129
+ try {
130
+ await auditSecurity("04", { cwd: root });
131
+ const preview = await auditSecurity("04", { cwd: root, dryRun: true });
132
+ const created = await auditSecurity("04", { cwd: root });
133
+ assert.equal(preview.status, "planned");
134
+ assert.equal(preview.path, created.path);
135
+ } finally { await rm(root, { recursive: true, force: true }); }
136
+ });
137
+
138
+ test("concurrent report creation is collision safe", async () => {
139
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
140
+ try {
141
+ const reports = await Promise.all(Array.from({ length: 8 }, () => auditSecurity("04", { cwd: root })));
142
+ assert.equal(new Set(reports.map((report) => report.path)).size, reports.length);
143
+ assert.ok(reports.every((report) => report.status === "created"));
144
+ } finally { await rm(root, { recursive: true, force: true }); }
145
+ });
146
+
147
+ test("malformed link encoding becomes an audit finding instead of a runtime error", async () => {
148
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
149
+ try {
150
+ await mkdir(join(root, ".agent-docs"), { recursive: true });
151
+ await mkdir(join(root, "docs"), { recursive: true });
152
+ await writeFile(join(root, ".agent-docs", "config.json"), JSON.stringify({ schemaVersion: 2, documentationRoot: "docs", documentationReports: "reports", securityReports: "reports/security", engineeringReports: "reports/engineering", templateOutput: "docs", interactionStyle: "standard", maxAgeDays: 30, requireMetadata: false, requiredDocuments: ["docs/index.md"] }));
153
+ await writeFile(join(root, "docs", "index.md"), "# Index\n\n[Malformed](bad%ZZ.md)\n");
154
+ const result = await auditDocumentation({ cwd: root });
155
+ assert.ok(result.checks.some((row) => row.rule === "invalid-link"));
156
+ } finally { await rm(root, { recursive: true, force: true }); }
157
+ });
158
+
159
+ test("forced skill installation removes stale owned files", async () => {
160
+ const root = await mkdtemp(join(tmpdir(), "agent-docs-kit-"));
161
+ try {
162
+ await installSkill("project", { cwd: root });
163
+ const stale = join(root, ".agents", "skills", "manage-project-quality", "references", "stale.md");
164
+ await writeFile(stale, "stale");
165
+ await installSkill("project", { cwd: root, force: true });
166
+ await assert.rejects(access(stale));
167
+ } finally { await rm(root, { recursive: true, force: true }); }
168
+ });
169
+
170
+ test("CLI rejects extra positional arguments and command-inapplicable options", async () => {
171
+ await assert.rejects(main(["doctor", "unexpected"]), /Invalid arguments/);
172
+ await assert.rejects(main(["website", "--report"]), /not valid here/);
173
+ await assert.rejects(main(["audit", "worksheet", "04"]), /Unknown command/);
174
+ await assert.rejects(main(["engineering", "engineering", "08"]), /Unknown command/);
175
+ assert.throws(() => optionsFromFlags({ name: "Not valid" }), /Invalid name/);
176
+ });
177
+
178
+ test("website command opens the documented URL without a shell dependency", () => {
179
+ let invocation;
180
+ const child = { once() {}, unref() {} };
181
+ const returned = openWebsite({
182
+ platform: "darwin",
183
+ spawnProcess(executable, arguments_, options) {
184
+ invocation = { executable, arguments_, options };
185
+ return child;
186
+ }
187
+ });
188
+ assert.equal(returned, websiteUrl);
189
+ assert.equal(invocation.executable, "open");
190
+ assert.deepEqual(invocation.arguments_, [websiteUrl]);
191
+ assert.equal(invocation.options.detached, true);
192
+ assert.equal(browserCommand("win32").executable, "cmd.exe");
193
+ assert.equal(browserCommand("linux").executable, "xdg-open");
194
+ });
195
+
196
+ test("help distinguishes no-install, local, and global command forms", () => {
197
+ assert.match(helpText, /npx @ghostbyte\/agent-docs-kit@latest \[command\]/);
198
+ assert.match(helpText, /npx agent-docs \[command\]/);
199
+ assert.match(helpText, /agent-docs \[command\]/);
200
+ assert.match(helpText, /agent-docs --version/);
201
+ assert.match(helpText, /npm view @ghostbyte\/agent-docs-kit version/);
202
+ assert.match(helpText, /npm install --global @ghostbyte\/agent-docs-kit@latest/);
203
+ });
204
+
205
+ test("website provides a complete installation guide without a GitHub header button", async () => {
206
+ const site = await readFile(new URL("../site/index.html", import.meta.url), "utf8");
207
+ assert.match(site, /Installation guide/);
208
+ assert.match(site, /node --version/);
209
+ assert.match(site, /Run without installing/);
210
+ assert.match(site, /Install the global command/);
211
+ assert.match(site, /<dt>33<\/dt><dd>Review areas<\/dd>/);
212
+ assert.match(site, /documentation guide/);
213
+ assert.match(site, /role="tab" aria-selected="true" aria-controls="code-panel-setup"/);
214
+ assert.match(site, /If <code>agent-docs<\/code> is not recognized/);
215
+ assert.doesNotMatch(site, /View on GitHub/);
216
+ assert.doesNotMatch(site, /git clone https:\/\/github\.com\/ghostbyte1014\/Agent_Docs_Kit\.git/);
217
+ assert.doesNotMatch(site, /Run the repository source/);
218
+
219
+ const commandOverview = site.indexOf('id="commands"');
220
+ const installationGuide = site.indexOf('id="quick-start"');
221
+ const packageContents = site.indexOf('id="contents"');
222
+ const cliReference = site.indexOf('id="command-reference"');
223
+ assert.ok(commandOverview < installationGuide, "installation should follow the command-mode overview");
224
+ assert.ok(installationGuide < packageContents, "package details should follow installation");
225
+ assert.ok(packageContents < cliReference, "the complete CLI reference should be the final main section");
226
+ });
@@ -0,0 +1,18 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { loadCatalog } from "../src/core/catalog.mjs";
5
+ import { resolveFromPackage } from "../src/core/paths.mjs";
6
+
7
+ test("catalog exposes all migrated review areas", async () => {
8
+ const catalog = await loadCatalog();
9
+ assert.equal(catalog.documentation.length, 7);
10
+ assert.equal(catalog.security.length, 12);
11
+ assert.equal(catalog.engineering.length, 14);
12
+ for (const entry of [...catalog.documentation, ...catalog.security, ...catalog.engineering]) {
13
+ await access(resolveFromPackage(entry.source));
14
+ const value = await readFile(resolveFromPackage(entry.source), "utf8");
15
+ assert.match(value, /^# /);
16
+ assert.equal((value.match(/^#\s+/gm) ?? []).length, 1);
17
+ }
18
+ });