okf-kit 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +124 -0
  2. package/dist/bundle.d.ts +2 -0
  3. package/dist/bundle.js +69 -0
  4. package/dist/bundle.js.map +1 -0
  5. package/dist/cli.d.ts +16 -0
  6. package/dist/cli.js +142 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/errors.d.ts +3 -0
  9. package/dist/errors.js +4 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/git.d.ts +15 -0
  12. package/dist/git.js +30 -0
  13. package/dist/git.js.map +1 -0
  14. package/dist/init.d.ts +11 -0
  15. package/dist/init.js +63 -0
  16. package/dist/init.js.map +1 -0
  17. package/dist/links.d.ts +15 -0
  18. package/dist/links.js +82 -0
  19. package/dist/links.js.map +1 -0
  20. package/dist/report.d.ts +9 -0
  21. package/dist/report.js +51 -0
  22. package/dist/report.js.map +1 -0
  23. package/dist/rules/frontmatter-required.d.ts +2 -0
  24. package/dist/rules/frontmatter-required.js +46 -0
  25. package/dist/rules/frontmatter-required.js.map +1 -0
  26. package/dist/rules/index.d.ts +9 -0
  27. package/dist/rules/index.js +16 -0
  28. package/dist/rules/index.js.map +1 -0
  29. package/dist/rules/links-resolve.d.ts +2 -0
  30. package/dist/rules/links-resolve.js +39 -0
  31. package/dist/rules/links-resolve.js.map +1 -0
  32. package/dist/rules/no-absolute-links.d.ts +2 -0
  33. package/dist/rules/no-absolute-links.js +25 -0
  34. package/dist/rules/no-absolute-links.js.map +1 -0
  35. package/dist/rules/reserved-files-bare.d.ts +2 -0
  36. package/dist/rules/reserved-files-bare.js +22 -0
  37. package/dist/rules/reserved-files-bare.js.map +1 -0
  38. package/dist/rules/sources-fresh.d.ts +2 -0
  39. package/dist/rules/sources-fresh.js +99 -0
  40. package/dist/rules/sources-fresh.js.map +1 -0
  41. package/dist/rules/sources-shape.d.ts +2 -0
  42. package/dist/rules/sources-shape.js +41 -0
  43. package/dist/rules/sources-shape.js.map +1 -0
  44. package/dist/templates.d.ts +14 -0
  45. package/dist/templates.js +293 -0
  46. package/dist/templates.js.map +1 -0
  47. package/dist/types.d.ts +43 -0
  48. package/dist/types.js +2 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/util.d.ts +23 -0
  51. package/dist/util.js +50 -0
  52. package/dist/util.js.map +1 -0
  53. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # okf-kit
