@tangle-network/agent-runtime 0.69.0 → 0.70.1

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 (67) hide show
  1. package/README.md +81 -13
  2. package/dist/agent.d.ts +4 -5
  3. package/dist/agent.js +3 -4
  4. package/dist/agent.js.map +1 -1
  5. package/dist/analyst-loop.d.ts +3 -4
  6. package/dist/{chunk-K4FQSEXQ.js → chunk-BGOLR66M.js} +6 -6
  7. package/dist/{chunk-G6U3GVK2.js → chunk-CRAH5EY2.js} +2 -2
  8. package/dist/{chunk-RTDI2DHD.js → chunk-HY4YOQM2.js} +6 -6
  9. package/dist/chunk-HY4YOQM2.js.map +1 -0
  10. package/dist/{chunk-QZ6FA5LM.js → chunk-LJZ5GC6C.js} +3 -3
  11. package/dist/{chunk-NLRA6434.js → chunk-YFOPWG74.js} +577 -525
  12. package/dist/chunk-YFOPWG74.js.map +1 -0
  13. package/dist/{coordination-Curpzeyc.d.ts → coordination-C7WxwHXq.d.ts} +8 -1
  14. package/dist/{delegates-CLFNAKyi.d.ts → delegates-DqAgo32T.d.ts} +105 -10
  15. package/dist/{improvement-adapter-BC4HhuAR.d.ts → improvement-adapter-CioiEE2z.d.ts} +1 -1
  16. package/dist/index.d.ts +439 -100
  17. package/dist/index.js +591 -130
  18. package/dist/index.js.map +1 -1
  19. package/dist/intelligence.d.ts +2 -3
  20. package/dist/{loop-runner-bin-B6dzNZC8.d.ts → loop-runner-bin-a8bu4O5-.d.ts} +4 -4
  21. package/dist/loop-runner-bin.d.ts +7 -10
  22. package/dist/loop-runner-bin.js +6 -7
  23. package/dist/loops.d.ts +3149 -16
  24. package/dist/loops.js +33 -60
  25. package/dist/mcp/bin.js +13 -14
  26. package/dist/mcp/bin.js.map +1 -1
  27. package/dist/mcp/index.d.ts +8 -12
  28. package/dist/mcp/index.js +10 -12
  29. package/dist/mcp/index.js.map +1 -1
  30. package/dist/{openai-tools-CA2N3-Ak.d.ts → openai-tools-DPx9Gzvn.d.ts} +1 -1
  31. package/dist/profiles.d.ts +94 -6
  32. package/dist/profiles.js +320 -9
  33. package/dist/profiles.js.map +1 -1
  34. package/dist/{router-client-30Y_pca8.d.ts → router-client-C7kp_ECN.d.ts} +37 -1
  35. package/dist/{substrate-CUgk7F7s.d.ts → substrate-BoRXgvka.d.ts} +52 -1
  36. package/dist/{types-p8dWBIXL.d.ts → types-BC3bZpH0.d.ts} +1 -1
  37. package/dist/{types-Crxftafi.d.ts → types-BYa2ZOAx.d.ts} +83 -2
  38. package/dist/{worktree-fanout-DUiKPApb.d.ts → worktree-fanout-gNfl0Byj.d.ts} +9 -37
  39. package/package.json +17 -37
  40. package/dist/audit.d.ts +0 -93
  41. package/dist/audit.js +0 -312
  42. package/dist/audit.js.map +0 -1
  43. package/dist/chunk-4B6U4CVQ.js +0 -15
  44. package/dist/chunk-4B6U4CVQ.js.map +0 -1
  45. package/dist/chunk-NLRA6434.js.map +0 -1
  46. package/dist/chunk-O2UPHN7X.js +0 -114
  47. package/dist/chunk-O2UPHN7X.js.map +0 -1
  48. package/dist/chunk-RTDI2DHD.js.map +0 -1
  49. package/dist/coder-2leJPOvC.d.ts +0 -52
  50. package/dist/improvement.d.ts +0 -208
  51. package/dist/improvement.js +0 -343
  52. package/dist/improvement.js.map +0 -1
  53. package/dist/local-harness-BE_h8szs.d.ts +0 -93
  54. package/dist/run-loop-D3PwlG7J.d.ts +0 -112
  55. package/dist/runtime-hooks-C7JwKb9E.d.ts +0 -70
  56. package/dist/runtime.d.ts +0 -3120
  57. package/dist/runtime.js +0 -263
  58. package/dist/runtime.js.map +0 -1
  59. package/dist/topology.d.ts +0 -126
  60. package/dist/topology.js +0 -333
  61. package/dist/topology.js.map +0 -1
  62. package/dist/workflow.d.ts +0 -551
  63. package/dist/workflow.js +0 -1781
  64. package/dist/workflow.js.map +0 -1
  65. /package/dist/{chunk-K4FQSEXQ.js.map → chunk-BGOLR66M.js.map} +0 -0
  66. /package/dist/{chunk-G6U3GVK2.js.map → chunk-CRAH5EY2.js.map} +0 -0
  67. /package/dist/{chunk-QZ6FA5LM.js.map → chunk-LJZ5GC6C.js.map} +0 -0
@@ -1,10 +1,8 @@
1
1
  import { AgentProfile } from '@tangle-network/agent-interface';
2
2
  import { AnalystFinding, DefaultVerdict } from '@tangle-network/agent-eval';
3
- import { h as AgentSpec, E as ExecutorRegistry, B as Budget, A as Agent, i as SpawnJournal, g as ResultBlobStore, j as RootHandle, k as SupervisedResult, N as NodeId, l as Settled, m as Spend, S as Scope, n as Executor, G as GitRunner } from './delegates-CLFNAKyi.js';
4
- import { I as Iteration } from './types-Crxftafi.js';
3
+ import { h as AgentSpec, E as ExecutorRegistry, B as Budget, A as Agent, i as SpawnJournal, g as ResultBlobStore, j as RootHandle, k as SupervisedResult, N as NodeId, l as Settled, m as Spend, S as Scope, n as Executor, L as LocalHarness, G as GitRunner, r as runLocalHarness } from './delegates-DqAgo32T.js';
4
+ import { b as RuntimeHooks, I as Iteration } from './types-BYa2ZOAx.js';
5
5
  import { BackendType } from '@tangle-network/sandbox';
6
- import { R as RuntimeHooks } from './runtime-hooks-C7JwKb9E.js';
7
- import { L as LocalHarness, r as runLocalHarness } from './local-harness-BE_h8szs.js';
8
6
 
9
7
  /**
10
8
  * @experimental
@@ -787,6 +785,12 @@ type EqualKOnCost = (arms: ReadonlyArray<EqualKArm>, options?: EqualKOnCostOptio
787
785
  * fail-closed (not delivered), never a crash.
788
786
  */
789
787
 
