@driftdev/cli 1.11.0 → 1.12.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/dist/drift.js CHANGED
@@ -51,8 +51,8 @@ function getGlobalConfigPath() {
51
51
  var init_global = () => {};
52
52
 
53
53
  // src/drift.ts
54
- import { readFileSync as readFileSync21 } from "node:fs";
55
- import * as path31 from "node:path";
54
+ import { readFileSync as readFileSync23 } from "node:fs";
55
+ import * as path34 from "node:path";
56
56
  import { fileURLToPath as fileURLToPath2 } from "node:url";
57
57
  import { Command } from "commander";
58
58
 
@@ -63,13 +63,37 @@ import {
63
63
  existsSync as existsSync2,
64
64
  mkdirSync as mkdirSync2,
65
65
  readdirSync,
66
- readFileSync,
66
+ readFileSync as readFileSync2,
67
67
  rmSync,
68
68
  statSync,
69
69
  writeFileSync
70
70
  } from "node:fs";
71
71
  import * as os2 from "node:os";
72
72
  import * as path2 from "node:path";
73
+
74
+ // src/utils/version.ts
75
+ import { readFileSync } from "node:fs";
76
+ import { dirname, join as join2 } from "node:path";
77
+ import { fileURLToPath } from "node:url";
78
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
79
+ var cached;
80
+ function getVersion() {
81
+ if (cached)
82
+ return cached;
83
+ for (const rel of ["../package.json", "../../package.json"]) {
84
+ try {
85
+ const pkg = JSON.parse(readFileSync(join2(__dirname2, rel), "utf-8"));
86
+ if (pkg.name === "@driftdev/cli" && typeof pkg.version === "string") {
87
+ cached = pkg.version;
88
+ return pkg.version;
89
+ }
90
+ } catch {}
91
+ }
92
+ cached = "0.0.0";
93
+ return cached;
94
+ }
95
+
96
+ // src/cache/spec-cache.ts
73
97
  var _noCache = false;
74
98
  function setNoCache(value) {
75
99
  _noCache = value;
@@ -141,7 +165,7 @@ function buildCacheKey(input) {
141
165
  const pkgJson = findPackageJson(absEntry);
142
166
  const pkgMtime = pkgJson ? getMtime(pkgJson) : 0;
143
167
  const srcMtime = getSourceMaxMtime(absEntry);
144
- const parts = [absEntry, String(entryMtime), String(pkgMtime), String(srcMtime)];
168
+ const parts = [getVersion(), absEntry, String(entryMtime), String(pkgMtime), String(srcMtime)];
145
169
  if (input.configHash)
146
170
  parts.push(input.configHash);
147
171
  return hashString(parts.join("|"));
@@ -154,7 +178,7 @@ function getCachedSpec(input) {
154
178
  if (!existsSync2(cacheFile))
155
179
  return null;
156
180
  try {
157
- const raw = JSON.parse(readFileSync(cacheFile, "utf-8"));
181
+ const raw = JSON.parse(readFileSync2(cacheFile, "utf-8"));
158
182
  return raw;
159
183
  } catch {
160
184
  return null;
@@ -177,7 +201,7 @@ function getConfigHash(configPath) {
177
201
  if (!configPath || !existsSync2(configPath))
178
202
  return;
179
203
  try {
180
- const content = readFileSync(configPath, "utf-8");
204
+ const content = readFileSync2(configPath, "utf-8");
181
205
  return hashString(content);
182
206
  } catch {
183
207
  return;
@@ -633,13 +657,13 @@ function formatWarning(message) {
633
657
  }
634
658
 
635
659
  // src/utils/resolve-specs.ts
636
- import { readFileSync as readFileSync5 } from "node:fs";
660
+ import { readFileSync as readFileSync6 } from "node:fs";
637
661
  import * as path7 from "node:path";
638
662
  import { extract as extract2 } from "@openpkg-ts/sdk";
639
663
  import { normalize as normalize2 } from "@openpkg-ts/spec";
640
664
 
641
665
  // src/config/loader.ts
642
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
666
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
643
667
  import * as path4 from "node:path";
644
668
 
645
669
  // src/config/drift-config.ts
@@ -731,7 +755,7 @@ function loadConfig(cwd = process.cwd()) {
731
755
  if (!existsSync4(absPath)) {
732
756
  throw new Error(`Config file not found: ${absPath}`);
733
757
  }
734
- const raw = JSON.parse(readFileSync2(absPath, "utf-8"));
758
+ const raw = JSON.parse(readFileSync3(absPath, "utf-8"));
735
759
  const result = validateConfig(raw);
736
760
  if (!result.ok) {
737
761
  throw new Error(`Invalid config at ${absPath}: ${result.errors.join(", ")}`);
@@ -744,7 +768,7 @@ function loadConfig(cwd = process.cwd()) {
744
768
  const driftConfigPath = path4.join(current, "drift.config.json");
745
769
  if (existsSync4(driftConfigPath)) {
746
770
  try {
747
- const raw = JSON.parse(readFileSync2(driftConfigPath, "utf-8"));
771
+ const raw = JSON.parse(readFileSync3(driftConfigPath, "utf-8"));
748
772
  const result = validateConfig(raw);
749
773
  if (!result.ok) {
750
774
  throw new Error(`Invalid config at ${driftConfigPath}: ${result.errors.join(", ")}`);
@@ -760,7 +784,7 @@ function loadConfig(cwd = process.cwd()) {
760
784
  const pkgPath = path4.join(current, "package.json");
761
785
  if (existsSync4(pkgPath)) {
762
786
  try {
763
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
787
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
764
788
  if (pkg.drift && typeof pkg.drift === "object") {
765
789
  const result = validateConfig(pkg.drift);
766
790
  if (!result.ok) {
@@ -781,7 +805,7 @@ function loadConfig(cwd = process.cwd()) {
781
805
  const globalPath = getGlobalConfigPath();
782
806
  if (existsSync4(globalPath)) {
783
807
  try {
784
- const raw = JSON.parse(readFileSync2(globalPath, "utf-8"));
808
+ const raw = JSON.parse(readFileSync3(globalPath, "utf-8"));
785
809
  const result = validateConfig(raw);
786
810
  if (result.ok) {
787
811
  return { config: result.config, configPath: globalPath };
@@ -792,18 +816,18 @@ function loadConfig(cwd = process.cwd()) {
792
816
  }
793
817
 
794
818
  // src/utils/detect-entry.ts
795
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
819
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
796
820
  import * as path6 from "node:path";
797
821
 
798
822
  // src/utils/workspaces.ts
799
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
823
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
800
824
  import * as path5 from "node:path";
801
825
  function detectWorkspaces(cwd) {
802
826
  const pkgPath = path5.join(cwd, "package.json");
803
827
  if (!existsSync5(pkgPath))
804
828
  return null;
805
829
  try {
806
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
830
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
807
831
  if (Array.isArray(pkg.workspaces))
808
832
  return pkg.workspaces;
809
833
  if (pkg.workspaces?.packages && Array.isArray(pkg.workspaces.packages))
@@ -847,7 +871,7 @@ function discoverPackages(cwd) {
847
871
  const pkgPath = path5.join(absDir, "package.json");
848
872
  if (existsSync5(pkgPath)) {
849
873
  try {
850
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
874
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
851
875
  if (pkg.name)
852
876
  name = pkg.name;
853
877
  if (pkg.private === true)
@@ -878,7 +902,7 @@ function detectEntry(cwd = process.cwd()) {
878
902
  }
879
903
  if (existsSync6(pkgPath)) {
880
904
  try {
881
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
905
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
882
906
  const typesField = pkg.types || pkg.typings;
883
907
  if (typesField && typeof typesField === "string") {
884
908
  const resolved = tryResolve("types", typesField);
@@ -1039,7 +1063,7 @@ function collectStringValues(value, depth = 0) {
1039
1063
 
1040
1064
  // src/utils/resolve-specs.ts
1041
1065
  function loadSpec(filePath) {
1042
- return JSON.parse(readFileSync5(path7.resolve(process.cwd(), filePath), "utf-8"));
1066
+ return JSON.parse(readFileSync6(path7.resolve(process.cwd(), filePath), "utf-8"));
1043
1067
  }
1044
1068
  async function resolveSpecs(opts) {
1045
1069
  const { config } = loadConfig();
@@ -1086,23 +1110,6 @@ async function resolveSpecs(opts) {
1086
1110
  throw new Error("Provide two spec files, or use --base <ref>");
1087
1111
  }
1088
1112
 
1089
- // src/utils/version.ts
1090
- import { readFileSync as readFileSync6 } from "node:fs";
1091
- import { dirname as dirname4, join as join7 } from "node:path";
1092
- import { fileURLToPath } from "node:url";
1093
- var __dirname2 = dirname4(fileURLToPath(import.meta.url));
1094
- var cached;
1095
- function getVersion() {
1096
- if (cached)
1097
- return cached;
1098
- try {
1099
- cached = JSON.parse(readFileSync6(join7(__dirname2, "../../package.json"), "utf-8")).version ?? "0.0.0";
1100
- } catch {
1101
- cached = "0.0.0";
1102
- }
1103
- return cached ?? "0.0.0";
1104
- }
1105
-
1106
1113
  // src/commands/breaking.ts
1107
1114
  function registerBreakingCommand(program) {
1108
1115
  program.command("breaking [old] [new]").description("Detect breaking changes between two specs").option("--base <ref>", "Git ref for old spec").option("--head <ref>", "Git ref for new spec (default: working tree)").option("--entry <file>", "Entry file for git ref extraction").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (oldPath, newPath, options) => {
@@ -2651,9 +2658,358 @@ function registerDiffCommand(program) {
2651
2658
  });
2652
2659
  }
2653
2660
 
2654
- // src/commands/examples.ts
2655
- import { readFileSync as readFileSync14 } from "node:fs";
2661
+ // src/commands/docs-map.ts
2662
+ import { writeFileSync as writeFileSync5 } from "node:fs";
2663
+ import * as path21 from "node:path";
2664
+ import { collectTypeKeys, extractDocumentedKeys as extractDocumentedKeys2 } from "@driftdev/sdk";
2665
+
2666
+ // src/config/docs-map.ts
2667
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
2656
2668
  import * as path18 from "node:path";
2669
+ var ANNOTATIONS = new Set(["prose-documented", "internal-by-convention", "ignore"]);
2670
+ function isStringArray(v) {
2671
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
2672
+ }
2673
+ function validateDocsMap(raw) {
2674
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2675
+ return { ok: false, errors: ["Docs map must be a JSON object"] };
2676
+ }
2677
+ const errors = [];
2678
+ const obj = raw;
2679
+ if (obj.version !== 1)
2680
+ errors.push('"version" must be 1');
2681
+ if (!Array.isArray(obj.pages)) {
2682
+ errors.push('"pages" must be an array');
2683
+ return { ok: false, errors };
2684
+ }
2685
+ obj.pages.forEach((p, i) => {
2686
+ const at = `"pages[${i}]"`;
2687
+ if (typeof p !== "object" || p === null) {
2688
+ errors.push(`${at} must be an object`);
2689
+ return;
2690
+ }
2691
+ const page = p;
2692
+ if (typeof page.page !== "string" || !page.page)
2693
+ errors.push(`${at}.page must be a string`);
2694
+ if (typeof page.type !== "string" || !page.type)
2695
+ errors.push(`${at}.type must be a string`);
2696
+ if (page.spec !== undefined && typeof page.spec !== "string")
2697
+ errors.push(`${at}.spec must be a string`);
2698
+ if (page.entry !== undefined && typeof page.entry !== "string")
2699
+ errors.push(`${at}.entry must be a string`);
2700
+ if (page.spec !== undefined && page.entry !== undefined)
2701
+ errors.push(`${at} must set at most one of "spec"/"entry" (omit both to use the scan target)`);
2702
+ if (page.sectionRe !== undefined) {
2703
+ if (typeof page.sectionRe !== "string") {
2704
+ errors.push(`${at}.sectionRe must be a string`);
2705
+ } else {
2706
+ try {
2707
+ new RegExp(page.sectionRe);
2708
+ } catch {
2709
+ errors.push(`${at}.sectionRe is not a valid regex: ${page.sectionRe}`);
2710
+ }
2711
+ }
2712
+ }
2713
+ for (const field of ["extraPages", "internal", "deprecated"]) {
2714
+ if (page[field] !== undefined && !isStringArray(page[field]))
2715
+ errors.push(`${at}.${field} must be an array of strings`);
2716
+ }
2717
+ if (page.replacements !== undefined) {
2718
+ const r = page.replacements;
2719
+ if (typeof r !== "object" || r === null || Array.isArray(r)) {
2720
+ errors.push(`${at}.replacements must be an object of oldKey → newKey strings`);
2721
+ } else if (!Object.values(r).every((v) => typeof v === "string")) {
2722
+ errors.push(`${at}.replacements values must be strings`);
2723
+ }
2724
+ }
2725
+ if (page.annotations !== undefined) {
2726
+ const a = page.annotations;
2727
+ if (typeof a !== "object" || a === null || Array.isArray(a)) {
2728
+ errors.push(`${at}.annotations must be an object of key → annotation`);
2729
+ } else {
2730
+ for (const [k, v] of Object.entries(a)) {
2731
+ if (typeof v !== "string" || !ANNOTATIONS.has(v))
2732
+ errors.push(`${at}.annotations["${k}"] must be one of: ${[...ANNOTATIONS].join(", ")} (got ${JSON.stringify(v)})`);
2733
+ }
2734
+ }
2735
+ }
2736
+ if (page.baselineGaps !== undefined && (typeof page.baselineGaps !== "number" || page.baselineGaps < 0 || !Number.isInteger(page.baselineGaps)))
2737
+ errors.push(`${at}.baselineGaps must be a non-negative integer`);
2738
+ });
2739
+ if (errors.length > 0)
2740
+ return { ok: false, errors };
2741
+ return { ok: true, map: obj };
2742
+ }
2743
+ function loadDocsMap(mapPath, cwd = process.cwd()) {
2744
+ const absPath = path18.resolve(cwd, mapPath);
2745
+ if (!existsSync13(absPath))
2746
+ throw new Error(`Docs map not found: ${absPath}`);
2747
+ let raw;
2748
+ try {
2749
+ raw = JSON.parse(readFileSync14(absPath, "utf-8"));
2750
+ } catch {
2751
+ throw new Error(`Invalid JSON in ${absPath}`);
2752
+ }
2753
+ const result = validateDocsMap(raw);
2754
+ if (!result.ok) {
2755
+ throw new Error(`Invalid docs map at ${absPath}: ${result.errors.join("; ")}`);
2756
+ }
2757
+ return { map: result.map, mapPath: absPath, dir: path18.dirname(absPath) };
2758
+ }
2759
+
2760
+ // src/utils/docs-corpus.ts
2761
+ import { readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
2762
+ import * as path19 from "node:path";
2763
+ import { discoverMarkdownFiles } from "@driftdev/sdk";
2764
+ function resolveDocsCorpus(cwd, docsPatterns, configDocs) {
2765
+ if (!docsPatterns || docsPatterns.length === 0) {
2766
+ return discoverMarkdownFiles(cwd, configDocs);
2767
+ }
2768
+ const include = [];
2769
+ for (const pattern of docsPatterns) {
2770
+ if (isDirectory(path19.resolve(cwd, pattern))) {
2771
+ include.push(path19.join(pattern, "**/*.md"), path19.join(pattern, "**/*.mdx"));
2772
+ } else {
2773
+ include.push(pattern);
2774
+ }
2775
+ }
2776
+ const files = discoverMarkdownFiles(cwd, { include });
2777
+ if (files.length === 0) {
2778
+ formatWarning(`--docs matched no markdown files: ${docsPatterns.join(", ")}`);
2779
+ }
2780
+ return files;
2781
+ }
2782
+ function isDirectory(p) {
2783
+ try {
2784
+ return statSync2(p).isDirectory();
2785
+ } catch {
2786
+ return false;
2787
+ }
2788
+ }
2789
+ function readPackageName(cwd = process.cwd()) {
2790
+ try {
2791
+ const pkgJson = JSON.parse(readFileSync15(path19.resolve(cwd, "package.json"), "utf-8"));
2792
+ return typeof pkgJson.name === "string" ? pkgJson.name : undefined;
2793
+ } catch {
2794
+ return;
2795
+ }
2796
+ }
2797
+
2798
+ // src/utils/key-coverage-runner.ts
2799
+ import { globSync, readFileSync as readFileSync16 } from "node:fs";
2800
+ import * as path20 from "node:path";
2801
+ import {
2802
+ computeKeyCoverage,
2803
+ DEFAULT_SECTION_RE,
2804
+ extractDocumentedKeys
2805
+ } from "@driftdev/sdk";
2806
+ function resolvePages(page, dir) {
2807
+ const files = [path20.resolve(dir, page.page)];
2808
+ for (const pattern of page.extraPages ?? []) {
2809
+ for (const match of globSync(pattern, { cwd: dir })) {
2810
+ files.push(path20.resolve(dir, match));
2811
+ }
2812
+ }
2813
+ return files.map((p) => ({ path: p, content: readFileSync16(p, "utf-8") }));
2814
+ }
2815
+ async function resolvePageSpec(page, dir, fallback) {
2816
+ if (page.spec) {
2817
+ const specPath = path20.resolve(dir, page.spec);
2818
+ const raw = JSON.parse(readFileSync16(specPath, "utf-8"));
2819
+ const spec = raw && typeof raw === "object" && "data" in raw && "ok" in raw ? raw.data : raw;
2820
+ if (!Array.isArray(spec?.exports) && !Array.isArray(spec?.types)) {
2821
+ throw new Error(`${page.spec}: not a spec file (no exports/types arrays)`);
2822
+ }
2823
+ return spec;
2824
+ }
2825
+ if (page.entry) {
2826
+ const { apiSpec } = await resolveTruth({ entry: path20.resolve(dir, page.entry) });
2827
+ return apiSpec;
2828
+ }
2829
+ if (!fallback) {
2830
+ throw new Error(`page "${page.page}": no "spec"/"entry" in map and no scan target to fall back to`);
2831
+ }
2832
+ return fallback;
2833
+ }
2834
+ async function runDocsCoverage(loaded, fallbackSpec) {
2835
+ const pages = [];
2836
+ const errors = [];
2837
+ const warnings = [];
2838
+ for (const page of loaded.map.pages) {
2839
+ const spec = await resolvePageSpec(page, loaded.dir, fallbackSpec);
2840
+ const corpus = extractDocumentedKeys(resolvePages(page, loaded.dir), page.sectionRe ? new RegExp(page.sectionRe, "i") : DEFAULT_SECTION_RE);
2841
+ const result = computeKeyCoverage(spec, page.type, corpus, {
2842
+ internal: page.internal,
2843
+ deprecated: page.deprecated,
2844
+ replacements: page.replacements,
2845
+ annotations: page.annotations
2846
+ });
2847
+ if (!result) {
2848
+ throw new Error(`page "${page.page}": type "${page.type}" not found in spec`);
2849
+ }
2850
+ const baseline = page.baselineGaps ?? 0;
2851
+ const failures = [];
2852
+ const pageWarnings = [];
2853
+ for (const ghost of result.ghosts) {
2854
+ failures.push(`ghost option \`${ghost.key}\` — documented but not in ${page.type}`);
2855
+ const loc = ghost.locations[0];
2856
+ errors.push({
2857
+ export: ghost.key,
2858
+ issue: `ghost option \`${ghost.key}\` — documented but does not exist on ${page.type} (or any spec type)`,
2859
+ filePath: loc?.file ?? path20.resolve(loaded.dir, page.page),
2860
+ line: loc?.line
2861
+ });
2862
+ }
2863
+ const gapCount = result.counts.gapsUserFacing;
2864
+ if (gapCount > baseline) {
2865
+ const newGaps = result.gaps.userFacing.slice(0, 10).map((g) => g.key);
2866
+ failures.push(`${gapCount} undocumented options (baseline ${baseline}) — drift grew: ${newGaps.join(", ")}${gapCount > 10 ? "…" : ""}`);
2867
+ errors.push({
2868
+ export: page.type,
2869
+ issue: `${gapCount} undocumented ${page.type} options (baseline ${baseline})`,
2870
+ filePath: path20.resolve(loaded.dir, page.page)
2871
+ });
2872
+ } else if (gapCount > 0) {
2873
+ pageWarnings.push(`${gapCount} known undocumented options (baseline ${baseline})`);
2874
+ }
2875
+ for (const inv of result.inversions) {
2876
+ pageWarnings.push(`documents deprecated \`${inv.documented}\` but not its replacement \`${inv.replacement}\``);
2877
+ warnings.push({
2878
+ export: inv.documented,
2879
+ issue: `documents deprecated \`${inv.documented}\` but not its replacement \`${inv.replacement}\``,
2880
+ filePath: path20.resolve(loaded.dir, page.page)
2881
+ });
2882
+ }
2883
+ pages.push({
2884
+ page: page.page,
2885
+ type: page.type,
2886
+ baselineGaps: baseline,
2887
+ status: failures.length > 0 ? "fail" : pageWarnings.length > 0 ? "warn" : "pass",
2888
+ failures,
2889
+ warnings: pageWarnings,
2890
+ result
2891
+ });
2892
+ }
2893
+ return {
2894
+ pages,
2895
+ pass: pages.every((p) => p.status !== "fail"),
2896
+ annotations: { errors, warnings }
2897
+ };
2898
+ }
2899
+
2900
+ // src/commands/docs-map.ts
2901
+ var MATCH_ALL = /(?:)/;
2902
+ var MIN_PAGE_KEYS = 3;
2903
+ var MIN_OVERLAP = 3;
2904
+ function typeKeySets(spec) {
2905
+ const out = new Map;
2906
+ for (const entry of [...spec.exports, ...spec.types ?? []]) {
2907
+ const keys = new Set(collectTypeKeys(entry).keys());
2908
+ if (keys.size >= MIN_OVERLAP && !out.has(entry.name))
2909
+ out.set(entry.name, keys);
2910
+ }
2911
+ return out;
2912
+ }
2913
+ function registerDocsMapCommand(program) {
2914
+ const docsMap = program.command("docs-map").description("Docs-map lifecycle: scaffold and ratchet the page→type artifact");
2915
+ docsMap.command("stub").description("Scaffold a docs map: option-doc pages + type candidates ranked by key overlap").option("--docs <patterns...>", "Docs corpus: glob patterns or directories").option("--lang <language>", "Source language (inferred otherwise)").option("--abi <path>", "ABI JSON file (Clarity)").option("--spec <path>", "OpenAPI document path or URL").option("--out <file>", "Write the stub map to a file (default: stdout only)").action(async (options) => {
2916
+ const startTime = Date.now();
2917
+ const version = getVersion();
2918
+ try {
2919
+ const lang = resolveLang({ lang: options.lang, spec: options.spec, abi: options.abi });
2920
+ const { config } = loadConfig();
2921
+ const entryFile = lang === "typescript" ? config.entry ? path21.resolve(process.cwd(), config.entry) : detectEntry() : undefined;
2922
+ const { apiSpec } = await resolveTruth({
2923
+ entry: entryFile,
2924
+ lang,
2925
+ spec: options.spec,
2926
+ abi: options.abi
2927
+ });
2928
+ const types = typeKeySets(apiSpec);
2929
+ const corpus = resolveDocsCorpus(process.cwd(), options.docs, config.docs);
2930
+ const pages = [];
2931
+ for (const file of corpus) {
2932
+ const extraction = extractDocumentedKeys2([{ path: file.path, content: file.content ?? "" }], MATCH_ALL);
2933
+ const pageKeys = new Set(extraction.documented.keys());
2934
+ if (pageKeys.size < MIN_PAGE_KEYS)
2935
+ continue;
2936
+ const ranked = [...types.entries()].map(([name, keys]) => ({
2937
+ type: name,
2938
+ overlap: [...pageKeys].filter((k) => keys.has(k)).length,
2939
+ keys: keys.size
2940
+ })).filter((c2) => c2.overlap >= MIN_OVERLAP).sort((a, b) => b.overlap - a.overlap).slice(0, 3);
2941
+ if (ranked.length === 0)
2942
+ continue;
2943
+ pages.push({
2944
+ page: path21.relative(process.cwd(), file.path),
2945
+ keys: pageKeys.size,
2946
+ type: ranked[0].type,
2947
+ candidates: ranked
2948
+ });
2949
+ }
2950
+ const stub = {
2951
+ $schema: "https://unpkg.com/@driftdev/cli/schemas/drift.docs-map.schema.json",
2952
+ version: 1,
2953
+ pages: pages.map((p) => ({
2954
+ page: p.page,
2955
+ type: p.type,
2956
+ baselineGaps: 0
2957
+ }))
2958
+ };
2959
+ if (options.out) {
2960
+ writeFileSync5(path21.resolve(process.cwd(), options.out), `${JSON.stringify(stub, null, 2)}
2961
+ `);
2962
+ }
2963
+ formatOutput("docs-map stub", { candidates: pages, stub, ...options.out ? { written: options.out } : {} }, startTime, version, undefined, {
2964
+ suggested: "drift-docs-map skill",
2965
+ reason: "review type mappings, add sectionRe/annotations, then set baselines"
2966
+ });
2967
+ } catch (err) {
2968
+ formatError("docs-map stub", err instanceof Error ? err.message : String(err), startTime, version);
2969
+ }
2970
+ });
2971
+ docsMap.command("baseline").description("Tighten baselineGaps to current gap counts (ratchet — never raises)").argument("<map>", "Docs map file").action(async (mapArg) => {
2972
+ const startTime = Date.now();
2973
+ const version = getVersion();
2974
+ try {
2975
+ const loaded = loadDocsMap(mapArg);
2976
+ let fallback;
2977
+ try {
2978
+ const { config } = loadConfig();
2979
+ const entryFile = config.entry ? path21.resolve(process.cwd(), config.entry) : detectEntry();
2980
+ fallback = (await resolveTruth({ entry: entryFile })).apiSpec;
2981
+ } catch {}
2982
+ const run = await runDocsCoverage(loaded, fallback);
2983
+ const changes = [];
2984
+ for (const result of run.pages) {
2985
+ const entry = loaded.map.pages.find((p) => p.page === result.page);
2986
+ if (!entry)
2987
+ continue;
2988
+ const current = result.result.counts.gapsUserFacing;
2989
+ const existing = entry.baselineGaps;
2990
+ if (existing === undefined || current < existing) {
2991
+ changes.push({ page: result.page, from: existing ?? current, to: current });
2992
+ entry.baselineGaps = current;
2993
+ }
2994
+ }
2995
+ if (changes.length > 0) {
2996
+ const out = {
2997
+ $schema: loaded.map.$schema,
2998
+ ...loaded.map
2999
+ };
3000
+ writeFileSync5(loaded.mapPath, `${JSON.stringify(out, null, 2)}
3001
+ `);
3002
+ }
3003
+ formatOutput("docs-map baseline", { changes, map: loaded.mapPath }, startTime, version);
3004
+ } catch (err) {
3005
+ formatError("docs-map baseline", err instanceof Error ? err.message : String(err), startTime, version);
3006
+ }
3007
+ });
3008
+ }
3009
+
3010
+ // src/commands/examples.ts
3011
+ import { readFileSync as readFileSync17 } from "node:fs";
3012
+ import * as path22 from "node:path";
2657
3013
  import { validateExamples } from "@driftdev/sdk";
2658
3014
 
2659
3015
  // src/formatters/examples.ts
@@ -2732,16 +3088,16 @@ function renderExamples(data) {
2732
3088
 
2733
3089
  // src/commands/examples.ts
2734
3090
  function findPackagePath(entryFile) {
2735
- let dir = path18.dirname(entryFile);
2736
- while (dir !== path18.dirname(dir)) {
3091
+ let dir = path22.dirname(entryFile);
3092
+ while (dir !== path22.dirname(dir)) {
2737
3093
  try {
2738
- readFileSync14(path18.join(dir, "package.json"), "utf-8");
3094
+ readFileSync17(path22.join(dir, "package.json"), "utf-8");
2739
3095
  return dir;
2740
3096
  } catch {
2741
- dir = path18.dirname(dir);
3097
+ dir = path22.dirname(dir);
2742
3098
  }
2743
3099
  }
2744
- return path18.dirname(entryFile);
3100
+ return path22.dirname(entryFile);
2745
3101
  }
2746
3102
  function registerExamplesCommand(program) {
2747
3103
  program.command("examples [entry]").description("Validate @example blocks on exports").option("--typecheck", "Type-check examples with TypeScript").option("--run", "Execute examples at runtime (implies --typecheck)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--min <n>", "Minimum presence threshold (exit 1 if below)").action(async (entry, options) => {
@@ -2809,7 +3165,7 @@ function registerExamplesCommand(program) {
2809
3165
  return;
2810
3166
  }
2811
3167
  const { config } = loadConfig();
2812
- const entryFile = entry ? path18.resolve(process.cwd(), entry) : config.entry ? path18.resolve(process.cwd(), config.entry) : detectEntry();
3168
+ const entryFile = entry ? path22.resolve(process.cwd(), entry) : config.entry ? path22.resolve(process.cwd(), config.entry) : detectEntry();
2813
3169
  const { spec } = await cachedExtract(entryFile);
2814
3170
  const exports = spec.exports ?? [];
2815
3171
  const packagePath = findPackagePath(entryFile);
@@ -2844,7 +3200,7 @@ function registerExamplesCommand(program) {
2844
3200
  }
2845
3201
 
2846
3202
  // src/commands/extract.ts
2847
- import * as path19 from "node:path";
3203
+ import * as path23 from "node:path";
2848
3204
  import { Drift } from "@driftdev/sdk";
2849
3205
  import { normalize as normalize6 } from "@openpkg-ts/spec";
2850
3206
 
@@ -2883,8 +3239,8 @@ function registerExtractCommand(program) {
2883
3239
  abi: options.abi
2884
3240
  });
2885
3241
  if (options.output) {
2886
- const { writeFileSync: writeFileSync5 } = await import("node:fs");
2887
- writeFileSync5(options.output, JSON.stringify(apiSpec, null, 2));
3242
+ const { writeFileSync: writeFileSync6 } = await import("node:fs");
3243
+ writeFileSync6(options.output, JSON.stringify(apiSpec, null, 2));
2888
3244
  process.stderr.write(`drift extract: wrote ${options.output}
2889
3245
  `);
2890
3246
  } else {
@@ -2912,7 +3268,7 @@ function registerExtractCommand(program) {
2912
3268
  formatOutput("extract", { packages: specs, ...skipped.length > 0 ? { skipped } : {} }, startTime, version);
2913
3269
  return;
2914
3270
  }
2915
- const entryFile = entry ? path19.resolve(process.cwd(), entry) : detectEntry();
3271
+ const entryFile = entry ? path23.resolve(process.cwd(), entry) : detectEntry();
2916
3272
  const hasFilters = !!(options.only || options.ignore);
2917
3273
  let spec;
2918
3274
  if (hasFilters) {
@@ -2937,8 +3293,8 @@ function registerExtractCommand(program) {
2937
3293
  spec = result.spec;
2938
3294
  }
2939
3295
  if (options.output) {
2940
- const { writeFileSync: writeFileSync5 } = await import("node:fs");
2941
- writeFileSync5(options.output, JSON.stringify(spec, null, 2));
3296
+ const { writeFileSync: writeFileSync6 } = await import("node:fs");
3297
+ writeFileSync6(options.output, JSON.stringify(spec, null, 2));
2942
3298
  process.stderr.write(`drift extract: wrote ${options.output}
2943
3299
  `);
2944
3300
  } else {
@@ -2951,8 +3307,8 @@ function registerExtractCommand(program) {
2951
3307
  }
2952
3308
 
2953
3309
  // src/commands/filter.ts
2954
- import { readFileSync as readFileSync15 } from "node:fs";
2955
- import * as path20 from "node:path";
3310
+ import { readFileSync as readFileSync18 } from "node:fs";
3311
+ import * as path24 from "node:path";
2956
3312
  import { filterSpec } from "@openpkg-ts/sdk";
2957
3313
 
2958
3314
  // src/formatters/filter.ts
@@ -2983,8 +3339,8 @@ function registerFilterCommand(program) {
2983
3339
  const startTime = Date.now();
2984
3340
  const version = getVersion();
2985
3341
  try {
2986
- const filePath = path20.resolve(process.cwd(), file);
2987
- const content = readFileSync15(filePath, "utf-8");
3342
+ const filePath = path24.resolve(process.cwd(), file);
3343
+ const content = readFileSync18(filePath, "utf-8");
2988
3344
  const spec = JSON.parse(content);
2989
3345
  const criteria = {};
2990
3346
  if (options.kind) {
@@ -3010,7 +3366,7 @@ function registerFilterCommand(program) {
3010
3366
  }
3011
3367
 
3012
3368
  // src/commands/get.ts
3013
- import * as path21 from "node:path";
3369
+ import * as path25 from "node:path";
3014
3370
  import { getExport, listExports } from "@openpkg-ts/sdk";
3015
3371
 
3016
3372
  // src/formatters/get.ts
@@ -3261,7 +3617,7 @@ function registerGetCommand(program) {
3261
3617
  let entryFile;
3262
3618
  let exportName;
3263
3619
  if (name) {
3264
- entryFile = path21.resolve(process.cwd(), nameOrEntry);
3620
+ entryFile = path25.resolve(process.cwd(), nameOrEntry);
3265
3621
  exportName = name;
3266
3622
  } else {
3267
3623
  entryFile = detectEntry();
@@ -3334,7 +3690,7 @@ function renderNotFound(exportName, suggestions, startTime, version) {
3334
3690
  }
3335
3691
 
3336
3692
  // src/commands/health.ts
3337
- import * as path22 from "node:path";
3693
+ import * as path26 from "node:path";
3338
3694
  import { computeDrift as computeDrift3, isExternalExport as isExternalExport2 } from "@driftdev/sdk";
3339
3695
 
3340
3696
  // src/formatters/health.ts
@@ -3455,9 +3811,9 @@ function registerHealthCommand(program) {
3455
3811
  return;
3456
3812
  }
3457
3813
  const { config } = loadConfig();
3458
- let entryFile = entry ? path22.resolve(process.cwd(), entry) : undefined;
3814
+ let entryFile = entry ? path26.resolve(process.cwd(), entry) : undefined;
3459
3815
  if (lang === "typescript" && !entryFile) {
3460
- entryFile = config.entry ? path22.resolve(process.cwd(), config.entry) : detectEntry();
3816
+ entryFile = config.entry ? path26.resolve(process.cwd(), config.entry) : detectEntry();
3461
3817
  }
3462
3818
  const {
3463
3819
  apiSpec: spec,
@@ -3522,8 +3878,8 @@ function registerHealthCommand(program) {
3522
3878
 
3523
3879
  // src/commands/init.ts
3524
3880
  init_global();
3525
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
3526
- import * as path23 from "node:path";
3881
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync19, writeFileSync as writeFileSync6 } from "node:fs";
3882
+ import * as path27 from "node:path";
3527
3883
  import { extract as extract6 } from "@openpkg-ts/sdk";
3528
3884
  import { normalize as normalize7 } from "@openpkg-ts/spec";
3529
3885
 
@@ -3829,14 +4185,14 @@ ${detailLine}`);
3829
4185
 
3830
4186
  // src/commands/init.ts
3831
4187
  async function scanPackage(cwd, pkgDir) {
3832
- const absDir = path23.join(cwd, pkgDir);
3833
- if (!existsSync13(absDir))
4188
+ const absDir = path27.join(cwd, pkgDir);
4189
+ if (!existsSync14(absDir))
3834
4190
  return null;
3835
- const pkgPath = path23.join(absDir, "package.json");
4191
+ const pkgPath = path27.join(absDir, "package.json");
3836
4192
  let name = pkgDir;
3837
- if (existsSync13(pkgPath)) {
4193
+ if (existsSync14(pkgPath)) {
3838
4194
  try {
3839
- const pkg = JSON.parse(readFileSync16(pkgPath, "utf-8"));
4195
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
3840
4196
  if (pkg.name)
3841
4197
  name = pkg.name;
3842
4198
  } catch {}
@@ -3854,7 +4210,7 @@ async function scanPackage(cwd, pkgDir) {
3854
4210
  }
3855
4211
  const coverage = total > 0 ? Math.round(documented / total * 100) : 100;
3856
4212
  const health = Math.round(coverage * 0.5 + 100 * 0.5);
3857
- return { name, entry: path23.relative(cwd, entryFile), exports: total, coverage, health };
4213
+ return { name, entry: path27.relative(cwd, entryFile), exports: total, coverage, health };
3858
4214
  } catch {
3859
4215
  return null;
3860
4216
  }
@@ -3891,13 +4247,13 @@ function registerInitCommand(program) {
3891
4247
  return;
3892
4248
  }
3893
4249
  const config = generateConfig(packages);
3894
- const configPath = opts.project ? path23.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
4250
+ const configPath = opts.project ? path27.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
3895
4251
  if (!opts.project) {
3896
4252
  const globalDir = getGlobalDir();
3897
- if (!existsSync13(globalDir))
4253
+ if (!existsSync14(globalDir))
3898
4254
  mkdirSync6(globalDir, { recursive: true });
3899
4255
  }
3900
- writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
4256
+ writeFileSync6(configPath, `${JSON.stringify(config, null, 2)}
3901
4257
  `);
3902
4258
  ensureProjectDir(cwd);
3903
4259
  const data = {
@@ -3915,7 +4271,7 @@ function registerInitCommand(program) {
3915
4271
  }
3916
4272
 
3917
4273
  // src/commands/lint.ts
3918
- import * as path25 from "node:path";
4274
+ import * as path28 from "node:path";
3919
4275
  import { buildExportRegistry, computeDrift as computeDrift4, detectProseDrift } from "@driftdev/sdk";
3920
4276
 
3921
4277
  // src/formatters/lint.ts
@@ -3972,44 +4328,6 @@ function emitAnnotations(issues, level = "error") {
3972
4328
  }
3973
4329
  }
3974
4330
 
3975
- // src/utils/docs-corpus.ts
3976
- import { readFileSync as readFileSync17, statSync as statSync2 } from "node:fs";
3977
- import * as path24 from "node:path";
3978
- import { discoverMarkdownFiles } from "@driftdev/sdk";
3979
- function resolveDocsCorpus(cwd, docsPatterns, configDocs) {
3980
- if (!docsPatterns || docsPatterns.length === 0) {
3981
- return discoverMarkdownFiles(cwd, configDocs);
3982
- }
3983
- const include = [];
3984
- for (const pattern of docsPatterns) {
3985
- if (isDirectory(path24.resolve(cwd, pattern))) {
3986
- include.push(path24.join(pattern, "**/*.md"), path24.join(pattern, "**/*.mdx"));
3987
- } else {
3988
- include.push(pattern);
3989
- }
3990
- }
3991
- const files = discoverMarkdownFiles(cwd, { include });
3992
- if (files.length === 0) {
3993
- formatWarning(`--docs matched no markdown files: ${docsPatterns.join(", ")}`);
3994
- }
3995
- return files;
3996
- }
3997
- function isDirectory(p) {
3998
- try {
3999
- return statSync2(p).isDirectory();
4000
- } catch {
4001
- return false;
4002
- }
4003
- }
4004
- function readPackageName(cwd = process.cwd()) {
4005
- try {
4006
- const pkgJson = JSON.parse(readFileSync17(path24.resolve(cwd, "package.json"), "utf-8"));
4007
- return typeof pkgJson.name === "string" ? pkgJson.name : undefined;
4008
- } catch {
4009
- return;
4010
- }
4011
- }
4012
-
4013
4331
  // src/commands/lint.ts
4014
4332
  function registerLintCommand(program) {
4015
4333
  program.command("lint [entry]").description("Cross-reference docs against the API surface for accuracy issues").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--lang <language>", "Source language (inferred from --spec/--abi/.clar; default typescript)").option("--abi <path>", "ABI JSON file (required for --lang clarity)").option("--spec <path>", "OpenAPI document: path or URL (implies --lang openapi)").option("--docs <patterns...>", "Markdown corpus for prose drift: glob patterns or directories (overrides repo-local defaults)").option("--annotations", "Emit GitHub Actions ::error annotations for findings").action(async (entry, options) => {
@@ -4064,9 +4382,9 @@ function registerLintCommand(program) {
4064
4382
  formatOutput("lint", { issues: [], count: 0 }, startTime, version, renderLint);
4065
4383
  return;
4066
4384
  }
4067
- let entryFile = entry ? path25.resolve(process.cwd(), entry) : undefined;
4385
+ let entryFile = entry ? path28.resolve(process.cwd(), entry) : undefined;
4068
4386
  if (lang === "typescript" && !entryFile) {
4069
- entryFile = config.entry ? path25.resolve(process.cwd(), config.entry) : detectEntry();
4387
+ entryFile = config.entry ? path28.resolve(process.cwd(), config.entry) : detectEntry();
4070
4388
  }
4071
4389
  const { apiSpec: spec } = await resolveTruth({
4072
4390
  entry: entryFile,
@@ -4132,7 +4450,7 @@ function registerLintCommand(program) {
4132
4450
  }
4133
4451
 
4134
4452
  // src/commands/list.ts
4135
- import * as path26 from "node:path";
4453
+ import * as path29 from "node:path";
4136
4454
  import { computeDrift as computeDrift5 } from "@driftdev/sdk";
4137
4455
  import { listExports as listExports2 } from "@openpkg-ts/sdk";
4138
4456
 
@@ -4247,7 +4565,7 @@ function registerListCommand(program) {
4247
4565
  } else {
4248
4566
  let entryFile;
4249
4567
  if (searchOrEntry && looksLikeFilePath(searchOrEntry)) {
4250
- entryFile = path26.resolve(process.cwd(), searchOrEntry);
4568
+ entryFile = path29.resolve(process.cwd(), searchOrEntry);
4251
4569
  } else if (searchOrEntry) {
4252
4570
  entryFile = detectEntry();
4253
4571
  searchTerm = searchOrEntry;
@@ -4323,7 +4641,7 @@ function truthFlags(args) {
4323
4641
  return out;
4324
4642
  }
4325
4643
  function runDrift(cliArgs, cwd) {
4326
- return new Promise((resolve19) => {
4644
+ return new Promise((resolve22) => {
4327
4645
  const child = spawn(process.execPath, [process.argv[1], ...cliArgs, "--json"], {
4328
4646
  cwd: cwd ?? process.cwd(),
4329
4647
  env: { ...process.env, NO_COLOR: "1" },
@@ -4343,9 +4661,9 @@ function runDrift(cliArgs, cwd) {
4343
4661
  try {
4344
4662
  ok = JSON.parse(stdout).ok === true;
4345
4663
  } catch {}
4346
- resolve19({ text, ok });
4664
+ resolve22({ text, ok });
4347
4665
  });
4348
- child.on("error", (err) => resolve19({ text: `Failed to run drift: ${err.message}`, ok: false }));
4666
+ child.on("error", (err) => resolve22({ text: `Failed to run drift: ${err.message}`, ok: false }));
4349
4667
  });
4350
4668
  }
4351
4669
  function toResult({ text, ok }) {
@@ -4503,8 +4821,8 @@ function diffArgs(command, args) {
4503
4821
 
4504
4822
  // src/commands/release.ts
4505
4823
  import { execSync as execSync4 } from "node:child_process";
4506
- import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
4507
- import * as path27 from "node:path";
4824
+ import { existsSync as existsSync15, readFileSync as readFileSync20 } from "node:fs";
4825
+ import * as path30 from "node:path";
4508
4826
  import { computeDrift as computeDrift6 } from "@driftdev/sdk";
4509
4827
 
4510
4828
  // src/formatters/release.ts
@@ -4552,11 +4870,11 @@ function getLastTag() {
4552
4870
  }
4553
4871
  }
4554
4872
  function getPackageVersion(cwd) {
4555
- const pkgPath = path27.join(cwd, "package.json");
4556
- if (!existsSync14(pkgPath))
4873
+ const pkgPath = path30.join(cwd, "package.json");
4874
+ if (!existsSync15(pkgPath))
4557
4875
  return null;
4558
4876
  try {
4559
- return JSON.parse(readFileSync18(pkgPath, "utf-8")).version ?? null;
4877
+ return JSON.parse(readFileSync20(pkgPath, "utf-8")).version ?? null;
4560
4878
  } catch {
4561
4879
  return null;
4562
4880
  }
@@ -4568,7 +4886,7 @@ function registerReleaseCommand(program) {
4568
4886
  const cwd = process.cwd();
4569
4887
  try {
4570
4888
  const { config } = loadConfig();
4571
- const entryFile = entry ? path27.resolve(cwd, entry) : config.entry ? path27.resolve(cwd, config.entry) : detectEntry();
4889
+ const entryFile = entry ? path30.resolve(cwd, entry) : config.entry ? path30.resolve(cwd, config.entry) : detectEntry();
4572
4890
  const { spec } = await cachedExtract(entryFile);
4573
4891
  const exports = spec.exports ?? [];
4574
4892
  const total = exports.length;
@@ -4692,8 +5010,8 @@ function renderReport(data) {
4692
5010
 
4693
5011
  // src/utils/scan-packages.ts
4694
5012
  import { execSync as execSync5 } from "node:child_process";
4695
- import { existsSync as existsSync15, readFileSync as readFileSync19 } from "node:fs";
4696
- import * as path28 from "node:path";
5013
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "node:fs";
5014
+ import * as path31 from "node:path";
4697
5015
  import { computeDrift as computeDrift7 } from "@driftdev/sdk";
4698
5016
  function detectPackageDirs2(cwd) {
4699
5017
  const workspaces = detectWorkspaces(cwd);
@@ -4712,14 +5030,14 @@ async function scanAllPackages(cwd) {
4712
5030
  const packageDirs = detectPackageDirs2(cwd);
4713
5031
  const results = [];
4714
5032
  for (const dir of packageDirs) {
4715
- const absDir = dir === "." ? cwd : path28.join(cwd, dir);
4716
- if (!existsSync15(absDir))
5033
+ const absDir = dir === "." ? cwd : path31.join(cwd, dir);
5034
+ if (!existsSync16(absDir))
4717
5035
  continue;
4718
5036
  let name = dir;
4719
- const pkgPath = path28.join(absDir, "package.json");
4720
- if (existsSync15(pkgPath)) {
5037
+ const pkgPath = path31.join(absDir, "package.json");
5038
+ if (existsSync16(pkgPath)) {
4721
5039
  try {
4722
- const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
5040
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf-8"));
4723
5041
  if (pkg.name)
4724
5042
  name = pkg.name;
4725
5043
  } catch {}
@@ -4815,7 +5133,7 @@ function registerReportCommand(program) {
4815
5133
  }
4816
5134
 
4817
5135
  // src/commands/scan.ts
4818
- import * as path29 from "node:path";
5136
+ import * as path32 from "node:path";
4819
5137
  import {
4820
5138
  buildExportRegistry as buildExportRegistry2,
4821
5139
  computeDrift as computeDrift8,
@@ -4851,6 +5169,20 @@ function renderScan(data, next) {
4851
5169
  }
4852
5170
  lines.push("");
4853
5171
  }
5172
+ if (data.docsCoverage) {
5173
+ lines.push(indent("Docs coverage"));
5174
+ lines.push(indent(c.gray(separator())));
5175
+ for (const page of data.docsCoverage.pages) {
5176
+ const mark = page.status === "fail" ? c.red(sym.x) : page.status === "warn" ? c.yellow("!") : c.green(sym.ok);
5177
+ const cts = page.counts;
5178
+ lines.push(indent(`${mark} ${page.page} ${c.dim(`${page.type}: ${cts.documented} documented, ${cts.code} in code, ${cts.gapsUserFacing} gaps, ${cts.ghosts} ghosts, ${cts.inversions} inversions`)}`));
5179
+ for (const f of page.failures)
5180
+ lines.push(indent(` ${c.red(f)}`));
5181
+ for (const w of page.warnings)
5182
+ lines.push(indent(` ${c.yellow(w)}`));
5183
+ }
5184
+ lines.push("");
5185
+ }
4854
5186
  if (data.pass) {
4855
5187
  lines.push(indent(`${c.green(sym.ok)} Scan passed`));
4856
5188
  } else {
@@ -4884,7 +5216,7 @@ function renderBatchScan(data) {
4884
5216
 
4885
5217
  // src/commands/scan.ts
4886
5218
  function registerScanCommand(program) {
4887
- program.command("scan [entry]").description("Run coverage + lint + prose drift in one pass").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--lang <language>", "Source language (inferred from --spec/--abi/.clar; default typescript)").option("--abi <path>", "ABI JSON file (required for --lang clarity)").option("--spec <path>", "OpenAPI document: path or URL (implies --lang openapi)").option("--docs <patterns...>", "Markdown corpus for prose drift: glob patterns or directories (overrides repo-local defaults)").action(async (entry, options) => {
5219
+ program.command("scan [entry]").description("Run coverage + lint + prose drift in one pass").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--lang <language>", "Source language (inferred from --spec/--abi/.clar; default typescript)").option("--abi <path>", "ABI JSON file (required for --lang clarity)").option("--spec <path>", "OpenAPI document: path or URL (implies --lang openapi)").option("--docs <patterns...>", "Markdown corpus for prose drift: glob patterns or directories (overrides repo-local defaults)").option("--docs-map <file>", "Docs map (page→type) activating key-coverage mode: gaps/ghosts/inversions per page").option("--annotations", "Emit GitHub Actions ::error/::warning annotations for findings").action(async (entry, options) => {
4888
5220
  const startTime = Date.now();
4889
5221
  const version = getVersion();
4890
5222
  try {
@@ -4898,6 +5230,10 @@ function registerScanCommand(program) {
4898
5230
  formatError("scan", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4899
5231
  return;
4900
5232
  }
5233
+ if (options.all && options.docsMap) {
5234
+ formatError("scan", "--docs-map is not supported with --all", startTime, version);
5235
+ return;
5236
+ }
4901
5237
  if (lang === "clarity" && !options.abi) {
4902
5238
  formatError("scan", "--abi is required when --lang clarity", startTime, version);
4903
5239
  return;
@@ -4959,9 +5295,9 @@ function registerScanCommand(program) {
4959
5295
  return;
4960
5296
  }
4961
5297
  const { config } = loadConfig();
4962
- let entryFile = entry ? path29.resolve(process.cwd(), entry) : undefined;
5298
+ let entryFile = entry ? path32.resolve(process.cwd(), entry) : undefined;
4963
5299
  if (lang === "typescript" && !entryFile) {
4964
- entryFile = config.entry ? path29.resolve(process.cwd(), config.entry) : detectEntry();
5300
+ entryFile = config.entry ? path32.resolve(process.cwd(), config.entry) : detectEntry();
4965
5301
  }
4966
5302
  const { apiSpec, packageName, packageVersion } = await resolveTruth({
4967
5303
  entry: entryFile,
@@ -5017,6 +5353,11 @@ function registerScanCommand(program) {
5017
5353
  formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
5018
5354
  }
5019
5355
  }
5356
+ let docsCoverage;
5357
+ if (options.docsMap) {
5358
+ const loaded = loadDocsMap(options.docsMap);
5359
+ docsCoverage = await runDocsCoverage(loaded, apiSpec);
5360
+ }
5020
5361
  const healthIssues = issues.map((i) => ({ export: i.export, issue: i.issue }));
5021
5362
  const h = computeHealth(total, documented, healthIssues);
5022
5363
  let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
@@ -5024,7 +5365,7 @@ function registerScanCommand(program) {
5024
5365
  const ratchet = computeRatchetMin(min);
5025
5366
  min = ratchet.effectiveMin;
5026
5367
  }
5027
- const pass = min === undefined || h.health >= min;
5368
+ const pass = (min === undefined || h.health >= min) && (docsCoverage?.pass ?? true);
5028
5369
  const data = {
5029
5370
  coverage: {
5030
5371
  score: coverageScore,
@@ -5037,7 +5378,26 @@ function registerScanCommand(program) {
5037
5378
  health: h.health,
5038
5379
  pass,
5039
5380
  packageName,
5040
- packageVersion
5381
+ packageVersion,
5382
+ ...docsCoverage ? {
5383
+ docsCoverage: {
5384
+ pass: docsCoverage.pass,
5385
+ pages: docsCoverage.pages.map((p) => ({
5386
+ page: p.page,
5387
+ type: p.type,
5388
+ status: p.status,
5389
+ baselineGaps: p.baselineGaps,
5390
+ counts: p.result.counts,
5391
+ failures: p.failures,
5392
+ warnings: p.warnings,
5393
+ gaps: p.result.gaps,
5394
+ ghosts: p.result.ghosts,
5395
+ inversions: p.result.inversions,
5396
+ documentedKeysFromOtherTypes: p.result.documentedKeysFromOtherTypes,
5397
+ annotated: p.result.annotated
5398
+ }))
5399
+ }
5400
+ } : {}
5041
5401
  };
5042
5402
  let next;
5043
5403
  if (issues.length > 0) {
@@ -5052,9 +5412,16 @@ function registerScanCommand(program) {
5052
5412
  };
5053
5413
  }
5054
5414
  formatOutput("scan", data, startTime, version, renderScan, next);
5415
+ if (options.annotations && docsCoverage) {
5416
+ emitAnnotations(docsCoverage.annotations.errors, "error");
5417
+ emitAnnotations(docsCoverage.annotations.warnings, "warning");
5418
+ }
5419
+ if (options.annotations && issues.length > 0)
5420
+ emitAnnotations(issues);
5055
5421
  if (!pass) {
5056
5422
  if (!shouldRenderHuman()) {
5057
- process.stderr.write(`scan failed: health ${h.health}%${min !== undefined ? ` (need ${min}%)` : ""}, ${issues.length} issues
5423
+ const covFails = docsCoverage ? docsCoverage.pages.flatMap((p) => p.failures.map((f) => `${p.page}: ${f}`)) : [];
5424
+ process.stderr.write(`scan failed: health ${h.health}%${min !== undefined ? ` (need ${min}%)` : ""}, ${issues.length} issues${covFails.length > 0 ? `; docs coverage: ${covFails.join(" | ")}` : ""}
5058
5425
  `);
5059
5426
  }
5060
5427
  process.exitCode = 1;
@@ -5105,8 +5472,8 @@ function registerSemverCommand(program) {
5105
5472
  }
5106
5473
 
5107
5474
  // src/commands/validate.ts
5108
- import { readFileSync as readFileSync20 } from "node:fs";
5109
- import * as path30 from "node:path";
5475
+ import { readFileSync as readFileSync22 } from "node:fs";
5476
+ import * as path33 from "node:path";
5110
5477
  import { validateSpec } from "@openpkg-ts/spec";
5111
5478
 
5112
5479
  // src/formatters/validate.ts
@@ -5132,8 +5499,8 @@ function registerValidateCommand(program) {
5132
5499
  const startTime = Date.now();
5133
5500
  const version = getVersion();
5134
5501
  try {
5135
- const filePath = path30.resolve(process.cwd(), file);
5136
- const content = readFileSync20(filePath, "utf-8");
5502
+ const filePath = path33.resolve(process.cwd(), file);
5503
+ const content = readFileSync22(filePath, "utf-8");
5137
5504
  const spec = JSON.parse(content);
5138
5505
  const result = validateSpec(spec);
5139
5506
  const data = {
@@ -5184,7 +5551,11 @@ var COMMAND_EXAMPLES = {
5184
5551
  validate: ["drift validate spec.json --json"],
5185
5552
  filter: ["drift filter spec.json --kind function --json"],
5186
5553
  report: ["drift report --json"],
5187
- cache: ["drift cache status", "drift cache clear"]
5554
+ cache: ["drift cache status", "drift cache clear"],
5555
+ "docs-map": [
5556
+ "drift docs-map stub --docs docs/ --out drift.docs-map.json",
5557
+ "drift docs-map baseline drift.docs-map.json"
5558
+ ]
5188
5559
  };
5189
5560
  function extractCapabilities(program) {
5190
5561
  const commands = [];
@@ -5235,6 +5606,15 @@ function extractCapabilities(program) {
5235
5606
  name: "examples",
5236
5607
  description: "Example validation results",
5237
5608
  operations: { read: "examples" }
5609
+ },
5610
+ {
5611
+ name: "docs-map",
5612
+ description: "Committed page→type map for docs key-coverage mode",
5613
+ operations: {
5614
+ create: "docs-map stub",
5615
+ update: "docs-map baseline",
5616
+ read: "scan --docs-map <file>"
5617
+ }
5238
5618
  }
5239
5619
  ],
5240
5620
  workflows: {
@@ -5251,6 +5631,10 @@ function extractCapabilities(program) {
5251
5631
  "pre-release": {
5252
5632
  steps: ["scan", "breaking", "release"],
5253
5633
  description: "Full pre-release quality gate"
5634
+ },
5635
+ "docs-key-coverage": {
5636
+ steps: ["docs-map stub", "scan --docs-map", "docs-map baseline"],
5637
+ description: "Gap/ghost/inversion gate: docs pages vs spec type keys"
5254
5638
  }
5255
5639
  }
5256
5640
  };
@@ -5258,13 +5642,13 @@ function extractCapabilities(program) {
5258
5642
 
5259
5643
  // src/drift.ts
5260
5644
  var __filename2 = fileURLToPath2(import.meta.url);
5261
- var __dirname3 = path31.dirname(__filename2);
5262
- var packageJson = JSON.parse(readFileSync21(path31.join(__dirname3, "../package.json"), "utf-8"));
5645
+ var __dirname3 = path34.dirname(__filename2);
5646
+ var packageJson = JSON.parse(readFileSync23(path34.join(__dirname3, "../package.json"), "utf-8"));
5263
5647
  var program = new Command;
5264
5648
  program.name("drift").description("drift — detect when your docs drift from your code").version(packageJson.version).option("--json", "Force JSON output (default when piped)").option("--human", "Force human-readable output (default in terminal)").option("--config <path>", "Path to drift config file").option("--cwd <dir>", "Run as if started in <dir>").option("--no-cache", "Bypass spec cache").option("--tools", "List all available tools for agent use (JSON)").hook("preAction", (_thisCommand) => {
5265
5649
  const opts = program.opts();
5266
5650
  if (opts.cwd) {
5267
- process.chdir(path31.resolve(opts.cwd));
5651
+ process.chdir(path34.resolve(opts.cwd));
5268
5652
  }
5269
5653
  setOutputMode({ json: opts.json, human: opts.human });
5270
5654
  setConfigPath(opts.config);
@@ -5276,6 +5660,7 @@ registerExtractCommand(program);
5276
5660
  registerListCommand(program);
5277
5661
  registerGetCommand(program);
5278
5662
  registerValidateCommand(program);
5663
+ registerDocsMapCommand(program);
5279
5664
  registerFilterCommand(program);
5280
5665
  registerCoverageCommand(program);
5281
5666
  registerExamplesCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftdev/cli",
3
- "version": "1.11.0",
3
+ "version": "1.12.1",
4
4
  "description": "Drift CLI - detect when your docs drift from your code",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@driftdev/clarity-adapter": "^1.0.1",
49
49
  "@driftdev/openapi-adapter": "^1.0.1",
50
- "@driftdev/sdk": "^1.11.0",
50
+ "@driftdev/sdk": "^1.12.0",
51
51
  "@modelcontextprotocol/sdk": "^1.29.0",
52
52
  "@openpkg-ts/sdk": "^0.43.0",
53
53
  "@openpkg-ts/spec": "^0.43.0",
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/@driftdev/cli/schemas/drift.docs-map.schema.json",
4
+ "title": "Drift docs map",
5
+ "description": "Page→type mapping for docs key-coverage mode (drift scan --docs-map). The committed artifact of the LLM-writes-the-map / machine-runs-the-map split: an agent may propose it, a human commits it, CI runs it deterministically.",
6
+ "type": "object",
7
+ "required": ["version", "pages"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "pages": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["page", "type"],
16
+ "properties": {
17
+ "page": {
18
+ "type": "string",
19
+ "description": "Docs page path, relative to this file's directory"
20
+ },
21
+ "extraPages": {
22
+ "type": "array",
23
+ "items": { "type": "string" },
24
+ "description": "Additional pages/globs merged into the same corpus (e.g. _snippets/*.mdx)"
25
+ },
26
+ "type": {
27
+ "type": "string",
28
+ "description": "Spec type whose property keys this page documents"
29
+ },
30
+ "spec": {
31
+ "type": "string",
32
+ "description": "Committed spec file to diff against (relative to this file). Mutually exclusive with entry; omit both to use the scan target."
33
+ },
34
+ "entry": {
35
+ "type": "string",
36
+ "description": "Entry file to extract the spec from. Mutually exclusive with spec."
37
+ },
38
+ "sectionRe": {
39
+ "type": "string",
40
+ "description": "Case-insensitive heading regex opening an options section (default: option|config)"
41
+ },
42
+ "internal": {
43
+ "type": "array",
44
+ "items": { "type": "string" },
45
+ "description": "Internal keys beyond the _-prefix convention (excluded from user-facing gaps)"
46
+ },
47
+ "deprecated": {
48
+ "type": "array",
49
+ "items": { "type": "string" },
50
+ "description": "Deprecated override — auto-derived from spec metadata when omitted"
51
+ },
52
+ "replacements": {
53
+ "type": "object",
54
+ "additionalProperties": { "type": "string" },
55
+ "description": "deprecatedKey → replacementKey override — auto-derived from spec deprecation reasons when omitted"
56
+ },
57
+ "annotations": {
58
+ "type": "object",
59
+ "additionalProperties": {
60
+ "enum": ["prose-documented", "internal-by-convention", "ignore"]
61
+ },
62
+ "description": "Agent-proposed, human-committed key classifications. prose-documented: genuinely documented in prose, excluded from gap FAIL; internal-by-convention: treated as internal; ignore: excluded entirely (state the reason in a nearby comment key)."
63
+ },
64
+ "baselineGaps": {
65
+ "type": "integer",
66
+ "minimum": 0,
67
+ "description": "Gap ratchet: CI fails when user-facing gaps exceed this committed count. Drift shrinks, never grows — use `drift docs-map baseline` to tighten."
68
+ }
69
+ },
70
+ "additionalProperties": true
71
+ }
72
+ }
73
+ },
74
+ "additionalProperties": true
75
+ }