@nazty_labs/common-ground 0.5.1

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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/SETUP.md +219 -0
  4. package/dist/access.d.ts +172 -0
  5. package/dist/access.js +175 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +198 -0
  8. package/dist/commands.d.ts +189 -0
  9. package/dist/commands.js +202 -0
  10. package/dist/discovery.d.ts +73 -0
  11. package/dist/discovery.js +417 -0
  12. package/dist/errors.d.ts +15 -0
  13. package/dist/errors.js +22 -0
  14. package/dist/export.d.ts +14 -0
  15. package/dist/export.js +86 -0
  16. package/dist/guidance.d.ts +11 -0
  17. package/dist/guidance.js +75 -0
  18. package/dist/hooks.d.ts +4 -0
  19. package/dist/hooks.js +141 -0
  20. package/dist/init.d.ts +304 -0
  21. package/dist/init.js +150 -0
  22. package/dist/maintenance.d.ts +126 -0
  23. package/dist/maintenance.js +23 -0
  24. package/dist/matching.d.ts +17 -0
  25. package/dist/matching.js +32 -0
  26. package/dist/model.d.ts +974 -0
  27. package/dist/model.js +21 -0
  28. package/dist/navigation.d.ts +164 -0
  29. package/dist/navigation.js +164 -0
  30. package/dist/operations.d.ts +10 -0
  31. package/dist/operations.js +130 -0
  32. package/dist/paging.d.ts +5 -0
  33. package/dist/paging.js +32 -0
  34. package/dist/retrieval.d.ts +146 -0
  35. package/dist/retrieval.js +150 -0
  36. package/dist/review-files.d.ts +3 -0
  37. package/dist/review-files.js +106 -0
  38. package/dist/review.d.ts +86 -0
  39. package/dist/review.js +124 -0
  40. package/dist/server.d.ts +8 -0
  41. package/dist/server.js +105 -0
  42. package/dist/source-search.d.ts +63 -0
  43. package/dist/source-search.js +245 -0
  44. package/dist/store.d.ts +452 -0
  45. package/dist/store.js +718 -0
  46. package/dist/version.d.ts +1 -0
  47. package/dist/version.js +2 -0
  48. package/dist/workflow.d.ts +450 -0
  49. package/dist/workflow.js +317 -0
  50. package/docs/architecture.md +65 -0
  51. package/docs/audit-0.4.0.md +42 -0
  52. package/docs/demo.md +42 -0
  53. package/docs/discovery.md +70 -0
  54. package/docs/knowledge-policy.md +51 -0
  55. package/docs/pillar-contract.md +98 -0
  56. package/docs/quiet-workflow.md +98 -0
  57. package/docs/releases.md +157 -0
  58. package/package.json +52 -0
  59. package/schemas/admission.schema.json +75 -0
  60. package/schemas/knowledge.schema.json +192 -0
  61. package/schemas/patch.schema.json +220 -0
  62. package/schemas/update.schema.json +218 -0
