@crewhaus/specialization-registry 0.1.4 → 0.1.6

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.
@@ -0,0 +1,61 @@
1
+ import { CrewhausError } from "@crewhaus/errors";
2
+ export declare class SpecializationRegistryError extends CrewhausError {
3
+ readonly name = "SpecializationRegistryError";
4
+ constructor(message: string, cause?: unknown);
5
+ }
6
+ /**
7
+ * One named invariant a specialization injects into a contract. The
8
+ * contract compiler maps each to a clause in the compiled YAML.
9
+ */
10
+ export type Invariant = {
11
+ readonly id: string;
12
+ readonly description: string;
13
+ /** Severity for the contract reviewer: must-have vs nice-to-have. */
14
+ readonly required: boolean;
15
+ };
16
+ /**
17
+ * A specialization record. `keywords` are used by the default
18
+ * confidence-scoring heuristic; the score is the fraction of keywords
19
+ * present in the input text (case-insensitive substring match).
20
+ */
21
+ export type Specialization = {
22
+ readonly name: string;
23
+ readonly description: string;
24
+ readonly keywords: ReadonlyArray<string>;
25
+ readonly invariants: ReadonlyArray<Invariant>;
26
+ /** Apply only when confidence >= this. Default 0.5. */
27
+ readonly confidenceThreshold: number;
28
+ };
29
+ export type SpecializationMatch = {
30
+ readonly specialization: Specialization;
31
+ readonly confidence: number;
32
+ /** Which keywords were hit; for audit + debugging. */
33
+ readonly matchedKeywords: ReadonlyArray<string>;
34
+ };
35
+ /**
36
+ * Built-in reference specializations. These are the ones the
37
+ * Meta-Engineering Harnesses paper specifically calls out as
38
+ * "recurring task domains" in their CTO-as-a-service deployment.
39
+ */
40
+ export declare const BUILTIN_SPECIALIZATIONS: ReadonlyArray<Specialization>;
41
+ /**
42
+ * Match a free-text issue/contract draft against the registered
43
+ * specializations. Returns the highest-confidence match whose score
44
+ * is >= its threshold, or undefined when no specialization matches.
45
+ *
46
+ * `mode: "strict"` skips auto-detection and only returns a match if
47
+ * the caller explicitly named one in `forceMatch`.
48
+ */
49
+ export type MatchOptions = {
50
+ readonly mode?: "auto" | "strict";
51
+ readonly forceMatch?: string;
52
+ readonly registry?: ReadonlyArray<Specialization>;
53
+ };
54
+ export declare function match(text: string, opts?: MatchOptions): SpecializationMatch | undefined;
55
+ /**
56
+ * Read JSON-encoded specialization records from a directory, merge
57
+ * them with the built-ins (overriding by `name`), and return the
58
+ * combined registry. Used by the contract compiler to pick up
59
+ * project-specific specializations from `.crewhaus/specializations/`.
60
+ */
61
+ export declare function loadRegistry(dir: string): ReadonlyArray<Specialization>;
package/dist/index.js ADDED
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Track G (§58) — `specialization-registry`. File-backed registry of
3
+ * domain specializations.
4
+ *
5
+ * Source: Meta-Engineering Harnesses (Sengupta et al., HireNimbus,
6
+ * arxiv 2605.25665, §4.4). A specialization is a named collection of
7
+ * domain-specific invariants — for "payments" the invariants are
8
+ * idempotency keys, explicit state transitions, trust-boundary checks;
9
+ * for "auth" they're token expiry, scope checks, etc. The contract
10
+ * compiler (Track G in the same plan) calls into this registry to
11
+ * inject these invariants into the compiled contract.
12
+ *
13
+ * The paper's key safety rule: specializations apply only when
14
+ * **confidence crosses a threshold**. Below that, the pipeline
15
+ * proceeds without specialization to avoid corrupting the contract
16
+ * with wrong domain assumptions. The registry stores
17
+ * `(name, invariants, confidenceThreshold)` triples and exposes a
18
+ * `match(text, opts) → SpecializationMatch | undefined` function.
19
+ *
20
+ * v0 ships:
21
+ * - In-memory registry seeded with three reference specializations
22
+ * (payments, auth, booking).
23
+ * - File-backed registry that reads from
24
+ * `.crewhaus/specializations/<name>.json` (overrides built-ins).
25
+ * - `match()` — keyword-based confidence scoring; the contract
26
+ * compiler can pass `mode: "strict"` to require explicit opt-in
27
+ * and skip auto-detection entirely.
28
+ *
29
+ * Cited paper: Meta-Engineering Harnesses (arxiv 2605.25665, §4.4).
30
+ */
31
+ import { readFileSync, readdirSync } from "node:fs";
32
+ import { join, resolve } from "node:path";
33
+ import { CrewhausError } from "@crewhaus/errors";
34
+ export class SpecializationRegistryError extends CrewhausError {
35
+ name = "SpecializationRegistryError";
36
+ constructor(message, cause) {
37
+ super("config", message, cause);
38
+ }
39
+ }
40
+ /**
41
+ * Built-in reference specializations. These are the ones the
42
+ * Meta-Engineering Harnesses paper specifically calls out as
43
+ * "recurring task domains" in their CTO-as-a-service deployment.
44
+ */
45
+ export const BUILTIN_SPECIALIZATIONS = Object.freeze([
46
+ Object.freeze({
47
+ name: "payments",
48
+ description: "Stripe / payment intent flows. Idempotency, state transitions, trust boundaries.",
49
+ keywords: Object.freeze([
50
+ "stripe",
51
+ "payment",
52
+ "paymentintent",
53
+ "charge",
54
+ "refund",
55
+ "invoice",
56
+ "checkout",
57
+ "billing",
58
+ ]),
59
+ invariants: Object.freeze([
60
+ {
61
+ id: "idempotency-key",
62
+ description: "Every state-mutating request must carry an idempotency key; the handler must short-circuit on duplicate keys.",
63
+ required: true,
64
+ },
65
+ {
66
+ id: "state-transitions",
67
+ description: "Document the allowed state transitions explicitly (pending → succeeded, pending → failed, etc.) and reject any other transition.",
68
+ required: true,
69
+ },
70
+ {
71
+ id: "trust-boundary-client-status",
72
+ description: "Never trust client-reported payment status; always verify against the upstream provider.",
73
+ required: true,
74
+ },
75
+ {
76
+ id: "discount-deduction-source",
77
+ description: "Final-invoice calculations must read deposit/discount amounts from a single source of truth, not recompute from disparate fields.",
78
+ required: false,
79
+ },
80
+ ]),
81
+ confidenceThreshold: 0.3,
82
+ }),
83
+ Object.freeze({
84
+ name: "auth",
85
+ description: "Authentication and session-token handling. Token expiry, scope checks, revocation.",
86
+ keywords: Object.freeze([
87
+ "jwt",
88
+ "session",
89
+ "token",
90
+ "oauth",
91
+ "login",
92
+ "logout",
93
+ "refresh-token",
94
+ "auth",
95
+ ]),
96
+ invariants: Object.freeze([
97
+ {
98
+ id: "token-expiry-check",
99
+ description: "Every token verification must check expiry (exp claim) and reject expired tokens.",
100
+ required: true,
101
+ },
102
+ {
103
+ id: "scope-check",
104
+ description: "Every protected endpoint must verify the token's scope grants the requested action.",
105
+ required: true,
106
+ },
107
+ {
108
+ id: "revocation-list",
109
+ description: "Maintain a revocation list (or short token lifetimes + refresh tokens) so compromised tokens can be invalidated.",
110
+ required: true,
111
+ },
112
+ ]),
113
+ confidenceThreshold: 0.3,
114
+ }),
115
+ Object.freeze({
116
+ name: "booking",
117
+ description: "Scheduling / appointment flows. Conflict detection, time-zone handling, cancellation windows.",
118
+ keywords: Object.freeze([
119
+ "booking",
120
+ "appointment",
121
+ "schedule",
122
+ "calendar",
123
+ "reservation",
124
+ "slot",
125
+ "availability",
126
+ ]),
127
+ invariants: Object.freeze([
128
+ {
129
+ id: "conflict-detection",
130
+ description: "Reserving a slot must use a transaction that rejects double-bookings (FOR UPDATE / SELECT FOR SHARE / optimistic-lock).",
131
+ required: true,
132
+ },
133
+ {
134
+ id: "timezone-explicit",
135
+ description: "Every time field must carry its time zone (or be normalised to UTC) — never assume the server's local time.",
136
+ required: true,
137
+ },
138
+ {
139
+ id: "cancellation-window",
140
+ description: "Cancellations must respect a documented cancellation window; the handler enforces it server-side regardless of UI affordance.",
141
+ required: false,
142
+ },
143
+ ]),
144
+ confidenceThreshold: 0.3,
145
+ }),
146
+ ]);
147
+ export function match(text, opts = {}) {
148
+ const registry = opts.registry ?? BUILTIN_SPECIALIZATIONS;
149
+ if (opts.mode === "strict") {
150
+ if (opts.forceMatch === undefined)
151
+ return undefined;
152
+ const spec = registry.find((s) => s.name === opts.forceMatch);
153
+ if (spec === undefined) {
154
+ throw new SpecializationRegistryError(`forceMatch "${opts.forceMatch}" not found in registry (known: ${registry
155
+ .map((s) => s.name)
156
+ .join(", ")})`);
157
+ }
158
+ return {
159
+ specialization: spec,
160
+ confidence: 1,
161
+ matchedKeywords: spec.keywords,
162
+ };
163
+ }
164
+ const lower = text.toLowerCase();
165
+ let best;
166
+ for (const spec of registry) {
167
+ const matched = [];
168
+ for (const kw of spec.keywords) {
169
+ if (lower.includes(kw.toLowerCase()))
170
+ matched.push(kw);
171
+ }
172
+ const confidence = spec.keywords.length === 0 ? 0 : matched.length / spec.keywords.length;
173
+ if (confidence >= spec.confidenceThreshold) {
174
+ if (best === undefined || confidence > best.confidence) {
175
+ best = { specialization: spec, confidence, matchedKeywords: matched };
176
+ }
177
+ }
178
+ }
179
+ return best;
180
+ }
181
+ /**
182
+ * Read JSON-encoded specialization records from a directory, merge
183
+ * them with the built-ins (overriding by `name`), and return the
184
+ * combined registry. Used by the contract compiler to pick up
185
+ * project-specific specializations from `.crewhaus/specializations/`.
186
+ */
187
+ export function loadRegistry(dir) {
188
+ const abs = resolve(dir);
189
+ let entries;
190
+ try {
191
+ entries = readdirSync(abs);
192
+ }
193
+ catch {
194
+ return BUILTIN_SPECIALIZATIONS;
195
+ }
196
+ const overrides = {};
197
+ for (const name of entries) {
198
+ if (!name.endsWith(".json"))
199
+ continue;
200
+ const path = join(abs, name);
201
+ let parsed;
202
+ try {
203
+ parsed = JSON.parse(readFileSync(path, "utf8"));
204
+ }
205
+ catch (err) {
206
+ throw new SpecializationRegistryError(`failed to parse ${path}: ${err.message}`);
207
+ }
208
+ const spec = parsed;
209
+ if (typeof spec.name !== "string" || spec.name.length === 0) {
210
+ throw new SpecializationRegistryError(`${path}: missing required "name" field`);
211
+ }
212
+ overrides[spec.name] = spec;
213
+ }
214
+ const merged = [];
215
+ const seen = new Set();
216
+ for (const builtin of BUILTIN_SPECIALIZATIONS) {
217
+ if (overrides[builtin.name] !== undefined) {
218
+ merged.push(overrides[builtin.name]);
219
+ seen.add(builtin.name);
220
+ }
221
+ else {
222
+ merged.push(builtin);
223
+ }
224
+ }
225
+ for (const [name, spec] of Object.entries(overrides)) {
226
+ if (!seen.has(name))
227
+ merged.push(spec);
228
+ }
229
+ return merged;
230
+ }
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "@crewhaus/specialization-registry",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Track G / §58 — file-backed registry of domain specializations (payments, booking, auth) that inject invariants into contract compilation. Source: Meta-Engineering Harnesses (arxiv 2605.25665, §4.4).",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
8
  "exports": {
9
- ".": "./src/index.ts"
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.4"
18
+ "@crewhaus/errors": "0.1.6"
16
19
  },
