sf-plugin-permission-sets 0.0.0-dev.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,16 +16,18 @@ Stop clicking through Setup to grant access. Commit a YAML file, open a PR, let
16
16
  - [Quick start](#quick-start)
17
17
  - [Permission files](#permission-files)
18
18
  - [Organizing files](#organizing-files)
19
+ - [Validations](#validations)
19
20
  - [Modes](#modes)
20
21
  - [Commands](#commands)
21
- - [CI/CD](#cicd)
22
22
  - [Inspiration & equivalents](#inspiration--equivalents)
23
+ - [Versioning](#versioning)
24
+ - [Architecture](#architecture)
23
25
 
24
26
  ---
25
27
 
26
28
  ## Why
27
29
 
28
- Permission set assignments drift. People get access for a project and keep it forever. Offboarding misses a set. Nobody can answer "who can see X and why?" without a SOQL spelunking session.
30
+ Permission set assignments drift. People get access for a project and keep it forever. Offboarding misses a set. Nobody can answer "who can see X and why?" without a SOQL spelunking session. And in higher environments those grants happen by hand in Setup, with no review and no trail.
29
31
 
30
32
  This plugin makes the desired state **declarative and reviewable**:
31
33
 
@@ -34,6 +36,8 @@ This plugin makes the desired state **declarative and reviewable**:
34
36
  - ✅ **Safe by default:** deletions are opt-in and guarded by a delete threshold.
35
37
  - ✅ **CI-native:** fully offline `check`, exit codes for gating, and `--json` on every command.
36
38
  - ✅ **Flexible at the edges:** pick your file layout (by permission set or by user) and your sync mode.
39
+ - ✅ **GitOps for access, the SFDX way:** assignments live in source and ship through the same git and CI pipeline as your metadata, instead of being clicked into Setup by hand.
40
+ - ✅ **Fewer hands in Setup for higher environments:** because access is applied from git through CI, fewer people need direct Setup access in UAT and production, and every change is a reviewed pull request with a git audit trail.
37
41
 
38
42
  ## Install
39
43
 
@@ -153,6 +157,18 @@ sf ps apply -o qa --file "permissions/qa/*.yml"
153
157
 
154
158
  The two compose: a directory per environment, each split into functional files.
155
159
 
160
+ ## Validations
161
+
162
+ Every run checks the files first. `check` runs the offline checks with no org, and `validate` adds the org-side checks. When files merge, most overlaps are unions rather than errors.
163
+
164
+ | Situation | Checked by | Severity | Result |
165
+ | --- | --- | :---: | --- |
166
+ | Same username key appears twice in one file | `check` (offline) | ❌ error | Rejected, the intent is ambiguous |
167
+ | Same target listed twice for a user, like `[Sales_Manager, Sales_Manager]` | `check` (offline) | ⚠️ warning | Deduped |
168
+ | A user with no scopes, or an empty list | `check` (offline) | ⚠️ warning | Ignored as a no-op |
169
+ | Same user in two files with different targets | `check` (offline) | ✅ ok | Merged into one model, the point of slicing |
170
+ | Declared user, permission set, group, or license missing or not unique | `validate` (online) | ❌ error | Run fails before any change |
171
+
156
172
  ## Modes
157
173
 
158
174
  A run performs two atomic operations: **add** missing assignments and **remove** undeclared ones. The mode selects which it actually executes. Set it with `--mode` (default `additive`):
@@ -181,29 +197,33 @@ Fully offline: runs in any CI job or pre-commit hook without org credentials.
181
197
 
182
198
  ```
183
199
  USAGE
184
- $ sf ps check -f <glob>... [--strict] [--json]
200
+ $ sf ps check -f <glob>... [--strict] [--watch] [--json]
185
201
 
186
202
  FLAGS
187
203
  -f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs are expanded by the plugin.
188
204
  --strict Treat warnings as errors.
205
+ -w, --watch Re-run on every change to a matched file. Stays in the foreground until you stop it (Ctrl-C).
189
206
 
190
207
  CHECKS
191
- valid YAML & schema (unknown keys rejected)
192
- duplicate assignees / duplicate (user, target) pairs
193
- conflicting intent across files
194
- empty or malformed assignee usernames
195
- internal referential integrity
208
+ - valid YAML & schema (unknown keys rejected)
209
+ - duplicate assignees / duplicate (user, target) pairs
210
+ - conflicting intent across files
211
+ - empty or malformed assignee usernames
212
+ - internal referential integrity
196
213
  ```
197
214
 
215
+ `--watch` is for the local edit loop: leave it running while you tweak the YAML and the findings refresh on every save. It re-expands the globs each run, so files you add or delete are picked up too. It's a foreground, interactive mode, so it can't be combined with `--json` and isn't meant for CI, where a single gated `check` run is what you want.
216
+
198
217
  ### `sf ps validate`
199
218
 
200
219
  ```
201
220
  USAGE
202
- $ sf ps validate -o <org> -f <glob>... [--json]
221
+ $ sf ps validate -o <org> -f <glob>... [--watch] [--json]
203
222
 
204
223
  FLAGS
205
224
  -o, --target-org=<org> (required) Org to resolve against.
206
225
  -f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
226
+ -w, --watch Re-run on every change to a matched file. Read-only, but each run queries the org, so point it at a scratch or dev org.
207
227
 
208
228
  Runs all offline checks, then verifies that every user (active), permission set,
209
229
  group, and license referenced actually exists and resolves uniquely.
@@ -213,13 +233,14 @@ group, and license referenced actually exists and resolves uniquely.
213
233
 
214
234
  ```
215
235
  USAGE
216
- $ sf ps plan -o <org> -f <glob>... [--mode <value>] [--fail-on-drift] [--json]
236
+ $ sf ps plan -o <org> -f <glob>... [--mode <value>] [--fail-on-drift] [--watch] [--json]
217
237
 
218
238
  FLAGS
219
239
  -o, --target-org=<org> (required)
220
240
  -f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
221
241
  --mode=<value> additive | destructive | sync [default: additive]
222
242
  --fail-on-drift Exit non-zero if any change is pending (for CI gates).
243
+ -w, --watch Re-diff against the org on every change to a matched file. Read-only (never applies), but each run queries the org.
223
244
  ```
224
245
 
225
246
  Example output:
@@ -277,69 +298,67 @@ FLAGS
277
298
  --permission-sets=<names> Comma-separated list to export (default: all assignable).
278
299
  ```
279
300
 
280
- ## CI/CD
301
+ ## Inspiration & equivalents
302
+
303
+ This plugin's command surface borrows ideas from tools you already know:
281
304
 
282
- A typical ladder: lint on every PR, plan against a sandbox, apply on merge:
305
+ - [Terraform](https://developer.hashicorp.com/terraform/docs)
306
+ - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/)
307
+ - [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/)
308
+ - [Salesforce CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
283
309
 
284
- ```yaml
285
- # .github/workflows/ps-gitops.yml
286
- name: ps-gitops
287
- on:
288
- pull_request:
289
- push:
290
- branches: [main]
291
-
292
- jobs:
293
- check:
294
- runs-on: ubuntu-latest
295
- steps:
296
- - uses: actions/checkout@v4
297
- - run: npm install -g @salesforce/cli
298
- - run: sf plugins install sf-plugin-permission-sets
299
- - run: sf ps check --file "permissions/*.yml" --strict
300
-
301
- plan:
302
- if: github.event_name == 'pull_request'
303
- needs: check
304
- runs-on: ubuntu-latest
305
- steps:
306
- - uses: actions/checkout@v4
307
- - run: npm install -g @salesforce/cli
308
- - run: sf plugins install sf-plugin-permission-sets
309
- # Auth via Sfdx auth URL stored in a secrets manager, never hardcode credentials
310
- - run: echo "$SF_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin --alias target
311
- env:
312
- SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }}
313
- - run: sf ps plan -o target --file "permissions/*.yml" --mode sync --fail-on-drift
314
-
315
- apply:
316
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
317
- needs: check
318
- runs-on: ubuntu-latest
319
- steps:
320
- - uses: actions/checkout@v4
321
- - run: npm install -g @salesforce/cli
322
- - run: sf plugins install sf-plugin-permission-sets
323
- - run: echo "$SF_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin --alias target
324
- env:
325
- SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }}
326
- - run: sf ps apply -o target --file "permissions/*.yml" --mode sync --no-prompt --max-deletes 25
310
+ ## Versioning
311
+
312
+ Releases follow [semantic versioning](https://semver.org). Snapshots are automatic, real releases are a manual decision.
313
+
314
+ **Automatic, no action needed:**
315
+
316
+ - Every push to `main` publishes a snapshot `0.0.0-dev.<run>` to the `dev` dist-tag.
317
+ - Creating a release triggers CI to build, stamp the version from the tag, publish it with provenance, and smoke-test the result.
318
+
319
+ **Manual, you decide and trigger:**
320
+
321
+ - Choosing the version bump (patch, minor, or major).
322
+ - Creating the GitHub Release, which is what triggers the publish above.
323
+
324
+ | Bump | When | Example tag |
325
+ | --- | --- | --- |
326
+ | patch | bug fix, no behavior change | `v0.1.1` |
327
+ | minor | new backward-compatible feature | `v0.2.0` |
328
+ | major | breaking change to a command, flag, or the YAML schema | `v1.0.0` |
329
+
330
+ Cut a release with a tag off `main`:
331
+
332
+ ```bash
333
+ gh release create v0.2.0 --target main --title v0.2.0 --notes "Add ps export"
327
334
  ```
328
335
 
329
- > **Credentials:** the plugin never reads or stores secrets itself. It uses orgs you've already authenticated with `sf`. In CI, inject auth from your platform's secrets store (as above), not from committed files.
336
+ | dist-tag | Published by | Install |
337
+ | --- | --- | --- |
338
+ | `latest` | manual release with a normal tag like `v1.2.0` | `sf plugins install sf-plugin-permission-sets` |
339
+ | `next` | manual release with a hyphenated tag like `v1.3.0-beta.1` | `sf plugins install sf-plugin-permission-sets@next` |
340
+ | `dev` | automatic on every push to `main` | `sf plugins install sf-plugin-permission-sets@dev` |
330
341
 
331
- ## Inspiration & equivalents
342
+ The `next` tag is selected whenever the version contains a hyphen, not by GitHub's prerelease checkbox.
343
+
344
+ ## Architecture
345
+
346
+ The plugin is layered so every command reuses the same core. Commands stay thin, services hold the orchestration, and core holds the reusable primitives.
347
+
348
+ - **Commands** (`src/commands/ps/`): oclif only. Parse flags, call a service, render output, set the exit code.
349
+ - **Services** (`src/services/`): one per command (`check` today, then `validate`, `plan`, `apply`, `export`). Each turns the core into a command's behavior.
350
+ - **Core** (`src/core/`): the reusable building blocks.
332
351
 
333
- The command surface borrows deliberately from tools you already know:
352
+ | Core module | Responsibility |
353
+ | --- | --- |
354
+ | `model` | Shared domain types (assignment, finding, result). |
355
+ | `schema` | The zod contract for a file, plus validation. |
356
+ | `parse` | File text to an object, with YAML and duplicate-key errors. |
357
+ | `normalize` | A validated file to canonical `(assignee, kind, target)` tuples, plus structural findings. |
358
+ | `load` | Expand globs, run parse then validate then normalize per file, and merge by union. |
359
+ | `report` | Format and count findings. |
334
360
 
335
- | This plugin | Terraform | CloudFormation / SAM | sf core |
336
- | -------------------- | ---------------------- | ------------------------------- | -------------------------- |
337
- | `ps check` | `terraform validate` | `sam validate --lint` | n/a |
338
- | `ps validate` | `terraform plan` (refresh) | `cfn validate-template` | `project deploy validate` |
339
- | `ps plan` | `terraform plan` | `cfn create-change-set` | `project deploy preview` |
340
- | `ps apply` | `terraform apply` | `cfn execute-change-set` / `sam deploy` | `project deploy start` |
341
- | `ps export` | `terraform import` | n/a | n/a |
342
- | `--fail-on-drift` | drift in plan exit code | `cfn detect-stack-drift` | n/a |
361
+ Commands are slices of one pipeline. `check` runs the offline **load** stage only. The online commands add **resolve** (names to ids), **fetch** (current org state), **diff** (desired vs actual), and **apply** (DML) on top, reusing load, report, model, and schema unchanged.
343
362
 
344
363
  ## License
345
364
 
@@ -0,0 +1,18 @@
1
+ import { SfCommand } from '@salesforce/sf-plugins-core';
2
+ import { Finding } from '../../core/model.js';
3
+ export type PsCheckResult = {
4
+ files: number;
5
+ users: number;
6
+ assignments: number;
7
+ findings: Finding[];
8
+ };
9
+ export default class Check extends SfCommand<PsCheckResult> {
10
+ static readonly summary: string;
11
+ static readonly description: string;
12
+ static readonly examples: string[];
13
+ static readonly flags: {
14
+ file: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
15
+ strict: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<PsCheckResult>;
18
+ }
@@ -0,0 +1,45 @@
1
+ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
2
+ import { Messages } from '@salesforce/core';
3
+ import { check } from '../../services/check.js';
4
+ import { formatFindings } from '../../core/report.js';
5
+ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
6
+ const messages = Messages.loadMessages('sf-plugin-permission-sets', 'ps.check');
7
+ export default class Check extends SfCommand {
8
+ static summary = messages.getMessage('summary');
9
+ static description = messages.getMessage('description');
10
+ static examples = messages.getMessages('examples');
11
+ static flags = {
12
+ file: Flags.string({
13
+ char: 'f',
14
+ summary: messages.getMessage('flags.file.summary'),
15
+ required: true,
16
+ multiple: true,
17
+ }),
18
+ strict: Flags.boolean({
19
+ summary: messages.getMessage('flags.strict.summary'),
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(Check);
24
+ const result = await check({ files: flags.file, strict: flags.strict });
25
+ for (const line of formatFindings(result.findings)) {
26
+ this.log(line);
27
+ }
28
+ const assignees = new Set(result.assignments.map((a) => a.assignee));
29
+ this.log('');
30
+ this.log(messages.getMessage('summary.counts', [String(result.errors), String(result.warnings)]));
31
+ if (result.failed) {
32
+ process.exitCode = 1;
33
+ if (!this.jsonEnabled()) {
34
+ this.error(messages.getMessage('error.failed'), { exit: 1 });
35
+ }
36
+ }
37
+ return {
38
+ files: result.files.length,
39
+ users: assignees.size,
40
+ assignments: result.assignments.length,
41
+ findings: result.findings,
42
+ };
43
+ }
44
+ }
45
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/commands/ps/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,QAAQ,CAAC,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,2BAA2B,EAAE,UAAU,CAAC,CAAC;AAShF,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,SAAwB;IAChD,MAAM,CAAU,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,CAAU,WAAW,GAAG,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACjE,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAE5D,MAAM,CAAU,KAAK,GAAG;QAC3B,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;YACf,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,oBAAoB,CAAC;YAClD,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;SACjB,CAAC;QACF,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC;YAClB,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC;SACvD,CAAC;KACL,CAAC;IAEK,KAAK,CAAC,GAAG;QACZ,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAExE,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAElG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACtB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;YACjE,CAAC;QACL,CAAC;QAED,OAAO;YACH,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;YAC1B,KAAK,EAAE,SAAS,CAAC,IAAI;YACrB,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM;YACtC,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC5B,CAAC;IACN,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"info.js","sourceRoot":"","sources":["../../../src/commands/ps/info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,QAAQ,CAAC,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,2BAA2B,EAAE,SAAS,CAAC,CAAC;AAO/E,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,SAAuB;IAChD,MAAM,CAAU,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,CAAU,WAAW,GAAG,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACjE,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAE5D,KAAK,CAAC,GAAG;QACd,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,IAAI,GAAG,2BAA2B,CAAC;QACzC,MAAM,WAAW,GAAG,qEAAqE,CAAC;QAC1F,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC"}
1
+ {"version":3,"file":"info.js","sourceRoot":"","sources":["../../../src/commands/ps/info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,QAAQ,CAAC,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,2BAA2B,EAAE,SAAS,CAAC,CAAC;AAO/E,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,SAAuB;IAC9C,MAAM,CAAU,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,CAAU,WAAW,GAAG,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACjE,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAE5D,KAAK,CAAC,GAAG;QACZ,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,IAAI,GAAG,2BAA2B,CAAC;QACzC,MAAM,WAAW,GAAG,qEAAqE,CAAC;QAC1F,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACjC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { DesiredAssignment, Finding, LoadResult } from './model.js';
2
+ /** Process one file's text through parse, validate, and normalize. Pure, no disk. */
3
+ export declare function checkContent(text: string, file: string): {
4
+ assignments: DesiredAssignment[];
5
+ findings: Finding[];
6
+ };
7
+ /** Expand the globs, read every matched file, and merge into one model by union. */
8
+ export declare function loadFiles(patterns: string[]): Promise<LoadResult>;
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { globby } from 'globby';
3
+ import { parseFile } from './parse.js';
4
+ import { validateFile } from './schema.js';
5
+ import { normalize } from './normalize.js';
6
+ /** Process one file's text through parse, validate, and normalize. Pure, no disk. */
7
+ export function checkContent(text, file) {
8
+ const parsed = parseFile(text, file);
9
+ if (!parsed.data) {
10
+ return { assignments: [], findings: parsed.findings };
11
+ }
12
+ const validated = validateFile(parsed.data, file);
13
+ if (!validated.data) {
14
+ return { assignments: [], findings: [...parsed.findings, ...validated.findings] };
15
+ }
16
+ const normalized = normalize(validated.data, file);
17
+ return {
18
+ assignments: normalized.assignments,
19
+ findings: [...parsed.findings, ...validated.findings, ...normalized.findings],
20
+ };
21
+ }
22
+ /** Expand the globs, read every matched file, and merge into one model by union. */
23
+ export async function loadFiles(patterns) {
24
+ const files = await globby(patterns);
25
+ if (files.length === 0) {
26
+ return {
27
+ files,
28
+ assignments: [],
29
+ findings: [{ level: 'error', code: 'NO_FILES', message: `no files matched: ${patterns.join(', ')}` }],
30
+ };
31
+ }
32
+ const findings = [];
33
+ const collected = [];
34
+ for (const file of files) {
35
+ // eslint-disable-next-line no-await-in-loop
36
+ const text = await readFile(file, 'utf8');
37
+ const res = checkContent(text, file);
38
+ findings.push(...res.findings);
39
+ collected.push(...res.assignments);
40
+ }
41
+ const seen = new Set();
42
+ const assignments = [];
43
+ for (const a of collected) {
44
+ const dedupeKey = `${a.assignee} ${a.kind} ${a.target}`;
45
+ if (seen.has(dedupeKey))
46
+ continue;
47
+ seen.add(dedupeKey);
48
+ assignments.push(a);
49
+ }
50
+ return { files, assignments, findings };
51
+ }
52
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/core/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,IAAY;IACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC1D,CAAC;IAED,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtF,CAAC;IAED,MAAM,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnD,OAAO;QACH,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,QAAQ,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;KAChF,CAAC;AACN,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAkB;IAC9C,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACH,KAAK;YACL,WAAW,EAAE,EAAE;YACf,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,qBAAqB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;SACxG,CAAC;IACN,CAAC;IAED,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAwB,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,4CAA4C;QAC5C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACrC,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/B,SAAS,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,WAAW,GAAwB,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;QACxD,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,SAAS;QAClC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,19 @@
1
+ export type Kind = 'permissionSet' | 'permissionSetGroup' | 'permissionSetLicense';
2
+ export type DesiredAssignment = {
3
+ assignee: string;
4
+ kind: Kind;
5
+ target: string;
6
+ };
7
+ export type FindingLevel = 'error' | 'warning';
8
+ export type Finding = {
9
+ level: FindingLevel;
10
+ code: string;
11
+ message: string;
12
+ file?: string;
13
+ line?: number;
14
+ };
15
+ export type LoadResult = {
16
+ files: string[];
17
+ assignments: DesiredAssignment[];
18
+ findings: Finding[];
19
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/core/model.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ import { FileShape } from './schema.js';
2
+ import { DesiredAssignment, Finding } from './model.js';
3
+ /**
4
+ * Turn a validated file into canonical (assignee, kind, target) tuples, and
5
+ * emit the structural findings: duplicate targets, empty lists, empty users.
6
+ */
7
+ export declare function normalize(data: FileShape, file: string): {
8
+ assignments: DesiredAssignment[];
9
+ findings: Finding[];
10
+ };
@@ -0,0 +1,45 @@
1
+ const KIND_KEYS = [
2
+ ['permissionSet', 'permissionSets'],
3
+ ['permissionSetGroup', 'permissionSetGroups'],
4
+ ['permissionSetLicense', 'permissionSetLicenses'],
5
+ ];
6
+ /**
7
+ * Turn a validated file into canonical (assignee, kind, target) tuples, and
8
+ * emit the structural findings: duplicate targets, empty lists, empty users.
9
+ */
10
+ export function normalize(data, file) {
11
+ const assignments = [];
12
+ const findings = [];
13
+ for (const [username, entry] of Object.entries(data.users)) {
14
+ let scopeCount = 0;
15
+ for (const [kind, key] of KIND_KEYS) {
16
+ const list = entry[key];
17
+ if (list === undefined)
18
+ continue;
19
+ if (list.length === 0) {
20
+ findings.push({ level: 'warning', code: 'EMPTY_LIST', message: `${username}: ${key} is empty`, file });
21
+ continue;
22
+ }
23
+ scopeCount += 1;
24
+ const seen = new Set();
25
+ for (const target of list) {
26
+ if (seen.has(target)) {
27
+ findings.push({
28
+ level: 'warning',
29
+ code: 'DUP_TARGET',
30
+ message: `${username}: ${target} is listed twice under ${key}`,
31
+ file,
32
+ });
33
+ continue;
34
+ }
35
+ seen.add(target);
36
+ assignments.push({ assignee: username, kind, target });
37
+ }
38
+ }
39
+ if (scopeCount === 0) {
40
+ findings.push({ level: 'warning', code: 'EMPTY_USER', message: `${username}: no scopes declared`, file });
41
+ }
42
+ }
43
+ return { assignments, findings };
44
+ }
45
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/core/normalize.ts"],"names":[],"mappings":"AAKA,MAAM,SAAS,GAA4B;IACvC,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACnC,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IAC7C,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;CACpD,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,IAAY;IACnD,MAAM,WAAW,GAAwB,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,IAAI,UAAU,GAAG,CAAC,CAAC;QAEnB,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YACxB,IAAI,IAAI,KAAK,SAAS;gBAAE,SAAS;YACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,QAAQ,KAAK,GAAG,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvG,SAAS;YACb,CAAC;YAED,UAAU,IAAI,CAAC,CAAC;YAChB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/B,KAAK,MAAM,MAAM,IAAI,IAAI,EAAE,CAAC;gBACxB,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;oBACnB,QAAQ,CAAC,IAAI,CAAC;wBACV,KAAK,EAAE,SAAS;wBAChB,IAAI,EAAE,YAAY;wBAClB,OAAO,EAAE,GAAG,QAAQ,KAAK,MAAM,0BAA0B,GAAG,EAAE;wBAC9D,IAAI;qBACP,CAAC,CAAC;oBACH,SAAS;gBACb,CAAC;gBACD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACjB,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC3D,CAAC;QACL,CAAC;QAED,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,QAAQ,sBAAsB,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9G,CAAC;IACL,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Finding } from './model.js';
2
+ /**
3
+ * Read one file's text into a plain object. Reports invalid YAML and duplicate
4
+ * keys (uniqueKeys), and treats an empty document as a warning.
5
+ */
6
+ export declare function parseFile(text: string, file: string): {
7
+ data?: unknown;
8
+ findings: Finding[];
9
+ };
@@ -0,0 +1,25 @@
1
+ import { parseDocument } from 'yaml';
2
+ /**
3
+ * Read one file's text into a plain object. Reports invalid YAML and duplicate
4
+ * keys (uniqueKeys), and treats an empty document as a warning.
5
+ */
6
+ export function parseFile(text, file) {
7
+ const doc = parseDocument(text, { uniqueKeys: true });
8
+ if (doc.errors.length > 0) {
9
+ return {
10
+ findings: doc.errors.map((err) => ({
11
+ level: 'error',
12
+ code: 'YAML',
13
+ message: err.message,
14
+ file,
15
+ line: err.linePos?.[0]?.line,
16
+ })),
17
+ };
18
+ }
19
+ const data = doc.toJS();
20
+ if (data === null || data === undefined) {
21
+ return { findings: [{ level: 'warning', code: 'EMPTY_FILE', message: 'file is empty', file }] };
22
+ }
23
+ return { data, findings: [] };
24
+ }
25
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/core/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAGrC;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY;IAChD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtD,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO;YACH,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC/B,KAAK,EAAE,OAAO;gBACd,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,IAAI;gBACJ,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI;aAC/B,CAAC,CAAC;SACN,CAAC;IACN,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAa,CAAC;IACnC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACpG,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAClC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Finding } from './model.js';
2
+ /** Render findings as human-readable lines. Shared by check, validate, and plan. */
3
+ export declare function formatFindings(findings: Finding[]): string[];
4
+ /** Count findings by level. */
5
+ export declare function countFindings(findings: Finding[]): {
6
+ errors: number;
7
+ warnings: number;
8
+ };
@@ -0,0 +1,15 @@
1
+ /** Render findings as human-readable lines. Shared by check, validate, and plan. */
2
+ export function formatFindings(findings) {
3
+ return findings.map((finding) => {
4
+ const where = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ''} ` : '';
5
+ return `${finding.level}: ${where}${finding.message}`;
6
+ });
7
+ }
8
+ /** Count findings by level. */
9
+ export function countFindings(findings) {
10
+ return {
11
+ errors: findings.filter((f) => f.level === 'error').length,
12
+ warnings: findings.filter((f) => f.level === 'warning').length,
13
+ };
14
+ }
15
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/core/report.ts"],"names":[],"mappings":"AAEA,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAAC,QAAmB;IAC9C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9F,OAAO,GAAG,OAAO,CAAC,KAAK,KAAK,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAC1D,CAAC,CAAC,CAAC;AACP,CAAC;AAED,+BAA+B;AAC/B,MAAM,UAAU,aAAa,CAAC,QAAmB;IAC7C,OAAO;QACH,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,MAAM;QAC1D,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;KACjE,CAAC;AACN,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { Finding } from './model.js';
3
+ export declare const userEntrySchema: z.ZodObject<{
4
+ permissionSets: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
+ permissionSetGroups: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ permissionSetLicenses: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ }, z.core.$strict>;
8
+ export declare const fileSchema: z.ZodObject<{
9
+ users: z.ZodRecord<z.ZodString, z.ZodObject<{
10
+ permissionSets: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ permissionSetGroups: z.ZodOptional<z.ZodArray<z.ZodString>>;
12
+ permissionSetLicenses: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
+ }, z.core.$strict>>;
14
+ }, z.core.$strict>;
15
+ export type FileShape = z.infer<typeof fileSchema>;
16
+ /** Validate a parsed object against the file contract, turning issues into findings. */
17
+ export declare function validateFile(data: unknown, file: string): {
18
+ data?: FileShape;
19
+ findings: Finding[];
20
+ };
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ const scopeList = z.array(z.string().min(1)).optional();
3
+ export const userEntrySchema = z.strictObject({
4
+ permissionSets: scopeList,
5
+ permissionSetGroups: scopeList,
6
+ permissionSetLicenses: scopeList,
7
+ });
8
+ export const fileSchema = z.strictObject({
9
+ users: z.record(z.string().min(1), userEntrySchema),
10
+ });
11
+ /** Validate a parsed object against the file contract, turning issues into findings. */
12
+ export function validateFile(data, file) {
13
+ const parsed = fileSchema.safeParse(data);
14
+ if (parsed.success) {
15
+ return { data: parsed.data, findings: [] };
16
+ }
17
+ return {
18
+ findings: parsed.error.issues.map((issue) => ({
19
+ level: 'error',
20
+ code: 'SCHEMA',
21
+ message: `${issue.path.join('.') || '(root)'}: ${issue.message}`,
22
+ file,
23
+ })),
24
+ };
25
+ }
26
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/core/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AAExD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IAC1C,cAAc,EAAE,SAAS;IACzB,mBAAmB,EAAE,SAAS;IAC9B,qBAAqB,EAAE,SAAS;CACnC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC;CACtD,CAAC,CAAC;AAIH,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,IAAa,EAAE,IAAY;IACpD,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC/C,CAAC;IACD,OAAO;QACH,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1C,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE;YAChE,IAAI;SACP,CAAC,CAAC;KACN,CAAC;AACN,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { DesiredAssignment, Finding } from '../core/model.js';
2
+ export type CheckOptions = {
3
+ files: string[];
4
+ strict: boolean;
5
+ };
6
+ export type CheckResult = {
7
+ files: string[];
8
+ assignments: DesiredAssignment[];
9
+ findings: Finding[];
10
+ errors: number;
11
+ warnings: number;
12
+ failed: boolean;
13
+ };
14
+ /** Offline check: load the files, validate them, and summarize the findings. */
15
+ export declare function check(options: CheckOptions): Promise<CheckResult>;
@@ -0,0 +1,17 @@
1
+ import { loadFiles } from '../core/load.js';
2
+ import { countFindings } from '../core/report.js';
3
+ /** Offline check: load the files, validate them, and summarize the findings. */
4
+ export async function check(options) {
5
+ const loaded = await loadFiles(options.files);
6
+ const { errors, warnings } = countFindings(loaded.findings);
7
+ const failed = errors > 0 || (options.strict && warnings > 0);
8
+ return {
9
+ files: loaded.files,
10
+ assignments: loaded.assignments,
11
+ findings: loaded.findings,
12
+ errors,
13
+ warnings,
14
+ failed,
15
+ };
16
+ }
17
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/services/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAiBlD,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,OAAqB;IAC7C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;IAE9D,OAAO;QACH,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM;QACN,QAAQ;QACR,MAAM;KACT,CAAC;AACN,CAAC"}