@diy-accounting-uk/diya-gl 1.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 (157) hide show
  1. package/README.md +63 -0
  2. package/bin/diya-gl-mcp.js +10 -0
  3. package/bin/diya-gl-read-workbook.js +11 -0
  4. package/bin/diya-gl-recalc.js +10 -0
  5. package/bin/diya-gl-write-workbook.js +12 -0
  6. package/bin/diya-gl.js +24 -0
  7. package/bin/run-dist-bin.js +23 -0
  8. package/dist/app/bin/build-diya-gl-spec.js +680 -0
  9. package/dist/app/bin/build-packages.js +149 -0
  10. package/dist/app/bin/build-reconciliation-pages.js +1052 -0
  11. package/dist/app/bin/build-sitemaps.js +51 -0
  12. package/dist/app/bin/compliance-report.js +125 -0
  13. package/dist/app/bin/cross-package-reconciliation.js +92 -0
  14. package/dist/app/bin/diya-gl-mcp.js +27 -0
  15. package/dist/app/bin/export.js +318 -0
  16. package/dist/app/bin/extract-scenarios.js +1348 -0
  17. package/dist/app/bin/generate.js +353 -0
  18. package/dist/app/bin/judge-reconciliation.js +649 -0
  19. package/dist/app/bin/reconcile.js +247 -0
  20. package/dist/app/bin/report.js +291 -0
  21. package/dist/app/bin/verify-roundtrip.js +941 -0
  22. package/dist/app/bin/verify-stability.js +429 -0
  23. package/dist/app/bin/write-workbook.js +106 -0
  24. package/dist/app/data/filing/ct-computation-v1.1.toml +1033 -0
  25. package/dist/app/data/filing/ct600-v3.toml +2329 -0
  26. package/dist/app/data/filing/frs105-formats.toml +244 -0
  27. package/dist/app/data/fixture-master-gaps.json +247 -0
  28. package/dist/app/data/hmrc/SOURCE.md +14 -0
  29. package/dist/app/data/hmrc/form-layouts/bst.json +116 -0
  30. package/dist/app/data/hmrc/form-layouts/ltd.json +452 -0
  31. package/dist/app/data/hmrc/form-layouts/se.json +485 -0
  32. package/dist/app/data/hmrc/form-layouts/taxi.json +117 -0
  33. package/dist/app/data/hmrc/sa103-mtd-mapping.json +1078 -0
  34. package/dist/app/data/hmrc/sa103f_mapping_v3.csv +111 -0
  35. package/dist/app/data/judge-rubric.md +52 -0
  36. package/dist/app/data/ltd-2020.toml +49 -0
  37. package/dist/app/data/ltd-2021.toml +49 -0
  38. package/dist/app/data/ltd-2022.toml +49 -0
  39. package/dist/app/data/ltd-2023.toml +49 -0
  40. package/dist/app/data/ltd-2024.toml +49 -0
  41. package/dist/app/data/ltd-2025.toml +49 -0
  42. package/dist/app/data/ltd-2026.toml +53 -0
  43. package/dist/app/data/ltd-2027.toml +51 -0
  44. package/dist/app/data/render-unrepresentable/bst.json +24 -0
  45. package/dist/app/data/render-unrepresentable/ltd.json +863 -0
  46. package/dist/app/data/render-unrepresentable/se.json +455 -0
  47. package/dist/app/data/render-unrepresentable/taxi.json +16 -0
  48. package/dist/app/data/roundtrip-budget.json +38 -0
  49. package/dist/app/data/roundtrip-matrix-budget.json +26 -0
  50. package/dist/app/data/roundtrip-unrepresentable.json +401 -0
  51. package/dist/app/data/se-2020-2021.toml +52 -0
  52. package/dist/app/data/se-2021-2022.toml +52 -0
  53. package/dist/app/data/se-2022-2023.toml +55 -0
  54. package/dist/app/data/se-2023-2024.toml +54 -0
  55. package/dist/app/data/se-2024-2025.toml +51 -0
  56. package/dist/app/data/se-2025-2026.toml +51 -0
  57. package/dist/app/data/se-2026-2027.toml +51 -0
  58. package/dist/app/data/volatile-cells.json +16 -0
  59. package/dist/app/lib/anchors/bst.js +79 -0
  60. package/dist/app/lib/anchors/ltd.js +626 -0
  61. package/dist/app/lib/anchors/run.js +100 -0
  62. package/dist/app/lib/anchors/se.js +456 -0
  63. package/dist/app/lib/anchors/taxi.js +164 -0
  64. package/dist/app/lib/app-resources.js +80 -0
  65. package/dist/app/lib/book-checks/ltd.js +408 -0
  66. package/dist/app/lib/book-checks/se.js +687 -0
  67. package/dist/app/lib/book-checks/taxi.js +215 -0
  68. package/dist/app/lib/book-checks.js +872 -0
  69. package/dist/app/lib/books-engine.js +197 -0
  70. package/dist/app/lib/books-interchange.js +533 -0
  71. package/dist/app/lib/calculators/bst.js +381 -0
  72. package/dist/app/lib/calculators/ltd.js +1938 -0
  73. package/dist/app/lib/calculators/se.js +1239 -0
  74. package/dist/app/lib/calculators/shared.js +131 -0
  75. package/dist/app/lib/calculators/taxi.js +461 -0
  76. package/dist/app/lib/canonical-report-value.js +77 -0
  77. package/dist/app/lib/compliance-report.js +341 -0
  78. package/dist/app/lib/diya-gl-calculator.js +52 -0
  79. package/dist/app/lib/diya-gl-canonical.js +355 -0
  80. package/dist/app/lib/diya-gl-edits-ltd.js +144 -0
  81. package/dist/app/lib/diya-gl-edits.js +361 -0
  82. package/dist/app/lib/diya-gl-loader.js +774 -0
  83. package/dist/app/lib/diya-gl-schema.js +212 -0
  84. package/dist/app/lib/generator.js +1912 -0
  85. package/dist/app/lib/guide.js +33 -0
  86. package/dist/app/lib/headlines.js +293 -0
  87. package/dist/app/lib/link-caches.js +421 -0
  88. package/dist/app/lib/ltd-layout.js +139 -0
  89. package/dist/app/lib/mcp/diya-gl-tools.js +330 -0
  90. package/dist/app/lib/mcp/jsonrpc-stdio.js +66 -0
  91. package/dist/app/lib/mcp/server.js +65 -0
  92. package/dist/app/lib/overtype-sidecar.js +180 -0
  93. package/dist/app/lib/package-builder.js +202 -0
  94. package/dist/app/lib/payslips-date-chain.js +481 -0
  95. package/dist/app/lib/payslips-layout.js +235 -0
  96. package/dist/app/lib/period-shift.js +111 -0
  97. package/dist/app/lib/product-workbook.js +317 -0
  98. package/dist/app/lib/products.js +27 -0
  99. package/dist/app/lib/provenance-data.js +36 -0
  100. package/dist/app/lib/provenance.js +92 -0
  101. package/dist/app/lib/report-generator.js +374 -0
  102. package/dist/app/lib/report-indicators.js +468 -0
  103. package/dist/app/lib/report-serializer.js +392 -0
  104. package/dist/app/lib/scenario-extractor.js +1403 -0
  105. package/dist/app/lib/scenario-loader.js +84 -0
  106. package/dist/app/lib/sitemap-builder.js +59 -0
  107. package/dist/app/lib/spreadsheet-runner.js +566 -0
  108. package/dist/app/lib/tax/capital-allowances.js +77 -0
  109. package/dist/app/lib/tax/corporation-tax.js +144 -0
  110. package/dist/app/lib/tax/income-tax.js +122 -0
  111. package/dist/app/lib/tax/mileage.js +69 -0
  112. package/dist/app/lib/tax/national-insurance.js +31 -0
  113. package/dist/app/lib/tax/vat.js +188 -0
  114. package/dist/app/lib/tax-year.js +31 -0
  115. package/dist/app/lib/template-formula-map.js +253 -0
  116. package/dist/app/lib/workbook-set.js +153 -0
  117. package/dist/app/lib/xlsx-exporter.js +2815 -0
  118. package/dist/app/lib/xlsx-parts.js +111 -0
  119. package/dist/app/lib/xlsx-reader.js +84 -0
  120. package/dist/app/products/bst.js +727 -0
  121. package/dist/app/products/ltd.js +4270 -0
  122. package/dist/app/products/se.js +3403 -0
  123. package/dist/app/products/taxi.js +912 -0
  124. package/dist/app/templates/bst/bst-excel.xlsx +0 -0
  125. package/dist/app/templates/bst/meta.toml +27 -0
  126. package/dist/app/templates/ltd/Cashaccount.xlsx +0 -0
  127. package/dist/app/templates/ltd/Companysecretary.xlsx +0 -0
  128. package/dist/app/templates/ltd/Creditcardaccount.xlsx +0 -0
  129. package/dist/app/templates/ltd/Currentaccount.xlsx +0 -0
  130. package/dist/app/templates/ltd/Dividend Voucher.docx +0 -0
  131. package/dist/app/templates/ltd/Financialaccounts.xlsx +0 -0
  132. package/dist/app/templates/ltd/Fixedassets.xlsx +0 -0
  133. package/dist/app/templates/ltd/Payslips.xlsx +0 -0
  134. package/dist/app/templates/ltd/Purchases.xlsx +0 -0
  135. package/dist/app/templates/ltd/Sales.xlsx +0 -0
  136. package/dist/app/templates/ltd/Salesinvoice.xlsx +0 -0
  137. package/dist/app/templates/ltd/Savingaccount.xlsx +0 -0
  138. package/dist/app/templates/ltd/Vatreturns.xlsx +0 -0
  139. package/dist/app/templates/ltd/expensesform.xlsx +0 -0
  140. package/dist/app/templates/ltd/meta.toml +54 -0
  141. package/dist/app/templates/meta.toml +12 -0
  142. package/dist/app/templates/se/Bank.xlsx +0 -0
  143. package/dist/app/templates/se/Cash.xlsx +0 -0
  144. package/dist/app/templates/se/Financialaccounts.xlsx +0 -0
  145. package/dist/app/templates/se/Fixedassets.xlsx +0 -0
  146. package/dist/app/templates/se/Payslips.xlsx +0 -0
  147. package/dist/app/templates/se/Purchases.xlsx +0 -0
  148. package/dist/app/templates/se/Sales.xlsx +0 -0
  149. package/dist/app/templates/se/Salesinvoice.xlsx +0 -0
  150. package/dist/app/templates/se/Vat.xlsx +0 -0
  151. package/dist/app/templates/se/meta.toml +43 -0
  152. package/dist/app/templates/taxi/meta.toml +43 -0
  153. package/dist/app/templates/taxi/taxi-excel.xlsx +0 -0
  154. package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json +713 -0
  155. package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-docs.md +930 -0
  156. package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-lines-v2.schema.json +220 -0
  157. package/package.json +37 -0
