@geraldmaron/construct 1.0.23 → 1.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.
- package/README.md +0 -2
- package/bin/construct +189 -207
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- package/lib/beads-client.mjs +9 -0
- package/lib/beads-optimistic.mjs +23 -71
- package/lib/bridges/copilot-proxy.mjs +116 -0
- package/lib/cli-commands.mjs +5 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.mjs +1 -1
- package/lib/document-extract/docling-client.mjs +16 -6
- package/lib/document-extract/docling-sidecar.py +32 -2
- package/lib/document-extract.mjs +37 -10
- package/lib/document-ingest.mjs +90 -5
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/host-capabilities.mjs +30 -0
- package/lib/ingest/docling-remote.mjs +90 -0
- package/lib/ingest/strategy.mjs +1 -1
- package/lib/init-unified.mjs +9 -13
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/ollama/capability-store.mjs +78 -0
- package/lib/ollama/provision-context.mjs +344 -0
- package/lib/opencode-config.mjs +148 -0
- package/lib/opencode-telemetry.mjs +7 -0
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/orchestration-policy.mjs +41 -6
- package/lib/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +12 -8
- package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
- package/lib/reconcile/index.mjs +0 -2
- package/lib/reflect/extractor.mjs +14 -1
- package/lib/reflect/salience.mjs +65 -0
- package/lib/rules-delivery.mjs +122 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -17
- package/lib/service-manager.mjs +79 -259
- package/lib/setup.mjs +44 -425
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- package/lib/storage/embeddings-engine.mjs +19 -5
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/personas/construct.md +1 -1
- package/platforms/capabilities.json +76 -0
- package/platforms/claude/settings.template.json +0 -7
- package/platforms/opencode/sync-config.mjs +121 -25
- package/rules/common/neurodivergent-output.md +1 -1
- package/rules/web/coding-style.md +8 -0
- package/rules/web/design-quality.md +8 -0
- package/rules/web/hooks.md +8 -0
- package/rules/web/patterns.md +8 -0
- package/rules/web/performance.md +8 -0
- package/rules/web/security.md +8 -0
- package/rules/web/testing.md +8 -0
- package/scripts/sync-specialists.mjs +174 -40
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-qa.md +1 -1
- package/specialists/prompts/cx-test-automation.md +12 -0
- package/specialists/registry.json +0 -8
- package/templates/docs/construct_guide.md +1 -1
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/reconcile/postgres-namespace.mjs +0 -102
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/unified-storage.mjs +0 -550
- package/lib/storage/vector-store.mjs +0 -100
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../schemas/platform-capabilities.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"hosts": {
|
|
5
|
+
"claude": {
|
|
6
|
+
"displayName": "Claude Code",
|
|
7
|
+
"hasNativeSubagents": false,
|
|
8
|
+
"instructionsOnly": false,
|
|
9
|
+
"supportsMcp": true,
|
|
10
|
+
"configFormat": "json",
|
|
11
|
+
"localModelProvisioning": "none",
|
|
12
|
+
"hooks": {
|
|
13
|
+
"supported": true,
|
|
14
|
+
"globalAllowlist": [
|
|
15
|
+
"pre:bash:block-no-verify",
|
|
16
|
+
"pre:bash:guard-dangerous",
|
|
17
|
+
"pre:edit:config-protection",
|
|
18
|
+
"pre:edit-guard",
|
|
19
|
+
"post:edit:json-validate",
|
|
20
|
+
"post:edit:scan-secrets"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"globalMcpAllowlist": ["context7"]
|
|
24
|
+
},
|
|
25
|
+
"codex": {
|
|
26
|
+
"displayName": "Codex",
|
|
27
|
+
"hasNativeSubagents": false,
|
|
28
|
+
"instructionsOnly": false,
|
|
29
|
+
"supportsMcp": true,
|
|
30
|
+
"configFormat": "toml",
|
|
31
|
+
"localModelProvisioning": "none",
|
|
32
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
33
|
+
"globalMcpAllowlist": []
|
|
34
|
+
},
|
|
35
|
+
"copilot": {
|
|
36
|
+
"displayName": "Copilot",
|
|
37
|
+
"hasNativeSubagents": false,
|
|
38
|
+
"instructionsOnly": true,
|
|
39
|
+
"supportsMcp": false,
|
|
40
|
+
"configFormat": "markdown",
|
|
41
|
+
"localModelProvisioning": "none",
|
|
42
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
43
|
+
"globalMcpAllowlist": []
|
|
44
|
+
},
|
|
45
|
+
"opencode": {
|
|
46
|
+
"displayName": "OpenCode",
|
|
47
|
+
"hasNativeSubagents": true,
|
|
48
|
+
"instructionsOnly": false,
|
|
49
|
+
"supportsMcp": true,
|
|
50
|
+
"configFormat": "json",
|
|
51
|
+
"localModelProvisioning": "modelfile",
|
|
52
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
53
|
+
"globalMcpAllowlist": []
|
|
54
|
+
},
|
|
55
|
+
"vscode": {
|
|
56
|
+
"displayName": "VS Code",
|
|
57
|
+
"hasNativeSubagents": true,
|
|
58
|
+
"instructionsOnly": false,
|
|
59
|
+
"supportsMcp": true,
|
|
60
|
+
"configFormat": "json",
|
|
61
|
+
"localModelProvisioning": "none",
|
|
62
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
63
|
+
"globalMcpAllowlist": []
|
|
64
|
+
},
|
|
65
|
+
"cursor": {
|
|
66
|
+
"displayName": "Cursor",
|
|
67
|
+
"hasNativeSubagents": true,
|
|
68
|
+
"instructionsOnly": false,
|
|
69
|
+
"supportsMcp": true,
|
|
70
|
+
"configFormat": "json",
|
|
71
|
+
"localModelProvisioning": "none",
|
|
72
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
73
|
+
"globalMcpAllowlist": []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* sync-config.mjs — Fetch free models from OpenRouter and
|
|
4
|
-
*
|
|
3
|
+
* sync-config.mjs — Fetch free models from OpenRouter and local models from Ollama,
|
|
4
|
+
* and sync into OpenCode config.
|
|
5
5
|
*/
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
-
import {
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { readOpenCodeConfig, writeOpenCodeConfig, findOpenCodeConfigPath } from "../../lib/opencode-config.mjs";
|
|
9
|
+
import { ensureLocalContextVariants, modelDigest } from "../../lib/ollama/provision-context.mjs";
|
|
10
|
+
import { isKnownCollapsed } from "../../lib/ollama/capability-store.mjs";
|
|
11
|
+
|
|
12
|
+
const LOCAL_NUM_CTX = Number(process.env.CONSTRUCT_LOCAL_NUM_CTX) || 32768;
|
|
8
13
|
|
|
9
14
|
const configPath = findOpenCodeConfigPath();
|
|
10
15
|
|
|
16
|
+
async function fetchLocalOllamaModels() {
|
|
17
|
+
try {
|
|
18
|
+
const r = spawnSync("ollama", ["list"], { encoding: "utf8" });
|
|
19
|
+
if (r.status !== 0) return [];
|
|
20
|
+
|
|
21
|
+
const lines = r.stdout.trim().split("\n").slice(1);
|
|
22
|
+
const models = lines.map(line => {
|
|
23
|
+
const parts = line.split(/\s+/).filter(Boolean);
|
|
24
|
+
const id = parts[0];
|
|
25
|
+
const size = parts[2];
|
|
26
|
+
return { id, name: id, size };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Identify redundant tags (same family, many tags)
|
|
30
|
+
const familyMap = new Map();
|
|
31
|
+
for (const m of models) {
|
|
32
|
+
const family = m.id.split(":")[0];
|
|
33
|
+
if (!familyMap.has(family)) familyMap.set(family, []);
|
|
34
|
+
familyMap.get(family).push(m);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [family, tags] of familyMap) {
|
|
38
|
+
if (tags.length > 2) {
|
|
39
|
+
console.log(`[cleanup] Potential redundant tags for ${family}: ${tags.map(t => t.id).join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return models;
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
async function fetchFreeModels() {
|
|
12
50
|
try {
|
|
13
51
|
const res = await fetch("https://openrouter.ai/api/v1/models");
|
|
@@ -31,39 +69,97 @@ async function main() {
|
|
|
31
69
|
|
|
32
70
|
const { config } = readOpenCodeConfig();
|
|
33
71
|
const freeModels = await fetchFreeModels();
|
|
72
|
+
const localModels = await fetchLocalOllamaModels();
|
|
34
73
|
|
|
35
|
-
if (freeModels.length === 0) {
|
|
36
|
-
console.log("No
|
|
74
|
+
if (freeModels.length === 0 && localModels.length === 0) {
|
|
75
|
+
console.log("No models fetched. Config unchanged.");
|
|
37
76
|
return;
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
if (!config.provider) config.provider = {};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
|
|
81
|
+
// Sync OpenRouter Free Models
|
|
82
|
+
if (freeModels.length > 0) {
|
|
83
|
+
if (!config.provider.openrouter) {
|
|
84
|
+
config.provider.openrouter = {
|
|
85
|
+
npm: "@ai-sdk/openai-compatible",
|
|
86
|
+
name: "OpenRouter",
|
|
87
|
+
options: {
|
|
88
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
89
|
+
headers: {}
|
|
90
|
+
},
|
|
91
|
+
models: {}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const existingOR = config.provider.openrouter.models ?? {};
|
|
95
|
+
for (const model of freeModels) {
|
|
96
|
+
const cleanName = model.name.replace(/\s*\(free\)/gi, "").trim();
|
|
97
|
+
existingOR[model.id] = { name: `[free] ${cleanName}` };
|
|
98
|
+
}
|
|
99
|
+
config.provider.openrouter.models = Object.fromEntries(
|
|
100
|
+
Object.entries(existingOR).sort((a, b) => (a[1].name ?? a[0]).localeCompare(b[1].name ?? b[0]))
|
|
101
|
+
);
|
|
51
102
|
}
|
|
52
103
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
104
|
+
// Sync Local Ollama Models
|
|
105
|
+
if (localModels.length > 0) {
|
|
106
|
+
if (!config.provider.ollama) {
|
|
107
|
+
config.provider.ollama = {
|
|
108
|
+
npm: "@ai-sdk/openai-compatible",
|
|
109
|
+
name: "Ollama",
|
|
110
|
+
options: {
|
|
111
|
+
baseURL: "http://localhost:11434/v1"
|
|
112
|
+
},
|
|
113
|
+
models: {}
|
|
114
|
+
};
|
|
58
115
|
}
|
|
59
|
-
|
|
116
|
+
// A raw model runs at Ollama's 4096 default over the /v1 path, which a Construct
|
|
117
|
+
// session's tool schemas overrun. Provision a context-extended Modelfile variant
|
|
118
|
+
// for each tool-capable model lacking a baked num_ctx (any size — capability does
|
|
119
|
+
// not track size) and register the variant in place of the raw tag so the model
|
|
120
|
+
// OpenCode actually talks to has a real context window.
|
|
121
|
+
|
|
122
|
+
const { mapping: variantMap, actions } = ensureLocalContextVariants({ numCtx: LOCAL_NUM_CTX });
|
|
123
|
+
for (const a of actions) {
|
|
124
|
+
if (a.action === "created" || a.action === "would-create") console.log(`[ollama] context variant ${a.action}: ${a.variant} (num_ctx ${LOCAL_NUM_CTX})`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const existingLocal = config.provider.ollama.models ?? {};
|
|
128
|
+
for (const model of localModels) {
|
|
129
|
+
const registerId = variantMap[model.id] || model.id;
|
|
130
|
+
// Register the context-extended variant in place of the raw tag so the model
|
|
131
|
+
// picker surfaces the windowed option, not the one Ollama serves at 4096.
|
|
60
132
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
133
|
+
if (registerId !== model.id) delete existingLocal[model.id];
|
|
134
|
+
existingLocal[registerId] = {
|
|
135
|
+
name: registerId.replace(/\s*\(local\)/gi, "").trim(),
|
|
136
|
+
family: registerId.includes("qwen") ? "qwen2" : "llama",
|
|
137
|
+
tool_call: true
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Capability honesty: a model the probe recorded as COLLAPSED (digest still
|
|
141
|
+
// matching) word-salads on the agentic loop. Warn rather than hide — the
|
|
142
|
+
// user keeps the choice and can re-probe — so a stale or false verdict never
|
|
143
|
+
// silently strands a working model.
|
|
144
|
+
if (isKnownCollapsed(model.id, modelDigest(model.id))) {
|
|
145
|
+
console.log(`[ollama] WARNING: ${model.id} probed COLLAPSED — not agentic-capable. Re-probe: construct doctor --probe-local`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
config.provider.ollama.models = Object.fromEntries(
|
|
149
|
+
Object.entries(existingLocal).sort((a, b) => (a[1].name ?? a[0]).localeCompare(b[1].name ?? b[0]))
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Agentic coherence is model-specific and not predictable from size: some small
|
|
153
|
+
// coder models collapse into repetition on an agentic system prompt while others
|
|
154
|
+
// run the same Construct payload cleanly. Surface the empirical probe rather than
|
|
155
|
+
// silently registering a model that will produce word salad.
|
|
156
|
+
|
|
157
|
+
console.log("[ollama] Verify a model handles agentic prompts before relying on it:");
|
|
158
|
+
console.log("[ollama] node lib/ollama/provision-context.mjs --probe --model=<id>");
|
|
159
|
+
}
|
|
64
160
|
|
|
65
161
|
writeOpenCodeConfig(config, configPath);
|
|
66
|
-
console.log(`Synced ${freeModels.length} free models
|
|
162
|
+
console.log(`Synced ${freeModels.length} free models and ${localModels.length} local models.`);
|
|
67
163
|
}
|
|
68
164
|
|
|
69
165
|
main();
|
|
@@ -28,7 +28,7 @@ Use headings in order (H2, then H3) and do not skip or nest deeper. One `h1`-equ
|
|
|
28
28
|
|
|
29
29
|
## 3. Scannable chunks
|
|
30
30
|
|
|
31
|
-
Short paragraphs.
|
|
31
|
+
Short paragraphs with white space between sections; prefer three short lines over one dense one. Use a bulleted or numbered list for genuinely parallel items — a set of options, steps, or independent facts — where scanning is the point. Keep reasoning, cause-and-effect, and narrative in prose: a wall of bullets fragments the logic that connects the points and is harder to follow, not easier. Bullets for what scans, sentences for what reasons.
|
|
32
32
|
|
|
33
33
|
## 4. Plain, literal language
|
|
34
34
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web coding style rule. Use when writing or reviewing web code that involves coding style.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/coding-style.md](../common/coding-style.md) with web-specific frontend content.
|
|
5
13
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web design quality rule. Use when writing or reviewing web code that involves design quality.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/patterns.md](../common/patterns.md) with web-specific design-quality guidance.
|
|
5
13
|
|
package/rules/web/hooks.md
CHANGED
package/rules/web/patterns.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web patterns rule. Use when writing or reviewing web code that involves patterns.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/patterns.md](../common/patterns.md) with web-specific patterns.
|
|
5
13
|
|
package/rules/web/performance.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web performance rule. Use when writing or reviewing web code that involves performance.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/performance.md](../common/performance.md) with web-specific performance content.
|
|
5
13
|
|
package/rules/web/security.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web security rule. Use when writing or reviewing web code that involves security.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/security.md](../common/security.md) with web-specific security content.
|
|
5
13
|
|
package/rules/web/testing.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web testing rule. Use when writing or reviewing web code that involves testing.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/testing.md](../common/testing.md) with web-specific testing content.
|
|
5
13
|
|