@@ -0,0 +1,417 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { parse } from 'jsonc-parser';
5
+ import { relativePath } from './model.js';
6
+ const ENTRY_LIMIT = 1500, FILE_BYTES = 256 * 1024, TOTAL_BYTES = 2 * 1024 * 1024, PROJECT_LIMIT = 50;
7
+ const excluded = new Set(['.git', 'node_modules', '.common-ground', 'dist', 'build', 'target', '.nx', '.vite', '.next', '.nuxt', '.svelte-kit', '.angular', '.turbo', '.yarn', '.pnpm-store', '.output', 'coverage',
8
+ '.vscode', '.idea', '.codex', '.claude', '.cursor', 'dep', 'bundled', 'vendored', 'vendor', 'third_party', 'third-party', 'thirdparty', 'external', 'extern', 'deps', 'dependencies', '.venv', 'venv', '__pycache__', 'pods', 'carthage', '.build', 'deriveddata', '.gradle', '.dart_tool', '.pub-cache', 'bin', 'obj']);
9
+ const inside = (file, root) => root === '.' || file === root || file.startsWith(`${root}/`);
10
+ const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
11
+ const markers = /^(CMakeLists\.txt|Makefile|meson\.build|package\.json|angular\.json|project\.json|nx\.json|turbo\.json|lerna\.json|pnpm-workspace\.ya?ml|Package\.swift|project\.pbxproj|pom\.xml|(?:build|settings)\.gradle(?:\.kts)?|AndroidManifest\.xml|go\.(?:mod|work)|pyproject\.toml|requirements(?:[-.][\w-]+)?\.txt|Pipfile|setup\.py|Cargo\.toml|Gemfile|composer\.json|pubspec\.yaml|app\.json|app\.config\.json|.*\.(?:csproj|fsproj|vbproj|sln|slnx))$/;
12
+ const source = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|java|kt|swift|c|cc|cpp|cxx|h|hh|hpp|hxx|m|py|go|rs|cs|fs|vb|rb|php|dart|xml|json|toml|ya?ml|gradle|kts)$/;
13
+ const ci = (file) => /^(?:.*\/)?(?:azure-pipelines[^/]*\.ya?ml|\.gitlab-ci\.ya?ml|Jenkinsfile)$/.test(file) || /(?:^|\/)(?:\.github\/workflows|\.circleci)\/[^/]+\.ya?ml$/.test(file) || /(?:^|\/)pipelines\/.*\.ya?ml$/.test(file);
14
+ const roles = {
15
+ 'ci-cd': ['CI/CD Pipelines', 'Pipeline definitions, templates and execution contracts.'],
16
+ 'workspace-tooling': ['Workspace Tooling', 'Shared workspace configuration and build orchestration.'],
17
+ 'web-components': ['Shared Libraries and Components', 'Reusable library and component contracts shared across applications.'],
18
+ 'web-applications': ['Web Applications', 'Web application structure, routing and runtime contracts.'],
19
+ 'mobile-applications': ['Mobile Applications', 'Mobile application structure and cross-platform runtime contracts.'],
20
+ 'native-applications': ['Native Applications and Packages', 'Native application and package structure and public contracts.'],
21
+ 'cpp-projects': ['C / C++ Projects', 'C and C++ application and library structure, build and runtime contracts.'],
22
+ 'jvm-applications': ['JVM Applications', 'Java and Kotlin project structure, build and runtime contracts.'],
23
+ 'javascript-projects': ['JavaScript / TypeScript Projects', 'JavaScript and TypeScript project structure and runtime contracts.'],
24
+ 'python-projects': ['Python Projects', 'Python application and package structure and runtime contracts.'],
25
+ 'go-projects': ['Go Projects', 'Go module structure and public application or package contracts.'],
26
+ 'rust-projects': ['Rust Projects', 'Rust crate structure and public application or library contracts.'],
27
+ 'dotnet-projects': ['.NET Projects', '.NET project structure, build and runtime contracts.'],
28
+ 'ruby-projects': ['Ruby Projects', 'Ruby application and library structure and runtime contracts.'],
29
+ 'php-projects': ['PHP Projects', 'PHP application and package structure and runtime contracts.'],
30
+ 'dart-projects': ['Dart Projects', 'Dart package structure and runtime contracts.'],
31
+ };
32
+ const npmSignals = { '@angular/core': 'Angular', react: 'React', 'react-native': 'React Native', expo: 'Expo', next: 'Next.js', vue: 'Vue', nuxt: 'Nuxt', svelte: 'Svelte', '@sveltejs/kit': 'SvelteKit', astro: 'Astro', express: 'Express', fastify: 'Fastify', '@nestjs/core': 'NestJS', typescript: 'TypeScript', nx: 'Nx', turbo: 'Turborepo', lerna: 'Lerna' };
33
+ /** Bounded breadth-first discovery keeps deep application trees from starving sibling manifests. */
34
+ async function scanFiles(store, exclude) {
35
+ const files = [], queue = ['.'], skipped = [], skippedPaths = [];
36
+ let inspectedEntries = 0, truncated = false;
37
+ for (let offset = 0; offset < queue.length && !truncated; offset++) {
38
+ const directory = queue[offset];
39
+ if (directory !== '.')
40
+ await store.safe(directory);
41
+ const children = [];
42
+ for await (const entry of await fs.opendir(path.join(store.root, directory))) {
43
+ if (inspectedEntries === ENTRY_LIMIT) {
44
+ truncated = true;
45
+ break;
46
+ }
47
+ inspectedEntries++;
48
+ const file = path.posix.join(directory, entry.name);
49
+ const reason = entry.isSymbolicLink() ? 'symlink' : excluded.has(entry.name.toLowerCase()) || entry.name.startsWith('.env') ? 'dependency, generated output or tool configuration' : exclude.some(p => inside(file, p)) ? 'explicit exclusion' : undefined;
50
+ if (reason) {
51
+ skippedPaths.push(file);
52
+ skipped.push({ path: entry.name.startsWith('.env') ? '[environment file]' : file, reason });
53
+ continue;
54
+ }
55
+ if (entry.isDirectory())
56
+ children.push(file);
57
+ else if (entry.isFile())
58
+ files.push(file);
59
+ }
60
+ queue.push(...children.sort((a, b) => {
61
+ const priority = (p) => /^(src|include|apps|packages|libs|services)(\/|$)/.test(p) ? 0 : 1;
62
+ return priority(a) - priority(b) || a.localeCompare(b);
63
+ }));
64
+ }
65
+ return { files: files.sort(), inspectedEntries, truncated, skipped: skipped.slice(0, 30), skippedPaths, skippedCount: skipped.length };
66
+ }
67
+ /** Collapse only homogeneous, fully scanned subtrees. Never infer whole-repository ownership. */
68
+ function ownershipHints(files, scan) {
69
+ const owned = new Set(files), directories = new Set();
70
+ for (const file of files) {
71
+ let dir = path.posix.dirname(file);
72
+ while (dir !== '.') {
73
+ directories.add(dir);
74
+ dir = path.posix.dirname(dir);
75
+ }
76
+ }
77
+ const chosen = [];
78
+ if (!scan.truncated)
79
+ for (const dir of [...directories].sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b))) {
80
+ if (dir === '.github' || chosen.some(parent => inside(dir, parent)) || scan.skippedPaths.some(file => inside(file, dir)))
81
+ continue;
82
+ const contents = scan.files.filter(file => inside(file, dir));
83
+ if (contents.some(file => /(^|\/)(tests?|examples?|samples?|bindings|python)\//.test(file.slice(dir.length + 1))))
84
+ continue;
85
+ // Mixed source types (for example bindings and a native library) need separate responsibility review.
86
+ const families = new Set(contents.map(file => /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/.test(file) ? 'native' : /\.py$/.test(file) ? 'python' : /\.[cm]?[jt]sx?$/.test(file) ? 'javascript' : 'other'));
87
+ families.delete('other');
88
+ if (contents.length > 1 && families.size <= 1 && contents.every(file => owned.has(file)))
89
+ chosen.push(dir);
90
+ }
91
+ const paths = [...chosen, ...files.filter(file => !chosen.some(dir => inside(file, dir)))];
92
+ return { paths: paths.slice(0, 30), directoryHints: chosen.slice(0, 30), truncated: paths.length > 30 };
93
+ }
94
+ /** Require independent implementation signals and a narrowly named specification before raising a review question. */
95
+ function formatArchitecturePrompts(files) {
96
+ const filenameWords = (file) => path.posix.basename(file, path.posix.extname(file))
97
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z])([A-Z][a-z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join(' ');
98
+ const implementations = files.filter(file => /\.(?:[cm]?[jt]sx?|java|kt|swift|c|cc|cpp|cxx|h|hh|hpp|hxx|m|py|go|rs|cs|fs|vb|rb|php|dart)$/i.test(file));
99
+ const roles = [/\b(?:headers?|validate|validation)\b/, /\b(?:nodes?|chunks?|writer|writing)\b/, /\b(?:hierarchy|hierarchies|serialize|serialization|serializer)\b/]
100
+ .map(pattern => implementations.filter(file => pattern.test(filenameWords(file))));
101
+ const specifications = files.filter(file => {
102
+ if (!/\.(?:md|mdx|rst|txt|adoc|pdf)$/i.test(file))
103
+ return false;
104
+ const name = filenameWords(file);
105
+ return /^(?:(?:file|binary|serialization) )?format(?: spec(?:ification)?)?$/.test(name)
106
+ || /^(?:spec|specification)$/.test(name) && /^(?:file[-_]?)?format$/i.test(path.posix.basename(path.posix.dirname(file)));
107
+ });
108
+ if (!specifications.length || roles.some(paths => !paths.length))
109
+ return [];
110
+ // A file with several suggestive words cannot stand in for independent source locations.
111
+ const distinct = (index, chosen) => {
112
+ if (index === roles.length)
113
+ return chosen;
114
+ for (const file of roles[index])
115
+ if (!chosen.includes(file)) {
116
+ const result = distinct(index + 1, [...chosen, file]);
117
+ if (result)
118
+ return result;
119
+ }
120
+ return undefined;
121
+ };
122
+ const representatives = distinct(0, []);
123
+ if (!representatives)
124
+ return [];
125
+ const paths = [...new Set([...representatives, ...specifications, ...roles.flat()])];
126
+ return [{ responsibility: 'Format architecture', paths: paths.slice(0, 10), pathCount: paths.length, pathsTruncated: paths.length > 10,
127
+ questions: ['Do these sources and the specification describe one coherent format contract: header/version validation, node/chunk writing and hierarchy serialization?',
128
+ 'Has navigation to this format repeatedly been needed beyond a single query, and would a small source entry help?',
129
+ 'Can the verified entry fit an existing approved chapter and its ownership boundaries?',
130
+ 'Has the developer directed adding the entry or any required ownership expansion after reviewing the source relationships?'],
131
+ basis: 'Heuristic review prompt from filenames only; relationships and topic coverage are unverified. No navigation entry, ownership boundary or fact is created.' }];
132
+ }
133
+ export async function discover(store, exclude = []) {
134
+ exclude = exclude.map(p => relativePath.parse(p));
135
+ const scan = await scanFiles(store, exclude), projects = new Map(), warnings = [];
136
+ let manifestBytesRead = 0;
137
+ const add = (root, technology, evidence, library = false) => {
138
+ const p = projects.get(root) ?? { root, technologies: new Set(), evidence: new Set(), library: false };
139
+ p.technologies.add(technology);
140
+ p.evidence.add(evidence);
141
+ p.library ||= library;
142
+ projects.set(root, p);
143
+ return p;
144
+ };
145
+ const warn = (file, reason) => warnings.push({ path: file, reason });
146
+ const read = async (file) => {
147
+ const safe = await store.safe(file), stat = await fs.stat(safe);
148
+ if (stat.size > FILE_BYTES) {
149
+ warn(file, 'manifest exceeds 256 KiB; content skipped');
150
+ return undefined;
151
+ }
152
+ if (manifestBytesRead + stat.size > TOTAL_BYTES) {
153
+ warn(file, '2 MiB manifest read budget exhausted');
154
+ return undefined;
155
+ }
156
+ // A bounded buffer also protects against a file growing between stat and read.
157
+ const handle = await fs.open(safe, 'r');
158
+ try {
159
+ const buffer = Buffer.alloc(FILE_BYTES + 1), { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
160
+ if (bytesRead > FILE_BYTES || manifestBytesRead + bytesRead > TOTAL_BYTES) {
161
+ warn(file, 'manifest grew beyond read budget; content skipped');
162
+ return undefined;
163
+ }
164
+ manifestBytesRead += bytesRead;
165
+ return buffer.subarray(0, bytesRead).toString('utf8');
166
+ }
167
+ finally {
168
+ await handle.close();
169
+ }
170
+ };
171
+ const json = (file, text) => {
172
+ const errors = [];
173
+ const data = parse(text, errors, { allowTrailingComma: true });
174
+ if (errors.length || !object(data)) {
175
+ warn(file, 'invalid JSON object; content signals skipped');
176
+ return undefined;
177
+ }
178
+ return data;
179
+ };
180
+ const manifestFiles = scan.files.filter(f => markers.test(path.posix.basename(f))).sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b));
181
+ for (const file of manifestFiles) {
182
+ const name = path.posix.basename(file), root = path.posix.dirname(file), text = await read(file);
183
+ // Filename signals remain useful even when content is too large or malformed.
184
+ if (name === 'CMakeLists.txt')
185
+ add(root, 'CMake', file);
186
+ if (name === 'meson.build')
187
+ add(root, 'Meson', file);
188
+ if (name === 'Package.swift')
189
+ add(root, 'Swift Package Manager', file);
190
+ if (name === 'project.pbxproj')
191
+ add(path.posix.dirname(root), 'Xcode', file);
192
+ if (name === 'pom.xml')
193
+ add(root, 'Maven', file);
194
+ if (/^build\.gradle/.test(name))
195
+ add(root, 'Gradle', file);
196
+ if (/^settings\.gradle/.test(name))
197
+ add(root, 'Gradle Workspace', file);
198
+ if (name === 'AndroidManifest.xml') {
199
+ const parts = file.split('/');
200
+ const src = parts.indexOf('src');
201
+ add(src >= 0 ? parts.slice(0, src).join('/') || '.' : root, 'Android', file);
202
+ }
203
+ if (name === 'go.mod')
204
+ add(root, 'Go', file);
205
+ if (name === 'go.work')
206
+ add(root, 'Go Workspace', file);
207
+ if (/^(pyproject\.toml|requirements.*\.txt|Pipfile|setup\.py)$/.test(name))
208
+ add(root, 'Python', file);
209
+ if (name === 'Cargo.toml')
210
+ add(root, 'Rust', file);
211
+ if (/\.(csproj|fsproj|vbproj|sln|slnx)$/.test(name))
212
+ add(root, '.NET', file);
213
+ if (name === 'Gemfile')
214
+ add(root, 'Ruby', file);
215
+ if (name === 'composer.json')
216
+ add(root, 'PHP', file);
217
+ if (name === 'pubspec.yaml')
218
+ add(root, 'Dart', file);
219
+ const workspace = { 'nx.json': 'Nx', 'turbo.json': 'Turborepo', 'lerna.json': 'Lerna', 'pnpm-workspace.yaml': 'pnpm Workspaces', 'pnpm-workspace.yml': 'pnpm Workspaces' };
220
+ if (workspace[name])
221
+ add(root, workspace[name], file);
222
+ if (name === 'angular.json')
223
+ add(root, 'Angular Workspace', file);
224
+ if (text === undefined)
225
+ continue;
226
+ if (name.endsWith('.json')) {
227
+ const data = json(file, text);
228
+ if (!data)
229
+ continue;
230
+ if (name === 'package.json') {
231
+ add(root, 'Node.js', file);
232
+ const deps = new Set(['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].flatMap(k => object(data[k]) ? Object.keys(data[k]) : []));
233
+ for (const [dependency, technology] of Object.entries(npmSignals))
234
+ if (deps.has(dependency))
235
+ add(root, technology, file);
236
+ if (Array.isArray(data.workspaces) || (object(data.workspaces) && Array.isArray(data.workspaces.packages)))
237
+ add(root, 'npm/Yarn Workspaces', file);
238
+ }
239
+ if (name === 'angular.json' && object(data.projects))
240
+ for (const value of Object.values(data.projects)) {
241
+ if (!object(value) || typeof value.root !== 'string')
242
+ continue;
243
+ if (value.root !== '' && value.root !== '.' && !relativePath.safeParse(value.root).success) {
244
+ warn(file, 'unsafe Angular project root ignored');
245
+ continue;
246
+ }
247
+ const child = path.posix.join(root, value.root);
248
+ if (!scan.files.some(f => inside(f, child))) {
249
+ warn(file, 'Angular project root outside scanned files; inspect manually');
250
+ continue;
251
+ }
252
+ add(child, 'Angular', file, value.projectType === 'library');
253
+ }
254
+ if (name === 'project.json' && (data.projectType === 'library' || data.projectType === 'application' || object(data.targets))) {
255
+ add(root, 'Nx Project', file, data.projectType === 'library');
256
+ const executors = object(data.targets) ? Object.values(data.targets).filter(object).map(t => t.executor ?? t.builder).filter((v) => typeof v === 'string') : [];
257
+ for (const executor of executors)
258
+ for (const [pattern, technology] of [[/^@(?:nx|nrwl)\/angular:|^@angular-devkit\/build-angular:/, 'Angular'], [/^@(?:nx|nrwl)\/next:/, 'Next.js'], [/^@(?:nx|nrwl)\/react-native:/, 'React Native'], [/^@(?:nx|nrwl)\/expo:/, 'Expo'], [/^@(?:nx|nrwl)\/react:/, 'React'], [/^@(?:nx|nrwl)\/vue:/, 'Vue']])
259
+ if (pattern.test(executor))
260
+ add(root, technology, file);
261
+ }
262
+ if ((name === 'app.json' || name === 'app.config.json') && object(data.expo)) {
263
+ add(root, 'Expo', file);
264
+ add(root, 'React Native', file);
265
+ }
266
+ if (name === 'composer.json' && object(data.require) && Object.hasOwn(data.require, 'laravel/framework'))
267
+ add(root, 'Laravel', file);
268
+ }
269
+ else {
270
+ // Configs are inspected as text, never evaluated, imported, or passed to package managers.
271
+ const content = text.replace(/^\s*#.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
272
+ if (/^(?:pom\.xml|(?:build|settings)\.gradle(?:\.kts)?)$/.test(name)) {
273
+ if (/org\.springframework\.boot/.test(content))
274
+ add(root, 'Spring Boot', file);
275
+ if (/com\.android\.(?:application|library)/.test(content))
276
+ add(root, 'Android', file);
277
+ }
278
+ if (/^(pyproject\.toml|requirements.*\.txt|Pipfile)$/.test(name))
279
+ for (const [pattern, label] of [[/\bdjango\b/i, 'Django'], [/\bfastapi\b/i, 'FastAPI'], [/\bflask\b/i, 'Flask']])
280
+ if (pattern.test(content))
281
+ add(root, label, file);
282
+ if (name === 'Gemfile' && /\bgem\s*\(?\s*['"]rails['"]/.test(content))
283
+ add(root, 'Rails', file);
284
+ if (name === 'pubspec.yaml' && /^\s*flutter\s*:/m.test(content))
285
+ add(root, 'Flutter', file);
286
+ if (name === 'Cargo.toml' && /^\s*\[workspace\]\s*$/m.test(content))
287
+ add(root, 'Cargo Workspace', file);
288
+ }
289
+ }
290
+ const nearest = (file) => [...projects.values()].filter(p => inside(file, p.root)).sort((a, b) => b.root.length - a.root.length)[0];
291
+ const conventional = (file) => file.match(/^(?:apps|packages|libs|services|projects)\/[^/]+/)?.[0] ?? '.';
292
+ const language = { '.c': 'C', '.cc': 'C++', '.cpp': 'C++', '.cxx': 'C++', '.hpp': 'C++', '.hxx': 'C++', '.java': 'Java', '.kt': 'Kotlin', '.swift': 'Swift', '.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.cs': '.NET', '.fs': '.NET', '.rb': 'Ruby', '.php': 'PHP', '.dart': 'Dart', '.vue': 'Vue', '.svelte': 'Svelte', '.astro': 'Astro',
293
+ '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript', '.ts': 'TypeScript', '.tsx': 'TypeScript', '.mts': 'TypeScript', '.cts': 'TypeScript' };
294
+ for (const file of scan.files) {
295
+ const technology = language[path.posix.extname(file)];
296
+ if (!technology)
297
+ continue;
298
+ const owner = nearest(file);
299
+ const fallback = conventional(file);
300
+ // A root workspace manifest must not swallow independent conventional application roots.
301
+ const root = owner && owner.root !== '.' ? owner.root : fallback;
302
+ add(root, technology, file);
303
+ }
304
+ // Shared UI/library paths are responsibility hints, not proof of React or any other framework.
305
+ for (const file of scan.files)
306
+ if (/^(?:packages\/(?:ui|components|shared)|libs\/[^/]+)\/.*\.[cm]?[jt]sx?$/.test(file)) {
307
+ const root = conventional(file), p = nearest(file);
308
+ if (!p || p.root === '.' || p.root === root)
309
+ add(root, 'JavaScript/TypeScript Library', file, true);
310
+ }
311
+ // Keep native platform subtrees of React Native/Flutter apps with their owning application.
312
+ const mobile = [...projects.values()].filter(p => p.technologies.has('React Native') || p.technologies.has('Expo') || p.technologies.has('Flutter')).sort((a, b) => a.root.length - b.root.length);
313
+ for (const p of [...projects.values()])
314
+ for (const parent of mobile) {
315
+ const relative = parent.root === '.' ? p.root : p.root.slice(parent.root.length + 1);
316
+ if (p !== parent && inside(p.root, parent.root) && /^(ios|android)(\/|$)/.test(relative)) {
317
+ for (const tech of p.technologies)
318
+ parent.technologies.add(tech);
319
+ for (const evidence of p.evidence)
320
+ parent.evidence.add(evidence);
321
+ projects.delete(p.root);
322
+ break;
323
+ }
324
+ }
325
+ const role = (p) => {
326
+ const has = (...names) => names.some(n => p.technologies.has(n));
327
+ if (has('React Native', 'Expo', 'Flutter'))
328
+ return 'mobile-applications';
329
+ if (p.library)
330
+ return 'web-components';
331
+ const workspace = has('Nx', 'Turborepo', 'Lerna', 'npm/Yarn Workspaces', 'pnpm Workspaces', 'Angular Workspace', 'Go Workspace', 'Cargo Workspace', 'Gradle Workspace');
332
+ const children = [...projects.keys()].some(root => root !== p.root && inside(root, p.root));
333
+ const ownSource = [...p.evidence].some(file => {
334
+ const relative = p.root === '.' ? file : file.slice(p.root.length + 1);
335
+ return source.test(file) && (/^(src|app|pages)\//.test(relative) || /^(App|index)\.[jt]sx?$/.test(relative));
336
+ });
337
+ if (workspace && children && !ownSource)
338
+ return 'workspace-tooling';
339
+ if (has('Angular', 'React', 'Next.js', 'Vue', 'Nuxt', 'Svelte', 'SvelteKit', 'Astro'))
340
+ return 'web-applications';
341
+ if (has('Android'))
342
+ return 'mobile-applications';
343
+ if (has('Swift', 'Swift Package Manager', 'Xcode'))
344
+ return 'native-applications';
345
+ if (has('Java', 'Kotlin', 'Maven', 'Gradle', 'Spring Boot'))
346
+ return 'jvm-applications';
347
+ if (has('C', 'C++', 'CMake', 'Meson'))
348
+ return 'cpp-projects';
349
+ for (const [tech, key] of [['Python', 'python'], ['Go', 'go'], ['.NET', 'dotnet'], ['Rust', 'rust'], ['Ruby', 'ruby'], ['PHP', 'php'], ['Dart', 'dart']])
350
+ if (has(tech))
351
+ return `${key}-projects`;
352
+ if (workspace)
353
+ return 'workspace-tooling';
354
+ return 'javascript-projects';
355
+ };
356
+ const groups = new Map();
357
+ const ordered = [...projects.values()].sort((a, b) => b.root.length - a.root.length || a.root.localeCompare(b.root));
358
+ for (const file of scan.files) {
359
+ if (ci(file)) {
360
+ const key = 'ci-cd', g = groups.get(key) ?? { project: { root: '.', technologies: new Set(['CI/CD']), evidence: new Set(), library: false }, files: [] };
361
+ g.files.push(file);
362
+ g.project.evidence.add(file);
363
+ groups.set(key, g);
364
+ continue;
365
+ }
366
+ if (!source.test(file) && !markers.test(path.posix.basename(file)))
367
+ continue;
368
+ const p = ordered.find(p => inside(file, p.root));
369
+ if (!p)
370
+ continue;
371
+ const key = `${role(p)}:${p.root}`, g = groups.get(key) ?? { project: p, files: [] };
372
+ g.files.push(file);
373
+ groups.set(key, g);
374
+ }
375
+ const definitions = [], detections = [], selected = new Set();
376
+ const candidates = [...groups].sort(([a], [b]) => a.localeCompare(b));
377
+ for (const [group, { project: p, files }] of candidates.slice(0, PROJECT_LIMIT)) {
378
+ const id = group === 'ci-cd' ? group : role(p), [title, scope] = roles[id];
379
+ let pillar = definitions.find(d => d.id === id);
380
+ if (!pillar) {
381
+ pillar = { id, title, scope, excludes: 'Responsibilities owned by other approved pillars.', chapters: [] };
382
+ definitions.push(pillar);
383
+ }
384
+ const chapterId = p.root === '.' ? 'overview' : `${p.root.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'project'}-${createHash('sha256').update(p.root).digest('hex').slice(0, 8)}`;
385
+ const hints = ownershipHints([...files].sort((a, b) => Number(p.evidence.has(b)) - Number(p.evidence.has(a)) || a.localeCompare(b)), scan), paths = hints.paths;
386
+ for (const file of scan.files)
387
+ if (paths.some(scope => inside(file, scope)))
388
+ selected.add(file);
389
+ pillar.chapters.push({ id: chapterId, title: p.root === '.' ? 'Overview' : `Project ${p.root}`.slice(0, 100), scope, excludes: 'Unrelated projects and other pillar responsibilities.', paths });
390
+ detections.push({ root: p.root, technologies: [...p.technologies].sort(), evidence: [...p.evidence].sort().slice(0, 6), evidenceCount: p.evidence.size,
391
+ chapterId: `${id}/${chapterId}`, pathHintCount: paths.length, pathsTruncated: hints.truncated, matchedFileCount: files.length,
392
+ ownershipCoverage: scan.truncated || hints.truncated ? 'incomplete' : 'scanned-files', directoryHints: hints.directoryHints,
393
+ rationale: `${title} is a coarse candidate from ${[...p.technologies].sort().join(', ')} signals at ${p.root}. Inspect source to separate runtime, interfaces, tests, examples and delivery responsibilities; technology labels do not establish ownership.` });
394
+ }
395
+ const responsibilityHints = [
396
+ { responsibility: 'Native library or application', pattern: /(^|\/)(cpp|include|native)(\/|$)/ },
397
+ { responsibility: 'Python interface or application', pattern: /(^|\/)(python|bindings)(\/|$)/ },
398
+ { responsibility: 'Test validation and fixtures', pattern: /(^|\/)(tests?|testing)(\/|$)/ },
399
+ { responsibility: 'Usage examples', pattern: /(^|\/)(examples?|samples?)(\/|$)/ },
400
+ { responsibility: 'Build and delivery', pattern: /(^|\/)(CMakeLists\.txt|pyproject\.toml|package\.json|\.release-it\.[^/]+)$|(^|\/)\.github\/workflows\// },
401
+ ].flatMap(({ responsibility, pattern }) => {
402
+ const paths = scan.files.filter(file => pattern.test(file));
403
+ return paths.length ? [{ responsibility, paths: paths.slice(0, 6), pathCount: paths.length, pathsTruncated: paths.length > 6,
404
+ rationale: 'Path conventions suggest a responsibility to review, not an approved boundary or a factual claim.' }] : [];
405
+ });
406
+ const deliveryFiles = scan.files.filter(file => ci(file) || /(^|\/)(package\.json|pyproject\.toml|setup\.py|CMakeLists\.txt|\.release-it\.[^/]+)$/.test(file));
407
+ const ownershipIncomplete = scan.truncated || candidates.length > PROJECT_LIMIT || detections.some(d => d.pathsTruncated);
408
+ return { schemaVersion: 2, requiresDeveloperApproval: true, scan: { inspectedFiles: scan.files.length, inspectedEntries: scan.inspectedEntries, truncated: scan.truncated, limit: ENTRY_LIMIT, excludedPaths: exclude, skipped: scan.skipped, skippedCount: scan.skippedCount, manifestBytesRead, manifestByteLimit: TOTAL_BYTES },
409
+ pillars: definitions, detections, detectedProjectCount: candidates.length, projectsTruncated: candidates.length > PROJECT_LIMIT,
410
+ responsibilityHints, ownershipIncomplete,
411
+ next: ownershipIncomplete ? 'Ownership hints are incomplete. Inspect omitted files and truncated trees, then expand or split boundaries before bootstrap. Do not publish the sampled map as complete.' : 'Review suggested responsibilities and directory boundaries against source, including unclassified and excluded areas, before bootstrap.',
412
+ coveragePrompts: [...(deliveryFiles.length ? [{ responsibility: 'Build and delivery', paths: deliveryFiles.slice(0, 10), pathCount: deliveryFiles.length, pathsTruncated: deliveryFiles.length > 10,
413
+ questions: ['How are versions chosen and releases triggered?', 'Which jobs build and test each artifact?', 'Where are artifacts published, and what gates publication?', 'Which distribution steps are external or not established by inspected source?'],
414
+ basis: 'Review prompts from detected filenames; validation does not establish completeness.' }] : []), ...formatArchitecturePrompts(scan.files)],
415
+ warnings: warnings.slice(0, 10), warningCount: warnings.length, unclassifiedSample: scan.files.filter(f => !selected.has(f)).slice(0, 30),
416
+ note: 'Heuristic candidates only. Frameworks are navigation signals, not facts or automatic pillar boundaries. Review and merge responsibility boundaries, expand sampled paths, and inspect unclassified or truncated areas. No facts or dependencies have been inferred.' };
417
+ }
@@ -0,0 +1,15 @@
1
+ /** Stable CLI error envelope. Unclassified domain failures use OPERATION_FAILED. */
2
+ export declare class GroundError extends Error {
3
+ code: string;
4
+ fields: string[];
5
+ recovery: string;
6
+ details?: unknown | undefined;
7
+ constructor(code: string, message: string, fields?: string[], recovery?: string, details?: unknown | undefined);
8
+ }
9
+ export declare function errorPayload(error: unknown): {
10
+ details?: {} | undefined;
11
+ code: string;
12
+ message: string;
13
+ fields: string[];
14
+ recovery: string;
15
+ };
package/dist/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ /** Stable CLI error envelope. Unclassified domain failures use OPERATION_FAILED. */
2
+ export class GroundError extends Error {
3
+ code;
4
+ fields;
5
+ recovery;
6
+ details;
7
+ constructor(code, message, fields = [], recovery = 'Inspect the command help and correct the request.', details) {
8
+ super(message);
9
+ this.code = code;
10
+ this.fields = fields;
11
+ this.recovery = recovery;
12
+ this.details = details;
13
+ }
14
+ }
15
+ export function errorPayload(error) {
16
+ const value = error;
17
+ if (error instanceof GroundError)
18
+ return { code: error.code, message: error.message, fields: error.fields, recovery: error.recovery, ...(error.details ? { details: error.details } : {}) };
19
+ if (Array.isArray(value.issues))
20
+ return { code: 'INVALID_INPUT', message: value.issues.map(i => `${i.path.join('.') || 'input'}: ${i.message}`).join('; '), fields: [...new Set(value.issues.map(i => i.path.join('.') || 'input'))], recovery: 'Inspect cground schema <operation> and correct the affected fields.' };
21
+ return { code: value.code ?? 'OPERATION_FAILED', message: value.message ?? String(error), fields: value.fields ?? (value.path ? [value.path] : []), recovery: value.recovery ?? 'Inspect cground <command> --help and correct the request.' };
22
+ }
@@ -0,0 +1,14 @@
1
+ import type { Store } from './store.js';
2
+ import type { RegistryRecord } from './model.js';
3
+ export declare const markdownPath = ".common-ground/local/knowledge.md";
4
+ /** Complete, deterministic view of shared knowledge; no timestamps or local drafts. */
5
+ export declare function renderKnowledge(registry: RegistryRecord | null): string;
6
+ /** Caller holds the store writer lock when publishing shared records. */
7
+ export declare function writeKnowledgeExport(store: Store, registry: RegistryRecord | null): Promise<{
8
+ path: string;
9
+ changed: boolean;
10
+ }>;
11
+ export declare function refreshKnowledgeExport(store: Store): Promise<{
12
+ path: string;
13
+ changed: boolean;
14
+ }>;
package/dist/export.js ADDED
@@ -0,0 +1,86 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ export const markdownPath = '.common-ground/local/knowledge.md';
4
+ const localName = 'local/knowledge.md';
5
+ const text = (value) => value.replace(/[\\`*_[\]<>#|]/g, '\\$&');
6
+ const paragraph = (value) => text(value).replace(/\r?\n/g, '\n\n');
7
+ const sourceLink = (file) => `[${text(file)}](../../${file.split('/').map(part => encodeURIComponent(part).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)).join('/')})`;
8
+ const factLink = (key) => `[${text(key)}](#fact-${key})`;
9
+ function quote(value) {
10
+ const fence = '`'.repeat(Math.max(3, ...[...value.matchAll(/`+/g)].map(m => m[0].length + 1)));
11
+ return `${fence}text\n${value}\n${fence}`;
12
+ }
13
+ /** Complete, deterministic view of shared knowledge; no timestamps or local drafts. */
14
+ export function renderKnowledge(registry) {
15
+ const lines = ['# Common Ground knowledge', '',
16
+ 'Generated from [knowledge.json](../knowledge.json). This local file is disposable; edit the shared registry through the reviewed knowledge workflow.', '',
17
+ 'This is a snapshot of stored knowledge, not proof that every assertion is currently true. Verify current source and run `cground validate` before relying on it.', ''];
18
+ if (!registry)
19
+ return [...lines, '## No approved knowledge yet', '',
20
+ 'Review the [proposed responsibility map](bootstrap.json) with your developer. After approval and fact population, this file will contain the complete shared knowledge.', ''].join('\n');
21
+ const chapters = registry.pillars.flatMap(p => p.chapters);
22
+ lines.push(`Schema version: ${registry.schemaVersion}. Pillars: ${registry.pillars.length}. Chapters: ${chapters.length}. Facts: ${chapters.reduce((n, c) => n + c.facts.length, 0)}.`, '', `Registry fingerprint (SHA-256): ${createHash('sha256').update(JSON.stringify(registry)).digest('hex')}`, '', '## Contents', '');
23
+ if (!registry.pillars.length)
24
+ lines.push('No pillars have been recorded.', '');
25
+ for (const p of registry.pillars) {
26
+ lines.push(`- [${text(p.title)}](#pillar-${p.id})`);
27
+ for (const c of p.chapters)
28
+ lines.push(` - [${text(c.title)}](#chapter-${p.id}/${c.id}) — ${c.facts.length} facts`);
29
+ }
30
+ lines.push('');
31
+ for (const p of registry.pillars) {
32
+ lines.push(`<a id="pillar-${p.id}"></a>`, '', `## ${text(p.title)}`, '', `**Pillar ID:** ${p.id}`, '', `**Scope:** ${paragraph(p.scope)}`, '', `**Excludes:** ${paragraph(p.excludes)}`, '');
33
+ for (const c of p.chapters) {
34
+ const key = `${p.id}/${c.id}`;
35
+ lines.push(`<a id="chapter-${key}"></a>`, '', `### ${text(c.title)}`, '', `**Chapter ID:** ${key} · **Revision:** ${c.revision}`, '', `**Scope:** ${paragraph(c.scope)}`, '', `**Excludes:** ${paragraph(c.excludes)}`, '', '**Owned paths**', '', ...c.paths.map(file => `- ${sourceLink(file)}`), '');
36
+ if (!c.facts.length)
37
+ lines.push('No facts recorded in this chapter.', '');
38
+ for (const f of c.facts) {
39
+ const factKey = `${key}/${f.id}`;
40
+ lines.push(`<a id="fact-${factKey}"></a>`, '', `#### ${f.id}`, '', paragraph(f.statement), '', `**Fact ID:** ${factKey}`, '', '**Source scope**', '', ...f.sourceScope.map(file => `- ${sourceLink(file)}`), '', '**Depends on**', '', ...(f.dependsOn.length ? f.dependsOn.map(dep => `- ${factLink(dep)}`) : ['None.']), '', '**Evidence**', '');
41
+ for (const e of f.evidence)
42
+ lines.push(sourceLink(e.path), '', quote(e.quote), '');
43
+ }
44
+ // Preserve every stored baseline without burying the prose in machine metadata.
45
+ lines.push('<details>', '<summary>Stored source hashes and dependency fingerprints</summary>', '', '**Source hashes**', '', '| Source | SHA-256 |', '| --- | --- |', ...Object.entries(c.sources).sort(([a], [b]) => a.localeCompare(b)).map(([file, hash]) => `| ${sourceLink(file)} | ${text(hash)} |`), '', '**Dependency fingerprints**', '', '| Fact | Fingerprint |', '| --- | --- |', ...Object.entries(c.dependencyFingerprints).sort(([a], [b]) => a.localeCompare(b)).map(([dep, hash]) => `| ${factLink(dep)} | ${text(hash)} |`), '', '</details>', '');
46
+ }
47
+ }
48
+ return lines.join('\n');
49
+ }
50
+ /** Caller holds the store writer lock when publishing shared records. */
51
+ export async function writeKnowledgeExport(store, registry) {
52
+ const content = renderKnowledge(registry);
53
+ await store.safe(markdownPath, true);
54
+ try {
55
+ if (await fs.readFile(store.file(localName), 'utf8') === content)
56
+ return { path: markdownPath, changed: false };
57
+ }
58
+ catch (e) {
59
+ if (e.code !== 'ENOENT')
60
+ throw e;
61
+ }
62
+ await fs.mkdir(store.file('local'), { recursive: true });
63
+ const temporary = store.file(`${localName}.${randomUUID()}.tmp`);
64
+ try {
65
+ await fs.writeFile(temporary, content, { flag: 'wx' });
66
+ await fs.rename(temporary, store.file(localName));
67
+ }
68
+ finally {
69
+ await fs.rm(temporary, { force: true });
70
+ }
71
+ return { path: markdownPath, changed: true };
72
+ }
73
+ export async function refreshKnowledgeExport(store) {
74
+ return store.lock(async () => {
75
+ let registry;
76
+ try {
77
+ registry = await store.read();
78
+ }
79
+ catch (e) {
80
+ if (e.code !== 'ENOENT')
81
+ throw e;
82
+ registry = null;
83
+ }
84
+ return writeKnowledgeExport(store, registry);
85
+ });
86
+ }
@@ -0,0 +1,11 @@
1
+ export declare const bootstrapNext: {
2
+ action: string;
3
+ approvalRequired: boolean;
4
+ command: string;
5
+ schema: string;
6
+ guide: string;
7
+ authorization: string;
8
+ };
9
+ export declare const rules = "## Common Ground\n\nCode is the source of truth; these notes only help navigation.\n\nFor repository questions, use cground lookup with a path or short query for facts and source locations. Lookup is stateless; default freshness is not checked. Open source and directory READMEs before relying on claims. Use --verify when live freshness matters. Derive temporary state live.\n\nKeep the developer task primary. After edits, cground assess --touched checks only this task's actual paths, including additions/deletions. No matching source change means no fact review or task bookkeeping; still review relevant local documentation. Source drift means check the affected claims, not automatic revision. Follow additional semantic connections found in source.\n\nIf corrections are needed, request assess --review. Before editing, read [.common-ground/POLICY.md](.common-ground/POLICY.md), every required chapter page and the listed sources/docs. prepare-patch works without taskId; complete review and conflict checks remain mandatory. Summarize actual corrections as before \u2192 after, why, source links and recommendation.\n\nTask contexts are optional for aggregated reporting and deferred additions. Start one when needed; propose_facts queues drafts, finish after the main task, then ask \"Ready to make the following facts available to the team?\" New facts, chapters, pillars and ownership expansion need developer direction. If started, finish the task once; reuse cached responses only within it and refresh:true after context loss.\n\nDefault to no write. Keep durable patterns, not debugging history; library-local detail belongs in its README. Resolve disputed behavior with the developer. No change means no Common Ground report. Setup: [.common-ground/START_HERE.md](.common-ground/START_HERE.md).\n";
10
+ export declare const policy = "## Common Ground\n\nCode is the source of truth. Notes cache durable repository knowledge; matching quotes and hashes establish mechanical consistency, not semantic truth.\n\n### Reading and writing invariants\n\n- Open relevant source and directory, ancestor, sibling, child and referenced documentation this session. Derive temporary state live. Do not record bugs, debugging history, activity logs, secrets or guesses.\n- Default to no write. Unrelated work and unchanged facts produce no shared knowledge changes. Library-local detail belongs in its README. Each fact holds one coherent, source-backed claim of at most 2,000 characters.\n- Review every existing fact in each required logical chapter before revision. Follow dependency and dependent links, request the review checklist, and verify all required source and documentation. A verification declaration does not prove an agent read or understood a file.\n- Correct verified contradictions in the affected scope quietly, preserving IDs. Merge duplicates and remove superseded claims with explicit reasons and repaired references. Never clean up unrelated responsibilities. Ask if uncertainty blocks the task; otherwise collect it for completion.\n- New facts, chapters, pillars and ownership expansion require developer direction. A new pillar also needs an uncovered standalone responsibility. Approval flags declare developer direction; they never grant autonomous permission or prove human review. Boundary approval does not approve facts. For initial setup, explicit delegation to save/publish may cover verified boundaries and facts within the requested scope; see START_HERE.md. Routine additions still follow the deferred approval workflow below.\n- Chapter paths define non-overlapping ownership. Fact source scopes stay inside ownership; supporting evidence may cite other repository files and is tracked for freshness. Exact evidence, safe paths and valid dependencies remain mandatory.\n- Publish a complete reviewed transaction. Source or registry conflicts reject publication. Unchanged reviews use ignored local receipts; only changed chapters are rewritten. Never bypass required tests based on notes.\n\n### Quiet task workflow\n\nUse stateless lookup for relevant facts and source paths; default freshness is not checked. After edits, assess actual task-touched paths (including additions and deletions). A matching unchanged source requires no fact review; changed source requires checking affected claims, not automatic revision. Review local/ancestor documentation relevant to the edit and follow semantic connections found in source. Empty touched paths require no review. Use assess --review when a correction needs complete chapters and verification paths. Neither command creates task state or requires finish.\n\nBefore correcting notes, read this policy and every page of all required chapters. prepare_patch attests reviewedAllFacts:true and the expected revision, then retains unchanged facts. Use prepare_patch without taskId and commit_update for verified corrections. Task contexts remain optional for aggregate reporting and deferred additions. Source or revision conflicts require fresh review. Citation-only repairs may use empty touchedPaths without a tidyId: only evidence may change; the fact ID, statement, sourceScope and dependsOn must remain unchanged. Supply a verified reason, read every required chapter, attest source/documentation verification and the expected revisions. Publication rechecks source and registry conflicts. Use review-plan and review-checklist for the selected chapter; do not invent touched paths.\n\nFor new facts, start a task when needed and queue them with propose_facts; pending drafts are never authoritative. Finish the developer task and checks, then finish any task context you started. Reuse cached task responses only within that task; refresh:true after context loss. Summarize actual corrections as before \u2192 after, why, source links and keep/reverify recommendation. Present additions together and ask \"Ready to make the following facts available to the team?\" Wait for explicit approval, read proposal-review and required chapters/source, then accept-facts with complete reviews and verification. Drop rejected entries first. Changed drafts need fresh verification and approval. Never require developer JSON review. With no changes or pending questions, give no Common Ground report.\n\nUse tidy only for developer-requested cleanup. It returns a local tidyId; the calling agent reads source and submits verified corrections. Common Ground runs no model or autonomous watcher. If unavailable, continue safe task work and report unresolved maintenance; never claim a failed write succeeded. Host tool approval settings remain authoritative.\n\nSee START_HERE.md for commands and state-specific setup. Keep knowledge.json and managed guidance tracked; local/ is disposable and ignored. Never remove another active task's files.\n";
11
+ export declare const startGuide = "# Start Here \u2014 Common Ground\n\nCommon Ground is a framework for shared, Git-backed repository knowledge. Its interfaces are the CLI and local MCP server. Code is the source of truth.\n\n## Initial setup: draft, verify, publish\n\n1. Run cground init. It creates guidance, MCP configuration and a local candidate map without creating knowledge.json. Use --skip-hook if Git metadata is protected; hook permission failures are advisory and setup continues. The setup report lists completed, skipped, failed and pending steps. Retry safely after fixing a failure; existing registries and bootstrap drafts are preserved. Install the reminder later with cground hook install.\n2. Read source and directory READMEs. Refine .common-ground/local/bootstrap.json into responsibility boundaries and verified facts. detections are raw technology/path signals; responsibilityHints and directory paths are suggestions. Inspect ownershipIncomplete, pathsTruncated, scan.truncated and unclassified samples. Expand or split incomplete boundaries. Follow coveragePrompts for release triggers, versioning, artifacts and publication where detected. Validity and unchanged evidence do not establish complete coverage.\n3. Use cground schema bootstrap and cground bootstrap --help --example. Prepare pillars plus batches of chapterId/facts, then run cground bootstrap PLAN.json --dry-run. This validates the whole transaction without writes and returns source-file counts and a preflight token. JSON commands also accept --stdin.\n4. Establish publication direction from the developer's request. \u201CInitialize\u201D or \u201Cgenerate a map\u201D means draft by default. \u201CInitialize and save/publish the map autonomously\u201D explicitly delegates publication of verified initial boundaries and facts within that scope. Honor an existing delegation without asking again. Otherwise present the exact boundaries and facts in plain language and ask \u201CReady to make the following facts available to the team?\u201D Wait for approval before publication. A draft-only request never authorizes publication; boundary-only approval never approves facts.\n5. With publication direction, apply the same payload using --approve --preflight TOKEN, run cground check, and summarize what was saved with source links and remaining coverage gaps. Source or payload changes require a fresh dry run and verification; confirm they remain within any delegated scope, or obtain new content approval. Flags declare direction and tokens detect changes; neither proves human review, identity, semantic truth or completeness. Host tool approval settings still apply.\n\nThe bootstrap path above is self-contained for an empty registry. Before revising existing knowledge, read [POLICY.md](POLICY.md) and every required chapter/source page. For separate setup stages, cground approve creates boundaries only; seed-batch --dry-run and seed-batch --approve --preflight TOKEN populate approved empty chapters with authorized facts. Use command help for exact inputs.\n\nA bootstrap-required or not-initialized response has no taskId. Continue the developer task from source. Never fabricate an empty registry or call task assess/propose/finish without a returned taskId. After setup, use lookup; task contexts are optional for deferred additions or aggregate reporting.\n\n## My build failed. Where do I go?\n\nFollow AGENTS.md for lookup and task-touched assessment. Lookup navigation contains separately labeled ownership/source hints, not facts; freshness is not checked by default. Read source when matches only partly cover a question. --verify checks selected facts and dependencies, not topic completeness or navigation hints.\n\nFor corrections, assess --review supplies complete chapters and verification paths. Read POLICY.md and every required page before prepare-patch and commit. For deferred additions and task reporting, POLICY.md describes the task_context workflow.\n\n## Setup and review commands\n\n- cground doctor checks setup; cground refresh-guidance updates managed instructions while preserving surrounding text. A healthy check returns next: null.\n- cground check [TARGET] validates structure, exact evidence and freshness. --all-results includes passing rows; follow nextCursor. --cleanup y creates a developer-requested cleanup plan, not a completed repair.\n- cground review [TARGET] summarizes changes against HEAD. --staged selects the index; --evidence includes changed quotations. Explain before \u2192 after, why, sources and a keep/approve/reverify recommendation.\n- cground export refreshes [local/knowledge.md](local/knowledge.md), the ignored reference excluding drafts. Successful knowledge writes refresh it too.\n- cground hook mute/unmute controls advisory reminders. Existing hook managers can call cground hook check.\n\nEvery command has --help. cground schema OPERATION gives CLI payloads; --both adds MCP arguments. Both MCP profiles expose these workflows through cground. Keep knowledge.json and managed guidance tracked; local/ is ignored. See POLICY.md for maintenance invariants and the framework's docs/pillar-contract.md and docs/quiet-workflow.md for full contracts.\n";