@aotter/mantle 0.0.11-alpha.61 → 0.0.11-alpha.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ Adopters install this one package and import from subpaths. Sub-packages remain
20
20
  |---|---|
21
21
  | `@aotter/mantle/spec` (or root) | Manifest grammar, validators, JSON-Schema→Zod, diagnostic catalog (no env / no IO) |
22
22
  | `@aotter/mantle/runtime` | Hexagonal runtime: domain ports, use cases, infrastructure helpers (no adapter deps) |
23
+ | `@aotter/mantle/runtime/testing` | Node-only crowded SQLite planner and HTTP sampling helpers |
23
24
  | `@aotter/mantle/cloudflare` | Cloudflare Workers adapter — D1, KV, R2, Better Auth, MCP via `@cloudflare/workers-oauth-provider` |
24
25
  | `@aotter/mantle/admin-ui` | Pre-built React 19 admin SPA bundle |
25
26
 
@@ -29,6 +30,13 @@ import { createCmsRuntime } from "@aotter/mantle/runtime";
29
30
  import { mountServerEndpoints } from "@aotter/mantle/cloudflare";
30
31
  ```
31
32
 
33
+ The package also installs `mantle-harness`:
34
+
35
+ ```bash
36
+ pnpm exec mantle-harness indexes --require-public
37
+ pnpm exec mantle-harness http --base-url http://127.0.0.1:8787 --route page=/en/example
38
+ ```
39
+
32
40
  ## Getting started
33
41
 
34
42
  Give the [Mantle repo](https://github.com/aotter/mantle) to a coding agent or
@@ -97,6 +105,8 @@ The `mantle-runtime` package never imports Cloudflare-specific types — adapter
97
105
  Queue wiring, retry/DLQ, idempotency, and delivery guarantees)
98
106
  - `node_modules/@aotter/mantle/docs/schema-indexes.md` (ordered composite
99
107
  JSON-field indexes, D1 query plans, and the safe Procedure SQL helper)
108
+ - `node_modules/@aotter/mantle/docs/performance-harness.md` (crowded SQLite,
109
+ Wrangler-local D1, cache HIT/MISS, and coding-agent guardrails)
100
110
  - `node_modules/@aotter/mantle/docs/adr/`
101
111
  - `node_modules/@aotter/mantle/skills/develop/SKILL.md`
102
112
  - `node_modules/@aotter/mantle/skills/plugin/SKILL.md`
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=harness-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-cli.d.ts","sourceRoot":"","sources":["../src/harness-cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ import { argv, stderr, stdout } from "node:process";
3
+ import { parseArgs } from "node:util";
4
+ import { ValidateManifestsUseCase, } from "@aotter/mantle-spec";
5
+ import { loadManifestsFromRoot } from "@aotter/mantle-spec/cli";
6
+ import { benchmarkHttpRoutes, inspectIndexCoverage, } from "@aotter/mantle-runtime/testing";
7
+ async function main() {
8
+ const command = argv[2];
9
+ if (!command || command === "--help" || command === "-h") {
10
+ printHelp();
11
+ return command ? 0 : 2;
12
+ }
13
+ if (command === "indexes")
14
+ return runIndexes(argv.slice(3));
15
+ if (command === "http")
16
+ return runHttp(argv.slice(3));
17
+ stderr.write(`Unknown subcommand: ${command}\n`);
18
+ return 2;
19
+ }
20
+ async function runIndexes(rawArgs) {
21
+ let values;
22
+ try {
23
+ ({ values } = parseArgs({
24
+ args: [...rawArgs],
25
+ options: {
26
+ manifests: { type: "string" },
27
+ rows: { type: "string" },
28
+ require: { type: "string", multiple: true },
29
+ "require-public": { type: "boolean" },
30
+ format: { type: "string" },
31
+ help: { type: "boolean", short: "h" },
32
+ },
33
+ }));
34
+ }
35
+ catch (error) {
36
+ stderr.write(`${message(error)}\n`);
37
+ return 2;
38
+ }
39
+ if (values.help) {
40
+ printIndexesHelp();
41
+ return 0;
42
+ }
43
+ const format = outputFormat(values.format);
44
+ const loaded = await loadManifestsFromRoot(values.manifests ?? "./manifests");
45
+ const validation = ValidateManifestsUseCase.run({
46
+ manifests: loaded.manifests,
47
+ filePaths: loaded.filePaths,
48
+ });
49
+ const errors = [...loaded.parseErrors, ...validation.diagnostics]
50
+ .filter((diagnostic) => diagnostic.severity === "error");
51
+ if (errors.length > 0) {
52
+ emitErrors(errors, format);
53
+ return 1;
54
+ }
55
+ if (loaded.manifests.length === 0) {
56
+ stderr.write(`No manifests found under ${values.manifests ?? "./manifests"}\n`);
57
+ return 2;
58
+ }
59
+ const rows = values.rows === undefined ? undefined : Number(values.rows);
60
+ if (rows !== undefined && (!Number.isFinite(rows) || rows <= 0)) {
61
+ stderr.write("--rows must be a positive number\n");
62
+ return 2;
63
+ }
64
+ const report = await inspectIndexCoverage(loaded.manifests, {
65
+ rowsPerSchema: rows,
66
+ requirePublic: values["require-public"] === true,
67
+ requiredViews: values.require ?? [],
68
+ });
69
+ emitIndexReport(report, format);
70
+ return report.summary.requiredFailures > 0 ? 1 : 0;
71
+ }
72
+ async function runHttp(rawArgs) {
73
+ let values;
74
+ try {
75
+ ({ values } = parseArgs({
76
+ args: [...rawArgs],
77
+ options: {
78
+ route: { type: "string", multiple: true },
79
+ "base-url": { type: "string" },
80
+ rounds: { type: "string" },
81
+ warmup: { type: "string" },
82
+ format: { type: "string" },
83
+ help: { type: "boolean", short: "h" },
84
+ },
85
+ }));
86
+ }
87
+ catch (error) {
88
+ stderr.write(`${message(error)}\n`);
89
+ return 2;
90
+ }
91
+ if (values.help) {
92
+ printHttpHelp();
93
+ return 0;
94
+ }
95
+ if (!values.route || values.route.length === 0) {
96
+ stderr.write("http requires at least one --route name=/path-or-url\n");
97
+ return 2;
98
+ }
99
+ const format = outputFormat(values.format);
100
+ const baseUrl = (values["base-url"] ?? "").replace(/\/$/, "");
101
+ const targets = values.route.map((raw) => {
102
+ const separator = raw.indexOf("=");
103
+ if (separator < 1)
104
+ throw new Error(`invalid --route ${JSON.stringify(raw)}`);
105
+ const name = raw.slice(0, separator);
106
+ const path = raw.slice(separator + 1);
107
+ return { name, url: path.startsWith("/") ? `${baseUrl}${path}` : path };
108
+ });
109
+ if (targets.some(({ url }) => !/^https?:\/\//.test(url))) {
110
+ stderr.write("route URLs must be absolute, or use --base-url with /paths\n");
111
+ return 2;
112
+ }
113
+ let report;
114
+ try {
115
+ report = await benchmarkHttpRoutes({
116
+ targets,
117
+ rounds: numericOption(values.rounds, "--rounds"),
118
+ warmup: numericOption(values.warmup, "--warmup"),
119
+ });
120
+ }
121
+ catch (error) {
122
+ stderr.write(`${message(error)}\n`);
123
+ return 1;
124
+ }
125
+ emitHttpReport(report, format);
126
+ return 0;
127
+ }
128
+ function emitIndexReport(report, format) {
129
+ if (format === "json") {
130
+ stdout.write(`${JSON.stringify(report, null, 2)}\n`);
131
+ return;
132
+ }
133
+ for (const path of report.paths) {
134
+ const status = path.required ? path.passed ? "PASS" : "FAIL" : "ADVISORY";
135
+ const detail = path.findings.length > 0
136
+ ? path.findings.join("; ")
137
+ : path.usedIndexes.join(", ") || "planner found no named index";
138
+ stdout.write(`${status} ${path.view}: ${detail}\n`);
139
+ }
140
+ for (const view of report.summary.missingRequiredViews) {
141
+ stdout.write(`FAIL ${view}: required View not found\n`);
142
+ }
143
+ stdout.write(`views=${report.summary.views} required=${report.summary.required} ` +
144
+ `failures=${report.summary.requiredFailures} rows/schema=${report.rowsPerSchema}\n`);
145
+ }
146
+ function emitHttpReport(report, format) {
147
+ if (format === "json") {
148
+ stdout.write(`${JSON.stringify(report, null, 2)}\n`);
149
+ return;
150
+ }
151
+ for (const result of report.results) {
152
+ const queries = result.queryCount ? ` queries.p95=${result.queryCount.p95}` : "";
153
+ const rows = result.rowsRead ? ` rows_read.p95=${result.rowsRead.p95}` : "";
154
+ stdout.write(`${result.name}: p50=${result.timingMs.p50.toFixed(2)}ms ` +
155
+ `p95=${result.timingMs.p95.toFixed(2)}ms${queries}${rows}\n`);
156
+ }
157
+ }
158
+ function emitErrors(errors, format) {
159
+ if (format === "json")
160
+ stdout.write(`${JSON.stringify({ errors }, null, 2)}\n`);
161
+ else
162
+ for (const error of errors)
163
+ stderr.write(`${error.code} ${error.path}: ${error.message}\n`);
164
+ }
165
+ function outputFormat(raw) {
166
+ if (raw === undefined)
167
+ return stdout.isTTY ? "text" : "json";
168
+ if (raw === "json" || raw === "text")
169
+ return raw;
170
+ throw new Error(`--format must be text or json; got ${JSON.stringify(raw)}`);
171
+ }
172
+ function numericOption(raw, name) {
173
+ if (raw === undefined)
174
+ return undefined;
175
+ const value = Number(raw);
176
+ if (!Number.isFinite(value) || value < 0)
177
+ throw new Error(`${name} must be a non-negative number`);
178
+ return value;
179
+ }
180
+ function message(error) {
181
+ return error instanceof Error ? error.message : String(error);
182
+ }
183
+ function printHelp() {
184
+ stdout.write(`mantle-harness — measured performance checks\n\n` +
185
+ `Usage: mantle-harness <indexes|http> [options]\n\n` +
186
+ ` indexes Execute compiled Views in crowded SQLite and inspect plans\n` +
187
+ ` http Sample a running Worker and report p50/p95 + metric headers\n`);
188
+ }
189
+ function printIndexesHelp() {
190
+ stdout.write(`Usage: mantle-harness indexes [options]\n\n` +
191
+ ` --manifests <dir> Manifest root (default: ./manifests)\n` +
192
+ ` --rows <n> Deterministic rows per Schema (default: 2000)\n` +
193
+ ` --require <view> Fail when this View lacks its access path; repeatable\n` +
194
+ ` --require-public Apply the gate to public Views\n` +
195
+ ` --format <fmt> text or json\n`);
196
+ }
197
+ function printHttpHelp() {
198
+ stdout.write(`Usage: mantle-harness http [options]\n\n` +
199
+ ` --base-url <url> Prefix for route paths\n` +
200
+ ` --route <name=url> Route label and path/URL; repeatable\n` +
201
+ ` --rounds <n> Measured requests per route (default: 20)\n` +
202
+ ` --warmup <n> Warmup requests per route (default: 2)\n` +
203
+ ` --format <fmt> text or json\n`);
204
+ }
205
+ main().then((code) => { process.exitCode = code; }, (error) => {
206
+ stderr.write(`${message(error)}\n`);
207
+ process.exitCode = 2;
208
+ });
209
+ //# sourceMappingURL=harness-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-cli.js","sourceRoot":"","sources":["../src/harness-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EACL,wBAAwB,GAEzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GAGrB,MAAM,gCAAgC,CAAC;AAIxC,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACzD,SAAS,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,CAAC,KAAK,CAAC,uBAAuB,OAAO,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,OAA0B;IAClD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;YACtB,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC;YAClB,OAAO,EAAE;gBACP,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC7B,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACxB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC3C,gBAAgB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBACrC,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;aACtC;SACF,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,gBAAgB,EAAE,CAAC;QACnB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,MAAM,CAAC,SAAS,IAAI,aAAa,CAAC,CAAC;IAC9E,MAAM,UAAU,GAAG,wBAAwB,CAAC,GAAG,CAAC;QAC9C,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,GAAG,UAAU,CAAC,WAAW,CAAC;SAC9D,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,CAAC,SAAS,IAAI,aAAa,IAAI,CAAC,CAAC;QAChF,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzE,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC;QAChE,MAAM,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACnD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE;QAC1D,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,MAAM,CAAC,gBAAgB,CAAC,KAAK,IAAI;QAChD,aAAa,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;KACpC,CAAC,CAAC;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,OAA0B;IAC/C,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;YACtB,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC;YAClB,OAAO,EAAE;gBACP,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACzC,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC9B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;aACtC;SACF,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,aAAa,EAAE,CAAC;QAChB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;QACvE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACvC,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,SAAS,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7E,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QACtC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,8DAA8D,CAAC,CAAC;QAC7E,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAA2B,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,mBAAmB,CAAC;YACjC,OAAO;YACP,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC;YAChD,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC;SACjD,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,CAAC,CAAC;IACX,CAAC;IACD,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,eAAe,CAAC,MAA2B,EAAE,MAAc;IAClE,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO;IACT,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACrC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAC1B,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,8BAA8B,CAAC;QAClE,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC;IACtD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,6BAA6B,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,CAAC,KAAK,CACV,SAAS,MAAM,CAAC,OAAO,CAAC,KAAK,aAAa,MAAM,CAAC,OAAO,CAAC,QAAQ,GAAG;QAClE,YAAY,MAAM,CAAC,OAAO,CAAC,gBAAgB,gBAAgB,MAAM,CAAC,aAAa,IAAI,CACtF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,MAA2B,EAAE,MAAc;IACjE,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO;IACT,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,MAAM,CAAC,KAAK,CACV,GAAG,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;YACxD,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,GAAG,IAAI,IAAI,CAC/D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAA6B,EAAE,MAAc;IAC/D,IAAI,MAAM,KAAK,MAAM;QAAE,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;;QAC3E,KAAK,MAAM,KAAK,IAAI,MAAM;YAAE,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;AACnG,CAAC;AAED,SAAS,YAAY,CAAC,GAAuB;IAC3C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7D,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,GAAG,CAAC;IACjD,MAAM,IAAI,KAAK,CAAC,sCAAsC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,aAAa,CAAC,GAAuB,EAAE,IAAY;IAC1D,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,gCAAgC,CAAC,CAAC;IACnG,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,SAAS;IAChB,MAAM,CAAC,KAAK,CAAC,kDAAkD;QAC7D,oDAAoD;QACpD,yEAAyE;QACzE,0EAA0E,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,gBAAgB;IACvB,MAAM,CAAC,KAAK,CAAC,6CAA6C;QACxD,6DAA6D;QAC7D,sEAAsE;QACtE,8EAA8E;QAC9E,uDAAuD;QACvD,qCAAqC,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,aAAa;IACpB,MAAM,CAAC,KAAK,CAAC,0CAA0C;QACrD,+CAA+C;QAC/C,6DAA6D;QAC7D,kEAAkE;QAClE,+DAA+D;QAC/D,qCAAqC,CAAC,CAAC;AAC3C,CAAC;AAED,IAAI,EAAE,CAAC,IAAI,CACT,CAAC,IAAI,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,EACtC,CAAC,KAAK,EAAE,EAAE;IACR,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "@aotter/mantle-runtime/testing";
2
+ //# sourceMappingURL=runtime-testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-testing.d.ts","sourceRoot":"","sources":["../src/runtime-testing.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "@aotter/mantle-runtime/testing";
2
+ //# sourceMappingURL=runtime-testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-testing.js","sourceRoot":"","sources":["../src/runtime-testing.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC"}
@@ -61,6 +61,14 @@ await runtime.bootInit();
61
61
 
62
62
  `bootInit()` runs canonical migrations, seeds `siteDefaults`, and validates the manifest set. Call it once before serving CMS traffic. The Cloudflare adapter's reference pattern is `packages/adapters/cloudflare/src/mount/bootRuntimeOnce.ts`.
63
63
 
64
+ Use purpose-shaped runtime surfaces for canonical data: `runtime.entryReader`
65
+ applies Schema index declarations, locale semantics, and the public `Entry`
66
+ projection; `runtime.siteConfig` owns site settings. `runtime.db` remains only
67
+ for source compatibility and is deprecated. An adapter that owns additional
68
+ tables should retain the `DatabaseDriver` it injected instead of reaching back
69
+ through the assembled runtime. Authoring continues through the pre-wired
70
+ content use cases.
71
+
64
72
  ## HTTP and MCP surfaces
65
73
 
66
74
  The runtime is a library, not an HTTP server. A new adapter must mount equivalent framework routes:
@@ -0,0 +1,104 @@
1
+ # Data access, cache policy, and performance harness
2
+
3
+ Mantle site code declares intent; Core owns the shared storage layout and the
4
+ Cloudflare adapter owns provider bindings. A site-building agent should not
5
+ need Mantle table names, generated-column names, KV prefixes, or D1 APIs to
6
+ make a normal content/API/page change.
7
+
8
+ ## Ownership
9
+
10
+ | Read or state | Owner | Notes |
11
+ |---|---|---|
12
+ | Entry get/list and public slug/data-field/published reads | `DatabaseEntryRepository` through `EntryRepository` / `EntryReader` | Schema-aware field resolution is shared here. |
13
+ | Manifest View execution | `ExecuteViewUseCase` + `ViewSqlCompiler` | The deliberate compiled-query exception; it still resolves declared Schema indexes. |
14
+ | Editable settings and code-owned locale/media policy | `DatabaseSiteConfigRepository` | Editable values and dynamic media tool policy are read fresh; boot-seeded locale policy may be memoized within the runtime instance. |
15
+ | Pending media uploads | `DatabasePendingUploadRepository` | Canonical, read-after-write D1 state; never publish-cache state. |
16
+ | Rendered HTML, Markdown, and `llms.txt` | `HtmlPublishOrchestrator` plus the Cloudflare public-route cache policy | Reproducible derivatives live in KV. Settings updates invalidate through a runtime use case. |
17
+ | D1/KV transport and optional query metrics | Cloudflare bindings | Bindings stay thin. Query/cache policy does not belong in a generic provider `BaseRepository`. |
18
+
19
+ `CmsRuntime.db` remains deprecated compatibility surface. New site code uses
20
+ Manifests, runtime use cases, `entryReader`, and `siteConfig`. A site may own
21
+ additional tables behind its own repository at the composition root, but it
22
+ must not query Mantle-owned tables through `runtime.db`.
23
+
24
+ ## Cache contract
25
+
26
+ - D1 is canonical for entries, site settings, media metadata, and pending
27
+ uploads. KV contains only reproducible public artifacts.
28
+ - A public KV hit checks the cache before loading full editable site settings.
29
+ Locale policy is the small boot-seeded exception. A warm entry/page artifact
30
+ therefore performs zero D1 queries.
31
+ - A safe cache miss renders from canonical state and schedules KV write-back
32
+ with the request execution context. It waits inline only when no execution
33
+ context exists, such as a direct unit call.
34
+ - Site-setting writes call the runtime settings use case, which completes
35
+ public-artifact invalidation before reporting success. HTTP routes do not
36
+ scan/delete KV prefixes themselves.
37
+ - Do not cache every repository read. Cross-isolate correctness for editable
38
+ data wins unless a read has a measured hot-path contract and explicit
39
+ invalidation.
40
+
41
+ ## Index coverage
42
+
43
+ The Node harness uses the real canonical migrations, generated Schema DDL,
44
+ real View compiler, deterministic skewed rows, and SQLite
45
+ `EXPLAIN QUERY PLAN`:
46
+
47
+ ```bash
48
+ pnpm exec mantle-harness indexes --manifests ./manifests --format text
49
+ pnpm exec mantle-harness indexes --require-public --format json
50
+ pnpm exec mantle-harness indexes --require account-members --format json
51
+ ```
52
+
53
+ Without `--require-public` or `--require`, findings are advisory. A required
54
+ path fails on an `entries` table scan, a temporary ORDER BY B-tree, or a
55
+ data-field predicate/order that does not use a declared Schema index.
56
+ Projection alone does not require an index. `mantle validate` remains a pure
57
+ correctness check; no performance grammar or manifest atom was added.
58
+
59
+ Use the machine report in CI. It includes the compiled SQL and parameters,
60
+ query-plan details, named indexes, scan/sort flags, result count, SQLite
61
+ version, fixture row count, and required-failure summary.
62
+
63
+ ## Worker/API/page sampling
64
+
65
+ Sample any running environment with the public HTTP helper:
66
+
67
+ ```bash
68
+ pnpm exec mantle-harness http \
69
+ --base-url http://127.0.0.1:8787 \
70
+ --route recent=/api/views/recent-posts \
71
+ --route page=/en/posts/hello \
72
+ --rounds 20 --warmup 2 --format json
73
+ ```
74
+
75
+ Timing always reports p50/p95/max. A test-only Worker wrapper may also return
76
+ `x-mantle-query-count` and `x-mantle-rows-read`; those become distributions in
77
+ the same report. Do not expose these diagnostic headers in production.
78
+
79
+ Core CI runs `pnpm bench:wrangler` against real Wrangler-local D1, KV, Worker
80
+ HTTP routing, View execution, and live page rendering. It compares 100 and
81
+ 10,000 row fixtures, then samples page MISS and HIT separately. CI gates
82
+ row-read scaling, endpoint query budgets, and zero-D1 warm hits, not absolute
83
+ milliseconds.
84
+
85
+ ## Seven findings: measured disposition
86
+
87
+ Measured on the deterministic 2026-08-01 Wrangler-local fixture; timings are
88
+ diagnostic, while query/row counts are the stable assertions.
89
+
90
+ | Finding | Disposition |
91
+ |---|---|
92
+ | Public KV hits read D1 first | Fixed. A 10,000-row warm page measured 0 queries / 0 rows read. |
93
+ | Slug/locale reads bypass generated indexes | Fixed by the shared schema-aware entry-read boundary. A 10,000-row page MISS measured 2 queries / 5 rows read. |
94
+ | OFFSET pagination | Accepted for the v0.1 bounded-result surfaces: every response is capped at 500 rows and public hot paths must stay shallow. Deep/export workloads require a purpose-shaped cursor API before they are declared hot. |
95
+ | Admin substring search scans | Accepted only for the authenticated Admin collection browser, with a 500-row response cap. Large/search-heavy sites should add a purpose-shaped indexed View or dedicated search service; do not expose this scan publicly. |
96
+ | Published list/sitemap/llms paths lack system indexes | Fixed with measured partial indexes for published global, locale, collection, and collection+locale ordering. The 100-row and 10,000-row API runs both measured 1 query / 20 rows read. |
97
+ | Page MISS waits for KV write-back | Fixed. Reproducible artifacts write through `waitUntil`; regression coverage proves response completion does not await KV. |
98
+ | Benchmark stops at fake in-process dispatch | Fixed by the Node planner and Wrangler-local Worker/API/page layers. The old dispatch microbenchmark remains a narrow CPU signal only. |
99
+
100
+ The retained OFFSET and substring-search trade-offs are visible exceptions,
101
+ not patterns for new public APIs. Re-measure before widening either scope.
102
+
103
+ See also [Schema indexes](./schema-indexes.md) and the official Cloudflare
104
+ [D1 index guidance](https://developers.cloudflare.com/d1/best-practices/use-indexes/).
@@ -141,7 +141,20 @@ site still owns actor resolution, account rules, aggregation, and output.
141
141
 
142
142
  ## Verify a plan
143
143
 
144
- Inspect the real database when adding or changing a hot path:
144
+ Prefer the shipped crowded-data harness for a manifest View:
145
+
146
+ ```bash
147
+ pnpm exec mantle-harness indexes --require-public --format text
148
+ ```
149
+
150
+ It applies Mantle's real canonical migrations and generated DDL, seeds skewed
151
+ rows, compiles and executes the real View SQL, then records
152
+ `EXPLAIN QUERY PLAN`. By default findings are advisory; use `--require-public`
153
+ or repeat `--require <view-name>` only for paths whose performance is part of
154
+ the contract. See [the performance harness](./performance-harness.md).
155
+
156
+ For site-owned SQL that cannot be represented by a View, inspect the real
157
+ database directly when adding or changing a hot path:
145
158
 
146
159
  ```sql