788
+ /**
789
+ * The deployable completion oracle passed to {@link gateOnDeliverable}: a `check` that
790
+ * decides DELIVERED (settles `valid` ⟺ it resolves true) plus an optional `describe` of
791
+ * what the spawn was supposed to produce. The check reads the child's output — never the
792
+ * model judging itself.
793
+ */
790
794
  interface DeliverableSpec<Out = unknown> {
791
795
  /** The deployable check that decides DELIVERED. `settled.valid ⟺ this resolves true`. */
792
796
  check: (out: Out) => boolean | Promise<boolean>;
@@ -813,15 +817,6 @@ declare function gateOnDeliverable<Out>(inner: Executor<Out>, deliverable: Deliv
813
817
  * (`isNonEmptyPatch` / `touchesSecretPath`) for reuse outside the full scorer.
814
818
  */
815
819
 
816
- /** @experimental The patch + its derived PASS signals the mechanical gate decides on. */
817
- interface CoderCheckInput {
818
- /** The unified diff produced by the run. */
819
- patch: string;
820
- /** Did `testCmd` exit clean? */
821
- testsPassed: boolean;
822
- /** Did `typecheckCmd` exit clean? */
823
- typecheckPassed: boolean;
824
- }
825
820
  /** @experimental The per-task constraints the mechanical gate enforces. */
826
821
  interface CoderCheckConstraints {
827
822
  /** Default 400. Hard cap; gate fails when exceeded. */
@@ -829,29 +824,6 @@ interface CoderCheckConstraints {
829
824
  /** Literal path prefixes the patch must not touch. */
830
825
  forbiddenPaths?: string[];
831
826
  }
832
- /** The unified-diff paths the patch touches — the `+++`/`---` headers, de-`a/`/`b/`-prefixed,
833
- * with `/dev/null` (a delete's other side) dropped. */
834
- declare function touchedPathsFromPatch(patch: string): string[];
835
- /** Count the added/removed content lines in a unified diff (excludes the `+++`/`---` headers). */
836
- declare function countDiffLines(patch: string): number;
837
- /** True when the patch actually changes something — at least one touched path AND non-blank body.
838
- * An empty patch can trivially "pass" tests/typecheck (nothing changed) yet does no work. */
839
- declare function isNonEmptyPatch(patch: string): boolean;
840
- /** The credential-shaped paths the patch touches (always-on floor, independent of `forbiddenPaths`). */
841
- declare function touchesSecretPath(patch: string): string[];
842
- /**
843
- * @experimental
844
- *
845
- * The pure mechanical gate — the SINGLE source of the no-op / always-on secret-path floor /
846
- * diff-size / forbidden-path / test / typecheck checks. No I/O: it scores a patch + its
847
- * already-derived pass signals.
848
- *
849
- * Checks in order: (1) no-op rejection, (2) always-on secret-path floor (independent of
850
- * `forbiddenPaths`), (3) forbidden-path, (4) diff-size cap, (5) tests, (6) typecheck.
851
- * Aggregate score: `0.5*tests + 0.3*typecheck + 0.2*(1 - diffLines/maxDiff)`; `valid` is the
852
- * conjunction of all six.
853
- */
854
- declare function runCoderChecks(input: CoderCheckInput, constraints?: CoderCheckConstraints): DefaultVerdict;
855
827
 
856
828
  /**
857
829
  * @experimental
@@ -1096,4 +1068,4 @@ interface WorktreeFanoutOptions extends PatchDeliverableOptions {
1096
1068
  */
1097
1069
  declare function worktreeFanout<Task>(options: WorktreeFanoutOptions): CombinatorShape<Task, WorktreePatchArtifact>;
1098
1070
 
1099
- export { type TrajectoryNode as $, type AuthoredHarness as A, type FlatWidenGate as B, type CorpusRecord as C, type DefinePersonaInput as D, type EqualKArm as E, type FanoutOptions as F, type LoopUntil as G, type LoopUntilState as H, type Panel as I, type PanelJudge as J, type PanelVerdict as K, type LoopUntilSpec as L, type PatchDeliverableOptions as M, type PersonaContext as N, type Outcome as O, type PanelSpec as P, type PersonaExecutors as Q, type RenderCorpusToInstructionsOptions as R, type ScopeAnalyzeInput as S, type TrajectoryReportOptions as T, type Pipeline as U, type VerifySpec as V, type WinnerStrategy as W, type RenderCorpusToInstructions as X, type RunPersonified as Y, type ShapeBudget as Z, type ShapeContext as _, type WorktreeFanoutOptions as a, type TrajectoryReportFn as a0, type Verify as a1, type Widen as a2, type WidenDecision as a3, type WidenLineage as a4, type WorktreeCliExecutorOptions as a5, type WorktreeCommandResult as a6, countDiffLines as a7, createWorktreeCliExecutor as a8, gateOnDeliverable as a9, isNonEmptyPatch as aa, patchDelivered as ab, runCoderChecks as ac, touchedPathsFromPatch as ad, touchesSecretPath as ae, worktreeFanout as af, type WorktreePatchArtifact as b, type Corpus as c, type AssertTraceDerivedFindings as d, type SteerContext as e, type ScopeAnalyst as f, type CombinatorShape as g, type ScopeWidenGate as h, type PipelineStage as i, type FanoutWinnerSelector as j, type WidenSpec as k, type CorpusFilter as l, type Persona as m, type RunPersonifiedOptions as n, type ShapeRegistry as o, type LoopShape as p, type EqualKOnCostOptions as q, type EqualKVerdict as r, type TrajectoryReport as s, type DeliverableSpec as t, type CoderCheckConstraints as u, type CoderCheckInput as v, type DefinePersona as w, type EqualKOnCost as x, type Fanout as y, type FanoutSynthesis as z };
1071
+ export { type Verify as $, type AuthoredHarness as A, type LoopUntilState as B, type CorpusRecord as C, type DefinePersonaInput as D, type EqualKArm as E, type FanoutOptions as F, type Panel as G, type PanelJudge as H, type PanelVerdict as I, type PatchDeliverableOptions as J, type PersonaContext as K, type LoopUntilSpec as L, type PersonaExecutors as M, type Pipeline as N, type Outcome as O, type PanelSpec as P, type RenderCorpusToInstructions as Q, type RenderCorpusToInstructionsOptions as R, type ScopeAnalyzeInput as S, type TrajectoryReportOptions as T, type RunPersonified as U, type VerifySpec as V, type WinnerStrategy as W, type ShapeBudget as X, type ShapeContext as Y, type TrajectoryNode as Z, type TrajectoryReportFn as _, type WorktreeFanoutOptions as a, type Widen as a0, type WidenDecision as a1, type WidenLineage as a2, type WorktreeCliExecutorOptions as a3, type WorktreeCommandResult as a4, createWorktreeCliExecutor as a5, gateOnDeliverable as a6, patchDelivered as a7, worktreeFanout as a8, type WorktreePatchArtifact as b, type Corpus as c, type AssertTraceDerivedFindings as d, type SteerContext as e, type ScopeAnalyst as f, type CombinatorShape as g, type ScopeWidenGate as h, type PipelineStage as i, type FanoutWinnerSelector as j, type WidenSpec as k, type CorpusFilter as l, type Persona as m, type RunPersonifiedOptions as n, type ShapeRegistry as o, type LoopShape as p, type EqualKOnCostOptions as q, type EqualKVerdict as r, type TrajectoryReport as s, type DeliverableSpec as t, type DefinePersona as u, type EqualKOnCost as v, type Fanout as w, type FanoutSynthesis as x, type FlatWidenGate as y, type LoopUntil as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-runtime",
3
- "version": "0.69.0",
3
+ "version": "0.70.1",
4
4
  "description": "Shared task-lifecycle skeleton for agents: a recursive loop kernel for chat turns, one-shot tasks, and multi-attempt loops, with trace capture and eval-gated self-improvement. Domain behavior lives in adapters; scoring and ship-gates in @tangle-network/agent-eval.",
5
5
  "homepage": "https://github.com/tangle-network/agent-runtime#readme",
6
6
  "repository": {
@@ -19,21 +19,6 @@
19
19
  "import": "./dist/index.js",
20
20
  "default": "./dist/index.js"
21
21
  },
22
- "./platform": {
23
- "types": "./dist/platform.d.ts",
24
- "import": "./dist/platform.js",
25
- "default": "./dist/platform.js"
26
- },
27
- "./analyst-loop": {
28
- "types": "./dist/analyst-loop.d.ts",
29
- "import": "./dist/analyst-loop.js",
30
- "default": "./dist/analyst-loop.js"
31
- },
32
- "./improvement": {
33
- "types": "./dist/improvement.d.ts",
34
- "import": "./dist/improvement.js",
35
- "default": "./dist/improvement.js"
36
- },
37
22
  "./agent": {
38
23
  "types": "./dist/agent.d.ts",
39
24
  "import": "./dist/agent.js",
@@ -44,35 +29,25 @@
44
29
  "import": "./dist/intelligence.js",
45
30
  "default": "./dist/intelligence.js"
46
31
  },
47
- "./runtime": {
48
- "types": "./dist/runtime.d.ts",
49
- "import": "./dist/runtime.js",
50
- "default": "./dist/runtime.js"
51
- },
52
32
  "./loops": {
53
33
  "types": "./dist/loops.d.ts",
54
34
  "import": "./dist/loops.js",
55
35
  "default": "./dist/loops.js"
56
36
  },
57
- "./topology": {
58
- "types": "./dist/topology.d.ts",
59
- "import": "./dist/topology.js",
60
- "default": "./dist/topology.js"
61
- },
62
- "./workflow": {
63
- "types": "./dist/workflow.d.ts",
64
- "import": "./dist/workflow.js",
65
- "default": "./dist/workflow.js"
37
+ "./analyst-loop": {
38
+ "types": "./dist/analyst-loop.d.ts",
39
+ "import": "./dist/analyst-loop.js",
40
+ "default": "./dist/analyst-loop.js"
66
41
  },
67
42
  "./profiles": {
68
43
  "types": "./dist/profiles.d.ts",
69
44
  "import": "./dist/profiles.js",
70
45
  "default": "./dist/profiles.js"
71
46
  },
72
- "./audit": {
73
- "types": "./dist/audit.d.ts",
74
- "import": "./dist/audit.js",
75
- "default": "./dist/audit.js"
47
+ "./platform": {
48
+ "types": "./dist/platform.d.ts",
49
+ "import": "./dist/platform.js",
50
+ "default": "./dist/platform.js"
76
51
  },
77
52
  "./mcp": {
78
53
  "types": "./dist/mcp/index.d.ts",
@@ -102,16 +77,21 @@
102
77
  "lint:fix": "biome check --write src tests examples",
103
78
  "typecheck": "tsc --noEmit && pnpm run typecheck:examples",
104
79
  "typecheck:examples": "tsc --noEmit -p tsconfig.examples.json",
105
- "verify:package": "node scripts/verify-package-exports.mjs"
80
+ "verify:package": "node scripts/verify-package-exports.mjs",
81
+ "docs:api": "typedoc",
82
+ "docs:freshness": "node scripts/check-docs-freshness.mjs",
83
+ "docs:check": "pnpm run docs:api && git diff --exit-code -- docs/api && pnpm run docs:freshness"
106
84
  },
107
85
  "devDependencies": {
108
86
  "@biomejs/biome": "^2.4.15",
109
- "@tangle-network/agent-eval": ">=0.93.0 <1.0.0",
87
+ "@tangle-network/agent-eval": ">=0.95.0 <1.0.0",
110
88
  "@tangle-network/agent-interface": ">=0.10.0 <1.0.0",
111
89
  "@tangle-network/sandbox": ">=0.8.0 <1.0.0",
112
90
  "@types/node": "^25.9.3",
113
91
  "playwright": "^1.61.0",
114
92
  "tsup": "^8.0.0",
93
+ "typedoc": "0.28.19",
94
+ "typedoc-plugin-markdown": "4.12.0",
115
95
  "typescript": "^5.7.0",
116
96
  "vitest": "^3.0.0"
117
97
  },
@@ -132,7 +112,7 @@
132
112
  "license": "MIT",
133
113
  "packageManager": "pnpm@10.28.0",
134
114
  "peerDependencies": {
135
- "@tangle-network/agent-eval": ">=0.93.0 <1.0.0",
115
+ "@tangle-network/agent-eval": ">=0.95.0 <1.0.0",
136
116
  "@tangle-network/agent-interface": ">=0.10.0 <1.0.0",
137
117
  "@tangle-network/agent-knowledge": ">=1.7.0 <2.0.0",
138
118
  "@tangle-network/sandbox": ">=0.8.0 <1.0.0",
package/dist/audit.d.ts DELETED
@@ -1,93 +0,0 @@
1
- import { U as UiFinding, a as UiLens } from './substrate-CUgk7F7s.js';
2
-
3
- /**
4
- * @experimental
5
- *
6
- * UI-audit issue writer — pure I/O. Takes a workspace dir + `UiFinding[]`
7
- * and emits:
8
- * - `<workspace>/issues/NNN--<lens>--<slug>.md` — one self-contained
9
- * GitHub-issue-ready Markdown per finding, with embedded screenshot
10
- * references.
11
- * - `<workspace>/registry.json` — finding index for dedup and audit
12
- * resume across iterations.
13
- * - `<workspace>/index.md` — human-readable rollup (severity / lens /
14
- * route counts plus a sorted finding list).
15
- *
16
- * The writer is deterministic, idempotent for `appendFindings()`, and
17
- * never invokes an LLM. It assigns the next monotonic id to a finding the
18
- * caller did not pre-id.
19
- */
20
-
21
- /** @experimental */
22
- interface AuditRegistry {
23
- schemaVersion: 1;
24
- findings: UiFinding[];
25
- /** Route → URL + captures sidecar; preserved across runs. */
26
- routes: Record<string, {
27
- url?: string;
28
- captures: AuditRegistryCapture[];
29
- }>;
30
- }
31
- /** @experimental */
32
- interface AuditRegistryCapture {
33
- file: string;
34
- viewport?: string;
35
- fullPage?: boolean;
36
- elementSelector?: string;
37
- capturedAt: string;
38
- }
39
- /** @experimental */
40
- declare function initAuditWorkspace(workspaceDir: string): Promise<void>;
41
- /** @experimental */
42
- declare function readAuditRegistry(workspaceDir: string): Promise<AuditRegistry>;
43
- /** @experimental */
44
- interface AppendFindingsResult {
45
- /** Findings with id + createdAt assigned, in input order. */
46
- written: UiFinding[];
47
- /** Workspace-relative path to each issue Markdown file, in input order. */
48
- files: string[];
49
- }
50
- /**
51
- * Append findings to a workspace, writing one Markdown file per finding
52
- * and updating registry.json. Assigns monotonically increasing ids to
53
- * findings that arrived without one.
54
- *
55
- * Findings already carrying an id that collides with the registry are
56
- * rejected — callers must either freshly mint findings (id undefined) or
57
- * use a separate update path. This protects against accidental overwrite.
58
- *
59
- * @experimental
60
- */
61
- declare function appendFindings(workspaceDir: string, findings: readonly UiFinding[]): Promise<AppendFindingsResult>;
62
- /** @experimental */
63
- interface RegisterCapturesOptions {
64
- route: string;
65
- url?: string;
66
- captures: readonly AuditRegistryCapture[];
67
- }
68
- /**
69
- * Record screenshots taken for a route in the registry, without filing a
70
- * finding. Useful when the auditor wants to remember which captures
71
- * exist for resume / dedup purposes.
72
- *
73
- * @experimental
74
- */
75
- declare function registerCaptures(workspaceDir: string, options: RegisterCapturesOptions): Promise<void>;
76
- /** @experimental */
77
- interface AuditIndex {
78
- /** Total findings in the workspace. */
79
- total: number;
80
- bySeverity: Record<UiFinding['severity'], number>;
81
- byLens: Partial<Record<UiLens, number>>;
82
- byRoute: Record<string, number>;
83
- }
84
- /** @experimental */
85
- declare function summarizeRegistry(reg: AuditRegistry): AuditIndex;
86
- /**
87
- * Regenerate `<workspace>/index.md` from registry.json.
88
- *
89
- * @experimental
90
- */
91
- declare function writeAuditIndex(workspaceDir: string): Promise<string>;
92
-
93
- export { type AppendFindingsResult, type AuditIndex, type AuditRegistry, type AuditRegistryCapture, type RegisterCapturesOptions, appendFindings, initAuditWorkspace, readAuditRegistry, registerCaptures, summarizeRegistry, writeAuditIndex };
package/dist/audit.js DELETED
@@ -1,312 +0,0 @@
1
- import {
2
- slugify
3
- } from "./chunk-4B6U4CVQ.js";
4
- import {
5
- UI_FINDING_SEVERITIES,
6
- UI_LENSES
7
- } from "./chunk-WIR4HOOJ.js";
8
- import "./chunk-DGUM43GV.js";
9
-
10
- // src/audit/issue-writer.ts
11
- import { promises as fs } from "fs";
12
- import path from "path";
13
- var SEVERITY_ORDER = {
14
- critical: 0,
15
- high: 1,
16
- med: 2,
17
- low: 3
18
- };
19
- function assertValidWorkspaceDir(dir) {
20
- if (typeof dir !== "string" || dir.length === 0) {
21
- throw new Error(
22
- `audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`
23
- );
24
- }
25
- if (!path.isAbsolute(dir)) {
26
- throw new Error(
27
- `audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`
28
- );
29
- }
30
- if (dir.split(path.sep).includes("..")) {
31
- throw new Error(
32
- `audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`
33
- );
34
- }
35
- }
36
- async function initAuditWorkspace(workspaceDir) {
37
- assertValidWorkspaceDir(workspaceDir);
38
- await fs.mkdir(path.join(workspaceDir, "issues"), { recursive: true });
39
- await fs.mkdir(path.join(workspaceDir, "screenshots"), { recursive: true });
40
- const regPath = path.join(workspaceDir, "registry.json");
41
- try {
42
- await fs.access(regPath);
43
- } catch {
44
- const fresh = { schemaVersion: 1, findings: [], routes: {} };
45
- await fs.writeFile(regPath, JSON.stringify(fresh, null, 2));
46
- }
47
- }
48
- async function readAuditRegistry(workspaceDir) {
49
- assertValidWorkspaceDir(workspaceDir);
50
- const regPath = path.join(workspaceDir, "registry.json");
51
- const text = await fs.readFile(regPath, "utf8");
52
- const parsed = JSON.parse(text);
53
- if (!parsed || typeof parsed !== "object") {
54
- throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`);
55
- }
56
- const reg = parsed;
57
- if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") {
58
- throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
59
- }
60
- return reg;
61
- }
62
- async function writeAuditRegistry(workspaceDir, reg) {
63
- const regPath = path.join(workspaceDir, "registry.json");
64
- const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
65
- await fs.writeFile(tmpPath, JSON.stringify(reg, null, 2));
66
- await fs.rename(tmpPath, regPath);
67
- }
68
- var workspaceLocks = /* @__PURE__ */ new Map();
69
- async function withWorkspaceLock(workspaceDir, fn) {
70
- const key = path.resolve(workspaceDir);
71
- const prev = workspaceLocks.get(key) ?? Promise.resolve();
72
- let signalDone;
73
- const done = new Promise((resolve) => {
74
- signalDone = resolve;
75
- });
76
- workspaceLocks.set(key, done);
77
- try {
78
- await prev;
79
- return await fn();
80
- } finally {
81
- signalDone();
82
- if (workspaceLocks.get(key) === done) workspaceLocks.delete(key);
83
- }
84
- }
85
- function assertFindingShape(f, index) {
86
- const where = `audit-writer: findings[${index}]`;
87
- if (!UI_LENSES.includes(f.lens)) {
88
- throw new Error(
89
- `${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join("|")}`
90
- );
91
- }
92
- if (!UI_FINDING_SEVERITIES.includes(f.severity)) {
93
- throw new Error(
94
- `${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join("|")}`
95
- );
96
- }
97
- for (const field of ["title", "route", "observation", "impact", "suggestedFix"]) {
98
- const v = f[field];
99
- if (typeof v !== "string" || v.trim().length === 0) {
100
- throw new Error(`${where}: ${field} must be a non-empty string`);
101
- }
102
- }
103
- if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) {
104
- throw new Error(`${where}: screenshots must be a non-empty array`);
105
- }
106
- for (let i = 0; i < f.screenshots.length; i += 1) {
107
- const s = f.screenshots[i];
108
- if (!s || typeof s.path !== "string" || s.path.length === 0) {
109
- throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
110
- }
111
- if (s.path.split(/[/\\]/).includes("..")) {
112
- throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
113
- }
114
- }
115
- }
116
- function nextFindingId(reg) {
117
- let max = 0;
118
- for (const f of reg.findings) {
119
- if (typeof f.id === "number" && f.id > max) max = f.id;
120
- }
121
- return max + 1;
122
- }
123
- function slugifyTitle(title) {
124
- return slugify(title, "title");
125
- }
126
- function renderFinding(finding) {
127
- if (!finding.id) {
128
- throw new Error("audit-writer: cannot render a finding without an assigned id");
129
- }
130
- if (finding.screenshots.length === 0) {
131
- throw new Error(`audit-writer: finding #${finding.id} has no screenshots`);
132
- }
133
- const id = finding.id;
134
- const numStr = String(id).padStart(3, "0");
135
- const metaLines = [
136
- `> **Issue #${numStr}** \xB7 **Severity:** ${finding.severity} \xB7 **Lens:** ${finding.lens}`,
137
- `> **Route:** \`${finding.route}\`${finding.url ? ` \xB7 **URL:** ${finding.url}` : ""}`
138
- ];
139
- if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`);
140
- if (finding.selector) metaLines.push(`> **Selector:** \`${finding.selector}\``);
141
- if (finding.tags && finding.tags.length > 0) {
142
- metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\`${t}\``).join(", ")}`);
143
- }
144
- if (finding.similarTo && finding.similarTo.length > 0) {
145
- metaLines.push(
146
- `> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, "0")}`).join(", ")}`
147
- );
148
- }
149
- metaLines.push(`> _Generated: ${finding.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()}_`);
150
- const reproSteps = finding.reproSteps ?? `1. Open \`${finding.route}\`${finding.url ? ` (${finding.url})` : ""}
151
- 2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \`${finding.selector}\`)` : ""}.`;
152
- const evidence = finding.screenshots.map((s) => `![${path.basename(s.path)}](../${s.path})`).join("\n\n");
153
- return [
154
- `# [UI] ${finding.title}`,
155
- "",
156
- metaLines.join("\n"),
157
- "",
158
- "## Observation",
159
- finding.observation.trim(),
160
- "",
161
- "## Why it matters",
162
- finding.impact.trim(),
163
- "",
164
- "## Steps to reproduce",
165
- reproSteps,
166
- "",
167
- "## Suggested fix",
168
- finding.suggestedFix.trim(),
169
- "",
170
- "## Evidence",
171
- evidence,
172
- "",
173
- "---",
174
- `<sub>Generated by agent-runtime/ui-auditor \xB7 lens=\`${finding.lens}\` \xB7 severity=\`${finding.severity}\`</sub>`,
175
- ""
176
- ].join("\n");
177
- }
178
- async function appendFindings(workspaceDir, findings) {
179
- assertValidWorkspaceDir(workspaceDir);
180
- for (let i = 0; i < findings.length; i += 1) {
181
- const f = findings[i];
182
- if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`);
183
- assertFindingShape(f, i);
184
- }
185
- return withWorkspaceLock(workspaceDir, async () => {
186
- await initAuditWorkspace(workspaceDir);
187
- const reg = await readAuditRegistry(workspaceDir);
188
- const usedIds = /* @__PURE__ */ new Set();
189
- for (const f of reg.findings) {
190
- if (typeof f.id === "number") usedIds.add(f.id);
191
- }
192
- const written = [];
193
- const files = [];
194
- let nextId = nextFindingId(reg);
195
- for (const incoming of findings) {
196
- let id = incoming.id;
197
- if (id !== void 0) {
198
- if (usedIds.has(id)) {
199
- throw new Error(
200
- `audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`
201
- );
202
- }
203
- } else {
204
- id = nextId;
205
- while (usedIds.has(id)) id += 1;
206
- nextId = id + 1;
207
- }
208
- usedIds.add(id);
209
- const createdAt = incoming.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
210
- const persisted = { ...incoming, id, createdAt };
211
- const slug = slugifyTitle(persisted.title);
212
- const fileName = `${String(id).padStart(3, "0")}--${persisted.lens}--${slug}.md`;
213
- const filePathAbs = path.join(workspaceDir, "issues", fileName);
214
- const filePathRel = `issues/${fileName}`;
215
- await fs.writeFile(filePathAbs, renderFinding(persisted));
216
- reg.findings.push(persisted);
217
- written.push(persisted);
218
- files.push(filePathRel);
219
- }
220
- await writeAuditRegistry(workspaceDir, reg);
221
- return { written, files };
222
- });
223
- }
224
- async function registerCaptures(workspaceDir, options) {
225
- assertValidWorkspaceDir(workspaceDir);
226
- if (typeof options.route !== "string" || options.route.trim().length === 0) {
227
- throw new Error("audit-writer: registerCaptures: route must be a non-empty string");
228
- }
229
- return withWorkspaceLock(workspaceDir, async () => {
230
- await initAuditWorkspace(workspaceDir);
231
- const reg = await readAuditRegistry(workspaceDir);
232
- const slot = reg.routes[options.route] ?? { captures: [] };
233
- if (options.url) slot.url = options.url;
234
- slot.captures = [...slot.captures, ...options.captures];
235
- reg.routes[options.route] = slot;
236
- await writeAuditRegistry(workspaceDir, reg);
237
- });
238
- }
239
- function summarizeRegistry(reg) {
240
- const bySeverity = {
241
- critical: 0,
242
- high: 0,
243
- med: 0,
244
- low: 0
245
- };
246
- const byLens = {};
247
- const byRoute = {};
248
- for (const f of reg.findings) {
249
- bySeverity[f.severity] += 1;
250
- byLens[f.lens] = (byLens[f.lens] ?? 0) + 1;
251
- byRoute[f.route] = (byRoute[f.route] ?? 0) + 1;
252
- }
253
- return { total: reg.findings.length, bySeverity, byLens, byRoute };
254
- }
255
- async function writeAuditIndex(workspaceDir) {
256
- assertValidWorkspaceDir(workspaceDir);
257
- const reg = await readAuditRegistry(workspaceDir);
258
- const summary = summarizeRegistry(reg);
259
- const sorted = [...reg.findings].sort((a, b) => {
260
- const sa = SEVERITY_ORDER[a.severity];
261
- const sb = SEVERITY_ORDER[b.severity];
262
- if (sa !== sb) return sa - sb;
263
- return (a.id ?? 0) - (b.id ?? 0);
264
- });
265
- const lines = [];
266
- lines.push("# UI audit index", "");
267
- lines.push(`_Generated: ${(/* @__PURE__ */ new Date()).toISOString()}_`, "");
268
- lines.push(`**Total findings:** ${summary.total}`, "");
269
- lines.push("## By severity");
270
- for (const s of ["critical", "high", "med", "low"]) {
271
- const n = summary.bySeverity[s] ?? 0;
272
- if (n > 0) lines.push(`- **${s}** \u2014 ${n}`);
273
- }
274
- lines.push("", "## By lens");
275
- const lensEntries = Object.entries(summary.byLens).map(([k, v]) => [
276
- k,
277
- v ?? 0
278
- ]);
279
- for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) {
280
- lines.push(`- **${lens}** \u2014 ${n}`);
281
- }
282
- lines.push("", "## By route");
283
- const routeEntries = Object.entries(summary.byRoute).map(([k, v]) => [
284
- k,
285
- v ?? 0
286
- ]);
287
- for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) {
288
- lines.push(`- \`${route}\` \u2014 ${n}`);
289
- }
290
- lines.push("", "## Findings", "");
291
- for (const f of sorted) {
292
- if (typeof f.id !== "number") continue;
293
- const num = String(f.id).padStart(3, "0");
294
- const slug = slugifyTitle(f.title);
295
- const file = `issues/${num}--${f.lens}--${slug}.md`;
296
- lines.push(`- [#${num} \`${f.severity}\` \`${f.lens}\` \`${f.route}\` \u2014 ${f.title}](${file})`);
297
- }
298
- lines.push("");
299
- const out = path.join(workspaceDir, "index.md");
300
- const body = lines.join("\n");
301
- await fs.writeFile(out, body);
302
- return out;
303
- }
304
- export {
305
- appendFindings,
306
- initAuditWorkspace,
307
- readAuditRegistry,
308
- registerCaptures,
309
- summarizeRegistry,
310
- writeAuditIndex
311
- };
312
- //# sourceMappingURL=audit.js.map
package/dist/audit.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/audit/issue-writer.ts"],"sourcesContent":["/**\n * @experimental\n *\n * UI-audit issue writer — pure I/O. Takes a workspace dir + `UiFinding[]`\n * and emits:\n * - `<workspace>/issues/NNN--<lens>--<slug>.md` — one self-contained\n * GitHub-issue-ready Markdown per finding, with embedded screenshot\n * references.\n * - `<workspace>/registry.json` — finding index for dedup and audit\n * resume across iterations.\n * - `<workspace>/index.md` — human-readable rollup (severity / lens /\n * route counts plus a sorted finding list).\n *\n * The writer is deterministic, idempotent for `appendFindings()`, and\n * never invokes an LLM. It assigns the next monotonic id to a finding the\n * caller did not pre-id.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { slugify } from '../profiles/ui-auditor/slugify'\nimport {\n UI_FINDING_SEVERITIES,\n UI_LENSES,\n type UiFinding,\n type UiLens,\n} from '../profiles/ui-auditor/substrate'\n\n/** @experimental */\nexport interface AuditRegistry {\n schemaVersion: 1\n findings: UiFinding[]\n /** Route → URL + captures sidecar; preserved across runs. */\n routes: Record<string, { url?: string; captures: AuditRegistryCapture[] }>\n}\n\n/** @experimental */\nexport interface AuditRegistryCapture {\n file: string\n viewport?: string\n fullPage?: boolean\n elementSelector?: string\n capturedAt: string\n}\n\nconst SEVERITY_ORDER: Record<UiFinding['severity'], number> = {\n critical: 0,\n high: 1,\n med: 2,\n low: 3,\n}\n\n// Validate workspaceDir at every public entry point. The MCP tool checks\n// the same shape at the wire boundary, but the writer is independently\n// exported via `@tangle-network/agent-runtime/audit`, so direct callers\n// would otherwise bypass that defense. Absolute + no `..` segments matches\n// the MCP tool's contract — any path that the writer joins for I/O must be\n// rooted at a deterministic location chosen by the caller.\nfunction assertValidWorkspaceDir(dir: string): void {\n if (typeof dir !== 'string' || dir.length === 0) {\n throw new Error(\n `audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`,\n )\n }\n if (!path.isAbsolute(dir)) {\n throw new Error(\n `audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`,\n )\n }\n if (dir.split(path.sep).includes('..')) {\n throw new Error(\n `audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`,\n )\n }\n}\n\n/** @experimental */\nexport async function initAuditWorkspace(workspaceDir: string): Promise<void> {\n assertValidWorkspaceDir(workspaceDir)\n await fs.mkdir(path.join(workspaceDir, 'issues'), { recursive: true })\n await fs.mkdir(path.join(workspaceDir, 'screenshots'), { recursive: true })\n const regPath = path.join(workspaceDir, 'registry.json')\n try {\n await fs.access(regPath)\n } catch {\n const fresh: AuditRegistry = { schemaVersion: 1, findings: [], routes: {} }\n await fs.writeFile(regPath, JSON.stringify(fresh, null, 2))\n }\n}\n\n/** @experimental */\nexport async function readAuditRegistry(workspaceDir: string): Promise<AuditRegistry> {\n assertValidWorkspaceDir(workspaceDir)\n const regPath = path.join(workspaceDir, 'registry.json')\n const text = await fs.readFile(regPath, 'utf8')\n const parsed = JSON.parse(text) as unknown\n if (!parsed || typeof parsed !== 'object') {\n throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`)\n }\n const reg = parsed as Partial<AuditRegistry>\n if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== 'object') {\n throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`)\n }\n return reg as AuditRegistry\n}\n\nasync function writeAuditRegistry(workspaceDir: string, reg: AuditRegistry): Promise<void> {\n const regPath = path.join(workspaceDir, 'registry.json')\n // Write to a sibling temp file and rename. POSIX rename is atomic, so a\n // reader at any instant sees either the pre-write OR post-write registry,\n // never a half-written file. Prevents corruption on crash mid-write.\n const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n await fs.writeFile(tmpPath, JSON.stringify(reg, null, 2))\n await fs.rename(tmpPath, regPath)\n}\n\n// Per-workspace serialization. The writer reads registry.json, mutates it\n// in memory, then writes it back; concurrent calls would race on the\n// monotonic-id assignment and clobber each other's findings. Each\n// workspace key holds the Promise of the most-recently-enqueued operation;\n// new operations chain off it so they run strictly serially.\nconst workspaceLocks = new Map<string, Promise<unknown>>()\n\nasync function withWorkspaceLock<T>(workspaceDir: string, fn: () => Promise<T>): Promise<T> {\n const key = path.resolve(workspaceDir)\n const prev = workspaceLocks.get(key) ?? Promise.resolve()\n let signalDone!: () => void\n const done = new Promise<void>((resolve) => {\n signalDone = resolve\n })\n workspaceLocks.set(key, done)\n try {\n await prev\n return await fn()\n } finally {\n signalDone()\n // Only delete if no later caller has overwritten the slot — preserves\n // the chain for any concurrent callers still waiting.\n if (workspaceLocks.get(key) === done) workspaceLocks.delete(key)\n }\n}\n\nfunction assertFindingShape(f: UiFinding, index: number): void {\n const where = `audit-writer: findings[${index}]`\n if (!UI_LENSES.includes(f.lens)) {\n throw new Error(\n `${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join('|')}`,\n )\n }\n if (!UI_FINDING_SEVERITIES.includes(f.severity)) {\n throw new Error(\n `${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join('|')}`,\n )\n }\n for (const field of ['title', 'route', 'observation', 'impact', 'suggestedFix'] as const) {\n const v = f[field]\n if (typeof v !== 'string' || v.trim().length === 0) {\n throw new Error(`${where}: ${field} must be a non-empty string`)\n }\n }\n if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) {\n throw new Error(`${where}: screenshots must be a non-empty array`)\n }\n for (let i = 0; i < f.screenshots.length; i += 1) {\n const s = f.screenshots[i]\n if (!s || typeof s.path !== 'string' || s.path.length === 0) {\n throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`)\n }\n // Defense-in-depth: screenshot paths are written verbatim into Markdown\n // and the registry; a downstream consumer that joins workspaceDir with\n // them would otherwise be exposed to traversal-read. Reject `..`\n // segments at the validation boundary.\n if (s.path.split(/[/\\\\]/).includes('..')) {\n throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`)\n }\n }\n}\n\nfunction nextFindingId(reg: AuditRegistry): number {\n let max = 0\n for (const f of reg.findings) {\n if (typeof f.id === 'number' && f.id > max) max = f.id\n }\n return max + 1\n}\n\nfunction slugifyTitle(title: string): string {\n return slugify(title, 'title')\n}\n\nfunction renderFinding(finding: UiFinding): string {\n if (!finding.id) {\n throw new Error('audit-writer: cannot render a finding without an assigned id')\n }\n if (finding.screenshots.length === 0) {\n throw new Error(`audit-writer: finding #${finding.id} has no screenshots`)\n }\n const id = finding.id\n const numStr = String(id).padStart(3, '0')\n\n const metaLines: string[] = [\n `> **Issue #${numStr}** · **Severity:** ${finding.severity} · **Lens:** ${finding.lens}`,\n `> **Route:** \\`${finding.route}\\`${finding.url ? ` · **URL:** ${finding.url}` : ''}`,\n ]\n if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`)\n if (finding.selector) metaLines.push(`> **Selector:** \\`${finding.selector}\\``)\n if (finding.tags && finding.tags.length > 0) {\n metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\\`${t}\\``).join(', ')}`)\n }\n if (finding.similarTo && finding.similarTo.length > 0) {\n metaLines.push(\n `> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, '0')}`).join(', ')}`,\n )\n }\n metaLines.push(`> _Generated: ${finding.createdAt ?? new Date().toISOString()}_`)\n\n const reproSteps =\n finding.reproSteps ??\n `1. Open \\`${finding.route}\\`${finding.url ? ` (${finding.url})` : ''}\\n2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \\`${finding.selector}\\`)` : ''}.`\n\n // Screenshot paths are workspace-relative; the issue file sits in\n // `<workspace>/issues/`, so we render `../<path>`.\n const evidence = finding.screenshots\n .map((s) => `![${path.basename(s.path)}](../${s.path})`)\n .join('\\n\\n')\n\n return [\n `# [UI] ${finding.title}`,\n '',\n metaLines.join('\\n'),\n '',\n '## Observation',\n finding.observation.trim(),\n '',\n '## Why it matters',\n finding.impact.trim(),\n '',\n '## Steps to reproduce',\n reproSteps,\n '',\n '## Suggested fix',\n finding.suggestedFix.trim(),\n '',\n '## Evidence',\n evidence,\n '',\n '---',\n `<sub>Generated by agent-runtime/ui-auditor · lens=\\`${finding.lens}\\` · severity=\\`${finding.severity}\\`</sub>`,\n '',\n ].join('\\n')\n}\n\n/** @experimental */\nexport interface AppendFindingsResult {\n /** Findings with id + createdAt assigned, in input order. */\n written: UiFinding[]\n /** Workspace-relative path to each issue Markdown file, in input order. */\n files: string[]\n}\n\n/**\n * Append findings to a workspace, writing one Markdown file per finding\n * and updating registry.json. Assigns monotonically increasing ids to\n * findings that arrived without one.\n *\n * Findings already carrying an id that collides with the registry are\n * rejected — callers must either freshly mint findings (id undefined) or\n * use a separate update path. This protects against accidental overwrite.\n *\n * @experimental\n */\nexport async function appendFindings(\n workspaceDir: string,\n findings: readonly UiFinding[],\n): Promise<AppendFindingsResult> {\n assertValidWorkspaceDir(workspaceDir)\n // Validate every finding BEFORE acquiring the lock so callers see a fast\n // input-shape error without blocking concurrent writers. Validation is\n // the only defense against path traversal via crafted `lens` values\n // (the filename interpolates lens directly) and against malformed\n // Markdown from missing required fields — TypeScript types are erased\n // at runtime, so this boundary cannot trust its callers.\n for (let i = 0; i < findings.length; i += 1) {\n const f = findings[i]\n if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`)\n assertFindingShape(f, i)\n }\n\n return withWorkspaceLock(workspaceDir, async () => {\n await initAuditWorkspace(workspaceDir)\n const reg = await readAuditRegistry(workspaceDir)\n const usedIds = new Set<number>()\n for (const f of reg.findings) {\n if (typeof f.id === 'number') usedIds.add(f.id)\n }\n\n const written: UiFinding[] = []\n const files: string[] = []\n let nextId = nextFindingId(reg)\n\n for (const incoming of findings) {\n let id = incoming.id\n if (id !== undefined) {\n if (usedIds.has(id)) {\n throw new Error(\n `audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`,\n )\n }\n } else {\n id = nextId\n while (usedIds.has(id)) id += 1\n nextId = id + 1\n }\n usedIds.add(id)\n\n const createdAt = incoming.createdAt ?? new Date().toISOString()\n const persisted: UiFinding = { ...incoming, id, createdAt }\n\n const slug = slugifyTitle(persisted.title)\n const fileName = `${String(id).padStart(3, '0')}--${persisted.lens}--${slug}.md`\n const filePathAbs = path.join(workspaceDir, 'issues', fileName)\n const filePathRel = `issues/${fileName}`\n\n await fs.writeFile(filePathAbs, renderFinding(persisted))\n reg.findings.push(persisted)\n written.push(persisted)\n files.push(filePathRel)\n }\n\n await writeAuditRegistry(workspaceDir, reg)\n return { written, files }\n })\n}\n\n/** @experimental */\nexport interface RegisterCapturesOptions {\n route: string\n url?: string\n captures: readonly AuditRegistryCapture[]\n}\n\n/**\n * Record screenshots taken for a route in the registry, without filing a\n * finding. Useful when the auditor wants to remember which captures\n * exist for resume / dedup purposes.\n *\n * @experimental\n */\nexport async function registerCaptures(\n workspaceDir: string,\n options: RegisterCapturesOptions,\n): Promise<void> {\n assertValidWorkspaceDir(workspaceDir)\n if (typeof options.route !== 'string' || options.route.trim().length === 0) {\n throw new Error('audit-writer: registerCaptures: route must be a non-empty string')\n }\n return withWorkspaceLock(workspaceDir, async () => {\n await initAuditWorkspace(workspaceDir)\n const reg = await readAuditRegistry(workspaceDir)\n const slot = reg.routes[options.route] ?? { captures: [] }\n if (options.url) slot.url = options.url\n slot.captures = [...slot.captures, ...options.captures]\n reg.routes[options.route] = slot\n await writeAuditRegistry(workspaceDir, reg)\n })\n}\n\n/** @experimental */\nexport interface AuditIndex {\n /** Total findings in the workspace. */\n total: number\n bySeverity: Record<UiFinding['severity'], number>\n byLens: Partial<Record<UiLens, number>>\n byRoute: Record<string, number>\n}\n\n/** @experimental */\nexport function summarizeRegistry(reg: AuditRegistry): AuditIndex {\n const bySeverity: Record<UiFinding['severity'], number> = {\n critical: 0,\n high: 0,\n med: 0,\n low: 0,\n }\n const byLens: Partial<Record<UiLens, number>> = {}\n const byRoute: Record<string, number> = {}\n for (const f of reg.findings) {\n bySeverity[f.severity] += 1\n byLens[f.lens] = (byLens[f.lens] ?? 0) + 1\n byRoute[f.route] = (byRoute[f.route] ?? 0) + 1\n }\n return { total: reg.findings.length, bySeverity, byLens, byRoute }\n}\n\n/**\n * Regenerate `<workspace>/index.md` from registry.json.\n *\n * @experimental\n */\nexport async function writeAuditIndex(workspaceDir: string): Promise<string> {\n assertValidWorkspaceDir(workspaceDir)\n const reg = await readAuditRegistry(workspaceDir)\n const summary = summarizeRegistry(reg)\n\n const sorted = [...reg.findings].sort((a, b) => {\n const sa = SEVERITY_ORDER[a.severity]\n const sb = SEVERITY_ORDER[b.severity]\n if (sa !== sb) return sa - sb\n return (a.id ?? 0) - (b.id ?? 0)\n })\n\n const lines: string[] = []\n lines.push('# UI audit index', '')\n lines.push(`_Generated: ${new Date().toISOString()}_`, '')\n lines.push(`**Total findings:** ${summary.total}`, '')\n lines.push('## By severity')\n for (const s of ['critical', 'high', 'med', 'low'] as const) {\n const n = summary.bySeverity[s] ?? 0\n if (n > 0) lines.push(`- **${s}** — ${n}`)\n }\n lines.push('', '## By lens')\n const lensEntries: [string, number][] = Object.entries(summary.byLens).map(([k, v]) => [\n k,\n v ?? 0,\n ])\n for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) {\n lines.push(`- **${lens}** — ${n}`)\n }\n lines.push('', '## By route')\n const routeEntries: [string, number][] = Object.entries(summary.byRoute).map(([k, v]) => [\n k,\n v ?? 0,\n ])\n for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) {\n lines.push(`- \\`${route}\\` — ${n}`)\n }\n lines.push('', '## Findings', '')\n for (const f of sorted) {\n if (typeof f.id !== 'number') continue\n const num = String(f.id).padStart(3, '0')\n const slug = slugifyTitle(f.title)\n const file = `issues/${num}--${f.lens}--${slug}.md`\n lines.push(`- [#${num} \\`${f.severity}\\` \\`${f.lens}\\` \\`${f.route}\\` — ${f.title}](${file})`)\n }\n lines.push('')\n\n const out = path.join(workspaceDir, 'index.md')\n const body = lines.join('\\n')\n await fs.writeFile(out, body)\n return out\n}\n"],"mappings":";;;;;;;;;;AAkBA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AA0BjB,IAAM,iBAAwD;AAAA,EAC5D,UAAU;AAAA,EACV,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AACP;AAQA,SAAS,wBAAwB,KAAmB;AAClD,MAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C,UAAM,IAAI;AAAA,MACR,8DAA8D,KAAK,UAAU,GAAG,CAAC;AAAA,IACnF;AAAA,EACF;AACA,MAAI,CAAC,KAAK,WAAW,GAAG,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,4DAA4D,KAAK,UAAU,GAAG,CAAC;AAAA,IACjF;AAAA,EACF;AACA,MAAI,IAAI,MAAM,KAAK,GAAG,EAAE,SAAS,IAAI,GAAG;AACtC,UAAM,IAAI;AAAA,MACR,kEAAkE,KAAK,UAAU,GAAG,CAAC;AAAA,IACvF;AAAA,EACF;AACF;AAGA,eAAsB,mBAAmB,cAAqC;AAC5E,0BAAwB,YAAY;AACpC,QAAM,GAAG,MAAM,KAAK,KAAK,cAAc,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACrE,QAAM,GAAG,MAAM,KAAK,KAAK,cAAc,aAAa,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1E,QAAM,UAAU,KAAK,KAAK,cAAc,eAAe;AACvD,MAAI;AACF,UAAM,GAAG,OAAO,OAAO;AAAA,EACzB,QAAQ;AACN,UAAM,QAAuB,EAAE,eAAe,GAAG,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE;AAC1E,UAAM,GAAG,UAAU,SAAS,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,EAC5D;AACF;AAGA,eAAsB,kBAAkB,cAA8C;AACpF,0BAAwB,YAAY;AACpC,QAAM,UAAU,KAAK,KAAK,cAAc,eAAe;AACvD,QAAM,OAAO,MAAM,GAAG,SAAS,SAAS,MAAM;AAC9C,QAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,UAAM,IAAI,MAAM,kCAAkC,OAAO,uBAAuB;AAAA,EAClF;AACA,QAAM,MAAM;AACZ,MAAI,IAAI,kBAAkB,KAAK,CAAC,MAAM,QAAQ,IAAI,QAAQ,KAAK,OAAO,IAAI,WAAW,UAAU;AAC7F,UAAM,IAAI,MAAM,kCAAkC,OAAO,4BAA4B;AAAA,EACvF;AACA,SAAO;AACT;AAEA,eAAe,mBAAmB,cAAsB,KAAmC;AACzF,QAAM,UAAU,KAAK,KAAK,cAAc,eAAe;AAIvD,QAAM,UAAU,GAAG,OAAO,QAAQ,QAAQ,GAAG,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAClH,QAAM,GAAG,UAAU,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AACxD,QAAM,GAAG,OAAO,SAAS,OAAO;AAClC;AAOA,IAAM,iBAAiB,oBAAI,IAA8B;AAEzD,eAAe,kBAAqB,cAAsB,IAAkC;AAC1F,QAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,QAAM,OAAO,eAAe,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACxD,MAAI;AACJ,QAAM,OAAO,IAAI,QAAc,CAAC,YAAY;AAC1C,iBAAa;AAAA,EACf,CAAC;AACD,iBAAe,IAAI,KAAK,IAAI;AAC5B,MAAI;AACF,UAAM;AACN,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA,eAAW;AAGX,QAAI,eAAe,IAAI,GAAG,MAAM,KAAM,gBAAe,OAAO,GAAG;AAAA,EACjE;AACF;AAEA,SAAS,mBAAmB,GAAc,OAAqB;AAC7D,QAAM,QAAQ,0BAA0B,KAAK;AAC7C,MAAI,CAAC,UAAU,SAAS,EAAE,IAAI,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR,GAAG,KAAK,kBAAkB,KAAK,UAAU,EAAE,IAAI,CAAC,YAAY,UAAU,KAAK,GAAG,CAAC;AAAA,IACjF;AAAA,EACF;AACA,MAAI,CAAC,sBAAsB,SAAS,EAAE,QAAQ,GAAG;AAC/C,UAAM,IAAI;AAAA,MACR,GAAG,KAAK,sBAAsB,KAAK,UAAU,EAAE,QAAQ,CAAC,YAAY,sBAAsB,KAAK,GAAG,CAAC;AAAA,IACrG;AAAA,EACF;AACA,aAAW,SAAS,CAAC,SAAS,SAAS,eAAe,UAAU,cAAc,GAAY;AACxF,UAAM,IAAI,EAAE,KAAK;AACjB,QAAI,OAAO,MAAM,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG;AAClD,YAAM,IAAI,MAAM,GAAG,KAAK,KAAK,KAAK,6BAA6B;AAAA,IACjE;AAAA,EACF;AACA,MAAI,CAAC,MAAM,QAAQ,EAAE,WAAW,KAAK,EAAE,YAAY,WAAW,GAAG;AAC/D,UAAM,IAAI,MAAM,GAAG,KAAK,yCAAyC;AAAA,EACnE;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,YAAY,QAAQ,KAAK,GAAG;AAChD,UAAM,IAAI,EAAE,YAAY,CAAC;AACzB,QAAI,CAAC,KAAK,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,WAAW,GAAG;AAC3D,YAAM,IAAI,MAAM,GAAG,KAAK,gBAAgB,CAAC,mCAAmC;AAAA,IAC9E;AAKA,QAAI,EAAE,KAAK,MAAM,OAAO,EAAE,SAAS,IAAI,GAAG;AACxC,YAAM,IAAI,MAAM,GAAG,KAAK,gBAAgB,CAAC,uCAAuC;AAAA,IAClF;AAAA,EACF;AACF;AAEA,SAAS,cAAc,KAA4B;AACjD,MAAI,MAAM;AACV,aAAW,KAAK,IAAI,UAAU;AAC5B,QAAI,OAAO,EAAE,OAAO,YAAY,EAAE,KAAK,IAAK,OAAM,EAAE;AAAA,EACtD;AACA,SAAO,MAAM;AACf;AAEA,SAAS,aAAa,OAAuB;AAC3C,SAAO,QAAQ,OAAO,OAAO;AAC/B;AAEA,SAAS,cAAc,SAA4B;AACjD,MAAI,CAAC,QAAQ,IAAI;AACf,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AACA,MAAI,QAAQ,YAAY,WAAW,GAAG;AACpC,UAAM,IAAI,MAAM,0BAA0B,QAAQ,EAAE,qBAAqB;AAAA,EAC3E;AACA,QAAM,KAAK,QAAQ;AACnB,QAAM,SAAS,OAAO,EAAE,EAAE,SAAS,GAAG,GAAG;AAEzC,QAAM,YAAsB;AAAA,IAC1B,cAAc,MAAM,yBAAsB,QAAQ,QAAQ,mBAAgB,QAAQ,IAAI;AAAA,IACtF,kBAAkB,QAAQ,KAAK,KAAK,QAAQ,MAAM,kBAAe,QAAQ,GAAG,KAAK,EAAE;AAAA,EACrF;AACA,MAAI,QAAQ,SAAU,WAAU,KAAK,mBAAmB,QAAQ,QAAQ,EAAE;AAC1E,MAAI,QAAQ,SAAU,WAAU,KAAK,qBAAqB,QAAQ,QAAQ,IAAI;AAC9E,MAAI,QAAQ,QAAQ,QAAQ,KAAK,SAAS,GAAG;AAC3C,cAAU,KAAK,eAAe,QAAQ,KAAK,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,EAChF;AACA,MAAI,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;AACrD,cAAU;AAAA,MACR,8BAA8B,QAAQ,UAAU,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,IACzG;AAAA,EACF;AACA,YAAU,KAAK,iBAAiB,QAAQ,cAAa,oBAAI,KAAK,GAAE,YAAY,CAAC,GAAG;AAEhF,QAAM,aACJ,QAAQ,cACR,aAAa,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK,QAAQ,GAAG,MAAM,EAAE;AAAA,6CAAgD,QAAQ,WAAW,iBAAiB,QAAQ,QAAQ,QAAQ,EAAE;AAIrL,QAAM,WAAW,QAAQ,YACtB,IAAI,CAAC,MAAM,KAAK,KAAK,SAAS,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,EACtD,KAAK,MAAM;AAEd,SAAO;AAAA,IACL,UAAU,QAAQ,KAAK;AAAA,IACvB;AAAA,IACA,UAAU,KAAK,IAAI;AAAA,IACnB;AAAA,IACA;AAAA,IACA,QAAQ,YAAY,KAAK;AAAA,IACzB;AAAA,IACA;AAAA,IACA,QAAQ,OAAO,KAAK;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,aAAa,KAAK;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,0DAAuD,QAAQ,IAAI,sBAAmB,QAAQ,QAAQ;AAAA,IACtG;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAqBA,eAAsB,eACpB,cACA,UAC+B;AAC/B,0BAAwB,YAAY;AAOpC,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK,GAAG;AAC3C,UAAM,IAAI,SAAS,CAAC;AACpB,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,0BAA0B,CAAC,gBAAgB;AACnE,uBAAmB,GAAG,CAAC;AAAA,EACzB;AAEA,SAAO,kBAAkB,cAAc,YAAY;AACjD,UAAM,mBAAmB,YAAY;AACrC,UAAM,MAAM,MAAM,kBAAkB,YAAY;AAChD,UAAM,UAAU,oBAAI,IAAY;AAChC,eAAW,KAAK,IAAI,UAAU;AAC5B,UAAI,OAAO,EAAE,OAAO,SAAU,SAAQ,IAAI,EAAE,EAAE;AAAA,IAChD;AAEA,UAAM,UAAuB,CAAC;AAC9B,UAAM,QAAkB,CAAC;AACzB,QAAI,SAAS,cAAc,GAAG;AAE9B,eAAW,YAAY,UAAU;AAC/B,UAAI,KAAK,SAAS;AAClB,UAAI,OAAO,QAAW;AACpB,YAAI,QAAQ,IAAI,EAAE,GAAG;AACnB,gBAAM,IAAI;AAAA,YACR,qCAAqC,EAAE,WAAW,KAAK,UAAU,SAAS,KAAK,CAAC;AAAA,UAClF;AAAA,QACF;AAAA,MACF,OAAO;AACL,aAAK;AACL,eAAO,QAAQ,IAAI,EAAE,EAAG,OAAM;AAC9B,iBAAS,KAAK;AAAA,MAChB;AACA,cAAQ,IAAI,EAAE;AAEd,YAAM,YAAY,SAAS,cAAa,oBAAI,KAAK,GAAE,YAAY;AAC/D,YAAM,YAAuB,EAAE,GAAG,UAAU,IAAI,UAAU;AAE1D,YAAM,OAAO,aAAa,UAAU,KAAK;AACzC,YAAM,WAAW,GAAG,OAAO,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,KAAK,UAAU,IAAI,KAAK,IAAI;AAC3E,YAAM,cAAc,KAAK,KAAK,cAAc,UAAU,QAAQ;AAC9D,YAAM,cAAc,UAAU,QAAQ;AAEtC,YAAM,GAAG,UAAU,aAAa,cAAc,SAAS,CAAC;AACxD,UAAI,SAAS,KAAK,SAAS;AAC3B,cAAQ,KAAK,SAAS;AACtB,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,UAAM,mBAAmB,cAAc,GAAG;AAC1C,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B,CAAC;AACH;AAgBA,eAAsB,iBACpB,cACA,SACe;AACf,0BAAwB,YAAY;AACpC,MAAI,OAAO,QAAQ,UAAU,YAAY,QAAQ,MAAM,KAAK,EAAE,WAAW,GAAG;AAC1E,UAAM,IAAI,MAAM,kEAAkE;AAAA,EACpF;AACA,SAAO,kBAAkB,cAAc,YAAY;AACjD,UAAM,mBAAmB,YAAY;AACrC,UAAM,MAAM,MAAM,kBAAkB,YAAY;AAChD,UAAM,OAAO,IAAI,OAAO,QAAQ,KAAK,KAAK,EAAE,UAAU,CAAC,EAAE;AACzD,QAAI,QAAQ,IAAK,MAAK,MAAM,QAAQ;AACpC,SAAK,WAAW,CAAC,GAAG,KAAK,UAAU,GAAG,QAAQ,QAAQ;AACtD,QAAI,OAAO,QAAQ,KAAK,IAAI;AAC5B,UAAM,mBAAmB,cAAc,GAAG;AAAA,EAC5C,CAAC;AACH;AAYO,SAAS,kBAAkB,KAAgC;AAChE,QAAM,aAAoD;AAAA,IACxD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,QAAM,SAA0C,CAAC;AACjD,QAAM,UAAkC,CAAC;AACzC,aAAW,KAAK,IAAI,UAAU;AAC5B,eAAW,EAAE,QAAQ,KAAK;AAC1B,WAAO,EAAE,IAAI,KAAK,OAAO,EAAE,IAAI,KAAK,KAAK;AACzC,YAAQ,EAAE,KAAK,KAAK,QAAQ,EAAE,KAAK,KAAK,KAAK;AAAA,EAC/C;AACA,SAAO,EAAE,OAAO,IAAI,SAAS,QAAQ,YAAY,QAAQ,QAAQ;AACnE;AAOA,eAAsB,gBAAgB,cAAuC;AAC3E,0BAAwB,YAAY;AACpC,QAAM,MAAM,MAAM,kBAAkB,YAAY;AAChD,QAAM,UAAU,kBAAkB,GAAG;AAErC,QAAM,SAAS,CAAC,GAAG,IAAI,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM;AAC9C,UAAM,KAAK,eAAe,EAAE,QAAQ;AACpC,UAAM,KAAK,eAAe,EAAE,QAAQ;AACpC,QAAI,OAAO,GAAI,QAAO,KAAK;AAC3B,YAAQ,EAAE,MAAM,MAAM,EAAE,MAAM;AAAA,EAChC,CAAC;AAED,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,oBAAoB,EAAE;AACjC,QAAM,KAAK,gBAAe,oBAAI,KAAK,GAAE,YAAY,CAAC,KAAK,EAAE;AACzD,QAAM,KAAK,uBAAuB,QAAQ,KAAK,IAAI,EAAE;AACrD,QAAM,KAAK,gBAAgB;AAC3B,aAAW,KAAK,CAAC,YAAY,QAAQ,OAAO,KAAK,GAAY;AAC3D,UAAM,IAAI,QAAQ,WAAW,CAAC,KAAK;AACnC,QAAI,IAAI,EAAG,OAAM,KAAK,OAAO,CAAC,aAAQ,CAAC,EAAE;AAAA,EAC3C;AACA,QAAM,KAAK,IAAI,YAAY;AAC3B,QAAM,cAAkC,OAAO,QAAQ,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM;AAAA,IACrF;AAAA,IACA,KAAK;AAAA,EACP,CAAC;AACD,aAAW,CAAC,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG;AAC/D,UAAM,KAAK,OAAO,IAAI,aAAQ,CAAC,EAAE;AAAA,EACnC;AACA,QAAM,KAAK,IAAI,aAAa;AAC5B,QAAM,eAAmC,OAAO,QAAQ,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM;AAAA,IACvF;AAAA,IACA,KAAK;AAAA,EACP,CAAC;AACD,aAAW,CAAC,OAAO,CAAC,KAAK,aAAa,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG;AACjE,UAAM,KAAK,OAAO,KAAK,aAAQ,CAAC,EAAE;AAAA,EACpC;AACA,QAAM,KAAK,IAAI,eAAe,EAAE;AAChC,aAAW,KAAK,QAAQ;AACtB,QAAI,OAAO,EAAE,OAAO,SAAU;AAC9B,UAAM,MAAM,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,GAAG;AACxC,UAAM,OAAO,aAAa,EAAE,KAAK;AACjC,UAAM,OAAO,UAAU,GAAG,KAAK,EAAE,IAAI,KAAK,IAAI;AAC9C,UAAM,KAAK,OAAO,GAAG,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,QAAQ,EAAE,KAAK,aAAQ,EAAE,KAAK,KAAK,IAAI,GAAG;AAAA,EAC/F;AACA,QAAM,KAAK,EAAE;AAEb,QAAM,MAAM,KAAK,KAAK,cAAc,UAAU;AAC9C,QAAM,OAAO,MAAM,KAAK,IAAI;AAC5B,QAAM,GAAG,UAAU,KAAK,IAAI;AAC5B,SAAO;AACT;","names":[]}