@sightmap/sightmap 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ sightmap lint [path] [--strict] [--rules <list>]
20
20
  sightmap check [path] [--stdin] [--level schema|quality] # combined validate + lint
21
21
  sightmap match <url> [path] [--method <m>] [--require-view]
22
22
  sightmap explain <query> [path] [--by name|path] [--type view|component|request] [--require-hit]
23
+ sightmap fmt [path] [--check] [--write] # default path: .sightmap
23
24
  sightmap check-conventions [path] # default path: .
24
25
  ```
25
26
 
@@ -64,6 +65,19 @@ $ sightmap explain 'app/(tabs)/index.tsx'
64
65
  component ArcCard matchedAs=path src=app/(tabs)/index.tsx
65
66
  ```
66
67
 
68
+ ### `sightmap fmt [path]`
69
+
70
+ Canonicalize `.sightmap/*.yaml` files per the spec's normative formatting rules. Comment- and HEADER-preserving.
71
+
72
+ ```bash
73
+ sightmap fmt --check # exit 1 if any file is not canonical (CI mode)
74
+ sightmap fmt --write # rewrite non-canonical files in place
75
+ ```
76
+
77
+ Diagnostics: `fmt.not-canonical` (warning), `fmt.parse-error` (error), `fmt.schema-invalid` (error). Files with parse or schema errors are left untouched.
78
+
79
+ Exit codes: `0` (canonical / no rewrites needed), `1` (`--check` found non-canonical files), `2` (parse / schema / IO error).
80
+
67
81
  ### `sightmap check-conventions [path]`
68
82
 
