@crewhaus/contract-compiler 0.1.3 → 0.1.5
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/dist/index.d.ts +95 -0
- package/dist/index.js +213 -0
- package/package.json +10 -7
- package/src/index.test.ts +0 -242
- package/src/index.ts +0 -287
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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 { type Invariant, type Specialization, type SpecializationMatch } from "@crewhaus/specialization-registry";
|
|
33
|
+
export declare class ContractCompilerError extends CrewhausError {
|
|
34
|
+
readonly name = "ContractCompilerError";
|
|
35
|
+
constructor(message: string, cause?: unknown);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A compiled contract — the structured artifact downstream tools
|
|
39
|
+
* (codegen, eval, optimizer) consume. The shape is intentionally
|
|
40
|
+
* minimal; richer fields can be added without breaking callers
|
|
41
|
+
* because every consumer treats unknown fields as opaque.
|
|
42
|
+
*/
|
|
43
|
+
export type CompiledContract = {
|
|
44
|
+
readonly title: string;
|
|
45
|
+
readonly summary: string;
|
|
46
|
+
readonly requirements: ReadonlyArray<Requirement>;
|
|
47
|
+
readonly invariants: ReadonlyArray<Invariant>;
|
|
48
|
+
readonly outOfScope: ReadonlyArray<string>;
|
|
49
|
+
readonly ambiguitiesResolved: ReadonlyArray<AmbiguityResolution>;
|
|
50
|
+
readonly specialization?: SpecializationMatch;
|
|
51
|
+
};
|
|
52
|
+
export type Requirement = {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly text: string;
|
|
55
|
+
readonly source: "user" | "specialization" | "completeness-pass";
|
|
56
|
+
};
|
|
57
|
+
export type AmbiguityResolution = {
|
|
58
|
+
readonly clause: string;
|
|
59
|
+
readonly originalText: string;
|
|
60
|
+
readonly resolution: string;
|
|
61
|
+
};
|
|
62
|
+
export type CompileContractOptions = {
|
|
63
|
+
/** The raw issue text the user supplied. */
|
|
64
|
+
readonly rawIssue: string;
|
|
65
|
+
/**
|
|
66
|
+
* Optional registry override. Defaults to BUILTIN_SPECIALIZATIONS.
|
|
67
|
+
* Pass `loadRegistry(".crewhaus/specializations")` to merge project
|
|
68
|
+
* overrides on top.
|
|
69
|
+
*/
|
|
70
|
+
readonly registry?: ReadonlyArray<Specialization>;
|
|
71
|
+
/**
|
|
72
|
+
* When set, force a specialization match (skip auto-detection).
|
|
73
|
+
* Useful when the user explicitly names the domain.
|
|
74
|
+
*/
|
|
75
|
+
readonly forceSpecialization?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional LLM-backed refiner. When supplied, replaces the rule-based
|
|
78
|
+
* completeness/ambiguity passes. The signature is `(draft) → refined`
|
|
79
|
+
* so the package stays pure for tests.
|
|
80
|
+
*/
|
|
81
|
+
readonly refiner?: RefinerFn;
|
|
82
|
+
};
|
|
83
|
+
export type RefinerFn = (draft: CompiledContract) => Promise<CompiledContract>;
|
|
84
|
+
/**
|
|
85
|
+
* Run the two-pass compilation. Returns the final contract; throws
|
|
86
|
+
* ContractCompilerError if the raw issue is empty (the completeness
|
|
87
|
+
* pass has nothing to work with).
|
|
88
|
+
*/
|
|
89
|
+
export declare function compileContract(opts: CompileContractOptions): Promise<CompiledContract>;
|
|
90
|
+
/**
|
|
91
|
+
* Render a CompiledContract back to a YAML-friendly fragment that can
|
|
92
|
+
* be appended into a `crewhaus.yaml`'s `agent.instructions` field.
|
|
93
|
+
* Strict — no host code, no env access, just string concatenation.
|
|
94
|
+
*/
|
|
95
|
+
export declare function renderContract(c: CompiledContract): string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
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 { match as matchSpecialization, } from "@crewhaus/specialization-registry";
|
|
33
|
+
export class ContractCompilerError extends CrewhausError {
|
|
34
|
+
name = "ContractCompilerError";
|
|
35
|
+
constructor(message, cause) {
|
|
36
|
+
super("config", message, cause);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run the two-pass compilation. Returns the final contract; throws
|
|
41
|
+
* ContractCompilerError if the raw issue is empty (the completeness
|
|
42
|
+
* pass has nothing to work with).
|
|
43
|
+
*/
|
|
44
|
+
export async function compileContract(opts) {
|
|
45
|
+
const raw = opts.rawIssue.trim();
|
|
46
|
+
if (raw.length === 0) {
|
|
47
|
+
throw new ContractCompilerError("rawIssue is empty");
|
|
48
|
+
}
|
|
49
|
+
// Pass 1 — completeness: extract requirements, identify
|
|
50
|
+
// state-transition / trust-boundary / error-handling gaps, inject
|
|
51
|
+
// specialization invariants when confidence is sufficient.
|
|
52
|
+
const matched = matchSpecialization(raw, {
|
|
53
|
+
...(opts.forceSpecialization !== undefined
|
|
54
|
+
? { mode: "strict", forceMatch: opts.forceSpecialization }
|
|
55
|
+
: {}),
|
|
56
|
+
...(opts.registry !== undefined ? { registry: opts.registry } : {}),
|
|
57
|
+
});
|
|
58
|
+
const userRequirements = extractRequirements(raw);
|
|
59
|
+
const completenessRequirements = inferCompletenessRequirements(raw);
|
|
60
|
+
const requirements = [
|
|
61
|
+
...userRequirements.map((text, i) => ({
|
|
62
|
+
id: `user-${i}`,
|
|
63
|
+
text,
|
|
64
|
+
source: "user",
|
|
65
|
+
})),
|
|
66
|
+
...completenessRequirements.map((text, i) => ({
|
|
67
|
+
id: `completeness-${i}`,
|
|
68
|
+
text,
|
|
69
|
+
source: "completeness-pass",
|
|
70
|
+
})),
|
|
71
|
+
];
|
|
72
|
+
const invariants = matched !== undefined ? [...matched.specialization.invariants] : [];
|
|
73
|
+
if (matched !== undefined) {
|
|
74
|
+
for (const inv of matched.specialization.invariants) {
|
|
75
|
+
requirements.push({
|
|
76
|
+
id: `spec-${matched.specialization.name}-${inv.id}`,
|
|
77
|
+
text: `${matched.specialization.name}: ${inv.description}`,
|
|
78
|
+
source: "specialization",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const draft = {
|
|
83
|
+
title: extractTitle(raw),
|
|
84
|
+
summary: raw,
|
|
85
|
+
requirements,
|
|
86
|
+
invariants,
|
|
87
|
+
outOfScope: [],
|
|
88
|
+
ambiguitiesResolved: [],
|
|
89
|
+
...(matched !== undefined ? { specialization: matched } : {}),
|
|
90
|
+
};
|
|
91
|
+
// Pass 2 — scope/ambiguity: remove obvious unsupported clauses,
|
|
92
|
+
// rewrite ambiguous wording. When a refiner is supplied, defer to
|
|
93
|
+
// it; otherwise apply the rule-based detectors.
|
|
94
|
+
const refined = opts.refiner !== undefined ? await opts.refiner(draft) : ambiguityPass(draft);
|
|
95
|
+
return refined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Bullet-extractor: lines starting with -, *, or numbered. Collects
|
|
99
|
+
* the trimmed body of each.
|
|
100
|
+
*/
|
|
101
|
+
function extractRequirements(text) {
|
|
102
|
+
const lines = text.split(/\r?\n/);
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const m = line.match(/^\s*[-*]\s+(.+)$/) ?? line.match(/^\s*\d+\.\s+(.+)$/);
|
|
106
|
+
if (m !== null && m[1] !== undefined)
|
|
107
|
+
out.push(m[1].trim());
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function extractTitle(text) {
|
|
112
|
+
const firstLine = text.split(/\r?\n/)[0]?.trim() ?? "";
|
|
113
|
+
if (firstLine.startsWith("#"))
|
|
114
|
+
return firstLine.replace(/^#+\s*/, "");
|
|
115
|
+
return firstLine.slice(0, 120);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Completeness inferences. Looks for known omissions and adds a
|
|
119
|
+
* one-line "the contract should also say" requirement so the
|
|
120
|
+
* specification author sees the gap explicitly. Idempotent: each rule
|
|
121
|
+
* only fires when the corresponding signal is *absent* from the raw
|
|
122
|
+
* text.
|
|
123
|
+
*/
|
|
124
|
+
function inferCompletenessRequirements(text) {
|
|
125
|
+
const lower = text.toLowerCase();
|
|
126
|
+
const inferred = [];
|
|
127
|
+
if (!/error|fail|exception|reject/.test(lower)) {
|
|
128
|
+
inferred.push("Specify the error-handling contract: which failures the agent must surface vs swallow.");
|
|
129
|
+
}
|
|
130
|
+
if (!/edge\s*case|boundary|invalid|empty/.test(lower)) {
|
|
131
|
+
inferred.push("Enumerate edge cases (empty inputs, invalid inputs, boundary conditions).");
|
|
132
|
+
}
|
|
133
|
+
if (/state|status|transition/.test(lower) && !/transition/.test(lower)) {
|
|
134
|
+
inferred.push("Document the allowed state transitions explicitly (rather than implying them).");
|
|
135
|
+
}
|
|
136
|
+
if (/(?:auth|token|secret|password|credential)/.test(lower) &&
|
|
137
|
+
!/(trust\s*bound|threat\s*model|attacker)/.test(lower)) {
|
|
138
|
+
inferred.push("Name the trust boundaries: what the system trusts the caller for vs what it independently verifies.");
|
|
139
|
+
}
|
|
140
|
+
return inferred;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Ambiguity pass. Heuristic detectors for the most common authoring
|
|
144
|
+
* mistakes the paper highlights: vague quantifiers ("some", "a few",
|
|
145
|
+
* "maybe"), passive-voice subject elision ("it should be handled"),
|
|
146
|
+
* and contradictory pairs detected by negation distance.
|
|
147
|
+
*
|
|
148
|
+
* Each resolved ambiguity gets recorded in `ambiguitiesResolved` so
|
|
149
|
+
* the contract reviewer sees the substitution explicitly.
|
|
150
|
+
*/
|
|
151
|
+
function ambiguityPass(draft) {
|
|
152
|
+
const resolutions = [];
|
|
153
|
+
const VAGUE_RE = /\b(some|a few|several|maybe|possibly|might|perhaps)\b/gi;
|
|
154
|
+
const newRequirements = [];
|
|
155
|
+
for (const req of draft.requirements) {
|
|
156
|
+
if (VAGUE_RE.test(req.text)) {
|
|
157
|
+
VAGUE_RE.lastIndex = 0;
|
|
158
|
+
const resolved = req.text.replace(VAGUE_RE, "[QUANTIFY]");
|
|
159
|
+
resolutions.push({
|
|
160
|
+
clause: req.id,
|
|
161
|
+
originalText: req.text,
|
|
162
|
+
resolution: "Vague quantifier flagged; replaced with [QUANTIFY] placeholder. The contract reviewer must supply a number or remove the clause.",
|
|
163
|
+
});
|
|
164
|
+
newRequirements.push({ ...req, text: resolved });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
newRequirements.push(req);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
...draft,
|
|
172
|
+
requirements: newRequirements,
|
|
173
|
+
ambiguitiesResolved: resolutions,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Render a CompiledContract back to a YAML-friendly fragment that can
|
|
178
|
+
* be appended into a `crewhaus.yaml`'s `agent.instructions` field.
|
|
179
|
+
* Strict — no host code, no env access, just string concatenation.
|
|
180
|
+
*/
|
|
181
|
+
export function renderContract(c) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
lines.push(`# Contract: ${c.title}`);
|
|
184
|
+
if (c.specialization !== undefined) {
|
|
185
|
+
lines.push(`# Specialization: ${c.specialization.specialization.name} (confidence ${c.specialization.confidence.toFixed(2)})`);
|
|
186
|
+
}
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("## Requirements");
|
|
189
|
+
for (const r of c.requirements) {
|
|
190
|
+
lines.push(`- [${r.source}] ${r.text}`);
|
|
191
|
+
}
|
|
192
|
+
if (c.invariants.length > 0) {
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("## Invariants");
|
|
195
|
+
for (const inv of c.invariants) {
|
|
196
|
+
lines.push(`- [${inv.required ? "required" : "optional"}] ${inv.id}: ${inv.description}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (c.outOfScope.length > 0) {
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push("## Out of scope");
|
|
202
|
+
for (const oos of c.outOfScope)
|
|
203
|
+
lines.push(`- ${oos}`);
|
|
204
|
+
}
|
|
205
|
+
if (c.ambiguitiesResolved.length > 0) {
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push("## Ambiguities resolved");
|
|
208
|
+
for (const a of c.ambiguitiesResolved) {
|
|
209
|
+
lines.push(`- ${a.clause}: ${a.resolution}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/contract-compiler",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Track G / §58 — two-pass contract compilation (completeness + scope/ambiguity). Source: Meta-Engineering Harnesses (arxiv 2605.25665, §4.2).",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"scripts": {
|
|
12
15
|
"test": "bun test src"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
15
|
-
"@crewhaus/errors": "0.1.
|
|
16
|
-
"@crewhaus/specialization-registry": "0.1.
|
|
18
|
+
"@crewhaus/errors": "0.1.5",
|
|
19
|
+
"@crewhaus/specialization-registry": "0.1.5"
|
|
17
20
|
},
|
|
18
21
|
"license": "Apache-2.0",
|
|
19
22
|
"author": {
|
|
@@ -33,5 +36,5 @@
|
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public"
|
|
35
38
|
},
|
|
36
|
-
"files": ["
|
|
39
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
37
40
|
}
|
package/src/index.test.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
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("compileContract — completeness inference branches", () => {
|
|
97
|
-
test("infers a state-transition requirement when status is implied but transitions are not", async () => {
|
|
98
|
-
const c = await compileContract({
|
|
99
|
-
// mentions "status" (matches state-rule's positive signal) but never
|
|
100
|
-
// says "transition" (negative signal absent) -> rule fires.
|
|
101
|
-
rawIssue: "Track the order status and surface invalid empty failures.",
|
|
102
|
-
});
|
|
103
|
-
const completeness = c.requirements.filter((r) => r.source === "completeness-pass");
|
|
104
|
-
expect(completeness.some((r) => /state transitions/i.test(r.text))).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("does not infer a state-transition requirement when 'transition' is already present", async () => {
|
|
108
|
-
const c = await compileContract({
|
|
109
|
-
rawIssue: "Document each status transition; handle invalid empty error inputs.",
|
|
110
|
-
});
|
|
111
|
-
const completeness = c.requirements.filter((r) => r.source === "completeness-pass");
|
|
112
|
-
expect(completeness.some((r) => /state transitions/i.test(r.text))).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("infers a trust-boundary requirement when secrets are mentioned without a threat model", async () => {
|
|
116
|
-
const c = await compileContract({
|
|
117
|
-
// mentions "token"/"password" but no trust-boundary/threat-model/attacker.
|
|
118
|
-
rawIssue: "Store the user password and issue a token. Handle invalid empty failures.",
|
|
119
|
-
});
|
|
120
|
-
const completeness = c.requirements.filter((r) => r.source === "completeness-pass");
|
|
121
|
-
expect(completeness.some((r) => /trust boundaries/i.test(r.text))).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("does not infer a trust-boundary requirement when a threat model is already named", async () => {
|
|
125
|
-
const c = await compileContract({
|
|
126
|
-
rawIssue:
|
|
127
|
-
"Handle the auth token under an explicit threat model. Cover invalid empty error inputs.",
|
|
128
|
-
});
|
|
129
|
-
const completeness = c.requirements.filter((r) => r.source === "completeness-pass");
|
|
130
|
-
expect(completeness.some((r) => /trust boundaries/i.test(r.text))).toBe(false);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("compileContract — title + requirement extraction edge cases", () => {
|
|
135
|
-
test("strips a leading markdown heading marker from the title", async () => {
|
|
136
|
-
const c = await compileContract({ rawIssue: "# Build the widget\nbody text" });
|
|
137
|
-
expect(c.title).toBe("Build the widget");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("truncates a long single-line title to 120 chars", async () => {
|
|
141
|
-
const long = "x".repeat(200);
|
|
142
|
-
const c = await compileContract({ rawIssue: long });
|
|
143
|
-
expect(c.title.length).toBe(120);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("ignores bullet markers that have no body", async () => {
|
|
147
|
-
// "-" / "1." with no following text must not produce a requirement.
|
|
148
|
-
const c = await compileContract({ rawIssue: "Title line\n-\n1.\n- real item" });
|
|
149
|
-
const userReqs = c.requirements.filter((r) => r.source === "user");
|
|
150
|
-
expect(userReqs.length).toBe(1);
|
|
151
|
-
expect(userReqs[0]?.text).toBe("real item");
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe("ambiguityPass — global-regex statefulness regression", () => {
|
|
156
|
-
test("flags vague quantifiers independently across consecutive requirements", async () => {
|
|
157
|
-
// Regression guard: VAGUE_RE carries the /g flag and is reused across the
|
|
158
|
-
// requirement loop. A leaked lastIndex would cause a later requirement to
|
|
159
|
-
// be skipped or mis-matched. Every vague line must be flagged regardless
|
|
160
|
-
// of position.
|
|
161
|
-
const c = await compileContract({
|
|
162
|
-
rawIssue: [
|
|
163
|
-
"Title",
|
|
164
|
-
"- return some results",
|
|
165
|
-
"- handle maybe the timeout",
|
|
166
|
-
"- a clean requirement with no vague words",
|
|
167
|
-
"- perhaps several edge cases",
|
|
168
|
-
"- another clean concrete requirement",
|
|
169
|
-
"- might retry once",
|
|
170
|
-
].join("\n"),
|
|
171
|
-
});
|
|
172
|
-
const userReqs = c.requirements.filter((r) => r.source === "user");
|
|
173
|
-
const flagged = userReqs.filter((r) => r.text.includes("[QUANTIFY]"));
|
|
174
|
-
// Lines containing some / maybe / perhaps+several / might -> 4 flagged.
|
|
175
|
-
expect(flagged.length).toBe(4);
|
|
176
|
-
// Each flagged requirement is recorded once in ambiguitiesResolved.
|
|
177
|
-
expect(c.ambiguitiesResolved.length).toBe(4);
|
|
178
|
-
// The two concrete requirements are passed through untouched.
|
|
179
|
-
expect(userReqs.some((r) => r.text === "a clean requirement with no vague words")).toBe(true);
|
|
180
|
-
expect(userReqs.some((r) => r.text === "another clean concrete requirement")).toBe(true);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("replaces every vague occurrence within a single requirement", async () => {
|
|
184
|
-
const c = await compileContract({ rawIssue: "Title\n- perhaps several retries" });
|
|
185
|
-
const req = c.requirements.find((r) => r.source === "user");
|
|
186
|
-
expect(req?.text).toBe("[QUANTIFY] [QUANTIFY] retries");
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("renderContract", () => {
|
|
191
|
-
test("emits a markdown-flavored block including invariants and ambiguities", async () => {
|
|
192
|
-
const c = await compileContract({
|
|
193
|
-
rawIssue: "Stripe paymentintent flow\n- Refund support\n- Some retries",
|
|
194
|
-
});
|
|
195
|
-
const md = renderContract(c);
|
|
196
|
-
expect(md).toContain("# Contract:");
|
|
197
|
-
expect(md).toContain("## Requirements");
|
|
198
|
-
expect(md).toContain("## Invariants");
|
|
199
|
-
expect(md).toContain("idempotency-key");
|
|
200
|
-
expect(md).toContain("## Ambiguities resolved");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("renders the specialization header line with confidence when matched", async () => {
|
|
204
|
-
const c = await compileContract({ rawIssue: "anything", forceSpecialization: "auth" });
|
|
205
|
-
const md = renderContract(c);
|
|
206
|
-
expect(md).toContain("# Specialization: auth (confidence 1.00)");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("renders required vs optional invariants distinctly", async () => {
|
|
210
|
-
const c = await compileContract({ rawIssue: "Stripe refund and invoice charge flow" });
|
|
211
|
-
const md = renderContract(c);
|
|
212
|
-
// payments has both required (idempotency-key) and optional
|
|
213
|
-
// (discount-deduction-source) invariants.
|
|
214
|
-
expect(md).toContain("- [required] idempotency-key:");
|
|
215
|
-
expect(md).toContain("- [optional] discount-deduction-source:");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("omits the optional sections when the contract has no invariants/oos/ambiguities", async () => {
|
|
219
|
-
// Plain input with explicit error+edge handling -> no specialization, no
|
|
220
|
-
// invariants, no completeness noise that triggers ambiguity, no out-of-scope.
|
|
221
|
-
const c = await compileContract({
|
|
222
|
-
rawIssue:
|
|
223
|
-
"Rename a CSS class with explicit error handling and edge case enumeration for empty + invalid inputs.",
|
|
224
|
-
});
|
|
225
|
-
const md = renderContract(c);
|
|
226
|
-
expect(md).not.toContain("## Invariants");
|
|
227
|
-
expect(md).not.toContain("## Out of scope");
|
|
228
|
-
expect(md).not.toContain("## Ambiguities resolved");
|
|
229
|
-
expect(md).not.toContain("# Specialization:");
|
|
230
|
-
expect(md).toContain("## Requirements");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("renders an out-of-scope section when present", async () => {
|
|
234
|
-
const c = await compileContract({
|
|
235
|
-
rawIssue: "Add a feature",
|
|
236
|
-
refiner: async (draft) => ({ ...draft, outOfScope: ["payments integration"] }),
|
|
237
|
-
});
|
|
238
|
-
const md = renderContract(c);
|
|
239
|
-
expect(md).toContain("## Out of scope");
|
|
240
|
-
expect(md).toContain("- payments integration");
|
|
241
|
-
});
|
|
242
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
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
|
-
}
|