knodin 0.10.5 → 0.10.7
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/dist/bin/cli.js +19 -3
- package/dist/src/credential-patterns.js +90 -0
- package/dist/src/diagnostics.js +12 -2
- package/dist/src/engine/candidate-database.js +37 -0
- package/dist/src/engine/index.js +92 -18
- package/dist/src/failure-diagnosis.js +7 -1
- package/dist/src/output-compression.js +14 -13
- package/docs/releases/0.10.6.md +139 -0
- package/docs/releases/0.10.7.md +79 -0
- package/package.json +31 -29
package/dist/bin/cli.js
CHANGED
|
@@ -401,7 +401,13 @@ function formatStatusHuman(result) {
|
|
|
401
401
|
const semanticNote = result.semanticReadiness && result.semanticReadiness !== "ready"
|
|
402
402
|
? ` Semantic search coverage is ${result.semanticReadiness}: \`search\` will under-return until embedding completes (\`knodin index\`).`
|
|
403
403
|
: "";
|
|
404
|
-
|
|
404
|
+
// `countsUnknown` means the graph could not be read, so these are placeholders
|
|
405
|
+
// rather than measurements. Printing "0 indexed files" makes an absent graph
|
|
406
|
+
// indistinguishable from a fully destroyed one.
|
|
407
|
+
const indexedCounts = result.coverage.countsUnknown
|
|
408
|
+
? "indexed and symbol counts unknown; the graph could not be read"
|
|
409
|
+
: `${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols`;
|
|
410
|
+
const coverage = `${result.coverage.sourceFiles} source files, ${indexedCounts}${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}`;
|
|
405
411
|
if (result.status === "indexing" && result.activity) {
|
|
406
412
|
const count = result.activity.phaseTotal === undefined
|
|
407
413
|
? ""
|
|
@@ -437,8 +443,18 @@ function formatStatusHuman(result) {
|
|
|
437
443
|
return `Graph content is intact but evidence is stale (${coverage}).${result.verification.mode === "persisted-audit"
|
|
438
444
|
? ` Cached deep-audit evidence is from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness was probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
|
|
439
445
|
: ""}\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
|
|
440
|
-
|
|
441
|
-
|
|
446
|
+
// When the graph could not be read, every source file lands in `missing.files`
|
|
447
|
+
// because none of them are indexed. That is ONE structural problem, not one
|
|
448
|
+
// per file: a 94-file repository with no database reported "95 issue(s)" and
|
|
449
|
+
// led with an arbitrary source filename, so an absent graph read as damage
|
|
450
|
+
// proportional to repository size. `missing.files` still carries the repair
|
|
451
|
+
// worklist — only the count and the headline shown to a human change here.
|
|
452
|
+
const outstanding = result.coverage.countsUnknown
|
|
453
|
+
? result.missing.records.length
|
|
454
|
+
: result.missing.files.length + result.missing.records.length;
|
|
455
|
+
const firstIssue = result.coverage.countsUnknown
|
|
456
|
+
? result.missing.records[0]
|
|
457
|
+
: (result.missing.files[0] ?? result.missing.records[0]);
|
|
442
458
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
443
459
|
const repairCommand = result.lifecycle?.status === "degraded" &&
|
|
444
460
|
result.missing.records.every((record) => result.lifecycle?.issues.includes(record))
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One credential-pattern set, shared by every redactor.
|
|
3
|
+
*
|
|
4
|
+
* knodin previously carried two independent lists — one in `diagnostics.ts` for
|
|
5
|
+
* recorded failures, one in `output-compression.ts` for captured command output
|
|
6
|
+
* — and they drifted apart in OPPOSITE directions. Diagnostics knew Slack and
|
|
7
|
+
* OpenAI but not AWS or JWTs; output compression knew AWS and JWTs but missed
|
|
8
|
+
* Slack, OpenAI and GitHub's fine-grained tokens. Neither list was a superset of
|
|
9
|
+
* the other, so a credential redacted on one path was disclosed on the other,
|
|
10
|
+
* and patching either one alone would only let them drift again (KNODIN-11).
|
|
11
|
+
*
|
|
12
|
+
* Callers keep their own replacement text: diagnostics collapses a match to a
|
|
13
|
+
* single marker, output compression names the provider. `preservedGroups` is
|
|
14
|
+
* what makes that possible — the leading captures a caller may echo back
|
|
15
|
+
* verbatim, so `password=` survives while its value does not.
|
|
16
|
+
*
|
|
17
|
+
* Fresh `RegExp` objects are built per call. A shared global regex carries
|
|
18
|
+
* `lastIndex` between uses, which turns a redactor into something that depends
|
|
19
|
+
* on what it scrubbed previously.
|
|
20
|
+
*/
|
|
21
|
+
export function credentialPatterns() {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
// `name=value` in any of its spellings. An optional auth scheme is
|
|
25
|
+
// consumed BEFORE the value, because `authorization: Bearer <token>`
|
|
26
|
+
// otherwise matches only the word "Bearer" — redacting the label and
|
|
27
|
+
// preserving the credential, exactly inverted (KNODIN-10).
|
|
28
|
+
label: "credential",
|
|
29
|
+
pattern: /\b(authorization|password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(?:(?:bearer|basic|digest|token)\s+)?[^\s,;]+/gi,
|
|
30
|
+
preservedGroups: 2,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// GitHub issues every format with an UNDERSCORE. A hyphen-only pattern
|
|
34
|
+
// matched a shape GitHub never mints and missed every shape it does, so
|
|
35
|
+
// both separators are accepted. `github_pat` is listed explicitly: it
|
|
36
|
+
// does not fit the gh[pousr] shape.
|
|
37
|
+
label: "github-token",
|
|
38
|
+
pattern: /\b(?:gh[pousr]|github_pat)[-_]\w{20,}\b/g,
|
|
39
|
+
preservedGroups: 0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "slack-token",
|
|
43
|
+
pattern: /\bxox[baprse]-[A-Za-z0-9-]{10,}\b/g,
|
|
44
|
+
preservedGroups: 0,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: "openai-key",
|
|
48
|
+
pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
49
|
+
preservedGroups: 0,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "aws-access-key",
|
|
53
|
+
pattern: /\bAKIA[A-Z0-9]{16}\b/g,
|
|
54
|
+
preservedGroups: 0,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "jwt",
|
|
58
|
+
pattern: /\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g,
|
|
59
|
+
preservedGroups: 0,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
// Bearer outside an `authorization:` assignment — a bare header line, or
|
|
63
|
+
// a curl command echoed into a log.
|
|
64
|
+
label: "bearer-token",
|
|
65
|
+
pattern: /\b(Bearer\s+)[-\w.~+/]{12,}=*/gi,
|
|
66
|
+
preservedGroups: 1,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Apply every shared pattern, letting the caller render the replacement.
|
|
72
|
+
*
|
|
73
|
+
* `marker` receives the provider label; preserved groups are prepended to
|
|
74
|
+
* whatever it returns.
|
|
75
|
+
*/
|
|
76
|
+
export function redactCredentials(text, marker) {
|
|
77
|
+
let result = text;
|
|
78
|
+
let count = 0;
|
|
79
|
+
for (const { label, pattern, preservedGroups } of credentialPatterns()) {
|
|
80
|
+
result = result.replace(pattern, (...values) => {
|
|
81
|
+
count++;
|
|
82
|
+
const kept = values
|
|
83
|
+
.slice(1, 1 + preservedGroups)
|
|
84
|
+
.map((value) => (typeof value === "string" ? value : ""))
|
|
85
|
+
.join("");
|
|
86
|
+
return `${kept}${marker(label)}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { text: result, count };
|
|
90
|
+
}
|
package/dist/src/diagnostics.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import zlib from "node:zlib";
|
|
8
8
|
import { compareBytes } from "./compare.js";
|
|
9
|
+
import { redactCredentials } from "./credential-patterns.js";
|
|
9
10
|
const CONFIG_PATH = ".knodin/diagnostics/config.json";
|
|
10
11
|
const JOURNAL_PATH = ".knodin/diagnostics/events.jsonl";
|
|
11
12
|
const JOURNAL_LOCK_PATH = ".knodin/diagnostics/events.lock";
|
|
@@ -528,8 +529,17 @@ function scrubText(raw, repo) {
|
|
|
528
529
|
};
|
|
529
530
|
for (const exact of [repo, os.homedir()].filter(Boolean).sort((a, b) => b.length - a.length))
|
|
530
531
|
replace(new RegExp(exact.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`), "g"), "<path>");
|
|
531
|
-
|
|
532
|
-
|
|
532
|
+
// Provider credentials, from the set shared with output-compression (see
|
|
533
|
+
// credential-patterns.ts). These used to be maintained here separately, and
|
|
534
|
+
// the two lists drifted: this one knew Slack and OpenAI but not AWS keys or
|
|
535
|
+
// JWTs, while output compression knew those and missed these. A credential
|
|
536
|
+
// safe on one path was disclosed on the other (KNODIN-11).
|
|
537
|
+
//
|
|
538
|
+
// `redactCredentials` counts its own replacements, so they are added to the
|
|
539
|
+
// running total rather than counted by the local `replace` helper.
|
|
540
|
+
const credentials = redactCredentials(value, () => "<secret>");
|
|
541
|
+
value = credentials.text;
|
|
542
|
+
redactions += credentials.count;
|
|
533
543
|
replace(/\b[A-Z]:\\(?:[^\s<>:"|?*]+\\)*[^\s<>:"|?*]*/g, "<path>");
|
|
534
544
|
replace(/(?:^|[\s('"`])\/(?:[^\s)'"`]+\/)*[^\s)'"`]*/g, "<path>");
|
|
535
545
|
replace(/\b(?:[A-Za-z0-9_.-]+\/)+(?:[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,12})\b/g, "<path>");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isSealedDatabase } from "./seal.js";
|
|
5
|
+
import { Database } from "./sqlite.js";
|
|
4
6
|
import { resolveDbPath, resolveStateDir } from "./state-paths.js";
|
|
5
7
|
const PROMOTION_SCHEMA_VERSION = 1;
|
|
6
8
|
const CANDIDATES_DIRECTORY = "candidates";
|
|
@@ -166,6 +168,29 @@ export function recoverInterruptedPromotion(repo) {
|
|
|
166
168
|
fsyncDirectory(path.dirname(marker));
|
|
167
169
|
return outcome;
|
|
168
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Whether the database currently at the active path carries embedded source.
|
|
173
|
+
*
|
|
174
|
+
* An active database that cannot be opened reports false ON PURPOSE. Replacing
|
|
175
|
+
* a damaged graph is exactly what promotion exists to do, and a graph too
|
|
176
|
+
* corrupt to open is the ordinary case for that — refusing here would break
|
|
177
|
+
* repair for the situation it is most needed in. The trade is deliberate: a
|
|
178
|
+
* sealed artifact that is ALSO unreadable can still be replaced, but such an
|
|
179
|
+
* artifact is already unrecoverable.
|
|
180
|
+
*/
|
|
181
|
+
function activeDatabaseIsSealed(activePath) {
|
|
182
|
+
let db = null;
|
|
183
|
+
try {
|
|
184
|
+
db = new Database(activePath, { readonly: true });
|
|
185
|
+
return isSealedDatabase(db);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
db?.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
169
194
|
/** Same-filesystem, marker-backed promotion. Caller must close and audit both databases first. */
|
|
170
195
|
export function promoteCandidateFile(repo, candidate) {
|
|
171
196
|
assertCandidate(repo, candidate);
|
|
@@ -176,6 +201,18 @@ export function promoteCandidateFile(repo, candidate) {
|
|
|
176
201
|
for (const suffix of ["-wal", "-shm"])
|
|
177
202
|
if (fs.existsSync(`${candidate.databasePath}${suffix}`))
|
|
178
203
|
throw new Error("candidate database has uncheckpointed sidecar files");
|
|
204
|
+
// Promotion REPLACES the active database. When that file is a sealed
|
|
205
|
+
// artifact, replacing it destroys the only copy of the source embedded in
|
|
206
|
+
// it. `repair` reaches here with a candidate built from the working tree,
|
|
207
|
+
// so an artifact sitting at the active path was emptied by a command whose
|
|
208
|
+
// entire purpose is to make a graph healthier (KNODIN-9).
|
|
209
|
+
//
|
|
210
|
+
// This is the chokepoint rather than a guard inside `repair`, because every
|
|
211
|
+
// promotion has the same consequence regardless of which caller arrives.
|
|
212
|
+
// An ordinary working graph is never sealed, so normal promotion and repair
|
|
213
|
+
// are unaffected.
|
|
214
|
+
if (fs.existsSync(activePath) && activeDatabaseIsSealed(activePath))
|
|
215
|
+
throw new Error("active database is a sealed artifact; promotion would replace it");
|
|
179
216
|
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
180
217
|
fsyncFile(candidate.databasePath);
|
|
181
218
|
const backupPath = path.join(stateDir, `db.sqlite.backup.${Date.now()}.${crypto.randomUUID().slice(0, 8)}`);
|
package/dist/src/engine/index.js
CHANGED
|
@@ -8779,26 +8779,28 @@ export async function getOrInitDb(repoPath, options = {}) {
|
|
|
8779
8779
|
else {
|
|
8780
8780
|
const stateDir = resolveStateDir(normalizedPath);
|
|
8781
8781
|
await fs.promises.mkdir(stateDir, { recursive: true });
|
|
8782
|
-
// Ensure .knodin is
|
|
8783
|
-
//
|
|
8784
|
-
//
|
|
8782
|
+
// Ensure .knodin is ignored, through `.git/info/exclude` rather than
|
|
8783
|
+
// `.gitignore`.
|
|
8784
|
+
//
|
|
8785
|
+
// `.gitignore` is a SHARED file: it is tracked by convention, so
|
|
8786
|
+
// writing it put a change into someone's working tree that they did
|
|
8787
|
+
// not ask for and would carry into their next commit. That is the
|
|
8788
|
+
// one thing `knodin init --scope personal` promises not to do, and
|
|
8789
|
+
// this ran regardless of scope — a bare `knodin index` did it too
|
|
8790
|
+
// (KNODIN-1).
|
|
8791
|
+
//
|
|
8792
|
+
// `.git/info/exclude` has the same effect and is repo-local: git
|
|
8793
|
+
// never commits it, so nothing reaches a diff or a teammate.
|
|
8794
|
+
//
|
|
8795
|
+
// Skipped for mirrors: their state lives outside the clone, and the
|
|
8796
|
+
// clone must stay byte-identical to the remote so a refetch has
|
|
8797
|
+
// nothing of ours to discard.
|
|
8785
8798
|
if (mayWriteToRepository(normalizedPath)) {
|
|
8786
8799
|
try {
|
|
8787
|
-
|
|
8788
|
-
let gitignoreContent = "";
|
|
8789
|
-
if (fs.existsSync(gitignorePath)) {
|
|
8790
|
-
gitignoreContent = await fs.promises.readFile(gitignorePath, "utf-8");
|
|
8791
|
-
}
|
|
8792
|
-
const lines = gitignoreContent.split("\n").map((l) => l.trim());
|
|
8793
|
-
if (!lines.includes(".knodin") &&
|
|
8794
|
-
!lines.includes(".knodin/") &&
|
|
8795
|
-
!lines.includes("/.knodin")) {
|
|
8796
|
-
const prefix = gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n") ? "\n" : "";
|
|
8797
|
-
await fs.promises.appendFile(gitignorePath, `${prefix}\n# knodin\n.knodin\n`);
|
|
8798
|
-
}
|
|
8800
|
+
await excludeStateDirectoryLocally(normalizedPath);
|
|
8799
8801
|
}
|
|
8800
8802
|
catch (e) {
|
|
8801
|
-
console.error("Failed to
|
|
8803
|
+
console.error("Failed to exclude .knodin from git", e);
|
|
8802
8804
|
}
|
|
8803
8805
|
}
|
|
8804
8806
|
dbPath = path.join(stateDir, "db.sqlite");
|
|
@@ -8860,6 +8862,16 @@ export async function getOrInitDb(repoPath, options = {}) {
|
|
|
8860
8862
|
// Runs after the CREATE TABLEs below, since the table must exist.
|
|
8861
8863
|
const needsOrphanPurge = storedVersion < KNODIN_SCHEMA_VERSION;
|
|
8862
8864
|
const needsEmbeddingRepresentationRefresh = storedVersion >= 22 && storedVersion < 23;
|
|
8865
|
+
// An artifact sealed by an older build carries that build's schema
|
|
8866
|
+
// version, so a newer knodin opening it for write lands here and drops
|
|
8867
|
+
// every table below — including the embedded source, which no working
|
|
8868
|
+
// tree can supply again. Today's artifacts report the current version
|
|
8869
|
+
// and never reach it; one from a single release ago would.
|
|
8870
|
+
//
|
|
8871
|
+
// Refuse rather than skip: skipping would leave the caller holding a
|
|
8872
|
+
// database it believes is writable and current, and it is neither.
|
|
8873
|
+
if (storedVersion < LAST_REBUILD_SCHEMA_VERSION && isSealedDatabase(db))
|
|
8874
|
+
throw new Error("refusing to rebuild the schema of a sealed artifact: it would drop the embedded source, which is the only copy");
|
|
8863
8875
|
if (storedVersion < LAST_REBUILD_SCHEMA_VERSION) {
|
|
8864
8876
|
db.run("DROP TRIGGER IF EXISTS after_symbol_insert;");
|
|
8865
8877
|
db.run("DROP TRIGGER IF EXISTS after_symbol_delete;");
|
|
@@ -10082,6 +10094,45 @@ function parseUnifiedDiff(stdout) {
|
|
|
10082
10094
|
}
|
|
10083
10095
|
return modifiedFiles;
|
|
10084
10096
|
}
|
|
10097
|
+
/**
|
|
10098
|
+
* Ignore `.knodin/` through `.git/info/exclude`, which git never commits.
|
|
10099
|
+
*
|
|
10100
|
+
* Resolved with `git rev-parse --git-path` rather than assembled by hand:
|
|
10101
|
+
* inside a linked worktree `.git` is a FILE pointing elsewhere, so
|
|
10102
|
+
* `<repo>/.git/info/exclude` is not a path that exists. Idempotent — a second
|
|
10103
|
+
* call over an already-excluded repository writes nothing.
|
|
10104
|
+
*/
|
|
10105
|
+
async function excludeStateDirectoryLocally(repoPath) {
|
|
10106
|
+
// A directory that is not a git repository has nothing to exclude, and git
|
|
10107
|
+
// exits non-zero there. That is an ordinary state — knodin indexes plain
|
|
10108
|
+
// directories — so it must not surface as an error. The previous
|
|
10109
|
+
// `.gitignore` write never had to ask git anything, which is why this case
|
|
10110
|
+
// only appears now.
|
|
10111
|
+
let raw;
|
|
10112
|
+
try {
|
|
10113
|
+
raw = runGit(repoPath, ["rev-parse", "--git-path", "info/exclude"]).trim();
|
|
10114
|
+
}
|
|
10115
|
+
catch {
|
|
10116
|
+
return;
|
|
10117
|
+
}
|
|
10118
|
+
if (!raw)
|
|
10119
|
+
return;
|
|
10120
|
+
const excludePath = path.isAbsolute(raw) ? raw : path.resolve(repoPath, raw);
|
|
10121
|
+
let existing = "";
|
|
10122
|
+
try {
|
|
10123
|
+
existing = await fs.promises.readFile(excludePath, "utf-8");
|
|
10124
|
+
}
|
|
10125
|
+
catch (error) {
|
|
10126
|
+
if (error.code !== "ENOENT")
|
|
10127
|
+
throw error;
|
|
10128
|
+
}
|
|
10129
|
+
const entries = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
10130
|
+
if (entries.has(".knodin") || entries.has(".knodin/") || entries.has("/.knodin"))
|
|
10131
|
+
return;
|
|
10132
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
10133
|
+
await fs.promises.mkdir(path.dirname(excludePath), { recursive: true });
|
|
10134
|
+
await fs.promises.appendFile(excludePath, `${prefix}.knodin/\n`, "utf-8");
|
|
10135
|
+
}
|
|
10085
10136
|
function runGit(repoPath, args) {
|
|
10086
10137
|
return child_process.execFileSync("git", args, {
|
|
10087
10138
|
cwd: repoPath,
|
|
@@ -12941,6 +12992,9 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12941
12992
|
let indexedFiles = 0;
|
|
12942
12993
|
let filesWithSymbols = 0;
|
|
12943
12994
|
let lastSuccessfulReconciliation = null;
|
|
12995
|
+
// No database, or counters that threw, means the zeros below were
|
|
12996
|
+
// never measured.
|
|
12997
|
+
let countsUnknown = !db;
|
|
12944
12998
|
if (db) {
|
|
12945
12999
|
try {
|
|
12946
13000
|
schemaVersion =
|
|
@@ -12957,7 +13011,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12957
13011
|
}
|
|
12958
13012
|
catch {
|
|
12959
13013
|
// A schema migration can briefly make counters unavailable;
|
|
12960
|
-
// the live operation remains the authoritative status.
|
|
13014
|
+
// the live operation remains the authoritative status. Whatever
|
|
13015
|
+
// was read before the throw is partial, so report the counts as
|
|
13016
|
+
// unknown rather than presenting a half-filled tally as measured.
|
|
13017
|
+
countsUnknown = true;
|
|
12961
13018
|
}
|
|
12962
13019
|
if (!activeDb)
|
|
12963
13020
|
db.close();
|
|
@@ -12980,6 +13037,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12980
13037
|
? Math.round((Math.min(indexedFiles, sourceFiles.length) / sourceFiles.length) * 10000) / 100
|
|
12981
13038
|
: 100,
|
|
12982
13039
|
skipped,
|
|
13040
|
+
...(countsUnknown ? { countsUnknown: true } : {}),
|
|
12983
13041
|
},
|
|
12984
13042
|
orphaned: { embeddings: 0, references: 0, dependencies: 0 },
|
|
12985
13043
|
missing: { files: [], records: [] },
|
|
@@ -13011,6 +13069,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13011
13069
|
filesWithSymbols: 0,
|
|
13012
13070
|
percent: sourceFiles.length ? 0 : 100,
|
|
13013
13071
|
skipped: buildCoverageSkips(collected.skippedByExtension, null),
|
|
13072
|
+
// There is no database to count, so these are placeholders.
|
|
13073
|
+
countsUnknown: true,
|
|
13014
13074
|
},
|
|
13015
13075
|
orphaned: { embeddings: 0, references: 0, dependencies: 0 },
|
|
13016
13076
|
missing: {
|
|
@@ -13183,6 +13243,9 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13183
13243
|
filesWithSymbols: 0,
|
|
13184
13244
|
percent: sourceFiles.length ? 0 : 100,
|
|
13185
13245
|
skipped: coverageSkips,
|
|
13246
|
+
// The schema is not one this build can read, so index_state is
|
|
13247
|
+
// not counted here even though rows may well exist.
|
|
13248
|
+
countsUnknown: true,
|
|
13186
13249
|
},
|
|
13187
13250
|
orphaned: { embeddings: 0, references: 0, dependencies: 0 },
|
|
13188
13251
|
missing: { files: sourceFiles, records: schemaProblems },
|
|
@@ -13531,7 +13594,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13531
13594
|
else
|
|
13532
13595
|
counts.indexed++;
|
|
13533
13596
|
}
|
|
13534
|
-
else {
|
|
13597
|
+
else if (shouldPurgeMissingFile(db)) {
|
|
13535
13598
|
db.run("BEGIN TRANSACTION;");
|
|
13536
13599
|
try {
|
|
13537
13600
|
deleteSymbolsForFile(db, file);
|
|
@@ -13547,6 +13610,17 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13547
13610
|
}
|
|
13548
13611
|
counts.removed++;
|
|
13549
13612
|
}
|
|
13613
|
+
else {
|
|
13614
|
+
// A sealed artifact's rows ARE the artifact: it has no working
|
|
13615
|
+
// tree, so "this file is missing" is its normal condition rather
|
|
13616
|
+
// than drift to clean up (KNODIN-9).
|
|
13617
|
+
//
|
|
13618
|
+
// This was the sixth purge site and the only unguarded one.
|
|
13619
|
+
// `removeIndexState` above carries its own guard, which is why
|
|
13620
|
+
// the damage looked so strange: index_state survived while the
|
|
13621
|
+
// symbols, references and dependencies around it were deleted.
|
|
13622
|
+
counts.skipped++;
|
|
13623
|
+
}
|
|
13550
13624
|
committedWork = true;
|
|
13551
13625
|
setMeta(db, "repairPostprocessingPending", "1");
|
|
13552
13626
|
completedFiles = fileIndex + 1;
|
|
@@ -26,7 +26,13 @@ const CODE_EXTENSIONS = [
|
|
|
26
26
|
".cpp",
|
|
27
27
|
".h",
|
|
28
28
|
".hpp",
|
|
29
|
-
|
|
29
|
+
// Longest first. `referenceAt` takes the FIRST extension that matches at a
|
|
30
|
+
// cursor, so a shorter extension listed earlier wins and truncates the path:
|
|
31
|
+
// `src/main.cpp:3:4` parsed as `src/main.c` with no line, pointing diagnosis
|
|
32
|
+
// at a file that does not exist. `.tsx` only worked because it happened to
|
|
33
|
+
// precede `.ts`. Sorting makes longest-match the rule rather than an
|
|
34
|
+
// accident of list order.
|
|
35
|
+
].sort((left, right) => right.length - left.length);
|
|
30
36
|
const MAX_ANALYSIS_BYTES = 4 * 1024 * 1024;
|
|
31
37
|
const MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
32
38
|
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { redactCredentials } from "./credential-patterns.js";
|
|
4
5
|
import { resolveStateDir } from "./engine/state-paths.js";
|
|
5
6
|
const DEFAULT_LINE_BUDGET = 200;
|
|
6
7
|
const DEFAULT_BYTE_BUDGET = 16_384;
|
|
@@ -90,20 +91,20 @@ function splitEvent(event) {
|
|
|
90
91
|
lines.push({ raw: "", sourceBytes: 0 });
|
|
91
92
|
return lines;
|
|
92
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Redact credentials from captured output.
|
|
96
|
+
*
|
|
97
|
+
* The pattern set is shared with `diagnostics.ts` (see credential-patterns.ts).
|
|
98
|
+
* It used to be maintained here separately, and the two lists drifted: this one
|
|
99
|
+
* knew AWS keys and JWTs but missed Slack, OpenAI and GitHub's fine-grained
|
|
100
|
+
* tokens, while diagnostics missed AWS and JWTs. A credential safe on one path
|
|
101
|
+
* was disclosed on the other (KNODIN-11).
|
|
102
|
+
*
|
|
103
|
+
* The provider-named markers are kept, because a reader of compressed output
|
|
104
|
+
* benefits from knowing WHAT was removed.
|
|
105
|
+
*/
|
|
93
106
|
function redact(text) {
|
|
94
|
-
|
|
95
|
-
const replace = (pattern, replacement) => {
|
|
96
|
-
text = text.replace(pattern, (...values) => {
|
|
97
|
-
count++;
|
|
98
|
-
return typeof replacement === "string" ? replacement : replacement(...values);
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
replace(/\b(authorization|password|passwd|secret|token|api[_-]?key)\s*([:=])\s*([^\s,;]+)/gi, (_match, name, separator) => `${name}${separator}[REDACTED:credential]`);
|
|
102
|
-
replace(/\bAKIA[A-Z0-9]{16}\b/g, "[REDACTED:aws-access-key]");
|
|
103
|
-
replace(/\bgh[pousr]_\w{20,}\b/g, "[REDACTED:github-token]");
|
|
104
|
-
replace(/\bBearer\s+[-\w.~+/]{12,}=*\b/gi, "Bearer [REDACTED:bearer-token]");
|
|
105
|
-
replace(/\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g, "[REDACTED:jwt]");
|
|
106
|
-
return { text, count };
|
|
107
|
+
return redactCredentials(text, (label) => `[REDACTED:${label}]`);
|
|
107
108
|
}
|
|
108
109
|
function csiSequenceEnd(text, escapeIndex) {
|
|
109
110
|
for (let index = escapeIndex + 2; index < text.length; index++) {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# knodin 0.10.6
|
|
2
|
+
|
|
3
|
+
Four fixes, continuing the theme 0.10.2 through 0.10.5 have been working on: **a
|
|
4
|
+
result that looks complete while quietly being wrong**. Three of the four were
|
|
5
|
+
found the same way — by writing a test per case and watching which ones failed,
|
|
6
|
+
rather than by reading the code and deciding it looked correct.
|
|
7
|
+
|
|
8
|
+
Two of them disclose credentials, so read that section first if you have ever
|
|
9
|
+
attached a diagnostics bundle to a support request.
|
|
10
|
+
|
|
11
|
+
## Credentials survived redaction
|
|
12
|
+
|
|
13
|
+
`knodin` scrubs secrets out of recorded failures before they can reach a
|
|
14
|
+
diagnostics bundle — the artefact a user hands to someone else when asking for
|
|
15
|
+
help. Two things it claimed to redact, it did not.
|
|
16
|
+
|
|
17
|
+
**GitHub tokens were never redacted at all.** The pattern required a hyphen
|
|
18
|
+
after the prefix:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
\b(?:ghp|github_pat|sk|xox[baprs])-[-A-Za-z0-9_]{10,}\b
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
GitHub issues both its formats with an *underscore*. So the two GitHub
|
|
25
|
+
alternatives matched a shape GitHub never mints and missed every shape it does.
|
|
26
|
+
Slack and OpenAI genuinely do use a hyphen, so the separator now accepts either
|
|
27
|
+
rather than being swapped.
|
|
28
|
+
|
|
29
|
+
**An authorization header leaked the credential and redacted its label.** The
|
|
30
|
+
value pattern stops at whitespace, so for the standard form
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
authorization: Bearer <token>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
it matched only the word `Bearer` — replacing the part that identifies the
|
|
37
|
+
header and preserving the part that authenticates. Exactly inverted. An optional
|
|
38
|
+
known scheme is now consumed first, so the credential after it is what gets
|
|
39
|
+
replaced.
|
|
40
|
+
|
|
41
|
+
This was quiet in an unhelpful way. The journal stores only a *fingerprint* of
|
|
42
|
+
the scrubbed message, never the message, so an unredacted credential is
|
|
43
|
+
invisible locally and appears only in the bundle that gets shared. The tests
|
|
44
|
+
assert redaction by fingerprint equivalence for that reason, and by
|
|
45
|
+
value-independence — two messages differing only in the secret must fingerprint
|
|
46
|
+
identically — which is how the `Bearer` case was found. An assertion written
|
|
47
|
+
against expected output text would have passed, because the output *did* contain
|
|
48
|
+
a redaction marker.
|
|
49
|
+
|
|
50
|
+
Redaction is on by default, so this needed no unusual configuration to hit.
|
|
51
|
+
|
|
52
|
+
A second redactor, in output compression, has different gaps in the opposite
|
|
53
|
+
direction: it catches GitHub classic but not fine-grained, and misses Slack and
|
|
54
|
+
OpenAI. Neither pattern list is a superset of the other. That one is filed
|
|
55
|
+
rather than patched here, because adding three regexes to one of two diverging
|
|
56
|
+
lists would only re-create the problem later.
|
|
57
|
+
|
|
58
|
+
## `repair` emptied a sealed artifact
|
|
59
|
+
|
|
60
|
+
Pointed at a sealed artifact, `knodin repair` deleted its symbols, references
|
|
61
|
+
and dependencies. A sealed artifact's embedded source is the only copy of that
|
|
62
|
+
source, so this was unrecoverable.
|
|
63
|
+
|
|
64
|
+
0.10.5 audited five sites that purge rows for files missing from disk and
|
|
65
|
+
guarded all five. This was a **sixth**, in repair's own removal branch, which
|
|
66
|
+
never consulted the guard.
|
|
67
|
+
|
|
68
|
+
It also explains a symptom that made no sense while the mechanism was unknown:
|
|
69
|
+
`index_state` survived intact while everything around it was deleted.
|
|
70
|
+
`removeIndexState`, called *inside* that same block, carries its own guard and
|
|
71
|
+
declined correctly — so the damage was the guard working on one line and being
|
|
72
|
+
absent from the four above it.
|
|
73
|
+
|
|
74
|
+
Two adjacent hazards are guarded alongside it: a schema rebuild would drop every
|
|
75
|
+
table of an artifact sealed by an older build, and promotion would overwrite a
|
|
76
|
+
sealed artifact sitting at the active path.
|
|
77
|
+
|
|
78
|
+
## `status` reported an unreadable graph as a measured zero
|
|
79
|
+
|
|
80
|
+
A 94-file repository whose database was missing printed:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Graph or lifecycle needs repair: 95 issue(s) found
|
|
84
|
+
(94 source files, 0 indexed files, 0 files with symbols)
|
|
85
|
+
First issue: <an arbitrary source file>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`repair`, seconds later, reported 94 indexed and 53 with symbols. The two
|
|
89
|
+
commands never counted differently. `status` was rendering a graph it could not
|
|
90
|
+
read as a measurement of zero.
|
|
91
|
+
|
|
92
|
+
The `95` was arithmetic, not a tally: the issue count is
|
|
93
|
+
`missing.files + missing.records`, and the no-database branch fills
|
|
94
|
+
`missing.files` with every source file. Ninety-four files plus one record. So
|
|
95
|
+
the count scaled with repository size, and a graph that had simply never been
|
|
96
|
+
built read as damage proportional to how much code you have.
|
|
97
|
+
|
|
98
|
+
Three sites emitted zeros they had not measured, and now report the counts as
|
|
99
|
+
unknown instead:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Graph or lifecycle needs repair: 1 issue(s) found
|
|
103
|
+
(3 source files, indexed and symbol counts unknown; the graph could not be read)
|
|
104
|
+
First issue: local index database is missing
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`missing.files` is deliberately still populated. `repair` consumes it as its
|
|
108
|
+
worklist and `seal` reads it as dirty paths, so emptying it — the obvious way to
|
|
109
|
+
stop the count inflating — would have left repair with nothing to do.
|
|
110
|
+
|
|
111
|
+
## C++ diagnostics pointed at files that do not exist
|
|
112
|
+
|
|
113
|
+
Source extensions are matched first-listed rather than longest, and `.c`
|
|
114
|
+
preceded `.cpp`. A C++ stack frame did not merely lose precision:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
in at f (src/main.cpp:3:4)
|
|
118
|
+
was { path: "src/main.c", line: null, column: null }
|
|
119
|
+
now { path: "src/main.cpp", line: 3, column: 4 }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A path to a file that does not exist, with the location dropped. `.tsx` worked
|
|
123
|
+
only because it happened to precede `.ts`; longest-match is now the rule rather
|
|
124
|
+
than an accident of list order.
|
|
125
|
+
|
|
126
|
+
## Coverage measures the parse path again
|
|
127
|
+
|
|
128
|
+
Not a shipped behaviour change, but worth recording because it changes what the
|
|
129
|
+
project's own numbers mean.
|
|
130
|
+
|
|
131
|
+
Generic source files are parsed off the main thread, and v8 coverage instruments
|
|
132
|
+
only the isolate it runs in — so everything the parse path did executed inside a
|
|
133
|
+
worker thread where it could not be observed, and was reported as untested. On
|
|
134
|
+
an identical set of shards, with no test changed, disabling parse workers for
|
|
135
|
+
the coverage run alone moved branch coverage from 74.85% to 77.16%: 414 branches
|
|
136
|
+
the suite was already exercising.
|
|
137
|
+
|
|
138
|
+
Sixteen functions had been sitting in the uncovered map looking like dead code.
|
|
139
|
+
They were not.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# knodin 0.10.7
|
|
2
|
+
|
|
3
|
+
Two fixes, both about knodin doing something to your repository or your
|
|
4
|
+
credentials that you did not ask for.
|
|
5
|
+
|
|
6
|
+
## knodin no longer writes to your .gitignore
|
|
7
|
+
|
|
8
|
+
Indexing appended `.knodin` to `.gitignore`. That file is **shared**: tracked by
|
|
9
|
+
convention, so the edit landed in your working tree and would ride into your next
|
|
10
|
+
commit — and on a narrowly-scoped branch, `git add -A` swept it in.
|
|
11
|
+
|
|
12
|
+
It ran regardless of scope, so a bare `knodin index` did it too. That matters
|
|
13
|
+
because `knodin init --scope personal` exists precisely to promise that nothing
|
|
14
|
+
reaches git.
|
|
15
|
+
|
|
16
|
+
The same protection now goes to `.git/info/exclude`, which git never commits.
|
|
17
|
+
The rule is resolved through `git rev-parse --git-path` rather than assembled by
|
|
18
|
+
hand, because inside a linked worktree `.git` is a file and
|
|
19
|
+
`<repo>/.git/info/exclude` is not a path that exists. A directory that is not a
|
|
20
|
+
git repository has nothing to exclude and is left alone silently — knodin indexes
|
|
21
|
+
plain directories too.
|
|
22
|
+
|
|
23
|
+
### What this changed about freshness reporting
|
|
24
|
+
|
|
25
|
+
Worth knowing if you have ever read the `staleness` field.
|
|
26
|
+
|
|
27
|
+
Because every repository knodin touched carried that untracked `.gitignore`, its
|
|
28
|
+
working tree was permanently dirty, and a dirty tree forces the reconcile path.
|
|
29
|
+
So a branch switch reported `reconciled`. With a genuinely clean tree the same
|
|
30
|
+
switch reports `fresh`.
|
|
31
|
+
|
|
32
|
+
Nothing about the answer changed — measured directly, the query returns the
|
|
33
|
+
symbols of the branch you are on, with no trace of the one you left. `fresh` is
|
|
34
|
+
accurate: the graph does match the tree. Only the label is different, and it is
|
|
35
|
+
different because the tree is finally clean.
|
|
36
|
+
|
|
37
|
+
Three internal assertions had been reading that side effect rather than the
|
|
38
|
+
behaviour they named, and now measure the returned symbols instead.
|
|
39
|
+
|
|
40
|
+
## One credential-pattern set, not two
|
|
41
|
+
|
|
42
|
+
knodin had **two independent secret redactors** — one for diagnostics bundles,
|
|
43
|
+
one for compressed command output — each with its own list. They had drifted in
|
|
44
|
+
opposite directions:
|
|
45
|
+
|
|
46
|
+
| | diagnostics | output compression |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| GitHub classic | yes | yes |
|
|
49
|
+
| GitHub fine-grained | yes | **no** |
|
|
50
|
+
| Slack | yes | **no** |
|
|
51
|
+
| OpenAI | yes | **no** |
|
|
52
|
+
| AWS access key | **no** | yes |
|
|
53
|
+
| JWT | **no** | yes |
|
|
54
|
+
|
|
55
|
+
Neither list was a superset of the other, so the same log line was scrubbed on
|
|
56
|
+
one path and disclosed on the other. Redaction is on by default, so nothing
|
|
57
|
+
unusual had to be configured to hit this.
|
|
58
|
+
|
|
59
|
+
Both now use one shared set covering every row. Each caller keeps its own
|
|
60
|
+
replacement text: compressed output still names the provider, because a reader
|
|
61
|
+
benefits from knowing what was removed, while diagnostics collapses to a single
|
|
62
|
+
marker. Both preserve the key name and drop only the value, so `password=`
|
|
63
|
+
survives and its secret does not.
|
|
64
|
+
|
|
65
|
+
Tests assert every provider on **both** paths — a property no single-module test
|
|
66
|
+
could hold. What redaction must leave alone is pinned too: a 40-character hex
|
|
67
|
+
git SHA passes through untouched, because redacting commit SHAs would make diffs
|
|
68
|
+
and review output unreadable.
|
|
69
|
+
|
|
70
|
+
## Coverage
|
|
71
|
+
|
|
72
|
+
Measured across all sixteen shards: 78.22% branches, 87.77% statements, 91.20%
|
|
73
|
+
functions, 90.33% lines.
|
|
74
|
+
|
|
75
|
+
The 77.16% recorded in 0.10.6 came from fifteen shards, the sixteenth having
|
|
76
|
+
failed to write its report. That number was described at the time as a floor. It
|
|
77
|
+
was not — a missing shard shrinks the denominator faster than the numerator, so
|
|
78
|
+
it flattered the result. A partial-shard measurement is unusable rather than
|
|
79
|
+
conservative.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"knodin": {
|
|
5
5
|
"compatibility": "compatible"
|
|
6
6
|
},
|
|
@@ -23,32 +23,45 @@
|
|
|
23
23
|
"knodin": "dist/bin/launcher.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
+
"*.wasm",
|
|
27
|
+
"benchmarks/competitors/SYNTHESIS.md",
|
|
26
28
|
"dist",
|
|
27
|
-
"skills",
|
|
28
|
-
"docs/prompts/declare-multi-repository-system.md",
|
|
29
|
-
"docs/DEAD-CODE-AND-IMPACT.md",
|
|
30
|
-
"docs/DOCTOR-AND-UPDATES.md",
|
|
31
29
|
"docs/BACKUP-RETENTION.md",
|
|
32
|
-
"docs/DIAGNOSTICS.md",
|
|
33
30
|
"docs/BEHAVIORAL-CONTRACT.md",
|
|
34
|
-
"docs/
|
|
31
|
+
"docs/CLI.md",
|
|
32
|
+
"docs/COMMAND-OUTPUT-COMPRESSION.md",
|
|
35
33
|
"docs/COMPARISON.md",
|
|
36
34
|
"docs/COMPETITIVE-LANDSCAPE-2026-08.md",
|
|
37
|
-
"docs/
|
|
35
|
+
"docs/CONTAINED-EXECUTION.md",
|
|
36
|
+
"docs/DEAD-CODE-AND-IMPACT.md",
|
|
37
|
+
"docs/DEMO.md",
|
|
38
|
+
"docs/DIAGNOSTICS.md",
|
|
39
|
+
"docs/DOCTOR-AND-UPDATES.md",
|
|
40
|
+
"docs/GIT-HISTORY-REVIEW.md",
|
|
38
41
|
"docs/HANDOFF.md",
|
|
42
|
+
"docs/INDEXING-POLICY-AND-PROVENANCE.md",
|
|
39
43
|
"docs/INSTALLATION.md",
|
|
40
44
|
"docs/MCP.md",
|
|
41
|
-
"docs/COMMAND-OUTPUT-COMPRESSION.md",
|
|
42
|
-
"docs/CONTAINED-EXECUTION.md",
|
|
43
45
|
"docs/PROGRESSIVE-EVIDENCE.md",
|
|
44
|
-
"docs/GIT-HISTORY-REVIEW.md",
|
|
45
|
-
"docs/SCIP-IMPORT.md",
|
|
46
|
-
"docs/CLI.md",
|
|
47
46
|
"docs/PT-ACCESS-RECOMMENDATION.md",
|
|
48
|
-
"docs/REPOSITORIES-AND-WORKTREES.md",
|
|
49
47
|
"docs/RELEASE-0.3-EVIDENCE.md",
|
|
50
|
-
"docs/
|
|
48
|
+
"docs/REPOSITORIES-AND-WORKTREES.md",
|
|
49
|
+
"docs/SCIP-IMPORT.md",
|
|
51
50
|
"docs/SHARED-INDEX-CONTRACT.md",
|
|
51
|
+
"docs/SIGNED-UPDATES.md",
|
|
52
|
+
"docs/SYSTEMS-AND-RELATIONSHIPS.md",
|
|
53
|
+
"docs/TELEMETRY.md",
|
|
54
|
+
"docs/TOKEN-OPTIMIZER-SCORECARD.md",
|
|
55
|
+
"docs/assets/knodin-favicon.svg",
|
|
56
|
+
"docs/prompts/declare-multi-repository-system.md",
|
|
57
|
+
"docs/releases/0.10.0.md",
|
|
58
|
+
"docs/releases/0.10.1.md",
|
|
59
|
+
"docs/releases/0.10.2.md",
|
|
60
|
+
"docs/releases/0.10.3.md",
|
|
61
|
+
"docs/releases/0.10.4.md",
|
|
62
|
+
"docs/releases/0.10.5.md",
|
|
63
|
+
"docs/releases/0.10.6.md",
|
|
64
|
+
"docs/releases/0.10.7.md",
|
|
52
65
|
"docs/releases/0.3.0.md",
|
|
53
66
|
"docs/releases/0.4.0.md",
|
|
54
67
|
"docs/releases/0.4.1.md",
|
|
@@ -71,25 +84,14 @@
|
|
|
71
84
|
"docs/releases/0.8.6.md",
|
|
72
85
|
"docs/releases/0.8.7.md",
|
|
73
86
|
"docs/releases/0.9.0.md",
|
|
74
|
-
"docs/releases/0.10.0.md",
|
|
75
|
-
"docs/releases/0.10.1.md",
|
|
76
|
-
"docs/releases/0.10.2.md",
|
|
77
|
-
"docs/releases/0.10.3.md",
|
|
78
|
-
"docs/releases/0.10.4.md",
|
|
79
|
-
"docs/releases/0.10.5.md",
|
|
80
|
-
"docs/assets/knodin-favicon.svg",
|
|
81
|
-
"docs/SYSTEMS-AND-RELATIONSHIPS.md",
|
|
82
|
-
"docs/TELEMETRY.md",
|
|
83
|
-
"docs/TOKEN-OPTIMIZER-SCORECARD.md",
|
|
84
|
-
"benchmarks/competitors/SYNTHESIS.md",
|
|
85
87
|
"roadmap/competitive-roadmap.md",
|
|
86
88
|
"schemas/release-attestation-v1.schema.json",
|
|
87
|
-
"schemas/support-bundle-v2.schema.json",
|
|
88
|
-
"schemas/shared-index-config-v1.schema.json",
|
|
89
89
|
"schemas/shared-index-branch-pointer-v1.schema.json",
|
|
90
|
+
"schemas/shared-index-config-v1.schema.json",
|
|
90
91
|
"schemas/shared-index-manifest-v1.schema.json",
|
|
91
92
|
"schemas/shared-index-provenance-v1.schema.json",
|
|
92
|
-
"
|
|
93
|
+
"schemas/support-bundle-v2.schema.json",
|
|
94
|
+
"skills"
|
|
93
95
|
],
|
|
94
96
|
"engines": {
|
|
95
97
|
"node": ">=24.0.0"
|