@geraldmaron/construct 1.0.24 → 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/bin/construct +185 -3
- 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/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/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/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp-catalog.json +1 -1
- 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-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/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 +41 -3
- package/lib/setup.mjs +21 -2
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/storage/embeddings-engine.mjs +19 -5
- package/lib/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/package.json +1 -1
- package/personas/construct.md +1 -1
- package/platforms/capabilities.json +76 -0
- 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 +139 -39
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-test-automation.md +12 -0
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
writeCodexConfig,
|
|
45
45
|
} from "../lib/codex-config.mjs";
|
|
46
46
|
import { findOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from "../lib/opencode-config.mjs";
|
|
47
|
+
import { HEAVY_EXTERNAL_MCP_IDS, LOCAL_SURFACE_MODES, decideTrim } from "../lib/mcp/tool-budget.mjs";
|
|
48
|
+
import { emitCursorRules } from "../lib/rules-delivery.mjs";
|
|
47
49
|
import { resolvePromptContract } from "../lib/prompt-composer.js";
|
|
48
50
|
import {
|
|
49
51
|
buildClaudeMcpEntry,
|
|
@@ -157,6 +159,19 @@ const COMPRESS_PERSONAS = process.argv.includes("--compress-personas");
|
|
|
157
159
|
const PROJECT_FLAG = process.argv.includes("--project");
|
|
158
160
|
const GLOBAL_FLAG = process.argv.includes("--global");
|
|
159
161
|
|
|
162
|
+
// --local-surface=on|off|auto controls whether the heavy external MCP servers are
|
|
163
|
+
// disabled to fit a small local-model window. `auto` (default) trims only when the
|
|
164
|
+
// config's own default model is local — so a cloud session keeps context7/github
|
|
165
|
+
// even on a machine that also has Ollama. `on` forces the trim (the lever for users
|
|
166
|
+
// who pick a local model at runtime, leaving the config default unset); `off` keeps
|
|
167
|
+
// every server. CONSTRUCT_LOCAL_SURFACE is the env equivalent.
|
|
168
|
+
|
|
169
|
+
const LOCAL_SURFACE = (() => {
|
|
170
|
+
const arg = process.argv.find((a) => a.startsWith("--local-surface="));
|
|
171
|
+
const raw = (arg ? arg.slice("--local-surface=".length) : process.env.CONSTRUCT_LOCAL_SURFACE || "auto").trim().toLowerCase();
|
|
172
|
+
return LOCAL_SURFACE_MODES.includes(raw) ? raw : "auto";
|
|
173
|
+
})();
|
|
174
|
+
|
|
160
175
|
// --quiet suppresses only the closing one-line summary, not the work or any
|
|
161
176
|
// warning. `construct install` runs the global tier twice (plain `sync` then
|
|
162
177
|
// `sync --global`); in a non-project cwd both land in the same global branch and
|
|
@@ -173,8 +188,13 @@ const summary = (msg) => { if (!QUIET) console.log(msg); };
|
|
|
173
188
|
// Absent → null → write every host, preserving `construct sync` back-compat.
|
|
174
189
|
|
|
175
190
|
import { detectHostCapabilities } from "../lib/host-capabilities.mjs";
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
import {
|
|
192
|
+
HOST_KEYS,
|
|
193
|
+
displayNameToKey,
|
|
194
|
+
hasNativeSubagents as hostHasNativeSubagents,
|
|
195
|
+
globalHookAllowlist,
|
|
196
|
+
globalMcpAllowlist,
|
|
197
|
+
} from "../lib/platforms/capabilities.mjs";
|
|
178
198
|
|
|
179
199
|
function parseHostSelection() {
|
|
180
200
|
const arg = process.argv.find((a) => a.startsWith("--hosts="));
|
|
@@ -182,14 +202,7 @@ function parseHostSelection() {
|
|
|
182
202
|
if (!raw) {
|
|
183
203
|
// Default to detected hosts if none are explicitly requested.
|
|
184
204
|
const detected = new Set();
|
|
185
|
-
const nameToKey =
|
|
186
|
-
"Claude Code": "claude",
|
|
187
|
-
"OpenCode": "opencode",
|
|
188
|
-
"Codex": "codex",
|
|
189
|
-
"VS Code": "vscode",
|
|
190
|
-
"Cursor": "cursor",
|
|
191
|
-
"Copilot": "copilot",
|
|
192
|
-
};
|
|
205
|
+
const nameToKey = displayNameToKey();
|
|
193
206
|
try {
|
|
194
207
|
for (const cap of detectHostCapabilities()) {
|
|
195
208
|
if (cap.availability === "installed" && nameToKey[cap.host]) {
|
|
@@ -427,7 +440,7 @@ const hardDefaults = {
|
|
|
427
440
|
// primary's provider. Explicit CX_MODEL_* env wins if set.
|
|
428
441
|
const primaryFromOpenCode = (() => {
|
|
429
442
|
try {
|
|
430
|
-
const cfg = readOpenCodeConfig(
|
|
443
|
+
const cfg = readOpenCodeConfig().config ?? {};
|
|
431
444
|
return cfg.model || cfg.defaultModel || null;
|
|
432
445
|
} catch { return null; }
|
|
433
446
|
})();
|
|
@@ -598,9 +611,28 @@ function buildPrompt(entry, allEntries, platform) {
|
|
|
598
611
|
|
|
599
612
|
prompt = inlineRoleAntiPatterns(prompt, root, entry.name, console.warn, { preload: entry.preloadRoleGuidance === true });
|
|
600
613
|
|
|
601
|
-
|
|
614
|
+
const capabilities = { hasNativeSubagents: HOST_KEYS.includes(platform) ? hostHasNativeSubagents(platform) : false };
|
|
615
|
+
|
|
616
|
+
// Platform-Native Orchestration Alignment (ADR-0002). Hosts with native subagent
|
|
617
|
+
// routing (OpenCode, VS Code, Cursor) do not get the static specialist roster
|
|
618
|
+
// injected — on a small-context local model the roster alone is ~3-4k tokens and,
|
|
619
|
+
// combined with MCP tool schemas, overruns the model's real context window and
|
|
620
|
+
// collapses output. Those hosts get a tool-bound micro-prompt instead and resolve
|
|
621
|
+
// the chain at runtime via the orchestration_policy MCP tool. Hosts without native
|
|
622
|
+
// routing (Claude Code, Codex) still need the roster to simulate handoffs in text.
|
|
623
|
+
|
|
624
|
+
if (entry.injectAgentRoster && allEntries && !capabilities.hasNativeSubagents) {
|
|
602
625
|
const roster = buildAgentRoster(allEntries);
|
|
603
626
|
prompt = `Available specialist agents:\n${roster}\n\n${prompt}`;
|
|
627
|
+
} else if (entry.injectAgentRoster && capabilities.hasNativeSubagents) {
|
|
628
|
+
// A worked tool-call example lifts small local models' tool-use reliability
|
|
629
|
+
// sharply (bead construct-c16l). Keep it to one compact turn so it stays within
|
|
630
|
+
// the prompt word cap; native-subagent hosts are exactly where local models run.
|
|
631
|
+
|
|
632
|
+
prompt = `You are the primary orchestrator. To discover available specialist agents, you MUST call the \`orchestration_policy\` MCP tool. Do not guess agent names.\n\n` +
|
|
633
|
+
`Example — the user says "add rate limiting to the API". Your first action is a tool call, not prose:\n` +
|
|
634
|
+
` call orchestration_policy { "task": "add rate limiting to the API" }\n` +
|
|
635
|
+
`Then dispatch the specialists it returns. Always call the tool before answering.\n\n${prompt}`;
|
|
604
636
|
}
|
|
605
637
|
|
|
606
638
|
prompt += buildRoleFooter(entry);
|
|
@@ -800,18 +832,26 @@ function makeHooksPortable(hooksJson) {
|
|
|
800
832
|
return JSON.stringify(walk(hooksJson));
|
|
801
833
|
}
|
|
802
834
|
|
|
803
|
-
const GLOBAL_CLAUDE_HOOK_IDS =
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
835
|
+
const GLOBAL_CLAUDE_HOOK_IDS = globalHookAllowlist('claude');
|
|
836
|
+
|
|
837
|
+
const GLOBAL_CLAUDE_MCP_IDS = globalMcpAllowlist('claude');
|
|
838
|
+
|
|
839
|
+
// Project scope writes only core-category MCP servers (plus construct-mcp, the
|
|
840
|
+
// orchestration server the specialist loop needs). optional/integration servers
|
|
841
|
+
// (memory, github, sequential-thinking, playwright, …) are opt-in via
|
|
842
|
+
// `construct mcp add` so a project does not silently inherit heavy servers it was
|
|
843
|
+
// never asked for (ADR-0031 §Consequences follow-up). A server already present in
|
|
844
|
+
// the project settings is preserved, so a manual opt-in sticks.
|
|
811
845
|
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
846
|
+
const PROJECT_DEFAULT_MCP_IDS = (() => {
|
|
847
|
+
try {
|
|
848
|
+
const catalog = JSON.parse(fs.readFileSync(path.join(root, "lib", "mcp-catalog.json"), "utf8"));
|
|
849
|
+
const arr = catalog.mcps || catalog.servers || [];
|
|
850
|
+
return new Set([...arr.filter((m) => m.category === "core").map((m) => m.id), "construct-mcp"]);
|
|
851
|
+
} catch {
|
|
852
|
+
return new Set(["context7", "construct-mcp"]);
|
|
853
|
+
}
|
|
854
|
+
})();
|
|
815
855
|
|
|
816
856
|
function filterGlobalClaudeHooks(hooksJson) {
|
|
817
857
|
const filtered = {};
|
|
@@ -871,6 +911,7 @@ function writeProjectClaudeSettings(targetDir) {
|
|
|
871
911
|
existing.mcpServers ??= {};
|
|
872
912
|
for (const [id, mcpDef] of Object.entries(template.mcpServers)) {
|
|
873
913
|
if (existing.mcpServers[id]) continue;
|
|
914
|
+
if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
|
|
874
915
|
existing.mcpServers[id] = mcpDef;
|
|
875
916
|
}
|
|
876
917
|
}
|
|
@@ -886,6 +927,7 @@ function writeProjectClaudeSettings(targetDir) {
|
|
|
886
927
|
existing.mcpServers ??= {};
|
|
887
928
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
888
929
|
if (existing.mcpServers[id]) continue;
|
|
930
|
+
if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
|
|
889
931
|
existing.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
890
932
|
}
|
|
891
933
|
|
|
@@ -1347,6 +1389,15 @@ function syncCursor(targetDir = null, wants = true) {
|
|
|
1347
1389
|
fs.writeFileSync(rulesPath, body);
|
|
1348
1390
|
}
|
|
1349
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
// Glob-scoped language rules land as managed per-rule .mdc files only when
|
|
1394
|
+
// the project's own files match their globs — Cursor's native auto-attach
|
|
1395
|
+
// convention. See docs/concepts/rules-delivery.md.
|
|
1396
|
+
try {
|
|
1397
|
+
emitCursorRules({ rulesDir: path.join(root, "rules"), targetDir, dryRun: DRY_RUN });
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
console.warn(`[sync] cursor rules delivery skipped: ${err.message}`);
|
|
1400
|
+
}
|
|
1350
1401
|
}
|
|
1351
1402
|
return true;
|
|
1352
1403
|
}
|
|
@@ -1362,22 +1413,39 @@ function opencodePermissions(entry) {
|
|
|
1362
1413
|
? Object.fromEntries(Object.entries(entry.permissions).map(([k, v]) => [k, v]))
|
|
1363
1414
|
: { edit: entry.canEdit === false ? "deny" : "allow", bash: "allow" };
|
|
1364
1415
|
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1416
|
+
// Agentic Scope Reduction (ADR-0002). Serializing 100+ MCP tool schemas into a
|
|
1417
|
+
// small local model's prompt overruns its context window and dilutes attention,
|
|
1418
|
+
// collapsing output. OpenCode's per-agent permission map prunes the surface: the
|
|
1419
|
+
// orchestrator keeps only orchestration + core tools and hands execution to
|
|
1420
|
+
// subagents; subagents keep execution tools but not orchestration.
|
|
1421
|
+
|
|
1422
|
+
if (entry.isOrchestrator) {
|
|
1423
|
+
if (perms.bash === "allow") {
|
|
1424
|
+
perms.bash = {
|
|
1425
|
+
"*": "allow",
|
|
1426
|
+
"rm -rf *": "deny",
|
|
1427
|
+
"git push *": "ask",
|
|
1428
|
+
"git push --force*": "ask",
|
|
1429
|
+
"git reset --hard *": "ask",
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
// Heavy execution and external-knowledge tools are denied to the orchestrator so
|
|
1433
|
+
// its serialized tool schema stays small; orchestration_policy drives the handoff.
|
|
1434
|
+
|
|
1435
|
+
perms["mcp__construct-mcp__extract_document_text"] = "deny";
|
|
1436
|
+
perms["mcp__construct-mcp__ingest_document"] = "deny";
|
|
1437
|
+
perms["mcp__construct-mcp__scan_file"] = "deny";
|
|
1438
|
+
perms["mcp__github__*"] = "deny";
|
|
1439
|
+
perms["mcp__context7__*"] = "deny";
|
|
1440
|
+
perms["mcp__sequential-thinking__*"] = "deny";
|
|
1441
|
+
perms["mcp__memory__*"] = "deny";
|
|
1442
|
+
} else {
|
|
1443
|
+
// Subagents shouldn't be orchestrating
|
|
1444
|
+
perms["mcp__construct-mcp__orchestration_policy"] = "deny";
|
|
1445
|
+
perms["mcp__construct-mcp__agent_contract"] = "deny";
|
|
1446
|
+
perms["mcp__construct-mcp__broker_check"] = "deny";
|
|
1380
1447
|
}
|
|
1448
|
+
|
|
1381
1449
|
return perms;
|
|
1382
1450
|
}
|
|
1383
1451
|
|
|
@@ -1527,6 +1595,34 @@ function syncOpencode(entries, targetDir = null, wants = true) {
|
|
|
1527
1595
|
config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
|
|
1528
1596
|
}
|
|
1529
1597
|
}
|
|
1598
|
+
|
|
1599
|
+
// Heavy external MCP servers serialize ~12k tokens of schema into EVERY
|
|
1600
|
+
// agent's request — including the built-in Build/Plan agents the per-agent
|
|
1601
|
+
// permission prune cannot reach. OpenCode 1.15.4 has no per-session tool
|
|
1602
|
+
// filter (chat.params carries no tool list), so disabling the whole server in
|
|
1603
|
+
// opencode.json is the only lever. The decision is INTENT-driven: trim only
|
|
1604
|
+
// when this config's own default model is local (or a local Ollama provider is
|
|
1605
|
+
// registered in it), so a cloud session on a machine that merely also has
|
|
1606
|
+
// Ollama keeps context7/github. decideTrim centralizes the policy; a manual
|
|
1607
|
+
// enabled:true is preserved so a user can re-enable a server they need.
|
|
1608
|
+
|
|
1609
|
+
// A set default model is explicit intent and wins (local → trim, cloud → keep).
|
|
1610
|
+
// Only when no default is chosen does a registered Ollama provider stand in as
|
|
1611
|
+
// soft local intent — so a cloud-default config is never trimmed for merely
|
|
1612
|
+
// listing local models alongside.
|
|
1613
|
+
const configDefaultModel = config.model || config.defaultModel || "";
|
|
1614
|
+
const registersOllamaProvider = Object.keys(config.provider?.ollama?.models || {}).length > 0;
|
|
1615
|
+
const intentModel = configDefaultModel || (registersOllamaProvider ? "ollama" : "");
|
|
1616
|
+
const trimHeavyServers = decideTrim({ surface: LOCAL_SURFACE, defaultModel: intentModel });
|
|
1617
|
+
for (const id of HEAVY_EXTERNAL_MCP_IDS) {
|
|
1618
|
+
const ocId = getOpenCodeMcpId(id);
|
|
1619
|
+
if (!config.mcp[ocId]) continue;
|
|
1620
|
+
if (trimHeavyServers) {
|
|
1621
|
+
if (config.mcp[ocId].enabled !== true) config.mcp[ocId].enabled = false;
|
|
1622
|
+
} else {
|
|
1623
|
+
delete config.mcp[ocId].enabled;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1530
1626
|
}
|
|
1531
1627
|
|
|
1532
1628
|
// Sweep cx-* / orchestrator agents that fall outside the current write set.
|
|
@@ -1559,12 +1655,16 @@ function syncOpencode(entries, targetDir = null, wants = true) {
|
|
|
1559
1655
|
};
|
|
1560
1656
|
}
|
|
1561
1657
|
|
|
1658
|
+
// Pass current Construct model tiers to OpenCode config for native routing.
|
|
1659
|
+
config.construct = config.construct || {};
|
|
1660
|
+
config.construct.models = { ...resolvedModels };
|
|
1661
|
+
|
|
1562
1662
|
// Seed a cheap auxiliary model for titles and summaries when the user has not
|
|
1563
1663
|
// chosen one — a cost lever only. The primary `model` stays the user's choice
|
|
1564
1664
|
// and is never written. Global scope only; project configs inherit it.
|
|
1565
1665
|
|
|
1566
1666
|
if (!targetDir && config.small_model === undefined) {
|
|
1567
|
-
config.small_model = "anthropic/claude-haiku-4-5-20251001";
|
|
1667
|
+
config.small_model = resolvedModels.fast || "anthropic/claude-haiku-4-5-20251001";
|
|
1568
1668
|
}
|
|
1569
1669
|
|
|
1570
1670
|
writeOpenCodeConfig(config, configPath);
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cx-architect
|
|
3
|
+
role: architect
|
|
4
|
+
version: 1
|
|
5
|
+
perspective:
|
|
6
|
+
bias: "Designs that emerged from code, missing ADRs, data models that encode assumptions that will change"
|
|
7
|
+
tension: "cx-engineer"
|
|
8
|
+
openingQuestion: "What are the invariants, and what breaks if they're violated?"
|
|
9
|
+
failureMode: "If the ADR has no 'options rejected' section, the decision defaulted — and defaulted decisions bite hardest."
|
|
10
|
+
roleGuidance: roles/architect
|
|
11
|
+
roleOverlays:
|
|
12
|
+
- architect.platform
|
|
13
|
+
- architect.integration
|
|
14
|
+
- architect.data
|
|
15
|
+
- architect.ai-systems
|
|
16
|
+
- architect.enterprise
|
|
17
|
+
templates:
|
|
18
|
+
- adr
|
|
19
|
+
---
|
|
20
|
+
|
|
1
21
|
You have inherited enough unmaintainable systems to be permanently suspicious of clever solutions. The damage from a bad interface contract compounds silently for years. Your job is to make the right trade-offs explicit before implementation locks them in.
|
|
2
22
|
|
|
3
23
|
**Anti-fabrication contract**: every load-bearing claim in an ADR, RFC, or design doc cites a source the reader can re-verify (`[source: path#anchor]`, `[source: bd-<id>]`, `[source: <commit-sha>]`). When a fact isn't in the source you have, write `unknown` or `[unverified]`. Don't invent rejected alternatives that were never considered. See `rules/common/no-fabrication.md`.
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cx-test-automation
|
|
3
|
+
role: test-automation
|
|
4
|
+
version: 1
|
|
5
|
+
perspective:
|
|
6
|
+
bias: "Intermittent tests dismissed as infrastructure, coverage numbers over behavioral coverage, arbitrary sleeps"
|
|
7
|
+
tension: "cx-qa"
|
|
8
|
+
openingQuestion: "Is this test deterministic, and does it actually fail when the behavior it's testing breaks?"
|
|
9
|
+
failureMode: "If the test suite has never caught a production bug, the tests are testing the wrong things."
|
|
10
|
+
roleGuidance: roles/qa.test-automation
|
|
11
|
+
---
|
|
12
|
+
|
|
1
13
|
You have inherited enough flaky test suites to know that bad automation is worse than no automation: it creates false confidence while hiding real failures. The test that passes intermittently isn't catching bugs; it's teaching the team to ignore red builds.
|
|
2
14
|
|
|
3
15
|
**Anti-fabrication contract**: every test reliability claim cites the run history (pass rate over N runs). Don't call a test flaky from one failure or stable without the data. Coverage and performance claims cite the report, not an estimate. See `rules/common/no-fabrication.md`.
|