@hawkeyexl/manni 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/batch-DFRCLY77.js +36 -0
- package/dist/build-Q5C6SX5J.js +31 -0
- package/dist/calibrate-XQILKXS3.js +31 -0
- package/dist/capture-JQMOC3BW.js +32 -0
- package/dist/check-HYNGXYJV.js +18 -0
- package/dist/check-HYNGXYJV.js.map +1 -0
- package/dist/check-NFZQLJKN.js +27 -0
- package/dist/check-NFZQLJKN.js.map +1 -0
- package/dist/check-U54XV6BP.js +40 -0
- package/dist/check-U54XV6BP.js.map +1 -0
- package/dist/chunk-252LNOEG.js +622 -0
- package/dist/chunk-252LNOEG.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-2U3ORT6D.js +230 -0
- package/dist/chunk-2U3ORT6D.js.map +1 -0
- package/dist/chunk-3AFUD75K.js +23 -0
- package/dist/chunk-3AFUD75K.js.map +1 -0
- package/dist/{chunk-3R23C5DT.js → chunk-4TSWU2SU.js} +2 -14
- package/dist/chunk-4TSWU2SU.js.map +1 -0
- package/dist/chunk-5KHIFVD2.js +1192 -0
- package/dist/chunk-5KHIFVD2.js.map +1 -0
- package/dist/chunk-5YXBLLAC.js +2165 -0
- package/dist/chunk-5YXBLLAC.js.map +1 -0
- package/dist/chunk-644JVKJ4.js +3872 -0
- package/dist/chunk-644JVKJ4.js.map +1 -0
- package/dist/chunk-6OYEI2VV.js +88 -0
- package/dist/chunk-6OYEI2VV.js.map +1 -0
- package/dist/chunk-77AINBYM.js +19 -0
- package/dist/chunk-77AINBYM.js.map +1 -0
- package/dist/chunk-77HX34KD.js +1287 -0
- package/dist/chunk-77HX34KD.js.map +1 -0
- package/dist/{chunk-MOQXWXSG.js → chunk-7QMCHA4N.js} +32 -240
- package/dist/chunk-7QMCHA4N.js.map +1 -0
- package/dist/chunk-BUXDLRDT.js +455 -0
- package/dist/chunk-BUXDLRDT.js.map +1 -0
- package/dist/chunk-C6OH6CNF.js +388 -0
- package/dist/chunk-C6OH6CNF.js.map +1 -0
- package/dist/chunk-CK7SKVHR.js +1042 -0
- package/dist/chunk-CK7SKVHR.js.map +1 -0
- package/dist/chunk-CYWHFSRT.js +6737 -0
- package/dist/chunk-CYWHFSRT.js.map +1 -0
- package/dist/chunk-ERMSO34F.js +17 -0
- package/dist/chunk-ERMSO34F.js.map +1 -0
- package/dist/{chunk-IOYXPBCL.js → chunk-G3T47CFV.js} +1 -7
- package/dist/{chunk-IOYXPBCL.js.map → chunk-G3T47CFV.js.map} +1 -1
- package/dist/chunk-G7NJSREW.js +979 -0
- package/dist/chunk-G7NJSREW.js.map +1 -0
- package/dist/chunk-GLP5PSXB.js +72 -0
- package/dist/chunk-GLP5PSXB.js.map +1 -0
- package/dist/chunk-H2DLH2JI.js +198 -0
- package/dist/chunk-H2DLH2JI.js.map +1 -0
- package/dist/chunk-H6FZ66XR.js +634 -0
- package/dist/chunk-H6FZ66XR.js.map +1 -0
- package/dist/{chunk-CI2GOU3A.js → chunk-IEQHSCZG.js} +56 -137
- package/dist/chunk-IEQHSCZG.js.map +1 -0
- package/dist/chunk-IKEWBMCN.js +98 -0
- package/dist/chunk-IKEWBMCN.js.map +1 -0
- package/dist/chunk-JOHKIAVH.js +5440 -0
- package/dist/chunk-JOHKIAVH.js.map +1 -0
- package/dist/chunk-JVTNMVYU.js +10921 -0
- package/dist/chunk-JVTNMVYU.js.map +1 -0
- package/dist/chunk-K3D6FPV6.js +164 -0
- package/dist/chunk-K3D6FPV6.js.map +1 -0
- package/dist/{chunk-MFDD3YGU.js → chunk-KCGV52AM.js} +69 -48
- package/dist/{chunk-MFDD3YGU.js.map → chunk-KCGV52AM.js.map} +1 -1
- package/dist/chunk-LAKAWRV5.js +1651 -0
- package/dist/chunk-LAKAWRV5.js.map +1 -0
- package/dist/chunk-LJ2RDH3F.js +49 -0
- package/dist/chunk-LJ2RDH3F.js.map +1 -0
- package/dist/chunk-M2JCK5F6.js +805 -0
- package/dist/chunk-M2JCK5F6.js.map +1 -0
- package/dist/chunk-MNH7QO4Z.js +15 -0
- package/dist/chunk-MNH7QO4Z.js.map +1 -0
- package/dist/{chunk-RVVCA4WL.js → chunk-MYC3O5EJ.js} +4 -4
- package/dist/chunk-MYMBCTDE.js +387 -0
- package/dist/chunk-MYMBCTDE.js.map +1 -0
- package/dist/chunk-NCPWXHDO.js +683 -0
- package/dist/chunk-NCPWXHDO.js.map +1 -0
- package/dist/chunk-NWZ2SLYL.js +2957 -0
- package/dist/chunk-NWZ2SLYL.js.map +1 -0
- package/dist/chunk-OBFENF3U.js +934 -0
- package/dist/chunk-OBFENF3U.js.map +1 -0
- package/dist/chunk-OWBRDNRZ.js +16 -0
- package/dist/chunk-OWBRDNRZ.js.map +1 -0
- package/dist/{chunk-KTSJ2ZHA.js → chunk-PAU352SV.js} +5 -3
- package/dist/{chunk-KTSJ2ZHA.js.map → chunk-PAU352SV.js.map} +1 -1
- package/dist/chunk-PCE7QHXJ.js +68 -0
- package/dist/chunk-PCE7QHXJ.js.map +1 -0
- package/dist/chunk-PCHZY2I7.js +299 -0
- package/dist/chunk-PCHZY2I7.js.map +1 -0
- package/dist/chunk-PHF3D7ER.js +1042 -0
- package/dist/chunk-PHF3D7ER.js.map +1 -0
- package/dist/chunk-Q35YIDBV.js +1202 -0
- package/dist/chunk-Q35YIDBV.js.map +1 -0
- package/dist/chunk-RLR4TKUZ.js +4398 -0
- package/dist/chunk-RLR4TKUZ.js.map +1 -0
- package/dist/chunk-RNVYGWIC.js +30 -0
- package/dist/chunk-RNVYGWIC.js.map +1 -0
- package/dist/chunk-SE4JN5XN.js +19 -0
- package/dist/chunk-SE4JN5XN.js.map +1 -0
- package/dist/chunk-TIUTIOFB.js +25 -0
- package/dist/chunk-TIUTIOFB.js.map +1 -0
- package/dist/chunk-TU2WUBVZ.js +2 -0
- package/dist/chunk-TU2WUBVZ.js.map +1 -0
- package/dist/chunk-TZOYZK2E.js +98 -0
- package/dist/chunk-TZOYZK2E.js.map +1 -0
- package/dist/chunk-UKZNLVJW.js +17 -0
- package/dist/chunk-UKZNLVJW.js.map +1 -0
- package/dist/chunk-UZC7LDAQ.js +178 -0
- package/dist/chunk-UZC7LDAQ.js.map +1 -0
- package/dist/chunk-V46OILVC.js +3090 -0
- package/dist/chunk-V46OILVC.js.map +1 -0
- package/dist/chunk-WQE54PNH.js +1975 -0
- package/dist/chunk-WQE54PNH.js.map +1 -0
- package/dist/chunk-X3BPBDGQ.js +52 -0
- package/dist/chunk-X3BPBDGQ.js.map +1 -0
- package/dist/chunk-XSLV76YK.js +54 -0
- package/dist/chunk-XSLV76YK.js.map +1 -0
- package/dist/{chunk-QROCFW5S.js → chunk-XYLUCIKQ.js} +3 -3
- package/dist/chunk-YEUYKR62.js +72 -0
- package/dist/chunk-YEUYKR62.js.map +1 -0
- package/dist/{chunk-ESJ5W7EV.js → chunk-YFQBFNQS.js} +8 -94
- package/dist/chunk-YFQBFNQS.js.map +1 -0
- package/dist/chunk-YIL4XLLJ.js +1661 -0
- package/dist/chunk-YIL4XLLJ.js.map +1 -0
- package/dist/chunk-YNHZHSSM.js +41 -0
- package/dist/chunk-YNHZHSSM.js.map +1 -0
- package/dist/chunk-YTCZRLTU.js +96 -0
- package/dist/chunk-YTCZRLTU.js.map +1 -0
- package/dist/chunk-YYBFZLAJ.js +374 -0
- package/dist/chunk-YYBFZLAJ.js.map +1 -0
- package/dist/chunk-ZXRAE55H.js +31 -0
- package/dist/chunk-ZXRAE55H.js.map +1 -0
- package/dist/cli-2QPR2OZG.js +232 -0
- package/dist/cli-2QPR2OZG.js.map +1 -0
- package/dist/cli-4IOPGZHR.js +1203 -0
- package/dist/cli-4IOPGZHR.js.map +1 -0
- package/dist/cli-5FL73WHA.js +346 -0
- package/dist/cli-5FL73WHA.js.map +1 -0
- package/dist/cli-C262QOWM.js +739 -0
- package/dist/cli-C262QOWM.js.map +1 -0
- package/dist/cli-LJLLIQB2.js +423 -0
- package/dist/cli-LJLLIQB2.js.map +1 -0
- package/dist/cli-OFPZ5MAS.js +626 -0
- package/dist/cli-OFPZ5MAS.js.map +1 -0
- package/dist/cli-SHSMSRQY.js +28 -0
- package/dist/cli-SHSMSRQY.js.map +1 -0
- package/dist/cli-SRHFUZHA.js +473 -0
- package/dist/cli-SRHFUZHA.js.map +1 -0
- package/dist/cli-URZQXL4K.js +552 -0
- package/dist/cli-URZQXL4K.js.map +1 -0
- package/dist/cli.d.ts +12 -2
- package/dist/cli.js +109 -7651
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +15 -3
- package/dist/docmeta.js.map +1 -1
- package/dist/github-FE6KCNYJ.js +27 -0
- package/dist/github-FE6KCNYJ.js.map +1 -0
- package/dist/github-QXL3VMK2.js +32 -0
- package/dist/github-QXL3VMK2.js.map +1 -0
- package/dist/index.d.ts +65 -3
- package/dist/index.js +407 -337
- package/dist/index.js.map +1 -1
- package/dist/json-7WV7KL6H.js +17 -0
- package/dist/json-7WV7KL6H.js.map +1 -0
- package/dist/json-SGZ6ZRFD.js +35 -0
- package/dist/json-SGZ6ZRFD.js.map +1 -0
- package/dist/lint-DL4LJRAY.js +28 -0
- package/dist/lint-DL4LJRAY.js.map +1 -0
- package/dist/list-6BAXTZUM.js +29 -0
- package/dist/list-6BAXTZUM.js.map +1 -0
- package/dist/list-7BKNYPSB.js +31 -0
- package/dist/list-7BKNYPSB.js.map +1 -0
- package/dist/page-3U2GCFNE.js +41 -0
- package/dist/page-3U2GCFNE.js.map +1 -0
- package/dist/picker-3XF36MJF.js +14 -0
- package/dist/picker-3XF36MJF.js.map +1 -0
- package/dist/pretty-2H57QVTY.js +43 -0
- package/dist/pretty-2H57QVTY.js.map +1 -0
- package/dist/pretty-N6DO4OOL.js +37 -0
- package/dist/pretty-N6DO4OOL.js.map +1 -0
- package/dist/reporters-M6LJ2IYU.js +21 -0
- package/dist/reporters-M6LJ2IYU.js.map +1 -0
- package/dist/reporters-N6GHXSS7.js +42 -0
- package/dist/reporters-N6GHXSS7.js.map +1 -0
- package/dist/reporters-UCNEV7PH.js +71 -0
- package/dist/reporters-UCNEV7PH.js.map +1 -0
- package/dist/run-XZDAREJE.js +33 -0
- package/dist/run-XZDAREJE.js.map +1 -0
- package/dist/term-5ICGKPC3.js +1592 -0
- package/dist/term-5ICGKPC3.js.map +1 -0
- package/dist/types-Y6TXHM7M.js +10 -0
- package/dist/types-Y6TXHM7M.js.map +1 -0
- package/dist/validate-GQRM77YN.js +16 -0
- package/dist/validate-GQRM77YN.js.map +1 -0
- package/package.json +1 -1
- package/plugin/manni/.claude-plugin/plugin.json +9 -0
- package/plugin/manni/hooks/hooks.json +52 -0
- package/plugin/manni/skills/check/SKILL.md +20 -0
- package/plugin/manni/skills/evals/SKILL.md +27 -0
- package/plugin/manni/skills/fix/SKILL.md +56 -0
- package/plugin/manni/skills/setup/SKILL.md +41 -0
- package/dist/batch-7JJWXWSF.js +0 -18
- package/dist/calibrate-IA2ARFMH.js +0 -13
- package/dist/capture-C32JXIFN.js +0 -15
- package/dist/chunk-3R23C5DT.js.map +0 -1
- package/dist/chunk-CI2GOU3A.js.map +0 -1
- package/dist/chunk-DP2ZA3WH.js +0 -26518
- package/dist/chunk-DP2ZA3WH.js.map +0 -1
- package/dist/chunk-ESJ5W7EV.js.map +0 -1
- package/dist/chunk-MOQXWXSG.js.map +0 -1
- package/dist/chunk-O6RVQKK6.js +0 -27170
- package/dist/chunk-O6RVQKK6.js.map +0 -1
- package/dist/picker-6HRMETE7.js +0 -12
- package/plugin/tracevals/.claude-plugin/plugin.json +0 -9
- package/plugin/tracevals/hooks/hooks.json +0 -17
- /package/dist/{batch-7JJWXWSF.js.map → batch-DFRCLY77.js.map} +0 -0
- /package/dist/{calibrate-IA2ARFMH.js.map → build-Q5C6SX5J.js.map} +0 -0
- /package/dist/{capture-C32JXIFN.js.map → calibrate-XQILKXS3.js.map} +0 -0
- /package/dist/{picker-6HRMETE7.js.map → capture-JQMOC3BW.js.map} +0 -0
- /package/dist/{chunk-RVVCA4WL.js.map → chunk-MYC3O5EJ.js.map} +0 -0
- /package/dist/{chunk-QROCFW5S.js.map → chunk-XYLUCIKQ.js.map} +0 -0
|
@@ -0,0 +1,4398 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DERIVED_KEYWORD,
|
|
4
|
+
DERIVED_STALE_SCHEMA,
|
|
5
|
+
DocmetaError,
|
|
6
|
+
FILE_SCHEMA_KEY,
|
|
7
|
+
ManifestCache,
|
|
8
|
+
STDIN_LABEL,
|
|
9
|
+
STDIN_TOKEN,
|
|
10
|
+
blockLines,
|
|
11
|
+
classifyRef,
|
|
12
|
+
colOf,
|
|
13
|
+
collectSchemaPins,
|
|
14
|
+
colonEnd,
|
|
15
|
+
contentEnd,
|
|
16
|
+
deepEqual,
|
|
17
|
+
detectEol,
|
|
18
|
+
detectStyle,
|
|
19
|
+
emptySource,
|
|
20
|
+
escapePointerSegment,
|
|
21
|
+
extractFrontmatter,
|
|
22
|
+
extractorByName,
|
|
23
|
+
extractorForExtension,
|
|
24
|
+
gitignoreOptions,
|
|
25
|
+
insertKey,
|
|
26
|
+
integrityOf,
|
|
27
|
+
keyLines,
|
|
28
|
+
keyString,
|
|
29
|
+
lineEnd,
|
|
30
|
+
loadConfig,
|
|
31
|
+
loadSchema,
|
|
32
|
+
locateFrontmatter,
|
|
33
|
+
positionForFactory,
|
|
34
|
+
publishedBuiltins,
|
|
35
|
+
rangeOf,
|
|
36
|
+
rebaseConfigSchemaRefs,
|
|
37
|
+
replaceBlockValue,
|
|
38
|
+
replaceFlowValue,
|
|
39
|
+
resolveElements,
|
|
40
|
+
resolveSchemaSetWithSource,
|
|
41
|
+
resolveTargetSet,
|
|
42
|
+
schemaLoadOptions,
|
|
43
|
+
schemaTrustRoot,
|
|
44
|
+
writeFileAtomic
|
|
45
|
+
} from "./chunk-JVTNMVYU.js";
|
|
46
|
+
import {
|
|
47
|
+
STDIN_LINES_MARKER
|
|
48
|
+
} from "./chunk-YEUYKR62.js";
|
|
49
|
+
import {
|
|
50
|
+
ENCRYPTION_KEY_FIELD,
|
|
51
|
+
ENCRYPTION_KEY_PREVIOUS_FIELD,
|
|
52
|
+
FAMILY_CONFIG_NAMES,
|
|
53
|
+
KEYED_PIN_PREFIX,
|
|
54
|
+
PAGE_PLACEHOLDER,
|
|
55
|
+
decryptValue,
|
|
56
|
+
externalMetadataOrigin,
|
|
57
|
+
findGitRoot,
|
|
58
|
+
findPageManifests,
|
|
59
|
+
hasPagePlaceholder,
|
|
60
|
+
isEncryptedValue,
|
|
61
|
+
isMember,
|
|
62
|
+
isValidEncryptionKey,
|
|
63
|
+
keyedPin,
|
|
64
|
+
matchesCollectionPaths,
|
|
65
|
+
neverOwned,
|
|
66
|
+
ownsKey,
|
|
67
|
+
pageManifestPath,
|
|
68
|
+
parseCollections,
|
|
69
|
+
sameFile
|
|
70
|
+
} from "./chunk-Q35YIDBV.js";
|
|
71
|
+
import {
|
|
72
|
+
ToolError,
|
|
73
|
+
errorMessage
|
|
74
|
+
} from "./chunk-G3T47CFV.js";
|
|
75
|
+
|
|
76
|
+
// src/meta/core/external-metadata-fetch.ts
|
|
77
|
+
var EXTERNAL_METADATA_FETCH_TIMEOUT_MS = 1e4;
|
|
78
|
+
var EXTERNAL_METADATA_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
79
|
+
var MAX_REDIRECTS = 5;
|
|
80
|
+
var RETRY_DELAY_MS = 500;
|
|
81
|
+
async function fetchExternalMetadata(url, opts = {}) {
|
|
82
|
+
if (opts.offline) {
|
|
83
|
+
throw new DocmetaError(
|
|
84
|
+
`Manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const timeoutMs = opts.timeoutMs ?? EXTERNAL_METADATA_FETCH_TIMEOUT_MS;
|
|
88
|
+
const maxBytes = opts.maxBytes ?? EXTERNAL_METADATA_FETCH_MAX_BYTES;
|
|
89
|
+
const env = opts.env ?? process.env;
|
|
90
|
+
let token;
|
|
91
|
+
if (opts.tokenEnv !== void 0) {
|
|
92
|
+
token = env[opts.tokenEnv];
|
|
93
|
+
if (token === void 0 || token === "") {
|
|
94
|
+
throw new DocmetaError(
|
|
95
|
+
`Manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let res = await requestWithRedirects(url, token, timeoutMs);
|
|
100
|
+
if (res instanceof Error || res.status >= 500) {
|
|
101
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
102
|
+
res = await requestWithRedirects(url, token, timeoutMs);
|
|
103
|
+
}
|
|
104
|
+
if (res instanceof Error) {
|
|
105
|
+
throw new DocmetaError(
|
|
106
|
+
`Manifest ${url} could not be fetched: ${res.message}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
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)" : "";
|
|
111
|
+
throw new DocmetaError(
|
|
112
|
+
`Manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return readCapped(url, res, maxBytes);
|
|
116
|
+
}
|
|
117
|
+
async function requestWithRedirects(url, token, timeoutMs) {
|
|
118
|
+
const origin = new URL(url).origin;
|
|
119
|
+
let current = url;
|
|
120
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
121
|
+
const sameOrigin = new URL(current).origin === origin;
|
|
122
|
+
const headers = { accept: "text/plain, application/x-yaml, */*" };
|
|
123
|
+
if (token !== void 0 && sameOrigin) headers.authorization = `Bearer ${token}`;
|
|
124
|
+
let res;
|
|
125
|
+
try {
|
|
126
|
+
res = await fetch(current, {
|
|
127
|
+
headers,
|
|
128
|
+
redirect: "manual",
|
|
129
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const isAbort = err instanceof Error && err.name === "TimeoutError";
|
|
133
|
+
return isAbort ? new Error(`timed out after ${timeoutMs}ms`) : new Error(err instanceof Error ? err.message : String(err));
|
|
134
|
+
}
|
|
135
|
+
if (res.status >= 300 && res.status < 400) {
|
|
136
|
+
const location = res.headers.get("location");
|
|
137
|
+
if (location === null) return res;
|
|
138
|
+
await res.body?.cancel();
|
|
139
|
+
current = new URL(location, current).toString();
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
return res;
|
|
143
|
+
}
|
|
144
|
+
return new Error(`more than ${MAX_REDIRECTS} redirects`);
|
|
145
|
+
}
|
|
146
|
+
async function readCapped(url, res, maxBytes) {
|
|
147
|
+
const body = res.body;
|
|
148
|
+
if (!body) return "";
|
|
149
|
+
const reader = body.getReader();
|
|
150
|
+
const chunks = [];
|
|
151
|
+
let total = 0;
|
|
152
|
+
for (; ; ) {
|
|
153
|
+
const { done, value } = await reader.read();
|
|
154
|
+
if (done || value === void 0) break;
|
|
155
|
+
total += value.byteLength;
|
|
156
|
+
if (total > maxBytes) {
|
|
157
|
+
await reader.cancel();
|
|
158
|
+
throw new DocmetaError(
|
|
159
|
+
`Manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
chunks.push(value);
|
|
163
|
+
}
|
|
164
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/shared/encryption-key.ts
|
|
168
|
+
import { spawnSync } from "node:child_process";
|
|
169
|
+
import { readFile } from "node:fs/promises";
|
|
170
|
+
import { basename as basename2, dirname as dirname2, join as join2, relative, resolve } from "node:path";
|
|
171
|
+
import { isDeepStrictEqual } from "node:util";
|
|
172
|
+
import { isMap, parseDocument, parse as parseYaml, stringify } from "yaml";
|
|
173
|
+
|
|
174
|
+
// src/shared/write-file.ts
|
|
175
|
+
import { chmod, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
176
|
+
import { basename, dirname, join } from "node:path";
|
|
177
|
+
var LOCKED = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
178
|
+
var RENAME_ATTEMPTS = 3;
|
|
179
|
+
function errorCode(err) {
|
|
180
|
+
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
181
|
+
}
|
|
182
|
+
var wait = (ms) => new Promise((resolve4) => {
|
|
183
|
+
setTimeout(resolve4, ms);
|
|
184
|
+
});
|
|
185
|
+
async function writeTextAtomic(path, text) {
|
|
186
|
+
const tmp = join(
|
|
187
|
+
dirname(path),
|
|
188
|
+
`.${basename(path)}.manni-${process.pid}-${Math.random().toString(36).slice(2, 8)}.tmp`
|
|
189
|
+
);
|
|
190
|
+
try {
|
|
191
|
+
await writeFile(tmp, text, "utf8");
|
|
192
|
+
try {
|
|
193
|
+
const { mode } = await stat(path);
|
|
194
|
+
await chmod(tmp, mode);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
for (let attempt = 1; ; attempt++) {
|
|
198
|
+
try {
|
|
199
|
+
await rename(tmp, path);
|
|
200
|
+
return;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const code = errorCode(err);
|
|
203
|
+
if (code === void 0 || !LOCKED.has(code) || attempt >= RENAME_ATTEMPTS) {
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
await wait(attempt * 50);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
await rm(tmp, { force: true });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/shared/encryption-key.ts
|
|
215
|
+
var ENCRYPTION_KEY_ENV = "MANNI_ENCRYPTION_KEY";
|
|
216
|
+
function resolveEncryptionKey(opts) {
|
|
217
|
+
const env = opts.env ?? process.env;
|
|
218
|
+
const fromEnv = env[ENCRYPTION_KEY_ENV];
|
|
219
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
220
|
+
if (!isValidEncryptionKey(fromEnv)) {
|
|
221
|
+
throw opts.toError(
|
|
222
|
+
`${ENCRYPTION_KEY_ENV} must be at least 32 hex or base64url characters.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return { key: fromEnv, source: "env" };
|
|
226
|
+
}
|
|
227
|
+
const fromFile = opts.file?.encryptionKey;
|
|
228
|
+
if (fromFile !== void 0) return { key: fromFile, source: "config" };
|
|
229
|
+
return { key: void 0, source: "none" };
|
|
230
|
+
}
|
|
231
|
+
function relativeSource(cwd, path) {
|
|
232
|
+
return relative(resolve(cwd), path).replace(/\\/g, "/");
|
|
233
|
+
}
|
|
234
|
+
function errorCode2(err) {
|
|
235
|
+
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
236
|
+
}
|
|
237
|
+
async function readIfPresent(path) {
|
|
238
|
+
try {
|
|
239
|
+
return await readFile(path, "utf8");
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (errorCode2(err) === "ENOENT") return null;
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function fileTarget(file, toError) {
|
|
246
|
+
if (file.kind === "legacy" || !file.wrapped) {
|
|
247
|
+
throw toError(
|
|
248
|
+
`${file.source} is a single-tool config file, which cannot hold the family ${ENCRYPTION_KEY_FIELD}. Move its keys under the tool's section in ${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}, or set ${ENCRYPTION_KEY_ENV}.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const text = await readIfPresent(file.path);
|
|
252
|
+
return { path: file.path, source: file.source, text: text ?? file.text };
|
|
253
|
+
}
|
|
254
|
+
async function defaultTarget(cwd) {
|
|
255
|
+
const root = findGitRoot(cwd) ?? resolve(cwd);
|
|
256
|
+
for (const name of FAMILY_CONFIG_NAMES) {
|
|
257
|
+
const path2 = join2(root, name);
|
|
258
|
+
const text = await readIfPresent(path2);
|
|
259
|
+
if (text !== null) return { path: path2, source: relativeSource(cwd, path2), text };
|
|
260
|
+
}
|
|
261
|
+
const path = join2(root, FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml");
|
|
262
|
+
return { path, source: relativeSource(cwd, path), text: null };
|
|
263
|
+
}
|
|
264
|
+
function withKey(text, key, previous, source, toError) {
|
|
265
|
+
const doc = parseDocument(text);
|
|
266
|
+
const [first] = doc.errors;
|
|
267
|
+
if (first !== void 0) {
|
|
268
|
+
throw toError(`${source}: invalid YAML: ${first.message}`);
|
|
269
|
+
}
|
|
270
|
+
if (doc.contents !== null && !isMap(doc.contents)) {
|
|
271
|
+
throw toError(`${source}: top level must be a mapping.`);
|
|
272
|
+
}
|
|
273
|
+
doc.set(ENCRYPTION_KEY_FIELD, key);
|
|
274
|
+
if (typeof previous === "string") doc.set(ENCRYPTION_KEY_PREVIOUS_FIELD, previous);
|
|
275
|
+
if (previous === null) doc.delete(ENCRYPTION_KEY_PREVIOUS_FIELD);
|
|
276
|
+
return doc.toString();
|
|
277
|
+
}
|
|
278
|
+
function isMapping(value) {
|
|
279
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
280
|
+
}
|
|
281
|
+
function verify(before, after, key, previous, source, toError) {
|
|
282
|
+
const old = before === null ? null : parseYaml(before);
|
|
283
|
+
const expected = Object.fromEntries(
|
|
284
|
+
Object.entries({
|
|
285
|
+
...isMapping(old) ? old : {},
|
|
286
|
+
[ENCRYPTION_KEY_FIELD]: key,
|
|
287
|
+
...typeof previous === "string" ? { [ENCRYPTION_KEY_PREVIOUS_FIELD]: previous } : {}
|
|
288
|
+
}).filter(([name]) => previous !== null || name !== ENCRYPTION_KEY_PREVIOUS_FIELD)
|
|
289
|
+
);
|
|
290
|
+
const reread = parseYaml(after);
|
|
291
|
+
if (!isMapping(reread) || !isDeepStrictEqual(reread, expected)) {
|
|
292
|
+
throw toError(
|
|
293
|
+
`Could not write ${ENCRYPTION_KEY_FIELD} to ${source}; add the key by hand.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function writeEncryptionKey(opts) {
|
|
298
|
+
const { file, key, toError, previous } = opts;
|
|
299
|
+
if (!isValidEncryptionKey(key)) {
|
|
300
|
+
throw toError(
|
|
301
|
+
`The ${ENCRYPTION_KEY_FIELD} must be at least 32 hex or base64url characters.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (typeof previous === "string" && !isValidEncryptionKey(previous)) {
|
|
305
|
+
throw toError(
|
|
306
|
+
`The ${ENCRYPTION_KEY_PREVIOUS_FIELD} must be at least 32 hex or base64url characters.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const target = file === null ? await defaultTarget(opts.cwd) : await fileTarget(file, toError);
|
|
310
|
+
const text = target.text === null ? stringify({
|
|
311
|
+
[ENCRYPTION_KEY_FIELD]: key,
|
|
312
|
+
...typeof previous === "string" ? { [ENCRYPTION_KEY_PREVIOUS_FIELD]: previous } : {}
|
|
313
|
+
}) : withKey(target.text, key, previous, target.source, toError);
|
|
314
|
+
verify(target.text, text, key, previous, target.source, toError);
|
|
315
|
+
if (opts.dryRun !== true) await writeTextAtomic(target.path, text);
|
|
316
|
+
return { path: target.path, source: target.source, created: target.text === null };
|
|
317
|
+
}
|
|
318
|
+
function isIgnoredByGit(path) {
|
|
319
|
+
const result = spawnSync("git", ["check-ignore", "-q", "--", basename2(path)], {
|
|
320
|
+
cwd: dirname2(path),
|
|
321
|
+
stdio: "ignore",
|
|
322
|
+
windowsHide: true
|
|
323
|
+
});
|
|
324
|
+
if (result.error !== void 0) return void 0;
|
|
325
|
+
if (result.status === 0) return true;
|
|
326
|
+
if (result.status === 1) return false;
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
329
|
+
var committedKeyWarning = (source) => `${source} is not ignored by git: once committed, anyone who can read the repository can decrypt every encrypted value. Prefer MANNI_ENCRYPTION_KEY for a shared repository.`;
|
|
330
|
+
|
|
331
|
+
// src/meta/core/encrypted.ts
|
|
332
|
+
var ENCRYPT_KEYWORD = "x-manni-encrypt";
|
|
333
|
+
var ENCRYPTED_PLAIN_SCHEMA = "encrypted:plain";
|
|
334
|
+
var ENCRYPTED_UNREADABLE_SCHEMA = "encrypted:unreadable";
|
|
335
|
+
var ENCRYPTED_KEYWORD = "encrypted";
|
|
336
|
+
var ENCRYPTED_PLACEHOLDER = "(encrypted)";
|
|
337
|
+
var META_CONTEXT = "meta";
|
|
338
|
+
var EncryptionRefusal = class extends DocmetaError {
|
|
339
|
+
};
|
|
340
|
+
var plainMessage = (pointer) => `${pointer} holds a plain value; its schema marks it ${ENCRYPT_KEYWORD}.`;
|
|
341
|
+
var unreadableMessage = (pointer) => `${pointer} does not decrypt under the current key: encrypted under another key, or edited by hand.`;
|
|
342
|
+
function unverifiedWarning(count2) {
|
|
343
|
+
const what = count2 === 1 ? "1 encrypted value was not verified" : `${count2} encrypted values were not verified`;
|
|
344
|
+
return `${what}: no encryption key is available. Set ${ENCRYPTION_KEY_ENV}, or run \`manni key set\`.`;
|
|
345
|
+
}
|
|
346
|
+
function lazyKey(file, env) {
|
|
347
|
+
let resolved;
|
|
348
|
+
return () => {
|
|
349
|
+
resolved ??= {
|
|
350
|
+
key: resolveEncryptionKey({
|
|
351
|
+
...env === void 0 ? {} : { env },
|
|
352
|
+
file: file ?? null,
|
|
353
|
+
toError: (m) => new EncryptionRefusal(m)
|
|
354
|
+
}).key
|
|
355
|
+
};
|
|
356
|
+
return resolved.key;
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function pointerOf(key) {
|
|
360
|
+
return `/${escapePointerSegment(key)}`;
|
|
361
|
+
}
|
|
362
|
+
function segments(pointer) {
|
|
363
|
+
if (pointer === "") return [];
|
|
364
|
+
return pointer.slice(1).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
365
|
+
}
|
|
366
|
+
function topLevelKeyOf(pointer) {
|
|
367
|
+
const parts = segments(pointer);
|
|
368
|
+
return parts.length === 1 ? parts[0] : void 0;
|
|
369
|
+
}
|
|
370
|
+
function isAtOrUnder(pointer, ancestor) {
|
|
371
|
+
return pointer === ancestor || ancestor === "" || pointer.startsWith(`${ancestor}/`);
|
|
372
|
+
}
|
|
373
|
+
function isRecord(value) {
|
|
374
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
375
|
+
}
|
|
376
|
+
function valueAt(data, pointer) {
|
|
377
|
+
let node = data;
|
|
378
|
+
for (const seg of segments(pointer)) {
|
|
379
|
+
if (Array.isArray(node)) {
|
|
380
|
+
const items = node;
|
|
381
|
+
node = /^\d+$/.test(seg) ? items[Number(seg)] : void 0;
|
|
382
|
+
} else if (isRecord(node)) {
|
|
383
|
+
node = Object.hasOwn(node, seg) ? node[seg] : void 0;
|
|
384
|
+
} else {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
function withValueAt(root, pointer, value) {
|
|
391
|
+
const parts = segments(pointer);
|
|
392
|
+
const set = (node, i) => {
|
|
393
|
+
const seg = parts[i];
|
|
394
|
+
if (seg === void 0) return value;
|
|
395
|
+
if (Array.isArray(node)) {
|
|
396
|
+
const items = [...node];
|
|
397
|
+
if (!/^\d+$/.test(seg) || Number(seg) >= items.length) return node;
|
|
398
|
+
items[Number(seg)] = set(items[Number(seg)], i + 1);
|
|
399
|
+
return items;
|
|
400
|
+
}
|
|
401
|
+
if (isRecord(node) && Object.hasOwn(node, seg)) {
|
|
402
|
+
return { ...node, [seg]: set(node[seg], i + 1) };
|
|
403
|
+
}
|
|
404
|
+
return node;
|
|
405
|
+
};
|
|
406
|
+
return set(root, 0);
|
|
407
|
+
}
|
|
408
|
+
var MAX_ROUNDS = 8;
|
|
409
|
+
async function encryptionView(opts) {
|
|
410
|
+
const { data, refs, validator, key, locate } = opts;
|
|
411
|
+
const view = {
|
|
412
|
+
data,
|
|
413
|
+
marked: [],
|
|
414
|
+
plain: [],
|
|
415
|
+
decrypted: [],
|
|
416
|
+
unreadable: [],
|
|
417
|
+
unverified: []
|
|
418
|
+
};
|
|
419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
420
|
+
const sealed = [];
|
|
421
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
422
|
+
const marks = await validator.markedPointers(view.data, refs);
|
|
423
|
+
const fresh = [...marks].filter((p) => !seen.has(p)).sort((a, b) => segments(a).length - segments(b).length);
|
|
424
|
+
if (fresh.length === 0) break;
|
|
425
|
+
for (const pointer of fresh) {
|
|
426
|
+
seen.add(pointer);
|
|
427
|
+
if (sealed.some((s) => isAtOrUnder(pointer, s))) continue;
|
|
428
|
+
const value = valueAt(data, pointer);
|
|
429
|
+
if (value === void 0) continue;
|
|
430
|
+
view.marked.push(pointer);
|
|
431
|
+
if (!isEncryptedValue(value)) {
|
|
432
|
+
if (locate?.(pointer) === void 0) view.plain.push(pointer);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
sealed.push(pointer);
|
|
436
|
+
const k = key();
|
|
437
|
+
if (k === void 0) {
|
|
438
|
+
view.unverified.push(pointer);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const opened = decryptValue(value, k, META_CONTEXT);
|
|
442
|
+
if (!opened.ok) {
|
|
443
|
+
view.unreadable.push(pointer);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
view.decrypted.push(pointer);
|
|
447
|
+
view.data = withValueAt(view.data, pointer, opened.value);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return view;
|
|
451
|
+
}
|
|
452
|
+
var VALUE_NAMING = /* @__PURE__ */ new Map([
|
|
453
|
+
["additionalProperties", "must NOT have additional properties"],
|
|
454
|
+
["unevaluatedProperties", "must NOT have unevaluated properties"],
|
|
455
|
+
["propertyNames", "property name must be valid"]
|
|
456
|
+
]);
|
|
457
|
+
function at(pointer, pos, locate) {
|
|
458
|
+
const located = locate?.(pointer);
|
|
459
|
+
if (located) {
|
|
460
|
+
return {
|
|
461
|
+
file: located.file,
|
|
462
|
+
...located.line != null ? { line: located.line } : {},
|
|
463
|
+
...located.col != null ? { col: located.col } : {}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const line = pos.lineFor(pointer);
|
|
467
|
+
const col = pos.colFor?.(pointer);
|
|
468
|
+
return { ...line != null ? { line } : {}, ...col != null ? { col } : {} };
|
|
469
|
+
}
|
|
470
|
+
function settleFindings(errors, view, pos, locate) {
|
|
471
|
+
const hidden = [...view.unreadable, ...view.unverified];
|
|
472
|
+
const out = [];
|
|
473
|
+
for (const e of errors) {
|
|
474
|
+
if (hidden.some((p) => isAtOrUnder(e.instancePath, p))) continue;
|
|
475
|
+
const inside = view.decrypted.find((p) => isAtOrUnder(e.instancePath, p));
|
|
476
|
+
const generic = VALUE_NAMING.get(e.keyword);
|
|
477
|
+
if (inside === void 0 || e.instancePath === inside && generic === void 0) {
|
|
478
|
+
out.push(e);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const { subject, file: _file, line: _line, col: _col, ...rest } = e;
|
|
482
|
+
void _file;
|
|
483
|
+
void _line;
|
|
484
|
+
void _col;
|
|
485
|
+
out.push({
|
|
486
|
+
...rest,
|
|
487
|
+
instancePath: inside,
|
|
488
|
+
message: generic ?? e.message,
|
|
489
|
+
...generic === void 0 && subject !== void 0 ? { subject } : {},
|
|
490
|
+
...at(inside, pos, locate)
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
function encryptionFindings(view, pos, locate) {
|
|
496
|
+
return [
|
|
497
|
+
...view.plain.map((p) => ({ p, schema: ENCRYPTED_PLAIN_SCHEMA, message: plainMessage(p) })),
|
|
498
|
+
...view.unreadable.map((p) => ({
|
|
499
|
+
p,
|
|
500
|
+
schema: ENCRYPTED_UNREADABLE_SCHEMA,
|
|
501
|
+
message: unreadableMessage(p)
|
|
502
|
+
}))
|
|
503
|
+
].map(({ p, schema, message }) => ({
|
|
504
|
+
schema,
|
|
505
|
+
keyword: ENCRYPTED_KEYWORD,
|
|
506
|
+
instancePath: p,
|
|
507
|
+
message,
|
|
508
|
+
...at(p, pos, locate)
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
function redactForModel(data, marked) {
|
|
512
|
+
let out = data;
|
|
513
|
+
for (const pointer of marked) {
|
|
514
|
+
if (valueAt(out, pointer) !== void 0) {
|
|
515
|
+
out = withValueAt(out, pointer, ENCRYPTED_PLACEHOLDER);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const scrub = (node) => {
|
|
519
|
+
if (isEncryptedValue(node)) return ENCRYPTED_PLACEHOLDER;
|
|
520
|
+
if (Array.isArray(node)) return node.map(scrub);
|
|
521
|
+
if (isRecord(node)) {
|
|
522
|
+
return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, scrub(v)]));
|
|
523
|
+
}
|
|
524
|
+
return node;
|
|
525
|
+
};
|
|
526
|
+
return scrub(out);
|
|
527
|
+
}
|
|
528
|
+
function redactedDerived(f) {
|
|
529
|
+
return {
|
|
530
|
+
...f,
|
|
531
|
+
...f.asserted !== void 0 ? { asserted: ENCRYPTED_PLACEHOLDER } : {},
|
|
532
|
+
...f.derived !== null ? { derived: ENCRYPTED_PLACEHOLDER } : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/meta/core/external-metadata.ts
|
|
537
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
538
|
+
import { isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
539
|
+
import {
|
|
540
|
+
LineCounter,
|
|
541
|
+
isMap as isMap2,
|
|
542
|
+
isNode,
|
|
543
|
+
isScalar,
|
|
544
|
+
isSeq,
|
|
545
|
+
parseDocument as parseDocument2
|
|
546
|
+
} from "yaml";
|
|
547
|
+
|
|
548
|
+
// src/shared/manifest-cas.ts
|
|
549
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
550
|
+
import { posix } from "node:path";
|
|
551
|
+
var COMMIT_ATTEMPTS = 5;
|
|
552
|
+
function conflictRefusal(file) {
|
|
553
|
+
return `${file} changed under the command while it was being written. Nothing was written to it. Re-run the command.`;
|
|
554
|
+
}
|
|
555
|
+
function heldManifest(path, file, before, absent = false) {
|
|
556
|
+
return { path, file, before, text: before, ops: [], bases: /* @__PURE__ */ new Map(), absent };
|
|
557
|
+
}
|
|
558
|
+
function encodeValue(value) {
|
|
559
|
+
return value === void 0 ? "absent" : JSON.stringify(value);
|
|
560
|
+
}
|
|
561
|
+
function baseKey(at2) {
|
|
562
|
+
const entry = at2.join === "path" ? posix.normalize(at2.entry) : at2.entry;
|
|
563
|
+
return `${entry}\0${at2.key}`;
|
|
564
|
+
}
|
|
565
|
+
function rebaseable(held, op, codec) {
|
|
566
|
+
const at2 = baseKey(op);
|
|
567
|
+
if (held.bases.has(at2)) return;
|
|
568
|
+
held.bases.set(at2, {
|
|
569
|
+
entry: op.entry,
|
|
570
|
+
key: op.key,
|
|
571
|
+
join: op.join,
|
|
572
|
+
value: encodeValue(codec.read(held.before, op))
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function rebase(held, current, codec) {
|
|
576
|
+
for (const base of held.bases.values()) {
|
|
577
|
+
if (encodeValue(codec.read(current, base)) !== base.value) return void 0;
|
|
578
|
+
}
|
|
579
|
+
let text = current;
|
|
580
|
+
for (const op of held.ops) text = codec.apply(text, op);
|
|
581
|
+
return text;
|
|
582
|
+
}
|
|
583
|
+
function rebaseline(held) {
|
|
584
|
+
held.before = held.text;
|
|
585
|
+
held.ops = [];
|
|
586
|
+
held.bases.clear();
|
|
587
|
+
held.absent = false;
|
|
588
|
+
}
|
|
589
|
+
async function settle(held, io) {
|
|
590
|
+
for (let attempt = 1; attempt <= COMMIT_ATTEMPTS; attempt++) {
|
|
591
|
+
let current;
|
|
592
|
+
try {
|
|
593
|
+
current = await io.read(held.path);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (held.absent && isMissing(error)) {
|
|
596
|
+
await io.write(held.path, held.text);
|
|
597
|
+
held.absent = false;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
601
|
+
throw io.toError(`Manifest ${held.file} could not be read: ${reason}`);
|
|
602
|
+
}
|
|
603
|
+
if (!held.absent && current === held.before) {
|
|
604
|
+
await io.write(held.path, held.text);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const replayed = rebase(held, current, io.codec);
|
|
608
|
+
if (replayed === void 0) throw io.toError(conflictRefusal(held.file));
|
|
609
|
+
held.before = current;
|
|
610
|
+
held.text = replayed;
|
|
611
|
+
held.absent = false;
|
|
612
|
+
}
|
|
613
|
+
throw io.toError(conflictRefusal(held.file));
|
|
614
|
+
}
|
|
615
|
+
function isMissing(error) {
|
|
616
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
617
|
+
}
|
|
618
|
+
async function readManifestOrEmpty(absPath, perPage) {
|
|
619
|
+
try {
|
|
620
|
+
return await readFile2(absPath, "utf8");
|
|
621
|
+
} catch (e) {
|
|
622
|
+
if (perPage && isMissing(e)) return "";
|
|
623
|
+
throw e;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/meta/core/external-metadata.ts
|
|
628
|
+
var EXTERNAL_OWNED_SCHEMA = "external:owned";
|
|
629
|
+
var EXTERNAL_DUPLICATE_SCHEMA = "external:duplicate";
|
|
630
|
+
var EXTERNAL_KEYWORD = "external";
|
|
631
|
+
var PATH_JOIN = "path";
|
|
632
|
+
var parsedManifests = new ManifestCache();
|
|
633
|
+
var impliedOwners = /* @__PURE__ */ new WeakMap();
|
|
634
|
+
var perPageRules = /* @__PURE__ */ new WeakMap();
|
|
635
|
+
var strayEntries = /* @__PURE__ */ new WeakSet();
|
|
636
|
+
var posix2 = (p) => p.split(sep).join("/");
|
|
637
|
+
function reportedPath(abs, base) {
|
|
638
|
+
const rel = relative2(base, abs);
|
|
639
|
+
return rel === "" ? "." : posix2(rel);
|
|
640
|
+
}
|
|
641
|
+
function reported(of, file) {
|
|
642
|
+
return of.file === file ? of : { ...of, file };
|
|
643
|
+
}
|
|
644
|
+
function externalMetadataJoin(manifest) {
|
|
645
|
+
return manifest.join ?? PATH_JOIN;
|
|
646
|
+
}
|
|
647
|
+
async function loadExternalMetadata(collections, opts) {
|
|
648
|
+
const configured = collections.flatMap(
|
|
649
|
+
(c) => c.externalMetadata.map((m) => ({ collection: c.name, manifest: m }))
|
|
650
|
+
);
|
|
651
|
+
if (configured.length === 0) return null;
|
|
652
|
+
const owners = /* @__PURE__ */ new Map();
|
|
653
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
654
|
+
const byField = /* @__PURE__ */ new Map();
|
|
655
|
+
const entries = [];
|
|
656
|
+
const resolvedBy = /* @__PURE__ */ new Map();
|
|
657
|
+
const rules = [];
|
|
658
|
+
const implied = [];
|
|
659
|
+
for (const { collection, manifest } of configured) {
|
|
660
|
+
const declared = collections.find(
|
|
661
|
+
(c) => c.name === collection && c.externalMetadata.includes(manifest)
|
|
662
|
+
);
|
|
663
|
+
if (declared !== void 0 && classifyRef(manifest.file).kind !== "url" && hasPagePlaceholder(manifest.file)) {
|
|
664
|
+
const owner = await loadPerPage(declared, manifest, opts, resolvedBy, { owners, byPath, byField, entries });
|
|
665
|
+
if (owner !== void 0) implied.push(owner);
|
|
666
|
+
rules.push({ collection: declared, manifest, configDir: opts.configDir });
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const join4 = externalMetadataJoin(manifest);
|
|
670
|
+
let parsed;
|
|
671
|
+
let file;
|
|
672
|
+
if (classifyRef(manifest.file).kind === "url") {
|
|
673
|
+
file = manifest.file;
|
|
674
|
+
const text = await fetchExternalMetadata(manifest.file, {
|
|
675
|
+
...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
|
|
676
|
+
...opts.offline !== void 0 ? { offline: opts.offline } : {},
|
|
677
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
678
|
+
...opts.env !== void 0 ? { env: opts.env } : {}
|
|
679
|
+
});
|
|
680
|
+
parsed = parseManifest(manifest, collection, text, file, opts.configDir, join4, ownership(declared, manifest));
|
|
681
|
+
} else {
|
|
682
|
+
const abs = isAbsolute(manifest.file) ? manifest.file : resolve2(opts.configDir, manifest.file);
|
|
683
|
+
const label = reportedPath(abs, opts.base);
|
|
684
|
+
file = label;
|
|
685
|
+
parsed = await parsedManifests.parse(
|
|
686
|
+
abs,
|
|
687
|
+
// `label` is deliberately not part of this. It is the manifest as this
|
|
688
|
+
// run reports it, which is a function of the run's base rather than of
|
|
689
|
+
// the bytes, so two runs over one corpus from two directories parse
|
|
690
|
+
// identically and differ only in a string each value carries. Keying
|
|
691
|
+
// on it would miss the cache for exactly the runs that share a
|
|
692
|
+
// process, which is what a test suite is. The copy-in below writes
|
|
693
|
+
// this run's spelling onto every value and entry instead.
|
|
694
|
+
JSON.stringify([
|
|
695
|
+
collection,
|
|
696
|
+
join4,
|
|
697
|
+
opts.configDir,
|
|
698
|
+
variantOf(declared, manifest)
|
|
699
|
+
]),
|
|
700
|
+
async () => parseManifest(
|
|
701
|
+
manifest,
|
|
702
|
+
collection,
|
|
703
|
+
await readManifest(abs, label),
|
|
704
|
+
label,
|
|
705
|
+
opts.configDir,
|
|
706
|
+
join4,
|
|
707
|
+
ownership(declared, manifest)
|
|
708
|
+
)
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
if (manifest.keys === void 0 && declared !== void 0) {
|
|
712
|
+
implied.push({ collection: declared, manifest, file, files: /* @__PURE__ */ new Set([file]) });
|
|
713
|
+
}
|
|
714
|
+
for (const key of manifest.keys ?? []) {
|
|
715
|
+
const list2 = owners.get(key);
|
|
716
|
+
if (list2) list2.push({ collection, file });
|
|
717
|
+
else owners.set(key, [{ collection, file }]);
|
|
718
|
+
}
|
|
719
|
+
let target;
|
|
720
|
+
if (join4 === PATH_JOIN) {
|
|
721
|
+
target = byPath;
|
|
722
|
+
} else {
|
|
723
|
+
const existing = byField.get(join4);
|
|
724
|
+
if (existing) target = existing;
|
|
725
|
+
else {
|
|
726
|
+
target = /* @__PURE__ */ new Map();
|
|
727
|
+
byField.set(join4, target);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const [indexKey, values] of parsed.values) {
|
|
731
|
+
const into = target.get(indexKey) ?? /* @__PURE__ */ new Map();
|
|
732
|
+
for (const [key, value] of values) into.set(key, reported(value, file));
|
|
733
|
+
target.set(indexKey, into);
|
|
734
|
+
}
|
|
735
|
+
for (const entry of parsed.entries) entries.push(reported(entry, file));
|
|
736
|
+
}
|
|
737
|
+
const index = { owners, byPath, byField, entries };
|
|
738
|
+
if (rules.length > 0) perPageRules.set(index, rules);
|
|
739
|
+
if (implied.length > 0) impliedOwners.set(index, implied);
|
|
740
|
+
return index;
|
|
741
|
+
}
|
|
742
|
+
function ownership(declared, manifest) {
|
|
743
|
+
if (manifest.keys !== void 0) {
|
|
744
|
+
const owned = new Set(manifest.keys);
|
|
745
|
+
return {
|
|
746
|
+
may: (key) => owned.has(key),
|
|
747
|
+
why: () => `which this manifest does not own. Add it to the manifest's "keys", or remove it from the entry.`
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const never = neverOwned(declared ?? { externalMetadata: [manifest] }, manifest);
|
|
751
|
+
return {
|
|
752
|
+
may: (key) => !never.has(key),
|
|
753
|
+
why: (key) => key === manifest.join ? "the field this manifest joins on, which the page carries. Remove it from the entry." : `which another manifest of this collection names in its "keys". Move it to that manifest, or remove it from the entry.`
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function variantOf(declared, manifest) {
|
|
757
|
+
if (manifest.keys !== void 0) return [...manifest.keys].sort();
|
|
758
|
+
return { never: [...neverOwned(declared ?? { externalMetadata: [manifest] }, manifest)].sort() };
|
|
759
|
+
}
|
|
760
|
+
async function loadPerPage(declared, manifest, opts, resolvedBy, into) {
|
|
761
|
+
const collection = declared.name;
|
|
762
|
+
const join4 = externalMetadataJoin(manifest);
|
|
763
|
+
const implied = manifest.keys === void 0 ? { collection: declared, manifest, file: manifest.file, files: /* @__PURE__ */ new Set() } : void 0;
|
|
764
|
+
for (const key of manifest.keys ?? []) {
|
|
765
|
+
const list2 = into.owners.get(key);
|
|
766
|
+
if (list2) list2.push({ collection, file: manifest.file });
|
|
767
|
+
else into.owners.set(key, [{ collection, file: manifest.file }]);
|
|
768
|
+
}
|
|
769
|
+
const target = indexFor(join4, into);
|
|
770
|
+
for (const pageAbs of opts.pages ?? []) {
|
|
771
|
+
const resolved = pageManifestPath(manifest.file, opts.configDir, pageAbs);
|
|
772
|
+
if ("outside" in resolved) {
|
|
773
|
+
if (resolved.pageRel.startsWith("../") && matchesCollectionPaths(declared, resolved.pageRel)) {
|
|
774
|
+
throw new DocmetaError(outsideRefusal(manifest, collection, resolved.pageRel));
|
|
775
|
+
}
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const rel = posix2(relative2(opts.configDir, pageAbs));
|
|
779
|
+
if (!isMember(declared, rel)) continue;
|
|
780
|
+
const earlier = resolvedBy.get(resolved.abs);
|
|
781
|
+
if (earlier !== void 0 && earlier !== pageAbs) {
|
|
782
|
+
const [a, b] = [reportedPath(earlier, opts.base), reportedPath(pageAbs, opts.base)].sort();
|
|
783
|
+
throw new DocmetaError(
|
|
784
|
+
`${a ?? ""} and ${b ?? ""} both resolve ${PAGE_PLACEHOLDER} to ${reportedPath(resolved.abs, opts.base)}. A ${PAGE_PLACEHOLDER} manifest names one page, so rename one of them.`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
resolvedBy.set(resolved.abs, pageAbs);
|
|
788
|
+
const label = reportedPath(resolved.abs, opts.base);
|
|
789
|
+
const parsed = await parsedManifests.parse(
|
|
790
|
+
resolved.abs,
|
|
791
|
+
JSON.stringify([collection, join4, opts.configDir, variantOf(declared, manifest)]),
|
|
792
|
+
async () => parseManifest(
|
|
793
|
+
manifest,
|
|
794
|
+
collection,
|
|
795
|
+
await readPageManifest(resolved.abs, label),
|
|
796
|
+
label,
|
|
797
|
+
opts.configDir,
|
|
798
|
+
join4,
|
|
799
|
+
ownership(declared, manifest)
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
implied?.files.add(label);
|
|
803
|
+
const pageLabel = reportedPath(pageAbs, opts.base);
|
|
804
|
+
for (const entry of parsed.entries) {
|
|
805
|
+
if (entry.abs === void 0 || sameFile(entry.abs, pageAbs)) continue;
|
|
806
|
+
const at2 = entry.line === void 0 ? label : `${label}:${String(entry.line)}`;
|
|
807
|
+
throw new DocmetaError(
|
|
808
|
+
`Manifest ${at2} names "${entry.spelled}", but ${PAGE_PLACEHOLDER} resolved this file for "${pageLabel}". A per-page manifest holds one entry, for its own page.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
for (const [indexKey, values] of parsed.values) {
|
|
812
|
+
const key = join4 === PATH_JOIN ? pageAbs : indexKey;
|
|
813
|
+
const merged = target.get(key) ?? /* @__PURE__ */ new Map();
|
|
814
|
+
for (const [owned, value] of values) merged.set(owned, reported(value, label));
|
|
815
|
+
target.set(key, merged);
|
|
816
|
+
}
|
|
817
|
+
for (const entry of parsed.entries) {
|
|
818
|
+
into.entries.push({
|
|
819
|
+
...reported(entry, label),
|
|
820
|
+
...entry.abs === void 0 ? {} : { abs: pageAbs }
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return implied;
|
|
825
|
+
}
|
|
826
|
+
function indexFor(join4, into) {
|
|
827
|
+
if (join4 === PATH_JOIN) return into.byPath;
|
|
828
|
+
const existing = into.byField.get(join4);
|
|
829
|
+
if (existing) return existing;
|
|
830
|
+
const created = /* @__PURE__ */ new Map();
|
|
831
|
+
into.byField.set(join4, created);
|
|
832
|
+
return created;
|
|
833
|
+
}
|
|
834
|
+
function outsideRefusal(manifest, collection, pageRel) {
|
|
835
|
+
const tail = `for "${pageRel}". A ${PAGE_PLACEHOLDER} manifest stays under the config file.`;
|
|
836
|
+
const origin = externalMetadataOrigin(manifest);
|
|
837
|
+
if (origin === void 0) {
|
|
838
|
+
return `externalMetadata file "${manifest.file}" of collection ${collection} resolves outside the config file's directory ${tail}`;
|
|
839
|
+
}
|
|
840
|
+
return `${origin.source}: ${origin.where}.file resolves outside ${origin.source}'s directory ${tail}`;
|
|
841
|
+
}
|
|
842
|
+
async function readPageManifest(abs, file) {
|
|
843
|
+
try {
|
|
844
|
+
return await readFile3(abs, "utf8");
|
|
845
|
+
} catch (err) {
|
|
846
|
+
if (isMissing(err)) return "";
|
|
847
|
+
throw unreadable(file, err);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function readManifest(abs, file) {
|
|
851
|
+
try {
|
|
852
|
+
return await readFile3(abs, "utf8");
|
|
853
|
+
} catch (err) {
|
|
854
|
+
throw unreadable(file, err);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function unreadable(file, err) {
|
|
858
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
859
|
+
return new DocmetaError(`Manifest ${file} could not be read: ${reason}`);
|
|
860
|
+
}
|
|
861
|
+
function parseManifest(manifest, collection, text, file, configDir, join4, owns) {
|
|
862
|
+
const target = /* @__PURE__ */ new Map();
|
|
863
|
+
const entries = [];
|
|
864
|
+
const lc = new LineCounter();
|
|
865
|
+
const doc = parseDocument2(text, { lineCounter: lc, uniqueKeys: false });
|
|
866
|
+
const problem = doc.errors[0];
|
|
867
|
+
if (problem) {
|
|
868
|
+
throw new DocmetaError(
|
|
869
|
+
`Manifest ${file} is not valid YAML: ${problem.message}`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const root = doc.contents;
|
|
873
|
+
if (root === null || isScalar(root) && root.value === null)
|
|
874
|
+
return { values: target, entries };
|
|
875
|
+
if (!isMap2(root)) {
|
|
876
|
+
throw new DocmetaError(
|
|
877
|
+
`Manifest ${file}: the manifest must be a mapping from document ${join4 === PATH_JOIN ? "path" : `"${join4}"`} to owned keys.`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const lineAt = (node) => {
|
|
881
|
+
const range = isNode(node) ? node.range : void 0;
|
|
882
|
+
return range ? lc.linePos(range[0]).line : void 0;
|
|
883
|
+
};
|
|
884
|
+
const seen = /* @__PURE__ */ new Map();
|
|
885
|
+
for (const pair of root.items) {
|
|
886
|
+
const spelled = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
|
|
887
|
+
const entryLine = lineAt(pair.key);
|
|
888
|
+
const where = entryLine === void 0 ? file : `${file}:${entryLine}`;
|
|
889
|
+
if (seen.has(spelled)) {
|
|
890
|
+
const first = seen.get(spelled);
|
|
891
|
+
throw new DocmetaError(
|
|
892
|
+
`Manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
seen.set(spelled, entryLine);
|
|
896
|
+
if (!isMap2(pair.value)) {
|
|
897
|
+
throw new DocmetaError(
|
|
898
|
+
`Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const indexKey = join4 === PATH_JOIN ? resolve2(configDir, spelled) : spelled;
|
|
902
|
+
const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
|
|
903
|
+
for (const kv of pair.value.items) {
|
|
904
|
+
const key = isScalar(kv.key) ? String(kv.key.value) : String(kv.key);
|
|
905
|
+
if (key === FILE_SCHEMA_KEY) {
|
|
906
|
+
throw new DocmetaError(
|
|
907
|
+
`Manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides" in the config.`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
if (!owns.may(key)) {
|
|
911
|
+
throw new DocmetaError(`Manifest ${where}: "${spelled}" sets "${key}", ${owns.why(key)}`);
|
|
912
|
+
}
|
|
913
|
+
const value = kv.value === null ? null : kv.value.toJS(doc, { maxAliasCount: 100 });
|
|
914
|
+
const line = lineAt(kv.key);
|
|
915
|
+
const lines = nodeLines(kv.value, lineAt);
|
|
916
|
+
values.set(key, {
|
|
917
|
+
value,
|
|
918
|
+
collection,
|
|
919
|
+
file,
|
|
920
|
+
...line === void 0 ? {} : { line },
|
|
921
|
+
...lines.size === 0 ? {} : { lines }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
target.set(indexKey, values);
|
|
925
|
+
entries.push({
|
|
926
|
+
collection,
|
|
927
|
+
join: join4,
|
|
928
|
+
...join4 === PATH_JOIN ? { abs: indexKey } : {},
|
|
929
|
+
spelled,
|
|
930
|
+
file,
|
|
931
|
+
...entryLine === void 0 ? {} : { line: entryLine }
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return { values: target, entries };
|
|
935
|
+
}
|
|
936
|
+
function nodeLines(node, lineAt, prefix = "", out = /* @__PURE__ */ new Map()) {
|
|
937
|
+
if (isMap2(node)) {
|
|
938
|
+
for (const kv of node.items) {
|
|
939
|
+
const seg = isScalar(kv.key) ? String(kv.key.value) : String(kv.key);
|
|
940
|
+
const pointer = `${prefix}/${escapePointerSegment(seg)}`;
|
|
941
|
+
const line = lineAt(kv.key) ?? lineAt(kv.value);
|
|
942
|
+
if (line !== void 0) out.set(pointer, line);
|
|
943
|
+
nodeLines(kv.value, lineAt, pointer, out);
|
|
944
|
+
}
|
|
945
|
+
} else if (isSeq(node)) {
|
|
946
|
+
node.items.forEach((item, i) => {
|
|
947
|
+
const pointer = `${prefix}/${String(i)}`;
|
|
948
|
+
const line = lineAt(item);
|
|
949
|
+
if (line !== void 0) out.set(pointer, line);
|
|
950
|
+
nodeLines(item, lineAt, pointer, out);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return out;
|
|
954
|
+
}
|
|
955
|
+
function joinValue(raw) {
|
|
956
|
+
if (typeof raw === "string") return raw;
|
|
957
|
+
if (typeof raw === "number" || typeof raw === "bigint") return String(raw);
|
|
958
|
+
if (typeof raw === "boolean") return String(raw);
|
|
959
|
+
return void 0;
|
|
960
|
+
}
|
|
961
|
+
function mergeExternalMetadata(label, extracted, index, memberOf2, base, options = {}) {
|
|
962
|
+
return mergeWith(label, extracted, index, memberOf2, base, options.encryptionKey, options.marked ?? UNKNOWN);
|
|
963
|
+
}
|
|
964
|
+
var UNKNOWN = /* @__PURE__ */ new Set();
|
|
965
|
+
var PROBE = /* @__PURE__ */ Symbol("probe");
|
|
966
|
+
function mergeWith(label, extracted, index, memberOf2, base, encryptionKey, marked) {
|
|
967
|
+
const none = () => void 0;
|
|
968
|
+
if (!index || memberOf2.length === 0) {
|
|
969
|
+
return { extracted, collisions: [], joins: [], locate: none };
|
|
970
|
+
}
|
|
971
|
+
const mine = (collection) => memberOf2.includes(collection);
|
|
972
|
+
const probing = marked === PROBE;
|
|
973
|
+
const implied = (impliedOwners.get(index) ?? []).filter((o) => mine(o.collection.name));
|
|
974
|
+
const impliedOwns = (o, key) => probing ? !neverOwned(o.collection, o.manifest).has(key) : ownsKey(o.collection, o.manifest, key, marked);
|
|
975
|
+
const ownersOf = (key) => pageOwners(index, key, memberOf2, probing ? void 0 : marked);
|
|
976
|
+
const impliedFrom = (sv) => implied.find((o) => o.collection.name === sv.collection && o.files.has(sv.file));
|
|
977
|
+
const collisions = [];
|
|
978
|
+
if (!probing) {
|
|
979
|
+
for (const key of Object.keys(extracted.data)) {
|
|
980
|
+
for (const owner of ownersOf(key)) {
|
|
981
|
+
collisions.push({ key, file: owner.file, collection: owner.collection });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const sources = [];
|
|
986
|
+
const entryOf = /* @__PURE__ */ new Map();
|
|
987
|
+
const joins = [];
|
|
988
|
+
if (label !== STDIN_LABEL) {
|
|
989
|
+
const byPath = index.byPath.get(resolve2(base, label));
|
|
990
|
+
if (byPath) {
|
|
991
|
+
sources.push(byPath);
|
|
992
|
+
const abs = resolve2(base, label);
|
|
993
|
+
entryOf.set(byPath, (sv) => index.entries.find((e) => e.file === sv.file && e.abs === abs));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
for (const [field, byValue] of index.byField) {
|
|
997
|
+
const raw = extracted.data[field];
|
|
998
|
+
let value;
|
|
999
|
+
let pageValue;
|
|
1000
|
+
if (isEncryptedValue(raw)) {
|
|
1001
|
+
const joinsHere = [...byValue.values()].some(
|
|
1002
|
+
(supplied) => [...supplied.values()].some((sv) => mine(sv.collection))
|
|
1003
|
+
);
|
|
1004
|
+
if (!joinsHere) continue;
|
|
1005
|
+
const key = encryptionKey?.();
|
|
1006
|
+
if (key === void 0) {
|
|
1007
|
+
throw new EncryptionRefusal(
|
|
1008
|
+
`externalMetadata join field "${field}" is encrypted on these pages, and no encryption key is available to match them.`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const opened = decryptValue(raw, key, META_CONTEXT);
|
|
1012
|
+
if (!opened.ok) continue;
|
|
1013
|
+
value = joinValue(opened.value);
|
|
1014
|
+
pageValue = raw;
|
|
1015
|
+
} else {
|
|
1016
|
+
value = joinValue(raw);
|
|
1017
|
+
}
|
|
1018
|
+
if (value === void 0) continue;
|
|
1019
|
+
const hit = byValue.get(value);
|
|
1020
|
+
if (!hit) continue;
|
|
1021
|
+
sources.push(hit);
|
|
1022
|
+
const spelled = value;
|
|
1023
|
+
entryOf.set(
|
|
1024
|
+
hit,
|
|
1025
|
+
(sv) => index.entries.find((e) => e.file === sv.file && e.join === field && e.spelled === spelled)
|
|
1026
|
+
);
|
|
1027
|
+
const first = [...hit.values()].find((sv) => mine(sv.collection));
|
|
1028
|
+
if (first) {
|
|
1029
|
+
joins.push({
|
|
1030
|
+
field,
|
|
1031
|
+
value,
|
|
1032
|
+
...pageValue === void 0 ? {} : { pageValue },
|
|
1033
|
+
file: first.file,
|
|
1034
|
+
collection: first.collection
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
for (const supplied of probing ? [] : sources) {
|
|
1039
|
+
for (const key of supplied.keys()) {
|
|
1040
|
+
const [a, b] = ownersOf(key);
|
|
1041
|
+
if (a && b) {
|
|
1042
|
+
throw new DocmetaError(
|
|
1043
|
+
`${label}: "${key}" is owned by manifests in two of its collections, ${a.collection} (${a.file}) and ${b.collection} (${b.file}); a key has one manifest per file. Narrow one collection's paths or exclude.`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (sources.length === 0) {
|
|
1049
|
+
return { extracted, collisions, joins, locate: none };
|
|
1050
|
+
}
|
|
1051
|
+
const data = { ...extracted.data };
|
|
1052
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const supplied of sources) {
|
|
1054
|
+
for (const [key, sv] of supplied) {
|
|
1055
|
+
if (!mine(sv.collection)) continue;
|
|
1056
|
+
const from = impliedFrom(sv);
|
|
1057
|
+
if (from !== void 0 && !impliedOwns(from, key)) {
|
|
1058
|
+
if (probing || marked === UNKNOWN) continue;
|
|
1059
|
+
throw unmarkedRefusal(label, key, sv, entryOf.get(supplied)?.(sv));
|
|
1060
|
+
}
|
|
1061
|
+
if (key in extracted.data) continue;
|
|
1062
|
+
data[key] = sv.value;
|
|
1063
|
+
merged.set(key, sv);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const locate = (pointer) => {
|
|
1067
|
+
const split = splitTopLevel(pointer);
|
|
1068
|
+
if (split === void 0) return void 0;
|
|
1069
|
+
const sv = merged.get(split.key);
|
|
1070
|
+
if (!sv) return void 0;
|
|
1071
|
+
const line = manifestLine(sv, split.rest);
|
|
1072
|
+
return { file: sv.file, ...line === void 0 ? {} : { line } };
|
|
1073
|
+
};
|
|
1074
|
+
return { extracted: { ...extracted, data }, collisions, joins, locate };
|
|
1075
|
+
}
|
|
1076
|
+
function unmarkedRefusal(label, key, sv, entry) {
|
|
1077
|
+
const line = entry?.line ?? sv.line;
|
|
1078
|
+
const where = line === void 0 ? sv.file : `${sv.file}:${String(line)}`;
|
|
1079
|
+
const spelled = entry?.spelled ?? label;
|
|
1080
|
+
return new DocmetaError(
|
|
1081
|
+
`Manifest ${where}: "${spelled}" sets "${key}", which this manifest does not own, because the page's schemas do not mark it x-manni-location: external. Mark it external in the page's schemas, or remove it from the entry.`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
async function mergeWithMarks(label, extracted, index, memberOf2, base, options) {
|
|
1085
|
+
const { encryptionKey, marks } = options;
|
|
1086
|
+
if (marks === void 0 || !hasImpliedOwner(index, memberOf2)) {
|
|
1087
|
+
return mergeWith(label, extracted, index, memberOf2, base, encryptionKey, UNKNOWN);
|
|
1088
|
+
}
|
|
1089
|
+
const probe = mergeWith(label, extracted, index, memberOf2, base, encryptionKey, PROBE).extracted.data;
|
|
1090
|
+
const marked = await marks(label, probe, memberOf2);
|
|
1091
|
+
const merged = mergeWith(label, extracted, index, memberOf2, base, encryptionKey, marked ?? UNKNOWN);
|
|
1092
|
+
return marked === void 0 ? merged : { ...merged, marked };
|
|
1093
|
+
}
|
|
1094
|
+
function hasImpliedOwner(index, memberOf2) {
|
|
1095
|
+
if (index === null) return false;
|
|
1096
|
+
return (impliedOwners.get(index) ?? []).some((o) => memberOf2.includes(o.collection.name));
|
|
1097
|
+
}
|
|
1098
|
+
function manifestLine(sv, rest) {
|
|
1099
|
+
if (rest === "" || sv.lines === void 0) return sv.line;
|
|
1100
|
+
return positionForFactory(sv.lines)(rest) ?? sv.line;
|
|
1101
|
+
}
|
|
1102
|
+
function splitTopLevel(pointer) {
|
|
1103
|
+
if (pointer === "") return void 0;
|
|
1104
|
+
if (!pointer.startsWith("/")) return { key: pointer, rest: "" };
|
|
1105
|
+
const slash = pointer.indexOf("/", 1);
|
|
1106
|
+
const seg = slash < 0 ? pointer.slice(1) : pointer.slice(1, slash);
|
|
1107
|
+
return {
|
|
1108
|
+
key: seg.replace(/~1/g, "/").replace(/~0/g, "~"),
|
|
1109
|
+
rest: slash < 0 ? "" : pointer.slice(slash)
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function externalMetadataPointer(key) {
|
|
1113
|
+
return `/${escapePointerSegment(key)}`;
|
|
1114
|
+
}
|
|
1115
|
+
function orphanEntries(index, loaded, base, collections) {
|
|
1116
|
+
if (!index) return [];
|
|
1117
|
+
const have = new Set(loaded.map((l) => resolve2(base, l)));
|
|
1118
|
+
const orphans = index.entries.filter(
|
|
1119
|
+
(e) => inCollections(e, collections) && e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
|
|
1120
|
+
);
|
|
1121
|
+
if (collections !== void 0) return orphans;
|
|
1122
|
+
return [...orphans, ...strayManifests(perPageRules.get(index) ?? [], have, base)];
|
|
1123
|
+
}
|
|
1124
|
+
function strayManifests(rules, loaded, base) {
|
|
1125
|
+
const strays = [];
|
|
1126
|
+
for (const { collection, manifest, configDir } of rules) {
|
|
1127
|
+
const read = /* @__PURE__ */ new Set();
|
|
1128
|
+
for (const pageAbs of loaded) {
|
|
1129
|
+
const resolved = pageManifestPath(manifest.file, configDir, pageAbs);
|
|
1130
|
+
if (!("outside" in resolved)) read.add(resolved.abs);
|
|
1131
|
+
}
|
|
1132
|
+
const found = findPageManifests(manifest.file, configDir, (rel) => isMember(collection, rel), read);
|
|
1133
|
+
for (const { abs, entries } of found) {
|
|
1134
|
+
const file = reportedPath(abs, base);
|
|
1135
|
+
for (const entry of entries) {
|
|
1136
|
+
if (loaded.has(entry.pageAbs)) continue;
|
|
1137
|
+
const stray = {
|
|
1138
|
+
collection: collection.name,
|
|
1139
|
+
join: PATH_JOIN,
|
|
1140
|
+
abs: entry.pageAbs,
|
|
1141
|
+
spelled: entry.spelled,
|
|
1142
|
+
file,
|
|
1143
|
+
...entry.line === void 0 ? {} : { line: entry.line }
|
|
1144
|
+
};
|
|
1145
|
+
strayEntries.add(stray);
|
|
1146
|
+
strays.push(stray);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return strays;
|
|
1151
|
+
}
|
|
1152
|
+
function inCollections(entry, collections) {
|
|
1153
|
+
return collections === void 0 || collections.includes(entry.collection);
|
|
1154
|
+
}
|
|
1155
|
+
function orphanJoins(index, matched, collections) {
|
|
1156
|
+
if (!index) return [];
|
|
1157
|
+
return index.entries.filter(
|
|
1158
|
+
(e) => inCollections(e, collections) && e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
function orphanError(orphans) {
|
|
1162
|
+
const first = orphans[0];
|
|
1163
|
+
if (!first) throw new Error("orphanError called with no orphans");
|
|
1164
|
+
const where = first.line === void 0 ? first.file : `${first.file}:${first.line}`;
|
|
1165
|
+
const more = orphans.length > 1 ? ` (${orphans.length - 1} more)` : "";
|
|
1166
|
+
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`;
|
|
1167
|
+
const remove = strayEntries.has(first) ? "the file" : "it";
|
|
1168
|
+
return new DocmetaError(
|
|
1169
|
+
`Manifest ${where} ${what}${more}. Fix the entry, or remove ${remove}.`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
function pageOwners(index, key, memberOf2, marked) {
|
|
1173
|
+
if (index === null) return [];
|
|
1174
|
+
return [
|
|
1175
|
+
...(index.owners.get(key) ?? []).filter((o) => memberOf2.includes(o.collection)),
|
|
1176
|
+
...(impliedOwners.get(index) ?? []).filter((o) => memberOf2.includes(o.collection.name) && ownsKey(o.collection, o.manifest, key, marked)).map((o) => ({ collection: o.collection.name, file: o.file }))
|
|
1177
|
+
];
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/meta/core/location.ts
|
|
1181
|
+
var LOCATION_KEYWORD = "x-manni-location";
|
|
1182
|
+
var LOCATION_EXTERNAL_SCHEMA = "location:external";
|
|
1183
|
+
var LOCATION_PAGE_SCHEMA = "location:page";
|
|
1184
|
+
var LOCATION_FINDING_KEYWORD = "location";
|
|
1185
|
+
function isFieldLocation(value) {
|
|
1186
|
+
return value === "page" || value === "external";
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/meta/core/graph-output.ts
|
|
1190
|
+
var GRAPH_OUTPUT_KEYWORD = "x-manni-graph-output";
|
|
1191
|
+
|
|
1192
|
+
// src/meta/core/validator.ts
|
|
1193
|
+
import * as AjvDraft07Ns from "ajv";
|
|
1194
|
+
import * as Ajv2019Ns from "ajv/dist/2019.js";
|
|
1195
|
+
import * as Ajv2020Ns from "ajv/dist/2020.js";
|
|
1196
|
+
import * as AjvDraft04Ns from "ajv-draft-04";
|
|
1197
|
+
import * as addFormatsNs from "ajv-formats";
|
|
1198
|
+
import { createRequire } from "node:module";
|
|
1199
|
+
var draft06MetaSchema = createRequire(import.meta.url)(
|
|
1200
|
+
"ajv/dist/refs/json-schema-draft-06.json"
|
|
1201
|
+
);
|
|
1202
|
+
var AjvDraft07 = AjvDraft07Ns.default;
|
|
1203
|
+
var Ajv2019 = Ajv2019Ns.default;
|
|
1204
|
+
var Ajv2020 = Ajv2020Ns.default;
|
|
1205
|
+
var AjvDraft04 = AjvDraft04Ns.default;
|
|
1206
|
+
var addFormats = addFormatsNs.default;
|
|
1207
|
+
function dialectOf(schema) {
|
|
1208
|
+
const meta = typeof schema.$schema === "string" ? schema.$schema : "";
|
|
1209
|
+
if (meta.includes("2019-09")) return "2019";
|
|
1210
|
+
if (meta.includes("draft-07") || meta.includes("draft/7") || meta.includes("draft-06") || meta.includes("draft/6")) {
|
|
1211
|
+
return "draft7";
|
|
1212
|
+
}
|
|
1213
|
+
if (meta.includes("draft-04") || meta.includes("draft/4")) return "draft4";
|
|
1214
|
+
return "2020";
|
|
1215
|
+
}
|
|
1216
|
+
function buildAjv(dialect, coerceTypes = false) {
|
|
1217
|
+
const opts = { allErrors: true, strict: false, coerceTypes };
|
|
1218
|
+
const ajv = dialect === "2019" ? new Ajv2019(opts) : dialect === "draft7" ? new AjvDraft07(opts) : dialect === "draft4" ? new AjvDraft04(opts) : new Ajv2020(opts);
|
|
1219
|
+
addFormats(ajv);
|
|
1220
|
+
if (dialect === "draft7") ajv.addMetaSchema(draft06MetaSchema);
|
|
1221
|
+
registerEncryptKeyword(ajv);
|
|
1222
|
+
registerLocationKeyword(ajv);
|
|
1223
|
+
registerGraphOutputKeyword(ajv);
|
|
1224
|
+
registerBuiltins(ajv, dialect);
|
|
1225
|
+
return ajv;
|
|
1226
|
+
}
|
|
1227
|
+
var markRecorder;
|
|
1228
|
+
function registerEncryptKeyword(ajv) {
|
|
1229
|
+
ajv.addKeyword({
|
|
1230
|
+
keyword: ENCRYPT_KEYWORD,
|
|
1231
|
+
metaSchema: { type: "boolean" },
|
|
1232
|
+
errors: false,
|
|
1233
|
+
validate: (schema, _data, _parent, cxt) => {
|
|
1234
|
+
if (schema === true && markRecorder !== void 0) {
|
|
1235
|
+
markRecorder.add(cxt?.instancePath ?? "");
|
|
1236
|
+
}
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function isMarkShapeError(err) {
|
|
1242
|
+
return err instanceof Error && err.message.includes(`"${ENCRYPT_KEYWORD}"`);
|
|
1243
|
+
}
|
|
1244
|
+
var locationRecorder;
|
|
1245
|
+
function topLevelKey(instancePath) {
|
|
1246
|
+
if (!instancePath.startsWith("/") || instancePath.indexOf("/", 1) !== -1) {
|
|
1247
|
+
return void 0;
|
|
1248
|
+
}
|
|
1249
|
+
return instancePath.slice(1).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1250
|
+
}
|
|
1251
|
+
function registerLocationKeyword(ajv) {
|
|
1252
|
+
ajv.addKeyword({
|
|
1253
|
+
keyword: LOCATION_KEYWORD,
|
|
1254
|
+
metaSchema: { type: "string", enum: ["page", "external"] },
|
|
1255
|
+
errors: false,
|
|
1256
|
+
validate: (schema, _data, _parent, cxt) => {
|
|
1257
|
+
if (locationRecorder !== void 0 && isFieldLocation(schema)) {
|
|
1258
|
+
const key = topLevelKey(cxt?.instancePath ?? "");
|
|
1259
|
+
if (key !== void 0) {
|
|
1260
|
+
const seen = locationRecorder.get(key);
|
|
1261
|
+
if (seen) seen.add(schema);
|
|
1262
|
+
else locationRecorder.set(key, /* @__PURE__ */ new Set([schema]));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
function isLocationShapeError(err) {
|
|
1270
|
+
return err instanceof Error && err.message.includes(`"${LOCATION_KEYWORD}"`);
|
|
1271
|
+
}
|
|
1272
|
+
var graphOutputRecorder;
|
|
1273
|
+
function registerGraphOutputKeyword(ajv) {
|
|
1274
|
+
ajv.addKeyword({
|
|
1275
|
+
keyword: GRAPH_OUTPUT_KEYWORD,
|
|
1276
|
+
metaSchema: { type: "boolean" },
|
|
1277
|
+
errors: false,
|
|
1278
|
+
validate: (schema, _data, _parent, cxt) => {
|
|
1279
|
+
if (graphOutputRecorder !== void 0 && typeof schema === "boolean") {
|
|
1280
|
+
const key = topLevelKey(cxt?.instancePath ?? "");
|
|
1281
|
+
if (key !== void 0) {
|
|
1282
|
+
const seen = graphOutputRecorder.get(key);
|
|
1283
|
+
if (seen) seen.add(schema);
|
|
1284
|
+
else graphOutputRecorder.set(key, /* @__PURE__ */ new Set([schema]));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
function isGraphOutputShapeError(err) {
|
|
1292
|
+
return err instanceof Error && err.message.includes(`"${GRAPH_OUTPUT_KEYWORD}"`);
|
|
1293
|
+
}
|
|
1294
|
+
function marksSchemaExternal(schema) {
|
|
1295
|
+
const properties = schema["properties"];
|
|
1296
|
+
if (typeof properties !== "object" || properties === null) return false;
|
|
1297
|
+
const own = properties[FILE_SCHEMA_KEY];
|
|
1298
|
+
if (typeof own !== "object" || own === null) return false;
|
|
1299
|
+
return own[LOCATION_KEYWORD] === "external";
|
|
1300
|
+
}
|
|
1301
|
+
function isRecord2(value) {
|
|
1302
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1303
|
+
}
|
|
1304
|
+
function definiteMarkConflict(root, keyword, isMark) {
|
|
1305
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1306
|
+
const localRef = (node, embedded) => {
|
|
1307
|
+
const ref = node["$ref"];
|
|
1308
|
+
if (embedded || typeof ref !== "string" || !ref.startsWith("#")) return void 0;
|
|
1309
|
+
const pointer = ref.slice(1);
|
|
1310
|
+
if (pointer === "") return root;
|
|
1311
|
+
if (!pointer.startsWith("/")) return void 0;
|
|
1312
|
+
let at2 = root;
|
|
1313
|
+
for (const raw of pointer.slice(1).split("/")) {
|
|
1314
|
+
let segment;
|
|
1315
|
+
try {
|
|
1316
|
+
segment = decodeURIComponent(raw).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1317
|
+
} catch {
|
|
1318
|
+
return void 0;
|
|
1319
|
+
}
|
|
1320
|
+
if (Array.isArray(at2)) at2 = at2[Number(segment)];
|
|
1321
|
+
else if (isRecord2(at2)) at2 = at2[segment];
|
|
1322
|
+
else return void 0;
|
|
1323
|
+
}
|
|
1324
|
+
return at2;
|
|
1325
|
+
};
|
|
1326
|
+
const isEmbedded = (node, embedded) => embedded || node !== root && typeof node["$id"] === "string";
|
|
1327
|
+
const allOf = (node) => Array.isArray(node["allOf"]) ? node["allOf"] : [];
|
|
1328
|
+
const property = (key, node, embedded, visited) => {
|
|
1329
|
+
if (!isRecord2(node) || visited.has(node)) return;
|
|
1330
|
+
visited.add(node);
|
|
1331
|
+
const inner = isEmbedded(node, embedded);
|
|
1332
|
+
const mark = node[keyword];
|
|
1333
|
+
if (isMark(mark)) {
|
|
1334
|
+
const seen = marks.get(key);
|
|
1335
|
+
if (seen) seen.add(mark);
|
|
1336
|
+
else marks.set(key, /* @__PURE__ */ new Set([mark]));
|
|
1337
|
+
}
|
|
1338
|
+
for (const entry of allOf(node)) property(key, entry, inner, visited);
|
|
1339
|
+
property(key, localRef(node, inner), inner, visited);
|
|
1340
|
+
};
|
|
1341
|
+
const object = (node, embedded, visited) => {
|
|
1342
|
+
if (!isRecord2(node) || visited.has(node)) return;
|
|
1343
|
+
visited.add(node);
|
|
1344
|
+
const inner = isEmbedded(node, embedded);
|
|
1345
|
+
const properties = node["properties"];
|
|
1346
|
+
if (isRecord2(properties)) {
|
|
1347
|
+
for (const [key, sub] of Object.entries(properties)) property(key, sub, inner, /* @__PURE__ */ new Set());
|
|
1348
|
+
}
|
|
1349
|
+
for (const entry of allOf(node)) object(entry, inner, visited);
|
|
1350
|
+
object(localRef(node, inner), inner, visited);
|
|
1351
|
+
};
|
|
1352
|
+
object(root, false, /* @__PURE__ */ new Set());
|
|
1353
|
+
for (const [key, seen] of marks) if (seen.size > 1) return key;
|
|
1354
|
+
return void 0;
|
|
1355
|
+
}
|
|
1356
|
+
function registerBuiltins(ajv, dialect) {
|
|
1357
|
+
for (const { id, url, legacyUrl, schema } of publishedBuiltins()) {
|
|
1358
|
+
ajv.addSchema(schema, url, void 0, false);
|
|
1359
|
+
ajv.addSchema(schema, legacyUrl, void 0, false);
|
|
1360
|
+
if (dialect === "draft4") ajv.addSchema(schema, id, void 0, false);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function registerRegistered(ajv, dialect, registered) {
|
|
1364
|
+
for (const { id, schema } of registered?.values() ?? []) {
|
|
1365
|
+
ajv.addSchema(schema, dialect === "draft4" ? id : void 0, void 0, false);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function compileWithFormats(schema, registered, options = {}) {
|
|
1369
|
+
const ajv = buildAjv("2020", options.coerce === true ? "array" : false);
|
|
1370
|
+
registerRegistered(ajv, "2020", registered);
|
|
1371
|
+
return ajv.compile(schema);
|
|
1372
|
+
}
|
|
1373
|
+
var formatRegistry;
|
|
1374
|
+
function formatsAjv() {
|
|
1375
|
+
if (!formatRegistry) {
|
|
1376
|
+
formatRegistry = new Ajv2020({ strict: false });
|
|
1377
|
+
addFormats(formatRegistry);
|
|
1378
|
+
}
|
|
1379
|
+
return formatRegistry;
|
|
1380
|
+
}
|
|
1381
|
+
var stringFormatsMemo;
|
|
1382
|
+
function stringFormatNames() {
|
|
1383
|
+
if (!stringFormatsMemo) {
|
|
1384
|
+
const names = /* @__PURE__ */ new Set();
|
|
1385
|
+
const registered = formatsAjv().formats;
|
|
1386
|
+
for (const [name, def] of Object.entries(registered)) {
|
|
1387
|
+
const enforcesStrings = def instanceof RegExp || typeof def === "function" || typeof def === "object" && def !== null && (def.type === void 0 || def.type === "string");
|
|
1388
|
+
if (enforcesStrings) names.add(name);
|
|
1389
|
+
}
|
|
1390
|
+
stringFormatsMemo = names;
|
|
1391
|
+
}
|
|
1392
|
+
return stringFormatsMemo;
|
|
1393
|
+
}
|
|
1394
|
+
var formatCheckers = /* @__PURE__ */ new Map();
|
|
1395
|
+
function validatesFormat(name, value) {
|
|
1396
|
+
let fn = formatCheckers.get(name);
|
|
1397
|
+
if (!fn) {
|
|
1398
|
+
fn = formatsAjv().compile({ type: "string", format: name });
|
|
1399
|
+
formatCheckers.set(name, fn);
|
|
1400
|
+
}
|
|
1401
|
+
return fn(value);
|
|
1402
|
+
}
|
|
1403
|
+
var Validator = class {
|
|
1404
|
+
/**
|
|
1405
|
+
* How this validator's schemas are loaded: the disk cache location, its TTL,
|
|
1406
|
+
* and `--offline`. Held per instance rather than read from module state, so
|
|
1407
|
+
* two differently-configured validations in one process each get their own
|
|
1408
|
+
* settings.
|
|
1409
|
+
*
|
|
1410
|
+
* That is not full isolation, and the difference matters to a library
|
|
1411
|
+
* caller: `schema-registry` keeps a process-wide memo of fetched schemas, so
|
|
1412
|
+
* one validator's successful fetch is visible to another. `offline` is
|
|
1413
|
+
* excluded from that sharing on purpose — an offline validator will not be
|
|
1414
|
+
* served something this process pulled over the network — but the memo is
|
|
1415
|
+
* still shared, so a URL fetched once is not re-fetched per instance.
|
|
1416
|
+
*/
|
|
1417
|
+
constructor(schemaOptions = {}) {
|
|
1418
|
+
this.schemaOptions = schemaOptions;
|
|
1419
|
+
}
|
|
1420
|
+
schemaOptions;
|
|
1421
|
+
ajvByDialect = /* @__PURE__ */ new Map();
|
|
1422
|
+
/**
|
|
1423
|
+
* Keyed on the in-flight *promise*, not the resolved validator. Caching the
|
|
1424
|
+
* result made this a check-then-act race: `fill` walks files through a worker
|
|
1425
|
+
* pool, so every worker missed the cache while the first `loadSchema` was
|
|
1426
|
+
* still pending and they all then compiled the same schema into the one
|
|
1427
|
+
* shared per-dialect Ajv. Ajv registers a schema's `$id` on the first compile
|
|
1428
|
+
* and rejects the second with "schema with key or id ... already exists",
|
|
1429
|
+
* which took down any multi-file run against an $id-bearing schema. Storing
|
|
1430
|
+
* the promise before the first await lets the losers await the one compile.
|
|
1431
|
+
*/
|
|
1432
|
+
cache = /* @__PURE__ */ new Map();
|
|
1433
|
+
ajvFor(dialect) {
|
|
1434
|
+
let ajv = this.ajvByDialect.get(dialect);
|
|
1435
|
+
if (!ajv) {
|
|
1436
|
+
ajv = buildAjv(dialect);
|
|
1437
|
+
registerRegistered(ajv, dialect, this.schemaOptions.registered);
|
|
1438
|
+
this.ajvByDialect.set(dialect, ajv);
|
|
1439
|
+
}
|
|
1440
|
+
return ajv;
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Synchronous by design: the `cache.set` has to happen in the same tick as
|
|
1444
|
+
* the miss, or a second caller can slip in before the entry exists.
|
|
1445
|
+
*/
|
|
1446
|
+
compile(ref) {
|
|
1447
|
+
const cached = this.cache.get(ref);
|
|
1448
|
+
if (cached) return cached;
|
|
1449
|
+
const pending = this.compileUncached(ref);
|
|
1450
|
+
this.cache.set(ref, pending);
|
|
1451
|
+
pending.catch(() => {
|
|
1452
|
+
if (this.cache.get(ref) === pending) this.cache.delete(ref);
|
|
1453
|
+
});
|
|
1454
|
+
return pending;
|
|
1455
|
+
}
|
|
1456
|
+
async compileUncached(ref) {
|
|
1457
|
+
const schema = await loadSchema(ref, this.schemaOptions);
|
|
1458
|
+
if (marksSchemaExternal(schema)) {
|
|
1459
|
+
throw new DocmetaError(
|
|
1460
|
+
`${ref}: "${FILE_SCHEMA_KEY}" cannot be stored in external metadata.`
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
const contradicted = definiteMarkConflict(schema, LOCATION_KEYWORD, isFieldLocation);
|
|
1464
|
+
if (contradicted !== void 0) {
|
|
1465
|
+
throw new DocmetaError(
|
|
1466
|
+
`${ref}: "${LOCATION_KEYWORD}" says both "page" and "external" for "${contradicted}".`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const graphContradicted = definiteMarkConflict(
|
|
1470
|
+
schema,
|
|
1471
|
+
GRAPH_OUTPUT_KEYWORD,
|
|
1472
|
+
(value) => typeof value === "boolean"
|
|
1473
|
+
);
|
|
1474
|
+
if (graphContradicted !== void 0) {
|
|
1475
|
+
throw new DocmetaError(
|
|
1476
|
+
`${ref}: "${GRAPH_OUTPUT_KEYWORD}" says both true and false for "${graphContradicted}".`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const ajv = this.ajvFor(dialectOf(schema));
|
|
1481
|
+
const id = typeof schema["$id"] === "string" ? schema["$id"] : void 0;
|
|
1482
|
+
const registered = id != null ? ajv.getSchema(id) : void 0;
|
|
1483
|
+
if (registered) {
|
|
1484
|
+
if (registered.schema === schema) return registered;
|
|
1485
|
+
const { $id: _taken, ...anonymous } = schema;
|
|
1486
|
+
return ajv.compile(anonymous);
|
|
1487
|
+
}
|
|
1488
|
+
return ajv.compile(schema);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
if (isMarkShapeError(err)) {
|
|
1491
|
+
throw new DocmetaError(
|
|
1492
|
+
`${ref}: "${ENCRYPT_KEYWORD}" must be true or false.`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
if (isLocationShapeError(err)) {
|
|
1496
|
+
throw new DocmetaError(
|
|
1497
|
+
`${ref}: "${LOCATION_KEYWORD}" must be "page" or "external".`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
if (isGraphOutputShapeError(err)) {
|
|
1501
|
+
throw new DocmetaError(
|
|
1502
|
+
`${ref}: "${GRAPH_OUTPUT_KEYWORD}" must be true or false.`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
throw new DocmetaError(
|
|
1506
|
+
`Schema "${ref}" failed to compile: ${errorMessage(err)}`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The instance pointers of `data` that carry `x-manni-encrypt: true` under
|
|
1512
|
+
* any schema in `refs`, in the order the validator evaluated them.
|
|
1513
|
+
*
|
|
1514
|
+
* A first pass with the recording keyword, so a mark counts exactly where
|
|
1515
|
+
* validation would evaluate it. Only present values are marked: Ajv applies
|
|
1516
|
+
* a property's subschema to a property that exists. A branch Ajv does not
|
|
1517
|
+
* take (`then` behind a failing `if`, an `anyOf` branch after a passing
|
|
1518
|
+
* one) contributes nothing; a branch it takes contributes its marks
|
|
1519
|
+
* whether or not the branch passes, because a ciphertext routinely fails
|
|
1520
|
+
* the rest of its own subschema until it is decrypted.
|
|
1521
|
+
*/
|
|
1522
|
+
async markedPointers(data, refs) {
|
|
1523
|
+
const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
|
|
1524
|
+
void _omit;
|
|
1525
|
+
const marks = /* @__PURE__ */ new Set();
|
|
1526
|
+
for (const ref of refs) {
|
|
1527
|
+
const fn = await this.compile(ref);
|
|
1528
|
+
markRecorder = marks;
|
|
1529
|
+
try {
|
|
1530
|
+
fn(subject);
|
|
1531
|
+
} finally {
|
|
1532
|
+
markRecorder = void 0;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return marks;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* The location each top-level key of `data` prefers under `refs`: `page` or
|
|
1539
|
+
* `external`, and the ref whose `x-manni-location` mark decided it. A key no
|
|
1540
|
+
* schema marks has no entry.
|
|
1541
|
+
*
|
|
1542
|
+
* A first pass with the recording keyword, as `markedPointers` runs, so a
|
|
1543
|
+
* mark counts exactly where validation would evaluate it, and a nested mark
|
|
1544
|
+
* is ignored. Only present values are marked: Ajv applies a property's
|
|
1545
|
+
* subschema to a property that exists. A caller asking where a key it is
|
|
1546
|
+
* about to write belongs passes `data` that already holds the candidate
|
|
1547
|
+
* value. `$schema` is stripped first, as validation does.
|
|
1548
|
+
*
|
|
1549
|
+
* Refs are evaluated in order and a later ref's mark wins. A ref that says
|
|
1550
|
+
* both values for one key unconditionally is refused when it compiles; one
|
|
1551
|
+
* whose evaluated branches say both gives that key no preference.
|
|
1552
|
+
*/
|
|
1553
|
+
async locationPreferences(data, refs) {
|
|
1554
|
+
const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
|
|
1555
|
+
void _omit;
|
|
1556
|
+
const preferences = /* @__PURE__ */ new Map();
|
|
1557
|
+
for (const ref of refs) {
|
|
1558
|
+
const fn = await this.compile(ref);
|
|
1559
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1560
|
+
locationRecorder = seen;
|
|
1561
|
+
try {
|
|
1562
|
+
fn(subject);
|
|
1563
|
+
} finally {
|
|
1564
|
+
locationRecorder = void 0;
|
|
1565
|
+
}
|
|
1566
|
+
for (const [key, locations] of seen) {
|
|
1567
|
+
const [location, ...others] = [...locations];
|
|
1568
|
+
if (location === void 0 || others.length > 0) continue;
|
|
1569
|
+
preferences.set(key, { location, schema: ref });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return preferences;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* The top-level property names the schemas in `refs` claim: the keys of
|
|
1576
|
+
* each schema's own `properties`, each once. What a page's schema set
|
|
1577
|
+
* claims decides which merge-safe fields a `derive:` block with no `fields`
|
|
1578
|
+
* manages on it (proposal 0069). A property reached only through `allOf`,
|
|
1579
|
+
* `$ref` or a conditional is not counted, so a schema that should manage a
|
|
1580
|
+
* field names it in its root `properties`. A schema that fails to load or
|
|
1581
|
+
* compile throws, as it would for `validate`.
|
|
1582
|
+
*/
|
|
1583
|
+
async claimedProperties(refs) {
|
|
1584
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1585
|
+
for (const ref of refs) {
|
|
1586
|
+
const schema = (await this.compile(ref)).schema;
|
|
1587
|
+
if (typeof schema !== "object" || schema === null) continue;
|
|
1588
|
+
const properties = schema["properties"];
|
|
1589
|
+
if (typeof properties !== "object" || properties === null || Array.isArray(properties)) continue;
|
|
1590
|
+
for (const key of Object.keys(properties)) claimed.add(key);
|
|
1591
|
+
}
|
|
1592
|
+
return claimed;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Whether each top-level key of `data` belongs in a published graph under
|
|
1596
|
+
* `refs`, as its `x-manni-graph-output` mark says. A key no schema marks has no
|
|
1597
|
+
* entry, which the caller reads as `true`: absent means harvested.
|
|
1598
|
+
*
|
|
1599
|
+
* A first pass with the recording keyword, as `locationPreferences` runs, so
|
|
1600
|
+
* a mark counts exactly where validation would evaluate it, and a mark nested
|
|
1601
|
+
* inside `graph` is ignored. Only present values are marked: Ajv applies a
|
|
1602
|
+
* property's subschema to a property that exists. `$schema` is stripped
|
|
1603
|
+
* first, as validation does.
|
|
1604
|
+
*
|
|
1605
|
+
* Refs are evaluated in order and a later ref's mark wins, so a house schema
|
|
1606
|
+
* listed after a vocabulary refines it. A schema that says both on paths
|
|
1607
|
+
* every document takes is refused when it compiles. A ref whose conditional
|
|
1608
|
+
* branches say both `true` and `false` gives that key no preference: with
|
|
1609
|
+
* `allErrors`, Ajv evaluates failing branches too, so neither value is that
|
|
1610
|
+
* ref's answer and an earlier ref's stands.
|
|
1611
|
+
*/
|
|
1612
|
+
async graphOutputPreferences(data, refs) {
|
|
1613
|
+
const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
|
|
1614
|
+
void _omit;
|
|
1615
|
+
const preferences = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const ref of refs) {
|
|
1617
|
+
const fn = await this.compile(ref);
|
|
1618
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1619
|
+
graphOutputRecorder = seen;
|
|
1620
|
+
try {
|
|
1621
|
+
fn(subject);
|
|
1622
|
+
} finally {
|
|
1623
|
+
graphOutputRecorder = void 0;
|
|
1624
|
+
}
|
|
1625
|
+
for (const [key, values] of seen) {
|
|
1626
|
+
const [value, ...others] = [...values];
|
|
1627
|
+
if (value === void 0 || others.length > 0) continue;
|
|
1628
|
+
preferences.set(key, value);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return preferences;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Validate `data` against every schema in `refs`. Returns all violations,
|
|
1635
|
+
* each tagged with the schema that produced it and a source line via
|
|
1636
|
+
* `lineFor`.
|
|
1637
|
+
*
|
|
1638
|
+
* `colFor` is optional and additive: this signature is public, so a fourth
|
|
1639
|
+
* *required* parameter — or a widened third one — would be a consumer break.
|
|
1640
|
+
* Callers with an extractor that supplies no column pass nothing and get the
|
|
1641
|
+
* previous behavior exactly.
|
|
1642
|
+
*/
|
|
1643
|
+
async validate(data, refs, lineFor, colFor, locate) {
|
|
1644
|
+
const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
|
|
1645
|
+
void _omit;
|
|
1646
|
+
const errors = [];
|
|
1647
|
+
for (const ref of refs) {
|
|
1648
|
+
const fn = await this.compile(ref);
|
|
1649
|
+
const ok = fn(subject);
|
|
1650
|
+
if (ok) continue;
|
|
1651
|
+
for (const e of fn.errors ?? []) {
|
|
1652
|
+
errors.push(toFieldError(ref, e, lineFor, colFor, locate));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return errors;
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
function subjectOf(e) {
|
|
1659
|
+
switch (e.keyword) {
|
|
1660
|
+
case "required":
|
|
1661
|
+
return e.params.missingProperty;
|
|
1662
|
+
case "additionalProperties":
|
|
1663
|
+
return e.params.additionalProperty;
|
|
1664
|
+
case "format":
|
|
1665
|
+
return e.params.format;
|
|
1666
|
+
case "type":
|
|
1667
|
+
return e.params.type;
|
|
1668
|
+
default:
|
|
1669
|
+
return void 0;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function toFieldError(schema, e, lineFor, colFor, locate) {
|
|
1673
|
+
const defined = e;
|
|
1674
|
+
const instancePath = e.instancePath;
|
|
1675
|
+
const subject = subjectOf(defined);
|
|
1676
|
+
let message = e.message ?? "is invalid";
|
|
1677
|
+
let wantsColumn = true;
|
|
1678
|
+
if (defined.keyword === "required") {
|
|
1679
|
+
message = `must have required property '${defined.params.missingProperty}'`;
|
|
1680
|
+
wantsColumn = false;
|
|
1681
|
+
} else if (defined.keyword === "additionalProperties") {
|
|
1682
|
+
message = `must NOT have additional property '${defined.params.additionalProperty}'`;
|
|
1683
|
+
wantsColumn = false;
|
|
1684
|
+
}
|
|
1685
|
+
const located = defined.keyword === "additionalProperties" ? locate?.(
|
|
1686
|
+
`${instancePath}/${escapePointerSegment(defined.params.additionalProperty)}`
|
|
1687
|
+
) ?? locate?.(instancePath) : locate?.(instancePath);
|
|
1688
|
+
if (located) {
|
|
1689
|
+
return {
|
|
1690
|
+
schema,
|
|
1691
|
+
instancePath,
|
|
1692
|
+
message,
|
|
1693
|
+
keyword: e.keyword,
|
|
1694
|
+
...subject != null ? { subject } : {},
|
|
1695
|
+
file: located.file,
|
|
1696
|
+
...located.line != null ? { line: located.line } : {},
|
|
1697
|
+
...wantsColumn && located.col != null ? { col: located.col } : {}
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
const line = lineFor(instancePath);
|
|
1701
|
+
const col = wantsColumn ? colFor?.(instancePath) : void 0;
|
|
1702
|
+
return {
|
|
1703
|
+
schema,
|
|
1704
|
+
instancePath,
|
|
1705
|
+
message,
|
|
1706
|
+
keyword: e.keyword,
|
|
1707
|
+
...subject != null ? { subject } : {},
|
|
1708
|
+
...line != null ? { line } : {},
|
|
1709
|
+
...col != null ? { col } : {}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/meta/core/page-marks.ts
|
|
1714
|
+
function pageMarks(opts) {
|
|
1715
|
+
const validator = lazyValidator(opts);
|
|
1716
|
+
return async (label, probe, memberOf2) => {
|
|
1717
|
+
const refs = pageRefs(opts, label, probe, memberOf2);
|
|
1718
|
+
if (refs === void 0) return void 0;
|
|
1719
|
+
try {
|
|
1720
|
+
const preferences = await validator().locationPreferences({ ...probe }, refs);
|
|
1721
|
+
const marked = /* @__PURE__ */ new Set();
|
|
1722
|
+
for (const [key, p] of preferences) if (p.location === "external") marked.add(key);
|
|
1723
|
+
return marked;
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
if (err instanceof DocmetaError) return void 0;
|
|
1726
|
+
throw err;
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function pageClaims(opts) {
|
|
1731
|
+
const validator = lazyValidator(opts);
|
|
1732
|
+
return async (label, probe, memberOf2) => {
|
|
1733
|
+
const refs = pageRefs(opts, label, probe, memberOf2);
|
|
1734
|
+
if (refs === void 0) return void 0;
|
|
1735
|
+
try {
|
|
1736
|
+
return await validator().claimedProperties(refs);
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
if (err instanceof DocmetaError) return void 0;
|
|
1739
|
+
throw err;
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function lazyValidator(opts) {
|
|
1744
|
+
let built;
|
|
1745
|
+
return () => opts.validator instanceof Validator ? opts.validator : built ??= opts.validator();
|
|
1746
|
+
}
|
|
1747
|
+
function pageSchemaSet(opts, label, probe, memberOf2) {
|
|
1748
|
+
return resolveSchemaSetWithSource({
|
|
1749
|
+
filePath: label,
|
|
1750
|
+
fileSchema: probe[FILE_SCHEMA_KEY],
|
|
1751
|
+
...opts.cliSchemas !== void 0 ? { cliSchemas: [...opts.cliSchemas] } : {},
|
|
1752
|
+
config: opts.config,
|
|
1753
|
+
memberOf: opts.memberOf?.(label) ?? memberOf2,
|
|
1754
|
+
fileBase: opts.cwd,
|
|
1755
|
+
trustRoot: opts.trustRoot
|
|
1756
|
+
}).schemas;
|
|
1757
|
+
}
|
|
1758
|
+
function pageRefs(opts, label, probe, memberOf2) {
|
|
1759
|
+
try {
|
|
1760
|
+
return pageSchemaSet(opts, label, probe, memberOf2);
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
if (err instanceof DocmetaError) return void 0;
|
|
1763
|
+
throw err;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
function marksValidator(opts) {
|
|
1767
|
+
return () => new Validator(
|
|
1768
|
+
schemaLoadOptions({
|
|
1769
|
+
root: opts.configDir ?? opts.cwd,
|
|
1770
|
+
fileBase: opts.cwd,
|
|
1771
|
+
ttlHours: opts.config?.schemaCache?.ttlHours,
|
|
1772
|
+
offline: opts.offline ?? opts.config?.offline,
|
|
1773
|
+
pins: collectSchemaPins(opts.config),
|
|
1774
|
+
registered: opts.config?.registered
|
|
1775
|
+
})
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
async function configMarks(configPath, cwd) {
|
|
1779
|
+
const loaded = await loadConfig(configPath, cwd);
|
|
1780
|
+
if (loaded === null) return void 0;
|
|
1781
|
+
const config = rebaseConfigSchemaRefs(loaded.config, loaded.dir, cwd);
|
|
1782
|
+
return pageMarks({
|
|
1783
|
+
validator: marksValidator({ config, cwd, configDir: loaded.dir }),
|
|
1784
|
+
config,
|
|
1785
|
+
cwd,
|
|
1786
|
+
trustRoot: schemaTrustRoot(cwd, loaded.dir)
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
async function metaSection(configPath, cwd) {
|
|
1790
|
+
const loaded = await loadConfig(configPath, cwd);
|
|
1791
|
+
if (loaded === null) return null;
|
|
1792
|
+
return rebaseConfigSchemaRefs(loaded.config, loaded.dir, cwd);
|
|
1793
|
+
}
|
|
1794
|
+
function familyMarks(opts) {
|
|
1795
|
+
let built;
|
|
1796
|
+
const defaults = () => pageMarks({
|
|
1797
|
+
validator: marksValidator({ config: null, cwd: opts.cwd, configDir: opts.configDir }),
|
|
1798
|
+
config: null,
|
|
1799
|
+
cwd: opts.cwd,
|
|
1800
|
+
trustRoot: schemaTrustRoot(opts.cwd, opts.configDir)
|
|
1801
|
+
});
|
|
1802
|
+
return () => built ??= (async () => {
|
|
1803
|
+
if (opts.configPath === void 0) return defaults();
|
|
1804
|
+
try {
|
|
1805
|
+
return await configMarks(opts.configPath, opts.cwd) ?? defaults();
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
if (err instanceof DocmetaError) return defaults();
|
|
1808
|
+
throw err;
|
|
1809
|
+
}
|
|
1810
|
+
})();
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/meta/core/projection.ts
|
|
1814
|
+
var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
|
|
1815
|
+
var RESERVED = new Set(SYSTEM_COLUMNS);
|
|
1816
|
+
function quoteIdent(name) {
|
|
1817
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
1818
|
+
}
|
|
1819
|
+
function bindValue(value) {
|
|
1820
|
+
if (value === void 0 || value === null) return null;
|
|
1821
|
+
switch (typeof value) {
|
|
1822
|
+
case "boolean":
|
|
1823
|
+
return value ? 1 : 0;
|
|
1824
|
+
case "number":
|
|
1825
|
+
return Number.isFinite(value) ? value : null;
|
|
1826
|
+
case "bigint":
|
|
1827
|
+
case "string":
|
|
1828
|
+
return value;
|
|
1829
|
+
default:
|
|
1830
|
+
return JSON.stringify(value);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function corpusDataColumns(entries, extraKeys = []) {
|
|
1834
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1835
|
+
for (const { extracted } of entries) {
|
|
1836
|
+
for (const key of Object.keys(extracted.data)) {
|
|
1837
|
+
if (key !== "" && !RESERVED.has(key)) keys.add(key);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
for (const key of extraKeys) {
|
|
1841
|
+
if (key !== "" && !RESERVED.has(key)) keys.add(key);
|
|
1842
|
+
}
|
|
1843
|
+
return [...keys].sort();
|
|
1844
|
+
}
|
|
1845
|
+
function createDocsTable(db, entries, dataColumns) {
|
|
1846
|
+
db.exec(
|
|
1847
|
+
`CREATE TABLE docs ("_path" TEXT PRIMARY KEY, "_format" TEXT, "_present" INTEGER, "_data" TEXT${dataColumns.map((c) => `, ${quoteIdent(c)}`).join("")})`
|
|
1848
|
+
);
|
|
1849
|
+
const insert = db.prepare(
|
|
1850
|
+
`INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
|
|
1851
|
+
);
|
|
1852
|
+
db.exec("BEGIN");
|
|
1853
|
+
let committed = false;
|
|
1854
|
+
try {
|
|
1855
|
+
for (const { label, extracted } of entries) {
|
|
1856
|
+
insert.run(
|
|
1857
|
+
label,
|
|
1858
|
+
extracted.format,
|
|
1859
|
+
extracted.present ? 1 : 0,
|
|
1860
|
+
JSON.stringify(extracted.data),
|
|
1861
|
+
...dataColumns.map((c) => bindValue(extracted.data[c]))
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
db.exec("COMMIT");
|
|
1865
|
+
committed = true;
|
|
1866
|
+
} finally {
|
|
1867
|
+
if (!committed) db.exec("ROLLBACK");
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function registerLineFor(db, entries) {
|
|
1871
|
+
const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
|
|
1872
|
+
db.function("lineFor", { deterministic: true }, (path, key) => {
|
|
1873
|
+
if (typeof path !== "string" || typeof key !== "string") return null;
|
|
1874
|
+
return byLabel.get(path)?.lineFor(key) ?? null;
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
function assertSingleStatement(sql) {
|
|
1878
|
+
const cut = topLevelSemicolon(sql);
|
|
1879
|
+
if (cut === -1) return;
|
|
1880
|
+
if (!isTrivia(sql.slice(cut + 1))) {
|
|
1881
|
+
throw new DocmetaError(
|
|
1882
|
+
"Run a single SQL statement per query; text after the first `;` would be silently ignored."
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function startsStringOrIdent(sql, i) {
|
|
1887
|
+
const ch = sql[i];
|
|
1888
|
+
return ch === "'" || ch === '"' || ch === "`" || ch === "[";
|
|
1889
|
+
}
|
|
1890
|
+
function startsComment(sql, i) {
|
|
1891
|
+
const ch = sql[i];
|
|
1892
|
+
return ch === "-" && sql[i + 1] === "-" || ch === "/" && sql[i + 1] === "*";
|
|
1893
|
+
}
|
|
1894
|
+
function skipStringOrIdent(sql, i) {
|
|
1895
|
+
const quote = sql[i];
|
|
1896
|
+
if (quote === "[") {
|
|
1897
|
+
let j2 = i + 1;
|
|
1898
|
+
while (j2 < sql.length && sql[j2] !== "]") j2++;
|
|
1899
|
+
return j2 + 1;
|
|
1900
|
+
}
|
|
1901
|
+
let j = i + 1;
|
|
1902
|
+
while (j < sql.length) {
|
|
1903
|
+
if (sql[j] === quote) {
|
|
1904
|
+
if (sql[j + 1] === quote) {
|
|
1905
|
+
j += 2;
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
return j + 1;
|
|
1909
|
+
}
|
|
1910
|
+
j++;
|
|
1911
|
+
}
|
|
1912
|
+
return j;
|
|
1913
|
+
}
|
|
1914
|
+
function skipComment(sql, i) {
|
|
1915
|
+
if (sql[i] === "-") {
|
|
1916
|
+
let j = i + 2;
|
|
1917
|
+
while (j < sql.length && sql[j] !== "\n") j++;
|
|
1918
|
+
return j < sql.length ? j + 1 : j;
|
|
1919
|
+
}
|
|
1920
|
+
const end = sql.indexOf("*/", i + 2);
|
|
1921
|
+
return end === -1 ? sql.length : end + 2;
|
|
1922
|
+
}
|
|
1923
|
+
function topLevelSemicolon(sql) {
|
|
1924
|
+
let i = 0;
|
|
1925
|
+
while (i < sql.length) {
|
|
1926
|
+
if (startsStringOrIdent(sql, i)) {
|
|
1927
|
+
i = skipStringOrIdent(sql, i);
|
|
1928
|
+
} else if (startsComment(sql, i)) {
|
|
1929
|
+
i = skipComment(sql, i);
|
|
1930
|
+
} else if (sql[i] === ";") {
|
|
1931
|
+
return i;
|
|
1932
|
+
} else {
|
|
1933
|
+
i++;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return -1;
|
|
1937
|
+
}
|
|
1938
|
+
var PARAM_NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
1939
|
+
function isParamName(name) {
|
|
1940
|
+
return PARAM_NAME_HEAD.exec(name)?.[0] === name;
|
|
1941
|
+
}
|
|
1942
|
+
function collectNamedParameters(sql) {
|
|
1943
|
+
const tokens = [];
|
|
1944
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1945
|
+
const prefixOf = /* @__PURE__ */ new Map();
|
|
1946
|
+
let i = 0;
|
|
1947
|
+
while (i < sql.length) {
|
|
1948
|
+
const ch = sql[i];
|
|
1949
|
+
if (startsStringOrIdent(sql, i)) {
|
|
1950
|
+
i = skipStringOrIdent(sql, i);
|
|
1951
|
+
} else if (startsComment(sql, i)) {
|
|
1952
|
+
i = skipComment(sql, i);
|
|
1953
|
+
} else if (ch === "$" || ch === ":" || ch === "@") {
|
|
1954
|
+
if (ch === ":" && sql[i + 1] === ":") {
|
|
1955
|
+
i += 2;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
const m = PARAM_NAME_HEAD.exec(sql.slice(i + 1));
|
|
1959
|
+
if (!m) {
|
|
1960
|
+
i++;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
const token = ch + m[0];
|
|
1964
|
+
const priorPrefix = prefixOf.get(m[0]);
|
|
1965
|
+
if (priorPrefix !== void 0 && priorPrefix !== ch) {
|
|
1966
|
+
throw new DocmetaError(
|
|
1967
|
+
`The statement references both ${priorPrefix}${m[0]} and ${token} \u2014 one name under two prefixes, which the engine rejects as conflicting. Use one spelling throughout.`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
prefixOf.set(m[0], ch);
|
|
1971
|
+
if (!seen.has(token)) {
|
|
1972
|
+
seen.add(token);
|
|
1973
|
+
tokens.push(token);
|
|
1974
|
+
}
|
|
1975
|
+
i += 1 + m[0].length;
|
|
1976
|
+
} else {
|
|
1977
|
+
i++;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return tokens;
|
|
1981
|
+
}
|
|
1982
|
+
function stripLeadingTrivia(sql) {
|
|
1983
|
+
let i = 0;
|
|
1984
|
+
for (; ; ) {
|
|
1985
|
+
while (i < sql.length && /\s/.test(sql[i] ?? "")) i++;
|
|
1986
|
+
if (startsComment(sql, i)) {
|
|
1987
|
+
i = skipComment(sql, i);
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
return sql.slice(i);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function isTrivia(text) {
|
|
1994
|
+
let i = 0;
|
|
1995
|
+
while (i < text.length) {
|
|
1996
|
+
const ch = text[i];
|
|
1997
|
+
if (ch === void 0 || /\s/.test(ch) || ch === ";") {
|
|
1998
|
+
i++;
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
if (startsComment(text, i)) {
|
|
2002
|
+
i = skipComment(text, i);
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
var sqliteModule;
|
|
2010
|
+
function loadSqlite() {
|
|
2011
|
+
sqliteModule ??= (async () => {
|
|
2012
|
+
const original = process.emitWarning.bind(process);
|
|
2013
|
+
const filtered = (warning, ...rest) => {
|
|
2014
|
+
const text = typeof warning === "string" ? warning : warning.message;
|
|
2015
|
+
if (text.includes("SQLite is an experimental feature")) return;
|
|
2016
|
+
Reflect.apply(original, process, [warning, ...rest]);
|
|
2017
|
+
};
|
|
2018
|
+
process.emitWarning = filtered;
|
|
2019
|
+
try {
|
|
2020
|
+
return await import("node:sqlite");
|
|
2021
|
+
} finally {
|
|
2022
|
+
process.emitWarning = original;
|
|
2023
|
+
}
|
|
2024
|
+
})();
|
|
2025
|
+
return sqliteModule;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/meta/core/collections.ts
|
|
2029
|
+
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolvePath, sep as sep2 } from "node:path";
|
|
2030
|
+
function memberOf(collections, configDir, base, label) {
|
|
2031
|
+
if (collections.length === 0) return [];
|
|
2032
|
+
if (label === STDIN_LABEL) return [];
|
|
2033
|
+
const rel = relative3(configDir, resolvePath(base, label)).split(sep2).join("/");
|
|
2034
|
+
if (isAbsolute2(rel)) return [];
|
|
2035
|
+
return collections.filter((c) => isMember(c, rel)).map((c) => c.name);
|
|
2036
|
+
}
|
|
2037
|
+
function retainMembers(files, membersFor) {
|
|
2038
|
+
return files.filter((label) => membersFor(label).length > 0);
|
|
2039
|
+
}
|
|
2040
|
+
function collectionNames(collections) {
|
|
2041
|
+
return (collections ?? []).map((c) => c.name);
|
|
2042
|
+
}
|
|
2043
|
+
function collectCollections(entries, params) {
|
|
2044
|
+
const declared = params.collections ?? [];
|
|
2045
|
+
if (declared.length === 0) return [];
|
|
2046
|
+
const base = params.base ?? ".";
|
|
2047
|
+
const configDir = params.configDir ?? base;
|
|
2048
|
+
const members = /* @__PURE__ */ new Map();
|
|
2049
|
+
for (const entry of entries) {
|
|
2050
|
+
for (const name of memberOf(declared, configDir, base, entry.label)) {
|
|
2051
|
+
const list2 = members.get(name);
|
|
2052
|
+
if (list2) list2.push(entry.label);
|
|
2053
|
+
else members.set(name, [entry.label]);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return declared.map((c) => ({ name: c.name, members: members.get(c.name) ?? [] }));
|
|
2057
|
+
}
|
|
2058
|
+
function quoteSqlString(value) {
|
|
2059
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2060
|
+
}
|
|
2061
|
+
function createCollectionViews(db, collections) {
|
|
2062
|
+
for (const c of collections) {
|
|
2063
|
+
const where = c.members.length === 0 ? "0" : `_path IN (${c.members.map(quoteSqlString).join(", ")})`;
|
|
2064
|
+
db.exec(
|
|
2065
|
+
`CREATE VIEW ${quoteIdent(c.name)} AS SELECT * FROM docs WHERE ${where}`
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/meta/core/relocation.ts
|
|
2071
|
+
import { access, readFile as readFile4, stat as stat2, unlink } from "node:fs/promises";
|
|
2072
|
+
import { basename as basename3, extname, isAbsolute as isAbsolute3, join as join3, relative as relative4, resolve as resolve3, sep as sep3 } from "node:path";
|
|
2073
|
+
import picomatch from "picomatch";
|
|
2074
|
+
import {
|
|
2075
|
+
LineCounter as LineCounter3,
|
|
2076
|
+
isMap as isMap4,
|
|
2077
|
+
isNode as isNode3,
|
|
2078
|
+
isScalar as isScalar3,
|
|
2079
|
+
isSeq as isSeq2,
|
|
2080
|
+
parse as parseYaml2,
|
|
2081
|
+
parseDocument as parseDocument4
|
|
2082
|
+
} from "yaml";
|
|
2083
|
+
|
|
2084
|
+
// src/meta/core/external-metadata-write.ts
|
|
2085
|
+
import { posix as posix3 } from "node:path";
|
|
2086
|
+
import {
|
|
2087
|
+
LineCounter as LineCounter2,
|
|
2088
|
+
isMap as isMap3,
|
|
2089
|
+
isNode as isNode2,
|
|
2090
|
+
isScalar as isScalar2,
|
|
2091
|
+
parseDocument as parseDocument3,
|
|
2092
|
+
stringify as stringify2
|
|
2093
|
+
} from "yaml";
|
|
2094
|
+
function spliceManifestValue(text, options) {
|
|
2095
|
+
const { entry, key, value, file } = options;
|
|
2096
|
+
const join4 = options.join ?? PATH_JOIN;
|
|
2097
|
+
const at2 = (line2) => {
|
|
2098
|
+
const name = file === void 0 ? "Manifest" : `Manifest ${file}`;
|
|
2099
|
+
if (line2 === void 0) return name;
|
|
2100
|
+
return file === void 0 ? `${name} line ${String(line2)}` : `${name}:${String(line2)}`;
|
|
2101
|
+
};
|
|
2102
|
+
if (key === FILE_SCHEMA_KEY) {
|
|
2103
|
+
throw new DocmetaError(
|
|
2104
|
+
`${at2()}: refusing to write "${FILE_SCHEMA_KEY}" for "${entry}"; a manifest never chooses the schema a document is judged by. Use "overrides" in the config.`
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
if (value === void 0) {
|
|
2108
|
+
throw new DocmetaError(`${at2()}: refusing to write "${key}" for "${entry}" with no value.`);
|
|
2109
|
+
}
|
|
2110
|
+
const eol = detectEol(text);
|
|
2111
|
+
const lc = new LineCounter2();
|
|
2112
|
+
const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
|
|
2113
|
+
const problem = doc.errors[0];
|
|
2114
|
+
if (problem) {
|
|
2115
|
+
throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
|
|
2116
|
+
}
|
|
2117
|
+
const keyLine = (node) => {
|
|
2118
|
+
const r = rangeOf(node);
|
|
2119
|
+
return r ? lc.linePos(r[0]).line : void 0;
|
|
2120
|
+
};
|
|
2121
|
+
const same = join4 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
|
|
2122
|
+
const root = doc.contents;
|
|
2123
|
+
let spelled = entry;
|
|
2124
|
+
let edit;
|
|
2125
|
+
if (root === null || isScalar2(root) && root.value === null && emptySource(text, root)) {
|
|
2126
|
+
edit = startManifest(text, entry, key, value, eol);
|
|
2127
|
+
} else if (!isMap3(root)) {
|
|
2128
|
+
throw new DocmetaError(
|
|
2129
|
+
`${at2()}: the manifest must be a mapping from document ${join4 === PATH_JOIN ? "path" : `"${join4}"`} to owned keys.`
|
|
2130
|
+
);
|
|
2131
|
+
} else {
|
|
2132
|
+
const style = detectStyle(text, root, eol);
|
|
2133
|
+
const matches = root.items.filter((p) => same(keyString(p.key), entry));
|
|
2134
|
+
const [first, second] = matches;
|
|
2135
|
+
if (first && second) {
|
|
2136
|
+
throw new DocmetaError(
|
|
2137
|
+
`${at2(keyLine(second.key))}: "${keyString(second.key)}" is named twice (first at line ${String(keyLine(first.key) ?? "?")}). Merge the two entries into one.`
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
if (first === void 0) {
|
|
2141
|
+
if (root.flow === true && root.items.length > 0) {
|
|
2142
|
+
throw new DocmetaError(
|
|
2143
|
+
`${at2()}: the manifest is a flow mapping ({ \u2026 }), which this writer does not add entries to. Rewrite it in block style.`
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
edit = appendEntry(text, root, entry, key, value, style);
|
|
2147
|
+
} else {
|
|
2148
|
+
spelled = keyString(first.key);
|
|
2149
|
+
const where = at2(keyLine(first.key));
|
|
2150
|
+
const v = first.value;
|
|
2151
|
+
if (!isMap3(v)) {
|
|
2152
|
+
throw new DocmetaError(`${where}: "${spelled}" must be a mapping of owned keys to values.`);
|
|
2153
|
+
}
|
|
2154
|
+
const hits = v.items.filter((kv) => keyString(kv.key) === key);
|
|
2155
|
+
const [hit, again] = hits;
|
|
2156
|
+
if (hit && again) {
|
|
2157
|
+
throw new DocmetaError(
|
|
2158
|
+
`${at2(keyLine(again.key))}: "${spelled}" sets "${key}" twice (first at line ${String(keyLine(hit.key) ?? "?")}). Remove one.`
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
const flowRefusal = () => new DocmetaError(
|
|
2162
|
+
`${where}: "${spelled}" is a flow mapping ({ \u2026 }), which this writer does not add keys to. Rewrite the entry in block style.`
|
|
2163
|
+
);
|
|
2164
|
+
const formRefusal = () => new DocmetaError(
|
|
2165
|
+
`${where}: "${spelled}" writes a key in a form this writer does not edit. Write it as "${key}: \u2026".`
|
|
2166
|
+
);
|
|
2167
|
+
let made;
|
|
2168
|
+
if (v.flow === true) {
|
|
2169
|
+
if (hit) made = replaceFlowValue(text, hit, value);
|
|
2170
|
+
else if (v.items.length === 0) made = fillEmptyEntry(text, first, key, value, style);
|
|
2171
|
+
else throw flowRefusal();
|
|
2172
|
+
if (!made) throw hit ? flowRefusal() : formRefusal();
|
|
2173
|
+
} else {
|
|
2174
|
+
made = hit ? replaceBlockValue(text, hit, value, style) : insertKey(text, v, key, value, style);
|
|
2175
|
+
if (!made) throw formRefusal();
|
|
2176
|
+
}
|
|
2177
|
+
edit = made;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
const out = text.slice(0, edit.start) + edit.insert + text.slice(edit.end);
|
|
2181
|
+
const line = readBack(
|
|
2182
|
+
doc,
|
|
2183
|
+
out,
|
|
2184
|
+
spelled,
|
|
2185
|
+
key,
|
|
2186
|
+
value,
|
|
2187
|
+
(why) => new DocmetaError(
|
|
2188
|
+
`${at2()}: writing "${key}" for "${entry}" did not read back as written, so nothing was written. ${why}`
|
|
2189
|
+
)
|
|
2190
|
+
);
|
|
2191
|
+
return { text: out, line };
|
|
2192
|
+
}
|
|
2193
|
+
function readManifestValue(text, options) {
|
|
2194
|
+
const join4 = options.join ?? PATH_JOIN;
|
|
2195
|
+
const doc = parseDocument3(text, { uniqueKeys: false });
|
|
2196
|
+
if (doc.errors.length > 0) return void 0;
|
|
2197
|
+
const root = doc.contents;
|
|
2198
|
+
if (!isMap3(root)) return void 0;
|
|
2199
|
+
const same = join4 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
|
|
2200
|
+
const pair = root.items.find((p) => same(keyString(p.key), options.entry));
|
|
2201
|
+
const held = pair?.value;
|
|
2202
|
+
if (!isMap3(held)) return void 0;
|
|
2203
|
+
const kv = held.items.find((p) => keyString(p.key) === options.key);
|
|
2204
|
+
const node = kv?.value;
|
|
2205
|
+
return isNode2(node) ? node.toJSON() : void 0;
|
|
2206
|
+
}
|
|
2207
|
+
function removeManifestKey(text, options) {
|
|
2208
|
+
const { entry, key, file } = options;
|
|
2209
|
+
const join4 = options.join ?? PATH_JOIN;
|
|
2210
|
+
const at2 = (line) => {
|
|
2211
|
+
const name = file === void 0 ? "Manifest" : `Manifest ${file}`;
|
|
2212
|
+
if (line === void 0) return name;
|
|
2213
|
+
return file === void 0 ? `${name} line ${String(line)}` : `${name}:${String(line)}`;
|
|
2214
|
+
};
|
|
2215
|
+
const eol = detectEol(text);
|
|
2216
|
+
const lc = new LineCounter2();
|
|
2217
|
+
const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
|
|
2218
|
+
const problem = doc.errors[0];
|
|
2219
|
+
if (problem) {
|
|
2220
|
+
throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
|
|
2221
|
+
}
|
|
2222
|
+
const keyLine = (node) => {
|
|
2223
|
+
const r = rangeOf(node);
|
|
2224
|
+
return r ? lc.linePos(r[0]).line : void 0;
|
|
2225
|
+
};
|
|
2226
|
+
const root = doc.contents;
|
|
2227
|
+
if (root === null || isScalar2(root) && root.value === null) return { text };
|
|
2228
|
+
if (!isMap3(root)) {
|
|
2229
|
+
throw new DocmetaError(
|
|
2230
|
+
`${at2()}: the manifest must be a mapping from document ${join4 === PATH_JOIN ? "path" : `"${join4}"`} to owned keys.`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
const same = join4 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
|
|
2234
|
+
const matches = root.items.filter((p) => same(keyString(p.key), entry));
|
|
2235
|
+
const [first, second] = matches;
|
|
2236
|
+
if (first && second) {
|
|
2237
|
+
throw new DocmetaError(
|
|
2238
|
+
`${at2(keyLine(second.key))}: "${keyString(second.key)}" is named twice (first at line ${String(keyLine(first.key) ?? "?")}). Merge the two entries into one.`
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
if (first === void 0) return { text };
|
|
2242
|
+
const spelled = keyString(first.key);
|
|
2243
|
+
const where = at2(keyLine(first.key));
|
|
2244
|
+
const v = first.value;
|
|
2245
|
+
if (!isMap3(v)) {
|
|
2246
|
+
throw new DocmetaError(`${where}: "${spelled}" must be a mapping of owned keys to values.`);
|
|
2247
|
+
}
|
|
2248
|
+
const hits = v.items.filter((kv) => keyString(kv.key) === key);
|
|
2249
|
+
const [hit, again] = hits;
|
|
2250
|
+
if (hit && again) {
|
|
2251
|
+
throw new DocmetaError(
|
|
2252
|
+
`${at2(keyLine(again.key))}: "${spelled}" sets "${key}" twice (first at line ${String(keyLine(hit.key) ?? "?")}). Remove one.`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
if (hit === void 0) return { text };
|
|
2256
|
+
const whole = v.items.length === 1;
|
|
2257
|
+
if (!whole && v.flow === true) {
|
|
2258
|
+
throw new DocmetaError(
|
|
2259
|
+
`${where}: "${spelled}" is a flow mapping ({ \u2026 }), which this writer does not remove keys from. Rewrite the entry in block style.`
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
const from = rangeOf(whole ? first.key : hit.key);
|
|
2263
|
+
const to = whole ? contentEnd(text, v) : contentEnd(text, hit.value) ?? contentEnd(text, hit.key);
|
|
2264
|
+
if (!from || to === void 0) {
|
|
2265
|
+
throw new DocmetaError(
|
|
2266
|
+
`${where}: "${spelled}" writes a key in a form this writer does not edit. Write it as "${key}: \u2026".`
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
let { start, end } = keyLines(text, from[0], to, eol);
|
|
2270
|
+
if (whole) {
|
|
2271
|
+
if (text.slice(start - 2 * eol.length, start) === eol + eol) start -= eol.length;
|
|
2272
|
+
else if (start === 0 && text.startsWith(eol, end)) end += eol.length;
|
|
2273
|
+
}
|
|
2274
|
+
const out = text.slice(0, start) + text.slice(end);
|
|
2275
|
+
const after = parseDocument3(out, { uniqueKeys: false });
|
|
2276
|
+
const fail = (why) => new DocmetaError(
|
|
2277
|
+
`${at2()}: removing "${key}" for "${entry}" did not read back as removed, so nothing was written. ${why}`
|
|
2278
|
+
);
|
|
2279
|
+
if (after.errors[0]) throw fail(`The result is not valid YAML: ${after.errors[0].message}`);
|
|
2280
|
+
const was = doc.toJS({ maxAliasCount: 100 });
|
|
2281
|
+
const wasRoot = isRecord3(was) ? was : {};
|
|
2282
|
+
const wasEntry = wasRoot[spelled];
|
|
2283
|
+
const rest = Object.fromEntries(
|
|
2284
|
+
Object.entries(isRecord3(wasEntry) ? wasEntry : {}).filter(([k]) => k !== key)
|
|
2285
|
+
);
|
|
2286
|
+
const kept = Object.entries(wasRoot).filter(([k]) => k !== spelled);
|
|
2287
|
+
if (Object.keys(rest).length > 0) kept.push([spelled, rest]);
|
|
2288
|
+
const now = after.toJS({ maxAliasCount: 100 });
|
|
2289
|
+
const expected = kept.length === 0 ? null : Object.fromEntries(kept);
|
|
2290
|
+
if (!deepEqual(now ?? null, expected)) throw fail("Another value in the manifest would have changed.");
|
|
2291
|
+
return { text: out };
|
|
2292
|
+
}
|
|
2293
|
+
function renameManifestEntry(text, options) {
|
|
2294
|
+
const { entry, to, file } = options;
|
|
2295
|
+
const join4 = options.join ?? PATH_JOIN;
|
|
2296
|
+
const at2 = (line) => {
|
|
2297
|
+
const name = file === void 0 ? "Manifest" : `Manifest ${file}`;
|
|
2298
|
+
if (line === void 0) return name;
|
|
2299
|
+
return file === void 0 ? `${name} line ${String(line)}` : `${name}:${String(line)}`;
|
|
2300
|
+
};
|
|
2301
|
+
const lc = new LineCounter2();
|
|
2302
|
+
const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
|
|
2303
|
+
const problem = doc.errors[0];
|
|
2304
|
+
if (problem) {
|
|
2305
|
+
throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
|
|
2306
|
+
}
|
|
2307
|
+
const keyLine = (node) => {
|
|
2308
|
+
const r2 = rangeOf(node);
|
|
2309
|
+
return r2 ? lc.linePos(r2[0]).line : void 0;
|
|
2310
|
+
};
|
|
2311
|
+
const root = doc.contents;
|
|
2312
|
+
if (root === null || isScalar2(root) && root.value === null) return { text };
|
|
2313
|
+
if (!isMap3(root)) {
|
|
2314
|
+
throw new DocmetaError(
|
|
2315
|
+
`${at2()}: the manifest must be a mapping from document ${join4 === PATH_JOIN ? "path" : `"${join4}"`} to owned keys.`
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
const same = join4 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
|
|
2319
|
+
const [first, second] = root.items.filter((p) => same(keyString(p.key), entry));
|
|
2320
|
+
if (first && second) {
|
|
2321
|
+
throw new DocmetaError(
|
|
2322
|
+
`${at2(keyLine(second.key))}: "${keyString(second.key)}" is named twice (first at line ${String(keyLine(first.key) ?? "?")}). Merge the two entries into one.`
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
if (first === void 0) return { text };
|
|
2326
|
+
const taken = root.items.find((p) => same(keyString(p.key), to));
|
|
2327
|
+
if (taken !== void 0) {
|
|
2328
|
+
throw new DocmetaError(
|
|
2329
|
+
`${at2(keyLine(taken.key))}: "${keyString(taken.key)}" already has an entry, so "${keyString(first.key)}" cannot be renamed to it. Merge the two entries into one.`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
const spelled = keyString(first.key);
|
|
2333
|
+
const r = rangeOf(first.key);
|
|
2334
|
+
const end = contentEnd(text, first.key);
|
|
2335
|
+
if (!r || end === void 0 || colonEnd(text, first) === void 0) {
|
|
2336
|
+
throw new DocmetaError(
|
|
2337
|
+
`${at2(keyLine(first.key))}: "${spelled}" is written in a form this writer does not edit. Write it as "${spelled}: \u2026".`
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
const quoted = isScalar2(first.key) && (first.key.type === "QUOTE_DOUBLE" || first.key.type === "QUOTE_SINGLE");
|
|
2341
|
+
const plain = stringify2(to, { lineWidth: 0 }).replace(/\n$/, "");
|
|
2342
|
+
const insert = quoted || plain.includes("\n") ? JSON.stringify(to) : plain;
|
|
2343
|
+
const out = text.slice(0, r[0]) + insert + text.slice(end);
|
|
2344
|
+
const after = parseDocument3(out, { uniqueKeys: false });
|
|
2345
|
+
const fail = (why) => new DocmetaError(
|
|
2346
|
+
`${at2()}: renaming "${spelled}" to "${to}" did not read back as renamed, so nothing was written. ${why}`
|
|
2347
|
+
);
|
|
2348
|
+
if (after.errors[0]) throw fail(`The result is not valid YAML: ${after.errors[0].message}`);
|
|
2349
|
+
const was = doc.toJS({ maxAliasCount: 100 });
|
|
2350
|
+
const wasRoot = isRecord3(was) ? was : {};
|
|
2351
|
+
const expected = Object.fromEntries(
|
|
2352
|
+
Object.entries(wasRoot).map(([k, v]) => [k === spelled ? to : k, v])
|
|
2353
|
+
);
|
|
2354
|
+
const now = after.toJS({ maxAliasCount: 100 });
|
|
2355
|
+
if (!deepEqual(now, expected)) throw fail("Another value in the manifest would have changed.");
|
|
2356
|
+
return { text: out };
|
|
2357
|
+
}
|
|
2358
|
+
function startManifest(text, entry, key, value, eol) {
|
|
2359
|
+
const style = { step: 2, indentSeq: true, eol };
|
|
2360
|
+
const block = blockLines({ [entry]: { [key]: value } }, 0, style);
|
|
2361
|
+
let insert;
|
|
2362
|
+
if (text === "" || text.endsWith("\n")) insert = block + eol;
|
|
2363
|
+
else insert = eol + block;
|
|
2364
|
+
return { start: text.length, end: text.length, insert };
|
|
2365
|
+
}
|
|
2366
|
+
function appendEntry(text, root, entry, key, value, style) {
|
|
2367
|
+
const body = { [entry]: { [key]: value } };
|
|
2368
|
+
if (root.flow === true) {
|
|
2369
|
+
const r = rangeOf(root);
|
|
2370
|
+
const start = r ? r[0] : text.length;
|
|
2371
|
+
const end = contentEnd(text, root) ?? start;
|
|
2372
|
+
return { start, end, insert: blockLines(body, colOf(text, start), style) };
|
|
2373
|
+
}
|
|
2374
|
+
const firstKey = rangeOf(root.items[0]?.key);
|
|
2375
|
+
const col = firstKey ? colOf(text, firstKey[0]) : 0;
|
|
2376
|
+
const pos = lineEnd(text, contentEnd(text, root) ?? text.length);
|
|
2377
|
+
const last = root.items.at(-1);
|
|
2378
|
+
const spaced = root.items.length > 1 && isNode2(last?.key) && last.key.spaceBefore === true;
|
|
2379
|
+
return {
|
|
2380
|
+
start: pos,
|
|
2381
|
+
end: pos,
|
|
2382
|
+
insert: style.eol + (spaced ? style.eol : "") + blockLines(body, col, style)
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function fillEmptyEntry(text, entryPair, key, value, style) {
|
|
2386
|
+
const start = colonEnd(text, entryPair);
|
|
2387
|
+
const keyRange = rangeOf(entryPair.key);
|
|
2388
|
+
const end = contentEnd(text, entryPair.value);
|
|
2389
|
+
if (start === void 0 || !keyRange || end === void 0) return void 0;
|
|
2390
|
+
const col = colOf(text, keyRange[0]) + style.step;
|
|
2391
|
+
return { start, end, insert: style.eol + blockLines({ [key]: value }, col, style) };
|
|
2392
|
+
}
|
|
2393
|
+
function readBack(before, out, spelled, key, value, fail) {
|
|
2394
|
+
const lc = new LineCounter2();
|
|
2395
|
+
const after = parseDocument3(out, { lineCounter: lc, uniqueKeys: false });
|
|
2396
|
+
const problem = after.errors[0];
|
|
2397
|
+
if (problem) throw fail(`The result is not valid YAML: ${problem.message}`);
|
|
2398
|
+
const was = before.toJS({ maxAliasCount: 100 });
|
|
2399
|
+
const now = after.toJS({ maxAliasCount: 100 });
|
|
2400
|
+
const wasRoot = isRecord3(was) ? was : {};
|
|
2401
|
+
const wasEntry = wasRoot[spelled];
|
|
2402
|
+
const expected = {
|
|
2403
|
+
...wasRoot,
|
|
2404
|
+
[spelled]: { ...isRecord3(wasEntry) ? wasEntry : {}, [key]: value }
|
|
2405
|
+
};
|
|
2406
|
+
const nowEntry = isRecord3(now) ? now[spelled] : void 0;
|
|
2407
|
+
if (!isRecord3(nowEntry) || !deepEqual(nowEntry[key], value)) {
|
|
2408
|
+
throw fail(
|
|
2409
|
+
"The value reads back differently. A Date, an undefined member, or another value YAML cannot hold does not survive the trip."
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
if (!deepEqual(now, expected)) {
|
|
2413
|
+
throw fail("Another value in the manifest would have changed.");
|
|
2414
|
+
}
|
|
2415
|
+
const root = after.contents;
|
|
2416
|
+
const pair = isMap3(root) ? root.items.find((p) => keyString(p.key) === spelled) : void 0;
|
|
2417
|
+
const entryMap = pair?.value;
|
|
2418
|
+
const kv = isMap3(entryMap) ? entryMap.items.find((p) => keyString(p.key) === key) : void 0;
|
|
2419
|
+
const r = rangeOf(kv?.value) ?? rangeOf(kv?.key);
|
|
2420
|
+
if (!r) throw fail("The written value could not be found again.");
|
|
2421
|
+
return lc.linePos(r[0]).line;
|
|
2422
|
+
}
|
|
2423
|
+
function isRecord3(x) {
|
|
2424
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/meta/core/relocation.ts
|
|
2428
|
+
function relocateFailed(result) {
|
|
2429
|
+
return result.summary.stayed > 0;
|
|
2430
|
+
}
|
|
2431
|
+
function relocationContext(run, extras) {
|
|
2432
|
+
return {
|
|
2433
|
+
cwd: extras.cwd,
|
|
2434
|
+
base: run.base,
|
|
2435
|
+
noConfig: extras.noConfig ?? false,
|
|
2436
|
+
config: run.config,
|
|
2437
|
+
...run.configDir !== void 0 ? { configDir: run.configDir } : {},
|
|
2438
|
+
...run.configPath !== void 0 ? { configPath: run.configPath } : {},
|
|
2439
|
+
...run.configFile !== void 0 ? { configFile: run.configFile } : {},
|
|
2440
|
+
collections: run.collections,
|
|
2441
|
+
declaredCollections: run.declaredCollections,
|
|
2442
|
+
targets: extras.targets.filter((t) => t !== STDIN_TOKEN),
|
|
2443
|
+
...extras.cliSchemas !== void 0 ? { cliSchemas: extras.cliSchemas } : {},
|
|
2444
|
+
validator: extras.validator,
|
|
2445
|
+
...extras.as !== void 0 ? { as: extras.as } : {},
|
|
2446
|
+
...extras.respectGitignore !== void 0 ? { respectGitignore: extras.respectGitignore } : {},
|
|
2447
|
+
...extras.env !== void 0 ? { env: extras.env } : {},
|
|
2448
|
+
...extras.onNotice !== void 0 ? { onNotice: extras.onNotice } : {}
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
var toPosix = (p) => p.split(sep3).join("/");
|
|
2452
|
+
function reported2(abs, base) {
|
|
2453
|
+
const rel = relative4(base, abs);
|
|
2454
|
+
return rel === "" ? "." : toPosix(rel);
|
|
2455
|
+
}
|
|
2456
|
+
function configSource(path, cwd) {
|
|
2457
|
+
return toPosix(relative4(cwd, path)) || basename3(path);
|
|
2458
|
+
}
|
|
2459
|
+
function cloneCollection(c) {
|
|
2460
|
+
return {
|
|
2461
|
+
...c,
|
|
2462
|
+
paths: [...c.paths],
|
|
2463
|
+
exclude: [...c.exclude],
|
|
2464
|
+
externalMetadata: c.externalMetadata.map((m) => m.keys === void 0 ? { ...m } : { ...m, keys: [...m.keys] })
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
var Model = class {
|
|
2468
|
+
constructor(ctx, configDir) {
|
|
2469
|
+
this.ctx = ctx;
|
|
2470
|
+
this.configDir = configDir;
|
|
2471
|
+
this.collections = ctx.declaredCollections.map(cloneCollection);
|
|
2472
|
+
for (const c of this.collections) {
|
|
2473
|
+
c.externalMetadata.forEach((m, index) => {
|
|
2474
|
+
const url = classifyRef(m.file).kind === "url";
|
|
2475
|
+
const perPage = !url && hasPagePlaceholder(m.file);
|
|
2476
|
+
const absPath = url || perPage ? void 0 : isAbsolute3(m.file) ? m.file : resolve3(configDir, m.file);
|
|
2477
|
+
this.manifests.push({
|
|
2478
|
+
collection: c.name,
|
|
2479
|
+
index,
|
|
2480
|
+
written: m.file,
|
|
2481
|
+
url,
|
|
2482
|
+
absPath,
|
|
2483
|
+
file: absPath === void 0 ? m.file : reported2(absPath, ctx.base),
|
|
2484
|
+
join: externalMetadataJoin(m),
|
|
2485
|
+
perPage,
|
|
2486
|
+
created: false,
|
|
2487
|
+
implied: m.keys === void 0,
|
|
2488
|
+
keysBefore: [...m.keys ?? []],
|
|
2489
|
+
keysAdded: [],
|
|
2490
|
+
keysRemoved: []
|
|
2491
|
+
});
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
ctx;
|
|
2496
|
+
configDir;
|
|
2497
|
+
collections;
|
|
2498
|
+
manifests = [];
|
|
2499
|
+
created = [];
|
|
2500
|
+
pathsAdded = [];
|
|
2501
|
+
targets;
|
|
2502
|
+
members(label) {
|
|
2503
|
+
return memberOf(this.collections, this.configDir, this.ctx.base, label);
|
|
2504
|
+
}
|
|
2505
|
+
/** The run's targets as `paths:` entries, in target order; resolved once, on first use. */
|
|
2506
|
+
targetPaths() {
|
|
2507
|
+
this.targets ??= Promise.all(this.ctx.targets.map((t) => collectionPath(t, this.ctx.cwd, this.configDir)));
|
|
2508
|
+
return this.targets;
|
|
2509
|
+
}
|
|
2510
|
+
manifestsOf(collection) {
|
|
2511
|
+
return this.manifests.filter((m) => m.collection === collection);
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* The one file `m` keeps a page's values in, and how the run reports it:
|
|
2515
|
+
* the page's own for a `{page}` manifest (proposal 0058), the declared file
|
|
2516
|
+
* for any other. `abs` is undefined only for a URL manifest.
|
|
2517
|
+
*/
|
|
2518
|
+
fileFor(m, pageAbs) {
|
|
2519
|
+
if (!m.perPage) return { abs: m.absPath, file: m.file };
|
|
2520
|
+
const resolved = pageManifestPath(m.written, this.configDir, pageAbs);
|
|
2521
|
+
if ("outside" in resolved) {
|
|
2522
|
+
const declared = this.ctx.declaredCollections.find((c) => c.name === m.collection)?.externalMetadata[m.index];
|
|
2523
|
+
throw new DocmetaError(
|
|
2524
|
+
outsideRefusal(declared ?? { file: m.written }, m.collection, resolved.pageRel)
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
return { abs: resolved.abs, file: reported2(resolved.abs, this.ctx.base) };
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* The manifest of one of `members` that owns `key`, in declaration order.
|
|
2531
|
+
* `marked` is what the page's schemas mark external, which is what a
|
|
2532
|
+
* manifest with no `keys` owns (proposal 0068).
|
|
2533
|
+
*/
|
|
2534
|
+
ownerOf(key, members, marked) {
|
|
2535
|
+
return this.manifests.find(
|
|
2536
|
+
(m) => members.includes(m.collection) && (m.implied ? this.impliedOwns(m, key, marked) : m.keysBefore.includes(key) || m.keysAdded.includes(key))
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
/** Does a keyless manifest own `key` for a page whose schemas mark `marked`? */
|
|
2540
|
+
impliedOwns(m, key, marked) {
|
|
2541
|
+
const collection = this.collections.find((c) => c.name === m.collection);
|
|
2542
|
+
const declared = collection?.externalMetadata[m.index];
|
|
2543
|
+
return collection !== void 0 && declared !== void 0 && ownsKey(collection, declared, key, marked);
|
|
2544
|
+
}
|
|
2545
|
+
/** Does one of `members` declare a manifest with no `keys`? */
|
|
2546
|
+
hasImplied(members) {
|
|
2547
|
+
return this.manifests.some((m) => m.implied && members.includes(m.collection));
|
|
2548
|
+
}
|
|
2549
|
+
/** The page's collection to home a new key in, first by the run's selection. */
|
|
2550
|
+
pick(members) {
|
|
2551
|
+
const selected = new Set(this.ctx.collections.map((c) => c.name));
|
|
2552
|
+
return members.find((m) => selected.has(m)) ?? members[0];
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* §3's four cases. `commit` makes the choice part of the model (a created
|
|
2556
|
+
* collection, an appended path, a new manifest), which planning wants and a
|
|
2557
|
+
* lookup does not.
|
|
2558
|
+
*/
|
|
2559
|
+
async home(label, commit, owns = () => false) {
|
|
2560
|
+
if (this.ctx.noConfig) {
|
|
2561
|
+
return { proposed: { kind: "none", reason: "no-config", collections: this.collections.length } };
|
|
2562
|
+
}
|
|
2563
|
+
const members = this.members(label);
|
|
2564
|
+
let collection = this.pick(members);
|
|
2565
|
+
let createsCollection = false;
|
|
2566
|
+
let addsPath;
|
|
2567
|
+
if (collection === void 0) {
|
|
2568
|
+
const rel = this.relative(label);
|
|
2569
|
+
if (this.collections.length === 0) {
|
|
2570
|
+
const paths = dedupe(
|
|
2571
|
+
(await this.targetPaths()).flatMap((p) => p === void 0 ? [] : [p])
|
|
2572
|
+
);
|
|
2573
|
+
if (rel === void 0 || !paths.some((p) => isMember(bare(p), rel))) {
|
|
2574
|
+
return { proposed: { kind: "none", reason: "collections", collections: 0 } };
|
|
2575
|
+
}
|
|
2576
|
+
collection = "default";
|
|
2577
|
+
createsCollection = true;
|
|
2578
|
+
if (commit) {
|
|
2579
|
+
this.collections.push({ name: collection, paths, exclude: [], externalMetadata: [] });
|
|
2580
|
+
this.created.push({ name: collection, paths: [...paths] });
|
|
2581
|
+
}
|
|
2582
|
+
} else if (this.collections.length === 1 && this.collections[0] !== void 0) {
|
|
2583
|
+
const only = this.collections[0];
|
|
2584
|
+
const target = (await this.targetPaths()).find(
|
|
2585
|
+
(p) => p !== void 0 && rel !== void 0 && isMember(bare(p), rel)
|
|
2586
|
+
);
|
|
2587
|
+
if (target === void 0) {
|
|
2588
|
+
return { proposed: { kind: "none", reason: "collections", collections: 1 } };
|
|
2589
|
+
}
|
|
2590
|
+
collection = only.name;
|
|
2591
|
+
addsPath = target;
|
|
2592
|
+
if (commit) {
|
|
2593
|
+
only.paths.push(target);
|
|
2594
|
+
this.pathsAdded.push({ collection: only.name, path: target });
|
|
2595
|
+
}
|
|
2596
|
+
} else {
|
|
2597
|
+
return { proposed: { kind: "none", reason: "collections", collections: this.collections.length } };
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const name = collection;
|
|
2601
|
+
const local = this.manifestsOf(name).find((m) => !m.url && (!m.implied || owns(m)));
|
|
2602
|
+
const proposedOf = (m) => ({
|
|
2603
|
+
kind: "collection",
|
|
2604
|
+
collection: name,
|
|
2605
|
+
manifest: m.file,
|
|
2606
|
+
createsManifest: m.created,
|
|
2607
|
+
createsCollection,
|
|
2608
|
+
...addsPath !== void 0 ? { addsPath } : {}
|
|
2609
|
+
});
|
|
2610
|
+
if (local !== void 0) return { manifest: local, proposed: proposedOf(local) };
|
|
2611
|
+
const fileName = `${name}.metadata.yaml`;
|
|
2612
|
+
const absPath = join3(this.configDir, fileName);
|
|
2613
|
+
const file = reported2(absPath, this.ctx.base);
|
|
2614
|
+
if (!commit) return { proposed: proposedOf({ file, created: true }) };
|
|
2615
|
+
if (await exists(absPath, file)) {
|
|
2616
|
+
throw new DocmetaError(
|
|
2617
|
+
`${file} already exists and is not a manifest of collection ${name}; declare it under externalMetadata, or move it.`
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
const owner = this.collections.find((c) => c.name === name);
|
|
2621
|
+
const ref = {
|
|
2622
|
+
collection: name,
|
|
2623
|
+
index: owner?.externalMetadata.length ?? 0,
|
|
2624
|
+
written: `./${fileName}`,
|
|
2625
|
+
url: false,
|
|
2626
|
+
absPath,
|
|
2627
|
+
file,
|
|
2628
|
+
join: PATH_JOIN,
|
|
2629
|
+
perPage: false,
|
|
2630
|
+
created: true,
|
|
2631
|
+
implied: false,
|
|
2632
|
+
keysBefore: [],
|
|
2633
|
+
keysAdded: [],
|
|
2634
|
+
keysRemoved: []
|
|
2635
|
+
};
|
|
2636
|
+
owner?.externalMetadata.push({ file: ref.written, keys: [] });
|
|
2637
|
+
this.manifests.push(ref);
|
|
2638
|
+
return { manifest: ref, proposed: proposedOf(ref) };
|
|
2639
|
+
}
|
|
2640
|
+
addKey(m, key) {
|
|
2641
|
+
if (m.implied || m.keysBefore.includes(key) || m.keysAdded.includes(key)) return;
|
|
2642
|
+
m.keysAdded.push(key);
|
|
2643
|
+
this.collections.find((c) => c.name === m.collection)?.externalMetadata[m.index]?.keys?.push(key);
|
|
2644
|
+
}
|
|
2645
|
+
removeKey(m, key) {
|
|
2646
|
+
if (!m.keysBefore.includes(key) || m.keysRemoved.includes(key)) return;
|
|
2647
|
+
m.keysRemoved.push(key);
|
|
2648
|
+
}
|
|
2649
|
+
/** The keys a manifest owns once the plan is applied. */
|
|
2650
|
+
finalKeys(m) {
|
|
2651
|
+
return [...m.keysBefore.filter((k) => !m.keysRemoved.includes(k)), ...m.keysAdded];
|
|
2652
|
+
}
|
|
2653
|
+
/** The collections as the config will declare them: removals applied, emptied manifests gone. */
|
|
2654
|
+
final() {
|
|
2655
|
+
return this.collections.map((c) => ({
|
|
2656
|
+
...c,
|
|
2657
|
+
externalMetadata: c.externalMetadata.flatMap((em, index) => {
|
|
2658
|
+
const ref = this.manifests.find((m) => m.collection === c.name && m.index === index);
|
|
2659
|
+
if (ref?.implied === true || ref === void 0 && em.keys === void 0) return [em];
|
|
2660
|
+
const keys = ref === void 0 ? em.keys ?? [] : this.finalKeys(ref);
|
|
2661
|
+
return keys.length === 0 ? [] : [{ ...em, keys }];
|
|
2662
|
+
})
|
|
2663
|
+
}));
|
|
2664
|
+
}
|
|
2665
|
+
/** A label relative to the config directory, posix; undefined outside it. */
|
|
2666
|
+
relative(label) {
|
|
2667
|
+
if (label === "<stdin>") return void 0;
|
|
2668
|
+
const rel = toPosix(relative4(this.configDir, resolve3(this.ctx.base, label)));
|
|
2669
|
+
return isAbsolute3(rel) || rel.startsWith("../") ? void 0 : rel;
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
function dedupe(xs) {
|
|
2673
|
+
return [...new Set(xs)];
|
|
2674
|
+
}
|
|
2675
|
+
function bare(path) {
|
|
2676
|
+
return { name: "", paths: [path], exclude: [], externalMetadata: [] };
|
|
2677
|
+
}
|
|
2678
|
+
async function collectionPath(target, cwd, configDir) {
|
|
2679
|
+
if (target === STDIN_TOKEN) return void 0;
|
|
2680
|
+
const posix4 = target.replace(/\\/g, "/");
|
|
2681
|
+
const scanned = picomatch.scan(posix4);
|
|
2682
|
+
if (scanned.isGlob) {
|
|
2683
|
+
const rel2 = toPosix(relative4(configDir, resolve3(cwd, scanned.base === "" ? "." : scanned.base)));
|
|
2684
|
+
if (isAbsolute3(rel2) || rel2 === ".." || rel2.startsWith("../")) return void 0;
|
|
2685
|
+
return rel2 === "" ? scanned.glob : `${rel2}/${scanned.glob}`;
|
|
2686
|
+
}
|
|
2687
|
+
const abs = resolve3(cwd, posix4);
|
|
2688
|
+
const rel = toPosix(relative4(configDir, abs));
|
|
2689
|
+
if (isAbsolute3(rel) || rel.startsWith("..")) return void 0;
|
|
2690
|
+
let directory = posix4.endsWith("/");
|
|
2691
|
+
if (!directory) {
|
|
2692
|
+
try {
|
|
2693
|
+
directory = (await stat2(abs)).isDirectory();
|
|
2694
|
+
} catch {
|
|
2695
|
+
directory = false;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (!directory) return rel;
|
|
2699
|
+
return rel === "" ? "**" : `${rel}/**`;
|
|
2700
|
+
}
|
|
2701
|
+
async function exists(absPath, file) {
|
|
2702
|
+
try {
|
|
2703
|
+
await access(absPath);
|
|
2704
|
+
return true;
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
if (err.code === "ENOENT") return false;
|
|
2707
|
+
throw new DocmetaError(`${file} could not be checked: ${errorMessage(err)}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
function plannedConfigDir(ctx) {
|
|
2711
|
+
return ctx.configDir ?? findGitRoot(ctx.cwd) ?? resolve3(ctx.cwd);
|
|
2712
|
+
}
|
|
2713
|
+
async function keyHome(ctx, label, data, key) {
|
|
2714
|
+
const model = new Model(ctx, plannedConfigDir(ctx));
|
|
2715
|
+
const members = model.members(label);
|
|
2716
|
+
const marked = model.hasImplied(members) ? await marksOf(ctx)(
|
|
2717
|
+
label,
|
|
2718
|
+
Object.hasOwn(data, key) ? data : { ...data, [key]: null },
|
|
2719
|
+
memberOf(ctx.declaredCollections, ctx.configDir ?? ctx.cwd, ctx.base, label)
|
|
2720
|
+
) : void 0;
|
|
2721
|
+
const owner = model.ownerOf(key, members, marked);
|
|
2722
|
+
if (owner !== void 0) {
|
|
2723
|
+
const at2 = model.fileFor(owner, resolve3(ctx.base, label));
|
|
2724
|
+
if (owner.url || at2.abs === void 0) {
|
|
2725
|
+
return { kind: "url", collection: owner.collection, file: owner.file };
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
kind: "manifest",
|
|
2729
|
+
collection: owner.collection,
|
|
2730
|
+
file: at2.file,
|
|
2731
|
+
absPath: at2.abs,
|
|
2732
|
+
join: owner.join,
|
|
2733
|
+
entry: entryFor(owner, label, data, model.configDir, ctx.base, lazyKey(ctx.configFile, ctx.env)),
|
|
2734
|
+
perPage: owner.perPage
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
return { kind: "unowned", home: (await model.home(label, false)).proposed };
|
|
2738
|
+
}
|
|
2739
|
+
function marksOf(ctx) {
|
|
2740
|
+
return pageMarks({
|
|
2741
|
+
validator: ctx.validator,
|
|
2742
|
+
config: ctx.config,
|
|
2743
|
+
cwd: ctx.cwd,
|
|
2744
|
+
trustRoot: schemaTrustRoot(ctx.cwd, ctx.configDir),
|
|
2745
|
+
...ctx.cliSchemas !== void 0 ? { cliSchemas: ctx.cliSchemas } : {}
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
function entryFor(m, label, data, configDir, base, key) {
|
|
2749
|
+
if (m.join === PATH_JOIN) return toPosix(relative4(configDir, resolve3(base, label)));
|
|
2750
|
+
let raw = data[m.join];
|
|
2751
|
+
if (isEncryptedValue(raw)) {
|
|
2752
|
+
const k = key();
|
|
2753
|
+
if (k === void 0) {
|
|
2754
|
+
throw new EncryptionRefusal(
|
|
2755
|
+
`externalMetadata join field "${m.join}" is encrypted on these pages, and no encryption key is available to match them.`
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
const opened = decryptValue(raw, k, META_CONTEXT);
|
|
2759
|
+
if (!opened.ok) return void 0;
|
|
2760
|
+
raw = opened.value;
|
|
2761
|
+
}
|
|
2762
|
+
if (typeof raw === "string") return raw;
|
|
2763
|
+
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
2764
|
+
return void 0;
|
|
2765
|
+
}
|
|
2766
|
+
async function relocationPages(ctx, request) {
|
|
2767
|
+
const pages = new Set(request.files.map((label) => resolve3(ctx.base, label)));
|
|
2768
|
+
const root = ctx.configDir ?? ctx.cwd;
|
|
2769
|
+
for (const collection of ctx.declaredCollections) {
|
|
2770
|
+
const perPage = collection.externalMetadata.some(
|
|
2771
|
+
(m) => classifyRef(m.file).kind !== "url" && hasPagePlaceholder(m.file)
|
|
2772
|
+
);
|
|
2773
|
+
if (!perPage) continue;
|
|
2774
|
+
const walked = await resolveTargetSet({
|
|
2775
|
+
inputs: [...collection.paths],
|
|
2776
|
+
cwd: root,
|
|
2777
|
+
allowEmpty: true,
|
|
2778
|
+
...gitignoreOptions({
|
|
2779
|
+
...ctx.respectGitignore !== void 0 ? { flag: ctx.respectGitignore } : {},
|
|
2780
|
+
...ctx.config?.respectGitignore !== void 0 ? { configured: ctx.config.respectGitignore } : {}
|
|
2781
|
+
})
|
|
2782
|
+
});
|
|
2783
|
+
for (const file of walked.files) pages.add(resolve3(root, file));
|
|
2784
|
+
}
|
|
2785
|
+
return [...pages];
|
|
2786
|
+
}
|
|
2787
|
+
async function planRelocation(ctx, request) {
|
|
2788
|
+
const configDir = plannedConfigDir(ctx);
|
|
2789
|
+
const model = new Model(ctx, configDir);
|
|
2790
|
+
const encryptionKey = lazyKey(ctx.configFile, ctx.env);
|
|
2791
|
+
const marks = marksOf(ctx);
|
|
2792
|
+
const index = await loadExternalMetadata(ctx.declaredCollections, {
|
|
2793
|
+
configDir: ctx.configDir ?? ctx.cwd,
|
|
2794
|
+
base: ctx.base,
|
|
2795
|
+
offline: ctx.config?.offline ?? false,
|
|
2796
|
+
pages: await relocationPages(ctx, request)
|
|
2797
|
+
});
|
|
2798
|
+
const trustRoot = schemaTrustRoot(ctx.cwd, ctx.configDir);
|
|
2799
|
+
const forced = ctx.as !== void 0 ? extractorByName(ctx.as) : void 0;
|
|
2800
|
+
const fields = request.fields === void 0 ? void 0 : new Set(request.fields);
|
|
2801
|
+
const wanted = (key) => key !== FILE_SCHEMA_KEY && (fields === void 0 || fields.has(key));
|
|
2802
|
+
const currentMembers = (label) => memberOf(ctx.declaredCollections, ctx.configDir ?? ctx.cwd, ctx.base, label);
|
|
2803
|
+
const pages = /* @__PURE__ */ new Map();
|
|
2804
|
+
const runSet = new Set(request.files);
|
|
2805
|
+
const load = async (label, beyond) => {
|
|
2806
|
+
const have = pages.get(label);
|
|
2807
|
+
if (have !== void 0) return have;
|
|
2808
|
+
const extractor = (beyond ? void 0 : forced) ?? extractorForExtension(extname(label));
|
|
2809
|
+
if (extractor === void 0) {
|
|
2810
|
+
if (beyond) return void 0;
|
|
2811
|
+
throw new DocmetaError(`Unsupported file type "${extname(label)}" for "${label}". Use --as to override.`);
|
|
2812
|
+
}
|
|
2813
|
+
const absPath = resolve3(ctx.base, label);
|
|
2814
|
+
const elements = resolveElements(label, ctx.config, currentMembers(label));
|
|
2815
|
+
let content = "";
|
|
2816
|
+
let own = {};
|
|
2817
|
+
let unreadable2;
|
|
2818
|
+
if (beyond) {
|
|
2819
|
+
try {
|
|
2820
|
+
content = await readFile4(absPath, "utf8");
|
|
2821
|
+
own = extractor.extract(content, label, { elements }).data;
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
unreadable2 = (errorMessage(err).split("\n")[0] ?? "").replace(/:\s*$/, "");
|
|
2824
|
+
}
|
|
2825
|
+
} else {
|
|
2826
|
+
content = await readFile4(absPath, "utf8");
|
|
2827
|
+
try {
|
|
2828
|
+
own = extractor.extract(content, label, { elements }).data;
|
|
2829
|
+
} catch (err) {
|
|
2830
|
+
throw new DocmetaError(`${label}: ${errorMessage(err)}`);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
const page = {
|
|
2834
|
+
label,
|
|
2835
|
+
absPath,
|
|
2836
|
+
format: extractor.name,
|
|
2837
|
+
content,
|
|
2838
|
+
elements,
|
|
2839
|
+
extractor,
|
|
2840
|
+
own,
|
|
2841
|
+
beyond,
|
|
2842
|
+
...unreadable2 !== void 0 ? { unreadable: unreadable2 } : {},
|
|
2843
|
+
intents: /* @__PURE__ */ new Map()
|
|
2844
|
+
};
|
|
2845
|
+
pages.set(label, page);
|
|
2846
|
+
return page;
|
|
2847
|
+
};
|
|
2848
|
+
const supplied = (m, page, key) => {
|
|
2849
|
+
if (m.created || index === null) return void 0;
|
|
2850
|
+
let sv;
|
|
2851
|
+
if (m.join === PATH_JOIN) {
|
|
2852
|
+
sv = index.byPath.get(page.absPath)?.get(key);
|
|
2853
|
+
} else {
|
|
2854
|
+
const entry = entryFor(m, page.label, page.own, configDir, ctx.base, encryptionKey);
|
|
2855
|
+
sv = entry === void 0 ? void 0 : index.byField.get(m.join)?.get(entry)?.get(key);
|
|
2856
|
+
}
|
|
2857
|
+
return sv !== void 0 && sv.collection === m.collection && sv.file === model.fileFor(m, page.absPath).file ? sv : void 0;
|
|
2858
|
+
};
|
|
2859
|
+
const stay = (page, key, reason, detail, links = {}) => {
|
|
2860
|
+
page.intents.set(key, { kind: "stay", reason, detail, ...links });
|
|
2861
|
+
};
|
|
2862
|
+
const urlDetail = (m) => `${m.file} is fetched and cannot be written`;
|
|
2863
|
+
const unreadableDetail = (page) => `this document could not be parsed: ${page.unreadable ?? ""}`;
|
|
2864
|
+
const planOut = (page, m, key, reason) => {
|
|
2865
|
+
if (page.intents.has(key)) return;
|
|
2866
|
+
if (m.url) {
|
|
2867
|
+
stay(page, key, "url-manifest", urlDetail(m), { toward: m });
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const entry = entryFor(m, page.label, page.own, configDir, ctx.base, encryptionKey);
|
|
2871
|
+
if (entry === void 0) {
|
|
2872
|
+
stay(page, key, "no-join-value", `this document has no ${m.join}, which ${m.file} joins on`, { toward: m });
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const held = supplied(m, page, key);
|
|
2876
|
+
if (held !== void 0 && !deepEqual(held.value, page.own[key])) {
|
|
2877
|
+
stay(page, key, "values-differ", `the page and ${held.file} hold different values`, { toward: m });
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
page.intents.set(key, { kind: "out", manifest: m, reason, drop: held !== void 0, entry });
|
|
2881
|
+
};
|
|
2882
|
+
const planIn = (page, m, key) => {
|
|
2883
|
+
if (page.intents.has(key)) return;
|
|
2884
|
+
const held = supplied(m, page, key);
|
|
2885
|
+
if (held === void 0) return;
|
|
2886
|
+
const entry = entryFor(m, page.label, page.own, configDir, ctx.base, encryptionKey);
|
|
2887
|
+
if (entry === void 0) return;
|
|
2888
|
+
if (Object.hasOwn(page.own, key)) {
|
|
2889
|
+
if (!deepEqual(held.value, page.own[key])) {
|
|
2890
|
+
stay(page, key, "values-differ", `the page and ${held.file} hold different values`, { blocks: m });
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
page.intents.set(key, { kind: "in", manifest: m, drop: true, entry, value: held.value });
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
page.intents.set(key, { kind: "in", manifest: m, drop: false, entry, value: held.value });
|
|
2897
|
+
};
|
|
2898
|
+
const preferencesOf = /* @__PURE__ */ new Map();
|
|
2899
|
+
const homeless = /* @__PURE__ */ new Map();
|
|
2900
|
+
const marked = /* @__PURE__ */ new Set();
|
|
2901
|
+
for (const label of request.files) {
|
|
2902
|
+
const page = await load(label, false);
|
|
2903
|
+
if (page === void 0) continue;
|
|
2904
|
+
const members = currentMembers(label);
|
|
2905
|
+
const merged = await mergeWithMarks(
|
|
2906
|
+
label,
|
|
2907
|
+
{ data: page.own, present: true, format: page.format, lineFor: () => void 0 },
|
|
2908
|
+
index,
|
|
2909
|
+
members,
|
|
2910
|
+
ctx.base,
|
|
2911
|
+
{ encryptionKey, marks }
|
|
2912
|
+
);
|
|
2913
|
+
const data = merged.extracted.data;
|
|
2914
|
+
const required = request.require?.get(label) ?? [];
|
|
2915
|
+
const resolved = resolveSchemaSetWithSource({
|
|
2916
|
+
filePath: label,
|
|
2917
|
+
fileSchema: data[FILE_SCHEMA_KEY],
|
|
2918
|
+
...ctx.cliSchemas !== void 0 ? { cliSchemas: [...ctx.cliSchemas] } : {},
|
|
2919
|
+
config: ctx.config,
|
|
2920
|
+
memberOf: members,
|
|
2921
|
+
fileBase: ctx.cwd,
|
|
2922
|
+
trustRoot,
|
|
2923
|
+
...ctx.onNotice !== void 0 ? { onNotice: ctx.onNotice } : {}
|
|
2924
|
+
});
|
|
2925
|
+
const probe = { ...data };
|
|
2926
|
+
for (const key of required) if (!(key in probe)) probe[key] = null;
|
|
2927
|
+
const prefs = /* @__PURE__ */ new Map();
|
|
2928
|
+
const external = /* @__PURE__ */ new Set();
|
|
2929
|
+
for (const [key, p] of await ctx.validator.locationPreferences(probe, resolved.schemas)) {
|
|
2930
|
+
prefs.set(key, p.location);
|
|
2931
|
+
marked.add(key);
|
|
2932
|
+
if (p.location === "external") external.add(key);
|
|
2933
|
+
}
|
|
2934
|
+
for (const key of required) if (!prefs.has(key)) prefs.set(key, "external");
|
|
2935
|
+
preferencesOf.set(label, prefs);
|
|
2936
|
+
const planned = model.members(label);
|
|
2937
|
+
const joins = new Set(
|
|
2938
|
+
model.manifests.filter((m) => planned.includes(m.collection) && m.join !== PATH_JOIN).map((m) => m.join)
|
|
2939
|
+
);
|
|
2940
|
+
for (const m of model.manifests) {
|
|
2941
|
+
if (planned.includes(m.collection)) for (const k of [...m.keysBefore, ...m.keysAdded]) marked.add(k);
|
|
2942
|
+
}
|
|
2943
|
+
const keys = dedupe([...Object.keys(data), ...required]).filter(wanted);
|
|
2944
|
+
for (const key of keys) {
|
|
2945
|
+
if (joins.has(key)) continue;
|
|
2946
|
+
const pref = prefs.get(key);
|
|
2947
|
+
const inPage = Object.hasOwn(page.own, key);
|
|
2948
|
+
const owner = model.ownerOf(key, model.members(label), external);
|
|
2949
|
+
if (owner !== void 0) {
|
|
2950
|
+
if (pref === "page") {
|
|
2951
|
+
if (owner.url) {
|
|
2952
|
+
if (supplied(owner, page, key) !== void 0) stay(page, key, "url-manifest", urlDetail(owner));
|
|
2953
|
+
} else if (owner.keysBefore.includes(key)) {
|
|
2954
|
+
model.removeKey(owner, key);
|
|
2955
|
+
}
|
|
2956
|
+
} else if (inPage) {
|
|
2957
|
+
planOut(page, owner, key, pref === "external" ? "preferred" : "owned");
|
|
2958
|
+
}
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
if (pref !== "external" || !(inPage || required.includes(key))) continue;
|
|
2962
|
+
const chosen = await model.home(label, true, (m) => model.impliedOwns(m, key, external));
|
|
2963
|
+
if ("manifest" in chosen) {
|
|
2964
|
+
if (chosen.manifest.implied) {
|
|
2965
|
+
if (inPage) planOut(page, chosen.manifest, key, "preferred");
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
model.addKey(chosen.manifest, key);
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
if (chosen.proposed.kind !== "none") continue;
|
|
2972
|
+
if (inPage) {
|
|
2973
|
+
stay(
|
|
2974
|
+
page,
|
|
2975
|
+
key,
|
|
2976
|
+
"no-home",
|
|
2977
|
+
chosen.proposed.reason === "no-config" ? "--no-config leaves this document no manifest" : `this document is in none of the ${chosen.proposed.collections} collections, so it has no manifest`
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
const entry = homeless.get(label) ?? {
|
|
2981
|
+
keys: [],
|
|
2982
|
+
reason: chosen.proposed.reason,
|
|
2983
|
+
collections: chosen.proposed.collections
|
|
2984
|
+
};
|
|
2985
|
+
entry.keys.push(key);
|
|
2986
|
+
homeless.set(label, entry);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (fields !== void 0) {
|
|
2990
|
+
for (const f of fields) {
|
|
2991
|
+
if (!marked.has(f) && !(request.require !== void 0 && [...request.require.values()].some((ks) => ks.includes(f)))) {
|
|
2992
|
+
throw new DocmetaError(
|
|
2993
|
+
`"${f}" has no x-manni-location mark and no owning manifest for any file in this run.`
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
const suspended = [];
|
|
2999
|
+
for (const page of pages.values()) {
|
|
3000
|
+
for (const [key, intent] of [...page.intents]) {
|
|
3001
|
+
const m = intent.kind === "out" ? intent.manifest : intent.kind === "stay" ? intent.toward : void 0;
|
|
3002
|
+
if (m === void 0 || !m.keysRemoved.includes(key)) continue;
|
|
3003
|
+
page.intents.delete(key);
|
|
3004
|
+
const prefs = preferencesOf.get(page.label);
|
|
3005
|
+
suspended.push({
|
|
3006
|
+
page,
|
|
3007
|
+
manifest: m,
|
|
3008
|
+
key,
|
|
3009
|
+
reason: intent.kind === "out" ? intent.reason : prefs?.get(key) === "external" ? "preferred" : "owned"
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
const grown = new Set(model.pathsAdded.map((p) => p.collection));
|
|
3014
|
+
const affected = new Set(grown);
|
|
3015
|
+
for (const m of model.manifests) {
|
|
3016
|
+
if (m.keysAdded.length > 0 || m.keysRemoved.length > 0) affected.add(m.collection);
|
|
3017
|
+
}
|
|
3018
|
+
for (const name of affected) {
|
|
3019
|
+
const collection = model.collections.find((c) => c.name === name);
|
|
3020
|
+
if (collection === void 0) continue;
|
|
3021
|
+
const walked = await resolveTargetSet({
|
|
3022
|
+
inputs: [...collection.paths],
|
|
3023
|
+
cwd: configDir,
|
|
3024
|
+
allowEmpty: true,
|
|
3025
|
+
...gitignoreOptions({
|
|
3026
|
+
...ctx.respectGitignore !== void 0 ? { flag: ctx.respectGitignore } : {},
|
|
3027
|
+
...ctx.config?.respectGitignore !== void 0 ? { configured: ctx.config.respectGitignore } : {}
|
|
3028
|
+
})
|
|
3029
|
+
});
|
|
3030
|
+
const labels = dedupe([
|
|
3031
|
+
...request.files,
|
|
3032
|
+
...walked.files.map((f) => reported2(resolve3(configDir, f), ctx.base))
|
|
3033
|
+
]).filter((label) => model.members(label).includes(name));
|
|
3034
|
+
for (const label of labels) {
|
|
3035
|
+
const page = await load(label, !runSet.has(label));
|
|
3036
|
+
if (page === void 0) continue;
|
|
3037
|
+
const prefs = preferencesOf.get(label);
|
|
3038
|
+
const covered = grown.has(name) && !currentMembers(label).includes(name);
|
|
3039
|
+
for (const m of model.manifestsOf(name)) {
|
|
3040
|
+
const out = dedupe([
|
|
3041
|
+
...m.keysAdded,
|
|
3042
|
+
...grown.has(name) ? m.keysBefore.filter((k) => !m.keysRemoved.includes(k)) : []
|
|
3043
|
+
]).filter((k) => covered ? k !== FILE_SCHEMA_KEY : wanted(k));
|
|
3044
|
+
if (page.unreadable !== void 0) {
|
|
3045
|
+
const detail = unreadableDetail(page);
|
|
3046
|
+
const changing = covered ? out : out.filter((k) => m.keysAdded.includes(k));
|
|
3047
|
+
for (const key of changing) if (!page.intents.has(key)) stay(page, key, "unreadable", detail);
|
|
3048
|
+
for (const key of m.keysRemoved) {
|
|
3049
|
+
const held = m.join !== PATH_JOIN || !m.created && index?.byPath.get(page.absPath)?.get(key)?.file === model.fileFor(m, page.absPath).file;
|
|
3050
|
+
if (held && !page.intents.has(key)) stay(page, key, "unreadable", detail, { blocks: m });
|
|
3051
|
+
}
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
for (const key of out) {
|
|
3055
|
+
if (!Object.hasOwn(page.own, key)) continue;
|
|
3056
|
+
const reason = m.keysAdded.includes(key) || prefs?.get(key) === "external" ? "preferred" : "owned";
|
|
3057
|
+
planOut(page, m, key, reason);
|
|
3058
|
+
}
|
|
3059
|
+
for (const key of m.keysRemoved) planIn(page, m, key);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
const orderedPages = () => {
|
|
3064
|
+
const all = [...pages.values()];
|
|
3065
|
+
const run = all.filter((p) => !p.beyond).sort((a, b) => compare(a.label, b.label));
|
|
3066
|
+
const beyond = all.filter((p) => p.beyond).sort((a, b) => compare(a.label, b.label));
|
|
3067
|
+
return [...run, ...beyond];
|
|
3068
|
+
};
|
|
3069
|
+
let dirty = new Set(pages.values());
|
|
3070
|
+
while (dirty.size > 0) {
|
|
3071
|
+
const pass = [...pages.values()].filter((p) => dirty.has(p));
|
|
3072
|
+
for (const page of pass) planPageText(page);
|
|
3073
|
+
const touched = /* @__PURE__ */ new Set();
|
|
3074
|
+
for (const page of pass) {
|
|
3075
|
+
for (const [key, intent] of page.intents) {
|
|
3076
|
+
if (intent.kind !== "stay" || intent.blocks === void 0) continue;
|
|
3077
|
+
const m = intent.blocks;
|
|
3078
|
+
if (!m.keysRemoved.includes(key)) continue;
|
|
3079
|
+
m.keysRemoved.splice(m.keysRemoved.indexOf(key), 1);
|
|
3080
|
+
for (const other of pages.values()) {
|
|
3081
|
+
const i = other.intents.get(key);
|
|
3082
|
+
if (i?.kind === "in" && i.manifest === m) {
|
|
3083
|
+
other.intents.delete(key);
|
|
3084
|
+
touched.add(other);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
for (const s of suspended) {
|
|
3088
|
+
if (s.manifest === m && s.key === key) {
|
|
3089
|
+
planOut(s.page, m, key, s.reason);
|
|
3090
|
+
touched.add(s.page);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
dirty = touched;
|
|
3096
|
+
}
|
|
3097
|
+
const writes = [];
|
|
3098
|
+
const lines = /* @__PURE__ */ new Map();
|
|
3099
|
+
for (const m of model.manifests) {
|
|
3100
|
+
if (m.url) continue;
|
|
3101
|
+
const undeclared = !m.implied && model.finalKeys(m).length === 0 && !m.created;
|
|
3102
|
+
const files = /* @__PURE__ */ new Map();
|
|
3103
|
+
const group = (at2) => {
|
|
3104
|
+
if (at2.abs === void 0) return void 0;
|
|
3105
|
+
const have = files.get(at2.abs);
|
|
3106
|
+
if (have !== void 0) return have;
|
|
3107
|
+
const made = { file: at2.file, outs: [], ins: [] };
|
|
3108
|
+
files.set(at2.abs, made);
|
|
3109
|
+
return made;
|
|
3110
|
+
};
|
|
3111
|
+
if (m.created) group({ abs: m.absPath, file: m.file });
|
|
3112
|
+
for (const page of orderedPages()) {
|
|
3113
|
+
for (const [key, intent] of orderedIntents(page)) {
|
|
3114
|
+
if ((intent.kind === "out" || intent.kind === "in") && intent.manifest === m) {
|
|
3115
|
+
const into = group(model.fileFor(m, page.absPath));
|
|
3116
|
+
if (intent.kind === "out") {
|
|
3117
|
+
into?.outs.push({ page: page.label, entry: intent.entry, key, value: page.own[key], drop: intent.drop });
|
|
3118
|
+
} else {
|
|
3119
|
+
into?.ins.push({ entry: intent.entry, key });
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
if (!undeclared) assertNoStrandedEntries(m, index, pages, configDir, (abs) => model.fileFor(m, abs).file);
|
|
3125
|
+
if (undeclared) continue;
|
|
3126
|
+
for (const [path, { file, outs, ins }] of files) {
|
|
3127
|
+
if (!m.created && outs.length === 0 && ins.length === 0) continue;
|
|
3128
|
+
const disk = m.created ? null : await readManifestText(path, file, m.perPage);
|
|
3129
|
+
let text = disk ?? "";
|
|
3130
|
+
const before = text;
|
|
3131
|
+
const where = { join: m.join, file };
|
|
3132
|
+
for (const o of outs) {
|
|
3133
|
+
if (!o.drop) text = spliceManifestValue(text, { ...where, entry: o.entry, key: o.key, value: o.value }).text;
|
|
3134
|
+
}
|
|
3135
|
+
for (const i of ins) text = removeManifestKey(text, { ...where, entry: i.entry, key: i.key }).text;
|
|
3136
|
+
const found = keyLines2(text, m.join);
|
|
3137
|
+
for (const o of outs) {
|
|
3138
|
+
const line = found.get(entryId(o.entry, m.join))?.get(o.key);
|
|
3139
|
+
if (line !== void 0) lines.set(lineId(o.page, file, o.key), line);
|
|
3140
|
+
}
|
|
3141
|
+
if (m.created || text !== before) {
|
|
3142
|
+
writes.push({ path, text, kind: "manifest", before: disk });
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
const finalCollections = model.final();
|
|
3147
|
+
const needsConfig = model.created.length > 0 || model.pathsAdded.length > 0 || model.manifests.some((m) => m.created || m.keysAdded.length > 0 || m.keysRemoved.length > 0);
|
|
3148
|
+
const target = await configTarget(ctx, configDir);
|
|
3149
|
+
let configCreated = false;
|
|
3150
|
+
if (needsConfig) {
|
|
3151
|
+
if (ctx.configFile !== void 0 && (ctx.configFile.kind === "legacy" || !ctx.configFile.wrapped)) {
|
|
3152
|
+
throw new DocmetaError(
|
|
3153
|
+
`${ctx.configFile.source} is a single-tool config file, which cannot hold collections:. Move its keys under meta: in ${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}.`
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
const text = editConfig(target.text, model, configSource(target.path, ctx.cwd));
|
|
3157
|
+
verifyConfig(target.text, text, finalCollections, configSource(target.path, ctx.cwd));
|
|
3158
|
+
writes.push({ path: target.path, text, kind: "config", before: target.disk });
|
|
3159
|
+
configCreated = target.text === null;
|
|
3160
|
+
}
|
|
3161
|
+
for (const page of orderedPages()) {
|
|
3162
|
+
if (page.text !== void 0 && page.text !== page.content) {
|
|
3163
|
+
writes.push({ path: page.absPath, text: page.text, kind: "page", before: page.content });
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
const result = buildResult({
|
|
3167
|
+
model,
|
|
3168
|
+
pages: orderedPages(),
|
|
3169
|
+
dryRun: true,
|
|
3170
|
+
lines: /* @__PURE__ */ new Map(),
|
|
3171
|
+
config: {
|
|
3172
|
+
file: needsConfig || ctx.configPath !== void 0 ? configSource(target.path, ctx.cwd) : null,
|
|
3173
|
+
created: configCreated,
|
|
3174
|
+
collectionsCreated: model.created,
|
|
3175
|
+
pathsAdded: model.pathsAdded
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
3178
|
+
return {
|
|
3179
|
+
result,
|
|
3180
|
+
writes,
|
|
3181
|
+
lines,
|
|
3182
|
+
collections: finalCollections,
|
|
3183
|
+
configDir,
|
|
3184
|
+
configPath: target.path,
|
|
3185
|
+
offers: buildOffers(model, orderedPages(), result, needsConfig ? configSource(target.path, ctx.cwd) : void 0),
|
|
3186
|
+
homeless: [...homeless].map(([file, h]) => ({ file, ...h }))
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
3190
|
+
function orderedIntents(page) {
|
|
3191
|
+
const order = Object.keys(page.own);
|
|
3192
|
+
const rank = (key) => {
|
|
3193
|
+
const i = order.indexOf(key);
|
|
3194
|
+
return i === -1 ? order.length : i;
|
|
3195
|
+
};
|
|
3196
|
+
return [...page.intents].sort(([a], [b]) => rank(a) - rank(b));
|
|
3197
|
+
}
|
|
3198
|
+
function planPageText(page) {
|
|
3199
|
+
const patch = {};
|
|
3200
|
+
const deletions = [];
|
|
3201
|
+
for (const [key, intent] of page.intents) {
|
|
3202
|
+
if (intent.kind === "out") deletions.push(key);
|
|
3203
|
+
else if (intent.kind === "in" && !intent.drop) patch[key] = intent.value;
|
|
3204
|
+
}
|
|
3205
|
+
delete page.text;
|
|
3206
|
+
if (deletions.length === 0 && Object.keys(patch).length === 0) return;
|
|
3207
|
+
const refuse = () => {
|
|
3208
|
+
for (const [key, intent] of page.intents) {
|
|
3209
|
+
if (intent.kind === "out" || intent.kind === "in" && !intent.drop) {
|
|
3210
|
+
page.intents.set(key, {
|
|
3211
|
+
kind: "stay",
|
|
3212
|
+
reason: "read-only-format",
|
|
3213
|
+
detail: `the ${page.format} format cannot write this document's metadata`,
|
|
3214
|
+
...intent.kind === "in" ? { blocks: intent.manifest } : {}
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
};
|
|
3219
|
+
const apply = page.extractor.apply;
|
|
3220
|
+
if (typeof apply !== "function") {
|
|
3221
|
+
refuse();
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
try {
|
|
3225
|
+
const next = apply(page.content, patch, {
|
|
3226
|
+
filePath: page.label,
|
|
3227
|
+
elements: page.elements,
|
|
3228
|
+
...deletions.length > 0 ? { deletions } : {}
|
|
3229
|
+
});
|
|
3230
|
+
const after = page.extractor.extract(next, page.label, { elements: page.elements }).data;
|
|
3231
|
+
const wrote = deletions.every((k) => !Object.hasOwn(after, k)) && Object.entries(patch).every(([k, v]) => deepEqual(after[k], v));
|
|
3232
|
+
if (!wrote) {
|
|
3233
|
+
refuse();
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
page.text = next;
|
|
3237
|
+
} catch (err) {
|
|
3238
|
+
if (!(err instanceof DocmetaError)) throw err;
|
|
3239
|
+
refuse();
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
function assertNoStrandedEntries(m, index, pages, configDir, fileOf) {
|
|
3243
|
+
if (index === null || m.keysRemoved.length === 0) return;
|
|
3244
|
+
for (const key of m.keysRemoved) {
|
|
3245
|
+
const moved = /* @__PURE__ */ new Set();
|
|
3246
|
+
for (const page of pages.values()) {
|
|
3247
|
+
const i = page.intents.get(key);
|
|
3248
|
+
if (i?.kind === "in" && i.manifest === m) moved.add(entryId(i.entry, m.join));
|
|
3249
|
+
}
|
|
3250
|
+
const stranded = m.join === PATH_JOIN ? [...index.byPath].filter(([abs, values]) => {
|
|
3251
|
+
const sv = values.get(key);
|
|
3252
|
+
return sv !== void 0 && sv.collection === m.collection && sv.file === fileOf(abs);
|
|
3253
|
+
}).map(([abs]) => toPosix(relative4(configDir, abs))) : [...index.byField.get(m.join) ?? /* @__PURE__ */ new Map()].filter(([, values]) => {
|
|
3254
|
+
const sv = values.get(key);
|
|
3255
|
+
return sv !== void 0 && sv.file === m.file && sv.collection === m.collection;
|
|
3256
|
+
}).map(([value]) => value);
|
|
3257
|
+
const first = stranded.find((e) => !moved.has(entryId(e, m.join)));
|
|
3258
|
+
if (first !== void 0) {
|
|
3259
|
+
throw new DocmetaError(
|
|
3260
|
+
`Manifest ${m.file} names "${first}", which is not a page of collection ${m.collection}, so ${key} cannot leave its keys while that entry sets it. Fix the entry, or remove it.`
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
async function readManifestText(path, file, perPage) {
|
|
3266
|
+
try {
|
|
3267
|
+
return await readFile4(path, "utf8");
|
|
3268
|
+
} catch (err) {
|
|
3269
|
+
if (perPage && isMissing(err)) return null;
|
|
3270
|
+
throw new DocmetaError(`Manifest ${file} could not be read: ${errorMessage(err)}`);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
var entryId = (entry, join4) => join4 === PATH_JOIN ? toPosix(entry).replace(/^\.\//, "").replace(/\/+/g, "/") : entry;
|
|
3274
|
+
var lineId = (page, manifest, key) => `${page}\0${manifest}\0${key}`;
|
|
3275
|
+
function keyLines2(text, join4) {
|
|
3276
|
+
const out = /* @__PURE__ */ new Map();
|
|
3277
|
+
const lc = new LineCounter3();
|
|
3278
|
+
const doc = parseDocument4(text, { lineCounter: lc, uniqueKeys: false });
|
|
3279
|
+
const root = doc.contents;
|
|
3280
|
+
if (!isMap4(root)) return out;
|
|
3281
|
+
for (const pair of root.items) {
|
|
3282
|
+
const spelled = isScalar3(pair.key) ? String(pair.key.value) : String(pair.key);
|
|
3283
|
+
if (!isMap4(pair.value)) continue;
|
|
3284
|
+
const keys = /* @__PURE__ */ new Map();
|
|
3285
|
+
for (const kv of pair.value.items) {
|
|
3286
|
+
const name = isScalar3(kv.key) ? String(kv.key.value) : String(kv.key);
|
|
3287
|
+
const range = isNode3(kv.key) ? kv.key.range : void 0;
|
|
3288
|
+
if (range) keys.set(name, lc.linePos(range[0]).line);
|
|
3289
|
+
}
|
|
3290
|
+
out.set(entryId(spelled, join4), keys);
|
|
3291
|
+
}
|
|
3292
|
+
return out;
|
|
3293
|
+
}
|
|
3294
|
+
async function configTarget(ctx, configDir) {
|
|
3295
|
+
if (ctx.configPath !== void 0) {
|
|
3296
|
+
try {
|
|
3297
|
+
const text = await readFile4(ctx.configPath, "utf8");
|
|
3298
|
+
return { path: ctx.configPath, text, disk: text };
|
|
3299
|
+
} catch (err) {
|
|
3300
|
+
if (err.code !== "ENOENT") {
|
|
3301
|
+
throw new DocmetaError(
|
|
3302
|
+
`Config file ${configSource(ctx.configPath, ctx.cwd)} could not be read: ${errorMessage(err)}`
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
return { path: ctx.configPath, text: ctx.configFile?.text ?? null, disk: null };
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
for (const name of FAMILY_CONFIG_NAMES) {
|
|
3309
|
+
const path = join3(configDir, name);
|
|
3310
|
+
try {
|
|
3311
|
+
const text = await readFile4(path, "utf8");
|
|
3312
|
+
return { path, text, disk: text };
|
|
3313
|
+
} catch {
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
return { path: join3(configDir, FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"), text: null, disk: null };
|
|
3317
|
+
}
|
|
3318
|
+
function editConfig(text, model, source) {
|
|
3319
|
+
const doc = parseDocument4(text ?? "");
|
|
3320
|
+
const [problem] = doc.errors;
|
|
3321
|
+
if (problem !== void 0) throw new DocmetaError(`${source}: invalid YAML: ${problem.message}`);
|
|
3322
|
+
if (doc.contents !== null && !isMap4(doc.contents)) {
|
|
3323
|
+
throw new DocmetaError(`${source}: top level must be a mapping.`);
|
|
3324
|
+
}
|
|
3325
|
+
const fail = () => {
|
|
3326
|
+
throw new DocmetaError(`Could not edit collections: in ${source}; make the change by hand.`);
|
|
3327
|
+
};
|
|
3328
|
+
const flowSeq = (node, quoted) => {
|
|
3329
|
+
if (!isSeq2(node)) return;
|
|
3330
|
+
node.flow = true;
|
|
3331
|
+
if (quoted) {
|
|
3332
|
+
for (const item of node.items) if (isScalar3(item)) item.type = "QUOTE_DOUBLE";
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
3335
|
+
const manifestNode = (d, file, keys) => {
|
|
3336
|
+
const node = d.createNode({ file, keys });
|
|
3337
|
+
flowSeq(node.get("keys", true), false);
|
|
3338
|
+
return node;
|
|
3339
|
+
};
|
|
3340
|
+
const declared = model.collections.length - model.created.length;
|
|
3341
|
+
const existing = doc.get("collections", true);
|
|
3342
|
+
if (declared > 0 && !isSeq2(existing)) fail();
|
|
3343
|
+
for (let i = 0; i < declared; i++) {
|
|
3344
|
+
const c = model.collections[i];
|
|
3345
|
+
const node = isSeq2(existing) ? existing.items[i] : void 0;
|
|
3346
|
+
if (c === void 0 || !isMap4(node)) return fail();
|
|
3347
|
+
for (const added of model.pathsAdded.filter((p) => p.collection === c.name)) {
|
|
3348
|
+
const paths = node.get("paths", true);
|
|
3349
|
+
if (!isSeq2(paths)) return fail();
|
|
3350
|
+
const scalar = doc.createNode(added.path);
|
|
3351
|
+
const first = paths.items[0];
|
|
3352
|
+
if (isScalar3(first) && first.type !== void 0) scalar.type = first.type;
|
|
3353
|
+
paths.add(scalar);
|
|
3354
|
+
}
|
|
3355
|
+
const refs = model.manifestsOf(c.name);
|
|
3356
|
+
for (const m of refs) {
|
|
3357
|
+
if (m.created) {
|
|
3358
|
+
let list3 = node.get("externalMetadata", true);
|
|
3359
|
+
if (!isSeq2(list3)) {
|
|
3360
|
+
node.set("externalMetadata", doc.createNode([]));
|
|
3361
|
+
list3 = node.get("externalMetadata", true);
|
|
3362
|
+
}
|
|
3363
|
+
if (!isSeq2(list3)) return fail();
|
|
3364
|
+
list3.add(manifestNode(doc, m.written, m.keysAdded));
|
|
3365
|
+
continue;
|
|
3366
|
+
}
|
|
3367
|
+
if (m.keysAdded.length === 0) continue;
|
|
3368
|
+
const entry = node.getIn(["externalMetadata", m.index], true);
|
|
3369
|
+
const keys = isMap4(entry) ? entry.get("keys", true) : void 0;
|
|
3370
|
+
if (!isSeq2(keys)) return fail();
|
|
3371
|
+
for (const key of m.keysAdded) keys.add(doc.createNode(key));
|
|
3372
|
+
}
|
|
3373
|
+
const list2 = node.get("externalMetadata", true);
|
|
3374
|
+
for (const m of [...refs].filter((r) => !r.created).sort((a, b) => b.index - a.index)) {
|
|
3375
|
+
if (m.keysRemoved.length === 0) continue;
|
|
3376
|
+
if (!isSeq2(list2)) return fail();
|
|
3377
|
+
const entry = list2.items[m.index];
|
|
3378
|
+
const keys = isMap4(entry) ? entry.get("keys", true) : void 0;
|
|
3379
|
+
if (!isSeq2(keys)) return fail();
|
|
3380
|
+
for (const key of m.keysRemoved) {
|
|
3381
|
+
const at2 = keys.items.findIndex((item) => (isScalar3(item) ? String(item.value) : String(item)) === key);
|
|
3382
|
+
if (at2 >= 0) keys.delete(at2);
|
|
3383
|
+
}
|
|
3384
|
+
if (keys.items.length === 0) list2.delete(m.index);
|
|
3385
|
+
}
|
|
3386
|
+
if (isSeq2(list2) && list2.items.length === 0) node.delete("externalMetadata");
|
|
3387
|
+
}
|
|
3388
|
+
for (const created of model.created) {
|
|
3389
|
+
const c = model.collections.find((x) => x.name === created.name);
|
|
3390
|
+
if (c === void 0) continue;
|
|
3391
|
+
const refs = model.manifestsOf(c.name).filter((m) => m.created);
|
|
3392
|
+
const node = doc.createNode({
|
|
3393
|
+
name: c.name,
|
|
3394
|
+
paths: c.paths,
|
|
3395
|
+
...refs.length > 0 ? { externalMetadata: refs.map((m) => ({ file: m.written, keys: m.keysAdded })) } : {}
|
|
3396
|
+
});
|
|
3397
|
+
flowSeq(node.get("paths", true), true);
|
|
3398
|
+
const made = node.get("externalMetadata", true);
|
|
3399
|
+
if (isSeq2(made)) {
|
|
3400
|
+
for (const item of made.items) if (isMap4(item)) flowSeq(item.get("keys", true), false);
|
|
3401
|
+
}
|
|
3402
|
+
const seq = doc.get("collections", true);
|
|
3403
|
+
if (isSeq2(seq)) seq.add(node);
|
|
3404
|
+
else doc.set("collections", doc.createNode([node]));
|
|
3405
|
+
}
|
|
3406
|
+
return doc.toString({ flowCollectionPadding: false });
|
|
3407
|
+
}
|
|
3408
|
+
function verifyConfig(before, after, expected, source) {
|
|
3409
|
+
const fail = (why) => {
|
|
3410
|
+
throw new DocmetaError(`Could not edit collections: in ${source}, so nothing was written. ${why}`);
|
|
3411
|
+
};
|
|
3412
|
+
let old;
|
|
3413
|
+
let now;
|
|
3414
|
+
try {
|
|
3415
|
+
old = before === null ? null : parseYaml2(before);
|
|
3416
|
+
now = parseYaml2(after);
|
|
3417
|
+
} catch (err) {
|
|
3418
|
+
return fail(errorMessage(err));
|
|
3419
|
+
}
|
|
3420
|
+
if (typeof now !== "object" || now === null || Array.isArray(now)) return fail("The result is not a mapping.");
|
|
3421
|
+
const nowMap = now;
|
|
3422
|
+
const oldMap = typeof old === "object" && old !== null && !Array.isArray(old) ? old : {};
|
|
3423
|
+
const parsed = parseCollections(nowMap["collections"], source, (m) => new DocmetaError(m));
|
|
3424
|
+
if (!deepEqual(parsed, expected)) return fail("The collections read back differently.");
|
|
3425
|
+
const rest = (m) => Object.fromEntries(Object.entries(m).filter(([k]) => k !== "collections"));
|
|
3426
|
+
if (!deepEqual(rest(nowMap), rest(oldMap))) return fail("Another key would have changed.");
|
|
3427
|
+
}
|
|
3428
|
+
function buildResult(input) {
|
|
3429
|
+
const { model, pages, dryRun, lines } = input;
|
|
3430
|
+
const files = [];
|
|
3431
|
+
for (const page of pages) {
|
|
3432
|
+
const moved = [];
|
|
3433
|
+
const stayed = [];
|
|
3434
|
+
const intents = orderedIntents(page);
|
|
3435
|
+
const fileOf = (m) => model.fileFor(m, page.absPath).file;
|
|
3436
|
+
for (const [key, intent] of intents) {
|
|
3437
|
+
if (intent.kind === "in") moved.push({ key, to: "page", from: fileOf(intent.manifest), reason: "preferred" });
|
|
3438
|
+
}
|
|
3439
|
+
for (const [key, intent] of intents) {
|
|
3440
|
+
if (intent.kind !== "out") continue;
|
|
3441
|
+
const manifest = fileOf(intent.manifest);
|
|
3442
|
+
const line = dryRun ? void 0 : lines.get(lineId(page.label, manifest, key));
|
|
3443
|
+
moved.push({
|
|
3444
|
+
key,
|
|
3445
|
+
to: "manifest",
|
|
3446
|
+
manifest,
|
|
3447
|
+
...line !== void 0 ? { line } : {},
|
|
3448
|
+
reason: intent.reason
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
for (const [key, intent] of intents) {
|
|
3452
|
+
if (intent.kind === "stay") stayed.push({ key, reason: intent.reason, detail: intent.detail });
|
|
3453
|
+
}
|
|
3454
|
+
if (moved.length > 0 || stayed.length > 0) files.push({ file: page.label, moved, stayed, beyond: page.beyond });
|
|
3455
|
+
}
|
|
3456
|
+
const touches = (m, page) => [...page.intents.values()].some((i) => (i.kind === "in" || i.kind === "out") && i.manifest === m);
|
|
3457
|
+
const manifests = model.manifests.filter(
|
|
3458
|
+
(m) => m.created || m.keysAdded.length > 0 || m.keysRemoved.length > 0 || pages.some((p) => touches(m, p))
|
|
3459
|
+
).map((m) => {
|
|
3460
|
+
const keys = model.finalKeys(m);
|
|
3461
|
+
return {
|
|
3462
|
+
file: m.file,
|
|
3463
|
+
collection: m.collection,
|
|
3464
|
+
index: m.index,
|
|
3465
|
+
created: m.created,
|
|
3466
|
+
keysAdded: [...m.keysAdded],
|
|
3467
|
+
keysRemoved: [...m.keysRemoved],
|
|
3468
|
+
keys,
|
|
3469
|
+
undeclared: !m.implied && keys.length === 0 && !m.created,
|
|
3470
|
+
beyond: pages.filter(
|
|
3471
|
+
(p) => p.beyond && [...p.intents].some(
|
|
3472
|
+
([k, i]) => (i.kind === "in" || i.kind === "out") && i.manifest === m || i.kind === "stay" && (m.keysAdded.includes(k) || m.keysRemoved.includes(k))
|
|
3473
|
+
)
|
|
3474
|
+
).length
|
|
3475
|
+
};
|
|
3476
|
+
});
|
|
3477
|
+
return {
|
|
3478
|
+
dryRun,
|
|
3479
|
+
config: input.config,
|
|
3480
|
+
manifests,
|
|
3481
|
+
files,
|
|
3482
|
+
summary: {
|
|
3483
|
+
files: files.length,
|
|
3484
|
+
moved: files.reduce((n, f) => n + f.moved.length, 0),
|
|
3485
|
+
stayed: files.reduce((n, f) => n + f.stayed.length, 0),
|
|
3486
|
+
manifestsCreated: manifests.filter((m) => m.created).length
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
async function applyRelocation(ctx, plan, io = {}) {
|
|
3491
|
+
const write = io.write ?? ((path, text) => writeFileAtomic(path, text, { createParents: true }));
|
|
3492
|
+
const remove = io.remove ?? ((path) => unlink(path));
|
|
3493
|
+
const label = (w) => w.kind === "config" ? configSource(w.path, ctx.cwd) : reported2(w.path, ctx.base);
|
|
3494
|
+
for (const w of plan.writes) {
|
|
3495
|
+
let now;
|
|
3496
|
+
try {
|
|
3497
|
+
now = await readFile4(w.path, "utf8");
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
if (err.code !== "ENOENT") {
|
|
3500
|
+
throw new DocmetaError(`${label(w)} could not be read: ${errorMessage(err)}`);
|
|
3501
|
+
}
|
|
3502
|
+
now = null;
|
|
3503
|
+
}
|
|
3504
|
+
if (now !== w.before) {
|
|
3505
|
+
throw new DocmetaError(
|
|
3506
|
+
`${label(w)} changed after the relocation was planned, so nothing was written. Run the command again.`
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
const rollBack = async (done, head) => {
|
|
3511
|
+
const failed = [];
|
|
3512
|
+
for (const w of [...done].reverse()) {
|
|
3513
|
+
try {
|
|
3514
|
+
if (w.before === null) await remove(w.path);
|
|
3515
|
+
else await write(w.path, w.before);
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
failed.push({ file: label(w), reason: errorMessage(err) });
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
const [first] = failed;
|
|
3521
|
+
if (first === void 0) {
|
|
3522
|
+
if (done.length === 0) return new DocmetaError(`${head} Nothing was changed.`);
|
|
3523
|
+
const files = done.length === 1 ? "the file already written was" : `the ${String(done.length)} files already written were`;
|
|
3524
|
+
return new DocmetaError(`${head} Nothing was changed: ${files} restored.`);
|
|
3525
|
+
}
|
|
3526
|
+
const one = failed.length === 1;
|
|
3527
|
+
return new DocmetaError(
|
|
3528
|
+
`${head} The run was rolled back, but ${list(failed.map((f) => f.file))} could not be restored: ${first.reason}. Restore ${one ? "it" : "them"} from version control.`
|
|
3529
|
+
);
|
|
3530
|
+
};
|
|
3531
|
+
const written = [];
|
|
3532
|
+
for (const w of plan.writes) {
|
|
3533
|
+
try {
|
|
3534
|
+
await write(w.path, w.text);
|
|
3535
|
+
} catch (err) {
|
|
3536
|
+
throw await rollBack(written, `Could not write ${label(w)}: ${errorMessage(err)}.`);
|
|
3537
|
+
}
|
|
3538
|
+
written.push(w);
|
|
3539
|
+
}
|
|
3540
|
+
if (plan.writes.some((w) => w.kind === "config")) {
|
|
3541
|
+
const narrowed = ctx.collections.length < ctx.declaredCollections.length;
|
|
3542
|
+
for (const next of plan.collections) {
|
|
3543
|
+
const have = ctx.declaredCollections.find((c) => c.name === next.name);
|
|
3544
|
+
if (have !== void 0) {
|
|
3545
|
+
have.paths = next.paths;
|
|
3546
|
+
have.externalMetadata = next.externalMetadata;
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
ctx.declaredCollections.push(next);
|
|
3550
|
+
if (!narrowed) ctx.collections.push(next);
|
|
3551
|
+
}
|
|
3552
|
+
ctx.configDir ??= plan.configDir;
|
|
3553
|
+
ctx.configPath ??= plan.configPath;
|
|
3554
|
+
}
|
|
3555
|
+
return {
|
|
3556
|
+
...plan.result,
|
|
3557
|
+
dryRun: false,
|
|
3558
|
+
files: plan.result.files.map((f) => ({
|
|
3559
|
+
...f,
|
|
3560
|
+
moved: f.moved.map((mv) => {
|
|
3561
|
+
if (mv.to !== "manifest") return mv;
|
|
3562
|
+
const line = plan.lines.get(lineId(f.file, mv.manifest, mv.key));
|
|
3563
|
+
return line === void 0 ? mv : { ...mv, line };
|
|
3564
|
+
})
|
|
3565
|
+
}))
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
function buildOffers(model, pages, result, config) {
|
|
3569
|
+
const names = dedupe(result.manifests.map((m) => m.collection));
|
|
3570
|
+
return names.map((name) => {
|
|
3571
|
+
const manifests = result.manifests.filter((m) => m.collection === name);
|
|
3572
|
+
const outs = pages.flatMap(
|
|
3573
|
+
(p) => [...p.intents].filter(([, i]) => i.kind === "out" && i.manifest.collection === name).map(([k]) => ({ page: p.label, key: k }))
|
|
3574
|
+
);
|
|
3575
|
+
const ins = pages.flatMap(
|
|
3576
|
+
(p) => [...p.intents].filter(([, i]) => i.kind === "in" && i.manifest.collection === name).map(([, i]) => i.kind === "in" ? i.manifest.file : "")
|
|
3577
|
+
);
|
|
3578
|
+
const created = model.created.find((c) => c.name === name);
|
|
3579
|
+
return {
|
|
3580
|
+
collection: name,
|
|
3581
|
+
...created !== void 0 ? { createsCollection: { paths: [...created.paths] } } : {},
|
|
3582
|
+
pathsAdded: model.pathsAdded.filter((p) => p.collection === name).map((p) => p.path),
|
|
3583
|
+
config,
|
|
3584
|
+
manifests: manifests.map((m) => ({
|
|
3585
|
+
file: m.file,
|
|
3586
|
+
created: m.created,
|
|
3587
|
+
keysAdded: [...m.keysAdded],
|
|
3588
|
+
keysRemoved: [...m.keysRemoved]
|
|
3589
|
+
})),
|
|
3590
|
+
keysOut: dedupe(manifests.flatMap((m) => m.keysAdded)),
|
|
3591
|
+
valuesOut: outs.length,
|
|
3592
|
+
pagesOut: new Set(outs.map((o) => o.page)).size,
|
|
3593
|
+
valuesIn: ins.length,
|
|
3594
|
+
manifestsIn: dedupe(ins)
|
|
3595
|
+
};
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
3599
|
+
var list = (xs) => xs.join(", ");
|
|
3600
|
+
function offerPrompt(offer, kind) {
|
|
3601
|
+
const config = offer.config ?? FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml";
|
|
3602
|
+
if (kind === "validate") {
|
|
3603
|
+
const parts = [];
|
|
3604
|
+
if (offer.valuesOut > 0) {
|
|
3605
|
+
parts.push(
|
|
3606
|
+
`${count(offer.valuesOut, "value")} in ${count(offer.pagesOut, "page")} ${offer.valuesOut === 1 ? "prefers" : "prefer"} external metadata`
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
if (offer.valuesIn > 0) {
|
|
3610
|
+
parts.push(
|
|
3611
|
+
`${count(offer.valuesIn, "value")} in ${list(offer.manifestsIn)} ${offer.valuesIn === 1 ? "prefers" : "prefer"} the page`
|
|
3612
|
+
);
|
|
3613
|
+
}
|
|
3614
|
+
const changes = [];
|
|
3615
|
+
if (offer.createsCollection !== void 0) {
|
|
3616
|
+
changes.push(`create collection ${offer.collection} (paths: ${list(offer.createsCollection.paths)})`);
|
|
3617
|
+
}
|
|
3618
|
+
for (const p of offer.pathsAdded) changes.push(`add ${p} to collection ${offer.collection}'s paths`);
|
|
3619
|
+
for (const m of offer.manifests) {
|
|
3620
|
+
if (m.created) changes.push(`create ${m.file}`);
|
|
3621
|
+
if (m.keysRemoved.length > 0) changes.push(`remove ${list(m.keysRemoved)} from ${m.file}'s keys`);
|
|
3622
|
+
if (!m.created && m.keysAdded.length > 0) changes.push(`add ${list(m.keysAdded)} to ${m.file}'s keys`);
|
|
3623
|
+
}
|
|
3624
|
+
return {
|
|
3625
|
+
notice: `in collection ${offer.collection}, ${parts.join(" and ")}.`,
|
|
3626
|
+
question: changes.length === 0 ? "Move them? " : `Move them (${changes.join(", ")})? `
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
const keys = list(offer.keysOut);
|
|
3630
|
+
const prefer = offer.keysOut.length === 1 ? "which the schema prefers" : "which their schemas prefer";
|
|
3631
|
+
const move = offer.pagesOut > 0 ? `move them out of ${count(offer.pagesOut, "page")}` : void 0;
|
|
3632
|
+
const tail = (steps2) => {
|
|
3633
|
+
const all = move === void 0 ? steps2 : [...steps2, move];
|
|
3634
|
+
if (all.length === 1) return `${capital(all[0] ?? "")}? `;
|
|
3635
|
+
if (all.length === 2) return `${capital(all[0] ?? "")} and ${all[1] ?? ""}? `;
|
|
3636
|
+
return `${capital(all.slice(0, -1).join(", "))}, and ${all.at(-1) ?? ""}? `;
|
|
3637
|
+
};
|
|
3638
|
+
const created = offer.manifests.find((m) => m.created);
|
|
3639
|
+
if (offer.createsCollection !== void 0) {
|
|
3640
|
+
const make = `Create collection ${offer.collection} (paths: ${list(offer.createsCollection.paths)}), with ${created?.file ?? `${offer.collection}.metadata.yaml`}, in ${config}`;
|
|
3641
|
+
return {
|
|
3642
|
+
notice: `no collection has a manifest for ${keys}, ${prefer} in external metadata.`,
|
|
3643
|
+
question: move === void 0 ? `${make}? ` : `${make}, and ${move}? `
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
const steps = offer.pathsAdded.map((p) => `add ${p} to collection ${offer.collection}'s paths`);
|
|
3647
|
+
if (created !== void 0) steps.push(`create ${created.file}`, `add it to ${config}`);
|
|
3648
|
+
for (const m of offer.manifests) {
|
|
3649
|
+
if (!m.created && m.keysAdded.length > 0) steps.push(`add ${list(m.keysAdded)} to ${m.file}'s keys`);
|
|
3650
|
+
}
|
|
3651
|
+
return {
|
|
3652
|
+
notice: `collection ${offer.collection} has no manifest for ${keys}, ${prefer} in external metadata.`,
|
|
3653
|
+
question: tail(steps)
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
function capital(s) {
|
|
3657
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3658
|
+
}
|
|
3659
|
+
function relocationOffers(plan) {
|
|
3660
|
+
return plan.offers;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// src/shared/pin.ts
|
|
3664
|
+
var LineRangeError = class extends ToolError {
|
|
3665
|
+
constructor(message) {
|
|
3666
|
+
super(message);
|
|
3667
|
+
this.name = "LineRangeError";
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
function normalizeText(text) {
|
|
3671
|
+
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
3672
|
+
return body.replace(/\r\n/g, "\n");
|
|
3673
|
+
}
|
|
3674
|
+
function splitLines(text) {
|
|
3675
|
+
const parts = normalizeText(text).split("\n");
|
|
3676
|
+
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
3677
|
+
return parts;
|
|
3678
|
+
}
|
|
3679
|
+
function sliceLines(lines, range, label) {
|
|
3680
|
+
const start = range?.start;
|
|
3681
|
+
if (start === void 0) return lines.join("\n");
|
|
3682
|
+
const end = range?.end ?? start;
|
|
3683
|
+
if (end < start) {
|
|
3684
|
+
throw new LineRangeError(`${label ?? "the source"} range ${String(start)}-${String(end)} ends before it starts.`);
|
|
3685
|
+
}
|
|
3686
|
+
if (start > lines.length || end > lines.length) {
|
|
3687
|
+
const first = start > lines.length ? start : end;
|
|
3688
|
+
throw new LineRangeError(`${label ?? "the source"} has ${lines.length} lines; line ${first} is out of range.`);
|
|
3689
|
+
}
|
|
3690
|
+
return lines.slice(start - 1, end).join("\n");
|
|
3691
|
+
}
|
|
3692
|
+
function hashRange(text, range, key) {
|
|
3693
|
+
return hashLines(sliceLines(splitLines(text), range), key);
|
|
3694
|
+
}
|
|
3695
|
+
function hashLines(joined, key) {
|
|
3696
|
+
return key === void 0 ? integrityOf(Buffer.from(joined, "utf8")) : keyedPin(joined, key);
|
|
3697
|
+
}
|
|
3698
|
+
function isKeyedPin(pin) {
|
|
3699
|
+
return pin.startsWith(KEYED_PIN_PREFIX);
|
|
3700
|
+
}
|
|
3701
|
+
function pinOfLines(lines, range) {
|
|
3702
|
+
if (range.start < 1 || range.end > lines.length || range.end < range.start) return void 0;
|
|
3703
|
+
return hashLines(lines.slice(range.start - 1, range.end).join("\n"));
|
|
3704
|
+
}
|
|
3705
|
+
var MOVE_WINDOW_LINES = 2e3;
|
|
3706
|
+
var MOVE_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
3707
|
+
var MAX_RANGE_LINES = 5e3;
|
|
3708
|
+
function findWindows(lines, length, pin, key, opts) {
|
|
3709
|
+
const starts = [];
|
|
3710
|
+
const lastStart = lines.length - length + 1;
|
|
3711
|
+
if (length < 1 || lastStart < 1) return { starts, truncated: false };
|
|
3712
|
+
const joined = (start) => lines.slice(start - 1, start - 1 + length).join("\n");
|
|
3713
|
+
const original = opts?.original;
|
|
3714
|
+
if (original !== void 0) {
|
|
3715
|
+
const first = original[0];
|
|
3716
|
+
for (let start = 1; start <= lastStart; start++) {
|
|
3717
|
+
if (lines[start - 1] !== first) continue;
|
|
3718
|
+
let same = true;
|
|
3719
|
+
for (let i = 1; i < length; i++) {
|
|
3720
|
+
if (lines[start - 1 + i] !== original[i]) {
|
|
3721
|
+
same = false;
|
|
3722
|
+
break;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
if (same && hashLines(joined(start), key) === pin) starts.push(start);
|
|
3726
|
+
}
|
|
3727
|
+
return { starts, truncated: false };
|
|
3728
|
+
}
|
|
3729
|
+
const counter = opts?.counter;
|
|
3730
|
+
const budget = counter?.left ?? opts?.budget ?? MOVE_BUDGET_BYTES;
|
|
3731
|
+
const around = opts?.around ?? 1;
|
|
3732
|
+
const bandStart = Math.max(1, around - MOVE_WINDOW_LINES);
|
|
3733
|
+
const bandEnd = Math.min(lastStart, around + MOVE_WINDOW_LINES);
|
|
3734
|
+
const order = [];
|
|
3735
|
+
for (let start = bandStart; start <= bandEnd; start++) order.push(start);
|
|
3736
|
+
for (let start = 1; start < bandStart; start++) order.push(start);
|
|
3737
|
+
for (let start = bandEnd + 1; start <= lastStart; start++) order.push(start);
|
|
3738
|
+
let spent = 0;
|
|
3739
|
+
for (const start of order) {
|
|
3740
|
+
const text = joined(start);
|
|
3741
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
3742
|
+
if (spent + bytes > budget) {
|
|
3743
|
+
if (counter !== void 0) counter.left = budget - spent;
|
|
3744
|
+
return { starts, truncated: true };
|
|
3745
|
+
}
|
|
3746
|
+
spent += bytes;
|
|
3747
|
+
if (hashLines(text, key) === pin) starts.push(start);
|
|
3748
|
+
}
|
|
3749
|
+
if (counter !== void 0) counter.left = budget - spent;
|
|
3750
|
+
return { starts, truncated: false };
|
|
3751
|
+
}
|
|
3752
|
+
var RANGE_SPEC = /^([1-9][0-9]*)(?:-([1-9][0-9]*))?$/;
|
|
3753
|
+
function parseLines(spec) {
|
|
3754
|
+
if (typeof spec === "number") {
|
|
3755
|
+
return Number.isInteger(spec) && spec >= 1 ? { start: spec, end: spec } : void 0;
|
|
3756
|
+
}
|
|
3757
|
+
const m = RANGE_SPEC.exec(spec);
|
|
3758
|
+
const startText = m?.[1];
|
|
3759
|
+
if (startText === void 0) return void 0;
|
|
3760
|
+
const start = Number(startText);
|
|
3761
|
+
const endText = m?.[2];
|
|
3762
|
+
const end = endText === void 0 ? start : Number(endText);
|
|
3763
|
+
return end < start ? void 0 : { start, end };
|
|
3764
|
+
}
|
|
3765
|
+
function lineSpec(lines) {
|
|
3766
|
+
return lines.start === lines.end ? lines.start : `${String(lines.start)}-${String(lines.end)}`;
|
|
3767
|
+
}
|
|
3768
|
+
function spellLines(lines) {
|
|
3769
|
+
return String(lineSpec(lines));
|
|
3770
|
+
}
|
|
3771
|
+
function toFileLines(lines, bodyLine) {
|
|
3772
|
+
return { start: bodyLine + lines.start - 1, end: bodyLine + lines.end - 1 };
|
|
3773
|
+
}
|
|
3774
|
+
function toBodyLines(lines, bodyLine) {
|
|
3775
|
+
return { start: lines.start - bodyLine + 1, end: lines.end - bodyLine + 1 };
|
|
3776
|
+
}
|
|
3777
|
+
function splitPageArgument(arg) {
|
|
3778
|
+
const text = arg.startsWith(STDIN_LINES_MARKER) ? `-${arg.slice(STDIN_LINES_MARKER.length)}` : arg;
|
|
3779
|
+
const m = /:([1-9][0-9]*)(?:-([1-9][0-9]*))?$/.exec(text);
|
|
3780
|
+
const head = m === null ? text : text.slice(0, m.index);
|
|
3781
|
+
if (m === null || head === "") return { page: arg };
|
|
3782
|
+
const start = Number(m[1]);
|
|
3783
|
+
const end = m[2] === void 0 ? start : Number(m[2]);
|
|
3784
|
+
if (end < start) {
|
|
3785
|
+
throw new LineRangeError(`Invalid range "${arg}": end line ${end} is before start line ${start}.`);
|
|
3786
|
+
}
|
|
3787
|
+
return { page: head, lines: { start, end } };
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// src/meta/core/derive/provenance.ts
|
|
3791
|
+
import picomatch2 from "picomatch";
|
|
3792
|
+
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
3793
|
+
var DEFAULT_MACHINES = ["*[bot]"];
|
|
3794
|
+
var PROVENANCE_POINTER = "/provenance";
|
|
3795
|
+
function isBlankLine(line) {
|
|
3796
|
+
return (line ?? "").trim() === "";
|
|
3797
|
+
}
|
|
3798
|
+
function trimBlank(body, span) {
|
|
3799
|
+
let { start, end } = span;
|
|
3800
|
+
while (start <= end && isBlankLine(body[start - 1])) start++;
|
|
3801
|
+
while (end >= start && isBlankLine(body[end - 1])) end--;
|
|
3802
|
+
return start > end ? void 0 : { start, end };
|
|
3803
|
+
}
|
|
3804
|
+
function blankEdges(body, span) {
|
|
3805
|
+
const first = isBlankLine(body[span.start - 1]);
|
|
3806
|
+
const last = isBlankLine(body[span.end - 1]);
|
|
3807
|
+
if (!first && !last) return void 0;
|
|
3808
|
+
if (trimBlank(body, span) === void 0) return "only";
|
|
3809
|
+
return first && last ? "both" : first ? "start" : "end";
|
|
3810
|
+
}
|
|
3811
|
+
var PLAIN_PIN = /^sha256-[0-9a-f]{64}$/;
|
|
3812
|
+
function provenanceEntries(value) {
|
|
3813
|
+
if (!Array.isArray(value)) return [];
|
|
3814
|
+
const out = [];
|
|
3815
|
+
for (const item of value) {
|
|
3816
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
|
|
3817
|
+
const record = item;
|
|
3818
|
+
const machine = record["generated-by"];
|
|
3819
|
+
const lines = record.lines;
|
|
3820
|
+
const integrity = record.integrity;
|
|
3821
|
+
if (typeof machine !== "string" || machine === "") continue;
|
|
3822
|
+
if (typeof lines !== "string" && typeof lines !== "number") continue;
|
|
3823
|
+
if (parseLines(lines) === void 0) continue;
|
|
3824
|
+
if (typeof integrity !== "string" || !PLAIN_PIN.test(integrity)) continue;
|
|
3825
|
+
out.push({ ...record, "generated-by": machine, lines, integrity });
|
|
3826
|
+
}
|
|
3827
|
+
return out;
|
|
3828
|
+
}
|
|
3829
|
+
function readProvenancePage(content, opts) {
|
|
3830
|
+
const lines = splitLines(content);
|
|
3831
|
+
const loc = opts?.fenced === false ? null : locateFrontmatter(content);
|
|
3832
|
+
if (loc === null) return { lines, bodyLine: 1, body: lines, stamp: [] };
|
|
3833
|
+
const head = content.slice(0, loc.closeEnd);
|
|
3834
|
+
const newlines = head.split("\n").length - 1;
|
|
3835
|
+
const bodyLine = newlines + (head.endsWith("\n") ? 1 : 2);
|
|
3836
|
+
let stamp = [];
|
|
3837
|
+
try {
|
|
3838
|
+
stamp = provenanceEntries(extractFrontmatter(content, "markdown").data.provenance);
|
|
3839
|
+
} catch {
|
|
3840
|
+
stamp = [];
|
|
3841
|
+
}
|
|
3842
|
+
return { lines, bodyLine, body: lines.slice(bodyLine - 1), stamp };
|
|
3843
|
+
}
|
|
3844
|
+
var HEADER = /^([0-9a-f]{40}(?:[0-9a-f]{24})?) ([1-9][0-9]*) ([1-9][0-9]*)(?: ([1-9][0-9]*))?$/;
|
|
3845
|
+
var ALL_ZERO = /^0+$/;
|
|
3846
|
+
function parseLinePorcelain(text) {
|
|
3847
|
+
const rows = text.split("\n");
|
|
3848
|
+
if (rows.length > 0 && rows[rows.length - 1] === "") rows.pop();
|
|
3849
|
+
const out = [];
|
|
3850
|
+
let i = 0;
|
|
3851
|
+
while (i < rows.length) {
|
|
3852
|
+
const headerRow = rows[i] ?? "";
|
|
3853
|
+
const m = HEADER.exec(headerRow);
|
|
3854
|
+
const sha = m?.[1];
|
|
3855
|
+
const orig = m?.[2];
|
|
3856
|
+
const final = m?.[3];
|
|
3857
|
+
if (sha === void 0 || orig === void 0 || final === void 0) {
|
|
3858
|
+
throw new Error(`porcelain: expected a header at row ${i + 1}, got ${JSON.stringify(headerRow)}`);
|
|
3859
|
+
}
|
|
3860
|
+
const finalLine = Number(final);
|
|
3861
|
+
const count2 = m?.[4];
|
|
3862
|
+
const fields = /* @__PURE__ */ new Map();
|
|
3863
|
+
i++;
|
|
3864
|
+
for (let row = rows[i]; row !== void 0 && !row.startsWith(" "); row = rows[i]) {
|
|
3865
|
+
const space2 = row.indexOf(" ");
|
|
3866
|
+
if (space2 === -1) fields.set(row, true);
|
|
3867
|
+
else fields.set(row.slice(0, space2), row.slice(space2 + 1));
|
|
3868
|
+
i++;
|
|
3869
|
+
}
|
|
3870
|
+
const contentRow = rows[i];
|
|
3871
|
+
if (contentRow === void 0) throw new Error(`porcelain: line ${finalLine} has no content row`);
|
|
3872
|
+
const author = fields.get("author");
|
|
3873
|
+
const filename = fields.get("filename");
|
|
3874
|
+
if (typeof author !== "string" || typeof filename !== "string") {
|
|
3875
|
+
throw new Error(`porcelain: line ${finalLine} has no full header; run blame with --line-porcelain`);
|
|
3876
|
+
}
|
|
3877
|
+
const mail = fields.get("author-mail");
|
|
3878
|
+
const previous = fields.get("previous");
|
|
3879
|
+
const space = typeof previous === "string" ? previous.indexOf(" ") : -1;
|
|
3880
|
+
out.push({
|
|
3881
|
+
sha,
|
|
3882
|
+
origLine: Number(orig),
|
|
3883
|
+
finalLine,
|
|
3884
|
+
...count2 !== void 0 ? { groupCount: Number(count2) } : {},
|
|
3885
|
+
author,
|
|
3886
|
+
authorMail: typeof mail === "string" ? mail.replace(/^<(.*)>$/, "$1") : "",
|
|
3887
|
+
filename,
|
|
3888
|
+
...typeof previous === "string" && space !== -1 ? { previous: { sha: previous.slice(0, space), filename: previous.slice(space + 1) } } : {},
|
|
3889
|
+
boundary: fields.get("boundary") === true,
|
|
3890
|
+
uncommitted: ALL_ZERO.test(sha),
|
|
3891
|
+
content: contentRow.slice(1)
|
|
3892
|
+
});
|
|
3893
|
+
i++;
|
|
3894
|
+
}
|
|
3895
|
+
return out;
|
|
3896
|
+
}
|
|
3897
|
+
var IDENTITY_GLOB = { literalBrackets: true, dot: true, bash: true };
|
|
3898
|
+
function machineIdentity(value, machines) {
|
|
3899
|
+
const m = /^(.*?)\s*<([^>]*)>\s*$/.exec(value);
|
|
3900
|
+
const name = m === null ? value.trim() : m[1] ?? "";
|
|
3901
|
+
const email = m === null ? "" : m[2] ?? "";
|
|
3902
|
+
const match = (s) => s !== "" && machines.some((glob) => picomatch2.isMatch(s, glob, IDENTITY_GLOB));
|
|
3903
|
+
if (!match(name) && !match(email)) return void 0;
|
|
3904
|
+
return name !== "" ? name : email;
|
|
3905
|
+
}
|
|
3906
|
+
function stampEvidence(commit, origLine, fenced) {
|
|
3907
|
+
const stamps = verifiedStamps(commit, fenced);
|
|
3908
|
+
if (stamps === void 0) return void 0;
|
|
3909
|
+
const body = origLine - stamps.bodyLine + 1;
|
|
3910
|
+
if (body < 1) return void 0;
|
|
3911
|
+
return stamps.entries.find((entry) => body >= entry.start && body <= entry.end)?.generatedBy;
|
|
3912
|
+
}
|
|
3913
|
+
function carriesStamp(commit, fenced) {
|
|
3914
|
+
return (verifiedStamps(commit, fenced)?.entries.length ?? 0) > 0;
|
|
3915
|
+
}
|
|
3916
|
+
function verifiedStamps(commit, fenced) {
|
|
3917
|
+
if (commit.blob === void 0) return void 0;
|
|
3918
|
+
const memo = fenced === false ? stampsUnfenced : stampsFenced;
|
|
3919
|
+
const known = memo.get(commit);
|
|
3920
|
+
if (known !== void 0) return known;
|
|
3921
|
+
const page = readProvenancePage(commit.blob, fenced === void 0 ? void 0 : { fenced });
|
|
3922
|
+
const entries = [];
|
|
3923
|
+
for (const entry of commit.stamp === void 0 ? page.stamp : provenanceEntries(commit.stamp)) {
|
|
3924
|
+
const span = parseLines(entry.lines);
|
|
3925
|
+
if (span !== void 0 && pinOfLines(page.body, span) === entry.integrity) {
|
|
3926
|
+
entries.push({ start: span.start, end: span.end, generatedBy: entry["generated-by"] });
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
const stamps = { bodyLine: page.bodyLine, entries };
|
|
3930
|
+
memo.set(commit, stamps);
|
|
3931
|
+
return stamps;
|
|
3932
|
+
}
|
|
3933
|
+
var stampsFenced = /* @__PURE__ */ new WeakMap();
|
|
3934
|
+
var stampsUnfenced = /* @__PURE__ */ new WeakMap();
|
|
3935
|
+
function recordedEvidence(line, ctx) {
|
|
3936
|
+
if (line.uncommitted) return { rule: 5, sha: line.sha };
|
|
3937
|
+
const commit = ctx.commits.get(line.sha);
|
|
3938
|
+
if (commit === void 0) throw new Error(`no commit ${line.sha}`);
|
|
3939
|
+
const stamped = stampEvidence(commit, line.origLine, ctx.fenced);
|
|
3940
|
+
if (stamped !== void 0) return { machine: stamped, rule: 2, sha: line.sha };
|
|
3941
|
+
if (carriesStamp(commit, ctx.fenced)) return { rule: 5, sha: line.sha };
|
|
3942
|
+
const generated = commit.trailers.generatedBy.map((v) => v.trim()).find((v) => v !== "");
|
|
3943
|
+
if (generated !== void 0) return { machine: generated, rule: 3, sha: line.sha };
|
|
3944
|
+
for (const value of commit.trailers.coAuthoredBy) {
|
|
3945
|
+
const machine = machineIdentity(value.trim(), ctx.machines);
|
|
3946
|
+
if (machine !== void 0) return { machine, rule: 4, sha: line.sha };
|
|
3947
|
+
}
|
|
3948
|
+
return { rule: 5, sha: line.sha };
|
|
3949
|
+
}
|
|
3950
|
+
function resolveLineEvidence(line, ctx) {
|
|
3951
|
+
if (line.uncommitted && ctx.generatedBy !== void 0 && ctx.generatedBy !== "") {
|
|
3952
|
+
return { machine: ctx.generatedBy, rule: 1, sha: line.sha };
|
|
3953
|
+
}
|
|
3954
|
+
return recordedEvidence(line, ctx);
|
|
3955
|
+
}
|
|
3956
|
+
function pinOf(lines, span) {
|
|
3957
|
+
const pin = pinOfLines(lines, span);
|
|
3958
|
+
if (pin === void 0) throw new Error(`provenance: lines ${spellLines(span)} fall outside the body`);
|
|
3959
|
+
return pin;
|
|
3960
|
+
}
|
|
3961
|
+
function contextOf(input) {
|
|
3962
|
+
return {
|
|
3963
|
+
commits: input.commits,
|
|
3964
|
+
machines: input.machines ?? DEFAULT_MACHINES,
|
|
3965
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
3966
|
+
...input.fenced !== void 0 ? { fenced: input.fenced } : {}
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
function readOptions(fenced) {
|
|
3970
|
+
return fenced === void 0 ? void 0 : { fenced };
|
|
3971
|
+
}
|
|
3972
|
+
function deriveProvenance(input) {
|
|
3973
|
+
const page = readProvenancePage(input.content, readOptions(input.fenced));
|
|
3974
|
+
const ctx = contextOf(input);
|
|
3975
|
+
const evidenceByLine = /* @__PURE__ */ new Map();
|
|
3976
|
+
for (const line of input.blame) {
|
|
3977
|
+
const body = line.finalLine - page.bodyLine + 1;
|
|
3978
|
+
if (body < 1) continue;
|
|
3979
|
+
if (isBlankLine(page.body[body - 1])) continue;
|
|
3980
|
+
evidenceByLine.set(body, resolveLineEvidence(line, ctx));
|
|
3981
|
+
}
|
|
3982
|
+
const groups = [];
|
|
3983
|
+
let open;
|
|
3984
|
+
for (let n = 1; n <= page.body.length; n++) {
|
|
3985
|
+
if (isBlankLine(page.body[n - 1])) continue;
|
|
3986
|
+
const evidence = evidenceByLine.get(n);
|
|
3987
|
+
const machine = evidence?.machine;
|
|
3988
|
+
if (open !== void 0 && machine !== void 0 && machine === open.machine) {
|
|
3989
|
+
open.end = n;
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
if (open !== void 0) groups.push(open);
|
|
3993
|
+
open = machine === void 0 || evidence === void 0 ? void 0 : { machine, start: n, end: n, first: evidence };
|
|
3994
|
+
}
|
|
3995
|
+
if (open !== void 0) groups.push(open);
|
|
3996
|
+
return {
|
|
3997
|
+
page,
|
|
3998
|
+
evidenceByLine,
|
|
3999
|
+
derived: groups.map((g) => {
|
|
4000
|
+
const span = { start: g.start, end: g.end };
|
|
4001
|
+
return {
|
|
4002
|
+
entry: { "generated-by": g.machine, lines: lineSpec(span), integrity: pinOf(page.body, span) },
|
|
4003
|
+
span,
|
|
4004
|
+
evidence: g.first
|
|
4005
|
+
};
|
|
4006
|
+
})
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
function nearest(candidates, recorded) {
|
|
4010
|
+
let best;
|
|
4011
|
+
for (const c of candidates) {
|
|
4012
|
+
const d = Math.abs(c.span.start - recorded.start);
|
|
4013
|
+
if (best === void 0 || d < best.d || d === best.d && c.span.start < best.c.span.start) best = { c, d };
|
|
4014
|
+
}
|
|
4015
|
+
return best?.c;
|
|
4016
|
+
}
|
|
4017
|
+
var sameSpan = (a, b) => a.start === b.start && a.end === b.end;
|
|
4018
|
+
function scanEvidence(evidenceByLine, span, machine) {
|
|
4019
|
+
let anyMachine = false;
|
|
4020
|
+
for (let n = span.start; n <= span.end; n++) {
|
|
4021
|
+
const evidence = evidenceByLine.get(n);
|
|
4022
|
+
if (evidence?.machine === void 0) continue;
|
|
4023
|
+
anyMachine = true;
|
|
4024
|
+
if (evidence.machine !== machine) return { contradiction: evidence, anyMachine };
|
|
4025
|
+
}
|
|
4026
|
+
return { anyMachine };
|
|
4027
|
+
}
|
|
4028
|
+
function compareProvenance(stamped, derivation) {
|
|
4029
|
+
const { page, evidenceByLine, derived } = derivation;
|
|
4030
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4031
|
+
const results = [];
|
|
4032
|
+
for (const entry of provenanceEntries(stamped)) {
|
|
4033
|
+
const recorded = parseLines(entry.lines);
|
|
4034
|
+
if (recorded === void 0) continue;
|
|
4035
|
+
const machine = entry["generated-by"];
|
|
4036
|
+
const takenHere = [...claimed].some((d) => sameSpan(d.span, recorded));
|
|
4037
|
+
if (!takenHere && pinOfLines(page.body, recorded) === entry.integrity) {
|
|
4038
|
+
const here = scanEvidence(evidenceByLine, recorded, machine);
|
|
4039
|
+
const blank2 = blankEdges(page.body, recorded);
|
|
4040
|
+
if (here.contradiction === void 0 && blank2 !== void 0) {
|
|
4041
|
+
results.push({ status: "stale", entry, span: recorded, blank: blank2 });
|
|
4042
|
+
continue;
|
|
4043
|
+
}
|
|
4044
|
+
if (here.contradiction === void 0) {
|
|
4045
|
+
const exact = derived.find(
|
|
4046
|
+
(d) => !claimed.has(d) && sameSpan(d.span, recorded) && d.entry.integrity === entry.integrity
|
|
4047
|
+
);
|
|
4048
|
+
if (exact !== void 0) claimed.add(exact);
|
|
4049
|
+
results.push({ status: "current", entry, span: recorded, ...here.anyMachine ? {} : { noEvidence: true } });
|
|
4050
|
+
continue;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const pool = derived.filter((d) => !claimed.has(d) && d.entry.integrity === entry.integrity);
|
|
4054
|
+
const hit = nearest(pool, recorded);
|
|
4055
|
+
if (hit !== void 0) {
|
|
4056
|
+
claimed.add(hit);
|
|
4057
|
+
if (hit.entry["generated-by"] !== machine) {
|
|
4058
|
+
results.push({ status: "stale", entry, span: hit.span, evidence: hit.evidence });
|
|
4059
|
+
} else {
|
|
4060
|
+
results.push({ status: sameSpan(hit.span, recorded) ? "current" : "moved", entry, span: hit.span });
|
|
4061
|
+
}
|
|
4062
|
+
continue;
|
|
4063
|
+
}
|
|
4064
|
+
const width = recorded.end - recorded.start + 1;
|
|
4065
|
+
const claimedSpans = [...claimed].map((d) => d.span);
|
|
4066
|
+
const windows = findWindows(page.body, width, entry.integrity, void 0, { around: recorded.start, budget: Infinity }).starts.map((start) => ({ span: { start, end: start + width - 1 } })).filter((w) => !claimedSpans.some((s) => sameSpan(s, w.span)));
|
|
4067
|
+
const found = nearest(windows, recorded);
|
|
4068
|
+
if (found === void 0) {
|
|
4069
|
+
results.push({ status: "changed", entry, span: recorded });
|
|
4070
|
+
continue;
|
|
4071
|
+
}
|
|
4072
|
+
const { contradiction, anyMachine } = scanEvidence(evidenceByLine, found.span, machine);
|
|
4073
|
+
if (contradiction !== void 0) {
|
|
4074
|
+
results.push({ status: "stale", entry, span: found.span, evidence: contradiction });
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
const blank = blankEdges(page.body, found.span);
|
|
4078
|
+
if (blank !== void 0) {
|
|
4079
|
+
results.push({ status: "stale", entry, span: found.span, blank });
|
|
4080
|
+
continue;
|
|
4081
|
+
}
|
|
4082
|
+
results.push({
|
|
4083
|
+
status: sameSpan(found.span, recorded) ? "current" : "moved",
|
|
4084
|
+
entry,
|
|
4085
|
+
span: found.span,
|
|
4086
|
+
...anyMachine ? {} : { noEvidence: true }
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
const covering = results.map((r) => r.span);
|
|
4090
|
+
for (const d of derived) {
|
|
4091
|
+
if (claimed.has(d)) continue;
|
|
4092
|
+
let uncovered = false;
|
|
4093
|
+
for (let n = d.span.start; n <= d.span.end && !uncovered; n++) {
|
|
4094
|
+
if (isBlankLine(page.body[n - 1])) continue;
|
|
4095
|
+
uncovered = !covering.some((s) => n >= s.start && n <= s.end);
|
|
4096
|
+
}
|
|
4097
|
+
if (uncovered) results.push({ status: "unset", entry: d.entry, span: d.span, evidence: d.evidence });
|
|
4098
|
+
}
|
|
4099
|
+
return results;
|
|
4100
|
+
}
|
|
4101
|
+
function planProvenanceWrite(comparisons, derivation) {
|
|
4102
|
+
const { body } = derivation.page;
|
|
4103
|
+
const same = (a, b) => a["generated-by"] === b["generated-by"] && String(a.lines) === String(b.lines) && a.integrity === b.integrity;
|
|
4104
|
+
const kept = [];
|
|
4105
|
+
const keep = (entry, span) => {
|
|
4106
|
+
if (!kept.some((k) => same(k.entry, entry))) kept.push({ entry, span });
|
|
4107
|
+
};
|
|
4108
|
+
for (const r of comparisons) {
|
|
4109
|
+
if (r.status === "current") keep(r.entry, r.span);
|
|
4110
|
+
else if (r.status === "moved") keep({ ...r.entry, lines: lineSpec(r.span) }, r.span);
|
|
4111
|
+
else if (r.status === "stale" && r.blank !== void 0) {
|
|
4112
|
+
const span = trimBlank(body, r.span);
|
|
4113
|
+
if (span !== void 0) keep({ ...r.entry, lines: lineSpec(span), integrity: pinOf(body, span) }, span);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
const outside = (k, d) => {
|
|
4117
|
+
if (k.span.end < d.start || k.span.start > d.end) return [k];
|
|
4118
|
+
const pieces = [];
|
|
4119
|
+
if (k.span.start < d.start) pieces.push({ start: k.span.start, end: d.start - 1 });
|
|
4120
|
+
if (k.span.end > d.end) pieces.push({ start: d.end + 1, end: k.span.end });
|
|
4121
|
+
return pieces.flatMap((piece) => {
|
|
4122
|
+
const span = trimBlank(body, piece);
|
|
4123
|
+
if (span === void 0) return [];
|
|
4124
|
+
return [{ entry: { ...k.entry, lines: lineSpec(span), integrity: pinOf(body, span) }, span }];
|
|
4125
|
+
});
|
|
4126
|
+
};
|
|
4127
|
+
let out = [...kept];
|
|
4128
|
+
const derived = /* @__PURE__ */ new Set();
|
|
4129
|
+
for (const d of derivation.derived) {
|
|
4130
|
+
if (kept.some((k) => same(k.entry, d.entry))) continue;
|
|
4131
|
+
out = out.flatMap((k) => derived.has(k) ? [k] : outside(k, d.span));
|
|
4132
|
+
const written = { entry: d.entry, span: d.span };
|
|
4133
|
+
derived.add(written);
|
|
4134
|
+
out.push(written);
|
|
4135
|
+
}
|
|
4136
|
+
const unique = [];
|
|
4137
|
+
for (const k of out) if (!unique.some((u) => same(u.entry, k.entry))) unique.push(k);
|
|
4138
|
+
return unique.sort((a, b) => a.span.start - b.span.start).map((k) => k.entry);
|
|
4139
|
+
}
|
|
4140
|
+
var short = (sha) => sha.slice(0, 7);
|
|
4141
|
+
function fileSpelling(span, bodyLine) {
|
|
4142
|
+
return spellLines(toFileLines(span, bodyLine));
|
|
4143
|
+
}
|
|
4144
|
+
function evidenceText(evidence) {
|
|
4145
|
+
if (evidence?.machine === void 0) return "pin";
|
|
4146
|
+
return evidence.rule === 1 ? "uncommitted" : `blame ${short(evidence.sha)}`;
|
|
4147
|
+
}
|
|
4148
|
+
function evidenceRef(evidence) {
|
|
4149
|
+
if (evidence === void 0) return "no evidence";
|
|
4150
|
+
return evidence.rule === 1 ? "uncommitted" : short(evidence.sha);
|
|
4151
|
+
}
|
|
4152
|
+
function rangeResults(comparisons, bodyLine) {
|
|
4153
|
+
return comparisons.map((r) => {
|
|
4154
|
+
const lines = fileSpelling(r.span, bodyLine);
|
|
4155
|
+
const recorded = parseLines(r.entry.lines);
|
|
4156
|
+
const from = recorded === void 0 ? void 0 : fileSpelling(recorded, bodyLine);
|
|
4157
|
+
const machine = r.entry["generated-by"];
|
|
4158
|
+
switch (r.status) {
|
|
4159
|
+
case "stale":
|
|
4160
|
+
return {
|
|
4161
|
+
lines,
|
|
4162
|
+
"generated-by": r.evidence?.machine ?? machine,
|
|
4163
|
+
integrity: r.entry.integrity,
|
|
4164
|
+
status: r.status,
|
|
4165
|
+
evidence: evidenceText(r.evidence),
|
|
4166
|
+
...from !== void 0 ? { from: { lines: from, "generated-by": machine } } : {},
|
|
4167
|
+
written: false
|
|
4168
|
+
};
|
|
4169
|
+
case "moved":
|
|
4170
|
+
return {
|
|
4171
|
+
lines,
|
|
4172
|
+
"generated-by": machine,
|
|
4173
|
+
integrity: r.entry.integrity,
|
|
4174
|
+
status: r.status,
|
|
4175
|
+
evidence: "pin",
|
|
4176
|
+
...from !== void 0 ? { from: { lines: from, "generated-by": machine } } : {},
|
|
4177
|
+
written: false
|
|
4178
|
+
};
|
|
4179
|
+
case "unset":
|
|
4180
|
+
return { lines, "generated-by": machine, integrity: r.entry.integrity, status: r.status, evidence: evidenceText(r.evidence), written: false };
|
|
4181
|
+
case "current":
|
|
4182
|
+
case "changed":
|
|
4183
|
+
return { lines, "generated-by": machine, integrity: r.entry.integrity, status: r.status, evidence: "pin", written: false };
|
|
4184
|
+
}
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
var BLANK_WORDS = {
|
|
4188
|
+
only: "hold only blank lines",
|
|
4189
|
+
start: "start on a blank line",
|
|
4190
|
+
end: "end on a blank line",
|
|
4191
|
+
both: "start and end on blank lines"
|
|
4192
|
+
};
|
|
4193
|
+
function provenanceFindings(comparisons, bodyLine) {
|
|
4194
|
+
const findings = [];
|
|
4195
|
+
for (const r of comparisons) {
|
|
4196
|
+
const machine = r.entry["generated-by"];
|
|
4197
|
+
const lines = fileSpelling(r.span, bodyLine);
|
|
4198
|
+
let message;
|
|
4199
|
+
switch (r.status) {
|
|
4200
|
+
case "changed":
|
|
4201
|
+
message = `provenance lines ${lines} changed since ${machine} wrote them`;
|
|
4202
|
+
break;
|
|
4203
|
+
case "stale":
|
|
4204
|
+
message = r.blank !== void 0 ? `provenance lines ${lines} say ${machine} but ${BLANK_WORDS[r.blank]}` : `provenance lines ${lines} say ${machine}; blame says ${r.evidence?.machine ?? "another machine"} (${evidenceRef(r.evidence)})`;
|
|
4205
|
+
break;
|
|
4206
|
+
case "unset":
|
|
4207
|
+
message = `provenance is unset for lines ${lines}; blame says ${machine} (${evidenceRef(r.evidence)})`;
|
|
4208
|
+
break;
|
|
4209
|
+
case "current":
|
|
4210
|
+
case "moved":
|
|
4211
|
+
continue;
|
|
4212
|
+
}
|
|
4213
|
+
findings.push({
|
|
4214
|
+
schema: DERIVED_STALE_SCHEMA,
|
|
4215
|
+
keyword: DERIVED_KEYWORD,
|
|
4216
|
+
subject: `provenance ${r.entry.integrity}`,
|
|
4217
|
+
instancePath: PROVENANCE_POINTER,
|
|
4218
|
+
message: `${message} \u2014 run manni meta derive`,
|
|
4219
|
+
line: toFileLines(r.span, bodyLine).start
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
return findings;
|
|
4223
|
+
}
|
|
4224
|
+
function renderProvenanceValue(entries, bodyLine) {
|
|
4225
|
+
return entries.map((e) => {
|
|
4226
|
+
const span = parseLines(e.lines);
|
|
4227
|
+
const lines = span === void 0 ? String(e.lines) : fileSpelling(span, bodyLine);
|
|
4228
|
+
return `lines ${lines} ${e["generated-by"]}`;
|
|
4229
|
+
}).join("; ");
|
|
4230
|
+
}
|
|
4231
|
+
function parseProvenanceTarget(arg) {
|
|
4232
|
+
try {
|
|
4233
|
+
return splitPageArgument(arg);
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
if (err instanceof LineRangeError) throw new DocmetaError(`${arg} ends before it starts.`);
|
|
4236
|
+
throw err;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
function attributeRange(input) {
|
|
4240
|
+
const { target } = input;
|
|
4241
|
+
const { page: path, lines: range } = parseProvenanceTarget(target);
|
|
4242
|
+
if (range === void 0) throw new Error(`provenance: ${target} names no lines`);
|
|
4243
|
+
const page = input.page ?? readProvenancePage(input.content, readOptions(input.fenced));
|
|
4244
|
+
if (range.end > page.lines.length) {
|
|
4245
|
+
throw new DocmetaError(
|
|
4246
|
+
`${path} has no lines ${spellLines(range)}: the file ends at line ${page.lines.length}.`
|
|
4247
|
+
);
|
|
4248
|
+
}
|
|
4249
|
+
if (range.start < page.bodyLine) {
|
|
4250
|
+
throw new DocmetaError(
|
|
4251
|
+
`${target} reaches into the frontmatter; provenance pins body lines, which start at line ${page.bodyLine}.`
|
|
4252
|
+
);
|
|
4253
|
+
}
|
|
4254
|
+
const ctx = contextOf(input);
|
|
4255
|
+
for (const line of input.blame) {
|
|
4256
|
+
if (line.finalLine < range.start || line.finalLine > range.end) continue;
|
|
4257
|
+
if (isBlankLine(page.lines[line.finalLine - 1])) continue;
|
|
4258
|
+
const evidence = recordedEvidence(line, ctx);
|
|
4259
|
+
if (evidence.machine !== void 0 && evidence.machine !== input.generatedBy) {
|
|
4260
|
+
throw new DocmetaError(
|
|
4261
|
+
`${target}: blame attributes these lines to ${evidence.machine} (${short(evidence.sha)}); --generated-by cannot overrule a recorded machine.`
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
const span = trimBlank(page.body, toBodyLines(range, page.bodyLine));
|
|
4266
|
+
if (span === void 0) return void 0;
|
|
4267
|
+
return { "generated-by": input.generatedBy, lines: lineSpec(span), integrity: pinOf(page.body, span) };
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
export {
|
|
4271
|
+
writeTextAtomic,
|
|
4272
|
+
ENCRYPTION_KEY_ENV,
|
|
4273
|
+
resolveEncryptionKey,
|
|
4274
|
+
writeEncryptionKey,
|
|
4275
|
+
isIgnoredByGit,
|
|
4276
|
+
committedKeyWarning,
|
|
4277
|
+
heldManifest,
|
|
4278
|
+
rebaseable,
|
|
4279
|
+
rebaseline,
|
|
4280
|
+
settle,
|
|
4281
|
+
isMissing,
|
|
4282
|
+
readManifestOrEmpty,
|
|
4283
|
+
EXTERNAL_METADATA_FETCH_TIMEOUT_MS,
|
|
4284
|
+
EXTERNAL_METADATA_FETCH_MAX_BYTES,
|
|
4285
|
+
fetchExternalMetadata,
|
|
4286
|
+
ENCRYPTED_PLACEHOLDER,
|
|
4287
|
+
META_CONTEXT,
|
|
4288
|
+
EncryptionRefusal,
|
|
4289
|
+
unreadableMessage,
|
|
4290
|
+
unverifiedWarning,
|
|
4291
|
+
lazyKey,
|
|
4292
|
+
pointerOf,
|
|
4293
|
+
topLevelKeyOf,
|
|
4294
|
+
isAtOrUnder,
|
|
4295
|
+
valueAt,
|
|
4296
|
+
withValueAt,
|
|
4297
|
+
encryptionView,
|
|
4298
|
+
settleFindings,
|
|
4299
|
+
encryptionFindings,
|
|
4300
|
+
redactForModel,
|
|
4301
|
+
redactedDerived,
|
|
4302
|
+
EXTERNAL_OWNED_SCHEMA,
|
|
4303
|
+
EXTERNAL_DUPLICATE_SCHEMA,
|
|
4304
|
+
EXTERNAL_KEYWORD,
|
|
4305
|
+
PATH_JOIN,
|
|
4306
|
+
reportedPath,
|
|
4307
|
+
externalMetadataJoin,
|
|
4308
|
+
loadExternalMetadata,
|
|
4309
|
+
outsideRefusal,
|
|
4310
|
+
mergeExternalMetadata,
|
|
4311
|
+
mergeWithMarks,
|
|
4312
|
+
externalMetadataPointer,
|
|
4313
|
+
orphanEntries,
|
|
4314
|
+
orphanJoins,
|
|
4315
|
+
orphanError,
|
|
4316
|
+
pageOwners,
|
|
4317
|
+
LOCATION_KEYWORD,
|
|
4318
|
+
LOCATION_EXTERNAL_SCHEMA,
|
|
4319
|
+
LOCATION_PAGE_SCHEMA,
|
|
4320
|
+
LOCATION_FINDING_KEYWORD,
|
|
4321
|
+
GRAPH_OUTPUT_KEYWORD,
|
|
4322
|
+
compileWithFormats,
|
|
4323
|
+
stringFormatNames,
|
|
4324
|
+
validatesFormat,
|
|
4325
|
+
Validator,
|
|
4326
|
+
pageMarks,
|
|
4327
|
+
pageClaims,
|
|
4328
|
+
pageSchemaSet,
|
|
4329
|
+
marksValidator,
|
|
4330
|
+
configMarks,
|
|
4331
|
+
metaSection,
|
|
4332
|
+
familyMarks,
|
|
4333
|
+
SYSTEM_COLUMNS,
|
|
4334
|
+
RESERVED,
|
|
4335
|
+
quoteIdent,
|
|
4336
|
+
bindValue,
|
|
4337
|
+
corpusDataColumns,
|
|
4338
|
+
createDocsTable,
|
|
4339
|
+
registerLineFor,
|
|
4340
|
+
assertSingleStatement,
|
|
4341
|
+
startsStringOrIdent,
|
|
4342
|
+
startsComment,
|
|
4343
|
+
skipStringOrIdent,
|
|
4344
|
+
skipComment,
|
|
4345
|
+
isParamName,
|
|
4346
|
+
collectNamedParameters,
|
|
4347
|
+
stripLeadingTrivia,
|
|
4348
|
+
loadSqlite,
|
|
4349
|
+
memberOf,
|
|
4350
|
+
retainMembers,
|
|
4351
|
+
collectionNames,
|
|
4352
|
+
collectCollections,
|
|
4353
|
+
createCollectionViews,
|
|
4354
|
+
spliceManifestValue,
|
|
4355
|
+
readManifestValue,
|
|
4356
|
+
removeManifestKey,
|
|
4357
|
+
renameManifestEntry,
|
|
4358
|
+
relocateFailed,
|
|
4359
|
+
relocationContext,
|
|
4360
|
+
keyHome,
|
|
4361
|
+
planRelocation,
|
|
4362
|
+
applyRelocation,
|
|
4363
|
+
offerPrompt,
|
|
4364
|
+
relocationOffers,
|
|
4365
|
+
LineRangeError,
|
|
4366
|
+
normalizeText,
|
|
4367
|
+
splitLines,
|
|
4368
|
+
sliceLines,
|
|
4369
|
+
hashRange,
|
|
4370
|
+
hashLines,
|
|
4371
|
+
isKeyedPin,
|
|
4372
|
+
pinOfLines,
|
|
4373
|
+
MOVE_WINDOW_LINES,
|
|
4374
|
+
MOVE_BUDGET_BYTES,
|
|
4375
|
+
MAX_RANGE_LINES,
|
|
4376
|
+
findWindows,
|
|
4377
|
+
parseLines,
|
|
4378
|
+
lineSpec,
|
|
4379
|
+
spellLines,
|
|
4380
|
+
toFileLines,
|
|
4381
|
+
toBodyLines,
|
|
4382
|
+
splitPageArgument,
|
|
4383
|
+
ZERO_SHA,
|
|
4384
|
+
DEFAULT_MACHINES,
|
|
4385
|
+
provenanceEntries,
|
|
4386
|
+
readProvenancePage,
|
|
4387
|
+
parseLinePorcelain,
|
|
4388
|
+
machineIdentity,
|
|
4389
|
+
deriveProvenance,
|
|
4390
|
+
compareProvenance,
|
|
4391
|
+
planProvenanceWrite,
|
|
4392
|
+
rangeResults,
|
|
4393
|
+
provenanceFindings,
|
|
4394
|
+
renderProvenanceValue,
|
|
4395
|
+
parseProvenanceTarget,
|
|
4396
|
+
attributeRange
|
|
4397
|
+
};
|
|
4398
|
+
//# sourceMappingURL=chunk-RLR4TKUZ.js.map
|