2
+
3
+ `okf-kit` validates knowledge bundles against the [Open Knowledge Format (OKF) v0.1 spec](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md), a shape for markdown-plus-frontmatter knowledge bundles meant to be readable by both humans and agents. The check catalog here was shaped by the Phase-0 OKF pilot in agent-tasks ([PR #385](https://github.com/LanNguyenSi/agent-tasks/pull/385)), where a few structural mistakes (bad links, absolute paths) turned out to be easy to make and easy to catch mechanically.
4
+
5
+ Part of [agent-dx](https://github.com/LanNguyenSi/agent-dx), playbooks and tooling for teams shipping with AI agents.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # one-off, no install
11
+ npx okf-kit check path/to/bundle
12
+
13
+ # or install it
14
+ npm install -g okf-kit
15
+ ```
16
+
17
+ Requires Node >= 20.
18
+
19
+ ## Quick start
20
+
21
+ ```bash
22
+ okf-kit check path/to/bundle
23
+
24
+ # explicit repo root, used for both sources-shape existence checks and
25
+ # sources-fresh staleness checks (see "repo-root auto-detection" below for
26
+ # what happens when you omit this)
27
+ okf-kit check path/to/bundle --repo-root /path/to/repo
28
+
29
+ # JSON output for tooling
30
+ okf-kit check path/to/bundle --json
31
+
32
+ # fail on warnings too, not just errors (STALE findings are warnings)
33
+ okf-kit check path/to/bundle --strict
34
+ ```
35
+
36
+ ## Scaffold a bundle (`init`)
37
+
38
+ ```bash
39
+ # scaffold docs/okf (the default target, relative to the current directory)
40
+ okf-kit init
41
+
42
+ # scaffold a specific directory instead
43
+ okf-kit init path/to/bundle
44
+
45
+ # an existing, non-empty target directory is refused (exit 2) unless forced;
46
+ # --force overwrites only the files init owns, nothing else in the directory
47
+ okf-kit init path/to/bundle --force
48
+ ```
49
+
50
+ `init` writes `index.md`, `log.md`, and one template doc per concept type: `overview-template.md`, `module-template.md`, `invariant-template.md`, `runbook-template.md`, plus `benchmark-template.md` for measuring whether the bundle helps. `index.md` and `log.md` carry no frontmatter (`reserved-files-bare`); every template doc carries full frontmatter (`type`, `title`, `description`, `tags`, `timestamp`, and, except for the benchmark template, `sources`) plus inline HTML-comment guidance on writing dense, source-verified, pointer-carrying docs instead of filler. All generated links are same-directory relative (`name.md`), never a leading-slash form.
51
+
52
+ ### Placeholder sources are intentional
53
+
54
+ Every template doc except `benchmark-template.md` ships with `sources: [path/to/covered/source]`, a placeholder, not a real path. Running `okf-kit check` against the freshly scaffolded bundle (with a repo root available, explicit or auto-detected) will report that placeholder as a `sources-shape` "does not exist" error on every template doc. That is intentional: it is the tool telling you which docs still need a real source path, not a bug in the scaffold. The `init` completion message repeats this so it isn't missed. Replace each placeholder with the real repo-root-relative path(s) the doc describes as you write it, and the error clears doc by doc.
55
+
56
+ ### Authoring guidance baked into the templates
57
+
58
+ - **`timestamp` means "last verified against sources," not "created on."** Bump it, and add a line to `log.md`, every time you re-verify a doc against its sources. Always use the real instant of verification (`new Date().toISOString()` or equivalent); never hand-write an artificial midnight datetime, `sources-fresh` staleness comparisons depend on it being real.
59
+ - **Never list the bundle's own directory in `sources`.** A bundle directory changes on every doc edit inside it, so a self-referential `sources` entry goes permanently stale. This happened to the OKF pilot's own `BENCHMARK.md` (`agent-tasks` `docs/okf/BENCHMARK.md`, `sources: [docs/okf/]`); `benchmark-template.md` here omits `sources` entirely for the same reason, since a benchmark record measures the bundle rather than describing a piece of the codebase.
60
+ - **Keep all links same-directory relative.** Use `name.md`, not `/name.md`; see `no-absolute-links` above for why a leading slash breaks once the bundle is viewed outside its own repository.
61
+
62
+ ## Check catalog
63
+
64
+ | Rule | Severity | What it enforces |
65
+ |------|----------|-------------------|
66
+ | `frontmatter-required` | error | Every non-reserved `.md` file has a frontmatter block that parses as YAML and carries a non-empty string `type`. |
67
+ | `reserved-files-bare` | error | Reserved files (`index.md`, `log.md`, at any depth) must not carry a frontmatter block. |
68
+ | `links-resolve` | error | Markdown links to other `.md` files in the bundle must resolve to a real file. Relative targets resolve against the containing file's directory; targets starting with `/` resolve against the bundle root. A relative target that climbs out of the bundle directory (`../outside.md`) and still resolves on disk is accepted; the rule checks resolution, not containment. |
69
+ | `no-absolute-links` | warning | Link targets should not start with `/`. GitHub resolves a leading slash against the repository root, not the bundle root, so an absolute link 404s once the bundle is viewed outside its own repository. Use a same-directory relative link instead. |
70
+ | `sources-shape` | error | Frontmatter `sources`, when present, must be a non-empty array of non-empty strings. With a repo root (explicit or auto-detected), each listed path (file or directory) must also exist under it. |
71
+ | `sources-fresh` | warning / notice | For docs with a `sources` list and a repo root, flags a source path whose last git commit is newer than the doc's `timestamp`. See "Staleness (sources-fresh)" below. |
72
+
73
+ ## repo-root auto-detection
74
+
75
+ **Behavior change:** when `--repo-root` is omitted, okf-kit runs `git rev-parse --show-toplevel` from the bundle directory and uses the result if it succeeds. A bundle that lives inside a git work tree therefore gets `sources-shape` existence checks and `sources-fresh` staleness checks by default now, not just when you pass `--repo-root` explicitly.
76
+
77
+ If the bundle is not inside a git work tree (or `git` is unavailable), repo-root stays unset: `sources-shape` skips existence checks exactly as before, and `sources-fresh` emits a single notice (`staleness skipped: not inside a git work tree`) rather than silently reporting nothing, so a "clean" run is never a fake pass.
78
+
79
+ Pass `--repo-root` explicitly to pin a specific root (useful in CI when the bundle and the code it documents live in different checkouts) or to opt out of the ambient repo (point it at the bundle directory itself to disable both checks' access to the rest of the repo).
80
+
81
+ ## Staleness (sources-fresh)
82
+
83
+ `sources-fresh` compares each frontmatter `sources` entry's last git commit time against the doc's `timestamp`. It never blocks a doc that has no `sources`, and it never invents an error where git can't give a real answer:
84
+
85
+ | Situation | Severity | Message |
86
+ |-----------|----------|---------|
87
+ | A source path's last commit is newer than the doc's `timestamp` | warning | `STALE: <path> changed <iso> after doc timestamp <iso>` |
88
+ | A source path exists but has no git history (untracked) | notice | `untracked by git, staleness unknown: <path>` |
89
+ | The doc's `timestamp` is missing or not a parseable date, while `sources` is present | notice | `staleness not assessable: no valid timestamp` |
90
+ | No repo root available (see auto-detection above) | notice | `staleness skipped: not inside a git work tree` |
91
+ | A source path does not exist on disk | (nothing) | left to `sources-shape`, not duplicated here |
92
+
93
+ STALE findings are warnings, so they are advisory by default; run with `--strict` to fail the build on them.
94
+
95
+ Known limitation: a `git log` call that fails for a reason other than "no history for this path" (for example a corrupt object or a transient git error) is reported the same way as a genuinely untracked path, the `untracked by git, staleness unknown` notice; okf-kit does not currently distinguish a real git failure from "no commits touch this path".
96
+
97
+ **Authoring guidance:** when you re-verify a doc against its sources, bump its frontmatter `timestamp` (and add a line to the bundle's `log.md`) so `sources-fresh` reflects that the doc is current again.
98
+
99
+ ## Exit codes
100
+
101
+ | Code | Meaning |
102
+ |------|---------|
103
+ | 0 | No errors (and, under `--strict`, no warnings either). |
104
+ | 1 | At least one error (or, under `--strict`, at least one warning). |
105
+ | 2 | CLI invocation error: bundle directory does not exist, `init`'s target directory is non-empty without `--force`, or a commander usage error (unknown option, missing argument, missing/unknown command). `--help` and `--version` still exit 0. |
106
+
107
+ ## CI usage
108
+
109
+ This is advisory: don't fail the build on warnings unless you pass `--strict`. Use a normal (non-shallow) checkout of the repo that owns the bundle: repo-root detection runs `git rev-parse --show-toplevel` from the `path/to/bundle` argument itself, not from the shell's working directory, and `sources-fresh` reads `git log`, so a shallow clone reports paths as untracked.
110
+
111
+ ```yaml
112
+ - name: OKF bundle check
113
+ run: npx okf-kit@0.3.0 check path/to/bundle
114
+ ```
115
+
116
+ Pin the version: an unpinned `npx okf-kit` picks up new rules on their release day, which turns an unrelated PR red.
117
+
118
+ ## Where this fits
119
+
120
+ okf-kit is the producer-side check: it validates a bundle you are authoring or maintaining. Consuming an OKF bundle at query time (loading, indexing, ranking passages for an agent) lives in [codebase-oracle](https://github.com/LanNguyenSi/codebase-oracle), a separate tool.
121
+
122
+ ## License
123
+
124
+ MIT, see [LICENSE](../../LICENSE) at repo root.
@@ -0,0 +1,2 @@
1
+ import type { BundleContext, RunGit } from "./types.js";
2
+ export declare function loadBundle(bundleDir: string, repoRoot?: string, runGit?: RunGit): BundleContext;
package/dist/bundle.js ADDED
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ const RESERVED_BASENAMES = new Set(["index.md", "log.md"]);
5
+ export function loadBundle(bundleDir, repoRoot, runGit) {
6
+ const files = walkMarkdownFiles(bundleDir);
7
+ const docs = files.map((absPath) => {
8
+ const relPath = path.relative(bundleDir, absPath).split(path.sep).join("/");
9
+ const basename = path.basename(absPath);
10
+ const raw = fs.readFileSync(absPath, "utf8");
11
+ const { frontmatter, body } = parseFrontmatter(raw);
12
+ return {
13
+ relPath,
14
+ basename,
15
+ isReserved: RESERVED_BASENAMES.has(basename),
16
+ raw,
17
+ frontmatter,
18
+ body,
19
+ };
20
+ });
21
+ docs.sort((a, b) => a.relPath.localeCompare(b.relPath));
22
+ return { bundleDir, repoRoot, docs, runGit };
23
+ }
24
+ function walkMarkdownFiles(dir) {
25
+ const out = [];
26
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
27
+ const full = path.join(dir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ out.push(...walkMarkdownFiles(full));
30
+ }
31
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
32
+ out.push(full);
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+ /**
38
+ * A frontmatter block is the first line being exactly `---` up to the next
39
+ * line that is exactly `---`. Anything else (no opening delimiter, or an
40
+ * opening delimiter with no matching close) counts as no frontmatter block
41
+ * at all, per the OKF v0.1 shape rule.
42
+ */
43
+ function parseFrontmatter(raw) {
44
+ const lines = raw.split(/\r?\n/);
45
+ if (lines[0] !== "---") {
46
+ return { frontmatter: { present: false }, body: raw };
47
+ }
48
+ let closingIndex = -1;
49
+ for (let i = 1; i < lines.length; i++) {
50
+ if (lines[i] === "---") {
51
+ closingIndex = i;
52
+ break;
53
+ }
54
+ }
55
+ if (closingIndex === -1) {
56
+ return { frontmatter: { present: false }, body: raw };
57
+ }
58
+ const yamlText = lines.slice(1, closingIndex).join("\n");
59
+ const body = lines.slice(closingIndex + 1).join("\n");
60
+ try {
61
+ const parsed = YAML.parse(yamlText);
62
+ return { frontmatter: { present: true, parsed }, body };
63
+ }
64
+ catch (err) {
65
+ const parseError = err instanceof Error ? err.message : String(err);
66
+ return { frontmatter: { present: true, parseError }, body };
67
+ }
68
+ }
69
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { UsageError } from "./errors.js";
3
+ import type { Finding, RunGit } from "./types.js";
4
+ export { UsageError };
5
+ export interface CheckOptions {
6
+ repoRoot?: string;
7
+ strict?: boolean;
8
+ /** Test-only override for git access; production code shells out to the real `git` binary. */
9
+ runGit?: RunGit;
10
+ }
11
+ export interface CheckResult {
12
+ bundleDir: string;
13
+ findings: Finding[];
14
+ exitCode: number;
15
+ }
16
+ export declare function runCheck(bundleDir: string, options?: CheckOptions): CheckResult;
package/dist/cli.js ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { pathToFileURL } from "node:url";
6
+ import { Command, CommanderError } from "commander";
7
+ import { loadBundle } from "./bundle.js";
8
+ import { UsageError } from "./errors.js";
9
+ import { detectRepoRoot } from "./git.js";
10
+ import { formatInitSummary, runInit } from "./init.js";
11
+ import { allRules } from "./rules/index.js";
12
+ import { renderJson, renderText, summarize } from "./report.js";
13
+ export { UsageError };
14
+ export function runCheck(bundleDir, options = {}) {
15
+ if (!fs.existsSync(bundleDir) || !fs.statSync(bundleDir).isDirectory()) {
16
+ throw new UsageError(`Bundle directory does not exist: ${bundleDir}`);
17
+ }
18
+ const resolvedBundleDir = path.resolve(bundleDir);
19
+ // When --repo-root is omitted, try to find the enclosing git work tree so
20
+ // sources-shape's existence check and sources-fresh's staleness check are
21
+ // active by default for any bundle that lives inside a git repo. A bundle
22
+ // outside any git work tree falls back to the pre-detection behavior
23
+ // (existence check skipped; sources-fresh reports one skip notice).
24
+ const repoRoot = options.repoRoot
25
+ ? path.resolve(options.repoRoot)
26
+ : detectRepoRoot(resolvedBundleDir, options.runGit);
27
+ const ctx = loadBundle(resolvedBundleDir, repoRoot, options.runGit);
28
+ const findings = allRules.flatMap((rule) => rule.run(ctx));
29
+ const summary = summarize(findings);
30
+ const exitCode = summary.errors > 0 || (Boolean(options.strict) && summary.warnings > 0)
31
+ ? 1
32
+ : 0;
33
+ return { bundleDir: resolvedBundleDir, findings, exitCode };
34
+ }
35
+ const program = new Command();
36
+ // Route commander's own usage errors (unknown option, missing argument, no
37
+ // matching command, ...) through a thrown CommanderError instead of an
38
+ // implicit process.exit(1), so they can be told apart from "the bundle has
39
+ // findings" (also 1) below. Must be called before `.command()` so the
40
+ // subcommand inherits it via copyInheritedSettings; also set explicitly on
41
+ // the subcommand for clarity.
42
+ program.exitOverride();
43
+ program
44
+ .name("okf-kit")
45
+ .description("Validate OKF v0.1 knowledge bundles")
46
+ .version(readVersion());
47
+ program
48
+ .command("check <bundleDir>")
49
+ .description("Check a knowledge bundle for OKF structural violations")
50
+ .option("-r, --repo-root <path>", "Repo root to verify frontmatter `sources` paths exist under and assess staleness against " +
51
+ "(auto-detected via `git rev-parse --show-toplevel` from the bundle dir when omitted)")
52
+ .option("-j, --json", "Output findings as JSON")
53
+ .option("-s, --strict", "Also fail (exit 1) when warnings are present")
54
+ .exitOverride()
55
+ .action((bundleDir, opts) => {
56
+ try {
57
+ const result = runCheck(bundleDir, {
58
+ repoRoot: opts.repoRoot,
59
+ strict: opts.strict,
60
+ });
61
+ const output = opts.json
62
+ ? renderJson(result.bundleDir, result.findings)
63
+ : renderText(result.bundleDir, result.findings);
64
+ process.stdout.write(output);
65
+ process.exit(result.exitCode);
66
+ }
67
+ catch (err) {
68
+ if (err instanceof UsageError) {
69
+ process.stderr.write(`okf-kit: ${err.message}\n`);
70
+ process.exit(2);
71
+ }
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ process.stderr.write(`okf-kit: ${msg}\n`);
74
+ process.exit(2);
75
+ }
76
+ });
77
+ program
78
+ .command("init [dir]")
79
+ .description("Scaffold a starter OKF v0.1 knowledge bundle (default target: docs/okf)")
80
+ .option("-f, --force", "Overwrite the scaffold files in a non-empty target directory (other existing files are left alone)")
81
+ .exitOverride()
82
+ .action((dir, opts) => {
83
+ try {
84
+ const result = runInit(dir ?? "docs/okf", { force: opts.force });
85
+ process.stdout.write(formatInitSummary(result));
86
+ process.exit(0);
87
+ }
88
+ catch (err) {
89
+ if (err instanceof UsageError) {
90
+ process.stderr.write(`okf-kit: ${err.message}\n`);
91
+ process.exit(2);
92
+ }
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ process.stderr.write(`okf-kit: ${msg}\n`);
95
+ process.exit(2);
96
+ }
97
+ });
98
+ // Commander codes for an explicit `-h`/`--help` or `-V`/`--version` flag:
99
+ // keep commander's own exit code (0) for these. Note `commander.help` is
100
+ // deliberately NOT in this set: in this commander version that code only
101
+ // fires for the "subcommands exist, none given, no action handler" path
102
+ // (Command.prototype._parseCommand calling `this.help({ error: true })`),
103
+ // which is itself a usage error and must fall through to exit 2 below, not
104
+ // be passed through as 0 or 1.
105
+ const PASSTHROUGH_EXIT_CODES = new Set([
106
+ "commander.helpDisplayed",
107
+ "commander.version",
108
+ ]);
109
+ // Only run the CLI (and its process.exit calls) when this file is executed
110
+ // directly (`node dist/cli.js ...`), not when it is merely imported for its
111
+ // exported runCheck/UsageError (as tests do): otherwise importing this
112
+ // module would parse the importer's own process.argv and could exit the
113
+ // importing process.
114
+ const isMainModule = process.argv[1] !== undefined &&
115
+ import.meta.url === pathToFileURL(process.argv[1]).href;
116
+ if (isMainModule) {
117
+ program.parseAsync().catch((err) => {
118
+ if (err instanceof CommanderError) {
119
+ if (PASSTHROUGH_EXIT_CODES.has(err.code)) {
120
+ process.exit(err.exitCode);
121
+ }
122
+ // Commander already wrote its own error message to stderr before
123
+ // throwing; a usage error (unknown option, missing argument, missing
124
+ // command, ...) is exit 2, distinct from exit 1 (bundle has findings).
125
+ process.exit(2);
126
+ }
127
+ process.stderr.write(`okf-kit: ${err instanceof Error ? err.message : String(err)}\n`);
128
+ process.exit(2);
129
+ });
130
+ }
131
+ function readVersion() {
132
+ try {
133
+ const url = new URL("../package.json", import.meta.url);
134
+ const text = fs.readFileSync(url, "utf8");
135
+ const pkg = JSON.parse(text);
136
+ return pkg.version ?? "0.0.0";
137
+ }
138
+ catch {
139
+ return "0.0.0";
140
+ }
141
+ }
142
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** Thrown for CLI usage problems (bad arguments, bad target state); the CLI wiring maps this to exit code 2. */
2
+ export declare class UsageError extends Error {
3
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,4 @@
1
+ /** Thrown for CLI usage problems (bad arguments, bad target state); the CLI wiring maps this to exit code 2. */
2
+ export class UsageError extends Error {
3
+ }
4
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gHAAgH;AAChH,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG"}
package/dist/git.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { RunGit } from "./types.js";
2
+ /**
3
+ * Default RunGit implementation: shells out to the real `git` binary.
4
+ * stderr is discarded (git's own "fatal: not a git repository" etc. text is
5
+ * an expected, silent signal here, not something to surface), and any
6
+ * failure (non-zero exit, git missing) resolves to null instead of
7
+ * throwing.
8
+ */
9
+ export declare const runGit: RunGit;
10
+ /**
11
+ * Resolves the git work tree root containing `startDir`, or undefined when
12
+ * `startDir` is not inside a git work tree (or git is unavailable). Used to
13
+ * auto-fill --repo-root when the CLI flag is omitted.
14
+ */
15
+ export declare function detectRepoRoot(startDir: string, git?: RunGit): string | undefined;
package/dist/git.js ADDED
@@ -0,0 +1,30 @@
1
+ import { execFileSync } from "node:child_process";
2
+ /**
3
+ * Default RunGit implementation: shells out to the real `git` binary.
4
+ * stderr is discarded (git's own "fatal: not a git repository" etc. text is
5
+ * an expected, silent signal here, not something to surface), and any
6
+ * failure (non-zero exit, git missing) resolves to null instead of
7
+ * throwing.
8
+ */
9
+ export const runGit = (args, cwd) => {
10
+ try {
11
+ return execFileSync("git", args, {
12
+ cwd,
13
+ encoding: "utf8",
14
+ stdio: ["ignore", "pipe", "ignore"],
15
+ }).trim();
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ /**
22
+ * Resolves the git work tree root containing `startDir`, or undefined when
23
+ * `startDir` is not inside a git work tree (or git is unavailable). Used to
24
+ * auto-fill --repo-root when the CLI flag is omitted.
25
+ */
26
+ export function detectRepoRoot(startDir, git = runGit) {
27
+ const result = git(["rev-parse", "--show-toplevel"], startDir);
28
+ return result ? result : undefined;
29
+ }
30
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAW,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC1C,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;YAC/B,GAAG;YACH,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACpC,CAAC,CAAC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAgB,EAChB,MAAc,MAAM;IAEpB,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC/D,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACrC,CAAC"}
package/dist/init.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export interface InitOptions {
2
+ force?: boolean;
3
+ /** Test-only override for "now"; production code uses the real clock. */
4
+ now?: () => Date;
5
+ }
6
+ export interface InitResult {
7
+ targetDir: string;
8
+ filesWritten: string[];
9
+ }
10
+ export declare function runInit(targetDir: string, options?: InitOptions): InitResult;
11
+ export declare function formatInitSummary(result: InitResult): string;
package/dist/init.js ADDED
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { UsageError } from "./errors.js";
4
+ import { benchmarkTemplate, indexTemplate, invariantTemplate, logTemplate, moduleTemplate, overviewTemplate, runbookTemplate, } from "./templates.js";
5
+ /** The exact set of filenames `init` owns: with --force, only these are overwritten, nothing else in the directory is touched. */
6
+ const SCAFFOLD_FILENAMES = [
7
+ "index.md",
8
+ "log.md",
9
+ "overview-template.md",
10
+ "module-template.md",
11
+ "invariant-template.md",
12
+ "runbook-template.md",
13
+ "benchmark-template.md",
14
+ ];
15
+ export function runInit(targetDir, options = {}) {
16
+ const resolvedDir = path.resolve(targetDir);
17
+ const now = options.now ?? (() => new Date());
18
+ const timestamp = now().toISOString();
19
+ if (fs.existsSync(resolvedDir)) {
20
+ if (!fs.statSync(resolvedDir).isDirectory()) {
21
+ throw new UsageError(`Target path exists and is not a directory: ${resolvedDir}`);
22
+ }
23
+ const isNonEmpty = fs.readdirSync(resolvedDir).length > 0;
24
+ if (isNonEmpty && !options.force) {
25
+ throw new UsageError(`Target directory is not empty: ${resolvedDir}. Re-run with --force to overwrite the scaffold ` +
26
+ "files okf-kit owns (index.md, log.md, and the *-template.md docs); any other existing files " +
27
+ "are left alone.");
28
+ }
29
+ }
30
+ fs.mkdirSync(resolvedDir, { recursive: true });
31
+ const contentByFilename = {
32
+ "index.md": indexTemplate(),
33
+ "log.md": logTemplate(timestamp),
34
+ "overview-template.md": overviewTemplate(timestamp),
35
+ "module-template.md": moduleTemplate(timestamp),
36
+ "invariant-template.md": invariantTemplate(timestamp),
37
+ "runbook-template.md": runbookTemplate(timestamp),
38
+ "benchmark-template.md": benchmarkTemplate(timestamp),
39
+ };
40
+ const filesWritten = [];
41
+ for (const filename of SCAFFOLD_FILENAMES) {
42
+ fs.writeFileSync(path.join(resolvedDir, filename), contentByFilename[filename]);
43
+ filesWritten.push(filename);
44
+ }
45
+ return { targetDir: resolvedDir, filesWritten };
46
+ }
47
+ export function formatInitSummary(result) {
48
+ const lines = [
49
+ `Scaffolded OKF bundle at ${result.targetDir}:`,
50
+ ...result.filesWritten.map((f) => ` ${f}`),
51
+ "",
52
+ "Next steps:",
53
+ " - Fill in each *-template.md placeholder and rename it to a real doc name.",
54
+ " - Replace the `path/to/covered/source` placeholder in each doc's `sources:` list with the",
55
+ " real repo-root-relative path(s) it describes.",
56
+ " - Until you do, `okf-kit check` will report those placeholders as missing source paths",
57
+ " (`sources-shape` errors) whenever it has a repo root to check against (inside a git repo, or",
58
+ " with --repo-root); that is intentional, not a bug, it flags unwritten sources so you don't",
59
+ " forget them.",
60
+ ];
61
+ return lines.join("\n") + "\n";
62
+ }
63
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export interface LinkRef {
2
+ /** The link destination after normalization, including any `#anchor`. */
3
+ target: string;
4
+ /** `target` with an optional trailing `#anchor` stripped. */
5
+ pathPart: string;
6
+ }
7
+ /**
8
+ * Extracts markdown link targets from body text, restricted to the targets
9
+ * the OKF checks care about: relative or bundle-root-relative `.md` links.
10
+ * Fenced code blocks are stripped first so example markdown inside a code
11
+ * fence is never mistaken for a real link. http(s):// and mailto: links and
12
+ * any target not ending in `.md` (after stripping an optional `#anchor`)
13
+ * are excluded.
14
+ */
15
+ export declare function extractMarkdownLinks(body: string): LinkRef[];
package/dist/links.js ADDED
@@ -0,0 +1,82 @@
1
+ const LINK_PATTERN = /\[[^\]]*\]\(([^)]+)\)/g;
2
+ /**
3
+ * Extracts markdown link targets from body text, restricted to the targets
4
+ * the OKF checks care about: relative or bundle-root-relative `.md` links.
5
+ * Fenced code blocks are stripped first so example markdown inside a code
6
+ * fence is never mistaken for a real link. http(s):// and mailto: links and
7
+ * any target not ending in `.md` (after stripping an optional `#anchor`)
8
+ * are excluded.
9
+ */
10
+ export function extractMarkdownLinks(body) {
11
+ const stripped = stripFencedCode(body);
12
+ const refs = [];
13
+ LINK_PATTERN.lastIndex = 0;
14
+ let match;
15
+ while ((match = LINK_PATTERN.exec(stripped)) !== null) {
16
+ const target = normalizeDestination(match[1]);
17
+ if (target.startsWith("http://") ||
18
+ target.startsWith("https://") ||
19
+ target.startsWith("mailto:")) {
20
+ continue;
21
+ }
22
+ const pathPart = target.split("#")[0];
23
+ if (!pathPart.endsWith(".md"))
24
+ continue;
25
+ refs.push({ target, pathPart });
26
+ }
27
+ return refs;
28
+ }
29
+ /**
30
+ * Normalizes a raw CommonMark link destination capture (the text between the
31
+ * link's parens) down to a bare path:
32
+ * - `<x.md>` form: unwrap the angle brackets (the destination can contain
33
+ * whitespace inside them; anything after the closing `>` is a title).
34
+ * - bare form: a link title, if present, is separated from the destination
35
+ * by whitespace and written as "t", 't', or (t); take the first
36
+ * whitespace-delimited token as the destination and drop the rest.
37
+ * - percent-decode the result (falling back to the raw string if it is not
38
+ * valid percent-encoding) so an encoded path compares equal to the real
39
+ * filename on disk.
40
+ */
41
+ function normalizeDestination(raw) {
42
+ let dest = raw.trim();
43
+ if (dest.startsWith("<")) {
44
+ const closeIdx = dest.indexOf(">", 1);
45
+ dest = closeIdx === -1 ? dest.slice(1) : dest.slice(1, closeIdx);
46
+ }
47
+ else {
48
+ const spaceIdx = dest.search(/\s/);
49
+ if (spaceIdx !== -1)
50
+ dest = dest.slice(0, spaceIdx);
51
+ }
52
+ try {
53
+ return decodeURIComponent(dest);
54
+ }
55
+ catch {
56
+ return dest;
57
+ }
58
+ }
59
+ function stripFencedCode(text) {
60
+ const lines = text.split(/\r?\n/);
61
+ const out = [];
62
+ let fenceMarker;
63
+ for (const line of lines) {
64
+ const trimmed = line.trim();
65
+ if (!fenceMarker &&
66
+ (trimmed.startsWith("```") || trimmed.startsWith("~~~"))) {
67
+ fenceMarker = trimmed.slice(0, 3);
68
+ out.push("");
69
+ continue;
70
+ }
71
+ if (fenceMarker) {
72
+ if (trimmed.startsWith(fenceMarker)) {
73
+ fenceMarker = undefined;
74
+ }
75
+ out.push("");
76
+ continue;
77
+ }
78
+ out.push(line);
79
+ }
80
+ return out.join("\n");
81
+ }
82
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { Finding } from "./types.js";
2
+ export interface Summary {
3
+ errors: number;
4
+ warnings: number;
5
+ notices: number;
6
+ }
7
+ export declare function summarize(findings: Finding[]): Summary;
8
+ export declare function renderText(bundleDir: string, findings: Finding[]): string;
9
+ export declare function renderJson(bundleDir: string, findings: Finding[]): string;