69
83
  Validate that the repo at `<path>` (default `.`) follows the [filename conventions](https://github.com/sightmap/spec/blob/main/docs/repo-conventions.md) for SEPs (`seps/NNNN-{slug}.md`) and conformance fixtures (`conformance/NNN-{slug}.fixture/`). Exit code `0` if clean, `1` if any violations. Pass `--json` for machine-readable output.
@@ -162,6 +176,9 @@ Codes are stable, kebab-case, and exported as constants:
162
176
  | `convention.fixture-dirname` | error | `check-conventions` |
163
177
  | `convention.invalid-slug` | error | `check-conventions` |
164
178
  | `convention.unexpected-file` | error | `check-conventions` |
179
+ | `fmt.not-canonical` | warning | `fmt --check` |
180
+ | `fmt.parse-error` | error | `fmt` |
181
+ | `fmt.schema-invalid` | error | `fmt` |
165
182
 
166
183
  Renames require an SEP. Future SDK ports (Python, Go) MUST emit identical codes for the same conditions.
167
184
 
@@ -101,6 +101,9 @@ declare const DUPLICATE_ROUTE = "duplicate-route";
101
101
  declare const ROUTE_SHADOWING = "route-shadowing";
102
102
  declare const UNKNOWN_SOURCE = "unknown-source";
103
103
  declare const SELECTOR_SYNTAX = "selector-syntax";
104
+ declare const FMT_NOT_CANONICAL = "fmt.not-canonical";
105
+ declare const FMT_PARSE_ERROR = "fmt.parse-error";
106
+ declare const FMT_SCHEMA_INVALID = "fmt.schema-invalid";
104
107
 
105
108
  /**
106
109
  * A single-file fragment after parsing and selector normalization.
@@ -250,4 +253,4 @@ interface ExplainOptions {
250
253
  }
251
254
  declare function explain(sightmap: Sightmap, query: string, opts?: ExplainOptions): ExplainResult;
252
255
 
253
- export { type Component as C, type Diagnostic as D, type ExplainHit as E, type Field as F, MERGE_COLLISION_COMPONENT as M, PARSE_ERROR as P, ROUTE_SHADOWING as R, type Sightmap as S, UNKNOWN_SOURCE as U, type ValidateResult as V, type SightmapV1 as a, DUPLICATE_ROUTE as b, DUPLICATE_VIEW_NAME as c, type ExplainMatchedAs as d, type ExplainResult as e, MERGE_COLLISION_VIEW as f, type MatchResult as g, type Request as h, type ResolvedComponent as i, type ResolvedRequest as j, type ResolvedView as k, SCHEMA_VALIDATION_FAILED as l, SELECTOR_SYNTAX as m, type Severity as n, type SightmapFragment as o, UNKNOWN_VERSION as p, type View as q, explain as r, match as s, merge as t, parse as u };
256
+ export { type Component as C, type Diagnostic as D, type ExplainHit as E, FMT_NOT_CANONICAL as F, MERGE_COLLISION_COMPONENT as M, PARSE_ERROR as P, ROUTE_SHADOWING as R, type Sightmap as S, UNKNOWN_SOURCE as U, type ValidateResult as V, type SightmapV1 as a, DUPLICATE_ROUTE as b, DUPLICATE_VIEW_NAME as c, type ExplainMatchedAs as d, type ExplainResult as e, FMT_PARSE_ERROR as f, FMT_SCHEMA_INVALID as g, type Field as h, MERGE_COLLISION_VIEW as i, type MatchResult as j, type Request as k, type ResolvedComponent as l, type ResolvedRequest as m, type ResolvedView as n, SCHEMA_VALIDATION_FAILED as o, SELECTOR_SYNTAX as p, type Severity as q, type SightmapFragment as r, UNKNOWN_VERSION as s, type View as t, explain as u, match as v, merge as w, parse as x };
package/dist/browser.d.ts CHANGED
@@ -1 +1 @@
1
- export { E as ExplainHit, d as ExplainMatchedAs, e as ExplainResult, g as MatchResult, i as ResolvedComponent, j as ResolvedRequest, k as ResolvedView, S as Sightmap, o as SightmapFragment, r as explain, s as match, t as merge, u as parse } from './browser-ChD_xQt8.js';
1
+ export { E as ExplainHit, d as ExplainMatchedAs, e as ExplainResult, j as MatchResult, l as ResolvedComponent, m as ResolvedRequest, n as ResolvedView, S as Sightmap, r as SightmapFragment, u as explain, v as match, w as merge, x as parse } from './browser-2wvcIicU.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parse.ts","../src/diagnostics.ts","../src/merge.ts","../src/routeMatch.ts","../src/resolver.ts","../src/match.ts","../src/explain.ts"],"sourcesContent":["import yaml from \"js-yaml\";\nimport type { SightmapFragment } from \"./sightmap.js\";\nimport type { Component } from \"./types.js\";\n\nexport interface ParseOptions {\n /** Optional source file path; recorded on the fragment for canonical-order merging. */\n sourceFile?: string;\n}\n\n/**\n * Parse a single sightmap file (YAML string or pre-parsed object).\n * Throws on parse error, missing version, or version mismatch.\n * Normalizes `selector: string` to `selector: [string]` everywhere.\n */\nexport function parse(input: string | object, opts: ParseOptions = {}): SightmapFragment {\n let doc: unknown;\n if (typeof input === \"string\") {\n try {\n doc = yaml.load(input);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`YAML parse error: ${msg}`);\n }\n } else {\n // Shallow-clone caller's object so we don't mutate it.\n doc = { ...input };\n }\n\n if (doc === null || typeof doc !== \"object\" || Array.isArray(doc)) {\n throw new Error(\"Expected sightmap document to be an object at the root\");\n }\n\n const obj = doc as Record<string, unknown>;\n if (obj[\"version\"] === undefined) {\n throw new Error(\"Missing required `version` field\");\n }\n if (obj[\"version\"] !== 1) {\n throw new Error(`Unsupported version: ${String(obj[\"version\"])} (expected 1)`);\n }\n\n // Normalize selectors recursively. Spread each view to avoid mutating\n // shared view objects when the caller passed a pre-parsed input.\n if (Array.isArray(obj[\"components\"])) {\n obj[\"components\"] = (obj[\"components\"] as Component[]).map(normalizeComponent);\n }\n if (Array.isArray(obj[\"views\"])) {\n obj[\"views\"] = (obj[\"views\"] as Array<Record<string, unknown>>).map((v) => {\n const out = { ...v };\n if (Array.isArray(out[\"components\"])) {\n out[\"components\"] = (out[\"components\"] as Component[]).map(normalizeComponent);\n }\n return out;\n });\n }\n\n const fragment = {\n ...obj,\n __brand: \"SightmapFragment\" as const,\n ...(opts.sourceFile !== undefined ? { __sourceFile: opts.sourceFile } : {}),\n } as SightmapFragment;\n return fragment;\n}\n\nfunction normalizeComponent(c: Component): Component {\n // Common migration error: `selectors` (plural) instead of `selector` (singular).\n // The schema only accepts singular `selector`; the singular field already takes\n // a string or string-array, so a plural alias would be redundant. Surface\n // explicitly rather than silently dropping the value.\n if ((c as unknown as Record<string, unknown>)[\"selectors\"] !== undefined && c.selector === undefined) {\n const name = (c as { name?: string }).name ?? \"(unnamed)\";\n throw new Error(\n `Component \"${name}\": use \\`selector\\` (singular), not \\`selectors\\` (plural). ` +\n `\\`selector\\` accepts either a single string or an array of strings.`,\n );\n }\n const sel = c.selector;\n const normalized: Component = {\n ...c,\n selector: typeof sel === \"string\" ? [sel] : (sel as [string, ...string[]]),\n };\n if (Array.isArray(c.children)) {\n normalized.children = c.children.map(normalizeComponent);\n }\n return normalized;\n}\n","export type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n severity: Severity;\n code: string;\n message: string;\n file?: string;\n path?: string;\n loc?: { line: number; column: number };\n source?: string;\n}\n\n// Stable, kebab-case codes. Renames require an SEP.\nexport const PARSE_ERROR = \"parse-error\";\nexport const SCHEMA_VALIDATION_FAILED = \"schema-validation-failed\";\nexport const UNKNOWN_VERSION = \"unknown-version\";\nexport const MERGE_COLLISION_VIEW = \"merge-collision-view\";\nexport const MERGE_COLLISION_COMPONENT = \"merge-collision-component\";\nexport const DUPLICATE_VIEW_NAME = \"duplicate-view-name\";\nexport const DUPLICATE_ROUTE = \"duplicate-route\";\nexport const ROUTE_SHADOWING = \"route-shadowing\";\nexport const UNKNOWN_SOURCE = \"unknown-source\";\nexport const SELECTOR_SYNTAX = \"selector-syntax\";\n\n// Repo-conventions codes (WI-6). See docs/repo-conventions.md in the spec repo.\nexport const CONVENTION_SEP_FILENAME = \"convention.sep-filename\";\nexport const CONVENTION_FIXTURE_DIRNAME = \"convention.fixture-dirname\";\nexport const CONVENTION_INVALID_SLUG = \"convention.invalid-slug\";\nexport const CONVENTION_UNEXPECTED_FILE = \"convention.unexpected-file\";\n","import type { SightmapFragment, Sightmap } from \"./sightmap.js\";\nimport type { View, Component, Request } from \"./types.js\";\nimport {\n MERGE_COLLISION_VIEW,\n MERGE_COLLISION_COMPONENT,\n type Diagnostic,\n} from \"./diagnostics.js\";\n\n/**\n * Merge fragments into a queryable Sightmap.\n *\n * Canonical order: fragments are sorted by `__sourceFile` (code-point order; locale-independent\n * for cross-environment determinism). Fragments without a `__sourceFile` sort first, and ES2019\n * sort stability preserves their input order. Within each fragment, declaration order is\n * preserved. The merged collection's order = (sourceFile order, then declaration order).\n *\n * Duplicate view names produce `merge-collision-view` warnings; duplicate global component\n * names produce `merge-collision-component`. The first occurrence wins.\n *\n * Returned arrays are fresh, but element objects (View/Component/Request) are shared with\n * the input fragments — callers must not mutate them.\n */\nexport function merge(fragments: SightmapFragment[]): Sightmap {\n const sorted = [...fragments].sort((a, b) => {\n const aFile = a.__sourceFile ?? \"\";\n const bFile = b.__sourceFile ?? \"\";\n return aFile < bFile ? -1 : aFile > bFile ? 1 : 0;\n });\n\n const views: View[] = [];\n const globalComponents: Component[] = [];\n const globalRequests: Request[] = [];\n const fileMemory: { memory: string[]; sourceFile: string }[] = [];\n const diagnostics: Diagnostic[] = [];\n\n const seenViewNames = new Map<string, string>(); // name → sourceFile\n const seenComponentNames = new Map<string, string>();\n\n for (const f of sorted) {\n const file = f.__sourceFile ?? \"<unknown>\";\n\n for (const v of f.views ?? []) {\n const prev = seenViewNames.get(v.name);\n if (prev !== undefined) {\n diagnostics.push({\n severity: \"warning\",\n code: MERGE_COLLISION_VIEW,\n message: `View name \"${v.name}\" defined in both \"${prev}\" and \"${file}\"; first occurrence wins.`,\n file,\n });\n } else {\n seenViewNames.set(v.name, file);\n }\n views.push(v); // keep all views; first-match-wins is enforced at resolve time\n }\n\n for (const c of f.components ?? []) {\n const prev = seenComponentNames.get(c.name);\n if (prev !== undefined) {\n diagnostics.push({\n severity: \"warning\",\n code: MERGE_COLLISION_COMPONENT,\n message: `Component name \"${c.name}\" defined in both \"${prev}\" and \"${file}\"; first occurrence wins.`,\n file,\n });\n } else {\n seenComponentNames.set(c.name, file);\n }\n globalComponents.push(c);\n }\n\n for (const r of f.requests ?? []) {\n globalRequests.push(r);\n }\n\n if (Array.isArray(f.memory) && f.memory.length > 0) {\n fileMemory.push({ memory: [...f.memory], sourceFile: file });\n }\n }\n\n return {\n version: 1,\n views,\n globalComponents,\n globalRequests,\n fileMemory,\n diagnostics,\n __brand: \"Sightmap\",\n };\n}\n","/**\n * Canonicalize a URL or pathname for matching.\n * - Accepts absolute URL or pathname.\n * - Strips scheme, host, query string, and fragment.\n * - Normalizes trailing slashes (except for the root \"/\").\n */\nexport function canonicalizeUrl(input: string): string {\n let s = input;\n // Strip absolute prefix.\n const protoMatch = /^[a-z][a-z0-9+.-]*:\\/\\/[^/]*/i.exec(s);\n if (protoMatch) {\n s = s.slice(protoMatch[0].length) || \"/\";\n }\n // Strip fragment.\n const hash = s.indexOf(\"#\");\n if (hash !== -1) s = s.slice(0, hash);\n // Strip query.\n const q = s.indexOf(\"?\");\n if (q !== -1) s = s.slice(0, q);\n // Trailing slash.\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\n/**\n * Test whether a glob route pattern matches a URL pathname.\n * Pattern syntax (per spec):\n * - Literal segments match themselves\n * - \"*\" matches exactly one path segment\n * - \"**\" matches any depth of segments\n * - \":param\" segments normalize to \"*\"\n * - Matching is case-sensitive\n * - Trailing slashes ignored\n */\nexport function routeMatch(pattern: string, url: string): boolean {\n const p = normalizePattern(pattern);\n const u = canonicalizeUrl(url);\n const patternSegs = splitSegments(p);\n const urlSegs = splitSegments(u);\n return matchSegs(patternSegs, urlSegs);\n}\n\nfunction normalizePattern(p: string): string {\n // Replace \":param\" segments with \"*\"\n let s = p\n .split(\"/\")\n .map((seg) => (seg.startsWith(\":\") ? \"*\" : seg))\n .join(\"/\");\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\nfunction splitSegments(path: string): string[] {\n if (path === \"/\" || path === \"\") return [];\n const trimmed = path.startsWith(\"/\") ? path.slice(1) : path;\n return trimmed.split(\"/\");\n}\n\nfunction matchSegs(pattern: string[], url: string[]): boolean {\n if (pattern.length === 0 && url.length === 0) return true;\n if (pattern.length === 0) return false;\n\n const head = pattern[0]!;\n const rest = pattern.slice(1);\n\n if (head === \"**\") {\n // Match zero or more segments.\n if (rest.length === 0) return true;\n for (let i = 0; i <= url.length; i++) {\n if (matchSegs(rest, url.slice(i))) return true;\n }\n return false;\n }\n\n if (url.length === 0) return false;\n if (head === \"*\" || head === url[0]) {\n return matchSegs(rest, url.slice(1));\n }\n return false;\n}\n","import type {\n Sightmap,\n MatchResult,\n ExplainHit,\n ResolvedView,\n ResolvedComponent,\n ResolvedRequest,\n} from \"./sightmap.js\";\nimport type { View, Component, Request } from \"./types.js\";\nimport { routeMatch } from \"./routeMatch.js\";\n\nexport function resolveByUrl(\n sightmap: Sightmap,\n url: string,\n method?: string,\n): MatchResult {\n // First-match-wins on views.\n let matchedView: View | null = null;\n for (const v of sightmap.views) {\n if (routeMatch(v.route, url)) {\n matchedView = v;\n break;\n }\n }\n\n const components: ResolvedComponent[] = [];\n for (const c of sightmap.globalComponents) {\n components.push(...flattenComponent(c, [], \"global\", undefined));\n }\n if (matchedView !== null && Array.isArray(matchedView.components)) {\n for (const c of matchedView.components) {\n components.push(...flattenComponent(c, [], \"view-scoped\", matchedView.name));\n }\n }\n\n const requests: ResolvedRequest[] = [];\n const requestPool: Request[] = [\n ...sightmap.globalRequests,\n ...((matchedView?.requests ?? [])),\n ];\n for (const req of requestPool) {\n if (!routeMatch(req.route, url)) continue;\n if (method !== undefined && req.method !== undefined && req.method !== method) continue;\n requests.push(toResolvedRequest(req));\n }\n\n const memory: string[] = [];\n for (const fm of sightmap.fileMemory) memory.push(...fm.memory);\n if (matchedView?.memory) memory.push(...matchedView.memory);\n\n return {\n view: matchedView !== null ? toResolvedView(matchedView) : null,\n components,\n requests,\n memory,\n };\n}\n\nexport function resolveByName(sightmap: Sightmap, name: string): ExplainHit[] {\n const hits: ExplainHit[] = [];\n for (const v of sightmap.views) {\n if (v.name === name) hits.push({ type: \"view\", matchedAs: \"name\", entry: toResolvedView(v) });\n }\n for (const c of sightmap.globalComponents) {\n for (const rc of flattenComponent(c, [], \"global\", undefined)) {\n if (rc.name === name) hits.push({ type: \"component\", matchedAs: \"name\", entry: rc });\n }\n }\n for (const v of sightmap.views) {\n for (const c of v.components ?? []) {\n for (const rc of flattenComponent(c, [], \"view-scoped\", v.name)) {\n if (rc.name === name) hits.push({ type: \"component\", matchedAs: \"name\", entry: rc });\n }\n }\n }\n for (const r of sightmap.globalRequests) {\n if (r.name === name) hits.push({ type: \"request\", matchedAs: \"name\", entry: toResolvedRequest(r) });\n }\n for (const v of sightmap.views) {\n for (const r of v.requests ?? []) {\n if (r.name === name) hits.push({ type: \"request\", matchedAs: \"name\", entry: toResolvedRequest(r) });\n }\n }\n return hits;\n}\n\nexport function resolveBySourcePath(sightmap: Sightmap, path: string): ExplainHit[] {\n const hits: ExplainHit[] = [];\n for (const v of sightmap.views) {\n if (v.source === path) hits.push({ type: \"view\", matchedAs: \"path\", entry: toResolvedView(v) });\n }\n for (const c of sightmap.globalComponents) {\n for (const rc of flattenComponent(c, [], \"global\", undefined)) {\n if (rc.source === path) hits.push({ type: \"component\", matchedAs: \"path\", entry: rc });\n }\n }\n for (const v of sightmap.views) {\n for (const c of v.components ?? []) {\n for (const rc of flattenComponent(c, [], \"view-scoped\", v.name)) {\n if (rc.source === path) hits.push({ type: \"component\", matchedAs: \"path\", entry: rc });\n }\n }\n }\n for (const r of sightmap.globalRequests) {\n if (r.source === path) hits.push({ type: \"request\", matchedAs: \"path\", entry: toResolvedRequest(r) });\n }\n return hits;\n}\n\nfunction flattenComponent(\n c: Component,\n parentChain: string[],\n scope: \"global\" | \"view-scoped\",\n scopedToView: string | undefined,\n): ResolvedComponent[] {\n const out: ResolvedComponent[] = [];\n const selector = Array.isArray(c.selector) ? c.selector : [c.selector as unknown as string];\n out.push({\n name: c.name,\n selector,\n ...(c.source !== undefined ? { source: c.source } : {}),\n ...(c.description !== undefined ? { description: c.description } : {}),\n memory: c.memory ? [...c.memory] : [],\n parentChain: [...parentChain],\n scope,\n ...(scopedToView !== undefined ? { scopedToView } : {}),\n definedIn: { file: \"<unknown>\" },\n });\n for (const child of c.children ?? []) {\n out.push(...flattenComponent(child, [...parentChain, c.name], scope, scopedToView));\n }\n return out;\n}\n\nfunction toResolvedView(v: View): ResolvedView {\n return {\n name: v.name,\n route: v.route,\n ...(v.source !== undefined ? { source: v.source } : {}),\n ...(v.description !== undefined ? { description: v.description } : {}),\n memory: v.memory ? [...v.memory] : [],\n definedIn: { file: \"<unknown>\" },\n };\n}\n\nfunction toResolvedRequest(r: Request): ResolvedRequest {\n return {\n name: r.name,\n route: r.route,\n ...(r.method !== undefined ? { method: r.method } : {}),\n ...(r.source !== undefined ? { source: r.source } : {}),\n ...(r.description !== undefined ? { description: r.description } : {}),\n ...(r.request !== undefined ? { request: { fields: r.request.fields ?? [] } } : {}),\n ...(r.response !== undefined ? { response: { fields: r.response.fields ?? [] } } : {}),\n ...(r.headers !== undefined ? { headers: r.headers } : {}),\n memory: r.memory ? [...r.memory] : [],\n definedIn: { file: \"<unknown>\" },\n };\n}\n","import type { Sightmap, MatchResult } from \"./sightmap.js\";\nimport { resolveByUrl } from \"./resolver.js\";\n\nexport interface MatchOptions {\n url: string;\n method?: string;\n}\n\nexport function match(sightmap: Sightmap, opts: MatchOptions): MatchResult {\n return resolveByUrl(sightmap, opts.url, opts.method);\n}\n","import type { Sightmap, ExplainResult, ExplainHit, ExplainMatchedAs } from \"./sightmap.js\";\nimport { resolveByName, resolveBySourcePath } from \"./resolver.js\";\n\nexport interface ExplainOptions {\n by?: \"name\" | \"path\";\n type?: \"view\" | \"component\" | \"request\";\n}\n\nexport function explain(\n sightmap: Sightmap,\n query: string,\n opts: ExplainOptions = {},\n): ExplainResult {\n const byName = opts.by === \"path\" ? [] : resolveByName(sightmap, query);\n const byPath = opts.by === \"name\" ? [] : resolveBySourcePath(sightmap, query);\n\n // Merge: when an entry appears in both lookups, label as \"name-and-path\".\n // Use entry identity (type + name) as the dedup key.\n const key = (h: ExplainHit): string => `${h.type}:${h.entry.name}`;\n const namedKeys = new Set(byName.map(key));\n const pathKeys = new Set(byPath.map(key));\n\n const merged: ExplainHit[] = [];\n for (const h of byName) {\n if (pathKeys.has(key(h))) {\n merged.push(withMatchedAs(h, \"name-and-path\"));\n } else {\n merged.push(h);\n }\n }\n for (const h of byPath) {\n if (!namedKeys.has(key(h))) {\n merged.push(h);\n }\n // (entries already in both have been pushed above with \"name-and-path\".)\n }\n\n const filtered =\n opts.type !== undefined ? merged.filter((h) => h.type === opts.type) : merged;\n return { query, hits: filtered };\n}\n\nfunction withMatchedAs(h: ExplainHit, matchedAs: ExplainMatchedAs): ExplainHit {\n switch (h.type) {\n case \"view\":\n return { type: \"view\", matchedAs, entry: h.entry };\n case \"component\":\n return { type: \"component\", matchedAs, entry: h.entry };\n case \"request\":\n return { type: \"request\", matchedAs, entry: h.entry };\n }\n}\n"],"mappings":";AAAA,OAAO,UAAU;AAcV,SAAS,MAAM,OAAwB,OAAqB,CAAC,GAAqB;AACvF,MAAI;AACJ,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI;AACF,YAAM,KAAK,KAAK,KAAK;AAAA,IACvB,SAAS,KAAK;AACZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAM,IAAI,MAAM,qBAAqB,GAAG,EAAE;AAAA,IAC5C;AAAA,EACF,OAAO;AAEL,UAAM,EAAE,GAAG,MAAM;AAAA,EACnB;AAEA,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjE,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AAEA,QAAM,MAAM;AACZ,MAAI,IAAI,SAAS,MAAM,QAAW;AAChC,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AACA,MAAI,IAAI,SAAS,MAAM,GAAG;AACxB,UAAM,IAAI,MAAM,wBAAwB,OAAO,IAAI,SAAS,CAAC,CAAC,eAAe;AAAA,EAC/E;AAIA,MAAI,MAAM,QAAQ,IAAI,YAAY,CAAC,GAAG;AACpC,QAAI,YAAY,IAAK,IAAI,YAAY,EAAkB,IAAI,kBAAkB;AAAA,EAC/E;AACA,MAAI,MAAM,QAAQ,IAAI,OAAO,CAAC,GAAG;AAC/B,QAAI,OAAO,IAAK,IAAI,OAAO,EAAqC,IAAI,CAAC,MAAM;AACzE,YAAM,MAAM,EAAE,GAAG,EAAE;AACnB,UAAI,MAAM,QAAQ,IAAI,YAAY,CAAC,GAAG;AACpC,YAAI,YAAY,IAAK,IAAI,YAAY,EAAkB,IAAI,kBAAkB;AAAA,MAC/E;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,QAAM,WAAW;AAAA,IACf,GAAG;AAAA,IACH,SAAS;AAAA,IACT,GAAI,KAAK,eAAe,SAAY,EAAE,cAAc,KAAK,WAAW,IAAI,CAAC;AAAA,EAC3E;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,GAAyB;AAKnD,MAAK,EAAyC,WAAW,MAAM,UAAa,EAAE,aAAa,QAAW;AACpG,UAAM,OAAQ,EAAwB,QAAQ;AAC9C,UAAM,IAAI;AAAA,MACR,cAAc,IAAI;AAAA,IAEpB;AAAA,EACF;AACA,QAAM,MAAM,EAAE;AACd,QAAM,aAAwB;AAAA,IAC5B,GAAG;AAAA,IACH,UAAU,OAAO,QAAQ,WAAW,CAAC,GAAG,IAAK;AAAA,EAC/C;AACA,MAAI,MAAM,QAAQ,EAAE,QAAQ,GAAG;AAC7B,eAAW,WAAW,EAAE,SAAS,IAAI,kBAAkB;AAAA,EACzD;AACA,SAAO;AACT;;;ACpEO,IAAM,uBAAuB;AAC7B,IAAM,4BAA4B;;;ACKlC,SAAS,MAAM,WAAyC;AAC7D,QAAM,SAAS,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM;AAC3C,UAAM,QAAQ,EAAE,gBAAgB;AAChC,UAAM,QAAQ,EAAE,gBAAgB;AAChC,WAAO,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,IAAI;AAAA,EAClD,CAAC;AAED,QAAM,QAAgB,CAAC;AACvB,QAAM,mBAAgC,CAAC;AACvC,QAAM,iBAA4B,CAAC;AACnC,QAAM,aAAyD,CAAC;AAChE,QAAM,cAA4B,CAAC;AAEnC,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,QAAM,qBAAqB,oBAAI,IAAoB;AAEnD,aAAW,KAAK,QAAQ;AACtB,UAAM,OAAO,EAAE,gBAAgB;AAE/B,eAAW,KAAK,EAAE,SAAS,CAAC,GAAG;AAC7B,YAAM,OAAO,cAAc,IAAI,EAAE,IAAI;AACrC,UAAI,SAAS,QAAW;AACtB,oBAAY,KAAK;AAAA,UACf,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,cAAc,EAAE,IAAI,sBAAsB,IAAI,UAAU,IAAI;AAAA,UACrE;AAAA,QACF,CAAC;AAAA,MACH,OAAO;AACL,sBAAc,IAAI,EAAE,MAAM,IAAI;AAAA,MAChC;AACA,YAAM,KAAK,CAAC;AAAA,IACd;AAEA,eAAW,KAAK,EAAE,cAAc,CAAC,GAAG;AAClC,YAAM,OAAO,mBAAmB,IAAI,EAAE,IAAI;AAC1C,UAAI,SAAS,QAAW;AACtB,oBAAY,KAAK;AAAA,UACf,UAAU;AAAA,UACV,MAAM;AAAA,UACN,SAAS,mBAAmB,EAAE,IAAI,sBAAsB,IAAI,UAAU,IAAI;AAAA,UAC1E;AAAA,QACF,CAAC;AAAA,MACH,OAAO;AACL,2BAAmB,IAAI,EAAE,MAAM,IAAI;AAAA,MACrC;AACA,uBAAiB,KAAK,CAAC;AAAA,IACzB;AAEA,eAAW,KAAK,EAAE,YAAY,CAAC,GAAG;AAChC,qBAAe,KAAK,CAAC;AAAA,IACvB;AAEA,QAAI,MAAM,QAAQ,EAAE,MAAM,KAAK,EAAE,OAAO,SAAS,GAAG;AAClD,iBAAW,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,KAAK,CAAC;AAAA,IAC7D;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,EACX;AACF;;;ACnFO,SAAS,gBAAgB,OAAuB;AACrD,MAAI,IAAI;AAER,QAAM,aAAa,gCAAgC,KAAK,CAAC;AACzD,MAAI,YAAY;AACd,QAAI,EAAE,MAAM,WAAW,CAAC,EAAE,MAAM,KAAK;AAAA,EACvC;AAEA,QAAM,OAAO,EAAE,QAAQ,GAAG;AAC1B,MAAI,SAAS,GAAI,KAAI,EAAE,MAAM,GAAG,IAAI;AAEpC,QAAM,IAAI,EAAE,QAAQ,GAAG;AACvB,MAAI,MAAM,GAAI,KAAI,EAAE,MAAM,GAAG,CAAC;AAE9B,MAAI,EAAE,SAAS,KAAK,EAAE,SAAS,GAAG,EAAG,KAAI,EAAE,MAAM,GAAG,EAAE;AACtD,SAAO;AACT;AAYO,SAAS,WAAW,SAAiB,KAAsB;AAChE,QAAM,IAAI,iBAAiB,OAAO;AAClC,QAAM,IAAI,gBAAgB,GAAG;AAC7B,QAAM,cAAc,cAAc,CAAC;AACnC,QAAM,UAAU,cAAc,CAAC;AAC/B,SAAO,UAAU,aAAa,OAAO;AACvC;AAEA,SAAS,iBAAiB,GAAmB;AAE3C,MAAI,IAAI,EACL,MAAM,GAAG,EACT,IAAI,CAAC,QAAS,IAAI,WAAW,GAAG,IAAI,MAAM,GAAI,EAC9C,KAAK,GAAG;AACX,MAAI,EAAE,SAAS,KAAK,EAAE,SAAS,GAAG,EAAG,KAAI,EAAE,MAAM,GAAG,EAAE;AACtD,SAAO;AACT;AAEA,SAAS,cAAc,MAAwB;AAC7C,MAAI,SAAS,OAAO,SAAS,GAAI,QAAO,CAAC;AACzC,QAAM,UAAU,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,IAAI;AACvD,SAAO,QAAQ,MAAM,GAAG;AAC1B;AAEA,SAAS,UAAU,SAAmB,KAAwB;AAC5D,MAAI,QAAQ,WAAW,KAAK,IAAI,WAAW,EAAG,QAAO;AACrD,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,OAAO,QAAQ,CAAC;AACtB,QAAM,OAAO,QAAQ,MAAM,CAAC;AAE5B,MAAI,SAAS,MAAM;AAEjB,QAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,aAAS,IAAI,GAAG,KAAK,IAAI,QAAQ,KAAK;AACpC,UAAI,UAAU,MAAM,IAAI,MAAM,CAAC,CAAC,EAAG,QAAO;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AAEA,MAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,MAAI,SAAS,OAAO,SAAS,IAAI,CAAC,GAAG;AACnC,WAAO,UAAU,MAAM,IAAI,MAAM,CAAC,CAAC;AAAA,EACrC;AACA,SAAO;AACT;;;ACpEO,SAAS,aACd,UACA,KACA,QACa;AAEb,MAAI,cAA2B;AAC/B,aAAW,KAAK,SAAS,OAAO;AAC9B,QAAI,WAAW,EAAE,OAAO,GAAG,GAAG;AAC5B,oBAAc;AACd;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAkC,CAAC;AACzC,aAAW,KAAK,SAAS,kBAAkB;AACzC,eAAW,KAAK,GAAG,iBAAiB,GAAG,CAAC,GAAG,UAAU,MAAS,CAAC;AAAA,EACjE;AACA,MAAI,gBAAgB,QAAQ,MAAM,QAAQ,YAAY,UAAU,GAAG;AACjE,eAAW,KAAK,YAAY,YAAY;AACtC,iBAAW,KAAK,GAAG,iBAAiB,GAAG,CAAC,GAAG,eAAe,YAAY,IAAI,CAAC;AAAA,IAC7E;AAAA,EACF;AAEA,QAAM,WAA8B,CAAC;AACrC,QAAM,cAAyB;AAAA,IAC7B,GAAG,SAAS;AAAA,IACZ,GAAK,aAAa,YAAY,CAAC;AAAA,EACjC;AACA,aAAW,OAAO,aAAa;AAC7B,QAAI,CAAC,WAAW,IAAI,OAAO,GAAG,EAAG;AACjC,QAAI,WAAW,UAAa,IAAI,WAAW,UAAa,IAAI,WAAW,OAAQ;AAC/E,aAAS,KAAK,kBAAkB,GAAG,CAAC;AAAA,EACtC;AAEA,QAAM,SAAmB,CAAC;AAC1B,aAAW,MAAM,SAAS,WAAY,QAAO,KAAK,GAAG,GAAG,MAAM;AAC9D,MAAI,aAAa,OAAQ,QAAO,KAAK,GAAG,YAAY,MAAM;AAE1D,SAAO;AAAA,IACL,MAAM,gBAAgB,OAAO,eAAe,WAAW,IAAI;AAAA,IAC3D;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,cAAc,UAAoB,MAA4B;AAC5E,QAAM,OAAqB,CAAC;AAC5B,aAAW,KAAK,SAAS,OAAO;AAC9B,QAAI,EAAE,SAAS,KAAM,MAAK,KAAK,EAAE,MAAM,QAAQ,WAAW,QAAQ,OAAO,eAAe,CAAC,EAAE,CAAC;AAAA,EAC9F;AACA,aAAW,KAAK,SAAS,kBAAkB;AACzC,eAAW,MAAM,iBAAiB,GAAG,CAAC,GAAG,UAAU,MAAS,GAAG;AAC7D,UAAI,GAAG,SAAS,KAAM,MAAK,KAAK,EAAE,MAAM,aAAa,WAAW,QAAQ,OAAO,GAAG,CAAC;AAAA,IACrF;AAAA,EACF;AACA,aAAW,KAAK,SAAS,OAAO;AAC9B,eAAW,KAAK,EAAE,cAAc,CAAC,GAAG;AAClC,iBAAW,MAAM,iBAAiB,GAAG,CAAC,GAAG,eAAe,EAAE,IAAI,GAAG;AAC/D,YAAI,GAAG,SAAS,KAAM,MAAK,KAAK,EAAE,MAAM,aAAa,WAAW,QAAQ,OAAO,GAAG,CAAC;AAAA,MACrF;AAAA,IACF;AAAA,EACF;AACA,aAAW,KAAK,SAAS,gBAAgB;AACvC,QAAI,EAAE,SAAS,KAAM,MAAK,KAAK,EAAE,MAAM,WAAW,WAAW,QAAQ,OAAO,kBAAkB,CAAC,EAAE,CAAC;AAAA,EACpG;AACA,aAAW,KAAK,SAAS,OAAO;AAC9B,eAAW,KAAK,EAAE,YAAY,CAAC,GAAG;AAChC,UAAI,EAAE,SAAS,KAAM,MAAK,KAAK,EAAE,MAAM,WAAW,WAAW,QAAQ,OAAO,kBAAkB,CAAC,EAAE,CAAC;AAAA,IACpG;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,oBAAoB,UAAoB,MAA4B;AAClF,QAAM,OAAqB,CAAC;AAC5B,aAAW,KAAK,SAAS,OAAO;AAC9B,QAAI,EAAE,WAAW,KAAM,MAAK,KAAK,EAAE,MAAM,QAAQ,WAAW,QAAQ,OAAO,eAAe,CAAC,EAAE,CAAC;AAAA,EAChG;AACA,aAAW,KAAK,SAAS,kBAAkB;AACzC,eAAW,MAAM,iBAAiB,GAAG,CAAC,GAAG,UAAU,MAAS,GAAG;AAC7D,UAAI,GAAG,WAAW,KAAM,MAAK,KAAK,EAAE,MAAM,aAAa,WAAW,QAAQ,OAAO,GAAG,CAAC;AAAA,IACvF;AAAA,EACF;AACA,aAAW,KAAK,SAAS,OAAO;AAC9B,eAAW,KAAK,EAAE,cAAc,CAAC,GAAG;AAClC,iBAAW,MAAM,iBAAiB,GAAG,CAAC,GAAG,eAAe,EAAE,IAAI,GAAG;AAC/D,YAAI,GAAG,WAAW,KAAM,MAAK,KAAK,EAAE,MAAM,aAAa,WAAW,QAAQ,OAAO,GAAG,CAAC;AAAA,MACvF;AAAA,IACF;AAAA,EACF;AACA,aAAW,KAAK,SAAS,gBAAgB;AACvC,QAAI,EAAE,WAAW,KAAM,MAAK,KAAK,EAAE,MAAM,WAAW,WAAW,QAAQ,OAAO,kBAAkB,CAAC,EAAE,CAAC;AAAA,EACtG;AACA,SAAO;AACT;AAEA,SAAS,iBACP,GACA,aACA,OACA,cACqB;AACrB,QAAM,MAA2B,CAAC;AAClC,QAAM,WAAW,MAAM,QAAQ,EAAE,QAAQ,IAAI,EAAE,WAAW,CAAC,EAAE,QAA6B;AAC1F,MAAI,KAAK;AAAA,IACP,MAAM,EAAE;AAAA,IACR;AAAA,IACA,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACrD,GAAI,EAAE,gBAAgB,SAAY,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACpE,QAAQ,EAAE,SAAS,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,aAAa,CAAC,GAAG,WAAW;AAAA,IAC5B;AAAA,IACA,GAAI,iBAAiB,SAAY,EAAE,aAAa,IAAI,CAAC;AAAA,IACrD,WAAW,EAAE,MAAM,YAAY;AAAA,EACjC,CAAC;AACD,aAAW,SAAS,EAAE,YAAY,CAAC,GAAG;AACpC,QAAI,KAAK,GAAG,iBAAiB,OAAO,CAAC,GAAG,aAAa,EAAE,IAAI,GAAG,OAAO,YAAY,CAAC;AAAA,EACpF;AACA,SAAO;AACT;AAEA,SAAS,eAAe,GAAuB;AAC7C,SAAO;AAAA,IACL,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,IACT,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACrD,GAAI,EAAE,gBAAgB,SAAY,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACpE,QAAQ,EAAE,SAAS,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,WAAW,EAAE,MAAM,YAAY;AAAA,EACjC;AACF;AAEA,SAAS,kBAAkB,GAA6B;AACtD,SAAO;AAAA,IACL,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,IACT,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACrD,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACrD,GAAI,EAAE,gBAAgB,SAAY,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;AAAA,IACpE,GAAI,EAAE,YAAY,SAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC;AAAA,IACjF,GAAI,EAAE,aAAa,SAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC;AAAA,IACpF,GAAI,EAAE,YAAY,SAAY,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxD,QAAQ,EAAE,SAAS,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC;AAAA,IACpC,WAAW,EAAE,MAAM,YAAY;AAAA,EACjC;AACF;;;ACtJO,SAAS,MAAM,UAAoB,MAAiC;AACzE,SAAO,aAAa,UAAU,KAAK,KAAK,KAAK,MAAM;AACrD;;;ACFO,SAAS,QACd,UACA,OACA,OAAuB,CAAC,GACT;AACf,QAAM,SAAS,KAAK,OAAO,SAAS,CAAC,IAAI,cAAc,UAAU,KAAK;AACtE,QAAM,SAAS,KAAK,OAAO,SAAS,CAAC,IAAI,oBAAoB,UAAU,KAAK;AAI5E,QAAM,MAAM,CAAC,MAA0B,GAAG,EAAE,IAAI,IAAI,EAAE,MAAM,IAAI;AAChE,QAAM,YAAY,IAAI,IAAI,OAAO,IAAI,GAAG,CAAC;AACzC,QAAM,WAAW,IAAI,IAAI,OAAO,IAAI,GAAG,CAAC;AAExC,QAAM,SAAuB,CAAC;AAC9B,aAAW,KAAK,QAAQ;AACtB,QAAI,SAAS,IAAI,IAAI,CAAC,CAAC,GAAG;AACxB,aAAO,KAAK,cAAc,GAAG,eAAe,CAAC;AAAA,IAC/C,OAAO;AACL,aAAO,KAAK,CAAC;AAAA,IACf;AAAA,EACF;AACA,aAAW,KAAK,QAAQ;AACtB,QAAI,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,GAAG;AAC1B,aAAO,KAAK,CAAC;AAAA,IACf;AAAA,EAEF;AAEA,QAAM,WACJ,KAAK,SAAS,SAAY,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,IAAI;AACzE,SAAO,EAAE,OAAO,MAAM,SAAS;AACjC;AAEA,SAAS,cAAc,GAAe,WAAyC;AAC7E,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,EAAE,MAAM,QAAQ,WAAW,OAAO,EAAE,MAAM;AAAA,IACnD,KAAK;AACH,aAAO,EAAE,MAAM,aAAa,WAAW,OAAO,EAAE,MAAM;AAAA,IACxD,KAAK;AACH,aAAO,EAAE,MAAM,WAAW,WAAW,OAAO,EAAE,MAAM;AAAA,EACxD;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/parse.ts","../src/diagnostics.ts","../src/merge.ts","../src/routeMatch.ts","../src/resolver.ts","../src/match.ts","../src/explain.ts"],"sourcesContent":["import yaml from \"js-yaml\";\nimport type { SightmapFragment } from \"./sightmap.js\";\nimport type { Component } from \"./types.js\";\n\nexport interface ParseOptions {\n /** Optional source file path; recorded on the fragment for canonical-order merging. */\n sourceFile?: string;\n}\n\n/**\n * Parse a single sightmap file (YAML string or pre-parsed object).\n * Throws on parse error, missing version, or version mismatch.\n * Normalizes `selector: string` to `selector: [string]` everywhere.\n */\nexport function parse(input: string | object, opts: ParseOptions = {}): SightmapFragment {\n let doc: unknown;\n if (typeof input === \"string\") {\n try {\n doc = yaml.load(input);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`YAML parse error: ${msg}`);\n }\n } else {\n // Shallow-clone caller's object so we don't mutate it.\n doc = { ...input };\n }\n\n if (doc === null || typeof doc !== \"object\" || Array.isArray(doc)) {\n throw new Error(\"Expected sightmap document to be an object at the root\");\n }\n\n const obj = doc as Record<string, unknown>;\n if (obj[\"version\"] === undefined) {\n throw new Error(\"Missing required `version` field\");\n }\n if (obj[\"version\"] !== 1) {\n throw new Error(`Unsupported version: ${String(obj[\"version\"])} (expected 1)`);\n }\n\n // Normalize selectors recursively. Spread each view to avoid mutating\n // shared view objects when the caller passed a pre-parsed input.\n if (Array.isArray(obj[\"components\"])) {\n obj[\"components\"] = (obj[\"components\"] as Component[]).map(normalizeComponent);\n }\n if (Array.isArray(obj[\"views\"])) {\n obj[\"views\"] = (obj[\"views\"] as Array<Record<string, unknown>>).map((v) => {\n const out = { ...v };\n if (Array.isArray(out[\"components\"])) {\n out[\"components\"] = (out[\"components\"] as Component[]).map(normalizeComponent);\n }\n return out;\n });\n }\n\n const fragment = {\n ...obj,\n __brand: \"SightmapFragment\" as const,\n ...(opts.sourceFile !== undefined ? { __sourceFile: opts.sourceFile } : {}),\n } as SightmapFragment;\n return fragment;\n}\n\nfunction normalizeComponent(c: Component): Component {\n // Common migration error: `selectors` (plural) instead of `selector` (singular).\n // The schema only accepts singular `selector`; the singular field already takes\n // a string or string-array, so a plural alias would be redundant. Surface\n // explicitly rather than silently dropping the value.\n if ((c as unknown as Record<string, unknown>)[\"selectors\"] !== undefined && c.selector === undefined) {\n const name = (c as { name?: string }).name ?? \"(unnamed)\";\n throw new Error(\n `Component \"${name}\": use \\`selector\\` (singular), not \\`selectors\\` (plural). ` +\n `\\`selector\\` accepts either a single string or an array of strings.`,\n );\n }\n const sel = c.selector;\n const normalized: Component = {\n ...c,\n selector: typeof sel === \"string\" ? [sel] : (sel as [string, ...string[]]),\n };\n if (Array.isArray(c.children)) {\n normalized.children = c.children.map(normalizeComponent);\n }\n return normalized;\n}\n","export type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Diagnostic {\n severity: Severity;\n code: string;\n message: string;\n file?: string;\n path?: string;\n loc?: { line: number; column: number };\n source?: string;\n}\n\n// Stable, kebab-case codes. Renames require an SEP.\nexport const PARSE_ERROR = \"parse-error\";\nexport const SCHEMA_VALIDATION_FAILED = \"schema-validation-failed\";\nexport const UNKNOWN_VERSION = \"unknown-version\";\nexport const MERGE_COLLISION_VIEW = \"merge-collision-view\";\nexport const MERGE_COLLISION_COMPONENT = \"merge-collision-component\";\nexport const DUPLICATE_VIEW_NAME = \"duplicate-view-name\";\nexport const DUPLICATE_ROUTE = \"duplicate-route\";\nexport const ROUTE_SHADOWING = \"route-shadowing\";\nexport const UNKNOWN_SOURCE = \"unknown-source\";\nexport const SELECTOR_SYNTAX = \"selector-syntax\";\n\n// fmt CLI diagnostic codes (sightmap/spec docs/authoring-conventions.md#diagnostic-codes-authoring-side).\nexport const FMT_NOT_CANONICAL = \"fmt.not-canonical\";\nexport const FMT_PARSE_ERROR = \"fmt.parse-error\";\nexport const FMT_SCHEMA_INVALID = \"fmt.schema-invalid\";\n\n// Repo-conventions codes (WI-6). See docs/repo-conventions.md in the spec repo.\nexport const CONVENTION_SEP_FILENAME = \"convention.sep-filename\";\nexport const CONVENTION_FIXTURE_DIRNAME = \"convention.fixture-dirname\";\nexport const CONVENTION_INVALID_SLUG = \"convention.invalid-slug\";\nexport const CONVENTION_UNEXPECTED_FILE = \"convention.unexpected-file\";\n","import type { SightmapFragment, Sightmap } from \"./sightmap.js\";\nimport type { View, Component, Request } from \"./types.js\";\nimport {\n MERGE_COLLISION_VIEW,\n MERGE_COLLISION_COMPONENT,\n type Diagnostic,\n} from \"./diagnostics.js\";\n\n/**\n * Merge fragments into a queryable Sightmap.\n *\n * Canonical order: fragments are sorted by `__sourceFile` (code-point order; locale-independent\n * for cross-environment determinism). Fragments without a `__sourceFile` sort first, and ES2019\n * sort stability preserves their input order. Within each fragment, declaration order is\n * preserved. The merged collection's order = (sourceFile order, then declaration order).\n *\n * Duplicate view names produce `merge-collision-view` warnings; duplicate global component\n * names produce `merge-collision-component`. The first occurrence wins.\n *\n * Returned arrays are fresh, but element objects (View/Component/Request) are shared with\n * the input fragments — callers must not mutate them.\n */\nexport function merge(fragments: SightmapFragment[]): Sightmap {\n const sorted = [...fragments].sort((a, b) => {\n const aFile = a.__sourceFile ?? \"\";\n const bFile = b.__sourceFile ?? \"\";\n return aFile < bFile ? -1 : aFile > bFile ? 1 : 0;\n });\n\n const views: View[] = [];\n const globalComponents: Component[] = [];\n const globalRequests: Request[] = [];\n const fileMemory: { memory: string[]; sourceFile: string }[] = [];\n const diagnostics: Diagnostic[] = [];\n\n const seenViewNames = new Map<string, string>(); // name → sourceFile\n const seenComponentNames = new Map<string, string>();\n\n for (const f of sorted) {\n const file = f.__sourceFile ?? \"<unknown>\";\n\n for (const v of f.views ?? []) {\n const prev = seenViewNames.get(v.name);\n if (prev !== undefined) {\n diagnostics.push({\n severity: \"warning\",\n code: MERGE_COLLISION_VIEW,\n message: `View name \"${v.name}\" defined in both \"${prev}\" and \"${file}\"; first occurrence wins.`,\n file,\n });\n } else {\n seenViewNames.set(v.name, file);\n }\n views.push(v); // keep all views; first-match-wins is enforced at resolve time\n }\n\n for (const c of f.components ?? []) {\n const prev = seenComponentNames.get(c.name);\n if (prev !== undefined) {\n diagnostics.push({\n severity: \"warning\",\n code: MERGE_COLLISION_COMPONENT,\n message: `Component name \"${c.name}\" defined in both \"${prev}\" and \"${file}\"; first occurrence wins.`,\n file,\n });\n } else {\n seenComponentNames.set(c.name, file);\n }\n globalComponents.push(c);\n }\n\n for (const r of f.requests ?? []) {\n globalRequests.push(r);\n }\n\n if (Array.isArray(f.memory) && f.memory.length > 0) {\n fileMemory.push({ memory: [...f.memory], sourceFile: file });\n }\n }\n\n return {\n version: 1,\n views,\n globalComponents,\n globalRequests,\n fileMemory,\n diagnostics,\n __brand: \"Sightmap\",\n };\n}\n","/**\n * Canonicalize a URL or pathname for matching.\n * - Accepts absolute URL or pathname.\n * - Strips scheme, host, query string, and fragment.\n * - Normalizes trailing slashes (except for the root \"/\").\n */\nexport function canonicalizeUrl(input: string): string {\n let s = input;\n // Strip absolute prefix.\n const protoMatch = /^[a-z][a-z0-9+.-]*:\\/\\/[^/]*/i.exec(s);\n if (protoMatch) {\n s = s.slice(protoMatch[0].length) || \"/\";\n }\n // Strip fragment.\n const hash = s.indexOf(\"#\");\n if (hash !== -1) s = s.slice(0, hash);\n // Strip query.\n const q = s.indexOf(\"?\");\n if (q !== -1) s = s.slice(0, q);\n // Trailing slash.\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\n/**\n * Test whether a glob route pattern matches a URL pathname.\n * Pattern syntax (per spec):\n * - Literal segments match themselves\n * - \"*\" matches exactly one path segment\n * - \"**\" matches any depth of segments\n * - \":param\" segments normalize to \"*\"\n * - Matching is case-sensitive\n * - Trailing slashes ignored\n */\nexport function routeMatch(pattern: string, url: string): boolean {\n const p = normalizePattern(pattern);\n const u = canonicalizeUrl(url);\n const patternSegs = splitSegments(p);\n const urlSegs = splitSegments(u);\n return matchSegs(patternSegs, urlSegs);\n}\n\nfunction normalizePattern(p: string): string {\n // Replace \":param\" segments with \"*\"\n let s = p\n .split(\"/\")\n .map((seg) => (seg.startsWith(\":\") ? \"*\" : seg))\n .join(\"/\");\n if (s.length > 1 && s.endsWith(\"/\")) s = s.slice(0, -1);\n return s;\n}\n\nfunction splitSegments(path: string): string[] {\n if (path === \"/\" || path === \"\") return [];\n const trimmed = path.startsWith(\"/\") ? path.slice(1) : path;\n return trimmed.split(\"/\");\n}\n\nfunction matchSegs(pattern: string[], url: string[]): boolean {\n if (pattern.length === 0 && url.length === 0) return true;\n if (pattern.length === 0) return false;\n\n const head = pattern[0]!;\n const rest = pattern.slice(1);\n\n if (head === \"**\") {\n // Match zero or more segments.\n if (rest.length === 0) return true;\n for (let i = 0; i <= url.length; i++) {\n if (matchSegs(rest, url.slice(i))) return true;\n }\n return false;\n }\n\n if (url.length === 0) return false;\n if (head === \"*\" || head === url[0]) {\n return matchSegs(rest, url.slice(1));\n }\n return false;\n}\n","import type {\n Sightmap,\n MatchResult,\n ExplainHit,\n ResolvedView,\n ResolvedComponent,\n ResolvedRequest,\n} from \"./sightmap.js\";\nimport type { View, Component, Request } from \"./types.js\";\nimport { routeMatch } from \"./routeMatch.js\";\n\nexport function resolveByUrl(\n sightmap: Sightmap,\n url: string,\n method?: string,\n): MatchResult {\n // First-match-wins on views.\n let matchedView: View | null = null;\n for (const v of sightmap.views) {\n if (routeMatch(v.route, url)) {\n matchedView = v;\n break;\n }\n }\n\n const components: ResolvedComponent[] = [];\n for (const c of sightmap.globalComponents) {\n components.push(...flattenComponent(c, [], \"global\", undefined));\n }\n if (matchedView !== null && Array.isArray(matchedView.components)) {\n for (const c of matchedView.components) {\n components.push(...flattenComponent(c, [], \"view-scoped\", matchedView.name));\n }\n }\n\n const requests: ResolvedRequest[] = [];\n const requestPool: Request[] = [\n ...sightmap.globalRequests,\n ...((matchedView?.requests ?? [])),\n ];\n for (const req of requestPool) {\n if (!routeMatch(req.route, url)) continue;\n if (method !== undefined && req.method !== undefined && req.method !== method) continue;\n requests.push(toResolvedRequest(req));\n }\n\n const memory: string[] = [];\n for (const fm of sightmap.fileMemory) memory.push(...fm.memory);\n if (matchedView?.memory) memory.push(...matchedView.memory);\n\n return {\n view: matchedView !== null ? toResolvedView(matchedView) : null,\n components,\n requests,\n memory,\n };\n}\n\nexport function resolveByName(sightmap: Sightmap, name: string): ExplainHit[] {\n const hits: ExplainHit[] = [];\n for (const v of sightmap.views) {\n if (v.name === name) hits.push({ type: \"view\", matchedAs: \"name\", entry: toResolvedView(v) });\n }\n for (const c of sightmap.globalComponents) {\n for (const rc of flattenComponent(c, [], \"global\", undefined)) {\n if (rc.name === name) hits.push({ type: \"component\", matchedAs: \"name\", entry: rc });\n }\n }\n for (const v of sightmap.views) {\n for (const c of v.components ?? []) {\n for (const rc of flattenComponent(c, [], \"view-scoped\", v.name)) {\n if (rc.name === name) hits.push({ type: \"component\", matchedAs: \"name\", entry: rc });\n }\n }\n }\n for (const r of sightmap.globalRequests) {\n if (r.name === name) hits.push({ type: \"request\", matchedAs: \"name\", entry: toResolvedRequest(r) });\n }\n for (const v of sightmap.views) {\n for (const r of v.requests ?? []) {\n if (r.name === name) hits.push({ type: \"request\", matchedAs: \"name\", entry: toResolvedRequest(r) });\n }\n }\n return hits;\n}\n\nexport function resolveBySourcePath(sightmap: Sightmap, path: string): ExplainHit[] {\n const hits: ExplainHit[] = [];\n for (const v of sightmap.views) {\n if (v.source === path) hits.push({ type: \"view\", matchedAs: \"path\", entry: toResolvedView(v) });\n }\n for (const c of sightmap.globalComponents) {\n for (const rc of flattenComponent(c, [], \"global\", undefined)) {\n if (rc.source === path) hits.push({ type: \"component\", matchedAs: \"path\", entry: rc });\n }\n }\n for (const v of sightmap.views) {\n for (const c of v.components ?? []) {\n for (const rc of flattenComponent(c, [], \"view-scoped\", v.name)) {\n if (rc.source === path) hits.push({ type: \"component\", matchedAs: \"path\", entry: rc });\n }\n }\n }\n for (const r of sightmap.globalRequests) {\n if (r.source === path) hits.push({ type: \"request\", matchedAs: \"path\", entry: toResolvedRequest(r) });\n }\n return hits;\n}\n\nfunction flattenComponent(\n c: Component,\n parentChain: string[],\n scope: \"global\" | \"view-scoped\",\n scopedToView: string | undefined,\n): ResolvedComponent[] {\n const out: ResolvedComponent[] = [];\n const selector = Array.isArray(c.selector) ? c.selector : [c.selector as unknown as string];\n out.push({\n name: c.name,\n selector,\n ...(c.source !== undefined ? { source: c.source } : {}),\n ...(c.description !== undefined ? { description: c.description } : {}),\n memory: c.memory ? [...c.memory] : [],\n parentChain: [...parentChain],\n scope,\n ...(scopedToView !== undefined ? { scopedToView } : {}),\n definedIn: { file: \"<unknown>\" },\n });\n for (const child of c.children ?? []) {\n out.push(...flattenComponent(child, [...parentChain, c.name], scope, scopedToView));\n }\n return out;\n}\n\nfunction toResolvedView(v: View): ResolvedView {\n return {\n name: v.name,\n route: v.route,\n ...(v.source !== undefined ? { source: v.source } : {}),\n ...(v.description !== undefined ? { description: v.description } : {}),\n memory: v.memory ? [...v.memory] : [],\n definedIn: { file: \"<unknown>\" },\n };\n}\n\nfunction toResolvedRequest(r: Request): ResolvedRequest {\n return {\n name: r.name,\n route: r.route,\n ...(r.method !== undefined ? { method: r.method } : {}),\n ...(r.source !== undefined ? { source: r.source } : {}),\n ...(r.description !== undefined ? { description: r.description } : {}),\n ...(r.request !== undefined ? { request: { fields: r.request.fields ?? [] } } : {}),\n ...(r.response !== undefined ? { response: { fields: r.response.fields ?? [] } } : {}),\n ...(r.headers !== undefined ? { headers: r.headers } : {}),\n memory: r.memory ? [...r.memory] : [],\n definedIn: { file: \"<unknown>\" },\n };\n}\n","import type { Sightmap, MatchResult } from \"./sightmap.js\";\nimport { resolveByUrl } from \"./resolver.js\";\n\nexport interface MatchOptions {\n url: string;\n method?: string;\n}\n\nexport function match(sightmap: Sightmap, opts: MatchOptions): MatchResult {\n return resolveByUrl(sightmap, opts.url, opts.method);\n}\n","import type { Sightmap, ExplainResult, ExplainHit, ExplainMatchedAs } from \"./sightmap.js\";\nimport { resolveByName, resolveBySourcePath } from \"./resolver.js\";\n\nexport interface ExplainOptions {\n by?: \"name\" | \"path\";\n type?: \"view\" | \"component\" | \"request\";\n}\n\nexport function explain(\n sightmap: Sightmap,\n query: string,\n opts: ExplainOptions = {},\n): ExplainResult {\n const byName = opts.by === \"path\" ? [] : resolveByName(sightmap, query);\n const byPath = opts.by === \"name\" ? [] : resolveBySourcePath(sightmap, query);\n\n // Merge: when an entry appears in both lookups, label as \"name-and-path\".\n // Use entry identity (type + name) as the dedup key.\n const key = (h: ExplainHit): string => `${h.type}:${h.entry.name}`;\n const namedKeys = new Set(byName.map(key));\n const pathKeys = new Set(byPath.map(key));\n\n const merged: ExplainHit[] = [];\n for (const h of byName) {\n if (pathKeys.has(key(h))) {\n merged.push(withMatchedAs(h, \"name-and-path\"));\n } else {\n merged.push(h);\n }\n }\n for (const h of byPath) {\n if (!namedKeys.has(key(h))) {\n merged.push(h);\n }\n // (entries already in both have been pushed above with \"name-and-path\".)\n }\n\n const filtered =\n opts.type !== undefined ? merged.filter((h) => h.type === opts.type) : merged;\n return { query, hits: filtered };\n}\n\nfunction withMatchedAs(h: ExplainHit, matchedAs: ExplainMatchedAs): ExplainHit {\n switch (h.type) {\n case \"view\":\n return { type: \"view\", matchedAs, entry: h.entry };\n case \"component\":\n return { type: \"component\", matchedAs, entry: h.entry };\n case \"request\":\n return { type: \"request\", matchedAs, entry: h.entry };\n }\n}\n"],"mappings":"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","names":[]}
package/dist/cli/index.js CHANGED
@@ -22,6 +22,9 @@ var DUPLICATE_ROUTE = "duplicate-route";
22
22
  var ROUTE_SHADOWING = "route-shadowing";
