@hawkeyexl/manni 0.1.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.
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hawkeyexl/manni",
6
- version: "0.1.0",
6
+ version: "0.3.0",
7
7
  description: "One command line for documentation that is meant to be checked. `manni meta` validates the presence and format of document metadata (Markdown frontmatter and more) against JSON Schema, built for CI. Formerly docmeta.",
8
8
  type: "module",
9
9
  bin: {
@@ -78,6 +78,7 @@ var package_default = {
78
78
  esbuild: "^0.28.2"
79
79
  },
80
80
  dependencies: {
81
+ "@axe-core/playwright": "^4.13.0",
81
82
  "@hawkeyexl/inference": "^0.3.1",
82
83
  "@xmldom/xmldom": "^0.9.12",
83
84
  ajv: "^8.17.1",
@@ -88,6 +89,7 @@ var package_default = {
88
89
  parse5: "^8.0.1",
89
90
  picocolors: "^1.1.1",
90
91
  picomatch: "^4.0.7",
92
+ "playwright-core": "^1.63.0",
91
93
  "smol-toml": "^1.8.0",
92
94
  yaml: "^2.6.1"
93
95
  },
@@ -152,8 +154,8 @@ var RENAME_ATTEMPTS = 3;
152
154
  function errorCode(err) {
153
155
  return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
154
156
  }
155
- var wait = (ms) => new Promise((resolve12) => {
156
- setTimeout(resolve12, ms);
157
+ var wait = (ms) => new Promise((resolve14) => {
158
+ setTimeout(resolve14, ms);
157
159
  });
158
160
  async function writeFileAtomic(path, contents) {
159
161
  const tmp = join(
@@ -2609,11 +2611,17 @@ var BUILTINS = /* @__PURE__ */ new Map([
2609
2611
  ["anthropic:claude-subagent:2.1", __default23]
2610
2612
  ]);
2611
2613
  function assertPublishableBuiltinId(id) {
2612
- if (id.split(":")[0] === "check") {
2614
+ const first = id.split(":")[0];
2615
+ if (first === "check") {
2613
2616
  throw new Error(
2614
2617
  `Built-in id "${id}" is not publishable: the "check" first segment is reserved for corpus check identities (check:<name>, proposal 0026).`
2615
2618
  );
2616
2619
  }
2620
+ if (first === "sidecar") {
2621
+ throw new Error(
2622
+ `Built-in id "${id}" is not publishable: the "sidecar" first segment is reserved for sidecar finding identities (sidecar:owned, proposal 0037).`
2623
+ );
2624
+ }
2617
2625
  }
2618
2626
  for (const id of BUILTINS.keys()) assertPublishableBuiltinId(id);
2619
2627
  function listBuiltins() {
@@ -2741,8 +2749,8 @@ var SCHEMA_KEYS = [
2741
2749
  "multipleOf"
2742
2750
  ];
2743
2751
  var RETRY_DELAY_MS = 500;
2744
- var wait2 = (ms) => new Promise((resolve12) => {
2745
- setTimeout(resolve12, ms);
2752
+ var wait2 = (ms) => new Promise((resolve14) => {
2753
+ setTimeout(resolve14, ms);
2746
2754
  });
2747
2755
  var RetryableFetchError = class extends DocmetaError {
2748
2756
  };
@@ -2994,6 +3002,277 @@ async function loadSchema(ref, options = {}) {
2994
3002
  }
2995
3003
  }
2996
3004
 
3005
+ // src/meta/core/resolve-schema.ts
3006
+ import { isAbsolute, relative, resolve } from "node:path";
3007
+ import picomatch from "picomatch";
3008
+ var DEFAULT_SCHEMAS = Object.freeze([
3009
+ "google:okf:0.1",
3010
+ "passo-uno:seven-action:1.0"
3011
+ ]);
3012
+ var FILE_SCHEMA_KEY = "$schema";
3013
+ function schemaEntryRef(entry) {
3014
+ return typeof entry === "string" ? entry : entry.ref;
3015
+ }
3016
+ function collectSchemaPins(config) {
3017
+ const pins = /* @__PURE__ */ new Map();
3018
+ for (const entry of config?.schemas ?? []) {
3019
+ if (typeof entry === "string") continue;
3020
+ if (entry.source === void 0 && entry.integrity === void 0) continue;
3021
+ pins.set(entry.ref, {
3022
+ ...entry.source !== void 0 ? { source: entry.source } : {},
3023
+ ...entry.integrity !== void 0 ? { integrity: entry.integrity } : {}
3024
+ });
3025
+ }
3026
+ return pins;
3027
+ }
3028
+ function rebaseConfigSchemaRefs(config, configDir, cwd) {
3029
+ if (resolve(configDir) === resolve(cwd)) return config;
3030
+ const rebase = (ref) => classifyRef(ref).kind === "file" && !isAbsolute(ref) ? resolve(configDir, ref) : ref;
3031
+ const rebaseEntry = (entry) => typeof entry === "string" ? rebase(entry) : {
3032
+ ...entry,
3033
+ ref: rebase(entry.ref),
3034
+ ...entry.source !== void 0 ? { source: rebase(entry.source) } : {}
3035
+ };
3036
+ return {
3037
+ ...config,
3038
+ ...config.schemas ? { schemas: config.schemas.map(rebaseEntry) } : {},
3039
+ ...config.overrides ? {
3040
+ overrides: config.overrides.map((o) => ({
3041
+ ...o,
3042
+ schemas: o.schemas.map(rebase)
3043
+ }))
3044
+ } : {}
3045
+ };
3046
+ }
3047
+ var matcherCache = /* @__PURE__ */ new Map();
3048
+ function overrideGlobs(files) {
3049
+ return typeof files === "string" ? [files] : files;
3050
+ }
3051
+ function matchesFileGlob(glob, filePath) {
3052
+ return matches(glob, filePath);
3053
+ }
3054
+ function matches(glob, filePath) {
3055
+ const key = JSON.stringify(glob);
3056
+ let m = matcherCache.get(key);
3057
+ if (!m) {
3058
+ m = picomatch(glob, { dot: true });
3059
+ matcherCache.set(key, m);
3060
+ }
3061
+ return m(filePath.replace(/\\/g, "/"));
3062
+ }
3063
+ function coerceFileSchema(value) {
3064
+ if (value === void 0 || value === null) return void 0;
3065
+ if (typeof value === "string") return [value];
3066
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
3067
+ return value;
3068
+ }
3069
+ throw new Error(
3070
+ `Invalid "${FILE_SCHEMA_KEY}": must be a string or a list of strings.`
3071
+ );
3072
+ }
3073
+ function dedupe(refs) {
3074
+ return [...new Set(refs)];
3075
+ }
3076
+ function hostAllowed(url, hosts) {
3077
+ const hostname = url.hostname.toLowerCase();
3078
+ const host = url.host.toLowerCase();
3079
+ return hosts.some((h) => {
3080
+ const want = h.trim().toLowerCase();
3081
+ return want === hostname || want === host;
3082
+ });
3083
+ }
3084
+ function assertDocumentRefAllowed(ref, mode, params) {
3085
+ const { kind } = classifyRef(ref);
3086
+ if (kind === "builtin" || isPublishedBuiltinUrl(ref)) return;
3087
+ if (kind === "url") {
3088
+ if (mode === "local") {
3089
+ throw new DocmetaError(
3090
+ `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": schemaTrust.documentRefs is "local", so a document may name a built-in id or a schema file inside the repository, but not a URL. Vendor it with \`manni meta schemas vendor ${ref}\` and reference the local copy, or put the URL in \`schemas:\` where an operator controls it.`
3091
+ );
3092
+ }
3093
+ const hosts = params.config?.schemaTrust?.hosts;
3094
+ if (!hosts || hosts.length === 0) return;
3095
+ let url;
3096
+ try {
3097
+ url = new URL(ref);
3098
+ } catch {
3099
+ throw new DocmetaError(
3100
+ `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": it cannot be parsed as a URL, so it cannot be checked against schemaTrust.hosts.`
3101
+ );
3102
+ }
3103
+ if (!hostAllowed(url, hosts)) {
3104
+ throw new DocmetaError(
3105
+ `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": host "${url.host}" is not in schemaTrust.hosts (${hosts.join(", ")}). Add the host there, or reference a schema the config already names.`
3106
+ );
3107
+ }
3108
+ return;
3109
+ }
3110
+ const root = params.trustRoot;
3111
+ if (!root) return;
3112
+ const abs = resolve(params.fileBase ?? process.cwd(), ref);
3113
+ const within = relative(root.dir, abs);
3114
+ if (within !== "" && !within.startsWith("..") && !isAbsolute(within)) return;
3115
+ const boundary = root.source === "git" ? `the repository root (${root.dir})` : root.source === "config" ? `${root.dir} \u2014 no git repository was found, so the config file's own directory is the boundary` : `${root.dir} \u2014 no git repository and no config file were found, so the directory the command was run from is the boundary`;
3116
+ const scope = root.source === "git" ? "the repository" : "the project";
3117
+ throw new DocmetaError(
3118
+ `Refusing the "${FILE_SCHEMA_KEY}" path "${ref}": it resolves outside ${boundary}. A document may only name a schema inside ${scope}; a path in \`schemas:\` or on \`--schema\` is not restricted this way.`
3119
+ );
3120
+ }
3121
+ function resolveSchemaSetWithSource(params) {
3122
+ const { filePath, fileSchema, cliSchemas, config } = params;
3123
+ if (cliSchemas && cliSchemas.length > 0) {
3124
+ return { schemas: dedupe(cliSchemas), source: "cli" };
3125
+ }
3126
+ const fromFile = coerceFileSchema(fileSchema);
3127
+ if (fromFile && fromFile.length > 0) {
3128
+ const mode = config?.schemaTrust?.documentRefs ?? "any";
3129
+ if (mode === "none") {
3130
+ params.onNotice?.(
3131
+ `${filePath}: "${FILE_SCHEMA_KEY}" is ignored (${fromFile.join(", ")}) \u2014 schemaTrust.documentRefs is "none", so the schema set comes from the config instead. Remove the key from the document, or set schemaTrust.documentRefs to "local" or "any" to honor it.`
3132
+ );
3133
+ } else {
3134
+ for (const ref of fromFile) assertDocumentRefAllowed(ref, mode, params);
3135
+ return { schemas: dedupe(fromFile), source: "document" };
3136
+ }
3137
+ }
3138
+ if (config?.overrides) {
3139
+ for (const [i, ov] of config.overrides.entries()) {
3140
+ if (matches(ov.files, filePath) && ov.schemas.length > 0) {
3141
+ return { schemas: dedupe(ov.schemas), source: "override", overrideIndex: i };
3142
+ }
3143
+ }
3144
+ }
3145
+ if (config?.schemas && config.schemas.length > 0) {
3146
+ return {
3147
+ schemas: dedupe(config.schemas.map(schemaEntryRef)),
3148
+ source: "config"
3149
+ };
3150
+ }
3151
+ return { schemas: [...DEFAULT_SCHEMAS], source: "default" };
3152
+ }
3153
+ function resolveSchemaSet(params) {
3154
+ return resolveSchemaSetWithSource(params).schemas;
3155
+ }
3156
+ function resolveElements(filePath, config) {
3157
+ const out = [];
3158
+ if (config?.elements) out.push(...config.elements);
3159
+ for (const ov of config?.overrides ?? []) {
3160
+ if (ov.elements && matches(ov.files, filePath)) out.push(...ov.elements);
3161
+ }
3162
+ return dedupe(out);
3163
+ }
3164
+
3165
+ // src/meta/core/sidecar-fetch.ts
3166
+ var SIDECAR_FETCH_TIMEOUT_MS = 1e4;
3167
+ var SIDECAR_FETCH_MAX_BYTES = 5 * 1024 * 1024;
3168
+ var MAX_REDIRECTS = 5;
3169
+ var RETRY_DELAY_MS2 = 500;
3170
+ function sidecarUrlProblem(url) {
3171
+ let parsed2;
3172
+ try {
3173
+ parsed2 = new URL(url);
3174
+ } catch {
3175
+ return "is not a valid URL";
3176
+ }
3177
+ if (parsed2.username !== "" || parsed2.password !== "") {
3178
+ return 'carries a credential in the URL; put the token in an environment variable and name it with "tokenEnv"';
3179
+ }
3180
+ if (parsed2.protocol === "https:") return null;
3181
+ if (parsed2.protocol === "http:" && isLoopback(parsed2.hostname)) return null;
3182
+ if (parsed2.protocol === "http:") {
3183
+ return "is plain http://; a bearer token over plaintext is a leak, and a public manifest is served over https too";
3184
+ }
3185
+ return `has the unsupported scheme "${parsed2.protocol}"`;
3186
+ }
3187
+ function isLoopback(host) {
3188
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.startsWith("127.");
3189
+ }
3190
+ async function fetchSidecar(url, opts = {}) {
3191
+ if (opts.offline) {
3192
+ throw new DocmetaError(
3193
+ `Sidecar manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
3194
+ );
3195
+ }
3196
+ const timeoutMs = opts.timeoutMs ?? SIDECAR_FETCH_TIMEOUT_MS;
3197
+ const maxBytes = opts.maxBytes ?? SIDECAR_FETCH_MAX_BYTES;
3198
+ const env = opts.env ?? process.env;
3199
+ let token;
3200
+ if (opts.tokenEnv !== void 0) {
3201
+ token = env[opts.tokenEnv];
3202
+ if (token === void 0 || token === "") {
3203
+ throw new DocmetaError(
3204
+ `Sidecar manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
3205
+ );
3206
+ }
3207
+ }
3208
+ let res = await requestWithRedirects(url, token, timeoutMs);
3209
+ if (res instanceof Error || res.status >= 500) {
3210
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS2));
3211
+ res = await requestWithRedirects(url, token, timeoutMs);
3212
+ }
3213
+ if (res instanceof Error) {
3214
+ throw new DocmetaError(
3215
+ `Sidecar manifest ${url} could not be fetched: ${res.message}`
3216
+ );
3217
+ }
3218
+ if (!res.ok) {
3219
+ const hint = res.status === 404 && token === void 0 ? ' (a private file answers 404 without a token; set "tokenEnv")' : res.status === 404 ? " (a private file answers 404 without a valid token)" : "";
3220
+ throw new DocmetaError(
3221
+ `Sidecar manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
3222
+ );
3223
+ }
3224
+ return readCapped(url, res, maxBytes);
3225
+ }
3226
+ async function requestWithRedirects(url, token, timeoutMs) {
3227
+ const origin = new URL(url).origin;
3228
+ let current = url;
3229
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
3230
+ const sameOrigin = new URL(current).origin === origin;
3231
+ const headers = { accept: "text/plain, application/x-yaml, */*" };
3232
+ if (token !== void 0 && sameOrigin) headers.authorization = `Bearer ${token}`;
3233
+ let res;
3234
+ try {
3235
+ res = await fetch(current, {
3236
+ headers,
3237
+ redirect: "manual",
3238
+ signal: AbortSignal.timeout(timeoutMs)
3239
+ });
3240
+ } catch (err) {
3241
+ const isAbort2 = err instanceof Error && err.name === "TimeoutError";
3242
+ return isAbort2 ? new Error(`timed out after ${timeoutMs}ms`) : new Error(err instanceof Error ? err.message : String(err));
3243
+ }
3244
+ if (res.status >= 300 && res.status < 400) {
3245
+ const location = res.headers.get("location");
3246
+ if (location === null) return res;
3247
+ await res.body?.cancel();
3248
+ current = new URL(location, current).toString();
3249
+ continue;
3250
+ }
3251
+ return res;
3252
+ }
3253
+ return new Error(`more than ${MAX_REDIRECTS} redirects`);
3254
+ }
3255
+ async function readCapped(url, res, maxBytes) {
3256
+ const body = res.body;
3257
+ if (!body) return "";
3258
+ const reader = body.getReader();
3259
+ const chunks = [];
3260
+ let total = 0;
3261
+ for (; ; ) {
3262
+ const { done, value } = await reader.read();
3263
+ if (done || value === void 0) break;
3264
+ total += value.byteLength;
3265
+ if (total > maxBytes) {
3266
+ await reader.cancel();
3267
+ throw new DocmetaError(
3268
+ `Sidecar manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
3269
+ );
3270
+ }
3271
+ chunks.push(value);
3272
+ }
3273
+ return Buffer.concat(chunks).toString("utf8");
3274
+ }
3275
+
2997
3276
  // src/meta/extractors/frontmatter.ts
2998
3277
  import { parseDocument, LineCounter, isMap, isSeq, isScalar } from "yaml";
2999
3278
  import { parse as parseToml } from "smol-toml";
@@ -3753,8 +4032,8 @@ function ditaShape(content, root, filePath) {
3753
4032
  const lower = filePath?.toLowerCase() ?? "";
3754
4033
  const byExtension2 = (lower.endsWith(".dita") || lower.endsWith(".ditamap")) && DITA_ROOTS.has(name2);
3755
4034
  if (!byDoctype && !byClass && !byArch && !byExtension2) return void 0;
3756
- const isMap4 = /^\s*[-+]\s+map\//.test(cls) || MAP_ROOTS.has(name2) || lower.endsWith(".ditamap");
3757
- return isMap4 ? { kind: "map", container: "topicmeta", preamble: /* @__PURE__ */ new Set(["title"]) } : {
4035
+ const isMap5 = /^\s*[-+]\s+map\//.test(cls) || MAP_ROOTS.has(name2) || lower.endsWith(".ditamap");
4036
+ return isMap5 ? { kind: "map", container: "topicmeta", preamble: /* @__PURE__ */ new Set(["title"]) } : {
3758
4037
  kind: "topic",
3759
4038
  container: "metadata",
3760
4039
  preamble: /* @__PURE__ */ new Set([
@@ -5179,202 +5458,477 @@ function listFormats() {
5179
5458
  }));
5180
5459
  }
5181
5460
 
5182
- // src/meta/core/resolve-schema.ts
5183
- import { isAbsolute, relative, resolve } from "node:path";
5184
- import picomatch from "picomatch";
5185
- var DEFAULT_SCHEMAS = Object.freeze([
5186
- "google:okf:0.1",
5187
- "passo-uno:seven-action:1.0"
5188
- ]);
5189
- var FILE_SCHEMA_KEY = "$schema";
5190
- function schemaEntryRef(entry) {
5191
- return typeof entry === "string" ? entry : entry.ref;
5192
- }
5193
- function collectSchemaPins(config) {
5194
- const pins = /* @__PURE__ */ new Map();
5195
- for (const entry of config?.schemas ?? []) {
5196
- if (typeof entry === "string") continue;
5197
- if (entry.source === void 0 && entry.integrity === void 0) continue;
5198
- pins.set(entry.ref, {
5199
- ...entry.source !== void 0 ? { source: entry.source } : {},
5200
- ...entry.integrity !== void 0 ? { integrity: entry.integrity } : {}
5201
- });
5461
+ // src/meta/core/sidecars.ts
5462
+ import { readFile as readFile3 } from "node:fs/promises";
5463
+ import { isAbsolute as isAbsolute2, relative as relative4, resolve as resolve5, sep as sep2 } from "node:path";
5464
+ import {
5465
+ LineCounter as LineCounter2,
5466
+ isMap as isMap3,
5467
+ isNode,
5468
+ isScalar as isScalar2,
5469
+ parseDocument as parseDocument3
5470
+ } from "yaml";
5471
+
5472
+ // src/meta/core/load-files.ts
5473
+ import { stat as stat3 } from "node:fs/promises";
5474
+ import { extname, relative as relative3, resolve as resolve4 } from "node:path";
5475
+ import fg from "fast-glob";
5476
+ import picomatch2 from "picomatch";
5477
+
5478
+ // src/meta/core/gitignore.ts
5479
+ import { spawn } from "node:child_process";
5480
+ import { dirname as dirname3, relative as relative2, resolve as resolve3, sep } from "node:path";
5481
+
5482
+ // src/shared/git-root.ts
5483
+ import { existsSync } from "node:fs";
5484
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
5485
+ function findGitRoot(cwd) {
5486
+ let dir = resolve2(cwd);
5487
+ for (; ; ) {
5488
+ if (existsSync(join3(dir, ".git"))) return dir;
5489
+ const parent = dirname2(dir);
5490
+ if (parent === dir) return null;
5491
+ dir = parent;
5202
5492
  }
5203
- return pins;
5204
5493
  }
5205
- function rebaseConfigSchemaRefs(config, configDir, cwd) {
5206
- if (resolve(configDir) === resolve(cwd)) return config;
5207
- const rebase = (ref) => classifyRef(ref).kind === "file" && !isAbsolute(ref) ? resolve(configDir, ref) : ref;
5208
- const rebaseEntry = (entry) => typeof entry === "string" ? rebase(entry) : {
5209
- ...entry,
5210
- ref: rebase(entry.ref),
5211
- ...entry.source !== void 0 ? { source: rebase(entry.source) } : {}
5212
- };
5494
+ function searchPath(cwd) {
5495
+ const start = resolve2(cwd);
5496
+ const root = findGitRoot(start);
5497
+ if (root === null) return [start];
5498
+ const chain = [];
5499
+ let dir = start;
5500
+ for (; ; ) {
5501
+ chain.push(dir);
5502
+ if (dir === root) return chain;
5503
+ const parent = dirname2(dir);
5504
+ if (parent === dir) return chain;
5505
+ dir = parent;
5506
+ }
5507
+ }
5508
+
5509
+ // src/meta/core/gitignore.ts
5510
+ var GITIGNORE_UNAVAILABLE = ".gitignore filtering was requested but git could not answer here (no repository, or no git on PATH); no files were skipped.";
5511
+ var unavailable = () => ({
5512
+ ignored: /* @__PURE__ */ new Set(),
5513
+ available: false
5514
+ });
5515
+ async function gitIgnored(paths, cwd) {
5516
+ if (paths.length === 0) {
5517
+ return { ignored: /* @__PURE__ */ new Set(), available: true };
5518
+ }
5519
+ const rootOf = /* @__PURE__ */ new Map();
5520
+ const byRoot = /* @__PURE__ */ new Map();
5521
+ let inSomeRepo = false;
5522
+ for (const spelled of paths) {
5523
+ const abs = resolve3(cwd, spelled);
5524
+ const dir = dirname3(abs);
5525
+ let root = rootOf.get(dir);
5526
+ if (root === void 0) {
5527
+ root = findGitRoot(dir);
5528
+ rootOf.set(dir, root);
5529
+ }
5530
+ if (root === null) continue;
5531
+ inSomeRepo = true;
5532
+ const rel = relative2(root, abs).split(sep).join("/");
5533
+ const bucket = byRoot.get(root);
5534
+ if (bucket) bucket.push({ rel, spelled });
5535
+ else byRoot.set(root, [{ rel, spelled }]);
5536
+ }
5537
+ if (!inSomeRepo) return unavailable();
5538
+ const ignored = /* @__PURE__ */ new Set();
5539
+ let answered = 0;
5540
+ for (const [root, bucket] of byRoot) {
5541
+ const answer = await checkIgnore(
5542
+ bucket.map((b) => b.rel),
5543
+ root
5544
+ );
5545
+ if (!answer.available) continue;
5546
+ answered += 1;
5547
+ for (const b of bucket) {
5548
+ if (answer.ignored.has(b.rel)) ignored.add(b.spelled);
5549
+ }
5550
+ }
5551
+ return answered > 0 ? { ignored, available: true } : unavailable();
5552
+ }
5553
+ function checkIgnore(paths, cwd) {
5554
+ return new Promise((settle) => {
5555
+ let done = false;
5556
+ const finish = (answer) => {
5557
+ if (done) return;
5558
+ done = true;
5559
+ settle(answer);
5560
+ };
5561
+ let child;
5562
+ try {
5563
+ child = spawn("git", ["check-ignore", "--stdin", "-z"], {
5564
+ cwd,
5565
+ windowsHide: true
5566
+ });
5567
+ } catch {
5568
+ finish(unavailable());
5569
+ return;
5570
+ }
5571
+ let out = "";
5572
+ child.stdout.setEncoding("utf8");
5573
+ child.stdout.on("data", (chunk) => {
5574
+ out += chunk;
5575
+ });
5576
+ child.stderr.resume();
5577
+ child.on("error", () => {
5578
+ finish(unavailable());
5579
+ });
5580
+ child.stdin.on("error", () => {
5581
+ });
5582
+ child.on("close", (code) => {
5583
+ if (code !== 0 && code !== 1) {
5584
+ finish(unavailable());
5585
+ return;
5586
+ }
5587
+ finish({
5588
+ ignored: new Set(out.split("\0").filter((p) => p !== "")),
5589
+ available: true
5590
+ });
5591
+ });
5592
+ child.stdin.end(paths.join("\0"));
5593
+ });
5594
+ }
5595
+
5596
+ // src/meta/core/load-files.ts
5597
+ var DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
5598
+ var STDIN_TOKEN = "-";
5599
+ var STDIN_LABEL = "<stdin>";
5600
+ function toPosix(p) {
5601
+ return p.replace(/\\/g, "/");
5602
+ }
5603
+ async function statOrNull(p) {
5604
+ try {
5605
+ return await stat3(p);
5606
+ } catch {
5607
+ return null;
5608
+ }
5609
+ }
5610
+ function gitignoreOptions(opts) {
5611
+ const asked = opts.flag === true || opts.flag === void 0 && opts.configured === true;
5612
+ const notice = opts.onNotice;
5213
5613
  return {
5214
- ...config,
5215
- ...config.schemas ? { schemas: config.schemas.map(rebaseEntry) } : {},
5216
- ...config.overrides ? {
5217
- overrides: config.overrides.map((o) => ({
5218
- ...o,
5219
- schemas: o.schemas.map(rebase)
5220
- }))
5614
+ respectGitignore: opts.flag ?? opts.configured ?? true,
5615
+ ...asked && notice ? {
5616
+ onGitignoreUnavailable: () => {
5617
+ notice(GITIGNORE_UNAVAILABLE);
5618
+ }
5221
5619
  } : {}
5222
5620
  };
5223
5621
  }
5224
- var matcherCache = /* @__PURE__ */ new Map();
5225
- function overrideGlobs(files) {
5226
- return typeof files === "string" ? [files] : files;
5227
- }
5228
- function matchesFileGlob(glob, filePath) {
5229
- return matches(glob, filePath);
5230
- }
5231
- function matches(glob, filePath) {
5232
- const key = JSON.stringify(glob);
5233
- let m = matcherCache.get(key);
5234
- if (!m) {
5235
- m = picomatch(glob, { dot: true });
5236
- matcherCache.set(key, m);
5622
+ async function resolveTargetSet(opts) {
5623
+ const cwd = opts.cwd ?? process.cwd();
5624
+ const exts = (opts.exts ?? supportedExtensions()).map(
5625
+ (e) => e.toLowerCase().startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
5626
+ );
5627
+ const ignore = [...DEFAULT_IGNORE, ...opts.exclude ?? []];
5628
+ const named = /* @__PURE__ */ new Set();
5629
+ const walked = /* @__PURE__ */ new Set();
5630
+ const keepByExt = (file) => exts.includes(extname(file).toLowerCase());
5631
+ const missing = [];
5632
+ for (const input of opts.inputs) {
5633
+ if (input === STDIN_TOKEN) continue;
5634
+ const posixInput = toPosix(input);
5635
+ const abs = resolve4(cwd, posixInput);
5636
+ const st = await statOrNull(abs);
5637
+ if (st?.isFile()) {
5638
+ named.add(toPosix(relative3(cwd, abs)));
5639
+ continue;
5640
+ }
5641
+ if (st?.isDirectory()) {
5642
+ const found2 = await fg(`${posixInput}/**/*`, {
5643
+ cwd,
5644
+ ignore,
5645
+ onlyFiles: true,
5646
+ dot: false
5647
+ });
5648
+ for (const f of found2) if (keepByExt(f)) walked.add(f);
5649
+ continue;
5650
+ }
5651
+ if (!picomatch2.scan(posixInput).isGlob) {
5652
+ missing.push(posixInput);
5653
+ continue;
5654
+ }
5655
+ const found = await fg(posixInput, {
5656
+ cwd,
5657
+ ignore,
5658
+ onlyFiles: true,
5659
+ dot: false
5660
+ });
5661
+ for (const f of found) if (keepByExt(f)) walked.add(f);
5237
5662
  }
5238
- return m(filePath.replace(/\\/g, "/"));
5663
+ if (missing.length > 0 && !opts.allowEmpty) {
5664
+ const names = missing.map((m) => `"${m}"`).join(", ");
5665
+ throw new DocmetaError(
5666
+ missing.length === 1 ? `File not found: ${names}.` : `Files not found: ${names}.`
5667
+ );
5668
+ }
5669
+ let gitignoreSkipped = 0;
5670
+ if (opts.respectGitignore !== false && walked.size > 0) {
5671
+ const candidates = [...walked].filter((f) => !named.has(f));
5672
+ const { ignored, available } = await gitIgnored(candidates, cwd);
5673
+ if (available) {
5674
+ for (const f of ignored) {
5675
+ if (walked.delete(f)) gitignoreSkipped += 1;
5676
+ }
5677
+ } else {
5678
+ opts.onGitignoreUnavailable?.();
5679
+ }
5680
+ }
5681
+ return {
5682
+ files: [.../* @__PURE__ */ new Set([...named, ...walked])].sort(),
5683
+ gitignoreSkipped
5684
+ };
5239
5685
  }
5240
- function coerceFileSchema(value) {
5241
- if (value === void 0 || value === null) return void 0;
5242
- if (typeof value === "string") return [value];
5243
- if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
5244
- return value;
5686
+ function assertNonEmpty(p) {
5687
+ if (p.allowEmpty || p.usingStdin || p.files.length > 0) return;
5688
+ const tried = p.inputs.map((i) => `"${i}"`).join(", ");
5689
+ const notes = [];
5690
+ if (p.exts && p.exts.length > 0) {
5691
+ notes.push(`extensions: ${p.exts.join(", ")}`);
5245
5692
  }
5246
- throw new Error(
5247
- `Invalid "${FILE_SCHEMA_KEY}": must be a string or a list of strings.`
5693
+ if (p.exclude && p.exclude.length > 0) {
5694
+ notes.push(`excludes: ${p.exclude.map((e) => `"${e}"`).join(", ")}`);
5695
+ }
5696
+ if (p.gitignoreSkipped && p.gitignoreSkipped > 0) {
5697
+ notes.push(
5698
+ `.gitignore skipped ${p.gitignoreSkipped} (pass --no-gitignore to check them)`
5699
+ );
5700
+ }
5701
+ throw new DocmetaError(
5702
+ [
5703
+ `No files matched. Patterns tried: ${tried}.`,
5704
+ ...notes.length > 0 ? [`Filters applied \u2014 ${notes.join("; ")}.`] : [],
5705
+ `Nothing was ${p.action}, so this is an error rather than a pass.`,
5706
+ `Pass --allow-empty (or set allowEmpty: true) if matching nothing is expected.`
5707
+ ].join("\n")
5248
5708
  );
5249
5709
  }
5250
- function dedupe(refs) {
5251
- return [...new Set(refs)];
5710
+
5711
+ // src/meta/core/sidecars.ts
5712
+ var SIDECAR_OWNED_SCHEMA = "sidecar:owned";
5713
+ var SIDECAR_DUPLICATE_SCHEMA = "sidecar:duplicate";
5714
+ var SIDECAR_KEYWORD = "sidecar";
5715
+ var PATH_JOIN = "path";
5716
+ var posix = (p) => p.split(sep2).join("/");
5717
+ function reportedPath(abs, base) {
5718
+ const rel = relative4(base, abs);
5719
+ return rel === "" ? "." : posix(rel);
5720
+ }
5721
+ function sidecarJoin(sidecar) {
5722
+ return sidecar.join ?? PATH_JOIN;
5723
+ }
5724
+ async function loadSidecars(config, opts) {
5725
+ const configured = config?.sidecars;
5726
+ if (!configured || configured.length === 0) return null;
5727
+ const owners = /* @__PURE__ */ new Map();
5728
+ const byPath = /* @__PURE__ */ new Map();
5729
+ const byField = /* @__PURE__ */ new Map();
5730
+ const entries = [];
5731
+ for (const sidecar of configured) {
5732
+ let text;
5733
+ let file;
5734
+ if (classifyRef(sidecar.file).kind === "url") {
5735
+ file = sidecar.file;
5736
+ text = await fetchSidecar(sidecar.file, {
5737
+ ...sidecar.tokenEnv !== void 0 ? { tokenEnv: sidecar.tokenEnv } : {},
5738
+ ...opts.offline !== void 0 ? { offline: opts.offline } : {},
5739
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
5740
+ ...opts.env !== void 0 ? { env: opts.env } : {}
5741
+ });
5742
+ } else {
5743
+ const abs = isAbsolute2(sidecar.file) ? sidecar.file : resolve5(opts.configDir, sidecar.file);
5744
+ file = reportedPath(abs, opts.base);
5745
+ text = await readManifest(abs, file);
5746
+ }
5747
+ for (const key of sidecar.keys) owners.set(key, file);
5748
+ const join7 = sidecarJoin(sidecar);
5749
+ let target;
5750
+ if (join7 === PATH_JOIN) {
5751
+ target = byPath;
5752
+ } else {
5753
+ const existing = byField.get(join7);
5754
+ if (existing) target = existing;
5755
+ else {
5756
+ target = /* @__PURE__ */ new Map();
5757
+ byField.set(join7, target);
5758
+ }
5759
+ }
5760
+ parseManifest(sidecar, text, file, opts.configDir, join7, target, entries);
5761
+ }
5762
+ return { owners, byPath, byField, entries };
5252
5763
  }
5253
- function hostAllowed(url, hosts) {
5254
- const hostname = url.hostname.toLowerCase();
5255
- const host = url.host.toLowerCase();
5256
- return hosts.some((h) => {
5257
- const want = h.trim().toLowerCase();
5258
- return want === hostname || want === host;
5259
- });
5764
+ async function readManifest(abs, file) {
5765
+ try {
5766
+ return await readFile3(abs, "utf8");
5767
+ } catch (err) {
5768
+ const reason = err instanceof Error ? err.message : String(err);
5769
+ throw new DocmetaError(
5770
+ `Sidecar manifest ${file} could not be read: ${reason}`
5771
+ );
5772
+ }
5260
5773
  }
5261
- function assertDocumentRefAllowed(ref, mode, params) {
5262
- const { kind } = classifyRef(ref);
5263
- if (kind === "builtin" || isPublishedBuiltinUrl(ref)) return;
5264
- if (kind === "url") {
5265
- if (mode === "local") {
5774
+ function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
5775
+ const lc = new LineCounter2();
5776
+ const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
5777
+ const problem = doc.errors[0];
5778
+ if (problem) {
5779
+ throw new DocmetaError(
5780
+ `Sidecar manifest ${file} is not valid YAML: ${problem.message}`
5781
+ );
5782
+ }
5783
+ const root = doc.contents;
5784
+ if (root === null || isScalar2(root) && root.value === null) return;
5785
+ if (!isMap3(root)) {
5786
+ throw new DocmetaError(
5787
+ `Sidecar manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
5788
+ );
5789
+ }
5790
+ const owned = new Set(sidecar.keys);
5791
+ const lineAt = (node) => {
5792
+ const range = isNode(node) ? node.range : void 0;
5793
+ return range ? lc.linePos(range[0]).line : void 0;
5794
+ };
5795
+ const seen = /* @__PURE__ */ new Map();
5796
+ for (const pair of root.items) {
5797
+ const spelled = isScalar2(pair.key) ? String(pair.key.value) : String(pair.key);
5798
+ const entryLine = lineAt(pair.key);
5799
+ const where = entryLine === void 0 ? file : `${file}:${entryLine}`;
5800
+ if (seen.has(spelled)) {
5801
+ const first = seen.get(spelled);
5266
5802
  throw new DocmetaError(
5267
- `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": schemaTrust.documentRefs is "local", so a document may name a built-in id or a schema file inside the repository, but not a URL. Vendor it with \`manni meta schemas vendor ${ref}\` and reference the local copy, or put the URL in \`schemas:\` where an operator controls it.`
5803
+ `Sidecar manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
5268
5804
  );
5269
5805
  }
5270
- const hosts = params.config?.schemaTrust?.hosts;
5271
- if (!hosts || hosts.length === 0) return;
5272
- let url;
5273
- try {
5274
- url = new URL(ref);
5275
- } catch {
5806
+ seen.set(spelled, entryLine);
5807
+ if (!isMap3(pair.value)) {
5276
5808
  throw new DocmetaError(
5277
- `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": it cannot be parsed as a URL, so it cannot be checked against schemaTrust.hosts.`
5809
+ `Sidecar manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
5278
5810
  );
5279
5811
  }
5280
- if (!hostAllowed(url, hosts)) {
5281
- throw new DocmetaError(
5282
- `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": host "${url.host}" is not in schemaTrust.hosts (${hosts.join(", ")}). Add the host there, or reference a schema the config already names.`
5283
- );
5812
+ const indexKey = join7 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
5813
+ const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
5814
+ for (const kv of pair.value.items) {
5815
+ const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
5816
+ if (key === FILE_SCHEMA_KEY) {
5817
+ throw new DocmetaError(
5818
+ `Sidecar manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a sidecar never chooses the schema a document is judged by; use "overrides" in the config.`
5819
+ );
5820
+ }
5821
+ if (!owned.has(key)) {
5822
+ throw new DocmetaError(
5823
+ `Sidecar manifest ${where}: "${spelled}" sets "${key}", which this sidecar does not own. Add it to the sidecar's "keys", or remove it from the entry.`
5824
+ );
5825
+ }
5826
+ const value = kv.value === null ? null : kv.value.toJS(doc, { maxAliasCount: 100 });
5827
+ const line = lineAt(kv.key);
5828
+ values.set(key, { value, file, ...line === void 0 ? {} : { line } });
5284
5829
  }
5285
- return;
5830
+ target.set(indexKey, values);
5831
+ entries.push({
5832
+ join: join7,
5833
+ ...join7 === PATH_JOIN ? { abs: indexKey } : {},
5834
+ spelled,
5835
+ file,
5836
+ ...entryLine === void 0 ? {} : { line: entryLine }
5837
+ });
5286
5838
  }
5287
- const root = params.trustRoot;
5288
- if (!root) return;
5289
- const abs = resolve(params.fileBase ?? process.cwd(), ref);
5290
- const within = relative(root.dir, abs);
5291
- if (within !== "" && !within.startsWith("..") && !isAbsolute(within)) return;
5292
- const boundary = root.source === "git" ? `the repository root (${root.dir})` : root.source === "config" ? `${root.dir} \u2014 no git repository was found, so the config file's own directory is the boundary` : `${root.dir} \u2014 no git repository and no config file were found, so the directory the command was run from is the boundary`;
5293
- const scope = root.source === "git" ? "the repository" : "the project";
5294
- throw new DocmetaError(
5295
- `Refusing the "${FILE_SCHEMA_KEY}" path "${ref}": it resolves outside ${boundary}. A document may only name a schema inside ${scope}; a path in \`schemas:\` or on \`--schema\` is not restricted this way.`
5296
- );
5297
5839
  }
5298
- function resolveSchemaSetWithSource(params) {
5299
- const { filePath, fileSchema, cliSchemas, config } = params;
5300
- if (cliSchemas && cliSchemas.length > 0) {
5301
- return { schemas: dedupe(cliSchemas), source: "cli" };
5302
- }
5303
- const fromFile = coerceFileSchema(fileSchema);
5304
- if (fromFile && fromFile.length > 0) {
5305
- const mode = config?.schemaTrust?.documentRefs ?? "any";
5306
- if (mode === "none") {
5307
- params.onNotice?.(
5308
- `${filePath}: "${FILE_SCHEMA_KEY}" is ignored (${fromFile.join(", ")}) \u2014 schemaTrust.documentRefs is "none", so the schema set comes from the config instead. Remove the key from the document, or set schemaTrust.documentRefs to "local" or "any" to honor it.`
5309
- );
5310
- } else {
5311
- for (const ref of fromFile) assertDocumentRefAllowed(ref, mode, params);
5312
- return { schemas: dedupe(fromFile), source: "document" };
5313
- }
5314
- }
5315
- if (config?.overrides) {
5316
- for (const [i, ov] of config.overrides.entries()) {
5317
- if (matches(ov.files, filePath) && ov.schemas.length > 0) {
5318
- return { schemas: dedupe(ov.schemas), source: "override", overrideIndex: i };
5319
- }
5320
- }
5840
+ function joinValue(raw) {
5841
+ if (typeof raw === "string") return raw;
5842
+ if (typeof raw === "number" || typeof raw === "bigint") return String(raw);
5843
+ if (typeof raw === "boolean") return String(raw);
5844
+ return void 0;
5845
+ }
5846
+ function mergeSidecars(label, extracted, index, base) {
5847
+ const none = () => void 0;
5848
+ if (!index) return { extracted, collisions: [], joins: [], locate: none };
5849
+ const collisions = [];
5850
+ for (const key of Object.keys(extracted.data)) {
5851
+ const file = index.owners.get(key);
5852
+ if (file !== void 0) collisions.push({ key, file });
5853
+ }
5854
+ const sources = [];
5855
+ const joins = [];
5856
+ if (label !== STDIN_LABEL) {
5857
+ const byPath = index.byPath.get(resolve5(base, label));
5858
+ if (byPath) sources.push(byPath);
5859
+ }
5860
+ for (const [field, byValue] of index.byField) {
5861
+ const value = joinValue(extracted.data[field]);
5862
+ if (value === void 0) continue;
5863
+ const hit = byValue.get(value);
5864
+ if (!hit) continue;
5865
+ sources.push(hit);
5866
+ const first = hit.values().next().value;
5867
+ joins.push({ field, value, file: first?.file ?? "manifest" });
5321
5868
  }
5322
- if (config?.schemas && config.schemas.length > 0) {
5323
- return {
5324
- schemas: dedupe(config.schemas.map(schemaEntryRef)),
5325
- source: "config"
5326
- };
5869
+ if (sources.length === 0) {
5870
+ return { extracted, collisions, joins, locate: none };
5327
5871
  }
5328
- return { schemas: [...DEFAULT_SCHEMAS], source: "default" };
5872
+ const data = { ...extracted.data };
5873
+ const merged = /* @__PURE__ */ new Map();
5874
+ for (const supplied of sources) {
5875
+ for (const [key, sv] of supplied) {
5876
+ if (key in extracted.data) continue;
5877
+ data[key] = sv.value;
5878
+ merged.set(key, sv);
5879
+ }
5880
+ }
5881
+ const locate = (pointer) => {
5882
+ const top = topLevelKey(pointer);
5883
+ if (top === void 0) return void 0;
5884
+ const sv = merged.get(top);
5885
+ if (!sv) return void 0;
5886
+ return { file: sv.file, ...sv.line === void 0 ? {} : { line: sv.line } };
5887
+ };
5888
+ return { extracted: { ...extracted, data }, collisions, joins, locate };
5329
5889
  }
5330
- function resolveSchemaSet(params) {
5331
- return resolveSchemaSetWithSource(params).schemas;
5890
+ function topLevelKey(pointer) {
5891
+ if (pointer === "") return void 0;
5892
+ if (!pointer.startsWith("/")) return pointer;
5893
+ const seg = pointer.slice(1).split("/")[0] ?? "";
5894
+ return seg.replace(/~1/g, "/").replace(/~0/g, "~");
5332
5895
  }
5333
- function resolveElements(filePath, config) {
5334
- const out = [];
5335
- if (config?.elements) out.push(...config.elements);
5336
- for (const ov of config?.overrides ?? []) {
5337
- if (ov.elements && matches(ov.files, filePath)) out.push(...ov.elements);
5338
- }
5339
- return dedupe(out);
5896
+ function sidecarPointer(key) {
5897
+ return `/${escapePointerSegment(key)}`;
5898
+ }
5899
+ function orphanEntries(index, loaded, base) {
5900
+ if (!index) return [];
5901
+ const have = new Set(loaded.map((l) => resolve5(base, l)));
5902
+ return index.entries.filter(
5903
+ (e) => e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
5904
+ );
5905
+ }
5906
+ function orphanJoins(index, matched) {
5907
+ if (!index) return [];
5908
+ return index.entries.filter(
5909
+ (e) => e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
5910
+ );
5911
+ }
5912
+ function orphanError(orphans) {
5913
+ const first = orphans[0];
5914
+ if (!first) throw new Error("orphanError called with no orphans");
5915
+ const where = first.line === void 0 ? first.file : `${first.file}:${first.line}`;
5916
+ const more = orphans.length > 1 ? ` (${orphans.length - 1} more)` : "";
5917
+ const what = first.join === PATH_JOIN ? `names "${first.spelled}", which this run did not load` : `names ${first.join} "${first.spelled}", which no loaded document carries`;
5918
+ return new DocmetaError(
5919
+ `Sidecar manifest ${where} ${what}${more}. Fix the entry, or remove it.`
5920
+ );
5340
5921
  }
5341
5922
 
5342
5923
  // src/meta/core/config.ts
5343
- import { resolve as resolve4 } from "node:path";
5924
+ import { resolve as resolve7 } from "node:path";
5344
5925
  import { parse as parseYaml2 } from "yaml";
5345
5926
 
5346
5927
  // src/shared/config-file.ts
5347
- import { readFile as readFile3 } from "node:fs/promises";
5348
- import { dirname as dirname3, join as join4, relative as relative2, resolve as resolve3 } from "node:path";
5928
+ import { readFile as readFile4 } from "node:fs/promises";
5929
+ import { dirname as dirname4, join as join4, relative as relative5, resolve as resolve6 } from "node:path";
5349
5930
  import { parse as parseYaml } from "yaml";
5350
5931
 
5351
- // src/shared/git-root.ts
5352
- import { existsSync } from "node:fs";
5353
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
5354
- function findGitRoot(cwd) {
5355
- let dir = resolve2(cwd);
5356
- for (; ; ) {
5357
- if (existsSync(join3(dir, ".git"))) return dir;
5358
- const parent = dirname2(dir);
5359
- if (parent === dir) return null;
5360
- dir = parent;
5361
- }
5362
- }
5363
- function searchPath(cwd) {
5364
- const start = resolve2(cwd);
5365
- const root = findGitRoot(start);
5366
- if (root === null) return [start];
5367
- const chain = [];
5368
- let dir = start;
5369
- for (; ; ) {
5370
- chain.push(dir);
5371
- if (dir === root) return chain;
5372
- const parent = dirname2(dir);
5373
- if (parent === dir) return chain;
5374
- dir = parent;
5375
- }
5376
- }
5377
-
5378
5932
  // src/shared/warn.ts
5379
5933
  var said = /* @__PURE__ */ new Set();
5380
5934
  function warn(message) {
@@ -5396,7 +5950,7 @@ var MOOSE_CONFIG_NAMES = [
5396
5950
  async function readDocument(path, source, opts) {
5397
5951
  let text;
5398
5952
  try {
5399
- text = await readFile3(path, "utf8");
5953
+ text = await readFile4(path, "utf8");
5400
5954
  } catch {
5401
5955
  return null;
5402
5956
  }
@@ -5423,10 +5977,10 @@ function slice(document, opts) {
5423
5977
  return null;
5424
5978
  }
5425
5979
  function relativeSource(cwd, path) {
5426
- return relative2(cwd, path).replace(/\\/g, "/");
5980
+ return relative5(cwd, path).replace(/\\/g, "/");
5427
5981
  }
5428
5982
  async function findConfigFile(cwd, opts) {
5429
- const start = resolve3(cwd);
5983
+ const start = resolve6(cwd);
5430
5984
  for (const dir of searchPath(start)) {
5431
5985
  const family = [
5432
5986
  { names: FAMILY_CONFIG_NAMES, kind: "manni" },
@@ -5470,7 +6024,7 @@ async function findConfigFile(cwd, opts) {
5470
6024
  return null;
5471
6025
  }
5472
6026
  async function readConfigFile(explicitPath, cwd, opts) {
5473
- const path = resolve3(cwd, explicitPath);
6027
+ const path = resolve6(cwd, explicitPath);
5474
6028
  const document = await readDocument(path, explicitPath, opts);
5475
6029
  if (document === null) {
5476
6030
  throw opts.toError(`Config file not found: "${explicitPath}".`);
@@ -5481,7 +6035,7 @@ async function readConfigFile(explicitPath, cwd, opts) {
5481
6035
  };
5482
6036
  return {
5483
6037
  path,
5484
- dir: dirname3(path),
6038
+ dir: dirname4(path),
5485
6039
  source: explicitPath,
5486
6040
  text: document.text,
5487
6041
  kind: "explicit",
@@ -5507,6 +6061,7 @@ function asElementPaths(value, where, source) {
5507
6061
  }
5508
6062
  var OVERRIDE_KEYS = ["name", "files", "schemas", "elements"];
5509
6063
  var CHECK_KEYS = ["name", "query"];
6064
+ var SIDECAR_KEYS = ["file", "keys", "tokenEnv", "join"];
5510
6065
  var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
5511
6066
  var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Set(["query"]);
5512
6067
  var FILL_KEYS = [
@@ -5523,6 +6078,7 @@ var CONFIG_KEYS = [
5523
6078
  "schemas",
5524
6079
  "overrides",
5525
6080
  "checks",
6081
+ "sidecars",
5526
6082
  "baseline",
5527
6083
  "allowEmpty",
5528
6084
  "respectGitignore",
@@ -5666,6 +6222,93 @@ function withSection(message, source, section) {
5666
6222
  if (/^[a-zA-Z]+(\[|\.| —)/.test(rest)) return `${head}${section}.${rest}`;
5667
6223
  return message;
5668
6224
  }
6225
+ function parseSidecars(raw, source) {
6226
+ if (!Array.isArray(raw)) {
6227
+ throw new DocmetaError(`${source}: "sidecars" must be a list.`);
6228
+ }
6229
+ const owners = /* @__PURE__ */ new Map();
6230
+ return raw.map((entry, i) => {
6231
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
6232
+ throw new DocmetaError(`${source}: sidecars[${i}] must be a mapping.`);
6233
+ }
6234
+ const e = entry;
6235
+ rejectUnknownKeys(e, SIDECAR_KEYS, `sidecars[${i}]`, source);
6236
+ if (typeof e.file !== "string" || e.file.trim() === "") {
6237
+ throw new DocmetaError(
6238
+ `${source}: sidecars[${i}].file must be a non-empty string naming the manifest, relative to the config file.`
6239
+ );
6240
+ }
6241
+ if (!Array.isArray(e.keys) || e.keys.length === 0 || e.keys.some((k) => typeof k !== "string" || k.trim() === "")) {
6242
+ throw new DocmetaError(
6243
+ `${source}: sidecars[${i}].keys must be a non-empty list of key names.`
6244
+ );
6245
+ }
6246
+ const isUrl = classifyRef(e.file).kind === "url";
6247
+ if (isUrl) {
6248
+ const problem = sidecarUrlProblem(e.file);
6249
+ if (problem !== null) {
6250
+ throw new DocmetaError(`${source}: sidecars[${i}].file ${problem}.`);
6251
+ }
6252
+ }
6253
+ if (e.tokenEnv !== void 0) {
6254
+ if (typeof e.tokenEnv !== "string" || e.tokenEnv.trim() === "") {
6255
+ throw new DocmetaError(
6256
+ `${source}: sidecars[${i}].tokenEnv must be the name of an environment variable.`
6257
+ );
6258
+ }
6259
+ if (!isUrl) {
6260
+ throw new DocmetaError(
6261
+ `${source}: sidecars[${i}].tokenEnv is set, but "file" is a path, so no request is made and the token would go nowhere. Remove it, or make "file" a URL.`
6262
+ );
6263
+ }
6264
+ }
6265
+ const keys = e.keys;
6266
+ if (e.join !== void 0) {
6267
+ if (typeof e.join !== "string" || e.join.trim() === "") {
6268
+ throw new DocmetaError(
6269
+ `${source}: sidecars[${i}].join must be "path" or the name of a top-level frontmatter field.`
6270
+ );
6271
+ }
6272
+ if (e.join === FILE_SCHEMA_KEY) {
6273
+ throw new DocmetaError(
6274
+ `${source}: sidecars[${i}].join may not be "${FILE_SCHEMA_KEY}".`
6275
+ );
6276
+ }
6277
+ if (keys.includes(e.join)) {
6278
+ throw new DocmetaError(
6279
+ `${source}: sidecars[${i}].join names "${e.join}", which the same entry owns \u2014 the value that selects an entry cannot come from the entry.`
6280
+ );
6281
+ }
6282
+ }
6283
+ const seen = /* @__PURE__ */ new Set();
6284
+ for (const key of keys) {
6285
+ if (key === FILE_SCHEMA_KEY) {
6286
+ throw new DocmetaError(
6287
+ `${source}: sidecars[${i}].keys may not include "${FILE_SCHEMA_KEY}" \u2014 a sidecar never chooses the schema a document is judged by; use "overrides".`
6288
+ );
6289
+ }
6290
+ if (seen.has(key)) {
6291
+ throw new DocmetaError(
6292
+ `${source}: sidecars[${i}].keys lists "${key}" twice.`
6293
+ );
6294
+ }
6295
+ seen.add(key);
6296
+ const owner = owners.get(key);
6297
+ if (owner !== void 0) {
6298
+ throw new DocmetaError(
6299
+ `${source}: sidecars[${i}].keys claims "${key}", which sidecars[${owner}] already owns \u2014 a key has exactly one sidecar.`
6300
+ );
6301
+ }
6302
+ owners.set(key, i);
6303
+ }
6304
+ return {
6305
+ file: e.file,
6306
+ keys,
6307
+ ...typeof e.tokenEnv === "string" ? { tokenEnv: e.tokenEnv } : {},
6308
+ ...typeof e.join === "string" ? { join: e.join } : {}
6309
+ };
6310
+ });
6311
+ }
5669
6312
  function parseConfigDocument(raw, source) {
5670
6313
  if (raw == null) return {};
5671
6314
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -5739,6 +6382,9 @@ function parseConfigDocument(raw, source) {
5739
6382
  if (obj.checks !== void 0) {
5740
6383
  config.checks = parseChecks(obj.checks, source);
5741
6384
  }
6385
+ if (obj.sidecars !== void 0) {
6386
+ config.sidecars = parseSidecars(obj.sidecars, source);
6387
+ }
5742
6388
  if (obj.elements !== void 0) {
5743
6389
  config.elements = asElementPaths(obj.elements, "elements", source);
5744
6390
  }
@@ -5907,8 +6553,8 @@ function parseFill(value, source) {
5907
6553
  function schemaTrustRoot(cwd, configDir) {
5908
6554
  const git = findGitRoot(cwd);
5909
6555
  if (git !== null) return { dir: git, source: "git" };
5910
- if (configDir !== void 0) return { dir: resolve4(configDir), source: "config" };
5911
- return { dir: resolve4(cwd), source: "cwd" };
6556
+ if (configDir !== void 0) return { dir: resolve7(configDir), source: "config" };
6557
+ return { dir: resolve7(cwd), source: "cwd" };
5912
6558
  }
5913
6559
  async function loadConfig(explicitPath, cwd = process.cwd()) {
5914
6560
  const file = explicitPath ? await readConfigFile(explicitPath, cwd, CONFIG_FILE) : await findConfigFile(cwd, CONFIG_FILE);
@@ -6089,7 +6735,7 @@ var Validator = class {
6089
6735
  * Callers with an extractor that supplies no column pass nothing and get the
6090
6736
  * previous behavior exactly.
6091
6737
  */
6092
- async validate(data, refs, lineFor, colFor) {
6738
+ async validate(data, refs, lineFor, colFor, locate) {
6093
6739
  const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
6094
6740
  void _omit;
6095
6741
  const errors = [];
@@ -6098,7 +6744,7 @@ var Validator = class {
6098
6744
  const ok = fn(subject);
6099
6745
  if (ok) continue;
6100
6746
  for (const e of fn.errors ?? []) {
6101
- errors.push(toFieldError(ref, e, lineFor, colFor));
6747
+ errors.push(toFieldError(ref, e, lineFor, colFor, locate));
6102
6748
  }
6103
6749
  }
6104
6750
  return errors;
@@ -6118,7 +6764,7 @@ function subjectOf(e) {
6118
6764
  return void 0;
6119
6765
  }
6120
6766
  }
6121
- function toFieldError(schema, e, lineFor, colFor) {
6767
+ function toFieldError(schema, e, lineFor, colFor, locate) {
6122
6768
  const defined = e;
6123
6769
  const instancePath = e.instancePath;
6124
6770
  const subject = subjectOf(defined);
@@ -6131,6 +6777,21 @@ function toFieldError(schema, e, lineFor, colFor) {
6131
6777
  message = `must NOT have additional property '${defined.params.additionalProperty}'`;
6132
6778
  wantsColumn = false;
6133
6779
  }
6780
+ const located = locate?.(instancePath) ?? (defined.keyword === "additionalProperties" ? locate?.(
6781
+ `${instancePath}/${escapePointerSegment(defined.params.additionalProperty)}`
6782
+ ) : void 0);
6783
+ if (located) {
6784
+ return {
6785
+ schema,
6786
+ instancePath,
6787
+ message,
6788
+ keyword: e.keyword,
6789
+ ...subject != null ? { subject } : {},
6790
+ file: located.file,
6791
+ ...located.line != null ? { line: located.line } : {},
6792
+ ...wantsColumn && located.col != null ? { col: located.col } : {}
6793
+ };
6794
+ }
6134
6795
  const line = lineFor(instancePath);
6135
6796
  const col = wantsColumn ? colFor?.(instancePath) : void 0;
6136
6797
  return {
@@ -6146,13 +6807,13 @@ function toFieldError(schema, e, lineFor, colFor) {
6146
6807
 
6147
6808
  // src/meta/commands/validate.ts
6148
6809
  import { existsSync as existsSync2 } from "node:fs";
6149
- import { readFile as readFile5 } from "node:fs/promises";
6150
- import { resolve as resolve7, extname as extname2 } from "node:path";
6810
+ import { readFile as readFile6 } from "node:fs/promises";
6811
+ import { resolve as resolve9, extname as extname2 } from "node:path";
6151
6812
 
6152
6813
  // src/meta/core/baseline.ts
6153
6814
  import { createHash as createHash3 } from "node:crypto";
6154
- import { readFile as readFile4 } from "node:fs/promises";
6155
- import { relative as relative3, resolve as resolve5 } from "node:path";
6815
+ import { readFile as readFile5 } from "node:fs/promises";
6816
+ import { relative as relative6, resolve as resolve8 } from "node:path";
6156
6817
  var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
6157
6818
  var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
6158
6819
  var BASELINE_VERSION = 1;
@@ -6160,13 +6821,13 @@ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
6160
6821
  function canonicalFilePath(file, ctx) {
6161
6822
  if (!ctx) return file;
6162
6823
  const from = ctx.runBase ?? ctx.base;
6163
- return relative3(ctx.base, resolve5(from, file)).replace(/\\/g, "/");
6824
+ return relative6(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
6164
6825
  }
6165
6826
  function canonicalSchemaRef(ref, ctx) {
6166
6827
  if (!ctx) return ref;
6167
6828
  if (classifyRef(ref).kind !== "file") return ref;
6168
- const abs = resolve5(ctx.cwd, ref);
6169
- return relative3(ctx.base, abs).replace(/\\/g, "/");
6829
+ const abs = resolve8(ctx.cwd, ref);
6830
+ return relative6(ctx.base, abs).replace(/\\/g, "/");
6170
6831
  }
6171
6832
  function fingerprint(e, ctx) {
6172
6833
  const parts = [
@@ -6195,303 +6856,129 @@ function serializeBaseline(baseline) {
6195
6856
  }
6196
6857
  function bad(source, detail) {
6197
6858
  throw new DocmetaError(`Baseline "${source}": ${detail}`);
6198
- }
6199
- function parseBaseline(text, source) {
6200
- let raw;
6201
- try {
6202
- raw = JSON.parse(stripBom(text));
6203
- } catch (err) {
6204
- bad(source, `invalid JSON: ${err.message}`);
6205
- }
6206
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6207
- bad(source, "top level must be an object.");
6208
- }
6209
- const obj = raw;
6210
- if (obj.version !== BASELINE_VERSION) {
6211
- bad(
6212
- source,
6213
- `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
6214
- );
6215
- }
6216
- const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
6217
- const rawEntries = obj.entries;
6218
- if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
6219
- bad(source, '"entries" must be an object mapping file paths to fingerprints.');
6220
- }
6221
- const entries = /* @__PURE__ */ Object.create(null);
6222
- for (const [file, value] of Object.entries(
6223
- rawEntries
6224
- )) {
6225
- if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
6226
- bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
6227
- }
6228
- const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
6229
- if (malformed !== void 0) {
6230
- bad(
6231
- source,
6232
- `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
6233
- );
6234
- }
6235
- entries[file] = value;
6236
- }
6237
- return { version: BASELINE_VERSION, generatedWith, entries };
6238
- }
6239
- async function readBaseline(absPath, source) {
6240
- let text;
6241
- try {
6242
- text = await readFile4(absPath, "utf8");
6243
- } catch (err) {
6244
- if (err.code === "ENOENT") return null;
6245
- throw new DocmetaError(
6246
- `Baseline "${source}" could not be read: ${err.message}`
6247
- );
6248
- }
6249
- return parseBaseline(text, source);
6250
- }
6251
- async function writeBaselineFile(absPath, baseline, source) {
6252
- try {
6253
- await writeFileAtomic(absPath, serializeBaseline(baseline));
6254
- } catch (err) {
6255
- throw new DocmetaError(
6256
- `Baseline "${source}" could not be written: ${err.message}`
6257
- );
6258
- }
6259
- }
6260
- function buildBaseline(results, generatedWith, ctx) {
6261
- const entries = /* @__PURE__ */ Object.create(null);
6262
- for (const r of results) {
6263
- if (r.errors.length === 0) continue;
6264
- const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
6265
- entries[canonicalFilePath(r.file, ctx)] = prints;
6266
- }
6267
- return { version: BASELINE_VERSION, generatedWith, entries };
6268
- }
6269
- function applyBaseline(results, baseline, ctx) {
6270
- let recorded = 0;
6271
- let suppressed = 0;
6272
- let stale = 0;
6273
- const applied = results.map((r) => {
6274
- const known = baseline.entries[canonicalFilePath(r.file, ctx)];
6275
- if (!known) return r;
6276
- const recordedHere = new Set(known);
6277
- recorded += recordedHere.size;
6278
- const seen = /* @__PURE__ */ new Set();
6279
- const fresh = [];
6280
- for (const e of r.errors) {
6281
- const print = fingerprint(e, ctx);
6282
- if (recordedHere.has(print)) {
6283
- seen.add(print);
6284
- suppressed += 1;
6285
- } else {
6286
- fresh.push(e);
6287
- }
6288
- }
6289
- stale += recordedHere.size - seen.size;
6290
- const baselined = r.errors.length - fresh.length;
6291
- return {
6292
- ...r,
6293
- ok: fresh.length === 0,
6294
- errors: fresh,
6295
- ...baselined > 0 ? { baselined } : {}
6296
- };
6297
- });
6298
- return { results: applied, recorded, suppressed, stale };
6299
- }
6300
- function diffBaselines(previous, next) {
6301
- const flatten = (b) => {
6302
- const out = /* @__PURE__ */ new Set();
6303
- if (!b) return out;
6304
- for (const [file, prints] of Object.entries(b.entries)) {
6305
- for (const p of prints) out.add(`${file}\0${p}`);
6306
- }
6307
- return out;
6308
- };
6309
- const before = flatten(previous);
6310
- const after = flatten(next);
6311
- let added = 0;
6312
- let removed = 0;
6313
- for (const k of after) if (!before.has(k)) added += 1;
6314
- for (const k of before) if (!after.has(k)) removed += 1;
6315
- return { added, removed };
6316
- }
6317
- function countFingerprints(baseline) {
6318
- let n = 0;
6319
- for (const prints of Object.values(baseline.entries)) n += prints.length;
6320
- return n;
6321
- }
6322
-
6323
- // src/meta/core/load-files.ts
6324
- import { stat as stat3 } from "node:fs/promises";
6325
- import { extname, relative as relative4, resolve as resolve6 } from "node:path";
6326
- import fg from "fast-glob";
6327
- import picomatch2 from "picomatch";
6328
-
6329
- // src/meta/core/gitignore.ts
6330
- import { spawn } from "node:child_process";
6331
- var GITIGNORE_UNAVAILABLE = ".gitignore filtering was requested but git could not answer here (no repository, or no git on PATH); no files were skipped.";
6332
- var unavailable = () => ({
6333
- ignored: /* @__PURE__ */ new Set(),
6334
- available: false
6335
- });
6336
- function gitIgnored(paths, cwd) {
6337
- if (paths.length === 0) {
6338
- return Promise.resolve({ ignored: /* @__PURE__ */ new Set(), available: true });
6339
- }
6340
- return new Promise((settle) => {
6341
- let done = false;
6342
- const finish = (answer) => {
6343
- if (done) return;
6344
- done = true;
6345
- settle(answer);
6346
- };
6347
- let child;
6348
- try {
6349
- child = spawn("git", ["check-ignore", "--stdin", "-z"], {
6350
- cwd,
6351
- windowsHide: true
6352
- });
6353
- } catch {
6354
- finish(unavailable());
6355
- return;
6356
- }
6357
- let out = "";
6358
- child.stdout.setEncoding("utf8");
6359
- child.stdout.on("data", (chunk) => {
6360
- out += chunk;
6361
- });
6362
- child.stderr.resume();
6363
- child.on("error", () => {
6364
- finish(unavailable());
6365
- });
6366
- child.stdin.on("error", () => {
6367
- });
6368
- child.on("close", (code) => {
6369
- if (code !== 0 && code !== 1) {
6370
- finish(unavailable());
6371
- return;
6372
- }
6373
- finish({
6374
- ignored: new Set(out.split("\0").filter((p) => p !== "")),
6375
- available: true
6376
- });
6377
- });
6378
- child.stdin.end(paths.join("\0"));
6379
- });
6380
- }
6381
-
6382
- // src/meta/core/load-files.ts
6383
- var DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
6384
- var STDIN_TOKEN = "-";
6385
- var STDIN_LABEL = "<stdin>";
6386
- function toPosix(p) {
6387
- return p.replace(/\\/g, "/");
6388
- }
6389
- async function statOrNull(p) {
6390
- try {
6391
- return await stat3(p);
6392
- } catch {
6393
- return null;
6394
- }
6395
- }
6396
- function gitignoreOptions(opts) {
6397
- const asked = opts.flag === true || opts.flag === void 0 && opts.configured === true;
6398
- const notice = opts.onNotice;
6399
- return {
6400
- respectGitignore: opts.flag ?? opts.configured ?? true,
6401
- ...asked && notice ? {
6402
- onGitignoreUnavailable: () => {
6403
- notice(GITIGNORE_UNAVAILABLE);
6404
- }
6405
- } : {}
6406
- };
6407
- }
6408
- async function resolveTargetSet(opts) {
6409
- const cwd = opts.cwd ?? process.cwd();
6410
- const exts = (opts.exts ?? supportedExtensions()).map(
6411
- (e) => e.toLowerCase().startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
6412
- );
6413
- const ignore = [...DEFAULT_IGNORE, ...opts.exclude ?? []];
6414
- const named = /* @__PURE__ */ new Set();
6415
- const walked = /* @__PURE__ */ new Set();
6416
- const keepByExt = (file) => exts.includes(extname(file).toLowerCase());
6417
- const missing = [];
6418
- for (const input of opts.inputs) {
6419
- if (input === STDIN_TOKEN) continue;
6420
- const posixInput = toPosix(input);
6421
- const abs = resolve6(cwd, posixInput);
6422
- const st = await statOrNull(abs);
6423
- if (st?.isFile()) {
6424
- named.add(toPosix(relative4(cwd, abs)));
6425
- continue;
6426
- }
6427
- if (st?.isDirectory()) {
6428
- const found2 = await fg(`${posixInput}/**/*`, {
6429
- cwd,
6430
- ignore,
6431
- onlyFiles: true,
6432
- dot: false
6433
- });
6434
- for (const f of found2) if (keepByExt(f)) walked.add(f);
6435
- continue;
6859
+ }
6860
+ function parseBaseline(text, source) {
6861
+ let raw;
6862
+ try {
6863
+ raw = JSON.parse(stripBom(text));
6864
+ } catch (err) {
6865
+ bad(source, `invalid JSON: ${err.message}`);
6866
+ }
6867
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6868
+ bad(source, "top level must be an object.");
6869
+ }
6870
+ const obj = raw;
6871
+ if (obj.version !== BASELINE_VERSION) {
6872
+ bad(
6873
+ source,
6874
+ `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
6875
+ );
6876
+ }
6877
+ const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
6878
+ const rawEntries = obj.entries;
6879
+ if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
6880
+ bad(source, '"entries" must be an object mapping file paths to fingerprints.');
6881
+ }
6882
+ const entries = /* @__PURE__ */ Object.create(null);
6883
+ for (const [file, value] of Object.entries(
6884
+ rawEntries
6885
+ )) {
6886
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
6887
+ bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
6436
6888
  }
6437
- if (!picomatch2.scan(posixInput).isGlob) {
6438
- missing.push(posixInput);
6439
- continue;
6889
+ const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
6890
+ if (malformed !== void 0) {
6891
+ bad(
6892
+ source,
6893
+ `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
6894
+ );
6440
6895
  }
6441
- const found = await fg(posixInput, {
6442
- cwd,
6443
- ignore,
6444
- onlyFiles: true,
6445
- dot: false
6446
- });
6447
- for (const f of found) if (keepByExt(f)) walked.add(f);
6896
+ entries[file] = value;
6448
6897
  }
6449
- if (missing.length > 0 && !opts.allowEmpty) {
6450
- const names = missing.map((m) => `"${m}"`).join(", ");
6898
+ return { version: BASELINE_VERSION, generatedWith, entries };
6899
+ }
6900
+ async function readBaseline(absPath, source) {
6901
+ let text;
6902
+ try {
6903
+ text = await readFile5(absPath, "utf8");
6904
+ } catch (err) {
6905
+ if (err.code === "ENOENT") return null;
6451
6906
  throw new DocmetaError(
6452
- missing.length === 1 ? `File not found: ${names}.` : `Files not found: ${names}.`
6907
+ `Baseline "${source}" could not be read: ${err.message}`
6453
6908
  );
6454
6909
  }
6455
- let gitignoreSkipped = 0;
6456
- if (opts.respectGitignore !== false && walked.size > 0) {
6457
- const candidates = [...walked].filter((f) => !named.has(f));
6458
- const { ignored, available } = await gitIgnored(candidates, cwd);
6459
- if (available) {
6460
- for (const f of ignored) {
6461
- if (walked.delete(f)) gitignoreSkipped += 1;
6910
+ return parseBaseline(text, source);
6911
+ }
6912
+ async function writeBaselineFile(absPath, baseline, source) {
6913
+ try {
6914
+ await writeFileAtomic(absPath, serializeBaseline(baseline));
6915
+ } catch (err) {
6916
+ throw new DocmetaError(
6917
+ `Baseline "${source}" could not be written: ${err.message}`
6918
+ );
6919
+ }
6920
+ }
6921
+ function buildBaseline(results, generatedWith, ctx) {
6922
+ const entries = /* @__PURE__ */ Object.create(null);
6923
+ for (const r of results) {
6924
+ if (r.errors.length === 0) continue;
6925
+ const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
6926
+ entries[canonicalFilePath(r.file, ctx)] = prints;
6927
+ }
6928
+ return { version: BASELINE_VERSION, generatedWith, entries };
6929
+ }
6930
+ function applyBaseline(results, baseline, ctx) {
6931
+ let recorded = 0;
6932
+ let suppressed = 0;
6933
+ let stale = 0;
6934
+ const applied = results.map((r) => {
6935
+ const known = baseline.entries[canonicalFilePath(r.file, ctx)];
6936
+ if (!known) return r;
6937
+ const recordedHere = new Set(known);
6938
+ recorded += recordedHere.size;
6939
+ const seen = /* @__PURE__ */ new Set();
6940
+ const fresh = [];
6941
+ for (const e of r.errors) {
6942
+ const print = fingerprint(e, ctx);
6943
+ if (recordedHere.has(print)) {
6944
+ seen.add(print);
6945
+ suppressed += 1;
6946
+ } else {
6947
+ fresh.push(e);
6462
6948
  }
6463
- } else {
6464
- opts.onGitignoreUnavailable?.();
6465
6949
  }
6466
- }
6467
- return {
6468
- files: [.../* @__PURE__ */ new Set([...named, ...walked])].sort(),
6469
- gitignoreSkipped
6950
+ stale += recordedHere.size - seen.size;
6951
+ const baselined = r.errors.length - fresh.length;
6952
+ return {
6953
+ ...r,
6954
+ ok: fresh.length === 0,
6955
+ errors: fresh,
6956
+ ...baselined > 0 ? { baselined } : {}
6957
+ };
6958
+ });
6959
+ return { results: applied, recorded, suppressed, stale };
6960
+ }
6961
+ function diffBaselines(previous, next) {
6962
+ const flatten = (b) => {
6963
+ const out = /* @__PURE__ */ new Set();
6964
+ if (!b) return out;
6965
+ for (const [file, prints] of Object.entries(b.entries)) {
6966
+ for (const p of prints) out.add(`${file}\0${p}`);
6967
+ }
6968
+ return out;
6470
6969
  };
6970
+ const before = flatten(previous);
6971
+ const after = flatten(next);
6972
+ let added = 0;
6973
+ let removed = 0;
6974
+ for (const k of after) if (!before.has(k)) added += 1;
6975
+ for (const k of before) if (!after.has(k)) removed += 1;
6976
+ return { added, removed };
6471
6977
  }
6472
- function assertNonEmpty(p) {
6473
- if (p.allowEmpty || p.usingStdin || p.files.length > 0) return;
6474
- const tried = p.inputs.map((i) => `"${i}"`).join(", ");
6475
- const notes = [];
6476
- if (p.exts && p.exts.length > 0) {
6477
- notes.push(`extensions: ${p.exts.join(", ")}`);
6478
- }
6479
- if (p.exclude && p.exclude.length > 0) {
6480
- notes.push(`excludes: ${p.exclude.map((e) => `"${e}"`).join(", ")}`);
6481
- }
6482
- if (p.gitignoreSkipped && p.gitignoreSkipped > 0) {
6483
- notes.push(
6484
- `.gitignore skipped ${p.gitignoreSkipped} (pass --no-gitignore to check them)`
6485
- );
6486
- }
6487
- throw new DocmetaError(
6488
- [
6489
- `No files matched. Patterns tried: ${tried}.`,
6490
- ...notes.length > 0 ? [`Filters applied \u2014 ${notes.join("; ")}.`] : [],
6491
- `Nothing was ${p.action}, so this is an error rather than a pass.`,
6492
- `Pass --allow-empty (or set allowEmpty: true) if matching nothing is expected.`
6493
- ].join("\n")
6494
- );
6978
+ function countFingerprints(baseline) {
6979
+ let n = 0;
6980
+ for (const prints of Object.values(baseline.entries)) n += prints.length;
6981
+ return n;
6495
6982
  }
6496
6983
 
6497
6984
  // src/meta/core/projection.ts
@@ -6898,16 +7385,16 @@ function parseErrorResult(file, format, message, keyword) {
6898
7385
  }
6899
7386
  function resolveBaselineRequest(opts, configured, configDir, cwd) {
6900
7387
  const named = (label) => ({
6901
- absPath: resolve7(cwd, label),
7388
+ absPath: resolve9(cwd, label),
6902
7389
  label
6903
7390
  });
6904
7391
  const implied = () => {
6905
7392
  const base = configDir ?? cwd;
6906
7393
  if (configured !== void 0) {
6907
- return { absPath: resolve7(base, configured), label: configured };
7394
+ return { absPath: resolve9(base, configured), label: configured };
6908
7395
  }
6909
- const current = resolve7(base, DEFAULT_BASELINE_PATH);
6910
- const legacy = resolve7(base, LEGACY_BASELINE_PATH);
7396
+ const current = resolve9(base, DEFAULT_BASELINE_PATH);
7397
+ const legacy = resolve9(base, LEGACY_BASELINE_PATH);
6911
7398
  if (!existsSync2(current) && existsSync2(legacy)) {
6912
7399
  warn(
6913
7400
  `"${LEGACY_BASELINE_PATH}" is the pre-rename baseline file name. Rename it to "${DEFAULT_BASELINE_PATH}".`
@@ -6975,6 +7462,11 @@ async function runValidate(opts) {
6975
7462
  action: "validated"
6976
7463
  });
6977
7464
  const trustRoot = schemaTrustRoot(cwd, configDir);
7465
+ const sidecars = await loadSidecars(config, {
7466
+ configDir: configDir ?? cwd,
7467
+ base,
7468
+ offline: opts.offline ?? config?.offline ?? false
7469
+ });
6978
7470
  const validator = new Validator(
6979
7471
  schemaLoadOptions({
6980
7472
  // The config's directory when a config governs the run, so one project
@@ -6990,9 +7482,14 @@ async function runValidate(opts) {
6990
7482
  })
6991
7483
  );
6992
7484
  const results = [];
7485
+ const joinHits = /* @__PURE__ */ new Map();
6993
7486
  const scoped = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
6994
7487
  usingStdin || opts.as !== void 0 || opts.exts !== void 0 || opts.exclude !== void 0 && opts.exclude.length > 0 || opts.respectGitignore !== void 0 || (opts.cliSchemas?.length ?? 0) > 0;
6995
7488
  const configuredChecks = config?.checks ?? [];
7489
+ if (!scoped) {
7490
+ const orphans = orphanEntries(sidecars, files, base);
7491
+ if (orphans.length > 0) throw orphanError(orphans);
7492
+ }
6996
7493
  const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
6997
7494
  const checkEntries = [];
6998
7495
  const checkResolved = /* @__PURE__ */ new Map();
@@ -7015,6 +7512,16 @@ async function runValidate(opts) {
7015
7512
  );
7016
7513
  return;
7017
7514
  }
7515
+ const merged = mergeSidecars(label, extracted, sidecars, base);
7516
+ extracted = merged.extracted;
7517
+ for (const j of merged.joins) {
7518
+ const byValue = joinHits.get(j.field) ?? /* @__PURE__ */ new Map();
7519
+ const line = extracted.lineFor(j.field);
7520
+ const hits = byValue.get(j.value) ?? [];
7521
+ hits.push({ label, file: j.file, ...line != null ? { line } : {} });
7522
+ byValue.set(j.value, hits);
7523
+ joinHits.set(j.field, byValue);
7524
+ }
7018
7525
  if (checksWillRun) checkEntries.push({ label, extracted });
7019
7526
  let resolved;
7020
7527
  try {
@@ -7044,7 +7551,8 @@ async function runValidate(opts) {
7044
7551
  extracted.data,
7045
7552
  schemaSet,
7046
7553
  extracted.lineFor,
7047
- extracted.colFor
7554
+ extracted.colFor,
7555
+ merged.locate
7048
7556
  );
7049
7557
  } catch (err) {
7050
7558
  if (!(err instanceof DocmetaError) || resolved.source !== "document") {
@@ -7055,6 +7563,17 @@ async function runValidate(opts) {
7055
7563
  );
7056
7564
  return;
7057
7565
  }
7566
+ for (const { key, file } of merged.collisions) {
7567
+ const line = extracted.lineFor(key);
7568
+ errors.push({
7569
+ schema: SIDECAR_OWNED_SCHEMA,
7570
+ keyword: SIDECAR_KEYWORD,
7571
+ subject: key,
7572
+ instancePath: sidecarPointer(key),
7573
+ message: `"${key}" is owned by sidecar ${file}; remove it from the document`,
7574
+ ...line != null ? { line } : {}
7575
+ });
7576
+ }
7058
7577
  results.push({
7059
7578
  file: label,
7060
7579
  format: extractor.name,
@@ -7073,9 +7592,37 @@ async function runValidate(opts) {
7073
7592
  await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
7074
7593
  }
7075
7594
  for (const file of files) {
7076
- const content = await readFile5(resolve7(base, file), "utf8");
7595
+ const content = await readFile6(resolve9(base, file), "utf8");
7077
7596
  await processOne(file, content, extname2(file));
7078
7597
  }
7598
+ if (sidecars) {
7599
+ const byLabel = new Map(results.map((r) => [r.file, r]));
7600
+ const matched = /* @__PURE__ */ new Map();
7601
+ for (const [field, byValue] of joinHits) {
7602
+ matched.set(field, new Set(byValue.keys()));
7603
+ for (const [value, hits] of byValue) {
7604
+ if (hits.length < 2) continue;
7605
+ for (const hit of hits) {
7606
+ const others = hits.filter((h) => h.label !== hit.label).map((h) => h.label).join(", ");
7607
+ const result = byLabel.get(hit.label);
7608
+ if (!result) continue;
7609
+ result.errors.push({
7610
+ schema: SIDECAR_DUPLICATE_SCHEMA,
7611
+ keyword: SIDECAR_KEYWORD,
7612
+ subject: value,
7613
+ instancePath: sidecarPointer(field),
7614
+ message: `${hits.length} documents carry ${field} "${value}"; ${hit.file} cannot tell them apart (${others})`,
7615
+ ...hit.line != null ? { line: hit.line } : {}
7616
+ });
7617
+ result.ok = false;
7618
+ }
7619
+ }
7620
+ }
7621
+ if (!scoped) {
7622
+ const orphans = orphanJoins(sidecars, matched);
7623
+ if (orphans.length > 0) throw orphanError(orphans);
7624
+ }
7625
+ }
7079
7626
  if (configuredChecks.length > 0 && opts.checks !== false) {
7080
7627
  if (scoped) {
7081
7628
  opts.onNotice?.("corpus checks skipped: run is scoped");
@@ -7165,14 +7712,14 @@ async function settleBaseline(results, request, ctx) {
7165
7712
  }
7166
7713
 
7167
7714
  // src/meta/commands/get.ts
7168
- import { readFile as readFile6 } from "node:fs/promises";
7169
- import { resolve as resolve8, extname as extname3 } from "node:path";
7715
+ import { readFile as readFile7 } from "node:fs/promises";
7716
+ import { resolve as resolve10, extname as extname3 } from "node:path";
7170
7717
  async function runGet(opts) {
7171
7718
  const cwd = opts.cwd ?? process.cwd();
7172
7719
  if (opts.fields.length === 0) {
7173
7720
  throw new DocmetaError("Specify at least one field to get.");
7174
7721
  }
7175
- const { config, inputs, base } = await resolveRunConfig({
7722
+ const { config, inputs, base, configDir } = await resolveRunConfig({
7176
7723
  cwd,
7177
7724
  configPath: opts.configPath,
7178
7725
  noConfig: opts.noConfig,
@@ -7218,6 +7765,11 @@ async function runGet(opts) {
7218
7765
  action: "read"
7219
7766
  });
7220
7767
  const out = [];
7768
+ const sidecars = await loadSidecars(config, {
7769
+ configDir: configDir ?? cwd,
7770
+ base,
7771
+ offline: opts.offline ?? config?.offline ?? false
7772
+ });
7221
7773
  const readOne = (label, content, extension) => {
7222
7774
  const extractor = forced ?? extractorForExtension(extension);
7223
7775
  if (!extractor) {
@@ -7227,9 +7779,14 @@ async function runGet(opts) {
7227
7779
  }
7228
7780
  let extracted;
7229
7781
  try {
7230
- extracted = extractor.extract(content, label, {
7231
- elements: resolveElements(label, config)
7232
- });
7782
+ extracted = mergeSidecars(
7783
+ label,
7784
+ extractor.extract(content, label, {
7785
+ elements: resolveElements(label, config)
7786
+ }),
7787
+ sidecars,
7788
+ base
7789
+ ).extracted;
7233
7790
  } catch (err) {
7234
7791
  if (err instanceof DocmetaError) throw err;
7235
7792
  const reason = err instanceof Error ? err.message : String(err);
@@ -7249,7 +7806,7 @@ async function runGet(opts) {
7249
7806
  readOne("<stdin>", opts.stdinContent ?? "", forced.extensions[0] ?? "");
7250
7807
  }
7251
7808
  for (const file of files) {
7252
- const content = await readFile6(resolve8(base, file), "utf8");
7809
+ const content = await readFile7(resolve10(base, file), "utf8");
7253
7810
  readOne(file, content, extname3(file));
7254
7811
  }
7255
7812
  return out;
@@ -7286,16 +7843,16 @@ function parseJsonPointer(pointer) {
7286
7843
 
7287
7844
  // src/meta/commands/schemas.ts
7288
7845
  import { existsSync as existsSync3 } from "node:fs";
7289
- import { mkdir as mkdir2, readFile as readFile7 } from "node:fs/promises";
7290
- import { dirname as dirname4, extname as extname4, isAbsolute as isAbsolute2, join as join5, relative as relative5, resolve as resolve9, sep } from "node:path";
7291
- import { parseDocument as parseDocument3, parse as parseYaml3, stringify } from "yaml";
7846
+ import { mkdir as mkdir2, readFile as readFile8 } from "node:fs/promises";
7847
+ import { dirname as dirname5, extname as extname4, isAbsolute as isAbsolute3, join as join5, relative as relative7, resolve as resolve11, sep as sep3 } from "node:path";
7848
+ import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
7292
7849
  function getSchemasInfo() {
7293
7850
  return { builtins: listBuiltins(), formats: listFormats() };
7294
7851
  }
7295
7852
  var DEFAULT_VENDOR_DIR = "./schema";
7296
7853
  var DEFAULT_CONFIG_NAME = "manni.config.yaml";
7297
7854
  function posixRelative(from, to) {
7298
- return relative5(from, to).split(sep).join("/");
7855
+ return relative7(from, to).split(sep3).join("/");
7299
7856
  }
7300
7857
  function vendorFileName(url) {
7301
7858
  let parsed2;
@@ -7360,7 +7917,7 @@ function renderConfig(existing, entries, section) {
7360
7917
  if (existing === null) {
7361
7918
  return stringify({ [META_SECTION]: { schemas: entries } });
7362
7919
  }
7363
- const doc = parseDocument3(existing);
7920
+ const doc = parseDocument4(existing);
7364
7921
  doc.setIn(section === void 0 ? ["schemas"] : [section, "schemas"], entries);
7365
7922
  return doc.toString();
7366
7923
  }
@@ -7381,13 +7938,13 @@ async function runVendorSchema(opts) {
7381
7938
  );
7382
7939
  }
7383
7940
  const loaded = await loadConfig(opts.configPath, cwd);
7384
- const configPath = loaded?.path ?? resolve9(cwd, DEFAULT_CONFIG_NAME);
7941
+ const configPath = loaded?.path ?? resolve11(cwd, DEFAULT_CONFIG_NAME);
7385
7942
  const configDir = loaded?.dir ?? cwd;
7386
- const absDir = resolve9(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
7943
+ const absDir = resolve11(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
7387
7944
  const absFile = join5(absDir, vendorFileName(url));
7388
7945
  await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
7389
7946
  const fromConfig = posixRelative(configDir, absFile);
7390
- const ref = isAbsolute2(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
7947
+ const ref = isAbsolute3(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
7391
7948
  const { bytes } = await fetchSchemaBytes(url, {
7392
7949
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
7393
7950
  ...opts.maxBytes !== void 0 ? { maxBytes: opts.maxBytes } : {}
@@ -7402,7 +7959,7 @@ async function runVendorSchema(opts) {
7402
7959
  }
7403
7960
  let unchanged = false;
7404
7961
  if (existsSync3(absFile)) {
7405
- const current = await readFile7(absFile);
7962
+ const current = await readFile8(absFile);
7406
7963
  unchanged = bytes.equals(current);
7407
7964
  if (!unchanged && !folded.replaced) {
7408
7965
  throw new DocmetaError(
@@ -7412,7 +7969,7 @@ async function runVendorSchema(opts) {
7412
7969
  }
7413
7970
  await mkdir2(absDir, { recursive: true });
7414
7971
  await writeFileAtomic(absFile, bytes);
7415
- const existing = loaded ? await readFile7(configPath, "utf8") : null;
7972
+ const existing = loaded ? await readFile8(configPath, "utf8") : null;
7416
7973
  const section = loaded ? loaded.section : META_SECTION;
7417
7974
  const text = renderConfig(existing, folded.entries, section);
7418
7975
  reparseConfig(text, posixRelative(cwd, configPath) || DEFAULT_CONFIG_NAME, section);
@@ -7531,7 +8088,7 @@ async function runInferSchema(opts) {
7531
8088
  `--min-coverage must be a percentage between 0 and 100, got ${minCoverage}.`
7532
8089
  );
7533
8090
  }
7534
- const { config, inputs, base } = await resolveRunConfig({
8091
+ const { config, inputs, base, configDir } = await resolveRunConfig({
7535
8092
  cwd,
7536
8093
  configPath: opts.configPath,
7537
8094
  noConfig: opts.noConfig,
@@ -7539,6 +8096,11 @@ async function runInferSchema(opts) {
7539
8096
  onConfigLoaded: opts.onConfigLoaded
7540
8097
  });
7541
8098
  const usingStdin = inputs.includes(STDIN_TOKEN);
8099
+ const sidecars = await loadSidecars(config, {
8100
+ configDir: configDir ?? cwd,
8101
+ base,
8102
+ offline: opts.offline ?? config?.offline ?? false
8103
+ });
7542
8104
  if (inputs.length === 0) {
7543
8105
  throw new DocmetaError(
7544
8106
  "No files to scan. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
@@ -7557,10 +8119,10 @@ async function runInferSchema(opts) {
7557
8119
  }
7558
8120
  let absOut;
7559
8121
  if (opts.out !== void 0) {
7560
- absOut = resolve9(cwd, opts.out);
8122
+ absOut = resolve11(cwd, opts.out);
7561
8123
  await assertNotIgnored(
7562
8124
  absOut,
7563
- dirname4(absOut),
8125
+ dirname5(absOut),
7564
8126
  cwd,
7565
8127
  opts.onNotice,
7566
8128
  DRAFT_IGNORE_TEXT
@@ -7611,9 +8173,14 @@ async function runInferSchema(opts) {
7611
8173
  filesScanned += 1;
7612
8174
  let extracted;
7613
8175
  try {
7614
- extracted = extractor.extract(content, label, {
7615
- elements: resolveElements(label, config)
7616
- });
8176
+ extracted = mergeSidecars(
8177
+ label,
8178
+ extractor.extract(content, label, {
8179
+ elements: resolveElements(label, config)
8180
+ }),
8181
+ sidecars,
8182
+ base
8183
+ ).extracted;
7617
8184
  } catch (err) {
7618
8185
  unreadable.push({
7619
8186
  file: label,
@@ -7635,7 +8202,7 @@ async function runInferSchema(opts) {
7635
8202
  scanOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
7636
8203
  }
7637
8204
  for (const file of files) {
7638
- const content = await readFile7(resolve9(base, file), "utf8");
8205
+ const content = await readFile8(resolve11(base, file), "utf8");
7639
8206
  scanOne(file, content, extname4(file));
7640
8207
  }
7641
8208
  for (const u of unreadable) {
@@ -7677,7 +8244,7 @@ async function runInferSchema(opts) {
7677
8244
  };
7678
8245
  const keys = kept.map(({ sawEmptyString: _drop, ...rest }) => rest);
7679
8246
  if (absOut !== void 0) {
7680
- await mkdir2(dirname4(absOut), { recursive: true });
8247
+ await mkdir2(dirname5(absOut), { recursive: true });
7681
8248
  await writeFileAtomic(absOut, `${JSON.stringify(draft, null, 2)}
7682
8249
  `);
7683
8250
  }
@@ -7696,9 +8263,9 @@ async function runInferSchema(opts) {
7696
8263
  // src/meta/commands/query.ts
7697
8264
  import { randomBytes } from "node:crypto";
7698
8265
  import { existsSync as existsSync4 } from "node:fs";
7699
- import { mkdir as mkdir3, open, readFile as readFile8, rename as rename2, rm as rm2 } from "node:fs/promises";
7700
- import { dirname as dirname5, isAbsolute as isAbsolute3, resolve as resolve10, extname as extname5, sep as sep2 } from "node:path";
7701
- import { parseDocument as parseDocument4, isMap as isMap3, isSeq as isSeq2, isScalar as isScalar2 } from "yaml";
8266
+ import { mkdir as mkdir3, open, readFile as readFile9, rename as rename2, rm as rm2 } from "node:fs/promises";
8267
+ import { dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve12, extname as extname5, sep as sep4 } from "node:path";
8268
+ import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
7702
8269
  async function runQuery(opts) {
7703
8270
  const cwd = opts.cwd ?? process.cwd();
7704
8271
  const sql = opts.sql.trim();
@@ -7778,6 +8345,11 @@ async function runQuery(opts) {
7778
8345
  action: "queried"
7779
8346
  });
7780
8347
  const entries = [];
8348
+ const sidecars = await loadSidecars(config, {
8349
+ configDir: configDir ?? cwd,
8350
+ base,
8351
+ offline: opts.offline ?? config?.offline ?? false
8352
+ });
7781
8353
  const readOne = (label, content, extension) => {
7782
8354
  const extractor = forced ?? extractorForExtension(extension);
7783
8355
  if (!extractor) {
@@ -7785,9 +8357,14 @@ async function runQuery(opts) {
7785
8357
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7786
8358
  );
7787
8359
  }
7788
- const extracted = extractor.extract(content, label, {
7789
- elements: resolveElements(label, config)
7790
- });
8360
+ const extracted = mergeSidecars(
8361
+ label,
8362
+ extractor.extract(content, label, {
8363
+ elements: resolveElements(label, config)
8364
+ }),
8365
+ sidecars,
8366
+ base
8367
+ ).extracted;
7791
8368
  entries.push({ label, extracted, extractor });
7792
8369
  };
7793
8370
  if (usingStdin) {
@@ -7805,10 +8382,10 @@ async function runQuery(opts) {
7805
8382
  readOne(STDIN_LABEL, opts.stdinContent ?? "", ext);
7806
8383
  }
7807
8384
  for (const file of files) {
7808
- const content = await readFile8(resolve10(base, file), "utf8");
8385
+ const content = await readFile9(resolve12(base, file), "utf8");
7809
8386
  readOne(file, content, extname5(file));
7810
8387
  }
7811
- const db = opts.db === void 0 ? void 0 : { resolved: resolve10(cwd, opts.db), display: opts.db };
8388
+ const db = opts.db === void 0 ? void 0 : { resolved: resolve12(cwd, opts.db), display: opts.db };
7812
8389
  const run = await runSql(sql, entries, {
7813
8390
  target: db,
7814
8391
  params: boundParams,
@@ -7820,6 +8397,7 @@ async function runQuery(opts) {
7820
8397
  configDir,
7821
8398
  configPath,
7822
8399
  trustRoot: schemaTrustRoot(cwd, configDir),
8400
+ sidecars,
7823
8401
  onNotice: opts.onNotice
7824
8402
  });
7825
8403
  return { ...run, frame: { cwd, base: configDir ?? cwd, runBase: base } };
@@ -7883,7 +8461,7 @@ async function runSql(sql, entries, ctx) {
7883
8461
  );
7884
8462
  const { DatabaseSync } = await loadSqlite();
7885
8463
  if (target) {
7886
- await mkdir3(dirname5(target.resolved), { recursive: true });
8464
+ await mkdir3(dirname6(target.resolved), { recursive: true });
7887
8465
  await prepareDbTarget(target.resolved, target.display);
7888
8466
  }
7889
8467
  let db;
@@ -8335,20 +8913,20 @@ function displayPath(ctx, abs) {
8335
8913
  }
8336
8914
  function assertSchemaWriteWithin(ctx, abs) {
8337
8915
  const within = posixRelative(ctx.trustRoot.dir, abs);
8338
- if (within !== "" && !within.startsWith("..") && !isAbsolute3(within)) return;
8916
+ if (within !== "" && !within.startsWith("..") && !isAbsolute4(within)) return;
8339
8917
  throw new DocmetaError(
8340
8918
  `The resolved schema "${abs}" lives outside ${ctx.trustRoot.dir}; DDL edits schemas inside the repository. Evolve it where it lives, or vendor a copy in.`
8341
8919
  );
8342
8920
  }
8343
8921
  function foldPath(p) {
8344
- const abs = resolve10(p);
8922
+ const abs = resolve12(p);
8345
8923
  return process.platform === "win32" ? abs.toLowerCase() : abs;
8346
8924
  }
8347
8925
  function filePinsByPath(ctx) {
8348
8926
  const byPath = /* @__PURE__ */ new Map();
8349
8927
  for (const [ref, pin] of collectSchemaPins(ctx.config)) {
8350
8928
  if (classifyRef(ref).kind !== "file") continue;
8351
- byPath.set(foldPath(resolve10(ctx.cwd, ref)), pin);
8929
+ byPath.set(foldPath(resolve12(ctx.cwd, ref)), pin);
8352
8930
  }
8353
8931
  return byPath;
8354
8932
  }
@@ -8376,13 +8954,13 @@ async function loadSetMembers(refs, ctx) {
8376
8954
  });
8377
8955
  continue;
8378
8956
  }
8379
- const abs = resolve10(ctx.cwd, ref);
8957
+ const abs = resolve12(ctx.cwd, ref);
8380
8958
  const canon = foldPath(abs);
8381
8959
  if (seenFiles.has(canon)) continue;
8382
8960
  seenFiles.add(canon);
8383
8961
  let text;
8384
8962
  try {
8385
- text = await readFile8(abs, "utf8");
8963
+ text = await readFile9(abs, "utf8");
8386
8964
  } catch {
8387
8965
  throw new DocmetaError(
8388
8966
  `Schema file not found: "${ref}" (looked at ${abs}).`
@@ -8576,7 +9154,7 @@ async function planSchemaMutation(ops, entries, ctx) {
8576
9154
  const [, name2, ver] = target.builtinId.split(":");
8577
9155
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "-");
8578
9156
  const forkName = `${safe(name2 ?? "schema")}-${safe(ver ?? "0")}.local.json`;
8579
- schemaAbs = resolve10(ctx.configDir ?? ctx.cwd, "schemas", forkName);
9157
+ schemaAbs = resolve12(ctx.configDir ?? ctx.cwd, "schemas", forkName);
8580
9158
  assertSchemaWriteWithin(ctx, schemaAbs);
8581
9159
  if (existsSync4(schemaAbs)) {
8582
9160
  throw new DocmetaError(
@@ -8585,7 +9163,7 @@ async function planSchemaMutation(ops, entries, ctx) {
8585
9163
  }
8586
9164
  await assertNotIgnored(
8587
9165
  schemaAbs,
8588
- dirname5(schemaAbs),
9166
+ dirname6(schemaAbs),
8589
9167
  ctx.cwd,
8590
9168
  ctx.onNotice,
8591
9169
  FORK_IGNORE_TEXT
@@ -8732,18 +9310,18 @@ async function planConfigEdit(ctx, edit, runRefs) {
8732
9310
  if (configPath === void 0 || configDir === void 0) {
8733
9311
  return { changes: [], repointed: false };
8734
9312
  }
8735
- const text = await readFile8(configPath, "utf8");
8736
- const doc = parseDocument4(stripBom(text));
9313
+ const text = await readFile9(configPath, "utf8");
9314
+ const doc = parseDocument5(stripBom(text));
8737
9315
  const display = displayPath(ctx, configPath);
8738
9316
  const rawRefOf = (item) => {
8739
- if (isScalar2(item) && typeof item.value === "string") return item.value;
8740
- if (isMap3(item)) {
9317
+ if (isScalar3(item) && typeof item.value === "string") return item.value;
9318
+ if (isMap4(item)) {
8741
9319
  const ref = item.get("ref");
8742
9320
  if (typeof ref === "string") return ref;
8743
9321
  }
8744
9322
  return void 0;
8745
9323
  };
8746
- const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute3(raw) ? resolve10(configDir, raw) : raw;
9324
+ const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute4(raw) ? resolve12(configDir, raw) : raw;
8747
9325
  const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
8748
9326
  const seqResolvesToRunSet = (node) => {
8749
9327
  if (!isSeq2(node)) return false;
@@ -8759,10 +9337,10 @@ async function planConfigEdit(ctx, edit, runRefs) {
8759
9337
  for (const item of node.items) {
8760
9338
  const raw = rawRefOf(item);
8761
9339
  if (raw === void 0 || !wants(raw)) continue;
8762
- if (isScalar2(item)) {
9340
+ if (isScalar3(item)) {
8763
9341
  item.value = spec.newRef;
8764
9342
  n += 1;
8765
- } else if (isMap3(item)) {
9343
+ } else if (isMap4(item)) {
8766
9344
  item.set("ref", spec.newRef);
8767
9345
  n += 1;
8768
9346
  }
@@ -8774,7 +9352,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
8774
9352
  const overrides = doc.get("overrides", true);
8775
9353
  if (isSeq2(overrides)) {
8776
9354
  for (const entry of overrides.items) {
8777
- if (isMap3(entry)) repointCount += repoint(entry.get("schemas", true));
9355
+ if (isMap4(entry)) repointCount += repoint(entry.get("schemas", true));
8778
9356
  }
8779
9357
  }
8780
9358
  const repointed = repointCount > 0;
@@ -8783,10 +9361,10 @@ async function planConfigEdit(ctx, edit, runRefs) {
8783
9361
  if (edit.integrity && isSeq2(topSeq)) {
8784
9362
  const wantPath = foldPath(edit.integrity.path);
8785
9363
  for (const item of topSeq.items) {
8786
- if (!isMap3(item)) continue;
9364
+ if (!isMap4(item)) continue;
8787
9365
  const raw = rawRefOf(item);
8788
9366
  if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
8789
- const abs = isAbsolute3(raw) ? raw : resolve10(configDir, raw);
9367
+ const abs = isAbsolute4(raw) ? raw : resolve12(configDir, raw);
8790
9368
  if (foldPath(abs) !== wantPath) continue;
8791
9369
  pinFrom = item.get("integrity");
8792
9370
  item.set("integrity", edit.integrity.value);
@@ -9007,7 +9585,7 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9007
9585
  `"${file}": no writable format for that extension.`
9008
9586
  );
9009
9587
  }
9010
- if (existsSync4(resolve10(ctx.base, file))) {
9588
+ if (existsSync4(resolve12(ctx.base, file))) {
9011
9589
  throw new DocmetaError(
9012
9590
  `"${file}" already exists; INSERT creates new files only.`
9013
9591
  );
@@ -9026,26 +9604,81 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9026
9604
  `"${from}" -> "${to}": a rename may not change the extension.`
9027
9605
  );
9028
9606
  }
9029
- if (existsSync4(resolve10(ctx.base, to))) {
9607
+ if (existsSync4(resolve12(ctx.base, to))) {
9030
9608
  throw new DocmetaError(
9031
9609
  `"${to}" already exists; the rename would overwrite it.`
9032
9610
  );
9033
9611
  }
9034
9612
  changes.push({ file: from, renamed: to, written: false });
9035
9613
  }
9614
+ refuseSidecarWrites(changes, entries, ctx);
9036
9615
  return changes;
9037
9616
  }
9038
9617
  function validateNewPath(p, base) {
9039
9618
  const refuse = (why) => {
9040
9619
  throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
9041
9620
  };
9042
- if (isAbsolute3(p)) refuse("it is absolute");
9621
+ if (isAbsolute4(p)) refuse("it is absolute");
9043
9622
  if (p.split(/[\\/]/).some((segment) => segment === "..")) {
9044
9623
  refuse("it traverses upward");
9045
9624
  }
9046
- const abs = resolve10(base, p);
9047
- if (abs === resolve10(base)) refuse("it points at the corpus root, not a file");
9048
- if (!abs.startsWith(resolve10(base) + sep2)) refuse("it escapes the corpus");
9625
+ const abs = resolve12(base, p);
9626
+ if (abs === resolve12(base)) refuse("it points at the corpus root, not a file");
9627
+ if (!abs.startsWith(resolve12(base) + sep4)) refuse("it escapes the corpus");
9628
+ }
9629
+ function refuseSidecarWrites(changes, entries, ctx) {
9630
+ const index = ctx.sidecars;
9631
+ if (!index) return;
9632
+ const owned = (key) => index.owners.get(key);
9633
+ const refuse = (file, key, owner) => {
9634
+ throw new DocmetaError(
9635
+ `"${file}": "${key}" is owned by sidecar ${owner}; edit the sidecar file instead.`
9636
+ );
9637
+ };
9638
+ const data = new Map(entries.map((e) => [e.label, e.extracted.data]));
9639
+ for (const c of changes) {
9640
+ if ("schema" in c || "config" in c) continue;
9641
+ if ("cleared" in c) {
9642
+ for (const key of Object.keys(data.get(c.file) ?? {})) {
9643
+ const owner = owned(key);
9644
+ if (owner !== void 0) refuse(c.file, key, owner);
9645
+ }
9646
+ continue;
9647
+ }
9648
+ if ("created" in c) {
9649
+ for (const key of Object.keys(c.to)) {
9650
+ const owner = owned(key);
9651
+ if (owner !== void 0) refuse(c.file, key, owner);
9652
+ }
9653
+ continue;
9654
+ }
9655
+ if ("renamed" in c) {
9656
+ const entry = index.byPath.get(resolve12(ctx.base, c.file));
9657
+ if (entry !== void 0 && entry.size > 0) {
9658
+ const manifest = [...entry.values()][0]?.file ?? "manifest";
9659
+ throw new DocmetaError(
9660
+ `"${c.file}": sidecar ${manifest} names it; rename the manifest entry first.`
9661
+ );
9662
+ }
9663
+ continue;
9664
+ }
9665
+ const keys = "renamedFrom" in c ? [c.key, c.renamedFrom] : [c.key];
9666
+ for (const key of keys) {
9667
+ const owner = owned(key);
9668
+ if (owner !== void 0) refuse(c.file, key, owner);
9669
+ const byValue = index.byField.get(key);
9670
+ if (byValue === void 0) continue;
9671
+ const raw = data.get(c.file)?.[key];
9672
+ const value = typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : void 0;
9673
+ const entry = value === void 0 ? void 0 : byValue.get(value);
9674
+ if (entry !== void 0) {
9675
+ const manifest = entry.values().next().value?.file ?? "manifest";
9676
+ throw new DocmetaError(
9677
+ `"${c.file}": "${key}" is the field sidecar ${manifest} joins on, and this document has an entry; change the manifest first.`
9678
+ );
9679
+ }
9680
+ }
9681
+ }
9049
9682
  }
9050
9683
  async function applyChanges(changes, entries, ctx, schemaPlan) {
9051
9684
  const schemaWrites = schemaPlan?.writes ?? [];
@@ -9073,14 +9706,14 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9073
9706
  const pendingWrites = [];
9074
9707
  const pendingRenames = [];
9075
9708
  for (const [label, ops] of grouped) {
9076
- const path = resolve10(ctx.base, label);
9709
+ const path = resolve12(ctx.base, label);
9077
9710
  if (ops.renamedTo !== void 0) {
9078
9711
  if (!existsSync4(path)) {
9079
9712
  throw new DocmetaError(
9080
9713
  `"${label}" is gone from disk since it was read; re-run the query.`
9081
9714
  );
9082
9715
  }
9083
- const toDisk = resolve10(ctx.base, ops.renamedTo);
9716
+ const toDisk = resolve12(ctx.base, ops.renamedTo);
9084
9717
  if (existsSync4(toDisk)) {
9085
9718
  throw new DocmetaError(
9086
9719
  `"${ops.renamedTo}" appeared on disk since the plan; re-run the query.`
@@ -9113,10 +9746,15 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9113
9746
  }
9114
9747
  const entry = byLabel.get(label);
9115
9748
  if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
9116
- const content = await readFile8(path, "utf8");
9117
- const current = entry.extractor.extract(content, label, {
9118
- elements: resolveElements(label, ctx.config)
9119
- });
9749
+ const content = await readFile9(path, "utf8");
9750
+ const current = mergeSidecars(
9751
+ label,
9752
+ entry.extractor.extract(content, label, {
9753
+ elements: resolveElements(label, ctx.config)
9754
+ }),
9755
+ ctx.sidecars,
9756
+ ctx.base
9757
+ ).extracted;
9120
9758
  if (ops.cleared) {
9121
9759
  if (!deepEqual(current.data, entry.extracted.data)) {
9122
9760
  throw new DocmetaError(
@@ -9150,9 +9788,14 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9150
9788
  deletions: ops.deletions
9151
9789
  });
9152
9790
  if (ops.deletions.length > 0) {
9153
- const check = entry.extractor.extract(applied, label, {
9154
- elements: resolveElements(label, ctx.config)
9155
- });
9791
+ const check = mergeSidecars(
9792
+ label,
9793
+ entry.extractor.extract(applied, label, {
9794
+ elements: resolveElements(label, ctx.config)
9795
+ }),
9796
+ ctx.sidecars,
9797
+ ctx.base
9798
+ ).extracted;
9156
9799
  for (const key of ops.deletions) {
9157
9800
  if (check.data[key] !== void 0) {
9158
9801
  throw new DocmetaError(
@@ -9175,7 +9818,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9175
9818
  }
9176
9819
  let now;
9177
9820
  try {
9178
- now = await readFile8(w.path, "utf8");
9821
+ now = await readFile9(w.path, "utf8");
9179
9822
  } catch {
9180
9823
  now = void 0;
9181
9824
  }
@@ -9186,10 +9829,10 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9186
9829
  }
9187
9830
  }
9188
9831
  const dirs = /* @__PURE__ */ new Set();
9189
- for (const w of schemaWrites) dirs.add(dirname5(w.path));
9190
- for (const r of pendingRenames) dirs.add(dirname5(r.to));
9832
+ for (const w of schemaWrites) dirs.add(dirname6(w.path));
9833
+ for (const r of pendingRenames) dirs.add(dirname6(r.to));
9191
9834
  for (const p of pendingWrites) {
9192
- if (p.ensureDir) dirs.add(dirname5(p.path));
9835
+ if (p.ensureDir) dirs.add(dirname6(p.path));
9193
9836
  }
9194
9837
  for (const d of dirs) await mkdir3(d, { recursive: true });
9195
9838
  const writeSchema = async () => {
@@ -9304,8 +9947,8 @@ function skipExpression(sql, from) {
9304
9947
  }
9305
9948
 
9306
9949
  // src/meta/commands/fill.ts
9307
- import { readFile as readFile9 } from "node:fs/promises";
9308
- import { resolve as resolve11, extname as extname6, join as join6 } from "node:path";
9950
+ import { readFile as readFile10 } from "node:fs/promises";
9951
+ import { resolve as resolve13, extname as extname6, join as join6 } from "node:path";
9309
9952
  import {
9310
9953
  completeValidatedJSON,
9311
9954
  makeProvider,
@@ -9578,6 +10221,11 @@ async function runFill(opts) {
9578
10221
  let turnsUsed = 0;
9579
10222
  let inFlight = 0;
9580
10223
  const turnsExhausted = () => maxTurns != null && turnsUsed + inFlight >= maxTurns;
10224
+ const sidecars = await loadSidecars(config, {
10225
+ configDir: configDir ?? cwd,
10226
+ base,
10227
+ offline: opts.offline ?? config?.offline ?? false
10228
+ });
9581
10229
  const processOne = async (label, content, extension) => {
9582
10230
  const extractor = forcedExtractor ?? extractorForExtension(extension);
9583
10231
  if (!extractor) {
@@ -9596,8 +10244,15 @@ async function runFill(opts) {
9596
10244
  }
9597
10245
  const elements = resolveElements(label, config);
9598
10246
  let extracted;
10247
+ let merged;
9599
10248
  try {
9600
- extracted = extractor.extract(content, label, { elements });
10249
+ merged = mergeSidecars(
10250
+ label,
10251
+ extractor.extract(content, label, { elements }),
10252
+ sidecars,
10253
+ base
10254
+ );
10255
+ extracted = merged.extracted;
9601
10256
  } catch (err) {
9602
10257
  return errorResult(label, extractor.name, err.message);
9603
10258
  }
@@ -9629,7 +10284,8 @@ async function runFill(opts) {
9629
10284
  extracted.data,
9630
10285
  schemaSet,
9631
10286
  extracted.lineFor,
9632
- extracted.colFor
10287
+ extracted.colFor,
10288
+ merged.locate
9633
10289
  );
9634
10290
  } catch (err) {
9635
10291
  if (!(err instanceof DocmetaError) || resolved.source !== "document") {
@@ -9643,6 +10299,19 @@ async function runFill(opts) {
9643
10299
  existingErrors,
9644
10300
  only
9645
10301
  );
10302
+ const ownedCandidates = sidecars ? candidates.flatMap((c) => {
10303
+ const file = sidecars.owners.get(c.key);
10304
+ return file === void 0 ? [] : [{ key: c.key, file }];
10305
+ }) : [];
10306
+ const firstOwned = ownedCandidates[0];
10307
+ if (firstOwned) {
10308
+ return errorResult(
10309
+ label,
10310
+ extractor.name,
10311
+ `"${firstOwned.key}" is owned by sidecar ${firstOwned.file}; manni meta fill cannot write a sidecar key. Add it to the manifest instead.`,
10312
+ schemaSet
10313
+ );
10314
+ }
9646
10315
  if (candidates.length === 0) {
9647
10316
  return {
9648
10317
  file: label,
@@ -9783,9 +10452,9 @@ async function runFill(opts) {
9783
10452
  const fields = gate(candidates, proposals, threshold);
9784
10453
  const accepted = fields.filter((f) => f.written);
9785
10454
  if (accepted.length > 0) {
9786
- const merged = { ...extracted.data, ...patchOf(accepted) };
10455
+ const merged2 = { ...extracted.data, ...patchOf(accepted) };
9787
10456
  const after = await validator.validate(
9788
- merged,
10457
+ merged2,
9789
10458
  schemaSet,
9790
10459
  extracted.lineFor,
9791
10460
  extracted.colFor
@@ -9829,7 +10498,7 @@ async function runFill(opts) {
9829
10498
  }
9830
10499
  const changed = next !== content;
9831
10500
  if (changed && !dryRun && label !== "<stdin>") {
9832
- await writeFileAtomic(resolve11(base, label), next);
10501
+ await writeFileAtomic(resolve13(base, label), next);
9833
10502
  }
9834
10503
  return {
9835
10504
  file: label,
@@ -9851,7 +10520,7 @@ async function runFill(opts) {
9851
10520
  );
9852
10521
  }
9853
10522
  const fileResults = await mapConcurrent(files, concurrency, async (file) => {
9854
- const content = await readFile9(resolve11(base, file), "utf8");
10523
+ const content = await readFile10(resolve13(base, file), "utf8");
9855
10524
  return processOne(file, content, extname6(file));
9856
10525
  });
9857
10526
  results.push(...fileResults);
@@ -10169,15 +10838,19 @@ async function mapConcurrent(items, limit, fn) {
10169
10838
  }
10170
10839
 
10171
10840
  // src/meta/reporters/sarif.ts
10172
- import { isAbsolute as isAbsolute4 } from "node:path";
10841
+ import { isAbsolute as isAbsolute5 } from "node:path";
10173
10842
 
10174
10843
  // src/meta/reporters/rule-id.ts
10175
10844
  var SYNTHETIC_SCHEMA = "(parse)";
10176
10845
  var PARSE_ERROR_RULE = "manni/parse-error";
10177
10846
  var SCHEMA_ERROR_RULE = "manni/schema-error";
10847
+ var SIDECAR_OWNED_RULE = "sidecar:owned/sidecar";
10848
+ var SIDECAR_DUPLICATE_RULE = "sidecar:duplicate/sidecar";
10178
10849
  var RESERVED_RULES = {
10179
10850
  [PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
10180
- [SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document."
10851
+ [SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document.",
10852
+ [SIDECAR_OWNED_RULE]: "The document carries a key a sidecar manifest owns; the manifest is the only place that key may be set.",
10853
+ [SIDECAR_DUPLICATE_RULE]: "Two documents carry the same value of a sidecar's join field, so one manifest entry matched both."
10181
10854
  };
10182
10855
  function ruleIdFor(e, frame) {
10183
10856
  if (e.schema === SYNTHETIC_SCHEMA) {
@@ -10205,7 +10878,7 @@ function uriFrame(frame) {
10205
10878
  }
10206
10879
  function artifactUri(file, frame) {
10207
10880
  const uri = canonicalFilePath(file, frame);
10208
- if (uri === "" || uri.startsWith("../") || isAbsolute4(uri)) return null;
10881
+ if (uri === "" || uri.startsWith("../") || isAbsolute5(uri)) return null;
10209
10882
  return uri;
10210
10883
  }
10211
10884
  function renderSarif(results, opts = {}) {
@@ -10219,12 +10892,13 @@ function renderSarif(results, opts = {}) {
10219
10892
  for (const r of results) {
10220
10893
  if (r.errors.length === 0) continue;
10221
10894
  if (r.file === STDIN_LABEL) continue;
10222
- const uri = artifactUri(r.file, pathFrame);
10223
- if (uri === null) {
10224
- dropped += r.errors.length;
10225
- continue;
10226
- }
10895
+ const docUri = artifactUri(r.file, pathFrame);
10227
10896
  for (const e of r.errors) {
10897
+ const uri = e.file != null ? artifactUri(e.file, pathFrame) : docUri;
10898
+ if (uri === null) {
10899
+ dropped += 1;
10900
+ continue;
10901
+ }
10228
10902
  const ruleId = ruleIdFor(e, frame);
10229
10903
  if (!rules.has(ruleId)) {
10230
10904
  rules.set(ruleId, {
@@ -10332,7 +11006,7 @@ function renderJunit(results, opts = {}) {
10332
11006
  }
10333
11007
  lines.push(`${open2}>`);
10334
11008
  for (const e of r.errors) {
10335
- const where = e.line != null ? ` (line ${e.line})` : "";
11009
+ const where = e.file != null ? ` (${e.file}${e.line != null ? `:${e.line}` : ""})` : e.line != null ? ` (line ${e.line})` : "";
10336
11010
  lines.push(
10337
11011
  ` <failure${attr("type", ruleIdFor(e, opts.frame))}${attr(
10338
11012
  "message",
@@ -10346,7 +11020,7 @@ function renderJunit(results, opts = {}) {
10346
11020
  return lines.join("\n");
10347
11021
  }
10348
11022
 
10349
- // src/meta/reporters/color.ts
11023
+ // src/shared/color.ts
10350
11024
  import pc from "picocolors";
10351
11025
  function palette(enabled) {
10352
11026
  return pc.createColors(enabled);
@@ -10358,7 +11032,7 @@ function shouldColor(opts) {
10358
11032
  return Boolean(opts.isTTY);
10359
11033
  }
10360
11034
 
10361
- // src/meta/reporters/github.ts
11035
+ // src/shared/github.ts
10362
11036
  function escapeWorkflowCommandMessage(message) {
10363
11037
  return message.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
10364
11038
  }
@@ -10441,7 +11115,7 @@ function renderPretty(results, summary, opts = {}) {
10441
11115
  }
10442
11116
  lines.push(`${c.red("\u2717")} ${r.file}${forgiven}`);
10443
11117
  for (const e of r.errors) {
10444
- const loc = e.line != null ? c.dim(` (line ${e.line})`) : "";
11118
+ const loc = e.file != null ? c.dim(` (${e.file}${e.line != null ? `:${e.line}` : ""})`) : e.line != null ? c.dim(` (line ${e.line})`) : "";
10445
11119
  lines.push(
10446
11120
  ` ${c.cyan(fieldLabel(e.instancePath))} ${e.message}${loc} ${c.dim(
10447
11121
  `[${e.schema}]`
@@ -10465,7 +11139,7 @@ function renderGithub(results) {
10465
11139
  const lines = [];
10466
11140
  for (const r of results) {
10467
11141
  for (const e of r.errors) {
10468
- const params = [`file=${escapeWorkflowCommandProperty(r.file)}`];
11142
+ const params = [`file=${escapeWorkflowCommandProperty(e.file ?? r.file)}`];
10469
11143
  if (e.line != null) params.push(`line=${e.line}`);
10470
11144
  if (e.col != null) params.push(`col=${e.col}`);
10471
11145
  const msg = escapeWorkflowCommandMessage(
@@ -10739,9 +11413,15 @@ function cellFrom(value) {
10739
11413
 
10740
11414
  export {
10741
11415
  package_default,
11416
+ palette,
11417
+ shouldColor,
11418
+ ToolError,
10742
11419
  programName,
10743
11420
  setProgramName,
10744
- ToolError,
11421
+ findConfigFile,
11422
+ readConfigFile,
11423
+ escapeWorkflowCommandMessage,
11424
+ escapeWorkflowCommandProperty,
10745
11425
  DocmetaError,
10746
11426
  toJsonText,
10747
11427
  writeFileAtomic,
@@ -10758,17 +11438,32 @@ export {
10758
11438
  fetchSchemaBytes,
10759
11439
  schemaLoadOptions,
10760
11440
  loadSchema,
11441
+ DEFAULT_SCHEMAS,
11442
+ schemaEntryRef,
11443
+ collectSchemaPins,
11444
+ rebaseConfigSchemaRefs,
11445
+ resolveSchemaSet,
11446
+ SIDECAR_FETCH_TIMEOUT_MS,
11447
+ SIDECAR_FETCH_MAX_BYTES,
11448
+ sidecarUrlProblem,
11449
+ fetchSidecar,
10761
11450
  locateFrontmatter,
10762
11451
  frontmatterInnerText,
10763
11452
  extractFrontmatter,
10764
11453
  applyFrontmatter,
10765
11454
  extractorForExtension,
10766
11455
  supportedExtensions,
10767
- DEFAULT_SCHEMAS,
10768
- schemaEntryRef,
10769
- collectSchemaPins,
10770
- rebaseConfigSchemaRefs,
10771
- resolveSchemaSet,
11456
+ SIDECAR_OWNED_SCHEMA,
11457
+ SIDECAR_DUPLICATE_SCHEMA,
11458
+ SIDECAR_KEYWORD,
11459
+ PATH_JOIN,
11460
+ sidecarJoin,
11461
+ loadSidecars,
11462
+ mergeSidecars,
11463
+ sidecarPointer,
11464
+ orphanEntries,
11465
+ orphanJoins,
11466
+ orphanError,
10772
11467
  parseConfig,
10773
11468
  loadConfig,
10774
11469
  resolveRunConfig,
@@ -10784,8 +11479,6 @@ export {
10784
11479
  runInferSchema,
10785
11480
  runQuery,
10786
11481
  runFill,
10787
- palette,
10788
- shouldColor,
10789
11482
  renderSarif,
10790
11483
  renderJunit,
10791
11484
  REPORT_FORMATS,
@@ -10811,4 +11504,4 @@ export {
10811
11504
  renderQuery,
10812
11505
  renderQueryCsv
10813
11506
  };
10814
- //# sourceMappingURL=chunk-Y6FSY35O.js.map
11507
+ //# sourceMappingURL=chunk-7M2YOXVG.js.map