147
160
  PRAGMA index_list("entries");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aotter/mantle",
3
- "version": "0.0.11-alpha.61",
3
+ "version": "0.0.11-alpha.63",
4
4
  "description": "Umbrella entry for @aotter/mantle. Adopters install this one package and import from subpaths: /spec, /runtime, /cloudflare, /admin-ui. Sub-packages remain individually installable on npm for tooling / alt-adapter authors. The Netlify adapter ships as a private workspace stub in v0.1 — its subpath will be added when the impl lands in v0.2.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://mantle.tools/",
@@ -13,6 +13,9 @@
13
13
  "sideEffects": false,
14
14
  "main": "./dist/spec.js",
15
15
  "types": "./dist/spec.d.ts",
16
+ "bin": {
17
+ "mantle-harness": "./dist/harness-cli.js"
18
+ },
16
19
  "publishConfig": {
17
20
  "access": "public",
18
21
  "tag": "alpha",
@@ -31,6 +34,10 @@
31
34
  "types": "./dist/runtime.d.ts",
32
35
  "import": "./dist/runtime.js"
33
36
  },
37
+ "./runtime/testing": {
38
+ "types": "./dist/runtime-testing.d.ts",
39
+ "import": "./dist/runtime-testing.js"
40
+ },
34
41
  "./cloudflare": {
35
42
  "types": "./dist/cloudflare.d.ts",
36
43
  "import": "./dist/cloudflare.js"
@@ -47,10 +54,10 @@
47
54
  "README.md"
48
55
  ],
