The Atlas doc.haus documentation, bound to its code
108 documents

Zero to a running demo

From a fresh clone to asking a fictional engagement letter about its liability cap — the fastest way to feel what doc.haus is.

services/ingest/src/seed.ts290 lines · CLAUSES L38–103
Outline 18 symbols
1import {
2 Document,
3 Packer,
4 Paragraph,
5 TextRun,
6 ImageRun,
7 AlignmentType,
8 BorderStyle,
9 Footer,
10 PageNumber,
11 Table,
12 TableRow,
13 TableCell,
14 WidthType,
15} from "docx"
16import { mkdirSync, writeFileSync } from "node:fs"
17import path from "node:path"
18import { createMatter, listMatters } from "./matter"
19import { ingestDocument } from "./ingest"
20
21// Seeds the demo matter: a wholly fictional letter of engagement, ingested
22// through the real pipeline so a first-time user lands on a matter that already
23// answers cited questions and runs a legal review — no upload, no data of their
24// own required.
25//
26// cd services/ingest && bun run seed
27//
28// It also writes the generated .docx to repo `demo/` so the same file can be
29// dropped into a new matter through the web UI. Everything below is invented;
30// Aldgate & Crane LLP and Aldgate Mills Limited do not exist.
31
32const DOC_NAME = "Letter of Engagement — Aldgate Mills.docx"
33const MATTER_TITLE = "Aldgate Mills — Engagement (Demo)"
34
35// Each clause is a bold "N. Title" heading paragraph followed by its body. The
36// ingest sectionizer keys sections off the leading clause number, so a citation
37// like [Letter of Engagement — Aldgate Mills § 9] resolves to clause 9.
38const CLAUSES: [string, string][] = [
39 [
40 "1. Scope of our work",
41 "We will advise on and complete the acquisition of the long leasehold of Unit 5, Saffron Wharf, London E1, including reviewing the agreement for lease and the lease, reporting to you on title and on the principal commercial terms, raising and reviewing enquiries, and dealing with completion and post-completion registration at HM Land Registry. We will not advise on the commercial merits of the transaction, on tax beyond Stamp Duty Land Tax, or on the physical condition of the property, which are outside the scope of this engagement unless separately agreed in writing.",
42 ],
43 [
44 "2. The people acting for you",
45 "Your matter will be handled by Daniel Crane, Partner (charged at £480 per hour), with support from Priya Nair, Associate (charged at £290 per hour). Routine work may be delegated to a trainee or paralegal at £160 per hour where that is cost-effective. We will tell you promptly if the person responsible for your matter changes.",
46 ],
47 [
48 "3. Our fees",
49 "Our fees are calculated principally by reference to the time spent at the hourly rates in clause 2. Our current estimate for this matter is £14,500 plus VAT and disbursements. An estimate is not a fixed quotation; if it becomes likely that the estimate will be exceeded, we will tell you before further significant costs are incurred and agree a revised estimate with you.",
50 ],
51 [
52 "4. Disbursements",
53 "Disbursements are expenses we pay on your behalf, such as Land Registry fees, search fees, and Stamp Duty Land Tax. We will normally ask you to put us in funds for substantial disbursements before we incur them.",
54 ],
55 [
56 "5. Billing and payment",
57 "We will deliver interim bills monthly as the matter progresses, with a final bill on completion. Each bill is payable within 14 days of its date. We reserve the right to charge interest on bills not paid within that period at 4% per year above the base rate of the Bank of England, calculated from the date of the bill until payment.",
58 ],
59 [
60 "6. Money on account",
61 "We ask you to pay £5,000 on account of our fees and disbursements before we begin substantive work. We will hold that money in our client account and apply it against our bills, asking you to top it up as the matter proceeds.",
62 ],
63 [
64 "7. Your responsibilities",
65 "You agree to give us clear and timely instructions, to provide the documents and information we reasonably request, to put us in funds as agreed, and to tell us promptly of any change to the transaction or your instructions. Delay in any of these may affect our estimate and the timetable.",
66 ],
67 [
68 "8. Confidentiality",
69 "We will keep your affairs confidential, save where disclosure is required by law or by our regulator, or where you authorise it. Our duty of confidentiality continues after this engagement ends.",
70 ],
71 [
72 "9. Limitation of liability",
73 "Our total aggregate liability to you arising out of or in connection with this engagement, whether in contract, tort (including negligence), breach of statutory duty or otherwise, is limited to £3 million, which corresponds to our professional indemnity insurance cover. We are not liable for any indirect or consequential loss, or for loss of profit, revenue, or anticipated savings. Nothing in this clause limits any liability that cannot lawfully be limited, including liability for death or personal injury caused by negligence or for fraud.",
74 ],
75 [
76 "10. Conflicts of interest",
77 "We have checked for conflicts of interest and are not aware of any that prevent us acting for you. If a conflict arises during the engagement, we will tell you and explain the options, which may include our ceasing to act.",
78 ],
79 [
80 "11. Data protection",
81 "We process your personal data as a controller in order to act for you, in accordance with the UK GDPR and the Data Protection Act 2018. We retain your file for seven years after the matter closes, after which we may destroy it without further reference to you. Our privacy notice gives further detail.",
82 ],
83 [
84 "12. Termination",
85 "You may end this engagement at any time by written notice. We may cease to act only for good reason, such as a conflict of interest, non-payment of our bills, or your failure to give instructions, and on giving you reasonable written notice. On termination you remain liable for our fees and disbursements incurred up to that point.",
86 ],
87 [
88 "13. Complaints",
89 "We aim to provide a high standard of service. If you are unhappy with our service or a bill, please raise it first with Daniel Crane. If we cannot resolve it, you may be entitled to complain to the Legal Ombudsman, normally within six months of our final response. You may also have the right to challenge a bill under the Solicitors Act 1974.",
90 ],
91 [
92 "14. Regulation",
93 "Aldgate & Crane LLP is authorised and regulated by the Solicitors Regulation Authority. We are bound by the SRA Standards and Regulations, which are available from the SRA.",
94 ],
95 [
96 "15. Governing law",
97 "This engagement and our terms of business are governed by the law of England and Wales, and the courts of England and Wales have exclusive jurisdiction over any dispute arising out of them.",
98 ],
99 [
100 "16. Acceptance",
101 "If these terms are acceptable, please sign and date below and return one copy to us. Work you ask us to carry out, or the payment of money on account, will in any event be taken as your acceptance of these terms.",
102 ],
103]
104
105// House palette for the (fictional) firm's letterhead.
106const NAVY = "1C2B3A"
107const GOLD = "C9A24B"
108const INK = "222222"
109
110// The firm's emblem — a serif "A&C" monogram in a gold-ruled navy square. A
111// committed static asset, read at build time so the .docx carries a real
112// embedded image with no image-processing dependency.
113const logoPng = await Bun.file(path.join(import.meta.dir, "..", "assets", "logo.png")).bytes()
114
115// 22 half-points = 11pt body; clause/heading sizes follow. Rules are drawn as
116// bottom paragraph borders so they print without a table.
117const RULE = { bottom: { style: BorderStyle.SINGLE, size: 6, space: 4, color: GOLD } }
118
119function body(text: string) {
120 return new Paragraph({
121 alignment: AlignmentType.JUSTIFIED,
122 spacing: { after: 160, line: 276 },
123 children: [new TextRun(text)],
124 })
125}
126
127function clause([title, text]: [string, string]) {
128 const [num, ...rest] = title.split(". ")
129 return [
130 new Paragraph({
131 spacing: { before: 220, after: 60 },
132 children: [
133 new TextRun({ text: `${num}.`, bold: true, color: GOLD }),
134 new TextRun({ text: ` ${rest.join(". ")}`, bold: true, color: NAVY, allCaps: true, size: 20 }),
135 ],
136 }),
137 body(text),
138 ]
139}
140
141// Two-column block: recipient on the left, our ref / date on the right. A
142// borderless table keeps the columns aligned the way a real letter sets them.
143function metaCell(lines: string[], alignment: (typeof AlignmentType)[keyof typeof AlignmentType]) {
144 return new TableCell({
145 width: { size: 50, type: WidthType.PERCENTAGE },
146 margins: { top: 0, bottom: 0, left: 0, right: 0 },
147 children: lines.map(
148 (text, i) =>
149 new Paragraph({
150 alignment,
151 spacing: { after: 20 },
152 children: [new TextRun({ text, bold: i === 0, color: INK, size: 19 })],
153 }),
154 ),
155 })
156}
157
158const NO_BORDERS = {
159 top: { style: BorderStyle.NONE, size: 0, color: "auto" },
160 bottom: { style: BorderStyle.NONE, size: 0, color: "auto" },
161 left: { style: BorderStyle.NONE, size: 0, color: "auto" },
162 right: { style: BorderStyle.NONE, size: 0, color: "auto" },
163 insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "auto" },
164 insideVertical: { style: BorderStyle.NONE, size: 0, color: "auto" },
165}
166
167const doc = new Document({
168 styles: { default: { document: { run: { font: "Georgia", size: 22, color: INK } } } },
169 sections: [
170 {
171 properties: { page: { margin: { top: 1100, bottom: 1100, left: 1300, right: 1300 } } },
172 footers: {
173 default: new Footer({
174 children: [
175 new Paragraph({
176 border: { top: { style: BorderStyle.SINGLE, size: 4, space: 6, color: GOLD } },
177 alignment: AlignmentType.CENTER,
178 spacing: { before: 60 },
179 children: [
180 new TextRun({
181 text: "Aldgate & Crane LLP — a limited liability partnership registered in England and Wales (OC384726). ",
182 size: 14,
183 color: "888888",
184 }),
185 new TextRun({ text: "Authorised and regulated by the Solicitors Regulation Authority.", size: 14, color: "888888" }),
186 ],
187 }),
188 new Paragraph({
189 alignment: AlignmentType.CENTER,
190 children: [new TextRun({ children: ["Page ", PageNumber.CURRENT, " of ", PageNumber.TOTAL_PAGES], size: 14, color: "888888" })],
191 }),
192 ],
193 }),
194 },
195 children: [
196 new Paragraph({
197 alignment: AlignmentType.CENTER,
198 spacing: { after: 40 },
199 children: [new ImageRun({ data: logoPng, type: "png", transformation: { width: 76, height: 76 } })],
200 }),
201 new Paragraph({
202 alignment: AlignmentType.CENTER,
203 spacing: { after: 20 },
204 children: [new TextRun({ text: "ALDGATE & CRANE LLP", bold: true, color: NAVY, size: 30, allCaps: true })],
205 }),
206 new Paragraph({
207 alignment: AlignmentType.CENTER,
208 spacing: { after: 120 },
209 border: RULE,
210 children: [new TextRun({ text: "S O L I C I T O R S", color: GOLD, size: 16 })],
211 }),
212 new Paragraph({
213 alignment: AlignmentType.CENTER,
214 spacing: { after: 240 },
215 children: [
216 new TextRun({ text: "14 Saffron Court, London EC3N 4QX", size: 18, color: "555555" }),
217 new TextRun({ text: " · +44 (0)20 7946 0042 · law@aldgatecrane.co.uk", size: 18, color: "555555" }),
218 ],
219 }),
220 new Table({
221 width: { size: 100, type: WidthType.PERCENTAGE },
222 borders: NO_BORDERS,
223 rows: [
224 new TableRow({
225 children: [
226 metaCell(
227 ["Aldgate Mills Limited", "FAO: Ms R. Okafor, Director", "27 Wharf Road", "London E1 8GW"],
228 AlignmentType.LEFT,
229 ),
230 metaCell(["Our ref: A&C/2026-0042", "3 June 2026", "By email and post"], AlignmentType.RIGHT),
231 ],
232 }),
233 ],
234 }),
235 new Paragraph({
236 alignment: AlignmentType.CENTER,
237 spacing: { before: 240, after: 160 },
238 children: [new TextRun({ text: "LETTER OF ENGAGEMENT", bold: true, color: NAVY, size: 26, allCaps: true })],
239 }),
240 body("Dear Ms Okafor,"),
241 new Paragraph({
242 alignment: AlignmentType.JUSTIFIED,
243 spacing: { after: 160, line: 276 },
244 children: [
245 new TextRun({ text: "Re: Proposed acquisition of the long leasehold of Unit 5, Saffron Wharf, London E1. ", bold: true }),
246 new TextRun(
247 "Thank you for instructing Aldgate & Crane LLP. This letter sets out the basis on which we will act for you. Please read it, and let us know if anything is unclear, before signing and returning the acceptance at the end.",
248 ),
249 ],
250 }),
251 ...CLAUSES.flatMap(clause),
252 new Paragraph({ spacing: { before: 240, after: 160 }, border: RULE, children: [] }),
253 body("Yours sincerely,"),
254 new Paragraph({
255 spacing: { before: 200, after: 40 },
256 children: [new TextRun({ text: "Daniel Crane", bold: true, color: NAVY })],
257 }),
258 body("Partner, for and on behalf of Aldgate & Crane LLP"),
259 new Paragraph({
260 spacing: { before: 240, after: 40 },
261 children: [new TextRun({ text: "Signed (client): ____________________________ Date: ______________", color: INK })],
262 }),
263 body("Ms R. Okafor, for and on behalf of Aldgate Mills Limited"),
264 ],
265 },
266 ],
267})
268
269const buffer = await Packer.toBuffer(doc)
270
271// Keep a copy in repo demo/ so the same .docx can be uploaded through the UI.
272const demoDir = path.join(import.meta.dir, "..", "..", "..", "demo")
273mkdirSync(demoDir, { recursive: true })
274writeFileSync(path.join(demoDir, "Letter-of-Engagement-Aldgate-Mills.docx"), buffer)
275
276// Idempotent: `start.sh --demo` runs this on every boot, so skip ingestion if the
277// demo matter is already present rather than piling up duplicates. The .docx above
278// is still rewritten so demo/ stays in sync with the seed script.
279const existing = listMatters().find((m) => m.title === MATTER_TITLE)
280if (existing) {
281 console.log(`Demo matter "${MATTER_TITLE}" already present (${existing.id}); skipping ingest.`)
282 process.exit(0)
283}
284
285const matter = createMatter(MATTER_TITLE, "A&C/2026-0042")
286const result = await ingestDocument(matter.dir, DOC_NAME, Buffer.from(buffer))
287console.log(`Seeded matter "${matter.title}" (${matter.id})`)
288console.log(`Ingested ${DOC_NAME}: ${result.sections} sections, ${result.chunks} chunks`)
289console.log(`Open the web app and select "${MATTER_TITLE}" to try cited Q&A and a legal review.`)
290