@agentskit/doc-bridge 1.10.1 → 1.11.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/CHANGELOG.md +33 -0
- package/action.yml +1 -1
- package/dist/cli/program.js +111 -6
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -1
- package/dist/{index-7wYGbllW.d.ts → index-CimDq6_e.d.ts} +2 -0
- package/dist/index.d.ts +51 -5
- package/dist/index.js +98 -4
- package/dist/index.js.map +1 -1
- package/docs/spec/config-v1.md +23 -1
- package/mcpb/manifest.json +1 -1
- package/package.json +2 -2
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/cli/program.ts +23 -2
- package/src/config/schema.ts +2 -0
- package/src/discovery/reproducibility.ts +109 -0
- package/src/doctor/run-doctor.ts +24 -0
- package/src/gates/run-gates.ts +44 -0
- package/src/index.ts +5 -0
- package/src/version.ts +1 -1
package/docs/spec/config-v1.md
CHANGED
|
@@ -343,6 +343,7 @@ type GatesConfig = {
|
|
|
343
343
|
|
|
344
344
|
type GateId =
|
|
345
345
|
| 'index-freshness'
|
|
346
|
+
| 'index-reproducible' // opt-in; in no preset
|
|
346
347
|
| 'human-guide-links'
|
|
347
348
|
| 'link-rot' // reserved; emits a diagnostic and is not executed
|
|
348
349
|
| 'okf-type'
|
|
@@ -358,7 +359,7 @@ type GateId =
|
|
|
358
359
|
| `standard` | + `human-guide-links` in v1 |
|
|
359
360
|
| `strict` | + `okf-type` in v1 |
|
|
360
361
|
|
|
361
|
-
Implemented gates include `index-freshness`, `human-guide-links`, `okf-type`, `docs-style`, and the opt-in `documentation-standard-v1`. For v1 compatibility, `link-rot`, `routing-currency`, and `bootstrap-size` remain accepted as reserved IDs; including one emits `AK_DOCS_RESERVED_GATE` and does not claim that the gate ran. Unknown IDs are rejected.
|
|
362
|
+
Implemented gates include `index-freshness`, `human-guide-links`, `okf-type`, `docs-style`, and the opt-in `index-reproducible` and `documentation-standard-v1`. For v1 compatibility, `link-rot`, `routing-currency`, and `bootstrap-size` remain accepted as reserved IDs; including one emits `AK_DOCS_RESERVED_GATE` and does not claim that the gate ran. Unknown IDs are rejected.
|
|
362
363
|
|
|
363
364
|
### Structural vs style validation
|
|
364
365
|
|
|
@@ -367,10 +368,31 @@ These gates are deterministic lint checks, not editorial grading:
|
|
|
367
368
|
| Gate | Kind | What it proves |
|
|
368
369
|
|------|------|----------------|
|
|
369
370
|
| `index-freshness` | structural | Generated index matches current docs/config |
|
|
371
|
+
| `index-reproducible` | structural | Every indexed path is committed, so a clean checkout rebuilds the same index |
|
|
370
372
|
| `human-guide-links` | structural | Local `humanDoc` links resolve through configured human-doc adapters |
|
|
371
373
|
| `okf-type` | OKF lint | Agent docs have required `type:` frontmatter when strict/required |
|
|
372
374
|
| `docs-style` | style lint | Opt-in deterministic profile checks for title, purpose, audience, examples, owner/source, task orientation, and stale wording |
|
|
373
375
|
|
|
376
|
+
### Why `index-reproducible` is opt-in
|
|
377
|
+
|
|
378
|
+
A scan walks what is on disk and has no reason to consult `.gitignore`, so a generated module or
|
|
379
|
+
document joins the corpus on a machine that has built and leaves it on one that has not. That is
|
|
380
|
+
harmless while the index is regenerated on every run, and a defect the moment the index is
|
|
381
|
+
committed for a gate to verify: two checkouts of the same commit produce different artifacts,
|
|
382
|
+
`index-freshness` reports staleness that nothing caused, and regenerating cannot fix it because the
|
|
383
|
+
next machine disagrees in the other direction.
|
|
384
|
+
|
|
385
|
+
`index-reproducible` asks Git which of the indexed paths it ignores, and names each one with the
|
|
386
|
+
rule that matched (`apps/docs-next/.gitignore:13:lib/ask-context.ts`) so the fix is one lookup
|
|
387
|
+
away: add the path to `safety.exclude`. It reports success, not failure, when the index is not
|
|
388
|
+
committed or the project is not a Git checkout — there is nothing to reproduce in either case. A
|
|
389
|
+
file that is both tracked and matched by an ignore rule is not flagged; being committed is the
|
|
390
|
+
point.
|
|
391
|
+
|
|
392
|
+
It is in no preset, because enabling it for every consumer would fail gates that pass for good
|
|
393
|
+
reasons. `ak-docs index` and `ak-docs doctor` report the same finding unconditionally — as a
|
|
394
|
+
diagnostic and a `warn` issue — so a repository learns about it before deciding to enforce it.
|
|
395
|
+
|
|
374
396
|
`docs-style` supports `google-dev-docs`, `playbook-okf`, and `custom` profiles. It is not part of the default path and does not grade prose quality; it checks for explicit structural signals. LLM critique remains planned optional behavior.
|
|
375
397
|
|
|
376
398
|
---
|
package/mcpb/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "doc-bridge",
|
|
4
4
|
"display_name": "Doc Bridge",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.11.0",
|
|
6
6
|
"description": "Deterministic repository handoffs for coding agents, running locally without an LLM or API key.",
|
|
7
7
|
"long_description": "Doc Bridge turns a repository's own documentation and ownership metadata into deterministic handoffs: where an agent should start, which paths it may edit, which checks it must run, and when a human must take over. The local connector exposes the same read-only contract available through Doc Bridge CLI and CI.",
|
|
8
8
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentskit/doc-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"mcpName": "io.github.AgentsKit-io/doc-bridge",
|
|
5
5
|
"description": "Human↔agent documentation bridge — deterministic handoffs, doc-site links, memory→docs, optional AgentsKit RAG/chat.",
|
|
6
6
|
"type": "module",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"@agentskit/ink": "0.10.9",
|
|
128
128
|
"@agentskit/react": "0.8.3",
|
|
129
129
|
"@anthropic-ai/mcpb": "2.1.2",
|
|
130
|
-
"@changesets/cli": "^3.0.
|
|
130
|
+
"@changesets/cli": "^3.0.3",
|
|
131
131
|
"@lhci/cli": "^0.15.1",
|
|
132
132
|
"@playwright/test": "1.63.0",
|
|
133
133
|
"@tailwindcss/postcss": "4.3.3",
|
package/src/cli/program.ts
CHANGED
|
@@ -99,6 +99,7 @@ import { independentlyAdjudicateStudyLedger, persistIndependentlyAdjudicatedLedg
|
|
|
99
99
|
import { formatStudyProviderCliText, parseStudyProviderCliConfig } from '../study/provider-cli.js'
|
|
100
100
|
import { calculateStudyMetrics, formatStudyMetricsText } from '../study/metrics.js'
|
|
101
101
|
import { formatStudyVerificationText, parseStudyVerificationBinding } from '../study/verification.js'
|
|
102
|
+
import { checkIndexReproducibility } from '../discovery/reproducibility.js'
|
|
102
103
|
|
|
103
104
|
type Command =
|
|
104
105
|
| 'help'
|
|
@@ -469,7 +470,11 @@ const loadProject = (configPath?: string) => {
|
|
|
469
470
|
return { config, configPath: path, root }
|
|
470
471
|
}
|
|
471
472
|
|
|
472
|
-
const indexDiagnostics = (
|
|
473
|
+
const indexDiagnostics = (
|
|
474
|
+
root: string,
|
|
475
|
+
config: DocBridgeConfigV1,
|
|
476
|
+
result: ReturnType<typeof buildDocBridgeIndex>,
|
|
477
|
+
): string[] => {
|
|
473
478
|
const diagnostics: string[] = []
|
|
474
479
|
const onlyDoc = result.index.knowledge.length === 1 ? result.index.knowledge[0] : undefined
|
|
475
480
|
if (onlyDoc?.path === config.corpus.agent.index) {
|
|
@@ -484,6 +489,22 @@ const indexDiagnostics = (config: DocBridgeConfigV1, result: ReturnType<typeof b
|
|
|
484
489
|
'No ownership handoffs yet. Add routing.options.ownership, package frontmatter (package + editRoot), or a monorepo plugin.',
|
|
485
490
|
)
|
|
486
491
|
}
|
|
492
|
+
/*
|
|
493
|
+
* Said at write time, because this is the moment the unreproducible artifact is created and the
|
|
494
|
+
* only moment the operator can see what their machine had that another will not.
|
|
495
|
+
*/
|
|
496
|
+
const reproducibility = checkIndexReproducibility(
|
|
497
|
+
root,
|
|
498
|
+
config.index?.outFile ?? '.doc-bridge/index.json',
|
|
499
|
+
result.index.knowledge.map((entry) => entry.path),
|
|
500
|
+
)
|
|
501
|
+
if (reproducibility.ignored.length > 0) {
|
|
502
|
+
const sample = reproducibility.ignored.slice(0, 5).map((entry) => `${entry.path} (${entry.rule})`)
|
|
503
|
+
diagnostics.push(
|
|
504
|
+
`${reproducibility.ignored.length} indexed path(s) are ignored by Git: ${sample.join(', ')}${reproducibility.ignored.length > sample.length ? `, and ${reproducibility.ignored.length - sample.length} more` : ''}.`,
|
|
505
|
+
'A clean checkout will not have them, so it builds a different index. Add them to safety.exclude in doc-bridge.config.json.',
|
|
506
|
+
)
|
|
507
|
+
}
|
|
487
508
|
return diagnostics
|
|
488
509
|
}
|
|
489
510
|
|
|
@@ -1749,7 +1770,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1749
1770
|
})
|
|
1750
1771
|
}
|
|
1751
1772
|
const result = buildDocBridgeIndex({ root, config })
|
|
1752
|
-
const diagnostics = indexDiagnostics(config, result)
|
|
1773
|
+
const diagnostics = indexDiagnostics(root, config, result)
|
|
1753
1774
|
const handoffCount = Object.keys(result.index.handoffs ?? {}).length
|
|
1754
1775
|
writeJson({
|
|
1755
1776
|
ok: true,
|
package/src/config/schema.ts
CHANGED
|
@@ -122,6 +122,7 @@ export const GatesConfigSchema = z
|
|
|
122
122
|
.array(
|
|
123
123
|
z.enum([
|
|
124
124
|
'index-freshness',
|
|
125
|
+
'index-reproducible',
|
|
125
126
|
'human-guide-links',
|
|
126
127
|
'link-rot',
|
|
127
128
|
'okf-type',
|
|
@@ -137,6 +138,7 @@ export const GatesConfigSchema = z
|
|
|
137
138
|
.array(
|
|
138
139
|
z.enum([
|
|
139
140
|
'index-freshness',
|
|
141
|
+
'index-reproducible',
|
|
140
142
|
'human-guide-links',
|
|
141
143
|
'link-rot',
|
|
142
144
|
'okf-type',
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a committed index could have been produced by a clean checkout.
|
|
3
|
+
*
|
|
4
|
+
* The recommended setup commits `.doc-bridge/index.json` so a gate can verify it, and that only
|
|
5
|
+
* works while the index is a function of committed content. A scan has no reason to know about
|
|
6
|
+
* `.gitignore` — it walks what is on disk — so a generated module or document silently joins the
|
|
7
|
+
* corpus on any machine that has built, and silently leaves it on one that has not. The index then
|
|
8
|
+
* disagrees with itself between two checkouts of the same commit, `index-freshness` reports an
|
|
9
|
+
* artifact that nothing invalidated, and regenerating cannot fix it because the next machine
|
|
10
|
+
* disagrees in the other direction.
|
|
11
|
+
*
|
|
12
|
+
* Dogfooding found this the expensive way: a 25-package monorepo whose gate passed locally and
|
|
13
|
+
* failed in CI because `pnpm lint` had generated one `.ts` file into the corpus before the index
|
|
14
|
+
* was written. The scan cannot guess which files are generated, but Git already knows, so the
|
|
15
|
+
* check is: of the paths this index carries, which ones does Git ignore?
|
|
16
|
+
*
|
|
17
|
+
* It only applies when the index is itself tracked. A consumer who regenerates on every run has
|
|
18
|
+
* nothing to reproduce, and reporting generated files to them would be noise.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from 'node:child_process'
|
|
22
|
+
|
|
23
|
+
export type IgnoredIndexEntry = {
|
|
24
|
+
readonly path: string
|
|
25
|
+
/** The ignore rule that matched, as `.gitignore:12:pattern`, so the fix is one lookup away. */
|
|
26
|
+
readonly rule: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type IndexReproducibility = {
|
|
30
|
+
/**
|
|
31
|
+
* Whether the question was answerable. False outside a Git checkout, when Git is unavailable, or
|
|
32
|
+
* when the index is not tracked — in all three cases `ignored` is empty and means nothing.
|
|
33
|
+
*/
|
|
34
|
+
readonly checked: boolean
|
|
35
|
+
/** Why the check did not run, for a caller that wants to say so. */
|
|
36
|
+
readonly skipped?: 'no-git' | 'index-untracked'
|
|
37
|
+
/** Indexed paths that Git ignores, in path order. */
|
|
38
|
+
readonly ignored: readonly IgnoredIndexEntry[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const NOT_CHECKED = (skipped: 'no-git' | 'index-untracked'): IndexReproducibility => ({
|
|
42
|
+
checked: false,
|
|
43
|
+
skipped,
|
|
44
|
+
ignored: [],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `undefined` means the command did not answer.
|
|
49
|
+
*
|
|
50
|
+
* `allowExit1` is for `check-ignore` alone, which exits 1 when nothing matched — the common answer
|
|
51
|
+
* rather than a failure, and it still writes whatever did match to stdout. The probes must not
|
|
52
|
+
* share that tolerance: `ls-files --error-unmatch` exits 1 precisely to say "not tracked", and
|
|
53
|
+
* reading that as success made the untracked case look checked.
|
|
54
|
+
*/
|
|
55
|
+
const git = (
|
|
56
|
+
root: string,
|
|
57
|
+
args: readonly string[],
|
|
58
|
+
options: { readonly input?: string; readonly allowExit1?: boolean } = {},
|
|
59
|
+
): string | undefined => {
|
|
60
|
+
try {
|
|
61
|
+
return execFileSync('git', args, {
|
|
62
|
+
cwd: root,
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
65
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
66
|
+
...(options.input === undefined ? {} : { input: options.input }),
|
|
67
|
+
})
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const status = (error as { status?: number }).status
|
|
70
|
+
if (options.allowExit1 && status === 1) return (error as { stdout?: string }).stdout ?? ''
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The paths a committed index carries that Git ignores.
|
|
77
|
+
*
|
|
78
|
+
* `git check-ignore` consults the index by default, so a tracked file that also matches an ignore
|
|
79
|
+
* pattern is correctly reported as not ignored — being committed is the whole point. A test pins
|
|
80
|
+
* that behaviour rather than trusting it.
|
|
81
|
+
*/
|
|
82
|
+
export const checkIndexReproducibility = (
|
|
83
|
+
root: string,
|
|
84
|
+
indexPath: string,
|
|
85
|
+
paths: readonly string[],
|
|
86
|
+
): IndexReproducibility => {
|
|
87
|
+
if (git(root, ['rev-parse', '--is-inside-work-tree']) === undefined) return NOT_CHECKED('no-git')
|
|
88
|
+
if (git(root, ['ls-files', '--error-unmatch', '--', indexPath]) === undefined) {
|
|
89
|
+
return NOT_CHECKED('index-untracked')
|
|
90
|
+
}
|
|
91
|
+
if (paths.length === 0) return { checked: true, ignored: [] }
|
|
92
|
+
|
|
93
|
+
const unique = [...new Set(paths)].sort()
|
|
94
|
+
const output = git(root, ['check-ignore', '--verbose', '-z', '--stdin'], {
|
|
95
|
+
input: `${unique.join('\0')}\0`,
|
|
96
|
+
allowExit1: true,
|
|
97
|
+
})
|
|
98
|
+
if (output === undefined) return NOT_CHECKED('no-git')
|
|
99
|
+
|
|
100
|
+
/* `-z --verbose` emits four NUL-terminated fields per match: source, line, pattern, path. */
|
|
101
|
+
const fields = output.split('\0')
|
|
102
|
+
const ignored: IgnoredIndexEntry[] = []
|
|
103
|
+
for (let index = 0; index + 3 < fields.length; index += 4) {
|
|
104
|
+
const [source, line, pattern, path] = [fields[index], fields[index + 1], fields[index + 2], fields[index + 3]]
|
|
105
|
+
if (!path) continue
|
|
106
|
+
ignored.push({ path, rule: `${source ?? '?'}:${line ?? '?'}:${pattern ?? '?'}` })
|
|
107
|
+
}
|
|
108
|
+
return { checked: true, ignored: ignored.sort((left, right) => left.path.localeCompare(right.path)) }
|
|
109
|
+
}
|
package/src/doctor/run-doctor.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { buildDocBridgeIndex } from '../index-builder/build-index.js'
|
|
|
8
8
|
import { scanAgentCorpus } from '../index-builder/scan-corpus.js'
|
|
9
9
|
import { runGates, type GateRunResult } from '../gates/run-gates.js'
|
|
10
10
|
import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
|
|
11
|
+
import { checkIndexReproducibility, type IndexReproducibility } from '../discovery/reproducibility.js'
|
|
11
12
|
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
12
13
|
import type { DiscoverySnapshotV1 } from '../schemas/knowledge.js'
|
|
13
14
|
import { doctorBadgeMetrics, type DoctorBadgeMetrics } from './badge.js'
|
|
@@ -104,6 +105,8 @@ export type DoctorCoverage = {
|
|
|
104
105
|
readonly hasIndex: boolean
|
|
105
106
|
}
|
|
106
107
|
readonly gates: GateRunResult
|
|
108
|
+
/** Whether a clean checkout would rebuild this index, or whether generated files leak into it. */
|
|
109
|
+
readonly reproducibility: IndexReproducibility
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
export type DoctorReport = {
|
|
@@ -265,6 +268,22 @@ const buildIssues = (coverage: DoctorCoverage): DoctorIssue[] => {
|
|
|
265
268
|
})
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
/*
|
|
272
|
+
* A warning, not an error: the index is not wrong, it is unreproducible, and every reader of it
|
|
273
|
+
* on another machine is the one who finds out. The fix belongs to the repository's
|
|
274
|
+
* `safety.exclude`, so the message names the rule that matched and lets the operator decide.
|
|
275
|
+
*/
|
|
276
|
+
const { ignored } = coverage.reproducibility
|
|
277
|
+
if (ignored.length > 0) {
|
|
278
|
+
const sample = ignored.slice(0, 5).map((entry) => `${entry.path} (${entry.rule})`)
|
|
279
|
+
issues.push({
|
|
280
|
+
severity: 'warn',
|
|
281
|
+
code: 'index-not-reproducible',
|
|
282
|
+
message: `${ignored.length} indexed path(s) are ignored by Git, so a clean checkout builds a different index: ${sample.join(', ')}${ignored.length > sample.length ? `, and ${ignored.length - sample.length} more` : ''}.`,
|
|
283
|
+
action: 'edit doc-bridge.config.json # add the generated paths to safety.exclude',
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
268
287
|
for (const id of coverage.packages.missingAgentDoc) {
|
|
269
288
|
issues.push({
|
|
270
289
|
severity: 'warn',
|
|
@@ -423,6 +442,11 @@ export const runDoctor = (root: string, config: DocBridgeConfigV1): DoctorReport
|
|
|
423
442
|
hasIndex,
|
|
424
443
|
},
|
|
425
444
|
gates,
|
|
445
|
+
reproducibility: checkIndexReproducibility(
|
|
446
|
+
root,
|
|
447
|
+
config.index?.outFile ?? '.doc-bridge/index.json',
|
|
448
|
+
index.knowledge.map((entry) => entry.path),
|
|
449
|
+
),
|
|
426
450
|
}
|
|
427
451
|
|
|
428
452
|
const issues = buildIssues(coverage)
|
package/src/gates/run-gates.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { buildDocBridgeIndex } from '../index-builder/build-index.js'
|
|
|
9
9
|
import { scanAgentCorpus } from '../index-builder/scan-corpus.js'
|
|
10
10
|
import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
|
|
11
11
|
import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
|
|
12
|
+
import { checkIndexReproducibility } from '../discovery/reproducibility.js'
|
|
12
13
|
|
|
13
14
|
export type GateId =
|
|
14
15
|
| 'index-freshness'
|
|
16
|
+
| 'index-reproducible'
|
|
15
17
|
| 'human-guide-links'
|
|
16
18
|
| 'okf-type'
|
|
17
19
|
| 'docs-style'
|
|
@@ -54,6 +56,48 @@ export const runGate = (
|
|
|
54
56
|
if (id === 'human-guide-links') return runHumanGuideLinksGate(root, config)
|
|
55
57
|
if (id === 'okf-type') return runOkfTypeGate(root, config)
|
|
56
58
|
if (id === 'docs-style') return runDocsStyleGate(root, config)
|
|
59
|
+
if (id === 'index-reproducible') {
|
|
60
|
+
/*
|
|
61
|
+
* Opt-in, and in no preset: a repository that regenerates the index on every run has nothing
|
|
62
|
+
* to enforce, and turning this on for everyone would fail gates that were passing for good
|
|
63
|
+
* reasons. `gates.include: ['index-reproducible']` is how a repository that commits the index
|
|
64
|
+
* says it wants the guarantee enforced rather than merely reported.
|
|
65
|
+
*/
|
|
66
|
+
let index
|
|
67
|
+
try {
|
|
68
|
+
index = loadDocBridgeIndex(root, config)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof IndexNotFoundError) return { id, ok: false, message: error.message }
|
|
71
|
+
throw error
|
|
72
|
+
}
|
|
73
|
+
const result = checkIndexReproducibility(
|
|
74
|
+
root,
|
|
75
|
+
config.index?.outFile ?? '.doc-bridge/index.json',
|
|
76
|
+
index.knowledge.map((entry) => entry.path),
|
|
77
|
+
)
|
|
78
|
+
if (!result.checked) {
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
ok: true,
|
|
82
|
+
message:
|
|
83
|
+
result.skipped === 'index-untracked'
|
|
84
|
+
? 'Index is not committed, so nothing has to reproduce it'
|
|
85
|
+
: 'Not a Git checkout; reproducibility was not checked',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (result.ignored.length === 0) {
|
|
89
|
+
return { id, ok: true, message: 'Every indexed path is committed' }
|
|
90
|
+
}
|
|
91
|
+
const sample = result.ignored.slice(0, 5).map((entry) => `${entry.path} (${entry.rule})`)
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
ok: false,
|
|
95
|
+
message: `${result.ignored.length} indexed path(s) are ignored by Git, so a clean checkout builds a different index. Add them to safety.exclude.`,
|
|
96
|
+
expected: 'every indexed path is committed',
|
|
97
|
+
actual: sample.join(', '),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
if (id !== 'index-freshness') throw new Error(`Unsupported gate "${id}"`)
|
|
58
102
|
|
|
59
103
|
let current: string
|
package/src/index.ts
CHANGED
|
@@ -98,6 +98,11 @@ export {
|
|
|
98
98
|
repositoryInputs,
|
|
99
99
|
type RepositoryInputsV1,
|
|
100
100
|
} from './index-builder/project-corpus.js'
|
|
101
|
+
export {
|
|
102
|
+
checkIndexReproducibility,
|
|
103
|
+
type IgnoredIndexEntry,
|
|
104
|
+
type IndexReproducibility,
|
|
105
|
+
} from './discovery/reproducibility.js'
|
|
101
106
|
export {
|
|
102
107
|
DOCUMENT_BODY_LIMIT,
|
|
103
108
|
EMPTY_OVERLAY_HASH,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '1.
|
|
1
|
+
export const PACKAGE_VERSION = '1.11.0'
|