knosky 0.4.1 → 0.6.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 +35 -0
- package/README.md +25 -1
- package/SECURITY.md +14 -0
- package/action/post-comment.mjs +89 -0
- package/action.yml +62 -0
- package/bin/knosky.mjs +39 -0
- package/core/benchmark-results.mjs +225 -0
- package/core/bundle.mjs +178 -0
- package/core/churn.mjs +6 -2
- package/core/ci.mjs +268 -0
- package/core/comparison.mjs +189 -0
- package/core/config.mjs +189 -0
- package/core/constants.mjs +13 -0
- package/core/cross-repo.mjs +111 -0
- package/core/destination.mjs +161 -0
- package/core/escalate.mjs +68 -0
- package/core/freshness.mjs +194 -0
- package/core/fs-indexer.mjs +10 -1
- package/core/key-store.mjs +348 -0
- package/core/ledger.mjs +141 -0
- package/core/lod.mjs +155 -0
- package/core/multi-model-benchmark.mjs +405 -0
- package/core/onboarding.mjs +223 -0
- package/core/overlays.mjs +45 -0
- package/core/pr-comment.mjs +198 -0
- package/core/protocol-spec.mjs +460 -0
- package/core/route.mjs +304 -0
- package/core/schema.mjs +275 -0
- package/mcp/server.mjs +34 -0
- package/package.json +3 -1
- package/renderer/city.template.html +824 -715
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// KnoSky PR-comment renderer (KSV2-CI2) — claims-disciplined public Markdown.
|
|
2
|
+
// Consumes CI1 artifacts (routeJson + safetyJson) and renders the polished
|
|
3
|
+
// PR-GPS comment that knosky ci posts on a pull request.
|
|
4
|
+
//
|
|
5
|
+
// CLAIMS DISCIPLINE — BINDING (D-162 §8):
|
|
6
|
+
// USE: "advisory PR-GPS report" / "advisory route you can ignore"
|
|
7
|
+
// USE: "route cache / freshness"
|
|
8
|
+
// USE: "reads metadata only / never uploads your code"
|
|
9
|
+
// USE: "network-silent (verify with --verify-airgap)"
|
|
10
|
+
// USE: "open protocol / reference implementation"
|
|
11
|
+
// NEVER: CI gate / blocks the build / gates the build
|
|
12
|
+
// NEVER: learns / learns your repo
|
|
13
|
+
// NEVER: understands your code
|
|
14
|
+
// NEVER: zero data risk
|
|
15
|
+
// NEVER: official standard
|
|
16
|
+
// NEVER: air-gap guarantee
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Internal helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format a confidence fraction [0..1] as a percent string, or 'n/a'.
|
|
24
|
+
* @param {unknown} val
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function fmtConfidence(val) {
|
|
28
|
+
if (typeof val === 'number' && isFinite(val)) {
|
|
29
|
+
return Math.round(val * 100) + '%';
|
|
30
|
+
}
|
|
31
|
+
return 'n/a';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract path + reason from a waypoint entry (string | { path, reason }).
|
|
36
|
+
* @param {unknown} wp
|
|
37
|
+
* @returns {{ path: string, reason: string }}
|
|
38
|
+
*/
|
|
39
|
+
function parsWaypoint(wp) {
|
|
40
|
+
if (typeof wp === 'string') return { path: wp, reason: '' };
|
|
41
|
+
const path = (wp && typeof wp.path === 'string') ? wp.path : '';
|
|
42
|
+
const reason = (wp && typeof wp.reason === 'string') ? wp.reason : '';
|
|
43
|
+
return { path, reason };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// renderPrComment — main export
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render the KnoSky PR-GPS comment as a Markdown string.
|
|
52
|
+
*
|
|
53
|
+
* This is a DRAFT — the final public wording is Paul's critical gate.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} opts
|
|
56
|
+
* @param {object} opts.routeJson CI1 route artifact ({ routes, base, head, advisory, … })
|
|
57
|
+
* @param {object} opts.safetyJson CI1 safety artifact ({ secrets_found, absolute_paths, … })
|
|
58
|
+
* @returns {string} Markdown comment body.
|
|
59
|
+
*/
|
|
60
|
+
export function renderPrComment({ routeJson = {}, safetyJson = {} } = {}) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
|
|
63
|
+
// -------------------------------------------------------------------------
|
|
64
|
+
// Header
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
lines.push('## 🧭 KnoSky PR-GPS');
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(
|
|
69
|
+
'> **DRAFT — advisory PR-GPS report.** ' +
|
|
70
|
+
'This is an advisory route you can ignore — it never gates or enforces a build outcome.',
|
|
71
|
+
);
|
|
72
|
+
lines.push('');
|
|
73
|
+
|
|
74
|
+
// -------------------------------------------------------------------------
|
|
75
|
+
// Per-file route sections
|
|
76
|
+
// -------------------------------------------------------------------------
|
|
77
|
+
const routes = Array.isArray(routeJson.routes) ? routeJson.routes : [];
|
|
78
|
+
|
|
79
|
+
if (routes.length === 0) {
|
|
80
|
+
lines.push('_No changed files detected in this PR — nothing to navigate._');
|
|
81
|
+
lines.push('');
|
|
82
|
+
} else {
|
|
83
|
+
for (const { file, route: routeDoc } of routes) {
|
|
84
|
+
lines.push(`### \`${file}\``);
|
|
85
|
+
lines.push('');
|
|
86
|
+
|
|
87
|
+
// Top ~5 waypoints
|
|
88
|
+
const waypoints = (Array.isArray(routeDoc && routeDoc.route) ? routeDoc.route : []).slice(0, 5);
|
|
89
|
+
if (waypoints.length === 0) {
|
|
90
|
+
lines.push('_No route waypoints found in the route cache for this file._');
|
|
91
|
+
} else {
|
|
92
|
+
lines.push('**Advisory route waypoints** _(top ' + waypoints.length + ', from route cache):_');
|
|
93
|
+
lines.push('');
|
|
94
|
+
for (const wp of waypoints) {
|
|
95
|
+
const { path, reason } = parsWaypoint(wp);
|
|
96
|
+
lines.push(`- \`${path}\`${reason ? ' — ' + reason : ''}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
lines.push('');
|
|
100
|
+
|
|
101
|
+
// Confidence
|
|
102
|
+
const conf = fmtConfidence(routeDoc && routeDoc.confidence);
|
|
103
|
+
lines.push(`**Confidence:** ${conf}`);
|
|
104
|
+
lines.push('');
|
|
105
|
+
|
|
106
|
+
// Freshness / coverage caveats
|
|
107
|
+
const caveats = Array.isArray(routeDoc && routeDoc.caveats) ? routeDoc.caveats : [];
|
|
108
|
+
const freshnessOrCoverage = caveats.filter(c =>
|
|
109
|
+
/recently changed|stale|freshness|coverage/i.test(c),
|
|
110
|
+
);
|
|
111
|
+
if (freshnessOrCoverage.length > 0) {
|
|
112
|
+
lines.push('**Route freshness / coverage caveats:**');
|
|
113
|
+
lines.push('');
|
|
114
|
+
for (const c of freshnessOrCoverage) lines.push(`- ${c}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Related tests
|
|
119
|
+
const tests = Array.isArray(routeDoc && routeDoc.tests) ? routeDoc.tests : [];
|
|
120
|
+
if (tests.length > 0) {
|
|
121
|
+
lines.push('**Related tests:**');
|
|
122
|
+
lines.push('');
|
|
123
|
+
for (const t of tests) {
|
|
124
|
+
const p = typeof t === 'string' ? t : (t && t.path);
|
|
125
|
+
if (p) lines.push(`- \`${p}\``);
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Related docs
|
|
131
|
+
const docs = Array.isArray(routeDoc && routeDoc.docs) ? routeDoc.docs : [];
|
|
132
|
+
if (docs.length > 0) {
|
|
133
|
+
lines.push('**Related docs:**');
|
|
134
|
+
lines.push('');
|
|
135
|
+
for (const d of docs) {
|
|
136
|
+
const p = typeof d === 'string' ? d : (d && d.path);
|
|
137
|
+
if (p) lines.push(`- \`${p}\``);
|
|
138
|
+
}
|
|
139
|
+
lines.push('');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
// Suggested reviewer / agent prompt block
|
|
146
|
+
// -------------------------------------------------------------------------
|
|
147
|
+
lines.push('---');
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('### 💬 Suggested reviewer / agent prompt');
|
|
150
|
+
lines.push('');
|
|
151
|
+
|
|
152
|
+
if (routes.length > 0) {
|
|
153
|
+
// Collect top waypoints across all changed files (unique, capped at 5)
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const topPaths = [];
|
|
156
|
+
for (const { route: routeDoc } of routes) {
|
|
157
|
+
const wps = Array.isArray(routeDoc && routeDoc.route) ? routeDoc.route : [];
|
|
158
|
+
for (const wp of wps.slice(0, 3)) {
|
|
159
|
+
const { path } = parsWaypoint(wp);
|
|
160
|
+
if (path && !seen.has(path)) {
|
|
161
|
+
seen.add(path);
|
|
162
|
+
topPaths.push(path);
|
|
163
|
+
if (topPaths.length >= 5) break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (topPaths.length >= 5) break;
|
|
167
|
+
}
|
|
168
|
+
const waypointList = topPaths.map(p => `\`${p}\``).join(', ');
|
|
169
|
+
lines.push(
|
|
170
|
+
`> Start your review at: ${waypointList || '_see routes above_'}. ` +
|
|
171
|
+
'Advisory only — verify before acting.',
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(
|
|
175
|
+
'> No route waypoints available for this PR. ' +
|
|
176
|
+
'Advisory only — verify before acting.',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push('');
|
|
181
|
+
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
// Footer — safety / privacy
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
lines.push('---');
|
|
186
|
+
lines.push('');
|
|
187
|
+
const secretsFound = (safetyJson && typeof safetyJson.secrets_found === 'number')
|
|
188
|
+
? safetyJson.secrets_found
|
|
189
|
+
: 0;
|
|
190
|
+
lines.push(
|
|
191
|
+
'_KnoSky reads metadata only — it never uploads your code. ' +
|
|
192
|
+
`Secrets scan: **${secretsFound}** potential secret(s) found in emitted artifacts. ` +
|
|
193
|
+
'network-silent (verify with --verify-airgap). ' +
|
|
194
|
+
'open protocol / reference implementation._',
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return lines.join('\n');
|
|
198
|
+
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// KnoSky machine-readable protocol spec (SAT-462).
|
|
2
|
+
// Exports authoritative JSON Schema (Draft 2020-12) objects for every protocol artifact.
|
|
3
|
+
// Pure ESM, no dependencies — the objects are plain JSON-serializable data.
|
|
4
|
+
//
|
|
5
|
+
// Four schemas are published:
|
|
6
|
+
// ROUTE_SCHEMA — the `route` artifact written by kc_route / kcRoute()
|
|
7
|
+
// INTENT_MANIFEST_SCHEMA — the `intent-manifest` artifact written by kc_bundle / kcBundle()
|
|
8
|
+
// CONFIG_SCHEMA — the `.knosky/config.yml` file (after YAML → JS object parse)
|
|
9
|
+
// CITY_SCHEMA — the city-data v2 envelope (core/CONTRACT.md)
|
|
10
|
+
//
|
|
11
|
+
// These complement, and must stay consistent with, the runtime validators in:
|
|
12
|
+
// core/schema.mjs (route + intent-manifest)
|
|
13
|
+
// core/config.mjs (config)
|
|
14
|
+
// core/contract.mjs (city)
|
|
15
|
+
//
|
|
16
|
+
// Protocol version constants (mirrors runtime constants in schema.mjs / config.mjs).
|
|
17
|
+
export const PROTOCOL_VERSION = '1.0'; // knosky_protocol field value
|
|
18
|
+
export const CITY_SCHEMA_VERSION = '2.0'; // schema_version field value
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Reusable sub-schemas
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** A path entry inside route[] / alternates[]: either a bare string or an object with a `path` field. */
|
|
25
|
+
const PATH_ENTRY = {
|
|
26
|
+
oneOf: [
|
|
27
|
+
{ type: 'string', minLength: 1 },
|
|
28
|
+
{
|
|
29
|
+
type: 'object',
|
|
30
|
+
required: ['path'],
|
|
31
|
+
properties: {
|
|
32
|
+
path: { type: 'string', minLength: 1 },
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: true,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** A KnoSky secret-scan result object embedded in the intent-manifest. */
|
|
40
|
+
const SECRET_SCAN_RESULT = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
required: ['status'],
|
|
43
|
+
properties: {
|
|
44
|
+
status: { type: 'string', enum: ['clean', 'blocked'] },
|
|
45
|
+
detail: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
additionalProperties: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** A provenance object embedded in every city node. */
|
|
51
|
+
const PROVENANCE = {
|
|
52
|
+
type: 'object',
|
|
53
|
+
required: ['store', 'ref'],
|
|
54
|
+
properties: {
|
|
55
|
+
store: { type: 'string', minLength: 1 },
|
|
56
|
+
ref: { type: 'string', minLength: 1 },
|
|
57
|
+
source_rev: { type: 'string' },
|
|
58
|
+
fetched_at: { type: 'string' },
|
|
59
|
+
},
|
|
60
|
+
additionalProperties: true,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// ROUTE_SCHEMA
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* JSON Schema (Draft 2020-12) for the KnoSky `route` artifact.
|
|
69
|
+
* Produced by kc_route / kcRoute(). Must agree with validateRouteDoc() in core/schema.mjs.
|
|
70
|
+
*
|
|
71
|
+
* @type {object}
|
|
72
|
+
*/
|
|
73
|
+
export const ROUTE_SCHEMA = {
|
|
74
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
75
|
+
$id: 'https://knosky.com/schemas/route.json',
|
|
76
|
+
title: 'KnoSky Route',
|
|
77
|
+
description:
|
|
78
|
+
'Advisory navigation route produced by kc_route. ' +
|
|
79
|
+
'Structural metadata only — never code analysis. ' +
|
|
80
|
+
'Always advisory: the `advisory` field is invariantly true.',
|
|
81
|
+
type: 'object',
|
|
82
|
+
required: [
|
|
83
|
+
'knosky_protocol',
|
|
84
|
+
'artifact_type',
|
|
85
|
+
'advisory',
|
|
86
|
+
'generated_at',
|
|
87
|
+
'destination',
|
|
88
|
+
'route',
|
|
89
|
+
'alternates',
|
|
90
|
+
'caveats',
|
|
91
|
+
'confidence',
|
|
92
|
+
],
|
|
93
|
+
additionalProperties: true,
|
|
94
|
+
properties: {
|
|
95
|
+
knosky_protocol: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
const: '1.0',
|
|
98
|
+
description: 'Protocol version. Always "1.0" for this schema.',
|
|
99
|
+
},
|
|
100
|
+
artifact_type: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
const: 'route',
|
|
103
|
+
description: 'Discriminator field. Always "route" for this artifact.',
|
|
104
|
+
},
|
|
105
|
+
advisory: {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
const: true,
|
|
108
|
+
description: 'Invariantly true — routes are advisory, never authoritative.',
|
|
109
|
+
},
|
|
110
|
+
generated_at: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
format: 'date-time',
|
|
113
|
+
description: 'ISO-8601 timestamp at which this document was generated.',
|
|
114
|
+
},
|
|
115
|
+
source_rev: {
|
|
116
|
+
type: ['string', 'null'],
|
|
117
|
+
description: 'VCS revision (e.g. git commit SHA) of the city index used. Null when unknown.',
|
|
118
|
+
},
|
|
119
|
+
destination: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'The navigation target string supplied by the caller.',
|
|
122
|
+
},
|
|
123
|
+
route: {
|
|
124
|
+
type: 'array',
|
|
125
|
+
items: PATH_ENTRY,
|
|
126
|
+
description: 'Ordered primary waypoints. Each entry is a path string or { path, id, reason, score }.',
|
|
127
|
+
},
|
|
128
|
+
alternates: {
|
|
129
|
+
type: 'array',
|
|
130
|
+
items: PATH_ENTRY,
|
|
131
|
+
description: 'Alternate waypoints beyond the primary route limit.',
|
|
132
|
+
},
|
|
133
|
+
caveats: {
|
|
134
|
+
type: 'array',
|
|
135
|
+
items: { type: 'string' },
|
|
136
|
+
description:
|
|
137
|
+
'Advisory notes. Always contains at least the mandatory advisory caveat. ' +
|
|
138
|
+
'May include churn, coverage, and staleness warnings.',
|
|
139
|
+
},
|
|
140
|
+
confidence: {
|
|
141
|
+
type: 'number',
|
|
142
|
+
minimum: 0,
|
|
143
|
+
maximum: 1,
|
|
144
|
+
description: 'Estimated confidence of the route [0..1]. 0 = no signal; ~0.95 = direct file match.',
|
|
145
|
+
},
|
|
146
|
+
tests: {
|
|
147
|
+
type: 'array',
|
|
148
|
+
items: { type: 'object' },
|
|
149
|
+
description: 'Test files in the candidate neighbourhood (informational).',
|
|
150
|
+
},
|
|
151
|
+
docs: {
|
|
152
|
+
type: 'array',
|
|
153
|
+
items: { type: 'object' },
|
|
154
|
+
description: 'Documentation files in the candidate neighbourhood (informational).',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// INTENT_MANIFEST_SCHEMA
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* JSON Schema (Draft 2020-12) for the KnoSky `intent-manifest` artifact.
|
|
165
|
+
* Produced by kc_bundle / kcBundle(). Must agree with validateIntentManifest() in core/schema.mjs.
|
|
166
|
+
*
|
|
167
|
+
* @type {object}
|
|
168
|
+
*/
|
|
169
|
+
export const INTENT_MANIFEST_SCHEMA = {
|
|
170
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
171
|
+
$id: 'https://knosky.com/schemas/intent-manifest.json',
|
|
172
|
+
title: 'KnoSky Intent Manifest',
|
|
173
|
+
description:
|
|
174
|
+
'Shareable, verifiable file-set manifest produced by kc_bundle. ' +
|
|
175
|
+
'Each path entry carries a SHA-256 digest. ' +
|
|
176
|
+
'The secret_scan field signals whether the manifest was produced by a clean or blocked scan.',
|
|
177
|
+
type: 'object',
|
|
178
|
+
required: [
|
|
179
|
+
'knosky_protocol',
|
|
180
|
+
'artifact_type',
|
|
181
|
+
'advisory',
|
|
182
|
+
'generated_at',
|
|
183
|
+
'paths',
|
|
184
|
+
'edges',
|
|
185
|
+
'secret_scan',
|
|
186
|
+
],
|
|
187
|
+
additionalProperties: true,
|
|
188
|
+
properties: {
|
|
189
|
+
knosky_protocol: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
const: '1.0',
|
|
192
|
+
description: 'Protocol version. Always "1.0" for this schema.',
|
|
193
|
+
},
|
|
194
|
+
artifact_type: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
const: 'intent-manifest',
|
|
197
|
+
description: 'Discriminator field. Always "intent-manifest" for this artifact.',
|
|
198
|
+
},
|
|
199
|
+
advisory: {
|
|
200
|
+
type: 'boolean',
|
|
201
|
+
const: true,
|
|
202
|
+
description: 'Invariantly true.',
|
|
203
|
+
},
|
|
204
|
+
generated_at: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
format: 'date-time',
|
|
207
|
+
description: 'ISO-8601 timestamp at which this document was generated.',
|
|
208
|
+
},
|
|
209
|
+
paths: {
|
|
210
|
+
type: 'array',
|
|
211
|
+
items: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
required: ['path', 'sha256'],
|
|
214
|
+
properties: {
|
|
215
|
+
path: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
minLength: 1,
|
|
218
|
+
description: 'Repo-relative path. Must not be absolute or contain ".." segments.',
|
|
219
|
+
},
|
|
220
|
+
sha256: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'Hex-encoded SHA-256 digest of the file at bundle time. Empty string when unreadable.',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
additionalProperties: true,
|
|
226
|
+
},
|
|
227
|
+
description: 'Ordered list of files covered by this manifest.',
|
|
228
|
+
},
|
|
229
|
+
edges: {
|
|
230
|
+
type: 'array',
|
|
231
|
+
items: { type: 'object' },
|
|
232
|
+
description: 'Dependency edges among the covered files.',
|
|
233
|
+
},
|
|
234
|
+
expiry: {
|
|
235
|
+
type: ['string', 'null'],
|
|
236
|
+
description: 'ISO-8601 expiry timestamp, or null for no expiry.',
|
|
237
|
+
},
|
|
238
|
+
secret_scan: {
|
|
239
|
+
...SECRET_SCAN_RESULT,
|
|
240
|
+
description:
|
|
241
|
+
'Result of the fail-closed secret scan run at bundle time. ' +
|
|
242
|
+
'"clean" means no secret-like values were detected; ' +
|
|
243
|
+
'"blocked" means the scan found a pattern match and the bundle should not be shared.',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// CONFIG_SCHEMA
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* JSON Schema (Draft 2020-12) for a KnoSky project config object
|
|
254
|
+
* (`.knosky/config.yml`, parsed to a JS object by loadConfig()).
|
|
255
|
+
* Must agree with validateConfig() in core/config.mjs.
|
|
256
|
+
*
|
|
257
|
+
* @type {object}
|
|
258
|
+
*/
|
|
259
|
+
export const CONFIG_SCHEMA = {
|
|
260
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
261
|
+
$id: 'https://knosky.com/schemas/config.json',
|
|
262
|
+
title: 'KnoSky Project Config',
|
|
263
|
+
description:
|
|
264
|
+
'KnoSky project configuration. Read from `.knosky/config.yml`; ' +
|
|
265
|
+
'missing fields default to the values shown in `default`. ' +
|
|
266
|
+
'Unknown keys are rejected by validateConfig().',
|
|
267
|
+
type: 'object',
|
|
268
|
+
required: [
|
|
269
|
+
'knosky_protocol',
|
|
270
|
+
'telemetry',
|
|
271
|
+
'absolute_paths',
|
|
272
|
+
'fail_on_secret',
|
|
273
|
+
'allow_excerpts',
|
|
274
|
+
'max_excerpt_chars',
|
|
275
|
+
'categories',
|
|
276
|
+
'ignore',
|
|
277
|
+
],
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
properties: {
|
|
280
|
+
knosky_protocol: {
|
|
281
|
+
type: 'string',
|
|
282
|
+
const: '1.0',
|
|
283
|
+
default: '1.0',
|
|
284
|
+
description: 'Protocol version. Must be "1.0".',
|
|
285
|
+
},
|
|
286
|
+
telemetry: {
|
|
287
|
+
type: 'boolean',
|
|
288
|
+
default: false,
|
|
289
|
+
description: 'Whether to emit telemetry. Defaults to false (never sends data out).',
|
|
290
|
+
},
|
|
291
|
+
absolute_paths: {
|
|
292
|
+
type: 'boolean',
|
|
293
|
+
default: false,
|
|
294
|
+
description: 'Embed absolute filesystem paths in the city output. Defaults to false (basename only).',
|
|
295
|
+
},
|
|
296
|
+
fail_on_secret: {
|
|
297
|
+
type: 'boolean',
|
|
298
|
+
default: true,
|
|
299
|
+
description: 'Abort the build if a secret-like value is detected. Defaults to true (fail-closed).',
|
|
300
|
+
},
|
|
301
|
+
allow_excerpts: {
|
|
302
|
+
type: 'boolean',
|
|
303
|
+
default: false,
|
|
304
|
+
description: 'Include file-content excerpts in the index. Defaults to false.',
|
|
305
|
+
},
|
|
306
|
+
max_excerpt_chars: {
|
|
307
|
+
type: 'integer',
|
|
308
|
+
minimum: 0,
|
|
309
|
+
default: 0,
|
|
310
|
+
description: 'Maximum characters per excerpt. 0 = no excerpts even when allow_excerpts is true.',
|
|
311
|
+
},
|
|
312
|
+
categories: {
|
|
313
|
+
type: 'array',
|
|
314
|
+
items: { type: 'string' },
|
|
315
|
+
default: [],
|
|
316
|
+
description: 'Explicit category (district) labels. Empty array = derived automatically.',
|
|
317
|
+
},
|
|
318
|
+
ignore: {
|
|
319
|
+
type: 'array',
|
|
320
|
+
items: { type: 'string' },
|
|
321
|
+
default: [],
|
|
322
|
+
description: 'Additional glob-style ignore patterns (appended to the built-in defaults and .gitignore).',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// CITY_SCHEMA
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* JSON Schema (Draft 2020-12) for a KnoSky city-data v2 envelope.
|
|
333
|
+
* Produced by the fs-indexer and consumed by the renderer / MCP server.
|
|
334
|
+
* Must agree with validateCity() + NODE_FIELD_ALLOWLIST in core/contract.mjs.
|
|
335
|
+
*
|
|
336
|
+
* @type {object}
|
|
337
|
+
*/
|
|
338
|
+
export const CITY_SCHEMA = {
|
|
339
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
340
|
+
$id: 'https://knosky.com/schemas/city.json',
|
|
341
|
+
title: 'KnoSky City Data (v2)',
|
|
342
|
+
description:
|
|
343
|
+
'N-category city-data envelope (contract v2). ' +
|
|
344
|
+
'Contains pointers and projections only — never full file bodies (decision D-146). ' +
|
|
345
|
+
'See core/CONTRACT.md for narrative spec.',
|
|
346
|
+
type: 'object',
|
|
347
|
+
required: ['schema_version', 'generated_at', 'categories', 'nodes'],
|
|
348
|
+
additionalProperties: true,
|
|
349
|
+
properties: {
|
|
350
|
+
schema_version: {
|
|
351
|
+
type: 'string',
|
|
352
|
+
const: '2.0',
|
|
353
|
+
description: 'City contract version. Always "2.0" for this schema.',
|
|
354
|
+
},
|
|
355
|
+
generated_at: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
format: 'date-time',
|
|
358
|
+
description: 'ISO-8601 timestamp at which the city was built.',
|
|
359
|
+
},
|
|
360
|
+
source: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
required: ['kind', 'ref'],
|
|
363
|
+
properties: {
|
|
364
|
+
kind: { type: 'string', enum: ['fs', 'github', 'board', 'legacy'] },
|
|
365
|
+
ref: { type: 'string' },
|
|
366
|
+
rev: { type: 'string' },
|
|
367
|
+
},
|
|
368
|
+
additionalProperties: false,
|
|
369
|
+
description: 'Where the city was built from.',
|
|
370
|
+
},
|
|
371
|
+
categories: {
|
|
372
|
+
type: 'array',
|
|
373
|
+
items: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
required: ['id', 'label', 'order'],
|
|
376
|
+
properties: {
|
|
377
|
+
id: { type: 'string', minLength: 1 },
|
|
378
|
+
label: { type: 'string', minLength: 1 },
|
|
379
|
+
color: { type: 'string' },
|
|
380
|
+
order: { type: 'integer', minimum: 0 },
|
|
381
|
+
},
|
|
382
|
+
additionalProperties: false,
|
|
383
|
+
},
|
|
384
|
+
description: 'N-category manifest. Replaces the hardcoded 4-district v1 model.',
|
|
385
|
+
},
|
|
386
|
+
node_count: {
|
|
387
|
+
type: 'integer',
|
|
388
|
+
minimum: 0,
|
|
389
|
+
description: 'Expected number of nodes. Must equal nodes.length when present.',
|
|
390
|
+
},
|
|
391
|
+
nodes: {
|
|
392
|
+
type: 'array',
|
|
393
|
+
items: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
required: ['id', 'kind', 'title', 'category', 'links', 'provenance'],
|
|
396
|
+
// additionalProperties:false enforces the serialization allowlist from contract.mjs.
|
|
397
|
+
additionalProperties: false,
|
|
398
|
+
properties: {
|
|
399
|
+
id: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
minLength: 1,
|
|
402
|
+
description: 'Stable, source-derived node identifier.',
|
|
403
|
+
},
|
|
404
|
+
kind: {
|
|
405
|
+
type: 'string',
|
|
406
|
+
description: 'Node kind: decision, spec, file, dir, doc, …',
|
|
407
|
+
},
|
|
408
|
+
title: {
|
|
409
|
+
type: 'string',
|
|
410
|
+
description: 'Short projection title (scrubbed; never a full file body).',
|
|
411
|
+
},
|
|
412
|
+
summary: {
|
|
413
|
+
type: 'string',
|
|
414
|
+
maxLength: 200,
|
|
415
|
+
description:
|
|
416
|
+
'Short excerpt projection (≤ 200 chars, scrubbed; never a full file body).',
|
|
417
|
+
},
|
|
418
|
+
category: {
|
|
419
|
+
type: 'string',
|
|
420
|
+
description: 'Category id. Must match an entry in the top-level categories[] manifest.',
|
|
421
|
+
},
|
|
422
|
+
status: { type: 'string' },
|
|
423
|
+
fact_date: { type: 'string' },
|
|
424
|
+
tags: {
|
|
425
|
+
type: 'array',
|
|
426
|
+
items: { type: 'string' },
|
|
427
|
+
},
|
|
428
|
+
headings: {
|
|
429
|
+
type: 'array',
|
|
430
|
+
items: { type: 'string' },
|
|
431
|
+
},
|
|
432
|
+
links: {
|
|
433
|
+
type: 'array',
|
|
434
|
+
items: { type: 'string' },
|
|
435
|
+
description: 'Edges to other node ids.',
|
|
436
|
+
},
|
|
437
|
+
churn: {
|
|
438
|
+
description: 'Recent-change signal. Object with `c` (commit count) or a plain number.',
|
|
439
|
+
},
|
|
440
|
+
provenance: {
|
|
441
|
+
...PROVENANCE,
|
|
442
|
+
description: 'Back-pointer to the live source. Fields: store (required), ref (required).',
|
|
443
|
+
},
|
|
444
|
+
visibility: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
enum: ['internal', 'public'],
|
|
447
|
+
},
|
|
448
|
+
sensitive: {
|
|
449
|
+
type: 'boolean',
|
|
450
|
+
description: 'Scrub/flag marker. True when a sensitive-term match was found.',
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
description:
|
|
455
|
+
'All nodes in the city. Node fields are strictly limited to the serialization allowlist ' +
|
|
456
|
+
'(id, kind, title, summary, category, status, fact_date, tags, headings, links, churn, ' +
|
|
457
|
+
'provenance, visibility, sensitive).',
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
};
|