@stonekeep/ai 0.0.0-stage → 0.8.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.
@@ -0,0 +1,106 @@
1
+ import { normalizeName } from '@stonekeep/core';
2
+ const SUBJECT_NAMES = /^(users?|customers?|members?|accounts?|people|persons?|patients?|clients?|profiles?|contacts?)$/;
3
+ const RETAIN_NAMES = /(invoice|payment|transaction|ledger|receipt|credit_note|tax)/;
4
+ const BOOKKEEPING = /^(_?prisma_migrations|schema_migrations|knex_migrations|flyway_schema_history|migrations|typeorm_metadata)$/;
5
+ /** Column-level strategy from name, type and classification. Deterministic, offline. */
6
+ export function heuristicStrategy(column, type, rule) {
7
+ const n = normalizeName(column);
8
+ const t = (type ?? '').toLowerCase();
9
+ // Not classified as personal data: only free text gets masked.
10
+ if (rule === null)
11
+ return /(^|_)(notes?|comments?|body|message|description|bio)$/.test(n) ? 'token' : 'keep';
12
+ if (/\b(inet|cidr)\b/.test(t))
13
+ return 'ip';
14
+ if (/\b(date|timestamp|datetime)\b/.test(t))
15
+ return 'date';
16
+ if (/\buuid\b/.test(t))
17
+ return 'uuid';
18
+ if (/\bjsonb?\b/.test(t))
19
+ return 'json-empty';
20
+ const numeric = /\b(int|integer|bigint|numeric|decimal|float|double|number)\b/.test(t);
21
+ switch (rule) {
22
+ case 'email':
23
+ return 'email';
24
+ case 'phone':
25
+ return numeric ? 'number' : 'phone';
26
+ case 'person-name':
27
+ if (/(^|_)(first|given|fore)/.test(n))
28
+ return 'first-name';
29
+ if (/(^|_)(last|family|sur|maiden)/.test(n))
30
+ return 'last-name';
31
+ return 'full-name';
32
+ case 'username':
33
+ return 'username';
34
+ case 'online-identifier':
35
+ return /(^|_)ip(_|$)|addr/.test(n) ? 'ip' : 'token';
36
+ case 'postal-address':
37
+ if (/zip|post_?code|postal/.test(n))
38
+ return 'zip';
39
+ if (/(^|_)(city|town)($|_)/.test(n))
40
+ return 'city';
41
+ if (/lat|lon|lng|geo|coord|gps/.test(n))
42
+ return 'geo';
43
+ return 'street';
44
+ case 'date-of-birth':
45
+ return 'date';
46
+ default:
47
+ return numeric ? 'number' : 'token';
48
+ }
49
+ }
50
+ /** A rule-based proposal (no LLM): used offline, as the MCP planning tool, and as the eval baseline. */
51
+ export function heuristicProposal(metadata) {
52
+ const scored = metadata.entities.map((e) => ({
53
+ e,
54
+ pii: e.columns.filter((c) => c.pii).length,
55
+ named: SUBJECT_NAMES.test(normalizeName(e.name)),
56
+ }));
57
+ const subject = scored.find((s) => s.named && s.pii > 0) ?? [...scored].sort((a, b) => b.pii - a.pii)[0];
58
+ const subjectName = subject?.e.name ?? 'users';
59
+ const key = subject?.e.columns.find((c) => c.name === 'id')?.name ?? 'id';
60
+ const subjectRef = normalizeName(subjectName).replace(/s$/, '');
61
+ const mask = [];
62
+ const retain = [];
63
+ const foreignKeys = [];
64
+ const exclude = [];
65
+ for (const e of metadata.entities) {
66
+ const n = normalizeName(e.name);
67
+ if (BOOKKEEPING.test(n)) {
68
+ exclude.push(e.name);
69
+ continue;
70
+ }
71
+ for (const c of e.columns) {
72
+ const s = heuristicStrategy(c.name, c.type, c.pii?.rule ?? null);
73
+ if (s !== 'keep')
74
+ mask.push({ table: e.name, column: c.name, strategy: s });
75
+ const cn = normalizeName(c.name);
76
+ if (e.name !== subjectName &&
77
+ /_(id|ref)$/.test(cn) &&
78
+ (cn.startsWith(`${subjectRef}_`) ||
79
+ /^(requester|owner|author|customer|account)_(id|ref)$/.test(cn))) {
80
+ foreignKeys.push({ table: e.name, column: c.name, references: `${subjectName}.${key}` });
81
+ }
82
+ }
83
+ if (RETAIN_NAMES.test(n) && e.name !== subjectName) {
84
+ retain.push({
85
+ table: e.name,
86
+ reason: 'Accounting records are usually subject to statutory retention (verify the period)',
87
+ anonymize: e.columns
88
+ .filter((c) => c.pii?.rule === 'email' || c.pii?.rule === 'phone')
89
+ .map((c) => c.name),
90
+ });
91
+ }
92
+ }
93
+ return {
94
+ subject: { table: subjectName, key },
95
+ retain,
96
+ foreignKeys,
97
+ mask,
98
+ exclude,
99
+ notes: ['Generated by the offline heuristic planner: review every entry before use.'],
100
+ };
101
+ }
102
+ export const heuristicProvider = (metadata) => ({
103
+ name: 'heuristic',
104
+ propose: () => Promise.resolve(heuristicProposal(metadata)),
105
+ });
106
+ //# sourceMappingURL=heuristic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heuristic.js","sourceRoot":"","sources":["../src/heuristic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export { PrivacyGuardError, assertSafeToSend, inspectPayload, type FindingKind, type GuardFinding, } from './guard.js';
2
+ export { buildSchemaMetadata, type MetadataInput, type SchemaMetadata } from './metadata.js';
3
+ export { ProposalSchema, type Proposal } from './proposal.js';
4
+ export { SYSTEM_PROMPT, buildPrompt } from './prompt.js';
5
+ export { DEFAULT_MODEL, GuardedProvider, createAnthropicProvider, type AnthropicProviderOptions, type PromptPayload, type ProposalProvider, } from './providers.js';
6
+ export { heuristicProposal, heuristicProvider, heuristicStrategy } from './heuristic.js';
7
+ export { renderProposal, type RenderedProposal } from './render.js';
8
+ export { diffLines, renderDiff, type DiffLine } from './diff.js';
9
+ export { runSetup, type SetupIo, type SetupOptions, type SetupResult } from './setup.js';
10
+ export { formatReport, goldenMetadata, loadGolden, runEval, scoreProposal, type EvalReport, type GoldenSet, } from './eval.js';
11
+ export { renderSkillMd } from './skill.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { PrivacyGuardError, assertSafeToSend, inspectPayload, } from './guard.js';
2
+ export { buildSchemaMetadata } from './metadata.js';
3
+ export { ProposalSchema } from './proposal.js';
4
+ export { SYSTEM_PROMPT, buildPrompt } from './prompt.js';
5
+ export { DEFAULT_MODEL, GuardedProvider, createAnthropicProvider, } from './providers.js';
6
+ export { heuristicProposal, heuristicProvider, heuristicStrategy } from './heuristic.js';
7
+ export { renderProposal } from './render.js';
8
+ export { diffLines, renderDiff } from './diff.js';
9
+ export { runSetup } from './setup.js';
10
+ export { formatReport, goldenMetadata, loadGolden, runEval, scoreProposal, } from './eval.js';
11
+ export { renderSkillMd } from './skill.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,GAGf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,mBAAmB,EAA2C,MAAM,eAAe,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAiB,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,aAAa,EACb,eAAe,EACf,uBAAuB,GAIxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAyB,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAiB,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAqD,MAAM,YAAY,CAAC;AACzF,OAAO,EACL,YAAY,EACZ,cAAc,EACd,UAAU,EACV,OAAO,EACP,aAAa,GAGd,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { DataMap, SchemaField } from '@stonekeep/core';
2
+ /**
3
+ * The ONLY data the setup assistant sends to an LLM: names, types and
4
+ * classifications. No values, file contents, or comments.
5
+ */
6
+ export interface SchemaMetadata {
7
+ entities: {
8
+ name: string;
9
+ columns: {
10
+ name: string;
11
+ type: string | null;
12
+ pii: {
13
+ category: string;
14
+ rule: string;
15
+ confidence: number;
16
+ } | null;
17
+ }[];
18
+ }[];
19
+ }
20
+ export interface MetadataInput {
21
+ datamap: DataMap;
22
+ /** All schema fields from `collectSchemaFields` (PII or not), for context. */
23
+ fields?: readonly SchemaField[];
24
+ }
25
+ /**
26
+ * Build the metadata payload from the datamap (+ optional full field list).
27
+ * Strict allowlist: every identifier and type name must match a narrow
28
+ * pattern, nested JSON paths are skipped, and the result is re-checked by the
29
+ * privacy guard. Throws PrivacyGuardError rather than silently dropping data.
30
+ */
31
+ export declare function buildSchemaMetadata(input: MetadataInput): SchemaMetadata;
@@ -0,0 +1,55 @@
1
+ import { IDENTIFIER, PrivacyGuardError, TYPE_NAME, assertSafeToSend, } from './guard.js';
2
+ /**
3
+ * Build the metadata payload from the datamap (+ optional full field list).
4
+ * Strict allowlist: every identifier and type name must match a narrow
5
+ * pattern, nested JSON paths are skipped, and the result is re-checked by the
6
+ * privacy guard. Throws PrivacyGuardError rather than silently dropping data.
7
+ */
8
+ export function buildSchemaMetadata(input) {
9
+ const findings = [];
10
+ const entities = new Map();
11
+ const column = (entity, field, path) => {
12
+ // Validate first: an unsafe name fails loudly even if it would be skipped below.
13
+ if (!IDENTIFIER.test(entity))
14
+ findings.push({ kind: 'identifier', path: `${path}.entity` });
15
+ if (!IDENTIFIER.test(field))
16
+ findings.push({ kind: 'identifier', path: `${path}.field` });
17
+ if (field.includes('.'))
18
+ return undefined; // nested JSON/OpenAPI paths are not database columns
19
+ let cols = entities.get(entity);
20
+ if (!cols) {
21
+ cols = new Map();
22
+ entities.set(entity, cols);
23
+ }
24
+ let col = cols.get(field);
25
+ if (!col) {
26
+ col = { name: field, type: null, pii: null };
27
+ cols.set(field, col);
28
+ }
29
+ return col;
30
+ };
31
+ (input.fields ?? []).forEach((f, i) => {
32
+ const col = column(f.entity, f.field, `$.fields[${i}]`);
33
+ if (col && f.rawType !== undefined) {
34
+ if (TYPE_NAME.test(f.rawType))
35
+ col.type = f.rawType;
36
+ else
37
+ findings.push({ kind: 'identifier', path: `$.fields[${i}].rawType` });
38
+ }
39
+ });
40
+ input.datamap.fields.forEach((f, i) => {
41
+ const col = column(f.entity, f.field, `$.datamap.fields[${i}]`);
42
+ if (col)
43
+ col.pii = { category: f.category, rule: f.rule, confidence: f.confidence };
44
+ });
45
+ if (findings.length > 0)
46
+ throw new PrivacyGuardError(findings);
47
+ const metadata = {
48
+ entities: [...entities.entries()]
49
+ .sort(([a], [b]) => (a < b ? -1 : 1))
50
+ .map(([name, cols]) => ({ name, columns: [...cols.values()] })),
51
+ };
52
+ assertSafeToSend(metadata);
53
+ return metadata;
54
+ }
55
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { SchemaMetadata } from './metadata.js';
2
+ export declare const SYSTEM_PROMPT: string;
3
+ export declare function buildPrompt(metadata: SchemaMetadata): {
4
+ system: string;
5
+ user: string;
6
+ };
package/dist/prompt.js ADDED
@@ -0,0 +1,20 @@
1
+ import { MASK_STRATEGIES } from '@stonekeep/core/engine';
2
+ export const SYSTEM_PROMPT = `You help engineers configure Stonekeep, a tool that exports, deletes, anonymizes and masks personal data.
3
+ You receive schema metadata only: entity (table) names, column names, column types, and the classification Stonekeep's scanner assigned to personal-data columns. You never see data values.
4
+
5
+ Propose a connector configuration:
6
+ - subject: the entity that represents an individual person (customer, user, member, patient...) and the column that identifies one person, normally its primary key (often "id").
7
+ - retain: entities holding records that law or accounting typically requires keeping after a person is erased, such as invoices, payments, tax or ledger records. Give a short reason and list only the personal-data columns in that entity that can be anonymized without breaking the record's purpose. Do not retain ordinary application data.
8
+ - foreignKeys: only relations that a database likely does not enforce, where a column name clearly points at the subject (for example "requester_id" or "customer_ref" referencing "users.id"). Leave the list empty when unsure: Stonekeep discovers enforced foreign keys itself.
9
+ - mask: one entry for every column classified as personal data, plus free-text columns that may contain it (notes, comments, bodies, messages). Strategies: ${MASK_STRATEGIES.join(', ')}. Match the strategy to the column type and meaning: emails -> email, phone numbers -> phone, first/last/full names -> first-name/last-name/full-name, IP addresses -> ip, postal address parts -> street/city/zip, coordinates -> geo, dates of birth -> date, numeric identifiers -> number, UUIDs -> uuid, free text and opaque identifiers (government ids, card numbers, tokens, external account ids) -> token, JSON blobs -> json-empty. Use keep only for columns that are not personal data.
10
+ - exclude: bookkeeping tables such as schema migrations.
11
+ - notes: up to 5 short points a reviewer should check. Never claim the result guarantees legal compliance.
12
+
13
+ Use only entity and column names that appear in the metadata, spelled exactly as given.`;
14
+ export function buildPrompt(metadata) {
15
+ return {
16
+ system: SYSTEM_PROMPT,
17
+ user: `Schema metadata (JSON):\n${JSON.stringify(metadata)}`,
18
+ };
19
+ }
20
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;8JAOiI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;;;;wFAIhG,CAAC;AAEzF,MAAM,UAAU,WAAW,CAAC,QAAwB;IAClD,OAAO;QACL,MAAM,EAAE,aAAa;QACrB,IAAI,EAAE,4BAA4B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;KAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod/v4';
2
+ /**
3
+ * What the assistant must return (enforced with structured outputs). Arrays
4
+ * instead of maps keep the JSON Schema strict-mode friendly.
5
+ */
6
+ export declare const ProposalSchema: z.ZodObject<{
7
+ subject: z.ZodObject<{
8
+ table: z.ZodString;
9
+ key: z.ZodString;
10
+ }, z.core.$strip>;
11
+ retain: z.ZodArray<z.ZodObject<{
12
+ table: z.ZodString;
13
+ reason: z.ZodString;
14
+ anonymize: z.ZodArray<z.ZodString>;
15
+ }, z.core.$strip>>;
16
+ foreignKeys: z.ZodArray<z.ZodObject<{
17
+ table: z.ZodString;
18
+ column: z.ZodString;
19
+ references: z.ZodString;
20
+ }, z.core.$strip>>;
21
+ mask: z.ZodArray<z.ZodObject<{
22
+ table: z.ZodString;
23
+ column: z.ZodString;
24
+ strategy: z.ZodEnum<{
25
+ number: "number";
26
+ email: "email";
27
+ phone: "phone";
28
+ date: "date";
29
+ "first-name": "first-name";
30
+ "last-name": "last-name";
31
+ "full-name": "full-name";
32
+ username: "username";
33
+ ip: "ip";
34
+ street: "street";
35
+ city: "city";
36
+ zip: "zip";
37
+ geo: "geo";
38
+ uuid: "uuid";
39
+ token: "token";
40
+ "json-empty": "json-empty";
41
+ null: "null";
42
+ keep: "keep";
43
+ }>;
44
+ }, z.core.$strip>>;
45
+ exclude: z.ZodArray<z.ZodString>;
46
+ notes: z.ZodArray<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export type Proposal = z.infer<typeof ProposalSchema>;
@@ -0,0 +1,38 @@
1
+ import { MASK_STRATEGIES } from '@stonekeep/core/engine';
2
+ import { z } from 'zod/v4';
3
+ /**
4
+ * What the assistant must return (enforced with structured outputs). Arrays
5
+ * instead of maps keep the JSON Schema strict-mode friendly.
6
+ */
7
+ export const ProposalSchema = z.object({
8
+ subject: z.object({
9
+ table: z.string().describe('Entity that represents the person (data subject), e.g. users'),
10
+ key: z.string().describe('Column that identifies the subject, usually its primary key'),
11
+ }),
12
+ retain: z
13
+ .array(z.object({
14
+ table: z.string(),
15
+ reason: z
16
+ .string()
17
+ .describe('Short legal/business reason, e.g. "Invoices must be kept for tax purposes"'),
18
+ anonymize: z
19
+ .array(z.string())
20
+ .describe('Personal-data columns to anonymize in retained rows'),
21
+ }))
22
+ .describe('Tables whose rows must be kept (e.g. accounting records) and detached from the person'),
23
+ foreignKeys: z
24
+ .array(z.object({
25
+ table: z.string(),
26
+ column: z.string(),
27
+ references: z.string().describe('"parent_table.column"'),
28
+ }))
29
+ .describe('Only relations the database probably does NOT enforce (soft references such as requester_id)'),
30
+ mask: z
31
+ .array(z.object({ table: z.string(), column: z.string(), strategy: z.enum(MASK_STRATEGIES) }))
32
+ .describe('Staging masking strategy per personal-data column'),
33
+ exclude: z
34
+ .array(z.string())
35
+ .describe('Tables Stonekeep should never touch (e.g. migration bookkeeping)'),
36
+ notes: z.array(z.string()).describe('Short review notes for the human; no personal data'),
37
+ });
38
+ //# sourceMappingURL=proposal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../src/proposal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAE3B;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8DAA8D,CAAC;QAC1F,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,6DAA6D,CAAC;KACxF,CAAC;IACF,MAAM,EAAE,CAAC;SACN,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;QACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,MAAM,EAAE,CAAC;aACN,MAAM,EAAE;aACR,QAAQ,CAAC,4EAA4E,CAAC;QACzF,SAAS,EAAE,CAAC;aACT,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;aACjB,QAAQ,CAAC,qDAAqD,CAAC;KACnE,CAAC,CACH;SACA,QAAQ,CACP,uFAAuF,CACxF;IACH,WAAW,EAAE,CAAC;SACX,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;QACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;KACzD,CAAC,CACH;SACA,QAAQ,CACP,8FAA8F,CAC/F;IACH,IAAI,EAAE,CAAC;SACJ,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;SAC7F,QAAQ,CAAC,mDAAmD,CAAC;IAChE,OAAO,EAAE,CAAC;SACP,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,CAAC,kEAAkE,CAAC;IAC/E,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,oDAAoD,CAAC;CAC1F,CAAC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { type Proposal } from './proposal.js';
3
+ export interface PromptPayload {
4
+ system: string;
5
+ user: string;
6
+ }
7
+ /** Something that turns a prompt about schema metadata into a proposal. */
8
+ export interface ProposalProvider {
9
+ readonly name: string;
10
+ propose(prompt: PromptPayload): Promise<Proposal>;
11
+ }
12
+ /**
13
+ * Wraps a provider so the privacy guard runs on the exact payload
14
+ * immediately before any network request. Providers that talk to an LLM are
15
+ * only ever handed out wrapped in this.
16
+ */
17
+ export declare class GuardedProvider implements ProposalProvider {
18
+ readonly name: string;
19
+ private readonly inner;
20
+ constructor(inner: ProposalProvider);
21
+ propose(prompt: PromptPayload): Promise<Proposal>;
22
+ }
23
+ export declare const DEFAULT_MODEL = "claude-opus-5-5";
24
+ export interface AnthropicProviderOptions {
25
+ model?: string;
26
+ apiKey?: string;
27
+ /** Inject fetch (tests). Default: global fetch. */
28
+ fetch?: typeof fetch;
29
+ client?: Anthropic;
30
+ }
31
+ /** The only way to obtain the Anthropic provider: always behind the privacy guard. */
32
+ export declare function createAnthropicProvider(options?: AnthropicProviderOptions): ProposalProvider;
@@ -0,0 +1,66 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { betaZodOutputFormat } from '@anthropic-ai/sdk/helpers/beta/zod';
3
+ import { StonekeepError } from '@stonekeep/core';
4
+ import { assertSafeToSend } from './guard.js';
5
+ import { ProposalSchema } from './proposal.js';
6
+ /**
7
+ * Wraps a provider so the privacy guard runs on the exact payload
8
+ * immediately before any network request. Providers that talk to an LLM are
9
+ * only ever handed out wrapped in this.
10
+ */
11
+ export class GuardedProvider {
12
+ name;
13
+ inner;
14
+ constructor(inner) {
15
+ this.inner = inner;
16
+ this.name = inner.name;
17
+ }
18
+ async propose(prompt) {
19
+ assertSafeToSend(prompt); // rejects before the inner provider can open a connection
20
+ return this.inner.propose(prompt);
21
+ }
22
+ }
23
+ export const DEFAULT_MODEL = 'claude-opus-5-5';
24
+ /** Claude via the official SDK with structured outputs (validated against ProposalSchema). */
25
+ class AnthropicProposalProvider {
26
+ name;
27
+ client;
28
+ model;
29
+ constructor(options) {
30
+ this.model = options.model ?? DEFAULT_MODEL;
31
+ this.name = `anthropic:${this.model}`;
32
+ this.client =
33
+ options.client ??
34
+ new Anthropic({
35
+ ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
36
+ ...(options.fetch !== undefined ? { fetch: options.fetch } : {}),
37
+ });
38
+ }
39
+ async propose(prompt) {
40
+ const response = await this.client.beta.messages.parse({
41
+ model: this.model,
42
+ max_tokens: 16000,
43
+ // Server-side fallback if a safety classifier declines the request.
44
+ betas: ['server-side-fallback-2026-07-01'],
45
+ fallbacks: 'default',
46
+ output_config: { effort: 'high', format: betaZodOutputFormat(ProposalSchema) },
47
+ system: prompt.system,
48
+ messages: [{ role: 'user', content: prompt.user }],
49
+ });
50
+ if (response.stop_reason === 'refusal') {
51
+ throw new StonekeepError('CONFIG', 'The model declined to produce a proposal; configure Stonekeep manually or try again');
52
+ }
53
+ if (response.stop_reason === 'max_tokens') {
54
+ throw new StonekeepError('CONFIG', 'The proposal was cut off (max_tokens); the schema may be too large for one request');
55
+ }
56
+ if (response.parsed_output === null) {
57
+ throw new StonekeepError('CONFIG', 'The model response did not match the proposal schema');
58
+ }
59
+ return response.parsed_output;
60
+ }
61
+ }
62
+ /** The only way to obtain the Anthropic provider: always behind the privacy guard. */
63
+ export function createAnthropicProvider(options = {}) {
64
+ return new GuardedProvider(new AnthropicProposalProvider(options));
65
+ }
66
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { SchemaMetadata } from './metadata.js';
2
+ import type { Proposal } from './proposal.js';
3
+ export interface RenderedProposal {
4
+ /** Connector config object (validated). */
5
+ config: Record<string, unknown>;
6
+ yaml: string;
7
+ /** Entries dropped because they reference names absent from the metadata, etc. */
8
+ warnings: string[];
9
+ }
10
+ /**
11
+ * Turn a proposal into a connector config. Unknown entities/columns are
12
+ * dropped (models can hallucinate names). `connection`, `saas`, `consent`
13
+ * and other settings in an existing config are kept as they are — the
14
+ * assistant never touches credentials or connections.
15
+ */
16
+ export declare function renderProposal(proposal: Proposal, metadata: SchemaMetadata, existing?: Record<string, unknown>): RenderedProposal;
package/dist/render.js ADDED
@@ -0,0 +1,86 @@
1
+ import { connectorConfigSchema } from '@stonekeep/core/engine';
2
+ import { stringify } from 'yaml';
3
+ const HEADER = `# stonekeep.connector.yaml — proposed by \`stonekeep setup\` from schema metadata only.
4
+ # Review every entry: the proposal is a starting point, not legal advice.
5
+ `;
6
+ /**
7
+ * Turn a proposal into a connector config. Unknown entities/columns are
8
+ * dropped (models can hallucinate names). `connection`, `saas`, `consent`
9
+ * and other settings in an existing config are kept as they are — the
10
+ * assistant never touches credentials or connections.
11
+ */
12
+ export function renderProposal(proposal, metadata, existing) {
13
+ const warnings = [];
14
+ const cols = new Map(metadata.entities.map((e) => [e.name, new Set(e.columns.map((c) => c.name))]));
15
+ const hasTable = (t, where) => {
16
+ if (cols.has(t))
17
+ return true;
18
+ warnings.push(`${where}: unknown table "${t}" dropped`);
19
+ return false;
20
+ };
21
+ const hasColumn = (t, c, where) => {
22
+ if (cols.get(t)?.has(c) === true)
23
+ return true;
24
+ warnings.push(`${where}: unknown column "${t}.${c}" dropped`);
25
+ return false;
26
+ };
27
+ const tables = {};
28
+ const table = (t) => (tables[t] ??= {});
29
+ for (const r of proposal.retain) {
30
+ if (!hasTable(r.table, 'retain') || r.table === proposal.subject.table)
31
+ continue;
32
+ table(r.table).retain = {
33
+ reason: r.reason,
34
+ reference: null,
35
+ anonymize: r.anonymize.filter((c) => hasColumn(r.table, c, 'retain.anonymize')),
36
+ };
37
+ }
38
+ for (const fk of proposal.foreignKeys) {
39
+ const [refTable = '', refCol = ''] = fk.references.split('.');
40
+ if (!hasTable(fk.table, 'foreignKeys') ||
41
+ !hasColumn(fk.table, fk.column, 'foreignKeys') ||
42
+ !hasTable(refTable, 'foreignKeys.references') ||
43
+ !hasColumn(refTable, refCol, 'foreignKeys.references'))
44
+ continue;
45
+ const t = table(fk.table);
46
+ t.foreignKeys = [
47
+ ...(t.foreignKeys ?? []),
48
+ { column: fk.column, references: `${refTable}.${refCol}` },
49
+ ];
50
+ }
51
+ for (const ex of proposal.exclude) {
52
+ if (hasTable(ex, 'exclude') && ex !== proposal.subject.table)
53
+ table(ex).exclude = true;
54
+ }
55
+ const maskColumns = {};
56
+ for (const m of proposal.mask) {
57
+ if (m.strategy !== 'keep' && hasTable(m.table, 'mask') && hasColumn(m.table, m.column, 'mask'))
58
+ maskColumns[`${m.table}.${m.column}`] = m.strategy;
59
+ }
60
+ let subject = proposal.subject;
61
+ if (!hasTable(subject.table, 'subject') ||
62
+ !hasColumn(subject.table, subject.key, 'subject.key')) {
63
+ subject = { table: subject.table, key: subject.key };
64
+ warnings.push('subject: the proposed subject does not exist in the metadata; fix it before use');
65
+ }
66
+ const base = existing ?? {};
67
+ const existingMask = base.mask ?? {};
68
+ const config = {
69
+ version: 1,
70
+ connector: 'postgres',
71
+ ...base,
72
+ subject,
73
+ tables,
74
+ mask: { ...existingMask, columns: maskColumns },
75
+ };
76
+ const valid = connectorConfigSchema.safeParse(config);
77
+ if (!valid.success) {
78
+ for (const i of valid.error.issues)
79
+ warnings.push(`invalid: ${i.path.join('.')}: ${i.message}`);
80
+ }
81
+ const notes = proposal.notes.length > 0
82
+ ? `# Review notes:\n${proposal.notes.map((n) => `# - ${n.replace(/\n/g, ' ')}`).join('\n')}\n`
83
+ : '';
84
+ return { config, yaml: `${HEADER}${notes}${stringify(config, { lineWidth: 0 })}`, warnings };
85
+ }
86
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { type SchemaMetadata } from './metadata.js';
2
+ import type { ProposalProvider } from './providers.js';
3
+ export interface SetupIo {
4
+ stdout: (text: string) => void;
5
+ stderr: (text: string) => void;
6
+ /** Ask the user; resolves with their raw answer. Absent when not interactive. */
7
+ ask?: (question: string) => Promise<string>;
8
+ color?: boolean;
9
+ }
10
+ export interface SetupOptions {
11
+ /** Repository root (for schema field context and stonekeep.config.json). */
12
+ root: string;
13
+ datamapPath: string;
14
+ connectorPath: string;
15
+ provider: ProposalProvider | ((metadata: SchemaMetadata) => ProposalProvider);
16
+ io: SetupIo;
17
+ }
18
+ export interface SetupResult {
19
+ saved: boolean;
20
+ yaml: string;
21
+ warnings: string[];
22
+ changed: boolean;
23
+ }
24
+ /**
25
+ * `stonekeep setup`: datamap -> schema metadata (guarded) -> provider ->
26
+ * validated YAML -> diff -> explicit "Y" -> save. Never saves without an
27
+ * explicit yes, and never in a non-interactive session.
28
+ */
29
+ export declare function runSetup(options: SetupOptions): Promise<SetupResult>;