@sightmap/sightmap 0.1.0 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -1,248 +1,11 @@
1
- /**
2
- * File-level memory entries surfaced whenever any definition from this file is active.
3
- */
4
- type Memory = string[];
5
- /**
6
- * Freeform notes surfaced to agents at runtime in the [Guide] section.
7
- */
8
- type Memory1 = string[];
9
- /**
10
- * A CSS selector, or a list of alternative selectors tried in order.
11
- */
12
- type Selector = string | [string, ...string[]];
13
- /**
14
- * A sightmap file: one YAML document under .sightmap/ describing views, components, requests, and memory entries. See https://github.com/sightmap/spec/blob/main/spec/v1/schema.md for the human-readable reference.
15
- */
16
- interface SightmapV1 {
17
- /**
18
- * Must be 1 for files that conform to this version of the spec.
19
- */
20
- version: 1;
21
- memory?: Memory;
22
- views?: View[];
23
- /**
24
- * Global components — matched against every view.
25
- */
26
- components?: Component[];
27
- /**
28
- * Global requests — matched against every view.
29
- */
30
- requests?: Request[];
31
- }
32
- interface View {
33
- name: string;
34
- /**
35
- * Glob pattern matched against the URL pathname. * matches one segment, ** matches any depth.
36
- */
37
- route: string;
38
- description?: string;
39
- source?: string;
40
- memory?: Memory1;
41
- components?: Component[];
42
- requests?: Request[];
43
- }
44
- interface Component {
45
- name: string;
46
- selector: Selector;
47
- source?: string;
48
- description?: string;
49
- memory?: Memory1;
50
- /**
51
- * Nested components. Selectors are scoped to the parent's matched subtree.
52
- */
53
- children?: Component[];
54
- }
55
- interface Request {
56
- name: string;
57
- /**
58
- * Glob pattern. Express-style :param segments normalize to *.
59
- */
60
- route: string;
61
- /**
62
- * HTTP method filter. Case-insensitive; commonly GET, POST, PUT, PATCH, DELETE.
63
- */
64
- method?: string;
65
- description?: string;
66
- source?: string;
67
- request?: Payload;
68
- response?: Payload;
69
- headers?: string[];
70
- memory?: Memory1;
71
- }
72
- interface Payload {
73
- fields?: Field[];
74
- }
75
- interface Field {
76
- name: string;
77
- type?: string;
78
- description?: string;
79
- }
80
-
81
- type Severity = "error" | "warning" | "info";
82
- interface Diagnostic {
83
- severity: Severity;
84
- code: string;
85
- message: string;
86
- file?: string;
87
- path?: string;
88
- loc?: {
89
- line: number;
90
- column: number;
91
- };
92
- source?: string;
93
- }
94
- declare const PARSE_ERROR = "parse-error";
95
- declare const SCHEMA_VALIDATION_FAILED = "schema-validation-failed";
96
- declare const UNKNOWN_VERSION = "unknown-version";
97
- declare const MERGE_COLLISION_VIEW = "merge-collision-view";
98
- declare const MERGE_COLLISION_COMPONENT = "merge-collision-component";
99
- declare const DUPLICATE_VIEW_NAME = "duplicate-view-name";
100
- declare const DUPLICATE_ROUTE = "duplicate-route";
101
- declare const ROUTE_SHADOWING = "route-shadowing";
102
- declare const UNKNOWN_SOURCE = "unknown-source";
103
- declare const SELECTOR_SYNTAX = "selector-syntax";
104
-
105
- /**
106
- * A single-file fragment after parsing and selector normalization.
107
- * Branded to prevent querying an unmerged fragment.
108
- */
109
- interface SightmapFragment extends SightmapV1 {
110
- readonly __brand: "SightmapFragment";
111
- readonly __sourceFile?: string;
112
- }
113
- /**
114
- * A merged, queryable sightmap produced by `merge()` or `loadDirectory()`.
115
- */
116
- interface Sightmap {
117
- readonly version: 1;
118
- readonly views: readonly View[];
119
- readonly globalComponents: readonly Component[];
120
- readonly globalRequests: readonly Request[];
121
- readonly fileMemory: readonly {
122
- memory: string[];
123
- sourceFile: string;
124
- }[];
125
- /**
126
- * Diagnostics produced during merge (e.g. collisions, duplicate names across files).
127
- * Lint diagnostics are returned separately by `lint()`.
128
- */
129
- readonly diagnostics: readonly Diagnostic[];
130
- readonly __brand: "Sightmap";
131
- }
132
- interface ResolvedView {
133
- name: string;
134
- route: string;
135
- source?: string;
136
- description?: string;
137
- memory: string[];
138
- definedIn: {
139
- file: string;
140
- line?: number;
141
- };
142
- }
143
- interface ResolvedComponent {
144
- name: string;
145
- selector: string[];
146
- source?: string;
147
- description?: string;
148
- memory: string[];
149
- parentChain: string[];
150
- scope: "global" | "view-scoped";
151
- /** Present iff `scope === "view-scoped"`. */
152
- scopedToView?: string;
153
- definedIn: {
154
- file: string;
155
- line?: number;
156
- };
157
- }
158
- interface ResolvedRequest {
159
- name: string;
160
- route: string;
161
- method?: string;
162
- source?: string;
163
- description?: string;
164
- request?: {
165
- fields: readonly Field[];
166
- };
167
- response?: {
168
- fields: readonly Field[];
169
- };
170
- headers?: readonly string[];
171
- memory: string[];
172
- definedIn: {
173
- file: string;
174
- line?: number;
175
- };
176
- }
177
- interface MatchResult {
178
- view: ResolvedView | null;
179
- components: ResolvedComponent[];
180
- requests: ResolvedRequest[];
181
- memory: string[];
182
- }
183
- type ExplainMatchedAs = "name" | "path" | "name-and-path";
184
- /**
185
- * Discriminated union: narrowing on `type` automatically narrows `entry`.
186
- * if (hit.type === "view") { hit.entry.route; } // no cast needed
187
- */
188
- type ExplainHit = {
189
- type: "view";
190
- matchedAs: ExplainMatchedAs;
191
- entry: ResolvedView;
192
- } | {
193
- type: "component";
194
- matchedAs: ExplainMatchedAs;
195
- entry: ResolvedComponent;
196
- } | {
197
- type: "request";
198
- matchedAs: ExplainMatchedAs;
199
- entry: ResolvedRequest;
200
- };
201
- interface ExplainResult {
202
- query: string;
203
- hits: ExplainHit[];
204
- }
205
- /** Result returned by validate() — never throws. */
206
- type ValidateResult = {
207
- ok: true;
208
- value: SightmapFragment;
209
- } | {
210
- ok: false;
211
- diagnostics: readonly Diagnostic[];
212
- };
213
-
214
- interface ParseOptions {
215
- /** Optional source file path; recorded on the fragment for canonical-order merging. */
216
- sourceFile?: string;
217
- }
218
- /**
219
- * Parse a single sightmap file (YAML string or pre-parsed object).
220
- * Throws on parse error, missing version, or version mismatch.
221
- * Normalizes `selector: string` to `selector: [string]` everywhere.
222
- */
223
- declare function parse(input: string | object, opts?: ParseOptions): SightmapFragment;
1
+ import { V as ValidateResult, S as Sightmap, D as Diagnostic, a as SightmapV1 } from './browser-ChD_xQt8.js';
2
+ export { C as Component, b as DUPLICATE_ROUTE, c as DUPLICATE_VIEW_NAME, E as ExplainHit, d as ExplainMatchedAs, e as ExplainResult, F as Field, M as MERGE_COLLISION_COMPONENT, f as MERGE_COLLISION_VIEW, g as MatchResult, P as PARSE_ERROR, R as ROUTE_SHADOWING, h as Request, i as ResolvedComponent, j as ResolvedRequest, k as ResolvedView, l as SCHEMA_VALIDATION_FAILED, m as SELECTOR_SYNTAX, n as Severity, o as SightmapFragment, U as UNKNOWN_SOURCE, p as UNKNOWN_VERSION, q as View, r as explain, s as match, t as merge, u as parse } from './browser-ChD_xQt8.js';
224
3
 
