@kb-labs/policy-contracts 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ This project is licensed under the same terms as the KB Labs Plugin Template root project.
2
+
3
+ See the root LICENSE file at the repository root for full details.
4
+
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @kb-labs/plugin-template-contracts
2
+
3
+ Lightweight public contracts package for the plugin: it describes guaranteed artifacts, commands, workflows, API payloads, and the version of these promises.
4
+
5
+ ## Why this package exists
6
+
7
+ Every KB Labs plugin is expected to publish a clear, lightweight “promise” to the rest of the ecosystem. This package is that promise: it contains only types, manifests, and validation helpers, so other teams (CLI, Workflow Engine, Studio, REST, marketplace tooling) can rely on a single source of truth without dragging in plugin runtime code.
8
+
9
+ ## Quick start checklist
10
+
11
+ 1. Clone this package as part of your plugin workspace (`packages/contracts`).
12
+ 2. Update `pluginContractsManifest` with your plugin ID and initial artifacts/commands.
13
+ 3. Adjust Zod schemas in `src/schema.ts` (or add new ones) to match your payloads.
14
+ 4. Bump `contractsVersion` whenever the public promise changes (SemVer rules below).
15
+ 5. Run `pnpm test` and `pnpm type-check` to ensure the manifest validates.
16
+ 6. Import the manifest in your CLI/REST/workflow code to avoid hard-coded IDs.
17
+
18
+ ### Renaming this package
19
+
20
+ When you turn the template into your own plugin:
21
+ - Change the npm name in `package.json` (e.g. `@kb-labs/my-plugin-contracts`).
22
+ - Update `pluginContractsManifest.pluginId` in `src/contract.ts`.
23
+ - Adjust aliases in `tsconfig.paths.json` and imports across the workspace (`@kb-labs/plugin-template-contracts` → your new name).
24
+ - Replace sample artifact IDs (`template.hello.*`) with your own naming scheme.
25
+
26
+ ## What's inside
27
+
28
+ - `pluginContractsManifest` — the single source of truth for the plugin's public capabilities
29
+ - TypeScript types (`src/types`) and Zod schemas (`src/schema`) for artifacts, commands, workflows, and API payloads
30
+ - `parsePluginContracts` utility for runtime validation of the manifest and third-party contracts
31
+
32
+ ## Versioning rules
33
+
34
+ - `contractsVersion` follows SemVer and is **independent** from the plugin's npm version.
35
+ - **MAJOR** — breaking changes (removing/renaming artifacts, changing payload formats).
36
+ - **MINOR** — backwards-compatible extensions (new artifacts, commands, fields).
37
+ - **PATCH** — documentation/metadata updates without altering payload formats.
38
+
39
+ ## Minimal manifest example
40
+
41
+ ```ts
42
+ import type { PluginContracts } from '@kb-labs/plugin-template-contracts';
43
+
44
+ export const pluginContractsManifest: PluginContracts = {
45
+ schema: 'kb.plugin.contracts/1',
46
+ pluginId: '@kb-labs/my-plugin',
47
+ contractsVersion: '1.0.0',
48
+ artifacts: {
49
+ 'my-plugin.result': {
50
+ id: 'my-plugin.result',
51
+ kind: 'json',
52
+ description: 'Primary output of the CLI command.'
53
+ }
54
+ }
55
+ // commands/workflows/api can be added later when needed
56
+ };
57
+ ```
58
+
59
+ ## Optional sections
60
+
61
+ All additional sections are **optional** — include only what your plugin actually supports:
62
+
63
+ - `commands` — define CLI or workflow commands that produce/consume artifacts.
64
+ - `workflows` — describe composed workflows and their steps.
65
+ - `api` — document REST (or future surfaces) when the plugin exposes them.
66
+
67
+ If your plugin only ships a CLI command, keep `commands` + `artifacts` and omit `workflows`/`api`. The Zod schema accepts missing sections.
68
+
69
+ ## Usage in plugin code
70
+
71
+ ```ts
72
+ import { pluginContractsManifest } from '@kb-labs/plugin-template-contracts';
73
+
74
+ const helloArtifactId = pluginContractsManifest.artifacts['template.hello.greeting'].id;
75
+ ```
76
+
77
+ Use the manifest to avoid magic strings, assert that required artifacts exist, or log which promises were fulfilled.
78
+
79
+ ## Who relies on the contract
80
+
81
+ - **Workflow Engine** — verifies allowed steps, required artifacts, and matches produced results with the contract.
82
+ - **Studio** — builds UI and hints based on declared artifacts and commands.
83
+ - **CLI / REST / other plugins** — reuse types and schemas as the source of truth, validate inputs/outputs.
84
+ - **Marketplace & QA tooling** — checks plugin compatibility and correctness before publishing.
85
+
86
+ ## Looking ahead
87
+
88
+ - Generate JSON Schema / OpenAPI from the `api` contract surface.
89
+ - Add automatic inspectors in Studio and validators for the marketplace.
90
+
@@ -0,0 +1,339 @@
1
+ import { z } from 'zod';
2
+
3
+ type PolicySeverity = 'error' | 'warning';
4
+ interface PolicyViolation {
5
+ rule: string;
6
+ severity: PolicySeverity;
7
+ message: string;
8
+ package?: string;
9
+ detail?: string;
10
+ file?: string;
11
+ }
12
+ interface RepoCheckResult {
13
+ path: string;
14
+ category: string | null;
15
+ violations: PolicyViolation[];
16
+ passed: string[];
17
+ }
18
+ interface CheckReport {
19
+ passed: boolean;
20
+ repos: RepoCheckResult[];
21
+ summary: {
22
+ total: number;
23
+ passed: number;
24
+ failed: number;
25
+ violations: number;
26
+ };
27
+ }
28
+ interface CategoryResult {
29
+ path: string;
30
+ category: string | null;
31
+ rules: string[];
32
+ }
33
+ interface PolicyCategoryConfig {
34
+ paths: string[];
35
+ rules: string[];
36
+ }
37
+ interface PolicyRuleConfig {
38
+ description: string;
39
+ severity: PolicySeverity;
40
+ config?: Record<string, unknown>;
41
+ }
42
+ interface PolicyConfig {
43
+ categories: Record<string, PolicyCategoryConfig>;
44
+ rules: Record<string, PolicyRuleConfig>;
45
+ }
46
+ interface ApiSnapshot {
47
+ packageName: string;
48
+ version: string;
49
+ symbols: string[];
50
+ extractedAt: string;
51
+ }
52
+
53
+ declare const PolicyRuleConfigSchema: z.ZodObject<{
54
+ description: z.ZodString;
55
+ severity: z.ZodEnum<["error", "warning"]>;
56
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
57
+ }, "strip", z.ZodTypeAny, {
58
+ description: string;
59
+ severity: "error" | "warning";
60
+ config?: Record<string, unknown> | undefined;
61
+ }, {
62
+ description: string;
63
+ severity: "error" | "warning";
64
+ config?: Record<string, unknown> | undefined;
65
+ }>;
66
+ declare const PolicyCategoryConfigSchema: z.ZodObject<{
67
+ paths: z.ZodArray<z.ZodString, "many">;
68
+ rules: z.ZodArray<z.ZodString, "many">;
69
+ }, "strip", z.ZodTypeAny, {
70
+ paths: string[];
71
+ rules: string[];
72
+ }, {
73
+ paths: string[];
74
+ rules: string[];
75
+ }>;
76
+ declare const PolicyConfigSchema: z.ZodObject<{
77
+ categories: z.ZodRecord<z.ZodString, z.ZodObject<{
78
+ paths: z.ZodArray<z.ZodString, "many">;
79
+ rules: z.ZodArray<z.ZodString, "many">;
80
+ }, "strip", z.ZodTypeAny, {
81
+ paths: string[];
82
+ rules: string[];
83
+ }, {
84
+ paths: string[];
85
+ rules: string[];
86
+ }>>;
87
+ rules: z.ZodRecord<z.ZodString, z.ZodObject<{
88
+ description: z.ZodString;
89
+ severity: z.ZodEnum<["error", "warning"]>;
90
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
91
+ }, "strip", z.ZodTypeAny, {
92
+ description: string;
93
+ severity: "error" | "warning";
94
+ config?: Record<string, unknown> | undefined;
95
+ }, {
96
+ description: string;
97
+ severity: "error" | "warning";
98
+ config?: Record<string, unknown> | undefined;
99
+ }>>;
100
+ }, "strip", z.ZodTypeAny, {
101
+ rules: Record<string, {
102
+ description: string;
103
+ severity: "error" | "warning";
104
+ config?: Record<string, unknown> | undefined;
105
+ }>;
106
+ categories: Record<string, {
107
+ paths: string[];
108
+ rules: string[];
109
+ }>;
110
+ }, {
111
+ rules: Record<string, {
112
+ description: string;
113
+ severity: "error" | "warning";
114
+ config?: Record<string, unknown> | undefined;
115
+ }>;
116
+ categories: Record<string, {
117
+ paths: string[];
118
+ rules: string[];
119
+ }>;
120
+ }>;
121
+ type PolicyConfigInput = z.input<typeof PolicyConfigSchema>;
122
+
123
+ /**
124
+ * Canonical error codes for every policy violation type.
125
+ *
126
+ * Attach `code` to a `PolicyViolation` to enable programmatic discrimination
127
+ * of violation kinds without parsing free-form message strings.
128
+ *
129
+ * All codes share the `POLICY_` prefix so they are unambiguous when mixed
130
+ * with error codes from other subsystems.
131
+ */
132
+ declare const PolicyErrorCode: {
133
+ /**
134
+ * A package depends on another package that belongs to a category outside
135
+ * the set of categories permitted for the depending package's own category.
136
+ * Produced by the `boundary-check` rule.
137
+ */
138
+ readonly BOUNDARY_VIOLATION: "POLICY_BOUNDARY_VIOLATION";
139
+ /**
140
+ * A plugin package imports an internal platform package directly instead of
141
+ * going through `@kb-labs/sdk`. Plugin packages must restrict their
142
+ * `@kb-labs/*` dependencies exclusively to `@kb-labs/sdk`.
143
+ * Produced by the `sdk-only-deps` rule.
144
+ */
145
+ readonly SDK_ONLY_DEP_VIOLATION: "POLICY_SDK_ONLY_DEP_VIOLATION";
146
+ /**
147
+ * The local `package.json` version is lower than the version already
148
+ * published to the npm registry. Published versions may never be decreased.
149
+ * Produced by the `no-rollback` rule.
150
+ */
151
+ readonly VERSION_ROLLBACK: "POLICY_VERSION_ROLLBACK";
152
+ /**
153
+ * One or more previously exported public symbols have been removed without
154
+ * a corresponding major-version bump, constituting a breaking API change.
155
+ * Produced by the `api-compat-check` / `no-breaking-without-major` rules.
156
+ */
157
+ readonly API_BREAKING_CHANGE: "POLICY_API_BREAKING_CHANGE";
158
+ /**
159
+ * A policy configuration references a rule name that has no registered
160
+ * check implementation. The rule will be skipped at runtime.
161
+ * Produced by the policy runner when an unknown rule key is encountered.
162
+ */
163
+ readonly UNKNOWN_RULE: "POLICY_UNKNOWN_RULE";
164
+ };
165
+ /**
166
+ * Union type of all valid `PolicyErrorCode` string values.
167
+ * Use this as the type for `PolicyViolation.code`.
168
+ */
169
+ type PolicyErrorCode = (typeof PolicyErrorCode)[keyof typeof PolicyErrorCode];
170
+ /**
171
+ * Human-readable descriptions for each `PolicyErrorCode`.
172
+ *
173
+ * These are intended as stable reference messages for documentation,
174
+ * tooling output, and IDE integrations. Individual violations also carry
175
+ * a context-specific `message` and optional `detail` field on the
176
+ * `PolicyViolation` object.
177
+ */
178
+ declare const POLICY_ERROR_MESSAGES: Readonly<Record<PolicyErrorCode, string>>;
179
+ /**
180
+ * Returns the stable descriptive message for a given `PolicyErrorCode`.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const msg = getPolicyErrorMessage(PolicyErrorCode.BOUNDARY_VIOLATION);
185
+ * // "A package depends on another package outside its allowed category boundaries. …"
186
+ * ```
187
+ */
188
+ declare function getPolicyErrorMessage(code: PolicyErrorCode): string;
189
+
190
+ /**
191
+ * Options that control which parts of a {@link PolicyViolation} are included
192
+ * in the formatted string.
193
+ */
194
+ interface FormatViolationOptions {
195
+ /**
196
+ * Include the `file` field as a path hint when it is present.
197
+ * @default true
198
+ */
199
+ includeFile?: boolean;
200
+ /**
201
+ * Include the `detail` field as an indented continuation line when present.
202
+ * @default true
203
+ */
204
+ includeDetail?: boolean;
205
+ /**
206
+ * Include the `package` field as a parenthetical suffix on the first line
207
+ * when it is present.
208
+ * @default false
209
+ */
210
+ includePackage?: boolean;
211
+ }
212
+ /**
213
+ * Formats a single {@link PolicyViolation} into a human-readable string that
214
+ * mirrors the canonical CLI output produced by `policy:check`.
215
+ *
216
+ * The returned string is **never** terminated with a newline so callers can
217
+ * join multiple violations however they like (e.g. `'\n'` for terminal output,
218
+ * `'<br>'` for HTML).
219
+ *
220
+ * Layout (each line only emitted when the corresponding field is present and
221
+ * its option is enabled):
222
+ *
223
+ * ```
224
+ * [severity] rule — message (package)
225
+ * → detail
226
+ * @ file
227
+ * ```
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * import { formatViolation } from '@kb-labs/policy-contracts';
232
+ *
233
+ * const line = formatViolation(violation);
234
+ * // "[error] boundary-check — pkg-a depends on pkg-b (category: plugins)"
235
+ *
236
+ * const withDetail = formatViolation(violation, { includeDetail: true });
237
+ * // "[error] boundary-check — pkg-a depends on pkg-b (category: plugins)\n → Category "platform" may only depend on: shared"
238
+ * ```
239
+ */
240
+ declare function formatViolation(violation: PolicyViolation, options?: FormatViolationOptions): string;
241
+
242
+ /**
243
+ * A mapping from rule identifier to the total number of violations produced
244
+ * by that rule across all inputs passed to {@link getViolationSummary}.
245
+ *
246
+ * Only rules that produced **at least one** violation appear as keys; rules
247
+ * that passed cleanly are omitted so callers can use a simple
248
+ * `Object.keys(summary).length === 0` emptiness check.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * const summary: ViolationSummary = { 'sdk-only-deps': 3, 'boundary-check': 1 };
253
+ * ```
254
+ */
255
+ type ViolationSummary = Record<string, number>;
256
+ /**
257
+ * Counts the number of violations per rule across a flat array of
258
+ * {@link PolicyViolation} objects.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * import { getViolationSummary } from '@kb-labs/policy-contracts';
263
+ *
264
+ * const violations: PolicyViolation[] = [
265
+ * { rule: 'sdk-only-deps', severity: 'error', message: 'pkg-a imports core' },
266
+ * { rule: 'boundary-check', severity: 'warning', message: 'pkg-b depends on plugins' },
267
+ * { rule: 'sdk-only-deps', severity: 'error', message: 'pkg-c imports core' },
268
+ * ];
269
+ *
270
+ * getViolationSummary(violations);
271
+ * // → { 'sdk-only-deps': 2, 'boundary-check': 1 }
272
+ * ```
273
+ */
274
+ declare function getViolationSummary(violations: PolicyViolation[]): ViolationSummary;
275
+ /**
276
+ * Counts the number of violations per rule for a single
277
+ * {@link RepoCheckResult}.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * import { getViolationSummary } from '@kb-labs/policy-contracts';
282
+ *
283
+ * getViolationSummary(repoResult);
284
+ * // → { 'boundary-check': 3 }
285
+ * ```
286
+ */
287
+ declare function getViolationSummary(result: RepoCheckResult): ViolationSummary;
288
+ /**
289
+ * Counts the number of violations per rule across **all** repos in a
290
+ * {@link CheckReport}, aggregating results from every
291
+ * `report.repos[n].violations` array.
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * import { getViolationSummary } from '@kb-labs/policy-contracts';
296
+ *
297
+ * getViolationSummary(report);
298
+ * // → { 'sdk-only-deps': 5, 'no-rollback': 2 }
299
+ * ```
300
+ */
301
+ declare function getViolationSummary(report: CheckReport): ViolationSummary;
302
+ /**
303
+ * Returns `true` when every repo in the report passed all policy rules
304
+ * (i.e. `report.passed === true`), `false` otherwise.
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * import { isPolicyPassing } from '@kb-labs/policy-contracts';
309
+ *
310
+ * if (!isPolicyPassing(report)) {
311
+ * process.exit(1);
312
+ * }
313
+ * ```
314
+ */
315
+ declare function isPolicyPassing(report: CheckReport): boolean;
316
+ /**
317
+ * Merges multiple {@link CheckReport} objects into a single consolidated report.
318
+ *
319
+ * All `repos` arrays are concatenated in input order. The `summary` counters
320
+ * (`total`, `passed`, `failed`, `violations`) are recomputed from the merged
321
+ * repo list. The top-level `passed` flag is `true` only when the merged result
322
+ * has **zero** violations.
323
+ *
324
+ * Repos that share the same `path` across different input reports are kept as
325
+ * separate entries (concatenation, not deduplication). Callers who need
326
+ * dedup-by-path can post-process the returned `repos` array.
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * import { mergeViolations } from '@kb-labs/policy-contracts';
331
+ *
332
+ * const combined = mergeViolations([reportA, reportB]);
333
+ * // combined.repos === [...reportA.repos, ...reportB.repos]
334
+ * // combined.passed === (combined.summary.violations === 0)
335
+ * ```
336
+ */
337
+ declare function mergeViolations(reports: CheckReport[]): CheckReport;
338
+
339
+ export { type ApiSnapshot, type CategoryResult, type CheckReport, type FormatViolationOptions, POLICY_ERROR_MESSAGES, type PolicyCategoryConfig, PolicyCategoryConfigSchema, type PolicyConfig, type PolicyConfigInput, PolicyConfigSchema, PolicyErrorCode, type PolicyRuleConfig, PolicyRuleConfigSchema, type PolicySeverity, type PolicyViolation, type RepoCheckResult, type ViolationSummary, formatViolation, getPolicyErrorMessage, getViolationSummary, isPolicyPassing, mergeViolations };
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/schema.ts
4
+ var PolicyRuleConfigSchema = z.object({
5
+ description: z.string(),
6
+ severity: z.enum(["error", "warning"]),
7
+ config: z.record(z.unknown()).optional()
8
+ });
9
+ var PolicyCategoryConfigSchema = z.object({
10
+ paths: z.array(z.string()),
11
+ rules: z.array(z.string())
12
+ });
13
+ var PolicyConfigSchema = z.object({
14
+ categories: z.record(PolicyCategoryConfigSchema),
15
+ rules: z.record(PolicyRuleConfigSchema)
16
+ });
17
+
18
+ // src/error-codes.ts
19
+ var PolicyErrorCode = {
20
+ /**
21
+ * A package depends on another package that belongs to a category outside
22
+ * the set of categories permitted for the depending package's own category.
23
+ * Produced by the `boundary-check` rule.
24
+ */
25
+ BOUNDARY_VIOLATION: "POLICY_BOUNDARY_VIOLATION",
26
+ /**
27
+ * A plugin package imports an internal platform package directly instead of
28
+ * going through `@kb-labs/sdk`. Plugin packages must restrict their
29
+ * `@kb-labs/*` dependencies exclusively to `@kb-labs/sdk`.
30
+ * Produced by the `sdk-only-deps` rule.
31
+ */
32
+ SDK_ONLY_DEP_VIOLATION: "POLICY_SDK_ONLY_DEP_VIOLATION",
33
+ /**
34
+ * The local `package.json` version is lower than the version already
35
+ * published to the npm registry. Published versions may never be decreased.
36
+ * Produced by the `no-rollback` rule.
37
+ */
38
+ VERSION_ROLLBACK: "POLICY_VERSION_ROLLBACK",
39
+ /**
40
+ * One or more previously exported public symbols have been removed without
41
+ * a corresponding major-version bump, constituting a breaking API change.
42
+ * Produced by the `api-compat-check` / `no-breaking-without-major` rules.
43
+ */
44
+ API_BREAKING_CHANGE: "POLICY_API_BREAKING_CHANGE",
45
+ /**
46
+ * A policy configuration references a rule name that has no registered
47
+ * check implementation. The rule will be skipped at runtime.
48
+ * Produced by the policy runner when an unknown rule key is encountered.
49
+ */
50
+ UNKNOWN_RULE: "POLICY_UNKNOWN_RULE"
51
+ };
52
+ var POLICY_ERROR_MESSAGES = {
53
+ [PolicyErrorCode.BOUNDARY_VIOLATION]: "A package depends on another package outside its allowed category boundaries. Each workspace category may only import packages from the explicitly permitted categories listed in the policy configuration.",
54
+ [PolicyErrorCode.SDK_ONLY_DEP_VIOLATION]: "A plugin package imports an internal platform package directly instead of going through @kb-labs/sdk. Plugin packages must limit their @kb-labs/* dependencies to @kb-labs/sdk only. Move required types and utilities to the SDK or use its re-exports.",
55
+ [PolicyErrorCode.VERSION_ROLLBACK]: "The local package.json version is lower than the version already published to the npm registry. Versions may never be decreased once published \u2014 restore the version to the published value or release a higher version.",
56
+ [PolicyErrorCode.API_BREAKING_CHANGE]: "One or more previously exported symbols have been removed without a corresponding major-version bump. Either restore the removed symbols to preserve backward compatibility, or increment the major version before removing them.",
57
+ [PolicyErrorCode.UNKNOWN_RULE]: 'A policy rule reference was encountered that has no registered check implementation. Verify that the rule name in the policy configuration exactly matches a supported rule identifier (e.g. "sdk-only-deps", "boundary-check", "no-rollback", "api-compat-check").'
58
+ };
59
+ function getPolicyErrorMessage(code) {
60
+ return POLICY_ERROR_MESSAGES[code];
61
+ }
62
+
63
+ // src/format.ts
64
+ function formatViolation(violation, options = {}) {
65
+ const { includeFile = true, includeDetail = true, includePackage = false } = options;
66
+ const packageSuffix = includePackage && violation.package ? ` (${violation.package})` : "";
67
+ const firstLine = `[${violation.severity}] ${violation.rule} \u2014 ${violation.message}${packageSuffix}`;
68
+ const lines = [firstLine];
69
+ if (includeDetail && violation.detail) {
70
+ lines.push(` \u2192 ${violation.detail}`);
71
+ }
72
+ if (includeFile && violation.file) {
73
+ lines.push(` @ ${violation.file}`);
74
+ }
75
+ return lines.join("\n");
76
+ }
77
+
78
+ // src/helpers.ts
79
+ function getViolationSummary(input) {
80
+ let violations;
81
+ if (Array.isArray(input)) {
82
+ violations = input;
83
+ } else if ("repos" in input) {
84
+ violations = input.repos.flatMap((r) => r.violations);
85
+ } else {
86
+ violations = input.violations;
87
+ }
88
+ const summary = {};
89
+ for (const v of violations) {
90
+ summary[v.rule] = (summary[v.rule] ?? 0) + 1;
91
+ }
92
+ return summary;
93
+ }
94
+ function isPolicyPassing(report) {
95
+ return report.passed;
96
+ }
97
+ function mergeViolations(reports) {
98
+ const repos = reports.flatMap((r) => r.repos);
99
+ const total = repos.length;
100
+ const failed = repos.filter((r) => r.violations.length > 0).length;
101
+ const passedRepos = total - failed;
102
+ const violations = repos.reduce((acc, r) => acc + r.violations.length, 0);
103
+ return {
104
+ passed: violations === 0,
105
+ repos,
106
+ summary: { total, passed: passedRepos, failed, violations }
107
+ };
108
+ }
109
+
110
+ export { POLICY_ERROR_MESSAGES, PolicyCategoryConfigSchema, PolicyConfigSchema, PolicyErrorCode, PolicyRuleConfigSchema, formatViolation, getPolicyErrorMessage, getViolationSummary, isPolicyPassing, mergeViolations };
111
+ //# sourceMappingURL=index.js.map
112
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/error-codes.ts","../src/format.ts","../src/helpers.ts"],"names":[],"mappings":";;;AAEO,IAAM,sBAAA,GAAyB,EAAE,MAAA,CAAO;AAAA,EAC7C,WAAA,EAAa,EAAE,MAAA,EAAO;AAAA,EACtB,UAAU,CAAA,CAAE,IAAA,CAAK,CAAC,OAAA,EAAS,SAAS,CAAC,CAAA;AAAA,EACrC,QAAQ,CAAA,CAAE,MAAA,CAAO,EAAE,OAAA,EAAS,EAAE,QAAA;AAChC,CAAC;AAEM,IAAM,0BAAA,GAA6B,EAAE,MAAA,CAAO;AAAA,EACjD,KAAA,EAAO,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,QAAQ,CAAA;AAAA,EACzB,KAAA,EAAO,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,QAAQ;AAC3B,CAAC;AAEM,IAAM,kBAAA,GAAqB,EAAE,MAAA,CAAO;AAAA,EACzC,UAAA,EAAY,CAAA,CAAE,MAAA,CAAO,0BAA0B,CAAA;AAAA,EAC/C,KAAA,EAAO,CAAA,CAAE,MAAA,CAAO,sBAAsB;AACxC,CAAC;;;ACPM,IAAM,eAAA,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,kBAAA,EAAoB,2BAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQpB,sBAAA,EAAwB,+BAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOxB,gBAAA,EAAkB,yBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlB,mBAAA,EAAqB,4BAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrB,YAAA,EAAc;AAChB;AAgBO,IAAM,qBAAA,GAAmE;AAAA,EAC9E,CAAC,eAAA,CAAgB,kBAAkB,GACjC,6MAAA;AAAA,EAIF,CAAC,eAAA,CAAgB,sBAAsB,GACrC,0PAAA;AAAA,EAIF,CAAC,eAAA,CAAgB,gBAAgB,GAC/B,+NAAA;AAAA,EAIF,CAAC,eAAA,CAAgB,mBAAmB,GAClC,mOAAA;AAAA,EAIF,CAAC,eAAA,CAAgB,YAAY,GAC3B;AAGJ;AAWO,SAAS,sBAAsB,IAAA,EAA+B;AACnE,EAAA,OAAO,sBAAsB,IAAI,CAAA;AACnC;;;AC5CO,SAAS,eAAA,CACd,SAAA,EACA,OAAA,GAAkC,EAAC,EAC3B;AACR,EAAA,MAAM,EAAE,WAAA,GAAc,IAAA,EAAM,gBAAgB,IAAA,EAAM,cAAA,GAAiB,OAAM,GAAI,OAAA;AAE7E,EAAA,MAAM,gBACJ,cAAA,IAAkB,SAAA,CAAU,UAAU,CAAA,EAAA,EAAK,SAAA,CAAU,OAAO,CAAA,CAAA,CAAA,GAAM,EAAA;AAEpE,EAAA,MAAM,SAAA,GAAY,CAAA,CAAA,EAAI,SAAA,CAAU,QAAQ,CAAA,EAAA,EAAK,SAAA,CAAU,IAAI,CAAA,QAAA,EAAM,SAAA,CAAU,OAAO,CAAA,EAAG,aAAa,CAAA,CAAA;AAElG,EAAA,MAAM,KAAA,GAAkB,CAAC,SAAS,CAAA;AAElC,EAAA,IAAI,aAAA,IAAiB,UAAU,MAAA,EAAQ;AACrC,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAO,SAAA,CAAU,MAAM,CAAA,CAAE,CAAA;AAAA,EACtC;AAEA,EAAA,IAAI,WAAA,IAAe,UAAU,IAAA,EAAM;AACjC,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,IAAA,EAAO,SAAA,CAAU,IAAI,CAAA,CAAE,CAAA;AAAA,EACpC;AAEA,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;;;ACXO,SAAS,oBACd,KAAA,EACkB;AAClB,EAAA,IAAI,UAAA;AAEJ,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AAExB,IAAA,UAAA,GAAa,KAAA;AAAA,EACf,CAAA,MAAA,IAAW,WAAW,KAAA,EAAO;AAE3B,IAAA,UAAA,GAAc,MAAsB,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,KAAM,EAAE,UAAU,CAAA;AAAA,EACvE,CAAA,MAAO;AAEL,IAAA,UAAA,GAAc,KAAA,CAA0B,UAAA;AAAA,EAC1C;AAEA,EAAA,MAAM,UAA4B,EAAC;AACnC,EAAA,KAAA,MAAW,KAAK,UAAA,EAAY;AAC1B,IAAA,OAAA,CAAQ,EAAE,IAAI,CAAA,GAAA,CAAK,QAAQ,CAAA,CAAE,IAAI,KAAK,CAAA,IAAK,CAAA;AAAA,EAC7C;AACA,EAAA,OAAO,OAAA;AACT;AAgBO,SAAS,gBAAgB,MAAA,EAA8B;AAC5D,EAAA,OAAO,MAAA,CAAO,MAAA;AAChB;AAwBO,SAAS,gBAAgB,OAAA,EAAqC;AACnE,EAAA,MAAM,QAAQ,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA,KAAM,EAAE,KAAK,CAAA;AAC5C,EAAA,MAAM,QAAQ,KAAA,CAAM,MAAA;AACpB,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,UAAA,CAAW,MAAA,GAAS,CAAC,CAAA,CAAE,MAAA;AAC5D,EAAA,MAAM,cAAc,KAAA,GAAQ,MAAA;AAC5B,EAAA,MAAM,UAAA,GAAa,KAAA,CAAM,MAAA,CAAO,CAAC,GAAA,EAAK,MAAM,GAAA,GAAM,CAAA,CAAE,UAAA,CAAW,MAAA,EAAQ,CAAC,CAAA;AACxE,EAAA,OAAO;AAAA,IACL,QAAQ,UAAA,KAAe,CAAA;AAAA,IACvB,KAAA;AAAA,IACA,SAAS,EAAE,KAAA,EAAO,MAAA,EAAQ,WAAA,EAAa,QAAQ,UAAA;AAAW,GAC5D;AACF","file":"index.js","sourcesContent":["import { z } from 'zod';\n\nexport const PolicyRuleConfigSchema = z.object({\n description: z.string(),\n severity: z.enum(['error', 'warning']),\n config: z.record(z.unknown()).optional(),\n});\n\nexport const PolicyCategoryConfigSchema = z.object({\n paths: z.array(z.string()),\n rules: z.array(z.string()),\n});\n\nexport const PolicyConfigSchema = z.object({\n categories: z.record(PolicyCategoryConfigSchema),\n rules: z.record(PolicyRuleConfigSchema),\n});\n\nexport type PolicyConfigInput = z.input<typeof PolicyConfigSchema>;\n","/**\n * Canonical error codes for every policy violation type.\n *\n * Attach `code` to a `PolicyViolation` to enable programmatic discrimination\n * of violation kinds without parsing free-form message strings.\n *\n * All codes share the `POLICY_` prefix so they are unambiguous when mixed\n * with error codes from other subsystems.\n */\nexport const PolicyErrorCode = {\n /**\n * A package depends on another package that belongs to a category outside\n * the set of categories permitted for the depending package's own category.\n * Produced by the `boundary-check` rule.\n */\n BOUNDARY_VIOLATION: 'POLICY_BOUNDARY_VIOLATION',\n\n /**\n * A plugin package imports an internal platform package directly instead of\n * going through `@kb-labs/sdk`. Plugin packages must restrict their\n * `@kb-labs/*` dependencies exclusively to `@kb-labs/sdk`.\n * Produced by the `sdk-only-deps` rule.\n */\n SDK_ONLY_DEP_VIOLATION: 'POLICY_SDK_ONLY_DEP_VIOLATION',\n\n /**\n * The local `package.json` version is lower than the version already\n * published to the npm registry. Published versions may never be decreased.\n * Produced by the `no-rollback` rule.\n */\n VERSION_ROLLBACK: 'POLICY_VERSION_ROLLBACK',\n\n /**\n * One or more previously exported public symbols have been removed without\n * a corresponding major-version bump, constituting a breaking API change.\n * Produced by the `api-compat-check` / `no-breaking-without-major` rules.\n */\n API_BREAKING_CHANGE: 'POLICY_API_BREAKING_CHANGE',\n\n /**\n * A policy configuration references a rule name that has no registered\n * check implementation. The rule will be skipped at runtime.\n * Produced by the policy runner when an unknown rule key is encountered.\n */\n UNKNOWN_RULE: 'POLICY_UNKNOWN_RULE',\n} as const;\n\n/**\n * Union type of all valid `PolicyErrorCode` string values.\n * Use this as the type for `PolicyViolation.code`.\n */\nexport type PolicyErrorCode = (typeof PolicyErrorCode)[keyof typeof PolicyErrorCode];\n\n/**\n * Human-readable descriptions for each `PolicyErrorCode`.\n *\n * These are intended as stable reference messages for documentation,\n * tooling output, and IDE integrations. Individual violations also carry\n * a context-specific `message` and optional `detail` field on the\n * `PolicyViolation` object.\n */\nexport const POLICY_ERROR_MESSAGES: Readonly<Record<PolicyErrorCode, string>> = {\n [PolicyErrorCode.BOUNDARY_VIOLATION]:\n 'A package depends on another package outside its allowed category boundaries. ' +\n 'Each workspace category may only import packages from the explicitly permitted categories ' +\n 'listed in the policy configuration.',\n\n [PolicyErrorCode.SDK_ONLY_DEP_VIOLATION]:\n 'A plugin package imports an internal platform package directly instead of going through ' +\n '@kb-labs/sdk. Plugin packages must limit their @kb-labs/* dependencies to @kb-labs/sdk only. ' +\n 'Move required types and utilities to the SDK or use its re-exports.',\n\n [PolicyErrorCode.VERSION_ROLLBACK]:\n 'The local package.json version is lower than the version already published to the npm registry. ' +\n 'Versions may never be decreased once published — restore the version to the published value or ' +\n 'release a higher version.',\n\n [PolicyErrorCode.API_BREAKING_CHANGE]:\n 'One or more previously exported symbols have been removed without a corresponding major-version ' +\n 'bump. Either restore the removed symbols to preserve backward compatibility, or increment the ' +\n 'major version before removing them.',\n\n [PolicyErrorCode.UNKNOWN_RULE]:\n 'A policy rule reference was encountered that has no registered check implementation. ' +\n 'Verify that the rule name in the policy configuration exactly matches a supported rule ' +\n 'identifier (e.g. \"sdk-only-deps\", \"boundary-check\", \"no-rollback\", \"api-compat-check\").',\n};\n\n/**\n * Returns the stable descriptive message for a given `PolicyErrorCode`.\n *\n * @example\n * ```ts\n * const msg = getPolicyErrorMessage(PolicyErrorCode.BOUNDARY_VIOLATION);\n * // \"A package depends on another package outside its allowed category boundaries. …\"\n * ```\n */\nexport function getPolicyErrorMessage(code: PolicyErrorCode): string {\n return POLICY_ERROR_MESSAGES[code];\n}\n","import type { PolicyViolation } from './types.js';\n\n/**\n * Options that control which parts of a {@link PolicyViolation} are included\n * in the formatted string.\n */\nexport interface FormatViolationOptions {\n /**\n * Include the `file` field as a path hint when it is present.\n * @default true\n */\n includeFile?: boolean;\n\n /**\n * Include the `detail` field as an indented continuation line when present.\n * @default true\n */\n includeDetail?: boolean;\n\n /**\n * Include the `package` field as a parenthetical suffix on the first line\n * when it is present.\n * @default false\n */\n includePackage?: boolean;\n}\n\n/**\n * Formats a single {@link PolicyViolation} into a human-readable string that\n * mirrors the canonical CLI output produced by `policy:check`.\n *\n * The returned string is **never** terminated with a newline so callers can\n * join multiple violations however they like (e.g. `'\\n'` for terminal output,\n * `'<br>'` for HTML).\n *\n * Layout (each line only emitted when the corresponding field is present and\n * its option is enabled):\n *\n * ```\n * [severity] rule — message (package)\n * → detail\n * @ file\n * ```\n *\n * @example\n * ```ts\n * import { formatViolation } from '@kb-labs/policy-contracts';\n *\n * const line = formatViolation(violation);\n * // \"[error] boundary-check — pkg-a depends on pkg-b (category: plugins)\"\n *\n * const withDetail = formatViolation(violation, { includeDetail: true });\n * // \"[error] boundary-check — pkg-a depends on pkg-b (category: plugins)\\n → Category \"platform\" may only depend on: shared\"\n * ```\n */\nexport function formatViolation(\n violation: PolicyViolation,\n options: FormatViolationOptions = {},\n): string {\n const { includeFile = true, includeDetail = true, includePackage = false } = options;\n\n const packageSuffix =\n includePackage && violation.package ? ` (${violation.package})` : '';\n\n const firstLine = `[${violation.severity}] ${violation.rule} — ${violation.message}${packageSuffix}`;\n\n const lines: string[] = [firstLine];\n\n if (includeDetail && violation.detail) {\n lines.push(` → ${violation.detail}`);\n }\n\n if (includeFile && violation.file) {\n lines.push(` @ ${violation.file}`);\n }\n\n return lines.join('\\n');\n}\n","import type { CheckReport, PolicyViolation, RepoCheckResult } from './types.js';\n\n/**\n * A mapping from rule identifier to the total number of violations produced\n * by that rule across all inputs passed to {@link getViolationSummary}.\n *\n * Only rules that produced **at least one** violation appear as keys; rules\n * that passed cleanly are omitted so callers can use a simple\n * `Object.keys(summary).length === 0` emptiness check.\n *\n * @example\n * ```ts\n * const summary: ViolationSummary = { 'sdk-only-deps': 3, 'boundary-check': 1 };\n * ```\n */\nexport type ViolationSummary = Record<string, number>;\n\n/**\n * Counts the number of violations per rule across a flat array of\n * {@link PolicyViolation} objects.\n *\n * @example\n * ```ts\n * import { getViolationSummary } from '@kb-labs/policy-contracts';\n *\n * const violations: PolicyViolation[] = [\n * { rule: 'sdk-only-deps', severity: 'error', message: 'pkg-a imports core' },\n * { rule: 'boundary-check', severity: 'warning', message: 'pkg-b depends on plugins' },\n * { rule: 'sdk-only-deps', severity: 'error', message: 'pkg-c imports core' },\n * ];\n *\n * getViolationSummary(violations);\n * // → { 'sdk-only-deps': 2, 'boundary-check': 1 }\n * ```\n */\nexport function getViolationSummary(violations: PolicyViolation[]): ViolationSummary;\n\n/**\n * Counts the number of violations per rule for a single\n * {@link RepoCheckResult}.\n *\n * @example\n * ```ts\n * import { getViolationSummary } from '@kb-labs/policy-contracts';\n *\n * getViolationSummary(repoResult);\n * // → { 'boundary-check': 3 }\n * ```\n */\nexport function getViolationSummary(result: RepoCheckResult): ViolationSummary;\n\n/**\n * Counts the number of violations per rule across **all** repos in a\n * {@link CheckReport}, aggregating results from every\n * `report.repos[n].violations` array.\n *\n * @example\n * ```ts\n * import { getViolationSummary } from '@kb-labs/policy-contracts';\n *\n * getViolationSummary(report);\n * // → { 'sdk-only-deps': 5, 'no-rollback': 2 }\n * ```\n */\nexport function getViolationSummary(report: CheckReport): ViolationSummary;\n\nexport function getViolationSummary(\n input: PolicyViolation[] | RepoCheckResult | CheckReport,\n): ViolationSummary {\n let violations: PolicyViolation[];\n\n if (Array.isArray(input)) {\n // Overload 1: flat PolicyViolation[]\n violations = input;\n } else if ('repos' in input) {\n // Overload 3: CheckReport — `repos` is unique to CheckReport; RepoCheckResult does not have it\n violations = (input as CheckReport).repos.flatMap((r) => r.violations);\n } else {\n // Overload 2: RepoCheckResult\n violations = (input as RepoCheckResult).violations;\n }\n\n const summary: ViolationSummary = {};\n for (const v of violations) {\n summary[v.rule] = (summary[v.rule] ?? 0) + 1;\n }\n return summary;\n}\n\n\n/**\n * Returns `true` when every repo in the report passed all policy rules\n * (i.e. `report.passed === true`), `false` otherwise.\n *\n * @example\n * ```ts\n * import { isPolicyPassing } from '@kb-labs/policy-contracts';\n *\n * if (!isPolicyPassing(report)) {\n * process.exit(1);\n * }\n * ```\n */\nexport function isPolicyPassing(report: CheckReport): boolean {\n return report.passed;\n}\n\n\n/**\n * Merges multiple {@link CheckReport} objects into a single consolidated report.\n *\n * All `repos` arrays are concatenated in input order. The `summary` counters\n * (`total`, `passed`, `failed`, `violations`) are recomputed from the merged\n * repo list. The top-level `passed` flag is `true` only when the merged result\n * has **zero** violations.\n *\n * Repos that share the same `path` across different input reports are kept as\n * separate entries (concatenation, not deduplication). Callers who need\n * dedup-by-path can post-process the returned `repos` array.\n *\n * @example\n * ```ts\n * import { mergeViolations } from '@kb-labs/policy-contracts';\n *\n * const combined = mergeViolations([reportA, reportB]);\n * // combined.repos === [...reportA.repos, ...reportB.repos]\n * // combined.passed === (combined.summary.violations === 0)\n * ```\n */\nexport function mergeViolations(reports: CheckReport[]): CheckReport {\n const repos = reports.flatMap((r) => r.repos);\n const total = repos.length;\n const failed = repos.filter((r) => r.violations.length > 0).length;\n const passedRepos = total - failed;\n const violations = repos.reduce((acc, r) => acc + r.violations.length, 0);\n return {\n passed: violations === 0,\n repos,\n summary: { total, passed: passedRepos, failed, violations },\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@kb-labs/policy-contracts",
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "description": "Types, interfaces, and schemas for the KB Labs policy plugin.",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./dist/*": "./dist/*"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "sideEffects": false,
20
+ "scripts": {
21
+ "clean": "rimraf dist",
22
+ "build": "tsup --config tsup.config.ts",
23
+ "dev": "tsup --config tsup.config.ts --watch",
24
+ "lint": "eslint src --ext .ts",
25
+ "lint:fix": "eslint . --fix",
26
+ "type-check": "tsc --noEmit",
27
+ "test": "vitest run --passWithNoTests",
28
+ "test:watch": "vitest"
29
+ },
30
+ "dependencies": {
31
+ "zod": "^3.23.8"
32
+ },
33
+ "devDependencies": {
34
+ "@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
35
+ "rimraf": "^6.0.1",
36
+ "tsup": "^8.5.0",
37
+ "typescript": "^5.6.3",
38
+ "vitest": "^3.2.4"
39
+ },
40
+ "engines": {
41
+ "node": ">=20.0.0",
42
+ "pnpm": ">=9.0.0"
43
+ }
44
+ }