@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,146 @@
1
+ import { z } from 'zod';
2
+ import { Store } from './store.js';
3
+ import { type RegistryRecord } from './model.js';
4
+ export declare function listPillars(store: Store, cursor?: string, limit?: number): Promise<{
5
+ items: {
6
+ id: string;
7
+ title: string;
8
+ scope: string;
9
+ excludes: string;
10
+ chapterCount: number;
11
+ }[];
12
+ total: number;
13
+ nextCursor: string | null;
14
+ }>;
15
+ export declare function listChapters(store: Store, pillarId: string, cursor?: string, limit?: number): Promise<{
16
+ items: {
17
+ chapterId: string;
18
+ title: string;
19
+ scope: string;
20
+ excludes: string;
21
+ revision: number;
22
+ factCount: number;
23
+ }[];
24
+ total: number;
25
+ nextCursor: string | null;
26
+ pillarId: string;
27
+ }>;
28
+ export declare function readChapter(store: Store, key: string, cursor?: string, limit?: number): Promise<{
29
+ chapterId: string;
30
+ title: string;
31
+ scope: string;
32
+ excludes: string;
33
+ paths: string[];
34
+ revision: number;
35
+ freshness: {
36
+ chapterId: string;
37
+ revision: number;
38
+ status: string;
39
+ locallyReviewed: boolean;
40
+ changedPaths: string[];
41
+ dependencyDrift: boolean;
42
+ error?: undefined;
43
+ } | {
44
+ chapterId: string;
45
+ revision: number;
46
+ status: string;
47
+ error: any;
48
+ locallyReviewed?: undefined;
49
+ changedPaths?: undefined;
50
+ dependencyDrift?: undefined;
51
+ };
52
+ facts: {
53
+ items: {
54
+ id: string;
55
+ statement: string;
56
+ evidencePaths: string[];
57
+ }[];
58
+ total: number;
59
+ nextCursor: string | null;
60
+ };
61
+ }>;
62
+ export declare function readFact(store: Store, key: string, id: string): Promise<{
63
+ chapterId: string;
64
+ revision: number;
65
+ fact: {
66
+ id: string;
67
+ statement: string;
68
+ evidence: {
69
+ path: string;
70
+ quote: string;
71
+ }[];
72
+ sourceScope: string[];
73
+ dependsOn: string[];
74
+ };
75
+ freshness: {
76
+ chapterId: string;
77
+ revision: number;
78
+ status: string;
79
+ locallyReviewed: boolean;
80
+ changedPaths: string[];
81
+ dependencyDrift: boolean;
82
+ error?: undefined;
83
+ } | {
84
+ chapterId: string;
85
+ revision: number;
86
+ status: string;
87
+ error: any;
88
+ locallyReviewed?: undefined;
89
+ changedPaths?: undefined;
90
+ dependencyDrift?: undefined;
91
+ };
92
+ }>;
93
+ export declare function search(store: Store, query: string, limit?: number, chapterId?: string, registry?: RegistryRecord, cache?: Map<string, Promise<string>>): Promise<{
94
+ freshness: any;
95
+ chapterId: string;
96
+ fact: {
97
+ id: string;
98
+ statement: string;
99
+ evidencePaths: string[];
100
+ };
101
+ matchedTerms: string[];
102
+ score: number;
103
+ directPath: boolean;
104
+ }[]>;
105
+ export declare const ReadKnowledge: z.ZodObject<{
106
+ cursor: z.ZodOptional<z.ZodString>;
107
+ limit: z.ZodOptional<z.ZodNumber>;
108
+ kind: z.ZodEnum<["pillars", "chapters", "chapter", "fact", "search", "owners", "graph", "review", "checklist", "tidy", "proposals", "proposal-review"]>;
109
+ target: z.ZodOptional<z.ZodString>;
110
+ query: z.ZodOptional<z.ZodString>;
111
+ paths: z.ZodOptional<z.ZodArray<z.ZodPipeline<z.ZodEffects<z.ZodString, string, string>, z.ZodEffects<z.ZodString, string, string>>, "many">>;
112
+ taskId: z.ZodOptional<z.ZodString>;
113
+ refresh: z.ZodOptional<z.ZodBoolean>;
114
+ evidence: z.ZodOptional<z.ZodBoolean>;
115
+ }, "strict", z.ZodTypeAny, {
116
+ kind: "chapters" | "pillars" | "search" | "chapter" | "fact" | "review" | "tidy" | "owners" | "graph" | "checklist" | "proposals" | "proposal-review";
117
+ evidence?: boolean | undefined;
118
+ paths?: string[] | undefined;
119
+ target?: string | undefined;
120
+ query?: string | undefined;
121
+ limit?: number | undefined;
122
+ cursor?: string | undefined;
123
+ taskId?: string | undefined;
124
+ refresh?: boolean | undefined;
125
+ }, {
126
+ kind: "chapters" | "pillars" | "search" | "chapter" | "fact" | "review" | "tidy" | "owners" | "graph" | "checklist" | "proposals" | "proposal-review";
127
+ evidence?: boolean | undefined;
128
+ paths?: string[] | undefined;
129
+ target?: string | undefined;
130
+ query?: string | undefined;
131
+ limit?: number | undefined;
132
+ cursor?: string | undefined;
133
+ taskId?: string | undefined;
134
+ refresh?: boolean | undefined;
135
+ }>;
136
+ export declare function readKnowledge(store: Store, input: unknown): Promise<unknown>;
137
+ export declare function reviewChecklist(store: Store, chapterIds: string[], touchedPaths: string[], cursor?: string, limit?: number): Promise<{
138
+ items: {
139
+ state: string;
140
+ kind: string;
141
+ path: string;
142
+ }[];
143
+ total: number;
144
+ nextCursor: string | null;
145
+ policy: string;
146
+ }>;
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod';
2
+ import { relativePath } from './model.js';
3
+ import { Workflow } from './workflow.js';
4
+ import { page } from './paging.js';
5
+ import { ownershipMap, pillarGraph, tidyPlan } from './navigation.js';
6
+ import { queryTerms, termWeights, queryPath, matchFields } from './matching.js';
7
+ export async function listPillars(store, cursor, limit) { return page((await store.read()).pillars.map(({ id, title, scope, excludes, chapters }) => ({ id, title, scope, excludes, chapterCount: chapters.length })), cursor, limit); }
8
+ export async function listChapters(store, pillarId, cursor, limit) {
9
+ const reg = await store.read();
10
+ const p = reg.pillars.find(p => p.id === pillarId);
11
+ if (!p)
12
+ throw new Error('Unknown pillar');
13
+ return { pillarId, ...page(p.chapters.map(c => ({ chapterId: `${p.id}/${c.id}`, title: c.title, scope: c.scope, excludes: c.excludes, revision: c.revision, factCount: c.facts.length })), cursor, limit) };
14
+ }
15
+ export async function readChapter(store, key, cursor, limit) {
16
+ const reg = await store.read();
17
+ const c = store.chapter(reg, key);
18
+ return { chapterId: key, title: c.title, scope: c.scope, excludes: c.excludes, paths: c.paths, revision: c.revision, freshness: await store.status(key, reg), facts: page(c.facts.map(({ id, statement, evidence }) => ({ id, statement, evidencePaths: [...new Set(evidence.map(e => e.path))] })), cursor, limit, String(c.revision)) };
19
+ }
20
+ export async function readFact(store, key, id) { const reg = await store.read(); const c = store.chapter(reg, key); const fact = c.facts.find(f => f.id === id); if (!fact)
21
+ throw new Error('Unknown fact'); return { chapterId: key, revision: c.revision, fact, freshness: await store.status(key, reg) }; }
22
+ export async function search(store, query, limit = 8, chapterId, registry, cache) {
23
+ if (!Number.isInteger(limit) || limit < 1 || limit > 20)
24
+ throw new Error('Search limit must be between 1 and 20');
25
+ const terms = queryTerms(query);
26
+ const reg = registry ?? await store.read();
27
+ if (chapterId)
28
+ store.chapter(reg, chapterId);
29
+ const entries = store.facts(reg).filter(entry => !chapterId || entry.chapterId === chapterId);
30
+ const { weights } = termWeights(terms, entries.map(({ key, fact }) => `${key} ${fact.statement} ${fact.evidence.map(e => e.path).join(' ')}`));
31
+ const path = queryPath(query);
32
+ const matches = entries.map(({ key, chapterId, fact }) => {
33
+ const match = matchFields(terms, `${key} ${fact.evidence.map(e => e.path).join(' ')}`, fact.statement, weights);
34
+ const directPath = !!path && fact.evidence.some(e => e.path === path || e.path.startsWith(`${path}/`));
35
+ return { chapterId, fact: { id: fact.id, statement: fact.statement, evidencePaths: [...new Set(fact.evidence.map(e => e.path))] }, matchedTerms: match.matchedTerms, score: match.weight, directPath };
36
+ }).filter(m => m.directPath || m.matchedTerms.length > 0).sort((a, b) => Number(b.directPath) - Number(a.directPath) || b.score - a.score || b.matchedTerms.length - a.matchedTerms.length || `${a.chapterId}/${a.fact.id}`.localeCompare(`${b.chapterId}/${b.fact.id}`)).slice(0, limit);
37
+ const states = new Map();
38
+ for (const m of matches)
39
+ if (!states.has(m.chapterId))
40
+ states.set(m.chapterId, await store.status(m.chapterId, reg, cache));
41
+ return matches.map(m => ({ ...m, freshness: states.get(m.chapterId) }));
42
+ }
43
+ const paging = { cursor: z.string().optional(), limit: z.number().int().min(1).max(20).optional() };
44
+ export const ReadKnowledge = z.object({
45
+ kind: z.enum(['pillars', 'chapters', 'chapter', 'fact', 'search', 'owners', 'graph', 'review', 'checklist', 'tidy', 'proposals', 'proposal-review']),
46
+ target: z.string().optional(), query: z.string().optional(), paths: z.array(relativePath).optional(),
47
+ taskId: z.string().uuid().optional(), refresh: z.boolean().optional(), evidence: z.boolean().optional(), ...paging,
48
+ }).strict();
49
+ const shortStatus = (status) => ({ status: status.status, ...(status.error ? { error: status.error } : {}) });
50
+ export async function readKnowledge(store, input) {
51
+ const args = ReadKnowledge.parse(input), { kind, target, query, paths = [], taskId, refresh, cursor, limit } = args;
52
+ const workflow = new Workflow(store), registry = await store.read(), cache = new Map();
53
+ let value;
54
+ let keys = [];
55
+ const required = () => { if (!target)
56
+ throw new Error('target is required for this read kind.'); return target; };
57
+ switch (kind) {
58
+ case 'pillars':
59
+ value = await listPillars(store, cursor, limit);
60
+ break;
61
+ case 'chapters':
62
+ value = await listChapters(store, required(), cursor, limit);
63
+ break;
64
+ case 'chapter': {
65
+ const key = required(), c = store.chapter(registry, key);
66
+ keys = [key];
67
+ value = { chapterId: key, revision: c.revision, scope: c.scope, excludes: c.excludes, paths: c.paths, freshness: shortStatus(await store.status(key, registry, cache)),
68
+ facts: page(args.evidence ? c.facts : c.facts.map(({ id, statement }) => ({ id, statement })), cursor, limit, String(c.revision), 12000) };
69
+ break;
70
+ }
71
+ case 'fact': {
72
+ const f = store.fact(registry, required());
73
+ keys = [f.chapterId];
74
+ value = { chapterId: f.chapterId, revision: f.chapter.revision, fact: f.fact, freshness: shortStatus(await store.status(f.chapterId, registry, cache)) };
75
+ break;
76
+ }
77
+ case 'search': {
78
+ if (!query)
79
+ throw new Error('query is required.');
80
+ const matches = await search(store, query, limit, target, registry, cache);
81
+ keys = [...new Set(matches.map(m => m.chapterId))];
82
+ value = { items: matches.map(m => ({ ...m, freshness: shortStatus(m.freshness) })) };
83
+ break;
84
+ }
85
+ case 'owners': {
86
+ const { basis, ...routes } = await ownershipMap(store, { path: paths[0], signal: query, cursor, limit }, registry, cache);
87
+ keys = routes.items.map(r => r.chapterId);
88
+ value = { ...routes, items: routes.items.map(({ matchedTerms, paths, score, ...r }) => ({ ...r, freshness: shortStatus(r.freshness) })) };
89
+ break;
90
+ }
91
+ case 'graph': {
92
+ const { basis, ...graph } = await pillarGraph(store, target, cursor, limit);
93
+ value = graph;
94
+ break;
95
+ }
96
+ case 'review': {
97
+ const key = required();
98
+ let chapterId = key, factIds;
99
+ if (key.split('/').length === 3) {
100
+ const f = store.fact(registry, key);
101
+ chapterId = f.chapterId;
102
+ factIds = [f.fact.id];
103
+ }
104
+ keys = store.related(registry, chapterId, factIds);
105
+ value = page(keys.map(chapterId => ({ chapterId, revision: store.chapter(registry, chapterId).revision, factCount: store.chapter(registry, chapterId).facts.length })), cursor, limit, JSON.stringify(registry));
106
+ break;
107
+ }
108
+ case 'checklist': {
109
+ const key = required();
110
+ keys = key.split('/').length === 3 ? store.related(registry, store.fact(registry, key).chapterId, [store.fact(registry, key).fact.id]) : store.resolveTarget(registry, key).flatMap(k => store.related(registry, k));
111
+ const { policy, ...checklist } = await reviewChecklist(store, [...new Set(keys)], paths, cursor, limit);
112
+ value = checklist;
113
+ break;
114
+ }
115
+ case 'tidy': {
116
+ const { policy, next, ...plan } = await tidyPlan(store, required(), cursor, limit);
117
+ value = plan;
118
+ break;
119
+ }
120
+ case 'proposals':
121
+ if (!taskId)
122
+ throw new Error('taskId required.');
123
+ return workflow.proposals(taskId, cursor, limit);
124
+ case 'proposal-review':
125
+ if (!taskId)
126
+ throw new Error('taskId required.');
127
+ return workflow.proposalReview(taskId, cursor, limit);
128
+ }
129
+ if (!taskId)
130
+ return value;
131
+ let context = registry;
132
+ if (keys.length) {
133
+ try {
134
+ context = await store.context(registry, [...new Set(keys.flatMap(k => store.related(registry, k)))], cache);
135
+ }
136
+ catch {
137
+ return value;
138
+ }
139
+ }
140
+ if (kind === 'checklist')
141
+ context = { context, documents: await store.fileSnapshots((await store.reviewFiles(registry, [...new Set(keys)], paths)).documentFiles) };
142
+ return workflow.reuse(taskId, { kind, target, query, paths, cursor, limit, evidence: args.evidence }, value, context, refresh);
143
+ }
144
+ export async function reviewChecklist(store, chapterIds, touchedPaths, cursor, limit) {
145
+ const files = await store.reviewFiles(await store.read(), chapterIds, touchedPaths);
146
+ const entries = [...files.sourceFiles.map(path => ({ kind: 'sourceFiles', path })), ...files.documentFiles.map(path => ({ kind: 'documentFiles', path }))];
147
+ const selected = page(entries, cursor, limit);
148
+ const snapshots = await store.fileSnapshots(selected.items.map(item => item.path));
149
+ return { policy: 'Open each current file in this session. If it is missing, verify deletion live. Skim whole chapters and relevant sibling, child, and reference documentation; follow additional code references manually. This checklist is bounded discovery, not proof of reading.', ...selected, items: selected.items.map(item => ({ ...item, state: snapshots[item.path] === 'missing' ? 'missing' : 'present' })) };
150
+ }
@@ -0,0 +1,3 @@
1
+ import type { Store } from './store.js';
2
+ /** Bounded discovery of local documentation; never follows symlinks or remote links. */
3
+ export declare function reviewDocuments(store: Store, paths: string[], mode?: 'full' | 'focused'): Promise<string[]>;
@@ -0,0 +1,106 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ const document = (name) => /\.(md|mdx|rst|adoc)$/i.test(name) || /^readme(?:\.[^/]+)?$/i.test(name);
4
+ const excluded = (file) => file.split('/').some(p => ['.git', 'node_modules', '.common-ground', 'dist', 'build', 'target', '.nx', '.next', 'coverage'].includes(p) || p.startsWith('.env'));
5
+ /** Bounded discovery of local documentation; never follows symlinks or remote links. */
6
+ export async function reviewDocuments(store, paths, mode = 'full') {
7
+ if (!paths.length)
8
+ return [];
9
+ const directories = new Set();
10
+ const files = new Set();
11
+ for (const file of paths) {
12
+ await store.safe(file, true);
13
+ if (excluded(file))
14
+ continue;
15
+ let directory = path.posix.dirname(file);
16
+ try {
17
+ if ((await fs.stat(path.join(store.root, file))).isDirectory())
18
+ directory = file;
19
+ }
20
+ catch (error) {
21
+ if (error.code !== 'ENOENT')
22
+ throw error;
23
+ }
24
+ if (directory !== '.')
25
+ directories.add(directory);
26
+ if (document(file))
27
+ files.add(file);
28
+ }
29
+ // A root-level source change does not force a recursive scan of the whole monorepo.
30
+ const direct = new Set(['.']);
31
+ for (const directory of directories) {
32
+ let current = directory;
33
+ while (current !== '.') {
34
+ direct.add(current);
35
+ current = path.posix.dirname(current);
36
+ }
37
+ }
38
+ for (const directory of direct) {
39
+ let entries;
40
+ try {
41
+ entries = await fs.readdir(path.join(store.root, directory), { withFileTypes: true });
42
+ }
43
+ catch (error) {
44
+ if (error.code === 'ENOENT')
45
+ continue;
46
+ throw error;
47
+ }
48
+ for (const entry of entries) {
49
+ const file = path.posix.join(directory, entry.name);
50
+ if (excluded(file) || entry.isSymbolicLink())
51
+ continue;
52
+ if (entry.isFile() && document(entry.name) && (mode === 'full' || /^(readme(?:\.[^/]+)?|AGENTS\.md)$/i.test(entry.name)))
53
+ files.add(file);
54
+ // Include sibling directory READMEs without crawling every sibling subsystem.
55
+ if (mode === 'full' && entry.isDirectory())
56
+ for (const child of await fs.readdir(path.join(store.root, file), { withFileTypes: true })) {
57
+ if (child.isFile() && /^readme(?:\.[^/]+)?$/i.test(child.name))
58
+ files.add(`${file}/${child.name}`);
59
+ }
60
+ }
61
+ }
62
+ if (mode === 'full' && directories.size) {
63
+ const scan = await store.walk([...directories], 10000);
64
+ if (scan.truncated)
65
+ throw new Error('Documentation discovery exceeds the beta scan limit; narrow chapter evidence scopes before review.');
66
+ for (const file of scan.files)
67
+ if (document(file))
68
+ files.add(file);
69
+ }
70
+ if (mode === 'focused')
71
+ return [...files].sort();
72
+ // Follow relative documentation references transitively, with cycles deduplicated.
73
+ for (const file of files) {
74
+ if (files.size > 1000)
75
+ throw new Error('Documentation review exceeds the beta limit of 1,000 files.');
76
+ let content;
77
+ try {
78
+ const safe = await store.safe(file);
79
+ if ((await fs.stat(safe)).size > 2_000_000)
80
+ throw new Error(`Documentation file too large: ${file}`);
81
+ content = await fs.readFile(safe, 'utf8');
82
+ }
83
+ catch (error) {
84
+ if (error.code === 'ENOENT')
85
+ continue;
86
+ throw error;
87
+ }
88
+ for (const match of content.matchAll(/\[[^\]]*\]\((<[^>]+>|[^\s)]+)[^)]*\)/g)) {
89
+ let target = match[1].replace(/^<|>$/g, '').split('#')[0];
90
+ try {
91
+ target = decodeURIComponent(target);
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ if (!target || /^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(target) || target.includes('\\'))
97
+ continue;
98
+ const relative = path.posix.normalize(path.posix.join(path.posix.dirname(file), target));
99
+ if (relative.startsWith('../') || excluded(relative) || !document(relative))
100
+ continue;
101
+ await store.safe(relative, true);
102
+ files.add(relative);
103
+ }
104
+ }
105
+ return [...files].sort();
106
+ }
@@ -0,0 +1,86 @@
1
+ import type { Store } from './store.js';
2
+ import type { Fact, RegistryRecord } from './model.js';
3
+ import type { z } from 'zod';
4
+ type FactRecord = z.infer<typeof Fact>;
5
+ export type FactChange = {
6
+ before: FactRecord | null;
7
+ after: FactRecord | null;
8
+ reason: string;
9
+ };
10
+ export declare function factReview(before: FactRecord | null, after: FactRecord | null, evidence?: boolean): {
11
+ evidenceDetails?: string | undefined;
12
+ change: string;
13
+ fields: {
14
+ [k: string]: {
15
+ before: {} | null;
16
+ after: {} | null;
17
+ };
18
+ };
19
+ evidencePaths: string[];
20
+ } | null;
21
+ export declare function knowledgeChanges(before: RegistryRecord, after: RegistryRecord, target?: string, evidence?: boolean): ({
22
+ evidenceDetails?: string | undefined;
23
+ change: string;
24
+ fields: {
25
+ [k: string]: {
26
+ before: {} | null;
27
+ after: {} | null;
28
+ };
29
+ };
30
+ evidencePaths: string[];
31
+ kind: "chapter" | "fact" | "pillar";
32
+ id: string;
33
+ label: string;
34
+ } | {
35
+ change: string;
36
+ fields: {
37
+ [k: string]: {
38
+ before: {} | null;
39
+ after: {} | null;
40
+ };
41
+ };
42
+ kind: "chapter" | "fact" | "pillar";
43
+ id: string;
44
+ label: string;
45
+ })[];
46
+ /** A derived diff, not an approval, semantic validator, or another tracked knowledge file. */
47
+ export declare function reviewKnowledge(store: Store, target?: string, staged?: boolean, evidence?: boolean, cursor?: string, limit?: number): Promise<{
48
+ items: ({
49
+ evidenceDetails?: string | undefined;
50
+ change: string;
51
+ fields: {
52
+ [k: string]: {
53
+ before: {} | null;
54
+ after: {} | null;
55
+ };
56
+ };
57
+ evidencePaths: string[];
58
+ kind: "chapter" | "fact" | "pillar";
59
+ id: string;
60
+ label: string;
61
+ } | {
62
+ change: string;
63
+ fields: {
64
+ [k: string]: {
65
+ before: {} | null;
66
+ after: {} | null;
67
+ };
68
+ };
69
+ kind: "chapter" | "fact" | "pillar";
70
+ id: string;
71
+ label: string;
72
+ })[];
73
+ total: number;
74
+ nextCursor: string | null;
75
+ target: string;
76
+ comparison: string;
77
+ writesKnowledge: boolean;
78
+ summary: {
79
+ pillars: number;
80
+ chapters: number;
81
+ facts: number;
82
+ };
83
+ guidance: string;
84
+ }>;
85
+ export declare function reviewText(result: Awaited<ReturnType<typeof reviewKnowledge>>): string;
86
+ export {};
package/dist/review.js ADDED
@@ -0,0 +1,124 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify, isDeepStrictEqual as equal } from 'node:util';
3
+ import { page } from './paging.js';
4
+ const evidencePaths = (fact) => [...new Set(fact?.evidence.map(e => e.path) ?? [])].sort();
5
+ const factFields = (fact) => ({ statement: fact.statement, evidence: [...fact.evidence].sort((a, b) => a.path.localeCompare(b.path) || a.quote.localeCompare(b.quote)), sourceScope: [...fact.sourceScope].sort(), dependsOn: [...fact.dependsOn].sort() });
6
+ function changedFields(before, after) {
7
+ return Object.fromEntries([...new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})])]
8
+ .filter(field => !equal(before?.[field], after?.[field]))
9
+ .map(field => [field, { before: before?.[field] ?? null, after: after?.[field] ?? null }]));
10
+ }
11
+ export function factReview(before, after, evidence = false) {
12
+ const fields = changedFields(before ? factFields(before) : undefined, after ? factFields(after) : undefined);
13
+ if (!Object.keys(fields).length)
14
+ return null;
15
+ // Exact changed quotes are available on demand; never silently omit that evidence changed.
16
+ if (fields.evidence && !evidence)
17
+ fields.evidence = { before: evidencePaths(before), after: evidencePaths(after) };
18
+ return { change: !before ? 'added' : !after ? 'removed' : 'updated', fields,
19
+ evidencePaths: evidencePaths(after ?? before), ...(fields.evidence && !evidence ? { evidenceDetails: 'changed; use evidence:true or --evidence for exact quotes' } : {}) };
20
+ }
21
+ function records(registry) {
22
+ const entries = new Map();
23
+ for (const pillar of registry.pillars) {
24
+ const { id, title, scope, excludes } = pillar;
25
+ entries.set(id, { kind: 'pillar', label: title, fields: { title, scope, excludes } });
26
+ for (const chapter of pillar.chapters) {
27
+ const key = `${id}/${chapter.id}`;
28
+ entries.set(key, { kind: 'chapter', label: `${title} / ${chapter.title}`, fields: { title: chapter.title, scope: chapter.scope, excludes: chapter.excludes, paths: [...chapter.paths].sort() } });
29
+ for (const fact of chapter.facts)
30
+ entries.set(`${key}/${fact.id}`, { kind: 'fact', label: `${title} / ${chapter.title}`, fields: factFields(fact), fact });
31
+ }
32
+ }
33
+ return entries;
34
+ }
35
+ export function knowledgeChanges(before, after, target = 'all', evidence = false) {
36
+ const old = records(before), current = records(after), ids = [...new Set([...old.keys(), ...current.keys()])];
37
+ if (target !== 'all') {
38
+ const matches = ids.filter(id => id === target || (id.split('/').length === 3 && id.endsWith(`/${target}`)));
39
+ if (matches.length !== 1)
40
+ throw new Error(matches.length ? 'Ambiguous target; use a qualified ID.' : 'Unknown target; use a pillar, chapter, fact, or all.');
41
+ target = matches[0];
42
+ }
43
+ return ids.filter(id => target === 'all' || id === target || id.startsWith(`${target}/`)).sort().flatMap(id => {
44
+ const previous = old.get(id), next = current.get(id), entry = next ?? previous;
45
+ const change = entry.kind === 'fact' ? factReview(previous?.fact ?? null, next?.fact ?? null, evidence) : {
46
+ change: !previous ? 'added' : !next ? 'removed' : 'updated', fields: changedFields(previous?.fields, next?.fields)
47
+ };
48
+ return change && Object.keys(change.fields).length ? [{ kind: entry.kind, id, label: entry.label, ...change }] : [];
49
+ });
50
+ }
51
+ const exec = promisify(execFile);
52
+ const empty = () => ({ schemaVersion: 2, pillars: [] });
53
+ /** A derived diff, not an approval, semantic validator, or another tracked knowledge file. */
54
+ export async function reviewKnowledge(store, target = 'all', staged = false, evidence = false, cursor, limit) {
55
+ let gitRoot = store.root;
56
+ const git = async (args) => (await exec('git', ['--literal-pathspecs', '-C', gitRoot, ...args], { maxBuffer: 16 * 1024 * 1024 })).stdout;
57
+ let prefix;
58
+ try {
59
+ prefix = (await git(['rev-parse', '--show-prefix'])).replace(/\r?\n$/, '');
60
+ gitRoot = (await git(['rev-parse', '--show-toplevel'])).replace(/\r?\n$/, '');
61
+ }
62
+ catch {
63
+ throw new Error('cground review requires a Git checkout. Task completion summaries work without Git.');
64
+ }
65
+ const file = `${prefix}.common-ground/knowledge.json`;
66
+ let head;
67
+ try {
68
+ head = (await git(['rev-parse', '--verify', '--quiet', 'HEAD'])).trim();
69
+ }
70
+ catch (error) {
71
+ if (error.code !== 1)
72
+ throw error;
73
+ }
74
+ let before = empty(), after;
75
+ if (head) {
76
+ const entry = await git(['ls-tree', '-z', head, '--', file]);
77
+ if (entry) {
78
+ if (!entry.startsWith('100644 ') && !entry.startsWith('100755 '))
79
+ throw new Error('Knowledge in HEAD must be a regular file.');
80
+ before = store.validateRegistry(JSON.parse(await git(['show', `${head}:${file}`])));
81
+ }
82
+ }
83
+ if (staged) {
84
+ const entries = (await git(['ls-files', '--stage', '-z', '--', file])).split('\0').filter(Boolean);
85
+ if (entries.some(entry => !/^100(?:644|755) [a-f0-9]+ 0\t/.test(entry)))
86
+ throw new Error('Resolve staged knowledge conflicts or unsupported file types before review.');
87
+ after = entries.length ? store.validateRegistry(JSON.parse(await git(['show', `:${file}`]))) : empty();
88
+ }
89
+ else {
90
+ try {
91
+ after = await store.read();
92
+ }
93
+ catch (error) {
94
+ if (error.code !== 'ENOENT')
95
+ throw error;
96
+ after = empty();
97
+ }
98
+ }
99
+ const items = knowledgeChanges(before, after, target, evidence);
100
+ return { target, comparison: staged ? 'HEAD to staged knowledge' : 'HEAD to working knowledge', writesKnowledge: false,
101
+ summary: { pillars: items.filter(i => i.kind === 'pillar').length, chapters: items.filter(i => i.kind === 'chapter').length, facts: items.filter(i => i.kind === 'fact').length },
102
+ guidance: 'Present only these changes in plain language with source links, the reason you verified, a recommendation and any uncertainty. This diff does not validate truth or establish approval. Do not ask the developer to read or edit JSON; fetch exact evidence only when needed.',
103
+ ...page(items, cursor, limit, JSON.stringify({ head, before, after, target, staged, evidence }), 12000) };
104
+ }
105
+ function valueText(value) { return value === null ? '(none)' : Array.isArray(value) ? value.map(v => typeof v === 'string' ? v : JSON.stringify(v)).join(', ') : String(value); }
106
+ export function reviewText(result) {
107
+ if (!result.total)
108
+ return 'No knowledge content changes. Revision and fingerprint-only changes are omitted.';
109
+ const lines = [`Knowledge review — ${result.comparison}`, `${result.summary.pillars} pillar, ${result.summary.chapters} chapter, ${result.summary.facts} fact changes.`, ''];
110
+ for (const item of result.items) {
111
+ lines.push(`${item.change.toUpperCase()} ${item.kind}: ${item.id} (${item.label})`);
112
+ for (const [field, change] of Object.entries(item.fields))
113
+ lines.push(` ${field}: ${valueText(change.before)} → ${valueText(change.after)}`);
114
+ if ('evidencePaths' in item)
115
+ lines.push(` Source: ${item.evidencePaths.join(', ')}`);
116
+ if ('evidenceDetails' in item && item.evidenceDetails)
117
+ lines.push(` Evidence: ${item.evidenceDetails}`);
118
+ lines.push('');
119
+ }
120
+ if (result.nextCursor)
121
+ lines.push(`More changes: rerun with --cursor ${result.nextCursor}`);
122
+ lines.push('Recommended: run this review through your coding agent to verify source, explain changes, and recommend what to keep or approve. No JSON editing is needed.');
123
+ return lines.join('\n');
124
+ }
@@ -0,0 +1,8 @@
1
+ export { ReadKnowledge, readKnowledge, listPillars, listChapters, readChapter, readFact, search, reviewChecklist } from './retrieval.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { Store } from './store.js';
4
+ export { page } from './paging.js';
5
+ export declare function createFullServer(store: Store): McpServer;
6
+ export declare function serve(store: Store, profile?: string): Promise<void>;
7
+ export declare function createServer(store: Store, profile?: string): McpServer;
8
+ export declare function registerOperations(server: McpServer, store: Store): void;