49
56
  "dependencies": {
50
- "@aotter/mantle-admin-ui": "0.0.11-alpha.61",
51
- "@aotter/mantle-cloudflare": "0.0.11-alpha.61",
52
- "@aotter/mantle-spec": "0.0.11-alpha.61",
53
- "@aotter/mantle-runtime": "0.0.11-alpha.61"
57
+ "@aotter/mantle-cloudflare": "0.0.11-alpha.63",
58
+ "@aotter/mantle-runtime": "0.0.11-alpha.63",
59
+ "@aotter/mantle-admin-ui": "0.0.11-alpha.63",
60
+ "@aotter/mantle-spec": "0.0.11-alpha.63"
54
61
  },
55
62
  "peerDependencies": {
56
63
  "@cloudflare/workers-oauth-provider": "^0.8.0",
@@ -117,11 +117,49 @@ Do not assume Cloudflare unless the project imports `@aotter/mantle/cloudflare`
117
117
  or its adapter config is visible. A future Netlify adapter should satisfy the
118
118
  same Core workflow through its own ports and provider setup.
119
119
 
120
+ Site code is a consumer of this abstraction. Use Manifests, runtime use cases,
121
+ `entryReader`, and `siteConfig`; do not query Mantle-owned `entries` or
122
+ `site_config`, reach through deprecated `runtime.db`, copy generated-column
123
+ names, or construct SDK KV keys. Cloudflare bindings belong only at the
124
+ composition root. If a normal feature cannot be expressed through a
125
+ purpose-shaped surface, treat that as a Core abstraction gap instead of
126
+ teaching the project Mantle internals.
127
+
128
+ ## Performance Loop
129
+
130
+ After changing a Schema index, View filter/order, public API, or rendered page,
131
+ run the project's index check when present. Otherwise run the installed
132
+ harness directly:
133
+
134
+ ```bash
135
+ pnpm exec mantle-harness indexes --require-public --format text
136
+ ```
137
+
138
+ The check uses crowded real SQLite and the shipped compiler. It complements
139
+ `pnpm validate`; it does not replace correctness validation. Declare the
140
+ smallest ordered index justified by the measured path and respect SQLite's
141
+ leftmost-prefix rule. Do not change user-visible filter or ordering semantics
142
+ just to make the gate pass. Do not add every permutation or cache every read.
143
+
144
+ For relevant Cloudflare serving changes, start the project and sample the
145
+ actual routes:
146
+
147
+ ```bash
148
+ pnpm exec mantle-harness http \
149
+ --base-url http://127.0.0.1:8787 \
150
+ --route page=/en/example \
151
+ --rounds 20 --warmup 2 --format text
152
+ ```
153
+
154
+ Prefer query plan, query count, `rows_read` scaling, and cache MISS/HIT
155
+ evidence. Do not create CI gates from absolute local milliseconds.
156
+
120
157
  ## Loop
121
158
 
122
159
  ```bash
123
160
  pnpm install --frozen-lockfile
124
161
  pnpm validate
162
+ pnpm check:indexes # when the project provides it
125
163
  pnpm typecheck
126
164
  pnpm check
127
165
  ```