@crewhaus/contract-compiler 0.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/package.json +42 -0
- package/src/index.test.ts +108 -0
- package/src/index.ts +287 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/contract-compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Track G / §58 — two-pass contract compilation (completeness + scope/ambiguity). Source: Meta-Engineering Harnesses (arxiv 2605.25665, §4.2).",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/errors": "0.0.0",
|
|
16
|
+
"@crewhaus/specialization-registry": "0.0.0"
|
|
17
|
+
},
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Max Meier",
|
|
21
|
+
"email": "max@studiomax.io",
|
|
22
|
+
"url": "https://studiomax.io"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
27
|
+
"directory": "packages/contract-compiler"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/contract-compiler#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "restricted"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"NOTICE"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ContractCompilerError, compileContract, renderContract } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("compileContract — pass 1 completeness", () => {
|
|
5
|
+
test("throws on empty input", async () => {
|
|
6
|
+
await expect(compileContract({ rawIssue: " " })).rejects.toThrow(ContractCompilerError);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("extracts bullet-list requirements", async () => {
|
|
10
|
+
const c = await compileContract({
|
|
11
|
+
rawIssue: "Implement search\n- Must handle empty queries\n- Should return top 10 results",
|
|
12
|
+
});
|
|
13
|
+
const userReqs = c.requirements.filter((r) => r.source === "user");
|
|
14
|
+
expect(userReqs.length).toBe(2);
|
|
15
|
+
expect(userReqs[0]?.text).toContain("empty queries");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("extracts numbered requirements", async () => {
|
|
19
|
+
const c = await compileContract({
|
|
20
|
+
rawIssue: "Add login\n1. JWT tokens\n2. Refresh after 1h",
|
|
21
|
+
});
|
|
22
|
+
const userReqs = c.requirements.filter((r) => r.source === "user");
|
|
23
|
+
expect(userReqs.length).toBe(2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("adds completeness-pass requirements when error handling is unmentioned", async () => {
|
|
27
|
+
const c = await compileContract({ rawIssue: "Add a button that increments a counter" });
|
|
28
|
+
const completeness = c.requirements.filter((r) => r.source === "completeness-pass");
|
|
29
|
+
expect(completeness.some((r) => /error-handling/i.test(r.text))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("skips completeness rule when the signal is already present", async () => {
|
|
33
|
+
const c = await compileContract({
|
|
34
|
+
rawIssue:
|
|
35
|
+
"Add /search endpoint with explicit error handling and edge case enumeration for empty + invalid inputs.",
|
|
36
|
+
});
|
|
37
|
+
const completeness = c.requirements.filter((r) => r.source === "completeness-pass");
|
|
38
|
+
expect(completeness.some((r) => /error-handling/i.test(r.text))).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("compileContract — specialization injection", () => {
|
|
43
|
+
test("injects payments invariants when input mentions stripe", async () => {
|
|
44
|
+
const c = await compileContract({
|
|
45
|
+
rawIssue: "Build a Stripe paymentintent + refund flow",
|
|
46
|
+
});
|
|
47
|
+
expect(c.specialization?.specialization.name).toBe("payments");
|
|
48
|
+
const ids = c.invariants.map((i) => i.id);
|
|
49
|
+
expect(ids).toContain("idempotency-key");
|
|
50
|
+
expect(ids).toContain("trust-boundary-client-status");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("does not inject any specialization for unrelated input", async () => {
|
|
54
|
+
const c = await compileContract({
|
|
55
|
+
rawIssue: "Style the homepage with a new color scheme",
|
|
56
|
+
});
|
|
57
|
+
expect(c.specialization).toBeUndefined();
|
|
58
|
+
expect(c.invariants.length).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("forceSpecialization overrides auto-detect", async () => {
|
|
62
|
+
const c = await compileContract({
|
|
63
|
+
rawIssue: "anything at all",
|
|
64
|
+
forceSpecialization: "auth",
|
|
65
|
+
});
|
|
66
|
+
expect(c.specialization?.specialization.name).toBe("auth");
|
|
67
|
+
expect(c.specialization?.confidence).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("compileContract — pass 2 ambiguity", () => {
|
|
72
|
+
test("flags vague quantifiers in requirements", async () => {
|
|
73
|
+
const c = await compileContract({
|
|
74
|
+
rawIssue: "Notes:\n- Return some results when the user searches",
|
|
75
|
+
});
|
|
76
|
+
expect(c.ambiguitiesResolved.length).toBeGreaterThan(0);
|
|
77
|
+
const r = c.requirements.find((r) => r.source === "user");
|
|
78
|
+
expect(r?.text).toContain("[QUANTIFY]");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("supplied refiner replaces the rule-based pass 2", async () => {
|
|
82
|
+
const c = await compileContract({
|
|
83
|
+
rawIssue: "Maybe handle the timeout case",
|
|
84
|
+
refiner: async (draft) => ({
|
|
85
|
+
...draft,
|
|
86
|
+
outOfScope: ["everything except the happy path"],
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
expect(c.outOfScope).toEqual(["everything except the happy path"]);
|
|
90
|
+
// Refiner replaced the rule-based pass, so vague quantifiers are NOT
|
|
91
|
+
// automatically flagged.
|
|
92
|
+
expect(c.ambiguitiesResolved.length).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("renderContract", () => {
|
|
97
|
+
test("emits a markdown-flavored block including invariants and ambiguities", async () => {
|
|
98
|
+
const c = await compileContract({
|
|
99
|
+
rawIssue: "Stripe paymentintent flow\n- Refund support\n- Some retries",
|
|
100
|
+
});
|
|
101
|
+
const md = renderContract(c);
|
|
102
|
+
expect(md).toContain("# Contract:");
|
|
103
|
+
expect(md).toContain("## Requirements");
|
|
104
|
+
expect(md).toContain("## Invariants");
|
|
105
|
+
expect(md).toContain("idempotency-key");
|
|
106
|
+
expect(md).toContain("## Ambiguities resolved");
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track G (§58) — `contract-compiler`. Two-pass compilation of a raw
|
|
3
|
+
* issue/spec draft into a structured contract suitable for downstream
|
|
4
|
+
* agent execution.
|
|
5
|
+
*
|
|
6
|
+
* Source: Meta-Engineering Harnesses (Sengupta et al., HireNimbus,
|
|
7
|
+
* arxiv 2605.25665, §4.2). The two-pass model:
|
|
8
|
+
*
|
|
9
|
+
* - **Pass 1 (completeness)** — Converts the raw issue to a
|
|
10
|
+
* structured draft, making implicit assumptions explicit through
|
|
11
|
+
* types, state transitions, edge cases, trust boundaries, error
|
|
12
|
+
* conditions.
|
|
13
|
+
* - **Pass 2 (scope/ambiguity)** — Reduces and clarifies. Removes
|
|
14
|
+
* unsupported requirements. Rewrites ambiguous clauses so
|
|
15
|
+
* downstream agents don't treat multiple interpretations as
|
|
16
|
+
* equally valid.
|
|
17
|
+
*
|
|
18
|
+
* Empirical motivation from the paper: **first-pass contracts can
|
|
19
|
+
* over-specify**, and over-specification is dangerous because
|
|
20
|
+
* downstream agents treat unsupported requirements as mandatory.
|
|
21
|
+
*
|
|
22
|
+
* v0 ships the pure-function pipeline. The "completeness" and
|
|
23
|
+
* "ambiguity" detectors here are rule-based; the production CLI wires
|
|
24
|
+
* an LLM-backed refiner on top via the `refiner?` option. Either way
|
|
25
|
+
* the pass shapes are the same and the registered specialization
|
|
26
|
+
* invariants are injected at the end of pass 1 (before ambiguity
|
|
27
|
+
* resolution can prune them).
|
|
28
|
+
*
|
|
29
|
+
* Cited paper: Meta-Engineering Harnesses (arxiv 2605.25665).
|
|
30
|
+
*/
|
|
31
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
32
|
+
import {
|
|
33
|
+
type Invariant,
|
|
34
|
+
type Specialization,
|
|
35
|
+
type SpecializationMatch,
|
|
36
|
+
match as matchSpecialization,
|
|
37
|
+
} from "@crewhaus/specialization-registry";
|
|
38
|
+
|
|
39
|
+
export class ContractCompilerError extends CrewhausError {
|
|
40
|
+
override readonly name = "ContractCompilerError";
|
|
41
|
+
constructor(message: string, cause?: unknown) {
|
|
42
|
+
super("config", message, cause);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A compiled contract — the structured artifact downstream tools
|
|
48
|
+
* (codegen, eval, optimizer) consume. The shape is intentionally
|
|
49
|
+
* minimal; richer fields can be added without breaking callers
|
|
50
|
+
* because every consumer treats unknown fields as opaque.
|
|
51
|
+
*/
|
|
52
|
+
export type CompiledContract = {
|
|
53
|
+
readonly title: string;
|
|
54
|
+
readonly summary: string;
|
|
55
|
+
readonly requirements: ReadonlyArray<Requirement>;
|
|
56
|
+
readonly invariants: ReadonlyArray<Invariant>;
|
|
57
|
+
readonly outOfScope: ReadonlyArray<string>;
|
|
58
|
+
readonly ambiguitiesResolved: ReadonlyArray<AmbiguityResolution>;
|
|
59
|
+
readonly specialization?: SpecializationMatch;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type Requirement = {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly text: string;
|
|
65
|
+
readonly source: "user" | "specialization" | "completeness-pass";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type AmbiguityResolution = {
|
|
69
|
+
readonly clause: string;
|
|
70
|
+
readonly originalText: string;
|
|
71
|
+
readonly resolution: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type CompileContractOptions = {
|
|
75
|
+
/** The raw issue text the user supplied. */
|
|
76
|
+
readonly rawIssue: string;
|
|
77
|
+
/**
|
|
78
|
+
* Optional registry override. Defaults to BUILTIN_SPECIALIZATIONS.
|
|
79
|
+
* Pass `loadRegistry(".crewhaus/specializations")` to merge project
|
|
80
|
+
* overrides on top.
|
|
81
|
+
*/
|
|
82
|
+
readonly registry?: ReadonlyArray<Specialization>;
|
|
83
|
+
/**
|
|
84
|
+
* When set, force a specialization match (skip auto-detection).
|
|
85
|
+
* Useful when the user explicitly names the domain.
|
|
86
|
+
*/
|
|
87
|
+
readonly forceSpecialization?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Optional LLM-backed refiner. When supplied, replaces the rule-based
|
|
90
|
+
* completeness/ambiguity passes. The signature is `(draft) → refined`
|
|
91
|
+
* so the package stays pure for tests.
|
|
92
|
+
*/
|
|
93
|
+
readonly refiner?: RefinerFn;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type RefinerFn = (draft: CompiledContract) => Promise<CompiledContract>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run the two-pass compilation. Returns the final contract; throws
|
|
100
|
+
* ContractCompilerError if the raw issue is empty (the completeness
|
|
101
|
+
* pass has nothing to work with).
|
|
102
|
+
*/
|
|
103
|
+
export async function compileContract(opts: CompileContractOptions): Promise<CompiledContract> {
|
|
104
|
+
const raw = opts.rawIssue.trim();
|
|
105
|
+
if (raw.length === 0) {
|
|
106
|
+
throw new ContractCompilerError("rawIssue is empty");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Pass 1 — completeness: extract requirements, identify
|
|
110
|
+
// state-transition / trust-boundary / error-handling gaps, inject
|
|
111
|
+
// specialization invariants when confidence is sufficient.
|
|
112
|
+
const matched = matchSpecialization(raw, {
|
|
113
|
+
...(opts.forceSpecialization !== undefined
|
|
114
|
+
? { mode: "strict", forceMatch: opts.forceSpecialization }
|
|
115
|
+
: {}),
|
|
116
|
+
...(opts.registry !== undefined ? { registry: opts.registry } : {}),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const userRequirements = extractRequirements(raw);
|
|
120
|
+
const completenessRequirements = inferCompletenessRequirements(raw);
|
|
121
|
+
const requirements: Requirement[] = [
|
|
122
|
+
...userRequirements.map((text, i) => ({
|
|
123
|
+
id: `user-${i}`,
|
|
124
|
+
text,
|
|
125
|
+
source: "user" as const,
|
|
126
|
+
})),
|
|
127
|
+
...completenessRequirements.map((text, i) => ({
|
|
128
|
+
id: `completeness-${i}`,
|
|
129
|
+
text,
|
|
130
|
+
source: "completeness-pass" as const,
|
|
131
|
+
})),
|
|
132
|
+
];
|
|
133
|
+
const invariants: Invariant[] =
|
|
134
|
+
matched !== undefined ? [...matched.specialization.invariants] : [];
|
|
135
|
+
if (matched !== undefined) {
|
|
136
|
+
for (const inv of matched.specialization.invariants) {
|
|
137
|
+
requirements.push({
|
|
138
|
+
id: `spec-${matched.specialization.name}-${inv.id}`,
|
|
139
|
+
text: `${matched.specialization.name}: ${inv.description}`,
|
|
140
|
+
source: "specialization",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const draft: CompiledContract = {
|
|
146
|
+
title: extractTitle(raw),
|
|
147
|
+
summary: raw,
|
|
148
|
+
requirements,
|
|
149
|
+
invariants,
|
|
150
|
+
outOfScope: [],
|
|
151
|
+
ambiguitiesResolved: [],
|
|
152
|
+
...(matched !== undefined ? { specialization: matched } : {}),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Pass 2 — scope/ambiguity: remove obvious unsupported clauses,
|
|
156
|
+
// rewrite ambiguous wording. When a refiner is supplied, defer to
|
|
157
|
+
// it; otherwise apply the rule-based detectors.
|
|
158
|
+
const refined = opts.refiner !== undefined ? await opts.refiner(draft) : ambiguityPass(draft);
|
|
159
|
+
return refined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Bullet-extractor: lines starting with -, *, or numbered. Collects
|
|
164
|
+
* the trimmed body of each.
|
|
165
|
+
*/
|
|
166
|
+
function extractRequirements(text: string): string[] {
|
|
167
|
+
const lines = text.split(/\r?\n/);
|
|
168
|
+
const out: string[] = [];
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const m = line.match(/^\s*[-*]\s+(.+)$/) ?? line.match(/^\s*\d+\.\s+(.+)$/);
|
|
171
|
+
if (m !== null && m[1] !== undefined) out.push(m[1].trim());
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractTitle(text: string): string {
|
|
177
|
+
const firstLine = text.split(/\r?\n/)[0]?.trim() ?? "";
|
|
178
|
+
if (firstLine.startsWith("#")) return firstLine.replace(/^#+\s*/, "");
|
|
179
|
+
return firstLine.slice(0, 120);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Completeness inferences. Looks for known omissions and adds a
|
|
184
|
+
* one-line "the contract should also say" requirement so the
|
|
185
|
+
* specification author sees the gap explicitly. Idempotent: each rule
|
|
186
|
+
* only fires when the corresponding signal is *absent* from the raw
|
|
187
|
+
* text.
|
|
188
|
+
*/
|
|
189
|
+
function inferCompletenessRequirements(text: string): string[] {
|
|
190
|
+
const lower = text.toLowerCase();
|
|
191
|
+
const inferred: string[] = [];
|
|
192
|
+
if (!/error|fail|exception|reject/.test(lower)) {
|
|
193
|
+
inferred.push(
|
|
194
|
+
"Specify the error-handling contract: which failures the agent must surface vs swallow.",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (!/edge\s*case|boundary|invalid|empty/.test(lower)) {
|
|
198
|
+
inferred.push("Enumerate edge cases (empty inputs, invalid inputs, boundary conditions).");
|
|
199
|
+
}
|
|
200
|
+
if (/state|status|transition/.test(lower) && !/transition/.test(lower)) {
|
|
201
|
+
inferred.push("Document the allowed state transitions explicitly (rather than implying them).");
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
/(?:auth|token|secret|password|credential)/.test(lower) &&
|
|
205
|
+
!/(trust\s*bound|threat\s*model|attacker)/.test(lower)
|
|
206
|
+
) {
|
|
207
|
+
inferred.push(
|
|
208
|
+
"Name the trust boundaries: what the system trusts the caller for vs what it independently verifies.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return inferred;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Ambiguity pass. Heuristic detectors for the most common authoring
|
|
216
|
+
* mistakes the paper highlights: vague quantifiers ("some", "a few",
|
|
217
|
+
* "maybe"), passive-voice subject elision ("it should be handled"),
|
|
218
|
+
* and contradictory pairs detected by negation distance.
|
|
219
|
+
*
|
|
220
|
+
* Each resolved ambiguity gets recorded in `ambiguitiesResolved` so
|
|
221
|
+
* the contract reviewer sees the substitution explicitly.
|
|
222
|
+
*/
|
|
223
|
+
function ambiguityPass(draft: CompiledContract): CompiledContract {
|
|
224
|
+
const resolutions: AmbiguityResolution[] = [];
|
|
225
|
+
const VAGUE_RE = /\b(some|a few|several|maybe|possibly|might|perhaps)\b/gi;
|
|
226
|
+
const newRequirements: Requirement[] = [];
|
|
227
|
+
for (const req of draft.requirements) {
|
|
228
|
+
if (VAGUE_RE.test(req.text)) {
|
|
229
|
+
VAGUE_RE.lastIndex = 0;
|
|
230
|
+
const resolved = req.text.replace(VAGUE_RE, "[QUANTIFY]");
|
|
231
|
+
resolutions.push({
|
|
232
|
+
clause: req.id,
|
|
233
|
+
originalText: req.text,
|
|
234
|
+
resolution:
|
|
235
|
+
"Vague quantifier flagged; replaced with [QUANTIFY] placeholder. The contract reviewer must supply a number or remove the clause.",
|
|
236
|
+
});
|
|
237
|
+
newRequirements.push({ ...req, text: resolved });
|
|
238
|
+
} else {
|
|
239
|
+
newRequirements.push(req);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
...draft,
|
|
244
|
+
requirements: newRequirements,
|
|
245
|
+
ambiguitiesResolved: resolutions,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Render a CompiledContract back to a YAML-friendly fragment that can
|
|
251
|
+
* be appended into a `crewhaus.yaml`'s `agent.instructions` field.
|
|
252
|
+
* Strict — no host code, no env access, just string concatenation.
|
|
253
|
+
*/
|
|
254
|
+
export function renderContract(c: CompiledContract): string {
|
|
255
|
+
const lines: string[] = [];
|
|
256
|
+
lines.push(`# Contract: ${c.title}`);
|
|
257
|
+
if (c.specialization !== undefined) {
|
|
258
|
+
lines.push(
|
|
259
|
+
`# Specialization: ${c.specialization.specialization.name} (confidence ${c.specialization.confidence.toFixed(2)})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("## Requirements");
|
|
264
|
+
for (const r of c.requirements) {
|
|
265
|
+
lines.push(`- [${r.source}] ${r.text}`);
|
|
266
|
+
}
|
|
267
|
+
if (c.invariants.length > 0) {
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push("## Invariants");
|
|
270
|
+
for (const inv of c.invariants) {
|
|
271
|
+
lines.push(`- [${inv.required ? "required" : "optional"}] ${inv.id}: ${inv.description}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (c.outOfScope.length > 0) {
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push("## Out of scope");
|
|
277
|
+
for (const oos of c.outOfScope) lines.push(`- ${oos}`);
|
|
278
|
+
}
|
|
279
|
+
if (c.ambiguitiesResolved.length > 0) {
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push("## Ambiguities resolved");
|
|
282
|
+
for (const a of c.ambiguitiesResolved) {
|
|
283
|
+
lines.push(`- ${a.clause}: ${a.resolution}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return lines.join("\n");
|
|
287
|
+
}
|