225
4
  interface ValidateOptions {
226
5
  sourceFile?: string;
227
6
  }
228
7
  declare function validate(input: string | object, opts?: ValidateOptions): ValidateResult;
229
8
 
230
- /**
231
- * Merge fragments into a queryable Sightmap.
232
- *
233
- * Canonical order: fragments are sorted by `__sourceFile` (code-point order; locale-independent
234
- * for cross-environment determinism). Fragments without a `__sourceFile` sort first, and ES2019
235
- * sort stability preserves their input order. Within each fragment, declaration order is
236
- * preserved. The merged collection's order = (sourceFile order, then declaration order).
237
- *
238
- * Duplicate view names produce `merge-collision-view` warnings; duplicate global component
239
- * names produce `merge-collision-component`. The first occurrence wins.
240
- *
241
- * Returned arrays are fresh, but element objects (View/Component/Request) are shared with
242
- * the input fragments — callers must not mutate them.
243
- */
244
- declare function merge(fragments: SightmapFragment[]): Sightmap;
245
-
246
9
  interface LoadDirectoryOptions {
247
10
  /** Optional root for `file` fields in diagnostics. Default: the directory passed in. */
248
11
  diagnosticRoot?: string;
@@ -256,18 +19,6 @@ interface LoadDirectoryOptions {
256
19
  */
257
20
  declare function loadDirectory(dir: string, opts?: LoadDirectoryOptions): Promise<Sightmap>;
258
21
 
259
- interface MatchOptions {
260
- url: string;
261
- method?: string;
262
- }
263
- declare function match(sightmap: Sightmap, opts: MatchOptions): MatchResult;
264
-
265
- interface ExplainOptions {
266
- by?: "name" | "path";
267
- type?: "view" | "component" | "request";
268
- }
269
- declare function explain(sightmap: Sightmap, query: string, opts?: ExplainOptions): ExplainResult;
270
-
271
22
  interface LintOptions {
272
23
  /** Per-rule enable/disable. Default: all rules enabled. */
273
24
  rules?: Record<string, boolean>;
@@ -276,4 +27,11 @@ interface LintOptions {
276
27
  }
277
28
  declare function lint(sightmap: Sightmap, opts?: LintOptions): Promise<Diagnostic[]>;
278
29
 
279
- export { type Component, DUPLICATE_ROUTE, DUPLICATE_VIEW_NAME, type Diagnostic, type ExplainHit, type ExplainMatchedAs, type ExplainResult, type Field, MERGE_COLLISION_COMPONENT, MERGE_COLLISION_VIEW, type MatchResult, PARSE_ERROR, ROUTE_SHADOWING, type Request, type ResolvedComponent, type ResolvedRequest, type ResolvedView, SCHEMA_VALIDATION_FAILED, SELECTOR_SYNTAX, type Severity, type Sightmap, type SightmapFragment, UNKNOWN_SOURCE, UNKNOWN_VERSION, type ValidateResult, type View, explain, lint, loadDirectory, match, merge, parse, validate };
30
+ /**
31
+ * Input shape for `format`: the value-side of `SightmapV1` plus any
32
+ * forward-compat unknown keys we should pass through.
33
+ */
34
+ type FormatInput = SightmapV1 & Record<string, unknown>;
35
+ declare function format(input: FormatInput): string;
36
+
37
+ export { Diagnostic, type FormatInput, Sightmap, ValidateResult, format, lint, loadDirectory, validate };
package/dist/index.js CHANGED
@@ -42,6 +42,12 @@ function parse(input, opts = {}) {
42
42
  return fragment;
43
43
  }
44
44
  function normalizeComponent(c) {
45
+ if (c["selectors"] !== void 0 && c.selector === void 0) {
46
+ const name = c.name ?? "(unnamed)";
47
+ throw new Error(
48
+ `Component "${name}": use \`selector\` (singular), not \`selectors\` (plural). \`selector\` accepts either a single string or an array of strings.`
49
+ );
50
+ }
45
51
  const sel = c.selector;
46
52
  const normalized = {
47
53
  ...c,
@@ -73,20 +79,25 @@ var SELECTOR_SYNTAX = "selector-syntax";
73
79
  import { existsSync, readFileSync } from "fs";
74
80
  import { fileURLToPath } from "url";
75
81
  import { resolve, dirname } from "path";
76
- var __dirname = dirname(fileURLToPath(import.meta.url));
77
- var schemaCandidates = [
78
- resolve(__dirname, "./vendored/sightmap.schema.json"),
79
- resolve(__dirname, "../vendored/sightmap.schema.json")
80
- ];
81
- var schemaPath = schemaCandidates.find((p) => existsSync(p));
82
- if (schemaPath === void 0) {
83
- throw new Error(
84
- `vendored sightmap.schema.json not found. Looked in: ${schemaCandidates.join(", ")}`
85
- );
82
+ var _ajvValidate;
83
+ function getValidator() {
84
+ if (_ajvValidate) return _ajvValidate;
85
+ const __dirname = dirname(fileURLToPath(import.meta.url));
86
+ const schemaCandidates = [
87
+ resolve(__dirname, "./vendored/sightmap.schema.json"),
88
+ resolve(__dirname, "../vendored/sightmap.schema.json")
89
+ ];
90
+ const schemaPath = schemaCandidates.find((p) => existsSync(p));
91
+ if (schemaPath === void 0) {
92
+ throw new Error(
93
+ `vendored sightmap.schema.json not found. Looked in: ${schemaCandidates.join(", ")}`
94
+ );
95
+ }
96
+ const schema = JSON.parse(readFileSync(schemaPath, "utf8"));
97
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
98
+ _ajvValidate = ajv.compile(schema);
99
+ return _ajvValidate;
86
100
  }
87
- var schema = JSON.parse(readFileSync(schemaPath, "utf8"));
88
- var ajv = new Ajv2020({ allErrors: true, strict: false });
89
- var ajvValidate = ajv.compile(schema);
90
101
  function validate(input, opts = {}) {
91
102
  const diagnostics = [];
92
103
  let doc;
@@ -127,6 +138,7 @@ function validate(input, opts = {}) {
127
138
  });
128
139
  return { ok: false, diagnostics };
129
140
  }
141
+ const ajvValidate = getValidator();
130
142
  const ok = ajvValidate(obj);
131
143
  if (!ok) {
132
144
  for (const e of ajvValidate.errors ?? []) {
@@ -521,29 +533,35 @@ function routeShadowing(sightmap) {
521
533
  const views = sightmap.views;
522
534
  for (let j = 1; j < views.length; j++) {
523
535
  const later = views[j];
524
- const representative = makeRepresentativeUrl(later.route);
536
+ const laterKey = matchSetKey(later.route);
537
+ const laterScore = specificity(later.route);
525
538
  for (let i = 0; i < j; i++) {
526
539
  const earlier = views[i];
527
540
  if (earlier.route === later.route) continue;
528
- if (routeMatch(earlier.route, representative)) {
529
- out.push({
530
- severity: "warning",
531
- code: ROUTE_SHADOWING,
532
- message: `Route "${later.route}" (view "${later.name}") is shadowed by earlier route "${earlier.route}" (view "${earlier.name}"); first-match-wins makes it unreachable.`
533
- });
534
- break;
535
- }
541
+ if (matchSetKey(earlier.route) !== laterKey) continue;
542
+ if (specificity(earlier.route) < laterScore) continue;
543
+ out.push({
544
+ severity: "warning",
545
+ code: ROUTE_SHADOWING,
546
+ message: `Route "${later.route}" (view "${later.name}") is shadowed by earlier route "${earlier.route}" (view "${earlier.name}"); they match the same URLs and the earlier route is at least as specific, making this route unreachable.`
547
+ });
548
+ break;
536
549
  }
537
550
  }
538
551
  return out;
539
552
  }
540
- function makeRepresentativeUrl(route) {
541
- const result = route.split("/").filter((seg) => seg !== "**").map((seg) => {
542
- if (seg === "*") return "__seg__";
543
- if (seg.startsWith(":")) return "__seg__";
544
- return seg;
545
- }).join("/").replace(/^\/+/, "/");
546
- return result || "/";
553
+ function matchSetKey(route) {
554
+ return route.split("/").map((seg) => seg.startsWith(":") ? "*" : seg).join("/");
555
+ }
556
+ function specificity(route) {
557
+ let total = 0;
558
+ for (const seg of route.split("/")) {
559
+ if (seg === "" || seg === "**") continue;
560
+ if (seg === "*") total += 1;
561
+ else if (seg.startsWith(":")) total += 2;
562
+ else total += 3;
563
+ }
564
+ return total;
547
565
  }
548
566
 
549
567
  // src/lintRules/unknownSource.ts
@@ -628,6 +646,32 @@ async function lint(sightmap, opts = {}) {
628
646
  }
629
647
  return out;
630
648
  }
649
+
650
+ // src/format/index.ts
651
+ import { stringify } from "yaml";
652
+ var CANONICAL_KEY_ORDER = [
653
+ "version",
654
+ "memory",
655
+ "views",
656
+ "components",
657
+ "requests"
658
+ ];
659
+ function format(input) {
660
+ const ordered = {};
661
+ for (const key of CANONICAL_KEY_ORDER) {
662
+ if (input[key] !== void 0) ordered[key] = input[key];
663
+ }
664
+ for (const key of Object.keys(input)) {
665
+ if (key.startsWith("__")) continue;
666
+ if (!(key in ordered)) ordered[key] = input[key];
667
+ }
668
+ const yaml3 = stringify(ordered, {
669
+ indent: 2,
670
+ lineWidth: 0,
671
+ minContentWidth: 0
672
+ });
673
+ return yaml3.endsWith("\n") ? yaml3 : yaml3 + "\n";
674
+ }
631
675
  export {
632
676
  DUPLICATE_ROUTE,
633
677
  DUPLICATE_VIEW_NAME,
@@ -640,6 +684,7 @@ export {
640
684
  UNKNOWN_SOURCE,
641
685
  UNKNOWN_VERSION,
642
686
  explain,
687
+ format,
643
688
  lint,
644
689
  loadDirectory,
645
690
  match,