@shrkcrft/cli 0.1.0-alpha.27 → 0.1.0-alpha.29

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.
Files changed (46) hide show
  1. package/dist/commands/baseline.command.d.ts +8 -0
  2. package/dist/commands/baseline.command.d.ts.map +1 -0
  3. package/dist/commands/baseline.command.js +542 -0
  4. package/dist/commands/changelog-data.d.ts.map +1 -1
  5. package/dist/commands/changelog-data.js +42 -0
  6. package/dist/commands/check.command.d.ts.map +1 -1
  7. package/dist/commands/check.command.js +147 -12
  8. package/dist/commands/command-catalog.d.ts.map +1 -1
  9. package/dist/commands/command-catalog.js +120 -0
  10. package/dist/commands/daily.commands.d.ts.map +1 -1
  11. package/dist/commands/daily.commands.js +11 -1
  12. package/dist/commands/gates.command.d.ts +16 -0
  13. package/dist/commands/gates.command.d.ts.map +1 -0
  14. package/dist/commands/gates.command.js +377 -0
  15. package/dist/commands/generated.command.d.ts +6 -0
  16. package/dist/commands/generated.command.d.ts.map +1 -0
  17. package/dist/commands/generated.command.js +544 -0
  18. package/dist/commands/help.command.d.ts.map +1 -1
  19. package/dist/commands/help.command.js +64 -2
  20. package/dist/commands/ingest.command.d.ts +11 -0
  21. package/dist/commands/ingest.command.d.ts.map +1 -1
  22. package/dist/commands/ingest.command.js +49 -23
  23. package/dist/commands/policy-lint.command.d.ts +37 -0
  24. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  25. package/dist/commands/policy-lint.command.js +167 -8
  26. package/dist/commands/registry.command.d.ts.map +1 -1
  27. package/dist/commands/registry.command.js +70 -13
  28. package/dist/commands/wiring.command.d.ts.map +1 -1
  29. package/dist/commands/wiring.command.js +25 -0
  30. package/dist/exit-codes.d.ts +27 -7
  31. package/dist/exit-codes.d.ts.map +1 -1
  32. package/dist/exit-codes.js +47 -8
  33. package/dist/finish/run-finish.d.ts.map +1 -1
  34. package/dist/finish/run-finish.js +9 -3
  35. package/dist/gates/gate-envelope.d.ts +64 -0
  36. package/dist/gates/gate-envelope.d.ts.map +1 -0
  37. package/dist/gates/gate-envelope.js +26 -0
  38. package/dist/gates/gate-rule-view.d.ts +35 -0
  39. package/dist/gates/gate-rule-view.d.ts.map +1 -0
  40. package/dist/gates/gate-rule-view.js +81 -0
  41. package/dist/gates/rule-coverage.d.ts +53 -0
  42. package/dist/gates/rule-coverage.d.ts.map +1 -0
  43. package/dist/gates/rule-coverage.js +165 -0
  44. package/dist/main.d.ts.map +1 -1
  45. package/dist/main.js +27 -3
  46. package/package.json +33 -33
@@ -458,32 +458,16 @@ export const contradictionsCommand = {
458
458
  return 0;
459
459
  },
460
460
  };
