@sightmap/sightmap 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/browser-ChD_xQt8.d.ts +253 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +353 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli/index.js +336 -79
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +10 -252
- package/dist/index.js +74 -29
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +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":"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","names":[]}
|
package/dist/cli/index.js
CHANGED
|
@@ -22,6 +22,8 @@ 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 CONVENTION_SEP_FILENAME = "convention.sep-filename";
|
|
26
|
+
var CONVENTION_FIXTURE_DIRNAME = "convention.fixture-dirname";
|
|
25
27
|
|
|
26
28
|
// src/parse.ts
|
|
27
29
|
import yaml from "js-yaml";
|
|
@@ -67,6 +69,12 @@ function parse(input, opts = {}) {
|
|
|
67
69
|
return fragment;
|
|
68
70
|
}
|
|
69
71
|
function normalizeComponent(c) {
|
|
72
|
+
if (c["selectors"] !== void 0 && c.selector === void 0) {
|
|
73
|
+
const name = c.name ?? "(unnamed)";
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Component "${name}": use \`selector\` (singular), not \`selectors\` (plural). \`selector\` accepts either a single string or an array of strings.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
70
78
|
const sel = c.selector;
|
|
71
79
|
const normalized = {
|
|
72
80
|
...c,
|
|
@@ -82,20 +90,25 @@ function normalizeComponent(c) {
|
|
|
82
90
|
import { existsSync, readFileSync } from "fs";
|
|
83
91
|
import { fileURLToPath } from "url";
|
|
84
92
|
import { resolve, dirname } from "path";
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
93
|
+
var _ajvValidate;
|
|
94
|
+
function getValidator() {
|
|
95
|
+
if (_ajvValidate) return _ajvValidate;
|
|
96
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
const schemaCandidates = [
|
|
98
|
+
resolve(__dirname, "./vendored/sightmap.schema.json"),
|
|
99
|
+
resolve(__dirname, "../vendored/sightmap.schema.json")
|
|
100
|
+
];
|
|
101
|
+
const schemaPath = schemaCandidates.find((p) => existsSync(p));
|
|
102
|
+
if (schemaPath === void 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`vendored sightmap.schema.json not found. Looked in: ${schemaCandidates.join(", ")}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const schema = JSON.parse(readFileSync(schemaPath, "utf8"));
|
|
108
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
109
|
+
_ajvValidate = ajv.compile(schema);
|
|
110
|
+
return _ajvValidate;
|
|
95
111
|
}
|
|
96
|
-
var schema = JSON.parse(readFileSync(schemaPath, "utf8"));
|
|
97
|
-
var ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
98
|
-
var ajvValidate = ajv.compile(schema);
|
|
99
112
|
function validate(input, opts = {}) {
|
|
100
113
|
const diagnostics = [];
|
|
101
114
|
let doc;
|
|
@@ -136,6 +149,7 @@ function validate(input, opts = {}) {
|
|
|
136
149
|
});
|
|
137
150
|
return { ok: false, diagnostics };
|
|
138
151
|
}
|
|
152
|
+
const ajvValidate = getValidator();
|
|
139
153
|
const ok = ajvValidate(obj);
|
|
140
154
|
if (!ok) {
|
|
141
155
|
for (const e of ajvValidate.errors ?? []) {
|
|
@@ -241,6 +255,12 @@ function formatMatch(r) {
|
|
|
241
255
|
}
|
|
242
256
|
return lines.join("\n");
|
|
243
257
|
}
|
|
258
|
+
function formatCheckConventions(diags) {
|
|
259
|
+
if (diags.length === 0) return "check-conventions: ok";
|
|
260
|
+
return formatDiagnostics(diags) + `
|
|
261
|
+
|
|
262
|
+
${diags.length} convention violation(s).`;
|
|
263
|
+
}
|
|
244
264
|
function formatExplain(r) {
|
|
245
265
|
if (r.hits.length === 0) return `no hits for "${r.query}".`;
|
|
246
266
|
const lines = [`${r.hits.length} hit(s) for "${r.query}":`];
|
|
@@ -425,85 +445,41 @@ function duplicateRoute(sightmap) {
|
|
|
425
445
|
return out;
|
|
426
446
|
}
|
|
427
447
|
|
|
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
448
|
// src/lintRules/routeShadowing.ts
|
|
479
449
|
function routeShadowing(sightmap) {
|
|
480
450
|
const out = [];
|
|
481
451
|
const views = sightmap.views;
|
|
482
452
|
for (let j = 1; j < views.length; j++) {
|
|
483
453
|
const later = views[j];
|
|
484
|
-
const
|
|
454
|
+
const laterKey = matchSetKey(later.route);
|
|
455
|
+
const laterScore = specificity(later.route);
|
|
485
456
|
for (let i = 0; i < j; i++) {
|
|
486
457
|
const earlier = views[i];
|
|
487
458
|
if (earlier.route === later.route) continue;
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
|
|
459
|
+
if (matchSetKey(earlier.route) !== laterKey) continue;
|
|
460
|
+
if (specificity(earlier.route) < laterScore) continue;
|
|
461
|
+
out.push({
|
|
462
|
+
severity: "warning",
|
|
463
|
+
code: ROUTE_SHADOWING,
|
|
464
|
+
message: `Route "${later.route}" (view "${later.name}") is shadowed by earlier route "${earlier.route}" (view "${earlier.name}"); they match the same URLs and the earlier route is at least as specific, making this route unreachable.`
|
|
465
|
+
});
|
|
466
|
+
break;
|
|
496
467
|
}
|
|
497
468
|
}
|
|
498
469
|
return out;
|
|
499
470
|
}
|
|
500
|
-
function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
471
|
+
function matchSetKey(route) {
|
|
472
|
+
return route.split("/").map((seg) => seg.startsWith(":") ? "*" : seg).join("/");
|
|
473
|
+
}
|
|
474
|
+
function specificity(route) {
|
|
475
|
+
let total = 0;
|
|
476
|
+
for (const seg of route.split("/")) {
|
|
477
|
+
if (seg === "" || seg === "**") continue;
|
|
478
|
+
if (seg === "*") total += 1;
|
|
479
|
+
else if (seg.startsWith(":")) total += 2;
|
|
480
|
+
else total += 3;
|
|
481
|
+
}
|
|
482
|
+
return total;
|
|
507
483
|
}
|
|
508
484
|
|
|
509
485
|
// src/lintRules/unknownSource.ts
|
|
@@ -631,6 +607,56 @@ function parseRules(spec) {
|
|
|
631
607
|
// src/cli/match.ts
|
|
632
608
|
import { resolve as resolve6 } from "path";
|
|
633
609
|
|
|
610
|
+
// src/routeMatch.ts
|
|
611
|
+
function canonicalizeUrl(input) {
|
|
612
|
+
let s = input;
|
|
613
|
+
const protoMatch = /^[a-z][a-z0-9+.-]*:\/\/[^/]*/i.exec(s);
|
|
614
|
+
if (protoMatch) {
|
|
615
|
+
s = s.slice(protoMatch[0].length) || "/";
|
|
616
|
+
}
|
|
617
|
+
const hash = s.indexOf("#");
|
|
618
|
+
if (hash !== -1) s = s.slice(0, hash);
|
|
619
|
+
const q = s.indexOf("?");
|
|
620
|
+
if (q !== -1) s = s.slice(0, q);
|
|
621
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
622
|
+
return s;
|
|
623
|
+
}
|
|
624
|
+
function routeMatch(pattern, url) {
|
|
625
|
+
const p = normalizePattern(pattern);
|
|
626
|
+
const u = canonicalizeUrl(url);
|
|
627
|
+
const patternSegs = splitSegments(p);
|
|
628
|
+
const urlSegs = splitSegments(u);
|
|
629
|
+
return matchSegs(patternSegs, urlSegs);
|
|
630
|
+
}
|
|
631
|
+
function normalizePattern(p) {
|
|
632
|
+
let s = p.split("/").map((seg) => seg.startsWith(":") ? "*" : seg).join("/");
|
|
633
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
634
|
+
return s;
|
|
635
|
+
}
|
|
636
|
+
function splitSegments(path) {
|
|
637
|
+
if (path === "/" || path === "") return [];
|
|
638
|
+
const trimmed = path.startsWith("/") ? path.slice(1) : path;
|
|
639
|
+
return trimmed.split("/");
|
|
640
|
+
}
|
|
641
|
+
function matchSegs(pattern, url) {
|
|
642
|
+
if (pattern.length === 0 && url.length === 0) return true;
|
|
643
|
+
if (pattern.length === 0) return false;
|
|
644
|
+
const head = pattern[0];
|
|
645
|
+
const rest = pattern.slice(1);
|
|
646
|
+
if (head === "**") {
|
|
647
|
+
if (rest.length === 0) return true;
|
|
648
|
+
for (let i = 0; i <= url.length; i++) {
|
|
649
|
+
if (matchSegs(rest, url.slice(i))) return true;
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if (url.length === 0) return false;
|
|
654
|
+
if (head === "*" || head === url[0]) {
|
|
655
|
+
return matchSegs(rest, url.slice(1));
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
|
|
634
660
|
// src/resolver.ts
|
|
635
661
|
function resolveByUrl(sightmap, url, method) {
|
|
636
662
|
let matchedView = null;
|
|
@@ -852,6 +878,210 @@ async function runExplain(opts) {
|
|
|
852
878
|
return code;
|
|
853
879
|
}
|
|
854
880
|
|
|
881
|
+
// src/cli/check.ts
|
|
882
|
+
import { resolve as resolve8, relative as relative3 } from "path";
|
|
883
|
+
async function runCheck(opts) {
|
|
884
|
+
const dir = resolve8(opts.cwd, opts.path);
|
|
885
|
+
const files = (await collectYamlFiles(dir)).sort();
|
|
886
|
+
const sightmap = await loadDirectory(dir, { diagnosticRoot: opts.cwd });
|
|
887
|
+
const diagnostics = [...sightmap.diagnostics];
|
|
888
|
+
if (opts.level === "quality") {
|
|
889
|
+
const lintDiags = await lint(sightmap, { root: opts.cwd });
|
|
890
|
+
diagnostics.push(...lintDiags);
|
|
891
|
+
}
|
|
892
|
+
const flagged = new Set(
|
|
893
|
+
diagnostics.filter((d) => d.file !== void 0).map((d) => d.file)
|
|
894
|
+
);
|
|
895
|
+
const fileResults = files.map((file) => {
|
|
896
|
+
const relPath = relative3(opts.cwd, file);
|
|
897
|
+
return { path: relPath, ok: !flagged.has(relPath) };
|
|
898
|
+
});
|
|
899
|
+
const ok = diagnostics.length === 0;
|
|
900
|
+
if (opts.json) {
|
|
901
|
+
emitJson(
|
|
902
|
+
makeEnvelope({
|
|
903
|
+
command: "check",
|
|
904
|
+
ok,
|
|
905
|
+
diagnostics,
|
|
906
|
+
result: {
|
|
907
|
+
files: fileResults,
|
|
908
|
+
level: opts.level,
|
|
909
|
+
views: sightmap.views.length
|
|
910
|
+
}
|
|
911
|
+
})
|
|
912
|
+
);
|
|
913
|
+
} else {
|
|
914
|
+
process.stdout.write(formatValidate({ files: fileResults }, diagnostics) + "\n");
|
|
915
|
+
}
|
|
916
|
+
return ok ? 0 : 1;
|
|
917
|
+
}
|
|
918
|
+
async function readAllStdin() {
|
|
919
|
+
const chunks = [];
|
|
920
|
+
for await (const chunk of process.stdin) {
|
|
921
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
922
|
+
}
|
|
923
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
924
|
+
}
|
|
925
|
+
async function runCheckFromStdin(opts) {
|
|
926
|
+
const text = opts.input ?? await readAllStdin();
|
|
927
|
+
const sourceFile = "<stdin>";
|
|
928
|
+
const diagnostics = [];
|
|
929
|
+
const validation = validate(text, { sourceFile });
|
|
930
|
+
let viewCount = 0;
|
|
931
|
+
if (!validation.ok) {
|
|
932
|
+
diagnostics.push(...validation.diagnostics);
|
|
933
|
+
} else {
|
|
934
|
+
const sightmap = merge([validation.value]);
|
|
935
|
+
viewCount = sightmap.views.length;
|
|
936
|
+
if (opts.level === "quality") {
|
|
937
|
+
diagnostics.push(...sightmap.diagnostics);
|
|
938
|
+
const lintDiags = await lint(sightmap);
|
|
939
|
+
diagnostics.push(...lintDiags);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const ok = diagnostics.length === 0;
|
|
943
|
+
const fileResults = [{ path: sourceFile, ok }];
|
|
944
|
+
if (opts.json) {
|
|
945
|
+
emitJson(
|
|
946
|
+
makeEnvelope({
|
|
947
|
+
command: "check",
|
|
948
|
+
ok,
|
|
949
|
+
diagnostics,
|
|
950
|
+
result: { files: fileResults, level: opts.level, views: viewCount }
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
} else {
|
|
954
|
+
process.stdout.write(formatValidate({ files: fileResults }, diagnostics) + "\n");
|
|
955
|
+
}
|
|
956
|
+
return ok ? 0 : 1;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/cli/checkConventions.ts
|
|
960
|
+
import { resolve as resolve9 } from "path";
|
|
961
|
+
|
|
962
|
+
// src/conventions/walker.ts
|
|
963
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
964
|
+
import { join as join3, relative as relative4 } from "path";
|
|
965
|
+
import { existsSync as existsSync2 } from "fs";
|
|
966
|
+
|
|
967
|
+
// src/conventions/slug.ts
|
|
968
|
+
var SLUG_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
969
|
+
function isValidSlug(s) {
|
|
970
|
+
return SLUG_REGEX.test(s);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/conventions/rules.ts
|
|
974
|
+
var SEP_FILE_RE = /^seps\/(\d{4})-(.+)\.md$/;
|
|
975
|
+
var FIXTURE_DIR_RE = /^conformance\/(\d{3})-(.+)\.fixture$/;
|
|
976
|
+
function checkSepFilename(relPath) {
|
|
977
|
+
const m = relPath.match(SEP_FILE_RE);
|
|
978
|
+
if (!m) {
|
|
979
|
+
return mkFail(
|
|
980
|
+
CONVENTION_SEP_FILENAME,
|
|
981
|
+
relPath,
|
|
982
|
+
`SEP filename must match seps/NNNN-{slug}.md (got "${relPath}")`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const slug = m[2];
|
|
986
|
+
if (!isValidSlug(slug)) {
|
|
987
|
+
return mkFail(
|
|
988
|
+
CONVENTION_SEP_FILENAME,
|
|
989
|
+
relPath,
|
|
990
|
+
`SEP slug "${slug}" violates ^[a-z0-9]+(-[a-z0-9]+)*$`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
return { ok: true };
|
|
994
|
+
}
|
|
995
|
+
function checkFixtureDirname(relPath) {
|
|
996
|
+
const m = relPath.match(FIXTURE_DIR_RE);
|
|
997
|
+
if (!m) {
|
|
998
|
+
if (relPath.startsWith("conformance/") && !relPath.endsWith(".fixture")) {
|
|
999
|
+
return mkFail(
|
|
1000
|
+
CONVENTION_FIXTURE_DIRNAME,
|
|
1001
|
+
relPath,
|
|
1002
|
+
`fixture directory must match conformance/NNN-{slug}.fixture/ (got "${relPath}"); did you forget the .fixture suffix?`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
return mkFail(
|
|
1006
|
+
CONVENTION_FIXTURE_DIRNAME,
|
|
1007
|
+
relPath,
|
|
1008
|
+
`fixture directory must match conformance/NNN-{slug}.fixture/ (got "${relPath}")`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const slug = m[2];
|
|
1012
|
+
if (!isValidSlug(slug)) {
|
|
1013
|
+
return mkFail(
|
|
1014
|
+
CONVENTION_FIXTURE_DIRNAME,
|
|
1015
|
+
relPath,
|
|
1016
|
+
`fixture slug "${slug}" violates ^[a-z0-9]+(-[a-z0-9]+)*$`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return { ok: true };
|
|
1020
|
+
}
|
|
1021
|
+
function mkFail(code, file, message) {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
diagnostics: [
|
|
1025
|
+
{
|
|
1026
|
+
severity: "error",
|
|
1027
|
+
code,
|
|
1028
|
+
message,
|
|
1029
|
+
file
|
|
1030
|
+
}
|
|
1031
|
+
]
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/conventions/walker.ts
|
|
1036
|
+
var IGNORED_LEAF_NAMES = /* @__PURE__ */ new Set(["README.md", ".gitkeep", ".DS_Store"]);
|
|
1037
|
+
async function walkRepo(opts) {
|
|
1038
|
+
const diagnostics = [];
|
|
1039
|
+
const root = opts.root;
|
|
1040
|
+
const sepsDir = join3(root, "seps");
|
|
1041
|
+
if (existsSync2(sepsDir)) {
|
|
1042
|
+
const entries = await readdir3(sepsDir, { withFileTypes: true });
|
|
1043
|
+
for (const e of entries) {
|
|
1044
|
+
if (!e.isFile()) continue;
|
|
1045
|
+
if (IGNORED_LEAF_NAMES.has(e.name)) continue;
|
|
1046
|
+
if (!e.name.endsWith(".md")) continue;
|
|
1047
|
+
const rel = relative4(root, join3(sepsDir, e.name));
|
|
1048
|
+
const r = checkSepFilename(rel);
|
|
1049
|
+
if (!r.ok && r.diagnostics) diagnostics.push(...r.diagnostics);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const confDir = join3(root, "conformance");
|
|
1053
|
+
if (existsSync2(confDir)) {
|
|
1054
|
+
const entries = await readdir3(confDir, { withFileTypes: true });
|
|
1055
|
+
for (const e of entries) {
|
|
1056
|
+
if (!e.isDirectory()) continue;
|
|
1057
|
+
if (IGNORED_LEAF_NAMES.has(e.name)) continue;
|
|
1058
|
+
const rel = relative4(root, join3(confDir, e.name));
|
|
1059
|
+
const r = checkFixtureDirname(rel);
|
|
1060
|
+
if (!r.ok && r.diagnostics) diagnostics.push(...r.diagnostics);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/cli/checkConventions.ts
|
|
1067
|
+
async function runCheckConventions(opts) {
|
|
1068
|
+
const root = resolve9(opts.cwd, opts.path);
|
|
1069
|
+
const result = await walkRepo({ root });
|
|
1070
|
+
if (opts.json) {
|
|
1071
|
+
emitJson(
|
|
1072
|
+
makeEnvelope({
|
|
1073
|
+
command: "check-conventions",
|
|
1074
|
+
ok: result.ok,
|
|
1075
|
+
diagnostics: result.diagnostics,
|
|
1076
|
+
result: { root }
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
} else {
|
|
1080
|
+
process.stdout.write(formatCheckConventions(result.diagnostics) + "\n");
|
|
1081
|
+
}
|
|
1082
|
+
return result.ok ? 0 : 1;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
855
1085
|
// src/cli/index.ts
|
|
856
1086
|
var program = new Command();
|
|
857
1087
|
program.name("sightmap").description("CLI for the Sightmap spec \u2014 validate, lint, match, explain.").version("0.1.0");
|
|
@@ -874,6 +1104,25 @@ program.command("lint [path]").description("Run quality checks beyond schema.").
|
|
|
874
1104
|
});
|
|
875
1105
|
process.exit(code);
|
|
876
1106
|
});
|
|
1107
|
+
program.command("check [path]").description("Run schema + quality checks (combined validate + lint).").option("--json", "machine-readable output").option("--level <level>", "'schema' or 'quality' (default 'quality')", "quality").option("--stdin", "read sightmap content from stdin instead of a path").action(
|
|
1108
|
+
async (path, opts) => {
|
|
1109
|
+
const level = opts.level === "schema" ? "schema" : "quality";
|
|
1110
|
+
if (opts.stdin === true) {
|
|
1111
|
+
const stdinCode = await runCheckFromStdin({
|
|
1112
|
+
level,
|
|
1113
|
+
json: opts.json === true
|
|
1114
|
+
});
|
|
1115
|
+
process.exit(stdinCode);
|
|
1116
|
+
}
|
|
1117
|
+
const code = await runCheck({
|
|
1118
|
+
path: path ?? ".sightmap",
|
|
1119
|
+
cwd: program.opts().cwd,
|
|
1120
|
+
level,
|
|
1121
|
+
json: opts.json === true
|
|
1122
|
+
});
|
|
1123
|
+
process.exit(code);
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
877
1126
|
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
1127
|
async (url, path, opts) => {
|
|
879
1128
|
const code = await runMatch({
|
|
@@ -901,6 +1150,14 @@ program.command("explain <query> [path]").description("Look up sightmap entries
|
|
|
901
1150
|
process.exit(code);
|
|
902
1151
|
}
|
|
903
1152
|
);
|
|
1153
|
+
program.command("check-conventions [path]").description("Validate repo filename conventions (SEPs, conformance fixtures).").option("--json", "machine-readable output").action(async (path, opts) => {
|
|
1154
|
+
const code = await runCheckConventions({
|
|
1155
|
+
path: path ?? ".",
|
|
1156
|
+
cwd: program.opts().cwd,
|
|
1157
|
+
json: opts.json === true
|
|
1158
|
+
});
|
|
1159
|
+
process.exit(code);
|
|
1160
|
+
});
|
|
904
1161
|
program.parseAsync(process.argv).catch((err) => {
|
|
905
1162
|
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}
|
|
906
1163
|
`);
|