knosky 0.3.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 ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to KnoSky. Versions are git-tagged on this repo.
4
+
5
+ ## [0.3.0] - 2026-06-29 — One-command launcher (npx)
6
+
7
+ ### Added
8
+ - `npx knosky [path]` one-command launcher: indexes the folder, builds and opens the city, prints the MCP config (Claude Code / Claude Desktop / Cursor / VS Code) and suggested first prompts, then starts the local MCP server. Flags: `--no-open`, `--no-serve`. Reuses the verified indexer/renderer/MCP and inherits fail-closed `--share-safe` safety.
9
+ - Root `package.json` with a `knosky` bin and a `files` allowlist for publishing.
10
+
11
+ ## [0.2.0] - 2026-06-29 — Security hardening + safe-share
12
+
13
+ ### Security
14
+ - **Generated artifacts hardened against injection.** City data is embedded as inert JSON (`<script type="application/json">`) and the builder escapes `<` and U+2028/U+2029, so a repo containing `</script>` can no longer execute code in a generated city.
15
+ - **Untrusted names escaped.** District/category names, file titles, and kinds render through HTML escaping; colors are validated. A folder named `<img onerror=...>` renders as harmless text, not a live element.
16
+ - **Fail-closed secret scanning.** Expanded patterns (GitHub, OpenAI, Stripe, Google, Slack, GitLab, npm, JWT, SSH, AWS, PEM). The build now **fails closed** if a secret-like value is detected (override with `--allow-leaks`).
17
+ - **postMessage bridge gated.** Disabled in the standalone build; only active in embedded mode with an origin allowlist.
18
+
19
+ ### Added
20
+ - `--share-safe` — strips the absolute root path (basename only) and prints a safety report.
21
+ - `--include-absolute-root` — opt back in to the full local path for private diagnostics.
22
+ - `SECURITY.md`, `PRIVACY.md`, `LIMITATIONS.md` — disclosure path + trust docs.
23
+
24
+ ### Changed
25
+ - **Privacy default:** the generated index stores the folder **basename**, not the absolute path.
26
+ - **Ignore accuracy:** inside a git repo, git's own ignore rules are applied (`git check-ignore`); the conservative parser remains the fallback for non-git folders.
27
+ - **MCP input caps:** `kc_search` bounds query length and result count; ids are length-capped — avoids token burn and runaway output.
28
+ - Committed a lockfile for reproducible MCP installs.
29
+
30
+ ## [0.1.0] - 2026-06-27 — Initial public release
31
+ - Local-first repo/folder → explorable isometric city (Kenney CC0 art).
32
+ - Local stdio MCP: `kc_search`, `kc_get_node`, `kc_list_categories`, `kc_get_provenance`.
33
+ - Single-file, self-contained city HTML. FSL-1.1-MIT license.
package/CREDITS.md ADDED
@@ -0,0 +1,14 @@
1
+ # Credits
2
+
3
+ ## Artwork
4
+ City sprites (buildings, city details, landscape tiles, vehicles) are by **Kenney** — <https://kenney.nl>.
5
+ Licensed **CC0 1.0 Universal (public domain)**: <https://creativecommons.org/publicdomain/zero/1.0/>.
6
+ No rights reserved; attribution appreciated but not required. The original texture atlases ship in
7
+ `renderer/art/kenney/`.
8
+
9
+ ## Software
10
+ KnoSky is released under the Functional Source License (FSL-1.1-MIT) — see [LICENSE.md](./LICENSE.md).
11
+
12
+ Dependencies:
13
+ - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) (MIT) — local MCP server.
14
+ - [`zod`](https://github.com/colinhacks/zod) (MIT) — schema validation.
package/LICENSE.md ADDED
@@ -0,0 +1,76 @@
1
+ # Functional Source License, Version 1.1, MIT Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-MIT
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Paul Poulose / Sathia
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software.
20
+
21
+ ### License Grant
22
+
23
+ Subject to your compliance with this License Grant and the Patents, Redistribution and Trademark clauses below, we hereby grant you the right to use, copy, modify, create derivative works, publicly perform, publicly display and redistribute the Software for any Permitted Purpose identified below.
24
+
25
+ ### Permitted Purpose
26
+
27
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use means making the Software available to others in a commercial product or service that:
28
+
29
+ 1. substitutes for the Software;
30
+
31
+ 2. substitutes for any other product or service we offer using the Software that exists as of the date we make the Software available; or
32
+
33
+ 3. offers the same or substantially similar functionality as the Software.
34
+
35
+ Permitted Purposes specifically include using the Software:
36
+
37
+ 1. for your internal use and access;
38
+
39
+ 2. for non-commercial education;
40
+
41
+ 3. for non-commercial research; and
42
+
43
+ 4. in connection with professional services that you provide to a licensee using the Software in accordance with these Terms and Conditions.
44
+
45
+ ### Patents
46
+
47
+ To the extent your use for a Permitted Purpose would necessarily infringe our patents, the license grant above includes a license under our patents. If you make a claim against any party that the Software infringes or contributes to the infringement of any patent, then your patent license to the Software ends immediately.
48
+
49
+ ### Redistribution
50
+
51
+ The Terms and Conditions apply to all copies, modifications and derivatives of the Software. If you redistribute any copies, modifications or derivatives of the Software, you must include a copy of or a link to these Terms and Conditions and not remove any copyright notices provided in or with the Software.
52
+
53
+ ### Disclaimer
54
+
55
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT.
56
+
57
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
58
+
59
+ ### Trademarks
60
+
61
+ Except for displaying the License Details and identifying us as the origin of the Software, you have no right under these Terms and Conditions to use our trademarks, trade names, service marks or product names.
62
+
63
+ ## Grant of Future License
64
+
65
+ We hereby irrevocably grant you an additional license to use the Software under the MIT License that is effective on the second anniversary of the date we make the Software available. On or after that date, you may use the Software under the MIT License, in which case the following will apply:
66
+
67
+ Permission is hereby granted, free of charge, to any person obtaining a copy of the Software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
68
+
69
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
70
+
71
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
72
+
73
+ ---
74
+
75
+ > Note (not part of the license): "KnoSky" and "Sathia" are trademarks of the Licensor. FSL gives
76
+ > free use without allowing a competing product; it converts to MIT two years after each version's release.
package/LIMITATIONS.md ADDED
@@ -0,0 +1,23 @@
1
+ # Known limitations
2
+
3
+ KnoSky is a **map and citation layer**, not a code-intelligence engine. Being clear about the boundary keeps expectations honest.
4
+
5
+ ## What KnoSky is good at
6
+
7
+ - Orienting you in a repo or docs folder fast: where things live, how areas connect.
8
+ - Giving you (and your AI assistant, via the local MCP) **cited pointers** back to the real source.
9
+ - A private, always-fresh, $0 index that runs entirely on your machine.
10
+
11
+ ## What KnoSky is not (yet)
12
+
13
+ - **Not deep code intelligence.** It does not parse call graphs, type information, runtime dependencies, or semantic architecture. It indexes pointers and light projections, not file bodies.
14
+ - **Not a full-text / code-RAG search.** It will not answer questions over the *contents* of your files. For deep code Q&A, your AI assistant should read the source it cites (that's what the MCP citations are for) or use a dedicated code-search tool.
15
+ - **Not a cloud service.** No sync, no multi-user, no hosted backend.
16
+
17
+ ## Practical notes
18
+
19
+ - **Ignore matching:** inside a git repo, git's own ignore rules are applied; outside git, a conservative `.gitignore`/`.kcignore` parser is used and may differ from git in edge cases.
20
+ - **Large repos:** indexing is capped (default 6000 files, `--max`) to stay fast; very large monorepos may need `--max` raised or extra `.kcignore` entries.
21
+ - **Categories:** the default categorizer uses top-level folder names. AI-suggested categories are opt-in and metadata-only.
22
+
23
+ See also: [README.md](README.md), [PRIVACY.md](PRIVACY.md), [SECURITY.md](SECURITY.md).
package/PRIVACY.md ADDED
@@ -0,0 +1,30 @@
1
+ # Privacy
2
+
3
+ KnoSky is **local-first**. It runs on your machine, reads your folder, and writes files on your machine. There is no KnoSky account, no server, and no telemetry. Your source code is never uploaded by KnoSky.
4
+
5
+ ## What KnoSky reads
6
+
7
+ To build the map, the indexer reads file and folder names and the **first ~4 KB** of text files (to pull a title, a few headings, and a one-line excerpt). It does **not** copy full file bodies.
8
+
9
+ ## What ends up in a generated city
10
+
11
+ A generated `city-data.json` / city HTML contains **pointers and light projections**, not your code:
12
+
13
+ - file paths (relative), folder/category names
14
+ - titles, a handful of headings, and a short (<=200 char) excerpt per file
15
+ - tags (file extensions), simple link relationships, and a source revision
16
+
17
+ It does **not** contain full file contents.
18
+
19
+ ## Defaults that protect you
20
+
21
+ - The absolute root path is **stripped to its basename** by default (use `--include-absolute-root` only for private, local diagnostics).
22
+ - Text projections run through a **secret/PII scrubber**, and the build **fails closed** if a secret-like value is detected (override only with `--allow-leaks`).
23
+ - `--share-safe` prints a safety report (files scanned/skipped, secrets found, risk level) before you share anything.
24
+ - `.gitignore` and a `.kcignore` are honored; inside a git repo, git's own ignore rules are applied.
25
+
26
+ ## What we don't promise
27
+
28
+ We do **not** claim "zero data risk." A generated city is like a directory listing with short snippets. Treat it accordingly: **review a city file (or build with `--share-safe`) before sharing it**, and open city files you did not generate the same way you'd open any untrusted HTML.
29
+
30
+ See also: [SECURITY.md](SECURITY.md), [LIMITATIONS.md](LIMITATIONS.md).
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # KnoSky
2
+
3
+ *Your knowledge as a skyline.*
4
+
5
+ **Turn any repo or folder of docs into a living, explorable city — and give your AI assistant grounded, cited answers from your own source. Runs entirely on your machine. Free.**
6
+
7
+ > *Born from building [Sathia](https://sathia.ai).*
8
+
9
+ ---
10
+
11
+ ## See it in 10 seconds
12
+
13
+ Open **[`demo/knosky-demo.html`](./demo/knosky-demo.html)** in your browser (just double-click it — it's a single self-contained file). That's a sample project rendered as a city. Now point it at your own.
14
+
15
+ ---
16
+
17
+ ## The problem
18
+ Your project grows faster than anyone can hold in their head. Hundreds of files, decisions, and docs across folders. A file tree tells you what files exist — not what the *system* is. So things get lost, decisions get re-litigated, and your AI assistant confidently makes things up about your own codebase.
19
+
20
+ ## What you get (the outcome)
21
+ - **See your whole project in one screen.** Instead of scrolling a file tree, you see the *shape* of everything — which areas are big, how they connect, where the gaps are. New collaborators get oriented in minutes, not weeks.
22
+ - **Find anything in seconds.** Search the city — or ask your assistant *"where does auth live / what did we decide about billing"* — and jump straight to the **live file**.
23
+ - **Your AI answers from YOUR source, with citations.** Connect it to Claude / Cursor / VS Code / Gemini and your assistant stops guessing about your codebase — it cites the real file, every time.
24
+ - **Zero setup tax, zero data risk.** Point it at a folder → a city in under a minute. Deterministic, **$0 tokens** to keep fresh, and **nothing ever leaves your machine.**
25
+
26
+ **Net:** faster comprehension, reliable recall of your own knowledge, and a grounded AI — without giving up privacy or paying a cent.
27
+
28
+ ## Who it's for
29
+ Developers, founders, and architects sitting on a sprawling repo or knowledge base who want to *understand and navigate their own work fast* — and want their AI assistant grounded in it — without uploading anything.
30
+
31
+ ---
32
+
33
+ ## How it works
34
+
35
+ **Quickstart:**
36
+ ```bash
37
+ git clone https://github.com/SathiaAI/knosky && cd knosky && npm install
38
+ node bin/knosky.mjs /path/to/your/repo
39
+ ```
40
+ Indexes the folder, opens the city, prints the MCP config for your AI assistant (Claude Code / Claude Desktop / Cursor / VS Code) plus a few starter prompts, and starts the local connector. Flags: `--no-open`, `--no-serve`.
41
+
42
+ *Coming to npm:* `npx knosky .` — the same thing in one command, no clone.
43
+
44
+ Want the individual pieces instead? Read on.
45
+
46
+ **Requirements:** [Node.js](https://nodejs.org) 18+.
47
+
48
+ **1. Build your city from a folder or repo**
49
+ ```bash
50
+ node core/fs-indexer.mjs --root /path/to/your/repo --out city-data.json --share-safe
51
+ node renderer/build-rich.mjs city-data.json city.html
52
+ ```
53
+ Open `city.html`. Every top-level folder is a **district**, every file a **building**, every building links to the real source.
54
+
55
+ **Flags:** `--share-safe` strips your absolute path (basename only) and prints a safety report — and the build **fails closed** if a secret-like value is detected. `--redact AcmeCorp,SecretProject` masks (and skips files matching) project-specific terms. `--include-absolute-root` keeps the full local path (private diagnostics only). `--allow-leaks` overrides the secret block (not recommended).
56
+
57
+ **2. Connect it to your AI assistant (MCP)**
58
+ ```bash
59
+ cd mcp && npm install && cd ..
60
+ # Claude Code:
61
+ claude mcp add knosky -e KC_CITY=/abs/path/city-data.json -- node /abs/path/mcp/server.mjs
62
+ ```
63
+ Or add to your Claude Desktop / Cursor / VS Code MCP config:
64
+ ```json
65
+ "knosky": {
66
+ "command": "node",
67
+ "args": ["/abs/path/mcp/server.mjs"],
68
+ "env": { "KC_CITY": "/abs/path/city-data.json" }
69
+ }
70
+ ```
71
+ Then ask: *"search KnoSky for what we decided about authentication."* Read-only tools exposed: `kc_search`, `kc_get_node`, `kc_list_categories`, `kc_get_provenance`.
72
+
73
+ ---
74
+
75
+ ## What it is **not** (on purpose)
76
+ - **Not a code-RAG engine.** It won't read your whole codebase and answer deep questions. It's a **map and a router** — it tells you *where* things are and hands you (or your AI) the source. (Reach for Cursor/Glean for deep code Q&A.)
77
+ - **Not a cloud service.** No upload, no account, nothing leaves your machine. (A hosted option may come later.)
78
+ - **Not a copy of your knowledge.** A navigable **index of pointers** — titles, headings, short excerpts that link back to the live file — never your full file contents.
79
+
80
+ ## Privacy & safety
81
+ - Runs locally; your source never leaves your machine.
82
+ - Indexes pointers + light projections (title, headings, ~200-char excerpt, tags) — **never full file bodies**.
83
+ - Skips `.git`, `node_modules`, `secrets/`, `keys/`, `.env*`, plus your `.gitignore`/`.kcignore` (git's own ignore rules apply inside a repo).
84
+ - Scrubs common secret/PII patterns and **fails the build closed** if a secret-like value is detected. Build with **`--share-safe`** for a safety report before sharing.
85
+ - Generated cities embed your data as inert JSON and escape untrusted file/folder names — opening or sharing a city won't execute injected code.
86
+
87
+ More: **[PRIVACY.md](./PRIVACY.md)** · **[LIMITATIONS.md](./LIMITATIONS.md)** · **[SECURITY.md](./SECURITY.md)** · **[CHANGELOG.md](./CHANGELOG.md)**
88
+
89
+ ## License & credits
90
+ Free to use under the **[Functional Source License (FSL-1.1-MIT)](./LICENSE.md)** — use it freely; you just can't repackage it as a competing product. Converts to MIT two years after each release. "KnoSky" is a trademark of the author.
91
+
92
+ City artwork is **[Kenney](https://kenney.nl)** (CC0 / public domain) — see [CREDITS.md](./CREDITS.md).
package/SECURITY.md ADDED
@@ -0,0 +1,32 @@
1
+ # Security Policy
2
+
3
+ KnoSky is a **local-first** tool: it runs on your machine, indexes your own folder, and writes a self-contained city file. It does not upload your source and has no hosted backend. The security surface that matters is the **generated artifact** (the city HTML / `city-data.json`) and the local indexer/MCP.
4
+
5
+ ## Supported versions
6
+
7
+ The latest release on the `main` branch is supported. KnoSky is pre-1.0; please run the newest version before reporting an issue.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Please report security issues **privately** — do not open a public issue for anything exploitable.
12
+
13
+ - Preferred: GitHub **"Report a vulnerability"** (Security → Advisories) on this repository, which opens a private channel.
14
+ - We aim to acknowledge reports within **5 business days** and to publish a fix or mitigation for confirmed issues as quickly as is practical.
15
+
16
+ When reporting, please include: the version/commit, steps to reproduce, and the impact you observed. A minimal proof-of-concept (e.g., a tiny repo or `city-data.json` that triggers the issue) helps a lot.
17
+
18
+ ## Safe testing guidelines
19
+
20
+ - Test only against repositories and folders **you own or control**.
21
+ - Do not include real secrets or third-party personal data in proof-of-concept artifacts.
22
+ - Do not perform testing that degrades the service for others (there is no shared service; this is a local tool).
23
+
24
+ ## Handling generated artifacts (for users)
25
+
26
+ - A generated city embeds **file names, paths, headings, and short excerpts** — not full file bodies. Treat a generated file as you would a directory listing with snippets.
27
+ - Build with `--share-safe` before sharing: it strips the absolute root path (basename only), runs a fail-closed secret scan, and prints a safety report. Builds **fail closed** if a secret-like value is detected (override only with `--allow-leaks`).
28
+ - Open city files you did not generate the same way you would any untrusted HTML.
29
+
30
+ ## Scope
31
+
32
+ In scope: injection in generated artifacts, secret leakage through projections, the local MCP server, and the indexer's privacy defaults. Out of scope: issues that require an attacker to already control your machine or your repository's contents with your knowledge.
package/bin/knosky.mjs ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // KnoSky one-command launcher: index a folder -> build the city -> open it ->
3
+ // print the MCP config + first prompts -> start the local MCP server.
4
+ // Reuses the verified core/renderer/mcp scripts as child processes (no new logic to trust).
5
+ //
6
+ // npx knosky [path] # default: current folder
7
+ // npx knosky . --no-open # don't auto-open the browser
8
+ // npx knosky . --no-serve # build + print config, don't start the MCP server
9
+ import { spawn, spawnSync } from 'node:child_process';
10
+ import path from 'node:path';
11
+ import fs from 'node:fs';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
15
+ const ROOT = path.resolve(HERE, '..');
16
+ const argv = process.argv.slice(2);
17
+ const flags = new Set(argv.filter(a => a.startsWith('--')));
18
+ const target = path.resolve(argv.find(a => !a.startsWith('--')) || '.');
19
+ const NODE = process.execPath;
20
+
21
+ if (!fs.existsSync(target)) { console.error('KnoSky: path not found: ' + target); process.exit(1); }
22
+
23
+ const outDir = path.join(target, '.knosky');
24
+ fs.mkdirSync(outDir, { recursive: true });
25
+ const cityJson = path.join(outDir, 'city-data.json');
26
+ const cityHtml = path.join(outDir, 'city.html');
27
+
28
+ const run = (script, args) => spawnSync(NODE, [path.join(ROOT, script), ...args], { stdio: 'inherit' });
29
+
30
+ console.log('\nKnoSky -> indexing ' + target);
31
+ let r = run('core/fs-indexer.mjs', ['--root', target, '--out', cityJson, '--share-safe']);
32
+ if (r.status !== 0) { console.error('\nKnoSky: indexing was blocked or failed (see above). Nothing was opened.'); process.exit(r.status || 1); }
33
+
34
+ r = run('renderer/build-rich.mjs', [cityJson, cityHtml]);
35
+ if (r.status !== 0) { console.error('\nKnoSky: building the city failed.'); process.exit(1); }
36
+
37
+ if (!flags.has('--no-open')) {
38
+ const isWin = process.platform === 'win32', isMac = process.platform === 'darwin';
39
+ const cmd = isWin ? 'cmd' : isMac ? 'open' : 'xdg-open';
40
+ const a = isWin ? ['/c', 'start', '', cityHtml] : [cityHtml];
41
+ try { spawn(cmd, a, { detached: true, stdio: 'ignore' }).unref(); } catch (_) {}
42
+ }
43
+
44
+ const mcpServer = path.join(ROOT, 'mcp', 'server.mjs');
45
+ const cfg = JSON.stringify({ mcpServers: { knosky: { command: 'node', args: [mcpServer], env: { KC_CITY: cityJson } } } }, null, 2);
46
+
47
+ console.log('\n City: ' + cityHtml);
48
+ console.log('\nConnect your AI assistant so it answers from THIS repo, with citations.');
49
+ console.log('Claude Code:');
50
+ console.log(' claude mcp add knosky -e KC_CITY="' + cityJson + '" -- node "' + mcpServer + '"');
51
+ console.log('\nClaude Desktop / Cursor / VS Code MCP config:');
52
+ console.log(cfg.split('\n').map(l => ' ' + l).join('\n'));
53
+ console.log('\nThen try one of these to see the loop (grounded, cited answers):');
54
+ console.log(' - "Using KnoSky, where does authentication live in this repo?"');
55
+ console.log(' - "Using KnoSky, what are the entry points of this project?"');
56
+ console.log(' - "Using KnoSky, which files should I read to understand billing?"');
57
+
58
+ if (flags.has('--no-serve')) {
59
+ console.log('\n(--no-serve) Not starting the MCP server. Run it later with:');
60
+ console.log(' KC_CITY="' + cityJson + '" node "' + mcpServer + '"\n');
61
+ process.exit(0);
62
+ }
63
+
64
+ console.log('\nStarting the local MCP server (Ctrl+C to stop)...\n');
65
+ const mcp = spawn(NODE, [mcpServer, cityJson], { stdio: 'inherit' });
66
+ mcp.on('exit', c => process.exit(c || 0));
67
+ process.on('SIGINT', () => { try { mcp.kill(); } catch (_) {} process.exit(0); });
@@ -0,0 +1,70 @@
1
+ # Knowledge City — Data Contract v2 (the shared spine)
2
+
3
+ > Phase 1a deliverable (SAT-385 → build). The one schema both faces (City + MCP) and every source
4
+ > (fs/github/board) depend on. Generalizes the fixed-4-district v1 → **N categories**. Pointers +
5
+ > projections ONLY — never file bodies (D-146). Council fixes baked in: a serialization **allowlist** + a
6
+ > best-effort **denylist scrub**. Proven to represent the current live city losslessly.
7
+
8
+ ## Envelope
9
+ ```jsonc
10
+ {
11
+ "schema_version": "2.0",
12
+ "generated_at": "<ISO8601>",
13
+ "source": { "kind": "fs|github|board|legacy", "ref": "<path/repo>", "rev": "<commit/rev>" },
14
+ "categories": [ { "id": "technical", "label": "Technical", "color": "#4f8cff", "order": 0 } ],
15
+ "node_count": 150,
16
+ "nodes": [ /* Node[] */ ]
17
+ }
18
+ ```
19
+ `categories[]` is the **N-category manifest** that replaces the hardcoded 4 districts — the renderer derives
20
+ its districts/colors from this, so the board scales to however many categories a source has.
21
+
22
+ ## Node
23
+ ```jsonc
24
+ {
25
+ "id": "decision:D-146", // stable, source-derived
26
+ "kind": "decision|spec|file|dir|doc|...",
27
+ "title": "Index, not copy", // projection
28
+ "summary": "<= 200 chars", // projection (excerpt, never full body)
29
+ "category": "governance", // category id (was "district")
30
+ "status": "locked", // optional
31
+ "fact_date": "2026-06-19", // optional
32
+ "tags": [], // optional projection
33
+ "headings": [], // optional projection (for nav/search)
34
+ "links": ["decision:D-147"], // edges to other node ids
35
+ "provenance": { "store": "...", "ref": "...", "source_rev": "...", "fetched_at": "..." },
36
+ "visibility": "internal|public",
37
+ "sensitive": false // scrub/flag marker
38
+ }
39
+ ```
40
+ Required: `id, kind, title, category, links, provenance`. Provenance required: `store, ref`.
41
+
42
+ ## Serialization allowlist (council fix)
43
+ Only these node fields may **ever** be written into the index:
44
+ `id, kind, title, summary, category, status, fact_date, tags, headings, links, provenance, visibility, sensitive`.
45
+ The serializer drops anything else and the validator **fails** on any non-allowlisted field — this is the
46
+ structural guard against a file `body` ever leaking into the index.
47
+
48
+ ## PII scrub (best-effort — NOT a security boundary)
49
+ Applied to every emitted text projection (title/summary/headings/tags): redact emails, `key|secret|token|password|bearer` assignments, AWS access-key IDs, private-key headers → `[REDACTED]`. The indexer also skips
50
+ `.git`, `node_modules`, `secrets/`, `keys/`, `.env*`, `dist/` by default, **plus** the repo's `.gitignore`
51
+ and a user `.kcignore`. Honest framing (per council): this reduces accidental leakage; it is not a guarantee,
52
+ and the local index is readable by the user's own assistant. First-run note will say so.
53
+
54
+ ## Projection limits
55
+ `summary` ≤ 200 chars. No field carries a full file body. Full content is reached only by dereferencing
56
+ `provenance` to the live source (which is what keeps the city fresh — "a map, not a copy").
57
+
58
+ ## Proof (this Phase-1a increment)
59
+ Adapting the current live city (`golden/city-data.golden.json`) through `adaptLegacy()` → contract v2:
60
+ - nodes **150 → 150** (no loss), edges **769 → 769** (no loss)
61
+ - categories derived: `product#0, technical#1, governance#2, design#3`
62
+ - `validateCity()` → **VALID**; scrub positive+negative tests **pass**
63
+ - reference instance written to `golden/city-data.v2.json`
64
+
65
+ Implementation: `core/contract.mjs` (schema + allowlist + scrub + validator + legacy adapter).
66
+ Reproduce: `node core/validate-golden.mjs`.
67
+
68
+ ## Open at G1 (founder call)
69
+ None on the schema itself (it's proven). Carry-forward decision (Phase 3, not blocking): the **activation
70
+ layer** — council unanimously recommends deferring it to the monetization tier so v1 stays fully offline.
@@ -0,0 +1,123 @@
1
+ // KC city-data CONTRACT v2 — the shared spine.
2
+ // Generalizes the fixed-4-district v1 → N categories. Pointers + projections ONLY (D-146); never bodies.
3
+ // Council fixes baked in: serialization ALLOWLIST at the edge + best-effort DENYLIST scrub.
4
+
5
+ export const SCHEMA_VERSION = '2.0';
6
+
7
+ // The ONLY node fields that may ever be serialized into the index. Anything else is dropped/flagged.
8
+ export const NODE_FIELD_ALLOWLIST = [
9
+ 'id', 'kind', 'title', 'summary', 'category', 'status',
10
+ 'fact_date', 'tags', 'headings', 'links', 'provenance', 'visibility', 'sensitive',
11
+ ];
12
+ export const NODE_REQUIRED = ['id', 'kind', 'title', 'category', 'links', 'provenance'];
13
+ export const PROVENANCE_REQUIRED = ['store', 'ref'];
14
+ export const CATEGORY_REQUIRED = ['id', 'label', 'order'];
15
+ export const SUMMARY_MAX = 200;
16
+
17
+ // Detection patterns for the fail-closed safe-share audit (broader than the scrub list below).
18
+ export const SECRET_PATTERNS = [
19
+ [/AKIA[0-9A-Z]{16}/g, 'aws-access-key'],
20
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/g, 'private-key'],
21
+ [/\bgh[posru]_[A-Za-z0-9]{20,}\b/g, 'github-token'],
22
+ [/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, 'github-pat'],
23
+ [/\bsk-[A-Za-z0-9]{20,}\b/g, 'openai-key'],
24
+ [/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, 'stripe-key'],
25
+ [/\bAIza[0-9A-Za-z_\-]{35}\b/g, 'google-api-key'],
26
+ [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, 'slack-token'],
27
+ [/\bglpat-[A-Za-z0-9_\-]{20,}\b/g, 'gitlab-pat'],
28
+ [/\bnpm_[A-Za-z0-9]{36}\b/g, 'npm-token'],
29
+ [/\beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\b/g, 'jwt'],
30
+ ];
31
+ export function findSecrets(text) {
32
+ const s = String(text); const hits = [];
33
+ for (const [re, kind] of SECRET_PATTERNS) { const m = s.match(re); if (m && m.length) hits.push([kind, m.length]); }
34
+ return hits;
35
+ }
36
+
37
+ // Best-effort secret/PII denylist applied to emitted text projections. Backed by the indexer's
38
+ // fail-closed findSecrets() audit on the final artifact (defense in depth, not a single boundary).
39
+ const DENY = [
40
+ /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, // emails
41
+ /\b(?:api[_-]?key|secret|token|password|passwd|bearer)\b\s*[:=]\s*\S+/gi,
42
+ ...SECRET_PATTERNS.map(([re]) => re),
43
+ ];
44
+ // Optional project-specific sensitive terms (e.g. an employer/customer name) → redacted too.
45
+ let EXTRA_TERMS = [];
46
+ export function setRedactTerms(terms) {
47
+ EXTRA_TERMS = (terms || []).map(t => String(t).trim()).filter(Boolean)
48
+ .map(t => new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'gi'));
49
+ }
50
+ export function scrubText(s) {
51
+ if (typeof s !== 'string') return s;
52
+ let out = s;
53
+ for (const re of DENY) out = out.replace(re, '[REDACTED]');
54
+ for (const re of EXTRA_TERMS) out = out.replace(re, '[REDACTED]');
55
+ return out;
56
+ }
57
+
58
+ // Default path/dir ignores (the indexer also honors the repo's .gitignore + a user .kcignore).
59
+ export const IGNORE_DEFAULTS = [
60
+ /(^|\/)\.git(\/|$)/, /(^|\/)node_modules(\/|$)/, /(^|\/)secrets?(\/|$)/i,
61
+ /(^|\/)keys?(\/|$)/i, /(^|\/)\.env/i, /(^|\/)dist(\/|$)/,
62
+ ];
63
+
64
+ // Serialize one node: keep ONLY allowlisted fields, scrub text, clamp summary.
65
+ export function serializeNode(n) {
66
+ const o = {};
67
+ for (const k of NODE_FIELD_ALLOWLIST) if (n[k] !== undefined) o[k] = n[k];
68
+ if (typeof o.title === 'string') o.title = scrubText(o.title);
69
+ if (typeof o.summary === 'string') o.summary = scrubText(o.summary).slice(0, SUMMARY_MAX);
70
+ if (Array.isArray(o.headings)) o.headings = o.headings.map(scrubText);
71
+ if (Array.isArray(o.tags)) o.tags = o.tags.map(scrubText);
72
+ return o;
73
+ }
74
+
75
+ // Validate a city envelope against the contract. Returns { ok, errors[] }.
76
+ export function validateCity(city) {
77
+ const errors = [];
78
+ if (!city || typeof city !== 'object') return { ok: false, errors: ['city is not an object'] };
79
+ if (city.schema_version !== SCHEMA_VERSION) errors.push(`schema_version != ${SCHEMA_VERSION}`);
80
+ if (!Array.isArray(city.categories)) errors.push('missing categories[] manifest');
81
+ if (!Array.isArray(city.nodes)) errors.push('missing nodes[]');
82
+ const catIds = new Set((city.categories || []).map(c => c.id));
83
+ (city.categories || []).forEach((c, i) => {
84
+ for (const k of CATEGORY_REQUIRED) if (c[k] === undefined) errors.push(`category[${i}] missing ${k}`);
85
+ });
86
+ const ids = new Set();
87
+ (city.nodes || []).forEach((n, i) => {
88
+ for (const k of NODE_REQUIRED) if (n[k] === undefined) errors.push(`node[${i}] missing ${k}`);
89
+ if (n.category && !catIds.has(n.category)) errors.push(`node[${i}] category '${n.category}' not in manifest`);
90
+ if (n.id) { if (ids.has(n.id)) errors.push(`duplicate id ${n.id}`); ids.add(n.id); }
91
+ if (n.provenance) for (const k of PROVENANCE_REQUIRED) if (n.provenance[k] === undefined) errors.push(`node[${i}] provenance missing ${k}`);
92
+ if (typeof n.summary === 'string' && n.summary.length > SUMMARY_MAX) errors.push(`node[${i}] summary > ${SUMMARY_MAX}`);
93
+ for (const k of Object.keys(n)) if (!NODE_FIELD_ALLOWLIST.includes(k)) errors.push(`node[${i}] non-allowlisted field '${k}'`);
94
+ });
95
+ if (city.node_count !== undefined && city.node_count !== (city.nodes || []).length) errors.push('node_count mismatch');
96
+ return { ok: errors.length === 0, errors };
97
+ }
98
+
99
+ const PALETTE = ['#4f8cff', '#34c759', '#ff9f0a', '#af52de', '#ff375f', '#5ac8fa', '#ffd60a', '#bf5af2', '#30d158', '#ff6482'];
100
+
101
+ // Build a categories[] manifest from a list of category ids (stable order + palette).
102
+ export function deriveCategories(ids) {
103
+ return ids.map((id, i) => ({
104
+ id, label: String(id).charAt(0).toUpperCase() + String(id).slice(1),
105
+ color: PALETTE[i % PALETTE.length], order: i,
106
+ }));
107
+ }
108
+
109
+ // Legacy adapter: v1 (district, no categories[]) → v2 (category + categories[] manifest), via the allowlist.
110
+ export function adaptLegacy(cityV1) {
111
+ const ids = [...new Set((cityV1.nodes || []).map(n => n.district).filter(Boolean))];
112
+ const categories = deriveCategories(ids);
113
+ const nodes = (cityV1.nodes || []).map(n => {
114
+ const { district, ...rest } = n;
115
+ return serializeNode({ ...rest, category: district });
116
+ });
117
+ return {
118
+ schema_version: SCHEMA_VERSION,
119
+ generated_at: cityV1.generated_at || new Date().toISOString(),
120
+ source: { kind: 'legacy', ref: cityV1.source_rev || 'unknown', rev: cityV1.source_rev || 'unknown' },
121
+ categories, node_count: nodes.length, nodes,
122
+ };
123
+ }