@@ -0,0 +1,212 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 DIY Accounting Ltd
3
+ //
4
+ // diya-gl-schema.js — The one validator for diya-gl book.toml and lines.jsonl
5
+ // data. Compiles the published v2 JSON Schemas (draft 2020-12) with ajv, and
6
+ // adds the two rules JSON Schema itself cannot state: that every line's
7
+ // accountMainID names a declared account, and that every
8
+ // diya-gl:hpAgreement, diya-gl:assetID and diya-gl:memberID names an entry
9
+ // on the matching book.toml register.
10
+
11
+ import Ajv2020 from "ajv/dist/2020.js";
12
+ import addFormats from "ajv-formats";
13
+ import standaloneCode from "ajv/dist/standalone/index.js";
14
+ import { readFileSync } from "fs";
15
+ import { resolve, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { BOOK_SCHEMA_RESOURCE, LINES_SCHEMA_RESOURCE, nodeResourceLoader } from "./app-resources.js";
18
+
19
+ // The two published schemas compile once, on the first validation rather than
20
+ // on import. A caller that cannot read files — the books page — hands them in
21
+ // with useSchemas() before validating anything; in Node they come off disk.
22
+ let validators = null;
23
+
24
+ function compileSchemas(bookSchema, linesSchema) {
25
+ const ajv = new Ajv2020({ allErrors: true });
26
+ addFormats(ajv);
27
+ return { book: ajv.compile(bookSchema), lines: ajv.compile(linesSchema) };
28
+ }
29
+
30
+ /**
31
+ * Supply the two v2 schemas rather than have them read from disk. The books
32
+ * page fetches them and calls this before its first validation.
33
+ * @param {Object} bookSchema - parsed diya-gl-book-v2.schema.json
34
+ * @param {Object} linesSchema - parsed diya-gl-lines-v2.schema.json
35
+ */
36
+ export function useSchemas(bookSchema, linesSchema) {
37
+ validators = compileSchemas(bookSchema, linesSchema);
38
+ }
39
+
40
+ /**
41
+ * Supply the two validators already compiled, rather than have this module
42
+ * compile the schemas itself. They must be shaped exactly like ajv's own
43
+ * compiled validators: called with the parsed data, each returns a boolean
44
+ * and leaves its failures on its own `.errors` property.
45
+ *
46
+ * This is the injection seam for a caller with no `new Function` - a Content
47
+ * Security Policy with no `unsafe-eval` forbids the `new Function` inside
48
+ * ajv.compile, which useSchemas() and loadSchemasFrom() both reach. The
49
+ * books bundle takes a different route to the same result (see
50
+ * scripts/build-books-bundle.mjs: it resolves ajv's own imports to functions
51
+ * generateStandaloneValidatorSource() built ahead of time, so useSchemas()
52
+ * and loadSchemasFrom() keep working unchanged); this function is the direct
53
+ * seam for a caller - a test proving the two validator sources agree, or a
54
+ * future one - that would rather hand the compiled validators in itself.
55
+ * @param {{book: Function, lines: Function}} generated
56
+ */
57
+ export function useValidators(generated) {
58
+ validators = generated;
59
+ }
60
+
61
+ /**
62
+ * Turn the two v2 schemas into the source of a standalone JavaScript module
63
+ * that exports `validateBook` and `validateLines` - ajv's own code
64
+ * generator, run once here rather than by `new Function` every time a page
65
+ * loads. The source still calls a bare `require("ajv-formats/dist/formats")`
66
+ * for its format checks, so a caller bundles it (esbuild, webpack, rollup)
67
+ * before running it anywhere the module has no `require`.
68
+ * @param {Object} bookSchema - parsed diya-gl-book-v2.schema.json
69
+ * @param {Object} linesSchema - parsed diya-gl-lines-v2.schema.json
70
+ * @returns {string} an ES module's source
71
+ */
72
+ export function generateStandaloneValidatorSource(bookSchema, linesSchema) {
73
+ const ajv = new Ajv2020({ allErrors: true, code: { source: true, esm: true } });
74
+ addFormats(ajv);
75
+ ajv.addSchema(bookSchema, "book");
76
+ ajv.addSchema(linesSchema, "lines");
77
+ return standaloneCode(ajv, { validateBook: "book", validateLines: "lines" });
78
+ }
79
+
80
+ /**
81
+ * Load and compile the two schemas through a resource loader, for a caller
82
+ * that has one but would rather not parse the JSON itself.
83
+ * @param {{readText: (path: string) => Promise<string>}} [resources]
84
+ */
85
+ export async function loadSchemasFrom(resources = nodeResourceLoader()) {
86
+ const [book, lines] = await Promise.all([
87
+ resources.readText(BOOK_SCHEMA_RESOURCE).then(JSON.parse),
88
+ resources.readText(LINES_SCHEMA_RESOURCE).then(JSON.parse),
89
+ ]);
90
+ useSchemas(book, lines);
91
+ }
92
+
93
+ // Validation is synchronous, and every caller in the pipeline relies on that,
94
+ // so the Node fallback reads the two files synchronously. A bundle with no
95
+ // file system reaches this only when the page forgot to call useSchemas(), and
96
+ // then it fails loudly rather than validating nothing.
97
+ function compiled() {
98
+ if (validators) return validators;
99
+ try {
100
+ const schemaDir = resolve(
101
+ dirname(fileURLToPath(import.meta.url)),
102
+ "..",
103
+ "..",
104
+ "web",
105
+ "spreadsheets.diyaccounting.co.uk",
106
+ "public",
107
+ "schema",
108
+ );
109
+ const read = (fileName) => JSON.parse(readFileSync(resolve(schemaDir, fileName), "utf8"));
110
+ validators = compileSchemas(read("diya-gl-book-v2.schema.json"), read("diya-gl-lines-v2.schema.json"));
111
+ } catch (cause) {
112
+ throw new Error(
113
+ "the diya-gl schemas could not be read; a caller with no file system has to call useSchemas() or loadSchemasFrom() first",
114
+ {
115
+ cause,
116
+ },
117
+ );
118
+ }
119
+ return validators;
120
+ }
121
+
122
+ function formatAjvError(prefix, error) {
123
+ return `${prefix}${error.instancePath || "/"} ${error.message}`;
124
+ }
125
+
126
+ // TOML dates parse to JS Date instances (smol-toml, and every other TOML
127
+ // parser this book format has used), never to strings. The schema declares
128
+ // them as ISO 8601 strings, matching lines.jsonl and the schema's own JSON
129
+ // Schema "date" format, so a book fresh off the parser is walked once to
130
+ // turn every Date into the string the schema expects before it is checked.
131
+ function withIsoDates(value) {
132
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
133
+ if (Array.isArray(value)) return value.map(withIsoDates);
134
+ if (value && typeof value === "object") {
135
+ const out = {};
136
+ for (const [key, v] of Object.entries(value)) out[key] = withIsoDates(v);
137
+ return out;
138
+ }
139
+ return value;
140
+ }
141
+
142
+ /**
143
+ * Validate a parsed book.toml against the published diya-gl-book-v2 schema.
144
+ * @param {Object} book - parsed book.toml (TOML dates as JS Date instances, as every TOML parser returns them)
145
+ * @returns {{valid: boolean, errors: string[]}}
146
+ */
147
+ export function validateBook(book) {
148
+ const validateBookSchema = compiled().book;
149
+ const valid = validateBookSchema(withIsoDates(book));
150
+ const errors = valid ? [] : validateBookSchema.errors.map((e) => formatAjvError("book", e));
151
+ return { valid, errors };
152
+ }
153
+
154
+ // Every section of the chart of accounts a line's accountMainID can name.
155
+ const ACCOUNT_SECTIONS = ["sales", "purchases", "bank", "capital", "assets", "liabilities"];
156
+
157
+ function declaredAccountCodes(book) {
158
+ const codes = new Set();
159
+ for (const section of ACCOUNT_SECTIONS) {
160
+ for (const code of Object.keys(book?.accounts?.[section] || {})) codes.add(code);
161
+ }
162
+ return codes;
163
+ }
164
+
165
+ function declaredIDs(entries, idField) {
166
+ return new Set((entries || []).map((entry) => entry[idField]).filter((id) => id !== undefined));
167
+ }
168
+
169
+ /**
170
+ * Validate a book's lines.jsonl entries against the published
171
+ * diya-gl-lines-v2 schema, then against the book: every accountMainID has
172
+ * to name a declared account, and every diya-gl:hpAgreement /
173
+ * diya-gl:assetID / diya-gl:memberID has to name an entry on the book's own
174
+ * hpAgreements / fixedAssets / members register. A register the book does
175
+ * not declare is not checked, because a product subset with no such
176
+ * register carries no lines that could name one either.
177
+ * @param {Array} lines - parsed lines.jsonl entries
178
+ * @param {Object} book - parsed book.toml, for the referential checks
179
+ * @returns {{valid: boolean, errors: string[]}}
180
+ */
181
+ export function validateLines(lines, book) {
182
+ const validateLineSchema = compiled().lines;
183
+ const errors = [];
184
+
185
+ const accountCodes = declaredAccountCodes(book);
186
+ const hpAgreementIDs = book?.hpAgreements ? declaredIDs(book.hpAgreements, "agreementID") : null;
187
+ const assetIDs = book?.fixedAssets ? declaredIDs(book.fixedAssets, "assetID") : null;
188
+ const memberIDs = book?.members ? declaredIDs(book.members, "memberID") : null;
189
+
190
+ lines.forEach((line, index) => {
191
+ const label = `line ${index + 1} (${line.entryNumber || "no entryNumber"})`;
192
+ if (!validateLineSchema(line)) {
193
+ for (const e of validateLineSchema.errors) errors.push(formatAjvError(`${label}: `, e));
194
+ }
195
+ if (line.accountMainID !== undefined && !accountCodes.has(line.accountMainID)) {
196
+ errors.push(`${label}: accountMainID "${line.accountMainID}" is not declared in book.toml accounts`);
197
+ }
198
+ if (hpAgreementIDs && line["diya-gl:hpAgreement"] !== undefined && !hpAgreementIDs.has(line["diya-gl:hpAgreement"])) {
199
+ errors.push(
200
+ `${label}: diya-gl:hpAgreement "${line["diya-gl:hpAgreement"]}" does not match any hpAgreements[].agreementID in book.toml`,
201
+ );
202
+ }
203
+ if (assetIDs && line["diya-gl:assetID"] !== undefined && !assetIDs.has(line["diya-gl:assetID"])) {
204
+ errors.push(`${label}: diya-gl:assetID "${line["diya-gl:assetID"]}" does not match any fixedAssets[].assetID in book.toml`);
205
+ }
206
+ if (memberIDs && line["diya-gl:memberID"] !== undefined && !memberIDs.has(line["diya-gl:memberID"])) {
207
+ errors.push(`${label}: diya-gl:memberID "${line["diya-gl:memberID"]}" does not match any members[].memberID in book.toml`);
208
+ }
209
+ });
210
+
211
+ return { valid: errors.length === 0, errors };
212
+ }