@driftdev/cli 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,32 @@ 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
+ try {
84
+ cached = JSON.parse(readFileSync(join2(__dirname2, "../../package.json"), "utf-8")).version ?? "0.0.0";
85
+ } catch {
86
+ cached = "0.0.0";
87
+ }
88
+ return cached ?? "0.0.0";
89
+ }
90
+
91
+ // src/cache/spec-cache.ts
73
92
  var _noCache = false;
74
93
  function setNoCache(value) {
75
94
  _noCache = value;
@@ -141,7 +160,7 @@ function buildCacheKey(input) {
141
160
  const pkgJson = findPackageJson(absEntry);
142
161
  const pkgMtime = pkgJson ? getMtime(pkgJson) : 0;
143
162
  const srcMtime = getSourceMaxMtime(absEntry);
144
- const parts = [absEntry, String(entryMtime), String(pkgMtime), String(srcMtime)];
163
+ const parts = [getVersion(), absEntry, String(entryMtime), String(pkgMtime), String(srcMtime)];
145
164
  if (input.configHash)
146
165
  parts.push(input.configHash);
147
166
  return hashString(parts.join("|"));
@@ -154,7 +173,7 @@ function getCachedSpec(input) {
154
173
  if (!existsSync2(cacheFile))
155
174
  return null;
156
175
  try {
157
- const raw = JSON.parse(readFileSync(cacheFile, "utf-8"));
176
+ const raw = JSON.parse(readFileSync2(cacheFile, "utf-8"));
158
177
  return raw;
159
178
  } catch {
160
179
  return null;
@@ -177,7 +196,7 @@ function getConfigHash(configPath) {
177
196
  if (!configPath || !existsSync2(configPath))
178
197
  return;
179
198
  try {
180
- const content = readFileSync(configPath, "utf-8");
199
+ const content = readFileSync2(configPath, "utf-8");
181
200
  return hashString(content);
182
201
  } catch {
183
202
  return;
@@ -633,13 +652,13 @@ function formatWarning(message) {
633
652
  }
634
653
 
635
654
  // src/utils/resolve-specs.ts
636
- import { readFileSync as readFileSync5 } from "node:fs";
655
+ import { readFileSync as readFileSync6 } from "node:fs";
637
656
  import * as path7 from "node:path";
638
657
  import { extract as extract2 } from "@openpkg-ts/sdk";
639
658
  import { normalize as normalize2 } from "@openpkg-ts/spec";
640
659
 
641
660
  // src/config/loader.ts
642
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
661
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
643
662
  import * as path4 from "node:path";
644
663
 
645
664
  // src/config/drift-config.ts
@@ -731,7 +750,7 @@ function loadConfig(cwd = process.cwd()) {
731
750
  if (!existsSync4(absPath)) {
732
751
  throw new Error(`Config file not found: ${absPath}`);
733
752
  }
734
- const raw = JSON.parse(readFileSync2(absPath, "utf-8"));
753
+ const raw = JSON.parse(readFileSync3(absPath, "utf-8"));
735
754
  const result = validateConfig(raw);
736
755
  if (!result.ok) {
737
756
  throw new Error(`Invalid config at ${absPath}: ${result.errors.join(", ")}`);
@@ -744,7 +763,7 @@ function loadConfig(cwd = process.cwd()) {
744
763
  const driftConfigPath = path4.join(current, "drift.config.json");
745
764
  if (existsSync4(driftConfigPath)) {
746
765
  try {
747
- const raw = JSON.parse(readFileSync2(driftConfigPath, "utf-8"));
766
+ const raw = JSON.parse(readFileSync3(driftConfigPath, "utf-8"));
748
767
  const result = validateConfig(raw);
749
768
  if (!result.ok) {
750
769
  throw new Error(`Invalid config at ${driftConfigPath}: ${result.errors.join(", ")}`);
@@ -760,7 +779,7 @@ function loadConfig(cwd = process.cwd()) {
760
779
  const pkgPath = path4.join(current, "package.json");
761
780
  if (existsSync4(pkgPath)) {
762
781
  try {
763
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
782
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
764
783
  if (pkg.drift && typeof pkg.drift === "object") {
765
784
  const result = validateConfig(pkg.drift);
766
785
  if (!result.ok) {
@@ -781,7 +800,7 @@ function loadConfig(cwd = process.cwd()) {
781
800
  const globalPath = getGlobalConfigPath();
782
801
  if (existsSync4(globalPath)) {
783
802
  try {
784
- const raw = JSON.parse(readFileSync2(globalPath, "utf-8"));
803
+ const raw = JSON.parse(readFileSync3(globalPath, "utf-8"));
785
804
  const result = validateConfig(raw);
786
805
  if (result.ok) {
787
806
  return { config: result.config, configPath: globalPath };
@@ -792,18 +811,18 @@ function loadConfig(cwd = process.cwd()) {
792
811
  }
793
812
 
794
813
  // src/utils/detect-entry.ts
795
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
814
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
796
815
  import * as path6 from "node:path";
797
816
 
798
817
  // src/utils/workspaces.ts
799
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
818
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
800
819
  import * as path5 from "node:path";
801
820
  function detectWorkspaces(cwd) {
802
821
  const pkgPath = path5.join(cwd, "package.json");
803
822
  if (!existsSync5(pkgPath))
804
823
  return null;
805
824
  try {
806
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
825
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
807
826
  if (Array.isArray(pkg.workspaces))
808
827
  return pkg.workspaces;
809
828
  if (pkg.workspaces?.packages && Array.isArray(pkg.workspaces.packages))
@@ -847,7 +866,7 @@ function discoverPackages(cwd) {
847
866
  const pkgPath = path5.join(absDir, "package.json");
848
867
  if (existsSync5(pkgPath)) {
849
868
  try {
850
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
869
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
851
870
  if (pkg.name)
852
871
  name = pkg.name;
853
872
  if (pkg.private === true)
@@ -878,7 +897,7 @@ function detectEntry(cwd = process.cwd()) {
878
897
  }
879
898
  if (existsSync6(pkgPath)) {
880
899
  try {
881
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
900
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
882
901
  const typesField = pkg.types || pkg.typings;
883
902
  if (typesField && typeof typesField === "string") {
884
903
  const resolved = tryResolve("types", typesField);
@@ -1039,7 +1058,7 @@ function collectStringValues(value, depth = 0) {
1039
1058
 
1040
1059
  // src/utils/resolve-specs.ts
1041
1060
  function loadSpec(filePath) {
1042
- return JSON.parse(readFileSync5(path7.resolve(process.cwd(), filePath), "utf-8"));
1061
+ return JSON.parse(readFileSync6(path7.resolve(process.cwd(), filePath), "utf-8"));
1043
1062
  }
1044
1063
  async function resolveSpecs(opts) {
1045
1064
  const { config } = loadConfig();
@@ -1086,23 +1105,6 @@ async function resolveSpecs(opts) {
1086
1105
  throw new Error("Provide two spec files, or use --base <ref>");
1087
1106
  }
1088
1107
 
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
1108
  // src/commands/breaking.ts
1107
1109
  function registerBreakingCommand(program) {
1108
1110
  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 +2653,358 @@ function registerDiffCommand(program) {
2651
2653
  });
2652
2654
  }
2653
2655
 
2654
- // src/commands/examples.ts
2655
- import { readFileSync as readFileSync14 } from "node:fs";
2656
+ // src/commands/docs-map.ts
2657
+ import { writeFileSync as writeFileSync5 } from "node:fs";
2658
+ import * as path21 from "node:path";
2659
+ import { collectTypeKeys, extractDocumentedKeys as extractDocumentedKeys2 } from "@driftdev/sdk";
2660
+
2661
+ // src/config/docs-map.ts
2662
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
2656
2663
  import * as path18 from "node:path";
2664
+ var ANNOTATIONS = new Set(["prose-documented", "internal-by-convention", "ignore"]);
2665
+ function isStringArray(v) {
2666
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
2667
+ }
2668
+ function validateDocsMap(raw) {
2669
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2670
+ return { ok: false, errors: ["Docs map must be a JSON object"] };
2671
+ }
2672
+ const errors = [];
2673
+ const obj = raw;
2674
+ if (obj.version !== 1)
2675
+ errors.push('"version" must be 1');
2676
+ if (!Array.isArray(obj.pages)) {
2677
+ errors.push('"pages" must be an array');
2678
+ return { ok: false, errors };
2679
+ }
2680
+ obj.pages.forEach((p, i) => {
2681
+ const at = `"pages[${i}]"`;
2682
+ if (typeof p !== "object" || p === null) {
2683
+ errors.push(`${at} must be an object`);
2684
+ return;
2685
+ }
2686
+ const page = p;
2687
+ if (typeof page.page !== "string" || !page.page)
2688
+ errors.push(`${at}.page must be a string`);
2689
+ if (typeof page.type !== "string" || !page.type)
2690
+ errors.push(`${at}.type must be a string`);
2691
+ if (page.spec !== undefined && typeof page.spec !== "string")
2692
+ errors.push(`${at}.spec must be a string`);
2693
+ if (page.entry !== undefined && typeof page.entry !== "string")
2694
+ errors.push(`${at}.entry must be a string`);
2695
+ if (page.spec !== undefined && page.entry !== undefined)
2696
+ errors.push(`${at} must set at most one of "spec"/"entry" (omit both to use the scan target)`);
2697
+ if (page.sectionRe !== undefined) {
2698
+ if (typeof page.sectionRe !== "string") {
2699
+ errors.push(`${at}.sectionRe must be a string`);
2700
+ } else {
2701
+ try {
2702
+ new RegExp(page.sectionRe);
2703
+ } catch {
2704
+ errors.push(`${at}.sectionRe is not a valid regex: ${page.sectionRe}`);
2705
+ }
2706
+ }
2707
+ }
2708
+ for (const field of ["extraPages", "internal", "deprecated"]) {
2709
+ if (page[field] !== undefined && !isStringArray(page[field]))
2710
+ errors.push(`${at}.${field} must be an array of strings`);
2711
+ }
2712
+ if (page.replacements !== undefined) {
2713
+ const r = page.replacements;
2714
+ if (typeof r !== "object" || r === null || Array.isArray(r)) {
2715
+ errors.push(`${at}.replacements must be an object of oldKey → newKey strings`);
2716
+ } else if (!Object.values(r).every((v) => typeof v === "string")) {
2717
+ errors.push(`${at}.replacements values must be strings`);
2718
+ }
2719
+ }
2720
+ if (page.annotations !== undefined) {
2721
+ const a = page.annotations;
2722
+ if (typeof a !== "object" || a === null || Array.isArray(a)) {
2723
+ errors.push(`${at}.annotations must be an object of key → annotation`);
2724
+ } else {
2725
+ for (const [k, v] of Object.entries(a)) {
2726
+ if (typeof v !== "string" || !ANNOTATIONS.has(v))
2727
+ errors.push(`${at}.annotations["${k}"] must be one of: ${[...ANNOTATIONS].join(", ")} (got ${JSON.stringify(v)})`);
2728
+ }
2729
+ }
2730
+ }
2731
+ if (page.baselineGaps !== undefined && (typeof page.baselineGaps !== "number" || page.baselineGaps < 0 || !Number.isInteger(page.baselineGaps)))
2732
+ errors.push(`${at}.baselineGaps must be a non-negative integer`);
2733
+ });
2734
+ if (errors.length > 0)
2735
+ return { ok: false, errors };
2736
+ return { ok: true, map: obj };
2737
+ }
2738
+ function loadDocsMap(mapPath, cwd = process.cwd()) {
2739
+ const absPath = path18.resolve(cwd, mapPath);
2740
+ if (!existsSync13(absPath))
2741
+ throw new Error(`Docs map not found: ${absPath}`);
2742
+ let raw;
2743
+ try {
2744
+ raw = JSON.parse(readFileSync14(absPath, "utf-8"));
2745
+ } catch {
2746
+ throw new Error(`Invalid JSON in ${absPath}`);
2747
+ }
2748
+ const result = validateDocsMap(raw);
2749
+ if (!result.ok) {
2750
+ throw new Error(`Invalid docs map at ${absPath}: ${result.errors.join("; ")}`);
2751
+ }
2752
+ return { map: result.map, mapPath: absPath, dir: path18.dirname(absPath) };
2753
+ }
2754
+
2755
+ // src/utils/docs-corpus.ts
2756
+ import { readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
2757
+ import * as path19 from "node:path";
2758
+ import { discoverMarkdownFiles } from "@driftdev/sdk";
2759
+ function resolveDocsCorpus(cwd, docsPatterns, configDocs) {
2760
+ if (!docsPatterns || docsPatterns.length === 0) {
2761
+ return discoverMarkdownFiles(cwd, configDocs);
2762
+ }
2763
+ const include = [];
2764
+ for (const pattern of docsPatterns) {
2765
+ if (isDirectory(path19.resolve(cwd, pattern))) {
2766
+ include.push(path19.join(pattern, "**/*.md"), path19.join(pattern, "**/*.mdx"));
2767
+ } else {
2768
+ include.push(pattern);
2769
+ }
2770
+ }
2771
+ const files = discoverMarkdownFiles(cwd, { include });
2772
+ if (files.length === 0) {
2773
+ formatWarning(`--docs matched no markdown files: ${docsPatterns.join(", ")}`);
2774
+ }
2775
+ return files;
2776
+ }
2777
+ function isDirectory(p) {
2778
+ try {
2779
+ return statSync2(p).isDirectory();
2780
+ } catch {
2781
+ return false;
2782
+ }
2783
+ }
2784
+ function readPackageName(cwd = process.cwd()) {
2785
+ try {
2786
+ const pkgJson = JSON.parse(readFileSync15(path19.resolve(cwd, "package.json"), "utf-8"));
2787
+ return typeof pkgJson.name === "string" ? pkgJson.name : undefined;
2788
+ } catch {
2789
+ return;
2790
+ }
2791
+ }
2792
+
2793
+ // src/utils/key-coverage-runner.ts
2794
+ import { globSync, readFileSync as readFileSync16 } from "node:fs";
2795
+ import * as path20 from "node:path";
2796
+ import {
2797
+ computeKeyCoverage,
2798
+ DEFAULT_SECTION_RE,
2799
+ extractDocumentedKeys
2800
+ } from "@driftdev/sdk";
2801
+ function resolvePages(page, dir) {
2802
+ const files = [path20.resolve(dir, page.page)];
2803
+ for (const pattern of page.extraPages ?? []) {
2804
+ for (const match of globSync(pattern, { cwd: dir })) {
2805
+ files.push(path20.resolve(dir, match));
2806
+ }
2807
+ }
2808
+ return files.map((p) => ({ path: p, content: readFileSync16(p, "utf-8") }));
2809
+ }
2810
+ async function resolvePageSpec(page, dir, fallback) {
2811
+ if (page.spec) {
2812
+ const specPath = path20.resolve(dir, page.spec);
2813
+ const raw = JSON.parse(readFileSync16(specPath, "utf-8"));
2814
+ const spec = raw && typeof raw === "object" && "data" in raw && "ok" in raw ? raw.data : raw;
2815
+ if (!Array.isArray(spec?.exports) && !Array.isArray(spec?.types)) {
2816
+ throw new Error(`${page.spec}: not a spec file (no exports/types arrays)`);
2817
+ }
2818
+ return spec;
2819
+ }
2820
+ if (page.entry) {
2821
+ const { apiSpec } = await resolveTruth({ entry: path20.resolve(dir, page.entry) });
2822
+ return apiSpec;
2823
+ }
2824
+ if (!fallback) {
2825
+ throw new Error(`page "${page.page}": no "spec"/"entry" in map and no scan target to fall back to`);
2826
+ }
2827
+ return fallback;
2828
+ }
2829
+ async function runDocsCoverage(loaded, fallbackSpec) {
2830
+ const pages = [];
2831
+ const errors = [];
2832
+ const warnings = [];
2833
+ for (const page of loaded.map.pages) {
2834
+ const spec = await resolvePageSpec(page, loaded.dir, fallbackSpec);
2835
+ const corpus = extractDocumentedKeys(resolvePages(page, loaded.dir), page.sectionRe ? new RegExp(page.sectionRe, "i") : DEFAULT_SECTION_RE);
2836
+ const result = computeKeyCoverage(spec, page.type, corpus, {
2837
+ internal: page.internal,
2838
+ deprecated: page.deprecated,
2839
+ replacements: page.replacements,
2840
+ annotations: page.annotations
2841
+ });
2842
+ if (!result) {
2843
+ throw new Error(`page "${page.page}": type "${page.type}" not found in spec`);
2844
+ }
2845
+ const baseline = page.baselineGaps ?? 0;
2846
+ const failures = [];
2847
+ const pageWarnings = [];
2848
+ for (const ghost of result.ghosts) {
2849
+ failures.push(`ghost option \`${ghost.key}\` — documented but not in ${page.type}`);
2850
+ const loc = ghost.locations[0];
2851
+ errors.push({
2852
+ export: ghost.key,
2853
+ issue: `ghost option \`${ghost.key}\` — documented but does not exist on ${page.type} (or any spec type)`,
2854
+ filePath: loc?.file ?? path20.resolve(loaded.dir, page.page),
2855
+ line: loc?.line
2856
+ });
2857
+ }
2858
+ const gapCount = result.counts.gapsUserFacing;
2859
+ if (gapCount > baseline) {
2860
+ const newGaps = result.gaps.userFacing.slice(0, 10).map((g) => g.key);
2861
+ failures.push(`${gapCount} undocumented options (baseline ${baseline}) — drift grew: ${newGaps.join(", ")}${gapCount > 10 ? "…" : ""}`);
2862
+ errors.push({
2863
+ export: page.type,
2864
+ issue: `${gapCount} undocumented ${page.type} options (baseline ${baseline})`,
2865
+ filePath: path20.resolve(loaded.dir, page.page)
2866
+ });
2867
+ } else if (gapCount > 0) {
2868
+ pageWarnings.push(`${gapCount} known undocumented options (baseline ${baseline})`);
2869
+ }
2870
+ for (const inv of result.inversions) {
2871
+ pageWarnings.push(`documents deprecated \`${inv.documented}\` but not its replacement \`${inv.replacement}\``);
2872
+ warnings.push({
2873
+ export: inv.documented,
2874
+ issue: `documents deprecated \`${inv.documented}\` but not its replacement \`${inv.replacement}\``,
2875
+ filePath: path20.resolve(loaded.dir, page.page)
2876
+ });
2877
+ }
2878
+ pages.push({
2879
+ page: page.page,
2880
+ type: page.type,
2881
+ baselineGaps: baseline,
2882
+ status: failures.length > 0 ? "fail" : pageWarnings.length > 0 ? "warn" : "pass",
2883
+ failures,
2884
+ warnings: pageWarnings,
2885
+ result
2886
+ });
2887
+ }
2888
+ return {
2889
+ pages,
2890
+ pass: pages.every((p) => p.status !== "fail"),
2891
+ annotations: { errors, warnings }
2892
+ };
2893
+ }
2894
+
2895
+ // src/commands/docs-map.ts
2896
+ var MATCH_ALL = /(?:)/;
2897
+ var MIN_PAGE_KEYS = 3;
2898
+ var MIN_OVERLAP = 3;
2899
+ function typeKeySets(spec) {
2900
+ const out = new Map;
2901
+ for (const entry of [...spec.exports, ...spec.types ?? []]) {
2902
+ const keys = new Set(collectTypeKeys(entry).keys());
2903
+ if (keys.size >= MIN_OVERLAP && !out.has(entry.name))
2904
+ out.set(entry.name, keys);
2905
+ }
2906
+ return out;
2907
+ }
2908
+ function registerDocsMapCommand(program) {
2909
+ const docsMap = program.command("docs-map").description("Docs-map lifecycle: scaffold and ratchet the page→type artifact");
2910
+ 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) => {
2911
+ const startTime = Date.now();
2912
+ const version = getVersion();
2913
+ try {
2914
+ const lang = resolveLang({ lang: options.lang, spec: options.spec, abi: options.abi });
2915
+ const { config } = loadConfig();
2916
+ const entryFile = lang === "typescript" ? config.entry ? path21.resolve(process.cwd(), config.entry) : detectEntry() : undefined;
2917
+ const { apiSpec } = await resolveTruth({
2918
+ entry: entryFile,
2919
+ lang,
2920
+ spec: options.spec,
2921
+ abi: options.abi
2922
+ });
2923
+ const types = typeKeySets(apiSpec);
2924
+ const corpus = resolveDocsCorpus(process.cwd(), options.docs, config.docs);
2925
+ const pages = [];
2926
+ for (const file of corpus) {
2927
+ const extraction = extractDocumentedKeys2([{ path: file.path, content: file.content ?? "" }], MATCH_ALL);
2928
+ const pageKeys = new Set(extraction.documented.keys());
2929
+ if (pageKeys.size < MIN_PAGE_KEYS)
2930
+ continue;
2931
+ const ranked = [...types.entries()].map(([name, keys]) => ({
2932
+ type: name,
2933
+ overlap: [...pageKeys].filter((k) => keys.has(k)).length,
2934
+ keys: keys.size
2935
+ })).filter((c2) => c2.overlap >= MIN_OVERLAP).sort((a, b) => b.overlap - a.overlap).slice(0, 3);
2936
+ if (ranked.length === 0)
2937
+ continue;
2938
+ pages.push({
2939
+ page: path21.relative(process.cwd(), file.path),
2940
+ keys: pageKeys.size,
2941
+ type: ranked[0].type,
2942
+ candidates: ranked
2943
+ });
2944
+ }
2945
+ const stub = {
2946
+ $schema: "https://unpkg.com/@driftdev/cli/schemas/drift.docs-map.schema.json",
2947
+ version: 1,
2948
+ pages: pages.map((p) => ({
2949
+ page: p.page,
2950
+ type: p.type,
2951
+ baselineGaps: 0
2952
+ }))
2953
+ };
2954
+ if (options.out) {
2955
+ writeFileSync5(path21.resolve(process.cwd(), options.out), `${JSON.stringify(stub, null, 2)}
2956
+ `);
2957
+ }
2958
+ formatOutput("docs-map stub", { candidates: pages, stub, ...options.out ? { written: options.out } : {} }, startTime, version, undefined, {
2959
+ suggested: "drift-docs-map skill",
2960
+ reason: "review type mappings, add sectionRe/annotations, then set baselines"
2961
+ });
2962
+ } catch (err) {
2963
+ formatError("docs-map stub", err instanceof Error ? err.message : String(err), startTime, version);
2964
+ }
2965
+ });
2966
+ docsMap.command("baseline").description("Tighten baselineGaps to current gap counts (ratchet — never raises)").argument("<map>", "Docs map file").action(async (mapArg) => {
2967
+ const startTime = Date.now();
2968
+ const version = getVersion();
2969
+ try {
2970
+ const loaded = loadDocsMap(mapArg);
2971
+ let fallback;
2972
+ try {
2973
+ const { config } = loadConfig();
2974
+ const entryFile = config.entry ? path21.resolve(process.cwd(), config.entry) : detectEntry();
2975
+ fallback = (await resolveTruth({ entry: entryFile })).apiSpec;
2976
+ } catch {}
2977
+ const run = await runDocsCoverage(loaded, fallback);
2978
+ const changes = [];
2979
+ for (const result of run.pages) {
2980
+ const entry = loaded.map.pages.find((p) => p.page === result.page);
2981
+ if (!entry)
2982
+ continue;
2983
+ const current = result.result.counts.gapsUserFacing;
2984
+ const existing = entry.baselineGaps;
2985
+ if (existing === undefined || current < existing) {
2986
+ changes.push({ page: result.page, from: existing ?? current, to: current });
2987
+ entry.baselineGaps = current;
2988
+ }
2989
+ }
2990
+ if (changes.length > 0) {
2991
+ const out = {
2992
+ $schema: loaded.map.$schema,
2993
+ ...loaded.map
2994
+ };
2995
+ writeFileSync5(loaded.mapPath, `${JSON.stringify(out, null, 2)}
2996
+ `);
2997
+ }
2998
+ formatOutput("docs-map baseline", { changes, map: loaded.mapPath }, startTime, version);
2999
+ } catch (err) {
3000
+ formatError("docs-map baseline", err instanceof Error ? err.message : String(err), startTime, version);
3001
+ }
3002
+ });
3003
+ }
3004
+
3005
+ // src/commands/examples.ts
3006
+ import { readFileSync as readFileSync17 } from "node:fs";
3007
+ import * as path22 from "node:path";
2657
3008
  import { validateExamples } from "@driftdev/sdk";
2658
3009
 
2659
3010
  // src/formatters/examples.ts
@@ -2732,16 +3083,16 @@ function renderExamples(data) {
2732
3083
 
2733
3084
  // src/commands/examples.ts
2734
3085
  function findPackagePath(entryFile) {
2735
- let dir = path18.dirname(entryFile);
2736
- while (dir !== path18.dirname(dir)) {
3086
+ let dir = path22.dirname(entryFile);
3087
+ while (dir !== path22.dirname(dir)) {
2737
3088
  try {
2738
- readFileSync14(path18.join(dir, "package.json"), "utf-8");
3089
+ readFileSync17(path22.join(dir, "package.json"), "utf-8");
2739
3090
  return dir;
2740
3091
  } catch {
2741
- dir = path18.dirname(dir);
3092
+ dir = path22.dirname(dir);
2742
3093
  }
2743
3094
  }
2744
- return path18.dirname(entryFile);
3095
+ return path22.dirname(entryFile);
2745
3096
  }
2746
3097
  function registerExamplesCommand(program) {
2747
3098
  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 +3160,7 @@ function registerExamplesCommand(program) {
2809
3160
  return;
2810
3161
  }
2811
3162
  const { config } = loadConfig();
2812
- const entryFile = entry ? path18.resolve(process.cwd(), entry) : config.entry ? path18.resolve(process.cwd(), config.entry) : detectEntry();
3163
+ const entryFile = entry ? path22.resolve(process.cwd(), entry) : config.entry ? path22.resolve(process.cwd(), config.entry) : detectEntry();
2813
3164
  const { spec } = await cachedExtract(entryFile);
2814
3165
  const exports = spec.exports ?? [];
2815
3166
  const packagePath = findPackagePath(entryFile);
@@ -2844,7 +3195,7 @@ function registerExamplesCommand(program) {
2844
3195
  }
2845
3196
 
2846
3197
  // src/commands/extract.ts
2847
- import * as path19 from "node:path";
3198
+ import * as path23 from "node:path";
2848
3199
  import { Drift } from "@driftdev/sdk";
2849
3200
  import { normalize as normalize6 } from "@openpkg-ts/spec";
2850
3201
 
@@ -2883,8 +3234,8 @@ function registerExtractCommand(program) {
2883
3234
  abi: options.abi
2884
3235
  });
2885
3236
  if (options.output) {
2886
- const { writeFileSync: writeFileSync5 } = await import("node:fs");
2887
- writeFileSync5(options.output, JSON.stringify(apiSpec, null, 2));
3237
+ const { writeFileSync: writeFileSync6 } = await import("node:fs");
3238
+ writeFileSync6(options.output, JSON.stringify(apiSpec, null, 2));
2888
3239
  process.stderr.write(`drift extract: wrote ${options.output}
2889
3240
  `);
2890
3241
  } else {
@@ -2912,7 +3263,7 @@ function registerExtractCommand(program) {
2912
3263
  formatOutput("extract", { packages: specs, ...skipped.length > 0 ? { skipped } : {} }, startTime, version);
2913
3264
  return;
2914
3265
  }
2915
- const entryFile = entry ? path19.resolve(process.cwd(), entry) : detectEntry();
3266
+ const entryFile = entry ? path23.resolve(process.cwd(), entry) : detectEntry();
2916
3267
  const hasFilters = !!(options.only || options.ignore);
2917
3268
  let spec;
2918
3269
  if (hasFilters) {
@@ -2937,8 +3288,8 @@ function registerExtractCommand(program) {
2937
3288
  spec = result.spec;
2938
3289
  }
2939
3290
  if (options.output) {
2940
- const { writeFileSync: writeFileSync5 } = await import("node:fs");
2941
- writeFileSync5(options.output, JSON.stringify(spec, null, 2));
3291
+ const { writeFileSync: writeFileSync6 } = await import("node:fs");
3292
+ writeFileSync6(options.output, JSON.stringify(spec, null, 2));
2942
3293
  process.stderr.write(`drift extract: wrote ${options.output}
2943
3294
  `);
2944
3295
  } else {
@@ -2951,8 +3302,8 @@ function registerExtractCommand(program) {
2951
3302
  }
2952
3303
 
2953
3304
  // src/commands/filter.ts
2954
- import { readFileSync as readFileSync15 } from "node:fs";
2955
- import * as path20 from "node:path";
3305
+ import { readFileSync as readFileSync18 } from "node:fs";
3306
+ import * as path24 from "node:path";
2956
3307
  import { filterSpec } from "@openpkg-ts/sdk";
2957
3308
 
2958
3309
  // src/formatters/filter.ts
@@ -2983,8 +3334,8 @@ function registerFilterCommand(program) {
2983
3334
  const startTime = Date.now();
2984
3335
  const version = getVersion();
2985
3336
  try {
2986
- const filePath = path20.resolve(process.cwd(), file);
2987
- const content = readFileSync15(filePath, "utf-8");
3337
+ const filePath = path24.resolve(process.cwd(), file);
3338
+ const content = readFileSync18(filePath, "utf-8");
2988
3339
  const spec = JSON.parse(content);
2989
3340
  const criteria = {};
2990
3341
  if (options.kind) {
@@ -3010,7 +3361,7 @@ function registerFilterCommand(program) {
3010
3361
  }
3011
3362
 
3012
3363
  // src/commands/get.ts
3013
- import * as path21 from "node:path";
3364
+ import * as path25 from "node:path";
3014
3365
  import { getExport, listExports } from "@openpkg-ts/sdk";
3015
3366
 
3016
3367
  // src/formatters/get.ts
@@ -3261,7 +3612,7 @@ function registerGetCommand(program) {
3261
3612
  let entryFile;
3262
3613
  let exportName;
3263
3614
  if (name) {
3264
- entryFile = path21.resolve(process.cwd(), nameOrEntry);
3615
+ entryFile = path25.resolve(process.cwd(), nameOrEntry);
3265
3616
  exportName = name;
3266
3617
  } else {
3267
3618
  entryFile = detectEntry();
@@ -3334,7 +3685,7 @@ function renderNotFound(exportName, suggestions, startTime, version) {
3334
3685
  }
3335
3686
 
3336
3687
  // src/commands/health.ts
3337
- import * as path22 from "node:path";
3688
+ import * as path26 from "node:path";
3338
3689
  import { computeDrift as computeDrift3, isExternalExport as isExternalExport2 } from "@driftdev/sdk";
3339
3690
 
3340
3691
  // src/formatters/health.ts
@@ -3455,9 +3806,9 @@ function registerHealthCommand(program) {
3455
3806
  return;
3456
3807
  }
3457
3808
  const { config } = loadConfig();
3458
- let entryFile = entry ? path22.resolve(process.cwd(), entry) : undefined;
3809
+ let entryFile = entry ? path26.resolve(process.cwd(), entry) : undefined;
3459
3810
  if (lang === "typescript" && !entryFile) {
3460
- entryFile = config.entry ? path22.resolve(process.cwd(), config.entry) : detectEntry();
3811
+ entryFile = config.entry ? path26.resolve(process.cwd(), config.entry) : detectEntry();
3461
3812
  }
3462
3813
  const {
3463
3814
  apiSpec: spec,
@@ -3522,8 +3873,8 @@ function registerHealthCommand(program) {
3522
3873
 
3523
3874
  // src/commands/init.ts
3524
3875
  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";
3876
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync19, writeFileSync as writeFileSync6 } from "node:fs";
3877
+ import * as path27 from "node:path";
3527
3878
  import { extract as extract6 } from "@openpkg-ts/sdk";
3528
3879
  import { normalize as normalize7 } from "@openpkg-ts/spec";
3529
3880
 
@@ -3829,14 +4180,14 @@ ${detailLine}`);
3829
4180
 
3830
4181
  // src/commands/init.ts
3831
4182
  async function scanPackage(cwd, pkgDir) {
3832
- const absDir = path23.join(cwd, pkgDir);
3833
- if (!existsSync13(absDir))
4183
+ const absDir = path27.join(cwd, pkgDir);
4184
+ if (!existsSync14(absDir))
3834
4185
  return null;
3835
- const pkgPath = path23.join(absDir, "package.json");
4186
+ const pkgPath = path27.join(absDir, "package.json");
3836
4187
  let name = pkgDir;
3837
- if (existsSync13(pkgPath)) {
4188
+ if (existsSync14(pkgPath)) {
3838
4189
  try {
3839
- const pkg = JSON.parse(readFileSync16(pkgPath, "utf-8"));
4190
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
3840
4191
  if (pkg.name)
3841
4192
  name = pkg.name;
3842
4193
  } catch {}
@@ -3854,7 +4205,7 @@ async function scanPackage(cwd, pkgDir) {
3854
4205
  }
3855
4206
  const coverage = total > 0 ? Math.round(documented / total * 100) : 100;
3856
4207
  const health = Math.round(coverage * 0.5 + 100 * 0.5);
3857
- return { name, entry: path23.relative(cwd, entryFile), exports: total, coverage, health };
4208
+ return { name, entry: path27.relative(cwd, entryFile), exports: total, coverage, health };
3858
4209
  } catch {
3859
4210
  return null;
3860
4211
  }
@@ -3891,13 +4242,13 @@ function registerInitCommand(program) {
3891
4242
  return;
3892
4243
  }
3893
4244
  const config = generateConfig(packages);
3894
- const configPath = opts.project ? path23.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
4245
+ const configPath = opts.project ? path27.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
3895
4246
  if (!opts.project) {
3896
4247
  const globalDir = getGlobalDir();
3897
- if (!existsSync13(globalDir))
4248
+ if (!existsSync14(globalDir))
3898
4249
  mkdirSync6(globalDir, { recursive: true });
3899
4250
  }
3900
- writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
4251
+ writeFileSync6(configPath, `${JSON.stringify(config, null, 2)}
3901
4252
  `);
3902
4253
  ensureProjectDir(cwd);
3903
4254
  const data = {
@@ -3915,7 +4266,7 @@ function registerInitCommand(program) {
3915
4266
  }
3916
4267
 
3917
4268
  // src/commands/lint.ts
3918
- import * as path25 from "node:path";
4269
+ import * as path28 from "node:path";
3919
4270
  import { buildExportRegistry, computeDrift as computeDrift4, detectProseDrift } from "@driftdev/sdk";
3920
4271
 
3921
4272
  // src/formatters/lint.ts
@@ -3972,44 +4323,6 @@ function emitAnnotations(issues, level = "error") {
3972
4323
  }
3973
4324
  }
3974
4325
 
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
4326
  // src/commands/lint.ts
4014
4327
  function registerLintCommand(program) {
4015
4328
  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 +4377,9 @@ function registerLintCommand(program) {
4064
4377
  formatOutput("lint", { issues: [], count: 0 }, startTime, version, renderLint);
4065
4378
  return;
4066
4379
  }
4067
- let entryFile = entry ? path25.resolve(process.cwd(), entry) : undefined;
4380
+ let entryFile = entry ? path28.resolve(process.cwd(), entry) : undefined;
4068
4381
  if (lang === "typescript" && !entryFile) {
4069
- entryFile = config.entry ? path25.resolve(process.cwd(), config.entry) : detectEntry();
4382
+ entryFile = config.entry ? path28.resolve(process.cwd(), config.entry) : detectEntry();
4070
4383
  }
4071
4384
  const { apiSpec: spec } = await resolveTruth({
4072
4385
  entry: entryFile,
@@ -4132,7 +4445,7 @@ function registerLintCommand(program) {
4132
4445
  }
4133
4446
 
4134
4447
  // src/commands/list.ts
4135
- import * as path26 from "node:path";
4448
+ import * as path29 from "node:path";
4136
4449
  import { computeDrift as computeDrift5 } from "@driftdev/sdk";
4137
4450
  import { listExports as listExports2 } from "@openpkg-ts/sdk";
4138
4451
 
@@ -4247,7 +4560,7 @@ function registerListCommand(program) {
4247
4560
  } else {
4248
4561
  let entryFile;
4249
4562
  if (searchOrEntry && looksLikeFilePath(searchOrEntry)) {
4250
- entryFile = path26.resolve(process.cwd(), searchOrEntry);
4563
+ entryFile = path29.resolve(process.cwd(), searchOrEntry);
4251
4564
  } else if (searchOrEntry) {
4252
4565
  entryFile = detectEntry();
4253
4566
  searchTerm = searchOrEntry;
@@ -4323,7 +4636,7 @@ function truthFlags(args) {
4323
4636
  return out;
4324
4637
  }
4325
4638
  function runDrift(cliArgs, cwd) {
4326
- return new Promise((resolve19) => {
4639
+ return new Promise((resolve22) => {
4327
4640
  const child = spawn(process.execPath, [process.argv[1], ...cliArgs, "--json"], {
4328
4641
  cwd: cwd ?? process.cwd(),
4329
4642
  env: { ...process.env, NO_COLOR: "1" },
@@ -4343,9 +4656,9 @@ function runDrift(cliArgs, cwd) {
4343
4656
  try {
4344
4657
  ok = JSON.parse(stdout).ok === true;
4345
4658
  } catch {}
4346
- resolve19({ text, ok });
4659
+ resolve22({ text, ok });
4347
4660
  });
4348
- child.on("error", (err) => resolve19({ text: `Failed to run drift: ${err.message}`, ok: false }));
4661
+ child.on("error", (err) => resolve22({ text: `Failed to run drift: ${err.message}`, ok: false }));
4349
4662
  });
4350
4663
  }
4351
4664
  function toResult({ text, ok }) {
@@ -4503,8 +4816,8 @@ function diffArgs(command, args) {
4503
4816
 
4504
4817
  // src/commands/release.ts
4505
4818
  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";
4819
+ import { existsSync as existsSync15, readFileSync as readFileSync20 } from "node:fs";
4820
+ import * as path30 from "node:path";
4508
4821
  import { computeDrift as computeDrift6 } from "@driftdev/sdk";
4509
4822
 
4510
4823
  // src/formatters/release.ts
@@ -4552,11 +4865,11 @@ function getLastTag() {
4552
4865
  }
4553
4866
  }
4554
4867
  function getPackageVersion(cwd) {
4555
- const pkgPath = path27.join(cwd, "package.json");
4556
- if (!existsSync14(pkgPath))
4868
+ const pkgPath = path30.join(cwd, "package.json");
4869
+ if (!existsSync15(pkgPath))
4557
4870
  return null;
4558
4871
  try {
4559
- return JSON.parse(readFileSync18(pkgPath, "utf-8")).version ?? null;
4872
+ return JSON.parse(readFileSync20(pkgPath, "utf-8")).version ?? null;
4560
4873
  } catch {
4561
4874
  return null;
4562
4875
  }
@@ -4568,7 +4881,7 @@ function registerReleaseCommand(program) {
4568
4881
  const cwd = process.cwd();
4569
4882
  try {
4570
4883
  const { config } = loadConfig();
4571
- const entryFile = entry ? path27.resolve(cwd, entry) : config.entry ? path27.resolve(cwd, config.entry) : detectEntry();
4884
+ const entryFile = entry ? path30.resolve(cwd, entry) : config.entry ? path30.resolve(cwd, config.entry) : detectEntry();
4572
4885
  const { spec } = await cachedExtract(entryFile);
4573
4886
  const exports = spec.exports ?? [];
4574
4887
  const total = exports.length;
@@ -4692,8 +5005,8 @@ function renderReport(data) {
4692
5005
 
4693
5006
  // src/utils/scan-packages.ts
4694
5007
  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";
5008
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "node:fs";
5009
+ import * as path31 from "node:path";
4697
5010
  import { computeDrift as computeDrift7 } from "@driftdev/sdk";
4698
5011
  function detectPackageDirs2(cwd) {
4699
5012
  const workspaces = detectWorkspaces(cwd);
@@ -4712,14 +5025,14 @@ async function scanAllPackages(cwd) {
4712
5025
  const packageDirs = detectPackageDirs2(cwd);
4713
5026
  const results = [];
4714
5027
  for (const dir of packageDirs) {
4715
- const absDir = dir === "." ? cwd : path28.join(cwd, dir);
4716
- if (!existsSync15(absDir))
5028
+ const absDir = dir === "." ? cwd : path31.join(cwd, dir);
5029
+ if (!existsSync16(absDir))
4717
5030
  continue;
4718
5031
  let name = dir;
4719
- const pkgPath = path28.join(absDir, "package.json");
4720
- if (existsSync15(pkgPath)) {
5032
+ const pkgPath = path31.join(absDir, "package.json");
5033
+ if (existsSync16(pkgPath)) {
4721
5034
  try {
4722
- const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
5035
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf-8"));
4723
5036
  if (pkg.name)
4724
5037
  name = pkg.name;
4725
5038
  } catch {}
@@ -4815,7 +5128,7 @@ function registerReportCommand(program) {
4815
5128
  }
4816
5129
 
4817
5130
  // src/commands/scan.ts
4818
- import * as path29 from "node:path";
5131
+ import * as path32 from "node:path";
4819
5132
  import {
4820
5133
  buildExportRegistry as buildExportRegistry2,
4821
5134
  computeDrift as computeDrift8,
@@ -4851,6 +5164,20 @@ function renderScan(data, next) {
4851
5164
  }
4852
5165
  lines.push("");
4853
5166
  }
5167
+ if (data.docsCoverage) {
5168
+ lines.push(indent("Docs coverage"));
5169
+ lines.push(indent(c.gray(separator())));
5170
+ for (const page of data.docsCoverage.pages) {
5171
+ const mark = page.status === "fail" ? c.red(sym.x) : page.status === "warn" ? c.yellow("!") : c.green(sym.ok);
5172
+ const cts = page.counts;
5173
+ 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`)}`));
5174
+ for (const f of page.failures)
5175
+ lines.push(indent(` ${c.red(f)}`));
5176
+ for (const w of page.warnings)
5177
+ lines.push(indent(` ${c.yellow(w)}`));
5178
+ }
5179
+ lines.push("");
5180
+ }
4854
5181
  if (data.pass) {
4855
5182
  lines.push(indent(`${c.green(sym.ok)} Scan passed`));
4856
5183
  } else {
@@ -4884,7 +5211,7 @@ function renderBatchScan(data) {
4884
5211
 
4885
5212
  // src/commands/scan.ts
4886
5213
  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) => {
5214
+ 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
5215
  const startTime = Date.now();
4889
5216
  const version = getVersion();
4890
5217
  try {
@@ -4898,6 +5225,10 @@ function registerScanCommand(program) {
4898
5225
  formatError("scan", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4899
5226
  return;
4900
5227
  }
5228
+ if (options.all && options.docsMap) {
5229
+ formatError("scan", "--docs-map is not supported with --all", startTime, version);
5230
+ return;
5231
+ }
4901
5232
  if (lang === "clarity" && !options.abi) {
4902
5233
  formatError("scan", "--abi is required when --lang clarity", startTime, version);
4903
5234
  return;
@@ -4959,9 +5290,9 @@ function registerScanCommand(program) {
4959
5290
  return;
4960
5291
  }
4961
5292
  const { config } = loadConfig();
4962
- let entryFile = entry ? path29.resolve(process.cwd(), entry) : undefined;
5293
+ let entryFile = entry ? path32.resolve(process.cwd(), entry) : undefined;
4963
5294
  if (lang === "typescript" && !entryFile) {
4964
- entryFile = config.entry ? path29.resolve(process.cwd(), config.entry) : detectEntry();
5295
+ entryFile = config.entry ? path32.resolve(process.cwd(), config.entry) : detectEntry();
4965
5296
  }
4966
5297
  const { apiSpec, packageName, packageVersion } = await resolveTruth({
4967
5298
  entry: entryFile,
@@ -5017,6 +5348,11 @@ function registerScanCommand(program) {
5017
5348
  formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
5018
5349
  }
5019
5350
  }
5351
+ let docsCoverage;
5352
+ if (options.docsMap) {
5353
+ const loaded = loadDocsMap(options.docsMap);
5354
+ docsCoverage = await runDocsCoverage(loaded, apiSpec);
5355
+ }
5020
5356
  const healthIssues = issues.map((i) => ({ export: i.export, issue: i.issue }));
5021
5357
  const h = computeHealth(total, documented, healthIssues);
5022
5358
  let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
@@ -5024,7 +5360,7 @@ function registerScanCommand(program) {
5024
5360
  const ratchet = computeRatchetMin(min);
5025
5361
  min = ratchet.effectiveMin;
5026
5362
  }
5027
- const pass = min === undefined || h.health >= min;
5363
+ const pass = (min === undefined || h.health >= min) && (docsCoverage?.pass ?? true);
5028
5364
  const data = {
5029
5365
  coverage: {
5030
5366
  score: coverageScore,
@@ -5037,7 +5373,26 @@ function registerScanCommand(program) {
5037
5373
  health: h.health,
5038
5374
  pass,
5039
5375
  packageName,
5040
- packageVersion
5376
+ packageVersion,
5377
+ ...docsCoverage ? {
5378
+ docsCoverage: {
5379
+ pass: docsCoverage.pass,
5380
+ pages: docsCoverage.pages.map((p) => ({
5381
+ page: p.page,
5382
+ type: p.type,
5383
+ status: p.status,
5384
+ baselineGaps: p.baselineGaps,
5385
+ counts: p.result.counts,
5386
+ failures: p.failures,
5387
+ warnings: p.warnings,
5388
+ gaps: p.result.gaps,
5389
+ ghosts: p.result.ghosts,
5390
+ inversions: p.result.inversions,
5391
+ documentedKeysFromOtherTypes: p.result.documentedKeysFromOtherTypes,
5392
+ annotated: p.result.annotated
5393
+ }))
5394
+ }
5395
+ } : {}
5041
5396
  };
5042
5397
  let next;
5043
5398
  if (issues.length > 0) {
@@ -5052,9 +5407,16 @@ function registerScanCommand(program) {
5052
5407
  };
5053
5408
  }
5054
5409
  formatOutput("scan", data, startTime, version, renderScan, next);
5410
+ if (options.annotations && docsCoverage) {
5411
+ emitAnnotations(docsCoverage.annotations.errors, "error");
5412
+ emitAnnotations(docsCoverage.annotations.warnings, "warning");
5413
+ }
5414
+ if (options.annotations && issues.length > 0)
5415
+ emitAnnotations(issues);
5055
5416
  if (!pass) {
5056
5417
  if (!shouldRenderHuman()) {
5057
- process.stderr.write(`scan failed: health ${h.health}%${min !== undefined ? ` (need ${min}%)` : ""}, ${issues.length} issues
5418
+ const covFails = docsCoverage ? docsCoverage.pages.flatMap((p) => p.failures.map((f) => `${p.page}: ${f}`)) : [];
5419
+ process.stderr.write(`scan failed: health ${h.health}%${min !== undefined ? ` (need ${min}%)` : ""}, ${issues.length} issues${covFails.length > 0 ? `; docs coverage: ${covFails.join(" | ")}` : ""}
5058
5420
  `);
5059
5421
  }
5060
5422
  process.exitCode = 1;
@@ -5105,8 +5467,8 @@ function registerSemverCommand(program) {
5105
5467
  }
5106
5468
 
5107
5469
  // src/commands/validate.ts
5108
- import { readFileSync as readFileSync20 } from "node:fs";
5109
- import * as path30 from "node:path";
5470
+ import { readFileSync as readFileSync22 } from "node:fs";
5471
+ import * as path33 from "node:path";
5110
5472
  import { validateSpec } from "@openpkg-ts/spec";
5111
5473
 
5112
5474
  // src/formatters/validate.ts
@@ -5132,8 +5494,8 @@ function registerValidateCommand(program) {
5132
5494
  const startTime = Date.now();
5133
5495
  const version = getVersion();
5134
5496
  try {
5135
- const filePath = path30.resolve(process.cwd(), file);
5136
- const content = readFileSync20(filePath, "utf-8");
5497
+ const filePath = path33.resolve(process.cwd(), file);
5498
+ const content = readFileSync22(filePath, "utf-8");
5137
5499
  const spec = JSON.parse(content);
5138
5500
  const result = validateSpec(spec);
5139
5501
  const data = {
@@ -5184,7 +5546,11 @@ var COMMAND_EXAMPLES = {
5184
5546
  validate: ["drift validate spec.json --json"],
5185
5547
  filter: ["drift filter spec.json --kind function --json"],
5186
5548
  report: ["drift report --json"],
5187
- cache: ["drift cache status", "drift cache clear"]
5549
+ cache: ["drift cache status", "drift cache clear"],
5550
+ "docs-map": [
5551
+ "drift docs-map stub --docs docs/ --out drift.docs-map.json",
5552
+ "drift docs-map baseline drift.docs-map.json"
5553
+ ]
5188
5554
  };
5189
5555
  function extractCapabilities(program) {
5190
5556
  const commands = [];
@@ -5235,6 +5601,15 @@ function extractCapabilities(program) {
5235
5601
  name: "examples",
5236
5602
  description: "Example validation results",
5237
5603
  operations: { read: "examples" }
5604
+ },
5605
+ {
5606
+ name: "docs-map",
5607
+ description: "Committed page→type map for docs key-coverage mode",
5608
+ operations: {
5609
+ create: "docs-map stub",
5610
+ update: "docs-map baseline",
5611
+ read: "scan --docs-map <file>"
5612
+ }
5238
5613
  }
5239
5614
  ],
5240
5615
  workflows: {
@@ -5251,6 +5626,10 @@ function extractCapabilities(program) {
5251
5626
  "pre-release": {
5252
5627
  steps: ["scan", "breaking", "release"],
5253
5628
  description: "Full pre-release quality gate"
5629
+ },
5630
+ "docs-key-coverage": {
5631
+ steps: ["docs-map stub", "scan --docs-map", "docs-map baseline"],
5632
+ description: "Gap/ghost/inversion gate: docs pages vs spec type keys"
5254
5633
  }
5255
5634
  }
5256
5635
  };
@@ -5258,13 +5637,13 @@ function extractCapabilities(program) {
5258
5637
 
5259
5638
  // src/drift.ts
5260
5639
  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"));
5640
+ var __dirname3 = path34.dirname(__filename2);
5641
+ var packageJson = JSON.parse(readFileSync23(path34.join(__dirname3, "../package.json"), "utf-8"));
5263
5642
  var program = new Command;
5264
5643
  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
5644
  const opts = program.opts();
5266
5645
  if (opts.cwd) {
5267
- process.chdir(path31.resolve(opts.cwd));
5646
+ process.chdir(path34.resolve(opts.cwd));
5268
5647
  }
5269
5648
  setOutputMode({ json: opts.json, human: opts.human });
5270
5649
  setConfigPath(opts.config);
@@ -5276,6 +5655,7 @@ registerExtractCommand(program);
5276
5655
  registerListCommand(program);
5277
5656
  registerGetCommand(program);
5278
5657
  registerValidateCommand(program);
5658
+ registerDocsMapCommand(program);
5279
5659
  registerFilterCommand(program);
5280
5660
  registerCoverageCommand(program);
5281
5661
  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.0",
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
+ }