docsgov 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -24
- package/dist/apispec/apispec.js +1 -1
- package/dist/apispec/apispec.test.js +2 -2
- package/dist/check/run.js +1 -1
- package/dist/check/run.test.js +6 -6
- package/dist/check/suggest.js +1 -1
- package/dist/check/tokens.js +1 -1
- package/dist/cmd/main.js +17 -17
- package/dist/cmd/main.test.js +13 -13
- package/dist/codeq/errors.js +2 -2
- package/dist/codeq/index.js +1 -1
- package/dist/codeq/resolver.test.js +1 -1
- package/dist/config/config.js +2 -2
- package/dist/config/config.test.js +5 -5
- package/dist/config/fs.js +1 -1
- package/dist/config/glob.js +1 -1
- package/dist/config/glob.test.js +1 -1
- package/dist/dedup/configload.js +3 -3
- package/dist/dedup/configload.test.js +5 -5
- package/dist/dedup/dedup.index.test.js +9 -9
- package/dist/dedup/dedup.js +5 -5
- package/dist/dedup/dedup.test.js +4 -4
- package/dist/dedup/dedupcfg/config.js +2 -2
- package/dist/dedup/embedder/cache.js +4 -4
- package/dist/dedup/embedder/cache.test.js +13 -13
- package/dist/dedup/embedder/embedder.js +2 -2
- package/dist/dedup/embedder/embedder.mock.test.js +1 -1
- package/dist/dedup/embedder/embedder.test.js +4 -4
- package/dist/dedup/embedder/session.test.js +1 -1
- package/dist/dedup/gitignore.js +5 -5
- package/dist/dedup/gitignore.test.js +2 -2
- package/dist/dedup/indexdb/indexdb.js +2 -2
- package/dist/repo/exists.test.js +7 -7
- package/dist/repo/index.js +1 -1
- package/dist/repo/overlay.test.js +6 -6
- package/dist/repo/repo.js +10 -10
- package/dist/repo/repo.test.js +35 -35
- package/dist/repo/testutil.js +1 -1
- package/dist/repo/write.test.js +12 -12
- package/dist/violation/types.js +1 -1
- package/package.json +2 -2
|
@@ -62,7 +62,7 @@ function writeFiles(repoRoot, files) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
function dbPathOf(repoRoot) {
|
|
65
|
-
return join(repoRoot, ".
|
|
65
|
+
return join(repoRoot, ".docsgov", "dedup", "index.db");
|
|
66
66
|
}
|
|
67
67
|
const noProgress = () => { };
|
|
68
68
|
beforeEach(() => {
|
|
@@ -71,16 +71,16 @@ beforeEach(() => {
|
|
|
71
71
|
// Index() calls Embedder.newEmbedder() with no explicit cacheDir, which would
|
|
72
72
|
// resolve to the host ~/.cache. Point it at a throwaway temp dir so the test
|
|
73
73
|
// never writes to the real home cache.
|
|
74
|
-
savedModelCache = process.env.
|
|
74
|
+
savedModelCache = process.env.docsgov_MODEL_CACHE;
|
|
75
75
|
const modelCache = mkdtempSync(join(tmpdir(), "dedup-index-model-"));
|
|
76
76
|
tmpDirs.push(modelCache);
|
|
77
|
-
process.env.
|
|
77
|
+
process.env.docsgov_MODEL_CACHE = modelCache;
|
|
78
78
|
});
|
|
79
79
|
afterEach(() => {
|
|
80
80
|
if (savedModelCache === undefined)
|
|
81
|
-
delete process.env.
|
|
81
|
+
delete process.env.docsgov_MODEL_CACHE;
|
|
82
82
|
else
|
|
83
|
-
process.env.
|
|
83
|
+
process.env.docsgov_MODEL_CACHE = savedModelCache;
|
|
84
84
|
for (const d of tmpDirs.splice(0))
|
|
85
85
|
rmSync(d, { recursive: true, force: true });
|
|
86
86
|
});
|
|
@@ -140,7 +140,7 @@ describe("dedup.Index error wiring", () => {
|
|
|
140
140
|
const repoRoot = newRepoRoot();
|
|
141
141
|
writeFiles(repoRoot, {
|
|
142
142
|
// thresh_high out of (0,1) range → validate() throws inside Load.
|
|
143
|
-
".
|
|
143
|
+
".docsgov/dedup/config.yml": "analyzer:\n thresh_high: 5\n",
|
|
144
144
|
"docs/a.md": "## A\n\nThis is a sufficiently long paragraph of prose used purely to make the section eligible.\n",
|
|
145
145
|
});
|
|
146
146
|
await expect(Index(repoRoot, noProgress)).rejects.toThrow(/dedup\.Index: load config/);
|
|
@@ -162,7 +162,7 @@ describe("dedup.Index error wiring", () => {
|
|
|
162
162
|
it("wraps an open-db failure on a corrupt index file", async () => {
|
|
163
163
|
const repoRoot = newRepoRoot();
|
|
164
164
|
writeFiles(repoRoot, {
|
|
165
|
-
".
|
|
165
|
+
".docsgov/dedup/index.db": "this is not a sqlite database",
|
|
166
166
|
"docs/a.md": "## A\n\nThis is a sufficiently long paragraph of prose used purely to make the section eligible.\n",
|
|
167
167
|
});
|
|
168
168
|
await expect(Index(repoRoot, noProgress)).rejects.toThrow(/dedup\.Index: open db/);
|
|
@@ -186,7 +186,7 @@ describe("dedup.Analyze error wiring", () => {
|
|
|
186
186
|
});
|
|
187
187
|
await Index(repoRoot, noProgress);
|
|
188
188
|
writeFiles(repoRoot, {
|
|
189
|
-
".
|
|
189
|
+
".docsgov/dedup/config.yml": "analyzer:\n thresh_high: 5\n",
|
|
190
190
|
});
|
|
191
191
|
await expect(Analyze(repoRoot)).rejects.toThrow(/dedup\.Analyze: load config/);
|
|
192
192
|
});
|
|
@@ -196,7 +196,7 @@ describe("dedup.Analyze error wiring", () => {
|
|
|
196
196
|
it("wraps an open-db failure on a corrupt index file", async () => {
|
|
197
197
|
const repoRoot = newRepoRoot();
|
|
198
198
|
writeFiles(repoRoot, {
|
|
199
|
-
".
|
|
199
|
+
".docsgov/dedup/index.db": "this is not a sqlite database",
|
|
200
200
|
});
|
|
201
201
|
await expect(Analyze(repoRoot)).rejects.toThrow(/dedup\.Analyze: open db/);
|
|
202
202
|
});
|
package/dist/dedup/dedup.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Top-level facade for the
|
|
1
|
+
// Top-level facade for the docsgov dedup subsystem: the Index and Analyze entry
|
|
2
2
|
// points the CLI calls.
|
|
3
3
|
//
|
|
4
4
|
// Ported from internal/dedup/dedup.go. Index walks <repoRoot>/docs/, extracts
|
|
5
5
|
// eligible sections, embeds new/changed ones, and persists the index to
|
|
6
|
-
// .
|
|
6
|
+
// .docsgov/dedup/index.db. Analyze loads that index and runs the layered
|
|
7
7
|
// duplicate-detection algorithm, returning a structured Report.
|
|
8
8
|
//
|
|
9
9
|
// Reconciling the mixed sync/async stack vs Go:
|
|
@@ -24,11 +24,11 @@ import { Load } from "./configload.js";
|
|
|
24
24
|
import { ensureDedupGitignore } from "./gitignore.js";
|
|
25
25
|
/** dbPathOf returns the index DB path under repoRoot (Go inlines this twice). */
|
|
26
26
|
function dbPathOf(repoRoot) {
|
|
27
|
-
return path.join(repoRoot, ".
|
|
27
|
+
return path.join(repoRoot, ".docsgov", "dedup", "index.db");
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Index walks <repoRoot>/docs/, extracts eligible sections, embeds new and
|
|
31
|
-
* changed ones, and persists the index to .
|
|
31
|
+
* changed ones, and persists the index to .docsgov/dedup/index.db.
|
|
32
32
|
*
|
|
33
33
|
* progress receives one-line status messages; pass a no-op for silence (Go's
|
|
34
34
|
* io.Discard) or one that writes to stderr from the CLI.
|
|
@@ -92,7 +92,7 @@ export async function Index(repoRoot, progress) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* Analyze loads the index from .
|
|
95
|
+
* Analyze loads the index from .docsgov/dedup/index.db, runs the layered
|
|
96
96
|
* duplicate-detection algorithm, and returns a structured Report.
|
|
97
97
|
*
|
|
98
98
|
* Throws ErrIndexMissing (wrapped) when the index DB does not exist.
|
package/dist/dedup/dedup.test.js
CHANGED
|
@@ -76,7 +76,7 @@ function writeFiles(repoRoot, files) {
|
|
|
76
76
|
}
|
|
77
77
|
/** dbPathOf mirrors the facade's index DB location. */
|
|
78
78
|
function dbPathOf(repoRoot) {
|
|
79
|
-
return join(repoRoot, ".
|
|
79
|
+
return join(repoRoot, ".docsgov", "dedup", "index.db");
|
|
80
80
|
}
|
|
81
81
|
/** createEmptyIndexDB opens (and immediately closes) a valid empty index DB. */
|
|
82
82
|
function createEmptyIndexDB(repoRoot) {
|
|
@@ -90,7 +90,7 @@ describe("dedup.Analyze", () => {
|
|
|
90
90
|
// WHY: a missing index must be a distinct, matchable error so the CLI can
|
|
91
91
|
// prompt the user to index first rather than reporting a generic failure.
|
|
92
92
|
it("throws ErrIndexMissing when the index DB does not exist", async () => {
|
|
93
|
-
const repoRoot = newRepoRoot(); // empty, no .
|
|
93
|
+
const repoRoot = newRepoRoot(); // empty, no .docsgov/dedup/index.db
|
|
94
94
|
await expect(Analyze(repoRoot)).rejects.toBeInstanceOf(ErrIndexMissing);
|
|
95
95
|
});
|
|
96
96
|
// WHY: an existing but empty index is a valid "no duplicates" state — Analyze
|
|
@@ -189,9 +189,9 @@ describe("dedup facade end-to-end (fake embedder)", () => {
|
|
|
189
189
|
});
|
|
190
190
|
describe("dedup.Index (real embedder)", () => {
|
|
191
191
|
// WHY: the Index path itself (real embedder, real walk, real persist) is only
|
|
192
|
-
// meaningful with the model. Gated behind
|
|
192
|
+
// meaningful with the model. Gated behind docsgov_E2E so CI never downloads
|
|
193
193
|
// ~1GB — the single allowed env-skip per the porting conventions.
|
|
194
|
-
const runE2E = process.env["
|
|
194
|
+
const runE2E = process.env["docsgov_E2E"] === "1";
|
|
195
195
|
it.skipIf(!runE2E)("indexes a corpus and persists sections", async () => {
|
|
196
196
|
const repoRoot = newRepoRoot();
|
|
197
197
|
writeFiles(repoRoot, {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// indexer can import it without creating an import cycle with the top-level
|
|
4
4
|
// dedup facade.
|
|
5
5
|
//
|
|
6
|
-
// The YAML overlay (Default() merged with .
|
|
6
|
+
// The YAML overlay (Default() merged with .docsgov/dedup/config.yml) lives in
|
|
7
7
|
// the parent dedup package; this leaf only declares the shapes and defaults.
|
|
8
8
|
// Because that overlay deserializes raw YAML keys onto these shapes, every
|
|
9
9
|
// property name here keeps its EXACT YAML serialized spelling (snake_case),
|
|
@@ -44,7 +44,7 @@ export function defaultConfig() {
|
|
|
44
44
|
"build",
|
|
45
45
|
".next",
|
|
46
46
|
".cache",
|
|
47
|
-
".
|
|
47
|
+
".docsgov",
|
|
48
48
|
"dedup-poc",
|
|
49
49
|
".venv",
|
|
50
50
|
],
|
|
@@ -4,8 +4,8 @@ import { Model } from "./constants.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* cacheDir returns the resolved model cache directory in precedence order:
|
|
6
6
|
* 1. explicit (the WithCacheDir option equivalent)
|
|
7
|
-
* 2.
|
|
8
|
-
* 3. ~/.cache/
|
|
7
|
+
* 2. docsgov_MODEL_CACHE env var
|
|
8
|
+
* 3. ~/.cache/docsgov/models/<sanitized-model-name>/
|
|
9
9
|
*
|
|
10
10
|
* The returned path is not guaranteed to exist; the caller creates it.
|
|
11
11
|
*/
|
|
@@ -13,11 +13,11 @@ export function cacheDir(explicit) {
|
|
|
13
13
|
if (explicit !== "") {
|
|
14
14
|
return explicit;
|
|
15
15
|
}
|
|
16
|
-
const env = process.env.
|
|
16
|
+
const env = process.env.docsgov_MODEL_CACHE;
|
|
17
17
|
if (env !== undefined && env !== "") {
|
|
18
18
|
return env;
|
|
19
19
|
}
|
|
20
20
|
// Sanitize the model name: replace / with _ for filesystem safety.
|
|
21
21
|
const sanitized = Model.replaceAll("/", "_");
|
|
22
|
-
return join(homedir(), ".cache", "
|
|
22
|
+
return join(homedir(), ".cache", "docsgov", "models", sanitized);
|
|
23
23
|
}
|
|
@@ -3,40 +3,40 @@ import { join } from "node:path";
|
|
|
3
3
|
import { afterEach, beforeEach, expect, test } from "vitest";
|
|
4
4
|
import { cacheDir } from "./cache.js";
|
|
5
5
|
import { Model } from "./constants.js";
|
|
6
|
-
// Snapshot/restore
|
|
6
|
+
// Snapshot/restore docsgov_MODEL_CACHE so tests don't leak into each other or
|
|
7
7
|
// the host env.
|
|
8
8
|
let savedEnv;
|
|
9
9
|
beforeEach(() => {
|
|
10
|
-
savedEnv = process.env.
|
|
11
|
-
delete process.env.
|
|
10
|
+
savedEnv = process.env.docsgov_MODEL_CACHE;
|
|
11
|
+
delete process.env.docsgov_MODEL_CACHE;
|
|
12
12
|
});
|
|
13
13
|
afterEach(() => {
|
|
14
14
|
if (savedEnv === undefined) {
|
|
15
|
-
delete process.env.
|
|
15
|
+
delete process.env.docsgov_MODEL_CACHE;
|
|
16
16
|
}
|
|
17
17
|
else {
|
|
18
|
-
process.env.
|
|
18
|
+
process.env.docsgov_MODEL_CACHE = savedEnv;
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
// WHY: tests pass an explicit cache dir (a temp dir) so CI never races on the
|
|
22
22
|
// shared host cache. The explicit option MUST win over both the env var and
|
|
23
23
|
// the default, or that isolation guarantee breaks.
|
|
24
24
|
test("explicit cache dir wins over env var and default", () => {
|
|
25
|
-
process.env.
|
|
25
|
+
process.env.docsgov_MODEL_CACHE = "/from/env";
|
|
26
26
|
expect(cacheDir("/explicit/dir")).toBe("/explicit/dir");
|
|
27
27
|
});
|
|
28
|
-
// WHY:
|
|
28
|
+
// WHY: docsgov_MODEL_CACHE lets operators relocate the multi-hundred-MB model
|
|
29
29
|
// off the home partition. It must be honored when no explicit dir is given.
|
|
30
|
-
test("
|
|
31
|
-
process.env.
|
|
30
|
+
test("docsgov_MODEL_CACHE is used when no explicit dir is given", () => {
|
|
31
|
+
process.env.docsgov_MODEL_CACHE = "/from/env";
|
|
32
32
|
expect(cacheDir("")).toBe("/from/env");
|
|
33
33
|
});
|
|
34
34
|
// WHY: an empty env var must NOT shadow the default (Go checks `env != ""`).
|
|
35
|
-
// A stray `
|
|
35
|
+
// A stray `docsgov_MODEL_CACHE=` in a shell would otherwise route the cache to
|
|
36
36
|
// the empty path.
|
|
37
|
-
test("empty
|
|
38
|
-
process.env.
|
|
39
|
-
const want = join(homedir(), ".cache", "
|
|
37
|
+
test("empty docsgov_MODEL_CACHE falls through to the default", () => {
|
|
38
|
+
process.env.docsgov_MODEL_CACHE = "";
|
|
39
|
+
const want = join(homedir(), ".cache", "docsgov", "models", Model.replaceAll("/", "_"));
|
|
40
40
|
expect(cacheDir("")).toBe(want);
|
|
41
41
|
});
|
|
42
42
|
// WHY: the default path sanitizes the model name by replacing "/" with "_".
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// sentence-transformers/paraphrase-multilingual-mpnet-base-v2 model.
|
|
4
4
|
//
|
|
5
5
|
// The embedder model is auto-downloaded on first use to a configurable cache
|
|
6
|
-
// directory (see NewOptions.cacheDir, the
|
|
7
|
-
// default ~/.cache/
|
|
6
|
+
// directory (see NewOptions.cacheDir, the docsgov_MODEL_CACHE env var, or the
|
|
7
|
+
// default ~/.cache/docsgov/models/<sanitized-model-name>/).
|
|
8
8
|
import { cacheDir } from "./cache.js";
|
|
9
9
|
import { Dimension, Model } from "./constants.js";
|
|
10
10
|
import { InferenceError } from "./errors.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Behavior tests for Embedder, the public facade over Session, with the model
|
|
2
2
|
// mocked so no ~1GB download happens. The real-model parity/golden gate stays
|
|
3
|
-
// in embedder.test.ts (
|
|
3
|
+
// in embedder.test.ts (docsgov_EMBED_E2E) and is intentionally untouched.
|
|
4
4
|
//
|
|
5
5
|
// vi.mock is hoisted above the imports and intercepts the dynamic
|
|
6
6
|
// `await import("@huggingface/transformers")` inside session.ts, so
|
|
@@ -26,15 +26,15 @@ function cosine(a, b) {
|
|
|
26
26
|
dot += (a[i] ?? 0) * (b[i] ?? 0);
|
|
27
27
|
return dot;
|
|
28
28
|
}
|
|
29
|
-
// E2E inference is gated by
|
|
29
|
+
// E2E inference is gated by docsgov_EMBED_E2E because it downloads the model
|
|
30
30
|
// (~1GB). Bit-parity vs Go is already proven (maxAbsDiff 3.6e-8 in the spike),
|
|
31
31
|
// so default CI runs only the fast unit suites above and below.
|
|
32
|
-
const e2e = process.env.
|
|
33
|
-
e2e("real inference (
|
|
32
|
+
const e2e = process.env.docsgov_EMBED_E2E ? describe : describe.skip;
|
|
33
|
+
e2e("real inference (docsgov_EMBED_E2E)", () => {
|
|
34
34
|
let emb;
|
|
35
35
|
let dir;
|
|
36
36
|
beforeEach(async () => {
|
|
37
|
-
dir = join(tmpdir(), `
|
|
37
|
+
dir = join(tmpdir(), `docsgov-embed-${process.pid}-${Date.now()}`);
|
|
38
38
|
emb = await Embedder.newEmbedder({ cacheDir: dir });
|
|
39
39
|
});
|
|
40
40
|
afterEach(async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Behavior tests for Session, the transformers.js pipeline wrapper, with the
|
|
2
2
|
// model mocked so no ~1GB download happens. The real-model parity gate lives in
|
|
3
|
-
// embedder.test.ts (
|
|
3
|
+
// embedder.test.ts (docsgov_EMBED_E2E) and is intentionally untouched.
|
|
4
4
|
//
|
|
5
5
|
// vi.mock is hoisted above the imports and intercepts BOTH the static and the
|
|
6
6
|
// dynamic `await import("@huggingface/transformers")` that session.ts performs,
|
package/dist/dedup/gitignore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Manages the
|
|
1
|
+
// Manages the docsgov-owned block in .docsgov/dedup/.gitignore that keeps the
|
|
2
2
|
// local SQLite index cache out of git.
|
|
3
3
|
//
|
|
4
4
|
// Ported from internal/dedup/gitignore.go. Go's os.ReadFile/WriteFile become
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// are preserved verbatim.
|
|
10
10
|
import { readFile, writeFile } from "node:fs/promises";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
/** dedupGitignoreHeader labels the block
|
|
13
|
-
export const dedupGitignoreHeader = "#
|
|
12
|
+
/** dedupGitignoreHeader labels the block docsgov manages in .docsgov/dedup/.gitignore. */
|
|
13
|
+
export const dedupGitignoreHeader = "# docsgov dedup index cache — local, rebuilt by `docsgov dedup`.";
|
|
14
14
|
/**
|
|
15
15
|
* dedupGitignoreRules are the patterns that keep the local SQLite index cache out
|
|
16
16
|
* of git: the DB plus its WAL-mode sidecars. config.yml and the .gitignore itself
|
|
@@ -24,7 +24,7 @@ function isNotExist(err) {
|
|
|
24
24
|
err.code === "ENOENT");
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* ensureDedupGitignore makes sure .
|
|
27
|
+
* ensureDedupGitignore makes sure .docsgov/dedup/.gitignore excludes the index
|
|
28
28
|
* cache. It is a no-op when every rule is already present (matched as an exact,
|
|
29
29
|
* trimmed line — comments and blanks ignored); otherwise it creates the file or
|
|
30
30
|
* appends only the missing rules, leaving any existing content intact so a user's
|
|
@@ -75,7 +75,7 @@ export function gitignoreLineSet(body) {
|
|
|
75
75
|
return set;
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* appendGitignoreBlock returns existing followed by the
|
|
78
|
+
* appendGitignoreBlock returns existing followed by the docsgov header and the
|
|
79
79
|
* missing rules. When existing is empty the block stands alone; otherwise it is
|
|
80
80
|
* separated from prior content by a blank line, and a trailing newline is added
|
|
81
81
|
* first if needed so the header never glues onto a user's last line.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Behavior-encoding tests for the
|
|
2
|
+
* Behavior-encoding tests for the docsgov-owned .docsgov/dedup/.gitignore block,
|
|
3
3
|
* ported from internal/dedup/gitignore_test.go.
|
|
4
4
|
*
|
|
5
5
|
* WHY each case matters: this file decides what stays out of git. The exact
|
|
@@ -49,7 +49,7 @@ describe("ensureDedupGitignore", () => {
|
|
|
49
49
|
const dir = newDir();
|
|
50
50
|
await ensureDedupGitignore(dir);
|
|
51
51
|
const got = readGitignore(dir);
|
|
52
|
-
const want = "#
|
|
52
|
+
const want = "# docsgov dedup index cache — local, rebuilt by `docsgov dedup`.\n" +
|
|
53
53
|
"index.db\nindex.db-wal\nindex.db-shm\n";
|
|
54
54
|
expect(got).toBe(want);
|
|
55
55
|
const set = gitignoreLineSet(got);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manages the dedup section index stored in a SQLite database.
|
|
3
3
|
*
|
|
4
|
-
* The DB lives at <repo_root>/.
|
|
4
|
+
* The DB lives at <repo_root>/.docsgov/dedup/index.db. Callers resolve the repo
|
|
5
5
|
* root and pass the absolute DB path to open — this package never walks for
|
|
6
|
-
* .
|
|
6
|
+
* .docsgov or touches the sentinel constant (that stays in internal/repo).
|
|
7
7
|
*
|
|
8
8
|
* Ported from internal/dedup/indexdb. node:sqlite is synchronous, so Go's
|
|
9
9
|
* context.Context / database/sql async plumbing is dropped: open/close/queries
|
package/dist/repo/exists.test.js
CHANGED
|
@@ -8,7 +8,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
8
8
|
// WHY: the docs guard calls exists to verify a referenced file is present.
|
|
9
9
|
// A false negative would flag a valid reference as a violation.
|
|
10
10
|
test("returns true for a file present at the exact-case path", async () => {
|
|
11
|
-
const root = await makeDir(".
|
|
11
|
+
const root = await makeDir(".docsgov", "docs/adr");
|
|
12
12
|
await nodefs.writeFile(path.join(root, "docs", "adr", "0001-foo.md"), "hello");
|
|
13
13
|
const r = await find(root);
|
|
14
14
|
expect(await r.exists("docs/adr/0001-foo.md")).toBe(true);
|
|
@@ -16,7 +16,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
16
16
|
// WHY: a reference to a non-existent file is the most common docs guard
|
|
17
17
|
// violation; exists must detect absence.
|
|
18
18
|
test("returns false for a missing file", async () => {
|
|
19
|
-
const root = await makeDir(".
|
|
19
|
+
const root = await makeDir(".docsgov", "docs/adr");
|
|
20
20
|
const r = await find(root);
|
|
21
21
|
expect(await r.exists("docs/adr/nonexistent.md")).toBe(false);
|
|
22
22
|
});
|
|
@@ -24,7 +24,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
24
24
|
// pass case-mismatched references. The segment-by-segment walk matches each
|
|
25
25
|
// entry by exact case so reports are byte-identical across platforms.
|
|
26
26
|
test("rejects a wrong-case filename segment", async () => {
|
|
27
|
-
const root = await makeDir(".
|
|
27
|
+
const root = await makeDir(".docsgov", "docs/adr");
|
|
28
28
|
await nodefs.writeFile(path.join(root, "docs", "adr", "0001-foo.md"), "hello");
|
|
29
29
|
const r = await find(root);
|
|
30
30
|
expect(await r.exists("docs/adr/0001-FOO.md")).toBe(false);
|
|
@@ -32,7 +32,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
32
32
|
// WHY: the exact-case rule must hold at EVERY segment, not just the filename;
|
|
33
33
|
// a wrong-case directory prefix must fail too.
|
|
34
34
|
test("rejects a wrong-case directory segment", async () => {
|
|
35
|
-
const root = await makeDir(".
|
|
35
|
+
const root = await makeDir(".docsgov", "docs/adr");
|
|
36
36
|
await nodefs.writeFile(path.join(root, "docs", "adr", "0001-foo.md"), "hello");
|
|
37
37
|
const r = await find(root);
|
|
38
38
|
expect(await r.exists("Docs/adr/0001-foo.md")).toBe(false);
|
|
@@ -40,7 +40,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
40
40
|
// WHY: the docs guard checks FILE references; a directory path is not a valid
|
|
41
41
|
// file reference and must not pass.
|
|
42
42
|
test("returns false for a directory path", async () => {
|
|
43
|
-
const root = await makeDir(".
|
|
43
|
+
const root = await makeDir(".docsgov", "docs/adr");
|
|
44
44
|
const r = await find(root);
|
|
45
45
|
expect(await r.exists("docs/adr")).toBe(false);
|
|
46
46
|
});
|
|
@@ -48,7 +48,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
48
48
|
// would abort on every reference to a file that doesn't exist yet, which is
|
|
49
49
|
// the guard's core purpose.
|
|
50
50
|
test("treats a missing path as false without throwing", async () => {
|
|
51
|
-
const root = await makeDir(".
|
|
51
|
+
const root = await makeDir(".docsgov");
|
|
52
52
|
const r = await find(root);
|
|
53
53
|
await expect(r.exists("does/not/exist.md")).resolves.toBe(false);
|
|
54
54
|
});
|
|
@@ -58,7 +58,7 @@ describe("exists: exact-case file presence for the docs guard", () => {
|
|
|
58
58
|
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
59
59
|
const ioErrorTest = isRoot || os.platform() === "win32" ? test.skip : test;
|
|
60
60
|
ioErrorTest("propagates a real I/O error rather than swallowing it as absent", async () => {
|
|
61
|
-
const root = await makeDir(".
|
|
61
|
+
const root = await makeDir(".docsgov", "docs");
|
|
62
62
|
const r = await find(root);
|
|
63
63
|
const docsPath = path.join(root, "docs");
|
|
64
64
|
await nodefs.chmod(docsPath, 0o000);
|
package/dist/repo/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Public surface of the repo package — the OS boundary for
|
|
1
|
+
// Public surface of the repo package — the OS boundary for docsgov.
|
|
2
2
|
export { Repo, PendingWrite, find } from "./repo.js";
|
|
3
3
|
export { RealFS, isNotExist } from "./fs.js";
|
|
4
4
|
export { OverlayFS } from "./overlay.js";
|
|
@@ -12,7 +12,7 @@ describe("withOverlay: single-path in-memory intercept", () => {
|
|
|
12
12
|
// Core contract: the check sees the flipped bytes even though the real file
|
|
13
13
|
// on disk still holds the original.
|
|
14
14
|
test("overlaid path returns the injected content, not the on-disk bytes", async () => {
|
|
15
|
-
const root = await makeDir(".
|
|
15
|
+
const root = await makeDir(".docsgov", "docs");
|
|
16
16
|
const r = await find(root);
|
|
17
17
|
await r.writeFile("docs/a.md", enc.encode("---\nstatus: planning\n---\n"));
|
|
18
18
|
const injected = enc.encode("---\nstatus: implemented\n---\n");
|
|
@@ -22,7 +22,7 @@ describe("withOverlay: single-path in-memory intercept", () => {
|
|
|
22
22
|
// The resolver reads source files (non-overlaid); if the overlay intercepted
|
|
23
23
|
// those, the resolver would see wrong data.
|
|
24
24
|
test("non-overlaid path delegates to the base on-disk content", async () => {
|
|
25
|
-
const root = await makeDir(".
|
|
25
|
+
const root = await makeDir(".docsgov", "docs", "pkg");
|
|
26
26
|
const r = await find(root);
|
|
27
27
|
await r.writeFile("docs/a.md", enc.encode("original\n"));
|
|
28
28
|
await r.writeFile("pkg/foo.go", enc.encode("package foo\n"));
|
|
@@ -32,7 +32,7 @@ describe("withOverlay: single-path in-memory intercept", () => {
|
|
|
32
32
|
// The check pipeline reads files via fs() too (direct reads and the markdown
|
|
33
33
|
// walk); the overlay FS must agree with overlay readFile.
|
|
34
34
|
test("fs() serves the injected content for the overlaid path", async () => {
|
|
35
|
-
const root = await makeDir(".
|
|
35
|
+
const root = await makeDir(".docsgov", "docs");
|
|
36
36
|
const r = await find(root);
|
|
37
37
|
await r.writeFile("docs/a.md", enc.encode("on-disk\n"));
|
|
38
38
|
const injected = enc.encode("overlay\n");
|
|
@@ -42,7 +42,7 @@ describe("withOverlay: single-path in-memory intercept", () => {
|
|
|
42
42
|
// If withOverlay mutated the base repo's FS, the real file would be
|
|
43
43
|
// effectively overwritten in memory — the exact problem being avoided.
|
|
44
44
|
test("creating an overlay does not mutate the base repo", async () => {
|
|
45
|
-
const root = await makeDir(".
|
|
45
|
+
const root = await makeDir(".docsgov", "docs");
|
|
46
46
|
const r = await find(root);
|
|
47
47
|
await r.writeFile("docs/a.md", enc.encode("status: planning\n"));
|
|
48
48
|
r.withOverlay("docs/a.md", enc.encode("status: implemented\n"));
|
|
@@ -53,7 +53,7 @@ describe("withOverlay: single-path in-memory intercept", () => {
|
|
|
53
53
|
// real on-disk tree (including the overlaid file). If readDir intercepted
|
|
54
54
|
// anything, the walk would miss or duplicate files.
|
|
55
55
|
test("readDir delegates to the base so the overlaid file is still enumerated", async () => {
|
|
56
|
-
const root = await makeDir(".
|
|
56
|
+
const root = await makeDir(".docsgov", "docs");
|
|
57
57
|
const r = await find(root);
|
|
58
58
|
await r.writeFile("docs/a.md", enc.encode("on-disk\n"));
|
|
59
59
|
await r.writeFile("docs/b.md", enc.encode("other\n"));
|
|
@@ -68,7 +68,7 @@ describe("withOverlay: single-path in-memory intercept", () => {
|
|
|
68
68
|
// the overlay's content rule — a sub() that diverged from base would make a
|
|
69
69
|
// nested read return the wrong bytes for non-overlaid files.
|
|
70
70
|
test("sub descends into the base subtree", async () => {
|
|
71
|
-
const root = await makeDir(".
|
|
71
|
+
const root = await makeDir(".docsgov", "docs", "pkg");
|
|
72
72
|
const r = await find(root);
|
|
73
73
|
await r.writeFile("docs/a.md", enc.encode("on-disk\n"));
|
|
74
74
|
await r.writeFile("pkg/foo.go", enc.encode("package foo\n"));
|
package/dist/repo/repo.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Package repo is the OS boundary for
|
|
1
|
+
// Package repo is the OS boundary for docsgov. It is the only package that
|
|
2
2
|
// touches the real filesystem directly and owns OS path handling. Everything
|
|
3
3
|
// else works with slash paths and the FS interface.
|
|
4
4
|
//
|
|
5
5
|
// Responsibilities:
|
|
6
|
-
// - Walk CWD up to the nearest ancestor containing a .
|
|
7
|
-
// - Fall back to the supplied directory when no .
|
|
6
|
+
// - Walk CWD up to the nearest ancestor containing a .docsgov/ directory.
|
|
7
|
+
// - Fall back to the supplied directory when no .docsgov/ exists (gen/add
|
|
8
8
|
// bootstrapping mode).
|
|
9
9
|
// - Return a Repo that exposes an FS and the OS root path.
|
|
10
10
|
// - CRLF -> LF normalisation on every file read.
|
|
@@ -12,8 +12,8 @@ import * as nodefs from "node:fs/promises";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { isNotExist, RealFS } from "./fs.js";
|
|
14
14
|
import { OverlayFS } from "./overlay.js";
|
|
15
|
-
// sentinel is the directory name that marks a
|
|
16
|
-
const sentinel = ".
|
|
15
|
+
// sentinel is the directory name that marks a docsgov repository root.
|
|
16
|
+
const sentinel = ".docsgov";
|
|
17
17
|
/**
|
|
18
18
|
* Holds the resolved repository root. It is the sole owner of the OS path; all
|
|
19
19
|
* callers work through the {@link FS} interface or the slash-path readFile
|
|
@@ -209,7 +209,7 @@ export class Repo {
|
|
|
209
209
|
}
|
|
210
210
|
/**
|
|
211
211
|
* Writes `content` to a temp file that is a sibling of the file at
|
|
212
|
-
* `slashPath` (same directory, ".
|
|
212
|
+
* `slashPath` (same directory, ".docsgov-flip.tmp" suffix), then returns a
|
|
213
213
|
* {@link PendingWrite}. The caller calls commit to either rename the temp
|
|
214
214
|
* into place (ok=true) or remove it (ok=false). The temp file is always in
|
|
215
215
|
* the same directory as the target so the rename stays atomic on a single
|
|
@@ -223,7 +223,7 @@ export class Repo {
|
|
|
223
223
|
catch (err) {
|
|
224
224
|
throw new Error(`repo.atomicWriteFile: mkdir parents for ${JSON.stringify(slashPath)}: ${errMsg(err)}`);
|
|
225
225
|
}
|
|
226
|
-
const tmpAbs = abs + ".
|
|
226
|
+
const tmpAbs = abs + ".docsgov-flip.tmp";
|
|
227
227
|
try {
|
|
228
228
|
await nodefs.writeFile(tmpAbs, content);
|
|
229
229
|
}
|
|
@@ -298,9 +298,9 @@ export class PendingWrite {
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
/**
|
|
301
|
-
* Walks up from `dir` to locate the nearest ancestor containing a .
|
|
301
|
+
* Walks up from `dir` to locate the nearest ancestor containing a .docsgov/
|
|
302
302
|
* directory. If none is found it falls back to `dir` itself (so gen and add can
|
|
303
|
-
* bootstrap a repo that has no .
|
|
303
|
+
* bootstrap a repo that has no .docsgov/ yet). `dir` is resolved to an absolute
|
|
304
304
|
* OS path first.
|
|
305
305
|
*/
|
|
306
306
|
export async function find(dir) {
|
|
@@ -322,7 +322,7 @@ export async function find(dir) {
|
|
|
322
322
|
}
|
|
323
323
|
const parent = path.dirname(current);
|
|
324
324
|
if (parent === current) {
|
|
325
|
-
// Reached the filesystem root without finding .
|
|
325
|
+
// Reached the filesystem root without finding .docsgov/. Fall back to the
|
|
326
326
|
// original dir (bootstrapping mode).
|
|
327
327
|
return new Repo(abs, new RealFS(abs));
|
|
328
328
|
}
|