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.
- package/README.md +124 -0
- package/dist/bundle.d.ts +2 -0
- package/dist/bundle.js +69 -0
- package/dist/bundle.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +142 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/git.d.ts +15 -0
- package/dist/git.js +30 -0
- package/dist/git.js.map +1 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +63 -0
- package/dist/init.js.map +1 -0
- package/dist/links.d.ts +15 -0
- package/dist/links.js +82 -0
- package/dist/links.js.map +1 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.js +51 -0
- package/dist/report.js.map +1 -0
- package/dist/rules/frontmatter-required.d.ts +2 -0
- package/dist/rules/frontmatter-required.js +46 -0
- package/dist/rules/frontmatter-required.js.map +1 -0
- package/dist/rules/index.d.ts +9 -0
- package/dist/rules/index.js +16 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/links-resolve.d.ts +2 -0
- package/dist/rules/links-resolve.js +39 -0
- package/dist/rules/links-resolve.js.map +1 -0
- package/dist/rules/no-absolute-links.d.ts +2 -0
- package/dist/rules/no-absolute-links.js +25 -0
- package/dist/rules/no-absolute-links.js.map +1 -0
- package/dist/rules/reserved-files-bare.d.ts +2 -0
- package/dist/rules/reserved-files-bare.js +22 -0
- package/dist/rules/reserved-files-bare.js.map +1 -0
- package/dist/rules/sources-fresh.d.ts +2 -0
- package/dist/rules/sources-fresh.js +99 -0
- package/dist/rules/sources-fresh.js.map +1 -0
- package/dist/rules/sources-shape.d.ts +2 -0
- package/dist/rules/sources-shape.js +41 -0
- package/dist/rules/sources-shape.js.map +1 -0
- package/dist/templates.d.ts +14 -0
- package/dist/templates.js +293 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +23 -0
- package/dist/util.js +50 -0
- package/dist/util.js.map +1 -0
- 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.
|
package/dist/bundle.d.ts
ADDED
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
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -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
|
package/dist/git.js.map
ADDED
|
@@ -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
|
package/dist/init.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|
package/dist/links.d.ts
ADDED
|
@@ -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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"}
|
package/dist/report.d.ts
ADDED
|
@@ -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;
|