461
- export const generatedCommand = {
462
- name: 'generated',
463
- description: 'Generated-code classifier. Subcommands: `report` (default), `protect --write-drafts`. Read-only by default.',
464
- usage: 'shrk [--cwd <dir>] generated [report|protect] [--format text|markdown|json] [--write-drafts]',
461
+ /** Classify the tree's generated code (the `report` half of `shrk generated`). */
462
+ export const generatedReportCommand = {
463
+ name: 'report',
464
+ description: 'Classify which parts of the tree are generated code. Read-only.',
465
+ usage: 'shrk [--cwd <dir>] generated report [--format text|markdown|json]',
465
466
  async run(args) {
466
- const sub = args.positional[0] === 'protect' ? 'protect' : 'report';
467
- const rest = { ...args, positional: args.positional.slice(args.positional[0] === 'report' || args.positional[0] === 'protect' ? 1 : 0) };
468
- const cwd = resolveCwd(rest);
469
- const format = parseFormat(flagString(rest, 'format'));
467
+ const cwd = resolveCwd(args);
468
+ const format = parseFormat(flagString(args, 'format'));
470
469
  const inspection = await inspectSharkcraft({ cwd });
471
470
  const report = buildGeneratedCodeReport({ inspection });
472
- if (sub === 'protect') {
473
- const writeDrafts = flagBool(rest, 'write-drafts');
474
- const outDir = nodePath.join(cwd, INGEST_BASE);
475
- if (writeDrafts) {
476
- if (!existsSync(outDir))
477
- mkdirSync(outDir, { recursive: true });
478
- const target = nodePath.join(outDir, 'GENERATED_PROTECT.md');
479
- writeFileSync(target, renderGeneratedCodeReportMarkdown(report), 'utf8');
480
- process.stdout.write(`Wrote ${target}\n`);
481
- return 0;
482
- }
483
- process.stdout.write(renderGeneratedCodeReportMarkdown(report) + '\n');
484
- process.stdout.write('\nRe-run with --write-drafts to save the recommended protect rules under sharkcraft/ingestion/.\n');
485
- return 0;
486
- }
487
471
  if (format === 'json')
488
472
  process.stdout.write(renderGeneratedCodeReportJson(report) + '\n');
489
473
  else if (format === 'markdown')
@@ -493,6 +477,48 @@ export const generatedCommand = {
493
477
  return 0;
494
478
  },
495
479
  };
480
+ /** Recommend protect rules for the classified generated roots. */
481
+ export const generatedProtectCommand = {
482
+ name: 'protect',
483
+ description: 'Recommend protect rules for the detected generated roots. Read-only unless --write-drafts (writes under sharkcraft/ingestion/).',
484
+ usage: 'shrk [--cwd <dir>] generated protect [--write-drafts]',
485
+ booleanFlags: new Set(['write-drafts']),
486
+ async run(args) {
487
+ const cwd = resolveCwd(args);
488
+ const inspection = await inspectSharkcraft({ cwd });
489
+ const report = buildGeneratedCodeReport({ inspection });
490
+ if (flagBool(args, 'write-drafts')) {
491
+ const outDir = nodePath.join(cwd, INGEST_BASE);
492
+ if (!existsSync(outDir))
493
+ mkdirSync(outDir, { recursive: true });
494
+ const target = nodePath.join(outDir, 'GENERATED_PROTECT.md');
495
+ writeFileSync(target, renderGeneratedCodeReportMarkdown(report), 'utf8');
496
+ process.stdout.write(`Wrote ${target}\n`);
497
+ return 0;
498
+ }
499
+ process.stdout.write(renderGeneratedCodeReportMarkdown(report) + '\n');
500
+ process.stdout.write('\nRe-run with --write-drafts to save the recommended protect rules under sharkcraft/ingestion/.\n');
501
+ return 0;
502
+ },
503
+ };
504
+ /**
505
+ * The `generated` group. The bare verb keeps its historical behaviour (the
506
+ * classifier report); `report` / `protect` are the classifier subverbs, and
507
+ * `list` / `check` / `update` / `explain` are the drift GATE (registered from
508
+ * `generated.command.ts`). One noun: that half FINDS what is generated, this
509
+ * half proves it has not drifted.
510
+ */
511
+ export const generatedCommand = {
512
+ name: 'generated',
513
+ description: 'Generated-code classifier (`report` default, `protect --write-drafts`) + the generated-artifact drift GATE (`list | check | update | explain`, see docs/generated-drift.md). Read-only by default.',
514
+ usage: 'shrk [--cwd <dir>] generated [report|protect] [--format text|markdown|json] [--write-drafts]\n (drift gate: shrk generated list | check [--headers-only] | update | explain --id <id>)',
515
+ booleanFlags: new Set(['write-drafts']),
516
+ async run(args) {
517
+ // Bare `shrk generated` (and any stray positional) → the classifier report,
518
+ // exactly as before the gate verbs joined the group.
519
+ return generatedReportCommand.run({ ...args, positional: [] });
520
+ },
521
+ };
496
522
  export const stabilityCommand = {
497
523
  name: 'stability',
498
524
  description: 'Stability classification (stable/experimental/deprecated/legacy/generated/internal/public-api/high-risk). Read-only.',
@@ -1,3 +1,40 @@
1
+ import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
2
+ import { type IPolicyFinding, type IPolicySuppression } from '@shrkcrft/boundaries';
1
3
  import { type ICommandHandler } from '../command-registry.js';
4
+ export declare const POLICY_EXPLAIN_SCHEMA: "sharkcraft.policy-explain/v1";
5
+ /** What ONE policy rule resolved to against the live tree. */
6
+ export interface IPolicyExplain {
7
+ readonly schema: typeof POLICY_EXPLAIN_SCHEMA;
8
+ readonly ruleId: string;
9
+ readonly description?: string;
10
+ readonly surface: PolicySurface;
11
+ readonly severity: 'error' | 'warning';
12
+ readonly pattern: string;
13
+ readonly scan: string;
14
+ /** Content units the rule scanned (files, or inline-template bodies). */
15
+ readonly unitsScanned: number;
16
+ readonly status: string;
17
+ /** Every hit that COUNTED, with file:line. */
18
+ readonly findings: readonly IPolicyFinding[];
19
+ /** Every hit an exemption or the scan zone dropped, and which one applied. */
20
+ readonly suppressed: readonly IPolicySuppression[];
21
+ readonly exemptFiles: readonly string[];
22
+ readonly exemptLines?: string;
23
+ readonly diagnostics: readonly string[];
24
+ /** Why the rule scanned nothing, when it did. */
25
+ readonly skipReason?: string;
26
+ }
27
+ /**
28
+ * Dry-run ONE policy rule and return everything it saw — including the hits an
29
+ * exemption swallowed.
30
+ *
31
+ * Showing suppressed hits is the point: an exemption that silently deletes a
32
+ * finding is indistinguishable from a stale glob, so both the kept and the
33
+ * dropped hits are reported, each labelled with the exemption that applied.
34
+ */
35
+ export declare function runPolicyExplain(cwd: string, rule: IPolicyRule, excludeDirs: readonly string[]): IPolicyExplain;
36
+ /** Render an {@link IPolicyExplain}. Always returns 0 — explain is informational. */
37
+ export declare function renderPolicyExplain(explain: IPolicyExplain, wantJson: boolean): number;
38
+ export declare const policyLintExplainCommand: ICommandHandler;
2
39
  export declare const policyLintCommand: ICommandHandler;
3
40
  //# sourceMappingURL=policy-lint.command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,iBAAiB,EAAE,eA6L/B,CAAC"}
1
+ {"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAQhC,eAAO,MAAM,qBAAqB,EAAG,8BAAuC,CAAC;AAE7E,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,iDAAiD;IACjD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,cAAc,CAqBhB;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CA0CtF;AAGD,eAAO,MAAM,wBAAwB,EAAE,eA8BtC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,eAoQ/B,CAAC"}
@@ -1,13 +1,117 @@
1
1
  import * as nodePath from 'node:path';
2
- import { runPolicyLint } from '@shrkcrft/boundaries';
2
+ import { runPolicyLint, } from '@shrkcrft/boundaries';
3
3
  import { classifyChangedScope, resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
4
4
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
5
+ import { ExitCode } from "../exit-codes.js";
6
+ import { buildGateEnvelope } from "../gates/gate-envelope.js";
5
7
  import { asJson, header, kv } from "../output/format-output.js";
6
8
  const VALID_SURFACES = new Set(['template', 'style', 'ts']);
9
+ export const POLICY_EXPLAIN_SCHEMA = 'sharkcraft.policy-explain/v1';
10
+ /**
11
+ * Dry-run ONE policy rule and return everything it saw — including the hits an
12
+ * exemption swallowed.
13
+ *
14
+ * Showing suppressed hits is the point: an exemption that silently deletes a
15
+ * finding is indistinguishable from a stale glob, so both the kept and the
16
+ * dropped hits are reported, each labelled with the exemption that applied.
17
+ */
18
+ export function runPolicyExplain(cwd, rule, excludeDirs) {
19
+ const report = runPolicyLint(cwd, [rule], { excludeDirs });
20
+ const result = report.rules[0];
21
+ const skip = report.skipped[0];
22
+ return {
23
+ schema: POLICY_EXPLAIN_SCHEMA,
24
+ ruleId: rule.id,
25
+ ...(rule.description ? { description: rule.description } : {}),
26
+ surface: rule.surface,
27
+ severity: rule.severity ?? 'error',
28
+ pattern: rule.pattern,
29
+ scan: rule.scan ?? 'all',
30
+ unitsScanned: result?.unitsScanned ?? 0,
31
+ status: result?.status ?? 'error',
32
+ findings: report.findings,
33
+ suppressed: report.suppressed,
34
+ exemptFiles: rule.exemptFiles ?? [],
35
+ ...(rule.exemptLines ? { exemptLines: rule.exemptLines } : {}),
36
+ diagnostics: report.diagnostics,
37
+ ...(skip ? { skipReason: skip.reason } : {}),
38
+ };
39
+ }
40
+ /** Render an {@link IPolicyExplain}. Always returns 0 — explain is informational. */
41
+ export function renderPolicyExplain(explain, wantJson) {
42
+ if (wantJson) {
43
+ process.stdout.write(asJson(explain) + '\n');
44
+ return 0;
45
+ }
46
+ process.stdout.write(header(`Policy explain: ${explain.ruleId} (${explain.surface})`));
47
+ if (explain.description)
48
+ process.stdout.write(` ${explain.description}\n`);
49
+ process.stdout.write(kv('pattern', `/${explain.pattern}/`) + '\n');
50
+ process.stdout.write(kv('scan zone', explain.scan) + '\n');
51
+ process.stdout.write(kv('units scanned', String(explain.unitsScanned)) + '\n');
52
+ process.stdout.write(kv('status', explain.status) + '\n');
53
+ if (explain.exemptFiles.length > 0) {
54
+ process.stdout.write(kv('exemptFiles', explain.exemptFiles.join(', ')) + '\n');
55
+ }
56
+ if (explain.exemptLines)
57
+ process.stdout.write(kv('exemptLines', explain.exemptLines) + '\n');
58
+ process.stdout.write(`\nHits that COUNT (${explain.findings.length}):\n`);
59
+ for (const f of explain.findings.slice(0, 60)) {
60
+ process.stdout.write(` ✗ ${f.match} (${f.file}:${f.line})${f.inlineTemplate ? ' [inline template]' : ''}\n`);
61
+ }
62
+ if (explain.findings.length > 60) {
63
+ process.stdout.write(` … (${explain.findings.length - 60} more)\n`);
64
+ }
65
+ if (explain.suppressed.length > 0) {
66
+ process.stdout.write(`\nHits an exemption DROPPED (${explain.suppressed.length}):\n`);
67
+ for (const s of explain.suppressed.slice(0, 60)) {
68
+ process.stdout.write(` – ${s.match} (${s.file}:${s.line}) via ${s.via}\n`);
69
+ }
70
+ if (explain.suppressed.length > 60) {
71
+ process.stdout.write(` … (${explain.suppressed.length - 60} more)\n`);
72
+ }
73
+ }
74
+ if (explain.skipReason) {
75
+ process.stdout.write(`\n! SKIPPED — ${explain.skipReason}. A rule that scans nothing is a bug in the rule,\n` +
76
+ ' not a pass. Fix the glob, or set `failOnEmpty: true` to make this a hard failure.\n');
77
+ }
78
+ for (const d of explain.diagnostics)
79
+ process.stdout.write(` ! ${d}\n`);
80
+ return 0;
81
+ }
82
+ export const policyLintExplainCommand = {
83
+ name: 'explain',
84
+ description: 'Dry-run ONE policyRule and print every hit with file:line — INCLUDING the hits an exemption or the scan zone dropped, each labelled with which one applied.',
85
+ usage: 'shrk policy-lint explain <ruleId> [--json]',
86
+ booleanFlags: new Set(['json']),
87
+ async run(args) {
88
+ const id = args.positional[0] ?? flagString(args, 'id');
89
+ if (!id) {
90
+ process.stderr.write('Usage: shrk policy-lint explain <ruleId> [--json]\n');
91
+ return ExitCode.UsageError;
92
+ }
93
+ const cwd = resolveCwd(args);
94
+ const loaded = await resolveProjectConfig(cwd);
95
+ if (!loaded.ok) {
96
+ process.stderr.write(`Could not load config: ${loaded.error.message}\n`);
97
+ return ExitCode.UsageError;
98
+ }
99
+ const rules = loaded.value.config.policyRules ?? [];
100
+ const rule = rules.find((r) => r.id === id);
101
+ if (!rule) {
102
+ process.stderr.write(`No policy rule "${id}". Configured: ${rules.map((r) => r.id).join(', ') || '(none)'}\n`);
103
+ return ExitCode.UsageError;
104
+ }
105
+ const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
106
+ const excludeDirs = rel && !rel.startsWith('..') ? [rel] : [];
107
+ return renderPolicyExplain(runPolicyExplain(cwd, rule, excludeDirs), flagBool(args, 'json'));
108
+ },
109
+ };
7
110
  export const policyLintCommand = {
8
111
  name: 'policy-lint',
9
112
  description: 'Lint template/markup, stylesheet, and AOT-invisible TS surfaces against data-defined policyRules[] (e.g. flag raw markup when a primitive exists). Sees `.html` files AND inline `template:` strings — surfaces tsc/AOT cannot. Deterministic; no AI.',
10
113
  usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--new-only] [--since <ref>] [--only <ids>] [--json]\n (--changed-only SCANS just the changed files; --new-only scans the whole tree but shows only findings the change introduced, hiding pre-existing baseline debt)',
114
+ booleanFlags: new Set(['json', 'changed-only', 'new-only']),
11
115
  async run(args) {
12
116
  const cwd = resolveCwd(args);
13
117
  const wantJson = flagBool(args, 'json');
@@ -25,7 +129,7 @@ export const policyLintCommand = {
25
129
  const bad = parts.filter((s) => !VALID_SURFACES.has(s));
26
130
  if (bad.length > 0) {
27
131
  process.stderr.write(`Unknown --surface "${bad.join(', ')}". Use template | style | ts.\n`);
28
- return 2;
132
+ return ExitCode.UsageError;
29
133
  }
30
134
  surfaces = parts;
31
135
  }
@@ -35,11 +139,12 @@ export const policyLintCommand = {
35
139
  const msg = loaded.error.message;
36
140
  if (wantJson) {
37
141
  process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', error: msg, rules: [], findings: [], diagnostics: [msg], evaluated: 0, verdict: 'errors' }) + '\n');
38
- return 1;
142
+ return ExitCode.UsageError;
39
143
  }
40
144
  process.stdout.write(header('Policy lint'));
41
145
  process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
42
- return 1;
146
+ // A broken config is a USAGE error (3), not "violations found" (1).
147
+ return ExitCode.UsageError;
43
148
  }
44
149
  const rules = loaded.value.config.policyRules ?? [];
45
150
  const planeDiagnostics = loaded.value.planeDiagnostics;
@@ -60,7 +165,7 @@ export const policyLintCommand = {
60
165
  const unknown = requested.filter((id) => !known.has(id));
61
166
  if (unknown.length > 0) {
62
167
  process.stderr.write(`Unknown --only rule id(s): ${unknown.join(', ')}. Configured: ${[...known].join(', ') || '(none)'}\n`);
63
- return 2;
168
+ return ExitCode.UsageError;
64
169
  }
65
170
  }
66
171
  let changedFiles;
@@ -116,8 +221,41 @@ export const policyLintCommand = {
116
221
  report = { ...report, findings: newFindings, verdict };
117
222
  }
118
223
  if (wantJson) {
119
- process.stdout.write(asJson({ ...report, ...(newOnly ? { newOnly: true, hiddenBaseline } : {}) }) + '\n');
120
- return report.verdict === 'errors' ? 1 : 0;
224
+ const exit = report.verdict === 'errors' || report.skipped.some((sk) => sk.failed)
225
+ ? ExitCode.Failure
226
+ : report.evaluated === 0 || report.skipped.length > 0
227
+ ? ExitCode.NotVerified
228
+ : ExitCode.VerifiedPass;
229
+ process.stdout.write(asJson({
230
+ ...report,
231
+ ...(newOnly ? { newOnly: true, hiddenBaseline } : {}),
232
+ gate: buildGateEnvelope('policy-lint', exit, report.rules.map((r) => {
233
+ const skip = report.skipped.find((s) => s.ruleId === r.ruleId);
234
+ return {
235
+ id: r.ruleId,
236
+ type: 'policy',
237
+ status: r.status,
238
+ severity: r.severity,
239
+ counts: {
240
+ units: r.unitsScanned,
241
+ findings: r.findingCount,
242
+ suppressed: r.suppressedCount,
243
+ },
244
+ violations: report.findings
245
+ .filter((f) => f.ruleId === r.ruleId)
246
+ .map((f) => ({
247
+ id: f.match,
248
+ file: f.file,
249
+ line: f.line,
250
+ message: f.message,
251
+ ...(f.suggest ? { hint: f.suggest } : {}),
252
+ })),
253
+ ...(skip ? { skipReason: skip.reason } : {}),
254
+ ...(r.error ? { error: r.error } : {}),
255
+ };
256
+ })),
257
+ }) + '\n');
258
+ return exit;
121
259
  }
122
260
  process.stdout.write(header('Policy lint'));
123
261
  // `evaluated` counts rules that actually scanned ≥1 file. When 0 rules
@@ -126,12 +264,23 @@ export const policyLintCommand = {
126
264
  if (report.evaluated === 0) {
127
265
  process.stdout.write(` ! Nothing evaluated — ${report.rules.length} rule(s) configured but none matched files in scope` +
128
266
  (changedOnly || since ? ' (changed-only).\n' : '.\n'));
129
- return 0;
267
+ for (const sk of report.skipped) {
268
+ process.stdout.write(` – ${sk.ruleId}: ${sk.reason}${sk.failed ? ' (failOnEmpty → FAILED)' : ''}\n`);
269
+ }
270
+ // Scanning nothing is not a pass. `2` = not verified (the repo-wide
271
+ // contract); a `failOnEmpty` rule promotes it to a real failure.
272
+ return report.skipped.some((sk) => sk.failed) ? 1 : 2;
130
273
  }
131
274
  process.stdout.write(kv('rules evaluated', `${report.evaluated} of ${report.rules.length}`) + '\n');
132
275
  const errors = report.findings.filter((f) => f.severity === 'error').length;
133
276
  const warnings = report.findings.filter((f) => f.severity === 'warning').length;
134
277
  process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
278
+ if (report.suppressed.length > 0) {
279
+ process.stdout.write(kv('suppressed', `${report.suppressed.length} hit(s) dropped by an exemption — see \`policy-lint explain <id>\``) + '\n');
280
+ }
281
+ for (const sk of report.skipped) {
282
+ process.stdout.write(` ${sk.failed ? '✗' : '–'} ${sk.ruleId} ${sk.failed ? 'FAILED' : 'SKIPPED'} — ${sk.reason}\n`);
283
+ }
135
284
  if (newOnly) {
136
285
  process.stdout.write(kv('scope', `new-only (${hiddenBaseline} pre-existing finding(s) hidden — run without --new-only to see all)`) + '\n');
137
286
  }
@@ -140,6 +289,16 @@ export const policyLintCommand = {
140
289
  for (const d of report.diagnostics)
141
290
  process.stdout.write(` ! ${d}\n`);
142
291
  }
292
+ if (report.skipped.some((sk) => sk.failed)) {
293
+ process.stdout.write('\nA rule that matched nothing is a bug in the rule, not a pass. (`error`-severity\n' +
294
+ ' rules fail on empty by default — set `failOnEmpty: false` if the set may be empty.)\n');
295
+ return 1;
296
+ }
297
+ // A non-failing skip still means "partially verified" — never a green 0.
298
+ if (report.skipped.length > 0 && report.findings.length === 0) {
299
+ process.stdout.write(`\n${report.skipped.length} rule(s) scanned nothing — partially verified, not a full green.\n`);
300
+ return 2;
301
+ }
143
302
  if (report.findings.length === 0 && report.diagnostics.length === 0) {
144
303
  process.stdout.write(newOnly
145
304
  ? `\nNo NEW policy violations from this change${hiddenBaseline > 0 ? ` (${hiddenBaseline} pre-existing hidden)` : ''}. ✓\n`
@@ -1 +1 @@
1
- {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AA4BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AA6IF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
1
+ {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AA8BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAQhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AAkMF,eAAO,MAAM,eAAe,EAAE,eA0C7B,CAAC"}
@@ -8,17 +8,20 @@
8
8
  * [--fail-if-taken] # guard: non-zero when taken (free → 0)
9
9
  * [--fail-if-missing] # guard: non-zero when NOT registered
10
10
  * shrk registry <name> where <id> [--json] # declaration (+ consumer) sites
11
+ * shrk registry <name> duplicates [--json] # ids declared in more than one place
11
12
  *
12
13
  * `<name>` resolves a `registries[]` declaration in sharkcraft.config.ts — one
13
14
  * deterministic multi-root scan that answers "is this id taken / where is it"
14
15
  * without an agent re-running a fragile grep.
15
16
  */
16
17
  import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
17
- import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
18
+ import { scanRegistry, registryDuplicates, registryExists, registryWhere, } from '@shrkcrft/boundaries';
18
19
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
19
20
  import { ExitCode } from "../exit-codes.js";
20
21
  import { asJson } from "../output/format-output.js";
21
22
  import { resolveRegistryNoun } from "./registry-resolve.js";
23
+ /** The inventory verbs, used to detect (and forgive) a verb-first invocation. */
24
+ const INVENTORY_VERBS = new Set(['list', 'exists', 'where', 'duplicates']);
22
25
  export const registryLifecycleCommand = {
23
26
  name: 'lifecycle',
24
27
  description: 'Scan the workspace for register/remove symmetry. Read-only.',
@@ -97,11 +100,20 @@ async function runRegistryInventory(args, name) {
97
100
  if (!decl) {
98
101
  const names = loaded.registries.map((r) => r.name);
99
102
  const avail = names.length > 0 ? `Declared registries: ${names.join(', ')}.` : 'No registries declared in sharkcraft.config.ts `registries[]`.';
100
- if (json)
101
- process.stdout.write(asJson({ error: `unknown registry "${name}"`, available: names }) + '\n');
102
- else
103
+ // Typing the verb first is the common stumble (siblings are verb-first).
104
+ // Name the correct grammar instead of only reporting the miss.
105
+ const verbFirst = INVENTORY_VERBS.has(name);
106
+ if (json) {
107
+ process.stdout.write(asJson({ error: `unknown registry "${name}"`, available: names, ...(verbFirst ? { hint: `did you mean 'registry <name> ${name}'?` } : {}) }) + '\n');
108
+ }
109
+ else {
103
110
  process.stderr.write(`No registry named "${name}". ${avail}\n`);
104
- return 2;
111
+ if (verbFirst) {
112
+ const example = names[0] ?? '<name>';
113
+ process.stderr.write(`"${name}" is a verb, not a registry — did you mean \`shrk registry ${example} ${name}\`?\n`);
114
+ }
115
+ }
116
+ return ExitCode.UsageError;
105
117
  }
106
118
  const inventory = scanRegistry(cwd, decl);
107
119
  if (action === 'list' || action === undefined) {
@@ -124,13 +136,13 @@ async function runRegistryInventory(args, name) {
124
136
  if (action === 'exists') {
125
137
  if (!id) {
126
138
  process.stderr.write(`Usage: shrk registry ${name} exists <id> [--resolve] [--fail-if-taken|--fail-if-missing]\n`);
127
- return 2;
139
+ return ExitCode.UsageError;
128
140
  }
129
141
  const failIfTaken = flagBool(args, 'fail-if-taken');
130
142
  const failIfMissing = flagBool(args, 'fail-if-missing');
131
143
  if (failIfTaken && failIfMissing) {
132
144
  process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
133
- return 2;
145
+ return ExitCode.UsageError;
134
146
  }
135
147
  // `--resolve` maps a human noun to the canonical registered id before the
136
148
  // existence test — via the registry's declared `aliases` map AND generic
@@ -166,10 +178,41 @@ async function runRegistryInventory(args, name) {
166
178
  process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
167
179
  return code;
168
180
  }
181
+ if (action === 'duplicates') {
182
+ // Two roots claiming the same id compile fine; whichever registration wins
183
+ // at runtime is an accident of load order. Every site is printed so the
184
+ // duplicate can be resolved, not merely detected.
185
+ const dupes = registryDuplicates(inventory);
186
+ if (json) {
187
+ process.stdout.write(asJson({
188
+ name: inventory.name,
189
+ scanned: inventory.entries.length,
190
+ duplicates: dupes,
191
+ diagnostics: [...inventory.diagnostics, ...loaded.planeDiagnostics],
192
+ }) + '\n');
193
+ return inventory.entries.length === 0 ? 2 : dupes.length > 0 ? 1 : 0;
194
+ }
195
+ if (inventory.entries.length === 0) {
196
+ process.stdout.write(`Registry "${inventory.name}" matched 0 ids — nothing was checked. This is NOT a pass;\n` +
197
+ ' the source selector is probably stale (see `shrk gates coverage`).\n');
198
+ return 2;
199
+ }
200
+ if (dupes.length === 0) {
201
+ process.stdout.write(`No duplicate ids in registry "${inventory.name}" (${inventory.entries.length} scanned). ✓\n`);
202
+ return 0;
203
+ }
204
+ process.stdout.write(`Duplicate ids in registry "${inventory.name}" (${dupes.length}):\n`);
205
+ for (const e of dupes) {
206
+ process.stdout.write(` ✗ ${e.id} (${e.sites.length} declarations)\n`);
207
+ for (const s of e.sites)
208
+ process.stdout.write(` ${s.file}:${s.line}\n`);
209
+ }
210
+ return 1;
211
+ }
169
212
  if (action === 'where') {
170
213
  if (!id) {
171
214
  process.stderr.write(`Usage: shrk registry ${name} where <id>\n`);
172
- return 2;
215
+ return ExitCode.UsageError;
173
216
  }
174
217
  const entry = registryWhere(inventory, id);
175
218
  if (json) {
@@ -187,13 +230,13 @@ async function runRegistryInventory(args, name) {
187
230
  process.stdout.write(` consumed ${s.file}:${s.line}\n`);
188
231
  return 0;
189
232
  }
190
- process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id>\n`);
191
- return 2;
233
+ process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id> | duplicates\n`);
234
+ return ExitCode.UsageError;
192
235
  }
193
236
  export const registryCommand = {
194
237
  name: 'registry',
195
238
  description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
196
- usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id>',
239
+ usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id> | <name> duplicates',
197
240
  // Guard-mode + query flags take no value — declare them so `exists <id>
198
241
  // --fail-if-taken` (flag last) and `exists --resolve <id>` (flag first) both
199
242
  // keep the id as a positional instead of swallowing it.
@@ -205,13 +248,27 @@ export const registryCommand = {
205
248
  return registryLifecycleCommand.run(args);
206
249
  }
207
250
  if (sub !== undefined && sub.length > 0) {
208
- // `<name> list | exists <id> | where <id>` sub is the registry name.
251
+ // Grammar is `registry <name> <verb>`, but `baseline`/`generated` read
252
+ // verb-first, so `registry list <name>` is the instinctive form. Accept
253
+ // BOTH: when arg1 is a known verb and arg2 names a declared registry,
254
+ // swap them. The canonical order is unchanged; the stumble is removed.
255
+ const second = args.positional[1];
256
+ if (INVENTORY_VERBS.has(sub) && second !== undefined && second.length > 0) {
257
+ const loaded = await loadRegistries(resolveCwd(args));
258
+ const known = loaded.ok && findRegistry(loaded.registries, second) !== undefined;
259
+ if (known) {
260
+ // [verb, name, ...rest] → [name, verb, ...rest]
261
+ args.positional = [second, sub, ...args.positional.slice(2)];
262
+ return runRegistryInventory(args, second);
263
+ }
264
+ }
265
+ // `<name> list | exists <id> | where <id> | duplicates` — sub is the name.
209
266
  return runRegistryInventory(args, sub);
210
267
  }
211
268
  const cwd = resolveCwd(args);
212
269
  const loaded = await loadRegistries(cwd);
213
270
  const names = loaded.ok ? loaded.registries.map((r) => r.name) : [];
214
- process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>\n' +
271
+ process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id> | <name> duplicates\n' +
215
272
  (names.length > 0 ? `Declared registries: ${names.join(', ')}.\n` : 'No registries declared (sharkcraft.config.ts `registries[]`).\n'));
216
273
  return 2;
217
274
  },
@@ -1 +1 @@
1
- {"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAsDrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
1
+ {"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAoFrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
@@ -24,6 +24,10 @@ export function renderWiringExplain(report, wantJson) {
24
24
  process.stdout.write(` ${report.description}\n`);
25
25
  if (report.groupBy)
26
26
  process.stdout.write(kv('groupBy', report.groupBy) + '\n');
27
+ if (report.registeredMode === 'intersection') {
28
+ process.stdout.write(kv('registeredMode', 'intersection (must be in EVERY sink)') + '\n');
29
+ }
30
+ process.stdout.write(kv('status', report.status) + '\n');
27
31
  process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)`) +
28
32
  '\n');
29
33
  process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)`) + '\n');
@@ -52,6 +56,27 @@ export function renderWiringExplain(report, wantJson) {
52
56
  process.stdout.write(` … (${report.registeredNotDeclared.length - SITE_DISPLAY_CAP} more)\n`);
53
57
  }
54
58
  }
59
+ if (report.overlap.length > 0) {
60
+ process.stdout.write(`\nPresent on BOTH sides (disjoint, ${report.overlap.length}):\n`);
61
+ for (const s of report.overlap.slice(0, SITE_DISPLAY_CAP)) {
62
+ process.stdout.write(` ✗ ${s.token} (${s.file}:${s.line})\n`);
63
+ }
64
+ if (report.overlap.length > SITE_DISPLAY_CAP) {
65
+ process.stdout.write(` … (${report.overlap.length - SITE_DISPLAY_CAP} more)\n`);
66
+ }
67
+ }
68
+ if (report.hops && report.hops.length > 0) {
69
+ process.stdout.write('\nChain hops:\n');
70
+ for (const h of report.hops) {
71
+ process.stdout.write(` hop ${h.index}: ${h.fromCount} → ${h.toCount}` +
72
+ `${h.missing > 0 ? ` ✗ ${h.missing} missing` : ' ✓'}\n`);
73
+ }
74
+ }
75
+ // A rule that checked NOTHING must never read like a clean pass.
76
+ if (report.skipReason) {
77
+ process.stdout.write(`\n! SKIPPED — ${report.skipReason}. A rule that matches nothing is a bug in the rule,\n` +
78
+ ' not a pass. Fix the selector, or set `failOnEmpty: true` to make this a hard failure.\n');
79
+ }
55
80
  for (const d of report.diagnostics)
56
81
  process.stdout.write(` ! ${d}\n`);
57
82
  process.stdout.write(`\nVerdict: ${report.verdict}\n`);
@@ -12,12 +12,19 @@
12
12
  * 0 VerifiedPass — checks ran over a NON-EMPTY scope and passed. Never
13
13
  * returned when zero units were evaluated.
14
14
  * 1 Failure — checks ran and found violations.
15
- * 2 NotVerified — indeterminate: empty evaluation scope, degraded
16
- * fallback, short-circuit, timeout, or "refused to run".
17
- * Distinct from both pass and fail so a chain can branch
18
- * on it (`|| handle-indeterminate`). This is also the
19
- * code the CLI already uses for usage errors — both mean
20
- * "did not produce a verified result".
15
+ * 2 NotVerified — indeterminate: empty evaluation scope, all rules
16
+ * skipped, degraded fallback, short-circuit, timeout, or
17
+ * "refused to run". Distinct from both pass and fail so a
18
+ * chain can branch on it (`|| handle-indeterminate`).
19
+ * 3 UsageError — the request itself was malformed: unloadable config, an
20
+ * unknown rule id, a bad flag value. Split out of `2` on
21
+ * the GATE verbs because the two demand different
22
+ * responses: `2` means "the gate ran but proved nothing"
23
+ * (investigate the rules), `3` means "the gate never
24
+ * started" (fix the invocation or the config). Non-gate
25
+ * verbs keep returning `2` for usage errors — widening the
26
+ * split across the whole CLI would churn a documented
27
+ * contract far beyond what it buys.
21
28
  *
22
29
  * The `gen --typecheck` pre-write gate already refuses-to-nonzero rather than
23
30
  * emit an unverified artifact; this generalizes that instinct across the gate
@@ -27,7 +34,8 @@
27
34
  export declare enum ExitCode {
28
35
  VerifiedPass = 0,
29
36
  Failure = 1,
30
- NotVerified = 2
37
+ NotVerified = 2,
38
+ UsageError = 3
31
39
  }
32
40
  /**
33
41
  * Promote a NotVerified (`2`) exit into a Failure-class nonzero (`1`) when the
@@ -61,12 +69,24 @@ export declare function argvHasExitTrailer(argv: readonly string[]): boolean;
61
69
  * so `check boundaries --json` (2 tokens) and a bare `finish` (1) both resolve.
62
70
  */
63
71
  export declare function isGateVerb(commandPath: string): boolean;
72
+ /**
73
+ * True when the argv carries the global `--no-hints` (before the `--`
74
+ * sentinel). Suppresses advisory one-liners like the piped-exit note while
75
+ * leaving every real diagnostic — and the `--exit-trailer` machine channel —
76
+ * untouched. For a caller that has already internalised the warning and just
77
+ * wants clean stderr in captured logs.
78
+ */
79
+ export declare function argvHasNoHints(argv: readonly string[]): boolean;
80
+ /** Reset the one-time hint latch. Test-only seam. */
81
+ export declare function resetPipeHintLatch(): void;
64
82
  /** Injectable surface for {@link emitPipeExitSignal} (isTTY + writer + trailer). */
65
83
  export interface IPipeExitOptions {
66
84
  /** True when shrk's stdout is NOT a terminal (i.e. piped/redirected). */
67
85
  readonly piped: boolean;
68
86
  /** True when `--exit-trailer` was requested. */
69
87
  readonly trailer: boolean;
88
+ /** True when `--no-hints` was requested — suppress the advisory note only. */
89
+ readonly noHints?: boolean;
70
90
  /** stderr writer; defaults to `process.stderr.write`. Overridable for tests. */
71
91
  readonly write?: (s: string) => void;
72
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,oBAAY,QAAQ;IAClB,YAAY,IAAI;IAChB,OAAO,IAAI;IACX,WAAW,IAAI;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM9D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMnE;AA4BD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,IAAI,CAWN"}
1
+ {"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,oBAAY,QAAQ;IAClB,YAAY,IAAI;IAChB,OAAO,IAAI;IACX,WAAW,IAAI;IACf,UAAU,IAAI;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM9D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMnE;AA4BD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM/D;AASD,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,IAAI,CAgBN"}