23
23
  var UNKNOWN_SOURCE = "unknown-source";
24
24
  var SELECTOR_SYNTAX = "selector-syntax";
25
+ var FMT_NOT_CANONICAL = "fmt.not-canonical";
26
+ var FMT_PARSE_ERROR = "fmt.parse-error";
27
+ var FMT_SCHEMA_INVALID = "fmt.schema-invalid";
25
28
  var CONVENTION_SEP_FILENAME = "convention.sep-filename";
26
29
  var CONVENTION_FIXTURE_DIRNAME = "convention.fixture-dirname";
27
30
 
@@ -270,6 +273,47 @@ function formatExplain(r) {
270
273
  }
271
274
  return lines.join("\n");
272
275
  }
276
+ function formatFmtCheck(lines) {
277
+ const body = lines.map((l) => {
278
+ switch (l.status) {
279
+ case "ok":
280
+ return `${l.path}: ok`;
281
+ case "not-canonical":
282
+ return `${l.path}: not canonical (run \`sightmap fmt --write\` to fix)`;
283
+ case "error":
284
+ return `${l.path}: error`;
285
+ case "rewritten":
286
+ return `${l.path}: rewritten`;
287
+ }
288
+ }).join("\n");
289
+ const need = lines.filter((l) => l.status === "not-canonical").length;
290
+ const total = lines.length;
291
+ const summary = total === 0 ? `No YAML files found.` : need === 0 ? `
292
+
293
+ All ${total} file(s) canonical.` : `
294
+
295
+ ${need} of ${total} file(s) need formatting.`;
296
+ return body + summary;
297
+ }
298
+ function formatFmtWrite(lines) {
299
+ const body = lines.map((l) => {
300
+ switch (l.status) {
301
+ case "ok":
302
+ return `${l.path}: ok`;
303
+ case "rewritten":
304
+ return `${l.path}: rewritten`;
305
+ case "error":
306
+ return `${l.path}: error`;
307
+ case "not-canonical":
308
+ return `${l.path}: not canonical`;
309
+ }
310
+ }).join("\n");
311
+ const rewritten = lines.filter((l) => l.status === "rewritten").length;
312
+ const summary = lines.length === 0 ? `No YAML files found.` : `
313
+
314
+ ${rewritten} file(s) rewritten.`;
315
+ return body + summary;
316
+ }
273
317
 
