dxcomplete 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.
Files changed (121) hide show
  1. package/.env.example +11 -0
  2. package/README.md +215 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +212 -0
  5. package/dist/http/server.d.ts +7 -0
  6. package/dist/http/server.js +236 -0
  7. package/dist/http/service.d.ts +7 -0
  8. package/dist/http/service.js +725 -0
  9. package/dist/init.d.ts +13 -0
  10. package/dist/init.js +128 -0
  11. package/dist/install-manifest.d.ts +25 -0
  12. package/dist/install-manifest.js +96 -0
  13. package/dist/mcp/docs.d.ts +98 -0
  14. package/dist/mcp/docs.js +438 -0
  15. package/dist/mcp/server.d.ts +20 -0
  16. package/dist/mcp/server.js +2345 -0
  17. package/dist/package-root.d.ts +2 -0
  18. package/dist/package-root.js +28 -0
  19. package/dist/runtime/actor.d.ts +14 -0
  20. package/dist/runtime/actor.js +42 -0
  21. package/dist/runtime/auth.d.ts +162 -0
  22. package/dist/runtime/auth.js +394 -0
  23. package/dist/runtime/check.d.ts +7 -0
  24. package/dist/runtime/check.js +16 -0
  25. package/dist/runtime/config.d.ts +17 -0
  26. package/dist/runtime/config.js +93 -0
  27. package/dist/runtime/mongo.d.ts +9 -0
  28. package/dist/runtime/mongo.js +56 -0
  29. package/dist/runtime/records.d.ts +336 -0
  30. package/dist/runtime/records.js +1463 -0
  31. package/dist/runtime/workspace.d.ts +19 -0
  32. package/dist/runtime/workspace.js +102 -0
  33. package/dist/upgrade.d.ts +20 -0
  34. package/dist/upgrade.js +246 -0
  35. package/dist/validate.d.ts +10 -0
  36. package/dist/validate.js +119 -0
  37. package/dist/version.d.ts +3 -0
  38. package/dist/version.js +12 -0
  39. package/docs/codex-integration.md +29 -0
  40. package/docs/cost-model.md +61 -0
  41. package/docs/decision-basis.md +57 -0
  42. package/docs/diagrams.md +31 -0
  43. package/docs/glossary.md +147 -0
  44. package/docs/index.md +60 -0
  45. package/docs/model.md +110 -0
  46. package/docs/open-questions.md +61 -0
  47. package/docs/roles.md +42 -0
  48. package/docs/taxonomy.md +96 -0
  49. package/docs/workflows.md +60 -0
  50. package/package.json +62 -0
  51. package/scripts/check-env-surface.mjs +136 -0
  52. package/scripts/check-public-copy.mjs +263 -0
  53. package/scripts/check-service-boundary.mjs +63 -0
  54. package/scripts/dogfood-work-order.mjs +506 -0
  55. package/scripts/smoke-mcp-http.mjs +3572 -0
  56. package/src/cli.ts +268 -0
  57. package/src/http/server.ts +314 -0
  58. package/src/http/service.ts +934 -0
  59. package/src/init.ts +227 -0
  60. package/src/install-manifest.ts +144 -0
  61. package/src/mcp/docs.ts +557 -0
  62. package/src/mcp/server.ts +3525 -0
  63. package/src/package-root.ts +31 -0
  64. package/src/runtime/actor.ts +61 -0
  65. package/src/runtime/auth.ts +673 -0
  66. package/src/runtime/check.ts +18 -0
  67. package/src/runtime/config.ts +128 -0
  68. package/src/runtime/mongo.ts +89 -0
  69. package/src/runtime/records.ts +2303 -0
  70. package/src/runtime/workspace.ts +155 -0
  71. package/src/upgrade.ts +356 -0
  72. package/src/validate.ts +139 -0
  73. package/src/version.ts +16 -0
  74. package/templates/github/workflows/dxcomplete.yml +16 -0
  75. package/templates/next/pages/api/auth/callback/google.js +12 -0
  76. package/templates/next/pages/api/dxcomplete/[...path].js +12 -0
  77. package/templates/next/pages/api/dxcomplete.js +12 -0
  78. package/templates/next/pages/api/mcp.js +12 -0
  79. package/templates/next/vercel.json +18 -0
  80. package/templates/process/README.md +38 -0
  81. package/templates/process/controls.yml +113 -0
  82. package/templates/process/cost-model.yml +71 -0
  83. package/templates/process/decision-basis.yml +53 -0
  84. package/templates/process/decisions/.gitkeep +1 -0
  85. package/templates/process/diagrams/00-decision-basis.mmd +24 -0
  86. package/templates/process/diagrams/00-overview.mmd +20 -0
  87. package/templates/process/diagrams/01-intake-triage.mmd +20 -0
  88. package/templates/process/diagrams/02-product-definition.mmd +14 -0
  89. package/templates/process/diagrams/03-engineering-execution.mmd +15 -0
  90. package/templates/process/diagrams/04-qa-verification.mmd +12 -0
  91. package/templates/process/diagrams/05-product-validation.mmd +12 -0
  92. package/templates/process/diagrams/06-change-release-control.mmd +16 -0
  93. package/templates/process/diagrams/07-deployment-operations.mmd +16 -0
  94. package/templates/process/diagrams/08-support-incident-management.mmd +16 -0
  95. package/templates/process/diagrams/09-problem-improvement.mmd +14 -0
  96. package/templates/process/diagrams/10-risk-control-management.mmd +14 -0
  97. package/templates/process/diagrams/11-audit-evidence-capture.mmd +13 -0
  98. package/templates/process/evidence/.gitkeep +1 -0
  99. package/templates/process/risks/.gitkeep +1 -0
  100. package/templates/process/roles.yml +96 -0
  101. package/templates/process/taxonomy.yml +514 -0
  102. package/templates/process/workflows.yml +210 -0
  103. package/website/.well-known/oauth-authorization-server +22 -0
  104. package/website/.well-known/oauth-protected-resource/api/dxcomplete/mcp +10 -0
  105. package/website/.well-known/oauth-protected-resource/api/mcp +10 -0
  106. package/website/README.md +12 -0
  107. package/website/app.js +36 -0
  108. package/website/flow.html +85 -0
  109. package/website/glossary.html +280 -0
  110. package/website/index.html +90 -0
  111. package/website/objects.html +287 -0
  112. package/website/outcomes.html +117 -0
  113. package/website/phase-build.html +101 -0
  114. package/website/phase-elicit.html +102 -0
  115. package/website/phase-go-live.html +103 -0
  116. package/website/phase-measure.html +93 -0
  117. package/website/phase-operate.html +102 -0
  118. package/website/phase-orient.html +92 -0
  119. package/website/phase-weigh.html +98 -0
  120. package/website/roles.html +52 -0
  121. package/website/styles.css +1169 -0
