@hawkeyexl/manni 4.3.1 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/batch-GFNLTGOH.js +36 -0
- package/dist/build-COYIQ25L.js +31 -0
- package/dist/calibrate-MDBLJKEE.js +31 -0
- package/dist/capture-LRML2PKV.js +32 -0
- package/dist/check-ASPIHA6B.js +40 -0
- package/dist/check-ASPIHA6B.js.map +1 -0
- package/dist/check-EDLBYL5N.js +27 -0
- package/dist/check-EDLBYL5N.js.map +1 -0
- package/dist/check-HYNGXYJV.js +18 -0
- package/dist/check-HYNGXYJV.js.map +1 -0
- package/dist/chunk-2B7ZNTPV.js +230 -0
- package/dist/chunk-2B7ZNTPV.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-37LP5HMZ.js +1042 -0
- package/dist/chunk-37LP5HMZ.js.map +1 -0
- package/dist/chunk-3VK37OR2.js +2957 -0
- package/dist/chunk-3VK37OR2.js.map +1 -0
- package/dist/chunk-4IUC2MNL.js +2165 -0
- package/dist/chunk-4IUC2MNL.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-57NEQRVO.js +10921 -0
- package/dist/chunk-57NEQRVO.js.map +1 -0
- package/dist/chunk-6JSSEWBG.js +6737 -0
- package/dist/chunk-6JSSEWBG.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-7UKO3ASZ.js +54 -0
- package/dist/chunk-7UKO3ASZ.js.map +1 -0
- package/dist/chunk-A5ULMVHQ.js +164 -0
- package/dist/chunk-A5ULMVHQ.js.map +1 -0
- package/dist/chunk-C4SQM22E.js +299 -0
- package/dist/chunk-C4SQM22E.js.map +1 -0
- package/dist/chunk-CK7SKVHR.js +1042 -0
- package/dist/chunk-CK7SKVHR.js.map +1 -0
- package/dist/{chunk-G5CM2VGQ.js → chunk-DXNZS72W.js} +8 -94
- package/dist/chunk-DXNZS72W.js.map +1 -0
- package/dist/chunk-EPH54CAY.js +374 -0
- package/dist/chunk-EPH54CAY.js.map +1 -0
- package/dist/{chunk-KJR7OVDX.js → chunk-FBEQCK47.js} +56 -137
- package/dist/chunk-FBEQCK47.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-GCQRPUSB.js +1287 -0
- package/dist/chunk-GCQRPUSB.js.map +1 -0
- package/dist/chunk-GLP5PSXB.js +72 -0
- package/dist/chunk-GLP5PSXB.js.map +1 -0
- package/dist/chunk-I2AH3CJJ.js +1192 -0
- package/dist/chunk-I2AH3CJJ.js.map +1 -0
- package/dist/chunk-IDV6HJCB.js +388 -0
- package/dist/chunk-IDV6HJCB.js.map +1 -0
- package/dist/chunk-IKEWBMCN.js +98 -0
- package/dist/chunk-IKEWBMCN.js.map +1 -0
- package/dist/chunk-JCDXE76J.js +52 -0
- package/dist/chunk-JCDXE76J.js.map +1 -0
- package/dist/chunk-JXLTXMJ7.js +455 -0
- package/dist/chunk-JXLTXMJ7.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-M3W7J6MU.js +4398 -0
- package/dist/chunk-M3W7J6MU.js.map +1 -0
- package/dist/chunk-MMTSSTG7.js +23 -0
- package/dist/chunk-MMTSSTG7.js.map +1 -0
- package/dist/chunk-MNH7QO4Z.js +15 -0
- package/dist/chunk-MNH7QO4Z.js.map +1 -0
- package/dist/chunk-MWJLZMBL.js +934 -0
- package/dist/chunk-MWJLZMBL.js.map +1 -0
- 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-NLXTXE5Y.js +1661 -0
- package/dist/chunk-NLXTXE5Y.js.map +1 -0
- package/dist/{chunk-CFDUZRM2.js → chunk-NXV7WE2D.js} +32 -240
- package/dist/chunk-NXV7WE2D.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-PIKOVN6W.js +1651 -0
- package/dist/chunk-PIKOVN6W.js.map +1 -0
- package/dist/chunk-PNPQTHBT.js +17 -0
- package/dist/chunk-PNPQTHBT.js.map +1 -0
- package/dist/chunk-PU6DFJMY.js +1975 -0
- package/dist/chunk-PU6DFJMY.js.map +1 -0
- package/dist/chunk-Q35YIDBV.js +1202 -0
- package/dist/chunk-Q35YIDBV.js.map +1 -0
- package/dist/{chunk-5HNHEUD5.js → chunk-QQUKGCAJ.js} +3 -3
- package/dist/chunk-QRWGPQKO.js +198 -0
- package/dist/chunk-QRWGPQKO.js.map +1 -0
- package/dist/{chunk-UBMMXS2Y.js → chunk-QZ6LRSNV.js} +69 -48
- package/dist/{chunk-UBMMXS2Y.js.map → chunk-QZ6LRSNV.js.map} +1 -1
- package/dist/chunk-RL5XWQOL.js +3872 -0
- package/dist/chunk-RL5XWQOL.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-SKSDRUUK.js +622 -0
- package/dist/chunk-SKSDRUUK.js.map +1 -0
- package/dist/chunk-TIUTIOFB.js +25 -0
- package/dist/chunk-TIUTIOFB.js.map +1 -0
- package/dist/chunk-TK3FC4RF.js +3090 -0
- package/dist/chunk-TK3FC4RF.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-UJCVAWP7.js +178 -0
- package/dist/chunk-UJCVAWP7.js.map +1 -0
- package/dist/chunk-UKZNLVJW.js +17 -0
- package/dist/chunk-UKZNLVJW.js.map +1 -0
- package/dist/{chunk-HAPTXWU2.js → chunk-UQISLDAV.js} +4 -4
- package/dist/chunk-WB3QM6PP.js +5440 -0
- package/dist/chunk-WB3QM6PP.js.map +1 -0
- package/dist/chunk-YB4IBW3M.js +96 -0
- package/dist/chunk-YB4IBW3M.js.map +1 -0
- package/dist/chunk-YEUYKR62.js +72 -0
- package/dist/chunk-YEUYKR62.js.map +1 -0
- package/dist/chunk-YJOGUXGP.js +634 -0
- package/dist/chunk-YJOGUXGP.js.map +1 -0
- package/dist/chunk-YNHZHSSM.js +41 -0
- package/dist/chunk-YNHZHSSM.js.map +1 -0
- package/dist/chunk-YTURO7SI.js +979 -0
- package/dist/chunk-YTURO7SI.js.map +1 -0
- package/dist/chunk-ZXRAE55H.js +31 -0
- package/dist/chunk-ZXRAE55H.js.map +1 -0
- package/dist/cli-3BIBYHFK.js +473 -0
- package/dist/cli-3BIBYHFK.js.map +1 -0
- package/dist/cli-673OTRGE.js +28 -0
- package/dist/cli-673OTRGE.js.map +1 -0
- package/dist/cli-6HIDIFVT.js +1203 -0
- package/dist/cli-6HIDIFVT.js.map +1 -0
- package/dist/cli-6UB5N6CU.js +232 -0
- package/dist/cli-6UB5N6CU.js.map +1 -0
- package/dist/cli-LFP5ZGYS.js +346 -0
- package/dist/cli-LFP5ZGYS.js.map +1 -0
- package/dist/cli-MN7PYMBI.js +552 -0
- package/dist/cli-MN7PYMBI.js.map +1 -0
- package/dist/cli-PYPX6O6E.js +739 -0
- package/dist/cli-PYPX6O6E.js.map +1 -0
- package/dist/cli-RPM5PELX.js +626 -0
- package/dist/cli-RPM5PELX.js.map +1 -0
- package/dist/cli-VI7VHWNZ.js +423 -0
- package/dist/cli-VI7VHWNZ.js.map +1 -0
- package/dist/cli.d.ts +12 -2
- package/dist/cli.js +109 -7661
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +15 -3
- package/dist/docmeta.js.map +1 -1
- package/dist/github-6QMYOHC3.js +27 -0
- package/dist/github-6QMYOHC3.js.map +1 -0
- package/dist/github-SXPGP5DB.js +32 -0
- package/dist/github-SXPGP5DB.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-SY7KLNC7.js +35 -0
- package/dist/json-SY7KLNC7.js.map +1 -0
- package/dist/lint-ASMJLLPA.js +28 -0
- package/dist/lint-ASMJLLPA.js.map +1 -0
- package/dist/list-KZNVTNVC.js +31 -0
- package/dist/list-KZNVTNVC.js.map +1 -0
- package/dist/list-Q57YTFUQ.js +29 -0
- package/dist/list-Q57YTFUQ.js.map +1 -0
- package/dist/page-KSGV5RD4.js +41 -0
- package/dist/page-KSGV5RD4.js.map +1 -0
- package/dist/picker-3XF36MJF.js +14 -0
- package/dist/picker-3XF36MJF.js.map +1 -0
- package/dist/pretty-A5EGTN35.js +37 -0
- package/dist/pretty-A5EGTN35.js.map +1 -0
- package/dist/pretty-CXSDXNAS.js +43 -0
- package/dist/pretty-CXSDXNAS.js.map +1 -0
- package/dist/reporters-HUY7UXFS.js +42 -0
- package/dist/reporters-HUY7UXFS.js.map +1 -0
- package/dist/reporters-K2VUKSAK.js +71 -0
- package/dist/reporters-K2VUKSAK.js.map +1 -0
- package/dist/reporters-M6LJ2IYU.js +21 -0
- package/dist/reporters-M6LJ2IYU.js.map +1 -0
- package/dist/run-3DYJNRIG.js +33 -0
- package/dist/run-3DYJNRIG.js.map +1 -0
- package/dist/term-BIQUPMOR.js +1592 -0
- package/dist/term-BIQUPMOR.js.map +1 -0
- package/dist/types-Y6TXHM7M.js +10 -0
- package/dist/types-Y6TXHM7M.js.map +1 -0
- package/dist/validate-53RKM2XG.js +16 -0
- package/dist/validate-53RKM2XG.js.map +1 -0
- package/package.json +1 -1
- package/plugin/manni/.claude-plugin/plugin.json +9 -0
- package/plugin/manni/hooks/hooks.json +69 -0
- package/plugin/manni/hooks/manni.mjs +157 -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-A5HZKYIW.js +0 -18
- package/dist/calibrate-S5K6QHUY.js +0 -13
- package/dist/capture-OZEZDABQ.js +0 -15
- package/dist/chunk-3R23C5DT.js.map +0 -1
- package/dist/chunk-CFDUZRM2.js.map +0 -1
- package/dist/chunk-G5CM2VGQ.js.map +0 -1
- package/dist/chunk-KJR7OVDX.js.map +0 -1
- package/dist/chunk-V4KUVE47.js +0 -27170
- package/dist/chunk-V4KUVE47.js.map +0 -1
- package/dist/chunk-ZQBTK3DO.js +0 -26518
- package/dist/chunk-ZQBTK3DO.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-A5HZKYIW.js.map → batch-GFNLTGOH.js.map} +0 -0
- /package/dist/{calibrate-S5K6QHUY.js.map → build-COYIQ25L.js.map} +0 -0
- /package/dist/{capture-OZEZDABQ.js.map → calibrate-MDBLJKEE.js.map} +0 -0
- /package/dist/{picker-6HRMETE7.js.map → capture-LRML2PKV.js.map} +0 -0
- /package/dist/{chunk-5HNHEUD5.js.map → chunk-QQUKGCAJ.js.map} +0 -0
- /package/dist/{chunk-HAPTXWU2.js.map → chunk-UQISLDAV.js.map} +0 -0
|
@@ -0,0 +1,3090 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MACHINES,
|
|
4
|
+
EXTERNAL_DUPLICATE_SCHEMA,
|
|
5
|
+
EXTERNAL_KEYWORD,
|
|
6
|
+
EXTERNAL_OWNED_SCHEMA,
|
|
7
|
+
LOCATION_EXTERNAL_SCHEMA,
|
|
8
|
+
LOCATION_FINDING_KEYWORD,
|
|
9
|
+
LOCATION_PAGE_SCHEMA,
|
|
10
|
+
PATH_JOIN,
|
|
11
|
+
RESERVED,
|
|
12
|
+
Validator,
|
|
13
|
+
ZERO_SHA,
|
|
14
|
+
assertSingleStatement,
|
|
15
|
+
attributeRange,
|
|
16
|
+
bindValue,
|
|
17
|
+
collectCollections,
|
|
18
|
+
collectNamedParameters,
|
|
19
|
+
compareProvenance,
|
|
20
|
+
corpusDataColumns,
|
|
21
|
+
createCollectionViews,
|
|
22
|
+
createDocsTable,
|
|
23
|
+
deriveProvenance,
|
|
24
|
+
encryptionFindings,
|
|
25
|
+
encryptionView,
|
|
26
|
+
externalMetadataJoin,
|
|
27
|
+
externalMetadataPointer,
|
|
28
|
+
isAtOrUnder,
|
|
29
|
+
keyHome,
|
|
30
|
+
lazyKey,
|
|
31
|
+
loadExternalMetadata,
|
|
32
|
+
loadSqlite,
|
|
33
|
+
machineIdentity,
|
|
34
|
+
memberOf,
|
|
35
|
+
mergeWithMarks,
|
|
36
|
+
orphanEntries,
|
|
37
|
+
orphanError,
|
|
38
|
+
orphanJoins,
|
|
39
|
+
outsideRefusal,
|
|
40
|
+
pageMarks,
|
|
41
|
+
parseLinePorcelain,
|
|
42
|
+
parseLines,
|
|
43
|
+
pointerOf,
|
|
44
|
+
provenanceEntries,
|
|
45
|
+
provenanceFindings,
|
|
46
|
+
quoteIdent,
|
|
47
|
+
readProvenancePage,
|
|
48
|
+
redactedDerived,
|
|
49
|
+
registerLineFor,
|
|
50
|
+
relocationContext,
|
|
51
|
+
reportedPath,
|
|
52
|
+
retainMembers,
|
|
53
|
+
settleFindings,
|
|
54
|
+
splitLines,
|
|
55
|
+
stripLeadingTrivia,
|
|
56
|
+
toFileLines,
|
|
57
|
+
unverifiedWarning
|
|
58
|
+
} from "./chunk-M3W7J6MU.js";
|
|
59
|
+
import {
|
|
60
|
+
CHECK_NAME,
|
|
61
|
+
DEFAULT_BASELINE_PATH,
|
|
62
|
+
DERIVE_SOURCES,
|
|
63
|
+
DocmetaError,
|
|
64
|
+
FILE_SCHEMA_KEY,
|
|
65
|
+
LEGACY_BASELINE_PATH,
|
|
66
|
+
PROVENANCE_FIELD,
|
|
67
|
+
STAMPED_DATE_FIELDS,
|
|
68
|
+
STDIN_LABEL,
|
|
69
|
+
STDIN_TOKEN,
|
|
70
|
+
assertNonEmpty,
|
|
71
|
+
canonicalSchemaRef,
|
|
72
|
+
classifyRef,
|
|
73
|
+
collectSchemaPins,
|
|
74
|
+
compareDerived,
|
|
75
|
+
derivableFields,
|
|
76
|
+
deriveCovers,
|
|
77
|
+
escapePointerSegment,
|
|
78
|
+
extractorByName,
|
|
79
|
+
extractorForExtension,
|
|
80
|
+
gitignoreOptions,
|
|
81
|
+
isBuiltinField,
|
|
82
|
+
isErrorSeverity,
|
|
83
|
+
locateFrontmatter,
|
|
84
|
+
managedFields,
|
|
85
|
+
resolveBaselineRequest,
|
|
86
|
+
resolveElements,
|
|
87
|
+
resolveRunConfig,
|
|
88
|
+
resolveSchemaSetWithSource,
|
|
89
|
+
resolveTargetSet,
|
|
90
|
+
schemaLoadOptions,
|
|
91
|
+
schemaTrustRoot,
|
|
92
|
+
settleBaseline,
|
|
93
|
+
staleFindings,
|
|
94
|
+
supportedExtensions,
|
|
95
|
+
toJsonText
|
|
96
|
+
} from "./chunk-57NEQRVO.js";
|
|
97
|
+
import {
|
|
98
|
+
findGitRoot,
|
|
99
|
+
hasPagePlaceholder,
|
|
100
|
+
ownsKey,
|
|
101
|
+
pageManifestPath
|
|
102
|
+
} from "./chunk-Q35YIDBV.js";
|
|
103
|
+
import {
|
|
104
|
+
errorMessage
|
|
105
|
+
} from "./chunk-G3T47CFV.js";
|
|
106
|
+
|
|
107
|
+
// src/meta/commands/validate.ts
|
|
108
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
109
|
+
import { resolve as resolve4, extname as extname2 } from "node:path";
|
|
110
|
+
|
|
111
|
+
// src/meta/core/derive/config.ts
|
|
112
|
+
var DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
|
|
113
|
+
function commandsOf(config) {
|
|
114
|
+
if (config?.commands === void 0) return void 0;
|
|
115
|
+
const out = {};
|
|
116
|
+
for (const [field, c] of Object.entries(config.commands)) {
|
|
117
|
+
out[field] = {
|
|
118
|
+
run: c.run,
|
|
119
|
+
timeoutMs: c.timeout === void 0 ? DEFAULT_COMMAND_TIMEOUT_MS : Math.round(c.timeout * 1e3)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function machinesOf(config) {
|
|
125
|
+
return config?.machines ?? DEFAULT_MACHINES;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/meta/core/derive/index.ts
|
|
129
|
+
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
130
|
+
|
|
131
|
+
// src/meta/core/derive/cache.ts
|
|
132
|
+
import { createHash } from "node:crypto";
|
|
133
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
134
|
+
import { join } from "node:path";
|
|
135
|
+
var REVIEW_CACHE_DIR = ".manni/meta/review-cache";
|
|
136
|
+
var REVIEW_CACHE_VERSION = 1;
|
|
137
|
+
var ReviewCache = class {
|
|
138
|
+
constructor(dir, enabled) {
|
|
139
|
+
this.dir = dir;
|
|
140
|
+
this.enabled = enabled;
|
|
141
|
+
}
|
|
142
|
+
dir;
|
|
143
|
+
enabled;
|
|
144
|
+
entryPath(key) {
|
|
145
|
+
const digest = createHash("sha256").update(key).digest("hex");
|
|
146
|
+
return join(this.dir, `${digest}.json`);
|
|
147
|
+
}
|
|
148
|
+
/** The cached change, or `undefined` for a miss. Never `null`: nulls are not stored. */
|
|
149
|
+
get(key) {
|
|
150
|
+
if (!this.enabled) return void 0;
|
|
151
|
+
let raw;
|
|
152
|
+
try {
|
|
153
|
+
raw = JSON.parse(readFileSync(this.entryPath(key), "utf8"));
|
|
154
|
+
} catch {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
if (!isRecord(raw)) return void 0;
|
|
158
|
+
if (raw.version !== REVIEW_CACHE_VERSION || raw.key !== key) return void 0;
|
|
159
|
+
const change = readChange(raw.change);
|
|
160
|
+
return change ?? void 0;
|
|
161
|
+
}
|
|
162
|
+
set(key, value) {
|
|
163
|
+
if (!this.enabled) return;
|
|
164
|
+
const entry = { version: REVIEW_CACHE_VERSION, key, change: value };
|
|
165
|
+
const path = this.entryPath(key);
|
|
166
|
+
const tmp = `${path}.${process.pid}-${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
167
|
+
try {
|
|
168
|
+
mkdirSync(this.dir, { recursive: true });
|
|
169
|
+
writeFileSync(tmp, `${JSON.stringify(entry, null, 2)}
|
|
170
|
+
`, "utf8");
|
|
171
|
+
renameSync(tmp, path);
|
|
172
|
+
} catch {
|
|
173
|
+
try {
|
|
174
|
+
rmSync(tmp, { force: true });
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function cachedClient(client, cache, identity) {
|
|
181
|
+
return {
|
|
182
|
+
detect: () => client.detect(),
|
|
183
|
+
status: () => client.status(),
|
|
184
|
+
async mergedChangeFor(sha) {
|
|
185
|
+
if (identity === null) return client.mergedChangeFor(sha);
|
|
186
|
+
const key = `${identity.host}/${identity.project}/${sha}`;
|
|
187
|
+
const hit = cache.get(key);
|
|
188
|
+
if (hit !== void 0) return hit;
|
|
189
|
+
const change = await client.mergedChangeFor(sha);
|
|
190
|
+
if (change !== null) cache.set(key, change);
|
|
191
|
+
return change;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function isRecord(v) {
|
|
196
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
197
|
+
}
|
|
198
|
+
function readChange(v) {
|
|
199
|
+
if (!isRecord(v)) return null;
|
|
200
|
+
const { id, mergedAt, approvals } = v;
|
|
201
|
+
if (typeof id !== "number" || typeof mergedAt !== "string") return null;
|
|
202
|
+
if (!Array.isArray(approvals)) return null;
|
|
203
|
+
const read = [];
|
|
204
|
+
for (const a of approvals) {
|
|
205
|
+
if (!isRecord(a)) return null;
|
|
206
|
+
if (typeof a.login !== "string" || typeof a.submittedAt !== "string") return null;
|
|
207
|
+
read.push({ login: a.login, submittedAt: a.submittedAt });
|
|
208
|
+
}
|
|
209
|
+
return { id, mergedAt, approvals: read };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/meta/core/derive/codeowners.ts
|
|
213
|
+
import { existsSync } from "node:fs";
|
|
214
|
+
import { readFile } from "node:fs/promises";
|
|
215
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
216
|
+
import picomatch from "picomatch";
|
|
217
|
+
var CODEOWNERS_LOCATIONS = [
|
|
218
|
+
".github/CODEOWNERS",
|
|
219
|
+
"CODEOWNERS",
|
|
220
|
+
"docs/CODEOWNERS",
|
|
221
|
+
".gitlab/CODEOWNERS"
|
|
222
|
+
];
|
|
223
|
+
function findCodeowners(root, explicit) {
|
|
224
|
+
if (explicit !== void 0) {
|
|
225
|
+
const path = resolve(root, explicit);
|
|
226
|
+
return existsSync(path) ? path : null;
|
|
227
|
+
}
|
|
228
|
+
for (const loc of CODEOWNERS_LOCATIONS) {
|
|
229
|
+
const path = resolve(root, ...loc.split("/"));
|
|
230
|
+
if (existsSync(path)) return path;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
var SECTION_HEADER = /^(\^?)\[([^\]]+)\](?:\[(\d+)\])?(.*)$/;
|
|
235
|
+
var TOKEN = /(?:\\.|[^\s\\])+/g;
|
|
236
|
+
function parseCodeowners(text2, path, label) {
|
|
237
|
+
const rules = [];
|
|
238
|
+
const sections = [];
|
|
239
|
+
let current;
|
|
240
|
+
const lines = text2.split(/\r?\n/);
|
|
241
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
242
|
+
const raw = lines[i] ?? "";
|
|
243
|
+
const line = raw.trim();
|
|
244
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
245
|
+
if (line.startsWith("[") || line.startsWith("^[")) {
|
|
246
|
+
const m = SECTION_HEADER.exec(line);
|
|
247
|
+
if (!m) continue;
|
|
248
|
+
const [, caret, rawName, approvals, rest] = m;
|
|
249
|
+
const name = (rawName ?? "").trim();
|
|
250
|
+
if (name === "") continue;
|
|
251
|
+
const owners2 = tokens(rest ?? "");
|
|
252
|
+
const existing = sections.find(
|
|
253
|
+
(s) => s.name.toLowerCase() === name.toLowerCase()
|
|
254
|
+
);
|
|
255
|
+
if (existing) {
|
|
256
|
+
if (owners2.length > 0) existing.defaultOwners = owners2;
|
|
257
|
+
current = existing;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const section = {
|
|
261
|
+
name,
|
|
262
|
+
optional: caret === "^",
|
|
263
|
+
...approvals !== void 0 ? { approvals: Number(approvals) } : {},
|
|
264
|
+
defaultOwners: owners2
|
|
265
|
+
};
|
|
266
|
+
sections.push(section);
|
|
267
|
+
current = section;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const [first, ...owners] = tokens(line);
|
|
271
|
+
if (first === void 0) continue;
|
|
272
|
+
const pattern = unescapePattern(first);
|
|
273
|
+
if (pattern === "" || pattern === "/") continue;
|
|
274
|
+
rules.push({
|
|
275
|
+
pattern,
|
|
276
|
+
owners,
|
|
277
|
+
line: i + 1,
|
|
278
|
+
...current ? { section: current.name } : {}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return { path, label, rules, sections };
|
|
282
|
+
}
|
|
283
|
+
function tokens(s) {
|
|
284
|
+
return s.match(TOKEN) ?? [];
|
|
285
|
+
}
|
|
286
|
+
function unescapePattern(token) {
|
|
287
|
+
return token.replace(/\\ /g, " ").replace(/^\\#/, "#");
|
|
288
|
+
}
|
|
289
|
+
function ownersFor(file, relPath) {
|
|
290
|
+
const matchers = matchersFor(file);
|
|
291
|
+
const defaults = new Map(file.sections.map((s) => [s.name, s.defaultOwners]));
|
|
292
|
+
const order = [void 0, ...file.sections.map((s) => s.name)];
|
|
293
|
+
const winners = /* @__PURE__ */ new Map();
|
|
294
|
+
file.rules.forEach((rule, i) => {
|
|
295
|
+
if (matchers[i]?.(relPath)) winners.set(rule.section, rule);
|
|
296
|
+
});
|
|
297
|
+
if (winners.size === 0) return null;
|
|
298
|
+
const owners = [];
|
|
299
|
+
let line = 0;
|
|
300
|
+
let lineFromOwner = false;
|
|
301
|
+
for (const section of order) {
|
|
302
|
+
const rule = winners.get(section);
|
|
303
|
+
if (!rule) continue;
|
|
304
|
+
const chosen = rule.owners.length > 0 ? rule.owners : (section === void 0 ? void 0 : defaults.get(section)) ?? [];
|
|
305
|
+
if (chosen.length > 0) {
|
|
306
|
+
line = rule.line;
|
|
307
|
+
lineFromOwner = true;
|
|
308
|
+
} else if (!lineFromOwner) {
|
|
309
|
+
line = rule.line;
|
|
310
|
+
}
|
|
311
|
+
for (const o of chosen) if (!owners.includes(o)) owners.push(o);
|
|
312
|
+
}
|
|
313
|
+
return { owners, line };
|
|
314
|
+
}
|
|
315
|
+
var matcherCache = /* @__PURE__ */ new WeakMap();
|
|
316
|
+
function matchersFor(file) {
|
|
317
|
+
let matchers = matcherCache.get(file);
|
|
318
|
+
if (!matchers) {
|
|
319
|
+
matchers = file.rules.map((r) => picomatch(toGlobs(r.pattern), PICOMATCH));
|
|
320
|
+
matcherCache.set(file, matchers);
|
|
321
|
+
}
|
|
322
|
+
return matchers;
|
|
323
|
+
}
|
|
324
|
+
var PICOMATCH = { dot: true, windows: false };
|
|
325
|
+
function toGlobs(pattern) {
|
|
326
|
+
let p = pattern;
|
|
327
|
+
const dirOnly = p.endsWith("/");
|
|
328
|
+
if (dirOnly) p = p.slice(0, -1);
|
|
329
|
+
let anchored = false;
|
|
330
|
+
if (p.startsWith("/")) {
|
|
331
|
+
anchored = true;
|
|
332
|
+
p = p.slice(1);
|
|
333
|
+
} else if (p.includes("/") && !p.startsWith("**/")) {
|
|
334
|
+
anchored = true;
|
|
335
|
+
}
|
|
336
|
+
const base = anchored || p.startsWith("**/") ? p : `**/${p}`;
|
|
337
|
+
if (dirOnly) return [`${base}/**`];
|
|
338
|
+
const last = p.slice(p.lastIndexOf("/") + 1);
|
|
339
|
+
return /[*?[]/.test(last) ? [base] : [base, `${base}/**`];
|
|
340
|
+
}
|
|
341
|
+
var NOT_FOUND_REASON = `no CODEOWNERS file found (looked for ${CODEOWNERS_LOCATIONS.join(", ")})`;
|
|
342
|
+
async function deriveFromCodeowners(inputs, opts) {
|
|
343
|
+
const records = /* @__PURE__ */ new Map();
|
|
344
|
+
const explicitPath = opts.explicit === void 0 ? void 0 : resolve(opts.configDir ?? process.cwd(), opts.explicit);
|
|
345
|
+
if (explicitPath !== void 0 && !existsSync(explicitPath)) {
|
|
346
|
+
return {
|
|
347
|
+
status: { available: false, reason: `CODEOWNERS not found at ${explicitPath}` },
|
|
348
|
+
records
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
352
|
+
const rootCache = /* @__PURE__ */ new Map();
|
|
353
|
+
for (const input of inputs) {
|
|
354
|
+
const dir = dirname(input.absPath);
|
|
355
|
+
let root = rootCache.get(dir);
|
|
356
|
+
if (root === void 0) {
|
|
357
|
+
root = opts.rootOf(input.absPath);
|
|
358
|
+
rootCache.set(dir, root);
|
|
359
|
+
}
|
|
360
|
+
if (root === null) continue;
|
|
361
|
+
const bucket = byRoot.get(root);
|
|
362
|
+
if (bucket) bucket.push(input);
|
|
363
|
+
else byRoot.set(root, [input]);
|
|
364
|
+
}
|
|
365
|
+
let found = 0;
|
|
366
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const [root, bucket] of byRoot) {
|
|
368
|
+
const path = explicitPath ?? findCodeowners(root);
|
|
369
|
+
if (path === null) {
|
|
370
|
+
for (const input of bucket) records.set(input.label, null);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
found += 1;
|
|
374
|
+
const label = toPosix(relative(opts.configDir ?? root, path));
|
|
375
|
+
const key = `${path}\0${label}`;
|
|
376
|
+
let file = parsed.get(key);
|
|
377
|
+
if (!file) {
|
|
378
|
+
file = readFile(path, "utf8").then(
|
|
379
|
+
(text2) => parseCodeowners(text2, path, label)
|
|
380
|
+
);
|
|
381
|
+
parsed.set(key, file);
|
|
382
|
+
}
|
|
383
|
+
let owners;
|
|
384
|
+
try {
|
|
385
|
+
owners = await file;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return {
|
|
388
|
+
status: {
|
|
389
|
+
available: false,
|
|
390
|
+
reason: `CODEOWNERS at ${label} could not be read: ${errorMessage(err)}`
|
|
391
|
+
},
|
|
392
|
+
records
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
for (const input of bucket) {
|
|
396
|
+
const rel = toPosix(relative(root, input.absPath));
|
|
397
|
+
const hit = ownersFor(owners, rel);
|
|
398
|
+
records.set(
|
|
399
|
+
input.label,
|
|
400
|
+
hit === null ? null : {
|
|
401
|
+
value: hit.owners,
|
|
402
|
+
source: "codeowners",
|
|
403
|
+
evidence: `${label}:${hit.line}`
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (byRoot.size === 0) {
|
|
409
|
+
return {
|
|
410
|
+
status: { available: false, reason: "no git repository contains the documents" },
|
|
411
|
+
records
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (found === 0 && explicitPath === void 0) {
|
|
415
|
+
return { status: { available: true, reason: NOT_FOUND_REASON }, records };
|
|
416
|
+
}
|
|
417
|
+
return { status: { available: true }, records };
|
|
418
|
+
}
|
|
419
|
+
function toPosix(p) {
|
|
420
|
+
return p.split(sep).join("/");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/meta/core/derive/spawn.ts
|
|
424
|
+
import { spawn } from "node:child_process";
|
|
425
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
426
|
+
var MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
427
|
+
var BinMissing = class extends Error {
|
|
428
|
+
};
|
|
429
|
+
function run(bin, args, opts) {
|
|
430
|
+
return new Promise((settle, reject) => {
|
|
431
|
+
let done = false;
|
|
432
|
+
const finish = (r) => {
|
|
433
|
+
if (done) return;
|
|
434
|
+
done = true;
|
|
435
|
+
settle(r);
|
|
436
|
+
};
|
|
437
|
+
const fail = (err) => {
|
|
438
|
+
if (done) return;
|
|
439
|
+
done = true;
|
|
440
|
+
reject(err);
|
|
441
|
+
};
|
|
442
|
+
let child;
|
|
443
|
+
try {
|
|
444
|
+
child = spawn(bin, [...opts.prefixArgs ?? [], ...args], {
|
|
445
|
+
cwd: opts.cwd,
|
|
446
|
+
windowsHide: true
|
|
447
|
+
});
|
|
448
|
+
} catch (err) {
|
|
449
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
let stdout = "";
|
|
453
|
+
let stderr = "";
|
|
454
|
+
let timedOut = false;
|
|
455
|
+
let tooLarge = false;
|
|
456
|
+
const stop = (r) => {
|
|
457
|
+
clearTimeout(timer);
|
|
458
|
+
child.kill();
|
|
459
|
+
setTimeout(() => child.kill("SIGKILL"), 1e3).unref();
|
|
460
|
+
finish({ code: null, ...r });
|
|
461
|
+
};
|
|
462
|
+
child.stdout.setEncoding("utf8");
|
|
463
|
+
child.stdout.on("data", (chunk) => {
|
|
464
|
+
stdout += chunk;
|
|
465
|
+
if (stdout.length > MAX_OUTPUT_BYTES) {
|
|
466
|
+
tooLarge = true;
|
|
467
|
+
stop({ stdout, stderr, timedOut, tooLarge });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
child.stderr.setEncoding("utf8");
|
|
471
|
+
child.stderr.on("data", (chunk) => {
|
|
472
|
+
stderr += chunk;
|
|
473
|
+
if (stderr.length > MAX_OUTPUT_BYTES) {
|
|
474
|
+
tooLarge = true;
|
|
475
|
+
stop({ stdout, stderr, timedOut, tooLarge });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
child.stdin.end();
|
|
479
|
+
const timer = setTimeout(() => {
|
|
480
|
+
timedOut = true;
|
|
481
|
+
stop({ stdout, stderr, timedOut, tooLarge });
|
|
482
|
+
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
483
|
+
child.on("error", (err) => {
|
|
484
|
+
clearTimeout(timer);
|
|
485
|
+
fail(err.code === "ENOENT" ? new BinMissing(err.message) : err);
|
|
486
|
+
});
|
|
487
|
+
child.on("close", (code) => {
|
|
488
|
+
clearTimeout(timer);
|
|
489
|
+
finish({ code, stdout, stderr, timedOut, tooLarge });
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function commandLine(bin, args) {
|
|
494
|
+
return [bin, ...args].join(" ");
|
|
495
|
+
}
|
|
496
|
+
function lastLine(text2) {
|
|
497
|
+
const lines = text2.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
498
|
+
return lines.at(-1) ?? "";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/meta/core/derive/command.ts
|
|
502
|
+
var PATH_PLACEHOLDER = "{path}";
|
|
503
|
+
function isPerFile(command) {
|
|
504
|
+
return command.run.some((arg) => arg.includes(PATH_PLACEHOLDER));
|
|
505
|
+
}
|
|
506
|
+
function argvFor(command, label) {
|
|
507
|
+
return command.run.map((arg) => arg.split(PATH_PLACEHOLDER).join(label));
|
|
508
|
+
}
|
|
509
|
+
function valueOf(stdout) {
|
|
510
|
+
const text2 = stdout.trim();
|
|
511
|
+
if (text2 === "") return null;
|
|
512
|
+
if (!/^[{["]/.test(text2)) return text2;
|
|
513
|
+
try {
|
|
514
|
+
return JSON.parse(text2);
|
|
515
|
+
} catch {
|
|
516
|
+
return text2;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function deriveFromCommands(inputs, commands, opts) {
|
|
520
|
+
const records = /* @__PURE__ */ new Map();
|
|
521
|
+
const fields = Object.keys(commands);
|
|
522
|
+
if (fields.length === 0 || inputs.length === 0) {
|
|
523
|
+
return { status: { available: true }, records };
|
|
524
|
+
}
|
|
525
|
+
for (const input of inputs) records.set(input.label, {});
|
|
526
|
+
for (const field of fields) {
|
|
527
|
+
const command = commands[field];
|
|
528
|
+
if (command === void 0) continue;
|
|
529
|
+
if (isPerFile(command)) {
|
|
530
|
+
for (const [label, record] of records) {
|
|
531
|
+
const outcome = await runOne(field, argvFor(command, label), command.timeoutMs, opts);
|
|
532
|
+
if ("reason" in outcome) return unavailable(outcome.reason);
|
|
533
|
+
record[field] = outcome.value;
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
const outcome = await runOne(field, command.run, command.timeoutMs, opts);
|
|
537
|
+
if ("reason" in outcome) return unavailable(outcome.reason);
|
|
538
|
+
for (const record of records.values()) record[field] = outcome.value;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { status: { available: true }, records };
|
|
542
|
+
}
|
|
543
|
+
function unavailable(reason) {
|
|
544
|
+
return { status: { available: false, reason }, records: /* @__PURE__ */ new Map() };
|
|
545
|
+
}
|
|
546
|
+
async function runOne(field, argv, timeoutMs, opts) {
|
|
547
|
+
const [program, ...args] = argv;
|
|
548
|
+
const where = `(derive.commands.${field})`;
|
|
549
|
+
if (program === void 0) {
|
|
550
|
+
return { reason: `no program to run ${where}` };
|
|
551
|
+
}
|
|
552
|
+
const line = argv.join(" ");
|
|
553
|
+
let result;
|
|
554
|
+
try {
|
|
555
|
+
result = await run(opts.spawn?.bin ?? program, args, {
|
|
556
|
+
...opts.spawn,
|
|
557
|
+
cwd: opts.spawn?.cwd ?? opts.cwd,
|
|
558
|
+
timeoutMs
|
|
559
|
+
});
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (err instanceof BinMissing) {
|
|
562
|
+
return { reason: `\`${program}\` is not on PATH ${where}` };
|
|
563
|
+
}
|
|
564
|
+
return { reason: `\`${line}\` could not be run: ${errorMessage(err)} ${where}` };
|
|
565
|
+
}
|
|
566
|
+
if (result.timedOut) {
|
|
567
|
+
return { reason: `\`${line}\` timed out after ${String(timeoutMs / 1e3)}s ${where}` };
|
|
568
|
+
}
|
|
569
|
+
if (result.tooLarge) {
|
|
570
|
+
return {
|
|
571
|
+
reason: `\`${line}\` wrote more than ${String(MAX_OUTPUT_BYTES / (1024 * 1024))} MiB; a command reports one field's value ${where}`
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (result.code !== 0) {
|
|
575
|
+
const last = lastLine(result.stderr);
|
|
576
|
+
const detail = last === "" ? "no output on stderr" : last;
|
|
577
|
+
const exit = result.code === null ? "killed by a signal" : `exit ${String(result.code)}`;
|
|
578
|
+
return { reason: `\`${line}\` failed (${exit}): ${detail} ${where}` };
|
|
579
|
+
}
|
|
580
|
+
const value = valueOf(result.stdout);
|
|
581
|
+
return { value: value === null ? null : { value, source: "command", evidence: line } };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/meta/core/derive/reviews.ts
|
|
585
|
+
import { resolve as resolvePath } from "node:path";
|
|
586
|
+
function parseOriginUrl(url) {
|
|
587
|
+
const trimmed = url.trim();
|
|
588
|
+
if (trimmed === "") return null;
|
|
589
|
+
if (isLocalPath(trimmed)) return null;
|
|
590
|
+
const scp = /^(?:[^@/:]+@)?([^/:]+):(?!\/\/)(.+)$/.exec(trimmed);
|
|
591
|
+
if (scp) {
|
|
592
|
+
const host = scp[1];
|
|
593
|
+
const path = scp[2];
|
|
594
|
+
if (host === void 0 || path === void 0) return null;
|
|
595
|
+
if (/^[A-Za-z]$/.test(host)) return null;
|
|
596
|
+
return project(host, path);
|
|
597
|
+
}
|
|
598
|
+
let parsed;
|
|
599
|
+
try {
|
|
600
|
+
parsed = new URL(trimmed);
|
|
601
|
+
} catch {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
if (!/^(https?|ssh|git):$/.test(parsed.protocol)) return null;
|
|
605
|
+
if (parsed.hostname === "") return null;
|
|
606
|
+
return project(parsed.hostname, parsed.pathname);
|
|
607
|
+
}
|
|
608
|
+
function isLocalPath(s) {
|
|
609
|
+
return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") || s.startsWith("\\\\");
|
|
610
|
+
}
|
|
611
|
+
function project(host, path) {
|
|
612
|
+
const segments = path.replace(/\.git$/, "").split("/").filter((s) => s !== "");
|
|
613
|
+
if (segments.length < 2) return null;
|
|
614
|
+
return { host, project: segments.join("/") };
|
|
615
|
+
}
|
|
616
|
+
function identityFromOrigin(host, project2, requested) {
|
|
617
|
+
const name = host.toLowerCase();
|
|
618
|
+
if (name.includes("github")) return { kind: "github", host, project: project2 };
|
|
619
|
+
if (name.includes("gitlab")) return { kind: "gitlab", host, project: project2 };
|
|
620
|
+
const only = requested !== void 0 && requested.length === 1 ? requested[0] : void 0;
|
|
621
|
+
return only === void 0 ? null : { kind: only, host, project: project2 };
|
|
622
|
+
}
|
|
623
|
+
var UnknownHostClient = class {
|
|
624
|
+
constructor(host) {
|
|
625
|
+
this.host = host;
|
|
626
|
+
}
|
|
627
|
+
host;
|
|
628
|
+
detect() {
|
|
629
|
+
return Promise.resolve(null);
|
|
630
|
+
}
|
|
631
|
+
status() {
|
|
632
|
+
return Promise.resolve({
|
|
633
|
+
available: false,
|
|
634
|
+
reason: `the origin remote is ${this.host}, which names neither GitHub nor GitLab; list exactly one of github, gitlab in derive.sources to say which it is`
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
mergedChangeFor() {
|
|
638
|
+
return Promise.resolve(null);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
function httpStatus(stderr) {
|
|
642
|
+
const m = /\b(?:HTTP\s+(\d{3})|(\d{3})\s+Not\s+Found)\b/i.exec(stderr);
|
|
643
|
+
const code = m?.[1] ?? m?.[2];
|
|
644
|
+
return code === void 0 ? null : Number(code);
|
|
645
|
+
}
|
|
646
|
+
function isNotFound(r) {
|
|
647
|
+
const status = httpStatus(r.stderr);
|
|
648
|
+
return status === 404 || status === 422;
|
|
649
|
+
}
|
|
650
|
+
var CliClient = class {
|
|
651
|
+
constructor(identity, spawnOpts) {
|
|
652
|
+
this.identity = identity;
|
|
653
|
+
this.spawnOpts = spawnOpts;
|
|
654
|
+
}
|
|
655
|
+
identity;
|
|
656
|
+
spawnOpts;
|
|
657
|
+
detect() {
|
|
658
|
+
return Promise.resolve(this.identity);
|
|
659
|
+
}
|
|
660
|
+
async status() {
|
|
661
|
+
if (this.identity === null) {
|
|
662
|
+
return { available: false, reason: "no origin remote to tell GitHub from GitLab" };
|
|
663
|
+
}
|
|
664
|
+
const { host } = this.identity;
|
|
665
|
+
let r;
|
|
666
|
+
try {
|
|
667
|
+
r = await this.run(["auth", "status", "--hostname", host]);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
if (err instanceof BinMissing) {
|
|
670
|
+
return {
|
|
671
|
+
available: false,
|
|
672
|
+
reason: `${this.tool} is not on PATH (origin is ${host}); install ${this.tool} and run ${this.tool} auth login`
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
throw err;
|
|
676
|
+
}
|
|
677
|
+
if (r.timedOut) throw this.timeout(["auth", "status", "--hostname", host]);
|
|
678
|
+
if (r.code !== 0) {
|
|
679
|
+
return {
|
|
680
|
+
available: false,
|
|
681
|
+
reason: `${this.tool} is not authenticated for ${host}; run ${this.tool} auth login`
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return { available: true };
|
|
685
|
+
}
|
|
686
|
+
async mergedChangeFor(sha) {
|
|
687
|
+
if (this.identity === null) return null;
|
|
688
|
+
return this.lookup(this.identity, sha);
|
|
689
|
+
}
|
|
690
|
+
get bin() {
|
|
691
|
+
return this.spawnOpts.bin ?? this.tool;
|
|
692
|
+
}
|
|
693
|
+
run(args) {
|
|
694
|
+
return run(this.bin, args, this.spawnOpts);
|
|
695
|
+
}
|
|
696
|
+
timeout(args) {
|
|
697
|
+
const ms = this.spawnOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
698
|
+
return new DocmetaError(
|
|
699
|
+
`${commandLine(this.tool, args)} timed out after ${ms}ms`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Run a call whose stdout is JSON. A not-found answer is `null`; any other
|
|
704
|
+
* failure throws, naming the command the user can re-run by hand. The
|
|
705
|
+
* message quotes the tool's own name rather than `bin`, which under test
|
|
706
|
+
* is `node`.
|
|
707
|
+
*/
|
|
708
|
+
async json(args) {
|
|
709
|
+
let r;
|
|
710
|
+
try {
|
|
711
|
+
r = await this.run(args);
|
|
712
|
+
} catch (err) {
|
|
713
|
+
if (err instanceof BinMissing) {
|
|
714
|
+
throw new DocmetaError(
|
|
715
|
+
`${commandLine(this.tool, args)} failed: ${this.tool} is not on PATH`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
throw err;
|
|
719
|
+
}
|
|
720
|
+
const cmd = commandLine(this.tool, args);
|
|
721
|
+
if (r.timedOut) throw this.timeout(args);
|
|
722
|
+
if (r.tooLarge) {
|
|
723
|
+
throw new DocmetaError(`${cmd} wrote more output than manni will read`);
|
|
724
|
+
}
|
|
725
|
+
if (r.code !== 0) {
|
|
726
|
+
if (isNotFound(r)) return null;
|
|
727
|
+
const detail = lastLine(r.stderr) || lastLine(r.stdout);
|
|
728
|
+
const exit = r.code === null ? "signal" : `exit ${r.code}`;
|
|
729
|
+
throw new DocmetaError(
|
|
730
|
+
`${cmd} failed (${exit})${detail === "" ? "" : `: ${detail}`}`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
return JSON.parse(r.stdout);
|
|
735
|
+
} catch {
|
|
736
|
+
throw new DocmetaError(`${cmd} did not return JSON`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
function isRecord2(v) {
|
|
741
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
742
|
+
}
|
|
743
|
+
function items(v) {
|
|
744
|
+
return Array.isArray(v) ? v.filter(isRecord2) : [];
|
|
745
|
+
}
|
|
746
|
+
function numberOf(v) {
|
|
747
|
+
return typeof v === "number" && Number.isInteger(v) ? v : null;
|
|
748
|
+
}
|
|
749
|
+
function stringOf(v) {
|
|
750
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
751
|
+
}
|
|
752
|
+
function timestampOf(v) {
|
|
753
|
+
if (v === void 0) return void 0;
|
|
754
|
+
return stringOf(v);
|
|
755
|
+
}
|
|
756
|
+
function normalizeLogin(login) {
|
|
757
|
+
return login.replace(/\[bot\]$/, "");
|
|
758
|
+
}
|
|
759
|
+
function latestApprovals(reviews) {
|
|
760
|
+
const latest = /* @__PURE__ */ new Map();
|
|
761
|
+
for (const r of reviews) {
|
|
762
|
+
if (r.state !== "APPROVED") continue;
|
|
763
|
+
const key = normalizeLogin(r.login);
|
|
764
|
+
const seen = latest.get(key);
|
|
765
|
+
if (seen === void 0 || r.submittedAt > seen.submittedAt) {
|
|
766
|
+
latest.set(key, { login: r.login, submittedAt: r.submittedAt });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return byTime([...latest.values()]);
|
|
770
|
+
}
|
|
771
|
+
function byTime(approvals) {
|
|
772
|
+
return [...approvals].sort(
|
|
773
|
+
(a, b) => a.submittedAt < b.submittedAt ? -1 : a.submittedAt > b.submittedAt ? 1 : 0
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
var GitHubClient = class extends CliClient {
|
|
777
|
+
tool = "gh";
|
|
778
|
+
async lookup(identity, sha) {
|
|
779
|
+
const { host, project: project2 } = identity;
|
|
780
|
+
const api = (endpoint) => this.json(["api", "--hostname", host, endpoint]);
|
|
781
|
+
let candidates = items(await api(`repos/${project2}/commits/${sha}/pulls`)).map(
|
|
782
|
+
(p) => ({ number: numberOf(p.number), mergedAt: timestampOf(p.merged_at) })
|
|
783
|
+
);
|
|
784
|
+
if (candidates.length === 0) {
|
|
785
|
+
const found = await this.json([
|
|
786
|
+
"pr",
|
|
787
|
+
"list",
|
|
788
|
+
"--search",
|
|
789
|
+
sha,
|
|
790
|
+
"--state",
|
|
791
|
+
"merged",
|
|
792
|
+
"--json",
|
|
793
|
+
"number,mergedAt",
|
|
794
|
+
"-R",
|
|
795
|
+
`${host}/${project2}`
|
|
796
|
+
]);
|
|
797
|
+
candidates = items(found).map((p) => ({
|
|
798
|
+
number: numberOf(p.number),
|
|
799
|
+
mergedAt: timestampOf(p.mergedAt)
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
for (const c of candidates) {
|
|
803
|
+
if (c.number === null) continue;
|
|
804
|
+
let mergedAt = c.mergedAt;
|
|
805
|
+
if (mergedAt === void 0) {
|
|
806
|
+
const pr = await api(`repos/${project2}/pulls/${c.number}`);
|
|
807
|
+
mergedAt = isRecord2(pr) ? stringOf(pr.merged_at) ?? null : null;
|
|
808
|
+
}
|
|
809
|
+
if (mergedAt === null) continue;
|
|
810
|
+
const raw = [];
|
|
811
|
+
for (let page = 1; ; page += 1) {
|
|
812
|
+
const batch = items(
|
|
813
|
+
await api(`repos/${project2}/pulls/${c.number}/reviews?per_page=100&page=${page}`)
|
|
814
|
+
);
|
|
815
|
+
raw.push(...batch);
|
|
816
|
+
if (batch.length < 100) break;
|
|
817
|
+
}
|
|
818
|
+
const reviews = raw.map((r) => ({
|
|
819
|
+
login: isRecord2(r.user) ? stringOf(r.user.login) : null,
|
|
820
|
+
state: stringOf(r.state),
|
|
821
|
+
submittedAt: stringOf(r.submitted_at)
|
|
822
|
+
})).filter(
|
|
823
|
+
(r) => r.login !== null && r.state !== null && r.submittedAt !== null
|
|
824
|
+
);
|
|
825
|
+
return { id: c.number, mergedAt, approvals: latestApprovals(reviews) };
|
|
826
|
+
}
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
var GitLabClient = class extends CliClient {
|
|
831
|
+
tool = "glab";
|
|
832
|
+
async lookup(identity, sha) {
|
|
833
|
+
const { host } = identity;
|
|
834
|
+
const api = (endpoint) => this.json(["api", "--hostname", host, endpoint]);
|
|
835
|
+
const merged = items(
|
|
836
|
+
await api(`projects/:fullpath/repository/commits/${sha}/merge_requests`)
|
|
837
|
+
).map((mr) => ({ iid: numberOf(mr.iid), mergedAt: stringOf(mr.merged_at) })).find(
|
|
838
|
+
(mr) => mr.iid !== null && mr.mergedAt !== null
|
|
839
|
+
);
|
|
840
|
+
if (merged === void 0) return null;
|
|
841
|
+
const answer = await api(`projects/:fullpath/merge_requests/${merged.iid}/approvals`);
|
|
842
|
+
const approvedBy = isRecord2(answer) ? items(answer.approved_by) : [];
|
|
843
|
+
const approvals = [];
|
|
844
|
+
const seen = /* @__PURE__ */ new Set();
|
|
845
|
+
for (const entry of approvedBy) {
|
|
846
|
+
const login = isRecord2(entry.user) ? stringOf(entry.user.username) : null;
|
|
847
|
+
if (login === null) continue;
|
|
848
|
+
const key = normalizeLogin(login);
|
|
849
|
+
if (seen.has(key)) continue;
|
|
850
|
+
seen.add(key);
|
|
851
|
+
approvals.push({ login, submittedAt: merged.mergedAt });
|
|
852
|
+
}
|
|
853
|
+
return { id: merged.iid, mergedAt: merged.mergedAt, approvals };
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
async function createReviewClient(root, opts) {
|
|
857
|
+
const spawnOpts = { ...opts.spawn, cwd: opts.spawn?.cwd ?? root };
|
|
858
|
+
const origin = await originUrl(root);
|
|
859
|
+
const parsed = origin === null ? null : parseOriginUrl(origin);
|
|
860
|
+
const identity = parsed === null ? null : identityFromOrigin(parsed.host, parsed.project, opts.requested);
|
|
861
|
+
if (parsed !== null && identity === null) return new UnknownHostClient(parsed.host);
|
|
862
|
+
const client = identity?.kind === "gitlab" ? new GitLabClient(identity, spawnOpts) : new GitHubClient(identity, spawnOpts);
|
|
863
|
+
const cache = new ReviewCache(resolvePath(root, opts.cacheDir), opts.cache);
|
|
864
|
+
return cachedClient(client, cache, identity);
|
|
865
|
+
}
|
|
866
|
+
async function originUrl(root) {
|
|
867
|
+
try {
|
|
868
|
+
const r = await run("git", ["remote", "get-url", "origin"], { cwd: root });
|
|
869
|
+
if (r.code !== 0 || r.timedOut || r.tooLarge) return null;
|
|
870
|
+
return stringOf(r.stdout.trim());
|
|
871
|
+
} catch {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
var NOTHING = { "reviewed-by": null, "last-reviewed": null };
|
|
876
|
+
async function deriveFromReviews(inputs, client) {
|
|
877
|
+
const records = /* @__PURE__ */ new Map();
|
|
878
|
+
const status = await client.status();
|
|
879
|
+
if (!status.available) return { status, records };
|
|
880
|
+
const identity = await client.detect();
|
|
881
|
+
const kind = identity?.kind ?? "github";
|
|
882
|
+
const bySha = /* @__PURE__ */ new Map();
|
|
883
|
+
const changeFor = (sha) => {
|
|
884
|
+
let pending = bySha.get(sha);
|
|
885
|
+
if (pending === void 0) {
|
|
886
|
+
pending = client.mergedChangeFor(sha);
|
|
887
|
+
bySha.set(sha, pending);
|
|
888
|
+
}
|
|
889
|
+
return pending;
|
|
890
|
+
};
|
|
891
|
+
for (const input of inputs) {
|
|
892
|
+
if (input.sha === null) {
|
|
893
|
+
records.set(input.label, NOTHING);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const change = await changeFor(input.sha);
|
|
897
|
+
records.set(input.label, factsFrom(change, kind));
|
|
898
|
+
}
|
|
899
|
+
return { status, records };
|
|
900
|
+
}
|
|
901
|
+
function factsFrom(change, kind) {
|
|
902
|
+
if (change === null || change.approvals.length === 0) return NOTHING;
|
|
903
|
+
const ordered = byTime(change.approvals);
|
|
904
|
+
const latest = ordered.at(-1);
|
|
905
|
+
if (latest === void 0) return NOTHING;
|
|
906
|
+
const evidence = kind === "gitlab" ? `gitlab MR !${change.id}` : `github PR #${change.id}`;
|
|
907
|
+
const dateEvidence = kind === "gitlab" ? `${evidence} (merge date; GitLab records no approval time)` : evidence;
|
|
908
|
+
return {
|
|
909
|
+
"reviewed-by": {
|
|
910
|
+
value: ordered.map((a) => a.login),
|
|
911
|
+
source: kind,
|
|
912
|
+
evidence
|
|
913
|
+
},
|
|
914
|
+
"last-reviewed": {
|
|
915
|
+
value: utcDate(latest.submittedAt),
|
|
916
|
+
source: kind,
|
|
917
|
+
evidence: dateEvidence
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function utcDate(iso) {
|
|
922
|
+
const ms = Date.parse(iso);
|
|
923
|
+
if (Number.isNaN(ms)) return iso;
|
|
924
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/meta/core/derive/git.ts
|
|
928
|
+
import { constants as bufferConstants } from "node:buffer";
|
|
929
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
930
|
+
import { availableParallelism } from "node:os";
|
|
931
|
+
import { dirname as dirname2, extname, isAbsolute, posix, relative as relative2, sep as sep2 } from "node:path";
|
|
932
|
+
import { parse as parseYaml } from "yaml";
|
|
933
|
+
|
|
934
|
+
// src/shared/concurrency.ts
|
|
935
|
+
async function mapConcurrent(items2, limit, fn) {
|
|
936
|
+
const out = new Array(items2.length);
|
|
937
|
+
let next = 0;
|
|
938
|
+
const workers = Array.from(
|
|
939
|
+
{ length: Math.max(1, Math.min(limit, items2.length)) },
|
|
940
|
+
async () => {
|
|
941
|
+
for (; ; ) {
|
|
942
|
+
const i = next++;
|
|
943
|
+
const item = items2[i];
|
|
944
|
+
if (i >= items2.length || item === void 0) return;
|
|
945
|
+
out[i] = await fn(item);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
await Promise.all(workers);
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/meta/core/derive/git.ts
|
|
954
|
+
var DEFAULT_BULK_THRESHOLD = 32;
|
|
955
|
+
var NULL_SHA = "0".repeat(40);
|
|
956
|
+
var MAX_GIT_OUTPUT_BYTES = Math.min(
|
|
957
|
+
512 * 1024 * 1024,
|
|
958
|
+
bufferConstants.MAX_STRING_LENGTH
|
|
959
|
+
);
|
|
960
|
+
var HistoryTooLarge = class extends Error {
|
|
961
|
+
};
|
|
962
|
+
var BlobsUnreadable = class extends Error {
|
|
963
|
+
constructor(detail) {
|
|
964
|
+
super(detail);
|
|
965
|
+
this.detail = detail;
|
|
966
|
+
}
|
|
967
|
+
detail;
|
|
968
|
+
};
|
|
969
|
+
var BlameUnreadable = class extends Error {
|
|
970
|
+
constructor(rel, detail) {
|
|
971
|
+
super(rel);
|
|
972
|
+
this.rel = rel;
|
|
973
|
+
this.detail = detail;
|
|
974
|
+
}
|
|
975
|
+
rel;
|
|
976
|
+
detail;
|
|
977
|
+
};
|
|
978
|
+
function bodyOf(content, fenced) {
|
|
979
|
+
if (fenced !== true) return content;
|
|
980
|
+
const loc = locateFrontmatter(content);
|
|
981
|
+
if (!loc) return content;
|
|
982
|
+
return content.slice(0, loc.openStart) + content.slice(loc.closeEnd);
|
|
983
|
+
}
|
|
984
|
+
async function deriveFromGit(inputs, opts) {
|
|
985
|
+
const records = /* @__PURE__ */ new Map();
|
|
986
|
+
const byRoot = groupByRoot(inputs);
|
|
987
|
+
if (byRoot.size === 0) {
|
|
988
|
+
return {
|
|
989
|
+
status: {
|
|
990
|
+
available: false,
|
|
991
|
+
reason: "no git repository contains the documents"
|
|
992
|
+
},
|
|
993
|
+
records
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
const threshold = opts.bulkThreshold ?? DEFAULT_BULK_THRESHOLD;
|
|
997
|
+
const machines = opts.machines ?? DEFAULT_MACHINES;
|
|
998
|
+
const wantsProvenance = opts.fields?.includes(PROVENANCE_FIELD) ?? false;
|
|
999
|
+
const maxBytes = opts.maxOutputBytes ?? MAX_GIT_OUTPUT_BYTES;
|
|
1000
|
+
const parse = yamlOnce();
|
|
1001
|
+
const reasons = [];
|
|
1002
|
+
for (const [root, bucket] of byRoot) {
|
|
1003
|
+
const run2 = (args, stdin) => runGit(args, root, stdin, maxBytes);
|
|
1004
|
+
const shallow = await run2(["rev-parse", "--is-shallow-repository"]);
|
|
1005
|
+
if (shallow.missing) {
|
|
1006
|
+
return {
|
|
1007
|
+
status: { available: false, reason: "git is not on PATH" },
|
|
1008
|
+
records: /* @__PURE__ */ new Map()
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (shallow.code !== 0) {
|
|
1012
|
+
reasons.push(`git could not read the repository at ${root}`);
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (text(shallow).trim() === "true") {
|
|
1016
|
+
reasons.push(
|
|
1017
|
+
`this checkout is shallow; use actions/checkout with fetch-depth: 0 (${root})`
|
|
1018
|
+
);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const found = await histories(
|
|
1023
|
+
run2,
|
|
1024
|
+
bucket.map((e) => e.rel),
|
|
1025
|
+
threshold
|
|
1026
|
+
);
|
|
1027
|
+
if (found === null) {
|
|
1028
|
+
reasons.push(`git could not read the history at ${root}`);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
await readOtherParentBlobs(run2, found);
|
|
1032
|
+
const blobs = await fetchBlobs(run2, neededBlobs(found));
|
|
1033
|
+
if (!await followTakenBodies(run2, bucket, found, blobs, threshold)) {
|
|
1034
|
+
reasons.push(`git could not read the history at ${root}`);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
const manifestStamps = await stampsInManifests(run2, root, bucket, found, blobs, parse);
|
|
1038
|
+
const facts = bucket.map(
|
|
1039
|
+
(entry) => judge(
|
|
1040
|
+
entry.input,
|
|
1041
|
+
root,
|
|
1042
|
+
found.get(entry.rel) ?? [],
|
|
1043
|
+
blobs,
|
|
1044
|
+
opts.now,
|
|
1045
|
+
machines,
|
|
1046
|
+
manifestStamps.get(entry.input.label)
|
|
1047
|
+
)
|
|
1048
|
+
);
|
|
1049
|
+
if (wantsProvenance) {
|
|
1050
|
+
const pages = await mapConcurrent(bucket, GIT_CONCURRENCY, async (entry) => ({
|
|
1051
|
+
entry,
|
|
1052
|
+
blame: await blameOf(run2, entry.rel, entry.input.content),
|
|
1053
|
+
history: found.get(entry.rel) ?? []
|
|
1054
|
+
}));
|
|
1055
|
+
const evidence = await commitEvidence(run2, root, pages, blobs, parse);
|
|
1056
|
+
const provenanceRun = {
|
|
1057
|
+
machines,
|
|
1058
|
+
...opts.generatedBy !== void 0 ? { generatedBy: opts.generatedBy } : {}
|
|
1059
|
+
};
|
|
1060
|
+
pages.forEach((page, i) => {
|
|
1061
|
+
const judged = facts[i];
|
|
1062
|
+
const commits = evidence[i];
|
|
1063
|
+
if (judged === void 0 || commits === void 0) return;
|
|
1064
|
+
const derived = provenanceFor(page.entry, page.blame, commits, provenanceRun);
|
|
1065
|
+
judged.provenance = derived.value;
|
|
1066
|
+
judged.provenanceDerivation = derived.derivation;
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
bucket.forEach((entry, i) => {
|
|
1070
|
+
const judged = facts[i];
|
|
1071
|
+
if (judged !== void 0) records.set(entry.input.label, judged);
|
|
1072
|
+
});
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
if (err instanceof BlobsUnreadable) {
|
|
1075
|
+
reasons.push(`git cat-file could not read the history (${root}): ${err.detail}`);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (err instanceof BlameUnreadable) {
|
|
1079
|
+
const why = err.detail === "" ? "" : `: ${err.detail}`;
|
|
1080
|
+
reasons.push(`git blame could not read ${err.rel} (${root})${why}`);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (!(err instanceof HistoryTooLarge)) throw err;
|
|
1084
|
+
reasons.push(`git history is too large to read in one pass (${root})`);
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (reasons.length > 0) {
|
|
1089
|
+
return {
|
|
1090
|
+
status: { available: false, reason: reasons.join("; ") },
|
|
1091
|
+
records: /* @__PURE__ */ new Map()
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return { status: { available: true }, records };
|
|
1095
|
+
}
|
|
1096
|
+
function groupByRoot(inputs) {
|
|
1097
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
1098
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
1099
|
+
for (const input of inputs) {
|
|
1100
|
+
const dir = dirname2(input.absPath);
|
|
1101
|
+
let root = rootOf.get(dir);
|
|
1102
|
+
if (root === void 0) {
|
|
1103
|
+
root = findGitRoot(dir);
|
|
1104
|
+
rootOf.set(dir, root);
|
|
1105
|
+
}
|
|
1106
|
+
if (root === null) continue;
|
|
1107
|
+
const rel = relative2(root, input.absPath).split(sep2).join("/");
|
|
1108
|
+
const bucket = byRoot.get(root);
|
|
1109
|
+
if (bucket) bucket.push({ input, rel });
|
|
1110
|
+
else byRoot.set(root, [{ input, rel }]);
|
|
1111
|
+
}
|
|
1112
|
+
return byRoot;
|
|
1113
|
+
}
|
|
1114
|
+
var RECORD_FORMAT = "--format=%x1e%H%x00%aI%x00%an%x00%ae%x00%(trailers:key=Co-authored-by,valueonly,unfold,separator=%x1f)%x00%(trailers:key=Reviewed-by,valueonly,unfold,separator=%x1f)%x00%(trailers:key=Generated-by,valueonly,unfold,separator=%x1f)%x00%P";
|
|
1115
|
+
var LOG_ARGS = [
|
|
1116
|
+
"-c",
|
|
1117
|
+
"core.quotePath=false",
|
|
1118
|
+
"log",
|
|
1119
|
+
"--raw",
|
|
1120
|
+
"--diff-merges=first-parent",
|
|
1121
|
+
"-M",
|
|
1122
|
+
"--no-abbrev",
|
|
1123
|
+
RECORD_FORMAT
|
|
1124
|
+
];
|
|
1125
|
+
function histories(run2, rels, threshold, rev = "HEAD") {
|
|
1126
|
+
return rels.length <= threshold ? perFileHistories(run2, rels, rev) : bulkHistories(run2, rels, rev);
|
|
1127
|
+
}
|
|
1128
|
+
async function perFileHistories(run2, rels, rev) {
|
|
1129
|
+
const found = /* @__PURE__ */ new Map();
|
|
1130
|
+
for (const rel of rels) {
|
|
1131
|
+
const out = await run2([...LOG_ARGS, rev, "--follow", "--", pathspec(rel)]);
|
|
1132
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1133
|
+
if (out.code !== 0) {
|
|
1134
|
+
if (await isUnbornHead(run2)) return found;
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
const attributed = attribute(parseLog(text(out)), [rel]);
|
|
1138
|
+
found.set(rel, attributed.get(rel) ?? []);
|
|
1139
|
+
}
|
|
1140
|
+
return found;
|
|
1141
|
+
}
|
|
1142
|
+
async function bulkHistories(run2, rels, rev) {
|
|
1143
|
+
const out = await run2([...LOG_ARGS, rev]);
|
|
1144
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1145
|
+
if (out.code !== 0) {
|
|
1146
|
+
if (await isUnbornHead(run2)) return /* @__PURE__ */ new Map();
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
return attribute(parseLog(text(out)), [...rels]);
|
|
1150
|
+
}
|
|
1151
|
+
async function followTakenBodies(run2, bucket, found, blobs, threshold) {
|
|
1152
|
+
for (; ; ) {
|
|
1153
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1154
|
+
for (const entry of bucket) {
|
|
1155
|
+
const history = found.get(entry.rel) ?? [];
|
|
1156
|
+
const taken = takenMerge(entry.input.extracted.fenced, history, blobs);
|
|
1157
|
+
if (taken === void 0) continue;
|
|
1158
|
+
const group = byParent.get(taken.parent) ?? [];
|
|
1159
|
+
group.push({ rel: entry.rel, keep: history.slice(0, taken.index), path: taken.path });
|
|
1160
|
+
byParent.set(taken.parent, group);
|
|
1161
|
+
}
|
|
1162
|
+
if (byParent.size === 0) return true;
|
|
1163
|
+
const walked = /* @__PURE__ */ new Map();
|
|
1164
|
+
for (const [parent, group] of byParent) {
|
|
1165
|
+
const paths = [...new Set(group.map((g) => g.path))];
|
|
1166
|
+
const side = await histories(run2, paths, threshold, parent);
|
|
1167
|
+
if (side === null) return false;
|
|
1168
|
+
for (const g of group) {
|
|
1169
|
+
const tail = side.get(g.path) ?? [];
|
|
1170
|
+
walked.set(g.rel, tail);
|
|
1171
|
+
found.set(g.rel, [...g.keep, ...tail]);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
await readOtherParentBlobs(run2, walked);
|
|
1175
|
+
const missing = new Set([...neededBlobs(walked)].filter((sha) => !blobs.has(sha)));
|
|
1176
|
+
for (const [sha, content] of await fetchBlobs(run2, missing)) blobs.set(sha, content);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function takenMerge(fenced, history, blobs) {
|
|
1180
|
+
const blobBody = (sha) => sha === null ? null : bodyOf(blobs.get(sha) ?? "", fenced);
|
|
1181
|
+
const index = history.findIndex((h) => !sameBody(blobBody(h.newBlob), blobBody(h.oldBlob)));
|
|
1182
|
+
const newest = history[index];
|
|
1183
|
+
if (newest === void 0) return void 0;
|
|
1184
|
+
const body = blobBody(newest.newBlob);
|
|
1185
|
+
const k = newest.otherParentBlobs.findIndex((b) => b !== null && sameBody(body, blobBody(b)));
|
|
1186
|
+
const parent = newest.otherParents[k];
|
|
1187
|
+
if (k === -1 || parent === void 0) return void 0;
|
|
1188
|
+
return { index, parent, path: newest.pathAtCommit };
|
|
1189
|
+
}
|
|
1190
|
+
function pathspec(rel) {
|
|
1191
|
+
return /[*?[\]]/.test(rel) ? `:(literal)${rel}` : rel;
|
|
1192
|
+
}
|
|
1193
|
+
async function isUnbornHead(run2) {
|
|
1194
|
+
const out = await run2(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
1195
|
+
return !out.missing && out.code !== 0;
|
|
1196
|
+
}
|
|
1197
|
+
function parseLog(text2) {
|
|
1198
|
+
const records = [];
|
|
1199
|
+
for (const chunk of text2.split("")) {
|
|
1200
|
+
if (chunk === "") continue;
|
|
1201
|
+
const nl = chunk.indexOf("\n");
|
|
1202
|
+
const header = nl === -1 ? chunk : chunk.slice(0, nl);
|
|
1203
|
+
const fields = header.split("\0");
|
|
1204
|
+
const sha = fields[0];
|
|
1205
|
+
if (sha === void 0 || sha === "") continue;
|
|
1206
|
+
const raw = [];
|
|
1207
|
+
if (nl !== -1) {
|
|
1208
|
+
for (const line of chunk.slice(nl + 1).split("\n")) {
|
|
1209
|
+
const entry = parseRawLine(line);
|
|
1210
|
+
if (entry) raw.push(entry);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
records.push({
|
|
1214
|
+
sha,
|
|
1215
|
+
authorDate: fields[1] ?? "",
|
|
1216
|
+
authorName: fields[2] ?? "",
|
|
1217
|
+
authorEmail: fields[3] ?? "",
|
|
1218
|
+
coAuthors: splitTrailers(fields[4]),
|
|
1219
|
+
reviewedBy: splitTrailers(fields[5]),
|
|
1220
|
+
generatedBy: splitTrailers(fields[6]),
|
|
1221
|
+
parents: (fields[7] ?? "").trim().split(" ").filter((p) => p !== ""),
|
|
1222
|
+
raw
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
return records;
|
|
1226
|
+
}
|
|
1227
|
+
function splitTrailers(field) {
|
|
1228
|
+
if (field === void 0 || field === "") return [];
|
|
1229
|
+
return field.split("").map((s) => s.trim()).filter((s) => s !== "");
|
|
1230
|
+
}
|
|
1231
|
+
function parseRawLine(line) {
|
|
1232
|
+
if (!line.startsWith(":")) return null;
|
|
1233
|
+
const tab = line.indexOf(" ");
|
|
1234
|
+
if (tab === -1) return null;
|
|
1235
|
+
const meta = line.slice(1, tab).split(" ");
|
|
1236
|
+
const oldBlob = meta[2];
|
|
1237
|
+
const newBlob = meta[3];
|
|
1238
|
+
const status = meta[4];
|
|
1239
|
+
if (oldBlob === void 0 || newBlob === void 0 || status === void 0) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
const paths = line.slice(tab + 1).split(" ").map(unquotePath);
|
|
1243
|
+
const first = paths[0];
|
|
1244
|
+
if (first === void 0) return null;
|
|
1245
|
+
const second = paths[1];
|
|
1246
|
+
const renamed = status.startsWith("R") || status.startsWith("C");
|
|
1247
|
+
return {
|
|
1248
|
+
status,
|
|
1249
|
+
oldBlob,
|
|
1250
|
+
newBlob,
|
|
1251
|
+
oldPath: first,
|
|
1252
|
+
newPath: renamed && second !== void 0 ? second : first
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function unquotePath(p) {
|
|
1256
|
+
if (p.length < 2 || !p.startsWith('"') || !p.endsWith('"')) return p;
|
|
1257
|
+
const inner = p.slice(1, -1);
|
|
1258
|
+
const bytes = [];
|
|
1259
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
1260
|
+
const ch = inner[i];
|
|
1261
|
+
if (ch !== "\\") {
|
|
1262
|
+
bytes.push(...Buffer.from(ch ?? "", "utf8"));
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const next = inner[i + 1];
|
|
1266
|
+
if (next === void 0) break;
|
|
1267
|
+
const octal = /^[0-7]{3}/.exec(inner.slice(i + 1));
|
|
1268
|
+
if (octal) {
|
|
1269
|
+
bytes.push(parseInt(octal[0], 8));
|
|
1270
|
+
i += 3;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const simple = {
|
|
1274
|
+
a: 7,
|
|
1275
|
+
b: 8,
|
|
1276
|
+
t: 9,
|
|
1277
|
+
n: 10,
|
|
1278
|
+
v: 11,
|
|
1279
|
+
f: 12,
|
|
1280
|
+
r: 13,
|
|
1281
|
+
'"': 34,
|
|
1282
|
+
"\\": 92
|
|
1283
|
+
};
|
|
1284
|
+
const code = simple[next];
|
|
1285
|
+
if (code === void 0) bytes.push(...Buffer.from(next, "utf8"));
|
|
1286
|
+
else bytes.push(code);
|
|
1287
|
+
i += 1;
|
|
1288
|
+
}
|
|
1289
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1290
|
+
}
|
|
1291
|
+
function attribute(records, wanted) {
|
|
1292
|
+
const histories2 = /* @__PURE__ */ new Map();
|
|
1293
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
1294
|
+
for (const rel of wanted) {
|
|
1295
|
+
histories2.set(rel, []);
|
|
1296
|
+
tracked.set(rel, rel);
|
|
1297
|
+
}
|
|
1298
|
+
for (const record of records) {
|
|
1299
|
+
if (record.raw.length === 0) continue;
|
|
1300
|
+
const byNewPath = /* @__PURE__ */ new Map();
|
|
1301
|
+
for (const entry of record.raw) byNewPath.set(entry.newPath, entry);
|
|
1302
|
+
for (const [rel, current] of tracked) {
|
|
1303
|
+
const entry = byNewPath.get(current);
|
|
1304
|
+
if (!entry) continue;
|
|
1305
|
+
histories2.get(rel)?.push({
|
|
1306
|
+
sha: record.sha,
|
|
1307
|
+
authorDate: record.authorDate,
|
|
1308
|
+
authorName: record.authorName,
|
|
1309
|
+
authorEmail: record.authorEmail,
|
|
1310
|
+
coAuthors: record.coAuthors,
|
|
1311
|
+
reviewedBy: record.reviewedBy,
|
|
1312
|
+
generatedBy: record.generatedBy,
|
|
1313
|
+
oldBlob: entry.oldBlob === NULL_SHA ? null : entry.oldBlob,
|
|
1314
|
+
newBlob: entry.newBlob === NULL_SHA ? null : entry.newBlob,
|
|
1315
|
+
pathAtCommit: entry.newPath,
|
|
1316
|
+
otherParents: record.parents.slice(1),
|
|
1317
|
+
otherParentBlobs: []
|
|
1318
|
+
});
|
|
1319
|
+
if (entry.oldPath !== entry.newPath) tracked.set(rel, entry.oldPath);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return histories2;
|
|
1323
|
+
}
|
|
1324
|
+
function neededBlobs(histories2) {
|
|
1325
|
+
const shas = /* @__PURE__ */ new Set();
|
|
1326
|
+
for (const history of histories2.values()) {
|
|
1327
|
+
for (const h of history) {
|
|
1328
|
+
if (h.oldBlob !== null) shas.add(h.oldBlob);
|
|
1329
|
+
if (h.newBlob !== null) shas.add(h.newBlob);
|
|
1330
|
+
for (const b of h.otherParentBlobs) if (b !== null) shas.add(b);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return shas;
|
|
1334
|
+
}
|
|
1335
|
+
async function readOtherParentBlobs(run2, histories2) {
|
|
1336
|
+
const merges = [];
|
|
1337
|
+
const specs = [];
|
|
1338
|
+
for (const history of histories2.values()) {
|
|
1339
|
+
for (const h of history) {
|
|
1340
|
+
if (h.otherParents.length === 0) continue;
|
|
1341
|
+
merges.push(h);
|
|
1342
|
+
for (const parent of h.otherParents) specs.push(`${parent}:${h.pathAtCommit}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (specs.length === 0) return;
|
|
1346
|
+
const out = await run2(["cat-file", "--batch-check"], `${specs.join("\n")}
|
|
1347
|
+
`);
|
|
1348
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1349
|
+
if (out.code !== 0) throw new BlobsUnreadable(`exit ${String(out.code)}`);
|
|
1350
|
+
const lines = text(out).split("\n");
|
|
1351
|
+
let i = 0;
|
|
1352
|
+
for (const h of merges) {
|
|
1353
|
+
h.otherParentBlobs = h.otherParents.map(() => blobOfCheckLine(lines[i++]));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function blobOfCheckLine(line) {
|
|
1357
|
+
const [sha, kind] = (line ?? "").split(" ");
|
|
1358
|
+
if (sha === void 0 || kind !== "blob" || !/^[0-9a-f]{40,64}$/.test(sha)) return null;
|
|
1359
|
+
return sha;
|
|
1360
|
+
}
|
|
1361
|
+
async function fetchBlobs(run2, shas) {
|
|
1362
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1363
|
+
if (shas.size === 0) return blobs;
|
|
1364
|
+
const out = await run2(["cat-file", "--batch"], `${[...shas].join("\n")}
|
|
1365
|
+
`);
|
|
1366
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1367
|
+
if (out.code !== 0) throw new BlobsUnreadable(`exit ${String(out.code)}`);
|
|
1368
|
+
const buf = out.raw;
|
|
1369
|
+
let cursor = 0;
|
|
1370
|
+
while (cursor < buf.length) {
|
|
1371
|
+
const nl = buf.indexOf(10, cursor);
|
|
1372
|
+
if (nl === -1) break;
|
|
1373
|
+
const header = buf.toString("utf8", cursor, nl).split(" ");
|
|
1374
|
+
cursor = nl + 1;
|
|
1375
|
+
const sha = header[0];
|
|
1376
|
+
const kind = header[1];
|
|
1377
|
+
const size = Number(header[2]);
|
|
1378
|
+
if (sha === void 0 || kind !== "blob" || !Number.isFinite(size)) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
blobs.set(sha, buf.toString("utf8", cursor, cursor + size));
|
|
1382
|
+
cursor += size + 1;
|
|
1383
|
+
}
|
|
1384
|
+
return blobs;
|
|
1385
|
+
}
|
|
1386
|
+
var git = (value, evidence) => ({
|
|
1387
|
+
value,
|
|
1388
|
+
source: "git",
|
|
1389
|
+
evidence
|
|
1390
|
+
});
|
|
1391
|
+
function factCommits(input, history, blobs) {
|
|
1392
|
+
const fenced = input.extracted.fenced;
|
|
1393
|
+
const blobBody = (sha) => sha === null ? null : bodyOf(blobs.get(sha) ?? "", fenced);
|
|
1394
|
+
const bodyChanging = [...history].reverse().filter((h) => {
|
|
1395
|
+
const body = blobBody(h.newBlob);
|
|
1396
|
+
if (sameBody(body, blobBody(h.oldBlob))) return false;
|
|
1397
|
+
return !h.otherParentBlobs.some((b) => b !== null && sameBody(body, blobBody(b)));
|
|
1398
|
+
});
|
|
1399
|
+
return {
|
|
1400
|
+
bodyChanging,
|
|
1401
|
+
oldest: history[history.length - 1],
|
|
1402
|
+
newestBody: bodyChanging[bodyChanging.length - 1]
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
async function stampsInManifests(run2, root, bucket, histories2, blobs, parse) {
|
|
1406
|
+
const reads = [];
|
|
1407
|
+
for (const entry of bucket) {
|
|
1408
|
+
const refs = entry.input.stampManifests;
|
|
1409
|
+
if (refs === void 0) continue;
|
|
1410
|
+
const { oldest, newestBody } = factCommits(entry.input, histories2.get(entry.rel) ?? [], blobs);
|
|
1411
|
+
for (const field of STAMPED_DATE_FIELDS) {
|
|
1412
|
+
const ref = refs[field];
|
|
1413
|
+
const at = field === "created" ? oldest : newestBody;
|
|
1414
|
+
if (ref === void 0 || at === void 0) continue;
|
|
1415
|
+
const rel = insideRoot(root, ref.absPath);
|
|
1416
|
+
if (rel === void 0) continue;
|
|
1417
|
+
const key = ref.join === "path" ? entryAt(ref.entry, entry.rel, at.pathAtCommit) : ref.entry;
|
|
1418
|
+
const revs = field === "created" ? [at.sha] : [at.sha, `${at.sha}^`];
|
|
1419
|
+
for (const rev of revs) {
|
|
1420
|
+
reads.push({ label: entry.input.label, field, rev, spec: `${rev}:${rel}`, key, join: ref.join });
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const out = /* @__PURE__ */ new Map();
|
|
1425
|
+
if (reads.length === 0) return out;
|
|
1426
|
+
const fetched = await fetchSpecs(run2, [...new Set(reads.map((r) => r.spec))]);
|
|
1427
|
+
const values = /* @__PURE__ */ new Map();
|
|
1428
|
+
const keyOf = (label, field, rev) => `${label}\0${field}\0${rev}`;
|
|
1429
|
+
for (const r of reads) {
|
|
1430
|
+
values.set(keyOf(r.label, r.field, r.rev), manifestValue(fetched.get(r.spec), r.key, r.join, r.field, parse));
|
|
1431
|
+
}
|
|
1432
|
+
for (const label of new Set(reads.map((r) => r.label))) {
|
|
1433
|
+
out.set(label, (field, rev) => values.get(keyOf(label, field, rev)));
|
|
1434
|
+
}
|
|
1435
|
+
return out;
|
|
1436
|
+
}
|
|
1437
|
+
function judge(input, root, history, blobs, now, machines, manifestStamp2) {
|
|
1438
|
+
const fenced = input.extracted.fenced;
|
|
1439
|
+
const extractor = extractorFor(input);
|
|
1440
|
+
const blobBody = (sha) => sha === null ? null : bodyOf(blobs.get(sha) ?? "", fenced);
|
|
1441
|
+
const stampsOf = (sha) => {
|
|
1442
|
+
if (sha === null) return {};
|
|
1443
|
+
const content = blobs.get(sha);
|
|
1444
|
+
return content === void 0 ? {} : extractStamps(extractor, content, input.absPath);
|
|
1445
|
+
};
|
|
1446
|
+
const stampAt = (field, h, parent) => {
|
|
1447
|
+
const kept = manifestStamp2?.(field, parent ? `${h.sha}^` : h.sha);
|
|
1448
|
+
if (kept !== void 0 && kept !== null) return kept;
|
|
1449
|
+
return stampsOf(parent ? h.oldBlob : h.newBlob)[field];
|
|
1450
|
+
};
|
|
1451
|
+
const { bodyChanging, oldest, newestBody } = factCommits(input, history, blobs);
|
|
1452
|
+
const head = history[0];
|
|
1453
|
+
const headBlob = head === void 0 ? null : head.newBlob;
|
|
1454
|
+
const created = oldest === void 0 ? null : createdOf(oldest, stampAt("created", oldest, false));
|
|
1455
|
+
const workingChanged = !sameBody(
|
|
1456
|
+
bodyOf(input.content, fenced),
|
|
1457
|
+
headBlob === null ? null : blobBody(headBlob)
|
|
1458
|
+
);
|
|
1459
|
+
let lastUpdated;
|
|
1460
|
+
let lastBodyCommit;
|
|
1461
|
+
if (workingChanged) {
|
|
1462
|
+
lastBodyCommit = null;
|
|
1463
|
+
const working = input.extracted.data["last-updated"];
|
|
1464
|
+
const committed = stampsOf(headBlob)["last-updated"];
|
|
1465
|
+
lastUpdated = working !== void 0 && working !== null && !sameValue(working, committed) ? git(working, "stamped in working tree") : git(utcDate2(now()), "uncommitted body change");
|
|
1466
|
+
} else if (newestBody === void 0) {
|
|
1467
|
+
lastBodyCommit = null;
|
|
1468
|
+
lastUpdated = null;
|
|
1469
|
+
} else {
|
|
1470
|
+
lastBodyCommit = newestBody.sha;
|
|
1471
|
+
lastUpdated = lastUpdatedOf(
|
|
1472
|
+
newestBody,
|
|
1473
|
+
stampAt("last-updated", newestBody, false),
|
|
1474
|
+
stampAt("last-updated", newestBody, true)
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
const authors = bodyChanging.length === 0 ? null : authorsOf(bodyChanging, machines);
|
|
1478
|
+
let reviewedBy = null;
|
|
1479
|
+
let lastReviewed = null;
|
|
1480
|
+
if (!workingChanged && newestBody !== void 0 && newestBody.reviewedBy.length > 0) {
|
|
1481
|
+
const evidence = `Reviewed-by trailer in ${short(newestBody.sha)}`;
|
|
1482
|
+
reviewedBy = git(newestBody.reviewedBy.map(nameOf), evidence);
|
|
1483
|
+
lastReviewed = git(datePart(newestBody.authorDate), evidence);
|
|
1484
|
+
}
|
|
1485
|
+
return {
|
|
1486
|
+
created,
|
|
1487
|
+
"last-updated": lastUpdated,
|
|
1488
|
+
authors,
|
|
1489
|
+
"reviewed-by": reviewedBy,
|
|
1490
|
+
"last-reviewed": lastReviewed,
|
|
1491
|
+
provenance: null,
|
|
1492
|
+
lastBodyCommit,
|
|
1493
|
+
root
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function createdOf(oldest, stamp) {
|
|
1497
|
+
if (stamp !== void 0 && stamp !== null) {
|
|
1498
|
+
return git(stamp, `stamped in ${short(oldest.sha)}`);
|
|
1499
|
+
}
|
|
1500
|
+
const date = datePart(oldest.authorDate);
|
|
1501
|
+
return git(date, `added in ${short(oldest.sha)} (${date})`);
|
|
1502
|
+
}
|
|
1503
|
+
function lastUpdatedOf(commit, stamp, before) {
|
|
1504
|
+
if (stamp !== void 0 && stamp !== null && !sameValue(stamp, before)) {
|
|
1505
|
+
return git(stamp, `stamped in ${short(commit.sha)}`);
|
|
1506
|
+
}
|
|
1507
|
+
const date = datePart(commit.authorDate);
|
|
1508
|
+
return git(date, `body changed in ${short(commit.sha)} (${date})`);
|
|
1509
|
+
}
|
|
1510
|
+
function authorsOf(bodyChanging, machines) {
|
|
1511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1512
|
+
const names = [];
|
|
1513
|
+
const add = (name, email) => {
|
|
1514
|
+
if (name === "") return;
|
|
1515
|
+
const identity = email === null || email === "" ? name : `${name} <${email}>`;
|
|
1516
|
+
if (machineIdentity(identity, machines) !== void 0) return;
|
|
1517
|
+
const key = email === null || email === "" ? `name:${name.toLowerCase()}` : email.toLowerCase();
|
|
1518
|
+
if (seen.has(key)) return;
|
|
1519
|
+
seen.add(key);
|
|
1520
|
+
names.push(name);
|
|
1521
|
+
};
|
|
1522
|
+
for (const h of bodyChanging) {
|
|
1523
|
+
add(h.authorName, h.authorEmail);
|
|
1524
|
+
for (const co of h.coAuthors) add(nameOf(co), emailOf(co));
|
|
1525
|
+
}
|
|
1526
|
+
const n = bodyChanging.length;
|
|
1527
|
+
return git(names, `${n} body-changing ${n === 1 ? "commit" : "commits"}`);
|
|
1528
|
+
}
|
|
1529
|
+
function provenanceFenced(extracted) {
|
|
1530
|
+
return extracted.fenced === true || extracted.format === "markdown" || extracted.format === "mdx";
|
|
1531
|
+
}
|
|
1532
|
+
var OUTSIDE_RANGE = "outside-the-attributed-range";
|
|
1533
|
+
var ATTRIBUTED = ":attributed";
|
|
1534
|
+
function provenanceFor(entry, blame, commits, opts) {
|
|
1535
|
+
const { input } = entry;
|
|
1536
|
+
const fenced = provenanceFenced(input.extracted);
|
|
1537
|
+
const base = { content: input.content, commits, machines: opts.machines, fenced };
|
|
1538
|
+
let derivation;
|
|
1539
|
+
const attribution = input.attribution;
|
|
1540
|
+
if (attribution !== void 0) {
|
|
1541
|
+
const page = readProvenancePage(input.content, { fenced });
|
|
1542
|
+
const { bodyLine } = page;
|
|
1543
|
+
const ranges = attribution.targets.flatMap((target) => {
|
|
1544
|
+
const entry2 = attributeRange({ ...base, blame, target, generatedBy: attribution.generatedBy, page });
|
|
1545
|
+
const span = entry2 === void 0 ? void 0 : parseLines(entry2.lines);
|
|
1546
|
+
return span === void 0 ? [] : [toFileLines(span, bodyLine)];
|
|
1547
|
+
});
|
|
1548
|
+
const inRange = (line) => ranges.some((range) => line.finalLine >= range.start && line.finalLine <= range.end);
|
|
1549
|
+
const attributed = new Map(commits);
|
|
1550
|
+
const trailers = { generatedBy: [attribution.generatedBy], coAuthoredBy: [] };
|
|
1551
|
+
attributed.set(OUTSIDE_RANGE, { sha: OUTSIDE_RANGE, trailers: { generatedBy: [], coAuthoredBy: [] } });
|
|
1552
|
+
const rekeyed = blame.map((line) => {
|
|
1553
|
+
if (inRange(line)) {
|
|
1554
|
+
if (line.uncommitted) return line;
|
|
1555
|
+
const sha = `${line.sha}${ATTRIBUTED}`;
|
|
1556
|
+
attributed.set(sha, { sha, trailers });
|
|
1557
|
+
return { ...line, sha };
|
|
1558
|
+
}
|
|
1559
|
+
return line.uncommitted ? { ...line, sha: OUTSIDE_RANGE, uncommitted: false } : line;
|
|
1560
|
+
});
|
|
1561
|
+
derivation = deriveProvenance({
|
|
1562
|
+
...base,
|
|
1563
|
+
commits: attributed,
|
|
1564
|
+
generatedBy: attribution.generatedBy,
|
|
1565
|
+
blame: rekeyed
|
|
1566
|
+
});
|
|
1567
|
+
} else {
|
|
1568
|
+
derivation = deriveProvenance({
|
|
1569
|
+
...base,
|
|
1570
|
+
blame,
|
|
1571
|
+
...opts.generatedBy !== void 0 ? { generatedBy: opts.generatedBy } : {}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
const entries = derivation.derived.map((d) => d.entry);
|
|
1575
|
+
return {
|
|
1576
|
+
value: entries.length === 0 ? null : git(entries, provenanceEvidence(derivation)),
|
|
1577
|
+
derivation
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function provenanceEvidence(derivation) {
|
|
1581
|
+
const shas = [];
|
|
1582
|
+
let uncommitted = false;
|
|
1583
|
+
for (const range of derivation.derived) {
|
|
1584
|
+
for (let n = range.span.start; n <= range.span.end; n++) {
|
|
1585
|
+
const evidence = derivation.evidenceByLine.get(n);
|
|
1586
|
+
if (evidence?.machine === void 0) continue;
|
|
1587
|
+
if (evidence.rule === 1) uncommitted = true;
|
|
1588
|
+
else if (!shas.includes(evidence.sha)) shas.push(evidence.sha);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const first = shas[0];
|
|
1592
|
+
if (first === void 0) return "uncommitted";
|
|
1593
|
+
const parts = [`blame ${short(first)}`];
|
|
1594
|
+
if (shas.length > 1) parts.push(`${String(shas.length)} commits`);
|
|
1595
|
+
if (uncommitted) parts.push("uncommitted");
|
|
1596
|
+
return parts.join(", ");
|
|
1597
|
+
}
|
|
1598
|
+
async function blameOf(run2, rel, content) {
|
|
1599
|
+
const out = await run2([
|
|
1600
|
+
"-c",
|
|
1601
|
+
"core.quotePath=false",
|
|
1602
|
+
"blame",
|
|
1603
|
+
"--no-ignore-revs-file",
|
|
1604
|
+
"--line-porcelain",
|
|
1605
|
+
"--",
|
|
1606
|
+
rel
|
|
1607
|
+
]);
|
|
1608
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1609
|
+
if (out.code !== 0) {
|
|
1610
|
+
const inHead = await run2(["cat-file", "-e", `HEAD:${rel}`]);
|
|
1611
|
+
if (inHead.code !== 0) {
|
|
1612
|
+
return splitLines(content).map((line, i) => ({
|
|
1613
|
+
sha: ZERO_SHA,
|
|
1614
|
+
origLine: i + 1,
|
|
1615
|
+
finalLine: i + 1,
|
|
1616
|
+
author: "Not Committed Yet",
|
|
1617
|
+
authorMail: "not.committed.yet",
|
|
1618
|
+
filename: rel,
|
|
1619
|
+
boundary: false,
|
|
1620
|
+
uncommitted: true,
|
|
1621
|
+
content: line
|
|
1622
|
+
}));
|
|
1623
|
+
}
|
|
1624
|
+
throw new BlameUnreadable(rel, out.stderr.trim());
|
|
1625
|
+
}
|
|
1626
|
+
return parseLinePorcelain(text(out));
|
|
1627
|
+
}
|
|
1628
|
+
async function commitEvidence(run2, root, pages, blobs, parse) {
|
|
1629
|
+
const plans = pages.map(({ entry, blame, history }) => {
|
|
1630
|
+
const pathAt = /* @__PURE__ */ new Map();
|
|
1631
|
+
for (const line of blame) {
|
|
1632
|
+
if (!line.uncommitted && !pathAt.has(line.sha)) pathAt.set(line.sha, unquotePath(line.filename));
|
|
1633
|
+
}
|
|
1634
|
+
const trailers = /* @__PURE__ */ new Map();
|
|
1635
|
+
const pageBlob = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const h of history) {
|
|
1637
|
+
trailers.set(h.sha, { generatedBy: h.generatedBy, coAuthoredBy: h.coAuthors });
|
|
1638
|
+
const blob = h.newBlob === null ? void 0 : blobs.get(h.newBlob);
|
|
1639
|
+
if (blob !== void 0) pageBlob.set(h.sha, blob);
|
|
1640
|
+
}
|
|
1641
|
+
const manifest = entry.input.provenanceManifest;
|
|
1642
|
+
const manifestRel = manifest === void 0 ? void 0 : insideRoot(root, manifest.absPath);
|
|
1643
|
+
return { entry, pathAt, trailers, pageBlob, manifest, manifestRel };
|
|
1644
|
+
});
|
|
1645
|
+
const unlogged = /* @__PURE__ */ new Set();
|
|
1646
|
+
const specs = /* @__PURE__ */ new Set();
|
|
1647
|
+
for (const { pathAt, trailers, pageBlob, manifestRel } of plans) {
|
|
1648
|
+
for (const [sha, path] of pathAt) {
|
|
1649
|
+
if (!trailers.has(sha)) unlogged.add(sha);
|
|
1650
|
+
if (!pageBlob.has(sha)) specs.add(`${sha}:${path}`);
|
|
1651
|
+
if (manifestRel !== void 0) specs.add(`${sha}:${manifestRel}`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const logged = /* @__PURE__ */ new Map();
|
|
1655
|
+
if (unlogged.size > 0) {
|
|
1656
|
+
const out = await run2(["log", "--no-walk=unsorted", "--stdin", RECORD_FORMAT], `${[...unlogged].join("\n")}
|
|
1657
|
+
`);
|
|
1658
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1659
|
+
if (out.code !== 0) throw new BlobsUnreadable(`git log exit ${String(out.code)}`);
|
|
1660
|
+
for (const record of parseLog(text(out))) {
|
|
1661
|
+
logged.set(record.sha, { generatedBy: record.generatedBy, coAuthoredBy: record.coAuthors });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const fetched = await fetchSpecs(run2, [...specs]);
|
|
1665
|
+
return plans.map(({ entry, pathAt, trailers, pageBlob, manifest, manifestRel }) => {
|
|
1666
|
+
const commits = /* @__PURE__ */ new Map();
|
|
1667
|
+
for (const [sha, path] of pathAt) {
|
|
1668
|
+
const blob = pageBlob.get(sha) ?? fetched.get(`${sha}:${path}`);
|
|
1669
|
+
commits.set(sha, {
|
|
1670
|
+
sha,
|
|
1671
|
+
trailers: trailers.get(sha) ?? logged.get(sha) ?? { generatedBy: [], coAuthoredBy: [] },
|
|
1672
|
+
...blob !== void 0 ? { blob } : {},
|
|
1673
|
+
...manifest !== void 0 ? {
|
|
1674
|
+
stamp: manifestRel === void 0 ? [] : manifestStamp(
|
|
1675
|
+
fetched.get(`${sha}:${manifestRel}`),
|
|
1676
|
+
manifest.join === "path" ? entryAt(manifest.entry, entry.rel, path) : manifest.entry,
|
|
1677
|
+
manifest.join,
|
|
1678
|
+
parse
|
|
1679
|
+
)
|
|
1680
|
+
} : {}
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
return commits;
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
function insideRoot(root, abs) {
|
|
1687
|
+
const rel = relative2(root, abs);
|
|
1688
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return void 0;
|
|
1689
|
+
return rel.split(sep2).join("/");
|
|
1690
|
+
}
|
|
1691
|
+
function entryAt(entry, rel, relAtCommit) {
|
|
1692
|
+
if (relAtCommit === rel) return entry;
|
|
1693
|
+
const fromHere = posix.relative(posix.dirname(rel), relAtCommit);
|
|
1694
|
+
return posix.join(posix.dirname(entry), fromHere);
|
|
1695
|
+
}
|
|
1696
|
+
function manifestStamp(text2, key, join2, parse) {
|
|
1697
|
+
return provenanceEntries(manifestValue(text2, key, join2, PROVENANCE_FIELD, parse));
|
|
1698
|
+
}
|
|
1699
|
+
function yamlOnce() {
|
|
1700
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1701
|
+
return (text2) => {
|
|
1702
|
+
if (parsed.has(text2)) return parsed.get(text2);
|
|
1703
|
+
let data;
|
|
1704
|
+
try {
|
|
1705
|
+
data = parseYaml(text2);
|
|
1706
|
+
} catch {
|
|
1707
|
+
data = void 0;
|
|
1708
|
+
}
|
|
1709
|
+
parsed.set(text2, data);
|
|
1710
|
+
return data;
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function manifestValue(text2, key, join2, field, parse) {
|
|
1714
|
+
if (text2 === void 0) return void 0;
|
|
1715
|
+
const data = parse(text2);
|
|
1716
|
+
if (!isRecord3(data)) return void 0;
|
|
1717
|
+
for (const [candidate, value] of Object.entries(data)) {
|
|
1718
|
+
const same = join2 === "path" ? posix.normalize(candidate) === posix.normalize(key) : candidate === key;
|
|
1719
|
+
if (same && isRecord3(value)) return value[field];
|
|
1720
|
+
}
|
|
1721
|
+
return void 0;
|
|
1722
|
+
}
|
|
1723
|
+
function isRecord3(x) {
|
|
1724
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
1725
|
+
}
|
|
1726
|
+
async function fetchSpecs(run2, specs) {
|
|
1727
|
+
const found = /* @__PURE__ */ new Map();
|
|
1728
|
+
if (specs.length === 0) return found;
|
|
1729
|
+
const out = await run2(["cat-file", "--batch"], `${specs.join("\n")}
|
|
1730
|
+
`);
|
|
1731
|
+
if (out.tooLarge && specs.length > 1) {
|
|
1732
|
+
const half = Math.ceil(specs.length / 2);
|
|
1733
|
+
const first = await fetchSpecs(run2, specs.slice(0, half));
|
|
1734
|
+
const rest = await fetchSpecs(run2, specs.slice(half));
|
|
1735
|
+
return new Map([...first, ...rest]);
|
|
1736
|
+
}
|
|
1737
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
1738
|
+
if (out.code !== 0) throw new BlobsUnreadable(`exit ${String(out.code)}`);
|
|
1739
|
+
for (const [spec, content] of parseSpecBatch(out.raw, specs)) found.set(spec, content);
|
|
1740
|
+
return found;
|
|
1741
|
+
}
|
|
1742
|
+
function parseSpecBatch(buf, specs) {
|
|
1743
|
+
const found = /* @__PURE__ */ new Map();
|
|
1744
|
+
let cursor = 0;
|
|
1745
|
+
for (const spec of specs) {
|
|
1746
|
+
const nl = buf.indexOf(10, cursor);
|
|
1747
|
+
if (nl === -1) throw new BlobsUnreadable(`the answer ends before ${spec}`);
|
|
1748
|
+
const header = buf.toString("utf8", cursor, nl);
|
|
1749
|
+
cursor = nl + 1;
|
|
1750
|
+
if (header.endsWith(" missing") || header.endsWith(" ambiguous")) continue;
|
|
1751
|
+
const parts = header.split(" ");
|
|
1752
|
+
const size = Number(parts[2]);
|
|
1753
|
+
if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
|
|
1754
|
+
throw new BlobsUnreadable(`unreadable header for ${spec}: ${header}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (parts[1] === "blob") found.set(spec, buf.toString("utf8", cursor, cursor + size));
|
|
1757
|
+
cursor += size + 1;
|
|
1758
|
+
}
|
|
1759
|
+
return found;
|
|
1760
|
+
}
|
|
1761
|
+
function extractorFor(input) {
|
|
1762
|
+
return extractorByName(input.extracted.format) ?? extractorForExtension(extname(input.absPath));
|
|
1763
|
+
}
|
|
1764
|
+
function extractStamps(extractor, content, filePath) {
|
|
1765
|
+
if (!extractor) return {};
|
|
1766
|
+
try {
|
|
1767
|
+
return extractor.extract(content, filePath).data;
|
|
1768
|
+
} catch {
|
|
1769
|
+
return {};
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function sameBody(a, b) {
|
|
1773
|
+
if (a === null || b === null) return a === b;
|
|
1774
|
+
return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
|
|
1775
|
+
}
|
|
1776
|
+
function sameValue(a, b) {
|
|
1777
|
+
return toJsonText(a) === toJsonText(b);
|
|
1778
|
+
}
|
|
1779
|
+
var short = (sha) => sha.slice(0, 7);
|
|
1780
|
+
var datePart = (iso) => {
|
|
1781
|
+
const d = new Date(iso);
|
|
1782
|
+
return Number.isNaN(d.getTime()) ? "" : utcDate2(d);
|
|
1783
|
+
};
|
|
1784
|
+
function utcDate2(d) {
|
|
1785
|
+
return d.toISOString().slice(0, 10);
|
|
1786
|
+
}
|
|
1787
|
+
function nameOf(trailer) {
|
|
1788
|
+
const lt = trailer.lastIndexOf("<");
|
|
1789
|
+
return (lt === -1 ? trailer : trailer.slice(0, lt)).trim();
|
|
1790
|
+
}
|
|
1791
|
+
function emailOf(trailer) {
|
|
1792
|
+
const m = /<([^>]*)>\s*$/.exec(trailer);
|
|
1793
|
+
return m?.[1]?.trim() ?? null;
|
|
1794
|
+
}
|
|
1795
|
+
var MAX_STDERR_BYTES = 16 * 1024;
|
|
1796
|
+
var GIT_CONCURRENCY = Math.min(8, availableParallelism());
|
|
1797
|
+
function text(out) {
|
|
1798
|
+
try {
|
|
1799
|
+
return out.raw.toString("utf8");
|
|
1800
|
+
} catch {
|
|
1801
|
+
throw new HistoryTooLarge();
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function runGit(args, cwd, stdin, maxBytes) {
|
|
1805
|
+
return new Promise((settle, reject) => {
|
|
1806
|
+
let done = false;
|
|
1807
|
+
const finish = (out) => {
|
|
1808
|
+
if (done) return;
|
|
1809
|
+
done = true;
|
|
1810
|
+
settle(out);
|
|
1811
|
+
};
|
|
1812
|
+
const fail = (err) => {
|
|
1813
|
+
if (done) return;
|
|
1814
|
+
done = true;
|
|
1815
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1816
|
+
};
|
|
1817
|
+
const failed = (missing) => ({
|
|
1818
|
+
code: null,
|
|
1819
|
+
raw: Buffer.alloc(0),
|
|
1820
|
+
missing,
|
|
1821
|
+
tooLarge: false,
|
|
1822
|
+
stderr: ""
|
|
1823
|
+
});
|
|
1824
|
+
let child;
|
|
1825
|
+
try {
|
|
1826
|
+
child = spawn2("git", args, { cwd, windowsHide: true });
|
|
1827
|
+
} catch {
|
|
1828
|
+
finish(failed(true));
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
let chunks = [];
|
|
1832
|
+
let total = 0;
|
|
1833
|
+
let tooLarge = false;
|
|
1834
|
+
child.stdout.on("data", (chunk) => {
|
|
1835
|
+
if (tooLarge) return;
|
|
1836
|
+
total += chunk.length;
|
|
1837
|
+
if (total > maxBytes) {
|
|
1838
|
+
tooLarge = true;
|
|
1839
|
+
chunks = [];
|
|
1840
|
+
child.kill();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
chunks.push(chunk);
|
|
1844
|
+
});
|
|
1845
|
+
const errChunks = [];
|
|
1846
|
+
let errTotal = 0;
|
|
1847
|
+
child.stderr.on("data", (chunk) => {
|
|
1848
|
+
if (errTotal >= MAX_STDERR_BYTES) return;
|
|
1849
|
+
const kept = chunk.subarray(0, MAX_STDERR_BYTES - errTotal);
|
|
1850
|
+
errTotal += kept.length;
|
|
1851
|
+
errChunks.push(kept);
|
|
1852
|
+
});
|
|
1853
|
+
child.on("error", (err) => {
|
|
1854
|
+
finish(failed(err.code === "ENOENT"));
|
|
1855
|
+
});
|
|
1856
|
+
child.stdin.on("error", () => {
|
|
1857
|
+
});
|
|
1858
|
+
child.on("close", (code) => {
|
|
1859
|
+
try {
|
|
1860
|
+
const raw = tooLarge ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
1861
|
+
const stderr = Buffer.concat(errChunks).toString("utf8");
|
|
1862
|
+
finish({ code, raw, missing: false, tooLarge, stderr });
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
fail(err);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
if (stdin === void 0) child.stdin.end();
|
|
1868
|
+
else child.stdin.end(stdin);
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/meta/core/derive/index.ts
|
|
1873
|
+
var FIELD_SOURCES = {
|
|
1874
|
+
created: ["git"],
|
|
1875
|
+
"last-updated": ["git"],
|
|
1876
|
+
authors: ["git"],
|
|
1877
|
+
owner: ["codeowners"],
|
|
1878
|
+
"reviewed-by": ["github", "gitlab", "git"],
|
|
1879
|
+
"last-reviewed": ["github", "gitlab", "git"],
|
|
1880
|
+
provenance: ["git"]
|
|
1881
|
+
};
|
|
1882
|
+
var REVIEW_SOURCES = ["github", "gitlab"];
|
|
1883
|
+
var HOST_NAME = {
|
|
1884
|
+
github: "GitHub",
|
|
1885
|
+
gitlab: "GitLab"
|
|
1886
|
+
};
|
|
1887
|
+
function sourcesFor(field, commands) {
|
|
1888
|
+
if (isBuiltinField(field)) return FIELD_SOURCES[field];
|
|
1889
|
+
if (commands !== void 0 && Object.hasOwn(commands, field)) return ["command"];
|
|
1890
|
+
return [];
|
|
1891
|
+
}
|
|
1892
|
+
function consultedSources(fields, sources, commands) {
|
|
1893
|
+
const needed = /* @__PURE__ */ new Set();
|
|
1894
|
+
for (const field of fields) {
|
|
1895
|
+
for (const s of sourcesFor(field, commands)) needed.add(s);
|
|
1896
|
+
}
|
|
1897
|
+
return sources.filter((s) => needed.has(s));
|
|
1898
|
+
}
|
|
1899
|
+
function assertSourcesAvailable(sources, hint) {
|
|
1900
|
+
for (const name of Object.keys(sources)) {
|
|
1901
|
+
const status = sources[name];
|
|
1902
|
+
if (status === void 0 || status.available) continue;
|
|
1903
|
+
const reason = status.reason ?? "it could not answer";
|
|
1904
|
+
throw new DocmetaError(
|
|
1905
|
+
`${name} source unavailable: ${reason}${hint === void 0 ? "" : `; ${hint}`}`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
async function deriveMetadata(inputs, ctx) {
|
|
1910
|
+
const consulted = new Set(consultedSources(ctx.fields, ctx.sources, ctx.commands));
|
|
1911
|
+
const sources = {};
|
|
1912
|
+
let git2 = /* @__PURE__ */ new Map();
|
|
1913
|
+
if (consulted.has("git")) {
|
|
1914
|
+
const result = await deriveFromGit(inputs, {
|
|
1915
|
+
cwd: ctx.cwd,
|
|
1916
|
+
now: ctx.now,
|
|
1917
|
+
fields: ctx.fields,
|
|
1918
|
+
...ctx.generatedBy !== void 0 ? { generatedBy: ctx.generatedBy } : {},
|
|
1919
|
+
...ctx.machines !== void 0 ? { machines: ctx.machines } : {}
|
|
1920
|
+
});
|
|
1921
|
+
sources.git = result.status;
|
|
1922
|
+
git2 = result.records;
|
|
1923
|
+
}
|
|
1924
|
+
const provenance = /* @__PURE__ */ new Map();
|
|
1925
|
+
for (const [label, facts] of git2) {
|
|
1926
|
+
if (facts.provenanceDerivation !== void 0) provenance.set(label, facts.provenanceDerivation);
|
|
1927
|
+
}
|
|
1928
|
+
const rootCache = /* @__PURE__ */ new Map();
|
|
1929
|
+
const rootOf = (absPath) => {
|
|
1930
|
+
const dir = dirname3(absPath);
|
|
1931
|
+
let root = rootCache.get(dir);
|
|
1932
|
+
if (root === void 0) {
|
|
1933
|
+
root = findGitRoot(dir);
|
|
1934
|
+
rootCache.set(dir, root);
|
|
1935
|
+
}
|
|
1936
|
+
return root;
|
|
1937
|
+
};
|
|
1938
|
+
const rootFor = (input) => git2.get(input.label)?.root ?? rootOf(input.absPath);
|
|
1939
|
+
let owners = /* @__PURE__ */ new Map();
|
|
1940
|
+
if (consulted.has("codeowners")) {
|
|
1941
|
+
const result = await deriveFromCodeowners(inputs, {
|
|
1942
|
+
explicit: ctx.codeowners,
|
|
1943
|
+
configDir: ctx.configDir,
|
|
1944
|
+
rootOf
|
|
1945
|
+
});
|
|
1946
|
+
sources.codeowners = result.status;
|
|
1947
|
+
owners = result.records;
|
|
1948
|
+
}
|
|
1949
|
+
const requested = REVIEW_SOURCES.filter((s) => consulted.has(s));
|
|
1950
|
+
let reviews = /* @__PURE__ */ new Map();
|
|
1951
|
+
if (requested.length > 0) {
|
|
1952
|
+
if (!consulted.has("git")) {
|
|
1953
|
+
for (const s of requested) {
|
|
1954
|
+
sources[s] = {
|
|
1955
|
+
available: false,
|
|
1956
|
+
reason: `the ${s} source needs the git source to find each document's commits; add git to sources`
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
} else if (sources.git?.available === false) {
|
|
1960
|
+
for (const s of requested) {
|
|
1961
|
+
sources[s] = {
|
|
1962
|
+
available: false,
|
|
1963
|
+
reason: `the git source could not answer, so the ${s} source cannot locate any commit`
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
} else {
|
|
1967
|
+
const result = await deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested);
|
|
1968
|
+
Object.assign(sources, result.status);
|
|
1969
|
+
reviews = result.records;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
let commanded = /* @__PURE__ */ new Map();
|
|
1973
|
+
const doomed = Object.values(sources).some((s) => !s.available);
|
|
1974
|
+
if (consulted.has("command") && ctx.commands !== void 0 && !doomed) {
|
|
1975
|
+
const wanted = new Set(ctx.fields);
|
|
1976
|
+
const commands = Object.fromEntries(
|
|
1977
|
+
Object.entries(ctx.commands).filter(([field]) => wanted.has(field))
|
|
1978
|
+
);
|
|
1979
|
+
if (Object.keys(commands).length > 0) {
|
|
1980
|
+
const result = await deriveFromCommands(inputs, commands, { cwd: ctx.configDir ?? ctx.cwd });
|
|
1981
|
+
sources.command = result.status;
|
|
1982
|
+
commanded = result.records;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const records = /* @__PURE__ */ new Map();
|
|
1986
|
+
for (const input of inputs) {
|
|
1987
|
+
const fields = {};
|
|
1988
|
+
const g = git2.get(input.label);
|
|
1989
|
+
const r = reviews.get(input.label);
|
|
1990
|
+
const c = commanded.get(input.label);
|
|
1991
|
+
for (const field of ctx.fields) {
|
|
1992
|
+
let value = null;
|
|
1993
|
+
for (const source of sourcesFor(field, ctx.commands)) {
|
|
1994
|
+
if (!consulted.has(source)) continue;
|
|
1995
|
+
const candidate = valueFrom(source, field, g, r, owners.get(input.label), c);
|
|
1996
|
+
if (candidate !== null) {
|
|
1997
|
+
value = candidate;
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
fields[field] = value;
|
|
2002
|
+
}
|
|
2003
|
+
records.set(input.label, { file: input.label, fields });
|
|
2004
|
+
}
|
|
2005
|
+
return { records, sources, ...provenance.size > 0 ? { provenance } : {} };
|
|
2006
|
+
}
|
|
2007
|
+
function valueFrom(source, field, git2, reviews, owner, commanded) {
|
|
2008
|
+
switch (source) {
|
|
2009
|
+
case "git":
|
|
2010
|
+
if (git2 === void 0 || !isBuiltinField(field) || field === "owner") return null;
|
|
2011
|
+
return git2[field];
|
|
2012
|
+
case "command":
|
|
2013
|
+
return commanded?.[field] ?? null;
|
|
2014
|
+
case "codeowners":
|
|
2015
|
+
return field === "owner" ? owner ?? null : null;
|
|
2016
|
+
case "github":
|
|
2017
|
+
case "gitlab": {
|
|
2018
|
+
if (reviews === void 0) return null;
|
|
2019
|
+
if (field !== "reviewed-by" && field !== "last-reviewed") return null;
|
|
2020
|
+
const fact = reviews[field];
|
|
2021
|
+
return fact !== null && fact.source === source ? fact : null;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
async function deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested) {
|
|
2026
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
2027
|
+
for (const input of inputs) {
|
|
2028
|
+
const root = rootFor(input);
|
|
2029
|
+
if (root === null) continue;
|
|
2030
|
+
const bucket = byRoot.get(root) ?? [];
|
|
2031
|
+
bucket.push(input);
|
|
2032
|
+
byRoot.set(root, bucket);
|
|
2033
|
+
}
|
|
2034
|
+
const status = {};
|
|
2035
|
+
const records = /* @__PURE__ */ new Map();
|
|
2036
|
+
const unavailable2 = (which, reason) => {
|
|
2037
|
+
for (const s of which) status[s] = { available: false, reason };
|
|
2038
|
+
return { status, records: /* @__PURE__ */ new Map() };
|
|
2039
|
+
};
|
|
2040
|
+
if (byRoot.size === 0) {
|
|
2041
|
+
return unavailable2(requested, "no git repository contains the documents");
|
|
2042
|
+
}
|
|
2043
|
+
const cacheDir = resolve2(ctx.configDir ?? ctx.cwd, REVIEW_CACHE_DIR);
|
|
2044
|
+
for (const [root, bucket] of byRoot) {
|
|
2045
|
+
const client = ctx.reviews ?? await createReviewClient(root, { cwd: ctx.cwd, cache: ctx.cache, cacheDir, requested });
|
|
2046
|
+
const identity = await client.detect();
|
|
2047
|
+
if (identity === null) {
|
|
2048
|
+
const s = await client.status();
|
|
2049
|
+
return unavailable2(requested, s.reason ?? "no origin remote to tell GitHub from GitLab");
|
|
2050
|
+
}
|
|
2051
|
+
const source = identity.kind;
|
|
2052
|
+
if (!requested.includes(source)) {
|
|
2053
|
+
return unavailable2(
|
|
2054
|
+
requested,
|
|
2055
|
+
`the origin remote is ${identity.host}, which is ${HOST_NAME[source]}; add ${source} to sources, or drop reviewed-by and last-reviewed from the managed fields`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
const result = await deriveFromReviews(
|
|
2059
|
+
bucket.map((input) => ({
|
|
2060
|
+
label: input.label,
|
|
2061
|
+
sha: git2.get(input.label)?.lastBodyCommit ?? null
|
|
2062
|
+
})),
|
|
2063
|
+
client
|
|
2064
|
+
);
|
|
2065
|
+
if (!result.status.available) {
|
|
2066
|
+
status[source] = result.status;
|
|
2067
|
+
return { status, records: /* @__PURE__ */ new Map() };
|
|
2068
|
+
}
|
|
2069
|
+
status[source] = result.status;
|
|
2070
|
+
for (const [label, facts] of result.records) records.set(label, facts);
|
|
2071
|
+
}
|
|
2072
|
+
return { status, records };
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/meta/core/derive/table.ts
|
|
2076
|
+
var DERIVED_VIEW = "derived";
|
|
2077
|
+
var DERIVED_ROWS = "_derived_rows";
|
|
2078
|
+
var RESOLVED_VIEW = "resolved";
|
|
2079
|
+
var ORIGIN_COLUMN = "_origin";
|
|
2080
|
+
var SOURCES_COLUMN = "_sources";
|
|
2081
|
+
function derivedColumns(commands) {
|
|
2082
|
+
return ["_path", ...derivableFields(commands), SOURCES_COLUMN];
|
|
2083
|
+
}
|
|
2084
|
+
function mentionsDerived(sql) {
|
|
2085
|
+
return /\b(?:from|join|update|into|table|view)\s+(?:"derived"|derived\b)/i.test(sql);
|
|
2086
|
+
}
|
|
2087
|
+
function fieldsForSql(sql, fields) {
|
|
2088
|
+
const named = (field) => new RegExp(`(?<![\\w-])${escapeRegExp(field)}(?![\\w-])`, "i").test(sql);
|
|
2089
|
+
if (sql.includes("*") || /\b_(?:sources|origin)\b/i.test(sql)) {
|
|
2090
|
+
return fields.filter((field) => field !== PROVENANCE_FIELD || named(field));
|
|
2091
|
+
}
|
|
2092
|
+
return fields.filter(named);
|
|
2093
|
+
}
|
|
2094
|
+
function escapeRegExp(s) {
|
|
2095
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2096
|
+
}
|
|
2097
|
+
function createDerivedView(db, records, columns) {
|
|
2098
|
+
const quoted = columns.map(quoteIdent);
|
|
2099
|
+
db.exec(
|
|
2100
|
+
`CREATE TABLE ${DERIVED_ROWS} (${quoted.map((c, i) => i === 0 ? `${c} TEXT PRIMARY KEY` : `${c} TEXT`).join(", ")})`
|
|
2101
|
+
);
|
|
2102
|
+
const insert = db.prepare(
|
|
2103
|
+
`INSERT INTO ${DERIVED_ROWS} VALUES (${quoted.map(() => "?").join(", ")})`
|
|
2104
|
+
);
|
|
2105
|
+
const fields = columns.slice(1, -1);
|
|
2106
|
+
db.exec("BEGIN");
|
|
2107
|
+
let committed = false;
|
|
2108
|
+
try {
|
|
2109
|
+
for (const [label, record] of records) {
|
|
2110
|
+
const sources = {};
|
|
2111
|
+
const values = fields.map((field) => {
|
|
2112
|
+
const derived = record.fields[field];
|
|
2113
|
+
if (derived == null) return null;
|
|
2114
|
+
sources[field] = { source: derived.source, evidence: derived.evidence };
|
|
2115
|
+
return bindValue(derived.value);
|
|
2116
|
+
});
|
|
2117
|
+
insert.run(label, ...values, JSON.stringify(sources));
|
|
2118
|
+
}
|
|
2119
|
+
db.exec("COMMIT");
|
|
2120
|
+
committed = true;
|
|
2121
|
+
} finally {
|
|
2122
|
+
if (!committed) db.exec("ROLLBACK");
|
|
2123
|
+
}
|
|
2124
|
+
db.exec(`CREATE VIEW ${DERIVED_VIEW} AS SELECT * FROM ${DERIVED_ROWS}`);
|
|
2125
|
+
}
|
|
2126
|
+
function resolvedColumns(docsColumns, commands) {
|
|
2127
|
+
const columns = ["_path"];
|
|
2128
|
+
const seen = /* @__PURE__ */ new Set(["_path", ORIGIN_COLUMN, SOURCES_COLUMN]);
|
|
2129
|
+
for (const column of docsColumns) {
|
|
2130
|
+
if (seen.has(column) || RESERVED.has(column)) continue;
|
|
2131
|
+
seen.add(column);
|
|
2132
|
+
columns.push(column);
|
|
2133
|
+
}
|
|
2134
|
+
for (const field of derivableFields(commands)) {
|
|
2135
|
+
if (seen.has(field)) continue;
|
|
2136
|
+
seen.add(field);
|
|
2137
|
+
columns.push(field);
|
|
2138
|
+
}
|
|
2139
|
+
columns.push(ORIGIN_COLUMN, SOURCES_COLUMN);
|
|
2140
|
+
return columns;
|
|
2141
|
+
}
|
|
2142
|
+
function mentionsResolved(sql) {
|
|
2143
|
+
return /\b(?:from|join|update|into|table|view)\s+(?:"resolved"|resolved\b)/i.test(sql);
|
|
2144
|
+
}
|
|
2145
|
+
function jsonPath(field) {
|
|
2146
|
+
return `$.${JSON.stringify(field)}`;
|
|
2147
|
+
}
|
|
2148
|
+
function sqlString(value) {
|
|
2149
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2150
|
+
}
|
|
2151
|
+
function createResolvedView(db, docsColumns, commands) {
|
|
2152
|
+
const columns = resolvedColumns(docsColumns, commands);
|
|
2153
|
+
const fields = columns.slice(1, -2);
|
|
2154
|
+
const inDocs = new Set(docsColumns);
|
|
2155
|
+
const inDerived = new Set(derivableFields(commands));
|
|
2156
|
+
const selects = [`d.${quoteIdent("_path")} AS ${quoteIdent("_path")}`];
|
|
2157
|
+
const origin = [];
|
|
2158
|
+
for (const field of fields) {
|
|
2159
|
+
const path = sqlString(jsonPath(field));
|
|
2160
|
+
const asserted = `json_type(d.${quoteIdent("_data")}, ${path}) IS NOT NULL`;
|
|
2161
|
+
const assertedValue = inDocs.has(field) ? `d.${quoteIdent(field)}` : `d.${quoteIdent("_data")} ->> ${path}`;
|
|
2162
|
+
if (inDerived.has(field)) {
|
|
2163
|
+
const derivedValue = `r.${quoteIdent(field)}`;
|
|
2164
|
+
selects.push(
|
|
2165
|
+
`CASE WHEN ${asserted} THEN ${assertedValue} ELSE ${derivedValue} END AS ${quoteIdent(field)}`
|
|
2166
|
+
);
|
|
2167
|
+
origin.push(
|
|
2168
|
+
`${sqlString(field)}, CASE WHEN ${asserted} THEN 'asserted' WHEN ${derivedValue} IS NOT NULL THEN 'derived' END`
|
|
2169
|
+
);
|
|
2170
|
+
} else {
|
|
2171
|
+
selects.push(`${assertedValue} AS ${quoteIdent(field)}`);
|
|
2172
|
+
origin.push(`${sqlString(field)}, CASE WHEN ${asserted} THEN 'asserted' END`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
selects.push(
|
|
2176
|
+
`json_patch('{}', json_object(${origin.join(", ")})) AS ${quoteIdent(ORIGIN_COLUMN)}`
|
|
2177
|
+
);
|
|
2178
|
+
selects.push(`r.${quoteIdent(SOURCES_COLUMN)} AS ${quoteIdent(SOURCES_COLUMN)}`);
|
|
2179
|
+
db.exec(
|
|
2180
|
+
`CREATE VIEW ${RESOLVED_VIEW} AS SELECT ${selects.join(", ")} FROM docs d LEFT JOIN ${DERIVED_ROWS} r USING (${quoteIdent("_path")})`
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
async function deriveForTable(inputs, ctx, hint, fields) {
|
|
2184
|
+
const derive = ctx.config?.derive;
|
|
2185
|
+
const commands = commandsOf(derive);
|
|
2186
|
+
const managesProvenance = derive?.fields?.includes(PROVENANCE_FIELD) === true && !fields.includes(PROVENANCE_FIELD);
|
|
2187
|
+
const result = await deriveMetadata(inputs, {
|
|
2188
|
+
cwd: ctx.cwd,
|
|
2189
|
+
base: ctx.base,
|
|
2190
|
+
...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
|
|
2191
|
+
sources: derive?.sources ?? [...DERIVE_SOURCES],
|
|
2192
|
+
fields: managesProvenance ? [...fields, PROVENANCE_FIELD] : fields,
|
|
2193
|
+
...derive?.codeowners !== void 0 ? { codeowners: derive.codeowners } : {},
|
|
2194
|
+
...commands !== void 0 ? { commands } : {},
|
|
2195
|
+
cache: ctx.cache,
|
|
2196
|
+
now: ctx.now ?? (() => /* @__PURE__ */ new Date()),
|
|
2197
|
+
machines: machinesOf(derive)
|
|
2198
|
+
});
|
|
2199
|
+
assertSourcesAvailable(result.sources, hint);
|
|
2200
|
+
return result.records;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// src/meta/core/checks.ts
|
|
2204
|
+
var CHECK_KEYWORD = "check";
|
|
2205
|
+
function checkSchemaRef(name) {
|
|
2206
|
+
if (!CHECK_NAME.test(name) || name.toLowerCase().endsWith(".json")) {
|
|
2207
|
+
throw new DocmetaError(
|
|
2208
|
+
`check name "${name}" must match [a-z0-9][a-z0-9._-]* and not end in ".json" \u2014 the name is part of each finding's identity (check:<name>).`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
return `check:${name}`;
|
|
2212
|
+
}
|
|
2213
|
+
var CONVENTION_COLUMNS = /* @__PURE__ */ new Set(["path", "line", "key", "message"]);
|
|
2214
|
+
function rowsToFindings(name, columns, rows, loaded) {
|
|
2215
|
+
if (!columns.includes("path")) {
|
|
2216
|
+
throw new DocmetaError(
|
|
2217
|
+
`check "${name}": the result has no \`path\` column. A check's SELECT names its findings through the column convention: \`path\` (required), and optionally \`line\`, \`key\`, and \`message\`; any other column is folded into the message.`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
const findings = /* @__PURE__ */ new Map();
|
|
2221
|
+
const extras = columns.filter((c) => !CONVENTION_COLUMNS.has(c));
|
|
2222
|
+
for (const row of rows) {
|
|
2223
|
+
const path = row.path;
|
|
2224
|
+
if (typeof path !== "string" || loaded !== void 0 && !loaded.has(path)) {
|
|
2225
|
+
throw new DocmetaError(
|
|
2226
|
+
`check "${name}": a result row names ${JSON.stringify(path)}, which this run did not load \u2014 a finding must attach to a file of the corpus. Fix the check's \`path\` column.`
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
const key = typeof row.key === "string" && row.key !== "" ? row.key : void 0;
|
|
2230
|
+
const line = asLine(row.line);
|
|
2231
|
+
const message = typeof row.message === "string" && row.message !== "" ? row.message : synthesizeMessage(extras, row);
|
|
2232
|
+
const err = {
|
|
2233
|
+
schema: checkSchemaRef(name),
|
|
2234
|
+
keyword: CHECK_KEYWORD,
|
|
2235
|
+
// RFC 6901 escaping, or a key containing `/` or `~` would build a
|
|
2236
|
+
// pointer that parses as a different (nested) location.
|
|
2237
|
+
instancePath: key === void 0 ? "" : `/${escapePointerSegment(key)}`,
|
|
2238
|
+
message,
|
|
2239
|
+
...line === void 0 ? {} : { line }
|
|
2240
|
+
};
|
|
2241
|
+
const bucket = findings.get(path);
|
|
2242
|
+
if (bucket) bucket.push(err);
|
|
2243
|
+
else findings.set(path, [err]);
|
|
2244
|
+
}
|
|
2245
|
+
return findings;
|
|
2246
|
+
}
|
|
2247
|
+
function asLine(value) {
|
|
2248
|
+
const n = typeof value === "bigint" ? Number(value) : typeof value === "number" ? value : void 0;
|
|
2249
|
+
return n !== void 0 && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2250
|
+
}
|
|
2251
|
+
function synthesizeMessage(extras, row) {
|
|
2252
|
+
const pairs = extras.flatMap((c) => {
|
|
2253
|
+
const value = row[c];
|
|
2254
|
+
if (value === null || value === void 0) return [];
|
|
2255
|
+
const text2 = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : JSON.stringify(value);
|
|
2256
|
+
return [`${c}=${text2}`];
|
|
2257
|
+
});
|
|
2258
|
+
return pairs.length > 0 ? pairs.join(", ") : "check matched";
|
|
2259
|
+
}
|
|
2260
|
+
async function runChecks(checks, entries, ctx = {}) {
|
|
2261
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2262
|
+
if (checks.length === 0) return merged;
|
|
2263
|
+
const { DatabaseSync } = await loadSqlite();
|
|
2264
|
+
const db = new DatabaseSync(":memory:");
|
|
2265
|
+
try {
|
|
2266
|
+
const dataColumns = corpusDataColumns(entries);
|
|
2267
|
+
createDocsTable(db, entries, dataColumns);
|
|
2268
|
+
registerLineFor(db, entries);
|
|
2269
|
+
createCollectionViews(db, collectCollections(entries, ctx));
|
|
2270
|
+
const wantsDerived = checks.filter(
|
|
2271
|
+
(c) => mentionsDerived(c.query) || mentionsResolved(c.query)
|
|
2272
|
+
);
|
|
2273
|
+
if (wantsDerived.length > 0) {
|
|
2274
|
+
if (ctx.derive === void 0) {
|
|
2275
|
+
throw new DocmetaError(
|
|
2276
|
+
`check "${wantsDerived[0]?.name ?? ""}": the SQL names the derived or resolved table, but this run cannot derive. Run it through \`manni meta validate\`, or drop the reference.`
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
const commands = ctx.commands;
|
|
2280
|
+
const readable = derivableFields(commands);
|
|
2281
|
+
const fields = /* @__PURE__ */ new Set();
|
|
2282
|
+
for (const c of wantsDerived) for (const f of fieldsForSql(c.query, readable)) fields.add(f);
|
|
2283
|
+
createDerivedView(db, await ctx.derive([...fields]), derivedColumns(commands));
|
|
2284
|
+
createResolvedView(db, dataColumns, commands);
|
|
2285
|
+
}
|
|
2286
|
+
db.exec("PRAGMA query_only = 1");
|
|
2287
|
+
const loaded = new Set(entries.map((e) => e.label));
|
|
2288
|
+
for (const check of checks) {
|
|
2289
|
+
const head = stripLeadingTrivia(check.query);
|
|
2290
|
+
if (/^(attach|vacuum)\b/i.test(head)) {
|
|
2291
|
+
throw new DocmetaError(
|
|
2292
|
+
`check "${check.name}": ${head.split(/\s/, 1)[0]?.toUpperCase() ?? "that statement"} is refused: it can write outside the docs table.`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
let tokens2;
|
|
2296
|
+
try {
|
|
2297
|
+
tokens2 = collectNamedParameters(check.query);
|
|
2298
|
+
} catch (err) {
|
|
2299
|
+
throw new DocmetaError(
|
|
2300
|
+
`check "${check.name}": ${errorMessage(err)}`
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
if (tokens2.length > 0) {
|
|
2304
|
+
throw new DocmetaError(
|
|
2305
|
+
`check "${check.name}": the SQL references ${tokens2.join(", ")}, but checks bind no parameters \u2014 an unbound name binds NULL and matches nothing, so the gate would pass on a typo. Inline the value in the SQL.`
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
let columns;
|
|
2309
|
+
let rows;
|
|
2310
|
+
try {
|
|
2311
|
+
assertSingleStatement(check.query);
|
|
2312
|
+
const stmt = db.prepare(check.query);
|
|
2313
|
+
columns = stmt.columns().map((c) => c.name);
|
|
2314
|
+
rows = stmt.all();
|
|
2315
|
+
} catch (err) {
|
|
2316
|
+
const message = errorMessage(err);
|
|
2317
|
+
throw new DocmetaError(
|
|
2318
|
+
message.includes("attempt to write a readonly database") ? `check "${check.name}": ${message} \u2014 checks are read-only by design; a check is a SELECT over the projection. Edit the corpus with \`manni meta query\` instead.` : `check "${check.name}": ${message}`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
for (const [path, errs] of rowsToFindings(
|
|
2322
|
+
check.name,
|
|
2323
|
+
columns,
|
|
2324
|
+
rows,
|
|
2325
|
+
loaded
|
|
2326
|
+
)) {
|
|
2327
|
+
const bucket = merged.get(path);
|
|
2328
|
+
if (bucket) bucket.push(...errs);
|
|
2329
|
+
else merged.set(path, errs);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
} finally {
|
|
2333
|
+
db.close();
|
|
2334
|
+
}
|
|
2335
|
+
return merged;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// src/meta/core/derive/provenance-place.ts
|
|
2339
|
+
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
|
|
2340
|
+
var toPosix2 = (p) => p.split(sep3).join("/");
|
|
2341
|
+
function provenanceManifests(collections, configDir, base, field = PROVENANCE_FIELD) {
|
|
2342
|
+
const out = [];
|
|
2343
|
+
for (const collection of collections) {
|
|
2344
|
+
for (const manifest of collection.externalMetadata) {
|
|
2345
|
+
const implied = manifest.keys === void 0;
|
|
2346
|
+
if (!implied && manifest.keys?.includes(field) !== true) continue;
|
|
2347
|
+
const url = classifyRef(manifest.file).kind === "url";
|
|
2348
|
+
if (url && !implied) throw new DocmetaError(urlRefusal(collection.name, field));
|
|
2349
|
+
if (url) {
|
|
2350
|
+
out.push({
|
|
2351
|
+
collection: collection.name,
|
|
2352
|
+
absPath: manifest.file,
|
|
2353
|
+
file: manifest.file,
|
|
2354
|
+
join: externalMetadataJoin(manifest),
|
|
2355
|
+
perPage: false,
|
|
2356
|
+
written: manifest.file,
|
|
2357
|
+
implied: { collection, manifest, url }
|
|
2358
|
+
});
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
const absPath = isAbsolute2(manifest.file) ? manifest.file : resolve3(configDir, manifest.file);
|
|
2362
|
+
const perPage = hasPagePlaceholder(manifest.file);
|
|
2363
|
+
out.push({
|
|
2364
|
+
collection: collection.name,
|
|
2365
|
+
absPath,
|
|
2366
|
+
file: perPage ? manifest.file : reportedPath(absPath, base),
|
|
2367
|
+
join: externalMetadataJoin(manifest),
|
|
2368
|
+
perPage,
|
|
2369
|
+
written: manifest.file,
|
|
2370
|
+
...implied ? { implied: { collection, manifest, url } } : {}
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return out;
|
|
2375
|
+
}
|
|
2376
|
+
function urlRefusal(collection, field) {
|
|
2377
|
+
return `collection ${collection}: ${field} cannot come from a URL manifest, because manni meta derive writes it.`;
|
|
2378
|
+
}
|
|
2379
|
+
function provenancePlace(label, data, manifests, collections, configDir, base, marked, field = PROVENANCE_FIELD) {
|
|
2380
|
+
if (manifests.length === 0) return void 0;
|
|
2381
|
+
const members = new Set(memberOf(collections, configDir, base, label));
|
|
2382
|
+
const owning = manifests.filter(
|
|
2383
|
+
(m) => members.has(m.collection) && (m.implied === void 0 || ownsKey(m.implied.collection, m.implied.manifest, field, marked))
|
|
2384
|
+
);
|
|
2385
|
+
const [declared, second] = owning;
|
|
2386
|
+
if (declared === void 0) return void 0;
|
|
2387
|
+
if (second !== void 0) {
|
|
2388
|
+
throw new DocmetaError(
|
|
2389
|
+
`${label} is in collections ${declared.collection} and ${second.collection}, and both keep ${field} in a manifest.`
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
if (declared.implied?.url === true) throw new DocmetaError(urlRefusal(declared.collection, field));
|
|
2393
|
+
const manifest = declared.perPage ? ownManifest(declared, label, collections, configDir, base) : declared;
|
|
2394
|
+
if (manifest.join === PATH_JOIN) {
|
|
2395
|
+
const entry = toPosix2(relative3(configDir, resolve3(base, label)));
|
|
2396
|
+
return { manifest, absPath: manifest.absPath, entry, join: manifest.join };
|
|
2397
|
+
}
|
|
2398
|
+
const value = data[manifest.join];
|
|
2399
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
2400
|
+
throw new DocmetaError(
|
|
2401
|
+
`${label} carries no ${manifest.join}, which ${manifest.file} joins on, so its ${field} has no entry there.`
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
return { manifest, absPath: manifest.absPath, entry: String(value), join: manifest.join };
|
|
2405
|
+
}
|
|
2406
|
+
function ownManifest(manifest, label, collections, configDir, base) {
|
|
2407
|
+
const resolved = pageManifestPath(manifest.written, configDir, resolve3(base, label));
|
|
2408
|
+
if ("outside" in resolved) {
|
|
2409
|
+
const declared = collections.find((c) => c.name === manifest.collection)?.externalMetadata.find((m) => m.file === manifest.written);
|
|
2410
|
+
throw new DocmetaError(
|
|
2411
|
+
outsideRefusal(declared ?? { file: manifest.written }, manifest.collection, resolved.pageRel)
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
return { ...manifest, absPath: resolved.abs, file: reportedPath(resolved.abs, base) };
|
|
2415
|
+
}
|
|
2416
|
+
function readerManifests(collections, configDir, base, field = PROVENANCE_FIELD) {
|
|
2417
|
+
const local = collections.map((c) => ({
|
|
2418
|
+
...c,
|
|
2419
|
+
externalMetadata: c.externalMetadata.filter((m) => classifyRef(m.file).kind !== "url")
|
|
2420
|
+
}));
|
|
2421
|
+
return provenanceManifests(local, configDir, base, field);
|
|
2422
|
+
}
|
|
2423
|
+
function readerPlace(label, data, manifests, collections, configDir, base, marked, field = PROVENANCE_FIELD) {
|
|
2424
|
+
try {
|
|
2425
|
+
const place = provenancePlace(label, data, manifests, collections, configDir, base, marked, field);
|
|
2426
|
+
return place === void 0 ? void 0 : { absPath: place.absPath, entry: place.entry, join: place.join };
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
if (err instanceof DocmetaError) return void 0;
|
|
2429
|
+
throw err;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
function stampManifests(label, data, collections, configDir, base, marked, fields) {
|
|
2433
|
+
const out = {};
|
|
2434
|
+
for (const field of STAMPED_DATE_FIELDS) {
|
|
2435
|
+
if (!fields.includes(field)) continue;
|
|
2436
|
+
const manifests = readerManifests(collections, configDir, base, field);
|
|
2437
|
+
const place = readerPlace(label, data, manifests, collections, configDir, base, marked, field);
|
|
2438
|
+
if (place !== void 0) out[field] = place;
|
|
2439
|
+
}
|
|
2440
|
+
return out;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/shared/cli-options.ts
|
|
2444
|
+
import { basename, relative as relative4 } from "node:path";
|
|
2445
|
+
function collect(value, prev) {
|
|
2446
|
+
return prev.concat([value]);
|
|
2447
|
+
}
|
|
2448
|
+
function splitList(value) {
|
|
2449
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2450
|
+
}
|
|
2451
|
+
var STDIN_REQUIRES_AS = "Reading from stdin (`-`) requires --as <format> to choose an extractor.";
|
|
2452
|
+
async function readStdin() {
|
|
2453
|
+
const chunks = [];
|
|
2454
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2455
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2456
|
+
}
|
|
2457
|
+
function configOption(value) {
|
|
2458
|
+
if (value === false) return { noConfig: true };
|
|
2459
|
+
return typeof value === "string" ? { configPath: value } : {};
|
|
2460
|
+
}
|
|
2461
|
+
function explicitFalse(value) {
|
|
2462
|
+
return value === false ? false : void 0;
|
|
2463
|
+
}
|
|
2464
|
+
function reportConfig(toStdout, cwd) {
|
|
2465
|
+
return (info) => {
|
|
2466
|
+
const where = (relative4(cwd, info.dir) || ".").replace(/\\/g, "/");
|
|
2467
|
+
const line = `Using ${basename(info.path)} (${where})
|
|
2468
|
+
`;
|
|
2469
|
+
(toStdout ? process.stdout : process.stderr).write(line);
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// src/meta/commands/validate.ts
|
|
2474
|
+
function parseErrorResult(file, format, message, keyword) {
|
|
2475
|
+
const err = {
|
|
2476
|
+
schema: "(parse)",
|
|
2477
|
+
instancePath: "",
|
|
2478
|
+
message,
|
|
2479
|
+
keyword
|
|
2480
|
+
};
|
|
2481
|
+
return { file, format, ok: false, schemas: [], errors: [err] };
|
|
2482
|
+
}
|
|
2483
|
+
function once(fn) {
|
|
2484
|
+
let pending;
|
|
2485
|
+
return () => {
|
|
2486
|
+
pending ??= fn();
|
|
2487
|
+
return pending;
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
async function runValidate(opts) {
|
|
2491
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2492
|
+
const run2 = await resolveRunConfig({
|
|
2493
|
+
cwd,
|
|
2494
|
+
configPath: opts.configPath,
|
|
2495
|
+
noConfig: opts.noConfig,
|
|
2496
|
+
inputs: opts.inputs,
|
|
2497
|
+
...opts.collections !== void 0 ? { collections: opts.collections } : {},
|
|
2498
|
+
onConfigLoaded: opts.onConfigLoaded
|
|
2499
|
+
});
|
|
2500
|
+
const {
|
|
2501
|
+
config,
|
|
2502
|
+
inputs,
|
|
2503
|
+
base,
|
|
2504
|
+
configDir,
|
|
2505
|
+
collections,
|
|
2506
|
+
declaredCollections,
|
|
2507
|
+
fromCollections,
|
|
2508
|
+
configFile
|
|
2509
|
+
} = run2;
|
|
2510
|
+
const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
|
|
2511
|
+
const usingStdin = inputs.includes(STDIN_TOKEN);
|
|
2512
|
+
if (inputs.length === 0) {
|
|
2513
|
+
throw new DocmetaError(
|
|
2514
|
+
"No files to validate. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
|
|
2518
|
+
if (opts.as && !forcedExtractor) {
|
|
2519
|
+
throw new DocmetaError(
|
|
2520
|
+
`Unknown format "${opts.as}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
const exts = opts.exts ?? (forcedExtractor ? forcedExtractor.extensions : void 0);
|
|
2524
|
+
const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
|
|
2525
|
+
const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
|
|
2526
|
+
const exclude = opts.exclude ?? [];
|
|
2527
|
+
const { files: walked, gitignoreSkipped } = await resolveTargetSet({
|
|
2528
|
+
inputs: fileInputs,
|
|
2529
|
+
exts,
|
|
2530
|
+
exclude,
|
|
2531
|
+
cwd: base,
|
|
2532
|
+
allowEmpty,
|
|
2533
|
+
...gitignoreOptions({
|
|
2534
|
+
flag: opts.respectGitignore,
|
|
2535
|
+
configured: config?.respectGitignore,
|
|
2536
|
+
onNotice: opts.onNotice
|
|
2537
|
+
})
|
|
2538
|
+
});
|
|
2539
|
+
const files = fromCollections ? retainMembers(walked, membersFor) : walked;
|
|
2540
|
+
assertNonEmpty({
|
|
2541
|
+
files,
|
|
2542
|
+
inputs: fileInputs,
|
|
2543
|
+
usingStdin,
|
|
2544
|
+
allowEmpty,
|
|
2545
|
+
exclude,
|
|
2546
|
+
exts,
|
|
2547
|
+
gitignoreSkipped,
|
|
2548
|
+
action: "validated"
|
|
2549
|
+
});
|
|
2550
|
+
const trustRoot = schemaTrustRoot(cwd, configDir);
|
|
2551
|
+
const externalMetadata = await loadExternalMetadata(collections, {
|
|
2552
|
+
configDir: configDir ?? cwd,
|
|
2553
|
+
base,
|
|
2554
|
+
offline: opts.offline ?? config?.offline ?? false,
|
|
2555
|
+
// A `{page}` manifest (0058) is read for exactly the pages this run reads.
|
|
2556
|
+
pages: files.map((file) => resolve4(base, file))
|
|
2557
|
+
});
|
|
2558
|
+
const validator = new Validator(
|
|
2559
|
+
schemaLoadOptions({
|
|
2560
|
+
// The config's directory when a config governs the run, so one project
|
|
2561
|
+
// keeps one cache no matter which directory the command was run from.
|
|
2562
|
+
root: configDir ?? cwd,
|
|
2563
|
+
// A relative file ref belongs to the run's directory, not the cache root.
|
|
2564
|
+
fileBase: cwd,
|
|
2565
|
+
ttlHours: config?.schemaCache?.ttlHours,
|
|
2566
|
+
offline: opts.offline ?? config?.offline,
|
|
2567
|
+
// Built from the rebased config, so a pinned local ref is keyed by the
|
|
2568
|
+
// same absolute spelling `resolveSchemaSet` will hand to `loadSchema`.
|
|
2569
|
+
pins: collectSchemaPins(config),
|
|
2570
|
+
registered: config?.registered
|
|
2571
|
+
})
|
|
2572
|
+
);
|
|
2573
|
+
const marks = pageMarks({
|
|
2574
|
+
validator,
|
|
2575
|
+
config,
|
|
2576
|
+
cwd,
|
|
2577
|
+
trustRoot,
|
|
2578
|
+
...opts.cliSchemas !== void 0 ? { cliSchemas: opts.cliSchemas } : {}
|
|
2579
|
+
});
|
|
2580
|
+
let relocation;
|
|
2581
|
+
const relocationCtx = () => relocation ??= relocationContext(run2, {
|
|
2582
|
+
cwd,
|
|
2583
|
+
targets: fromCollections ? [] : opts.inputs,
|
|
2584
|
+
validator,
|
|
2585
|
+
...opts.noConfig !== void 0 ? { noConfig: opts.noConfig } : {},
|
|
2586
|
+
...opts.env !== void 0 ? { env: opts.env } : {}
|
|
2587
|
+
});
|
|
2588
|
+
const refFrame = { cwd, base: configDir ?? cwd, runBase: base };
|
|
2589
|
+
const locationFindings = async (label, own, data, refs, members, merged, lineFor) => {
|
|
2590
|
+
const preferences = await validator.locationPreferences(data, refs);
|
|
2591
|
+
if (preferences.size === 0) return [];
|
|
2592
|
+
const owned = new Set(merged.collisions.map((c) => c.key));
|
|
2593
|
+
const declaredMembers = declaredCollections === collections ? members : memberOf(declaredCollections, configDir ?? cwd, base, label);
|
|
2594
|
+
const joins = new Set(
|
|
2595
|
+
declaredCollections.filter((c) => declaredMembers.includes(c.name)).flatMap((c) => c.externalMetadata.map(externalMetadataJoin)).filter((join2) => join2 !== PATH_JOIN)
|
|
2596
|
+
);
|
|
2597
|
+
const findings = [];
|
|
2598
|
+
for (const key of Object.keys(data)) {
|
|
2599
|
+
const preference = preferences.get(key);
|
|
2600
|
+
if (preference === void 0) continue;
|
|
2601
|
+
const ref = canonicalSchemaRef(preference.schema, refFrame);
|
|
2602
|
+
const pointer = externalMetadataPointer(key);
|
|
2603
|
+
const common = {
|
|
2604
|
+
keyword: LOCATION_FINDING_KEYWORD,
|
|
2605
|
+
subject: key,
|
|
2606
|
+
instancePath: pointer,
|
|
2607
|
+
severity: "warning"
|
|
2608
|
+
};
|
|
2609
|
+
if (Object.hasOwn(own, key)) {
|
|
2610
|
+
if (preference.location !== "external" || owned.has(key) || joins.has(key)) continue;
|
|
2611
|
+
const line = lineFor(key);
|
|
2612
|
+
const none = await homeless(label, own, key);
|
|
2613
|
+
findings.push({
|
|
2614
|
+
schema: LOCATION_EXTERNAL_SCHEMA,
|
|
2615
|
+
...common,
|
|
2616
|
+
// relocate refuses --no-config (U2), so it is no answer there.
|
|
2617
|
+
// Nor is it one for stdin, which relocate refuses: no advice there.
|
|
2618
|
+
message: opts.noConfig ? `"${key}" is stored in the page; ${ref} prefers external metadata, and --no-config leaves it no manifest.` : label === STDIN_LABEL ? `"${key}" is stored in the page; ${ref} prefers external metadata.` : none > 0 ? `"${key}" is stored in the page; ${ref} prefers external metadata, and this document is in none of the ${String(none)} collections.` : `"${key}" is stored in the page; ${ref} prefers external metadata. Run manni meta relocate.`,
|
|
2619
|
+
...line != null ? { line } : {}
|
|
2620
|
+
});
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
if (preference.location !== "page") continue;
|
|
2624
|
+
const at = merged.locate(pointer);
|
|
2625
|
+
if (at === void 0) continue;
|
|
2626
|
+
findings.push({
|
|
2627
|
+
schema: LOCATION_PAGE_SCHEMA,
|
|
2628
|
+
...common,
|
|
2629
|
+
// A fetched manifest cannot give a value up, so relocate is no
|
|
2630
|
+
// answer; the value moves in the repository that serves it.
|
|
2631
|
+
message: /^https?:\/\//.test(at.file) ? `"${key}" is stored in manifest ${at.file}; ${ref} prefers the page, and a fetched manifest cannot be written. Move it in that repository.` : `"${key}" is stored in manifest ${at.file}; ${ref} prefers the page. Run manni meta relocate.`,
|
|
2632
|
+
file: at.file,
|
|
2633
|
+
...at.line != null ? { line: at.line } : {}
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
return findings;
|
|
2637
|
+
};
|
|
2638
|
+
const homeless = async (label, own, key) => {
|
|
2639
|
+
if (label === STDIN_LABEL) return 0;
|
|
2640
|
+
const home = await keyHome(relocationCtx(), label, own, key);
|
|
2641
|
+
return home.kind === "unowned" && home.home.kind === "none" && home.home.reason === "collections" && home.home.collections > 1 ? home.home.collections : 0;
|
|
2642
|
+
};
|
|
2643
|
+
const results = [];
|
|
2644
|
+
const skippedDefaultOnly = /* @__PURE__ */ new Set();
|
|
2645
|
+
const defaultOnly = (label, members) => {
|
|
2646
|
+
try {
|
|
2647
|
+
return resolveSchemaSetWithSource({
|
|
2648
|
+
filePath: label,
|
|
2649
|
+
fileSchema: void 0,
|
|
2650
|
+
cliSchemas: opts.cliSchemas,
|
|
2651
|
+
config,
|
|
2652
|
+
memberOf: members,
|
|
2653
|
+
fileBase: cwd,
|
|
2654
|
+
trustRoot
|
|
2655
|
+
}).source === "default";
|
|
2656
|
+
} catch {
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
const joinHits = /* @__PURE__ */ new Map();
|
|
2661
|
+
const scopedToCollections = (opts.collections?.length ?? 0) > 0;
|
|
2662
|
+
const scopedByFlags = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
|
|
2663
|
+
usingStdin || opts.as !== void 0 || opts.exts !== void 0 || opts.exclude !== void 0 && opts.exclude.length > 0 || opts.respectGitignore !== void 0 || (opts.cliSchemas?.length ?? 0) > 0;
|
|
2664
|
+
const scoped = scopedByFlags || scopedToCollections;
|
|
2665
|
+
const configuredChecks = config?.checks ?? [];
|
|
2666
|
+
if (!scopedByFlags) {
|
|
2667
|
+
const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
|
|
2668
|
+
const orphans = orphanEntries(externalMetadata, files, base, covered);
|
|
2669
|
+
if (orphans.length > 0) throw orphanError(orphans);
|
|
2670
|
+
}
|
|
2671
|
+
const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
|
|
2672
|
+
const checkEntries = [];
|
|
2673
|
+
const encryptionKey = lazyKey(configFile, opts.env);
|
|
2674
|
+
let unverified = 0;
|
|
2675
|
+
const deriveConfig = config?.derive;
|
|
2676
|
+
const configuredFields = managedFields(deriveConfig);
|
|
2677
|
+
const defaultedFields = deriveConfig !== void 0 && deriveConfig.fields === void 0;
|
|
2678
|
+
const deriveWillRun = deriveConfig !== void 0 && configuredFields.length > 0 && opts.derive !== false;
|
|
2679
|
+
const pageManaged = /* @__PURE__ */ new Map();
|
|
2680
|
+
const derivedChecks = checksWillRun ? configuredChecks.filter((c) => mentionsDerived(c.query) || mentionsResolved(c.query)) : [];
|
|
2681
|
+
const checksNeedDerived = derivedChecks.length > 0;
|
|
2682
|
+
const deriveInputs = [];
|
|
2683
|
+
const managedViews = /* @__PURE__ */ new Map();
|
|
2684
|
+
const keepDeriveInputs = deriveWillRun || checksNeedDerived;
|
|
2685
|
+
const deriveCommands = commandsOf(deriveConfig);
|
|
2686
|
+
const deriveFields = new Set(deriveWillRun && !defaultedFields ? configuredFields : []);
|
|
2687
|
+
const readable = derivableFields(deriveCommands);
|
|
2688
|
+
for (const c of derivedChecks) for (const f of fieldsForSql(c.query, readable)) deriveFields.add(f);
|
|
2689
|
+
const provenanceManifests2 = keepDeriveInputs && (deriveFields.has(PROVENANCE_FIELD) || deriveWillRun && configuredFields.includes(PROVENANCE_FIELD)) ? readerManifests(declaredCollections, configDir ?? cwd, base) : [];
|
|
2690
|
+
const derivedOnce = once(async () => {
|
|
2691
|
+
const derived = await deriveMetadata(deriveInputs, {
|
|
2692
|
+
cwd,
|
|
2693
|
+
base,
|
|
2694
|
+
...configDir !== void 0 ? { configDir } : {},
|
|
2695
|
+
sources: deriveConfig?.sources ?? [...DERIVE_SOURCES],
|
|
2696
|
+
fields: [...deriveFields],
|
|
2697
|
+
...deriveConfig?.codeowners !== void 0 ? { codeowners: deriveConfig.codeowners } : {},
|
|
2698
|
+
...deriveCommands !== void 0 ? { commands: deriveCommands } : {},
|
|
2699
|
+
cache: opts.cache ?? true,
|
|
2700
|
+
now: opts.now ?? (() => /* @__PURE__ */ new Date()),
|
|
2701
|
+
machines: machinesOf(deriveConfig)
|
|
2702
|
+
});
|
|
2703
|
+
assertSourcesAvailable(
|
|
2704
|
+
derived.sources,
|
|
2705
|
+
deriveWillRun ? "pass --no-derive to skip the comparison, or narrow derive.sources" : "narrow derive.sources in manni.config.yaml"
|
|
2706
|
+
);
|
|
2707
|
+
for (const [name, status] of Object.entries(derived.sources)) {
|
|
2708
|
+
if (status.available && status.reason !== void 0) {
|
|
2709
|
+
opts.onNotice?.(`${name}: ${status.reason}`);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return derived;
|
|
2713
|
+
});
|
|
2714
|
+
const processOne = async (label, content, extension) => {
|
|
2715
|
+
const extractor = forcedExtractor ?? extractorForExtension(extension);
|
|
2716
|
+
if (!extractor) {
|
|
2717
|
+
throw new DocmetaError(
|
|
2718
|
+
`Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
const members = membersFor(label);
|
|
2722
|
+
let extracted;
|
|
2723
|
+
try {
|
|
2724
|
+
extracted = extractor.extract(content, label, {
|
|
2725
|
+
elements: resolveElements(label, config, members)
|
|
2726
|
+
});
|
|
2727
|
+
} catch (err) {
|
|
2728
|
+
if (err instanceof DocmetaError) throw err;
|
|
2729
|
+
if (opts.skipDefaultOnly === true && defaultOnly(label, members)) {
|
|
2730
|
+
skippedDefaultOnly.add(label);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
results.push(
|
|
2734
|
+
parseErrorResult(label, extractor.name, errorMessage(err), "parse")
|
|
2735
|
+
);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
const merged = await mergeWithMarks(label, extracted, externalMetadata, members, base, {
|
|
2739
|
+
encryptionKey,
|
|
2740
|
+
marks
|
|
2741
|
+
});
|
|
2742
|
+
if (keepDeriveInputs && label !== STDIN_LABEL && (checksNeedDerived || deriveCovers(deriveConfig, members))) {
|
|
2743
|
+
const place = provenanceManifests2.length === 0 ? void 0 : readerPlace(label, extracted.data, provenanceManifests2, declaredCollections, configDir ?? cwd, base, merged.marked);
|
|
2744
|
+
const owned = declaredCollections.length === 0 ? {} : stampManifests(label, extracted.data, declaredCollections, configDir ?? cwd, base, merged.marked, STAMPED_DATE_FIELDS);
|
|
2745
|
+
deriveInputs.push({
|
|
2746
|
+
label,
|
|
2747
|
+
absPath: resolve4(base, label),
|
|
2748
|
+
content,
|
|
2749
|
+
extracted,
|
|
2750
|
+
...place !== void 0 ? { provenanceManifest: place } : {},
|
|
2751
|
+
...Object.keys(owned).length > 0 ? { stampManifests: owned } : {}
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
const own = extracted.data;
|
|
2755
|
+
extracted = merged.extracted;
|
|
2756
|
+
if (deriveWillRun && label !== STDIN_LABEL) {
|
|
2757
|
+
managedViews.set(label, {
|
|
2758
|
+
data: extracted.data,
|
|
2759
|
+
marked: [],
|
|
2760
|
+
hidden: [],
|
|
2761
|
+
refs: [],
|
|
2762
|
+
locate: merged.locate
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
for (const j of merged.joins) {
|
|
2766
|
+
const byValue = joinHits.get(j.field) ?? /* @__PURE__ */ new Map();
|
|
2767
|
+
const line = extracted.lineFor(j.field);
|
|
2768
|
+
const hits = byValue.get(j.value) ?? [];
|
|
2769
|
+
hits.push({
|
|
2770
|
+
label,
|
|
2771
|
+
file: j.file,
|
|
2772
|
+
shown: j.pageValue ?? j.value,
|
|
2773
|
+
...line != null ? { line } : {}
|
|
2774
|
+
});
|
|
2775
|
+
byValue.set(j.value, hits);
|
|
2776
|
+
joinHits.set(j.field, byValue);
|
|
2777
|
+
}
|
|
2778
|
+
if (checksWillRun) checkEntries.push({ label, extracted });
|
|
2779
|
+
let resolved;
|
|
2780
|
+
try {
|
|
2781
|
+
resolved = resolveSchemaSetWithSource({
|
|
2782
|
+
filePath: label,
|
|
2783
|
+
fileSchema: extracted.data[FILE_SCHEMA_KEY],
|
|
2784
|
+
cliSchemas: opts.cliSchemas,
|
|
2785
|
+
config,
|
|
2786
|
+
memberOf: members,
|
|
2787
|
+
// A document's own `$schema` is measured from the run's directory, the
|
|
2788
|
+
// same base `loadSchema` will read it from, and contained to the
|
|
2789
|
+
// repository the run is standing in.
|
|
2790
|
+
fileBase: cwd,
|
|
2791
|
+
trustRoot,
|
|
2792
|
+
onNotice: opts.onNotice
|
|
2793
|
+
});
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
results.push(
|
|
2796
|
+
parseErrorResult(label, extractor.name, errorMessage(err), "schema")
|
|
2797
|
+
);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
if (opts.skipDefaultOnly === true && resolved.source === "default") {
|
|
2801
|
+
skippedDefaultOnly.add(label);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const schemaSet = resolved.schemas;
|
|
2805
|
+
if (deriveWillRun && defaultedFields && label !== STDIN_LABEL && deriveCovers(deriveConfig, members)) {
|
|
2806
|
+
let claimed = /* @__PURE__ */ new Set();
|
|
2807
|
+
try {
|
|
2808
|
+
claimed = await validator.claimedProperties(schemaSet);
|
|
2809
|
+
} catch (err) {
|
|
2810
|
+
if (!(err instanceof DocmetaError)) throw err;
|
|
2811
|
+
}
|
|
2812
|
+
const own2 = configuredFields.filter((f) => claimed.has(f));
|
|
2813
|
+
pageManaged.set(label, own2);
|
|
2814
|
+
for (const f of own2) deriveFields.add(f);
|
|
2815
|
+
}
|
|
2816
|
+
let errors;
|
|
2817
|
+
try {
|
|
2818
|
+
const view = await encryptionView({
|
|
2819
|
+
data: extracted.data,
|
|
2820
|
+
refs: schemaSet,
|
|
2821
|
+
validator,
|
|
2822
|
+
key: encryptionKey,
|
|
2823
|
+
locate: merged.locate
|
|
2824
|
+
});
|
|
2825
|
+
unverified += view.unverified.length;
|
|
2826
|
+
if (deriveWillRun && label !== STDIN_LABEL) {
|
|
2827
|
+
managedViews.set(label, {
|
|
2828
|
+
data: view.data,
|
|
2829
|
+
marked: view.marked,
|
|
2830
|
+
hidden: [...view.unreadable, ...view.unverified],
|
|
2831
|
+
refs: schemaSet,
|
|
2832
|
+
locate: merged.locate
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
errors = [
|
|
2836
|
+
...settleFindings(
|
|
2837
|
+
await validator.validate(
|
|
2838
|
+
view.data,
|
|
2839
|
+
schemaSet,
|
|
2840
|
+
extracted.lineFor,
|
|
2841
|
+
extracted.colFor,
|
|
2842
|
+
merged.locate
|
|
2843
|
+
),
|
|
2844
|
+
view,
|
|
2845
|
+
extracted,
|
|
2846
|
+
merged.locate
|
|
2847
|
+
),
|
|
2848
|
+
...encryptionFindings(view, extracted, merged.locate),
|
|
2849
|
+
// Over the decrypted view, the object validation evaluated: a mark
|
|
2850
|
+
// counts where Ajv takes a branch, and a branch may read a value.
|
|
2851
|
+
...await locationFindings(label, own, view.data, schemaSet, members, merged, extracted.lineFor)
|
|
2852
|
+
];
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
if (!(err instanceof DocmetaError) || resolved.source !== "document") {
|
|
2855
|
+
throw err;
|
|
2856
|
+
}
|
|
2857
|
+
results.push(
|
|
2858
|
+
parseErrorResult(label, extractor.name, err.message, "schema")
|
|
2859
|
+
);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
for (const { key, file, collection } of merged.collisions) {
|
|
2863
|
+
const line = extracted.lineFor(key);
|
|
2864
|
+
errors.push({
|
|
2865
|
+
schema: EXTERNAL_OWNED_SCHEMA,
|
|
2866
|
+
keyword: EXTERNAL_KEYWORD,
|
|
2867
|
+
subject: key,
|
|
2868
|
+
instancePath: externalMetadataPointer(key),
|
|
2869
|
+
message: `"${key}" is owned by manifest ${file} (collection ${collection}); remove it from the document`,
|
|
2870
|
+
...line != null ? { line } : {}
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
results.push({
|
|
2874
|
+
file: label,
|
|
2875
|
+
format: extractor.name,
|
|
2876
|
+
ok: !errors.some(isErrorSeverity),
|
|
2877
|
+
schemas: schemaSet,
|
|
2878
|
+
errors
|
|
2879
|
+
});
|
|
2880
|
+
};
|
|
2881
|
+
if (usingStdin) {
|
|
2882
|
+
const content = opts.stdinContent ?? "";
|
|
2883
|
+
if (!forcedExtractor) {
|
|
2884
|
+
throw new DocmetaError(
|
|
2885
|
+
STDIN_REQUIRES_AS
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
|
|
2889
|
+
}
|
|
2890
|
+
for (const file of files) {
|
|
2891
|
+
const content = await readFile2(resolve4(base, file), "utf8");
|
|
2892
|
+
await processOne(file, content, extname2(file));
|
|
2893
|
+
}
|
|
2894
|
+
if (unverified > 0) opts.onNotice?.(unverifiedWarning(unverified));
|
|
2895
|
+
if (externalMetadata) {
|
|
2896
|
+
const byLabel = new Map(results.map((r) => [r.file, r]));
|
|
2897
|
+
const matched = /* @__PURE__ */ new Map();
|
|
2898
|
+
for (const [field, byValue] of joinHits) {
|
|
2899
|
+
matched.set(field, new Set(byValue.keys()));
|
|
2900
|
+
for (const hits of byValue.values()) {
|
|
2901
|
+
if (hits.length < 2) continue;
|
|
2902
|
+
for (const hit of hits) {
|
|
2903
|
+
const others = hits.filter((h) => h.label !== hit.label).map((h) => h.label).join(", ");
|
|
2904
|
+
const result = byLabel.get(hit.label);
|
|
2905
|
+
if (!result) continue;
|
|
2906
|
+
result.errors.push({
|
|
2907
|
+
schema: EXTERNAL_DUPLICATE_SCHEMA,
|
|
2908
|
+
keyword: EXTERNAL_KEYWORD,
|
|
2909
|
+
subject: hit.shown,
|
|
2910
|
+
instancePath: externalMetadataPointer(field),
|
|
2911
|
+
message: `${hits.length} documents carry ${field} "${hit.shown}"; ${hit.file} cannot tell them apart (${others})`,
|
|
2912
|
+
...hit.line != null ? { line: hit.line } : {}
|
|
2913
|
+
});
|
|
2914
|
+
result.ok = false;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
if (!scopedByFlags) {
|
|
2919
|
+
const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
|
|
2920
|
+
const orphans = orphanJoins(externalMetadata, matched, covered);
|
|
2921
|
+
if (orphans.length > 0) throw orphanError(orphans);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
if (configuredChecks.length > 0 && opts.checks !== false) {
|
|
2925
|
+
if (scoped) {
|
|
2926
|
+
opts.onNotice?.(
|
|
2927
|
+
scopedToCollections ? `corpus checks skipped: run is scoped to collections ${collections.map((c) => c.name).join(", ")}` : "corpus checks skipped: run is scoped"
|
|
2928
|
+
);
|
|
2929
|
+
} else {
|
|
2930
|
+
const findings = await runChecks(configuredChecks, checkEntries, {
|
|
2931
|
+
// Every declared collection is a view (0041 rule 7), holding the
|
|
2932
|
+
// members this run loaded — decided by the same path arithmetic
|
|
2933
|
+
// `membersFor` uses, so a check and a `manni meta query` over the same
|
|
2934
|
+
// corpus see the same collections.
|
|
2935
|
+
collections: declaredCollections,
|
|
2936
|
+
...configDir !== void 0 ? { configDir } : {},
|
|
2937
|
+
base,
|
|
2938
|
+
// A check that names the `derived` table (0040) gets the same view
|
|
2939
|
+
// `query` builds, from the run's one derivation — which already
|
|
2940
|
+
// covers every field the checks can read, so the list handed back
|
|
2941
|
+
// here is not needed. Built only when asked for, because building
|
|
2942
|
+
// it spawns git.
|
|
2943
|
+
derive: async () => (await derivedOnce()).records,
|
|
2944
|
+
// Its columns include the configured commands' keys (0042), which a
|
|
2945
|
+
// check's context cannot read from a config since 0041.
|
|
2946
|
+
...deriveCommands !== void 0 ? { commands: deriveCommands } : {}
|
|
2947
|
+
});
|
|
2948
|
+
const byFile = new Map(results.map((r) => [r.file, r]));
|
|
2949
|
+
for (const [file, errs] of findings) {
|
|
2950
|
+
const result = byFile.get(file);
|
|
2951
|
+
if (!result && skippedDefaultOnly.has(file)) continue;
|
|
2952
|
+
if (!result) {
|
|
2953
|
+
throw new DocmetaError(
|
|
2954
|
+
`check findings for "${file}" have no validation result to attach to.`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
result.errors.push(...errs);
|
|
2958
|
+
result.ok = result.ok && !errs.some(isErrorSeverity);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
if (deriveWillRun && deriveInputs.length > 0) {
|
|
2963
|
+
const derived = await derivedOnce();
|
|
2964
|
+
const byFile = new Map(results.map((r) => [r.file, r]));
|
|
2965
|
+
for (const input of deriveInputs) {
|
|
2966
|
+
const record = derived.records.get(input.label);
|
|
2967
|
+
const result = byFile.get(input.label);
|
|
2968
|
+
if (!record || !result) continue;
|
|
2969
|
+
const managed = managedViews.get(input.label);
|
|
2970
|
+
const managedHere = !deriveCovers(deriveConfig, membersFor(input.label)) ? [] : defaultedFields ? pageManaged.get(input.label) ?? [] : configuredFields;
|
|
2971
|
+
const fields = managedHere.filter(
|
|
2972
|
+
(field) => !(managed?.hidden.some((p) => isAtOrUnder(p, pointerOf(field))) ?? false)
|
|
2973
|
+
);
|
|
2974
|
+
let marked = [];
|
|
2975
|
+
if (managed !== void 0) {
|
|
2976
|
+
const stamped = { ...managed.data };
|
|
2977
|
+
for (const field of fields) {
|
|
2978
|
+
const d = record.fields[field];
|
|
2979
|
+
if (d != null) stamped[field] = d.value;
|
|
2980
|
+
}
|
|
2981
|
+
marked = [...managed.marked, ...await validator.markedPointers(stamped, managed.refs)];
|
|
2982
|
+
}
|
|
2983
|
+
const asserted = (field) => managed !== void 0 ? managed.data[field] : input.extracted.data[field];
|
|
2984
|
+
const findings = fields.flatMap((field) => {
|
|
2985
|
+
if (field === PROVENANCE_FIELD) {
|
|
2986
|
+
const derivation = derived.provenance?.get(input.label);
|
|
2987
|
+
if (derivation === void 0) return [];
|
|
2988
|
+
return provenanceFindings(
|
|
2989
|
+
compareProvenance(provenanceEntries(asserted(field)), derivation),
|
|
2990
|
+
derivation.page.bodyLine
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
const compared = compareDerived(field, asserted(field), record.fields[field]);
|
|
2994
|
+
const at = pointerOf(field);
|
|
2995
|
+
return staleFindings(
|
|
2996
|
+
[marked.some((p) => isAtOrUnder(p, at)) ? redactedDerived(compared) : compared],
|
|
2997
|
+
input.extracted.lineFor,
|
|
2998
|
+
managed?.locate
|
|
2999
|
+
);
|
|
3000
|
+
});
|
|
3001
|
+
if (findings.length === 0) continue;
|
|
3002
|
+
result.errors.push(...findings);
|
|
3003
|
+
result.ok = false;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const frame = { cwd, base: configDir ?? cwd, runBase: base };
|
|
3007
|
+
const { results: reported, baseline } = await settleBaseline(
|
|
3008
|
+
results,
|
|
3009
|
+
resolveBaselineRequest(opts, config?.baseline, configDir, cwd, {
|
|
3010
|
+
current: DEFAULT_BASELINE_PATH,
|
|
3011
|
+
legacy: LEGACY_BASELINE_PATH
|
|
3012
|
+
}),
|
|
3013
|
+
frame
|
|
3014
|
+
);
|
|
3015
|
+
const failed = reported.filter((r) => !r.ok).length;
|
|
3016
|
+
const count = (keep) => reported.reduce((n, r) => n + r.errors.filter(keep).length, 0);
|
|
3017
|
+
const warnings = count((e) => e.severity === "warning");
|
|
3018
|
+
const notices = count((e) => e.severity === "notice");
|
|
3019
|
+
const summary = {
|
|
3020
|
+
files: reported.length,
|
|
3021
|
+
passed: reported.length - failed,
|
|
3022
|
+
failed,
|
|
3023
|
+
errors: count(isErrorSeverity),
|
|
3024
|
+
// Omitted at zero, like `gitignoreSkipped` below: only the location
|
|
3025
|
+
// findings (proposal 0047) are warnings, so a run with no
|
|
3026
|
+
// `x-manni-location` marks keeps the summary as it was.
|
|
3027
|
+
...warnings > 0 ? { warnings } : {},
|
|
3028
|
+
...notices > 0 ? { notices } : {},
|
|
3029
|
+
// Omitted when nothing was skipped: there is nothing to audit, and the
|
|
3030
|
+
// JSON summary stays as it was for every run in a clean repo.
|
|
3031
|
+
...gitignoreSkipped > 0 ? { gitignoreSkipped } : {},
|
|
3032
|
+
...baseline ? { baseline } : {}
|
|
3033
|
+
};
|
|
3034
|
+
return { results: reported, summary, frame };
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
export {
|
|
3038
|
+
collect,
|
|
3039
|
+
splitList,
|
|
3040
|
+
STDIN_REQUIRES_AS,
|
|
3041
|
+
readStdin,
|
|
3042
|
+
configOption,
|
|
3043
|
+
explicitFalse,
|
|
3044
|
+
reportConfig,
|
|
3045
|
+
commandsOf,
|
|
3046
|
+
machinesOf,
|
|
3047
|
+
REVIEW_CACHE_DIR,
|
|
3048
|
+
REVIEW_CACHE_VERSION,
|
|
3049
|
+
ReviewCache,
|
|
3050
|
+
cachedClient,
|
|
3051
|
+
BinMissing,
|
|
3052
|
+
run,
|
|
3053
|
+
PATH_PLACEHOLDER,
|
|
3054
|
+
isPerFile,
|
|
3055
|
+
argvFor,
|
|
3056
|
+
valueOf,
|
|
3057
|
+
deriveFromCommands,
|
|
3058
|
+
parseOriginUrl,
|
|
3059
|
+
identityFromOrigin,
|
|
3060
|
+
GitHubClient,
|
|
3061
|
+
GitLabClient,
|
|
3062
|
+
createReviewClient,
|
|
3063
|
+
deriveFromReviews,
|
|
3064
|
+
mapConcurrent,
|
|
3065
|
+
provenanceFenced,
|
|
3066
|
+
FIELD_SOURCES,
|
|
3067
|
+
sourcesFor,
|
|
3068
|
+
consultedSources,
|
|
3069
|
+
assertSourcesAvailable,
|
|
3070
|
+
deriveMetadata,
|
|
3071
|
+
DERIVED_VIEW,
|
|
3072
|
+
DERIVED_ROWS,
|
|
3073
|
+
RESOLVED_VIEW,
|
|
3074
|
+
derivedColumns,
|
|
3075
|
+
mentionsDerived,
|
|
3076
|
+
fieldsForSql,
|
|
3077
|
+
createDerivedView,
|
|
3078
|
+
resolvedColumns,
|
|
3079
|
+
mentionsResolved,
|
|
3080
|
+
createResolvedView,
|
|
3081
|
+
deriveForTable,
|
|
3082
|
+
rowsToFindings,
|
|
3083
|
+
provenanceManifests,
|
|
3084
|
+
provenancePlace,
|
|
3085
|
+
readerManifests,
|
|
3086
|
+
readerPlace,
|
|
3087
|
+
stampManifests,
|
|
3088
|
+
runValidate
|
|
3089
|
+
};
|
|
3090
|
+
//# sourceMappingURL=chunk-TK3FC4RF.js.map
|