274
318
  // src/cli/validate.ts
275
319
  async function runValidate(opts) {
@@ -1082,6 +1126,377 @@ async function runCheckConventions(opts) {
1082
1126
  return result.ok ? 0 : 1;
1083
1127
  }
1084
1128
 
1129
+ // src/cli/fmt.ts
1130
+ import { readFile as readFile3, stat as stat2, writeFile } from "fs/promises";
1131
+ import { resolve as resolve10, relative as relative5 } from "path";
1132
+
1133
+ // src/format/canonicalize.ts
1134
+ import { parseDocument, isMap, isSeq, isScalar } from "yaml";
1135
+
1136
+ // src/format/canonical-rules.ts
1137
+ var TOP_LEVEL_KEY_ORDER = [
1138
+ "version",
1139
+ "memory",
1140
+ "views",
1141
+ "components",
1142
+ "requests"
1143
+ ];
1144
+ var VIEW_KEY_ORDER = [
1145
+ "name",
1146
+ "route",
1147
+ "description",
1148
+ "components",
1149
+ "memory",
1150
+ "requests"
1151
+ ];
1152
+ var COMPONENT_KEY_ORDER = [
1153
+ "name",
1154
+ "selector",
1155
+ "description",
1156
+ "children",
1157
+ "memory"
1158
+ ];
1159
+ var REQUEST_KEY_ORDER = [
1160
+ "name",
1161
+ "route",
1162
+ "method",
1163
+ "description",
1164
+ "source",
1165
+ "request",
1166
+ "response",
1167
+ "headers",
1168
+ "memory"
1169
+ ];
1170
+ function compareByName(a, b) {
1171
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
1172
+ }
1173
+ function compareRequests(a, b) {
1174
+ if (a.route !== b.route) return a.route < b.route ? -1 : 1;
1175
+ const am = a.method ?? "";
1176
+ const bm = b.method ?? "";
1177
+ return am < bm ? -1 : am > bm ? 1 : 0;
1178
+ }
1179
+
1180
+ // src/format/canonicalize.ts
1181
+ function canonicalize(input, opts) {
1182
+ const doc = parseDocument(input, { keepSourceTokens: false });
1183
+ const first = doc.errors[0];
1184
+ if (first !== void 0) {
1185
+ const loc = first.linePos?.[0] ? { line: first.linePos[0].line, column: first.linePos[0].col } : void 0;
1186
+ return {
1187
+ kind: "parse-error",
1188
+ diagnostics: [
1189
+ {
1190
+ severity: "error",
1191
+ code: FMT_PARSE_ERROR,
1192
+ message: first.message,
1193
+ file: opts.file,
1194
+ ...loc !== void 0 ? { loc } : {}
1195
+ }
1196
+ ]
1197
+ };
1198
+ }
1199
+ const v = validate(input, { sourceFile: opts.file });
1200
+ if (!v.ok) {
1201
+ const diagnostics = v.diagnostics.map((d) => ({
1202
+ ...d,
1203
+ severity: "error",
1204
+ code: FMT_SCHEMA_INVALID,
1205
+ file: opts.file
1206
+ }));
1207
+ return { kind: "schema-invalid", diagnostics };
1208
+ }
1209
+ if (isMap(doc.contents)) {
1210
+ reorderTopLevelKeys(doc.contents);
1211
+ reorderEntriesUnder(doc.contents, "views", VIEW_KEY_ORDER);
1212
+ reorderEntriesUnder(doc.contents, "components", COMPONENT_KEY_ORDER);
1213
+ reorderEntriesUnder(doc.contents, "requests", REQUEST_KEY_ORDER);
1214
+ forEachViewEntry(doc.contents, (view) => {
1215
+ reorderEntriesUnder(view, "components", COMPONENT_KEY_ORDER);
1216
+ reorderEntriesUnder(view, "requests", REQUEST_KEY_ORDER);
1217
+ forEachComponentEntry(view, "components", (cmp) => {
1218
+ reorderChildrenRecursive(cmp);
1219
+ });
1220
+ });
1221
+ forEachComponentEntry(doc.contents, "components", (cmp) => {
1222
+ reorderChildrenRecursive(cmp);
1223
+ });
1224
+ sortTopLevelSeq(doc.contents, "views", (a, b) => {
1225
+ const an = scalarPropFromMapItem(a, "name");
1226
+ const bn = scalarPropFromMapItem(b, "name");
1227
+ return compareByName({ name: an ?? "" }, { name: bn ?? "" });
1228
+ });
1229
+ sortTopLevelSeq(doc.contents, "components", (a, b) => {
1230
+ const an = scalarPropFromMapItem(a, "name");
1231
+ const bn = scalarPropFromMapItem(b, "name");
1232
+ return compareByName({ name: an ?? "" }, { name: bn ?? "" });
1233
+ });
1234
+ sortTopLevelSeq(doc.contents, "requests", (a, b) => {
1235
+ return compareRequests(
1236
+ { route: scalarPropFromMapItem(a, "route") ?? "", method: scalarPropFromMapItem(a, "method") },
1237
+ { route: scalarPropFromMapItem(b, "route") ?? "", method: scalarPropFromMapItem(b, "method") }
1238
+ );
1239
+ });
1240
+ visitAllScalars(doc, (scalar) => {
1241
+ if (typeof scalar.value !== "string") return;
1242
+ scalar.type = chooseQuoteType(scalar.value);
1243
+ });
1244
+ normalizeBlankLines(doc);
1245
+ }
1246
+ const output = serialize(doc);
1247
+ return { kind: "canonical", text: output, changed: output !== input };
1248
+ }
1249
+ function reorderTopLevelKeys(map) {
1250
+ reorderMapKeys(map, TOP_LEVEL_KEY_ORDER);
1251
+ }
1252
+ function reorderMapKeys(map, keyOrder) {
1253
+ const items = map.items;
1254
+ const byKey = /* @__PURE__ */ new Map();
1255
+ const unknownInOrder = [];
1256
+ for (const item of items) {
1257
+ const key = scalarKey(item);
1258
+ if (key === null) {
1259
+ unknownInOrder.push(item);
1260
+ continue;
1261
+ }
1262
+ if (keyOrder.includes(key)) {
1263
+ byKey.set(key, item);
1264
+ } else {
1265
+ unknownInOrder.push(item);
1266
+ }
1267
+ }
1268
+ const reordered = [];
1269
+ for (const k of keyOrder) {
1270
+ const p = byKey.get(k);
1271
+ if (p !== void 0) reordered.push(p);
1272
+ }
1273
+ reordered.push(...unknownInOrder);
1274
+ map.items = reordered;
1275
+ }
1276
+ function reorderEntriesUnder(map, key, keyOrder) {
1277
+ const seq = lookupSeq(map, key);
1278
+ if (seq === null) return;
1279
+ for (const item of seq.items) {
1280
+ if (isMap(item)) reorderMapKeys(item, keyOrder);
1281
+ }
1282
+ }
1283
+ function forEachViewEntry(root, fn) {
1284
+ const seq = lookupSeq(root, "views");
1285
+ if (seq === null) return;
1286
+ for (const item of seq.items) {
1287
+ if (isMap(item)) fn(item);
1288
+ }
1289
+ }
1290
+ function forEachComponentEntry(root, key, fn) {
1291
+ const seq = lookupSeq(root, key);
1292
+ if (seq === null) return;
1293
+ for (const item of seq.items) {
1294
+ if (isMap(item)) fn(item);
1295
+ }
1296
+ }
1297
+ function reorderChildrenRecursive(cmp) {
1298
+ const children = lookupSeq(cmp, "children");
1299
+ if (children === null) return;
1300
+ for (const child of children.items) {
1301
+ if (!isMap(child)) continue;
1302
+ reorderMapKeys(child, COMPONENT_KEY_ORDER);
1303
+ reorderChildrenRecursive(child);
1304
+ }
1305
+ }
1306
+ function lookupSeq(map, key) {
1307
+ for (const item of map.items) {
1308
+ if (scalarKey(item) === key && isSeq(item.value)) return item.value;
1309
+ }
1310
+ return null;
1311
+ }
1312
+ function scalarKey(pair) {
1313
+ const k = pair.key ?? null;
1314
+ if (k === null) return null;
1315
+ const v = typeof k === "object" && k !== null && "value" in k ? k.value : k;
1316
+ return typeof v === "string" ? v : null;
1317
+ }
1318
+ function sortTopLevelSeq(root, key, cmp) {
1319
+ const seq = lookupSeq(root, key);
1320
+ if (seq === null) return;
1321
+ seq.items = seq.items.slice().sort(cmp);
1322
+ }
1323
+ function scalarPropFromMapItem(item, key) {
1324
+ if (!isMap(item)) return void 0;
1325
+ for (const pair of item.items) {
1326
+ if (scalarKey(pair) !== key) continue;
1327
+ const v = pair.value;
1328
+ if (v !== null && typeof v === "object" && "value" in v) {
1329
+ const inner = v.value;
1330
+ return typeof inner === "string" ? inner : void 0;
1331
+ }
1332
+ return typeof v === "string" ? v : void 0;
1333
+ }
1334
+ return void 0;
1335
+ }
1336
+ function visitAllScalars(doc, fn) {
1337
+ walk(doc.contents, fn);
1338
+ }
1339
+ function walk(node, fn) {
1340
+ if (node === null || node === void 0) return;
1341
+ if (isScalar(node)) {
1342
+ fn(node);
1343
+ return;
1344
+ }
1345
+ if (isMap(node)) {
1346
+ for (const pair of node.items) {
1347
+ walk(pair.key, fn);
1348
+ walk(pair.value, fn);
1349
+ }
1350
+ return;
1351
+ }
1352
+ if (isSeq(node)) {
1353
+ for (const item of node.items) walk(item, fn);
1354
+ }
1355
+ }
1356
+ function normalizeBlankLines(doc) {
1357
+ const root = doc.contents;
1358
+ if (!isMap(root)) return;
1359
+ const firstPair = root.items[0];
1360
+ if (firstPair !== void 0) {
1361
+ const keyNode = firstPair.key;
1362
+ const docMutable = doc;
1363
+ const keyComment = keyNode?.commentBefore;
1364
+ const docComment = docMutable.commentBefore;
1365
+ if (typeof keyComment === "string" && keyComment.length > 0) {
1366
+ docMutable.commentBefore = keyComment;
1367
+ keyNode.commentBefore = null;
1368
+ }
1369
+ const hasHeader = typeof docMutable.commentBefore === "string" && docMutable.commentBefore.length > 0 || typeof docComment === "string" && docComment.length > 0;
1370
+ firstPair.spaceBefore = hasHeader;
1371
+ }
1372
+ for (const key of ["views", "components", "requests"]) {
1373
+ const seq = lookupSeq(root, key);
1374
+ if (seq === null) continue;
1375
+ seq.items.forEach((item, idx) => {
1376
+ if (typeof item === "object" && item !== null) {
1377
+ item.spaceBefore = idx > 0;
1378
+ }
1379
+ });
1380
+ for (const item of seq.items) {
1381
+ if (isMap(item)) clearInnerBlankLines(item);
1382
+ }
1383
+ }
1384
+ }
1385
+ function clearInnerBlankLines(map) {
1386
+ for (const pair of map.items) {
1387
+ pair.spaceBefore = false;
1388
+ if (isSeq(pair.value)) {
1389
+ for (const item of pair.value.items) {
1390
+ if (typeof item === "object" && item !== null) {
1391
+ item.spaceBefore = false;
1392
+ }
1393
+ if (isMap(item)) clearInnerBlankLines(item);
1394
+ }
1395
+ } else if (isMap(pair.value)) {
1396
+ clearInnerBlankLines(pair.value);
1397
+ }
1398
+ }
1399
+ }
1400
+ function chooseQuoteType(value) {
1401
+ if (requiresDoubleQuotes(value)) return "QUOTE_DOUBLE";
1402
+ if (value.includes('"')) return "QUOTE_SINGLE";
1403
+ if (isPlainSafe(value)) return "PLAIN";
1404
+ return "QUOTE_SINGLE";
1405
+ }
1406
+ function requiresDoubleQuotes(s) {
1407
+ return /[\x00-\x1f]/.test(s);
1408
+ }
1409
+ function isPlainSafe(s) {
1410
+ if (s.length === 0) return false;
1411
+ const first = s[0];
1412
+ if ("-?:,[]{}#&*!|>'\"%@`".includes(first)) return false;
1413
+ if (first === " " || first === " ") return false;
1414
+ if (s.includes(": ")) return false;
1415
+ if (s.includes(" #")) return false;
1416
+ if (/^(true|false|null|~|y|n|yes|no|on|off|True|False|Null|Yes|No|On|Off|TRUE|FALSE|NULL|YES|NO|ON|OFF)$/.test(s)) return false;
1417
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return false;
1418
+ if (/^0[xob]/.test(s)) return false;
1419
+ return true;
1420
+ }
1421
+ function serialize(doc) {
1422
+ const out = doc.toString({
1423
+ indent: 2,
1424
+ lineWidth: 0,
1425
+ minContentWidth: 0,
1426
+ blockQuote: "literal"
1427
+ });
1428
+ return out.endsWith("\n") ? out : out + "\n";
1429
+ }
1430
+
1431
+ // src/cli/fmt.ts
1432
+ async function runFmt(opts) {
1433
+ const target = resolve10(opts.cwd, opts.path);
1434
+ const files = (await resolveTarget(target)).sort();
1435
+ const fileResults = [];
1436
+ const diagnostics = [];
1437
+ for (const file of files) {
1438
+ const relPath = relative5(opts.cwd, file);
1439
+ await processFile(file, relPath, opts, fileResults, diagnostics);
1440
+ }
1441
+ const errCount = diagnostics.filter((d) => d.severity === "error").length;
1442
+ const warnCount = diagnostics.filter((d) => d.severity === "warning").length;
1443
+ const ok = errCount === 0 && warnCount === 0;
1444
+ if (opts.json) {
1445
+ emitJson(
1446
+ makeEnvelope({
1447
+ command: "fmt",
1448
+ ok,
1449
+ diagnostics,
1450
+ result: { files: fileResults }
1451
+ })
1452
+ );
1453
+ } else {
1454
+ const lines = fileResults.map((f) => {
1455
+ const hasErr = diagnostics.some((d) => d.severity === "error" && d.file === f.path);
1456
+ if (hasErr) return { path: f.path, status: "error" };
1457
+ if (opts.write) {
1458
+ return { path: f.path, status: f.changed ? "rewritten" : "ok" };
1459
+ }
1460
+ return { path: f.path, status: f.matchesExpected ? "ok" : "not-canonical" };
1461
+ });
1462
+ const out = opts.write ? formatFmtWrite(lines) : formatFmtCheck(lines);
1463
+ process.stdout.write(out + "\n");
1464
+ }
1465
+ if (errCount > 0) return 2;
1466
+ if (warnCount > 0) return 1;
1467
+ return 0;
1468
+ }
1469
+ async function resolveTarget(target) {
1470
+ const st = await stat2(target);
1471
+ if (st.isFile()) return [target];
1472
+ return collectYamlFiles(target);
1473
+ }
1474
+ async function processFile(absPath, relPath, opts, fileResults, diagnostics) {
1475
+ const text = await readFile3(absPath, "utf8");
1476
+ const result = canonicalize(text, { file: relPath });
1477
+ if (result.kind === "parse-error" || result.kind === "schema-invalid") {
1478
+ diagnostics.push(...result.diagnostics);
1479
+ fileResults.push(opts.write ? { path: relPath, changed: false } : { path: relPath, matchesExpected: false });
1480
+ return;
1481
+ }
1482
+ if (opts.write) {
1483
+ if (result.changed) {
1484
+ await writeFile(absPath, result.text, "utf8");
1485
+ }
1486
+ fileResults.push({ path: relPath, changed: result.changed });
1487
+ return;
1488
+ }
1489
+ fileResults.push({ path: relPath, matchesExpected: !result.changed });
1490
+ if (result.changed) {
1491
+ diagnostics.push({
1492
+ severity: "warning",
1493
+ code: FMT_NOT_CANONICAL,
1494
+ message: "file is not in canonical form (run `sightmap fmt --write` to fix)",
1495
+ file: relPath
1496
+ });
1497
+ }
1498
+ }
1499
+
1085
1500
  // src/cli/index.ts
1086
1501
  var program = new Command();
1087
1502
  program.name("sightmap").description("CLI for the Sightmap spec \u2014 validate, lint, match, explain.").version("0.1.0");
@@ -1158,6 +1573,23 @@ program.command("check-conventions [path]").description("Validate repo filename
1158
1573
  });
1159
1574
  process.exit(code);
1160
1575
  });
1576
+ program.command("fmt [path]").description("Canonicalize .sightmap/*.yaml files (default --check; --write to fix in place).").option("--check", "exit non-zero if any file is not canonical (default mode)").option("--write", "rewrite non-canonical files in place").option("--json", "machine-readable output").action(
1577
+ async (path, opts) => {
1578
+ if (opts.check === true && opts.write === true) {
1579
+ process.stderr.write("fatal: --check and --write are mutually exclusive\n");
1580
+ process.exit(2);
1581
+ }
1582
+ const code = await runFmt({
1583
+ path: path ?? ".sightmap",
1584
+ cwd: program.opts().cwd,
1585
+ check: opts.write !== true,
1586
+ // default true unless --write
1587
+ write: opts.write === true,
1588
+ json: opts.json === true
1589
+ });
1590
+ process.exit(code);
1591
+ }
1592
+ );
1161
1593
  program.parseAsync(process.argv).catch((err) => {
1162
1594
  process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}
1163
1595
  `);