@@ -0,0 +1,60 @@
1
+ # Draft Workflows
2
+
3
+ These workflows are editable drafts. They describe likely lifecycle paths without declaring them final.
4
+
5
+ ## Current Workflow Areas
6
+
7
+ - Intake and triage
8
+ - Decision basis
9
+ - Statement and expectation capture
10
+ - Requirement elicitation
11
+ - Current-state cost context attempt
12
+ - Itemized cost estimate
13
+ - Benefits
14
+ - Weigh outcome
15
+ - Product definition
16
+ - Engineering execution
17
+ - QA verification
18
+ - Product validation
19
+ - Change and release control
20
+ - Deployment and operations
21
+ - Operational Registry maintenance
22
+ - Support and incident management
23
+ - Problem and improvement management
24
+ - Risk and control management
25
+ - Audit and evidence capture
26
+ - Actual cost / benefit observations
27
+ - Estimate refinement
28
+
29
+ ## Draft End-To-End Flow
30
+
31
+ 1. A signal enters through feedback, authoritative request, support ticket, incident, or strategic direction.
32
+ 2. Statement capture preserves the user's own words and links the work to the Workspace context.
33
+ 3. Orient captures statement, confirms wording before recording, and restates expectations with approval where needed.
34
+ 4. Elicitation translates expectations into requirements, dependencies, constraints, and unknowns.
35
+ 5. Engineer review notes can be added to expectations or requirements when input should stay visible.
36
+ 6. Current-state cost context is attempted. It may be complete, partial, unavailable, or undisclosed.
37
+ 7. An itemized cost Estimate is generated from the elicited requirement set where cost reasoning is needed.
38
+ 8. Benefits are captured where useful, including qualitative benefits when amounts are not available.
39
+ 9. A decision basis is prepared for Weigh, including important review notes where present, the Estimate and Benefits where considered, and any Decision entries or linked inputs that informed the outcome.
40
+ 10. Owner records a Commitment or a Deferral. If committing, Owner and Engineer move the committed requirement set into build planning. If deferring, the unmet conditions remain visible.
41
+ 11. Engineer implements tasks and may use Codex assistance where appropriate.
42
+ 12. Tester verifies completed work against requirements and acceptance criteria.
43
+ 13. Owner validates whether the result is the right outcome.
44
+ 14. Change and release control records the service change plan, execution steps, rollback path, notice, vetoes, emergency posture, and result.
45
+ 15. Operator carries out the change in external operational tooling, then monitors and runs the service.
46
+ 16. Environment and Component records are kept current when the operational inventory changes.
47
+ 17. Support Agent handles user-facing issues and routes signals back into Owner or Operator follow-up.
48
+ 18. Actual cost / benefit observations are captured where available.
49
+ 19. Audit evidence is captured across decisions, controls, releases, deployments, measurement, and verification.
50
+
51
+ ## Workflow Questions
52
+
53
+ - Which steps require explicit approval?
54
+ - Which steps can be combined for low-risk changes?
55
+ - What cost visibility is enough to proceed when baseline data is unavailable?
56
+ - How should proceeding past an open checkpoint be shown when the risk remains open?
57
+ - What evidence is required before release?
58
+ - What notice, veto, and emergency evidence is enough for a Change record?
59
+ - What operational inventory belongs in Environment and Component records rather than Journal?
60
+ - What is the smallest useful workflow for a solo project?
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "dxcomplete",
3
+ "version": "0.1.0",
4
+ "description": "Reusable DX Complete / Complete Engineering documentation and scaffold kit.",
5
+ "type": "module",
6
+ "bin": {
7
+ "dxcomplete": "dist/cli.js"
8
+ },
9
+ "exports": {
10
+ "./http": {
11
+ "types": "./dist/http/server.d.ts",
12
+ "default": "./dist/http/server.js"
13
+ },
14
+ "./service": {
15
+ "types": "./dist/http/service.d.ts",
16
+ "default": "./dist/http/service.js"
17
+ }
18
+ },
19
+ "files": [
20
+ ".env.example",
21
+ "dist",
22
+ "docs",
23
+ "scripts",
24
+ "src",
25
+ "templates",
26
+ "website",
27
+ "README.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "check": "tsc --noEmit -p tsconfig.json && node scripts/check-public-copy.mjs && node scripts/check-env-surface.mjs && node scripts/check-service-boundary.mjs",
32
+ "check:public-copy": "node scripts/check-public-copy.mjs",
33
+ "check:env-surface": "node scripts/check-env-surface.mjs",
34
+ "check:service-boundary": "node scripts/check-service-boundary.mjs",
35
+ "smoke:mcp:http": "node scripts/smoke-mcp-http.mjs --depth=light",
36
+ "smoke:mcp:http:light": "node scripts/smoke-mcp-http.mjs --depth=light",
37
+ "smoke:mcp:http:deep": "node scripts/smoke-mcp-http.mjs --depth=deep",
38
+ "smoke:mcp:http:full": "node scripts/smoke-mcp-http.mjs --depth=deep",
39
+ "smoke:mcp:http:surface": "node scripts/smoke-mcp-http.mjs --area=surface",
40
+ "smoke:mcp:http:docs": "node scripts/smoke-mcp-http.mjs --area=docs",
41
+ "smoke:mcp:http:records": "node scripts/smoke-mcp-http.mjs --area=records",
42
+ "smoke:mcp:http:weigh": "node scripts/smoke-mcp-http.mjs --area=weigh",
43
+ "smoke:mcp:http:change": "node scripts/smoke-mcp-http.mjs --area=change",
44
+ "smoke:mcp:http:tickets": "node scripts/smoke-mcp-http.mjs --area=tickets",
45
+ "smoke:mcp:http:journal": "node scripts/smoke-mcp-http.mjs --area=journal",
46
+ "dogfood:work-order": "node scripts/dogfood-work-order.mjs",
47
+ "validate:package": "node dist/cli.js validate --target . --package-layout",
48
+ "runtime:check": "node dist/cli.js check-runtime"
49
+ },
50
+ "engines": {
51
+ "node": "22.x"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.17.0",
55
+ "typescript": "^5.8.0"
56
+ },
57
+ "dependencies": {
58
+ "@modelcontextprotocol/sdk": "^1.29.0",
59
+ "mongodb": "^7.2.0",
60
+ "zod": "^4.4.3"
61
+ }
62
+ }
@@ -0,0 +1,136 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
+
7
+ const allowedEnvVars = new Map([
8
+ ["DXC_MONGODB_URI", "secret"],
9
+ ["DXC_DATABASE_NAME", "environment-specific value"],
10
+ ["DXC_GOOGLE_CLIENT_ID", "provisioning detail"],
11
+ ["DXC_GOOGLE_CLIENT_SECRET", "secret"],
12
+ ["DXC_SERVICE_PROVISIONING_SECRET", "secret"],
13
+ ["DXC_SERVICE_URL", "environment-specific value"],
14
+ ["DXC_SERVICE_CLIENT_ID", "provisioning detail"],
15
+ ["DXC_SERVICE_CLIENT_SECRET", "secret"]
16
+ ]);
17
+
18
+ const allowedClassifications = new Set([
19
+ "secret",
20
+ "provisioning detail",
21
+ "environment-specific value"
22
+ ]);
23
+
24
+ const scanTargets = [
25
+ ".env.example",
26
+ "README.md",
27
+ "AGENTS.md",
28
+ "package.json",
29
+ "api",
30
+ "scripts",
31
+ "src",
32
+ "dist",
33
+ "docs",
34
+ "templates",
35
+ "website"
36
+ ];
37
+
38
+ const docEnvPattern = /\b[A-Z][A-Z0-9]+(?:_[A-Z0-9]+)+\b/g;
39
+ const quotedEnvPattern = /["'`]([A-Z][A-Z0-9]+(?:_[A-Z0-9]+)+)["'`]/g;
40
+ const failures = [];
41
+ const references = new Map();
42
+
43
+ for (const [name, classification] of allowedEnvVars) {
44
+ if (!allowedClassifications.has(classification)) {
45
+ failures.push(`${name}: invalid env classification "${classification}".`);
46
+ }
47
+ }
48
+
49
+ for (const target of scanTargets) {
50
+ const targetPath = path.join(rootDir, target);
51
+ if (!existsSync(targetPath)) continue;
52
+
53
+ for (const filePath of listFiles(targetPath)) {
54
+ scanFile(filePath);
55
+ }
56
+ }
57
+
58
+ for (const [name, locations] of references) {
59
+ if (!allowedEnvVars.has(name)) {
60
+ failures.push(
61
+ `${name}: referenced but not allowed. Add it to scripts/check-env-surface.mjs with classification secret, provisioning detail, or environment-specific value, or remove it. First seen at ${locations[0]}.`
62
+ );
63
+ }
64
+ }
65
+
66
+ if (failures.length > 0) {
67
+ console.error("Environment surface check failed:");
68
+ for (const failure of failures) {
69
+ console.error(`- ${failure}`);
70
+ }
71
+ process.exit(1);
72
+ }
73
+
74
+ console.log("Environment surface check passed.");
75
+
76
+ function listFiles(targetPath) {
77
+ const stats = statSync(targetPath);
78
+ if (stats.isFile()) {
79
+ return shouldScan(targetPath) ? [targetPath] : [];
80
+ }
81
+
82
+ const files = [];
83
+ for (const entry of readdirSync(targetPath)) {
84
+ if (entry === "node_modules" || entry === ".git") continue;
85
+ files.push(...listFiles(path.join(targetPath, entry)));
86
+ }
87
+ return files;
88
+ }
89
+
90
+ function shouldScan(filePath) {
91
+ const relativePath = path.relative(rootDir, filePath);
92
+ if (relativePath === "package-lock.json") return false;
93
+
94
+ return [
95
+ ".example",
96
+ ".html",
97
+ ".js",
98
+ ".json",
99
+ ".md",
100
+ ".mjs",
101
+ ".ts",
102
+ ".yml",
103
+ ".yaml"
104
+ ].some((extension) => filePath.endsWith(extension));
105
+ }
106
+
107
+ function scanFile(filePath) {
108
+ const relativePath = path.relative(rootDir, filePath);
109
+ const content = readFileSync(filePath, "utf8");
110
+
111
+ if (relativePath === ".env.example") {
112
+ for (const [index, line] of content.split(/\r?\n/).entries()) {
113
+ const trimmed = line.trim();
114
+ if (!trimmed || trimmed.startsWith("#")) continue;
115
+ const key = trimmed.split("=", 1)[0]?.trim();
116
+ if (key) addReference(key, `${relativePath}:${index + 1}`);
117
+ }
118
+ return;
119
+ }
120
+
121
+ const isSourceLike = /\.(?:js|json|mjs|ts)$/.test(filePath);
122
+ const pattern = isSourceLike ? quotedEnvPattern : docEnvPattern;
123
+ pattern.lastIndex = 0;
124
+
125
+ for (const match of content.matchAll(pattern)) {
126
+ const name = isSourceLike ? match[1] : match[0];
127
+ addReference(name, relativePath);
128
+ }
129
+ }
130
+
131
+ function addReference(name, location) {
132
+ if (!references.has(name)) {
133
+ references.set(name, []);
134
+ }
135
+ references.get(name).push(location);
136
+ }
@@ -0,0 +1,263 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
+ const websiteDir = path.join(rootDir, "website");
7
+
8
+ const forbiddenPatterns = [
9
+ { pattern: /\bMermaid\b/i, reason: "Mermaid/source details are not public-facing copy." },
10
+ { pattern: /\bflowchart\b/i, reason: "Diagram source terms are not public-facing copy." },
11
+ { pattern: /\bdiagram source\b/i, reason: "Source labels are not public-facing copy." },
12
+ { pattern: /\bCLI\b/, reason: "Command-line internals do not belong in public website copy." },
13
+ { pattern: /\bMCP\b/, reason: "MCP internals do not belong in public website copy." },
14
+ { pattern: /\bMongoDB\b/, reason: "Storage internals do not belong in public website copy." },
15
+ { pattern: /\bnpx\b/i, reason: "Install command details do not belong in public website copy." },
16
+ { pattern: /\bcommand-line\b/i, reason: "Command-line internals do not belong in public website copy." },
17
+ { pattern: /\bbootstrap\b/i, reason: "Implementation language does not belong in public website copy." },
18
+ { pattern: /\bscaffold(?:ing)?\b/i, reason: "Scaffold language is maintainer-facing." },
19
+ { pattern: /\btemplates?\b/i, reason: "Template language is maintainer-facing." },
20
+ { pattern: /\bpackage\b/i, reason: "Package details are maintainer-facing." },
21
+ { pattern: /\bimplementation\b/i, reason: "Implementation mechanics are maintainer-facing." },
22
+ { pattern: /\bconfiguration\b/i, reason: "Configuration mechanics are maintainer-facing." },
23
+ { pattern: /\bdeveloper\b/i, reason: "Developer-facing language should stay out of public pages." },
24
+ { pattern: /\binternal\b/i, reason: "Internal-facing language should stay out of public pages." },
25
+ { pattern: /\bTODO\b/i, reason: "TODOs should not appear in public website copy." },
26
+ { pattern: /\bdraft\b/i, reason: "Draft labels should not appear in public website copy." },
27
+ { pattern: /\bplaceholder\b/i, reason: "Placeholder labels should not appear in public website copy." }
28
+ ];
29
+
30
+ const htmlFiles = readdirSync(websiteDir)
31
+ .filter((file) => file.endsWith(".html"))
32
+ .sort();
33
+
34
+ const jsFiles = readdirSync(websiteDir)
35
+ .filter((file) => file.endsWith(".js"))
36
+ .sort();
37
+
38
+ const mdFiles = readdirSync(websiteDir)
39
+ .filter((file) => file.endsWith(".md"))
40
+ .sort();
41
+
42
+ const failures = [];
43
+ const alphabet = "abcdefghijklmnopqrstuvwxyz".split("");
44
+
45
+ function visibleTextFromHtml(html) {
46
+ return html
47
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
48
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
49
+ .replace(/<!--[\s\S]*?-->/g, " ")
50
+ .replace(/<[^>]+>/g, " ")
51
+ .replace(/&nbsp;/g, " ")
52
+ .replace(/&amp;/g, "&")
53
+ .replace(/&lt;/g, "<")
54
+ .replace(/&gt;/g, ">")
55
+ .replace(/&quot;/g, '"')
56
+ .replace(/&#39;/g, "'")
57
+ .replace(/\s+/g, " ")
58
+ .trim();
59
+ }
60
+
61
+ function addCopyFailures(file, text) {
62
+ for (const { pattern, reason } of forbiddenPatterns) {
63
+ const match = text.match(pattern);
64
+ if (match) {
65
+ failures.push({
66
+ file,
67
+ reason,
68
+ detail: `matched "${match[0]}"`
69
+ });
70
+ }
71
+ }
72
+ }
73
+
74
+ function addBrokenLinkFailures(file, html) {
75
+ const linkPattern = /href=["']\.\/([^"'#?]+\.html)(?:[?#][^"']*)?["']/g;
76
+ for (const match of html.matchAll(linkPattern)) {
77
+ const linkedFile = match[1];
78
+ const linkedPath = path.join(websiteDir, linkedFile);
79
+ if (!existsSync(linkedPath)) {
80
+ failures.push({
81
+ file,
82
+ reason: "Local website link points to a missing page.",
83
+ detail: linkedFile
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ function plainText(html) {
90
+ return html
91
+ .replace(/<[^>]+>/g, " ")
92
+ .replace(/&nbsp;/g, " ")
93
+ .replace(/&amp;/g, "&")
94
+ .replace(/&lt;/g, "<")
95
+ .replace(/&gt;/g, ">")
96
+ .replace(/&quot;/g, '"')
97
+ .replace(/&#39;/g, "'")
98
+ .replace(/\s+/g, " ")
99
+ .trim();
100
+ }
101
+
102
+ function normalizeTerm(term) {
103
+ return term.toLowerCase().replace(/\s+/g, " ").trim();
104
+ }
105
+
106
+ function compareTerms(left, right) {
107
+ return left.localeCompare(right, "en", { sensitivity: "base" });
108
+ }
109
+
110
+ function addGlossaryFailures() {
111
+ const glossaryFile = "glossary.html";
112
+ const glossarySource = readFileSync(path.join(websiteDir, glossaryFile), "utf8");
113
+ const sectionPattern = /<section id="([a-z])" class="glossary-letter">([\s\S]*?)<\/section>/g;
114
+ const sections = [...glossarySource.matchAll(sectionPattern)].map((match) => ({
115
+ id: match[1],
116
+ body: match[2]
117
+ }));
118
+ const sectionIds = sections.map((section) => section.id);
119
+ const expectedSectionIds = [...sectionIds].sort();
120
+
121
+ if (sectionIds.join("") !== expectedSectionIds.join("")) {
122
+ failures.push({
123
+ file: `website/${glossaryFile}`,
124
+ reason: "Glossary letter sections are not in alphabetical order.",
125
+ detail: sectionIds.join(", ")
126
+ });
127
+ }
128
+
129
+ const terms = [];
130
+ for (const section of sections) {
131
+ const sectionTerms = [...section.body.matchAll(/<dt>([\s\S]*?)<span>/g)]
132
+ .map((match) => plainText(match[1]));
133
+ const expectedTerms = [...sectionTerms].sort(compareTerms);
134
+
135
+ if (sectionTerms.join("||") !== expectedTerms.join("||")) {
136
+ failures.push({
137
+ file: `website/${glossaryFile}`,
138
+ reason: `Glossary terms under ${section.id.toUpperCase()} are not alphabetical.`,
139
+ detail: sectionTerms.join(", ")
140
+ });
141
+ }
142
+
143
+ for (const term of sectionTerms) {
144
+ if (term.charAt(0).toLowerCase() !== section.id) {
145
+ failures.push({
146
+ file: `website/${glossaryFile}`,
147
+ reason: "Glossary term appears under the wrong letter.",
148
+ detail: `${term} under ${section.id.toUpperCase()}`
149
+ });
150
+ }
151
+ terms.push(term);
152
+ }
153
+ }
154
+
155
+ const normalizedTerms = terms.map(normalizeTerm);
156
+ const duplicateTerms = normalizedTerms.filter((term, index) => normalizedTerms.indexOf(term) !== index);
157
+ for (const term of [...new Set(duplicateTerms)]) {
158
+ failures.push({
159
+ file: `website/${glossaryFile}`,
160
+ reason: "Glossary contains a duplicate term.",
161
+ detail: term
162
+ });
163
+ }
164
+
165
+ const linkLetters = [...glossarySource.matchAll(/<a href="#([a-z])">[A-Z]<\/a>/g)].map((match) => match[1]);
166
+ const disabledLetters = [...glossarySource.matchAll(/<span aria-disabled="true">([A-Z])<\/span>/g)].map((match) => match[1].toLowerCase());
167
+ const indexLetters = [...linkLetters, ...disabledLetters].sort();
168
+
169
+ if (indexLetters.join("") !== alphabet.join("")) {
170
+ failures.push({
171
+ file: `website/${glossaryFile}`,
172
+ reason: "Glossary A-Z index must include every letter exactly once.",
173
+ detail: indexLetters.join("")
174
+ });
175
+ }
176
+
177
+ for (const sectionId of sectionIds) {
178
+ if (!linkLetters.includes(sectionId)) {
179
+ failures.push({
180
+ file: `website/${glossaryFile}`,
181
+ reason: "Glossary A-Z index is missing a link for a populated letter.",
182
+ detail: sectionId.toUpperCase()
183
+ });
184
+ }
185
+ }
186
+
187
+ for (const disabledLetter of disabledLetters) {
188
+ if (sectionIds.includes(disabledLetter)) {
189
+ failures.push({
190
+ file: `website/${glossaryFile}`,
191
+ reason: "Glossary A-Z index disables a populated letter.",
192
+ detail: disabledLetter.toUpperCase()
193
+ });
194
+ }
195
+ }
196
+
197
+ const glossarySet = new Set(normalizedTerms);
198
+ for (const requiredTerm of publicTermsThatNeedGlossaryEntries()) {
199
+ if (!glossarySet.has(normalizeTerm(requiredTerm))) {
200
+ failures.push({
201
+ file: `website/${glossaryFile}`,
202
+ reason: "Glossary is missing a term used by the public website.",
203
+ detail: requiredTerm
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ function publicTermsThatNeedGlossaryEntries() {
210
+ const terms = new Set();
211
+
212
+ const recordsSource = readFileSync(path.join(websiteDir, "objects.html"), "utf8");
213
+ for (const match of recordsSource.matchAll(/<span class="record-name">([\s\S]*?)<\/span>/g)) {
214
+ terms.add(plainText(match[1]));
215
+ }
216
+ for (const match of recordsSource.matchAll(/<dt>([\s\S]*?)<\/dt>/g)) {
217
+ terms.add(plainText(match[1]));
218
+ }
219
+
220
+ for (const file of htmlFiles.filter((file) => file.startsWith("phase-"))) {
221
+ const source = readFileSync(path.join(websiteDir, file), "utf8");
222
+ const recordsMatch = source.match(/<h2>Records<\/h2>[\s\S]*?<ul>([\s\S]*?)<\/ul>/);
223
+ if (!recordsMatch) continue;
224
+ for (const match of recordsMatch[1].matchAll(/<li>([\s\S]*?)<\/li>/g)) {
225
+ terms.add(plainText(match[1]));
226
+ }
227
+ }
228
+
229
+ const rolesSource = readFileSync(path.join(websiteDir, "roles.html"), "utf8");
230
+ for (const match of rolesSource.matchAll(/<button[^>]*>([\s\S]*?)<\/button>/g)) {
231
+ terms.add(plainText(match[1]));
232
+ }
233
+
234
+ return [...terms].sort(compareTerms);
235
+ }
236
+
237
+ for (const file of htmlFiles) {
238
+ const source = readFileSync(path.join(websiteDir, file), "utf8");
239
+ addCopyFailures(`website/${file}`, visibleTextFromHtml(source));
240
+ addBrokenLinkFailures(`website/${file}`, source);
241
+ }
242
+
243
+ for (const file of jsFiles) {
244
+ const source = readFileSync(path.join(websiteDir, file), "utf8");
245
+ addCopyFailures(`website/${file}`, source);
246
+ }
247
+
248
+ for (const file of mdFiles) {
249
+ const source = readFileSync(path.join(websiteDir, file), "utf8");
250
+ addCopyFailures(`website/${file}`, source);
251
+ }
252
+
253
+ addGlossaryFailures();
254
+
255
+ if (failures.length > 0) {
256
+ console.error("Public website copy check failed:");
257
+ for (const failure of failures) {
258
+ console.error(`- ${failure.file}: ${failure.reason} (${failure.detail})`);
259
+ }
260
+ process.exit(1);
261
+ }
262
+
263
+ console.log("Public website copy check passed.");
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
+ const workspaceBoundaryFiles = [
7
+ "src/http/server.ts",
8
+ "api/mcp.js",
9
+ "api/dxcomplete.js",
10
+ "api/auth/callback/google.js",
11
+ "templates/next/pages/api/mcp.js",
12
+ "templates/next/pages/api/dxcomplete.js",
13
+ "templates/next/pages/api/dxcomplete/[...path].js",
14
+ "templates/next/pages/api/auth/callback/google.js"
15
+ ];
16
+ const bannedPatterns = [
17
+ {
18
+ pattern: /\bconnectRuntime\b/,
19
+ reason: "workspace MCP code must not open the runtime database"
20
+ },
21
+ {
22
+ pattern: /runtime\/mongo|runtime\\mongo|\.\.\/runtime\/mongo|\.\.\/dist\/runtime\/mongo/,
23
+ reason: "workspace MCP code must not import the Mongo runtime"
24
+ },
25
+ {
26
+ pattern: /runtime\/records|runtime\\records|\.\.\/runtime\/records|\.\.\/dist\/runtime\/records/,
27
+ reason: "workspace MCP code must not import runtime record modules"
28
+ },
29
+ {
30
+ pattern: /runtime\/auth|runtime\\auth|\.\.\/runtime\/auth|\.\.\/dist\/runtime\/auth/,
31
+ reason: "workspace MCP code must not import central auth/storage modules"
32
+ },
33
+ {
34
+ pattern: /\bDXC_MONGODB_URI\b/,
35
+ reason: "workspace MCP code must not reference Mongo credentials"
36
+ },
37
+ {
38
+ pattern: /\bDXC_GOOGLE_CLIENT_ID\b|\bDXC_GOOGLE_CLIENT_SECRET\b/,
39
+ reason: "workspace MCP code must not reference Google OAuth provisioning secrets"
40
+ }
41
+ ];
42
+ const failures = [];
43
+
44
+ for (const relativePath of workspaceBoundaryFiles) {
45
+ const absolutePath = path.join(rootDir, relativePath);
46
+ const content = readFileSync(absolutePath, "utf8");
47
+
48
+ for (const { pattern, reason } of bannedPatterns) {
49
+ if (pattern.test(content)) {
50
+ failures.push(`${relativePath}: ${reason}.`);
51
+ }
52
+ }
53
+ }
54
+
55
+ if (failures.length > 0) {
56
+ console.error("Service boundary check failed:");
57
+ for (const failure of failures) {
58
+ console.error(`- ${failure}`);
59
+ }
60
+ process.exit(1);
61
+ }
62
+
63
+ console.log("Service boundary check passed.");