17
20
  "license": "Apache-2.0",
18
21
  "author": {
@@ -32,5 +35,5 @@
32
35
  "publishConfig": {
33
36
  "access": "public"
34
37
  },
35
- "files": ["src", "README.md", "LICENSE", "NOTICE"]
38
+ "files": ["dist", "README.md", "LICENSE", "NOTICE"]
36
39
  }
package/src/index.test.ts DELETED
@@ -1,121 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { BUILTIN_SPECIALIZATIONS, SpecializationRegistryError, loadRegistry, match } from "./index";
6
-
7
- describe("BUILTIN_SPECIALIZATIONS", () => {
8
- test("includes payments, auth, booking", () => {
9
- const names = BUILTIN_SPECIALIZATIONS.map((s) => s.name);
10
- expect(names).toContain("payments");
11
- expect(names).toContain("auth");
12
- expect(names).toContain("booking");
13
- });
14
-
15
- test("payments specialization names the four key invariants", () => {
16
- const p = BUILTIN_SPECIALIZATIONS.find((s) => s.name === "payments");
17
- const ids = p?.invariants.map((i) => i.id);
18
- expect(ids).toContain("idempotency-key");
19
- expect(ids).toContain("state-transitions");
20
- expect(ids).toContain("trust-boundary-client-status");
21
- });
22
- });
23
-
24
- describe("match (auto mode)", () => {
25
- test("matches payments when text mentions Stripe + paymentintent + refund", () => {
26
- const result = match("Implement Stripe paymentIntent flow with refund support and idempotency");
27
- expect(result?.specialization.name).toBe("payments");
28
- expect(result?.confidence).toBeGreaterThanOrEqual(0.3);
29
- });
30
-
31
- test("matches auth when text mentions jwt + login + token", () => {
32
- const result = match("We need to add JWT-based login with refresh-token and revocation");
33
- expect(result?.specialization.name).toBe("auth");
34
- });
35
-
36
- test("matches booking on appointment + slot + calendar", () => {
37
- const result = match("Online booking with appointment slot calendar availability");
38
- expect(result?.specialization.name).toBe("booking");
39
- });
40
-
41
- test("returns undefined when no specialization is confident enough", () => {
42
- expect(match("Add a CSS spinner to the homepage")).toBeUndefined();
43
- });
44
-
45
- test("returns the highest-confidence specialization when multiple match", () => {
46
- // Both auth and payments keywords; auth has more keyword density.
47
- const result = match("session token jwt oauth refresh-token login logout stripe");
48
- expect(result?.specialization.name).toBe("auth");
49
- });
50
- });
51
-
52
- describe("match (strict mode)", () => {
53
- test("returns named specialization with confidence 1 when forceMatch is set", () => {
54
- const result = match("anything at all", { mode: "strict", forceMatch: "payments" });
55
- expect(result?.specialization.name).toBe("payments");
56
- expect(result?.confidence).toBe(1);
57
- });
58
-
59
- test("returns undefined when forceMatch is omitted in strict mode", () => {
60
- expect(match("stripe refund paymentintent", { mode: "strict" })).toBeUndefined();
61
- });
62
-
63
- test("throws when forceMatch names an unknown specialization", () => {
64
- expect(() => match("x", { mode: "strict", forceMatch: "nonexistent" })).toThrow(
65
- SpecializationRegistryError,
66
- );
67
- });
68
- });
69
-
70
- describe("loadRegistry", () => {
71
- let dir: string;
72
- beforeEach(() => {
73
- dir = mkdtempSync(join(tmpdir(), "spec-registry-test-"));
74
- });
75
- afterEach(() => {
76
- rmSync(dir, { recursive: true, force: true });
77
- });
78
-
79
- test("returns built-ins when directory is empty", () => {
80
- const r = loadRegistry(dir);
81
- expect(r.length).toBe(BUILTIN_SPECIALIZATIONS.length);
82
- });
83
-
84
- test("returns built-ins when directory is missing", () => {
85
- const r = loadRegistry(join(dir, "missing"));
86
- expect(r.length).toBe(BUILTIN_SPECIALIZATIONS.length);
87
- });
88
-
89
- test("project-local files override built-ins by name", () => {
90
- const override = {
91
- name: "payments",
92
- description: "custom payments",
93
- keywords: ["stripe"],
94
- invariants: [{ id: "x", description: "x", required: true }],
95
- confidenceThreshold: 0.1,
96
- };
97
- writeFileSync(join(dir, "payments.json"), JSON.stringify(override));
98
- const r = loadRegistry(dir);
99
- const p = r.find((s) => s.name === "payments");
100
- expect(p?.description).toBe("custom payments");
101
- expect(p?.invariants.length).toBe(1);
102
- });
103
-
104
- test("project-local file with a new name is appended", () => {
105
- const fresh = {
106
- name: "ecommerce-checkout",
107
- description: "new spec",
108
- keywords: ["checkout", "cart"],
109
- invariants: [{ id: "i", description: "i", required: true }],
110
- confidenceThreshold: 0.5,
111
- };
112
- writeFileSync(join(dir, "ecommerce.json"), JSON.stringify(fresh));
113
- const r = loadRegistry(dir);
114
- expect(r.some((s) => s.name === "ecommerce-checkout")).toBe(true);
115
- });
116
-
117
- test("throws on malformed JSON", () => {
118
- writeFileSync(join(dir, "bad.json"), "{this is not json}");
119
- expect(() => loadRegistry(dir)).toThrow(SpecializationRegistryError);
120
- });
121
- });
package/src/index.ts DELETED
@@ -1,287 +0,0 @@
1
- /**
2
- * Track G (§58) — `specialization-registry`. File-backed registry of
3
- * domain specializations.
4
- *
5
- * Source: Meta-Engineering Harnesses (Sengupta et al., HireNimbus,
6
- * arxiv 2605.25665, §4.4). A specialization is a named collection of
7
- * domain-specific invariants — for "payments" the invariants are
8
- * idempotency keys, explicit state transitions, trust-boundary checks;
9
- * for "auth" they're token expiry, scope checks, etc. The contract
10
- * compiler (Track G in the same plan) calls into this registry to
11
- * inject these invariants into the compiled contract.
12
- *
13
- * The paper's key safety rule: specializations apply only when
14
- * **confidence crosses a threshold**. Below that, the pipeline
15
- * proceeds without specialization to avoid corrupting the contract
16
- * with wrong domain assumptions. The registry stores
17
- * `(name, invariants, confidenceThreshold)` triples and exposes a
18
- * `match(text, opts) → SpecializationMatch | undefined` function.
19
- *
20
- * v0 ships:
21
- * - In-memory registry seeded with three reference specializations
22
- * (payments, auth, booking).
23
- * - File-backed registry that reads from
24
- * `.crewhaus/specializations/<name>.json` (overrides built-ins).
25
- * - `match()` — keyword-based confidence scoring; the contract
26
- * compiler can pass `mode: "strict"` to require explicit opt-in
27
- * and skip auto-detection entirely.
28
- *
29
- * Cited paper: Meta-Engineering Harnesses (arxiv 2605.25665, §4.4).
30
- */
31
- import { readFileSync, readdirSync } from "node:fs";
32
- import { join, resolve } from "node:path";
33
- import { CrewhausError } from "@crewhaus/errors";
34
-
35
- export class SpecializationRegistryError extends CrewhausError {
36
- override readonly name = "SpecializationRegistryError";
37
- constructor(message: string, cause?: unknown) {
38
- super("config", message, cause);
39
- }
40
- }
41
-
42
- /**
43
- * One named invariant a specialization injects into a contract. The
44
- * contract compiler maps each to a clause in the compiled YAML.
45
- */
46
- export type Invariant = {
47
- readonly id: string;
48
- readonly description: string;
49
- /** Severity for the contract reviewer: must-have vs nice-to-have. */
50
- readonly required: boolean;
51
- };
52
-
53
- /**
54
- * A specialization record. `keywords` are used by the default
55
- * confidence-scoring heuristic; the score is the fraction of keywords
56
- * present in the input text (case-insensitive substring match).
57
- */
58
- export type Specialization = {
59
- readonly name: string;
60
- readonly description: string;
61
- readonly keywords: ReadonlyArray<string>;
62
- readonly invariants: ReadonlyArray<Invariant>;
63
- /** Apply only when confidence >= this. Default 0.5. */
64
- readonly confidenceThreshold: number;
65
- };
66
-
67
- export type SpecializationMatch = {
68
- readonly specialization: Specialization;
69
- readonly confidence: number;
70
- /** Which keywords were hit; for audit + debugging. */
71
- readonly matchedKeywords: ReadonlyArray<string>;
72
- };
73
-
74
- /**
75
- * Built-in reference specializations. These are the ones the
76
- * Meta-Engineering Harnesses paper specifically calls out as
77
- * "recurring task domains" in their CTO-as-a-service deployment.
78
- */
79
- export const BUILTIN_SPECIALIZATIONS: ReadonlyArray<Specialization> = Object.freeze([
80
- Object.freeze({
81
- name: "payments",
82
- description: "Stripe / payment intent flows. Idempotency, state transitions, trust boundaries.",
83
- keywords: Object.freeze([
84
- "stripe",
85
- "payment",
86
- "paymentintent",
87
- "charge",
88
- "refund",
89
- "invoice",
90
- "checkout",
91
- "billing",
92
- ]),
93
- invariants: Object.freeze([
94
- {
95
- id: "idempotency-key",
96
- description:
97
- "Every state-mutating request must carry an idempotency key; the handler must short-circuit on duplicate keys.",
98
- required: true,
99
- },
100
- {
101
- id: "state-transitions",
102
- description:
103
- "Document the allowed state transitions explicitly (pending → succeeded, pending → failed, etc.) and reject any other transition.",
104
- required: true,
105
- },
106
- {
107
- id: "trust-boundary-client-status",
108
- description:
109
- "Never trust client-reported payment status; always verify against the upstream provider.",
110
- required: true,
111
- },
112
- {
113
- id: "discount-deduction-source",
114
- description:
115
- "Final-invoice calculations must read deposit/discount amounts from a single source of truth, not recompute from disparate fields.",
116
- required: false,
117
- },
118
- ]),
119
- confidenceThreshold: 0.3,
120
- }),
121
- Object.freeze({
122
- name: "auth",
123
- description:
124
- "Authentication and session-token handling. Token expiry, scope checks, revocation.",
125
- keywords: Object.freeze([
126
- "jwt",
127
- "session",
128
- "token",
129
- "oauth",
130
- "login",
131
- "logout",
132
- "refresh-token",
133
- "auth",
134
- ]),
135
- invariants: Object.freeze([
136
- {
137
- id: "token-expiry-check",
138
- description:
139
- "Every token verification must check expiry (exp claim) and reject expired tokens.",
140
- required: true,
141
- },
142
- {
143
- id: "scope-check",
144
- description:
145
- "Every protected endpoint must verify the token's scope grants the requested action.",
146
- required: true,
147
- },
148
- {
149
- id: "revocation-list",
150
- description:
151
- "Maintain a revocation list (or short token lifetimes + refresh tokens) so compromised tokens can be invalidated.",
152
- required: true,
153
- },
154
- ]),
155
- confidenceThreshold: 0.3,
156
- }),
157
- Object.freeze({
158
- name: "booking",
159
- description:
160
- "Scheduling / appointment flows. Conflict detection, time-zone handling, cancellation windows.",
161
- keywords: Object.freeze([
162
- "booking",
163
- "appointment",
164
- "schedule",
165
- "calendar",
166
- "reservation",
167
- "slot",
168
- "availability",
169
- ]),
170
- invariants: Object.freeze([
171
- {
172
- id: "conflict-detection",
173
- description:
174
- "Reserving a slot must use a transaction that rejects double-bookings (FOR UPDATE / SELECT FOR SHARE / optimistic-lock).",
175
- required: true,
176
- },
177
- {
178
- id: "timezone-explicit",
179
- description:
180
- "Every time field must carry its time zone (or be normalised to UTC) — never assume the server's local time.",
181
- required: true,
182
- },
183
- {
184
- id: "cancellation-window",
185
- description:
186
- "Cancellations must respect a documented cancellation window; the handler enforces it server-side regardless of UI affordance.",
187
- required: false,
188
- },
189
- ]),
190
- confidenceThreshold: 0.3,
191
- }),
192
- ]);
193
-
194
- /**
195
- * Match a free-text issue/contract draft against the registered
196
- * specializations. Returns the highest-confidence match whose score
197
- * is >= its threshold, or undefined when no specialization matches.
198
- *
199
- * `mode: "strict"` skips auto-detection and only returns a match if
200
- * the caller explicitly named one in `forceMatch`.
201
- */
202
- export type MatchOptions = {
203
- readonly mode?: "auto" | "strict";
204
- readonly forceMatch?: string;
205
- readonly registry?: ReadonlyArray<Specialization>;
206
- };
207
-
208
- export function match(text: string, opts: MatchOptions = {}): SpecializationMatch | undefined {
209
- const registry = opts.registry ?? BUILTIN_SPECIALIZATIONS;
210
- if (opts.mode === "strict") {
211
- if (opts.forceMatch === undefined) return undefined;
212
- const spec = registry.find((s) => s.name === opts.forceMatch);
213
- if (spec === undefined) {
214
- throw new SpecializationRegistryError(
215
- `forceMatch "${opts.forceMatch}" not found in registry (known: ${registry
216
- .map((s) => s.name)
217
- .join(", ")})`,
218
- );
219
- }
220
- return {
221
- specialization: spec,
222
- confidence: 1,
223
- matchedKeywords: spec.keywords,
224
- };
225
- }
226
- const lower = text.toLowerCase();
227
- let best: SpecializationMatch | undefined;
228
- for (const spec of registry) {
229
- const matched: string[] = [];
230
- for (const kw of spec.keywords) {
231
- if (lower.includes(kw.toLowerCase())) matched.push(kw);
232
- }
233
- const confidence = spec.keywords.length === 0 ? 0 : matched.length / spec.keywords.length;
234
- if (confidence >= spec.confidenceThreshold) {
235
- if (best === undefined || confidence > best.confidence) {
236
- best = { specialization: spec, confidence, matchedKeywords: matched };
237
- }
238
- }
239
- }
240
- return best;
241
- }
242
-
243
- /**
244
- * Read JSON-encoded specialization records from a directory, merge
245
- * them with the built-ins (overriding by `name`), and return the
246
- * combined registry. Used by the contract compiler to pick up
247
- * project-specific specializations from `.crewhaus/specializations/`.
248
- */
249
- export function loadRegistry(dir: string): ReadonlyArray<Specialization> {
250
- const abs = resolve(dir);
251
- let entries: string[];
252
- try {
253
- entries = readdirSync(abs);
254
- } catch {
255
- return BUILTIN_SPECIALIZATIONS;
256
- }
257
- const overrides: Record<string, Specialization> = {};
258
- for (const name of entries) {
259
- if (!name.endsWith(".json")) continue;
260
- const path = join(abs, name);
261
- let parsed: unknown;
262
- try {
263
- parsed = JSON.parse(readFileSync(path, "utf8"));
264
- } catch (err) {
265
- throw new SpecializationRegistryError(`failed to parse ${path}: ${(err as Error).message}`);
266
- }
267
- const spec = parsed as Specialization;
268
- if (typeof spec.name !== "string" || spec.name.length === 0) {
269
- throw new SpecializationRegistryError(`${path}: missing required "name" field`);
270
- }
271
- overrides[spec.name] = spec;
272
- }
273
- const merged: Specialization[] = [];
274
- const seen = new Set<string>();
275
- for (const builtin of BUILTIN_SPECIALIZATIONS) {
276
- if (overrides[builtin.name] !== undefined) {
277
- merged.push(overrides[builtin.name] as Specialization);
278
- seen.add(builtin.name);
279
- } else {
280
- merged.push(builtin);
281
- }
282
- }
283
- for (const [name, spec] of Object.entries(overrides)) {
284
- if (!seen.has(name)) merged.push(spec);
285
- }
286
- return merged;
287
- }