@agentskit/doc-bridge 1.10.0 → 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 +59 -0
- package/action.yml +1 -1
- package/dist/cli/program.js +125 -9
- 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 +69 -7
- package/dist/index.js +113 -7
- 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 +6 -0
- package/src/retrieval/project.ts +33 -6
- package/src/schemas/retrieval-index.ts +6 -1
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 25f16f3: Say when a committed index could not have come from a clean checkout.
|
|
8
|
+
|
|
9
|
+
A scan walks what is on disk and has no reason to consult `.gitignore`, so a generated module or
|
|
10
|
+
document silently joins the corpus on a machine that has built and silently leaves it on one that
|
|
11
|
+
has not. That is harmless while the index is rebuilt on every run. It is a defect the moment the
|
|
12
|
+
index is committed so a gate can verify it — which is the setup `index-freshness` exists for: two
|
|
13
|
+
checkouts of the same commit then produce different artifacts, the gate reports staleness that
|
|
14
|
+
nothing caused, and regenerating cannot fix it because the next machine disagrees in the other
|
|
15
|
+
direction.
|
|
16
|
+
|
|
17
|
+
Dogfooding found this the expensive way. On a 25-package monorepo the freshness gate passed locally
|
|
18
|
+
and failed in CI, twice, because `pnpm lint` had generated one `.ts` file into the corpus before the
|
|
19
|
+
index was written; CI lints a narrower scope and never had the file. Diagnosing it took reading two
|
|
20
|
+
CI logs and diffing file inventories. Git already knew the answer.
|
|
21
|
+
|
|
22
|
+
`ak-docs index` now names those paths as it writes, and `ak-docs doctor` raises an
|
|
23
|
+
`index-not-reproducible` warning, each with the ignore rule that matched — for example
|
|
24
|
+
`apps/docs-next/lib/ask-context.ts (apps/docs-next/.gitignore:13:lib/ask-context.ts)` — so the fix
|
|
25
|
+
is one lookup away: add the path to `safety.exclude`.
|
|
26
|
+
|
|
27
|
+
Both are unconditional and neither changes an exit code or the doctor's score. Enforcement is the
|
|
28
|
+
new `index-reproducible` gate, which is in no preset and has to be requested with
|
|
29
|
+
`gates.include: ['index-reproducible']`, because turning it on for every consumer would fail gates
|
|
30
|
+
that pass for good reasons.
|
|
31
|
+
|
|
32
|
+
The check reports success rather than failure when the index is not committed or the project is not
|
|
33
|
+
a Git checkout, since there is nothing to reproduce in either case, and it does not flag a file that
|
|
34
|
+
is both tracked and matched by an ignore rule — being committed is the point.
|
|
35
|
+
|
|
36
|
+
## 1.10.1
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- 0926c57: Keep a committed index fresh across commits.
|
|
41
|
+
|
|
42
|
+
The retrieval projection sealed its content hash over the discovery snapshot's hash, and a
|
|
43
|
+
snapshot's hash covers its `sourceRevision` — the commit SHA when the working tree is clean, a
|
|
44
|
+
digest of the scanned files when it is not. That is right for an artifact whose job is to say what
|
|
45
|
+
one revision looked like, and wrong as a projection input: the projection is a function of what the
|
|
46
|
+
snapshot observed, not of where it observed it.
|
|
47
|
+
|
|
48
|
+
The consequence only appears in a repository that commits `.doc-bridge/index.json`, which is the
|
|
49
|
+
recommended setup: committing the index changes the revision that the next run hashes, so the
|
|
50
|
+
artifact was stale the moment it landed — landing it is a commit. `ak-docs gate run` reported
|
|
51
|
+
`index-freshness` failing on an index that nothing had invalidated, and no regenerate could fix it,
|
|
52
|
+
because the fix was itself a commit. Dogfooding on a 25-package monorepo, the gate could not be
|
|
53
|
+
made to pass twice in a row.
|
|
54
|
+
|
|
55
|
+
The seal is now over what the snapshot observed: the entities, the relations and the analyzer
|
|
56
|
+
identity that produced them, alongside the overlay and configuration hashes it already covered. The
|
|
57
|
+
artifact still carries `snapshotHash`, now documented as provenance rather than a seal input, so a
|
|
58
|
+
reader can still say which snapshot a projection came from. `RETRIEVAL_PROJECTION_VERSION` goes to
|
|
59
|
+
2, so no reader compares a hash across the change, and every index's content hash changes once on
|
|
60
|
+
the next `ak-docs index`.
|
|
61
|
+
|
|
3
62
|
## 1.10.0
|
|
4
63
|
|
|
5
64
|
### Minor Changes
|
package/action.yml
CHANGED
package/dist/cli/program.js
CHANGED
|
@@ -430,6 +430,7 @@ var GatesConfigSchema = z.object({
|
|
|
430
430
|
include: z.array(
|
|
431
431
|
z.enum([
|
|
432
432
|
"index-freshness",
|
|
433
|
+
"index-reproducible",
|
|
433
434
|
"human-guide-links",
|
|
434
435
|
"link-rot",
|
|
435
436
|
"okf-type",
|
|
@@ -442,6 +443,7 @@ var GatesConfigSchema = z.object({
|
|
|
442
443
|
exclude: z.array(
|
|
443
444
|
z.enum([
|
|
444
445
|
"index-freshness",
|
|
446
|
+
"index-reproducible",
|
|
445
447
|
"human-guide-links",
|
|
446
448
|
"link-rot",
|
|
447
449
|
"okf-type",
|
|
@@ -3616,8 +3618,13 @@ var RetrievalIndexV1Schema = z5.object({
|
|
|
3616
3618
|
schemaVersion: z5.literal(RETRIEVAL_INDEX_SCHEMA_VERSION),
|
|
3617
3619
|
contentHash: hash2,
|
|
3618
3620
|
contentHashAlgo: z5.literal("sha256-normalized-v1"),
|
|
3619
|
-
/**
|
|
3621
|
+
/**
|
|
3622
|
+
* Which snapshot this was projected from. Provenance, not a seal input: it carries the
|
|
3623
|
+
* snapshot's `sourceRevision`, and the projection is a function of what the snapshot observed
|
|
3624
|
+
* rather than of the revision it was observed at.
|
|
3625
|
+
*/
|
|
3620
3626
|
snapshotHash: hash2,
|
|
3627
|
+
/** The inputs the projection is a function of. Same hashes, same projection. */
|
|
3621
3628
|
overlayHash: hash2,
|
|
3622
3629
|
configurationHash: hash2,
|
|
3623
3630
|
lexiconVersion: z5.number().int().nonnegative().max(1e3),
|
|
@@ -4055,7 +4062,7 @@ var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root
|
|
|
4055
4062
|
};
|
|
4056
4063
|
|
|
4057
4064
|
// src/version.ts
|
|
4058
|
-
var PACKAGE_VERSION = "1.
|
|
4065
|
+
var PACKAGE_VERSION = "1.11.0";
|
|
4059
4066
|
|
|
4060
4067
|
// src/index-builder/capabilities.ts
|
|
4061
4068
|
var renderCapabilitiesJson = (config, index, paths) => {
|
|
@@ -5633,7 +5640,7 @@ var resolveSearchParams = (configured) => ({
|
|
|
5633
5640
|
});
|
|
5634
5641
|
|
|
5635
5642
|
// src/retrieval/project.ts
|
|
5636
|
-
var RETRIEVAL_PROJECTION_VERSION =
|
|
5643
|
+
var RETRIEVAL_PROJECTION_VERSION = 2;
|
|
5637
5644
|
var DOCUMENT_BODY_LIMIT = 4e3;
|
|
5638
5645
|
var MAX_EDGES = 64;
|
|
5639
5646
|
var MAX_ALIASES = 32;
|
|
@@ -5641,6 +5648,12 @@ var MAX_TAGS = 32;
|
|
|
5641
5648
|
var MAX_SYMBOLS = 256;
|
|
5642
5649
|
var MAX_SUMMARY = 400;
|
|
5643
5650
|
var EMPTY_OVERLAY_HASH = sha256NormalizedV1({ accepted: [] });
|
|
5651
|
+
var snapshotObservationHash = (snapshot) => sha256NormalizedV1({
|
|
5652
|
+
pipelineVersion: snapshot.pipelineVersion,
|
|
5653
|
+
analyzerVersions: snapshot.analyzerVersions,
|
|
5654
|
+
entities: snapshot.entities,
|
|
5655
|
+
relations: snapshot.relations
|
|
5656
|
+
});
|
|
5644
5657
|
var CONFIDENCE_RANK = { observed: 0, declared: 1, fuzzy: 2, proposed: 3 };
|
|
5645
5658
|
var weakerConfidence = (a, b) => CONFIDENCE_RANK[a] >= CONFIDENCE_RANK[b] ? a : b;
|
|
5646
5659
|
var relationConfidence = (relation) => relation.metadata?.confidence === "fuzzy" ? "fuzzy" : relation.provenance;
|
|
@@ -5901,7 +5914,7 @@ var projectRetrievalIndex = (options) => {
|
|
|
5901
5914
|
};
|
|
5902
5915
|
const contentHash = sha256NormalizedV1({
|
|
5903
5916
|
projectionVersion: RETRIEVAL_PROJECTION_VERSION,
|
|
5904
|
-
|
|
5917
|
+
observationHash: snapshotObservationHash(snapshot),
|
|
5905
5918
|
overlayHash: base.overlayHash,
|
|
5906
5919
|
configurationHash: base.configurationHash,
|
|
5907
5920
|
lexiconVersion: base.lexiconVersion,
|
|
@@ -8014,6 +8027,50 @@ var loadFreshDocBridgeIndex = (root, config) => {
|
|
|
8014
8027
|
return index;
|
|
8015
8028
|
};
|
|
8016
8029
|
|
|
8030
|
+
// src/discovery/reproducibility.ts
|
|
8031
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
8032
|
+
var NOT_CHECKED = (skipped) => ({
|
|
8033
|
+
checked: false,
|
|
8034
|
+
skipped,
|
|
8035
|
+
ignored: []
|
|
8036
|
+
});
|
|
8037
|
+
var git = (root, args, options = {}) => {
|
|
8038
|
+
try {
|
|
8039
|
+
return execFileSync2("git", args, {
|
|
8040
|
+
cwd: root,
|
|
8041
|
+
encoding: "utf8",
|
|
8042
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
8043
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
8044
|
+
...options.input === void 0 ? {} : { input: options.input }
|
|
8045
|
+
});
|
|
8046
|
+
} catch (error) {
|
|
8047
|
+
const status = error.status;
|
|
8048
|
+
if (options.allowExit1 && status === 1) return error.stdout ?? "";
|
|
8049
|
+
return void 0;
|
|
8050
|
+
}
|
|
8051
|
+
};
|
|
8052
|
+
var checkIndexReproducibility = (root, indexPath, paths) => {
|
|
8053
|
+
if (git(root, ["rev-parse", "--is-inside-work-tree"]) === void 0) return NOT_CHECKED("no-git");
|
|
8054
|
+
if (git(root, ["ls-files", "--error-unmatch", "--", indexPath]) === void 0) {
|
|
8055
|
+
return NOT_CHECKED("index-untracked");
|
|
8056
|
+
}
|
|
8057
|
+
if (paths.length === 0) return { checked: true, ignored: [] };
|
|
8058
|
+
const unique3 = [...new Set(paths)].sort();
|
|
8059
|
+
const output = git(root, ["check-ignore", "--verbose", "-z", "--stdin"], {
|
|
8060
|
+
input: `${unique3.join("\0")}\0`,
|
|
8061
|
+
allowExit1: true
|
|
8062
|
+
});
|
|
8063
|
+
if (output === void 0) return NOT_CHECKED("no-git");
|
|
8064
|
+
const fields = output.split("\0");
|
|
8065
|
+
const ignored = [];
|
|
8066
|
+
for (let index = 0; index + 3 < fields.length; index += 4) {
|
|
8067
|
+
const [source, line, pattern, path] = [fields[index], fields[index + 1], fields[index + 2], fields[index + 3]];
|
|
8068
|
+
if (!path) continue;
|
|
8069
|
+
ignored.push({ path, rule: `${source ?? "?"}:${line ?? "?"}:${pattern ?? "?"}` });
|
|
8070
|
+
}
|
|
8071
|
+
return { checked: true, ignored: ignored.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
8072
|
+
};
|
|
8073
|
+
|
|
8017
8074
|
// src/gates/run-gates.ts
|
|
8018
8075
|
var RESERVED_GATE_IDS = /* @__PURE__ */ new Set(["link-rot", "routing-currency", "bootstrap-size"]);
|
|
8019
8076
|
var runGate = (root, config, id) => {
|
|
@@ -8031,6 +8088,38 @@ var runGate = (root, config, id) => {
|
|
|
8031
8088
|
if (id === "human-guide-links") return runHumanGuideLinksGate(root, config);
|
|
8032
8089
|
if (id === "okf-type") return runOkfTypeGate(root, config);
|
|
8033
8090
|
if (id === "docs-style") return runDocsStyleGate(root, config);
|
|
8091
|
+
if (id === "index-reproducible") {
|
|
8092
|
+
let index;
|
|
8093
|
+
try {
|
|
8094
|
+
index = loadDocBridgeIndex(root, config);
|
|
8095
|
+
} catch (error) {
|
|
8096
|
+
if (error instanceof IndexNotFoundError) return { id, ok: false, message: error.message };
|
|
8097
|
+
throw error;
|
|
8098
|
+
}
|
|
8099
|
+
const result = checkIndexReproducibility(
|
|
8100
|
+
root,
|
|
8101
|
+
config.index?.outFile ?? ".doc-bridge/index.json",
|
|
8102
|
+
index.knowledge.map((entry) => entry.path)
|
|
8103
|
+
);
|
|
8104
|
+
if (!result.checked) {
|
|
8105
|
+
return {
|
|
8106
|
+
id,
|
|
8107
|
+
ok: true,
|
|
8108
|
+
message: result.skipped === "index-untracked" ? "Index is not committed, so nothing has to reproduce it" : "Not a Git checkout; reproducibility was not checked"
|
|
8109
|
+
};
|
|
8110
|
+
}
|
|
8111
|
+
if (result.ignored.length === 0) {
|
|
8112
|
+
return { id, ok: true, message: "Every indexed path is committed" };
|
|
8113
|
+
}
|
|
8114
|
+
const sample = result.ignored.slice(0, 5).map((entry) => `${entry.path} (${entry.rule})`);
|
|
8115
|
+
return {
|
|
8116
|
+
id,
|
|
8117
|
+
ok: false,
|
|
8118
|
+
message: `${result.ignored.length} indexed path(s) are ignored by Git, so a clean checkout builds a different index. Add them to safety.exclude.`,
|
|
8119
|
+
expected: "every indexed path is committed",
|
|
8120
|
+
actual: sample.join(", ")
|
|
8121
|
+
};
|
|
8122
|
+
}
|
|
8034
8123
|
if (id !== "index-freshness") throw new Error(`Unsupported gate "${id}"`);
|
|
8035
8124
|
let current;
|
|
8036
8125
|
try {
|
|
@@ -9061,7 +9150,7 @@ var draftMemoryPromotion = (classifications) => {
|
|
|
9061
9150
|
};
|
|
9062
9151
|
|
|
9063
9152
|
// src/memory/github-pr.ts
|
|
9064
|
-
import { execFileSync as
|
|
9153
|
+
import { execFileSync as execFileSync3, spawnSync } from "child_process";
|
|
9065
9154
|
import { existsSync as existsSync15, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
9066
9155
|
import { join as join15 } from "path";
|
|
9067
9156
|
var run = (cmd, args, cwd) => {
|
|
@@ -9188,7 +9277,7 @@ ${auth.out}`
|
|
|
9188
9277
|
];
|
|
9189
9278
|
let prUrl = "";
|
|
9190
9279
|
try {
|
|
9191
|
-
prUrl =
|
|
9280
|
+
prUrl = execFileSync3("gh", prArgs, { cwd: root, encoding: "utf8" }).trim();
|
|
9192
9281
|
} catch (error) {
|
|
9193
9282
|
const message = error instanceof Error ? error.message : String(error);
|
|
9194
9283
|
return {
|
|
@@ -10159,6 +10248,16 @@ var buildIssues = (coverage) => {
|
|
|
10159
10248
|
action: "ak-docs index"
|
|
10160
10249
|
});
|
|
10161
10250
|
}
|
|
10251
|
+
const { ignored } = coverage.reproducibility;
|
|
10252
|
+
if (ignored.length > 0) {
|
|
10253
|
+
const sample = ignored.slice(0, 5).map((entry) => `${entry.path} (${entry.rule})`);
|
|
10254
|
+
issues.push({
|
|
10255
|
+
severity: "warn",
|
|
10256
|
+
code: "index-not-reproducible",
|
|
10257
|
+
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` : ""}.`,
|
|
10258
|
+
action: "edit doc-bridge.config.json # add the generated paths to safety.exclude"
|
|
10259
|
+
});
|
|
10260
|
+
}
|
|
10162
10261
|
for (const id of coverage.packages.missingAgentDoc) {
|
|
10163
10262
|
issues.push({
|
|
10164
10263
|
severity: "warn",
|
|
@@ -10296,7 +10395,12 @@ var runDoctor = (root, config) => {
|
|
|
10296
10395
|
message: freshnessMessage,
|
|
10297
10396
|
hasIndex
|
|
10298
10397
|
},
|
|
10299
|
-
gates
|
|
10398
|
+
gates,
|
|
10399
|
+
reproducibility: checkIndexReproducibility(
|
|
10400
|
+
root,
|
|
10401
|
+
config.index?.outFile ?? ".doc-bridge/index.json",
|
|
10402
|
+
index.knowledge.map((entry) => entry.path)
|
|
10403
|
+
)
|
|
10300
10404
|
};
|
|
10301
10405
|
const issues = buildIssues(coverage);
|
|
10302
10406
|
const score = computeScore(coverage);
|
|
@@ -16972,7 +17076,7 @@ var loadProject = (configPath) => {
|
|
|
16972
17076
|
const root = projectRootFromConfigPath(path, config.project?.root);
|
|
16973
17077
|
return { config, configPath: path, root };
|
|
16974
17078
|
};
|
|
16975
|
-
var indexDiagnostics = (config, result) => {
|
|
17079
|
+
var indexDiagnostics = (root, config, result) => {
|
|
16976
17080
|
const diagnostics = [];
|
|
16977
17081
|
const onlyDoc = result.index.knowledge.length === 1 ? result.index.knowledge[0] : void 0;
|
|
16978
17082
|
if (onlyDoc?.path === config.corpus.agent.index) {
|
|
@@ -16987,6 +17091,18 @@ var indexDiagnostics = (config, result) => {
|
|
|
16987
17091
|
"No ownership handoffs yet. Add routing.options.ownership, package frontmatter (package + editRoot), or a monorepo plugin."
|
|
16988
17092
|
);
|
|
16989
17093
|
}
|
|
17094
|
+
const reproducibility = checkIndexReproducibility(
|
|
17095
|
+
root,
|
|
17096
|
+
config.index?.outFile ?? ".doc-bridge/index.json",
|
|
17097
|
+
result.index.knowledge.map((entry) => entry.path)
|
|
17098
|
+
);
|
|
17099
|
+
if (reproducibility.ignored.length > 0) {
|
|
17100
|
+
const sample = reproducibility.ignored.slice(0, 5).map((entry) => `${entry.path} (${entry.rule})`);
|
|
17101
|
+
diagnostics.push(
|
|
17102
|
+
`${reproducibility.ignored.length} indexed path(s) are ignored by Git: ${sample.join(", ")}${reproducibility.ignored.length > sample.length ? `, and ${reproducibility.ignored.length - sample.length} more` : ""}.`,
|
|
17103
|
+
"A clean checkout will not have them, so it builds a different index. Add them to safety.exclude in doc-bridge.config.json."
|
|
17104
|
+
);
|
|
17105
|
+
}
|
|
16990
17106
|
return diagnostics;
|
|
16991
17107
|
};
|
|
16992
17108
|
var diagnosticNextCommands = (config, result) => {
|
|
@@ -18114,7 +18230,7 @@ var runCli2 = (argv) => {
|
|
|
18114
18230
|
});
|
|
18115
18231
|
}
|
|
18116
18232
|
const result = buildDocBridgeIndex({ root, config });
|
|
18117
|
-
const diagnostics = indexDiagnostics(config, result);
|
|
18233
|
+
const diagnostics = indexDiagnostics(root, config, result);
|
|
18118
18234
|
const handoffCount = Object.keys(result.index.handoffs ?? {}).length;
|
|
18119
18235
|
writeJson2({
|
|
18120
18236
|
ok: true,
|