@sightmap/sightmap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,909 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/validate.ts
7
+ import { readFile } from "fs/promises";
8
+ import { resolve as resolve2, relative } from "path";
9
+
10
+ // src/validate.ts
11
+ import yaml2 from "js-yaml";
12
+ import { Ajv2020 } from "ajv/dist/2020.js";
13
+
14
+ // src/diagnostics.ts
15
+ var PARSE_ERROR = "parse-error";
16
+ var SCHEMA_VALIDATION_FAILED = "schema-validation-failed";
17
+ var UNKNOWN_VERSION = "unknown-version";
18
+ var MERGE_COLLISION_VIEW = "merge-collision-view";
19
+ var MERGE_COLLISION_COMPONENT = "merge-collision-component";
20
+ var DUPLICATE_VIEW_NAME = "duplicate-view-name";
21
+ var DUPLICATE_ROUTE = "duplicate-route";
22
+ var ROUTE_SHADOWING = "route-shadowing";
23
+ var UNKNOWN_SOURCE = "unknown-source";
24
+ var SELECTOR_SYNTAX = "selector-syntax";
25
+
26
+ // src/parse.ts
27
+ import yaml from "js-yaml";
28
+ function parse(input, opts = {}) {
29
+ let doc;
30
+ if (typeof input === "string") {
31
+ try {
32
+ doc = yaml.load(input);
33
+ } catch (err) {
34
+ const msg = err instanceof Error ? err.message : String(err);
35
+ throw new Error(`YAML parse error: ${msg}`);
36
+ }
37
+ } else {
38
+ doc = { ...input };
39
+ }
40
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
41
+ throw new Error("Expected sightmap document to be an object at the root");
42
+ }
43
+ const obj = doc;
44
+ if (obj["version"] === void 0) {
45
+ throw new Error("Missing required `version` field");
46
+ }
47
+ if (obj["version"] !== 1) {
48
+ throw new Error(`Unsupported version: ${String(obj["version"])} (expected 1)`);
49
+ }
50
+ if (Array.isArray(obj["components"])) {
51
+ obj["components"] = obj["components"].map(normalizeComponent);
52
+ }
53
+ if (Array.isArray(obj["views"])) {
54
+ obj["views"] = obj["views"].map((v) => {
55
+ const out = { ...v };
56
+ if (Array.isArray(out["components"])) {
57
+ out["components"] = out["components"].map(normalizeComponent);
58
+ }
59
+ return out;
60
+ });
61
+ }
62
+ const fragment = {
63
+ ...obj,
64
+ __brand: "SightmapFragment",
65
+ ...opts.sourceFile !== void 0 ? { __sourceFile: opts.sourceFile } : {}
66
+ };
67
+ return fragment;
68
+ }
69
+ function normalizeComponent(c) {
70
+ const sel = c.selector;
71
+ const normalized = {
72
+ ...c,
73
+ selector: typeof sel === "string" ? [sel] : sel
74
+ };
75
+ if (Array.isArray(c.children)) {
76
+ normalized.children = c.children.map(normalizeComponent);
77
+ }
78
+ return normalized;
79
+ }
80
+
81
+ // src/validate.ts
82
+ import { existsSync, readFileSync } from "fs";
83
+ import { fileURLToPath } from "url";
84
+ import { resolve, dirname } from "path";
85
+ var __dirname = dirname(fileURLToPath(import.meta.url));
86
+ var schemaCandidates = [
87
+ resolve(__dirname, "./vendored/sightmap.schema.json"),
88
+ resolve(__dirname, "../vendored/sightmap.schema.json")
89
+ ];
90
+ var 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
+ var schema = JSON.parse(readFileSync(schemaPath, "utf8"));
97
+ var ajv = new Ajv2020({ allErrors: true, strict: false });
98
+ var ajvValidate = ajv.compile(schema);
99
+ function validate(input, opts = {}) {
100
+ const diagnostics = [];
101
+ let doc;
102
+ if (typeof input === "string") {
103
+ try {
104
+ doc = yaml2.load(input);
105
+ } catch (err) {
106
+ const e = err;
107
+ diagnostics.push({
108
+ severity: "error",
109
+ code: PARSE_ERROR,
110
+ message: e.message ?? "YAML parse error",
111
+ ...opts.sourceFile !== void 0 ? { file: opts.sourceFile } : {},
112
+ ...e.mark ? { loc: { line: (e.mark.line ?? 0) + 1, column: (e.mark.column ?? 0) + 1 } } : {}
113
+ });
114
+ return { ok: false, diagnostics };
115
+ }
116
+ } else {
117
+ doc = input;
118
+ }
119
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
120
+ diagnostics.push({
121
+ severity: "error",
122
+ code: SCHEMA_VALIDATION_FAILED,
123
+ message: "Expected document root to be an object",
124
+ ...opts.sourceFile !== void 0 ? { file: opts.sourceFile } : {}
125
+ });
126
+ return { ok: false, diagnostics };
127
+ }
128
+ const obj = doc;
129
+ if (obj["version"] !== 1) {
130
+ diagnostics.push({
131
+ severity: "error",
132
+ code: UNKNOWN_VERSION,
133
+ message: obj["version"] === void 0 ? "Missing required `version` field" : `Unsupported version: ${String(obj["version"])} (expected 1)`,
134
+ ...opts.sourceFile !== void 0 ? { file: opts.sourceFile } : {},
135
+ path: "/version"
136
+ });
137
+ return { ok: false, diagnostics };
138
+ }
139
+ const ok = ajvValidate(obj);
140
+ if (!ok) {
141
+ for (const e of ajvValidate.errors ?? []) {
142
+ diagnostics.push({
143
+ severity: "error",
144
+ code: SCHEMA_VALIDATION_FAILED,
145
+ message: `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim(),
146
+ ...opts.sourceFile !== void 0 ? { file: opts.sourceFile } : {},
147
+ ...e.instancePath ? { path: e.instancePath } : {}
148
+ });
149
+ }
150
+ return { ok: false, diagnostics };
151
+ }
152
+ let value;
153
+ try {
154
+ value = parse(obj, opts.sourceFile !== void 0 ? { sourceFile: opts.sourceFile } : {});
155
+ } catch (err) {
156
+ diagnostics.push({
157
+ severity: "error",
158
+ code: SCHEMA_VALIDATION_FAILED,
159
+ message: err instanceof Error ? err.message : String(err),
160
+ ...opts.sourceFile !== void 0 ? { file: opts.sourceFile } : {}
161
+ });
162
+ return { ok: false, diagnostics };
163
+ }
164
+ return { ok: true, value };
165
+ }
166
+
167
+ // src/cli/_fsHelpers.ts
168
+ import { readdir } from "fs/promises";
169
+ import { join, extname } from "path";
170
+ async function collectYamlFiles(dir) {
171
+ const out = [];
172
+ const entries = await readdir(dir, { withFileTypes: true });
173
+ for (const e of entries) {
174
+ const p = join(dir, e.name);
175
+ if (e.isDirectory()) {
176
+ out.push(...await collectYamlFiles(p));
177
+ } else if (e.isFile()) {
178
+ const ext = extname(p).toLowerCase();
179
+ if (ext === ".yaml" || ext === ".yml") out.push(p);
180
+ }
181
+ }
182
+ return out;
183
+ }
184
+
185
+ // src/cli/envelope.ts
186
+ function makeEnvelope(args) {
187
+ return {
188
+ ok: args.ok,
189
+ command: args.command,
190
+ diagnostics: [...args.diagnostics],
191
+ result: args.result
192
+ };
193
+ }
194
+ function emitJson(env) {
195
+ process.stdout.write(JSON.stringify(env, null, 2) + "\n");
196
+ }
197
+
198
+ // src/cli/humanFormat.ts
199
+ var SEV_TAG = {
200
+ error: "ERROR",
201
+ warning: "warn ",
202
+ info: "info "
203
+ };
204
+ function formatDiagnostics(diags) {
205
+ if (diags.length === 0) return "";
206
+ const lines = diags.map((d) => {
207
+ const where = [d.file, d.path, d.loc ? `${d.loc.line}:${d.loc.column}` : null].filter(Boolean).join(" ");
208
+ return `${SEV_TAG[d.severity]} ${d.code} ${where}
209
+ ${d.message}`;
210
+ });
211
+ return lines.join("\n");
212
+ }
213
+ function formatValidate(result, diags) {
214
+ const fileLines = result.files.map((f) => `${f.ok ? "ok " : "FAIL "} ${f.path}`).join("\n");
215
+ const diagLines = formatDiagnostics(diags);
216
+ return [fileLines, "", diagLines, "", `${result.files.length} file(s) checked.`].filter(Boolean).join("\n");
217
+ }
218
+ function formatLint(diags) {
219
+ if (diags.length === 0) return "lint: no issues.";
220
+ return formatDiagnostics(diags);
221
+ }
222
+ function formatMatch(r) {
223
+ const lines = [];
224
+ if (r.view === null) {
225
+ lines.push("no view matched.");
226
+ } else {
227
+ lines.push(`view ${r.view.name} (route ${r.view.route})`);
228
+ if (r.view.source !== void 0) lines.push(` source ${r.view.source}`);
229
+ }
230
+ for (const c of r.components) {
231
+ const chain = c.parentChain.length > 0 ? ` (in ${c.parentChain.join(" > ")})` : "";
232
+ lines.push(`comp ${c.name}${chain} ${c.scope} selector=${c.selector.join(" | ")}`);
233
+ }
234
+ for (const q of r.requests) {
235
+ lines.push(`req ${q.name} ${q.method ?? "ANY"} ${q.route}`);
236
+ }
237
+ if (r.memory.length > 0) {
238
+ lines.push("");
239
+ lines.push("[Guide]");
240
+ for (const m of r.memory) lines.push(` - ${m}`);
241
+ }
242
+ return lines.join("\n");
243
+ }
244
+ function formatExplain(r) {
245
+ if (r.hits.length === 0) return `no hits for "${r.query}".`;
246
+ const lines = [`${r.hits.length} hit(s) for "${r.query}":`];
247
+ for (const h of r.hits) {
248
+ const e = h.entry;
249
+ lines.push(` ${h.type} ${e.name ?? "?"} matchedAs=${h.matchedAs}${e.source !== void 0 ? ` src=${e.source}` : ""}`);
250
+ }
251
+ return lines.join("\n");
252
+ }
253
+
254
+ // src/cli/validate.ts
255
+ async function runValidate(opts) {
256
+ const dir = resolve2(opts.cwd, opts.path);
257
+ const files = (await collectYamlFiles(dir)).sort();
258
+ const fileResults = [];
259
+ const diagnostics = [];
260
+ for (const file of files) {
261
+ const relPath = relative(opts.cwd, file);
262
+ const text = await readFile(file, "utf8");
263
+ const r = validate(text, { sourceFile: relPath });
264
+ fileResults.push({ path: relPath, ok: r.ok });
265
+ if (!r.ok) diagnostics.push(...r.diagnostics);
266
+ }
267
+ const ok = diagnostics.length === 0;
268
+ if (opts.json) {
269
+ emitJson(
270
+ makeEnvelope({
271
+ command: "validate",
272
+ ok,
273
+ diagnostics,
274
+ result: { files: fileResults }
275
+ })
276
+ );
277
+ } else {
278
+ process.stdout.write(formatValidate({ files: fileResults }, diagnostics) + "\n");
279
+ }
280
+ return ok ? 0 : 1;
281
+ }
282
+
283
+ // src/cli/lint.ts
284
+ import { resolve as resolve5 } from "path";
285
+
286
+ // src/loadDirectory.ts
287
+ import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
288
+ import { join as join2, extname as extname2, relative as relative2, resolve as resolve3 } from "path";
289
+
290
+ // src/merge.ts
291
+ function merge(fragments) {
292
+ const sorted = [...fragments].sort((a, b) => {
293
+ const aFile = a.__sourceFile ?? "";
294
+ const bFile = b.__sourceFile ?? "";
295
+ return aFile < bFile ? -1 : aFile > bFile ? 1 : 0;
296
+ });
297
+ const views = [];
298
+ const globalComponents = [];
299
+ const globalRequests = [];
300
+ const fileMemory = [];
301
+ const diagnostics = [];
302
+ const seenViewNames = /* @__PURE__ */ new Map();
303
+ const seenComponentNames = /* @__PURE__ */ new Map();
304
+ for (const f of sorted) {
305
+ const file = f.__sourceFile ?? "<unknown>";
306
+ for (const v of f.views ?? []) {
307
+ const prev = seenViewNames.get(v.name);
308
+ if (prev !== void 0) {
309
+ diagnostics.push({
310
+ severity: "warning",
311
+ code: MERGE_COLLISION_VIEW,
312
+ message: `View name "${v.name}" defined in both "${prev}" and "${file}"; first occurrence wins.`,
313
+ file
314
+ });
315
+ } else {
316
+ seenViewNames.set(v.name, file);
317
+ }
318
+ views.push(v);
319
+ }
320
+ for (const c of f.components ?? []) {
321
+ const prev = seenComponentNames.get(c.name);
322
+ if (prev !== void 0) {
323
+ diagnostics.push({
324
+ severity: "warning",
325
+ code: MERGE_COLLISION_COMPONENT,
326
+ message: `Component name "${c.name}" defined in both "${prev}" and "${file}"; first occurrence wins.`,
327
+ file
328
+ });
329
+ } else {
330
+ seenComponentNames.set(c.name, file);
331
+ }
332
+ globalComponents.push(c);
333
+ }
334
+ for (const r of f.requests ?? []) {
335
+ globalRequests.push(r);
336
+ }
337
+ if (Array.isArray(f.memory) && f.memory.length > 0) {
338
+ fileMemory.push({ memory: [...f.memory], sourceFile: file });
339
+ }
340
+ }
341
+ return {
342
+ version: 1,
343
+ views,
344
+ globalComponents,
345
+ globalRequests,
346
+ fileMemory,
347
+ diagnostics,
348
+ __brand: "Sightmap"
349
+ };
350
+ }
351
+
352
+ // src/loadDirectory.ts
353
+ async function loadDirectory(dir, opts = {}) {
354
+ const absDir = resolve3(dir);
355
+ const root = opts.diagnosticRoot !== void 0 ? resolve3(opts.diagnosticRoot) : absDir;
356
+ const files = await collectYamlFiles2(absDir);
357
+ files.sort();
358
+ const fragments = [];
359
+ const loadDiagnostics = [];
360
+ for (const file of files) {
361
+ const relPath = relative2(root, file);
362
+ const text = await readFile2(file, "utf8");
363
+ const r = validate(text, { sourceFile: relPath });
364
+ if (r.ok) {
365
+ fragments.push(r.value);
366
+ } else {
367
+ loadDiagnostics.push(...r.diagnostics);
368
+ }
369
+ }
370
+ const merged = merge(fragments);
371
+ return {
372
+ ...merged,
373
+ diagnostics: [...loadDiagnostics, ...merged.diagnostics]
374
+ };
375
+ }
376
+ async function collectYamlFiles2(dir) {
377
+ const out = [];
378
+ const entries = await readdir2(dir, { withFileTypes: true });
379
+ for (const e of entries) {
380
+ const p = join2(dir, e.name);
381
+ if (e.isDirectory()) {
382
+ out.push(...await collectYamlFiles2(p));
383
+ } else if (e.isFile()) {
384
+ const ext = extname2(p).toLowerCase();
385
+ if (ext === ".yaml" || ext === ".yml") out.push(p);
386
+ }
387
+ }
388
+ return out;
389
+ }
390
+
391
+ // src/lintRules/duplicateName.ts
392
+ function duplicateName(sightmap) {
393
+ const seen = /* @__PURE__ */ new Map();
394
+ const out = [];
395
+ sightmap.views.forEach((v) => {
396
+ const count = (seen.get(v.name) ?? 0) + 1;
397
+ seen.set(v.name, count);
398
+ if (count > 1) {
399
+ out.push({
400
+ severity: "warning",
401
+ code: DUPLICATE_VIEW_NAME,
402
+ message: `Duplicate view name "${v.name}" (occurrence ${count}).`
403
+ });
404
+ }
405
+ });
406
+ return out;
407
+ }
408
+
409
+ // src/lintRules/duplicateRoute.ts
410
+ function duplicateRoute(sightmap) {
411
+ const seen = /* @__PURE__ */ new Map();
412
+ const out = [];
413
+ for (const v of sightmap.views) {
414
+ const prev = seen.get(v.route);
415
+ if (prev !== void 0) {
416
+ out.push({
417
+ severity: "warning",
418
+ code: DUPLICATE_ROUTE,
419
+ message: `Route "${v.route}" is defined by both "${prev}" and "${v.name}". Only "${prev}" will match.`
420
+ });
421
+ } else {
422
+ seen.set(v.route, v.name);
423
+ }
424
+ }
425
+ return out;
426
+ }
427
+
428
+ // src/routeMatch.ts
429
+ function canonicalizeUrl(input) {
430
+ let s = input;
431
+ const protoMatch = /^[a-z][a-z0-9+.-]*:\/\/[^/]*/i.exec(s);
432
+ if (protoMatch) {
433
+ s = s.slice(protoMatch[0].length) || "/";
434
+ }
435
+ const hash = s.indexOf("#");
436
+ if (hash !== -1) s = s.slice(0, hash);
437
+ const q = s.indexOf("?");
438
+ if (q !== -1) s = s.slice(0, q);
439
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
440
+ return s;
441
+ }
442
+ function routeMatch(pattern, url) {
443
+ const p = normalizePattern(pattern);
444
+ const u = canonicalizeUrl(url);
445
+ const patternSegs = splitSegments(p);
446
+ const urlSegs = splitSegments(u);
447
+ return matchSegs(patternSegs, urlSegs);
448
+ }
449
+ function normalizePattern(p) {
450
+ let s = p.split("/").map((seg) => seg.startsWith(":") ? "*" : seg).join("/");
451
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
452
+ return s;
453
+ }
454
+ function splitSegments(path) {
455
+ if (path === "/" || path === "") return [];
456
+ const trimmed = path.startsWith("/") ? path.slice(1) : path;
457
+ return trimmed.split("/");
458
+ }
459
+ function matchSegs(pattern, url) {
460
+ if (pattern.length === 0 && url.length === 0) return true;
461
+ if (pattern.length === 0) return false;
462
+ const head = pattern[0];
463
+ const rest = pattern.slice(1);
464
+ if (head === "**") {
465
+ if (rest.length === 0) return true;
466
+ for (let i = 0; i <= url.length; i++) {
467
+ if (matchSegs(rest, url.slice(i))) return true;
468
+ }
469
+ return false;
470
+ }
471
+ if (url.length === 0) return false;
472
+ if (head === "*" || head === url[0]) {
473
+ return matchSegs(rest, url.slice(1));
474
+ }
475
+ return false;
476
+ }
477
+
478
+ // src/lintRules/routeShadowing.ts
479
+ function routeShadowing(sightmap) {
480
+ const out = [];
481
+ const views = sightmap.views;
482
+ for (let j = 1; j < views.length; j++) {
483
+ const later = views[j];
484
+ const representative = makeRepresentativeUrl(later.route);
485
+ for (let i = 0; i < j; i++) {
486
+ const earlier = views[i];
487
+ if (earlier.route === later.route) continue;
488
+ if (routeMatch(earlier.route, representative)) {
489
+ out.push({
490
+ severity: "warning",
491
+ code: ROUTE_SHADOWING,
492
+ message: `Route "${later.route}" (view "${later.name}") is shadowed by earlier route "${earlier.route}" (view "${earlier.name}"); first-match-wins makes it unreachable.`
493
+ });
494
+ break;
495
+ }
496
+ }
497
+ }
498
+ return out;
499
+ }
500
+ function makeRepresentativeUrl(route) {
501
+ const result = route.split("/").filter((seg) => seg !== "**").map((seg) => {
502
+ if (seg === "*") return "__seg__";
503
+ if (seg.startsWith(":")) return "__seg__";
504
+ return seg;
505
+ }).join("/").replace(/^\/+/, "/");
506
+ return result || "/";
507
+ }
508
+
509
+ // src/lintRules/unknownSource.ts
510
+ import { stat } from "fs/promises";
511
+ import { resolve as resolve4 } from "path";
512
+ async function unknownSource(sightmap, opts = {}) {
513
+ const root = opts.root ?? process.cwd();
514
+ const out = [];
515
+ const seen = /* @__PURE__ */ new Set();
516
+ const check = async (source, label) => {
517
+ if (source === void 0) return;
518
+ if (seen.has(source)) return;
519
+ seen.add(source);
520
+ try {
521
+ await stat(resolve4(root, source));
522
+ } catch {
523
+ out.push({
524
+ severity: "warning",
525
+ code: UNKNOWN_SOURCE,
526
+ message: `${label}: source path "${source}" does not exist on disk (relative to ${root}).`
527
+ });
528
+ }
529
+ };
530
+ for (const v of sightmap.views) {
531
+ await check(v.source, `view "${v.name}"`);
532
+ for (const c of v.components ?? []) {
533
+ await walkComponent(c, `component "${c.name}"`, check);
534
+ }
535
+ }
536
+ for (const c of sightmap.globalComponents) {
537
+ await walkComponent(c, `component "${c.name}"`, check);
538
+ }
539
+ for (const r of sightmap.globalRequests) {
540
+ await check(r.source, `request "${r.name}"`);
541
+ }
542
+ return out;
543
+ }
544
+ async function walkComponent(c, label, check) {
545
+ await check(c.source, label);
546
+ for (const child of c.children ?? []) {
547
+ await walkComponent(child, `component "${child.name}"`, check);
548
+ }
549
+ }
550
+
551
+ // src/lintRules/selectorSyntax.ts
552
+ import { parse as parseSelector } from "css-what";
553
+ function selectorSyntax(sightmap) {
554
+ const out = [];
555
+ const visit = (c, scope) => {
556
+ const selectors = Array.isArray(c.selector) ? c.selector : [c.selector];
557
+ selectors.forEach((sel, i) => {
558
+ try {
559
+ parseSelector(sel);
560
+ } catch (err) {
561
+ const msg = err instanceof Error ? err.message : String(err);
562
+ out.push({
563
+ severity: "warning",
564
+ code: SELECTOR_SYNTAX,
565
+ message: `${scope}: selector at index ${i} ("${sel}") failed to parse: ${msg}`
566
+ });
567
+ }
568
+ });
569
+ for (const child of c.children ?? []) visit(child, `component "${child.name}"`);
570
+ };
571
+ for (const c of sightmap.globalComponents) visit(c, `component "${c.name}"`);
572
+ for (const v of sightmap.views) {
573
+ for (const c of v.components ?? []) visit(c, `component "${c.name}"`);
574
+ }
575
+ return out;
576
+ }
577
+
578
+ // src/lint.ts
579
+ async function lint(sightmap, opts = {}) {
580
+ const enabled = (code) => opts.rules?.[code] !== false;
581
+ const out = [];
582
+ if (enabled("duplicate-view-name")) out.push(...duplicateName(sightmap));
583
+ if (enabled("duplicate-route")) out.push(...duplicateRoute(sightmap));
584
+ if (enabled("route-shadowing")) out.push(...routeShadowing(sightmap));
585
+ if (enabled("selector-syntax")) out.push(...selectorSyntax(sightmap));
586
+ if (enabled("unknown-source")) {
587
+ out.push(...await unknownSource(sightmap, opts.root !== void 0 ? { root: opts.root } : {}));
588
+ }
589
+ return out;
590
+ }
591
+
592
+ // src/cli/lint.ts
593
+ async function runLint(opts) {
594
+ const dir = resolve5(opts.cwd, opts.path);
595
+ const sightmap = await loadDirectory(dir, { diagnosticRoot: opts.cwd });
596
+ const ruleConfig = parseRules(opts.rules);
597
+ const lintDiags = await lint(sightmap, {
598
+ ...ruleConfig !== void 0 ? { rules: ruleConfig } : {},
599
+ root: opts.cwd
600
+ });
601
+ const all = [...sightmap.diagnostics, ...lintDiags];
602
+ const hasError = all.some((d) => d.severity === "error");
603
+ const hasWarning = all.some((d) => d.severity === "warning");
604
+ let code = 0;
605
+ if (hasError) code = 1;
606
+ else if (opts.strict && hasWarning) code = 1;
607
+ if (opts.json) {
608
+ emitJson(
609
+ makeEnvelope({
610
+ command: "lint",
611
+ ok: code === 0,
612
+ diagnostics: all,
613
+ result: {}
614
+ })
615
+ );
616
+ } else {
617
+ process.stdout.write(formatLint(all) + "\n");
618
+ }
619
+ return code;
620
+ }
621
+ function parseRules(spec) {
622
+ if (spec === void 0) return void 0;
623
+ const out = {};
624
+ for (const tok of spec.split(",").map((s) => s.trim()).filter(Boolean)) {
625
+ if (tok.startsWith("-")) out[tok.slice(1)] = false;
626
+ else out[tok] = true;
627
+ }
628
+ return out;
629
+ }
630
+
631
+ // src/cli/match.ts
632
+ import { resolve as resolve6 } from "path";
633
+
634
+ // src/resolver.ts
635
+ function resolveByUrl(sightmap, url, method) {
636
+ let matchedView = null;
637
+ for (const v of sightmap.views) {
638
+ if (routeMatch(v.route, url)) {
639
+ matchedView = v;
640
+ break;
641
+ }
642
+ }
643
+ const components = [];
644
+ for (const c of sightmap.globalComponents) {
645
+ components.push(...flattenComponent(c, [], "global", void 0));
646
+ }
647
+ if (matchedView !== null && Array.isArray(matchedView.components)) {
648
+ for (const c of matchedView.components) {
649
+ components.push(...flattenComponent(c, [], "view-scoped", matchedView.name));
650
+ }
651
+ }
652
+ const requests = [];
653
+ const requestPool = [
654
+ ...sightmap.globalRequests,
655
+ ...matchedView?.requests ?? []
656
+ ];
657
+ for (const req of requestPool) {
658
+ if (!routeMatch(req.route, url)) continue;
659
+ if (method !== void 0 && req.method !== void 0 && req.method !== method) continue;
660
+ requests.push(toResolvedRequest(req));
661
+ }
662
+ const memory = [];
663
+ for (const fm of sightmap.fileMemory) memory.push(...fm.memory);
664
+ if (matchedView?.memory) memory.push(...matchedView.memory);
665
+ return {
666
+ view: matchedView !== null ? toResolvedView(matchedView) : null,
667
+ components,
668
+ requests,
669
+ memory
670
+ };
671
+ }
672
+ function resolveByName(sightmap, name) {
673
+ const hits = [];
674
+ for (const v of sightmap.views) {
675
+ if (v.name === name) hits.push({ type: "view", matchedAs: "name", entry: toResolvedView(v) });
676
+ }
677
+ for (const c of sightmap.globalComponents) {
678
+ for (const rc of flattenComponent(c, [], "global", void 0)) {
679
+ if (rc.name === name) hits.push({ type: "component", matchedAs: "name", entry: rc });
680
+ }
681
+ }
682
+ for (const v of sightmap.views) {
683
+ for (const c of v.components ?? []) {
684
+ for (const rc of flattenComponent(c, [], "view-scoped", v.name)) {
685
+ if (rc.name === name) hits.push({ type: "component", matchedAs: "name", entry: rc });
686
+ }
687
+ }
688
+ }
689
+ for (const r of sightmap.globalRequests) {
690
+ if (r.name === name) hits.push({ type: "request", matchedAs: "name", entry: toResolvedRequest(r) });
691
+ }
692
+ for (const v of sightmap.views) {
693
+ for (const r of v.requests ?? []) {
694
+ if (r.name === name) hits.push({ type: "request", matchedAs: "name", entry: toResolvedRequest(r) });
695
+ }
696
+ }
697
+ return hits;
698
+ }
699
+ function resolveBySourcePath(sightmap, path) {
700
+ const hits = [];
701
+ for (const v of sightmap.views) {
702
+ if (v.source === path) hits.push({ type: "view", matchedAs: "path", entry: toResolvedView(v) });
703
+ }
704
+ for (const c of sightmap.globalComponents) {
705
+ for (const rc of flattenComponent(c, [], "global", void 0)) {
706
+ if (rc.source === path) hits.push({ type: "component", matchedAs: "path", entry: rc });
707
+ }
708
+ }
709
+ for (const v of sightmap.views) {
710
+ for (const c of v.components ?? []) {
711
+ for (const rc of flattenComponent(c, [], "view-scoped", v.name)) {
712
+ if (rc.source === path) hits.push({ type: "component", matchedAs: "path", entry: rc });
713
+ }
714
+ }
715
+ }
716
+ for (const r of sightmap.globalRequests) {
717
+ if (r.source === path) hits.push({ type: "request", matchedAs: "path", entry: toResolvedRequest(r) });
718
+ }
719
+ return hits;
720
+ }
721
+ function flattenComponent(c, parentChain, scope, scopedToView) {
722
+ const out = [];
723
+ const selector = Array.isArray(c.selector) ? c.selector : [c.selector];
724
+ out.push({
725
+ name: c.name,
726
+ selector,
727
+ ...c.source !== void 0 ? { source: c.source } : {},
728
+ ...c.description !== void 0 ? { description: c.description } : {},
729
+ memory: c.memory ? [...c.memory] : [],
730
+ parentChain: [...parentChain],
731
+ scope,
732
+ ...scopedToView !== void 0 ? { scopedToView } : {},
733
+ definedIn: { file: "<unknown>" }
734
+ });
735
+ for (const child of c.children ?? []) {
736
+ out.push(...flattenComponent(child, [...parentChain, c.name], scope, scopedToView));
737
+ }
738
+ return out;
739
+ }
740
+ function toResolvedView(v) {
741
+ return {
742
+ name: v.name,
743
+ route: v.route,
744
+ ...v.source !== void 0 ? { source: v.source } : {},
745
+ ...v.description !== void 0 ? { description: v.description } : {},
746
+ memory: v.memory ? [...v.memory] : [],
747
+ definedIn: { file: "<unknown>" }
748
+ };
749
+ }
750
+ function toResolvedRequest(r) {
751
+ return {
752
+ name: r.name,
753
+ route: r.route,
754
+ ...r.method !== void 0 ? { method: r.method } : {},
755
+ ...r.source !== void 0 ? { source: r.source } : {},
756
+ ...r.description !== void 0 ? { description: r.description } : {},
757
+ ...r.request !== void 0 ? { request: { fields: r.request.fields ?? [] } } : {},
758
+ ...r.response !== void 0 ? { response: { fields: r.response.fields ?? [] } } : {},
759
+ ...r.headers !== void 0 ? { headers: r.headers } : {},
760
+ memory: r.memory ? [...r.memory] : [],
761
+ definedIn: { file: "<unknown>" }
762
+ };
763
+ }
764
+
765
+ // src/match.ts
766
+ function match(sightmap, opts) {
767
+ return resolveByUrl(sightmap, opts.url, opts.method);
768
+ }
769
+
770
+ // src/cli/match.ts
771
+ async function runMatch(opts) {
772
+ const dir = resolve6(opts.cwd, opts.path);
773
+ const sightmap = await loadDirectory(dir, { diagnosticRoot: opts.cwd });
774
+ const result = match(sightmap, {
775
+ url: opts.url,
776
+ ...opts.method !== void 0 ? { method: opts.method } : {}
777
+ });
778
+ const code = opts.requireView && result.view === null ? 1 : 0;
779
+ if (opts.json) {
780
+ emitJson(
781
+ makeEnvelope({
782
+ command: "match",
783
+ ok: code === 0,
784
+ diagnostics: [...sightmap.diagnostics],
785
+ result
786
+ })
787
+ );
788
+ } else {
789
+ process.stdout.write(formatMatch(result) + "\n");
790
+ }
791
+ return code;
792
+ }
793
+
794
+ // src/cli/explain.ts
795
+ import { resolve as resolve7 } from "path";
796
+
797
+ // src/explain.ts
798
+ function explain(sightmap, query, opts = {}) {
799
+ const byName = opts.by === "path" ? [] : resolveByName(sightmap, query);
800
+ const byPath = opts.by === "name" ? [] : resolveBySourcePath(sightmap, query);
801
+ const key = (h) => `${h.type}:${h.entry.name}`;
802
+ const namedKeys = new Set(byName.map(key));
803
+ const pathKeys = new Set(byPath.map(key));
804
+ const merged = [];
805
+ for (const h of byName) {
806
+ if (pathKeys.has(key(h))) {
807
+ merged.push(withMatchedAs(h, "name-and-path"));
808
+ } else {
809
+ merged.push(h);
810
+ }
811
+ }
812
+ for (const h of byPath) {
813
+ if (!namedKeys.has(key(h))) {
814
+ merged.push(h);
815
+ }
816
+ }
817
+ const filtered = opts.type !== void 0 ? merged.filter((h) => h.type === opts.type) : merged;
818
+ return { query, hits: filtered };
819
+ }
820
+ function withMatchedAs(h, matchedAs) {
821
+ switch (h.type) {
822
+ case "view":
823
+ return { type: "view", matchedAs, entry: h.entry };
824
+ case "component":
825
+ return { type: "component", matchedAs, entry: h.entry };
826
+ case "request":
827
+ return { type: "request", matchedAs, entry: h.entry };
828
+ }
829
+ }
830
+
831
+ // src/cli/explain.ts
832
+ async function runExplain(opts) {
833
+ const dir = resolve7(opts.cwd, opts.path);
834
+ const sightmap = await loadDirectory(dir, { diagnosticRoot: opts.cwd });
835
+ const result = explain(sightmap, opts.query, {
836
+ ...opts.by !== void 0 ? { by: opts.by } : {},
837
+ ...opts.type !== void 0 ? { type: opts.type } : {}
838
+ });
839
+ const code = opts.requireHit && result.hits.length === 0 ? 1 : 0;
840
+ if (opts.json) {
841
+ emitJson(
842
+ makeEnvelope({
843
+ command: "explain",
844
+ ok: code === 0,
845
+ diagnostics: [...sightmap.diagnostics],
846
+ result
847
+ })
848
+ );
849
+ } else {
850
+ process.stdout.write(formatExplain(result) + "\n");
851
+ }
852
+ return code;
853
+ }
854
+
855
+ // src/cli/index.ts
856
+ var program = new Command();
857
+ program.name("sightmap").description("CLI for the Sightmap spec \u2014 validate, lint, match, explain.").version("0.1.0");
858
+ program.option("--cwd <dir>", "working directory (resolves [path] arguments)", process.cwd());
859
+ program.command("validate [path]").description("Validate .sightmap/ against the schema.").option("--json", "machine-readable output").action(async (path, opts) => {
860
+ const code = await runValidate({
861
+ path: path ?? ".sightmap",
862
+ cwd: program.opts().cwd,
863
+ json: opts.json === true
864
+ });
865
+ process.exit(code);
866
+ });
867
+ program.command("lint [path]").description("Run quality checks beyond schema.").option("--json", "machine-readable output").option("--strict", "treat all warnings as errors (exit 1)").option("--rules <list>", "comma-separated list of rule codes to disable (prefix '-') or only-enable").action(async (path, opts) => {
868
+ const code = await runLint({
869
+ path: path ?? ".sightmap",
870
+ cwd: program.opts().cwd,
871
+ json: opts.json === true,
872
+ strict: opts.strict === true,
873
+ ...opts.rules !== void 0 ? { rules: opts.rules } : {}
874
+ });
875
+ process.exit(code);
876
+ });
877
+ program.command("match <url> [path]").description("Resolve sightmap context for a URL.").option("--method <m>", "HTTP method filter for requests").option("--json", "machine-readable output").option("--require-view", "exit 1 if no view matched").action(
878
+ async (url, path, opts) => {
879
+ const code = await runMatch({
880
+ url,
881
+ path: path ?? ".sightmap",
882
+ cwd: program.opts().cwd,
883
+ json: opts.json === true,
884
+ requireView: opts.requireView === true,
885
+ ...opts.method !== void 0 ? { method: opts.method } : {}
886
+ });
887
+ process.exit(code);
888
+ }
889
+ );
890
+ program.command("explain <query> [path]").description("Look up sightmap entries by name and/or source path.").option("--by <kind>", "scope to 'name' or 'path'").option("--type <kind>", "scope to 'view', 'component', or 'request'").option("--json", "machine-readable output").option("--require-hit", "exit 1 if zero hits").action(
891
+ async (query, path, opts) => {
892
+ const code = await runExplain({
893
+ query,
894
+ path: path ?? ".sightmap",
895
+ cwd: program.opts().cwd,
896
+ json: opts.json === true,
897
+ requireHit: opts.requireHit === true,
898
+ ...opts.by !== void 0 ? { by: opts.by } : {},
899
+ ...opts.type !== void 0 ? { type: opts.type } : {}
900
+ });
901
+ process.exit(code);
902
+ }
903
+ );
904
+ program.parseAsync(process.argv).catch((err) => {
905
+ process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}
906
+ `);
907
+ process.exit(2);
908
+ });
909
+ //# sourceMappingURL=index.js.map